Skip to content

fix(cli): let render_summary own the stage prefix exactly once (#504) - #505

Merged
jasonssdev merged 1 commit into
mainfrom
fix/504-curate-summary-double-prefix
Aug 9, 2026
Merged

fix(cli): let render_summary own the stage prefix exactly once (#504)#505
jasonssdev merged 1 commit into
mainfrom
fix/504-curate-summary-double-prefix

Conversation

@jasonssdev

Copy link
Copy Markdown
Owner

Closes #504

The defect

render_summary (src/openkos/cli/curate.py:1213) prefixes every line with "{stage}: ". Sixteen notice sites named their own stage as well, so those lines rendered doubled:

Identity: Identity: applied 1, skipped 0.
Structure: Structure: skipped -- Ollama unavailable (see above).

The probe-derived notices — probe.unavailable (curate.py:294) and probe.empty_message (curate.py:325,513,716,912) — never named a stage, so they rendered correctly. A single summary block therefore mixed two formats depending on which branch produced the outcome:

Identity: Identity: applied 1, skipped 0.      <- doubled
Structure: No untyped edges found.             <- correct

The issue as originally filed claimed uniform doubling and listed seven sites. Both were wrong and are corrected in this comment: the defect is inconsistency, and there are sixteen sites. The original repro was misleading because it constructed notices with the stage name instead of using real ones — it proved render_summary prepends, not that real notices double.

The fix

Strip the stage name from all sixteen sites; leave render_summary as the sole owner of the prefix. This adopts the convention the probe-derived paths were already using rather than inventing one. Each notice's post-prefix text is preserved byte for byte, so output changes only by losing the duplicate.

Blast radius verified as contained:

  • StageOutcome.notice is consumed at exactly one place (curate.py:1213); render_summary has exactly one caller (src/openkos/cli/main.py:11326).
  • ctx.ollama_unavailable_notice stores one of these strings but is only ever tested is not None (curate.py:1127), never printed on its own.
  • probe.notice (curate.py:1117) is a different object echoed raw to stderr — untouched.
  • Three stage.name uses correctly remain: the stderr refusal (curate.py:241), stage_notice observability (curate.py:1100), and the prefix owner itself (curate.py:1213).

Both docstrings now state the invariant so the next notice does not reintroduce it.

Why it survived this long

All 26 summary assertions in tests/unit/cli/test_curate.py were substring checks, and "Identity: applied 1" is a substring of "Identity: Identity: applied 1". 22 are now full-line assertions through a new _lines() helper; the other 4 are not summary-line assertions (one absence check, two fixture values, one equality against a production-built outcome).

That conversion — not the prefix removal — is the part that keeps this fixed.

Verification

  • TDD: the failing test came first. RED showed 'Identity: Identity: applied 1, skipped 0.' sitting beside the correct 'Structure: No untyped edges found.'
  • uv run pytest -q4037 passed, 1 skipped (was 4035; test_curate.py 98 → 100).
  • uv run mypy . clean on 179 files; ruff check and ruff format --check clean.
  • Mutation-verified: reintroducing Identity: at curate.py:469 alone fails six tests, then reverted with the inverse edit.

Disclosure

No review receipt. Two independent reasons, both stated plainly:

  1. The lens reviewers cannot complete in this runtime — they are provisioned Read, Grep, Glob with no shell while their protocol mandates gentle-ai review inspect-candidate and forbids substituting live files. Three launches on the previous candidate returned inspection: incomplete.
  2. I committed before opening the review, so the provider froze an empty candidate (0 files, 0 lines), classified it low risk with zero lenses, and would have issued a receipt that reviewed nothing. That lineage (review-505bbb08be465499) was deliberately left unfinalized rather than presented as coverage.

No PASS was authored in either case. The change stands on the suite, the lints, and the mutation evidence above.

`render_summary` prefixes every line with `"{stage}: "`, but sixteen of the
notice sites named their own stage as well, so those lines rendered doubled
-- `Identity: Identity: applied 1, skipped 0.` The probe-derived notices
(`probe.unavailable`, `probe.empty_message`) never named a stage and
rendered correctly, so a single summary block mixed two formats depending
on which branch produced the outcome.

Strip the stage name from all sixteen notice sites and leave
`render_summary` as the sole owner of the prefix. That aligns every line
with the probe-derived ones, which were already right -- this adopts the
convention half the paths were using, it does not invent one. Each notice's
post-prefix text is preserved byte for byte, so rendered output changes
only by losing the duplicate.

`StageOutcome.notice` is consumed at exactly one place and `render_summary`
has exactly one caller, so nothing else observes the change.
`ctx.ollama_unavailable_notice` stores one of these strings but is only ever
tested for `is not None`, never printed on its own. Both docstrings now
state the invariant.

The bug survived because all 26 summary assertions were substring checks,
and `"Identity: applied 1"` is a substring of `"Identity: Identity: applied
1"`. 22 of them are now full-line assertions through a new `_lines()`
helper; the other 4 were not summary-line assertions. Verified by
reintroducing the prefix at one site: six tests fail.

Closes #504
@jasonssdev
jasonssdev merged commit 01f216f into main Aug 9, 2026
6 checks passed
@jasonssdev
jasonssdev deleted the fix/504-curate-summary-double-prefix branch August 9, 2026 14:26
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.

curate's summary double-prefixes every stage line (Identity: Identity: applied 1, skipped 0.)

1 participant