@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,123 @@
1
+ import { Chess, SQUARES, type Color, type PieceSymbol, type Square } from "chess.js";
2
+ import { pieceIdAt } from "./poc-piece-refs";
3
+ import { applyIdentityMove } from "./piece-identities";
4
+ import type { IdentityState } from "./types";
5
+
6
+ export type OccupiedRayPiece = {
7
+ pieceId: string;
8
+ currentSquare: Square;
9
+ pieceType: PieceSymbol;
10
+ color: Color;
11
+ };
12
+
13
+ export type ExchangeRayAlignmentWitness = {
14
+ movesUci: string[];
15
+ finalFen: string;
16
+ slider: OccupiedRayPiece;
17
+ front: OccupiedRayPiece;
18
+ rear: OccupiedRayPiece;
19
+ ray: Square[];
20
+ };
21
+
22
+ type RayDirection = readonly [number, number];
23
+
24
+ const NOMINAL_VALUES: Record<PieceSymbol, number> = { p: 1, n: 3, b: 3, r: 5, q: 9, k: 0 };
25
+ const ORTHOGONAL_DIRECTIONS: readonly RayDirection[] = [[1, 0], [-1, 0], [0, 1], [0, -1]];
26
+ const DIAGONAL_DIRECTIONS: readonly RayDirection[] = [[1, 1], [1, -1], [-1, 1], [-1, -1]];
27
+
28
+ function moveUci(move: { from: string; to: string; promotion?: string }) {
29
+ return `${move.from}${move.to}${move.promotion || ""}`;
30
+ }
31
+
32
+ function squareAt(file: number, rank: number): Square | null {
33
+ if (file < 0 || file > 7 || rank < 0 || rank > 7) return null;
34
+ return `${String.fromCharCode(97 + file)}${rank + 1}` as Square;
35
+ }
36
+
37
+ function occupiedOpponentRayAlignments(board: Chess, identities: IdentityState) {
38
+ const result: Array<Omit<ExchangeRayAlignmentWitness, "movesUci" | "finalFen">> = [];
39
+ for (const sliderSquare of SQUARES) {
40
+ const slider = board.get(sliderSquare);
41
+ if (!slider || !["b", "r", "q"].includes(slider.type)) continue;
42
+ const directions = slider.type === "b"
43
+ ? DIAGONAL_DIRECTIONS
44
+ : slider.type === "r"
45
+ ? ORTHOGONAL_DIRECTIONS
46
+ : [...ORTHOGONAL_DIRECTIONS, ...DIAGONAL_DIRECTIONS];
47
+ const originFile = sliderSquare.charCodeAt(0) - 97;
48
+ const originRank = Number(sliderSquare[1]) - 1;
49
+ for (const [fileStep, rankStep] of directions) {
50
+ const ray: Square[] = [sliderSquare];
51
+ const occupied: Array<{ square: Square; color: Color; type: PieceSymbol; pieceId: string | null }> = [];
52
+ for (let distance = 1; distance < 8; distance += 1) {
53
+ const square = squareAt(originFile + fileStep * distance, originRank + rankStep * distance);
54
+ if (!square) break;
55
+ ray.push(square);
56
+ const piece = board.get(square);
57
+ if (!piece) continue;
58
+ occupied.push({ square, color: piece.color, type: piece.type, pieceId: pieceIdAt(identities, square) });
59
+ if (occupied.length === 2) break;
60
+ }
61
+ const front = occupied[0];
62
+ const rear = occupied[1];
63
+ const sliderPieceId = pieceIdAt(identities, sliderSquare);
64
+ if (!front || !rear || !sliderPieceId || !front.pieceId || !rear.pieceId) continue;
65
+ if (front.color === slider.color || rear.color !== front.color) continue;
66
+ if (rear.type !== "k" && NOMINAL_VALUES[rear.type] <= NOMINAL_VALUES[front.type]) continue;
67
+ result.push({
68
+ slider: { pieceId: sliderPieceId, currentSquare: sliderSquare, pieceType: slider.type, color: slider.color },
69
+ front: { pieceId: front.pieceId, currentSquare: front.square, pieceType: front.type, color: front.color },
70
+ rear: { pieceId: rear.pieceId, currentSquare: rear.square, pieceType: rear.type, color: rear.color },
71
+ ray: ray.slice(0, ray.indexOf(rear.square) + 1),
72
+ });
73
+ }
74
+ }
75
+ return result;
76
+ }
77
+
78
+ function alignmentKey(alignment: Omit<ExchangeRayAlignmentWitness, "movesUci" | "finalFen">) {
79
+ return [
80
+ alignment.slider.pieceId,
81
+ alignment.slider.currentSquare,
82
+ alignment.front.pieceId,
83
+ alignment.front.currentSquare,
84
+ alignment.rear.pieceId,
85
+ alignment.rear.currentSquare,
86
+ alignment.ray.join("-"),
87
+ ].join("|");
88
+ }
89
+
90
+ export function exchangeRayAlignmentWitnesses(
91
+ fenAfter: string,
92
+ identityAfter: IdentityState,
93
+ exchangeLines: readonly (readonly { uci: string }[])[],
94
+ ): ExchangeRayAlignmentWitness[] {
95
+ const baseline = new Chess(fenAfter);
96
+ const baselineKeys = new Set(occupiedOpponentRayAlignments(baseline, identityAfter).map(alignmentKey));
97
+ const result = new Map<string, ExchangeRayAlignmentWitness>();
98
+ for (const line of exchangeLines) {
99
+ const board = new Chess(fenAfter);
100
+ let identities = identityAfter;
101
+ for (const item of line) {
102
+ const move = board.moves({ verbose: true }).find((candidate) => moveUci(candidate) === item.uci);
103
+ if (!move) throw new Error(`Same-square witness contains an illegal move: ${item.uci}`);
104
+ board.move({ from: move.from, to: move.to, promotion: move.promotion });
105
+ identities = applyIdentityMove(identities, { from: move.from, to: move.to, promotion: move.promotion }).state;
106
+ }
107
+ const movesUci = line.map((item) => item.uci);
108
+ const finalFen = board.fen();
109
+ for (const alignment of occupiedOpponentRayAlignments(board, identities)) {
110
+ const key = alignmentKey(alignment);
111
+ if (baselineKeys.has(key)) continue;
112
+ const witness = { movesUci, finalFen, ...alignment };
113
+ const existing = result.get(key);
114
+ const witnessText = movesUci.join(" ");
115
+ const existingText = existing?.movesUci.join(" ") || "";
116
+ if (!existing || movesUci.length < existing.movesUci.length || (movesUci.length === existing.movesUci.length && witnessText < existingText)) {
117
+ result.set(key, witness);
118
+ }
119
+ }
120
+ }
121
+ return [...result.values()].sort((left, right) => left.movesUci.join(" ").localeCompare(right.movesUci.join(" "))
122
+ || alignmentKey(left).localeCompare(alignmentKey(right)));
123
+ }
@@ -0,0 +1,94 @@
1
+ type Obj = Record<string, unknown>;
2
+ const object = (v: unknown): v is Obj => !!v && typeof v === 'object' && !Array.isArray(v);
3
+
4
+ const COLUMNS = {
5
+ mover: ['pieceId','pieceIdAfter','fromSquare','toSquare','color','pieceTypeBefore','pieceTypeAfter','attackersBefore','attackersAfter'],
6
+ unmoved: ['pieceId','square','pieceType','attackersBefore','attackersAfter'],
7
+ rays: ['pieceId','fromSquare','color','pieceType','direction','gainedSquares','lostSquares'],
8
+ control: ['square','color','attackersBefore','attackersAfter'],
9
+ } as const;
10
+ type ColumnGroup = keyof typeof COLUMNS;
11
+ const GROUPS: ColumnGroup[] = ['mover','unmoved','rays','control'];
12
+
13
+ function exactKeys(value: unknown, columns: readonly string[]): value is Obj {
14
+ return object(value) && Object.keys(value).length === columns.length
15
+ && columns.every(column => Object.hasOwn(value,column));
16
+ }
17
+
18
+ function encodeRows(value: Obj, columns: readonly string[]): unknown[] {
19
+ return columns.map(column => value[column]);
20
+ }
21
+
22
+ function decodeRow(row: unknown, columns: readonly string[]): Obj | null {
23
+ if (!Array.isArray(row) || row.length !== columns.length) return null;
24
+ return Object.fromEntries(columns.map((column,index) => [column,row[index]]));
25
+ }
26
+
27
+ /** Restores the precise bounded projection, including its coverage/omission markers.
28
+ * Unknown versions or schemas pass through unchanged rather than losing fields. */
29
+ export function expandMechanismPresentation(value: unknown): unknown {
30
+ if (!object(value) || value.encoding !== 'move-mechanism/1') return value;
31
+ if (!object(value.columns) || !object(value.rows)
32
+ || Object.keys(value.columns).length !== GROUPS.length
33
+ || Object.keys(value.rows).length !== GROUPS.length) return value;
34
+ for (const group of GROUPS) {
35
+ if (JSON.stringify(value.columns[group]) !== JSON.stringify(COLUMNS[group])) return value;
36
+ }
37
+ const mover = decodeRow(value.rows.mover,COLUMNS.mover);
38
+ if (!mover || !Array.isArray(value.rows.unmoved) || !Array.isArray(value.rows.rays)
39
+ || !Array.isArray(value.rows.control)) return value;
40
+ const unmoved = value.rows.unmoved.map(row => decodeRow(row,COLUMNS.unmoved));
41
+ const rays = value.rows.rays.map(row => decodeRow(row,COLUMNS.rays));
42
+ const control = value.rows.control.map(row => decodeRow(row,COLUMNS.control));
43
+ if ([...unmoved,...rays,...control].some(row => row === null)) return value;
44
+ const {encoding:_encoding,columns:_columns,rows:_rows,...rest} = value;
45
+ void _encoding; void _columns; void _rows;
46
+ return {...rest,mover,attackedUnmovedFriendlyPieces:unmoved,
47
+ stationarySliderRays:rays,controlDelta:control};
48
+ }
49
+
50
+ /** A measured but untransmitted square is not a negative result. Full control
51
+ * deltas stay on the immutable evidence record for retrieval. */
52
+ export function compactMechanism(value: unknown, targetSquares: string[]): unknown {
53
+ if (!object(value) || !Array.isArray(value.controlDelta)) return value;
54
+ const targets = [...new Set(targetSquares)].filter(s => /^[a-h][1-8]$/.test(s));
55
+ const controlDelta = value.controlDelta.filter(d => object(d) && targets.includes(String(d.square)));
56
+ const rays = Array.isArray(value.stationarySliderRays) ? value.stationarySliderRays.map(ray => {
57
+ if (!object(ray)) return ray;
58
+ const {beforeSquares:_before,afterSquares:_after,...changes} = ray;
59
+ void _before; void _after;
60
+ return changes;
61
+ }) : value.stationarySliderRays;
62
+ const projected: Obj = {...value, stationarySliderRays:rays, controlDelta,
63
+ presentation:{controlSquares:targets, omittedControlDeltaCount:value.controlDelta.length-controlDelta.length,
64
+ scope:'All changed stationary rays and mover/unmoved attack measurements retained. Control deltas shown only for controlSquares; an omitted square is not checked-negative in this view. Full ray extents and all control deltas are retrievable.'}};
65
+ // Additions to the canonical contract must travel verbatim until an explicit
66
+ // versioned schema can represent them; never silently discard an unknown fact.
67
+ if (!exactKeys(value,['mover','attackedUnmovedFriendlyPieces','stationarySliderRays','controlDelta','checksRun','scope'])
68
+ || !exactKeys(projected.mover,COLUMNS.mover)
69
+ || !Array.isArray(projected.attackedUnmovedFriendlyPieces)
70
+ || !projected.attackedUnmovedFriendlyPieces.every(item => exactKeys(item,COLUMNS.unmoved))
71
+ || !Array.isArray(rays) || !rays.every(item => exactKeys(item,COLUMNS.rays))
72
+ || !controlDelta.every(item => exactKeys(item,COLUMNS.control))) return projected;
73
+ return {
74
+ encoding:'move-mechanism/1',columns:COLUMNS,
75
+ rows:{
76
+ mover:encodeRows(projected.mover,COLUMNS.mover),
77
+ unmoved:projected.attackedUnmovedFriendlyPieces.map(item => encodeRows(item as Obj,COLUMNS.unmoved)),
78
+ rays:rays.map(item => encodeRows(item as Obj,COLUMNS.rays)),
79
+ control:controlDelta.map(item => encodeRows(item,COLUMNS.control)),
80
+ },
81
+ checksRun:projected.checksRun,scope:projected.scope,presentation:projected.presentation,
82
+ };
83
+ }
84
+
85
+ /** Root + first reply by default; explicit one-based replay targets, and moves
86
+ * landing on inspected squares, can expose a deeper mechanism without a search. */
87
+ export function presentMoveMechanisms(moves: unknown[], inspectPlies: number[] = [], inspectSquares: string[] = []): unknown[] {
88
+ return moves.flatMap((fact,index) => {
89
+ if (!object(fact) || !object(fact.mechanism)) return [];
90
+ if (index >= 2 && !inspectPlies.includes(index+1) && !inspectSquares.includes(String(fact.to))) return [];
91
+ const targets = [...inspectSquares, ...moves.slice(index,index+3).flatMap(m => object(m) && typeof m.to === 'string' ? [m.to] : [])];
92
+ return [{ply:index+1,uci:fact.uci,san:fact.san,mechanism:compactMechanism(fact.mechanism,targets)}];
93
+ });
94
+ }
@@ -0,0 +1,185 @@
1
+ import { Chess, SQUARES, type Color, type PieceSymbol, type Square } from "chess.js";
2
+ import { applyIdentityMove, createIdentityState } from "./piece-identities";
3
+ import type { IdentityState } from "./types";
4
+
5
+ type Direction = "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW";
6
+ type RayStep = readonly [Direction, number, number];
7
+
8
+ const DIRECTIONS: readonly RayStep[] = [
9
+ ["N", 0, 1], ["NE", 1, 1], ["E", 1, 0], ["SE", 1, -1],
10
+ ["S", 0, -1], ["SW", -1, -1], ["W", -1, 0], ["NW", -1, 1],
11
+ ];
12
+
13
+ export type MoveMechanismFacts = {
14
+ mover: {
15
+ pieceId: string;
16
+ pieceIdAfter: string;
17
+ fromSquare: Square;
18
+ toSquare: Square;
19
+ color: Color;
20
+ pieceTypeBefore: PieceSymbol;
21
+ pieceTypeAfter: PieceSymbol;
22
+ attackersBefore: Square[];
23
+ attackersAfter: Square[];
24
+ };
25
+ attackedUnmovedFriendlyPieces: Array<{
26
+ pieceId: string;
27
+ square: Square;
28
+ pieceType: PieceSymbol;
29
+ attackersBefore: Square[];
30
+ attackersAfter: Square[];
31
+ }>;
32
+ stationarySliderRays: Array<{
33
+ pieceId: string;
34
+ fromSquare: Square;
35
+ color: Color;
36
+ pieceType: PieceSymbol;
37
+ direction: Direction;
38
+ beforeSquares: Square[];
39
+ afterSquares: Square[];
40
+ gainedSquares: Square[];
41
+ lostSquares: Square[];
42
+ }>;
43
+ controlDelta: Array<{
44
+ square: Square;
45
+ color: Color;
46
+ attackersBefore: Square[];
47
+ attackersAfter: Square[];
48
+ }>;
49
+ checksRun: string[];
50
+ scope: string;
51
+ };
52
+
53
+ function enemy(color: Color): Color {
54
+ return color === "w" ? "b" : "w";
55
+ }
56
+
57
+ function attackers(board: Chess, square: Square, color: Color): Square[] {
58
+ return board.attackers(square, color).sort((a, b) => SQUARES.indexOf(a) - SQUARES.indexOf(b));
59
+ }
60
+
61
+ function ray(board: Chess, origin: Square, fileStep: number, rankStep: number): Square[] {
62
+ const squares: Square[] = [];
63
+ const file = origin.charCodeAt(0) - 97;
64
+ const rank = Number(origin[1]) - 1;
65
+ for (let distance = 1; distance < 8; distance += 1) {
66
+ const f = file + fileStep * distance;
67
+ const r = rank + rankStep * distance;
68
+ if (f < 0 || f > 7 || r < 0 || r > 7) break;
69
+ const square = `${String.fromCharCode(97 + f)}${r + 1}` as Square;
70
+ squares.push(square);
71
+ // The first blocker is controlled, but nothing behind it is visible.
72
+ if (board.get(square)) break;
73
+ }
74
+ return squares;
75
+ }
76
+
77
+ function sliderDirections(type: PieceSymbol): readonly RayStep[] {
78
+ if (type === "q") return DIRECTIONS;
79
+ return DIRECTIONS.filter(([, dx, dy]) => type === "r" ? !dx || !dy : !!dx && !!dy);
80
+ }
81
+
82
+ /** Complete board-local geometric measurements for one already-admitted legal move. */
83
+ export function moveMechanismFacts(
84
+ before: Chess,
85
+ after: Chess,
86
+ from: Square,
87
+ to: Square,
88
+ beforeIds?: IdentityState,
89
+ afterIds?: IdentityState,
90
+ ): MoveMechanismFacts {
91
+ const moverBefore = before.get(from);
92
+ const moverAfter = after.get(to);
93
+ if (!moverBefore || !moverAfter || moverBefore.color !== moverAfter.color) {
94
+ throw new Error(`Move mechanism has no corresponding mover at ${from}-${to}`);
95
+ }
96
+ const beforeState = beforeIds || createIdentityState(before.fen());
97
+ if (beforeState.fen !== before.fen() || (afterIds && afterIds.fen !== after.fen())) {
98
+ throw new Error("Move mechanism identity position does not match the board");
99
+ }
100
+ // Resolve promotion by the resulting board, since from/to alone can name four
101
+ // distinct legal moves. This also rejects an unrelated after-position.
102
+ const actualMove = before.moves({ verbose: true }).find((move) => {
103
+ if (move.from !== from || move.to !== to) return false;
104
+ const replay = new Chess(before.fen());
105
+ replay.move({ from, to, promotion: move.promotion });
106
+ return replay.fen() === after.fen();
107
+ });
108
+ if (!actualMove) throw new Error(`Move mechanism after-position is not a legal ${from}-${to} result`);
109
+ const afterState = afterIds || applyIdentityMove(beforeState, {
110
+ from, to, promotion: actualMove.promotion,
111
+ }).state;
112
+ const pieceId = beforeState.squares[from];
113
+ const pieceIdAfter = afterState.squares[to];
114
+ if (!pieceId || !pieceIdAfter || (actualMove.promotion
115
+ ? afterState.pieces[pieceIdAfter]?.promotedFrom !== pieceId
116
+ : pieceIdAfter !== pieceId)) {
117
+ throw new Error("Move mechanism mover identities do not match");
118
+ }
119
+
120
+ const attackedUnmovedFriendlyPieces: MoveMechanismFacts["attackedUnmovedFriendlyPieces"] = [];
121
+ const stationarySliderRays: MoveMechanismFacts["stationarySliderRays"] = [];
122
+ const color = moverBefore.color;
123
+ for (const square of SQUARES) {
124
+ const piece = before.get(square);
125
+ const samePiece = after.get(square);
126
+ const id = beforeState.squares[square];
127
+ if (!piece || !samePiece || !id || afterState.squares[square] !== id
128
+ || piece.color !== samePiece.color || piece.type !== samePiece.type) continue;
129
+ if (piece.color === color) {
130
+ const beforeAttackers = attackers(before, square, enemy(color));
131
+ const afterAttackers = attackers(after, square, enemy(color));
132
+ if (beforeAttackers.length || afterAttackers.length) {
133
+ attackedUnmovedFriendlyPieces.push({
134
+ pieceId: id, square, pieceType: piece.type,
135
+ attackersBefore: beforeAttackers, attackersAfter: afterAttackers,
136
+ });
137
+ }
138
+ }
139
+ if (piece.type !== "r" && piece.type !== "b" && piece.type !== "q") continue;
140
+ for (const [direction, dx, dy] of sliderDirections(piece.type)) {
141
+ const beforeSquares = ray(before, square, dx, dy);
142
+ const afterSquares = ray(after, square, dx, dy);
143
+ const beforeSet = new Set(beforeSquares);
144
+ const afterSet = new Set(afterSquares);
145
+ const gainedSquares = afterSquares.filter((target) => !beforeSet.has(target));
146
+ const lostSquares = beforeSquares.filter((target) => !afterSet.has(target));
147
+ if (gainedSquares.length || lostSquares.length) {
148
+ stationarySliderRays.push({
149
+ pieceId: id, fromSquare: square, color: piece.color, pieceType: piece.type,
150
+ direction, beforeSquares, afterSquares, gainedSquares, lostSquares,
151
+ });
152
+ }
153
+ }
154
+ }
155
+
156
+ const controlDelta: MoveMechanismFacts["controlDelta"] = [];
157
+ for (const square of SQUARES) {
158
+ for (const attackingColor of ["w", "b"] as const) {
159
+ const attackersBefore = attackers(before, square, attackingColor);
160
+ const attackersAfter = attackers(after, square, attackingColor);
161
+ if (attackersBefore.length !== attackersAfter.length
162
+ || attackersBefore.some((origin, index) => origin !== attackersAfter[index])) {
163
+ controlDelta.push({ square, color: attackingColor, attackersBefore, attackersAfter });
164
+ }
165
+ }
166
+ }
167
+ return {
168
+ mover: {
169
+ pieceId, pieceIdAfter, fromSquare: from, toSquare: to, color,
170
+ pieceTypeBefore: moverBefore.type, pieceTypeAfter: moverAfter.type,
171
+ attackersBefore: attackers(before, from, enemy(color)),
172
+ attackersAfter: attackers(after, to, enemy(color)),
173
+ },
174
+ attackedUnmovedFriendlyPieces,
175
+ stationarySliderRays,
176
+ controlDelta,
177
+ checksRun: [
178
+ "mover-geometric-attackers-before-after",
179
+ "unmoved-friendly-piece-geometric-attackers-before-after",
180
+ "stationary-slider-visible-rays-before-after",
181
+ "all-square-both-color-geometric-control-delta",
182
+ ],
183
+ scope: "Complete board-local geometric measurements at this move. Attackers include pinned pieces; visible slider rays include the first occupied square. Empty lists mean measured none or no change. No claim of legal captures, safe retreat, exchange profit, forced purpose, or strategic importance.",
184
+ };
185
+ }
@@ -0,0 +1,13 @@
1
+ import { generateText, jsonSchema, tool, type LanguageModel } from 'ai';
2
+ import type { AnswerModelConnector } from './answer-model-connector';
3
+ /** Native SDK performs exactly one model turn; pipeline owns tool dispatch. */
4
+ export function createNativeAnswerConnector(model:LanguageModel):AnswerModelConnector {
5
+ return {kind:'model-turn',capabilities:{toolRequests:true,toolFreeComposition:true,opaqueContinuation:true,outputTokenLimit:'requested',usageReporting:'optional'},async invoke(request) {
6
+ const result=await generateText({model,system:request.system,messages:request.messages,
7
+ tools:request.tools.length ? Object.fromEntries(request.tools.map(d=>[d.name,tool({description:d.description,inputSchema:jsonSchema<Record<string,unknown>>(d.inputSchema as Parameters<typeof jsonSchema>[0])})])) : undefined,
8
+ toolChoice:request.toolChoice,maxOutputTokens:request.maxOutputTokens,maxRetries:0,abortSignal:request.signal,
9
+ providerOptions:{openai:{store:false,include:['reasoning.encrypted_content'],reasoningEffort:request.effort}}});
10
+ return {text:result.text,toolCalls:result.toolCalls.map(c=>({toolCallId:c.toolCallId,toolName:c.toolName,input:c.input as Record<string,unknown>})),
11
+ finishReason:result.finishReason,usage:result.usage,response:result.response,providerMetadata:result.providerMetadata};
12
+ }};
13
+ }
@@ -0,0 +1,47 @@
1
+ export type WikibooksTarget = { title: string; relation: 'current' | 'before' | 'continuation' };
2
+ export type WikibooksPage = {
3
+ title: string;
4
+ relation: WikibooksTarget['relation'] | 'ancestor';
5
+ matchedTargets?: Array<{ requestedTitle: string; relation: WikibooksTarget['relation']; ancestorPlies: number; transposition: boolean }>;
6
+ requestedTitle?: string;
7
+ ancestorPlies?: number;
8
+ transposition?: boolean;
9
+ positionFen: string;
10
+ revisionId: number;
11
+ revisionTimestamp: string;
12
+ url: string;
13
+ authorHistoryUrl: string;
14
+ attribution: string;
15
+ license: 'CC BY-SA 4.0';
16
+ licenseUrl: string;
17
+ prose: string;
18
+ };
19
+ export type WikibooksContextResult = {
20
+ status: 'available' | 'outside-scope' | 'unavailable' | 'invalid-context' | 'aborted';
21
+ contextHash: string | null;
22
+ scope?: 'game-review';
23
+ pages: WikibooksPage[];
24
+ /** Local usable-prose catalog title candidates checked during this lookup. */
25
+ localIndexCandidateCount: number;
26
+ /** Recorded-game positions checked against the local catalog before title selection. */
27
+ localIndexPositionCheckCount: number;
28
+ /** Distinct catalog titles selected for possible live revision retrieval. */
29
+ localIndexSelectedTitleCount: number;
30
+ /** Selected titles already present in this answer's transport cache. */
31
+ cacheHitTitleCount: number;
32
+ /** Actual MediaWiki fetch attempts, retained alongside requestCount for clarity. */
33
+ networkRequestCount: number;
34
+ /** Exact titles sent in those fetch attempts, including redirects and continuations. */
35
+ networkRequestedTitles: string[];
36
+ requestCount: number;
37
+ timingMs: number;
38
+ limitations: string[];
39
+ };
40
+ export type WikibooksPlan = {
41
+ status: 'in-scope' | 'outside-scope' | 'invalid-context';
42
+ rootTitle: string;
43
+ beforeTitle: string | null;
44
+ currentTitle: string | null;
45
+ currentFen: string | null;
46
+ };
47
+
@@ -0,0 +1,53 @@
1
+ import type { WikibooksPage } from './opening-source-contracts';
2
+ import { EVIDENCE_SHARING_VERSION, expandSharedEvidence } from './lossless-evidence-sharing';
3
+ export type OpeningSourceCredit = { title: string; revisionId: number };
4
+ export const OPENING_SOURCE_LICENSE_URL = 'https://creativecommons.org/licenses/by-sa/4.0/';
5
+ export function openingSourceLinks(source: OpeningSourceCredit) {
6
+ const title=encodeURIComponent(source.title.replaceAll(' ','_'));
7
+ return {revision:`https://en.wikibooks.org/w/index.php?title=${title}&oldid=${source.revisionId}`,
8
+ history:`https://en.wikibooks.org/w/index.php?title=${title}&action=history`};
9
+ }
10
+ /** Persist only bounded attribution metadata, never fetched prose or model data. */
11
+ export function parseOpeningSourceCredits(value: unknown): OpeningSourceCredit[] {
12
+ if(!Array.isArray(value)||value.length>512)throw Error('Invalid opening sources.');
13
+ const result:OpeningSourceCredit[]=[],seen=new Set<string>();
14
+ for(const item of value) {
15
+ if(!item||typeof item!=='object'||typeof item.title!=='string'||!item.title.startsWith('Chess Opening Theory/')
16
+ ||item.title.length>2000||/[\u0000-\u001f\u007f]/.test(item.title)||!Number.isSafeInteger(item.revisionId)||item.revisionId<=0)throw Error('Invalid opening source.');
17
+ const key=item.title+'@'+item.revisionId;
18
+ if(!seen.has(key)){seen.add(key);result.push({title:item.title,revisionId:item.revisionId});}
19
+ }
20
+ return result;
21
+ }
22
+ const key=(page:OpeningSourceCredit)=>page.title+'@'+page.revisionId;
23
+ /** Match actual prompt packets against accepted fetched prose. Never credit a
24
+ * lookup merely because it completed: tools may run on the last allowed call. */
25
+ export function openingSourcesInMessages(messages:unknown,known:ReadonlyMap<string,WikibooksPage>):OpeningSourceCredit[] {
26
+ const found=new Map<string,OpeningSourceCredit>();
27
+ function visit(value:unknown,depth:number) {
28
+ if(depth>30||value==null)return;
29
+ if(typeof value==='string') {
30
+ if(!/^[\s]*[\[{]/.test(value))return;
31
+ try {visit(JSON.parse(value),depth+1);} catch { /* Ordinary prose. */ }
32
+ return;
33
+ }
34
+ if(Array.isArray(value)){for(const item of value)visit(item,depth+1);return;}
35
+ if(typeof value!=='object')return;
36
+ const record=value as Record<string,unknown>;
37
+ if(record.encoding===EVIDENCE_SHARING_VERSION) {
38
+ // Each self-contained packet owns its dictionary. An invalid packet must not
39
+ // leak credits from definitions that were never delivered as content.
40
+ try { visit(expandSharedEvidence(record),depth+1); } catch { /* Invalid shared packet. */ }
41
+ return;
42
+ }
43
+ if(record.authority==='untrusted-opening-commentary'&&Array.isArray(record.pages))for(const page of record.pages) {
44
+ const original=page&&known.get(key(page));
45
+ if(original&&typeof page.prose==='string'&&page.prose.trim()&&page.prose===original.prose) {
46
+ try {const [credit]=parseOpeningSourceCredits([original]);found.set(key(credit),credit);} catch { /* Never persist malformed source metadata. */ }
47
+ }
48
+ }
49
+ for(const item of Object.values(record))visit(item,depth+1);
50
+ }
51
+ visit(messages,0);return [...found.values()];
52
+ }
53
+ export const openingSourceKey=key;
@@ -0,0 +1,98 @@
1
+ import { Chess } from "chess.js";
2
+ import type { ReasoningPositionContext } from "./poc-contracts";
3
+ import type { PositionAnalysisProvider, PositionAnalysisRequest, PositionAnalysisResult } from "./poc-tools";
4
+ import { SIMPLE_EVIDENCE_PREPARATION_VERSION, type SimpleEvidencePreparation } from "./simple-evidence-preparation";
5
+ import { engineSearchFailure } from './engine-search-failure';
6
+
7
+ type Scope = "hypothetical-opponent-turn-before" | "actual-after";
8
+ type LeadLine = { root: string; uci: string; san: string; depth: number; scoreCp: number | null; matePly: number | null; shorterThanRequested: boolean };
9
+
10
+ /** Cheap engine acquisition only. These leads do not register evidence or UI witnesses. */
11
+ export async function prepareOpponentThreats(context: ReasoningPositionContext, engine: PositionAnalysisProvider,
12
+ signal?: AbortSignal, onProgress?: (message: string) => void, options: { parallel?: boolean } = {}): Promise<SimpleEvidencePreparation | undefined> {
13
+ if (context.kind === "position") return undefined;
14
+ const started = performance.now();
15
+ const rawRecords: { scope: Scope; fen: string; request: Omit<PositionAnalysisRequest, "signal">; result: PositionAnalysisResult }[] = [];
16
+ const probes: Record<string, unknown>[] = [];
17
+ const unavailable: ({ scope: Scope; reason: string } & Partial<ReturnType<typeof engineSearchFailure>>)[] = [];
18
+ let searchCount: 0 | 1 | 2 = 0;
19
+ let searchesSucceeded = 0, searchesFailed = 0;
20
+ const targets: { scope: Scope; fen: string; depth: number }[] = [];
21
+ try {
22
+ const before = new Chess(context.fenBefore);
23
+ if (before.isCheck()) unavailable.push({ scope: "hypothetical-opponent-turn-before", reason: "cannot-skip-check" });
24
+ else {
25
+ const fields = context.fenBefore.split(" ");
26
+ fields[1] = before.turn() === "w" ? "b" : "w";
27
+ fields[3] = "-";
28
+ targets.push({ scope: "hypothetical-opponent-turn-before", fen: fields.join(" "), depth: 18 });
29
+ }
30
+ } catch { unavailable.push({ scope: "hypothetical-opponent-turn-before", reason: "invalid-position" }); }
31
+ targets.push({ scope: "actual-after", fen: context.fenAfter, depth: 20 });
32
+ const probeController = options.parallel ? new AbortController() : undefined;
33
+ const workSignal = probeController ? (signal ? AbortSignal.any([signal, probeController.signal]) : probeController.signal) : signal;
34
+ const examine = async (target: typeof targets[number]) => {
35
+ const records: typeof rawRecords = [], targetProbes: typeof probes = [], issues: typeof unavailable = [];
36
+ workSignal?.throwIfAborted();
37
+ let dispatched = false, completed = false;
38
+ try {
39
+ if (new Chess(target.fen).isGameOver()) {
40
+ issues.push({ scope: target.scope, reason: "terminal-position" });
41
+ return { records, targetProbes, issues };
42
+ }
43
+ const request = { fen: target.fen, rootMoves: [], depth: target.depth, multiPv: 3, minimumPvPlies: 10 };
44
+ dispatched = true;
45
+ searchCount = (searchCount + 1) as 1 | 2;
46
+ onProgress?.(`Examining ${target.scope === "actual-after" ? "opponent responses" : "potential opponent plans"} · depth ${target.depth}`);
47
+ const result = await engine.analyze({ ...request, signal: workSignal });
48
+ workSignal?.throwIfAborted();
49
+ searchesSucceeded++;
50
+ completed = true;
51
+ records.push({ scope: target.scope, fen: target.fen, request, result });
52
+ const lines: LeadLine[] = [];
53
+ for (const line of result.lines) {
54
+ try {
55
+ const board = new Chess(target.fen);
56
+ const san = line.pv.map(move => board.move({ from: move.slice(0, 2), to: move.slice(2, 4), promotion: move.slice(4) || undefined }).san);
57
+ if (!line.pv.length || line.pv[0] !== line.rootMove) throw Error("invalid-root");
58
+ lines.push({ root: line.rootMove, uci: line.pv.join(" "), san: san.join(" "), depth: line.depth,
59
+ scoreCp: line.scoreCp, matePly: line.matePly, shorterThanRequested: line.pv.length < 10 });
60
+ } catch { issues.push({ scope: target.scope, reason: "invalid-continuation-omitted" }); }
61
+ }
62
+ targetProbes.push({ scope: target.scope, fen: target.fen,
63
+ requested: { depth: request.depth, multiPv: request.multiPv, minimumPvPlies: request.minimumPvPlies },
64
+ engine: result.engine, engineBuild: result.engineBuild, provenance: result.cacheProvenance,
65
+ sourceProvenance: result.sourceCacheProvenance, command: result.command,
66
+ perspective: target.fen.split(" ")[1] === "w" ? "white" : "black",
67
+ units: "centipawns; positive favors perspective; matePly is signed mate-in-moves (legacy name)",
68
+ coveredRoots: lines.map(line => line.root), lines });
69
+ } catch (error) {
70
+ workSignal?.throwIfAborted();
71
+ if (dispatched && !completed) searchesFailed++;
72
+ issues.push({ scope: target.scope, reason: "probe-unavailable", ...engineSearchFailure(error) });
73
+ }
74
+ return { records, targetProbes, issues };
75
+ };
76
+ let acquired: Awaited<ReturnType<typeof examine>>[];
77
+ if (options.parallel) {
78
+ const work = targets.map(examine);
79
+ try { acquired = await Promise.all(work); }
80
+ catch (error) {
81
+ probeController!.abort(error);
82
+ await Promise.allSettled(work);
83
+ throw error;
84
+ }
85
+ } else {
86
+ acquired = [];
87
+ for (const target of targets) acquired.push(await examine(target));
88
+ }
89
+ // Completion order must not change the preparation evidence's target order.
90
+ for (const result of acquired) {
91
+ rawRecords.push(...result.records);
92
+ probes.push(...result.targetProbes);
93
+ unavailable.push(...result.issues);
94
+ }
95
+ return { version: SIMPLE_EVIDENCE_PREPARATION_VERSION, searchCount, searchesSucceeded, searchesFailed, elapsedMs: performance.now() - started,
96
+ rawRecords, investigationLeads: { version: "opponent-threat-leads/2", fenBefore: context.fenBefore, fenAfter: context.fenAfter, probes, unavailable,
97
+ limitation: "Investigation leads, NOT registered/citable evidence. Hypothetical before flips only turn and clears en-passant: skipping is not a legal move. FEN-root searches omit prior repetition history. These scores are not a measured value of the selected move. Top 3 roots only, not exhaustive defense coverage. Raw engine records retained in diagnostic; tactical geometry omitted here. Use ordinary tools to validate relevant actual lines, captures, checks and recaptures and register interactive references. Do not turn an incidental opponent blunder into the purpose of the move." } };
98
+ }