Skip to content

fix(engine): make debtScore a measurement; scope rehydration candidates (OX-M6, OX-M7) - #38

Merged
ojassug merged 1 commit into
mainfrom
audit/lane-a-engine
Aug 29, 2026
Merged

fix(engine): make debtScore a measurement; scope rehydration candidates (OX-M6, OX-M7)#38
ojassug merged 1 commit into
mainfrom
audit/lane-a-engine

Conversation

@ojassug

@ojassug ojassug commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Audit OX-M7 and OX-M6, both in src/core/engine/index.ts. DECISIONS §64.

Measured over a corpus frozen at 8b447ce289 files, 578 rows, both CLI routes, ratio 0.3.
Three arms, comparison engines built with an src-only tsconfig; dist hash moved
32c40f61c05e3a7284583d99d959d9b93e76, so no arm was compared against itself.

M7 — debtScore was a constant

computeDebtBreakdown added metadata.originalBytes to elidedBytes for any item flagged
elided. But originalBytes is the item's entire pre-transform length and elided is a
boolean on the whole item — so an item that lost 5% of its bytes contributed 100% of its size to
the numerator. On the CLI a single file is a single-item bundle, making elidedBytes === totalBytes whenever anything was elided at all.

at the 35.00 ceiling distribution
baseline 317 / 317 rows carrying debt constant
fixed 0 / 317 1.31 → 34.99, median 33.08

Whether the file lost 4.7% or 66.8%, it scored 35.00. Math.min(1.0, …) in calculateDebt was
clamping a ratio with no business exceeding 1, which is why nothing ever looked wrong. Over the 101
reducing rows the implied ratio now tracks the measured byte cut with correlation 1.0000
0.047 against 0.047, 0.198 against 0.198, 0.429 against 0.429.

No output moved. Per-row across 578 rows the only field that changed is debtScore;
outputSha, byteIdentical, tokenBefore, tokenAfter, reduction, fallbackUsed,
driftScore, planMode and stageCount are identical.

The audit's stated mechanism was wrong and the finding was right. It described a denominator
mixing pre- and post-transform sizes. That does not happen — every stage setting elided also sets
originalBytes, so the denominator was already a clean sum of original sizes. The numerator was
the defect, and it was saturated, not skewed.

M6 — an empty candidate set meant "restore everything"

The guard read candidates && candidates.size > 0 && !candidates.has(item.id). The size > 0
clause exists for the missing ledger case; it also swallowed the case where a ledger exists and
reports zero items below the confidence threshold, turning "nothing needs restoring" into
"restore every elision in the bundle".

Reachability was checked, not assumed. No bundled entry point hits it — the CLI passes neither
hasher nor ledger, MCP and bench pass a hasher but no ledger, and the Gateway passes a ledger but
no hasher and plans only cleanup:session-dedup. It is reachable through the exported
optimize API. Reproduced there: a 1,481-byte item came back at exactly 1,481 bytes, every elision
undone, with debtScore recomputed to 0 on the restored bundle so the trace reported no debt
either.

Its corpus arm differs on 0 of 578 rows — and that is not evidence of correctness. The harness
runs CLI routes, which supply no ledger, so it cannot see the shape at all. This is §56's lesson
with the sign reversed: byte-identical is not inert, and here it is not even informative.

The test carries two controls because the first version passed against the unfixed code. The
default maxDebtThreshold of 75 is unreachable on turn 1 — confidence penalty 0, turn age 0,
elision term capped at 35 — so the branch it claimed to exercise never ran. One control proves the
setup elides at all; the other proves the branch is entered under maxDebtThreshold: 1, via the
no-ledger path where a full restore is the intended behaviour rather than the bug.

What this does not establish

  • Nothing about the Gateway. Neither fix changes Gateway behaviour, and neither was measured
    there.
  • Nothing about multi-item CLI bundles. The harness runs one file per invocation. M7's
    correction is larger on multi-item bundles, but that is reasoned, not measured.
  • No absolute figure here is comparable to §2. This corpus is 289 files at 8b447ce; the
    recipe expected 62 TypeScript and 17 prose and selected 63 and 18, because the repo has grown.
    Only the per-row A/B over this one frozen corpus means anything.
  • --max-debt still gates nothing on the CLI. Debt is a real number now, not yet a live gate:
    the default threshold of 75 stays unreachable without a ledger. Whether the threshold or weights
    should change is a separate question and was not touched.

Verification

npm run typecheck, npm run lint, npm run build and npx vitest run all pass: 86 files /
791 tests
. Both fixes confirmed failing against the unfixed tree first.

🤖 Generated with Claude Code

Audit OX-M7 and OX-M6. DECISIONS §64. Measured over a corpus frozen at
8b447ce -- 289 files, 578 rows, both CLI routes, ratio 0.3.

M7: computeDebtBreakdown added metadata.originalBytes to elidedBytes for
any item flagged `elided`. But originalBytes is the item's ENTIRE
pre-transform length and `elided` is a boolean on the whole item, so an
item that lost 5% of its bytes contributed 100% of its size to the
numerator. On the CLI a single file is a single-item bundle, making
elidedBytes === totalBytes whenever anything was elided at all.

Baseline: 317 of the 317 rows carrying any debt scored debtScore exactly
35.00 -- the weightElisionRatio * 100 ceiling -- whether the file lost
4.7% or 66.8%. Math.min(1.0, ...) in calculateDebt was clamping a ratio
with no business exceeding 1, which is why nothing looked wrong. The
number was a constant wearing the name of a measurement.

After (bytes actually removed): 0 of 317 at the ceiling, distribution
1.31 -> 34.99, and over the 101 reducing rows the implied ratio tracks
the measured byte cut with correlation 1.0000.

Output did not move: per-row across 578 rows the only field that changed
is debtScore. outputSha, byteIdentical, tokenBefore, tokenAfter,
reduction, fallbackUsed, driftScore, planMode and stageCount identical.

The audit described a denominator mixing pre- and post-transform sizes.
That is not what happened -- every stage setting `elided` also sets
originalBytes, so the denominator was already clean. The numerator was
the defect, and saturated rather than skewed.

M6: attemptAutomatedRehydration guarded with `candidates &&
candidates.size > 0 && !candidates.has(item.id)`. The size > 0 clause is
for the MISSING ledger case; it also swallowed a ledger reporting zero
items below threshold, turning "nothing needs restoring" into "restore
every elision in the bundle".

No bundled entry point reaches it: the CLI passes neither hasher nor
ledger, MCP and bench pass a hasher but no ledger, and the Gateway passes
a ledger but no hasher and plans only session-dedup. It IS reachable
through the exported `optimize` API. Reproduced there: a 1,481-byte item
came back at exactly 1,481 bytes, every elision undone, with debtScore
recomputed to 0 on the restored bundle so the trace showed no debt.

Its corpus arm differs on 0 of 578 rows. That is what the structure
predicts and is NOT evidence of correctness -- the harness runs CLI
routes, which supply no ledger, so it cannot see the shape. §56's lesson
with the sign reversed.

The M6 test carries two controls because the first version passed against
the unfixed code: the default maxDebtThreshold of 75 is unreachable on
turn 1, so the branch it claimed to exercise never ran.

Verified: typecheck, lint and build clean, 86 files / 791 tests. Both
fixes confirmed failing against the unfixed tree first. Comparison engine
built with an src-only tsconfig; dist hash moved 32c40f61c05e ->
3a7284583d99 -> d959d9b93e76 across the three arms.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ojassug
ojassug merged commit f9db85a into main Aug 29, 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