Skip to content

fix(dor): stop the reconcile sweep mis-reading every routed issue as un-routed - #1001

Merged
TaekeK merged 2 commits into
mainfrom
bugfixes/reconcile-field-shift
Aug 11, 2026
Merged

fix(dor): stop the reconcile sweep mis-reading every routed issue as un-routed#1001
TaekeK merged 2 commits into
mainfrom
bugfixes/reconcile-field-shift

Conversation

@TaekeK

@TaekeK TaekeK commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #995 / #996. The field-shift below is a regression from #963.

Every hourly sweep was mailing out exceptions that were not real. 30 of the 31 items across the two health issues (#886, #938) were false.

The bug

The per-issue record was built with @tsv and read back with IFS=$'\t'. Tab is an IFS whitespace character, so bash collapses a run of tabs into a single delimiter — an empty field in the middle of a record shifts every field after it left.

sk_label sits in that middle slot and is empty on almost every issue (only a live build holds a sidekick). So the overwhelmingly common record, …<TAB><TAB>state:out-of-pipeline, parsed as:

sk_label=[state:out-of-pipeline]   state_label=[]     <- before
sk_label=[]   state_label=[state:out-of-pipeline]     <- after

Both lines are real output from gh issue list against this repo with the same jq program, old separator vs new.

Two consequences, both live:

  1. Every correctly-routed issue fell into the "no state:* label" branch and was reported "the agent likely never ran" — 29 on the Feature board plus [CI] Lockfile-only PRs skip both Vitest jobs — every npm Dependabot PR ships untested #997 on the Bug board. [CI] Lockfile-only PRs skip both Vitest jobs — every npm Dependabot PR ships untested #997 has carried state:out-of-pipeline since the moment it was triaged.
  2. That branch continues, so everything below it — the drift check (🔀), the approval-backlog count (🚦) and the stale-waiting warnings () — has never run for a routed issue in the sweep's entire life.

sk_label was inserted into the middle of the record by #963, which is where this starts.

The fix

Join the record on US (\u001f) instead. It is not IFS whitespace, so empty fields survive, and neither an issue number nor a GitHub label name can contain a control character. issues_tsv is renamed issues_rows, the old name having become a lie.

That is the whole behavioural change: two lines.

Scope note

An earlier revision of this PR also made Out of pipeline terminal in the closed-issue check, to silence the 🔚 lines on #995 and #874. That was reverted — leaving the pipeline is not the same as being filed away, and the 🔚 line is the only reminder that a closed issue is still parked there. Done remains the single resting state on the board. Test 7 now pins that behaviour instead of removing it.

Does this undo #996?

No. #996's build_phase() change is correct and all 9 of its assertions still pass here. Its target population — build-phase issues, which deliberately carry no state label — parses fine either way, because there state_label is a trailing empty field that read strips cleanly. Its reading of #370 was right.

What it could not see is the complementary population: issues that do have a state label. #996 fixed the control flow while the record feeding it was scrambled, so the sweep kept lying — just about a different set of issues.

Tests

The harness #996 shipped could not have caught this: its fixture hand-encoded the record, so the jq → read contract was duplicated in the test rather than exercised by it. The stub now serves the JSON gh would actually return and applies the script's own --jq program to it — the contract is under test.

8 new assertions, 17 total. Against main, the 4 that target the field shift fail and the 9 existing #995 assertions pass:

FAIL  a labelled issue is not mis-reported as un-routed
FAIL  the drift check can see the state label again
FAIL  …and names both sides of the disagreement
FAIL  a correctly-routed issue publishes no health report at all

The four closed-issue assertions pass against main too — they exist to pin behaviour that had no coverage at all, so the 🔚 signal cannot be quietly removed later.

Verification against live data

What the sweep will report after this

#886 drops to a single line — 🚦 2 issue(s) waiting in Awaiting approval — a real signal that has never fired. Four stale warnings (#872, #869, #680, #373, all at 13d) will appear as they cross the 14d threshold. #938 goes empty and auto-closes.

🤖 Generated with Claude Code

…un-routed

The per-issue record was `@tsv` read back with `IFS=$'\t'`. Tab is an IFS
*whitespace* character, so bash collapses a run of tabs into one delimiter: an
empty field in the middle of the record shifts every field after it left.

`sk_label` is empty on almost every issue (only a live build holds a sidekick),
so the common record `...<TAB><TAB>state:decompose` parsed as
sk_label='state:decompose', state_label=''. Two consequences, both live:

  * every correctly-routed issue took the "no state:* label" branch and was
    reported "the agent likely never ran" — 30 of them on the Feature board and
    #997 on the Bug board, all false;
  * that branch `continue`s, so the drift check, the approval-backlog count and
    the stale-waiting warnings below it never ran for any routed issue at all.

Join on US (\x1f) instead, which is not IFS whitespace, so empty fields survive.
Verified against live repo data: #997 now yields sk_label='',
state_label='state:out-of-pipeline'; under the old scheme it yields the reverse.
A read-only replay over both boards shows all 31 current exceptions go silent
and none turns into a drift flag.

Also treat "Out of pipeline" as terminal in the closed-issue check. The column
means "handled by the normal dev flow", so closing from it is the expected end,
not drift — #995 and #874 were being nagged about forever for being resolved
exactly as intended.

The harness could not have caught this: its fixture hand-encoded the record, so
the jq -> `read` contract was duplicated rather than tested. The stub now serves
the JSON `gh` would return and applies the script's OWN --jq program to it.
7 new assertions; the 4 that target these defects fail against main, the 9
existing #995 assertions still pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reverts the second half of the previous commit. Leaving the pipeline is not the
same as being filed away: a closed issue sitting in "Out of pipeline" still has
to be walked over to Done, and the 🔚 line is the only reminder that it is
there. Done stays the single resting state on the board.

The field-shift fix is untouched — that is what silences the 31 false 🕳️ lines.

Test 7 now pins the behaviour rather than removing it: a closed "Out of
pipeline" issue MUST be flagged, with a second closed row as the control so
both arms of the assertion are two-sided. 17 assertions; the same 4 field-shift
ones fail against main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@TaekeK
TaekeK merged commit 83da0e3 into main Aug 11, 2026
29 checks passed
@TaekeK
TaekeK deleted the bugfixes/reconcile-field-shift branch August 11, 2026 08:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant