Skip to content

Phase 1c: one bad item no longer reverts the good ones - #14

Merged
ojassug merged 1 commit into
mainfrom
phase-1c
Aug 11, 2026
Merged

Phase 1c: one bad item no longer reverts the good ones#14
ojassug merged 1 commit into
mainfrom
phase-1c

Conversation

@ojassug

@ojassug ojassug commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Phase 1c — per-item repair. The audit's §3.1, and the binding constraint on multi-file value
since H5 made multi-item bundles reachable. Full reasoning in DECISIONS §47.

Stacked on #13 (H2/M1/M11), which is still
open — this branches from it. Merge that one first.

The problem, measured

Validation is bundle-scoped and fallback was all-or-nothing. On the frozen 45-file Python corpus
at --target-reduction-ratio 0.3:

stages achieved      42.52%
26 CONSTRAINT_DIRECTIVE_LOST errors across 14 items
drift                0.0359   (gate: 0.40)
AST                  clean
emitted               0.00%

Fourteen items reverted forty-five.

Result

bundle before after reverted fallback
45 Python files 0.00% 22.73% 14 of 45 false
61 TypeScript files 0.00% 19.47% 21 of 61 false

How

Three pieces, in dependency order.

Attribution became data. It already existed — as prose. ValidationIssue carried
"…in item [<id>]…" interpolated into message and nothing else, which is unusable by anything
that has to act on it; recovering it by regex would have been audit M5b exactly.
ValidationIssue.itemId is now a field, populated by the checks that knew it all along.

validate() reports what it can attribute. Drift splits: SEMANTIC_DRIFT_UNMEASURABLE is
attributable (the measurement gate refuses specific items and unwitnessedItemIds names them,
§33); SEMANTIC_DRIFT_EXCEEDED is not, being a whole-bundle set comparison.

The engine repairs before giving up. Named items are reverted, and the candidate goes back
through the same validate, adopted only if it passes. Shaped like — and placed after — the
automated-rehydration attempt already there, because it is the same move. The load-bearing
property: repair changes which bundle is offered, never what counts as valid.

Two things worth reviewing closely

The refusal gate was tried too strict, and measurement corrected it. The first rule was
"refuse if any error is unattributable" — it reads as conservative and is wrong. The TypeScript
bundle fails on both attributable constraint losses and SEMANTIC_DRIFT_EXCEEDED at 0.4122, so
the failure that named nothing discarded the attribution that named 21 items, and the run stayed
at 0.00%. The question is not "is every error attributed?" but "is there a principled subset to
revert?"
— safe because the candidate is re-validated regardless, and reverting items lowers
semantic loss, which is what drift measures:

python      drift  0.0359 -> 0.0141
typescript  drift  0.4122 -> 0.0056

Reverting a subset when no error names anything would be guessing, and still refuses.

Repair declines when every changed item would be reverted, routing to the real fallback
instead. Not cosmetic: fallback echoes request.rawInput and the CLI writes the original
Buffer, while repair renders from items — §35 exists because those are not the same bytes
for input that isn't valid UTF-8. "Every item" accounts for pruning, since a bundle whose
survivors were all reverted is still a real reduction if the planner dropped items.

Verification

  • 606 tests passing (was 599), typecheck and lint clean.
  • 574 of 574 corpus rows identical to the pre-1c engine, same frozen corpus, varying only
    dist/. Expected: the harness measures single-file runs, where repair cannot fire. Phase 1c
    adds value where the audit said it was missing and nowhere else.
  • 14 of 14 single-file fallbacks still byte-identical, none reporting itemsReverted — a
    one-item bundle can never be a partial success, and the code says so rather than relying on it
    not arising.

One pass, not a loop to fixpoint: a loop terminates but costs a whole-bundle AST pass per
iteration (O(n²) on the bundles that need it most). One pass sufficed on both corpora.

trace.itemsReverted names what was put back, present only on a partial success — without it a
reduction with fallbackUsed: false conceals that anything was restored, which is invariant 10's
shape.

A measurement caution

The TypeScript bucket read 19.76% against a previous 23.03% and looked like a regression.
The pre-1c engine reads 19.76% on the same frozen corpus too — the corpus changed because this
change edits src/, and src/ is the TypeScript bucket. Now recorded in the status doc's
traps.

Still open on this axis

Drift remains a bundle-scoped score. It is repairable in practice — reverting items lowers it —
but it never names an item itself, so a bundle failing on drift alone still falls back whole.

🤖 Generated with Claude Code

Phase 1c — per-item repair. Audit §3.1, and the binding constraint on multi-file
value since H5 made multi-item bundles reachable. Full reasoning in DECISIONS §47.

Validation is bundle-scoped and fallback was all-or-nothing. Measured on the
frozen 45-file Python corpus at targetReductionRatio 0.3, the stages achieved
42.52% and the run emitted 0.00%: 26 CONSTRAINT_DIRECTIVE_LOST errors across 14
items reverted all 45, with drift at 0.0359 against a 0.40 gate and AST clean.

  45 Python files      0.00% -> 22.73%   14 of 45 reverted
  61 TypeScript files  0.00% -> 19.47%   21 of 61 reverted

Three pieces, in dependency order.

Attribution became data. It already existed as prose — `ValidationIssue` carried
"…in item [<id>]…" interpolated into `message` and nothing else, which is unusable
by anything that has to act on it, and recovering it by regex would have been
audit M5b exactly. `ValidationIssue.itemId` is now a field, populated by the AST
and constraint checks that knew it all along.

`validate()` says what it can and cannot attribute, via `FailureAttribution`.
Drift splits: SEMANTIC_DRIFT_UNMEASURABLE is attributable, because the measurement
gate refuses specific items and `unwitnessedItemIds` names them (§33);
SEMANTIC_DRIFT_EXCEEDED is not, being a set comparison over the whole bundle.

The engine reverts named items, re-validates through the **same** `validate`, and
adopts the result only if it passes. Deliberately shaped like, and placed after,
the automated-rehydration attempt already there — it is the same move. The
load-bearing property is that repair changes which bundle is offered, never what
counts as valid; nothing here decides an item is acceptable.

The refusal gate was tried too strict and corrected by measurement. "Refuse if any
error is unattributable" reads as the conservative choice and is wrong: the
TypeScript bundle fails on both attributable constraint losses and
SEMANTIC_DRIFT_EXCEEDED at 0.4122, so the failure naming nothing discarded the
attribution naming 21 items and the run stayed at 0.00%. The question is not "is
every error attributed?" but "is there a principled subset to revert?" — safe
because the candidate is re-validated regardless, and reverting items lowers
semantic loss, which is what drift measures: drift fell 0.4122 -> 0.0056
(TypeScript) and 0.0359 -> 0.0141 (Python). Reverting a subset when *no* error
names anything would be guessing, and still refuses.

Repair declines, routing to the real fallback, when every changed item would be
reverted. That is a fallback wearing a different name, and the distinction is
load-bearing: fallback echoes `request.rawInput` and the CLI writes the original
Buffer, while repair renders from items — §35 exists because those are not the
same bytes for input that is not valid UTF-8. Measured: 14 of 14 single-file
fallbacks remain byte-identical and none reports itemsReverted. "Every item"
accounts for pruning, since a bundle whose survivors were all reverted is still a
real reduction if the planner dropped items.

One pass, not a loop to fixpoint. A loop terminates — each pass reverts at least
one more item — but costs a whole-bundle AST pass per iteration, so worst case is
O(n²) validations on the bundles that need it most. One pass sufficed on both
corpora; a second round of attributable failures falls back rather than iterating.

`trace.itemsReverted` names what was put back, present only on a partial success.
Without it the outcome is a reduction with fallbackUsed false and no indication
anything was restored — invariant 10's shape.

574 of 574 corpus rows are identical to the pre-1c engine, same frozen corpus,
varying only dist/. That is the expected result: the harness measures single-file
runs, where repair cannot fire. Phase 1c adds value where the audit said it was
missing and nowhere else.

One measurement caution, now in the status doc: the TypeScript bucket read 19.76%
against a previous 23.03% and looked like a regression. The pre-1c engine reads
19.76% on the same frozen corpus too — the corpus changed because this change
edits src/, and src/ *is* the TypeScript bucket.

Suite: 606 passing (was 599), typecheck and lint clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ojassug
ojassug changed the base branch from audit-decisions to main August 11, 2026 11:23
@ojassug ojassug closed this Aug 11, 2026
@ojassug ojassug reopened this Aug 11, 2026
@ojassug
ojassug merged commit aafd9b9 into main Aug 11, 2026
3 checks passed
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