From 42ec1a6df9ff088a72dbf95308e688823813586b Mon Sep 17 00:00:00 2001 From: "jerod.wilkerson" <30474318+jerodw@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:14:33 -0600 Subject: [PATCH 1/5] Record the mandate conferred for story-129 --- .harness/history/mandate-log.jsonl | 1 + 1 file changed, 1 insertion(+) diff --git a/.harness/history/mandate-log.jsonl b/.harness/history/mandate-log.jsonl index 0815887..dd7fe81 100644 --- a/.harness/history/mandate-log.jsonl +++ b/.harness/history/mandate-log.jsonl @@ -33,3 +33,4 @@ {"story_id": "story-123", "timestamp": "2026-09-08 20:30:12", "conferred_by": "jerod.wilkerson <30474318+jerodw@users.noreply.github.com>", "source_kind": "human", "recorded_by": "l5-plan", "discarded_session_block": false} {"story_id": "story-126", "timestamp": "2026-09-09 00:38:53", "conferred_by": "jerod.wilkerson <30474318+jerodw@users.noreply.github.com>", "source_kind": "human", "recorded_by": "l5-plan", "discarded_session_block": false} {"story_id": "story-128", "timestamp": "2026-09-09 08:02:40", "conferred_by": "jerod.wilkerson <30474318+jerodw@users.noreply.github.com>", "source_kind": "human", "recorded_by": "l5-plan", "discarded_session_block": false} +{"story_id": "story-129", "timestamp": "2026-09-09 09:14:32", "conferred_by": "jerod.wilkerson <30474318+jerodw@users.noreply.github.com>", "source_kind": "human", "recorded_by": "l5-plan", "discarded_session_block": false} From ac46322beca77312ca6aea329bd4fb560fc0cb17 Mon Sep 17 00:00:00 2001 From: "jerod.wilkerson" <30474318+jerodw@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:14:33 -0600 Subject: [PATCH 2/5] Plan story-129: The Status field's name is matched the same way when it is read and when it is written --- .harness/stories/story-129.yaml | 118 ++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 .harness/stories/story-129.yaml diff --git a/.harness/stories/story-129.yaml b/.harness/stories/story-129.yaml new file mode 100644 index 0000000..ae2215b --- /dev/null +++ b/.harness/stories/story-129.yaml @@ -0,0 +1,118 @@ +story: + id: story-129 + title: The Status field's name is matched the same way when it is read and when it is written + description: | + Planned from the brief filed under key 117. + + The sync script asks "is this the field this target configured?" in three + places, and they do not agree. Reading what the board currently says + normalizes both sides — spaces removed, case ignored — so a target that + configures STATUS_FIELD="status" against a board whose field is titled + "Status" reads the value successfully. Resolving the id to write with does + not normalize at all: it matches the configured name verbatim against the + names the board reports. So the read succeeds, finds the field empty, and + the write then resolves no id. + + For Status that ends the filing: the guard exits 75, the entry stays + pending, and it is retried for ever without ever landing. The same two + lookups serve the five classification fields, where the failure is quieter + — the field is skipped with a line on stderr, the filing exits 0, and the + board is simply missing the value. Any difference in case or internal + spacing that the read tolerates and the write does not produces one of + those two outcomes. + + This story gives field-name matching a single definition and has all three + lookups refer to it, so the three sites cannot drift apart again. The + option-name half of the option lookup is untouched: options continue to + match the board verbatim. + + The suite cannot see the defect today because the one configuration it + drives cannot exhibit it. THIS_TARGETS_STATUS_FIELD names the configured + value, and seeded_board() names the board's field from that same constant, + so the two are the same string by construction; the existing test for a + field name carrying a space renames both halves together for the same + reason. The new test deliberately makes them differ. + + workflow: story-workflow + +tasks: + - Define, once, in templates/sync/github.sh, what makes two field names the same — the configured name and the board's name compared with their spaces removed and their case ignored — and have the read of the item's current value and both id lookups refer to that one definition rather than each carrying its own rule. + - Leave the option-name comparison in the option lookup matching the board verbatim; only the field-name half of that lookup adopts the shared rule. + - Apply the identical edit to .harness/sync/github.sh, which is this repository's installed copy of that template and is held to differing from it in nothing but the constant assignments at the top. + - Add coverage to tests/test_filed_query.py driving a configured field name that differs from the board's in case, for the Status field and for one classification field, over both sync copies. + - Record the change in .harness/docs/ARCHITECTURE.md. + +acceptance_criteria: + - templates/sync/github.sh carries one definition of what makes two field names the same, and the lookup that reads the item's current value, the lookup that resolves a field's id, and the field-name half of the lookup that resolves an option's id each refer to that definition rather than spelling a rule of their own. + - The option-name comparison in the option lookup still matches the configured option value against the board's option names verbatim, unchanged by this story. + - .harness/sync/github.sh carries the same edit, and the standing assertion that the installed copy differs from its template only in constant assignments still holds. + - A filing driven with the Status field name configured in a case the board does not use resolves the field, sets the item's Status to the configured option, and exits 0 — where before this story it exited 75 saying the project has no field of that name. + - A filing driven with a classification field name configured in a case the board does not use writes that field's value onto the board — where before this story the field was skipped and the value was written nowhere. + - Both of those cases are driven against the template and against the installed copy, in the shape the module's existing board tests use for that pairing. + - The new test fails against the code as it stands before this story and passes after it, so it holds the rule rather than describing it. + - The full suite passes. + +technical_plan: + implementation_steps: + - In templates/sync/github.sh, introduce a single named definition of the field-name comparison beside the three lookups, expressing the rule the read already applies: both the board's name and the configured name have their spaces removed and their case ignored before they are compared. + - Rewrite the lookup that reads the item's current field value to use that definition in place of the normalization it spells inline today, so the rule has one home rather than one of its three. + - Rewrite the lookup that resolves a field's id to compare under that definition instead of matching the configured name verbatim. This lookup serves both the Status write and every classification-field write, so one edit reaches all six configured names. + - Rewrite the field-name half of the lookup that resolves an option's id the same way, and leave its option-name half comparing verbatim. + - Say in the script's own comments what the rule is and that all three lookups share it, so the next reader does not have to infer the sharing from the code. + - Copy the edit into .harness/sync/github.sh unchanged, leaving its constant assignments as they are — the two files are held byte-identical outside those constants, so an edit to one is an edit to both. + - In tests/test_filed_query.py, add a case that seeds the board with the field names it already uses, drives a filing with the Status field name configured in a different case, and asserts the item lands in the configured column and the script exits 0. + - Add the companion case for one classification field, driven the same way, asserting the value reaches the board rather than being skipped. + - Drive both cases over the template and the installed copy, following the existing parametrization over the two sync copies, and drive the configured-name override through the mechanism the module already uses to override a constant for one filing. + - Record in .harness/docs/ARCHITECTURE.md what the two halves disagreed about, what one rule replaced them, and why the existing tests could not have caught it. + likely_file_changes: + - file: templates/sync/github.sh + stage: implementer + reason: Holds the three lookups that disagree; the shared definition and all three call sites are added here. + - file: .harness/sync/github.sh + stage: implementer + reason: This repository's installed copy of that template, held to differing from it only in constant assignments, so the same edit must land here or the standing assertion reddens. + - file: tests/test_filed_query.py + stage: tester + reason: Where the sync script's board behaviour is driven; gains the cases for a configured name differing from the board's in case, for Status and for a classification field. + - file: .harness/docs/ARCHITECTURE.md + stage: documenter + reason: Records the change and why the existing coverage could not exhibit the defect. + +scope: + modify: + - templates/sync/github.sh + - .harness/sync/github.sh + - tests/test_filed_query.py + - .harness/docs/ARCHITECTURE.md + do_not_modify: + - templates/query/github.sh + - .harness/query/github.sh + - .harness/config.yaml + - orchestration/ + - schemas/ + - prompts/ + - workflows/ + +verification_requirements: + - Confirm that templates/sync/github.sh states the field-name rule once and that all three lookups refer to that one statement, rather than three sites that happen to agree today. + - Confirm the option-name comparison still matches verbatim against the board's option names, and that the story changed only the field-name half of that lookup. + - Confirm .harness/sync/github.sh carries the same edit and that the existing assertion comparing it to its template passes. + - Confirm the new test cases drive a configured field name that differs from the board's in case for both the Status field and a classification field, and that they are driven over both sync copies. + - Confirm the new cases fail against the pre-story scripts — the Status case by the filing exiting 75, the classification case by the value not reaching the board — so the coverage holds the rule rather than restating it. + - Confirm the full suite passes. + +constraints: + - A configured name and a board name that already match exactly must keep resolving exactly as they do today; this story widens what matches and narrows nothing. + - The option-name rule is untouched. An option value that does not match the board's option names verbatim must still resolve to nothing, and must still cost the Status filing an exit 75 and cost a classification field that field alone. + - The two failure classifications are unchanged: a Status field that resolves to no id remains transient, and a classification field that resolves to no id remains a line on stderr with the filing exiting 0. + - The board is still written only where the item reports the field empty; nothing in this story writes over a value a person set. + - The key marker, the path marker prefix and the payload marker prefix are untouched, as is the pairing between the sync script and templates/query/github.sh. + - The project, its field list and the item's field values are still each read at most once per filing. + - .harness/sync/github.sh keeps its own constant values; only the lines it shares with its template change. + +mandate: + source: + kind: human + conferred_at: 2026-09-09 09:14:32 + conferred_by: jerod.wilkerson <30474318+jerodw@users.noreply.github.com> + recorded_by: l5-plan From 2f8d252566bd57a749de8f35c1900b13a0a02bce Mon Sep 17 00:00:00 2001 From: "jerod.wilkerson" <30474318+jerodw@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:48:56 -0600 Subject: [PATCH 3/5] story-129: The Status field's name is matched the same way when it is read and when it is written Implemented by the l5 harness story workflow. --- .harness/docs/ARCHITECTURE.md | 16 ++ .harness/history/execution-log.jsonl | 1 + .harness/history/retry-log.jsonl | 1 + .harness/sync/github.sh | 31 ++-- templates/sync/github.sh | 31 ++-- tests/test_config_keys_are_obeyed.py | 37 ++++- tests/test_filed_query.py | 213 ++++++++++++++++++++++++++- 7 files changed, 309 insertions(+), 21 deletions(-) diff --git a/.harness/docs/ARCHITECTURE.md b/.harness/docs/ARCHITECTURE.md index 6649dbc..3ac304c 100644 --- a/.harness/docs/ARCHITECTURE.md +++ b/.harness/docs/ARCHITECTURE.md @@ -2047,6 +2047,22 @@ story-128 changes how the board block learns what a board already says about the **The stub `gh` in `tests/test_filed_query.py` grew the read and lost the listing**, and the assertions moved with it: the ledger records the graphql call as it records the `project` subcommands, `OMIT_VARIABLE` now makes the node read report no such item rather than making the listing report nothing, and the read failing is driven beside it so both routes to *unobtainable* are exercised. The assertions new in kind rather than repointed are that a filing writing every field records no project item-list call at all, that the transient stderr carries no number, and that a board seeded past the retired bound files an item added at the end of its order. A scan holds both sync scripts to mentioning neither `gh project item-list` nor `ITEM_LIST_LIMIT`, with a control that plants a listing back into a script and requires it reported, so the retirement is held by something that reddens rather than by this paragraph. +## A field name is matched by one rule wherever it is matched + +story-129 gives the sync script's field-name comparison a single home. The script asked "is this the field this target configured?" in three places and the three did not agree: the read of the item's current value normalized both sides — spaces removed, case ignored — while the lookup that resolves a field's id and the field-name half of the lookup that resolves an option's id matched the configured name against the board's **verbatim**. A target configuring `STATUS_FIELD="status"` against a board titled `Status` therefore read the field successfully, found it empty, and then resolved no id to write into. + +**The two halves failed differently, and neither failure named its cause.** For Status a field resolving to no id is transient: the filing exits 75, the outbox entry stays pending, and every later sweep retries a configuration that cannot land — the same never-lands shape story-128's bounded listing had, arrived at from a different direction. For the five classification fields it is quiet: the field is skipped with a line on stderr, the filing exits 0, and the board is simply missing a value nobody was told about. Any difference in case or internal spacing that the read tolerated and the write did not produced one of those two. + +**The fix is one definition rather than three sites made to agree.** `SAME_FIELD_NAME` holds a `jq` definition of `same_field_name($a; $b)` — both names compared with their spaces removed and their case ignored — and `board_value`, `field_id_for` and `option_id_for` each prepend it to their own program and call it. It is a `jq` function rather than a shell one because the comparison happens inside `jq` in all three places. This widens only: a configured name that already matched the board exactly still matches, since equality survives removing spaces and lowering case on both sides. One edit reaches all six configured names, because every write resolves its id through `field_id_for`. + +**The option-name half is deliberately untouched and stays verbatim.** An option is the value a person reads off the board, so the board's own spelling is the one that goes there: an option value that does not match the board exactly still resolves to nothing, and still costs the Status filing a transient exit and a classification field that field alone. The script says which three sites share the rule and that option names sit outside it, so the sharing does not have to be inferred from the code. Nothing else moved — the write is still made only where the item reports the field empty, the two failure classifications are unchanged, and the project, its field list and the item's field values are each still read at most once per filing. + +**The existing coverage could not have caught this, and the reason is worth knowing before writing the next board test.** `THIS_TARGETS_STATUS_FIELD` supplied both the configured name and, through `seeded_board()`, the board's own name, so the two sides were the same string *by construction*; the older test for a field name carrying a space renamed both halves together for the same reason. A test that derives the board's state from the value under test cannot exhibit a disagreement between them. The new cases in `tests/test_filed_query.py` derive the configured name from the board's by `swapcase()` and assert both that it differs and that it differs in case alone, over both sync copies — one for Status, one for a classification field, and one holding the option-name rule still narrow. + +**Beside the filings sits a scan that holds the sharing rather than the agreement.** The filings assert the three lookups agree today; `test_every_field_name_lookup_defers_to_one_statement_of_sameness` asserts they cannot come to disagree tomorrow — the script states the rule once, no line outside that statement states it again, and each of the three defers to it. The shared definition is matched by its shape rather than its name, so the assertion is about stating-the-rule-once and not about a chosen identifier. Its control renders two templates this repository does not ship — one whose id lookup matches verbatim, one whose read spells the normalization inline — and requires each reported. + +`.harness/sync/github.sh` carries the identical edit, since the installed copy is held to differing from its template in nothing but the constant assignments at the top. This is the shape recorded under "A planned story is published onto the item it was planned from" above: **an edit to a shipped script is an edit to two files.** + ## Run directory anatomy .harness/runs/story-001/ diff --git a/.harness/history/execution-log.jsonl b/.harness/history/execution-log.jsonl index 19b90cc..1d56625 100644 --- a/.harness/history/execution-log.jsonl +++ b/.harness/history/execution-log.jsonl @@ -55,3 +55,4 @@ {"story_id": "story-123", "status": "completed", "retry_count": 0, "timestamp": "2026-09-08 21:27:35"} {"story_id": "story-126", "status": "completed", "retry_count": 0, "timestamp": "2026-09-09 01:42:55"} {"story_id": "story-128", "status": "completed", "retry_count": 0, "timestamp": "2026-09-09 08:32:13"} +{"story_id": "story-129", "status": "completed", "retry_count": 0, "timestamp": "2026-09-09 09:48:56"} diff --git a/.harness/history/retry-log.jsonl b/.harness/history/retry-log.jsonl index 3a1286a..3872a96 100644 --- a/.harness/history/retry-log.jsonl +++ b/.harness/history/retry-log.jsonl @@ -46,3 +46,4 @@ {"story_id": "story-118", "timestamp": "2026-09-08 19:14:16", "stage": "tester", "retry_stage": "tester"} {"story_id": "story-126", "timestamp": "2026-09-09 01:13:04", "stage": "tester", "retry_stage": "tester"} {"story_id": "story-126", "timestamp": "2026-09-09 01:20:52", "stage": "tester", "retry_stage": "tester"} +{"story_id": "story-129", "timestamp": "2026-09-09 09:28:14", "stage": "tester", "retry_stage": "tester"} diff --git a/.harness/sync/github.sh b/.harness/sync/github.sh index b5e68a5..8779507 100755 --- a/.harness/sync/github.sh +++ b/.harness/sync/github.sh @@ -341,23 +341,36 @@ if [ -n "$PROJECT" ]; then || fail_transient "the field values of item ${item_id} in project ${PROJECT} could not be obtained, so its fields are unknown" } + # What makes two field names the same, said once for all three lookups below. + # The configured name and the board's name are compared with their spaces + # removed and their case ignored, so a target that configures "status" against + # a board whose field is titled "Status" resolves that field however the name + # is spelled. Every lookup that matches a field name -- the read of this item's + # current value, the resolution of a field's id, and the field-name half of the + # resolution of an option's id -- refers to this definition rather than + # carrying a rule of its own, so the three cannot come to disagree: a name the + # read tolerates is a name the write resolves. Option names are not matched by + # it; they are compared against the board verbatim. + SAME_FIELD_NAME='def same_field_name($a; $b): + ($a | gsub(" "; "") | ascii_downcase) == ($b | gsub(" "; "") | ascii_downcase);' + # What the read reports this item's named field as, empty where the board - # reports none. The name is matched with its spaces removed and its case - # ignored on both sides, so a board whose field names carry spaces resolves - # the same way however the key is spelled. + # reports none. board_value() { - printf '%s' "$item" | jq -r --arg name "$1" \ - '[to_entries[] | select((.key | gsub(" "; "") | ascii_downcase) == ($name | gsub(" "; "") | ascii_downcase)) | .value] | .[0] // "" | tostring' + printf '%s' "$item" | jq -r --arg name "$1" "$SAME_FIELD_NAME"' + [to_entries[] | select(same_field_name(.key; $name)) | .value] | .[0] // "" | tostring' } field_id_for() { - printf '%s' "$fields" | jq -r --arg name "$1" \ - '[.fields[]? | select(.name == $name) | .id] | .[0] // ""' + printf '%s' "$fields" | jq -r --arg name "$1" "$SAME_FIELD_NAME"' + [.fields[]? | select(same_field_name(.name; $name)) | .id] | .[0] // ""' } + # The field name is matched by the shared definition; the option name is + # matched against the board verbatim, which is deliberate and unchanged. option_id_for() { - printf '%s' "$fields" | jq -r --arg name "$1" --arg option "$2" \ - '[.fields[]? | select(.name == $name) | .options[]? | select(.name == $option) | .id] | .[0] // ""' + printf '%s' "$fields" | jq -r --arg name "$1" --arg option "$2" "$SAME_FIELD_NAME"' + [.fields[]? | select(same_field_name(.name; $name)) | .options[]? | select(.name == $option) | .id] | .[0] // ""' } if [ -n "$STATUS_OPTION" ]; then diff --git a/templates/sync/github.sh b/templates/sync/github.sh index 969e762..87c7467 100644 --- a/templates/sync/github.sh +++ b/templates/sync/github.sh @@ -341,23 +341,36 @@ if [ -n "$PROJECT" ]; then || fail_transient "the field values of item ${item_id} in project ${PROJECT} could not be obtained, so its fields are unknown" } + # What makes two field names the same, said once for all three lookups below. + # The configured name and the board's name are compared with their spaces + # removed and their case ignored, so a target that configures "status" against + # a board whose field is titled "Status" resolves that field however the name + # is spelled. Every lookup that matches a field name -- the read of this item's + # current value, the resolution of a field's id, and the field-name half of the + # resolution of an option's id -- refers to this definition rather than + # carrying a rule of its own, so the three cannot come to disagree: a name the + # read tolerates is a name the write resolves. Option names are not matched by + # it; they are compared against the board verbatim. + SAME_FIELD_NAME='def same_field_name($a; $b): + ($a | gsub(" "; "") | ascii_downcase) == ($b | gsub(" "; "") | ascii_downcase);' + # What the read reports this item's named field as, empty where the board - # reports none. The name is matched with its spaces removed and its case - # ignored on both sides, so a board whose field names carry spaces resolves - # the same way however the key is spelled. + # reports none. board_value() { - printf '%s' "$item" | jq -r --arg name "$1" \ - '[to_entries[] | select((.key | gsub(" "; "") | ascii_downcase) == ($name | gsub(" "; "") | ascii_downcase)) | .value] | .[0] // "" | tostring' + printf '%s' "$item" | jq -r --arg name "$1" "$SAME_FIELD_NAME"' + [to_entries[] | select(same_field_name(.key; $name)) | .value] | .[0] // "" | tostring' } field_id_for() { - printf '%s' "$fields" | jq -r --arg name "$1" \ - '[.fields[]? | select(.name == $name) | .id] | .[0] // ""' + printf '%s' "$fields" | jq -r --arg name "$1" "$SAME_FIELD_NAME"' + [.fields[]? | select(same_field_name(.name; $name)) | .id] | .[0] // ""' } + # The field name is matched by the shared definition; the option name is + # matched against the board verbatim, which is deliberate and unchanged. option_id_for() { - printf '%s' "$fields" | jq -r --arg name "$1" --arg option "$2" \ - '[.fields[]? | select(.name == $name) | .options[]? | select(.name == $option) | .id] | .[0] // ""' + printf '%s' "$fields" | jq -r --arg name "$1" --arg option "$2" "$SAME_FIELD_NAME"' + [.fields[]? | select(same_field_name(.name; $name)) | .options[]? | select(.name == $option) | .id] | .[0] // ""' } if [ -n "$STATUS_OPTION" ]; then diff --git a/tests/test_config_keys_are_obeyed.py b/tests/test_config_keys_are_obeyed.py index 5455f5f..da47890 100644 --- a/tests/test_config_keys_are_obeyed.py +++ b/tests/test_config_keys_are_obeyed.py @@ -294,6 +294,24 @@ DEFAULT_FILED_QUERY_TIMEOUT = filed_query.DEFAULT_TIMEOUT_SECONDS DEFAULT_FILED_QUERY_MAX_ITEMS = filed_query.DEFAULT_MAX_ITEMS +#: The bound the query proofs that are *not* about the bound configure instead +#: of `FILED_QUERY_TIMEOUT`. Two proofs here spawn a command and read what it +#: answered — the one for `filed_query_command` and the one for +#: `filed_query_max_items` — and neither says anything about how long a command +#: may run. Left under the tight bound, each of them was also asserting that +#: this machine spawns a shell inside 6.5 seconds while the suite runs its +#: modules in parallel and several of them spawn whole nested pytest runs, and +#: that assertion is the one that goes red on a loaded machine. Neither may +#: report inconclusive in its place: both are proof nodes, and the pristine +#: control requires every proof node to have *passed*. +#: +#: So the number is made irrelevant to them rather than widened: the bound is +#: an input to these two and the subject of neither, and it is set to the value +#: the harness itself falls back to so that nothing about the number is a claim +#: this module makes here. The bound's own two proofs are untouched and still +#: pin it from both sides at `FILED_QUERY_TIMEOUT`. +BOUND_THE_OTHER_QUERY_PROOFS_RUN_UNDER = DEFAULT_FILED_QUERY_TIMEOUT + #: How long the fixture allows an item-update command to run. Not a whole #: number of seconds, which no harness would pick: the default written in #: harness source is a minute and this repository configures none. It stands in @@ -1965,6 +1983,16 @@ def asked_what_is_filed(tmp_path: Path, *, sleeps: int = 0, items: int = 1, QUERY_PATHS, harness_config.load_config(target), target) +def not_about_the_bound() -> dict[str, object]: + """The departure a query proof that says nothing about the bound makes. + + The key is read off the query module rather than written here, so it is the + key the harness reads. See `BOUND_THE_OTHER_QUERY_PROOFS_RUN_UNDER` for why + these proofs configure a bound of their own. + """ + return {filed_query.TIMEOUT_KEY: BOUND_THE_OTHER_QUERY_PROOFS_RUN_UNDER} + + def test_filed_query_command_is_the_command_the_question_is_put_to(tmp_path): """The configured command is the one that answered, observed at the answer. @@ -1972,8 +2000,12 @@ def test_filed_query_command_is_the_command_the_question_is_put_to(tmp_path): printed, and that script sits at the configured path and nowhere else. A harness that had stopped reading the key would ask nothing at all, and the answer would know nothing instead of reporting an item. + + Nothing here is about how long a command may run, so the query is not held + to the tight bound the two proofs below pin — the command key is what is + asserted, and the machine's spawn latency is not. """ - answer = asked_what_is_filed(tmp_path) + answer = asked_what_is_filed(tmp_path, **not_about_the_bound()) assert answer.answered is True, answer.reason assert [item.key for item in answer.items] == [f"{QUERY_ITEM_KEY}-0"] @@ -2053,7 +2085,8 @@ def test_filed_query_max_items_is_the_bound_on_what_one_answer_carries(tmp_path) item and name nothing. """ answer = asked_what_is_filed(tmp_path, - items=ITEMS_THE_COMMAND_ANSWERS_WITH) + items=ITEMS_THE_COMMAND_ANSWERS_WITH, + **not_about_the_bound()) assert answer.answered is True, answer.reason assert [item.key for item in answer.items] == [ f"{QUERY_ITEM_KEY}-{ordinal}" for ordinal in range(FILED_QUERY_MAX_ITEMS)] diff --git a/tests/test_filed_query.py b/tests/test_filed_query.py index b6d812d..bfb06a2 100644 --- a/tests/test_filed_query.py +++ b/tests/test_filed_query.py @@ -125,7 +125,11 @@ the template differing in a line of mechanics, which the same predicate reports; * "neither sync script invokes git" sits beside a rendering of one with a - commit added, which the same scan reports. + commit added, which the same scan reports; + * "no field-name lookup states a rule of its own" sits beside two renderings + of the template — one whose id lookup matches the board verbatim and one + whose read spells the normalization inline — each of which the scan that + exists to catch it reports. Every command driven as a `filed_query_command` here is a file this module wrote, and `fixture_command_problems` is what makes that a checked property @@ -2702,6 +2706,213 @@ def rename(project): "a field the board already reports was written over" +def in_a_case_the_board_does_not_use(name: str) -> str: + """The same name, spelled in a case the board does not spell it in. + + Derived from whatever the board calls the field rather than written out, so + a field renamed on the seeded board is still driven at a name differing + from it in case alone. Both halves of that are asserted here: a name that + came back identical would make the filings below the ordinary filings every + test above already makes, and a name that differed in anything but case + would be asking a wider question than the one these tests ask. + """ + swapped = name.swapcase() + assert swapped != name, name + assert swapped.lower() == name.lower(), (swapped, name) + return swapped + + +@needs_jq +@pytest.mark.parametrize("script", BOTH_SYNC_COPIES) +def test_a_status_field_configured_in_another_case_is_resolved_and_written( + script, tmp_path): + """A target that configures `status` against a board titled `Status` files. + + The read of the item's current value has always ignored case, so the field + read as empty; the lookup that resolves the id to write with matched the + board verbatim, so the write resolved nothing and the filing exited + transiently — leaving the entry pending and retried for ever without ever + landing. One rule shared by both lookups is what makes the name the read + tolerates a name the write resolves. + + The board keeps the field names it is seeded with, and only the configured + name is spelled differently, so what resolves the field can only be the + comparison and not a board rewritten to suit it. + """ + environment, ledger = stub_tracker(tmp_path) + named = {TEMPLATE_CONSTANTS["STATUS_FIELD"][0]: + in_a_case_the_board_does_not_use(THIS_TARGETS_STATUS_FIELD)} + + result = sync_to_the_board(script, tmp_path, environment, + key="k-status-in-another-case", + payload=a_filed_brief(), breaking=named) + + assert result.returncode == 0, result.stderr + assert len(board_items(ledger)) == 1 + assert board_items(ledger)[0]["status"] == THIS_TARGETS_STATUS_OPTION + + +@needs_jq +@pytest.mark.parametrize("script", BOTH_SYNC_COPIES) +def test_a_classification_field_configured_in_another_case_reaches_the_board( + script, tmp_path): + """The same disagreement on a classification field, where it was quieter. + + A classification field whose id resolves to nothing is said on stderr and + skipped, so the filing exits 0 and the value is simply written nowhere — + the failure a developer reads as a board that is merely missing a column's + value. Driven at the same axis the rest of the brief is driven at, so the + value on the board can only have come from the payload. + """ + environment, ledger = stub_tracker(tmp_path) + axis = CLASSIFICATION[0] + brief = a_filed_brief() + named = {TEMPLATE_CONSTANTS[axis.constant][0]: + in_a_case_the_board_does_not_use(axis.field_name)} + + result = sync_to_the_board(script, tmp_path, environment, + key="k-classification-in-another-case", + payload=brief, breaking=named) + + assert result.returncode == 0, result.stderr + assert board_field_value(ledger, axis.field_name) == \ + str(brief[axis.payload_field]), result.stderr + assert board_items(ledger)[0]["status"] == THIS_TARGETS_STATUS_OPTION + + +@needs_jq +@pytest.mark.parametrize("script", BOTH_SYNC_COPIES) +def test_a_status_option_configured_in_another_case_still_resolves_to_nothing( + script, tmp_path): + """What the field-name rule widened, the option-name rule did not. + + An option is the value a person reads off the board, and the board's own + spelling of it is the one that goes there — so an option value that does + not match the board verbatim still resolves to nothing, and for the Status + field that still costs the filing a transient exit with the entry left + pending. The field name here is configured exactly as the board spells it, + so the only thing differing is the option. + + That the item's Status is left unwritten is controlled by the ordinary + filing above, where the same drive with the option spelled as the board + spells it writes it. + """ + environment, ledger = stub_tracker(tmp_path) + configured = in_a_case_the_board_does_not_use(THIS_TARGETS_STATUS_OPTION) + + result = sync_to_the_board(script, tmp_path, environment, + key="k-option-in-another-case", + breaking={TEMPLATE_CONSTANTS["STATUS_OPTION"][0]: + configured}) + + assert result.returncode == TRANSIENT_EXIT, result.stderr + assert configured in result.stderr + assert board_items(ledger)[0].get("status", "") == "" + + +#: The three lookups that match a configured field name against the board's, +#: by the names the sync scripts give them: the read of this item's current +#: value, the resolution of a field's id, and the resolution of an option's id. +#: Written here rather than derived from the script, because the claim is about +#: these three in particular — a list read off the script would grow with a +#: fourth lookup and go on passing whatever that fourth one did. +FIELD_NAME_LOOKUPS = ("board_value", "field_id_for", "option_id_for") + +#: How a jq program spells the comparison this story gave one home: a name +#: matched with its spaces removed and its case ignored. Anywhere but inside +#: the shared definition, one of these is a lookup carrying a rule of its own. +NORMALIZING_IDIOM = re.compile(r'gsub\(" "; ""\)|ascii_downcase') + +#: The shared definition itself: a jq function of two names, up to the `;` that +#: closes it. Matched by its shape rather than by its name, so what is asserted +#: is that the script states the rule once and defers to it — not that it +#: chose a particular name for it. +SHARED_FIELD_NAME_RULE = re.compile( + r"""def (?P[a-z_]+)\(\$[a-z]+; *\$[a-z]+\):(?P.*?);(?=['"]|[ \t]*$)""", + re.DOTALL | re.MULTILINE) + + +def sync_function_body(text: str, name: str) -> str: + """One shell function's body, as the sync scripts lay them out.""" + found = re.search( + r"^[ \t]*%s\(\) \{\n(?P.*?)^[ \t]*\}$" % re.escape(name), + text, re.MULTILINE | re.DOTALL) + assert found, f"the script declares no {name}" + return found.group("body") + + +def rules_of_sameness_outside_the_shared_one(text: str) -> list[str]: + """Every line stating what makes two field names the same, other than the + one statement of it the script is supposed to hold. + + The shared definition is cut out of the text and what is scanned is what is + left, so a lookup that spelled the normalization inline is a line reported + here whether or not it also calls the shared one. + """ + stated = SHARED_FIELD_NAME_RULE.search(text) + assert stated, "the script states no shared field-name comparison at all" + assert NORMALIZING_IDIOM.search(stated.group("rule")), stated.group("rule") + elsewhere = text[:stated.start()] + text[stated.end():] + return [line for line in elsewhere.splitlines() + if NORMALIZING_IDIOM.search(line)] + + +def lookups_not_deferring_to_the_shared_rule(text: str) -> list[str]: + """Every field-name lookup whose body does not call the shared rule.""" + stated = SHARED_FIELD_NAME_RULE.search(text) + assert stated, "the script states no shared field-name comparison at all" + return [name for name in FIELD_NAME_LOOKUPS + if stated.group("name") not in sync_function_body(text, name)] + + +@pytest.mark.parametrize("script", BOTH_SYNC_COPIES) +def test_every_field_name_lookup_defers_to_one_statement_of_sameness(script): + """Both are shipped artifacts and both are the subject here. + + What the two filings above assert is that the three lookups agree today. + What this asserts is why they cannot come to disagree tomorrow: the script + says once what makes two field names the same, no line outside that + statement says it again, and each of the three lookups defers to it rather + than carrying a rule of its own. Three sites that happened to agree would + pass the filings and fail this. + """ + text = script.read_text(encoding="utf-8") + + assert rules_of_sameness_outside_the_shared_one(text) == [] + assert lookups_not_deferring_to_the_shared_rule(text) == [] + + +def test_those_scans_report_a_lookup_that_went_its_own_way(tmp_path): + """The control for both absences above, over two renderings of the template + that this repository does not ship. + + The first has the id lookup matching the configured name against the board + verbatim, which is the drift this story removed; the second has the read of + the item's value spelling the normalization inline, which is where the rule + used to live. Each is reported by the scan that exists to catch it, so + silence over the shipped copies is the copies and not a scan that stopped + seeing anything. + """ + text = TEMPLATE_SYNC.read_text(encoding="utf-8") + stated = SHARED_FIELD_NAME_RULE.search(text) + assert stated, "the template states no shared field-name comparison at all" + calling = f'{stated.group("name")}(.name; $name)' + + drifted = text.replace(f"select({calling}) | .id", + "select(.name == $name) | .id") + assert drifted != text, "the id lookup was not found to drift" + assert lookups_not_deferring_to_the_shared_rule(drifted) == ["field_id_for"] + + inline = text.replace( + f'select({stated.group("name")}(.key; $name))', + 'select((.key | gsub(" "; "") | ascii_downcase)' + ' == ($name | gsub(" "; "") | ascii_downcase))') + assert inline != text, "the read was not found to spell a rule inline" + reported = rules_of_sameness_outside_the_shared_one(inline) + assert reported, "the scan sees no rule stated outside the shared one" + assert all("ascii_downcase" in line for line in reported), reported + + #: The subcommand that listed a whole project to find one item in it. Written #: here rather than derived, because what is asserted is that no script and no #: filing names it any more — a name derived from the scripts would be the From 2b7d7879afa502ee4d7eb369a9ac2535ab563461 Mon Sep 17 00:00:00 2001 From: "jerod.wilkerson" <30474318+jerodw@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:52:45 -0600 Subject: [PATCH 4/5] l5 recorded a post-story inspection of story-129 --- .harness/history/inspection-log.jsonl | 1 + 1 file changed, 1 insertion(+) diff --git a/.harness/history/inspection-log.jsonl b/.harness/history/inspection-log.jsonl index e781fcd..7d8ff5b 100644 --- a/.harness/history/inspection-log.jsonl +++ b/.harness/history/inspection-log.jsonl @@ -19,3 +19,4 @@ {"story_id": "story-123", "mode": "narrow", "cost_usd": 1.7901849999999995, "scope_files": 60, "invocations": 1, "timestamp": "2026-09-08 21:32:25", "findings": 3, "filed": 3, "dropped": 0} {"story_id": "story-126", "mode": "narrow", "cost_usd": 1.9372045000000002, "scope_files": 60, "invocations": 1, "timestamp": "2026-09-09 01:47:09", "findings": 3, "filed": 3, "dropped": 0} {"story_id": "story-128", "mode": "narrow", "cost_usd": 1.633331, "scope_files": 60, "invocations": 1, "timestamp": "2026-09-09 08:37:45", "findings": 2, "filed": 2, "dropped": 0} +{"story_id": "story-129", "mode": "narrow", "cost_usd": 1.3884794999999999, "scope_files": 60, "invocations": 1, "timestamp": "2026-09-09 09:52:45", "findings": 1, "filed": 1, "dropped": 0} From 8131876e36606792f199ca79791b945bd225ca62 Mon Sep 17 00:00:00 2001 From: "jerod.wilkerson" <30474318+jerodw@users.noreply.github.com> Date: Wed, 9 Sep 2026 12:38:03 -0600 Subject: [PATCH 5/5] Fold the interpreter's pathlib rendering out of the signature comparison MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3.13 split pathlib internally, so a Path annotation renders as pathlib._local.Path where 3.10 and 3.14 render pathlib.Path. The test compares rendered signatures against expected strings, so only 3.13 disagreed — the type is the same type and no resolver's signature changed. Folded the way the HARNESS_ROOT default already is, so the assertion stays about the resolvers rather than about which interpreter read them. Surfaced by the matrix moving to 3.10, 3.13 and 3.14. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019WkxmJ5ArLEaC2RDPGkbRU --- tests/test_shared_baseline_resolution.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_shared_baseline_resolution.py b/tests/test_shared_baseline_resolution.py index 4c5604a..986a93e 100644 --- a/tests/test_shared_baseline_resolution.py +++ b/tests/test_shared_baseline_resolution.py @@ -300,11 +300,17 @@ def test_the_five_resolvers_keep_their_signatures(): default or moved a parameter between positional and keyword-only reports a different string here. The `HARNESS_ROOT` default renders as the repository path, which would make this a machine-specific literal, so it is folded - back to the name it is written under. + back to the name it is written under. The `Path` annotation is folded for + the same reason and it is an interpreter difference rather than a machine + one: 3.13 split `pathlib` internally and renders the annotation as + `pathlib._local.Path`, where 3.10 and 3.14 render `pathlib.Path`. The type + is the same type, so the fold keeps this an assertion about the resolvers' + signatures rather than about which interpreter read them. """ for name, expected in RESOLVER_SIGNATURES.items(): rendered = str(inspect.signature(getattr(conftest, name))) rendered = rendered.replace(repr(conftest.HARNESS_ROOT), "HARNESS_ROOT") + rendered = rendered.replace("pathlib._local.Path", "pathlib.Path") assert rendered == expected, name # The control: the comparison can differ. A resolver's signature read