@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,256 @@
1
+ import { Chess, type Square } from 'chess.js';
2
+ import { historicalProseRoots, type RecordedReferenceLine } from './historical-prose';
3
+ import { simpleProseReferences } from './simple-prose-references';
4
+ import type { ReasoningEvidenceRecord, ReasoningPositionContext } from './poc-contracts';
5
+
6
+ export type CausalClaimCheck = {
7
+ claim: string;
8
+ /** Half-open UTF-16 offsets into the final reference-repaired answer. */
9
+ claimRange: { start: number; end: number };
10
+ sentenceContext: { text: string; start: number; end: number };
11
+ reference: string;
12
+ predicate: 'moving-piece-escape' | 'stationary-slider-ray' | 'square-control-release';
13
+ verdict: 'supported' | 'contradicted' | 'unresolved';
14
+ reason: string;
15
+ };
16
+
17
+ const names: Record<string, string> = { rook: 'r', bishop: 'b', queen: 'q', knight: 'n', pawn: 'p', king: 'k' };
18
+ const uci = /^[a-h][1-8][a-h][1-8][qrbn]?$/;
19
+ const pieceBinding = /\b(the|a|white(?:'s|’s)?|black(?:'s|’s)?|your)\s+(rook|bishop|queen|knight|pawn|king)\s+(?:on|at)\s+([a-h][1-8])\b/i;
20
+ const compactBinding = /\b([RBQNKP])([a-h][1-8])\b/;
21
+
22
+ /** Mask markup punctuation without changing string offsets (including surrogate pairs). */
23
+ function sentenceRanges(answer: string) {
24
+ const masked = answer.replace(/\[[^\]\n]+\]\([^\n)]*\)/g, text => ' '.repeat(text.length))
25
+ .replace(/\b\d+\.(?:\.\.)?/g, text => ' '.repeat(text.length));
26
+ const ranges: Array<{start:number;end:number}> = [];
27
+ let start = 0;
28
+ for (const boundary of masked.matchAll(/[.!?](?:[”"’']?)(?=\s|$)|\n+/g)) {
29
+ const end = boundary.index + boundary[0].length;
30
+ ranges.push({start,end});
31
+ start = end;
32
+ }
33
+ ranges.push({start,end:answer.length});
34
+ return ranges;
35
+ }
36
+
37
+ function trimRange(answer:string, start:number, end:number) {
38
+ while(start<end && /\s/.test(answer[start])) start++;
39
+ while(end>start && /\s/.test(answer[end-1])) end--;
40
+ return {start,end};
41
+ }
42
+
43
+ function binding(clause: string): {type:string;at:Square;color?:'w'|'b'}|null {
44
+ const all=[...clause.matchAll(new RegExp(pieceBinding.source,'gi'))];
45
+ const compact=[...clause.matchAll(new RegExp(compactBinding.source,'g'))];
46
+ const identities=[...all.map(match=>`${names[match[2].toLowerCase()]}:${match[3]}`),
47
+ ...compact.map(match=>`${match[1].toLowerCase()}:${match[2]}`)];
48
+ if(new Set(identities).size>1)return null;
49
+ const named=clause.match(pieceBinding);
50
+ if(named) return {type:names[named[2].toLowerCase()],at:named[3] as Square,
51
+ color:/^white/i.test(named[1])?'w':/^black/i.test(named[1])?'b':undefined};
52
+ return compact[0] ? {type:compact[0][1].toLowerCase(),at:compact[0][2] as Square} : null;
53
+ }
54
+
55
+ function direction(from:Square,to:Square):[number,number]|null {
56
+ const dx=to.charCodeAt(0)-from.charCodeAt(0),dy=Number(to[1])-Number(from[1]);
57
+ if(!dx && !dy || dx && dy && Math.abs(dx)!==Math.abs(dy))return null;
58
+ return [Math.sign(dx),Math.sign(dy)];
59
+ }
60
+
61
+ function ray(board:Chess,from:Square,to:Square):boolean {
62
+ const d=direction(from,to);
63
+ if(!d)return false;
64
+ let file=from.charCodeAt(0)+d[0],rank=Number(from[1])+d[1];
65
+ while(file!==to.charCodeAt(0)||rank!==Number(to[1])) {
66
+ if(board.get(`${String.fromCharCode(file)}${rank}` as Square))return false;
67
+ file+=d[0];rank+=d[1];
68
+ }
69
+ return true;
70
+ }
71
+
72
+ function parseTarget(target:string, historical:ReturnType<typeof historicalProseRoots>, label:string,
73
+ context:ReasoningPositionContext, records:readonly ReasoningEvidenceRecord[],recordedLine?:RecordedReferenceLine) {
74
+ const match=/^game:(0|[1-9]\d*):([a-h1-8qrbn,]+)$/.exec(target);
75
+ if(!match)return null;
76
+ const root=historical.roots.get(Number(match[1]));
77
+ if(!root)return null;
78
+ const path=match[2].split(',');
79
+ if(!path.length||path.length>32||path.some(move=>!uci.test(move)))return null;
80
+ try {
81
+ const before=new Chess(root.context.fenAfter);
82
+ const start=new Chess(before.fen());
83
+ let san='';
84
+ for(const move of path) {
85
+ const actual=before.move({from:move.slice(0,2),to:move.slice(2,4),promotion:move[4]});
86
+ if(actual.from+actual.to+(actual.promotion??'')!==move) return null;
87
+ san=actual.san;
88
+ }
89
+ const last=path.at(-1)!;
90
+ // A recorded path must reproduce the frozen record, not merely be legal.
91
+ const recorded=historical.recorded?.slice(Number(match[1]),Number(match[1])+path.length);
92
+ if(recorded?.length===path.length && recorded.every((move,i)=>move===path[i])) {
93
+ // witnessed recorded path
94
+ } else if(!root.records.length || simpleProseReferences(`[${label}](${target})`,context,records,recordedLine).coverage.resolved!==1) return null;
95
+ // Only single-move links have unambiguous move-local prose binding here.
96
+ if(path.length!==1)return { ambiguous:true as const };
97
+ const fields=start.fen().split(' ');
98
+ const notation=label.trim().match(/^(?:(\d+)\s*(\.{1,3}|…)\s*)?([^\s]+)$/);
99
+ if(!notation||notation[3].replace(/[!?]+$/,'')!==san
100
+ || notation[1] && (Number(notation[1])!==Number(fields[5])||(notation[2]==='.'?'w':'b')!==fields[1])) return null;
101
+ return {ambiguous:false as const,before:start,after:before,from:last.slice(0,2) as Square,to:last.slice(2,4) as Square};
102
+ }catch{return null;}
103
+ }
104
+
105
+ /** Bounded geometry diagnostics only. No answer modification, strategic judgment or intent inference. */
106
+ export function checkCausalClaims(answer:string,context:ReasoningPositionContext,
107
+ records:readonly ReasoningEvidenceRecord[],recordedLine?:RecordedReferenceLine):CausalClaimCheck[] {
108
+ const historical=historicalProseRoots(context,records,recordedLine);
109
+ const output:CausalClaimCheck[]=[];
110
+ const sentences=sentenceRanges(answer);
111
+ const links=[...answer.matchAll(/\[([^\]\n]+)\]\(([^)\n]+)\)/g)];
112
+ let sentenceIndex=0;
113
+ for(const [linkIndex,match] of links.entries()) {
114
+ const [full,label,reference]=match;
115
+ if(!reference.startsWith('game:'))continue;
116
+ const index=match.index;
117
+ while(sentences[sentenceIndex].end<=index && sentenceIndex<sentences.length-1)sentenceIndex++;
118
+ const sentence=trimRange(answer,sentences[sentenceIndex].start,sentences[sentenceIndex].end);
119
+ const previous=links[linkIndex-1],next=links[linkIndex+1];
120
+ // Neighbouring links cannot lend their piece binding to this move.
121
+ const claimRange=trimRange(answer,Math.max(sentence.start,previous?previous.index+previous[0].length:0),
122
+ Math.min(sentence.end,next?.index??answer.length));
123
+ const left=answer.slice(claimRange.start,index).replace(/[*_`]/g,'');
124
+ const right=answer.slice(index+full.length,claimRange.end).replace(/[*_`]/g,'');
125
+ // The SAN label names the move, not a second piece in the explanatory prose.
126
+ const clause=`${left} ${right}`.trim();
127
+ // Readable context is complete, but predicate recognition stays local.
128
+ // Longer sentences must not attach a later subject's action to this move.
129
+ const local=right.slice(0,90);
130
+ const escape=/\b(?:escap(?:e|ed|es|ing)|fle(?:e|d)|evad(?:e|ed|es|ing))\b/i.test(local)
131
+ || /\bmoved?\s+out\s+of\s+attack\b/i.test(local)
132
+ || /\b(?:escap(?:e|ed|es|ing)|fle(?:e|d)|evad(?:e|ed|es|ing))\s+(?:with|by|on)\s*$/i.test(left);
133
+ const presentAttack=/\b(?:escap(?:e|ed|es|ing)|fle(?:e|d)|evad(?:e|ed|es|ing))\s+(?:from\s+)?(?:(?:the|an?)\s+)?(?:(?:rook|bishop|queen|knight|pawn|king)['’]s\s+)?attack\b/i.test(local)
134
+ || /\bmoved?\s+out\s+of\s+attack\b/i.test(local);
135
+ const targets=[...local.matchAll(/\b(?:toward(?:s)?|to|along)\s+([a-h][1-8])\b/gi)];
136
+ const slider=/\b(?:open(?:s|ed|ing)?|clear(?:s|ed|ing)?|block(?:s|ed|ing)?|clos(?:e|es|ed|ing))\b/i.exec(local);
137
+ const competingSliderVerbs=[...local.matchAll(/\b(?:open(?:s|ed|ing)?|clear(?:s|ed|ing)?|block(?:s|ed|ing)?|clos(?:e|es|ed|ing))\b/gi)];
138
+ const release=/\b(?:releas(?:e|es|ed|ing)|los(?:e|es|t|ing)|no longer controls?)\s+(?:control\s+(?:of|over)\s+)?([a-h][1-8])\b/i.exec(local);
139
+ if(!escape&&!slider&& !release)continue;
140
+ let parsedReady=false,parsedValue:ReturnType<typeof parseTarget>;
141
+ const parsed=()=>{
142
+ if(!parsedReady){parsedValue=parseTarget(reference,historical,label,context,records,recordedLine);parsedReady=true;}
143
+ return parsedValue;
144
+ };
145
+ const negated=/\b(?:not|never|didn't|did not|doesn't|does not)\b/i.test(clause);
146
+ const strategicConclusion=/\b(?:force(?:d|s)?|mate|tempo|wins?|winning)\b/i.test(clause);
147
+ const result=(predicate:CausalClaimCheck['predicate'],verdict:CausalClaimCheck['verdict'],reason:string)=>
148
+ output.push({claim:answer.slice(claimRange.start,claimRange.end),claimRange,
149
+ sentenceContext:{...sentence,text:answer.slice(sentence.start,sentence.end)},reference,predicate,
150
+ verdict:negated||strategicConclusion?'unresolved':verdict,
151
+ reason:negated?'Negated mechanism wording is outside this narrow check.'
152
+ :strategicConclusion?'This clause mixes geometry with a forcing, mating, winning or tempo claim.':reason});
153
+ if(escape) {
154
+ let bound=binding(clause);
155
+ const directEscape=/^\s*[,;:]?\s*(?:both\s+)?(?:escapes?|escaped|evades?|evaded|flees?|fled|moves?\s+out\s+of\s+attack)\b/i.test(local);
156
+ // Only a direct finite verb with a neutral lead-in licenses the move as
157
+ // grammatical subject. This is not general pronoun or noun resolution.
158
+ const moveSubject=/^\s*(?:(?:instead|however|then|here|but|and|now)[,:]?\s*)?$/i.test(left)
159
+ && directEscape;
160
+ if(!bound && moveSubject && !pieceBinding.test(clause) && !compactBinding.test(clause)) {
161
+ const move=parsed();
162
+ if(move && !move.ambiguous) {
163
+ const mover=move.before.get(move.from);
164
+ if(mover)bound={type:mover.type,at:move.from,color:mover.color};
165
+ }
166
+ }
167
+ // A condition before another link can still qualify this whole sentence.
168
+ const futureOrConditional=/\b(?:if|unless|future|potential|possible|could|might|may|would|will|later|threat|threatened|threatening|preemptive|anticipat(?:e|ed|ing))\b/i
169
+ .test(answer.slice(sentence.start,sentence.end));
170
+ if(!directEscape||!presentAttack||futureOrConditional)result('moving-piece-escape','unresolved','Escape wording does not unambiguously assert a present attack on the moving piece.');
171
+ else if(!bound)result('moving-piece-escape','unresolved','Move or moving piece is not uniquely bound to a witnessed single move and square.');
172
+ else {
173
+ const move=parsed();
174
+ if(!move||move.ambiguous)result('moving-piece-escape','unresolved','Move is not uniquely bound to a witnessed single ply.');
175
+ else {
176
+ const mover=move.before.get(move.from);
177
+ if(!mover||bound.at!==move.from||bound.type!==mover.type||bound.color&&bound.color!==mover.color)result('moving-piece-escape','unresolved','Named piece does not identify the moving piece at the referenced ply.');
178
+ else {
179
+ const attacked=move.before.attackers(move.from,mover.color==='w'?'b':'w').length>0;
180
+ const stillAttacked=move.after.attackers(move.to,mover.color==='w'?'b':'w').length>0;
181
+ const attackerType=/\b(rook|bishop|queen|knight|pawn|king)['’]s\s+attack\b/i.exec(local)?.[1]?.toLowerCase();
182
+ if(attackerType && !move.before.attackers(move.from,mover.color==='w'?'b':'w')
183
+ .some(square=>move.before.get(square)?.type===names[attackerType])) {
184
+ result('moving-piece-escape','unresolved','The named kind of attacker is not established on the starting square.');
185
+ continue;
186
+ }
187
+ result('moving-piece-escape',!attacked?'contradicted':stillAttacked?'unresolved':'supported',!attacked
188
+ ?'The moving piece was not geometrically attacked on its starting square.'
189
+ :stillAttacked?'The piece was attacked on both squares; whether this was an escape is not established.'
190
+ :'The moving piece left an attacked square for a geometrically unattacked square; safety or necessity is not established.');
191
+ }
192
+ }
193
+ }
194
+ }
195
+ // Opening a file or keeping the center closed describes a wider structure,
196
+ // not necessarily a ray from a stationary slider. Enter this predicate only
197
+ // when prose explicitly names both the piece and a target square.
198
+ const explicitSliderRay=slider && targets.length>0 &&
199
+ (pieceBinding.test(clause)||compactBinding.test(clause));
200
+ if(explicitSliderRay) {
201
+ const bound=binding(clause),wantOpen=/^(?:open|clear)/i.test(slider[0]);
202
+ if(!bound||targets.length!==1||competingSliderVerbs.length!==1)
203
+ result('stationary-slider-ray','unresolved','Slider, target ray, causal clause or referenced move is ambiguous.');
204
+ else {
205
+ const move=parsed();
206
+ if(!move||move.ambiguous) {
207
+ result('stationary-slider-ray','unresolved','Referenced move is not a witnessed single ply.');
208
+ continue;
209
+ }
210
+ const target=targets[0];
211
+ const piece=move.before.get(bound.at),same=move.after.get(bound.at);
212
+ const d=direction(bound.at,target[1] as Square);
213
+ const namedAxis=new RegExp(`\\b(diagonal|rank|file)\\s+(?:toward(?:s)?|to|along)\\s+${target[1]}\\b`,'i').exec(local)?.[1]?.toLowerCase();
214
+ const axis=d ? d[0]&&d[1]?'diagonal':d[0]?'rank':'file' : null;
215
+ const valid=piece&&same&&piece.type===bound.type&&same.type===piece.type&&same.color===piece.color&&(!bound.color||bound.color===piece.color)
216
+ && (piece.type==='q'||piece.type==='r'&&d&&(!d[0]||!d[1])||piece.type==='b'&&d&&!!d[0]&&!!d[1]);
217
+ if(piece&&same&&piece.type===bound.type&&same.type===piece.type&&same.color===piece.color&&(!bound.color||bound.color===piece.color)&&(!d||namedAxis&&namedAxis!==axis))
218
+ result('stationary-slider-ray','contradicted','The named target is not on the claimed ray from the stationary piece.');
219
+ else if(!valid)result('stationary-slider-ray','unresolved','The named stationary slider or its geometric ray is not established at this ply.');
220
+ else {
221
+ const previous=ray(move.before,bound.at,target[1] as Square),next=ray(move.after,bound.at,target[1] as Square);
222
+ const changed=wantOpen?!previous&&next:previous&&!next;
223
+ result('stationary-slider-ray',changed?'supported':'contradicted',
224
+ `The named ray was ${previous?'clear':'blocked'} before and ${next?'clear':'blocked'} after the move; this checks geometry only.`);
225
+ }
226
+ }
227
+ }
228
+ // Explicit source piece and target square only. A bare 'releases g4' has no controller.
229
+ if(release) {
230
+ const bound=binding(clause),target=release[1] as Square;
231
+ if(!bound)result('square-control-release','unresolved','Controlling piece or referenced ply is ambiguous.');
232
+ else {
233
+ const move=parsed();
234
+ if(!move||move.ambiguous) {
235
+ result('square-control-release','unresolved','Referenced move is not a witnessed single ply.');
236
+ continue;
237
+ }
238
+ const piece=move.before.get(bound.at);
239
+ if(!piece||piece.type!==bound.type||bound.color&&bound.color!==piece.color)result('square-control-release','unresolved','Named controller is not on its stated square before the move.');
240
+ else {
241
+ const prior=move.before.attackers(target,piece.color).includes(bound.at);
242
+ const finalSquare=bound.at===move.from?move.to:bound.at;
243
+ const nextPiece=move.after.get(finalSquare);
244
+ if(!nextPiece||nextPiece.color!==piece.color||nextPiece.type!==piece.type)
245
+ result('square-control-release','unresolved','The named controller cannot be tracked across this move.');
246
+ else {
247
+ const current=move.after.attackers(target,piece.color).includes(finalSquare);
248
+ result('square-control-release',prior&&!current?'supported':'contradicted',
249
+ `The named piece ${prior?'controlled':'did not control'} ${target} before and ${current?'controls':'does not control'} it after the move.`);
250
+ }
251
+ }
252
+ }
253
+ }
254
+ }
255
+ return output;
256
+ }
@@ -0,0 +1,21 @@
1
+ /** Stable, position-independent geometry. a1 is dark, regardless of orientation. */
2
+ export type SquareColor = 'light' | 'dark';
3
+
4
+ export function chessSquareColor(square: string): SquareColor | null {
5
+ if (!/^[a-h][1-8]$/.test(square)) return null;
6
+ return (square.charCodeAt(0) - 97 + Number(square[1]) - 1) % 2 === 0 ? 'dark' : 'light';
7
+ }
8
+
9
+ const squares = Array.from({ length: 64 }, (_, i) =>
10
+ `${String.fromCharCode(97 + i % 8)}${1 + Math.floor(i / 8)}`);
11
+
12
+ /** Grouping is a complete square->colour dictionary without 64 repeated keys. */
13
+ export const CHESS_REFERENCE_DICTIONARY = Object.freeze({
14
+ version: 'square-colors-v1',
15
+ scope: 'Position-independent square colors; not piece ownership or a position assessment.',
16
+ squareColors: Object.freeze({
17
+ light: Object.freeze(squares.filter(square => chessSquareColor(square) === 'light')),
18
+ dark: Object.freeze(squares.filter(square => chessSquareColor(square) === 'dark')),
19
+ }),
20
+ use: 'A bishop occupies squares of its color complex. Identify the bishop and the relevant position before applying this reference; do not infer which bishop a move frees from square colors alone.',
21
+ });
@@ -0,0 +1,30 @@
1
+ import type { ReasoningPositionContext } from './poc-contracts';
2
+ import {hasMoveClassificationQuestion,type QuestionRoutingDecision} from './question-routing';
3
+
4
+ const normalizedSan=(san:string)=>san.replace(/[+#?!]+$/,'');
5
+ /** Question-only notation extraction; these are references, not verified moves. */
6
+ export function classificationQuestionMoveReferences(question:string) {
7
+ const pattern=/(?<![A-Za-z0-9])(?:(\d+)\s*(\.{1,3}|…)\s*)?(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;
8
+ return [...question.matchAll(pattern)].map(match=>({san:normalizedSan(match[3]),moveNumber:match[1]?Number(match[1]):null,color:match[2]?(match[2]==='.'?'w':'b'):null}));
9
+ }
10
+ /** Resolve wording against frozen context AFTER Jev; never change board selection. */
11
+ export function classificationTargetAtSelection(decision:QuestionRoutingDecision,question:string,context:ReasoningPositionContext) {
12
+ if(!hasMoveClassificationQuestion(decision))return {applies:false,resolution:'no-classification'};
13
+ if(context.kind==='position'||!context.moveUci)return {applies:false,resolution:'no-played-selection'};
14
+ const reference=decision.answers?.classification_reference?.choice;
15
+ if(reference==='selection_relative')return {applies:true,resolution:'selection-relative'};
16
+ // An affirmative classification intent plus the existing local-reference head
17
+ // can resolve a contradictory/missing secondary head, only without named moves.
18
+ const localReference=decision.answers?.local_reference;
19
+ if((!reference||reference==='none')&&localReference?.type==='choice'&&localReference.choice==='yes'&&classificationQuestionMoveReferences(question).length===0)
20
+ return {applies:true,resolution:'unnamed-local-classification'};
21
+ if(reference!=='named_move')return {applies:false,resolution:reference==='other_relative'?'other-relative':'unresolved-reference'};
22
+ const references=classificationQuestionMoveReferences(question);
23
+ const unique=[...new Map(references.map(ref=>[JSON.stringify(ref),ref])).values()];
24
+ // Multiple distinct named moves require model-directed disambiguation. Never
25
+ // silently make an alternative or quoted/other named move the selected target.
26
+ if(unique.length!==1)return {applies:false,resolution:'ambiguous-named-reference',references};
27
+ const ref=unique[0],fields=context.fenBefore.split(' ');
28
+ const matches=ref.san===normalizedSan(context.moveSan)&&(!ref.moveNumber||ref.moveNumber===Number(fields[5]))&&(!ref.color||ref.color===fields[1]);
29
+ return {applies:matches,resolution:matches?'named-selected-move':'named-other-move',references};
30
+ }
@@ -0,0 +1,34 @@
1
+ import type { SimpleBoardAnalysis } from './simple-board-analysis';
2
+
3
+ /** Preserve decision provenance without repeating the full board-analysis snapshots. */
4
+ export function classificationReassessment(previous: SimpleBoardAnalysis | undefined, current: SimpleBoardAnalysis | undefined) {
5
+ const previousLabel = previous?.snapshot.classification;
6
+ const currentLabel = current?.snapshot.classification;
7
+ const sameContext = Boolean(previous && current && previous.record.contextHash === current.record.contextHash);
8
+ const labelChanged = sameContext && Boolean(previousLabel && currentLabel && previousLabel !== currentLabel);
9
+ const superseded = Boolean(sameContext && currentLabel && previous!.snapshot.evidenceId !== current!.snapshot.evidenceId);
10
+ const summary = (analysis: NonNullable<SimpleBoardAnalysis>, role: 'previous-analysis' | 'current-analysis') => ({
11
+ role,
12
+ evidenceId: analysis.snapshot.evidenceId,
13
+ classification: analysis.snapshot.classification,
14
+ lossCp: analysis.snapshot.evaluation.lossCp,
15
+ recordedRequestedDepth: analysis.snapshot.recordedRequestedDepth,
16
+ beforeDepth: analysis.snapshot.beforeDepth,
17
+ afterDepth: analysis.snapshot.afterDepth,
18
+ });
19
+ return {
20
+ version: 'classification-reassessment/1',
21
+ status: !currentLabel ? 'current-decision-unavailable' : !previousLabel ? 'original-decision-unavailable' : !sameContext ? 'context-mismatch' : labelChanged ? 'changed' : 'unchanged',
22
+ labelChanged,
23
+ preparationRequestedDepth: 18,
24
+ ...(previous ? {previous: {
25
+ ...summary(previous, 'previous-analysis'),
26
+ ...(superseded ? {superseded: true, supersededBy: current!.snapshot.evidenceId} : {}),
27
+ ...(labelChanged ? {recordedReason: previous.snapshot.recordedReason} : {}),
28
+ }} : {}),
29
+ ...(current && (sameContext || !previous) ? {current: summary(current, 'current-analysis')} : {}),
30
+ limitation: 'These are attributed canonical decisions before and after preparation, not causal proof. preparationRequestedDepth is the preparation search request, not the recorded depth of every classification dependency. The full current analysis is in boardAnalysis; retrieve the previous evidenceId for omitted historical detail. A missing or mismatched decision does not establish a classification change.',
31
+ };
32
+ }
33
+
34
+ export const CLASSIFICATION_REASSESSMENT_GUIDANCE = `For questions that include the selected move's classification, Knight's Eye may prepare a deeper before-position search and refresh the board's canonical classification before answering. classificationReassessment contains compact decision summaries: the current full analysis is in boardAnalysis; previous is an attributed historical summary, superseded only for a matching current context. retrieve_evidence with the previous evidenceId restores omitted historical details when needed. preparationRequestedDepth names the preparation request, not the recorded depth of either decision. When classificationReassessment.status is changed and labelChanged is true, acknowledge the change explicitly in first-person Knight's Eye language, for example: “I had a better look at this move and changed its classification from Great to Best because …”. Use the previous recordedReason and the current boardAnalysis decision details to explain the supported reason. Distinguish why we originally assigned the old label from why the refreshed decision differs; do not defend an outdated label as the current one. A changed score with an unchanged label is not a classification change. If the original or current decision, its reason, or the search is unavailable, state the material uncertainty and never invent a change or explanation. A recorded candidate-gap flag is a bounded policy signal, not proof that only one move preserves a favorable position.`;
@@ -0,0 +1,125 @@
1
+ import { presentMoveMechanisms } from './mechanism-presentation';
2
+ import { Chess } from 'chess.js';
3
+ import type { MaterialFormat } from './material-format';
4
+ import type { ReasoningEvidenceRecord, ReasoningToolResult } from './poc-contracts';
5
+ import { compactSimpleEngineResult } from './simple-evidence-view';
6
+
7
+ type Obj = Record<string, unknown>;
8
+ const object = (value: unknown): value is Obj => value !== null && typeof value === 'object' && !Array.isArray(value);
9
+ const known = (value: Obj, keys: readonly string[]) => Object.keys(value).every(key => keys.includes(key));
10
+ const detailsKeys = [
11
+ 'phase', 'rootMoves', 'rootSelection', 'bestMove', 'engine', 'engineBuild', 'searchScope',
12
+ 'rootMoveFacts', 'continuationMoveFacts', 'lines', 'minimumPvPlies',
13
+ 'requestedPvCoverage', 'authority', 'branchExpansion',
14
+ ] as const;
15
+ const lineKeys = [
16
+ 'multipv', 'rootMove', 'pv', 'depth', 'nodes', 'scoreCp', 'matePly', 'scoreScope', 'scoreProvenance',
17
+ 'rawPv', 'pvCompletion', 'material',
18
+ ] as const;
19
+ const completionKeys = ['requestedPlies', 'actualPlies', 'status', 'conditional', 'segments'] as const;
20
+ const segmentKeys = [
21
+ 'fen', 'startPly', 'depth', 'pv', 'scoreCp', 'matePly', 'scoreScope', 'scoreProvenance',
22
+ 'command', 'engine', 'engineBuild', 'cacheProvenance',
23
+ ] as const;
24
+
25
+ /** The initial projection must not silently discard a new engine field. */
26
+ export function compactSearchRecord(record: ReasoningEvidenceRecord, playedMove?: string): ReasoningEvidenceRecord | null {
27
+ const data = record.data as Obj;
28
+ if (!known(data, ['recordedPosition', 'details']) || !object(data.recordedPosition) || !object(data.details)) return null;
29
+ const recordedPosition = data.recordedPosition;
30
+ const details = data.details;
31
+ if (!known(details, detailsKeys) || !object(details.searchScope) || typeof details.searchScope.fen !== 'string'
32
+ || !Array.isArray(details.lines) || !details.lines.length || !Array.isArray(details.continuationMoveFacts)) return null;
33
+ const positionKeys = ['recordHash', 'ply', 'startFen', 'fen', 'historyUci', 'rootContextHash', 'nextRecordedMove', 'scope'];
34
+ if (!known(recordedPosition, positionKeys) || !Number.isInteger(recordedPosition.ply)
35
+ || typeof recordedPosition.fen !== 'string') return null;
36
+
37
+ const strongestOther = details.lines.find(line => object(line) && line.rootMove !== playedMove);
38
+ const mechanismAlternative = details.bestMove && details.bestMove !== playedMove ? details.bestMove
39
+ : object(strongestOther) ? strongestOther.rootMove : details.bestMove;
40
+ const lines: Obj[] = [];
41
+ for (const line of details.lines) {
42
+ if (!object(line) || !known(line, lineKeys) || !Array.isArray(line.pv)
43
+ || !line.pv.every(move => typeof move === 'string')
44
+ || typeof line.rootMove !== 'string' || !object(line.pvCompletion)
45
+ || !known(line.pvCompletion, completionKeys) || !Array.isArray(line.pvCompletion.segments)) return null;
46
+ // SAN is useful to the answering model; derive it from the exact rooted PV,
47
+ // never from move-number strings that may belong to another position.
48
+ let pvSan: string[];
49
+ try {
50
+ const board = new Chess(details.searchScope.fen);
51
+ pvSan = line.pv.map(move => {
52
+ if (!/^[a-h][1-8][a-h][1-8][qrbn]?$/.test(move)) throw Error('invalid PV move');
53
+ const replayed = board.move({from:move.slice(0,2),to:move.slice(2,4),promotion:move.slice(4)||undefined});
54
+ if (!replayed) throw Error('illegal PV move');
55
+ return replayed.san;
56
+ });
57
+ } catch { return null; }
58
+ const segments: Obj[] = [];
59
+ for (const segment of line.pvCompletion.segments) {
60
+ if (!object(segment) || !known(segment,segmentKeys)
61
+ || !Array.isArray(segment.pv) || !Number.isInteger(segment.startPly)) return null;
62
+ const start = segment.startPly as number;
63
+ const same = start >= 0 && JSON.stringify(line.pv.slice(start,start+segment.pv.length)) === JSON.stringify(segment.pv);
64
+ const {pv: _pv, ...remaining} = segment;
65
+ void _pv;
66
+ segments.push(same ? {...remaining,pvFromLinePly:start,pvLength:segment.pv.length} : {...remaining,pv:segment.pv});
67
+ }
68
+ const {segments:_segments, ...completion} = line.pvCompletion;
69
+ void _segments;
70
+ const material = object(line.material) && object(line.material.start) && object(line.material.end)
71
+ ? {authority:line.material.authority,
72
+ startBalance:line.material.start.balance,
73
+ endBalance:line.material.end.balance,
74
+ balanceChangeFromStart:line.material.balanceChangeFromStart,
75
+ scope:line.material.scope}
76
+ : undefined;
77
+ // rawPv is sometimes different from the displayed continuation. Retain it
78
+ // in that case; only equal bytes may be omitted.
79
+ const separateRawPv = Array.isArray(line.rawPv)
80
+ && JSON.stringify(line.rawPv) !== JSON.stringify(line.pv) ? {rawPv:line.rawPv} : {};
81
+ const continuation = details.continuationMoveFacts.find(f => object(f) && f.rootMove === line.rootMove);
82
+ const mechanismRelevant = line.rootMove === playedMove || line.rootMove === mechanismAlternative
83
+ || (!mechanismAlternative && lines.length === 0);
84
+ const mechanismFacts = mechanismRelevant && object(continuation) && Array.isArray(continuation.moves) ? presentMoveMechanisms(continuation.moves) : [];
85
+ lines.push({
86
+ ...(object(continuation) && Array.isArray(continuation.moves) ? {mechanismAvailablePlies:continuation.moves.length} : {}),
87
+ ...(mechanismFacts.length ? {mechanismFacts} : {}),
88
+ ...(line.multipv !== undefined ? {multipv:line.multipv} : {}), rootMove:line.rootMove,
89
+ ...(playedMove ? {playedMove:playedMove === line.rootMove} : {}), pv:line.pv, pvSan,
90
+ depth:line.depth, scoreCp:line.scoreCp, matePly:line.matePly,
91
+ scoreScope:line.scoreScope, ...(line.scoreProvenance ? {scoreProvenance:line.scoreProvenance} : {}), pvCompletion:{...completion,segments},
92
+ ...(material ? {materialSummary:material} : {}), ...separateRawPv,
93
+ });
94
+ }
95
+ const {historyUci:_historyUci, ...root} = recordedPosition;
96
+ void _historyUci;
97
+ const {rootMoveFacts:_rootMoveFacts,continuationMoveFacts:_continuationMoveFacts,lines:_lines,...search} = details;
98
+ void _rootMoveFacts; void _continuationMoveFacts; void _lines;
99
+ const projection = {
100
+ ...record,
101
+ data:{recordedPosition:root,details:{...search,lines,
102
+ modelPresentation:{scope:'compact initial preparation only; supplied PVs are complete, mechanismFacts cover first two plies for played and strongest roots with scoped control targets; deeper geometry remains retrievable',
103
+ mechanismRoots:lines.filter(line=>Array.isArray(line.mechanismFacts)).map(line=>line.rootMove),
104
+ omitted:['otherRootMechanisms','rootMoveFacts','continuationMoveFacts','captureGeometry','materialCheckpoints','duplicatePvSegmentMoves'],
105
+ retrieveFullEvidenceId:record.id,
106
+ segmentPvReference:'A segment with pvFromLinePly=N and pvLength=L repeats exactly L moves of that line.pv starting at zero-based index N; its own depth, score, FEN, command and cache provenance remain specific to that segment.',
107
+ instruction:'For deeper or omitted geometry, other control squares, piece identities or material checkpoints, call retrieve_evidence with this evidence ID. This reuses the acquired record without another engine search. mechanismAvailablePlies states the retained geometry horizon; inspect later moves with test_game_line/inspectPlies.'}}},
108
+ // replay duplicates the full PV arrays. The PVs above remain available for
109
+ // game links, while the immutable replay stays in the authoritative record.
110
+ replay:null,
111
+ };
112
+ return projection as ReasoningEvidenceRecord;
113
+ }
114
+
115
+ /** Only automatic preparation is thinned. The caller keeps the original tool
116
+ * result in its evidence ledger and observable trace for retrieval/replay. */
117
+ export function compactPreparedSearchResult(result: ReasoningToolResult, playedMove?: string, format?: MaterialFormat): unknown {
118
+ const packet = {
119
+ contextHash:result.contextHash,
120
+ evidence:result.evidence.map(record => compactSearchRecord(record,playedMove) ?? record),
121
+ limitations:[...result.limitations,
122
+ 'Initial preparation shows complete supplied PV moves and bounded scores, with first-two-ply mechanism measurements for played/strongest roots, but omits deeper geometry and material checkpoints. retrieve_evidence(evidenceIds) returns the already acquired full records without a new search.'],
123
+ };
124
+ return compactSimpleEngineResult(packet,format);
125
+ }