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

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 +24 -0
  5. package/README.md +141 -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 +72 -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,224 @@
1
+ import {
2
+ DEFAULT_CONCEPT_REGISTRY,
3
+ conceptDefinition,
4
+ type ConceptRegistry,
5
+ } from "./concept-registry";
6
+ import { stableHash } from "./stable";
7
+ import {
8
+ CHESS_REASONING_POLICY_VERSION,
9
+ CHESS_REASONING_SCHEMA_VERSION,
10
+ type IdeaGraph,
11
+ type IdentityState,
12
+ type ProofStrength,
13
+ type SemanticAttributes,
14
+ type SemanticEntity,
15
+ type SemanticParticipant,
16
+ type SemanticRelation,
17
+ type SemanticValue,
18
+ } from "./types";
19
+
20
+ const FALLBACK_TERMINAL_STATES = new Set(["converted", "completed", "refuted", "abandoned", "expired"]);
21
+
22
+ function cloneValue(value: SemanticValue): SemanticValue {
23
+ if (Array.isArray(value)) return value.map(cloneValue);
24
+ if (value && typeof value === "object") {
25
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneValue(item)]));
26
+ }
27
+ return value;
28
+ }
29
+
30
+ function cloneAttributes(attributes: SemanticAttributes): SemanticAttributes {
31
+ return Object.fromEntries(Object.entries(attributes).map(([key, value]) => [key, cloneValue(value)]));
32
+ }
33
+
34
+ function cloneParticipants(participants: SemanticParticipant[]): SemanticParticipant[] {
35
+ return participants.map((item) => ({ role: item.role, ref: { ...item.ref } }));
36
+ }
37
+
38
+ export type CreateSemanticEntityInput = {
39
+ id?: string;
40
+ branchId: string;
41
+ entityType: string;
42
+ concepts: string[];
43
+ owner?: SemanticEntity["owner"];
44
+ createdByPly: number;
45
+ createdByUci: string;
46
+ participants?: SemanticParticipant[];
47
+ attributes?: SemanticAttributes;
48
+ proofStrength?: ProofStrength | null;
49
+ state?: string;
50
+ evidence?: string[];
51
+ };
52
+
53
+ export function createSemanticEntity(
54
+ input: CreateSemanticEntityInput,
55
+ registry: ConceptRegistry = DEFAULT_CONCEPT_REGISTRY,
56
+ ): SemanticEntity {
57
+ if (!input.concepts.length) throw new Error("A semantic entity must reference at least one registered concept");
58
+ for (const concept of input.concepts) {
59
+ if (!conceptDefinition(registry, concept)) throw new Error(`Unknown concept key: ${concept}`);
60
+ }
61
+ const concepts = [...new Set(input.concepts)].sort();
62
+ const participants = cloneParticipants(input.participants || []).sort((left, right) =>
63
+ `${left.role}:${left.ref.domain}:${left.ref.id}`.localeCompare(`${right.role}:${right.ref.domain}:${right.ref.id}`));
64
+ const attributes = cloneAttributes(input.attributes || {});
65
+ const evidence = [...new Set(input.evidence || [])].sort();
66
+ const primary = conceptDefinition(registry, concepts[0]);
67
+ const state = input.state || primary?.lifecycle?.initialState || "proposed";
68
+ const identity = {
69
+ branchId: input.branchId,
70
+ entityType: input.entityType,
71
+ concepts,
72
+ owner: input.owner ?? null,
73
+ createdByPly: input.createdByPly,
74
+ createdByUci: input.createdByUci,
75
+ participants,
76
+ attributes,
77
+ };
78
+ const id = input.id || `entity:${input.branchId}:${input.createdByPly}:${stableHash(identity)}`;
79
+ return {
80
+ schemaVersion: CHESS_REASONING_SCHEMA_VERSION,
81
+ id,
82
+ ...identity,
83
+ proofStrength: input.proofStrength ?? null,
84
+ state,
85
+ evidence,
86
+ transitions: [{
87
+ ply: input.createdByPly,
88
+ uci: input.createdByUci,
89
+ state,
90
+ annotation: state,
91
+ evidence,
92
+ }],
93
+ };
94
+ }
95
+
96
+ export type CreateSemanticRelationInput = {
97
+ id?: string;
98
+ branchId: string;
99
+ relationType: string;
100
+ from: string;
101
+ to: string;
102
+ createdAtPly: number;
103
+ attributes?: SemanticAttributes;
104
+ evidence?: string[];
105
+ };
106
+
107
+ export function createSemanticRelation(input: CreateSemanticRelationInput): SemanticRelation {
108
+ const attributes = cloneAttributes(input.attributes || {});
109
+ const evidence = [...new Set(input.evidence || [])].sort();
110
+ const identity = {
111
+ branchId: input.branchId,
112
+ relationType: input.relationType,
113
+ from: input.from,
114
+ to: input.to,
115
+ createdAtPly: input.createdAtPly,
116
+ attributes,
117
+ };
118
+ return {
119
+ id: input.id || `relation:${input.branchId}:${stableHash(identity)}`,
120
+ ...identity,
121
+ evidence,
122
+ };
123
+ }
124
+
125
+ export function createIdeaGraph(branchId = "main", ply = 0): IdeaGraph {
126
+ const seed = { branchId, ply, entities: [], relations: [], candidateOutcomes: [] };
127
+ return {
128
+ schemaVersion: CHESS_REASONING_SCHEMA_VERSION,
129
+ policyVersion: CHESS_REASONING_POLICY_VERSION,
130
+ branchId,
131
+ ply,
132
+ inputHash: stableHash(seed),
133
+ entities: [],
134
+ relations: [],
135
+ candidateOutcomes: [],
136
+ };
137
+ }
138
+
139
+ export function extendIdeaGraph(
140
+ graph: IdeaGraph,
141
+ entities: SemanticEntity[],
142
+ relations: SemanticRelation[] = [],
143
+ ply = graph.ply,
144
+ ): IdeaGraph {
145
+ const entityMap = new Map(graph.entities.map((entity) => [entity.id, entity]));
146
+ for (const entity of entities) {
147
+ if (entity.branchId !== graph.branchId) throw new Error(`Entity ${entity.id} belongs to another branch`);
148
+ const existing = entityMap.get(entity.id);
149
+ if (existing && stableHash(existing) !== stableHash(entity)) throw new Error(`Conflicting semantic entity ID: ${entity.id}`);
150
+ entityMap.set(entity.id, entity);
151
+ }
152
+ const allEntities = [...entityMap.values()].sort((left, right) => left.id.localeCompare(right.id));
153
+ const ids = new Set(allEntities.map((entity) => entity.id));
154
+ const relationMap = new Map(graph.relations.map((relation) => [relation.id, relation]));
155
+ for (const relation of relations) {
156
+ if (relation.branchId !== graph.branchId) throw new Error(`Relation ${relation.id} belongs to another branch`);
157
+ if (!ids.has(relation.from) || !ids.has(relation.to)) {
158
+ throw new Error(`Relation ${relation.id} references an unknown semantic entity`);
159
+ }
160
+ const existing = relationMap.get(relation.id);
161
+ if (existing && stableHash(existing) !== stableHash(relation)) throw new Error(`Conflicting semantic relation ID: ${relation.id}`);
162
+ relationMap.set(relation.id, relation);
163
+ }
164
+ const allRelations = [...relationMap.values()].sort((left, right) => left.id.localeCompare(right.id));
165
+ const copy = {
166
+ ...graph,
167
+ ply,
168
+ entities: allEntities,
169
+ relations: allRelations,
170
+ candidateOutcomes: graph.candidateOutcomes.map((outcome) => ({ ...outcome, evidence: [...outcome.evidence] })),
171
+ };
172
+ return { ...copy, inputHash: stableHash({ previous: graph.inputHash, ...copy, inputHash: undefined }) };
173
+ }
174
+
175
+ export function forkIdeaGraph(graph: IdeaGraph, branchId: string): IdeaGraph {
176
+ const idMap = new Map(graph.entities.map((entity) => [
177
+ entity.id,
178
+ `entity:${branchId}:${entity.createdByPly}:${stableHash({ source: entity.id, branchId })}`,
179
+ ]));
180
+ const entities = graph.entities.map((entity) => ({
181
+ ...entity,
182
+ id: idMap.get(entity.id) as string,
183
+ branchId,
184
+ concepts: [...entity.concepts],
185
+ participants: cloneParticipants(entity.participants),
186
+ attributes: cloneAttributes(entity.attributes),
187
+ evidence: [...entity.evidence],
188
+ transitions: entity.transitions.map((transition) => ({ ...transition, evidence: [...transition.evidence] })),
189
+ }));
190
+ const relations = graph.relations.map((relation) => createSemanticRelation({
191
+ branchId,
192
+ relationType: relation.relationType,
193
+ from: idMap.get(relation.from) as string,
194
+ to: idMap.get(relation.to) as string,
195
+ createdAtPly: relation.createdAtPly,
196
+ attributes: relation.attributes,
197
+ evidence: relation.evidence,
198
+ }));
199
+ const copy = {
200
+ ...graph,
201
+ branchId,
202
+ entities,
203
+ relations,
204
+ candidateOutcomes: graph.candidateOutcomes.map((outcome) => ({ ...outcome, evidence: [...outcome.evidence] })),
205
+ };
206
+ return { ...copy, inputHash: stableHash({ source: graph.inputHash, branchId, entities, relations }) };
207
+ }
208
+
209
+ export function graphIdentityCompatible(identity: IdentityState, graph: IdeaGraph): boolean {
210
+ return identity.branchId === graph.branchId && identity.ply === graph.ply;
211
+ }
212
+
213
+ export function isTerminalEntity(
214
+ entity: SemanticEntity,
215
+ registry: ConceptRegistry = DEFAULT_CONCEPT_REGISTRY,
216
+ ): boolean {
217
+ const registeredTerminal = entity.concepts.some((concept) =>
218
+ conceptDefinition(registry, concept)?.lifecycle?.terminalStates.includes(entity.state));
219
+ return registeredTerminal || FALLBACK_TERMINAL_STATES.has(entity.state);
220
+ }
221
+
222
+ export function openEntityCount(graph: IdeaGraph, registry: ConceptRegistry = DEFAULT_CONCEPT_REGISTRY): number {
223
+ return graph.entities.filter((entity) => !isTerminalEntity(entity, registry)).length;
224
+ }
@@ -0,0 +1,115 @@
1
+ import type { AnswerScope, ReviewSubject } from './question-routing';
2
+ import type { ReasoningEvidenceRecord } from './poc-contracts';
3
+ import { Chess } from 'chess.js';
4
+ import type { RecordedMainLine } from './answer-host-contracts';
5
+ import type { PlayerPerspective } from '../player-perspective';
6
+ import type { ReasoningPositionContext, ReasoningToolName } from './poc-contracts';
7
+ import { recordedMainLineResult } from './recorded-main-line';
8
+ import { recordedPositionContext } from './recorded-position-tools';
9
+ import { matchedRootComparison } from './matched-comparison';
10
+ import type { PositionAnalysisResult } from './poc-tools';
11
+
12
+ export type InitialSearch = {tool:ReasoningToolName;input:Record<string,unknown>;reason:string;playedMove?:string};
13
+ export type InitialSearchPlan = {version:'initial-search-plan/1';requests:InitialSearch[];limitations:string[];classificationReassessment?:boolean;reviewScope?:{subject:ReviewSubject;resolvedSide:'white'|'black'|'both'|'unknown'}};
14
+ const normalize=(san:string)=>san.replace(/[+#?!]+$/,'');
15
+ const uci=(m:{from:string;to:string;promotion?:string})=>m.from+m.to+(m.promotion??'');
16
+ /** Recognition is deliberately narrow: unresolved language stays with the answering model. */
17
+ export function planInitialSearches(input:{question:string;context:ReasoningPositionContext;mainLine?:RecordedMainLine;reviewGame?:boolean;reviewSubject?:ReviewSubject;omitSelectedContext?:boolean;playerPerspective?:PlayerPerspective|null;answerScope?:AnswerScope;positionPreparation?:boolean;classificationQuestion?:boolean}):InitialSearchPlan {
18
+ const {question:q,context,mainLine}=input;
19
+ const result:InitialSearchPlan={version:'initial-search-plan/1',requests:[],limitations:[]};
20
+ const record=mainLine?recordedMainLineResult(mainLine,context):undefined;
21
+ if(input.classificationQuestion && !input.omitSelectedContext && context.kind!=='position') {
22
+ result.classificationReassessment=true;
23
+ result.requests.push({tool:'explore_continuations',input:{phase:'before',depth:18,multiPv:3,minimumPvPlies:10},reason:'Reassess the selected move classification using a depth-18 unrestricted before-position search. Preserve the original decision and refresh the canonical label afterwards.'});
24
+ }
25
+ const names=[...q.matchAll(/(?<![A-Za-z0-9])(?:O-O(?:-O)?|[KQRBN][a-h]?[1-8]?x?[a-h][1-8](?:=[QRBN])?|[a-h]x?[a-h]?[1-8](?:=[QRBN])?)[+#]?(?![A-Za-z0-9])/g)].map(m=>normalize(m[0]));
26
+ const alternative=/\b(better|instead|alternative|compare|rather)\b|\bwhat (?:if|about)\b|\b(?:could|should)\b.*\bplay\b/i.test(q);
27
+ const negated=/\b(?:don't|do not|no need to|without)\b.*\b(?:compare|analy[sz]e|instead|alternative)\b/i.test(q);
28
+ if(names.length && names.length<=6 && alternative && !negated){
29
+ let fen:string|undefined,phase:'before'|'after'='before',ply:number|undefined,played:string|undefined;
30
+ const numbered=[...q.matchAll(/\b(\d+)\s*(\.{1,3}|…)\s*(?=[KQRBNa-hO])/g)];
31
+ const unboundNumber=/\b\d+\b/.test(q.replace(/\b(\d+)\s*(\.{1,3}|…)\s*(?=[KQRBNa-hO])/g,''));
32
+ if(numbered.length && !unboundNumber && record && mainLine){
33
+ const roots=numbered.map(m=>record.recordedMainLine.moves.flatMap((move,i)=>move.moveNumber===Number(m[1])&&move.color===(m[2]==='.'?'w':'b')?[i]:[]));
34
+ if(roots.every(r=>r.length===1&&r[0]===roots[0][0])){ply=roots[0][0];fen=recordedPositionContext(mainLine,ply).fenAfter;played=mainLine.moves[ply]?.uci;}
35
+ }else if(!numbered.length && !/\b\d+\b/.test(q) && !input.omitSelectedContext && !/\b(later|earlier|previous|opening|endgame|middlegame|move\s+\d+)\b/i.test(q)){
36
+ phase=/\b(next|reply|respond)\b/i.test(q)?'after':'before';
37
+ fen=phase==='after'?context.fenAfter:context.fenBefore;
38
+ played=phase==='before'&&context.kind!=='position'?context.moveUci:undefined;
39
+ // Without an explicit role cue, legal in both roots is genuinely ambiguous.
40
+ const explicitRole=/\b(instead|alternative|rather|than|next|reply|respond)\b/i.test(q);
41
+ if(!explicitRole && context.fenBefore!==context.fenAfter){const other=new Chess(context.fenAfter).moves().map(normalize);if(names.every(n=>other.includes(n)))fen=undefined;}
42
+ }
43
+ if(fen){
44
+ const legal=new Chess(fen).moves({verbose:true});
45
+ const resolved=names.map(n=>legal.filter(m=>normalize(m.san)===n));
46
+ if(resolved.every(ms=>ms.length===1)){
47
+ const roots=[...new Set([...resolved.map(ms=>uci(ms[0])),...(played?[played]:[])])];
48
+ if(roots.length && roots.length<=6 && (!played||roots.some(r=>r!==played))){
49
+ result.requests.push({tool:ply===undefined?'compare_moves':'analyze_game_position',input:{...(ply===undefined?{phase,movesUci:roots}:{ply,rootMoves:roots}),depth:18,minimumPvPlies:10},reason:'Explicit named legal alternatives at one resolved root; compare with the recorded/selected move when applicable.'});
50
+ return result;
51
+ }
52
+ }
53
+ }
54
+ result.limitations.push('Named moves were not unambiguously resolved for automatic search; the model may choose the root and query.');
55
+ }
56
+ // The actual current-position assessment starts with the same unrestricted
57
+ // search available to the answering model. Do not search unrelated board
58
+ // state for general theory, omitted selections or retrospective reviews.
59
+ if(input.positionPreparation!==false && !input.omitSelectedContext && !input.reviewGame
60
+ && (input.answerScope==='position_analysis' || (!input.answerScope && context.kind==='position'))
61
+ && !names.length) {
62
+ try {
63
+ const phase=context.kind==='position'?'before':'after';
64
+ const fen=phase==='before'?context.fenBefore:context.fenAfter;
65
+ if(!new Chess(fen).isGameOver())result.requests.push({tool:'explore_continuations',
66
+ input:{phase,depth:18,multiPv:3,minimumPvPlies:24},
67
+ reason:'Initial current-position candidates and continuations. Reuse this evidence and investigate additional relevant plans with the available tools; these returned moves are not exhaustive.'});
68
+ } catch {result.limitations.push('Current-position preparation unavailable: invalid position.');}
69
+ return result;
70
+ }
71
+ if(!record || !mainLine || !input.reviewGame || !input.omitSelectedContext)return result;
72
+ if(names.length || /\b(?:endgame|middlegame|late|last|after|before)\b|\bmove\s+\d+/i.test(q)){result.limitations.push('Specific move or phase boundary requires model-directed investigation.');return result;}
73
+ const subject=input.reviewSubject??'unspecified';
74
+ const relative=subject==='self'||subject==='opponent';
75
+ const playerSide=input.playerPerspective?.side;
76
+ const side=subject==='white'||subject==='black'?subject:relative&&playerSide
77
+ ?subject==='self'?playerSide:playerSide==='white'?'black':'white':undefined;
78
+ result.reviewScope={subject,resolvedSide:side??(relative?'unknown':'both')};
79
+ if(relative&&!side){result.limitations.push('Player side unknown; defer personal/opponent review preparation to the answering model.');return result;}
80
+ const opening=/\bopening\b/i.test(q);
81
+ if(opening && mainLine.startFen!==new Chess().fen()){result.limitations.push('Opening boundary unknown for a custom starting position.');return result;}
82
+ if(opening)result.limitations.push('Preparation uses the first 12 full moves as an opening-review heuristic, not a claim that the opening ends there.');
83
+ const eligible=record.recordedAnalysis.rows.filter(row=>{
84
+ const move=record.recordedMainLine.moves[row[0]-1];
85
+ return (!side||move.color===(side==='white'?'w':'b'))&&(!opening||move.moveNumber<=12);
86
+ });
87
+ // All major classifications qualify, including missed faster mates with zero cp loss.
88
+ const rank=(row:typeof eligible[number])=>row[8]??(row[9]!==null&&row[10]!==null?Math.max(0,Math.abs(row[10])-Math.abs(row[9])):0);
89
+ const ordered=(rows:typeof eligible)=>rows.sort((a,b)=>rank(b)-rank(a)||a[0]-b[0]);
90
+ const major=ordered(eligible.filter(row=>['blunder','mistake','miss'].includes(row[5])));
91
+ const optional=major.length<10?ordered(eligible.filter(row=>row[5]==='dubious')).slice(0,Math.min(5,10-major.length)):[];
92
+ for(const row of [...major,...optional]) {
93
+ const score=row[9]!==null||row[10]!==null?`mate before ${row[9]??'not found'}, mate after ${row[10]??'not found'}`:row[8]!==null?`saved search delta ${row[8]}cp`:'no comparable centipawn loss';
94
+ result.requests.push({tool:'analyze_game_position',input:{ply:row[0]-1,multiPv:3,depth:18,minimumPvPlies:10},playedMove:mainLine.moves[row[0]-1].uci,reason:`Review candidate at recorded ply ${row[0]}: saved ${row[5]}, ${score} at depth ${row[2]}. Discover strongest alternatives and ensure a matched comparison with the played move; classification is historical.`});
95
+ }
96
+ if(!result.requests.length)result.limitations.push('No matching saved Blunder, Mistake, Miss or Dubious moves; model may investigate other useful moments.');
97
+ return result;
98
+ }
99
+
100
+ /** Use the same recorded-position comparison tool the model uses. Discovery may
101
+ * already include played+best at the same depth; otherwise compare explicitly. */
102
+ export function preparedMoveComparison(request:InitialSearch,evidence:ReasoningEvidenceRecord[]):InitialSearch|undefined {
103
+ if(!request.playedMove || request.input.rootMoves) return;
104
+ for(const record of evidence) {
105
+ const data=record.data as Record<string,unknown>,details=data?.details as Record<string,unknown>|undefined;
106
+ const lines=details?.lines as Array<{rootMove?:string;depth?:number}>|undefined;
107
+ if(!Array.isArray(lines)||!lines.length)continue;
108
+ const roots=lines.flatMap(l=>typeof l.rootMove==='string'?[l.rootMove]:[]);
109
+ if(!roots.length)continue;
110
+ const scope=details?.searchScope as Record<string,unknown>|undefined;
111
+ if(roots.includes(request.playedMove)&&scope&&typeof scope.fen==='string'&&matchedRootComparison({...details,command:scope.command,cacheProvenance:scope.cacheProvenance,sourceCacheProvenance:scope.sourceCacheProvenance} as unknown as PositionAnalysisResult,scope.fen,roots).status==='matched')return;
112
+ const rootMoves=[...new Set([roots[0],...roots.slice(1,3),request.playedMove])];
113
+ return {tool:'analyze_game_position',input:{ply:request.input.ply,rootMoves,depth:18,minimumPvPlies:10},reason:`Matched comparison of played ${request.playedMove} and discovered strongest alternatives at recorded root ${request.input.ply}. Use these within-search differences for relative move claims.`};
114
+ }
115
+ }
@@ -0,0 +1,135 @@
1
+ import type { ModelMessage } from 'ai';
2
+
3
+ export const EVIDENCE_SHARING_VERSION = 'evidence-sharing/1';
4
+ export const EVIDENCE_SHARING_INSTRUCTIONS = `Evidence encoding evidence-sharing/1 is lossless and self-contained. Read content with each {"$shared":"sN"} replaced by values.sN in the SAME envelope; dictionary values can themselves contain these references. Then interpret existing engine/metadata encodings normally. All values are already supplied: no tool is needed to expand them. References share identical values only, never authority, position scope, scores or search provenance. Dictionary IDs are not evidence IDs or citations.`;
5
+ // Keep the server-only prose tree-shakeable from the browser serializer. A test
6
+ // binds this sizing allowance to the server instruction's current length.
7
+ export const EVIDENCE_SHARING_GUIDANCE_CHARS = 490;
8
+ type Obj = Record<string, unknown>;
9
+ const object = (x: unknown): x is Obj => x !== null && typeof x === 'object' && !Array.isArray(x);
10
+
11
+ /** Exact JSON-value DAG. Unlike the older metadata pass, definitions are also
12
+ * interned recursively, so repeated children of different shared parents survive
13
+ * only once. Literal strings are counted after object sharing, since repeated
14
+ * object subtrees otherwise exaggerate their actual delivered frequency. No
15
+ * cross-message state or chess inference is involved. */
16
+ export function shareEvidence(value: unknown): unknown {
17
+ // A caller-owned envelope is already encoded; never nest/reinterpret it.
18
+ if (object(value) && value.encoding === EVIDENCE_SHARING_VERSION) return value;
19
+ const counts = new Map<string, number>();
20
+ const keys = new WeakMap<object, string>();
21
+ let collision = false;
22
+ const count = (x: unknown) => {
23
+ if (!x || typeof x !== 'object') return;
24
+ if (Object.hasOwn(x, '$shared')) collision = true;
25
+ const key = JSON.stringify(x);
26
+ keys.set(x, key);
27
+ if (key.length >= 96) counts.set(key, (counts.get(key) ?? 0) + 1);
28
+ for (const child of Object.values(x)) count(child);
29
+ };
30
+ count(value);
31
+ if (collision) return value;
32
+ const values: Obj = {}, ids = new Map<string, string>();
33
+ const children = (x: unknown): unknown => Array.isArray(x) ? x.map(visit)
34
+ : object(x) ? Object.fromEntries(Object.entries(x).map(([k, v]) => [k, visit(v)])) : x;
35
+ const visit = (x: unknown): unknown => {
36
+ if (!x || typeof x !== 'object') return x;
37
+ const key = keys.get(x)!;
38
+ if ((counts.get(key) ?? 0) < 2) return children(x);
39
+ let id = ids.get(key);
40
+ if (!id) {
41
+ id = `s${ids.size}`; ids.set(key, id);
42
+ values[id] = children(x);
43
+ }
44
+ return { $shared: id };
45
+ };
46
+ let content = children(value);
47
+ const stringCounts = new Map<string, number>();
48
+ const objectDefinitions = Object.entries(values);
49
+ const countStrings = (x: unknown): void => {
50
+ if (typeof x === 'string') { stringCounts.set(x, (stringCounts.get(x) ?? 0) + 1); return; }
51
+ if (Array.isArray(x)) { x.forEach(countStrings); return; }
52
+ if (object(x) && !Object.hasOwn(x, '$shared')) Object.values(x).forEach(countStrings);
53
+ };
54
+ countStrings(content);
55
+ for (const [, definition] of objectDefinitions) countStrings(definition);
56
+
57
+ const stringIds = new Map<string, string>();
58
+ const shareStrings = (x: unknown): unknown => {
59
+ if (typeof x === 'string') {
60
+ const count = stringCounts.get(x) ?? 0;
61
+ let id = stringIds.get(x);
62
+ if (!id && count >= 2) {
63
+ const nextId = `s${ids.size + stringIds.size}`;
64
+ const literalLength = JSON.stringify(x).length;
65
+ const pointerLength = JSON.stringify({ $shared: nextId }).length;
66
+ const definitionLength = JSON.stringify(nextId).length + 1 + literalLength + 1;
67
+ if (count * literalLength > count * pointerLength + definitionLength) {
68
+ id = nextId; stringIds.set(x, id);
69
+ values[id] = x;
70
+ }
71
+ }
72
+ return id ? { $shared: id } : x;
73
+ }
74
+ if (Array.isArray(x)) return x.map(shareStrings);
75
+ if (object(x) && !Object.hasOwn(x, '$shared')) return Object.fromEntries(Object.entries(x).map(([k, v]) => [k, shareStrings(v)]));
76
+ return x;
77
+ };
78
+ content = shareStrings(content);
79
+ // Iterate a snapshot: new string definitions must remain literal values.
80
+ for (const [id, definition] of objectDefinitions) values[id] = shareStrings(definition);
81
+ const result = { encoding: EVIDENCE_SHARING_VERSION, values, content };
82
+ // Pay for the decoding guidance too; small packets stay unchanged.
83
+ return (ids.size || stringIds.size) && JSON.stringify(result).length + EVIDENCE_SHARING_GUIDANCE_CHARS + 128 < JSON.stringify(value).length ? result : value;
84
+ }
85
+
86
+ /** Both SDK and hosted-provider message shapes contain JSON strings. Detect an
87
+ * actual envelope, never an incidental mention of the codec in ordinary prose. */
88
+ export function hasSharedEvidence(value: unknown): boolean {
89
+ if (typeof value === 'string') {
90
+ if (!value.startsWith('{') || !value.includes(EVIDENCE_SHARING_VERSION)) return false;
91
+ try { return hasSharedEvidence(JSON.parse(value)); } catch { return false; }
92
+ }
93
+ if (Array.isArray(value)) return value.some(hasSharedEvidence);
94
+ if (!object(value)) return false;
95
+ if (value.encoding === EVIDENCE_SHARING_VERSION && object(value.values) && Object.hasOwn(value, 'content')) return true;
96
+ return Object.values(value).some(hasSharedEvidence);
97
+ }
98
+
99
+ /** Strict verifier; malformed/dangling/cyclic dictionaries never decode silently. */
100
+ export function expandSharedEvidence(value: unknown): unknown {
101
+ if (!object(value) || value.encoding !== EVIDENCE_SHARING_VERSION) return value;
102
+ if (!object(value.values) || !Object.hasOwn(value, 'content')) throw Error('invalid-evidence-sharing');
103
+ const values = value.values, active = new Set<string>(), expanded = new Map<string, unknown>();
104
+ const visit = (x: unknown): unknown => {
105
+ if (Array.isArray(x)) return x.map(visit);
106
+ if (!object(x)) return x;
107
+ if (Object.hasOwn(x, '$shared')) {
108
+ const id = x.$shared;
109
+ if (Object.keys(x).length !== 1 || typeof id !== 'string' || !Object.hasOwn(values, id) || active.has(id)) throw Error('invalid-evidence-sharing-reference');
110
+ if (!expanded.has(id)) { active.add(id); expanded.set(id, visit(values[id])); active.delete(id); }
111
+ return expanded.get(id);
112
+ }
113
+ return Object.fromEntries(Object.entries(x).map(([k, v]) => [k, visit(v)]));
114
+ };
115
+ return visit(value.content);
116
+ }
117
+
118
+ /** Serialize a copy at the model boundary, retaining native tool identity and
119
+ * opaque assistant/provider continuation objects without modification. */
120
+ export function shareEvidenceMessages(messages: ModelMessage[]): ModelMessage[] {
121
+ return messages.map(message => {
122
+ if (message.role === 'user' && typeof message.content === 'string') {
123
+ let data: unknown;
124
+ try { data = JSON.parse(message.content); } catch { return message; }
125
+ if (!object(data) || data.scope !== 'CURRENT_QUESTION') return message;
126
+ const shared = shareEvidence(data);
127
+ return shared === data ? message : { ...message, content: JSON.stringify(shared) };
128
+ }
129
+ if (message.role === 'tool') return { ...message, content: message.content.map(part => {
130
+ if (part.type !== 'tool-result' || part.output.type !== 'json') return part;
131
+ return { ...part, output: { ...part.output, value: shareEvidence(part.output.value) as typeof part.output.value } };
132
+ }) };
133
+ return message;
134
+ });
135
+ }
@@ -0,0 +1,40 @@
1
+ import { Chess } from 'chess.js';
2
+ import type { PositionAnalysisResult } from './poc-tools';
3
+
4
+ export type MatchedComparison = {
5
+ status: 'matched' | 'unmatched';
6
+ actualDepth: number | null;
7
+ rootMoves: string[];
8
+ reason: string;
9
+ };
10
+
11
+ /** One retained engine response establishes search identity; candidate caches do not. */
12
+ export function matchedRootComparison(result: PositionAnalysisResult, fen: string, requiredRoots: readonly string[] = []): MatchedComparison {
13
+ const lines = Array.isArray(result.lines) ? result.lines : [];
14
+ const rootMoves = lines.flatMap(line => line && typeof line.rootMove === 'string' ? [line.rootMove] : []);
15
+ const unmatched = (reason: string): MatchedComparison => ({status:'unmatched',actualDepth:null,rootMoves,reason});
16
+ if ((result.sourceCacheProvenance ?? result.cacheProvenance) !== 'engine') return unmatched('Common engine search identity is unavailable.');
17
+ if (rootMoves.length !== lines.length) return unmatched('A compared root is malformed.');
18
+ if (lines.length < 2 || new Set(rootMoves).size !== rootMoves.length || requiredRoots.some(root => !rootMoves.includes(root))) return unmatched('The search does not cover all compared moves.');
19
+ try {
20
+ const legal = new Set(new Chess(fen).moves({verbose:true}).map(move => move.from + move.to + (move.promotion || '')));
21
+ if (rootMoves.some(root => !legal.has(root))) return unmatched('A compared move is not legal at this root.');
22
+ } catch { return unmatched('The comparison root is invalid.'); }
23
+ const depth = lines[0].depth, perspective = fen.split(' ')[1] === 'w' ? 'white' : fen.split(' ')[1] === 'b' ? 'black' : null;
24
+ if (!perspective || !Number.isSafeInteger(depth) || depth < 1 || lines.some(line => line.depth !== depth)) return unmatched('The moves have no common achieved depth.');
25
+ for (const line of lines) {
26
+ if (line.scoreScope?.fen !== fen || line.scoreScope.perspective !== perspective || line.scoreScope.positiveMeans !== 'advantage for perspective color') return unmatched('The score roots or perspectives differ.');
27
+ if (line.matePly == null ? !Number.isFinite(line.scoreCp) || Math.abs(line.scoreCp!) >= 100000 : !Number.isFinite(line.matePly)) return unmatched('A compared score is unavailable.');
28
+ if (line.matePly == null ? line.scoreScope.scoreCpUnit !== 'centipawns' : line.scoreScope.matePlyUnit !== 'signed mate-in-moves (legacy field name, not plies)') return unmatched('The score units are unknown.');
29
+ const origin = line.scoreProvenance;
30
+ if (origin && (origin.kind !== 'root-search' || origin.fen !== fen || origin.originatingDepth !== depth)) return unmatched('A score is not a matching root-search measurement.');
31
+ // Old direct engine responses have known single-dispatch identity even when
32
+ // they predate per-line origin metadata. A cache of saved candidates does not.
33
+ if (origin?.engineBuild != null && origin.engineBuild !== result.engineBuild) return unmatched('The engine builds differ.');
34
+ if (origin?.command != null && result.command && origin.command !== result.command) return unmatched('The search commands differ.');
35
+ }
36
+ if (new Set(lines.flatMap(line => typeof line.scoreProvenance?.command === 'string' ? [line.scoreProvenance.command] : [])).size > 1) return unmatched('The search commands differ.');
37
+ return {status:'matched',actualDepth:depth,rootMoves,reason:'These root scores came from one engine search at a common achieved depth.'};
38
+ }
39
+
40
+ export const MATCHED_COMPARISON_GUIDANCE = `For an explicit claim that one move is better than another, use and cite a matched comparison covering both moves at the same exact root, from one engine search at a common achieved depth. searchScope.comparison identifies matched or unmatched results. A completed engine comparison can be reused. Stored board alternatives are historical observations; their greater reported depth does not supersede a matched comparison or establish a precise relative advantage. Reuse initial preparation when its matched search already covers both choices. Otherwise request compare_moves (or the appropriate recorded-position comparison) while investigation is open. Never subtract separately cached candidate scores to support a better-than claim. If a matched comparison is unavailable or searches disagree, explain the supported chess mechanism and qualify the ranking; do not claim an absolute ordering or search again merely to resolve small cross-depth differences.`;
@@ -0,0 +1,55 @@
1
+ import { Chess } from 'chess.js';
2
+
3
+ const pieces = { p: 'pawn', n: 'knight', b: 'bishop', r: 'rook', q: 'queen' } as const;
4
+ type Counts = Record<typeof pieces[keyof typeof pieces], number>;
5
+ const empty = (): Counts => ({ pawn: 0, knight: 0, bishop: 0, rook: 0, queen: 0 });
6
+ const difference = (a: Counts, b: Counts): Counts => Object.fromEntries(
7
+ Object.values(pieces).map(name => [name, a[name] - b[name]]),
8
+ ) as Counts;
9
+
10
+ /** Absolute counts, independent of turn, engine evaluation or starting inventory. */
11
+ export function materialAtPosition(board: Chess) {
12
+ const white = empty(), black = empty();
13
+ for (const piece of board.board().flat()) {
14
+ if (piece && piece.type !== 'k') (piece.color === 'w' ? white : black)[pieces[piece.type]]++;
15
+ }
16
+ const whiteMinusBlack = difference(white, black);
17
+ const extras = (sign: number) => Object.entries(whiteMinusBlack)
18
+ .filter(([, n]) => n * sign > 0)
19
+ .map(([name, n]) => `${Math.abs(n)} ${name}${Math.abs(n) === 1 ? '' : 's'}`);
20
+ const w = extras(1), b = extras(-1);
21
+ return { white, black, whiteMinusBlack, balance: !w.length && !b.length
22
+ ? 'Equal piece counts.'
23
+ : [w.length ? `White has ${w.join(' and ')} more than Black` : '',
24
+ b.length ? `Black has ${b.join(' and ')} more than White` : ''].filter(Boolean).join('; ') + '.' };
25
+ }
26
+
27
+ /** Count by legal replay; never report a partial replay as a complete endpoint. */
28
+ export function materialAlongLine(startFen: string, movesUci: readonly string[]) {
29
+ const board = new Chess(startFen), start = materialAtPosition(board);
30
+ const changes: { ply: number; san: string; whiteMinusBlack: Counts; balance: string }[] = [];
31
+ for (const [index, uci] of movesUci.entries()) {
32
+ if (!/^[a-h][1-8][a-h][1-8][qrbn]?$/.test(uci)) throw Error('Invalid material replay move');
33
+ const move = board.move({ from: uci.slice(0, 2), to: uci.slice(2, 4), promotion: uci.slice(4) || undefined });
34
+ if (move.captured || move.promotion) {
35
+ const { whiteMinusBlack, balance } = materialAtPosition(board);
36
+ changes.push({ ply: index + 1, san: move.san, whiteMinusBlack, balance });
37
+ }
38
+ }
39
+ const end = materialAtPosition(board);
40
+ return { authority: 'legal-replay-piece-counts', startFen, plies: movesUci.length,
41
+ start, changes, endFen: board.fen(), end,
42
+ balanceChangeFromStart: difference(end.whiteMinusBlack, start.whiteMinusBlack),
43
+ scope: 'Absolute piece counts at the stated positions; balanceChangeFromStart is only a change, not the final balance. Conditional line, not proof of force, safety or compensation.' };
44
+ }
45
+
46
+ export function materialForVariations<T extends { rootMove: string; pv: string[] }>(startFen: string, lines: readonly T[]) {
47
+ return lines.map(line => {
48
+ try {
49
+ if (!line.pv.length || line.rootMove !== line.pv[0]) throw Error('Missing root');
50
+ return { ...line, material: materialAlongLine(startFen, line.pv) };
51
+ } catch {
52
+ return { ...line, material: { status: 'unavailable', reason: 'The complete variation could not be legally replayed from its stated root.' } };
53
+ }
54
+ });
55
+ }
@@ -0,0 +1,47 @@
1
+ /** Presentation only: the legally computed canonical material record is immutable. */
2
+ export type MaterialFormat = 'raw' | 'hybrid' | 'text';
3
+ export const DEFAULT_MATERIAL_FORMAT: MaterialFormat = 'hybrid';
4
+ const order = ['pawn', 'knight', 'bishop', 'rook', 'queen'] as const;
5
+ type PieceCounts = Record<typeof order[number], number>;
6
+ interface MaterialSnapshot { white: PieceCounts; black: PieceCounts; whiteMinusBlack: PieceCounts; balance: string }
7
+ const obj = (v: unknown): v is Record<string, unknown> => !!v && typeof v === 'object' && !Array.isArray(v);
8
+ const keys = (v: Record<string, unknown>, expected: string[]) => Object.keys(v).every(k => expected.includes(k));
9
+ const counts = (v: unknown): v is Record<typeof order[number], number> => obj(v)
10
+ && keys(v, [...order]) && order.every(k => Number.isInteger(v[k]));
11
+ const snapshot = (v: unknown): v is MaterialSnapshot => obj(v)
12
+ && keys(v, ['white','black','whiteMinusBlack','balance'])
13
+ && counts(v.white) && counts(v.black) && counts(v.whiteMinusBlack) && typeof v.balance === 'string';
14
+ const row = (v: Record<string, number>) => order.map(k => v[k]);
15
+ const inventory = (v: Record<string, number>) => order.map(k => `${v[k]} ${k}${v[k] === 1 ? '' : 's'}`).join(', ');
16
+ const textSnapshot = (v: MaterialSnapshot) => `White: ${inventory(v.white)}. Black: ${inventory(v.black)}. ${v.balance}`;
17
+ const hybridSnapshot = (v: MaterialSnapshot) => ({white:row(v.white),black:row(v.black),balance:v.balance});
18
+
19
+ function formattedMaterial(v: unknown, format: Exclude<MaterialFormat,'raw'>): unknown {
20
+ if (snapshot(v)) return format === 'text' ? {encoding:'material-text/1',position:textSnapshot(v)}
21
+ : {encoding:'material-table/1',pieceOrder:order.join(' '),...hybridSnapshot(v)};
22
+ if (!obj(v) || v.authority !== 'legal-replay-piece-counts'
23
+ || !keys(v,['authority','startFen','plies','start','changes','endFen','end','balanceChangeFromStart','scope'])
24
+ || typeof v.startFen !== 'string' || typeof v.endFen !== 'string' || !Number.isInteger(v.plies)
25
+ || !snapshot(v.start) || !snapshot(v.end) || !counts(v.balanceChangeFromStart) || typeof v.scope !== 'string'
26
+ || !Array.isArray(v.changes) || v.changes.some(c => !obj(c) || !keys(c,['ply','san','whiteMinusBlack','balance'])
27
+ || !Number.isInteger(c.ply) || typeof c.san !== 'string' || !counts(c.whiteMinusBlack) || typeof c.balance !== 'string')) return v;
28
+ const balanceChangeFromStart = v.balanceChangeFromStart;
29
+ const shared = {authority:v.authority,startFen:v.startFen,plies:v.plies,endFen:v.endFen,scope:v.scope};
30
+ if (format === 'hybrid') return {encoding:'material-table/1',...shared,pieceOrder:order.join(' '),
31
+ start:hybridSnapshot(v.start),
32
+ checkpointColumns:'ply san pawn knight bishop rook queen; absolute White minus Black',
33
+ checkpoints:v.changes.map(c => [c.ply,c.san,...row(c.whiteMinusBlack)]),
34
+ end:hybridSnapshot(v.end),balanceChangeFromStart:row(v.balanceChangeFromStart)};
35
+ return {encoding:'material-text/1',...shared,convention:'Balances list all differences; unmentioned piece types have equal counts.',start:textSnapshot(v.start),
36
+ checkpoints:Object.fromEntries(v.changes.map(c => [`After ply ${c.ply} (${c.san})`,c.balance])),
37
+ end:textSnapshot(v.end),
38
+ balanceChangeFromStart:`Change only, White minus Black: ${order.map(k=>`${k} ${balanceChangeFromStart[k] >= 0 ? '+' : ''}${balanceChangeFromStart[k]}`).join(', ')}.`};
39
+ }
40
+
41
+ export function projectMaterial(value: unknown, format: MaterialFormat = DEFAULT_MATERIAL_FORMAT): unknown {
42
+ if (!['raw','hybrid','text'].includes(format)) throw Error('invalid-material-format');
43
+ if (format === 'raw') return value;
44
+ const visit = (v: unknown): unknown => Array.isArray(v) ? v.map(visit) : obj(v)
45
+ ? Object.fromEntries(Object.entries(v).map(([k,x]) => [k,k === 'material' ? formattedMaterial(x,format) : visit(x)])) : v;
46
+ return visit(value);
47
+ }