@knights-eye-chess/ai-analysis 0.0.0-stage → 0.1.0-dev.17

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 (258) hide show
  1. package/LICENSE +235 -0
  2. package/LICENSING.md +26 -0
  3. package/NOTICE +5 -0
  4. package/NPM-PUBLISHING.md +28 -0
  5. package/README.md +139 -2
  6. package/SOURCE-ORIGIN.json +16 -0
  7. package/THIRD_PARTY_NOTICES.md +18 -0
  8. package/build.mjs +22 -0
  9. package/dist/answer-pipeline.test.mjs +375 -0
  10. package/dist/chunk-PLKNJRPF.js +10662 -0
  11. package/dist/chunk-XQDZJOYD.js +2515 -0
  12. package/dist/index.js +146 -0
  13. package/dist/owner-tests/chess-reasoning-compact-search-preparation.test.mjs +963 -0
  14. package/dist/owner-tests/mechanism-encoding.test.mjs +529 -0
  15. package/dist/owner-tests/move-mechanism-facts.test.mjs +396 -0
  16. package/dist/owner-tests/pipeline-host-preparation.test.mjs +10556 -0
  17. package/dist/owner-tests/position-preparation.test.mjs +625 -0
  18. package/dist/owner-tests/preparation-consolidation.test.mjs +1052 -0
  19. package/dist/owner-tests/prose-reference-admission.test.mjs +1770 -0
  20. package/dist/owner-tests/score-comparison-notices.test.mjs +240 -0
  21. package/dist/owner-tests/simple-engine-presentation.test.mjs +233 -0
  22. package/dist/owner-tests/tool-input-loop-guard.test.mjs +78 -0
  23. package/dist/research.js +2274 -0
  24. package/dist/runtime-closure.json +103 -0
  25. package/dist/shared-runtime-closure.json +39 -0
  26. package/dist/shared.js +152 -0
  27. package/dist/types/ai-request-failure.d.ts +14 -0
  28. package/dist/types/ai-request-limits.d.ts +4 -0
  29. package/dist/types/ai-routing-diagnostics.d.ts +14 -0
  30. package/dist/types/chess-reasoning/ablation-adaptive-evidence-loop.d.ts +127 -0
  31. package/dist/types/chess-reasoning/ablation-adaptive-runtime.d.ts +178 -0
  32. package/dist/types/chess-reasoning/ablation-direct-synthesis.d.ts +1030 -0
  33. package/dist/types/chess-reasoning/ablation-evidence-bundle.d.ts +185 -0
  34. package/dist/types/chess-reasoning/ablation-evidence-capture.d.ts +37 -0
  35. package/dist/types/chess-reasoning/ablation-lean-direct-runtime.d.ts +193 -0
  36. package/dist/types/chess-reasoning/ablation-model-facing-evidence.d.ts +38 -0
  37. package/dist/types/chess-reasoning/ablation-transport-correction.d.ts +13 -0
  38. package/dist/types/chess-reasoning/ai-sdk-runtime-support.d.ts +93 -0
  39. package/dist/types/chess-reasoning/answer-host-contracts.d.ts +59 -0
  40. package/dist/types/chess-reasoning/answer-model-connector.d.ts +68 -0
  41. package/dist/types/chess-reasoning/answer-pipeline-exports.d.ts +98 -0
  42. package/dist/types/chess-reasoning/answer-pipeline.d.ts +41 -0
  43. package/dist/types/chess-reasoning/answer-reference-validation.d.ts +40 -0
  44. package/dist/types/chess-reasoning/answer-tool-host.d.ts +8 -0
  45. package/dist/types/chess-reasoning/branch-expansion-policy.d.ts +13 -0
  46. package/dist/types/chess-reasoning/branch-expansion.d.ts +39 -0
  47. package/dist/types/chess-reasoning/causal-claim-checks.d.ts +21 -0
  48. package/dist/types/chess-reasoning/chess-reference-dictionary.d.ts +13 -0
  49. package/dist/types/chess-reasoning/classification-question-target.d.ts +22 -0
  50. package/dist/types/chess-reasoning/classification-reassessment.d.ts +31 -0
  51. package/dist/types/chess-reasoning/compact-search-preparation.d.ts +7 -0
  52. package/dist/types/chess-reasoning/concept-registry.d.ts +99 -0
  53. package/dist/types/chess-reasoning/conversation-context.d.ts +12 -0
  54. package/dist/types/chess-reasoning/engine-search-failure.d.ts +7 -0
  55. package/dist/types/chess-reasoning/evidence-citation-normalization.d.ts +9 -0
  56. package/dist/types/chess-reasoning/evidence-licenses.d.ts +57 -0
  57. package/dist/types/chess-reasoning/evidence-selector-contract.d.ts +5 -0
  58. package/dist/types/chess-reasoning/existing-analysis-context.d.ts +104 -0
  59. package/dist/types/chess-reasoning/fact-observation-provider.d.ts +15 -0
  60. package/dist/types/chess-reasoning/fact-provider.d.ts +111 -0
  61. package/dist/types/chess-reasoning/fresh-engine-pv-witnesses.d.ts +67 -0
  62. package/dist/types/chess-reasoning/historical-prose.d.ts +23 -0
  63. package/dist/types/chess-reasoning/idea-graph.d.ts +34 -0
  64. package/dist/types/chess-reasoning/initial-search-plan.d.ts +37 -0
  65. package/dist/types/chess-reasoning/lossless-evidence-sharing.d.ts +18 -0
  66. package/dist/types/chess-reasoning/matched-comparison.d.ts +10 -0
  67. package/dist/types/chess-reasoning/material-accounting.d.ts +80 -0
  68. package/dist/types/chess-reasoning/material-format.d.ts +4 -0
  69. package/dist/types/chess-reasoning/mechanical-fact-provider.d.ts +33 -0
  70. package/dist/types/chess-reasoning/mechanical-ray-facts.d.ts +19 -0
  71. package/dist/types/chess-reasoning/mechanism-presentation.d.ts +9 -0
  72. package/dist/types/chess-reasoning/move-mechanism-facts.d.ts +45 -0
  73. package/dist/types/chess-reasoning/native-answer-connector.d.ts +4 -0
  74. package/dist/types/chess-reasoning/opening-source-contracts.d.ts +54 -0
  75. package/dist/types/chess-reasoning/opening-source-credits.d.ts +16 -0
  76. package/dist/types/chess-reasoning/opponent-threat-preparation.d.ts +7 -0
  77. package/dist/types/chess-reasoning/piece-identities.d.ts +11 -0
  78. package/dist/types/chess-reasoning/poc-contracts.d.ts +1026 -0
  79. package/dist/types/chess-reasoning/poc-hypotheses.d.ts +74 -0
  80. package/dist/types/chess-reasoning/poc-move-witnesses.d.ts +13 -0
  81. package/dist/types/chess-reasoning/poc-observations.d.ts +23 -0
  82. package/dist/types/chess-reasoning/poc-piece-refs.d.ts +12 -0
  83. package/dist/types/chess-reasoning/poc-plan-model.d.ts +45 -0
  84. package/dist/types/chess-reasoning/poc-semantic-text.d.ts +8 -0
  85. package/dist/types/chess-reasoning/poc-tools.d.ts +81 -0
  86. package/dist/types/chess-reasoning/portable-answer-runtime.d.ts +291 -0
  87. package/dist/types/chess-reasoning/position-analysis-contracts.d.ts +31 -0
  88. package/dist/types/chess-reasoning/position-analysis-scope.d.ts +3 -0
  89. package/dist/types/chess-reasoning/preparation-consolidation.d.ts +21 -0
  90. package/dist/types/chess-reasoning/production-observation-provider.d.ts +16 -0
  91. package/dist/types/chess-reasoning/question-routing.d.ts +326 -0
  92. package/dist/types/chess-reasoning/recorded-main-line.d.ts +37 -0
  93. package/dist/types/chess-reasoning/recorded-position-tools.d.ts +11 -0
  94. package/dist/types/chess-reasoning/recorded-reference-context.d.ts +13 -0
  95. package/dist/types/chess-reasoning/recorded-selected-classification.d.ts +36 -0
  96. package/dist/types/chess-reasoning/reference-limits.d.ts +2 -0
  97. package/dist/types/chess-reasoning/research-exports.d.ts +197 -0
  98. package/dist/types/chess-reasoning/saved-score-presentation.d.ts +14 -0
  99. package/dist/types/chess-reasoning/score-comparison-notices.d.ts +33 -0
  100. package/dist/types/chess-reasoning/server-simple-policy.d.ts +31 -0
  101. package/dist/types/chess-reasoning/simple-agent-config.d.ts +73 -0
  102. package/dist/types/chess-reasoning/simple-board-analysis.d.ts +85 -0
  103. package/dist/types/chess-reasoning/simple-compact-evidence.d.ts +7 -0
  104. package/dist/types/chess-reasoning/simple-completion.d.ts +23 -0
  105. package/dist/types/chess-reasoning/simple-engine-presentation.d.ts +3 -0
  106. package/dist/types/chess-reasoning/simple-evidence-preparation.d.ts +17 -0
  107. package/dist/types/chess-reasoning/simple-evidence-view.d.ts +17 -0
  108. package/dist/types/chess-reasoning/simple-explanation-coverage.d.ts +90 -0
  109. package/dist/types/chess-reasoning/simple-fact-delivery.d.ts +43 -0
  110. package/dist/types/chess-reasoning/simple-fallback-premise.d.ts +5 -0
  111. package/dist/types/chess-reasoning/simple-prose-references.d.ts +108 -0
  112. package/dist/types/chess-reasoning/simple-runtime-version.d.ts +1 -0
  113. package/dist/types/chess-reasoning/stable.d.ts +2 -0
  114. package/dist/types/chess-reasoning/tool-input-loop-guard.d.ts +5 -0
  115. package/dist/types/chess-reasoning/types.d.ts +180 -0
  116. package/dist/types/chess-reasoning/visible-support-lines.d.ts +12 -0
  117. package/dist/types/chess-reasoning/wikibooks-index.d.ts +30 -0
  118. package/dist/types/chess-reasoning/wikibooks-tool-context.d.ts +10 -0
  119. package/dist/types/player-perspective.d.ts +16 -0
  120. package/dist/types/shared-exports.d.ts +22 -0
  121. package/docs/API.md +82 -0
  122. package/docs/OWNERSHIP-ADOPTION.md +35 -0
  123. package/docs/OWNERSHIP-DRIFT.json +55 -0
  124. package/docs/RESEARCH-API.md +28 -0
  125. package/licenses/third-party/INVENTORY.json +202 -0
  126. package/licenses/third-party/ai-7.0.85/LICENSE +13 -0
  127. package/licenses/third-party/ai-sdk--gateway-4.0.69/LICENSE +13 -0
  128. package/licenses/third-party/ai-sdk--provider-4.0.9/LICENSE +13 -0
  129. package/licenses/third-party/ai-sdk--provider-utils-5.0.34/APACHE-2.0.txt +73 -0
  130. package/licenses/third-party/ai-sdk--provider-utils-5.0.34/src/to-json-schema/zod3-to-json-schema/LICENSE +16 -0
  131. package/licenses/third-party/chess.js-1.4.0/LICENSE +23 -0
  132. package/licenses/third-party/eventsource-parser-3.1.1/LICENSE +21 -0
  133. package/licenses/third-party/json-schema-0.4.0/LICENSE +195 -0
  134. package/licenses/third-party/standard-schema--spec-1.1.0/LICENSE +21 -0
  135. package/licenses/third-party/types--json-schema-7.0.15/LICENSE +21 -0
  136. package/licenses/third-party/types--node-22.19.19/LICENSE +21 -0
  137. package/licenses/third-party/undici-7.30.0/LICENSE +21 -0
  138. package/licenses/third-party/undici-7.30.0/lib/web/fetch/LICENSE +21 -0
  139. package/licenses/third-party/undici-types-6.21.0/LICENSE +21 -0
  140. package/licenses/third-party/vercel--oidc-3.2.0/LICENSE +202 -0
  141. package/licenses/third-party/workflow--serde-4.1.0/APACHE-2.0.txt +73 -0
  142. package/licenses/third-party/workflow--serde-4.1.0/LICENSE.md +3 -0
  143. package/licenses/third-party/zod-4.6.5/LICENSE +21 -0
  144. package/npm-release.json +5 -0
  145. package/package.json +72 -4
  146. package/scripts/check-release-tooling.mjs +22 -0
  147. package/scripts/pack.mjs +68 -0
  148. package/scripts/publish-reviewed.mjs +85 -0
  149. package/scripts/release-tooling.json +13 -0
  150. package/scripts/update-dependency.mjs +44 -0
  151. package/scripts/verify-release.mjs +52 -0
  152. package/src/ai-request-failure.ts +34 -0
  153. package/src/ai-request-limits.ts +4 -0
  154. package/src/ai-routing-diagnostics.ts +33 -0
  155. package/src/chess-reasoning/ablation-adaptive-evidence-loop.ts +515 -0
  156. package/src/chess-reasoning/ablation-adaptive-runtime.ts +924 -0
  157. package/src/chess-reasoning/ablation-direct-synthesis.ts +1128 -0
  158. package/src/chess-reasoning/ablation-evidence-bundle.ts +726 -0
  159. package/src/chess-reasoning/ablation-evidence-capture.ts +513 -0
  160. package/src/chess-reasoning/ablation-lean-direct-runtime.ts +670 -0
  161. package/src/chess-reasoning/ablation-model-facing-evidence.ts +212 -0
  162. package/src/chess-reasoning/ablation-transport-correction.ts +106 -0
  163. package/src/chess-reasoning/ai-sdk-runtime-support.ts +381 -0
  164. package/src/chess-reasoning/answer-host-contracts.ts +10 -0
  165. package/src/chess-reasoning/answer-model-connector.ts +83 -0
  166. package/src/chess-reasoning/answer-pipeline-exports.ts +98 -0
  167. package/src/chess-reasoning/answer-pipeline.ts +195 -0
  168. package/src/chess-reasoning/answer-reference-validation.ts +124 -0
  169. package/src/chess-reasoning/answer-tool-host.ts +10 -0
  170. package/src/chess-reasoning/branch-expansion-policy.ts +32 -0
  171. package/src/chess-reasoning/branch-expansion.ts +80 -0
  172. package/src/chess-reasoning/causal-claim-checks.ts +256 -0
  173. package/src/chess-reasoning/chess-reference-dictionary.ts +21 -0
  174. package/src/chess-reasoning/classification-question-target.ts +30 -0
  175. package/src/chess-reasoning/classification-reassessment.ts +34 -0
  176. package/src/chess-reasoning/compact-search-preparation.ts +125 -0
  177. package/src/chess-reasoning/concept-registry.ts +275 -0
  178. package/src/chess-reasoning/conversation-context.ts +29 -0
  179. package/src/chess-reasoning/engine-search-failure.ts +11 -0
  180. package/src/chess-reasoning/evidence-citation-normalization.ts +15 -0
  181. package/src/chess-reasoning/evidence-licenses.ts +166 -0
  182. package/src/chess-reasoning/evidence-selector-contract.ts +5 -0
  183. package/src/chess-reasoning/existing-analysis-context.ts +544 -0
  184. package/src/chess-reasoning/fact-observation-provider.ts +134 -0
  185. package/src/chess-reasoning/fact-provider.ts +250 -0
  186. package/src/chess-reasoning/fresh-engine-pv-witnesses.ts +128 -0
  187. package/src/chess-reasoning/historical-prose.ts +116 -0
  188. package/src/chess-reasoning/idea-graph.ts +224 -0
  189. package/src/chess-reasoning/initial-search-plan.ts +115 -0
  190. package/src/chess-reasoning/lossless-evidence-sharing.ts +135 -0
  191. package/src/chess-reasoning/matched-comparison.ts +40 -0
  192. package/src/chess-reasoning/material-accounting.ts +55 -0
  193. package/src/chess-reasoning/material-format.ts +47 -0
  194. package/src/chess-reasoning/mechanical-fact-provider.ts +1602 -0
  195. package/src/chess-reasoning/mechanical-ray-facts.ts +123 -0
  196. package/src/chess-reasoning/mechanism-presentation.ts +94 -0
  197. package/src/chess-reasoning/move-mechanism-facts.ts +185 -0
  198. package/src/chess-reasoning/native-answer-connector.ts +13 -0
  199. package/src/chess-reasoning/opening-source-contracts.ts +47 -0
  200. package/src/chess-reasoning/opening-source-credits.ts +53 -0
  201. package/src/chess-reasoning/opponent-threat-preparation.ts +98 -0
  202. package/src/chess-reasoning/piece-identities.ts +159 -0
  203. package/src/chess-reasoning/poc-contracts.ts +1097 -0
  204. package/src/chess-reasoning/poc-hypotheses.ts +759 -0
  205. package/src/chess-reasoning/poc-move-witnesses.ts +170 -0
  206. package/src/chess-reasoning/poc-observations.ts +611 -0
  207. package/src/chess-reasoning/poc-piece-refs.ts +62 -0
  208. package/src/chess-reasoning/poc-plan-model.ts +345 -0
  209. package/src/chess-reasoning/poc-semantic-text.ts +10 -0
  210. package/src/chess-reasoning/poc-tools.ts +978 -0
  211. package/src/chess-reasoning/portable-answer-runtime.ts +684 -0
  212. package/src/chess-reasoning/position-analysis-contracts.ts +88 -0
  213. package/src/chess-reasoning/position-analysis-scope.ts +20 -0
  214. package/src/chess-reasoning/preparation-consolidation.ts +91 -0
  215. package/src/chess-reasoning/production-observation-provider.ts +41 -0
  216. package/src/chess-reasoning/question-routing.ts +332 -0
  217. package/src/chess-reasoning/recorded-main-line.ts +55 -0
  218. package/src/chess-reasoning/recorded-position-tools.ts +78 -0
  219. package/src/chess-reasoning/recorded-reference-context.ts +44 -0
  220. package/src/chess-reasoning/recorded-selected-classification.ts +54 -0
  221. package/src/chess-reasoning/reference-limits.ts +2 -0
  222. package/src/chess-reasoning/research-exports.ts +197 -0
  223. package/src/chess-reasoning/saved-score-presentation.ts +6 -0
  224. package/src/chess-reasoning/score-comparison-notices.ts +115 -0
  225. package/src/chess-reasoning/server-simple-policy.ts +75 -0
  226. package/src/chess-reasoning/simple-agent-config.ts +58 -0
  227. package/src/chess-reasoning/simple-board-analysis.ts +57 -0
  228. package/src/chess-reasoning/simple-compact-evidence.ts +79 -0
  229. package/src/chess-reasoning/simple-completion.ts +37 -0
  230. package/src/chess-reasoning/simple-engine-presentation.ts +95 -0
  231. package/src/chess-reasoning/simple-evidence-preparation.ts +32 -0
  232. package/src/chess-reasoning/simple-evidence-view.ts +69 -0
  233. package/src/chess-reasoning/simple-explanation-coverage.ts +118 -0
  234. package/src/chess-reasoning/simple-fact-delivery.ts +52 -0
  235. package/src/chess-reasoning/simple-fallback-premise.ts +65 -0
  236. package/src/chess-reasoning/simple-prose-references.ts +528 -0
  237. package/src/chess-reasoning/simple-runtime-version.ts +2 -0
  238. package/src/chess-reasoning/stable.ts +18 -0
  239. package/src/chess-reasoning/tool-input-loop-guard.ts +38 -0
  240. package/src/chess-reasoning/types.ts +195 -0
  241. package/src/chess-reasoning/visible-support-lines.ts +44 -0
  242. package/src/chess-reasoning/wikibooks-index.ts +56 -0
  243. package/src/chess-reasoning/wikibooks-tool-context.ts +91 -0
  244. package/src/player-perspective.ts +41 -0
  245. package/src/shared-exports.ts +23 -0
  246. package/test/answer-pipeline.test.mjs +375 -0
  247. package/test/owner/chess-reasoning-compact-search-preparation.test.mjs +111 -0
  248. package/test/owner/mechanism-encoding.test.mjs +78 -0
  249. package/test/owner/move-mechanism-facts.test.mjs +105 -0
  250. package/test/owner/pipeline-host-preparation.test.mjs +115 -0
  251. package/test/owner/position-preparation.test.mjs +22 -0
  252. package/test/owner/preparation-consolidation.test.mjs +74 -0
  253. package/test/owner/prose-reference-admission.test.mjs +26 -0
  254. package/test/owner/score-comparison-notices.test.mjs +116 -0
  255. package/test/owner/simple-engine-presentation.test.mjs +50 -0
  256. package/test/owner/tool-input-loop-guard.test.mjs +25 -0
  257. package/test-types.mts +23 -0
  258. package/tsconfig.json +22 -0
@@ -0,0 +1,726 @@
1
+ import {
2
+ REASONING_POC_VERSION,
3
+ reasoningContextHash,
4
+ validateReasoningPositionContext,
5
+ type ReasoningEvidenceRecord,
6
+ type ReasoningObservation,
7
+ type ReasoningPositionContext,
8
+ type ReasoningToolDescriptor,
9
+ type ReasoningToolName,
10
+ } from "./poc-contracts";
11
+ import { effectiveLicensesForRecord } from "./evidence-licenses";
12
+ import { REFERENCE_DOMAINS } from "./concept-registry";
13
+ import { collectReasoningMoveWitnesses, type ReasoningMoveWitness } from "./poc-move-witnesses";
14
+ import { createPocIdentityTimeline } from "./poc-piece-refs";
15
+ import { stableHash, stableJson } from "./stable";
16
+ import { SQUARES } from "chess.js";
17
+
18
+ /**
19
+ * A deliberately pre-semantic snapshot for the architecture ablation. This
20
+ * is not a replay/checkpoint format: hypothesis, graph, and disposition
21
+ * state must remain outside the acquisition/deliberation boundary.
22
+ */
23
+ export const ABLATION_EVIDENCE_BUNDLE_VERSION = "architecture-ablation-evidence-bundle/1" as const;
24
+ export const ABLATION_PROBE_TELEMETRY_VERSION = "architecture-ablation-probe-telemetry/2" as const;
25
+
26
+ export const PRECOMMITTED_PROBE_ATTRIBUTION_CATEGORIES = Object.freeze([
27
+ "fact-unavailable",
28
+ "fact-available-not-presented",
29
+ "fact-presented-ignored",
30
+ "fact-recognized-not-probed",
31
+ "hypothesis-formed-later-discarded",
32
+ "correct-graph-poor-rendering",
33
+ "unsupported-claim-admitted",
34
+ "safe-fail-closed-termination",
35
+ ] as const);
36
+
37
+ export type ProbeAttributionCategory = typeof PRECOMMITTED_PROBE_ATTRIBUTION_CATEGORIES[number];
38
+ export type AblationBundleOmissionReason = "budget" | "not-requested" | "unavailable" | "withheld";
39
+
40
+ export type AblationEvidenceClosureCoverage = {
41
+ complete: boolean;
42
+ covered: string[];
43
+ omitted: Array<{ domain: "observation" | "evidence" | "tool"; id: string; reason: AblationBundleOmissionReason }>;
44
+ unavailable: Array<{ id: string; reason: string }>;
45
+ limitations: string[];
46
+ };
47
+
48
+ export type AblationEvidenceReferenceAllowlists = {
49
+ evidenceIds: string[];
50
+ observationIds: string[];
51
+ toolNames: ReasoningToolName[];
52
+ semanticReferences: Array<{ domain: "piece" | "square" | "move"; id: string }>;
53
+ moveWitnesses: ReasoningMoveWitness[];
54
+ };
55
+
56
+ export type AblationEvidenceBundleLineage =
57
+ | { kind: "original" }
58
+ | {
59
+ kind: "enriched";
60
+ originalBundleHash: string;
61
+ acquiredEvidenceIds: string[];
62
+ acquiredObservationIds: string[];
63
+ };
64
+
65
+ export type AblationEvidenceClosureBundle = {
66
+ schemaVersion: typeof ABLATION_EVIDENCE_BUNDLE_VERSION;
67
+ contextHash: string;
68
+ context: ReasoningPositionContext;
69
+ question: string;
70
+ observations: ReasoningObservation[];
71
+ evidence: ReasoningEvidenceRecord[];
72
+ referenceAllowlists: AblationEvidenceReferenceAllowlists;
73
+ coverage: AblationEvidenceClosureCoverage;
74
+ lineage: AblationEvidenceBundleLineage;
75
+ bundleHash: string;
76
+ };
77
+
78
+ export type CreateAblationEvidenceClosureBundleInput = Omit<AblationEvidenceClosureBundle, "schemaVersion" | "contextHash" | "lineage" | "bundleHash">;
79
+
80
+ export type EnrichAblationEvidenceClosureBundleInput = {
81
+ observations: ReasoningObservation[];
82
+ evidence: ReasoningEvidenceRecord[];
83
+ referenceAllowlists?: Partial<AblationEvidenceReferenceAllowlists>;
84
+ coverage: AblationEvidenceClosureCoverage;
85
+ };
86
+
87
+ export type AblationAnchorAcquirability = {
88
+ /** Evaluator metadata only; no evaluator anchor is carried in this value. */
89
+ telemetryOnly: true;
90
+ status: "not-applicable" | "acquirable-from-admitted-evidence" | "requires-probe" | "unacquirable-within-budget";
91
+ basisEvidenceIds: string[];
92
+ rationale: string;
93
+ };
94
+
95
+ export type AblationProbeEffect = "materially-changed" | "supported" | "narrowed" | "rejected" | "no-change" | "failure";
96
+
97
+ export type AblationCalledProbe = {
98
+ callId: string;
99
+ requestId: string;
100
+ tool: ReasoningToolName;
101
+ request: {
102
+ descriptor: ReasoningToolDescriptor;
103
+ input: Record<string, unknown>;
104
+ selectionRationale: string;
105
+ };
106
+ target: { fen: string; historyThroughTargetHash: string };
107
+ permittedLimits: {
108
+ maxToolCalls: number;
109
+ maxProbeCalls: number;
110
+ engine: { depth: number | null; multiPv: number | null; threads: number | null; rootMoves: string[] } | null;
111
+ };
112
+ approval: { approved: boolean; budgetReason: string; remainingToolCalls: number; remainingProbeCalls: number };
113
+ status: { started: boolean; finished: "not-started" | "returned" | "failed" | "cancelled" };
114
+ returned: { lines: number; coverage: { complete: boolean; covered: string[]; unresolved: string[] } } | null;
115
+ admission: { status: "admitted" | "withheld" | "rejected" | "not-produced"; reason: string };
116
+ admittedEvidence: Array<{ evidenceId: string; licenses: string[] }>;
117
+ presentation: { presentedToModel: boolean; reason: string };
118
+ semanticConsumption: { status: "not-reached" | "not-consumed" | "consumed" | "misused"; hypothesisIds: string[] };
119
+ modelStatedRationale: string | null;
120
+ effect: AblationProbeEffect;
121
+ hypothesisIds: string[];
122
+ usage: { inputTokens: number | null; outputTokens: number | null; latencyMs: number | null; cache: { hits: number; misses: number }; cost: number | null; corrections: number };
123
+ };
124
+
125
+ export type AblationProbeTelemetry = {
126
+ schemaVersion: typeof ABLATION_PROBE_TELEMETRY_VERSION;
127
+ bundleHash: string;
128
+ beforeProbeEvidenceIds: string[];
129
+ postSaliencePresentedFactIds: string[];
130
+ availableTools: ReasoningToolDescriptor[];
131
+ calledProbes: AblationCalledProbe[];
132
+ missedAnchorAttributions: Array<{
133
+ /** Opaque evaluator-side handle; it never identifies or carries the anchor. */
134
+ anchorId: string;
135
+ category: ProbeAttributionCategory;
136
+ acquirability: AblationAnchorAcquirability;
137
+ unusedRelevantTools: ReasoningToolName[];
138
+ }>;
139
+ stopping: { rationale: string; unusedRelevantTools: ReasoningToolName[] };
140
+ usage: {
141
+ modelCalls: number;
142
+ toolCalls: number;
143
+ inputTokens: number | null;
144
+ outputTokens: number | null;
145
+ latencyMs: number | null;
146
+ cache: { hits: number; misses: number };
147
+ cost: number | null;
148
+ corrections: number;
149
+ };
150
+ telemetryHash: string;
151
+ };
152
+
153
+ export type CreateAblationProbeTelemetryInput = Omit<AblationProbeTelemetry, "schemaVersion" | "bundleHash" | "telemetryHash">;
154
+
155
+ const BUNDLE_KEYS = new Set([
156
+ "schemaVersion", "contextHash", "context", "question", "observations", "evidence",
157
+ "referenceAllowlists", "coverage", "lineage", "bundleHash",
158
+ ]);
159
+ const TELEMETRY_KEYS = new Set([
160
+ "schemaVersion", "bundleHash", "beforeProbeEvidenceIds", "postSaliencePresentedFactIds",
161
+ "availableTools", "calledProbes", "missedAnchorAttributions",
162
+ "stopping", "usage", "telemetryHash",
163
+ ]);
164
+ const FORBIDDEN_BUNDLE_TOP_LEVEL_KEYS = new Set([
165
+ "hypothesis", "hypotheses", "relation", "relations", "graph", "graphs", "disposition",
166
+ "dispositions", "claim", "claims", "answer", "answers", "evaluatorAnchor", "evaluatorAnchors",
167
+ "idealProse", "answerKey", "laterMoves",
168
+ ]);
169
+ const FORBIDDEN_C_ARTIFACT_KEYS = new Set([
170
+ "selectedEntities", "selectedRelations", "sourceHypothesisIds", "hypothesisLedgerRevision",
171
+ ]);
172
+
173
+ function isRecord(value: unknown): value is Record<string, unknown> {
174
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
175
+ }
176
+
177
+ function deepFreeze<T>(value: T): T {
178
+ if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
179
+ Object.freeze(value);
180
+ for (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child);
181
+ return value;
182
+ }
183
+
184
+ function assertDeepFrozen(value: unknown, path = "bundle"): void {
185
+ if (!value || typeof value !== "object") return;
186
+ if (!Object.isFrozen(value)) throw new Error(`Ablation evidence bundle mutation detected at ${path}`);
187
+ for (const [key, child] of Object.entries(value as Record<string, unknown>)) assertDeepFrozen(child, `${path}.${key}`);
188
+ }
189
+
190
+ function clone<T>(value: T): T {
191
+ return structuredClone(value);
192
+ }
193
+
194
+ function uniqueSorted(values: readonly string[], label: string): string[] {
195
+ if (values.some((value) => typeof value !== "string" || !value.trim())) throw new Error(`${label} must contain non-empty strings`);
196
+ return [...new Set(values)].sort();
197
+ }
198
+
199
+ function sameStrings(left: readonly string[], right: readonly string[]): boolean {
200
+ return left.length === right.length && left.every((value, index) => value === right[index]);
201
+ }
202
+
203
+ function assertNoSemanticArtifacts(value: unknown, path = "bundle"): void {
204
+ if (Array.isArray(value)) {
205
+ value.forEach((item, index) => assertNoSemanticArtifacts(item, `${path}[${index}]`));
206
+ return;
207
+ }
208
+ if (!isRecord(value)) return;
209
+ for (const [key, child] of Object.entries(value)) {
210
+ if (FORBIDDEN_C_ARTIFACT_KEYS.has(key)) throw new Error(`Ablation evidence closure cannot contain semantic artifact ${path}.${key}`);
211
+ // Existing C records have graph/review transport envelopes. Generic
212
+ // provider dictionaries may legitimately use ordinary words such as
213
+ // relation or candidate, so reject the known artifact shape, not prose.
214
+ if ((key === "review" && isRecord(child) && ("hypotheses" in child || "selectedEntities" in child))
215
+ || (key === "evidence" && isRecord(child) && child.kind === "graph")) {
216
+ throw new Error(`Ablation evidence closure cannot contain semantic artifact ${path}.${key}`);
217
+ }
218
+ assertNoSemanticArtifacts(child, `${path}.${key}`);
219
+ }
220
+ }
221
+
222
+ function assertNoEvaluatorPayload(value: unknown, path: string): void {
223
+ if (Array.isArray(value)) {
224
+ value.forEach((item, index) => assertNoEvaluatorPayload(item, `${path}[${index}]`));
225
+ return;
226
+ }
227
+ if (!isRecord(value)) return;
228
+ for (const [key, child] of Object.entries(value)) {
229
+ if (["evaluatorAnchor", "evaluatorAnchors", "idealProse", "answerKey", "laterMoves"].includes(key)) {
230
+ throw new Error(`Ablation probe telemetry cannot carry evaluator payload at ${path}.${key}`);
231
+ }
232
+ assertNoEvaluatorPayload(child, `${path}.${key}`);
233
+ }
234
+ }
235
+
236
+ function bundleHash(value: Omit<AblationEvidenceClosureBundle, "bundleHash">): string {
237
+ return stableHash(value);
238
+ }
239
+
240
+ function telemetryHash(value: Omit<AblationProbeTelemetry, "telemetryHash">): string {
241
+ return stableHash(value);
242
+ }
243
+
244
+ export function ablationHistoryThroughTargetHash(context: ReasoningPositionContext): string {
245
+ const checked = validateReasoningPositionContext(context);
246
+ return stableHash({ historyStartFen: checked.historyStartFen || null, historyUci: [...checked.historyUci, ...(checked.kind === "position" ? [] : [checked.moveUci])] });
247
+ }
248
+
249
+ function normalizeCoverage(value: AblationEvidenceClosureCoverage): AblationEvidenceClosureCoverage {
250
+ if (!isRecord(value) || typeof value.complete !== "boolean" || !Array.isArray(value.covered)
251
+ || !Array.isArray(value.omitted) || !Array.isArray(value.unavailable) || !Array.isArray(value.limitations)) {
252
+ throw new Error("Ablation evidence closure requires explicit coverage metadata");
253
+ }
254
+ const omitted = value.omitted.map((item) => {
255
+ if (!isRecord(item) || (item.domain !== "observation" && item.domain !== "evidence" && item.domain !== "tool")
256
+ || typeof item.id !== "string" || !item.id.trim()
257
+ || (item.reason !== "budget" && item.reason !== "not-requested" && item.reason !== "unavailable" && item.reason !== "withheld")) {
258
+ throw new Error("Ablation evidence closure has malformed omission metadata");
259
+ }
260
+ return { domain: item.domain, id: item.id, reason: item.reason };
261
+ }).sort((left, right) => stableJson(left).localeCompare(stableJson(right)));
262
+ const unavailable = value.unavailable.map((item) => {
263
+ if (!isRecord(item) || typeof item.id !== "string" || !item.id.trim() || typeof item.reason !== "string" || !item.reason.trim()) {
264
+ throw new Error("Ablation evidence closure has malformed unavailable metadata");
265
+ }
266
+ return { id: item.id, reason: item.reason };
267
+ }).sort((left, right) => stableJson(left).localeCompare(stableJson(right)));
268
+ const normalized = {
269
+ complete: value.complete,
270
+ covered: uniqueSorted(value.covered, "Ablation evidence closure coverage"),
271
+ omitted,
272
+ unavailable,
273
+ limitations: uniqueSorted(value.limitations, "Ablation evidence closure limitations"),
274
+ };
275
+ if (normalized.complete && (normalized.omitted.length || normalized.unavailable.length)) {
276
+ throw new Error("Complete ablation evidence closure cannot report omissions or unavailable coverage");
277
+ }
278
+ if (!normalized.complete && !normalized.omitted.length && !normalized.unavailable.length && !normalized.limitations.length) {
279
+ throw new Error("Incomplete ablation evidence closure must state an omission, unavailable domain, or limitation");
280
+ }
281
+ return normalized;
282
+ }
283
+
284
+ function normalizeObservations(value: readonly ReasoningObservation[], contextHash: string, evidenceById: Map<string, ReasoningEvidenceRecord>): ReasoningObservation[] {
285
+ const ids = new Set<string>();
286
+ return value.map((observation) => {
287
+ if (!observation || observation.schemaVersion !== REASONING_POC_VERSION || observation.contextHash !== contextHash
288
+ || !observation.id?.trim() || !observation.observationType?.trim() || !Array.isArray(observation.participants)
289
+ || !isRecord(observation.attributes) || !isRecord(observation.provenance) || !Array.isArray(observation.factEvidenceIds)
290
+ || !Array.isArray(observation.probeRecipes) || ids.has(observation.id)) {
291
+ throw new Error("Ablation evidence closure contains malformed or duplicate admitted observation");
292
+ }
293
+ ids.add(observation.id);
294
+ if (!observation.factEvidenceIds.length || observation.factEvidenceIds.some((id) => !evidenceById.has(id))) {
295
+ throw new Error(`Ablation observation ${observation.id} references evidence outside the closure`);
296
+ }
297
+ return clone(observation);
298
+ }).sort((left, right) => left.id.localeCompare(right.id));
299
+ }
300
+
301
+ function normalizeEvidence(value: readonly ReasoningEvidenceRecord[], contextHash: string): ReasoningEvidenceRecord[] {
302
+ const ids = new Set<string>();
303
+ return value.map((record) => {
304
+ if (!record || record.schemaVersion !== REASONING_POC_VERSION || record.contextHash !== contextHash
305
+ || !record.id?.trim() || !record.source?.trim() || !record.summary?.trim() || !isRecord(record.data)
306
+ || !Array.isArray(record.licenses) || (record.kind !== "context" && record.kind !== "fact" && record.kind !== "probe")
307
+ || ids.has(record.id)) {
308
+ throw new Error("Ablation evidence closure contains malformed, duplicate, or semantic evidence");
309
+ }
310
+ ids.add(record.id);
311
+ // This is the production central ceiling, not a second license policy.
312
+ effectiveLicensesForRecord(record);
313
+ return clone(record);
314
+ }).sort((left, right) => left.id.localeCompare(right.id));
315
+ }
316
+
317
+ function normalizeAllowlists(
318
+ value: AblationEvidenceReferenceAllowlists,
319
+ evidence: readonly ReasoningEvidenceRecord[],
320
+ observations: readonly ReasoningObservation[],
321
+ context: ReasoningPositionContext,
322
+ ): AblationEvidenceReferenceAllowlists {
323
+ if (!isRecord(value) || !Array.isArray(value.evidenceIds) || !Array.isArray(value.observationIds)
324
+ || !Array.isArray(value.toolNames) || !Array.isArray(value.semanticReferences) || !Array.isArray(value.moveWitnesses)) {
325
+ throw new Error("Ablation evidence closure requires reference allowlists");
326
+ }
327
+ const evidenceIds = uniqueSorted(value.evidenceIds, "Evidence reference allowlist");
328
+ const observationIds = uniqueSorted(value.observationIds, "Observation reference allowlist");
329
+ const toolNames = uniqueSorted(value.toolNames, "Tool reference allowlist") as ReasoningToolName[];
330
+ const admittedEvidence = new Set(evidence.map((record) => record.id));
331
+ const admittedObservations = new Set(observations.map((observation) => observation.id));
332
+ if (evidenceIds.some((id) => !admittedEvidence.has(id)) || observationIds.some((id) => !admittedObservations.has(id))) {
333
+ throw new Error("Ablation reference allowlist cannot name unadmitted records");
334
+ }
335
+ const timeline = createPocIdentityTimeline(context);
336
+ const knownPieces = new Set([...Object.keys(timeline.before.pieces), ...Object.keys(timeline.after.pieces)]);
337
+ const knownMoves = new Set([...context.historyUci, ...(context.kind === "position" ? [] : [context.moveUci]), ...collectReasoningMoveWitnesses(evidence, context).map((witness) => witness.uci)]);
338
+ const semanticReferences = [...new Map(value.semanticReferences.map((reference) => {
339
+ if (!isRecord(reference) || (reference.domain !== REFERENCE_DOMAINS.PIECE && reference.domain !== REFERENCE_DOMAINS.SQUARE && reference.domain !== REFERENCE_DOMAINS.MOVE)
340
+ || typeof reference.id !== "string" || !reference.id.trim()) {
341
+ throw new Error("Ablation semantic reference allowlist requires production domain/id references");
342
+ }
343
+ if ((reference.domain === REFERENCE_DOMAINS.PIECE && !knownPieces.has(reference.id))
344
+ || (reference.domain === REFERENCE_DOMAINS.SQUARE && !SQUARES.includes(reference.id as typeof SQUARES[number]))
345
+ || (reference.domain === REFERENCE_DOMAINS.MOVE && !knownMoves.has(reference.id))) {
346
+ throw new Error(`Ablation semantic reference is not grounded by the context or admitted evidence: ${reference.domain}:${reference.id}`);
347
+ }
348
+ const normalized = { domain: reference.domain, id: reference.id } as { domain: "piece" | "square" | "move"; id: string };
349
+ return [`${normalized.domain}:${normalized.id}`, normalized] as const;
350
+ })).values()].sort((left, right) => `${left.domain}:${left.id}`.localeCompare(`${right.domain}:${right.id}`));
351
+ const admittedWitnesses = new Map(collectReasoningMoveWitnesses(evidence, context).map((witness) => [
352
+ `${witness.evidenceId}:${witness.uci}:${witness.pieceId}:${witness.san}`,
353
+ witness,
354
+ ]));
355
+ const moveWitnesses = [...new Map(value.moveWitnesses.map((witness) => {
356
+ if (!isRecord(witness) || typeof witness.evidenceId !== "string" || typeof witness.uci !== "string"
357
+ || typeof witness.pieceId !== "string" || typeof witness.san !== "string") {
358
+ throw new Error("Ablation move witness allowlist is malformed");
359
+ }
360
+ const key = `${witness.evidenceId}:${witness.uci}:${witness.pieceId}:${witness.san}`;
361
+ if (!admittedWitnesses.has(key)) throw new Error("Ablation move witness must be replayable from admitted evidence with stable identity and SAN");
362
+ return [key, clone(admittedWitnesses.get(key)!)] as const;
363
+ })).values()].sort((left, right) => `${left.evidenceId}:${left.uci}:${left.pieceId}:${left.san}`.localeCompare(`${right.evidenceId}:${right.uci}:${right.pieceId}:${right.san}`));
364
+ return { evidenceIds, observationIds, toolNames, semanticReferences, moveWitnesses };
365
+ }
366
+
367
+ function normalizedBundle(input: Omit<AblationEvidenceClosureBundle, "schemaVersion" | "contextHash" | "bundleHash">): Omit<AblationEvidenceClosureBundle, "bundleHash"> {
368
+ if (!isRecord(input)) throw new Error("Ablation evidence closure must be an object");
369
+ if (Object.keys(input).some((key) => FORBIDDEN_BUNDLE_TOP_LEVEL_KEYS.has(key))) throw new Error("Ablation evidence closure cannot contain top-level semantic artifacts");
370
+ if (Object.keys(input).some((key) => !BUNDLE_KEYS.has(key))) throw new Error("Ablation evidence closure contains unsupported fields");
371
+ assertNoSemanticArtifacts({ observations: input.observations, evidence: input.evidence });
372
+ const context = validateReasoningPositionContext(clone(input.context));
373
+ const contextHash = reasoningContextHash(context);
374
+ if (typeof input.question !== "string" || !input.question.trim() || input.question.length > 4_000) {
375
+ throw new Error("Ablation evidence closure requires a bounded question");
376
+ }
377
+ const evidence = normalizeEvidence(input.evidence, contextHash);
378
+ const observations = normalizeObservations(input.observations, contextHash, new Map(evidence.map((record) => [record.id, record])));
379
+ const referenceAllowlists = normalizeAllowlists(input.referenceAllowlists, evidence, observations, context);
380
+ const coverage = normalizeCoverage(input.coverage);
381
+ if (!isRecord(input.lineage) || (input.lineage.kind !== "original" && input.lineage.kind !== "enriched")) {
382
+ throw new Error("Ablation evidence closure has malformed lineage");
383
+ }
384
+ const lineage = input.lineage.kind === "original"
385
+ ? { kind: "original" as const }
386
+ : {
387
+ kind: "enriched" as const,
388
+ originalBundleHash: typeof input.lineage.originalBundleHash === "string" ? input.lineage.originalBundleHash : "",
389
+ acquiredEvidenceIds: uniqueSorted(input.lineage.acquiredEvidenceIds || [], "Acquired evidence lineage"),
390
+ acquiredObservationIds: uniqueSorted(input.lineage.acquiredObservationIds || [], "Acquired observation lineage"),
391
+ };
392
+ if (lineage.kind === "enriched") {
393
+ if (!lineage.originalBundleHash || !lineage.acquiredEvidenceIds.length) throw new Error("Enriched ablation evidence closure requires original lineage and acquired evidence");
394
+ const evidenceIds = new Set(evidence.map((record) => record.id));
395
+ const observationIds = new Set(observations.map((observation) => observation.id));
396
+ if (lineage.acquiredEvidenceIds.some((id) => !evidenceIds.has(id)) || lineage.acquiredObservationIds.some((id) => !observationIds.has(id))) {
397
+ throw new Error("Enriched ablation lineage references records outside the closure");
398
+ }
399
+ }
400
+ return {
401
+ schemaVersion: ABLATION_EVIDENCE_BUNDLE_VERSION,
402
+ contextHash,
403
+ context,
404
+ question: input.question,
405
+ observations,
406
+ evidence,
407
+ referenceAllowlists,
408
+ coverage,
409
+ lineage,
410
+ };
411
+ }
412
+
413
+ export function createAblationEvidenceClosureBundle(input: CreateAblationEvidenceClosureBundleInput): Readonly<AblationEvidenceClosureBundle> {
414
+ const normalized = normalizedBundle({ ...input, lineage: { kind: "original" } });
415
+ return deepFreeze({ ...normalized, bundleHash: bundleHash(normalized) });
416
+ }
417
+
418
+ export function enrichAblationEvidenceClosureBundle(
419
+ original: Readonly<AblationEvidenceClosureBundle>,
420
+ input: EnrichAblationEvidenceClosureBundleInput,
421
+ ): Readonly<AblationEvidenceClosureBundle> {
422
+ validateAblationEvidenceClosureBundle(original);
423
+ if (original.lineage.kind !== "original") throw new Error("An ablation evidence closure can only be enriched from its original bundle");
424
+ const priorEvidenceIds = new Set(original.evidence.map((record) => record.id));
425
+ const priorObservationIds = new Set(original.observations.map((observation) => observation.id));
426
+ const addedEvidence = input.evidence.filter((record) => !priorEvidenceIds.has(record.id));
427
+ const addedObservations = input.observations.filter((observation) => !priorObservationIds.has(observation.id));
428
+ if (!addedEvidence.length) throw new Error("An enriched ablation evidence closure requires newly acquired evidence");
429
+ const evidence = [...original.evidence, ...input.evidence];
430
+ const observations = [...original.observations, ...input.observations];
431
+ const defaultAllowlists = {
432
+ evidenceIds: evidence.map((record) => record.id),
433
+ observationIds: observations.map((observation) => observation.id),
434
+ toolNames: original.referenceAllowlists.toolNames,
435
+ semanticReferences: original.referenceAllowlists.semanticReferences,
436
+ moveWitnesses: original.referenceAllowlists.moveWitnesses,
437
+ };
438
+ const normalized = normalizedBundle({
439
+ context: original.context,
440
+ question: original.question,
441
+ observations,
442
+ evidence,
443
+ referenceAllowlists: { ...defaultAllowlists, ...(input.referenceAllowlists || {}) },
444
+ coverage: input.coverage,
445
+ lineage: {
446
+ kind: "enriched",
447
+ originalBundleHash: original.bundleHash,
448
+ acquiredEvidenceIds: addedEvidence.map((record) => record.id),
449
+ acquiredObservationIds: addedObservations.map((observation) => observation.id),
450
+ },
451
+ });
452
+ return deepFreeze({ ...normalized, bundleHash: bundleHash(normalized) });
453
+ }
454
+
455
+ export function validateAblationEvidenceClosureBundle(value: unknown): asserts value is Readonly<AblationEvidenceClosureBundle> {
456
+ if (!isRecord(value) || Object.keys(value).some((key) => !BUNDLE_KEYS.has(key)) || value.schemaVersion !== ABLATION_EVIDENCE_BUNDLE_VERSION) {
457
+ throw new Error("Unsupported ablation evidence closure bundle");
458
+ }
459
+ assertDeepFrozen(value);
460
+ const { bundleHash: suppliedHash, schemaVersion: _schemaVersion, contextHash: _contextHash, ...unhashed } = value as AblationEvidenceClosureBundle;
461
+ void _schemaVersion;
462
+ void _contextHash;
463
+ const normalized = normalizedBundle(unhashed);
464
+ const expectedHash = bundleHash(normalized);
465
+ if (value.contextHash !== normalized.contextHash || suppliedHash !== expectedHash || stableJson(value) !== stableJson({ ...normalized, bundleHash: expectedHash })) {
466
+ throw new Error("Ablation evidence closure bundle integrity check failed");
467
+ }
468
+ }
469
+
470
+ function normalizeTelemetry(
471
+ bundle: Readonly<AblationEvidenceClosureBundle>,
472
+ input: Omit<AblationProbeTelemetry, "schemaVersion" | "bundleHash" | "telemetryHash">,
473
+ ): Omit<AblationProbeTelemetry, "telemetryHash"> {
474
+ if (!isRecord(input) || Object.keys(input).some((key) => !TELEMETRY_KEYS.has(key))) throw new Error("Ablation probe telemetry contains unsupported fields");
475
+ const evidenceById = new Map(bundle.evidence.map((record) => [record.id, record]));
476
+ const beforeProbeEvidenceIds = uniqueSorted(input.beforeProbeEvidenceIds, "Before-probe evidence");
477
+ const postSaliencePresentedFactIds = uniqueSorted(input.postSaliencePresentedFactIds, "Post-salience presented facts");
478
+ if (beforeProbeEvidenceIds.some((id) => !evidenceById.has(id)) || postSaliencePresentedFactIds.some((id) => !evidenceById.has(id))) {
479
+ throw new Error("Ablation probe telemetry references evidence outside its bundle");
480
+ }
481
+ if (postSaliencePresentedFactIds.some((id) => !["context", "fact", "probe"].includes(evidenceById.get(id)!.kind))) {
482
+ throw new Error("Post-salience telemetry must reference admitted factual evidence");
483
+ }
484
+ if (!Array.isArray(input.availableTools) || !Array.isArray(input.calledProbes) || !Array.isArray(input.missedAnchorAttributions)) {
485
+ throw new Error("Ablation probe telemetry requires live tool availability and call sequence");
486
+ }
487
+ const availableTools = input.availableTools.map((descriptor) => {
488
+ if (!descriptor || typeof descriptor.name !== "string" || !descriptor.description?.trim()
489
+ || (descriptor.category !== "fact" && descriptor.category !== "probe" && descriptor.category !== "graph") || !isRecord(descriptor.inputSchema)) {
490
+ throw new Error("Ablation probe telemetry received malformed live tool descriptor");
491
+ }
492
+ return clone(descriptor);
493
+ }).sort((left, right) => left.name.localeCompare(right.name));
494
+ if (new Set(availableTools.map((tool) => tool.name)).size !== availableTools.length) throw new Error("Ablation probe telemetry has duplicate available tools");
495
+ const availableNames = new Set<ReasoningToolName>(availableTools.map((tool) => tool.name));
496
+ const expectedTargetHistoryHash = ablationHistoryThroughTargetHash(bundle.context);
497
+ const calledProbes = input.calledProbes.map((call, index) => {
498
+ if (!isRecord(call) || typeof call.callId !== "string" || !call.callId.trim() || typeof call.tool !== "string" || !availableNames.has(call.tool as ReasoningToolName)) {
499
+ throw new Error(`Ablation probe telemetry call ${index} is not a live available tool`);
500
+ }
501
+ if (call.modelStatedRationale !== null && (typeof call.modelStatedRationale !== "string" || call.modelStatedRationale.length > 500)) {
502
+ throw new Error(`Ablation probe telemetry call ${index} has an invalid stated rationale`);
503
+ }
504
+ if (typeof call.requestId !== "string" || !call.requestId.trim() || !isRecord(call.request)
505
+ || !isRecord(call.request.descriptor) || !isRecord(call.request.input) || typeof call.request.selectionRationale !== "string" || !call.request.selectionRationale.trim() || call.request.selectionRationale.length > 500) {
506
+ throw new Error(`Ablation probe telemetry call ${index} has an incomplete immutable request`);
507
+ }
508
+ const liveDescriptor = availableTools.find((descriptor) => descriptor.name === call.tool);
509
+ if (!liveDescriptor || (liveDescriptor.category !== "fact" && liveDescriptor.category !== "probe") || stableJson(call.request.descriptor) !== stableJson(liveDescriptor)) {
510
+ throw new Error(`Ablation probe telemetry call ${index} request descriptor is not the live descriptor`);
511
+ }
512
+ assertNoEvaluatorPayload(call.request.input, `calledProbes[${index}].request.input`);
513
+ if (!isRecord(call.target) || call.target.fen !== bundle.context.fenAfter || call.target.historyThroughTargetHash !== expectedTargetHistoryHash) {
514
+ throw new Error(`Ablation probe telemetry call ${index} target is not bound to the bundle context`);
515
+ }
516
+ if (!isRecord(call.permittedLimits) || !Number.isInteger(call.permittedLimits.maxToolCalls) || call.permittedLimits.maxToolCalls < 0
517
+ || !Number.isInteger(call.permittedLimits.maxProbeCalls) || call.permittedLimits.maxProbeCalls < 0) {
518
+ throw new Error(`Ablation probe telemetry call ${index} has malformed permitted limits`);
519
+ }
520
+ const engine = call.permittedLimits.engine;
521
+ if (engine !== null && (!isRecord(engine) || !Array.isArray(engine.rootMoves)
522
+ || [engine.depth, engine.multiPv, engine.threads].some((value) => value !== null && (!Number.isInteger(value) || value < 1))
523
+ || engine.rootMoves.some((move) => typeof move !== "string" || !/^[a-h][1-8][a-h][1-8][qrbn]?$/.test(move)))) {
524
+ throw new Error(`Ablation probe telemetry call ${index} has malformed engine limits`);
525
+ }
526
+ if (!isRecord(call.approval) || typeof call.approval.approved !== "boolean" || typeof call.approval.budgetReason !== "string" || !call.approval.budgetReason.trim()
527
+ || !Number.isInteger(call.approval.remainingToolCalls) || call.approval.remainingToolCalls < 0 || !Number.isInteger(call.approval.remainingProbeCalls) || call.approval.remainingProbeCalls < 0) {
528
+ throw new Error(`Ablation probe telemetry call ${index} has malformed approval/budget decision`);
529
+ }
530
+ if (!isRecord(call.status) || typeof call.status.started !== "boolean" || !["not-started", "returned", "failed", "cancelled"].includes(String(call.status.finished))
531
+ || (!call.status.started && call.status.finished !== "not-started")) {
532
+ throw new Error(`Ablation probe telemetry call ${index} has malformed execution status`);
533
+ }
534
+ if (!call.approval.approved && call.status.started) throw new Error(`Ablation probe telemetry call ${index} started without approval`);
535
+ if (call.status.finished === "returned" && (!call.approval.approved || !call.status.started)) {
536
+ throw new Error(`Ablation probe telemetry call ${index} returned without approved execution`);
537
+ }
538
+ if (call.returned !== null && (!isRecord(call.returned) || !Number.isInteger(call.returned.lines) || call.returned.lines < 0 || !isRecord(call.returned.coverage)
539
+ || typeof call.returned.coverage.complete !== "boolean" || !Array.isArray(call.returned.coverage.covered) || !Array.isArray(call.returned.coverage.unresolved))) {
540
+ throw new Error(`Ablation probe telemetry call ${index} has malformed returned-line coverage`);
541
+ }
542
+ if (call.status.finished === "returned" && call.returned === null) throw new Error(`Ablation probe telemetry call ${index} returned without a coverage summary`);
543
+ if (!isRecord(call.admission) || !["admitted", "withheld", "rejected", "not-produced"].includes(String(call.admission.status))
544
+ || typeof call.admission.reason !== "string" || !call.admission.reason.trim() || !Array.isArray(call.admittedEvidence)) {
545
+ throw new Error(`Ablation probe telemetry call ${index} has malformed admission result`);
546
+ }
547
+ const admittedEvidence = call.admittedEvidence.map((item) => {
548
+ if (!isRecord(item) || typeof item.evidenceId !== "string" || !evidenceById.has(item.evidenceId) || !Array.isArray(item.licenses)) {
549
+ throw new Error(`Ablation probe telemetry call ${index} names unadmitted evidence`);
550
+ }
551
+ const licenses = uniqueSorted(item.licenses, "Admitted evidence licenses");
552
+ if (!sameStrings(licenses, effectiveLicensesForRecord(evidenceById.get(item.evidenceId)!))) {
553
+ throw new Error(`Ablation probe telemetry call ${index} evidence licenses do not match admission`);
554
+ }
555
+ return { evidenceId: item.evidenceId, licenses };
556
+ }).sort((left, right) => left.evidenceId.localeCompare(right.evidenceId));
557
+ if (new Set(admittedEvidence.map((item) => item.evidenceId)).size !== admittedEvidence.length) throw new Error(`Ablation probe telemetry call ${index} has duplicate admitted evidence`);
558
+ if ((call.admission.status === "admitted") !== Boolean(admittedEvidence.length)) throw new Error(`Ablation probe telemetry call ${index} admission must exactly match admitted evidence`);
559
+ if (admittedEvidence.length && (call.status.finished !== "returned" || call.admission.status !== "admitted")) {
560
+ throw new Error(`Ablation probe telemetry call ${index} admitted evidence without a returned admitted result`);
561
+ }
562
+ if (!isRecord(call.presentation) || typeof call.presentation.presentedToModel !== "boolean" || typeof call.presentation.reason !== "string" || !call.presentation.reason.trim()) {
563
+ throw new Error(`Ablation probe telemetry call ${index} has malformed presentation result`);
564
+ }
565
+ if (call.presentation.presentedToModel && !admittedEvidence.length) throw new Error(`Ablation probe telemetry call ${index} presented evidence without admission`);
566
+ if (!isRecord(call.semanticConsumption) || !["not-reached", "not-consumed", "consumed", "misused"].includes(String(call.semanticConsumption.status))
567
+ || !Array.isArray(call.semanticConsumption.hypothesisIds)) {
568
+ throw new Error(`Ablation probe telemetry call ${index} has malformed semantic consumption`);
569
+ }
570
+ const consumedHypothesisIds = uniqueSorted(call.semanticConsumption.hypothesisIds, "Semantic consumption hypothesis IDs");
571
+ if ((call.semanticConsumption.status === "not-reached" || call.semanticConsumption.status === "not-consumed") && consumedHypothesisIds.length) {
572
+ throw new Error(`Ablation probe telemetry call ${index} attaches hypotheses without semantic consumption`);
573
+ }
574
+ if ((call.semanticConsumption.status === "consumed" || call.semanticConsumption.status === "misused")
575
+ && (!call.presentation.presentedToModel || !consumedHypothesisIds.length)) {
576
+ throw new Error(`Ablation probe telemetry call ${index} consumed evidence without presented hypotheses`);
577
+ }
578
+ if (!Array.isArray(call.hypothesisIds) || !["materially-changed", "supported", "narrowed", "rejected", "no-change", "failure"].includes(String(call.effect))) {
579
+ throw new Error(`Ablation probe telemetry call ${index} has an invalid probe effect`);
580
+ }
581
+ const hypothesisIds = uniqueSorted(call.hypothesisIds, "Probe effect hypothesis IDs");
582
+ if ((call.effect === "no-change" || call.effect === "failure") && hypothesisIds.length) {
583
+ throw new Error(`Ablation probe telemetry call ${index} cannot attach hypotheses to ${call.effect}`);
584
+ }
585
+ if (call.effect !== "no-change" && call.effect !== "failure" && !hypothesisIds.length) {
586
+ throw new Error(`Ablation probe telemetry call ${index} must identify affected hypotheses`);
587
+ }
588
+ const materialEffect = call.effect === "materially-changed" || call.effect === "supported" || call.effect === "narrowed" || call.effect === "rejected";
589
+ if (materialEffect && (call.semanticConsumption.status !== "consumed" && call.semanticConsumption.status !== "misused"
590
+ || !sameStrings(hypothesisIds, consumedHypothesisIds))) {
591
+ throw new Error(`Ablation probe telemetry call ${index} material effect must match consumed hypotheses`);
592
+ }
593
+ if ((call.semanticConsumption.status === "not-reached" || call.semanticConsumption.status === "not-consumed")
594
+ && (call.effect !== "no-change" && call.effect !== "failure" || hypothesisIds.length)) {
595
+ throw new Error(`Ablation probe telemetry call ${index} unconsumed evidence must have no material hypothesis effect`);
596
+ }
597
+ if (!isRecord(call.usage) || !isRecord(call.usage.cache) || !Number.isInteger(call.usage.cache.hits) || !Number.isInteger(call.usage.cache.misses)
598
+ || call.usage.cache.hits < 0 || call.usage.cache.misses < 0 || !Number.isInteger(call.usage.corrections) || call.usage.corrections < 0
599
+ || [call.usage.inputTokens, call.usage.outputTokens, call.usage.latencyMs, call.usage.cost].some((value) => value !== null && (!Number.isFinite(value) || value < 0))) {
600
+ throw new Error(`Ablation probe telemetry call ${index} has malformed per-call usage`);
601
+ }
602
+ return {
603
+ callId: call.callId, requestId: call.requestId, tool: call.tool as ReasoningToolName,
604
+ request: { descriptor: clone(liveDescriptor), input: clone(call.request.input), selectionRationale: call.request.selectionRationale },
605
+ target: { fen: bundle.context.fenAfter, historyThroughTargetHash: expectedTargetHistoryHash },
606
+ permittedLimits: { maxToolCalls: call.permittedLimits.maxToolCalls, maxProbeCalls: call.permittedLimits.maxProbeCalls, engine: engine === null ? null : { depth: engine.depth as number | null, multiPv: engine.multiPv as number | null, threads: engine.threads as number | null, rootMoves: uniqueSorted(engine.rootMoves, "Engine root moves") } },
607
+ approval: { approved: call.approval.approved, budgetReason: call.approval.budgetReason, remainingToolCalls: call.approval.remainingToolCalls, remainingProbeCalls: call.approval.remainingProbeCalls },
608
+ status: { started: call.status.started, finished: call.status.finished as AblationCalledProbe["status"]["finished"] },
609
+ returned: call.returned === null ? null : { lines: call.returned.lines, coverage: { complete: call.returned.coverage.complete, covered: uniqueSorted(call.returned.coverage.covered, "Returned coverage"), unresolved: uniqueSorted(call.returned.coverage.unresolved, "Returned coverage") } },
610
+ admission: { status: call.admission.status as AblationCalledProbe["admission"]["status"], reason: call.admission.reason },
611
+ admittedEvidence,
612
+ presentation: { presentedToModel: call.presentation.presentedToModel, reason: call.presentation.reason },
613
+ semanticConsumption: { status: call.semanticConsumption.status as AblationCalledProbe["semanticConsumption"]["status"], hypothesisIds: consumedHypothesisIds },
614
+ modelStatedRationale: call.modelStatedRationale, effect: call.effect as AblationProbeEffect, hypothesisIds,
615
+ usage: { inputTokens: call.usage.inputTokens, outputTokens: call.usage.outputTokens, latencyMs: call.usage.latencyMs, cache: { hits: call.usage.cache.hits, misses: call.usage.cache.misses }, cost: call.usage.cost, corrections: call.usage.corrections },
616
+ };
617
+ });
618
+ if (new Set(calledProbes.map((call) => call.callId)).size !== calledProbes.length) throw new Error("Ablation probe telemetry call IDs must be unique");
619
+ const missedAnchorAttributions = input.missedAnchorAttributions.map((row, index) => {
620
+ if (!isRecord(row) || typeof row.anchorId !== "string" || !row.anchorId.trim()
621
+ || !PRECOMMITTED_PROBE_ATTRIBUTION_CATEGORIES.includes(row.category as ProbeAttributionCategory)
622
+ || !isRecord(row.acquirability) || row.acquirability.telemetryOnly !== true
623
+ || !["not-applicable", "acquirable-from-admitted-evidence", "requires-probe", "unacquirable-within-budget"].includes(String(row.acquirability.status))
624
+ || !Array.isArray(row.acquirability.basisEvidenceIds) || typeof row.acquirability.rationale !== "string" || !row.acquirability.rationale.trim()
625
+ || !Array.isArray(row.unusedRelevantTools)
626
+ || Object.keys(row).some((key) => !["anchorId", "category", "acquirability", "unusedRelevantTools"].includes(key))
627
+ || Object.keys(row.acquirability).some((key) => !["telemetryOnly", "status", "basisEvidenceIds", "rationale"].includes(key))) {
628
+ throw new Error(`Ablation missed-anchor attribution ${index} is malformed`);
629
+ }
630
+ const basisEvidenceIds = uniqueSorted(row.acquirability.basisEvidenceIds, "Anchor acquirability evidence");
631
+ if (basisEvidenceIds.some((id) => !evidenceById.has(id))) throw new Error("Anchor acquirability cannot cite evidence outside the bundle");
632
+ const unusedRelevantTools = uniqueSorted(row.unusedRelevantTools, "Unused relevant tools") as ReasoningToolName[];
633
+ if (unusedRelevantTools.some((tool) => !availableNames.has(tool)) || unusedRelevantTools.some((tool) => calledProbes.some((call) => call.tool === tool))) {
634
+ throw new Error("Unused relevant tool telemetry conflicts with the live call sequence");
635
+ }
636
+ return {
637
+ anchorId: row.anchorId,
638
+ category: row.category as ProbeAttributionCategory,
639
+ acquirability: {
640
+ telemetryOnly: true as const,
641
+ status: row.acquirability.status as AblationAnchorAcquirability["status"],
642
+ basisEvidenceIds,
643
+ rationale: row.acquirability.rationale,
644
+ },
645
+ unusedRelevantTools,
646
+ };
647
+ }).sort((left, right) => left.anchorId.localeCompare(right.anchorId));
648
+ if (new Set(missedAnchorAttributions.map((row) => row.anchorId)).size !== missedAnchorAttributions.length) {
649
+ throw new Error("Ablation missed-anchor attribution requires one row per unique anchor ID");
650
+ }
651
+ if (!isRecord(input.stopping) || typeof input.stopping.rationale !== "string" || !input.stopping.rationale.trim()
652
+ || !Array.isArray(input.stopping.unusedRelevantTools)
653
+ || Object.keys(input.stopping).some((key) => key !== "rationale" && key !== "unusedRelevantTools")) {
654
+ throw new Error("Ablation probe telemetry requires a final stopping rationale");
655
+ }
656
+ const stoppingUnusedTools = uniqueSorted(input.stopping.unusedRelevantTools, "Stopping unused relevant tools") as ReasoningToolName[];
657
+ if (stoppingUnusedTools.some((tool) => !availableNames.has(tool)) || stoppingUnusedTools.some((tool) => calledProbes.some((call) => call.tool === tool))) {
658
+ throw new Error("Ablation stopping rationale names invalid unused relevant tools");
659
+ }
660
+ const usage = input.usage;
661
+ if (!isRecord(usage) || !Number.isInteger(usage.modelCalls) || usage.modelCalls < 0
662
+ || !Number.isInteger(usage.toolCalls) || usage.toolCalls !== calledProbes.length || usage.toolCalls < 0
663
+ || !isRecord(usage.cache) || !Number.isInteger(usage.cache.hits) || !Number.isInteger(usage.cache.misses)
664
+ || usage.cache.hits < 0 || usage.cache.misses < 0 || !Number.isInteger(usage.corrections) || usage.corrections < 0
665
+ || [usage.inputTokens, usage.outputTokens, usage.latencyMs, usage.cost].some((value) => value !== null && (!Number.isFinite(value) || value < 0))) {
666
+ throw new Error("Ablation probe telemetry usage is malformed");
667
+ }
668
+ const minimum = (field: "inputTokens" | "outputTokens" | "cost" | "latencyMs") => calledProbes.reduce((total, call) => total + (call.usage[field] || 0), 0);
669
+ if ((usage.inputTokens !== null && usage.inputTokens < minimum("inputTokens"))
670
+ || (usage.outputTokens !== null && usage.outputTokens < minimum("outputTokens"))
671
+ || (usage.cost !== null && usage.cost < minimum("cost"))
672
+ || (usage.latencyMs !== null && calledProbes.some((call) => call.usage.latencyMs !== null && usage.latencyMs! < call.usage.latencyMs))) {
673
+ throw new Error("Ablation probe telemetry totals are smaller than recorded per-call usage");
674
+ }
675
+ const probeCacheHits = calledProbes.reduce((total, call) => total + call.usage.cache.hits, 0);
676
+ const probeCacheMisses = calledProbes.reduce((total, call) => total + call.usage.cache.misses, 0);
677
+ const probeCorrections = calledProbes.reduce((total, call) => total + call.usage.corrections, 0);
678
+ if (usage.cache.hits < probeCacheHits || usage.cache.misses < probeCacheMisses || usage.corrections < probeCorrections) {
679
+ throw new Error("Ablation probe telemetry totals are smaller than recorded per-call counters");
680
+ }
681
+ return {
682
+ schemaVersion: ABLATION_PROBE_TELEMETRY_VERSION,
683
+ bundleHash: bundle.bundleHash,
684
+ beforeProbeEvidenceIds,
685
+ postSaliencePresentedFactIds,
686
+ availableTools,
687
+ calledProbes,
688
+ missedAnchorAttributions,
689
+ stopping: { rationale: input.stopping.rationale, unusedRelevantTools: stoppingUnusedTools },
690
+ usage: { modelCalls: usage.modelCalls, toolCalls: usage.toolCalls, inputTokens: usage.inputTokens, outputTokens: usage.outputTokens, latencyMs: usage.latencyMs, cache: { hits: usage.cache.hits, misses: usage.cache.misses }, cost: usage.cost, corrections: usage.corrections },
691
+ };
692
+ }
693
+
694
+ export function createAblationProbeTelemetry(
695
+ bundle: Readonly<AblationEvidenceClosureBundle>,
696
+ input: CreateAblationProbeTelemetryInput,
697
+ ): Readonly<AblationProbeTelemetry> {
698
+ validateAblationEvidenceClosureBundle(bundle);
699
+ const normalized = normalizeTelemetry(bundle, input);
700
+ return deepFreeze({ ...normalized, telemetryHash: telemetryHash(normalized) });
701
+ }
702
+
703
+ export function validateAblationProbeTelemetry(
704
+ bundle: Readonly<AblationEvidenceClosureBundle>,
705
+ value: unknown,
706
+ ): asserts value is Readonly<AblationProbeTelemetry> {
707
+ validateAblationEvidenceClosureBundle(bundle);
708
+ if (!isRecord(value) || Object.keys(value).some((key) => !TELEMETRY_KEYS.has(key))
709
+ || value.schemaVersion !== ABLATION_PROBE_TELEMETRY_VERSION || value.bundleHash !== bundle.bundleHash) {
710
+ throw new Error("Unsupported ablation probe telemetry");
711
+ }
712
+ assertDeepFrozen(value, "telemetry");
713
+ const { telemetryHash: suppliedHash, schemaVersion: _schemaVersion, bundleHash: _bundleHash, ...unhashed } = value as AblationProbeTelemetry;
714
+ void _schemaVersion;
715
+ void _bundleHash;
716
+ const normalized = normalizeTelemetry(bundle, unhashed);
717
+ const expectedHash = telemetryHash(normalized);
718
+ if (suppliedHash !== expectedHash || stableJson(value) !== stableJson({ ...normalized, telemetryHash: expectedHash })) {
719
+ throw new Error("Ablation probe telemetry integrity check failed");
720
+ }
721
+ }
722
+
723
+ /** The fixed category order makes reporting stable and prevents ad-hoc labels. */
724
+ export function precommittedProbeAttributionCategories(): readonly ProbeAttributionCategory[] {
725
+ return PRECOMMITTED_PROBE_ATTRIBUTION_CATEGORIES;
726
+ }