Commit 0715857
feat(mt#4970): Exclude log-only-family records from the injected fire count
## Summary
`injectedFiresSinceLastReview` is the count the review thresholds key off, and its whole purpose (mt#3197) is to exclude detections the operator never saw. It computed that from `suppressionReasons` alone.
That is one of two ways a fire fails to reach the operator. The other is **per-FAMILY log-only gating inside a LIVE detector**: `turn-end-untaken-action-scan.ts` declares `LOG_ONLY_FAMILIES`, whose matches reach the calibration record and never `additionalContext`. Those records carry no suppression reason — nothing suppressed them; they were never eligible — and are otherwise **byte-identical** to an injecting family's.
So `untaken-action`'s 2026-09-04 window reported **120 injected fires where 23 reached the agent**. Every FP rate a reviewer computed over that denominator was 5x off, and the threshold that declared the log review-due fired on a population 81% invisible to the agent it was measuring.
## Key changes
- **Writer declares, sweep reads.** `toCalibrationMatches` marks each match whose family is log-only; the sweep excludes it via `isLogOnlyFamilyRecord` and reports it under `logOnlyFamilySinceLastReview`. A per-detector family table on the *reading* side is the drift hazard mt#4465 recorded for `judgedText` — two places would have to agree about `LOG_ONLY_FAMILIES` forever, and only one is edited when an arm flips.
- **`every`, not `some`.** A record carrying a log-only match *alongside* an injecting one DID reach the agent. 8 of the live window's 23 injected records are that shape; a `some` test would have swung the count wrong the other way.
- **`allWithheld` closes the cliff this exclusion opens.** See below.
- **Counted, not dropped.** The log-only arm's fire volume is exactly the evidence a decision to promote it would rest on.
### The cliff, and why it needed closing in the same PR
mt#4049 shipped `allSuppressed` days ago because keying cadence off the injected count inverts for a detector suppressing *all* its fires — the count pins at 0 and no count-bearing leg can fire.
Adding this exclusion opens the identical cliff through a **third column**: an all-log-only detector has `injectedFires === 0` **and** `suppressedSinceLastReview === 0`, so `atCountThreshold` is false, `allSuppressed` is false, and the log falls out of review entirely.
`allWithheld` (`injected === 0 && suppressed + logOnly >= FIRES_THRESHOLD`) **replaces** `allSuppressed` in the routing and equals it wherever no log-only family exists — every detector but `untaken-action` today, asserted as a test. `evaluatedOnly` is deliberately **outside** the union: those records never matched, so nothing is there to classify and their absence from review is correct rather than a gap.
## Testing
Execution evidence:
```
$ bun test --preload ./tests/setup.ts ./src/domain/calibration/calibration-sweep.log-only.test.ts
13 pass
0 fail
32 expect() calls
```
**AT2** (`a mixed record counts as injected, not log-only`), **AT3** (`a detector with no log-only families reports an unchanged count`, which also asserts `allWithheld === allSuppressed`), **AT4** (`false when the marker is ABSENT`) and **AT5** (`an all-log-only log at volume stays routable and keeps its records`) all pass in that run, named by number.
`calibration-sweep.ts` and the untaken-action hook are both widely imported, so the full related-test closure was run:
```
$ bun scripts/run-related-tests.ts src/domain/calibration/calibration-sweep.ts \
.minsky/hooks/turn-end-untaken-action-scan.ts \
src/domain/calibration/calibration-sweep.log-only.test.ts scripts/verify-log-only-exclusion.ts
3631 pass
0 fail
Ran 3631 tests across 93 files. [27.41s]
```
**AT1** — the live corpus, via the shipped predicate:
```
$ bun scripts/verify-log-only-exclusion.ts --log <state-dir>/untaken-action-calibration.jsonl
window: first 190 of 217 records
as-is (pre-declaration) injected= 120 logOnly= 0 suppressed= 70 evalOnly= 0
with declaration applied injected= 23 logOnly= 97 suppressed= 70 evalOnly= 0
OK: 97 records move from injected to log-only once the writer's declaration is present
(120 -> 23); the columns balance, and the as-is pass is unchanged at 120 because an absent
marker is not a declaration (AT4).
```
Guard canaries: `[PASS] turn-end-untaken-action-scan (registry, expects=warn)` — 74 total, 0 failed. Typecheck: 0 errors across 8 projects. Lint: 0 errors, 0 warnings.
Negative control — removing `!isLogOnlyFamilyRecord(r)` from the injected filter, restoring the pre-fix count exactly:
```
(fail) AT1: log-only records are counted under their own name, not as injected
(fail) AT2 end-to-end: a mixed record counts as injected, not log-only
(fail) a SUPPRESSED log-only record counts as suppressed, not log-only
(fail) AT5: an all-log-only log at volume stays routable and keeps its records
(fail) suppressed and log-only volume COMBINE to clear the bar
8 pass
5 fail
```
and the live artifact fails diagnostically rather than silently:
```
with declaration applied injected= 120 logOnly= 97
FAIL: 0 records left the injected column but 97 arrived in the log-only column. The two must
balance — a record may not be dropped from both or counted in both.
```
Reverted; the passing runs above are on the shipped tree.
## Deploy verification
`src/domain/calibration/calibration-sweep.ts` and its two test files return **true** from `isDeploySurfaceFile` (predicate run over the full changed set before writing this — the other 8 files return false, so no `[no-deploy-impact]` tag is claimed). Post-merge I will run `deployment_wait-for-latest` for `minsky-mcp` with `notBefore` set to the merge timestamp and `expectCommitSha` set to the merge SHA, and read `buildIdentity` rather than the status alone.
## Spec-decision reconciliation
Two calls, both recorded in the spec rather than left for review:
**SC3 vs AT4 — a genuine conflict, resolved in AT4's favour.** SC3 asks that the 2026-09-04 window report 23. AT4 requires a pre-declaration record to count as injected so historical FP rates do not shift under a reader. Every record in that window predates the declaration, so both cannot hold of the same run. The sweep treats an absent marker as "not declared"; reclassifying 97 already-acked records would move an FP rate under anyone who had computed one. Practical impact is nil — the watermark was advanced to 215 on 2026-09-04, so that window is acked and will not be swept again. SC3's figure is instead produced by the verification artifact above. Read SC3 as *"a window whose records carry the declaration reports 23."*
**SC5's "prefer generalizing over a parallel gate."** `allSuppressed` keeps its exact prior meaning and name; the generalization is `allWithheld`, which replaces it in the routing rather than sitting beside it. Widening `allSuppressed`'s own predicate was rejected: it would make `allSuppressed: true` for a log that suppressed nothing, which is a false statement to its six existing readers (including `scripts/verify-all-suppressed-routing.ts` from mt#4049) rather than a cosmetic naming issue.
## Notes for review
- Consumer enumeration was done at plan time and typecheck confirmed it: the only two sites needing changes were the fixture builders in `calibration-sweep.review-due.test.ts` and `calibration-review-cadence-detector.test.ts`, both of which now DERIVE the new fields the way they already derive `allSuppressed`, so no existing fixture changes behavior.
- `logOnly` is added to `CONSUMED_MATCH_KEYS` so it cannot also ride in `detectorFields` — mem#827 records reviewers reading that sub-object as if it were the record, and this field is read by the counting path.
- **mt#4318 does not subsume this** and vice-versa: that task surfaces a whole DETECTOR's `injection_enabled` posture; this is a per-FAMILY gate inside a detector that is live. `untaken-action` would read `injection_enabled: true` under mt#4318's fix and the 97 would still be miscounted. Both specs already reconcile each other; the skill note added here points at mt#4318 for the sibling case.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01TPNyYTreM4F7PRbvh4xA5b
Co-Authored-By: minsky-ai[bot] <minsky-ai[bot]@users.noreply.github.com>15 files changed
Lines changed: 822 additions & 19 deletions
File tree
- .claude
- hooks
- skills/calibration-review
- .minsky
- hooks
- skills/calibration-review
- docs/architecture/hooks
- scripts
- lib
- src
- domain/calibration
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
358 | 358 | | |
359 | 359 | | |
360 | 360 | | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
361 | 374 | | |
362 | 375 | | |
363 | 376 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1124 | 1124 | | |
1125 | 1125 | | |
1126 | 1126 | | |
1127 | | - | |
| 1127 | + | |
1128 | 1128 | | |
1129 | 1129 | | |
1130 | 1130 | | |
1131 | 1131 | | |
| 1132 | + | |
| 1133 | + | |
| 1134 | + | |
| 1135 | + | |
| 1136 | + | |
| 1137 | + | |
| 1138 | + | |
| 1139 | + | |
| 1140 | + | |
| 1141 | + | |
| 1142 | + | |
| 1143 | + | |
| 1144 | + | |
| 1145 | + | |
| 1146 | + | |
| 1147 | + | |
| 1148 | + | |
| 1149 | + | |
| 1150 | + | |
| 1151 | + | |
| 1152 | + | |
| 1153 | + | |
| 1154 | + | |
| 1155 | + | |
| 1156 | + | |
| 1157 | + | |
| 1158 | + | |
| 1159 | + | |
| 1160 | + | |
| 1161 | + | |
| 1162 | + | |
| 1163 | + | |
| 1164 | + | |
| 1165 | + | |
1132 | 1166 | | |
1133 | 1167 | | |
1134 | 1168 | | |
| |||
1511 | 1545 | | |
1512 | 1546 | | |
1513 | 1547 | | |
1514 | | - | |
| 1548 | + | |
1515 | 1549 | | |
1516 | 1550 | | |
1517 | 1551 | | |
| |||
1644 | 1678 | | |
1645 | 1679 | | |
1646 | 1680 | | |
1647 | | - | |
| 1681 | + | |
1648 | 1682 | | |
1649 | 1683 | | |
1650 | 1684 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
240 | 240 | | |
241 | 241 | | |
242 | 242 | | |
243 | | - | |
| 243 | + | |
244 | 244 | | |
245 | 245 | | |
246 | 246 | | |
247 | 247 | | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
248 | 270 | | |
249 | 271 | | |
250 | 272 | | |
| |||
Lines changed: 27 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
123 | 123 | | |
124 | 124 | | |
125 | 125 | | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
126 | 137 | | |
127 | 138 | | |
128 | 139 | | |
| |||
217 | 228 | | |
218 | 229 | | |
219 | 230 | | |
| 231 | + | |
220 | 232 | | |
221 | 233 | | |
222 | 234 | | |
| |||
271 | 283 | | |
272 | 284 | | |
273 | 285 | | |
| 286 | + | |
274 | 287 | | |
275 | 288 | | |
276 | 289 | | |
| |||
342 | 355 | | |
343 | 356 | | |
344 | 357 | | |
| 358 | + | |
345 | 359 | | |
346 | 360 | | |
347 | 361 | | |
| |||
394 | 408 | | |
395 | 409 | | |
396 | 410 | | |
| 411 | + | |
397 | 412 | | |
398 | 413 | | |
399 | 414 | | |
| |||
415 | 430 | | |
416 | 431 | | |
417 | 432 | | |
| 433 | + | |
418 | 434 | | |
419 | 435 | | |
420 | 436 | | |
| |||
436 | 452 | | |
437 | 453 | | |
438 | 454 | | |
| 455 | + | |
439 | 456 | | |
440 | 457 | | |
441 | 458 | | |
| |||
489 | 506 | | |
490 | 507 | | |
491 | 508 | | |
| 509 | + | |
492 | 510 | | |
493 | 511 | | |
494 | 512 | | |
| |||
503 | 521 | | |
504 | 522 | | |
505 | 523 | | |
| 524 | + | |
506 | 525 | | |
507 | 526 | | |
508 | 527 | | |
| |||
522 | 541 | | |
523 | 542 | | |
524 | 543 | | |
| 544 | + | |
525 | 545 | | |
526 | 546 | | |
527 | 547 | | |
| |||
553 | 573 | | |
554 | 574 | | |
555 | 575 | | |
| 576 | + | |
556 | 577 | | |
557 | 578 | | |
558 | 579 | | |
| |||
565 | 586 | | |
566 | 587 | | |
567 | 588 | | |
| 589 | + | |
568 | 590 | | |
569 | 591 | | |
570 | 592 | | |
| |||
599 | 621 | | |
600 | 622 | | |
601 | 623 | | |
| 624 | + | |
602 | 625 | | |
603 | 626 | | |
604 | 627 | | |
| |||
628 | 651 | | |
629 | 652 | | |
630 | 653 | | |
| 654 | + | |
631 | 655 | | |
632 | 656 | | |
633 | 657 | | |
| |||
665 | 689 | | |
666 | 690 | | |
667 | 691 | | |
| 692 | + | |
668 | 693 | | |
669 | 694 | | |
670 | 695 | | |
| |||
794 | 819 | | |
795 | 820 | | |
796 | 821 | | |
| 822 | + | |
797 | 823 | | |
798 | 824 | | |
799 | 825 | | |
| |||
854 | 880 | | |
855 | 881 | | |
856 | 882 | | |
| 883 | + | |
857 | 884 | | |
858 | 885 | | |
859 | 886 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
355 | 355 | | |
356 | 356 | | |
357 | 357 | | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
358 | 371 | | |
359 | 372 | | |
360 | 373 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1121 | 1121 | | |
1122 | 1122 | | |
1123 | 1123 | | |
1124 | | - | |
| 1124 | + | |
1125 | 1125 | | |
1126 | 1126 | | |
1127 | 1127 | | |
1128 | 1128 | | |
| 1129 | + | |
| 1130 | + | |
| 1131 | + | |
| 1132 | + | |
| 1133 | + | |
| 1134 | + | |
| 1135 | + | |
| 1136 | + | |
| 1137 | + | |
| 1138 | + | |
| 1139 | + | |
| 1140 | + | |
| 1141 | + | |
| 1142 | + | |
| 1143 | + | |
| 1144 | + | |
| 1145 | + | |
| 1146 | + | |
| 1147 | + | |
| 1148 | + | |
| 1149 | + | |
| 1150 | + | |
| 1151 | + | |
| 1152 | + | |
| 1153 | + | |
| 1154 | + | |
| 1155 | + | |
| 1156 | + | |
| 1157 | + | |
| 1158 | + | |
| 1159 | + | |
| 1160 | + | |
| 1161 | + | |
| 1162 | + | |
1129 | 1163 | | |
1130 | 1164 | | |
1131 | 1165 | | |
| |||
1508 | 1542 | | |
1509 | 1543 | | |
1510 | 1544 | | |
1511 | | - | |
| 1545 | + | |
1512 | 1546 | | |
1513 | 1547 | | |
1514 | 1548 | | |
| |||
1641 | 1675 | | |
1642 | 1676 | | |
1643 | 1677 | | |
1644 | | - | |
| 1678 | + | |
1645 | 1679 | | |
1646 | 1680 | | |
1647 | 1681 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
238 | 238 | | |
239 | 239 | | |
240 | 240 | | |
241 | | - | |
| 241 | + | |
242 | 242 | | |
243 | 243 | | |
244 | 244 | | |
245 | 245 | | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
246 | 268 | | |
247 | 269 | | |
248 | 270 | | |
| |||
0 commit comments