agentfootprint 9.103.0 → 9.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +168 -88
  3. package/CLAUDE.md +3 -2
  4. package/ai-instructions/claude-code/SKILL.md +1 -1
  5. package/dist/classify/index.js +20 -0
  6. package/dist/classify/index.js.map +1 -0
  7. package/dist/classify/mock.js +44 -0
  8. package/dist/classify/mock.js.map +1 -0
  9. package/dist/classify/types.js +47 -0
  10. package/dist/classify/types.js.map +1 -0
  11. package/dist/classify/typesafe.js +249 -0
  12. package/dist/classify/typesafe.js.map +1 -0
  13. package/dist/core/Agent.js +5 -0
  14. package/dist/core/Agent.js.map +1 -1
  15. package/dist/core/agent/AgentBuilder.js +14 -0
  16. package/dist/core/agent/AgentBuilder.js.map +1 -1
  17. package/dist/core/agent/findings/judge.js +173 -0
  18. package/dist/core/agent/findings/judge.js.map +1 -0
  19. package/dist/core/agent/findings/ledger.js +50 -6
  20. package/dist/core/agent/findings/ledger.js.map +1 -1
  21. package/dist/core/agent/findings/types.js +8 -1
  22. package/dist/core/agent/findings/types.js.map +1 -1
  23. package/dist/core/agent/stages/toolCalls.js +65 -0
  24. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  25. package/dist/doors/classify.js +52 -0
  26. package/dist/doors/classify.js.map +1 -0
  27. package/dist/doors/skill-graph.js +9 -1
  28. package/dist/doors/skill-graph.js.map +1 -1
  29. package/dist/esm/classify/index.d.ts +13 -0
  30. package/dist/esm/classify/index.js +14 -0
  31. package/dist/esm/classify/index.js.map +1 -0
  32. package/dist/esm/classify/mock.d.ts +26 -0
  33. package/dist/esm/classify/mock.js +40 -0
  34. package/dist/esm/classify/mock.js.map +1 -0
  35. package/dist/esm/classify/types.d.ts +111 -0
  36. package/dist/esm/classify/types.js +43 -0
  37. package/dist/esm/classify/types.js.map +1 -0
  38. package/dist/esm/classify/typesafe.d.ts +58 -0
  39. package/dist/esm/classify/typesafe.js +245 -0
  40. package/dist/esm/classify/typesafe.js.map +1 -0
  41. package/dist/esm/core/Agent.js +5 -0
  42. package/dist/esm/core/Agent.js.map +1 -1
  43. package/dist/esm/core/agent/AgentBuilder.js +14 -0
  44. package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
  45. package/dist/esm/core/agent/findings/judge.d.ts +84 -0
  46. package/dist/esm/core/agent/findings/judge.js +168 -0
  47. package/dist/esm/core/agent/findings/judge.js.map +1 -0
  48. package/dist/esm/core/agent/findings/ledger.d.ts +16 -6
  49. package/dist/esm/core/agent/findings/ledger.js +50 -6
  50. package/dist/esm/core/agent/findings/ledger.js.map +1 -1
  51. package/dist/esm/core/agent/findings/types.d.ts +72 -1
  52. package/dist/esm/core/agent/findings/types.js +7 -0
  53. package/dist/esm/core/agent/findings/types.js.map +1 -1
  54. package/dist/esm/core/agent/stages/toolCalls.d.ts +15 -0
  55. package/dist/esm/core/agent/stages/toolCalls.js +42 -0
  56. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  57. package/dist/esm/core/agent/types.d.ts +17 -0
  58. package/dist/esm/doors/classify.d.ts +35 -0
  59. package/dist/esm/doors/classify.js +36 -0
  60. package/dist/esm/doors/classify.js.map +1 -0
  61. package/dist/esm/doors/skill-graph.d.ts +2 -0
  62. package/dist/esm/doors/skill-graph.js +5 -0
  63. package/dist/esm/doors/skill-graph.js.map +1 -1
  64. package/dist/esm/events/payloads.d.ts +50 -0
  65. package/dist/esm/events/registry.d.ts +5 -1
  66. package/dist/esm/events/registry.js +4 -0
  67. package/dist/esm/events/registry.js.map +1 -1
  68. package/dist/esm/index.d.ts +1 -1
  69. package/dist/esm/index.js.map +1 -1
  70. package/dist/esm/lib/injection-engine/classifierScorer.d.ts +44 -0
  71. package/dist/esm/lib/injection-engine/classifierScorer.js +91 -0
  72. package/dist/esm/lib/injection-engine/classifierScorer.js.map +1 -0
  73. package/dist/esm/lib/injection-engine/index.d.ts +1 -0
  74. package/dist/esm/lib/injection-engine/index.js +1 -0
  75. package/dist/esm/lib/injection-engine/index.js.map +1 -1
  76. package/dist/events/registry.js +4 -0
  77. package/dist/events/registry.js.map +1 -1
  78. package/dist/index.js.map +1 -1
  79. package/dist/lib/injection-engine/classifierScorer.js +95 -0
  80. package/dist/lib/injection-engine/classifierScorer.js.map +1 -0
  81. package/dist/lib/injection-engine/index.js +6 -2
  82. package/dist/lib/injection-engine/index.js.map +1 -1
  83. package/dist/types/classify/index.d.ts +14 -0
  84. package/dist/types/classify/index.d.ts.map +1 -0
  85. package/dist/types/classify/mock.d.ts +27 -0
  86. package/dist/types/classify/mock.d.ts.map +1 -0
  87. package/dist/types/classify/types.d.ts +112 -0
  88. package/dist/types/classify/types.d.ts.map +1 -0
  89. package/dist/types/classify/typesafe.d.ts +59 -0
  90. package/dist/types/classify/typesafe.d.ts.map +1 -0
  91. package/dist/types/core/Agent.d.ts.map +1 -1
  92. package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
  93. package/dist/types/core/agent/findings/judge.d.ts +85 -0
  94. package/dist/types/core/agent/findings/judge.d.ts.map +1 -0
  95. package/dist/types/core/agent/findings/ledger.d.ts +16 -6
  96. package/dist/types/core/agent/findings/ledger.d.ts.map +1 -1
  97. package/dist/types/core/agent/findings/types.d.ts +72 -1
  98. package/dist/types/core/agent/findings/types.d.ts.map +1 -1
  99. package/dist/types/core/agent/stages/toolCalls.d.ts +15 -0
  100. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  101. package/dist/types/core/agent/types.d.ts +17 -0
  102. package/dist/types/core/agent/types.d.ts.map +1 -1
  103. package/dist/types/doors/classify.d.ts +36 -0
  104. package/dist/types/doors/classify.d.ts.map +1 -0
  105. package/dist/types/doors/skill-graph.d.ts +2 -0
  106. package/dist/types/doors/skill-graph.d.ts.map +1 -1
  107. package/dist/types/events/payloads.d.ts +50 -0
  108. package/dist/types/events/payloads.d.ts.map +1 -1
  109. package/dist/types/events/registry.d.ts +5 -1
  110. package/dist/types/events/registry.d.ts.map +1 -1
  111. package/dist/types/index.d.ts +1 -1
  112. package/dist/types/index.d.ts.map +1 -1
  113. package/dist/types/lib/injection-engine/classifierScorer.d.ts +45 -0
  114. package/dist/types/lib/injection-engine/classifierScorer.d.ts.map +1 -0
  115. package/dist/types/lib/injection-engine/index.d.ts +1 -0
  116. package/dist/types/lib/injection-engine/index.d.ts.map +1 -1
  117. package/package.json +14 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,80 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [9.104.0] - 2026-09-17
9
+
10
+ ### Added — a calibrated judge beside the model's own standings
11
+
12
+ - **`agentfootprint/classify`** — a new door for a classifier that SCORES
13
+ declared candidates instead of generating text: the `Classifier` port
14
+ (`{ name; classify(request, signal?) }`), the request shapes (`choice` over
15
+ declared options, `noul` as a probability, `score` on a declared scale),
16
+ the answer shapes (the pick, its confidence and the WHOLE distribution),
17
+ `ClassifierError` (`status?`, `retryable`), and two adapters —
18
+ `typesafe()` (TypeSafe "System One", model `jev-latest`; key from
19
+ `TYPESAFE_API_KEY` or `apiKey`, refused at construction when missing; one
20
+ `fetch`, no SDK; exponential backoff on 429/529 up to `maxRetries`;
21
+ `latencyMs` measured around the whole call; the wire mapped field for
22
+ field, probabilities never renormalised, the key never in an error) and
23
+ `mockClassifier(script)` for tests and the bench (every request on
24
+ `calls`). Verified 2026-09-17 by one real call; the suite maps that probe
25
+ against a stubbed `fetch` and never calls the hosted classifier.
26
+ - **`.findings({ judge })`** — a SECOND SOURCE on the findings ledger. After
27
+ every tool result lands and before the next model call, the judge is
28
+ asked what the RESULT is worth for the proposition the model declared on
29
+ the call, or for the user's question when none was declared (never why
30
+ the tool was called), and its answer is filed as a `JudgmentRow` beside
31
+ the model's own `StandingRow`: `source: 'judge'`, the judge's name and
32
+ the provider's model string, `against: 'proposition' | 'question'`, the
33
+ standing, the distribution, the confidence, `testsSubject`, usage,
34
+ latency, `clipped` when the result was cut at 4000 characters, the
35
+ iteration. A failed call is a `JudgmentErrorRow` (status, the provider's
36
+ message, latency) — never a guessed standing — and the run continues.
37
+ `foldLedger` gains `judgments` (last wins) and leaves `standingOf` the
38
+ model's; nothing is served from a judgment in this release (policy A —
39
+ the design page names B–D and leaves them to the bench). The probe's
40
+ disagreement — the model said `ruled-out`, the judge said `noise` at
41
+ 0.69 — is the first recorded fact of the second source. Root exports
42
+ `JudgmentRow` / `JudgmentErrorRow`; `agent.findings()` returns the rows
43
+ as before.
44
+ - **Events** `agentfootprint.findings.judged` (`toolCallId`, `toolName`,
45
+ `iteration`, `against`, `standing`, `confidence`, `latencyMs`,
46
+ `inputTokens?`, `outputTokens?`) and `agentfootprint.findings.judge_failed`
47
+ (`toolCallId`, `toolName`, `iteration`, `status?`, `latencyMs`) —
48
+ identities, enums and numbers, never the state; and
49
+ `agentfootprint.findings.standing` gains `agrees?: boolean`, the model's
50
+ standing against the judge's current judgment of the same result, present
51
+ exactly when a judgment row exists (the judge files before the model call
52
+ that declares, so the standing event is where both readings exist). 115
53
+ typed events across 25 domains.
54
+ - Only a result the tool PRODUCED is judged: a permission denial, a halt, a
55
+ fail-closed refusal or a declined check-in files no judgment row and
56
+ spends no classifier call — a denial text is not a tool result.
57
+ - `typesafe()` refuses a reply without the provider's `model` string or an
58
+ answer whose numeric field (`confidence`, `score`, `noul`) is missing or
59
+ not a finite number, as a `ClassifierError` — never `'unknown'` or `NaN`
60
+ on the record; a per-attempt `timeout` ends the call after one attempt,
61
+ not retryable.
62
+ - **`classifierScorer(classifier)`** on `agentfootprint/skill-graph` — an
63
+ `EntryScorer` whose ranking IS the provider's distribution (the scored
64
+ choice, design step 2): one `choice` question over the entry candidates
65
+ with `{ id: description }` as criteria, `score` and `relevance` both the
66
+ probability as sent (a candidate the provider did not score is absent
67
+ from the ranking, never a padded 0), `chosen` the provider's own pick,
68
+ `scorer: 'classifier:<model>'`. A provider failure returns `scorer:
69
+ 'classifier:unavailable'` with an empty ranking and no `chosen`, and the
70
+ existing cold-start entry pick takes over — never a guess.
71
+ - **Bench** `bench/findings-shuffle.mjs`: `AF_SHUFFLE_JUDGE=mock|typesafe`
72
+ arms every armed condition with a judge and adds `judge-accuracy`,
73
+ `judge-agrees`, `judge-tokens` and `judge-latency-ms`, with the classifier
74
+ calls stated on the cost line before the first is made; the mock judge
75
+ scripts the planted truth at a fixed confidence and the smoke laws pin
76
+ it. Unset, the bench prints the 9.103.0 table byte for byte.
77
+ - Byte identity: the 17 references pass untouched; `agent-findings-judge`
78
+ is the one new reference, generated alone with a scripted judge — its
79
+ delta over `agent-findings` is the two judgment rows and the declared
80
+ proposition, and nothing under `served.*`.
81
+
8
82
  ## [9.103.0] - 2026-09-17
9
83
 
10
84
  ### Added — an answer-turn ask on the served ledger piece, as a dial
@@ -70,7 +144,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
70
144
  the refusal engine saw a declared fact and a declared noise result as the
71
145
  same bytes. By the model's claim only: a turn is the removal unit and its
72
146
  standing is its most valuable result's (`fact > open > undeclared >
73
- ruled-out > noise`); a result the model never named is undeclared and is not
147
+ ruled-out > noise`); a result the model never named is undeclared and is not
74
148
  held; the library reads no result's text to decide otherwise.
75
149
  - A fact hold never exists without its ceiling and its stand-down. The
76
150
  ceiling is spent newest first and a held turn already inside
@@ -138,7 +212,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
138
212
  So from the second call on, the reserved `_findings` property on every
139
213
  served schema binds the ids the model may name: `previous[].toolCallId`
140
214
  carries `enum: <the tool results on the wire with no standing yet, newest
141
- first, at most 32>` and says "one of the ids listed; a result not listed
215
+ first, at most 32>` and says "one of the ids listed; a result not listed
142
216
  cannot be named here"; a clipped list states the cap. The list is what the
143
217
  model can still READ: a result with no standing, a `fact` (stood on in the
144
218
  piece, served verbatim under the default mode) and an `open` result (served
@@ -161,7 +235,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
161
235
  `receipt.tools.schemaHashes` moves when the offer does. The law holds at
162
236
  the schema AND at the row: the offer is what the model may COPY, never what
163
237
  the library resolves — an id outside it still files as written, `unknownId:
164
- true`, never mapped to a position or a tool name — and every id INSIDE it
238
+ true`, never mapped to a position or a tool name — and every id INSIDE it
165
239
  resolves, because a standing is identified against the same served history
166
240
  the offer was read from (`findings/offer.ts · knownResults`: the served
167
241
  `role: 'tool'` messages plus the previous batch), so an id copied from the
@@ -214,7 +288,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
214
288
 
215
289
  - Since 9.101.0, `withoutFindingsArgument` recognised the library's
216
290
  decoration by REFERENCE only (`properties._findings ===
217
- FINDINGS_ARGUMENT_SCHEMA`), and the reference never holds on the live path:
291
+ FINDINGS_ARGUMENT_SCHEMA`), and the reference never holds on the live path:
218
292
  the served list `callLLM` reads is the committed `dynamicToolSchemas`, a
219
293
  `structuredClone` of what the slot planted. So the choice seam's enum fence
220
294
  (`declaredEnumValuesOf(withoutFindingsArgument(schema))`, the
@@ -229,7 +303,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
229
303
  written. An armed agent may therefore file an `unsupported-argument`
230
304
  finding it previously suppressed. Its unit test had passed because it
231
305
  handed the function the live reference; `test/core/agent/findings/
232
- reserved.test.ts` now pins the clone.
306
+ reserved.test.ts` now pins the clone.
233
307
 
234
308
  ### Unchanged — the unarmed agent, and the two armed references regenerated alone
235
309
 
@@ -326,7 +400,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
326
400
  `open`. Bounded, every overflow stated, an empty bucket omitted. On the
327
401
  wire, and only on the wire, a tool result the model judged `noise` or
328
402
  `ruled-out` is served as a ticket, `{"collapsed":true,"standing":…,
329
- "toolCallId":…}`, in place of its content; `open` and undeclared results
403
+ "toolCallId":…}`, in place of its content; `open` and undeclared results
330
404
  stay verbatim; nothing is dropped or reordered, and `toolName` /
331
405
  `toolCallId` are untouched, so the tool_use/tool_result pair stays
332
406
  wire-valid. `history` never changes — the window stage stays its only
@@ -620,7 +694,7 @@ footprintjs the range admits (`^9.21.1`).
620
694
  touched before); `cache-transform`'s "that gap is the stronger claim" is
621
695
  measured for every (gap, shared field) pairing that occurs beside it on
622
696
  every fixture view, intact and damaged, against that gap's own claim (an
623
- unmeasured kind fails by name; "stronger" is *may be short*, ≤ — on a fresh
697
+ unmeasured kind fails by name; "stronger" is _may be short_, ≤ — on a fresh
624
698
  run with its base stripped the rebuild recovers everything);
625
699
  `forced-tool-schema`'s "the tool list is complete" runs on both chart
626
700
  shapes. The design page marks the three built; the general statement — a
@@ -677,7 +751,7 @@ footprintjs the range admits (`^9.21.1`).
677
751
  (`servedView.ts` · `readReceipt`), which checks `basis.epoch` and nothing
678
752
  else. A stored receipt with a `basis` and no `cache` container therefore
679
753
  made `servedAt(snapshot, k)` throw `Cannot read properties of undefined
680
- (reading 'strategy')` where 9.92 built a view. WHY it matters: a recording is
754
+ (reading 'strategy')` where 9.92 built a view. WHY it matters: a recording is
681
755
  older than the reader that opens it, and every recording a consumer already
682
756
  held was minted by the release that minted it — the lens found this the day
683
757
  after 9.93.0 shipped, on a recording it already had. That breaks the family's
@@ -688,7 +762,7 @@ footprintjs the range admits (`^9.21.1`).
688
762
  the check does not verify declared optional (`cache?`, and `strategy?`
689
763
  within it) — and `receiptAt` answers that type, so the compiler holds every
690
764
  reader to it; `viewOf` reads the strategy through it (`receipt?.cache?.
691
- strategy !== null`). A receipt with no `cache` yields a view, its
765
+ strategy !== null`). A receipt with no `cache` yields a view, its
692
766
  `cache-transform` gap IS raised (the 9.93 rule: absence still raises; only a
693
767
  receipt SAYING `null` lifts it), and its `basis` and every hash row it
694
768
  carries still verify. Nothing is repaired: no `cache: {}` is fabricated and
@@ -698,9 +772,9 @@ footprintjs the range admits (`^9.21.1`).
698
772
  `omittedForAttention`) sits behind the same narrowing and the same type; no
699
773
  second reader existed in the library. `Receipt` itself is unchanged — it is
700
774
  the MINTED shape and a mint still writes every container.
701
- THE VINTAGE LAW, now in `src/lib/time-travel/README.md`: *a reader reads
775
+ THE VINTAGE LAW, now in `src/lib/time-travel/README.md`: _a reader reads
702
776
  the receipt it is handed; a missing container is a fact about the vintage,
703
- never a throw.* `test/lib/time-travel/receipt-vintage.test.ts` ages real
777
+ never a throw._ `test/lib/time-travel/receipt-vintage.test.ts` ages real
704
778
  recordings three ways — no `cache` (the shape the lens hit), `cache` without
705
779
  `strategy` (the 9.88.0–9.92.1 mint, byte for byte), `strategy: null` (the
706
780
  9.93.0 mint on a chart with no strategy) — and requires a view, the gap, the
@@ -711,8 +785,8 @@ footprintjs the range admits (`^9.21.1`).
711
785
  `receiptAt(s, k)!.cache.transform` unguarded must guard the container — the
712
786
  compiler now says on the page what the runtime said with a throw.
713
787
  ```ts
714
- const receipt = receiptAt(olderRecording, 1)!; // minted by 9.92
715
- receipt.cache?.strategy; // undefined — not null, not repaired
788
+ const receipt = receiptAt(olderRecording, 1)!; // minted by 9.92
789
+ receipt.cache?.strategy; // undefined — not null, not repaired
716
790
  servedAt(olderRecording, 1)!.gaps.map((g) => g.gap); // ['cache-transform', 'provider-defaults']
717
791
  ```
718
792
 
@@ -759,7 +833,7 @@ one iteration (below), and because every bundled consumer's output changes.
759
833
  catalogs and answers.
760
834
  ```ts
761
835
  const agent = Agent.create({ provider, model }).tool(lookup).selfExplain().build();
762
- await agent.run({ message: 'Refund order A-1001?' }); // pack not loaded
836
+ await agent.run({ message: 'Refund order A-1001?' }); // pack not loaded
763
837
  await agent.run({ message: 'Why did you approve it?' }); // loads on the iteration read_skill opened it
764
838
  ```
765
839
  - **The ESM build now carries `sideEffects`.** A bundler reads that flag from
@@ -827,7 +901,7 @@ window and no cache strategy records a different byte except the one new key.
827
901
  release has no key, and a reader treats that as "cannot say", never as
828
902
  `null`.
829
903
  ```ts
830
- receiptAt(agent.getSnapshot()!, 1)!.cache.strategy; // '*' — an agent on the mock provider
904
+ receiptAt(agent.getSnapshot()!, 1)!.cache.strategy; // '*' — an agent on the mock provider
831
905
  receiptAt(llmCall.getSnapshot()!, 1)!.cache.strategy; // null — nothing between assembly and the port
832
906
  ```
833
907
  - **`Receipt.omittedForAttention` is written** — by the agent chart's window
@@ -847,8 +921,11 @@ window and no cache strategy records a different byte except the one new key.
847
921
  `no-receipt-on-chart`, the one gap that can lose it. An agent without a
848
922
  window hands both stages the deps they always had.
849
923
  ```ts
850
- const agent = Agent.create({ provider, model }).system('bot').tool(lookup)
851
- .window(slidingWindow({ keepRecentTurns: 1 })).build();
924
+ const agent = Agent.create({ provider, model })
925
+ .system('bot')
926
+ .tool(lookup)
927
+ .window(slidingWindow({ keepRecentTurns: 1 }))
928
+ .build();
852
929
  await agent.run({ message: 'go' });
853
930
  receiptAt(agent.getSnapshot()!, 3)!.omittedForAttention; // { count: 2, hashes: [...] } — the pair that left at this head
854
931
  ```
@@ -966,7 +1043,7 @@ consequences, one example each:
966
1043
  (`ServedToolParties.lastServed`) or the name's only holder when it was never
967
1044
  served; every such dispatch is on the record as the new
968
1045
  `agentfootprint.tools.answered_off_wire` (`{ toolName, toolCallId,
969
- iteration, answeredBy, answeredById? }`). A pause that re-dispatches on
1046
+ iteration, answeredBy, answeredById? }`). A pause that re-dispatches on
970
1047
  resume (a middleware ask, a check-in, a credential consent) carries the
971
1048
  served party on its checkpoint (`pausedToolParty`, pause-path-only), so a
972
1049
  resume in a FRESH Agent instance — empty closure, no Compose — cannot fall
@@ -980,7 +1057,7 @@ consequences, one example each:
980
1057
  is named as itself.
981
1058
  - **A dead claim is reported.** New `agentfootprint.tools.claim_swallowed`
982
1059
  (`ToolsClaimSwallowedPayload`: `{ toolName, iteration, lostBy, lostById?,
983
- wonBy, wonById? }`, names only) fires once per iteration for every party
1060
+ wonBy, wonById? }`, names only) fires once per iteration for every party
984
1061
  whose claim to a name is held by somebody else — whether its contract
985
1062
  competed (an active skill against a provider) or never reached the merge
986
1063
  (the same skill while inactive; a provider tool whose name a static
@@ -1050,8 +1127,8 @@ Two reasons were on the record for that. **Neither survived.** The first — tha
1050
1127
  `Receipt.cache.transform` has no honest value on a chart running no cache
1051
1128
  strategy — was already refuted in 9.88.0 and left refuted in the source: an
1052
1129
  `Agent` with a pass-through strategy records `'unchanged'` today, and the value
1053
- is true for the same reason where no strategy exists at all, because *the
1054
- request that went out IS the request that was assembled*. **No fourth enum value
1130
+ is true for the same reason where no strategy exists at all, because _the
1131
+ request that went out IS the request that was assembled_. **No fourth enum value
1055
1132
  was added.** The second — the salt — never applied to `LLMCall`, which owns its
1056
1133
  executor and mints a run id exactly as `Agent` does. It DOES apply to the two
1057
1134
  messageAPI charts, and that was verified rather than assumed: footprintjs stamps
@@ -1094,8 +1171,8 @@ than salting every hash with an empty string.
1094
1171
  unnoticed for as long as no receipt existed to contradict it. The read is now
1095
1172
  a fallback chain (the agent key first, so no agent recording changes), the
1096
1173
  same shape the conversation has had since 9.88.0. Red before, on
1097
- `test/lib/time-travel/receipt-conformance.test.ts` · *the tool the model was
1098
- served is hashed by the receipt AND rebuilt from the log*.
1174
+ `test/lib/time-travel/receipt-conformance.test.ts` · _the tool the model was
1175
+ served is hashed by the receipt AND rebuilt from the log_.
1099
1176
 
1100
1177
  ### Changed
1101
1178
 
@@ -1103,7 +1180,7 @@ than salting every hash with an empty string.
1103
1180
  printed sentence never named a chart, so nothing in it went false; the
1104
1181
  MECHANISM comment beside it now names the shapes that still reach it — a
1105
1182
  messageAPI chart handed no run id, a run that declined with `recordReceipt:
1106
- false`, a recording made before 9.88.0, and a consumer's own `call-llm` stage,
1183
+ false`, a recording made before 9.88.0, and a consumer's own `call-llm` stage,
1107
1184
  which this library does not mint for. The cause `'no-receipt-committed'` is
1108
1185
  still driven by a REAL run in both walks; the driver moved from an `LLMCall`
1109
1186
  (which now mints) to a messageAPI chart with no run id.
@@ -1193,12 +1270,12 @@ import { milestoneTag, milestoneStopsStrategy } from 'agentfootprint';
1193
1270
 
1194
1271
  // A reader with NO agent id conventions — footprintjs's own strategy, our word:
1195
1272
  const turns = timeTravel(agent.getSnapshot()!, { strategy: tagStops([milestoneTag('llm-turn')]) });
1196
- turns.stops.map((s) => s.label); // ['Run start', 'CallLLM', 'CallLLM', 'Run end']
1197
- turns.stops[1].meta; // ['milestone:llm-turn', 'milestone-label:LLM turn']
1273
+ turns.stops.map((s) => s.label); // ['Run start', 'CallLLM', 'CallLLM', 'Run end']
1274
+ turns.stops[1].meta; // ['milestone:llm-turn', 'milestone-label:LLM turn']
1198
1275
 
1199
1276
  // The agent's own axis, unchanged in shape — tag first, id fallback:
1200
1277
  const all = timeTravel(agent.getSnapshot()!, { strategy: milestoneStopsStrategy });
1201
- all.stops[5].meta; // { kind: 'llm-turn', label: 'LLM turn' } — read off the bundle
1278
+ all.stops[5].meta; // { kind: 'llm-turn', label: 'LLM turn' } — read off the bundle
1202
1279
  ```
1203
1280
 
1204
1281
  ## [9.89.3] - 2026-09-10
@@ -1283,13 +1360,13 @@ for byte as before.
1283
1360
  const tool = flowchartAsTool({
1284
1361
  name: 'seeded',
1285
1362
  description: 'Seeds a subflow with the key and reads it there.',
1286
- flowchart: chart, // parent writes apiKey; the subflow is seeded with it
1363
+ flowchart: chart, // parent writes apiKey; the subflow is seeded with it
1287
1364
  keepRecord: true,
1288
1365
  redact: { keys: ['apiKey'] },
1289
1366
  });
1290
- await tool.execute({}, ctx); // → '{"apiKey":"REDACTED","seen":26}' — the subflow computed on the real 26-byte key
1367
+ await tool.execute({}, ctx); // → '{"apiKey":"REDACTED","seen":26}' — the subflow computed on the real 26-byte key
1291
1368
  const record = JSON.stringify(innerRunsOf(tool)!.get(ctx.toolCallId)!);
1292
- record.includes('sk-live-'); // false — result, log, mirror, stageReads, narrative Input: line, history[0], parent log
1369
+ record.includes('sk-live-'); // false — result, log, mirror, stageReads, narrative Input: line, history[0], parent log
1293
1370
  ```
1294
1371
 
1295
1372
  The law's other half is unchanged and now pinned in the same file: the live
@@ -1320,7 +1397,7 @@ changes, and a tool without `redact` behaves byte for byte as before.
1320
1397
  policy-redacted key never entered the inner commit log — and the option's
1321
1398
  own documentation drew the wrong conclusion from that. The live state view
1322
1399
  is not a commit: it is the run's raw heap, and its scrubbed twin (the
1323
- *redacted mirror* footprintjs maintains beside it) is served only by
1400
+ _redacted mirror_ footprintjs maintains beside it) is served only by
1324
1401
  `getSnapshot({ redact: true })`. Both tools called `getSnapshot()` bare, so
1325
1402
  the string the model read (`JSON.stringify(snapshot.values)`, or whatever a
1326
1403
  `resultMapper` built from it), the envelope's state, and a kept record's
@@ -1341,13 +1418,13 @@ changes, and a tool without `redact` behaves byte for byte as before.
1341
1418
  const tool = flowchartAsTool({
1342
1419
  name: 'weather_advice',
1343
1420
  description: 'Forecast tomorrow and advise on biking.',
1344
- flowchart: adviceChart, // writes scope.apiKey = 'sk-…'
1421
+ flowchart: adviceChart, // writes scope.apiKey = 'sk-…'
1345
1422
  keepRecord: true,
1346
1423
  redact: { keys: ['apiKey'] },
1347
1424
  });
1348
- await tool.execute({}, ctx); // → '{"apiKey":"REDACTED","advice":"bike"}'
1425
+ await tool.execute({}, ctx); // → '{"apiKey":"REDACTED","advice":"bike"}'
1349
1426
  const { snapshot } = innerRunsOf(tool)!.get(ctx.toolCallId)!.recording!;
1350
- snapshot.sharedState.apiKey; // 'REDACTED' — was the plaintext
1427
+ snapshot.sharedState.apiKey; // 'REDACTED' — was the plaintext
1351
1428
  JSON.stringify(snapshot).includes('sk-'); // false — in every field the log scrubs
1352
1429
  ```
1353
1430
 
@@ -1385,7 +1462,8 @@ both additive: a 9.88.0 consumer compiles and behaves identically.
1385
1462
  object a consumer already holds:
1386
1463
 
1387
1464
  ```ts
1388
- receiptHash(runId, toolDigestInput(servedAt(k).tools.schemas[i])) === receiptAt(k).tools.schemaHashes[name]
1465
+ receiptHash(runId, toolDigestInput(servedAt(k).tools.schemas[i])) ===
1466
+ receiptAt(k).tools.schemaHashes[name];
1389
1467
  ```
1390
1468
 
1391
1469
  It takes an `LLMToolSchema` — the tool as handed to the provider port, which
@@ -1413,7 +1491,7 @@ both additive: a 9.88.0 consumer compiles and behaves identically.
1413
1491
  all of it by hand against 9.17. This was one of them. The hand-rolled guard,
1414
1492
  the re-partition loop and `milestoneOf`'s re-derivation are gone;
1415
1493
  `milestoneStops` is now one expression, `filterStops(commitStops(log, tree),
1416
- keep)`, and the one owner of the `[start, …stages, end]` contract is the
1494
+ keep)`, and the one owner of the `[start, …stages, end]` contract is the
1417
1495
  library that returns it. Public names and signatures are unchanged
1418
1496
  (`milestoneOf`, `milestoneStops`, `milestoneStopsStrategy`); the stops are
1419
1497
  now typed `Stop<Milestone>`, so `cursor.at()?.meta?.kind` is typed, and
@@ -1543,14 +1621,14 @@ that file now exists. All of it is closed below, at the root, additively.
1543
1621
  strategy actually applied, three scalars each (`field`, `boundaryIndex`,
1544
1622
  `ttl`). `transformHash` is a digest over the whole prepared request: inside a
1545
1623
  run it says only "something changed", and across epochs it is not comparable
1546
- at all — so it could not answer *did the breakpoints move between call 3 and
1547
- call 4?*, which is the question that decides an Anthropic bill. Two receipts'
1624
+ at all — so it could not answer _did the breakpoints move between call 3 and
1625
+ call 4?_, which is the question that decides an Anthropic bill. Two receipts'
1548
1626
  `markersApplied` answer it by inspection.
1549
1627
  - **`receipt.cache.transform`** — `'unchanged' | 'rewritten' | 'unknown'`.
1550
1628
  Branch on this, never on `transformHash === null`.
1551
1629
  - **`RECEIPT_BOUNDARY`** — the one sentence every receipt field is true at, so
1552
- a renderer prints the library's own wording: *a receipt describes the request
1553
- as this library last saw it.* Where that is — the provider port — is in the
1630
+ a renderer prints the library's own wording: _a receipt describes the request
1631
+ as this library last saw it._ Where that is — the provider port — is in the
1554
1632
  comment beside the constant, because the sentence is printed and the sixth
1555
1633
  round's rule is that a printed sentence names no mechanism.
1556
1634
  - **`ServedView.basis`** — `{ model, provider, runId }`, read off the receipt.
@@ -1689,7 +1767,7 @@ that file now exists. All of it is closed below, at the root, additively.
1689
1767
  terminal, in a diff and in review, and one `sed` away from being eaten. Same
1690
1768
  bytes, same hashes; they can now be read.
1691
1769
  - **`test/lib/time-travel/receipt-conformance.test.ts` · `describe('a redacted
1692
- run')` was a NO-OP, and two READMEs documented what it pretended to prove.**
1770
+ run')` was a NO-OP, and two READMEs documented what it pretended to prove.**
1693
1771
  It passed `redact: [...]` to `Agent.create`, which has no such option;
1694
1772
  `tsconfig.json` excludes `test/`, so the unknown key was never typechecked and
1695
1773
  was silently dropped. The run was not redacted, and the READMEs' "honest edge"
@@ -1862,6 +1940,7 @@ a sentence is TRUE, and three of them were not.
1862
1940
  conditionally instead would mean inferring "no strategy ran" from a recording,
1863
1941
  which is the absence-of-evidence reading this whole feature refuses; recorded
1864
1942
  as entry 8 of `docs/design/2026-09-recorded-not-built.md`.
1943
+
1865
1944
  #### The fifth and sixth rounds — a printed gap sentence stops describing code
1866
1945
 
1867
1946
  Five review rounds, and each one found NEW false prose in the sentences the
@@ -1871,7 +1950,7 @@ composed once and read many times is a PREDICTION** — one surface over: compos
1871
1950
  once, and read against every later version of the code it describes.
1872
1951
 
1873
1952
  The reproduction is one shipped string. `SERVED_GAPS['no-receipt-on-chart'].why`
1874
- said *"THREE causes and none of them is a hole in this view: …"*. A fourth path
1953
+ said _"THREE causes and none of them is a hole in this view: …"_. A fourth path
1875
1954
  was then added — a value under the receipt key refused because it carries no
1876
1955
  basis — and BOTH halves went false at once: four causes, and that one IS a hole.
1877
1956
  Nobody edited the string. Nobody had to.
@@ -1883,12 +1962,12 @@ one, and the reason is the whole story of this release.
1883
1962
 
1884
1963
  Five rounds tried to write TRUE mechanism sentences and the rate of new
1885
1964
  falsehoods held constant. So the sixth put one question to all ten printed
1886
- sentences — *could this become false without anyone editing it?* — and NINE
1965
+ sentences — _could this become false without anyone editing it?_ — and NINE
1887
1966
  could, two of them being false the day they shipped. Exactly one could not:
1888
1967
 
1889
- > `UNGAPPED_FIELDS.gaps` — *"The account itself rather than a fact about the
1968
+ > `UNGAPPED_FIELDS.gaps` — _"The account itself rather than a fact about the
1890
1969
  > request: a gap naming this list would be the account excusing its own
1891
- > absence."*
1970
+ > absence."_
1892
1971
 
1893
1972
  It survives because it makes **no claim about code**. It says what the field
1894
1973
  means inside the account, and nothing outside the sentence can falsify it. Every
@@ -1923,8 +2002,8 @@ of them still make a claim a code edit falsifies.** Exactly one does not —
1923
2002
  `UNGAPPED_FIELDS.gaps` — and it does not because it is SELF-REFERENTIAL: it says
1924
2003
  what its field is inside the account, not anything about the request. The other
1925
2004
  ten cannot copy that shape, because a sentence that tells a reader something
1926
- USEFUL — *may be SHORT*, *absent means unknown*, *the tool list is complete and
1927
- the schemas are one short* — is a claim about how the rebuild behaves, and the
2005
+ USEFUL — _may be SHORT_, _absent means unknown_, _the tool list is complete and
2006
+ the schemas are one short_ — is a claim about how the rebuild behaves, and the
1928
2007
  rebuild is code. **The reduction changed the VOCABULARY of the claims, not their
1929
2008
  CLASS.**
1930
2009
 
@@ -1968,20 +2047,20 @@ spot than six rounds of rewriting produced, and it is the whole of it.
1968
2047
  a doc is FOR; the other three do not, because a cause count goes stale in a
1969
2048
  doc exactly as it does in a constant.
1970
2049
  - **Every printed gap sentence is one or two sentences and names nothing.**
1971
- `no-receipt-on-chart` now reads in full: *"Nothing on this view has been
2050
+ `no-receipt-on-chart` now reads in full: _"Nothing on this view has been
1972
2051
  checked against what went out. Every field below is missing as a whole, and an
1973
2052
  absence among them says nothing about the call — not even that a dial was left
1974
- unset."* `forced-tool-schema` is 26 words. `callRuntimeStageId`'s excuse is 12.
2053
+ unset."_ `forced-tool-schema` is 26 words. `callRuntimeStageId`'s excuse is 12.
1975
2054
  - **TWO SENTENCES WERE FALSE THE DAY THEY SHIPPED, and both were mechanism
1976
2055
  claims, so the rule deletes the category rather than the instances.**
1977
- `cache-transform` said *"only its INPUTS are on the record"* while three of
2056
+ `cache-transform` said _"only its INPUTS are on the record"_ while three of
1978
2057
  the fields it covers are OUTPUTS that are on the record — `cache.transform`
1979
2058
  (the verdict of comparing what the strategy was given against what it handed
1980
2059
  back), `cache.transformHash` (the digest of the result, when they differed)
1981
2060
  and `cache.markersApplied` (the breakpoints actually applied, as against the
1982
2061
  candidates in `scope.cacheMarkers`, which are the inputs). And
1983
- `no-receipt-on-chart` opened *"No receipt was found for this epoch"* and
1984
- closed *"absent here means unrecorded"* — both false under
2062
+ `no-receipt-on-chart` opened _"No receipt was found for this epoch"_ and
2063
+ closed _"absent here means unrecorded"_ — both false under
1985
2064
  `'receipt-shape-rejected'`, where a receipt WAS written and the read refused
1986
2065
  it, which is to say the sentence asserted which cause applied and was wrong
1987
2066
  for one of two. Neither can be written under the new rule. Both are corrected
@@ -1990,9 +2069,9 @@ spot than six rounds of rewriting produced, and it is the whole of it.
1990
2069
  - **`RECEIPT_BOUNDARY` was the one printed sentence exempted from the rule, and
1991
2070
  the exemption is gone.** It named `LLMProvider.complete` and `complete()` — a
1992
2071
  module and a call, printed to a reader who cannot open either. It now reads
1993
- *"A receipt describes the request as this library last saw it. Whatever
2072
+ _"A receipt describes the request as this library last saw it. Whatever
1994
2073
  handled it after that could have changed it, and nothing on the receipt would
1995
- show that."* The port, the decorated provider, the vendor adapter and the
2074
+ show that."_ The port, the decorated provider, the vendor adapter and the
1996
2075
  vendor's own defaults are in the comment above the constant. One string, one
1997
2076
  rule set: the walk no longer strips it before judging, and asserts it on its
1998
2077
  own as well as inside the two entries that quote it.
@@ -2027,8 +2106,8 @@ spot than six rounds of rewriting produced, and it is the whole of it.
2027
2106
  measurement is unchanged and still re-taken by the walk on every run; it reads
2028
2107
  the claim from the COMMENT beside the entry now, so a chart that starts
2029
2108
  supplying one still fails the suite instead of aging the sentence. What a
2030
- reader is shown is what the field means: *"Absent means nobody recorded a
2031
- drop, never that nothing was dropped."*
2109
+ reader is shown is what the field means: _"Absent means nobody recorded a
2110
+ drop, never that nothing was dropped."_
2032
2111
  - **`examples/observability/24-receipt-at-the-stop.ts` clipped a printed reason
2033
2112
  mid-version-number.** It printed `reason.split('.')[0]`, so "measured on
2034
2113
  9.88.0" reached a reader as "measured on 9." — a fragment that reads as a
@@ -2038,8 +2117,7 @@ spot than six rounds of rewriting produced, and it is the whole of it.
2038
2117
  coverage.** The header claimed the vacuous-row guard was closed "by driving
2039
2118
  each row on a run that reaches its own fields". Measured, that clause is
2040
2119
  false: `no-fold-base` moves 7 of the 11 fields it names, `no-run-log` 1 of 4,
2041
- `no-receipt-on-chart` 3 of 8, and only `no-conversation-on-record` reaches all
2042
- 3. The reasons are structural — a receipt-only field never appears on a view,
2120
+ `no-receipt-on-chart` 3 of 8, and only `no-conversation-on-record` reaches all 3. The reasons are structural — a receipt-only field never appears on a view,
2043
2121
  so removing the receipt cannot MOVE it — so the header names the numbers per
2044
2122
  row, a test pins them so the table cannot go stale, and it says plainly that a
2045
2123
  field claim divergence does not reach is carried by the coverage half and by
@@ -2063,56 +2141,57 @@ each of them, which is how the library closes everything else, and it found a
2063
2141
  different class of defect: not prose that names a mechanism, but prose that is
2064
2142
  plain, short, rule-abiding and UNTRUE OF THE VIEW IT IS PRINTED BESIDE.
2065
2143
 
2066
- - **`no-run-log` claimed a loss that a run says did not happen.** It read *"The
2067
- fields below could not be fully recovered here"* — an assertion that recovery
2144
+ - **`no-run-log` claimed a loss that a run says did not happen.** It read _"The
2145
+ fields below could not be fully recovered here"_ — an assertion that recovery
2068
2146
  DID fail. Measured on the ordinary view that raises it, a
2069
2147
  `'dynamic-grouped'` agent with one plain tool and its `commitLog` emptied:
2070
2148
  `tools.names`, `tools.schemas`, `tools.forced` and `messages.requestOnly` all
2071
2149
  come back BYTE-IDENTICAL to the intact view. The gap fires and costs nothing,
2072
2150
  because that run has no forced tool name and no `wants` to lose. It now reads
2073
- *"The fields below may be SHORT: a name can be missing from the tool list, and
2151
+ _"The fields below may be SHORT: a name can be missing from the tool list, and
2074
2152
  a line that went out with the request can be missing too. An absence below is
2075
2153
  not evidence that there was nothing there — read the whole recording rather
2076
- than a piece of it."* **The repair is the sentence and not the condition**,
2154
+ than a piece of it."_ **The repair is the sentence and not the condition**,
2077
2155
  and the reason is that the condition cannot be narrowed by anything the read
2078
2156
  can see: whether the run had a constant to lose is recorded in the log whose
2079
2157
  absence raises the gap. Both directions are asserted — emptying the run log
2080
2158
  takes a forced-output run's tool list from one name to none and a staged-refs
2081
2159
  run's request-only line to nothing, and takes nothing at all from the plain
2082
2160
  run. Its second clause was loose as well: it said a line could go missing
2083
- *"from the conversation"*, and a request-only line is by construction in no
2161
+ _"from the conversation"_, and a request-only line is by construction in no
2084
2162
  conversation — asserted now against the wire, which carries it as the last
2085
2163
  message of the request while the rebuilt `messages.asSent` does not contain it.
2086
2164
  - **Three sentences were true where they were composed and misleading where they
2087
2165
  were PRINTED.** One sentence, several contexts: the library's own Honest
2088
2166
  Sentence law says it has to hold in all of them.
2089
- - `cache-transform` ended by quoting `RECEIPT_BOUNDARY` — *"A receipt
2090
- describes the request as this library last saw it"* — and it is raised on
2167
+
2168
+ - `cache-transform` ended by quoting `RECEIPT_BOUNDARY` — _"A receipt
2169
+ describes the request as this library last saw it"_ — and it is raised on
2091
2170
  EVERY view, including a receipt-less one. Measured: an `LLMCall` view
2092
2171
  carries exactly `no-receipt-on-chart` and `cache-transform`, so the reader
2093
2172
  was told what a receipt describes beside a view that has none. The quote is
2094
2173
  gone from it and the claim survives in the entry's own words, in the
2095
- vocabulary of a view: *"…and nothing on this view would show it."*
2174
+ vocabulary of a view: _"…and nothing on this view would show it."_
2096
2175
  `provider-defaults` keeps the quote and is the only entry that may have it —
2097
2176
  it is pushed inside `if (receipt !== undefined)`, so a view carrying it
2098
2177
  always has a receipt for the sentence to be about. Asserted across every run
2099
2178
  in the new file: a gap whose `why` includes the boundary appears only on a
2100
2179
  view whose `basis` is defined.
2101
- - `no-fold-base` said the view's number *"may differ from the one the receipt
2102
- for this turn carries"* — printed on views that carry no receipt (a
2180
+ - `no-fold-base` said the view's number _"may differ from the one the receipt
2181
+ for this turn carries"_ — printed on views that carry no receipt (a
2103
2182
  base-less `LLMCall` recording raises both gaps at once). It now says what
2104
- the NUMBER means: *"The turn number below may be this turn's place in run
2105
- order rather than the number the run itself gave it."* Asserted on the
2183
+ the NUMBER means: _"The turn number below may be this turn's place in run
2184
+ order rather than the number the run itself gave it."_ Asserted on the
2106
2185
  resumed run whose base and `iteration` writes are gone: the view calls the
2107
2186
  second turn 1 while the run's own count for it was 2.
2108
- - `no-receipt-on-chart` closed *"their absence here is a gap in the record,
2109
- never a call made without them"*. True of each field AS A WHOLE and false
2187
+ - `no-receipt-on-chart` closed _"their absence here is a gap in the record,
2188
+ never a call made without them"_. True of each field AS A WHOLE and false
2110
2189
  one level down, which is the level a reader reads at: a receipt always
2111
2190
  carries `params` and always carries a `cache.transform` verdict, and an
2112
2191
  absence INSIDE `params` — measured `{}` on an agent that set no dials — IS a
2113
2192
  call made without one. It now claims nothing about what is inside a field it
2114
- cannot see: *"…an absence among them says nothing about the call — not even
2115
- that a dial was left unset."*
2193
+ cannot see: _"…an absence among them says nothing about the call — not even
2194
+ that a dial was left unset."_
2116
2195
 
2117
2196
  - **`test/lib/time-travel/gap-sentences.test.ts` — the assertion, beside the
2118
2197
  sentence.** One real run per catalogue entry and per `UNGAPPED_FIELDS` key,
@@ -2201,6 +2280,7 @@ plain, short, rule-abiding and UNTRUE OF THE VIEW IT IS PRINTED BESIDE.
2201
2280
  hand-listed, so a gap whose damage nobody wrote down still gets only the
2202
2281
  account half. Neither half reads a `why`. That is a person's job, and it is
2203
2282
  where this release's last three defects came from.
2283
+
2204
2284
  - `test/lib/time-travel/keyed-fold-equivalence.test.ts` — 9 tests: every key at
2205
2285
  every commit of a real run, checked against `stateAt` itself, including a
2206
2286
  merge with no `set` anchor (which cannot be folded at all without the base),
@@ -2250,9 +2330,9 @@ the examples it pulls in — a repair of its own, not a line item in this releas
2250
2330
  The hole is closed instead in `test/type-regressions/`, which already compiles
2251
2331
  under `npm run test:types`.
2252
2332
 
2253
- `ServedGap.fields` carries TWO relations on one list. Most entries mean *the
2254
- rebuild cannot produce this field*; `cache-transform`'s composition fields mean
2255
- *it can, and both sides agree, but only up to the cache strategy*. A checker
2333
+ `ServedGap.fields` carries TWO relations on one list. Most entries mean _the
2334
+ rebuild cannot produce this field_; `cache-transform`'s composition fields mean
2335
+ _it can, and both sides agree, but only up to the cache strategy_. A checker
2256
2336
  that granted the second as an excuse would stop checking fields the record
2257
2337
  proves perfectly well — which is exactly what happened when the composition
2258
2338
  fields were added, and it silently disabled one clause of the law. The clause
@@ -2396,7 +2476,7 @@ Iteration → … → Run end.
2396
2476
  becomes a one-line reader of it.
2397
2477
 
2398
2478
  - **Which keys are visible where, in the grouped shape.** The settled skill
2399
- cursor for turn *k* is on the OUTER axis, at iteration *k* (`currentSkillId`).
2479
+ cursor for turn _k_ is on the OUTER axis, at iteration _k_ (`currentSkillId`).
2400
2480
  Inside the drill, `currentSkillId` is the value the turn STARTED from — it
2401
2481
  crosses the mount as a read-only input — and the move the turn made is
2402
2482
  `nextSkillCursor`, merged back out by the outputMapper. Both logs are truthful
@@ -2442,7 +2522,7 @@ Iteration → … → Run end.
2442
2522
  - **17 tests over real runs**, not fixtures: the axis in both chart shapes, the
2443
2523
  partition property (every commit belongs to exactly one stop), a jump that
2444
2524
  lands and a miss that names a nearest without moving, the skill graph read
2445
- along the commits with the wire as witness (the skill whose body rode turn *k*
2525
+ along the commits with the wire as witness (the skill whose body rode turn _k_
2446
2526
  is the one that iteration's own log settled on), marks that survive jumps and
2447
2527
  never appear in the recording, what `'start'` folds on this axis versus the
2448
2528
  port's, a resumed run's axis, and a log with no milestones in it. One more
@@ -2477,7 +2557,7 @@ Everything else here is one review of 9.86.0, taken finding by finding.
2477
2557
  `TurnRoute.by`'s six values, so the clause was composed for the other four too:
2478
2558
  false for `'continuity'` (the cursor was carried over from the previous turn and
2479
2559
  nothing moved it — the verdict every follow-up produces under `{ strictness:
2480
- 'guard', continuity: 'conversation' }`), false for `'menu'` resolved by the
2560
+ 'guard', continuity: 'conversation' }`), false for `'menu'` resolved by the
2481
2561
  model's own pick, and unprovable for `'none'`. The composer now takes
2482
2562
  `turnStartedBy: TurnRoute['by']` and says one past fact per value — "the turn's
2483
2563
  start had already been resolved decisively", "the cursor had been carried over
@@ -2570,7 +2650,7 @@ Everything else here is one review of 9.86.0, taken finding by finding.
2570
2650
  The ten 9.86.0 rows cited `buildToolRegistry.ts` line ranges from the 9.85.0 tree
2571
2651
  that the same release had moved by about twenty-nine lines, beside a
2572
2652
  `buildToolsSlot.ts` line from HEAD. They name the checks now (`holders.includes
2573
- (PRESENT_TOOL_NAME)`, the `seenNames` loop, the `sharedSkillTools` backfill), and
2653
+ (PRESENT_TOOL_NAME)`, the `seenNames` loop, the `sharedSkillTools` backfill), and
2574
2654
  the baseline's `note` says so. The `present-vs-mcp` row no longer claims the MCP
2575
2655
  cell proves the blind spot is the provider channel: both claimants mount through
2576
2656
  `staticTools()`, so the cell shows an MCP catalog inherits that seam unchanged and
@@ -2685,11 +2765,11 @@ the cursor?", and one scope key answers "which skill ids may this role see?".
2685
2765
  export inside the tool-calls stage, not on any package door — _corrected in
2686
2766
  9.86.1_), which
2687
2767
  names the dispatch roster: `Unknown tool 'X' on that call. Tool names that
2688
- resolved to an implementation on that call: …`, or, with an empty roster, that
2768
+ resolved to an implementation on that call: …`, or, with an empty roster, that
2689
2769
  none did. The leading `Unknown tool` token is preserved, so every matcher on it
2690
2770
  is untouched.
2691
2771
 
2692
- It says *resolved*, not *could be dispatched*, because two gates sit between
2772
+ It says _resolved_, not _could be dispatched_, because two gates sit between
2693
2773
  resolution and a tool running — the `tool_call` permission check and the
2694
2774
  middleware chain — and neither is asked to phrase an error. And the roster is
2695
2775
  role-filtered before it is named: it used to read the dispatch map raw and could
@@ -2703,10 +2783,10 @@ the cursor?", and one scope key answers "which skill ids may this role see?".
2703
2783
  Fold holds.** Three sentences branched on `length > 0` over an already-filtered
2704
2784
  array, so "the graph held nothing" and "the role filter emptied it" composed the
2705
2785
  same words. A cursor whose only declared hop was hidden answered `read_skill`
2706
- with *"No skill was reachable from 'alpha' when that call was made."* while the
2786
+ with _"No skill was reachable from 'alpha' when that call was made."_ while the
2707
2787
  graph was routing `alpha`; a `'guard'` menu whose every id had been hidden since
2708
- the turn started said *"no menu was outstanding when that call was made.
2709
- Declared routes moved the cursor instead."* — two false clauses in one breath.
2788
+ the turn started said _"no menu was outstanding when that call was made.
2789
+ Declared routes moved the cursor instead."_ — two false clauses in one breath.
2710
2790
 
2711
2791
  A model told the map is a dead end stops asking for the door it may not be
2712
2792
  shown, and the checker cannot see it: every one of those sentences passes
@@ -2719,8 +2799,8 @@ the cursor?", and one scope key answers "which skill ids may this role see?".
2719
2799
 
2720
2800
  The fourth sentence was the one the model reads to CHOOSE. `describeOffer`
2721
2801
  computed its columns from an already-filtered catalog, so a cursor whose only
2722
- declared hop is hidden was told *"Nothing is reachable from here — answer with
2723
- the skill you are in, or finish."* while the graph held that edge. It classifies
2802
+ declared hop is hidden was told _"Nothing is reachable from here — answer with
2803
+ the skill you are in, or finish."_ while the graph held that edge. It classifies
2724
2804
  the hop set over the unfiltered catalog now and drops the clause when the filter
2725
2805
  is what emptied it; with nothing wired out at all the sentence still stands,
2726
2806
  because that absence is one the description has evidence for. Reaching it meant