@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,759 @@
1
+ import { DEFAULT_CONCEPT_REGISTRY, PARTICIPANT_ROLES, REFERENCE_DOMAINS, type ConceptRegistry } from "./concept-registry";
2
+ import {
3
+ REASONING_POC_VERSION,
4
+ type ReasoningEvidenceRecord,
5
+ type ReasoningHypothesisDecision,
6
+ type ReasoningHypothesisProposal,
7
+ type ReasoningHypothesisRecord,
8
+ type ReasoningHypothesisProvenance,
9
+ type ReasoningHypothesisRelationRecord,
10
+ type ReasoningPositionContext,
11
+ type ReasoningRankedObservationSet,
12
+ type ReasoningToolDescriptor,
13
+ } from "./poc-contracts";
14
+ import { collectReasoningMoveWitnesses, reasoningEvidenceReplayStartPhase } from "./poc-move-witnesses";
15
+ import { createPocIdentityTimeline, squareForPiece } from "./poc-piece-refs";
16
+ import { stableHash } from "./stable";
17
+ import { effectiveLicensesForRecord, licensesBoundedRouteRelevance, licensesSemanticChessGrounding, withEvidenceLicenses } from "./evidence-licenses";
18
+ import type { ProofStrength, SemanticAttributes, SemanticParticipant, SemanticValue } from "./types";
19
+
20
+ const PROOF_RANK: Record<ProofStrength, number> = {
21
+ observed: 1,
22
+ supported: 2,
23
+ forced: 3,
24
+ };
25
+
26
+ function isRouteRelevanceEvidence(record: ReasoningEvidenceRecord | undefined): boolean {
27
+ return Boolean(record
28
+ && record.proofStrength !== null
29
+ && licensesBoundedRouteRelevance(effectiveLicensesForRecord(record)));
30
+ }
31
+
32
+ function hasEvidenceForProofStrength(
33
+ record: Pick<ReasoningHypothesisRecord, "evidenceIds" | "proofStrength">,
34
+ evidence: Map<string, ReasoningEvidenceRecord>,
35
+ ): boolean {
36
+ return record.proofStrength === null || record.evidenceIds.some((id) => {
37
+ const strength = evidence.get(id)?.proofStrength;
38
+ return Boolean(strength && PROOF_RANK[strength] >= PROOF_RANK[record.proofStrength!]);
39
+ });
40
+ }
41
+
42
+ export const RECORD_HYPOTHESES_DESCRIPTOR: ReasoningToolDescriptor = {
43
+ name: "record_hypotheses",
44
+ category: "graph",
45
+ description: "Record model-originated semantic hypotheses and causal relations in the provisional Hypothesis Ledger. Hypotheses may cite observations or be novel with zero source observations; novel concrete claims still need evidence before acceptance. parentHypothesisIds means refinement lineage only, never causal order: normally use [] and express enables/supports/explains order in relations. A true refinement may name an earlier ledger ID or same-batch localId and must preserve parent owner/concepts/anchors or include explicit refinementReason and removedAnchorReasons attributes.",
46
+ inputSchema: {
47
+ type: "object",
48
+ required: ["hypotheses", "relations"],
49
+ properties: {
50
+ hypotheses: {
51
+ type: "array",
52
+ minItems: 1,
53
+ maxItems: 16,
54
+ items: {
55
+ type: "object",
56
+ required: ["localId", "entityType", "concepts", "owner", "participants", "attributes", "proofStrength", "evidenceIds", "sourceObservationIds", "parentHypothesisIds", "anchorRequirements"],
57
+ properties: {
58
+ localId: { type: "string", maxLength: 160 },
59
+ entityType: { type: "string", maxLength: 160 },
60
+ concepts: { type: "array", minItems: 1, maxItems: 4, items: { type: "string", maxLength: 160 } },
61
+ owner: { type: ["string", "null"], enum: ["w", "b", null] },
62
+ participants: participantArraySchema(12),
63
+ attributes: { type: "object", maxProperties: 24, propertyNames: { type: "string", minLength: 1, maxLength: 80 }, additionalProperties: true },
64
+ proofStrength: { type: ["string", "null"], enum: ["observed", "supported", "forced", null] },
65
+ evidenceIds: { type: "array", maxItems: 12, items: { type: "string", maxLength: 200 } },
66
+ sourceObservationIds: { type: "array", maxItems: 12, items: { type: "string", maxLength: 200 } },
67
+ parentHypothesisIds: { type: "array", maxItems: 8, description: "Refinement lineage only, not causal order. Usually []. True refinements may use existing hypothesis IDs or same-batch localIds and must satisfy refinement compatibility.", items: { type: "string", maxLength: 200 } },
68
+ anchorRequirements: {
69
+ type: "array",
70
+ maxItems: 12,
71
+ items: {
72
+ type: "object",
73
+ required: ["participant", "disposition"],
74
+ properties: { participant: participantSchema(), disposition: { type: "string", enum: ["required"] } },
75
+ additionalProperties: false,
76
+ },
77
+ },
78
+ },
79
+ additionalProperties: false,
80
+ },
81
+ },
82
+ relations: {
83
+ type: "array",
84
+ maxItems: 24,
85
+ items: {
86
+ type: "object",
87
+ required: ["relationType", "from", "to", "attributes", "evidenceIds", "sourceObservationIds"],
88
+ properties: {
89
+ relationType: { type: "string", maxLength: 160 },
90
+ from: { type: "string", maxLength: 200 },
91
+ to: { type: "string", maxLength: 200 },
92
+ attributes: { type: "object", maxProperties: 24, propertyNames: { type: "string", minLength: 1, maxLength: 80 }, additionalProperties: true },
93
+ evidenceIds: { type: "array", minItems: 1, maxItems: 12, items: { type: "string", maxLength: 200 } },
94
+ sourceObservationIds: { type: "array", maxItems: 12, items: { type: "string", maxLength: 200 } },
95
+ },
96
+ additionalProperties: false,
97
+ },
98
+ },
99
+ },
100
+ additionalProperties: false,
101
+ },
102
+ };
103
+
104
+ export const REVIEW_HYPOTHESES_DESCRIPTOR: ReasoningToolDescriptor = {
105
+ name: "review_hypotheses",
106
+ category: "graph",
107
+ description: "Record terminal acceptance, rejection, or supersession for model-created hypotheses, or temporarily mark a hypothesis needs-probe. Resolve every independent critic finding explicitly; a later good hypothesis cannot make an objection to an earlier hypothesis disappear. A finding resolution must agree with the hypothesis decision and exact supersession targets. Evidence cited on rejected, superseded, or safe-to-omit resolutions is audit context only and never strengthens the discarded or optional hypothesis. Reject paraphrase duplicates covered by another source-linked record while keeping every used observation linked to at least one accepted hypothesis. Accepted means admissible, not required in the answer. A terminal batch that would orphan a used observation is rejected atomically and may be corrected once. Every recorded hypothesis and critic finding must be terminal before explain_plan.",
108
+ inputSchema: {
109
+ type: "object",
110
+ required: ["decisions", "criticFindingResolutions"],
111
+ properties: {
112
+ decisions: {
113
+ type: "array",
114
+ minItems: 1,
115
+ maxItems: 16,
116
+ items: {
117
+ type: "object",
118
+ required: ["hypothesisId", "disposition", "rationale", "supersededBy"],
119
+ properties: {
120
+ hypothesisId: { type: "string", maxLength: 200 },
121
+ disposition: { type: "string", enum: ["accept", "reject", "needs-probe", "supersede"] },
122
+ rationale: { type: "string", minLength: 1, maxLength: 500 },
123
+ supersededBy: { type: "array", maxItems: 8, items: { type: "string", maxLength: 200 } },
124
+ },
125
+ additionalProperties: false,
126
+ },
127
+ },
128
+ criticFindingResolutions: {
129
+ type: "array",
130
+ maxItems: 16,
131
+ items: {
132
+ type: "object",
133
+ required: ["findingId", "hypothesisId", "disposition", "rationale", "evidenceIds", "supersededBy", "resolvedByHypothesisIds"],
134
+ properties: {
135
+ findingId: { type: "string", maxLength: 200 },
136
+ hypothesisId: { type: "string", maxLength: 200 },
137
+ disposition: { type: "string", enum: ["rejected", "superseded", "retained-with-new-evidence", "retained-safe-to-omit", "resolved-by-associated-hypotheses", "resolved-existing-evidence"] },
138
+ rationale: { type: "string", minLength: 1, maxLength: 800 },
139
+ evidenceIds: { type: "array", maxItems: 12, items: { type: "string", maxLength: 200 } },
140
+ supersededBy: { type: "array", maxItems: 8, items: { type: "string", maxLength: 200 } },
141
+ resolvedByHypothesisIds: { type: "array", maxItems: 8, uniqueItems: true, items: { type: "string", maxLength: 200 } },
142
+ },
143
+ additionalProperties: false,
144
+ },
145
+ },
146
+ },
147
+ additionalProperties: false,
148
+ },
149
+ };
150
+
151
+ export const EXPLAIN_PLAN_SELECTION_DESCRIPTOR: ReasoningToolDescriptor = {
152
+ name: "explain_plan",
153
+ category: "graph",
154
+ description: "Materialize only accepted Hypothesis Ledger records and select a bounded explanation graph with one connected primary chain plus any required side components. Pass ledger IDs, not a second copy of semantic entities.",
155
+ inputSchema: {
156
+ type: "object",
157
+ required: ["hypothesisIds", "relationIds", "startHypothesisId", "goalHypothesisId"],
158
+ properties: {
159
+ hypothesisIds: { type: "array", minItems: 1, maxItems: 12, items: { type: "string", maxLength: 200 } },
160
+ relationIds: { type: "array", maxItems: 20, items: { type: "string", maxLength: 200 } },
161
+ startHypothesisId: { type: "string", maxLength: 200 },
162
+ goalHypothesisId: { type: "string", maxLength: 200 },
163
+ requiredHypothesisIds: { type: "array", maxItems: 10, items: { type: "string", maxLength: 200 } },
164
+ maxEntities: { type: "integer", minimum: 1, maximum: 12 },
165
+ },
166
+ additionalProperties: false,
167
+ },
168
+ };
169
+
170
+ function participantSchema() {
171
+ return {
172
+ type: "object",
173
+ required: ["role", "ref"],
174
+ properties: {
175
+ role: { type: "string", maxLength: 160 },
176
+ ref: {
177
+ type: "object",
178
+ required: ["domain", "id"],
179
+ properties: { domain: { type: "string", maxLength: 160 }, id: { type: "string", maxLength: 200 } },
180
+ additionalProperties: false,
181
+ },
182
+ },
183
+ additionalProperties: false,
184
+ };
185
+ }
186
+
187
+ function participantArraySchema(maxItems: number) {
188
+ return { type: "array", maxItems, items: participantSchema() };
189
+ }
190
+
191
+ function isRecord(value: unknown): value is Record<string, unknown> {
192
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
193
+ }
194
+
195
+ function boundedString(value: unknown, label: string, max = 200): string {
196
+ if (typeof value !== "string" || !value.trim() || value.length > max) throw new Error(`${label} must be a non-empty string of at most ${max} characters`);
197
+ return value.trim();
198
+ }
199
+
200
+ function strings(value: unknown, label: string, max: number, allowEmpty = true): string[] {
201
+ if (!Array.isArray(value) || (!allowEmpty && !value.length) || value.length > max) throw new Error(`${label} has an invalid number of values`);
202
+ return [...new Set(value.map((item, index) => boundedString(item, `${label}[${index}]`)))];
203
+ }
204
+
205
+ const FORBIDDEN_KEYS = new Set(["isFork", "isSkewer", "isMainLine"]);
206
+
207
+ function semanticValue(value: unknown, label: string, depth = 0): SemanticValue {
208
+ if (depth > 5) throw new Error(`${label} exceeds the semantic-value depth limit`);
209
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
210
+ if (typeof value === "number" && Number.isFinite(value)) return value;
211
+ if (Array.isArray(value)) {
212
+ if (value.length > 24) throw new Error(`${label} exceeds the semantic-value item limit`);
213
+ return value.map((item, index) => semanticValue(item, `${label}[${index}]`, depth + 1));
214
+ }
215
+ if (!isRecord(value)) throw new Error(`${label} is not a JSON semantic value`);
216
+ if (Object.keys(value).length > 24) throw new Error(`${label} exceeds the semantic-value field limit`);
217
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => {
218
+ if (FORBIDDEN_KEYS.has(key)) throw new Error(`${key} must be a registry value, not a semantic flag`);
219
+ return [boundedString(key, `${label} key`, 80), semanticValue(item, `${label}.${key}`, depth + 1)];
220
+ }));
221
+ }
222
+
223
+ function attributes(value: unknown, label: string): SemanticAttributes {
224
+ if (value == null) return {};
225
+ const parsed = semanticValue(value, label);
226
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") throw new Error(`${label} must be an object`);
227
+ return parsed;
228
+ }
229
+
230
+ function participantValue(value: unknown, label: string): SemanticParticipant {
231
+ if (!isRecord(value) || !isRecord(value.ref)) throw new Error(`${label} is malformed`);
232
+ return { role: boundedString(value.role, `${label}.role`, 160), ref: { domain: boundedString(value.ref.domain, `${label}.ref.domain`, 160), id: boundedString(value.ref.id, `${label}.ref.id`, 200) } };
233
+ }
234
+
235
+ function participantValues(value: unknown, label: string): SemanticParticipant[] {
236
+ if (!Array.isArray(value) || value.length > 12) throw new Error(`${label} must contain at most 12 participants`);
237
+ return value.map((item, index) => participantValue(item, `${label}[${index}]`));
238
+ }
239
+
240
+ function proof(value: unknown): ProofStrength | null {
241
+ if (value == null) return null;
242
+ if (value === "observed" || value === "supported" || value === "forced") return value;
243
+ throw new Error("proofStrength must be observed, supported, forced, or null");
244
+ }
245
+
246
+ function participantKey(value: SemanticParticipant): string {
247
+ return `${value.role}:${value.ref.domain}:${value.ref.id}`;
248
+ }
249
+
250
+ type ConceptAcceptanceValidator = (
251
+ hypothesis: ReasoningHypothesisRecord,
252
+ records: ReasoningEvidenceRecord[],
253
+ ) => string[];
254
+
255
+ const CONCEPT_ACCEPTANCE_VALIDATORS: Readonly<Record<string, ConceptAcceptanceValidator>> = Object.freeze({
256
+ "material-offer-liability/1": (hypothesis, records) => {
257
+ const contradictedCapture = records.find((record) => {
258
+ if (record.kind !== "fact" || record.data.kind !== "move.legal_recaptures_after") return false;
259
+ const data = isRecord(record.data.data) ? record.data.data : null;
260
+ return data?.targetPieceColor === hypothesis.owner
261
+ && data.allCapturesLoseAfterImmediateSameSquareRecapture === true;
262
+ });
263
+ const contradictedData = contradictedCapture && isRecord(contradictedCapture.data.data)
264
+ ? contradictedCapture.data.data
265
+ : null;
266
+ const captureMoves = Array.isArray(contradictedData?.moves)
267
+ ? contradictedData.moves.flatMap((move) => isRecord(move) && typeof move.uci === "string" ? [move.uci] : [])
268
+ : [];
269
+ const strongerBoundedEvidence = captureMoves.length > 0 && records.some((record) => {
270
+ if (record.kind !== "probe" || (record.proofStrength !== "supported" && record.proofStrength !== "forced")) return false;
271
+ const roots = Array.isArray(record.data.rootMoves) ? record.data.rootMoves.filter((move): move is string => typeof move === "string") : [];
272
+ return captureMoves.every((move) => roots.includes(move));
273
+ });
274
+ return contradictedCapture && !strongerBoundedEvidence
275
+ ? [`Hypothesis ${hypothesis.id} calls the owner's moved piece a material offer even though cited exhaustive immediate-capture evidence shows every capture can be answered on the same square with a negative nominal balance delta for the capturing side (${contradictedCapture.id})`]
276
+ : [];
277
+ },
278
+ });
279
+
280
+ /**
281
+ * Pure policy-only validation shared by every semantic admission boundary.
282
+ * It does not alter Ledger status, review provenance, or hypothesis meaning.
283
+ */
284
+ export function validateHypothesisAcceptancePolicies(
285
+ hypothesis: ReasoningHypothesisRecord,
286
+ evidence: Map<string, ReasoningEvidenceRecord>,
287
+ registry: ConceptRegistry = DEFAULT_CONCEPT_REGISTRY,
288
+ ): string[] {
289
+ const records = hypothesis.evidenceIds.map((id) => evidence.get(id)).filter((record): record is ReasoningEvidenceRecord => Boolean(record));
290
+ return hypothesis.concepts.flatMap((concept) => {
291
+ const policy = registry[concept]?.acceptancePolicy;
292
+ if (!policy) return [];
293
+ const validator = CONCEPT_ACCEPTANCE_VALIDATORS[policy];
294
+ return validator
295
+ ? validator(hypothesis, records)
296
+ : [`Hypothesis ${hypothesis.id} uses concept ${concept} with unavailable acceptance policy ${policy}`];
297
+ });
298
+ }
299
+
300
+ export type ExplainPlanSelection = {
301
+ hypothesisIds: string[];
302
+ relationIds: string[];
303
+ startHypothesisId: string;
304
+ goalHypothesisId: string;
305
+ requiredHypothesisIds: string[];
306
+ maxEntities: number;
307
+ };
308
+
309
+ export type InternalExplainPlanRequest = {
310
+ hypothesisLedgerRevision: string;
311
+ hypotheses: {
312
+ entities: Array<ReasoningHypothesisProposal & { hypothesisId: string }>;
313
+ relations: Array<{
314
+ relationType: string;
315
+ from: string;
316
+ to: string;
317
+ attributes: SemanticAttributes;
318
+ evidenceIds: string[];
319
+ hypothesisRelationId: string;
320
+ }>;
321
+ };
322
+ startEntityId: string;
323
+ goalEntityId: string;
324
+ requiredEntityIds: string[];
325
+ maxEntities: number;
326
+ };
327
+
328
+ export class HypothesisLedger {
329
+ private readonly records = new Map<string, ReasoningHypothesisRecord>();
330
+ private readonly relations = new Map<string, ReasoningHypothesisRelationRecord>();
331
+ private readonly observations: ReasoningRankedObservationSet;
332
+
333
+ constructor(
334
+ private readonly contextHash: string,
335
+ observations: ReasoningRankedObservationSet,
336
+ private readonly evidence: Map<string, ReasoningEvidenceRecord>,
337
+ private readonly maxHypotheses: number,
338
+ private readonly context?: ReasoningPositionContext,
339
+ private readonly registry: ConceptRegistry = DEFAULT_CONCEPT_REGISTRY,
340
+ ) {
341
+ this.observations = observations;
342
+ }
343
+
344
+ allRecords(): ReasoningHypothesisRecord[] {
345
+ return [...this.records.values()].map((record) => ({ ...record, participants: record.participants.map((item) => ({ ...item, ref: { ...item.ref } })) }));
346
+ }
347
+
348
+ allRelations(): ReasoningHypothesisRelationRecord[] {
349
+ return [...this.relations.values()].map((record) => ({ ...record }));
350
+ }
351
+
352
+ proposalRevision(): string {
353
+ const records = [...this.records.values()].map(({ status: _status, review: _review, evidenceIds: _evidenceIds, ...record }) => {
354
+ void _status;
355
+ void _review;
356
+ // Terminal review may attach evidence that resolves a critic finding.
357
+ // Observation dispositions describe which semantic proposals they map
358
+ // to, so strengthening an otherwise unchanged proposal must not make
359
+ // those dispositions stale. Membership/meaning changes still change
360
+ // this revision because proposal IDs and every semantic field remain.
361
+ void _evidenceIds;
362
+ return record;
363
+ });
364
+ return stableHash({ records, relations: [...this.relations.values()] });
365
+ }
366
+
367
+ finalRevision(): string {
368
+ return stableHash({ records: [...this.records.values()], relations: [...this.relations.values()] });
369
+ }
370
+
371
+ record(
372
+ value: unknown,
373
+ provenance: ReasoningHypothesisProvenance = {
374
+ provider: "knights-eye/model-runtime",
375
+ version: "unversioned",
376
+ origin: "model",
377
+ },
378
+ ): { records: ReasoningHypothesisRecord[]; relations: ReasoningHypothesisRelationRecord[]; localToHypothesisIds: Record<string, string> } {
379
+ if (!isRecord(value) || !Array.isArray(value.hypotheses) || !Array.isArray(value.relations)) throw new Error("record_hypotheses requires hypotheses and relations arrays");
380
+ if (!value.hypotheses.length) throw new Error("record_hypotheses cannot record an empty hypothesis batch");
381
+ if (value.hypotheses.length + this.records.size > this.maxHypotheses) throw new Error("Hypothesis Ledger budget exhausted");
382
+ const observationById = new Map(this.observations.observations.map((item) => [item.id, item]));
383
+ const proposals: ReasoningHypothesisProposal[] = value.hypotheses.map((item, index) => {
384
+ if (!isRecord(item)) throw new Error(`hypotheses[${index}] is malformed`);
385
+ const owner = item.owner == null ? null : item.owner;
386
+ if (owner !== null && owner !== "w" && owner !== "b") throw new Error(`hypotheses[${index}].owner must be w, b, or null`);
387
+ const concepts = strings(item.concepts, `hypotheses[${index}].concepts`, 4, false);
388
+ for (const concept of concepts) if (!this.registry[concept]) throw new Error(`Unknown concept key: ${concept}`);
389
+ const sourceObservationIds = strings(item.sourceObservationIds, `hypotheses[${index}].sourceObservationIds`, 12);
390
+ for (const id of sourceObservationIds) {
391
+ const observation = observationById.get(id);
392
+ if (!observation) throw new Error(`Unknown source observation: ${id}`);
393
+ if (!observation.presentedToModel) throw new Error(`Observation was not presented to the model: ${id}`);
394
+ }
395
+ const evidenceIds = strings(item.evidenceIds, `hypotheses[${index}].evidenceIds`, 12);
396
+ for (const id of evidenceIds) if (!this.evidence.has(id)) throw new Error(`Hypothesis references missing or stale evidence: ${id}`);
397
+ const parentHypothesisIds = strings(item.parentHypothesisIds, `hypotheses[${index}].parentHypothesisIds`, 8);
398
+ const participants = participantValues(item.participants, `hypotheses[${index}].participants`);
399
+ if (!Array.isArray(item.anchorRequirements) || item.anchorRequirements.length > 12) throw new Error(`hypotheses[${index}].anchorRequirements is invalid`);
400
+ const anchorRequirements = item.anchorRequirements.map((anchor, anchorIndex) => {
401
+ if (!isRecord(anchor) || anchor.disposition !== "required") throw new Error(`hypotheses[${index}].anchorRequirements[${anchorIndex}] is malformed`);
402
+ const required = participantValue(anchor.participant, `hypotheses[${index}].anchorRequirements[${anchorIndex}].participant`);
403
+ if (!participants.some((participant) => participantKey(participant) === participantKey(required))) throw new Error(`Required realization anchor is absent from hypothesis ${String(item.localId)}`);
404
+ return { participant: required, disposition: "required" as const };
405
+ });
406
+ return {
407
+ localId: boundedString(item.localId, `hypotheses[${index}].localId`, 160),
408
+ entityType: boundedString(item.entityType, `hypotheses[${index}].entityType`, 160),
409
+ concepts,
410
+ owner,
411
+ participants,
412
+ attributes: attributes(item.attributes, `hypotheses[${index}].attributes`),
413
+ proofStrength: proof(item.proofStrength),
414
+ evidenceIds,
415
+ sourceObservationIds,
416
+ parentHypothesisIds,
417
+ anchorRequirements,
418
+ };
419
+ });
420
+ const localIds = new Set(proposals.map((item) => item.localId));
421
+ if (localIds.size !== proposals.length) throw new Error("record_hypotheses localIds must be unique within the call");
422
+ for (const proposal of proposals) {
423
+ for (const id of proposal.parentHypothesisIds) {
424
+ if (!this.records.has(id) && !localIds.has(id)) throw new Error(`Unknown parent hypothesis: ${id}`);
425
+ if (id === proposal.localId) throw new Error(`Hypothesis ${proposal.localId} cannot be its own parent`);
426
+ }
427
+ }
428
+ const visitState = new Map<string, "visiting" | "visited">();
429
+ const proposalByLocalId = new Map(proposals.map((proposal) => [proposal.localId, proposal]));
430
+ const visit = (localId: string) => {
431
+ if (visitState.get(localId) === "visiting") throw new Error("Same-call hypothesis lineage must be acyclic");
432
+ if (visitState.get(localId) === "visited") return;
433
+ visitState.set(localId, "visiting");
434
+ for (const parentId of proposalByLocalId.get(localId)?.parentHypothesisIds || []) if (localIds.has(parentId)) visit(parentId);
435
+ visitState.set(localId, "visited");
436
+ };
437
+ for (const localId of localIds) visit(localId);
438
+ const localToHypothesisIds: Record<string, string> = Object.fromEntries(proposals.map((proposal) => [
439
+ proposal.localId,
440
+ `hypothesis:${stableHash({ contextHash: this.contextHash, proposal })}`,
441
+ ]));
442
+ for (const id of Object.values(localToHypothesisIds)) if (this.records.has(id)) throw new Error(`Hypothesis revision repeats an existing proposal: ${id}`);
443
+ const recorded = proposals.map((proposal) => {
444
+ const id = localToHypothesisIds[proposal.localId];
445
+ const parentHypothesisIds = proposal.parentHypothesisIds.map((parentId) => localToHypothesisIds[parentId] || parentId);
446
+ const record: ReasoningHypothesisRecord = {
447
+ ...proposal,
448
+ parentHypothesisIds,
449
+ schemaVersion: REASONING_POC_VERSION,
450
+ id,
451
+ contextHash: this.contextHash,
452
+ origin: provenance.origin,
453
+ provenance: { provider: provenance.provider, version: provenance.version },
454
+ status: "proposed",
455
+ review: null,
456
+ };
457
+ return record;
458
+ });
459
+ for (const record of recorded) assertDirectedDestinationAnchors(record, this.evidence, this.context);
460
+ const proofIssues = recorded
461
+ .filter((record) => !hasEvidenceForProofStrength(record, this.evidence))
462
+ .map((record) => `Hypothesis ${record.localId} (${record.id}) overstates available proof as ${record.proofStrength}`);
463
+ if (proofIssues.length) {
464
+ throw new Error(`Hypothesis batch rejected ${proofIssues.length} proof overstatement(s): ${proofIssues.join(" | ")}`);
465
+ }
466
+ const pendingRecordIds = new Set(recorded.map((record) => record.id));
467
+ const pendingRelationIds = new Set<string>();
468
+ const relations = value.relations.map((item, index) => {
469
+ if (!isRecord(item)) throw new Error(`relations[${index}] is malformed`);
470
+ const fromInput = boundedString(item.from, `relations[${index}].from`);
471
+ const toInput = boundedString(item.to, `relations[${index}].to`);
472
+ const fromHypothesisId = localToHypothesisIds[fromInput] || (this.records.has(fromInput) || pendingRecordIds.has(fromInput) ? fromInput : "");
473
+ const toHypothesisId = localToHypothesisIds[toInput] || (this.records.has(toInput) || pendingRecordIds.has(toInput) ? toInput : "");
474
+ if (!fromHypothesisId || !toHypothesisId) throw new Error(`relations[${index}] references an unknown hypothesis`);
475
+ const evidenceIds = strings(item.evidenceIds, `relations[${index}].evidenceIds`, 12);
476
+ if (!evidenceIds.length) throw new Error(`Relation hypothesis ${index} requires compatible evidence`);
477
+ for (const id of evidenceIds) if (!this.evidence.has(id)) throw new Error(`Relation references missing or stale evidence: ${id}`);
478
+ if (!evidenceIds.some((id) => {
479
+ const record = this.evidence.get(id);
480
+ return Boolean(record && licensesSemanticChessGrounding(effectiveLicensesForRecord(record)));
481
+ })) throw new Error(`Relation hypothesis ${index} lacks evidence licensed for semantic chess grounding`);
482
+ const sourceObservationIds = strings(item.sourceObservationIds, `relations[${index}].sourceObservationIds`, 12);
483
+ for (const id of sourceObservationIds) if (!observationById.has(id)) throw new Error(`Relation references unknown observation: ${id}`);
484
+ const relationType = boundedString(item.relationType, `relations[${index}].relationType`, 160);
485
+ const relation: ReasoningHypothesisRelationRecord = {
486
+ schemaVersion: REASONING_POC_VERSION,
487
+ id: `hypothesis-relation:${stableHash({ contextHash: this.contextHash, relationType, fromHypothesisId, toHypothesisId, attributes: item.attributes })}`,
488
+ contextHash: this.contextHash,
489
+ relationType,
490
+ fromHypothesisId,
491
+ toHypothesisId,
492
+ attributes: attributes(item.attributes, `relations[${index}].attributes`),
493
+ evidenceIds,
494
+ sourceObservationIds,
495
+ };
496
+ if (this.relations.has(relation.id) || pendingRelationIds.has(relation.id)) throw new Error(`Hypothesis revision repeats an existing relation: ${relation.id}`);
497
+ pendingRelationIds.add(relation.id);
498
+ return relation;
499
+ });
500
+ for (const record of recorded) this.records.set(record.id, record);
501
+ for (const relation of relations) this.relations.set(relation.id, relation);
502
+ return { records: recorded, relations, localToHypothesisIds };
503
+ }
504
+
505
+ review(
506
+ value: unknown,
507
+ validateProspective?: (records: ReasoningHypothesisRecord[]) => void,
508
+ supplementalEvidenceByHypothesisId: Map<string, string[]> = new Map(),
509
+ ): { decisions: ReasoningHypothesisDecision[]; evidence: ReasoningEvidenceRecord } {
510
+ if (!isRecord(value) || !Array.isArray(value.decisions) || !value.decisions.length || value.decisions.length > this.maxHypotheses) throw new Error("review_hypotheses requires decisions");
511
+ const observationById = new Map(this.observations.observations.map((item) => [item.id, item]));
512
+ const seen = new Set<string>();
513
+ const decisions = value.decisions.map((item, index): ReasoningHypothesisDecision => {
514
+ if (!isRecord(item)) throw new Error(`decisions[${index}] is malformed`);
515
+ const hypothesisId = boundedString(item.hypothesisId, `decisions[${index}].hypothesisId`);
516
+ if (seen.has(hypothesisId)) throw new Error(`Duplicate hypothesis decision: ${hypothesisId}`);
517
+ seen.add(hypothesisId);
518
+ const record = this.records.get(hypothesisId);
519
+ if (!record) throw new Error(`Unknown hypothesis: ${hypothesisId}`);
520
+ const disposition = item.disposition;
521
+ if (disposition !== "accept" && disposition !== "reject" && disposition !== "needs-probe" && disposition !== "supersede") throw new Error(`Invalid hypothesis disposition for ${hypothesisId}`);
522
+ const supersededBy = strings(item.supersededBy, `decisions[${index}].supersededBy`, 8);
523
+ if (disposition === "supersede") {
524
+ if (!supersededBy.length) throw new Error(`Superseded hypothesis ${hypothesisId} requires supersededBy`);
525
+ for (const childId of supersededBy) {
526
+ const child = this.records.get(childId);
527
+ if (!child || !child.parentHypothesisIds.includes(hypothesisId)) throw new Error(`Superseding hypothesis ${childId} must preserve lineage to ${hypothesisId}`);
528
+ }
529
+ } else if (supersededBy.length) {
530
+ throw new Error(`Only supersede decisions may include supersededBy`);
531
+ }
532
+ const decision: ReasoningHypothesisDecision = { hypothesisId, disposition, rationale: boundedString(item.rationale, `decisions[${index}].rationale`, 500), supersededBy };
533
+ return decision;
534
+ });
535
+ const acceptanceIssues: string[] = [];
536
+ for (const decision of decisions.filter((item) => item.disposition === "accept")) {
537
+ const storedRecord = this.records.get(decision.hypothesisId)!;
538
+ const record = {
539
+ ...storedRecord,
540
+ evidenceIds: [...new Set([...storedRecord.evidenceIds, ...(supplementalEvidenceByHypothesisId.get(storedRecord.id) || [])])],
541
+ };
542
+ try {
543
+ this.validateRealization(record);
544
+ } catch (error) {
545
+ acceptanceIssues.push(error instanceof Error ? error.message : `Hypothesis ${record.id} has invalid realization anchors`);
546
+ }
547
+ if (!record.evidenceIds.length) acceptanceIssues.push(`Accepted hypothesis ${record.id} requires compatible evidence`);
548
+ for (const id of record.evidenceIds) {
549
+ if (!this.evidence.has(id)) acceptanceIssues.push(`Accepted hypothesis ${record.id} references missing evidence ${id}`);
550
+ }
551
+ if (!record.evidenceIds.some((id) => {
552
+ const evidence = this.evidence.get(id);
553
+ return Boolean(evidence && licensesSemanticChessGrounding(effectiveLicensesForRecord(evidence)));
554
+ })) acceptanceIssues.push(`Accepted hypothesis ${record.id} lacks evidence licensed for semantic chess grounding`);
555
+ const usesGeneratedRouteAffordance = record.sourceObservationIds.some((id) =>
556
+ observationById.get(id)?.observationType === "conditional-route-affordance");
557
+ if (usesGeneratedRouteAffordance && !record.evidenceIds.some((id) => isRouteRelevanceEvidence(this.evidence.get(id)))) {
558
+ acceptanceIssues.push(`Accepted hypothesis ${record.id} promotes a generated conditional route without separate bounded relevance evidence`);
559
+ }
560
+ acceptanceIssues.push(...validateHypothesisAcceptancePolicies(record, this.evidence, this.registry));
561
+ if (!hasEvidenceForProofStrength(record, this.evidence)) acceptanceIssues.push(`Accepted hypothesis ${record.id} overstates available proof as ${record.proofStrength}`);
562
+ }
563
+ if (acceptanceIssues.length) throw new Error(`Hypothesis review rejected ${acceptanceIssues.length} acceptance issue(s): ${acceptanceIssues.join(" | ")}`);
564
+ const decisionsById = new Map(decisions.map((decision) => [decision.hypothesisId, decision]));
565
+ const prospectiveRecords = this.allRecords().map((record) => {
566
+ const decision = decisionsById.get(record.id);
567
+ const evidenceIds = [...new Set([...record.evidenceIds, ...(supplementalEvidenceByHypothesisId.get(record.id) || [])])];
568
+ if (!decision) return { ...record, evidenceIds };
569
+ return {
570
+ ...record,
571
+ evidenceIds,
572
+ status: decision.disposition === "accept"
573
+ ? "accepted" as const
574
+ : decision.disposition === "reject"
575
+ ? "rejected" as const
576
+ : decision.disposition === "supersede"
577
+ ? "superseded" as const
578
+ : "needs-probe" as const,
579
+ review: { rationale: decision.rationale, supersededBy: [...decision.supersededBy] },
580
+ };
581
+ });
582
+ validateProspective?.(prospectiveRecords);
583
+ for (const decision of decisions) {
584
+ const record = this.records.get(decision.hypothesisId)!;
585
+ record.evidenceIds = [...new Set([...record.evidenceIds, ...(supplementalEvidenceByHypothesisId.get(record.id) || [])])];
586
+ const { disposition, supersededBy } = decision;
587
+ record.status = disposition === "accept"
588
+ ? "accepted"
589
+ : disposition === "reject"
590
+ ? "rejected"
591
+ : disposition === "supersede"
592
+ ? "superseded"
593
+ : "needs-probe";
594
+ record.review = { rationale: decision.rationale, supersededBy };
595
+ }
596
+ const evidence: ReasoningEvidenceRecord = withEvidenceLicenses({
597
+ schemaVersion: REASONING_POC_VERSION,
598
+ id: `evidence:hypothesis-review:${stableHash({ contextHash: this.contextHash, decisions })}`,
599
+ contextHash: this.contextHash,
600
+ kind: "hypothesis-review",
601
+ source: "knights-eye/hypothesis-ledger-review",
602
+ proofStrength: null,
603
+ summary: `Recorded ${decisions.length} model hypothesis decision(s); review is semantic judgment, not chess proof.`,
604
+ data: { decisions },
605
+ coverage: {
606
+ complete: this.allRecords().every((record) => ["accepted", "rejected", "superseded"].includes(record.status)),
607
+ covered: this.allRecords().filter((record) => ["accepted", "rejected", "superseded"].includes(record.status)).map((record) => record.id),
608
+ unresolved: this.allRecords().filter((record) => !["accepted", "rejected", "superseded"].includes(record.status)).map((record) => record.id),
609
+ },
610
+ replay: { ledger: "hypothesis", contextHash: this.contextHash },
611
+ });
612
+ this.evidence.set(evidence.id, evidence);
613
+ return { decisions, evidence };
614
+ }
615
+
616
+ toInternalPlanRequest(value: unknown): InternalExplainPlanRequest {
617
+ const selection = this.parseSelection(value);
618
+ const unresolved = this.allRecords().filter((record) => record.status === "proposed" || record.status === "needs-probe");
619
+ if (unresolved.length) throw new Error(`Every recorded hypothesis requires a terminal decision before explain_plan; unresolved: ${unresolved.map((item) => item.id).join(", ")}`);
620
+ const selected = selection.hypothesisIds.map((id) => {
621
+ const record = this.records.get(id);
622
+ if (!record) throw new Error(`Unknown hypothesis selected for explain_plan: ${id}`);
623
+ if (record.status !== "accepted") throw new Error(`Only accepted hypotheses can enter explain_plan: ${id}`);
624
+ this.validateRealization(record);
625
+ return record;
626
+ });
627
+ const selectedIds = new Set(selected.map((record) => record.id));
628
+ const relations = selection.relationIds.map((id) => {
629
+ const relation = this.relations.get(id);
630
+ if (!relation) throw new Error(`Unknown hypothesis relation selected for explain_plan: ${id}`);
631
+ if (!selectedIds.has(relation.fromHypothesisId) || !selectedIds.has(relation.toHypothesisId)) throw new Error(`Selected relation ${id} leaves the selected hypothesis set`);
632
+ if (!relation.evidenceIds.some((evidenceId) => {
633
+ const record = this.evidence.get(evidenceId);
634
+ return Boolean(record && licensesSemanticChessGrounding(effectiveLicensesForRecord(record)));
635
+ })) throw new Error(`Selected relation ${id} lacks evidence licensed for semantic chess grounding`);
636
+ const routeEndpoint = [relation.fromHypothesisId, relation.toHypothesisId].some((hypothesisId) =>
637
+ this.records.get(hypothesisId)?.sourceObservationIds.some((observationId) =>
638
+ this.observations.observations.find((observation) => observation.id === observationId)?.observationType === "conditional-route-affordance"));
639
+ const routeRelevanceEvidenceIds = [
640
+ ...relation.evidenceIds,
641
+ ...[relation.fromHypothesisId, relation.toHypothesisId].flatMap((hypothesisId) => this.records.get(hypothesisId)?.evidenceIds || []),
642
+ ];
643
+ if (routeEndpoint && !routeRelevanceEvidenceIds.some((evidenceId) => isRouteRelevanceEvidence(this.evidence.get(evidenceId)))) {
644
+ throw new Error(`Selected relation ${id} promotes a generated conditional route without separate bounded relevance evidence`);
645
+ }
646
+ return relation;
647
+ });
648
+ for (const id of [selection.startHypothesisId, selection.goalHypothesisId, ...selection.requiredHypothesisIds]) if (!selectedIds.has(id)) throw new Error(`Selected endpoint ${id} is not in hypothesisIds`);
649
+ return {
650
+ hypothesisLedgerRevision: this.finalRevision(),
651
+ hypotheses: {
652
+ entities: selected.map((record) => ({
653
+ localId: record.id,
654
+ entityType: record.entityType,
655
+ concepts: record.concepts,
656
+ owner: record.owner,
657
+ participants: record.participants,
658
+ attributes: { ...record.attributes, sourceObservationIds: record.sourceObservationIds },
659
+ proofStrength: record.proofStrength,
660
+ evidenceIds: record.evidenceIds,
661
+ sourceObservationIds: record.sourceObservationIds,
662
+ parentHypothesisIds: record.parentHypothesisIds,
663
+ anchorRequirements: record.anchorRequirements,
664
+ hypothesisId: record.id,
665
+ })),
666
+ relations: relations.map((relation) => ({ relationType: relation.relationType, from: relation.fromHypothesisId, to: relation.toHypothesisId, attributes: relation.attributes, evidenceIds: relation.evidenceIds, hypothesisRelationId: relation.id })),
667
+ },
668
+ startEntityId: selection.startHypothesisId,
669
+ goalEntityId: selection.goalHypothesisId,
670
+ requiredEntityIds: selection.requiredHypothesisIds,
671
+ maxEntities: selection.maxEntities,
672
+ };
673
+ }
674
+
675
+ private parseSelection(value: unknown): ExplainPlanSelection {
676
+ if (!isRecord(value)) throw new Error("explain_plan requires a selection object");
677
+ const hypothesisIds = strings(value.hypothesisIds, "hypothesisIds", 12, false);
678
+ if (hypothesisIds.length < 1) throw new Error("explain_plan requires at least one hypothesisId");
679
+ const relationIds = strings(value.relationIds, "relationIds", 20);
680
+ return {
681
+ hypothesisIds,
682
+ relationIds,
683
+ startHypothesisId: boundedString(value.startHypothesisId, "startHypothesisId"),
684
+ goalHypothesisId: boundedString(value.goalHypothesisId, "goalHypothesisId"),
685
+ requiredHypothesisIds: strings(value.requiredHypothesisIds || [], "requiredHypothesisIds", 10),
686
+ maxEntities: value.maxEntities == null ? 8 : Number(value.maxEntities),
687
+ };
688
+ }
689
+
690
+ private validateRealization(record: ReasoningHypothesisRecord) {
691
+ for (const anchor of record.anchorRequirements) {
692
+ if (!record.participants.some((participant) => participantKey(participant) === participantKey(anchor.participant))) throw new Error(`Hypothesis ${record.id} lost required realization anchor ${participantKey(anchor.participant)}`);
693
+ }
694
+ for (const parentId of record.parentHypothesisIds) {
695
+ const parent = this.records.get(parentId);
696
+ if (!parent) throw new Error(`Hypothesis ${record.id} has missing parent ${parentId}`);
697
+ if (parent.owner && record.owner && parent.owner !== record.owner) throw new Error(`Hypothesis ${record.id} contradicts parent owner`);
698
+ const overlap = parent.concepts.some((concept) => record.concepts.includes(concept));
699
+ if (!overlap && typeof record.attributes.refinementReason !== "string") throw new Error(`Hypothesis ${record.id} changes parent concepts without an explicit refinementReason`);
700
+ const removedAnchorReasons = record.attributes.removedAnchorReasons;
701
+ for (const anchor of parent.anchorRequirements) {
702
+ const key = participantKey(anchor.participant);
703
+ if (record.participants.some((participant) => participantKey(participant) === key)) continue;
704
+ if (!isRecord(removedAnchorReasons) || typeof removedAnchorReasons[key] !== "string" || !removedAnchorReasons[key].trim()) {
705
+ throw new Error(`Hypothesis ${record.id} drops parent realization anchor ${key} without an explicit removedAnchorReasons entry`);
706
+ }
707
+ }
708
+ }
709
+ this.validateDirectedDestinationAnchors(record);
710
+ }
711
+
712
+ private validateDirectedDestinationAnchors(record: ReasoningHypothesisRecord) {
713
+ assertDirectedDestinationAnchors(record, this.evidence, this.context);
714
+ }
715
+ }
716
+
717
+ /**
718
+ * Actor/subject plus destination/goal is an exact typed movement assertion,
719
+ * even when the surrounding hypothesis remains provisional semantic meaning.
720
+ * Reject impossible bindings before they enter the Ledger so a later reviewer
721
+ * cannot accidentally promote a supporting piece into the mover's role.
722
+ */
723
+ export function assertDirectedDestinationAnchors(
724
+ record: Pick<ReasoningHypothesisRecord, "id" | "localId" | "participants" | "evidenceIds">,
725
+ evidenceById: Map<string, ReasoningEvidenceRecord>,
726
+ context: ReasoningPositionContext | undefined,
727
+ ) {
728
+ if (!context) return;
729
+ const actors = record.participants.filter((participant) =>
730
+ participant.ref.domain === REFERENCE_DOMAINS.PIECE
731
+ && (participant.role === PARTICIPANT_ROLES.ACTOR || participant.role === PARTICIPANT_ROLES.SUBJECT));
732
+ const destinations = record.participants.filter((participant) =>
733
+ participant.ref.domain === REFERENCE_DOMAINS.SQUARE
734
+ && (participant.role === PARTICIPANT_ROLES.GOAL || participant.role === PARTICIPANT_ROLES.DESTINATION));
735
+ if (!actors.length || !destinations.length) return;
736
+ const records = record.evidenceIds.flatMap((id) => {
737
+ const evidence = evidenceById.get(id);
738
+ return evidence ? [evidence] : [];
739
+ });
740
+ const witnesses = collectReasoningMoveWitnesses(records, context);
741
+ const timeline = createPocIdentityTimeline(context);
742
+ const grounded = (pieceId: string, square: string) => records.some((evidence) => {
743
+ const phase = reasoningEvidenceReplayStartPhase(evidence, context);
744
+ const state = phase === "before" ? timeline.before : phase === "after" ? timeline.after : null;
745
+ return state && squareForPiece(state, pieceId) === square;
746
+ }) || witnesses.some((witness) => witness.pieceId === pieceId && witness.uci.slice(2, 4) === square);
747
+ for (const actor of actors) {
748
+ if (!destinations.some((destination) => grounded(actor.ref.id, destination.ref.id))) {
749
+ const squares = destinations.map((destination) => destination.ref.id).join(", ");
750
+ throw new Error(`Hypothesis localId ${record.localId} (${record.id}) has an actor/destination anchor not grounded by cited legal evidence: actor ${actor.ref.id} does not reach goal/destination ${squares}`);
751
+ }
752
+ }
753
+ for (const destination of destinations) {
754
+ if (!actors.some((actor) => grounded(actor.ref.id, destination.ref.id))) {
755
+ const pieces = actors.map((actor) => actor.ref.id).join(", ");
756
+ throw new Error(`Hypothesis localId ${record.localId} (${record.id}) has a destination anchor without a grounded actor in cited legal evidence: goal/destination ${destination.ref.id} is not reached by ${pieces}`);
757
+ }
758
+ }
759
+ }