fix(memory): print the downstream drop counters, not just compute them (D2 follow-up)

07f8438 added `skippedScore` / `skippedNoEvidence` / `truncated` to the recall
result. They were correct and covered, and completely invisible: the debug log
line still printed only `skipped` / `skippedDim`, and `stats().last_recall`
carried only `skipped_fp` / `skipped_dim`.

A counter nobody can read is indistinguishable from no counter. The whole point
of D2 was that an operator whose threshold is swallowing the corpus reads
`skipped=0` and concludes nothing was excluded — that operator reads the LOG, not
the returned array.

## What this adds

- the debug line now ends its degradation group with
  `... skippedScore=<n> skippedNoEvidence=<n> truncated=<n> err=<...>`
- `last_recall` carries `skipped_score` / `skipped_no_evidence` / `truncated`,
  so the durable surface says the same thing the transient line does

Nothing else changes: the counters were already computed, and the existing
`skipped=0 skippedDim=0` substring assertions still match because the new fields
follow them.

## Criterion

L2 in `test/memory-log.test.js` already asserts the degradation group is printed
in one line; it now also requires
`skippedScore=0 skippedNoEvidence=0 truncated=0` to be present. That is the
"printed, not merely computed" half — the values themselves are pinned by
`test/recall-accounting.test.js` (non-zero cases).

Mutation: deleting the three fields from the log line turns L2 red —

    not ok 2 - L2: debugLog=true emits one line covering recall, budget, degradation and injection
    error: 'downstream drop counters'

Gates: npm test 298/298 (0 skipped), npm run check 12/12, memory-log 16/16.
This commit is contained in:
2026-11-05 11:44:11 +08:00
parent 07f8438964
commit 891582041d
2 changed files with 15 additions and 1 deletions

View File

@ -296,6 +296,7 @@ export class StableMem extends Service {
* disagree — the failure mode D4 exists to prevent.
*
* @type {{mode: string, skipped_fp: number, skipped_dim: number,
* skipped_score: number, skipped_no_evidence: number, truncated: number,
* embed_error: string|null, at: number} | null}
*/
lastRecall = null
@ -555,10 +556,18 @@ export class StableMem extends Service {
// case; 'ok' is recorded explicitly so the stats reader never has to
// distinguish "no field" from "no problem".
mode: hits.degraded ?? 'ok',
// `mode` says whether the answer is trustworthy. The three counters below
// say how many candidates were dropped DOWNSTREAM of the space filter —
// without them an operator whose threshold is swallowing the corpus reads
// `skipped=0` and concludes nothing was excluded. See
// recall-accounting.test.js (D2).
skipped_fp: hits.skipped ?? 0,
// Real, not a placeholder: the recall path applies the F4b dimension filter
// and reports it separately from the space skip.
skipped_dim: hits.skippedDim ?? 0,
skipped_score: hits.skippedScore ?? 0,
skipped_no_evidence: hits.skippedNoEvidence ?? 0,
truncated: hits.truncated ?? 0,
embed_error: hits.degradedError ?? null,
at: Date.now(),
}
@ -670,7 +679,9 @@ export class StableMem extends Service {
}`,
`degraded=${degraded} skipped=${raw?.skipped ?? 0} skippedDim=${
raw?.skippedDim ?? 0
} err=${summarizeError(raw?.degradedError)}`,
} skippedScore=${raw?.skippedScore ?? 0} skippedNoEvidence=${
raw?.skippedNoEvidence ?? 0
} truncated=${raw?.truncated ?? 0} err=${summarizeError(raw?.degradedError)}`,
`inject=${injectChars}ch`,
].join(' | ')

View File

@ -170,6 +170,9 @@ test('L2: debugLog=true emits one line covering recall, budget, degradation and
// degradation: the empty case is reported as healthy, explicitly.
assert.match(record.text, /degraded=no_embedder/, 'the mode is named, not omitted')
assert.match(record.text, /skipped=0 skippedDim=0/, 'skip counters')
// D2: the three downstream counters must be PRINTED, not merely computed.
// A counter nobody can read is indistinguishable from no counter at all.
assert.match(record.text, /skippedScore=0 skippedNoEvidence=0 truncated=0/, 'downstream drop counters')
assert.match(record.text, /err=-/, 'no error digest when there is no error')
// injection: the exact string length that reached the model.