@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,1602 @@
1
+ import { Chess, SQUARES, type Color, type Move, type PieceSymbol, type Square } from "chess.js";
2
+ import { PARTICIPANT_ROLES, REFERENCE_DOMAINS } from "./concept-registry";
3
+ import {
4
+ CHESS_FACT_RESULT_VERSION,
5
+ CHESS_FACT_SCHEMA_VERSION,
6
+ CHESS_FACT_REQUEST_VERSION,
7
+ validateChessFactRecord,
8
+ validateChessFactRequest,
9
+ type ChessFactAuthority,
10
+ type ChessFactCapability,
11
+ type ChessFactCoverage,
12
+ type ChessFactPhase,
13
+ type ChessFactProvider,
14
+ type ChessFactRecord,
15
+ type ChessFactRequest,
16
+ type ChessFactResult,
17
+ } from "./fact-provider";
18
+ import { reasoningContextHash, validateReasoningPositionContext, type ReasoningPositionContext } from "./poc-contracts";
19
+ import { createPocIdentityTimeline, pieceIdAt } from "./poc-piece-refs";
20
+ import { applyIdentityMove } from "./piece-identities";
21
+ import { exchangeRayAlignmentWitnesses } from "./mechanical-ray-facts";
22
+ import { stableHash } from "./stable";
23
+ import type { IdentityState, RegistryKey, SemanticParticipant, SemanticValue } from "./types";
24
+
25
+ export const MECHANICAL_CHESS_FACT_PROVIDER_ID = "knights-eye/mechanical-chess-facts" as const;
26
+ export const MECHANICAL_CHESS_FACT_PROVIDER_VERSION = "mechanical-chess-facts/18" as const;
27
+
28
+ const MAX_FACT_SUMMARY_CHARACTERS = 1_000;
29
+
30
+ const PIECE_NAMES: Record<PieceSymbol, string> = {
31
+ p: "pawn",
32
+ n: "knight",
33
+ b: "bishop",
34
+ r: "rook",
35
+ q: "queen",
36
+ k: "king",
37
+ };
38
+
39
+ const NOMINAL_VALUES: Record<PieceSymbol, number> = { p: 1, n: 3, b: 3, r: 5, q: 9, k: 0 };
40
+
41
+ const CAPABILITIES: readonly ChessFactCapability[] = Object.freeze([
42
+ { kind: "castling.king_rook_transition", phases: ["transition"], authorities: ["legal-exact"], coverageDomains: ["target-move"], expandable: true, limitations: [] },
43
+ { kind: "king.check_state", phases: ["before", "after"], authorities: ["legal-exact"], coverageDomains: ["side-to-move-check-state"], expandable: true, limitations: [] },
44
+ { kind: "king.checkers_after", phases: ["after"], authorities: ["legal-exact"], coverageDomains: ["checking-pieces-after"], expandable: true, limitations: [] },
45
+ { kind: "king.legal_moves", phases: ["after"], authorities: ["legal-exact"], coverageDomains: ["legal-king-moves-after"], expandable: true, limitations: ["This is exhaustive for king moves, not for every legal response."] },
46
+ { kind: "king.geometry_after", phases: ["after"], authorities: ["legal-exact"], coverageDomains: ["king-square-geometry-after"], expandable: true, limitations: ["Distance and alignment are exact geometry, not an opposition or zugzwang label."] },
47
+ { kind: "king_rook.castling_history", phases: ["path"], authorities: ["legal-exact"], coverageDomains: ["prior-castling-events"], expandable: true, limitations: ["A prior castling event does not by itself establish current king safety or the purpose of the target move."] },
48
+ { kind: "line.occupied_ray_alignment_after_exchange", phases: ["path"], authorities: ["legal-exact"], coverageDomains: ["new-opposing-slider-ray-alignments-with-higher-value-rear-piece-after-one-legal-exchange-witness"], expandable: true, limitations: ["An exact newly created occupied ray does not by itself establish a pin, tactic, forced continuation, or favorable exchange."] },
49
+ { kind: "material.inventory", phases: ["after"], authorities: ["legal-exact"], coverageDomains: ["after-position-piece-inventory"], expandable: true, limitations: [] },
50
+ { kind: "material.nominal_delta", phases: ["transition"], authorities: ["deterministic-derived"], coverageDomains: ["target-move-material"], expandable: true, limitations: ["Nominal values are arithmetic, not an engine evaluation or exchange proof."] },
51
+ { kind: "move.blocks_check_line", phases: ["transition"], authorities: ["legal-exact"], coverageDomains: ["target-move-check-resolution"], expandable: true, limitations: [] },
52
+ { kind: "move.capture_check_delta", phases: ["transition"], authorities: ["legal-exact"], coverageDomains: ["target-move-capture-check"], expandable: true, limitations: [] },
53
+ { kind: "move.capture_response_availability_after", phases: ["after"], authorities: ["deterministic-derived"], coverageDomains: ["target-capture-pre-capture-geometric-defenders-capturing-side-blocked-slider-rays-and-legal-same-square-recaptures-after"], expandable: true, limitations: ["This exhaustively records immediate legal same-square recaptures of an admitted target capture, pre-capture geometric defenders with none, and aligned sliders of the capturing side whose occupied ray prevents that capture. It is not SEE, a safety claim, or a conclusion about exchange quality, intent, or strategic relevance."] },
54
+ { kind: "move.legal_transition", phases: ["transition"], authorities: ["legal-exact"], coverageDomains: ["target-move"], expandable: true, limitations: [] },
55
+ { kind: "move.legal_recaptures_after", phases: ["after"], authorities: ["deterministic-derived"], coverageDomains: ["legal-captures-of-target-mover-after"], expandable: true, limitations: ["Immediate same-square replies and nominal material arithmetic are not SEE, engine evaluation, or proof of longer-term exchange soundness."] },
56
+ { kind: "move.unblocks_line", phases: ["transition"], authorities: ["legal-exact"], coverageDomains: ["target-move-slider-lines"], expandable: true, limitations: [] },
57
+ { kind: "piece.absolute_pin", phases: ["after"], authorities: ["legal-exact"], coverageDomains: ["side-to-move-absolute-pins-after"], expandable: true, limitations: ["Pins are emitted only when the side to move is not already in check."] },
58
+ { kind: "piece.attack_delta", phases: ["transition"], authorities: ["deterministic-derived"], coverageDomains: ["moved-piece-geometric-attacks"], expandable: true, limitations: ["Attack geometry is not exchange safety or a claim that pressure is decisive."] },
59
+ { kind: "piece.legal_moves", phases: ["after"], authorities: ["legal-exact"], coverageDomains: ["legal-moves-from-attacked-piece"], expandable: true, limitations: ["Emitted for occupied opponent pieces newly attacked by the target move."] },
60
+ { kind: "pawn.advance_state", phases: ["transition"], authorities: ["deterministic-derived"], coverageDomains: ["target-pawn-advance"], expandable: true, limitations: ["Forward and diagonal square geometry does not prove that a future pawn move will remain legal after the opponent replies; passed-pawn geometry and distance do not prove promotion or a winning result."] },
61
+ { kind: "position.legal_moves", phases: ["after"], authorities: ["legal-exact"], coverageDomains: ["all-legal-moves-after"], expandable: true, limitations: [] },
62
+ { kind: "position.legal_move_count", phases: ["after"], authorities: ["legal-exact"], coverageDomains: ["all-legal-moves-after"], expandable: true, limitations: [] },
63
+ { kind: "square.friendly_nonpawn_reachers_after", phases: ["after"], authorities: ["deterministic-derived"], coverageDomains: ["target-destination-friendly-nonpawn-reachers"], expandable: true, limitations: ["Movement geometry plus friendly occupancy does not establish that the occupied square is strategically desirable for the other piece."] },
64
+ { kind: "square.target_destination_attackers_after", phases: ["after"], authorities: ["deterministic-derived"], coverageDomains: ["target-move-destination-attack-map-after"], expandable: true, limitations: ["Complete post-move destination attack map, including pawns and pinned pieces; not safety, SEE, or strategy."] },
65
+ { kind: "square.same_square_exchange_witnesses", phases: ["path"], authorities: ["deterministic-derived"], coverageDomains: ["changed-square-exchange-witnesses"], expandable: true, limitations: ["These are legal same-square capture witnesses with nominal material arithmetic, not SEE, forced, best, or exhaustive over moves away from the square."] },
66
+ { kind: "square.attacker_delta", phases: ["transition"], authorities: ["deterministic-derived"], coverageDomains: ["single-square-attacker-set-transition"], expandable: true, limitations: ["Geometric attackers include pinned pieces and do not establish exchange safety or strategic importance. Legal entry/capture witnesses establish only the recorded replay, not whether the entry is strategically desirable."] },
67
+ { kind: "square.attackers_delta_inventory", phases: ["transition"], authorities: ["deterministic-derived"], coverageDomains: ["all-square-attacker-sets-transition"], expandable: true, limitations: ["Panoramic inventory; use the per-square records for concise model presentation."] },
68
+ ]);
69
+
70
+ type CandidateRecord = ChessFactRecord & { priority: number; budgetProtected?: boolean };
71
+
72
+ /** A replayable immediate-capture response fact in the ordinary fact contract. */
73
+ export type AdmittedCaptureResponseAvailabilityFact = ChessFactRecord;
74
+
75
+ export type AdmittedCaptureResponseAvailabilityInput = {
76
+ /** The target-move context that owns stable identities and the record hash. */
77
+ context: ReasoningPositionContext;
78
+ /** Must be the hash of `context`; callers cannot attach a path fact elsewhere. */
79
+ contextHash: string;
80
+ /** Which exact target-context phase the supplied line starts from. */
81
+ start: "before" | "after";
82
+ /** An admitted, fully declared line whose final move must be a legal capture. */
83
+ movesUci: readonly string[];
84
+ };
85
+
86
+ function bytes(value: unknown) {
87
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
88
+ }
89
+
90
+ function opposite(color: Color): Color {
91
+ return color === "w" ? "b" : "w";
92
+ }
93
+
94
+ function colorName(color: Color): string {
95
+ return color === "w" ? "White" : "Black";
96
+ }
97
+
98
+ function moveUci(move: { from: string; to: string; promotion?: string }) {
99
+ return `${move.from}${move.to}${move.promotion || ""}`;
100
+ }
101
+
102
+ function squareCoordinates(square: Square) {
103
+ return { file: square.charCodeAt(0) - 97, rank: Number(square[1]) - 1 };
104
+ }
105
+
106
+ export function squaresBetween(from: Square, to: Square): Square[] {
107
+ const left = squareCoordinates(from);
108
+ const right = squareCoordinates(to);
109
+ const fileDelta = right.file - left.file;
110
+ const rankDelta = right.rank - left.rank;
111
+ const aligned = fileDelta === 0 || rankDelta === 0 || Math.abs(fileDelta) === Math.abs(rankDelta);
112
+ if (!aligned) return [];
113
+ const fileStep = Math.sign(fileDelta);
114
+ const rankStep = Math.sign(rankDelta);
115
+ const squares: Square[] = [];
116
+ let file = left.file + fileStep;
117
+ let rank = left.rank + rankStep;
118
+ while (file !== right.file || rank !== right.rank) {
119
+ squares.push(`${String.fromCharCode(97 + file)}${rank + 1}` as Square);
120
+ file += fileStep;
121
+ rank += rankStep;
122
+ }
123
+ return squares;
124
+ }
125
+
126
+ function attackedSquares(board: Chess, origin: Square, color: Color): Square[] {
127
+ return SQUARES.filter((square) => board.attackers(square, color).includes(origin));
128
+ }
129
+
130
+ function stableAttackers(board: Chess, state: IdentityState, square: Square, color: Color) {
131
+ return board.attackers(square, color).map((origin) => ({
132
+ pieceId: pieceIdAt(state, origin),
133
+ currentSquare: origin,
134
+ pieceType: board.get(origin)?.type || null,
135
+ })).filter((item): item is { pieceId: string; currentSquare: Square; pieceType: PieceSymbol } => Boolean(item.pieceId && item.pieceType));
136
+ }
137
+
138
+ function sliderReachesSquare(pieceType: PieceSymbol, from: Square, to: Square) {
139
+ const left = squareCoordinates(from);
140
+ const right = squareCoordinates(to);
141
+ const fileDistance = Math.abs(right.file - left.file);
142
+ const rankDistance = Math.abs(right.rank - left.rank);
143
+ if (fileDistance === 0 && rankDistance === 0) return false;
144
+ const orthogonal = fileDistance === 0 || rankDistance === 0;
145
+ const diagonal = fileDistance === rankDistance;
146
+ return pieceType === "r" ? orthogonal : pieceType === "b" ? diagonal : pieceType === "q" ? orthogonal || diagonal : false;
147
+ }
148
+
149
+ function blockedSliderRecaptureRays(
150
+ board: Chess,
151
+ state: IdentityState,
152
+ targetSquare: Square,
153
+ color: Color,
154
+ ) {
155
+ return SQUARES.flatMap((sliderSquare) => {
156
+ const slider = board.get(sliderSquare);
157
+ if (!slider || slider.color !== color || !sliderReachesSquare(slider.type, sliderSquare, targetSquare)) return [];
158
+ const between = squaresBetween(sliderSquare, targetSquare);
159
+ const occupiedBlockingSquares = between.flatMap((square) => {
160
+ const blocker = board.get(square);
161
+ const pieceId = pieceIdAt(state, square);
162
+ return blocker && pieceId ? [{
163
+ pieceId,
164
+ currentSquare: square,
165
+ pieceType: blocker.type,
166
+ color: blocker.color,
167
+ }] : [];
168
+ });
169
+ const pieceId = pieceIdAt(state, sliderSquare);
170
+ if (!pieceId || !occupiedBlockingSquares.length) return [];
171
+ return [{
172
+ pieceId,
173
+ currentSquare: sliderSquare,
174
+ pieceType: slider.type,
175
+ ray: [sliderSquare, ...between, targetSquare],
176
+ occupiedBlockingSquares,
177
+ }];
178
+ });
179
+ }
180
+
181
+ function attackerSetChanges(
182
+ before: Chess,
183
+ after: Chess,
184
+ timeline: ReturnType<typeof createPocIdentityTimeline>,
185
+ ) {
186
+ return (["w", "b"] as Color[]).flatMap((color) => SQUARES.flatMap((square) => {
187
+ const beforeAttackers = stableAttackers(before, timeline.before, square, color);
188
+ const afterAttackers = stableAttackers(after, timeline.after, square, color);
189
+ const beforeIds = new Set(beforeAttackers.map((item) => item.pieceId));
190
+ const afterIds = new Set(afterAttackers.map((item) => item.pieceId));
191
+ const lost = beforeAttackers.filter((item) => !afterIds.has(item.pieceId));
192
+ const gained = afterAttackers.filter((item) => !beforeIds.has(item.pieceId));
193
+ if (!lost.length && !gained.length) return [];
194
+ return [{ square, color, beforeAttackers, afterAttackers, lost, gained }];
195
+ }));
196
+ }
197
+
198
+ function inventory(board: Chess, state: IdentityState) {
199
+ return board.board().flat().filter(Boolean).map((piece) => ({
200
+ pieceId: pieceIdAt(state, piece!.square),
201
+ color: piece!.color,
202
+ type: piece!.type,
203
+ square: piece!.square,
204
+ }));
205
+ }
206
+
207
+ function nominalMaterial(board: Chess, color: Color) {
208
+ return board.board().flat().filter((piece) => piece?.color === color).reduce((total, piece) => total + NOMINAL_VALUES[piece!.type], 0);
209
+ }
210
+
211
+ function nominalMaterialBalance(board: Chess, color: Color) {
212
+ return nominalMaterial(board, color) - nominalMaterial(board, opposite(color));
213
+ }
214
+
215
+ function applyVerboseMove(board: Chess, move: Pick<Move, "from" | "to" | "promotion">) {
216
+ return board.move({ from: move.from, to: move.to, promotion: move.promotion });
217
+ }
218
+
219
+ function immediateCaptureOutcomes(fenAfter: string, captures: Move[]) {
220
+ const baseline = new Chess(fenAfter);
221
+ return captures.map((capture) => {
222
+ const afterCapture = new Chess(fenAfter);
223
+ applyVerboseMove(afterCapture, capture);
224
+ const baselineBalance = nominalMaterialBalance(baseline, capture.color);
225
+ const immediateSameSquareRecaptures = afterCapture.moves({ verbose: true })
226
+ .filter((reply) => reply.to === capture.to && Boolean(reply.captured))
227
+ .map((reply) => {
228
+ const afterReply = new Chess(afterCapture.fen());
229
+ applyVerboseMove(afterReply, reply);
230
+ return {
231
+ uci: moveUci(reply),
232
+ san: reply.san,
233
+ nominalBalanceDeltaForCapturingSide: nominalMaterialBalance(afterReply, capture.color) - baselineBalance,
234
+ };
235
+ });
236
+ return {
237
+ capture: { uci: moveUci(capture), san: capture.san },
238
+ capturingColor: capture.color,
239
+ nominalBalanceDeltaAfterCapture: nominalMaterialBalance(afterCapture, capture.color) - baselineBalance,
240
+ immediateSameSquareRecaptures,
241
+ minimumNominalBalanceDeltaAfterImmediateSameSquareRecapture: immediateSameSquareRecaptures.length
242
+ ? Math.min(...immediateSameSquareRecaptures.map((reply) => reply.nominalBalanceDeltaForCapturingSide))
243
+ : null,
244
+ };
245
+ });
246
+ }
247
+
248
+ function pseudoCaptureKingExposure(
249
+ board: Chess,
250
+ from: Square,
251
+ to: Square,
252
+ color: Color,
253
+ ) {
254
+ const defender = board.get(from);
255
+ if (!defender || defender.color !== color) return null;
256
+ const replay = new Chess(board.fen());
257
+ replay.remove(from);
258
+ replay.remove(to);
259
+ replay.put({ type: defender.type, color: defender.color }, to);
260
+ const kingSquare = replay.findPiece({ type: "k", color })[0];
261
+ if (!kingSquare) return null;
262
+ const checkerSquares = replay.attackers(kingSquare, opposite(color));
263
+ if (!checkerSquares.length) return null;
264
+ return {
265
+ kind: "own-king-exposure",
266
+ kingSquare,
267
+ checkerSquares,
268
+ checkingLines: checkerSquares.map((checkerSquare) => ({
269
+ checkerSquare,
270
+ ray: [checkerSquare, ...squaresBetween(checkerSquare, kingSquare), kingSquare],
271
+ })),
272
+ };
273
+ }
274
+
275
+ function absolutePinRayConstraints(
276
+ board: Chess,
277
+ defenderSquare: Square,
278
+ color: Color,
279
+ ) {
280
+ if (board.isCheck()) return [];
281
+ const kingSquare = board.findPiece({ type: "k", color })[0];
282
+ if (!kingSquare) return [];
283
+ const removed = new Chess(board.fen());
284
+ removed.remove(defenderSquare);
285
+ return removed.attackers(kingSquare, opposite(color)).flatMap((pinnerSquare) => {
286
+ const pinner = removed.get(pinnerSquare);
287
+ if (!pinner || !new Set<PieceSymbol>(["b", "r", "q"]).has(pinner.type)
288
+ || !squaresBetween(pinnerSquare, kingSquare).includes(defenderSquare)) return [];
289
+ return [{
290
+ kind: "absolute-pin-ray",
291
+ pinnerSquare,
292
+ pinnerType: pinner.type,
293
+ kingSquare,
294
+ ray: [pinnerSquare, ...squaresBetween(pinnerSquare, kingSquare), kingSquare],
295
+ }];
296
+ });
297
+ }
298
+
299
+ function sameSquareLineOutcomes(fenAfter: string, lines: ReturnType<typeof sameSquareExchangeWitnesses>) {
300
+ return lines.map((line) => {
301
+ const baseline = new Chess(fenAfter);
302
+ const board = new Chess(fenAfter);
303
+ let firstCapturingColor: Color | null = null;
304
+ let baselineBalance = 0;
305
+ const nominalBalanceDeltasAfterEachPly: number[] = [];
306
+ for (const item of line) {
307
+ const legal = board.moves({ verbose: true }).find((move) => moveUci(move) === item.uci);
308
+ if (!legal) throw new Error(`Same-square witness contains an illegal move: ${item.uci}`);
309
+ if (!firstCapturingColor && legal.captured) {
310
+ firstCapturingColor = legal.color;
311
+ baselineBalance = nominalMaterialBalance(baseline, legal.color);
312
+ }
313
+ applyVerboseMove(board, legal);
314
+ nominalBalanceDeltasAfterEachPly.push(firstCapturingColor
315
+ ? nominalMaterialBalance(board, firstCapturingColor) - baselineBalance
316
+ : 0);
317
+ }
318
+ return {
319
+ movesUci: line.map((item) => item.uci),
320
+ firstCapturingColor,
321
+ nominalBalanceDeltasAfterEachPly,
322
+ finalNominalBalanceDeltaForFirstCapturingSide: firstCapturingColor
323
+ ? nominalMaterialBalance(board, firstCapturingColor) - baselineBalance
324
+ : null,
325
+ };
326
+ });
327
+ }
328
+
329
+ function kingDistance(left: Square, right: Square) {
330
+ const a = squareCoordinates(left);
331
+ const b = squareCoordinates(right);
332
+ return {
333
+ file: Math.abs(a.file - b.file),
334
+ rank: Math.abs(a.rank - b.rank),
335
+ chebyshev: Math.max(Math.abs(a.file - b.file), Math.abs(a.rank - b.rank)),
336
+ manhattan: Math.abs(a.file - b.file) + Math.abs(a.rank - b.rank),
337
+ };
338
+ }
339
+
340
+ function isPassedPawn(board: Chess, square: Square, color: Color) {
341
+ const location = squareCoordinates(square);
342
+ const opponent = opposite(color);
343
+ const blockers = board.findPiece({ type: "p", color: opponent }).filter((candidate) => {
344
+ const target = squareCoordinates(candidate);
345
+ const ahead = color === "w" ? target.rank > location.rank : target.rank < location.rank;
346
+ return ahead && Math.abs(target.file - location.file) <= 1;
347
+ });
348
+ return { passed: blockers.length === 0, opposingPawnBlockers: blockers };
349
+ }
350
+
351
+ function priorCastlingEvents(context: ReasoningPositionContext) {
352
+ const board = context.historyStartFen ? new Chess(context.historyStartFen) : new Chess();
353
+ return context.historyUci.flatMap((uci, plyIndex) => {
354
+ const move = board.move({ from: uci.slice(0, 2), to: uci.slice(2, 4), promotion: uci.slice(4) || undefined });
355
+ if (!move || (!move.isKingsideCastle() && !move.isQueensideCastle())) return [];
356
+ const rank = move.color === "w" ? "1" : "8";
357
+ return [{
358
+ plyIndex,
359
+ color: move.color,
360
+ uci,
361
+ san: move.san,
362
+ king: { from: move.from, to: move.to },
363
+ rook: {
364
+ from: `${move.isKingsideCastle() ? "h" : "a"}${rank}`,
365
+ to: `${move.isKingsideCastle() ? "f" : "d"}${rank}`,
366
+ },
367
+ }];
368
+ });
369
+ }
370
+
371
+ function sameSquareExchangeWitnesses(fenAfter: string, square: Square) {
372
+ const root = new Chess(fenAfter);
373
+ const entries = root.moves({ verbose: true }).filter((move) => move.to === square);
374
+ return entries.flatMap((entry) => {
375
+ const afterEntry = new Chess(fenAfter);
376
+ afterEntry.move({ from: entry.from, to: entry.to, promotion: entry.promotion });
377
+ const captures = afterEntry.moves({ verbose: true }).filter((move) => move.to === square && Boolean(move.captured));
378
+ return captures.flatMap((capture) => {
379
+ const afterCapture = new Chess(afterEntry.fen());
380
+ afterCapture.move({ from: capture.from, to: capture.to, promotion: capture.promotion });
381
+ const recaptures = afterCapture.moves({ verbose: true }).filter((move) => move.to === square && Boolean(move.captured));
382
+ const prefix = [
383
+ { uci: moveUci(entry), san: entry.san },
384
+ { uci: moveUci(capture), san: capture.san },
385
+ ];
386
+ return recaptures.length
387
+ ? recaptures.map((recapture) => [...prefix, { uci: moveUci(recapture), san: recapture.san }])
388
+ : [prefix];
389
+ });
390
+ });
391
+ }
392
+
393
+ function participantForSquare(
394
+ state: IdentityState,
395
+ square: Square,
396
+ role: RegistryKey,
397
+ ): { participant: SemanticParticipant; location: Record<string, SemanticValue> } | null {
398
+ const pieceId = pieceIdAt(state, square);
399
+ if (!pieceId) return null;
400
+ return {
401
+ participant: { role, ref: { domain: REFERENCE_DOMAINS.PIECE, id: pieceId } },
402
+ location: { pieceId, currentSquare: square },
403
+ };
404
+ }
405
+
406
+ function targetMoveParticipants(
407
+ timeline: ReturnType<typeof createPocIdentityTimeline>,
408
+ from: Square,
409
+ to: Square,
410
+ uci: string,
411
+ ): { participants: SemanticParticipant[]; locations: Record<string, SemanticValue>[]; afterParticipants: SemanticParticipant[] } {
412
+ const actorBefore = participantForSquare(timeline.before, from, PARTICIPANT_ROLES.ACTOR);
413
+ const actorAfter = participantForSquare(timeline.after, to, PARTICIPANT_ROLES.ACTOR);
414
+ const moveParticipant = { role: PARTICIPANT_ROLES.ACTOR, ref: { domain: REFERENCE_DOMAINS.MOVE, id: uci } };
415
+ const originParticipant = { role: "origin", ref: { domain: REFERENCE_DOMAINS.SQUARE, id: from } };
416
+ const destinationParticipant = { role: PARTICIPANT_ROLES.DESTINATION, ref: { domain: REFERENCE_DOMAINS.SQUARE, id: to } };
417
+ // Promotion retires the pawn identity and creates a distinct piece identity.
418
+ // Retain both ends of that transition instead of placing the retired pawn on
419
+ // the promotion square, and let after-only facts select the resulting piece.
420
+ const actorAfterAddsIdentity = actorAfter && actorAfter.participant.ref.id !== actorBefore?.participant.ref.id;
421
+ return {
422
+ participants: [
423
+ moveParticipant,
424
+ ...(actorBefore ? [actorBefore.participant] : []),
425
+ ...(actorAfterAddsIdentity ? [actorAfter.participant] : []),
426
+ originParticipant,
427
+ destinationParticipant,
428
+ ],
429
+ locations: [
430
+ ...(actorBefore ? [{ ...actorBefore.location, phase: "before" }] : []),
431
+ ...(actorAfter ? [{ ...actorAfter.location, phase: "after" }] : []),
432
+ ],
433
+ afterParticipants: [moveParticipant, ...(actorAfter ? [actorAfter.participant] : []), destinationParticipant],
434
+ };
435
+ }
436
+
437
+ function phaseState(
438
+ timeline: ReturnType<typeof createPocIdentityTimeline>,
439
+ phase: "before" | "after",
440
+ ) {
441
+ return phase === "before" ? timeline.before : timeline.after;
442
+ }
443
+
444
+ function createRecord(input: {
445
+ contextHash: string;
446
+ kind: string;
447
+ phase: ChessFactPhase;
448
+ authority: ChessFactAuthority;
449
+ priority: number;
450
+ /**
451
+ * One direct target-move fact may be retained ahead of ordinary presentation
452
+ * budgeting. This is deliberately limited to exact destination geometry;
453
+ * it prevents a fact-cap from silently removing the board state needed to
454
+ * inspect an immediate capture, without making any safety conclusion.
455
+ */
456
+ budgetProtected?: boolean;
457
+ summary: string;
458
+ participants: SemanticParticipant[];
459
+ data: Record<string, SemanticValue>;
460
+ pieceLocations: Record<string, SemanticValue>[];
461
+ coverage: ChessFactCoverage;
462
+ replay: Record<string, SemanticValue> | null;
463
+ derivedFromIds?: string[];
464
+ }): CandidateRecord {
465
+ const summaryTruncated = input.summary.length > MAX_FACT_SUMMARY_CHARACTERS;
466
+ const summary = summaryTruncated
467
+ ? `${input.summary.slice(0, MAX_FACT_SUMMARY_CHARACTERS - 1).trimEnd()}…`
468
+ : input.summary;
469
+ const data = {
470
+ ...input.data,
471
+ pieceLocations: input.pieceLocations,
472
+ ...(summaryTruncated ? { summaryTruncated: true } : {}),
473
+ };
474
+ const identity = {
475
+ provider: MECHANICAL_CHESS_FACT_PROVIDER_ID,
476
+ version: MECHANICAL_CHESS_FACT_PROVIDER_VERSION,
477
+ contextHash: input.contextHash,
478
+ kind: input.kind,
479
+ phase: input.phase,
480
+ data,
481
+ };
482
+ const record: CandidateRecord = {
483
+ schemaVersion: CHESS_FACT_SCHEMA_VERSION,
484
+ id: `fact:${stableHash(identity)}`,
485
+ contextHash: input.contextHash,
486
+ kind: input.kind,
487
+ phase: input.phase,
488
+ authority: input.authority,
489
+ summary,
490
+ participants: input.participants,
491
+ data,
492
+ coverage: input.coverage,
493
+ provenance: {
494
+ provider: MECHANICAL_CHESS_FACT_PROVIDER_ID,
495
+ version: MECHANICAL_CHESS_FACT_PROVIDER_VERSION,
496
+ method: input.authority === "legal-exact" ? "chess.js-legal-state" : "chess.js-deterministic-delta",
497
+ derivedFromIds: input.derivedFromIds || [],
498
+ },
499
+ replay: input.replay,
500
+ priority: input.priority,
501
+ ...(input.budgetProtected ? { budgetProtected: true } : {}),
502
+ };
503
+ const validated = { ...record };
504
+ delete (validated as Partial<CandidateRecord>).priority;
505
+ delete (validated as Partial<CandidateRecord>).budgetProtected;
506
+ validateChessFactRecord(validated);
507
+ return record;
508
+ }
509
+
510
+ /**
511
+ * Derive the immediate legal recapture availability after a capture already
512
+ * admitted by a replayable line. This never discovers a continuation: the
513
+ * caller supplies every UCI move, and the result is rooted at the declared
514
+ * target-context phase. Stable identities are replayed along that exact
515
+ * prefix so a later capture can still name the original piece accurately.
516
+ */
517
+ export function deriveAdmittedCaptureResponseAvailabilityFact(
518
+ input: AdmittedCaptureResponseAvailabilityInput,
519
+ ): AdmittedCaptureResponseAvailabilityFact {
520
+ validateReasoningPositionContext(input.context);
521
+ const rootContextHash = reasoningContextHash(input.context);
522
+ if (input.contextHash !== rootContextHash) {
523
+ throw new Error("Admitted capture response fact contextHash does not match its root context");
524
+ }
525
+ if (input.start !== "before" && input.start !== "after") {
526
+ throw new Error("Admitted capture response fact start must be before or after");
527
+ }
528
+ if (!Array.isArray(input.movesUci) || input.movesUci.length === 0) {
529
+ throw new Error("Admitted capture response fact requires a non-empty admitted UCI line");
530
+ }
531
+ if (input.movesUci.some((uci) => !/^[a-h][1-8][a-h][1-8][qrbn]?$/.test(uci))) {
532
+ throw new Error("Admitted capture response fact line contains invalid UCI");
533
+ }
534
+
535
+ const rootTimeline = createPocIdentityTimeline(input.context);
536
+ const replayFen = input.start === "before" ? input.context.fenBefore : input.context.fenAfter;
537
+ const board = new Chess(replayFen);
538
+ let state = input.start === "before" ? rootTimeline.before : rootTimeline.after;
539
+ if (state.fen !== board.fen()) throw new Error("Admitted capture response fact identity start does not match replay phase");
540
+
541
+ let beforeCapture: Chess | null = null;
542
+ let beforeCaptureState: IdentityState | null = null;
543
+ let capture: Move | null = null;
544
+ for (const [index, uci] of input.movesUci.entries()) {
545
+ const legal = board.moves({ verbose: true }).find((move) => moveUci(move) === uci);
546
+ if (!legal) throw new Error(`Admitted capture response fact line is illegal at ply ${index}: ${uci}`);
547
+ if (index === input.movesUci.length - 1) {
548
+ beforeCapture = new Chess(board.fen());
549
+ beforeCaptureState = state;
550
+ capture = legal;
551
+ }
552
+ let nextState: IdentityState;
553
+ try {
554
+ nextState = applyIdentityMove(state, {
555
+ from: legal.from,
556
+ to: legal.to,
557
+ promotion: legal.promotion,
558
+ }).state;
559
+ } catch {
560
+ throw new Error(`Admitted capture response fact stable-identity replay failed at ply ${index}: ${uci}`);
561
+ }
562
+ applyVerboseMove(board, legal);
563
+ if (nextState.fen !== board.fen()) throw new Error(`Admitted capture response fact identity replay mismatch at ply ${index}: ${uci}`);
564
+ state = nextState;
565
+ }
566
+ if (!capture || !beforeCapture || !beforeCaptureState) throw new Error("Admitted capture response fact failed to replay its final move");
567
+ if (!capture.captured) throw new Error("Admitted capture response fact final move must be a capture");
568
+
569
+ const afterCapture = board;
570
+ const afterCaptureState = state;
571
+ const from = capture.from as Square;
572
+ const to = capture.to as Square;
573
+ const moverColor = capture.color;
574
+ const captureTimeline = { before: beforeCaptureState, after: afterCaptureState } as ReturnType<typeof createPocIdentityTimeline>;
575
+ const target = targetMoveParticipants(captureTimeline, from, to, moveUci(capture));
576
+ const captureSquare = (capture.isEnPassant() ? `${to[0]}${from[1]}` : to) as Square;
577
+ const captured = participantForSquare(beforeCaptureState, captureSquare, PARTICIPANT_ROLES.TARGET);
578
+ if (captured) {
579
+ target.participants.push(captured.participant);
580
+ target.locations.push({ ...captured.location, phase: "before" });
581
+ }
582
+
583
+ const movedPieceAfter = afterCapture.get(to);
584
+ if (!movedPieceAfter) throw new Error("Admitted capture response fact final capture has no mover on its destination");
585
+ const legalRecaptures = afterCapture.moves({ verbose: true }).filter((reply) => (
586
+ reply.to === to && reply.captured === movedPieceAfter.type
587
+ ));
588
+ const defenderColor = opposite(moverColor);
589
+ const preCaptureGeometricDefenders = stableAttackers(beforeCapture, beforeCaptureState, to, defenderColor);
590
+ const preCaptureBlockedSliderCapturers = blockedSliderRecaptureRays(beforeCapture, beforeCaptureState, to, moverColor);
591
+ const responseSquares = [
592
+ ...preCaptureGeometricDefenders.map((defender) => defender.currentSquare),
593
+ ...preCaptureBlockedSliderCapturers.flatMap((slider) => [
594
+ slider.currentSquare,
595
+ ...slider.occupiedBlockingSquares.map((blocker) => blocker.currentSquare),
596
+ ]),
597
+ ].filter((square, index, all) => all.indexOf(square) === index);
598
+ const responseParticipants = responseSquares.flatMap((square) => {
599
+ const participant = participantForSquare(beforeCaptureState, square, PARTICIPANT_ROLES.RESOURCE);
600
+ return participant ? [{ square, participant }] : [];
601
+ });
602
+ const defenders = preCaptureGeometricDefenders.map((defender) => {
603
+ const legalSameSquareRecaptures = legalRecaptures.filter((reply) => (
604
+ pieceIdAt(afterCaptureState, reply.from as Square) === defender.pieceId
605
+ ));
606
+ const ownKingExposure = pseudoCaptureKingExposure(afterCapture, defender.currentSquare, to, defenderColor);
607
+ const exclusionConstraints = legalSameSquareRecaptures.length
608
+ ? []
609
+ : [
610
+ ...absolutePinRayConstraints(afterCapture, defender.currentSquare, defenderColor),
611
+ ...(ownKingExposure ? [ownKingExposure] : []),
612
+ ];
613
+ return {
614
+ ...defender,
615
+ legalSameSquareRecaptures: legalSameSquareRecaptures.map((reply) => ({
616
+ uci: moveUci(reply),
617
+ san: reply.san,
618
+ from: reply.from,
619
+ to: reply.to,
620
+ })),
621
+ exclusionConstraints,
622
+ };
623
+ });
624
+ const unavailableGeometricDefenders = defenders.filter((defender) => defender.legalSameSquareRecaptures.length === 0);
625
+ const responsePieceLocations: Record<string, SemanticValue>[] = responseParticipants.flatMap(({ square, participant }) => [
626
+ { pieceId: participant.participant.ref.id, currentSquare: square, phase: "before" },
627
+ { pieceId: participant.participant.ref.id, currentSquare: square, phase: "after" },
628
+ ]);
629
+ const responseLocations = [...target.locations, ...responsePieceLocations]
630
+ .filter((item, index, all) => all.findIndex((candidate) => candidate.pieceId === item.pieceId && candidate.currentSquare === item.currentSquare && candidate.phase === item.phase) === index);
631
+ const isDirectTargetCapture = input.start === "before"
632
+ && input.movesUci.length === 1
633
+ && input.movesUci[0] === input.context.moveUci;
634
+ const record = createRecord({
635
+ contextHash: input.contextHash,
636
+ kind: "move.capture_response_availability_after",
637
+ phase: "after",
638
+ authority: "deterministic-derived",
639
+ priority: unavailableGeometricDefenders.length || preCaptureBlockedSliderCapturers.length ? 95 : legalRecaptures.length ? 84 : 72,
640
+ summary: `After the admitted capture ${capture.san} on ${to}, legal same-square recaptures are ${legalRecaptures.map((reply) => reply.san).join(", ") || "none"}. Pre-capture geometric defenders of ${to} with no legal same-square recapture are ${unavailableGeometricDefenders.map((defender) => defender.currentSquare).join(", ") || "none"}. Pre-capture aligned sliders of the capturing side blocked by occupied rays are ${preCaptureBlockedSliderCapturers.map((slider) => slider.currentSquare).join(", ") || "none"}. This records legal availability and exact ray geometry only; it does not establish safety, exchange quality, intent, or strategic relevance.`,
641
+ participants: [...target.participants, ...responseParticipants.map((item) => item.participant.participant)]
642
+ .filter((item, index, all) => all.findIndex((candidate) => candidate.role === item.role && candidate.ref.domain === item.ref.domain && candidate.ref.id === item.ref.id) === index),
643
+ data: {
644
+ capture: {
645
+ uci: moveUci(capture),
646
+ san: capture.san,
647
+ from,
648
+ to,
649
+ capturedSquare: captureSquare,
650
+ capturedType: capture.captured,
651
+ },
652
+ defenderColor,
653
+ preCaptureGeometricDefenders: defenders,
654
+ preCaptureBlockedSliderCapturers,
655
+ legalSameSquareRecaptures: legalRecaptures.map((reply) => ({
656
+ uci: moveUci(reply),
657
+ san: reply.san,
658
+ from: reply.from,
659
+ to: reply.to,
660
+ moverPieceId: pieceIdAt(afterCaptureState, reply.from as Square),
661
+ })),
662
+ unavailableGeometricDefenderPieceIds: unavailableGeometricDefenders.map((defender) => defender.pieceId),
663
+ ...(isDirectTargetCapture ? {} : { admittedReplay: { start: input.start, movesUci: [...input.movesUci] } }),
664
+ },
665
+ pieceLocations: responseLocations,
666
+ coverage: {
667
+ domain: "target-capture-pre-capture-geometric-defenders-capturing-side-blocked-slider-rays-and-legal-same-square-recaptures-after",
668
+ exhaustive: true,
669
+ covered: [
670
+ moveUci(capture),
671
+ ...preCaptureGeometricDefenders.map((defender) => defender.pieceId),
672
+ ...preCaptureBlockedSliderCapturers.flatMap((slider) => [slider.pieceId, ...slider.occupiedBlockingSquares.map((blocker) => blocker.pieceId)]),
673
+ ...legalRecaptures.map(moveUci),
674
+ ],
675
+ unresolved: [],
676
+ },
677
+ replay: {
678
+ fen: replayFen,
679
+ movesUci: [...input.movesUci],
680
+ linesUci: legalRecaptures.length
681
+ ? legalRecaptures.map((reply) => [...input.movesUci, moveUci(reply)])
682
+ : [[...input.movesUci]],
683
+ },
684
+ });
685
+ const fact = { ...record };
686
+ Reflect.deleteProperty(fact, "priority");
687
+ Reflect.deleteProperty(fact, "budgetProtected");
688
+ return fact;
689
+ }
690
+
691
+ function checkRecord(input: {
692
+ board: Chess;
693
+ phase: "before" | "after";
694
+ timeline: ReturnType<typeof createPocIdentityTimeline>;
695
+ contextHash: string;
696
+ fen: string;
697
+ }): CandidateRecord {
698
+ const color = input.board.turn();
699
+ const kingSquare = input.board.findPiece({ type: "k", color })[0];
700
+ if (!kingSquare) throw new Error("Mechanical fact provider requires both kings");
701
+ const checkerSquares = input.board.attackers(kingSquare, opposite(color));
702
+ const state = phaseState(input.timeline, input.phase);
703
+ const king = participantForSquare(state, kingSquare, PARTICIPANT_ROLES.TARGET);
704
+ const checkers = checkerSquares.flatMap((square) => {
705
+ const participant = participantForSquare(state, square, PARTICIPANT_ROLES.ACTOR);
706
+ return participant ? [participant] : [];
707
+ });
708
+ return createRecord({
709
+ contextHash: input.contextHash,
710
+ kind: "king.check_state",
711
+ phase: input.phase,
712
+ authority: "legal-exact",
713
+ priority: input.phase === "before" && checkerSquares.length ? 99 : input.phase === "after" && checkerSquares.length ? 97 : 52,
714
+ summary: checkerSquares.length
715
+ ? `${color === "w" ? "White" : "Black"} is in check; the checking piece${checkerSquares.length === 1 ? " is" : "s are"} on ${checkerSquares.join(", ")}.`
716
+ : `${color === "w" ? "White" : "Black"}, the side to move, is not in check.`,
717
+ participants: [...(king ? [king.participant] : []), ...checkers.map((item) => item.participant)],
718
+ data: {
719
+ sideToMove: color,
720
+ kingSquare,
721
+ inCheck: checkerSquares.length > 0,
722
+ checkerSquares,
723
+ checkingLines: checkerSquares.map((square) => ({ checker: square, between: squaresBetween(square, kingSquare) })),
724
+ },
725
+ pieceLocations: [...(king ? [{ ...king.location, phase: input.phase }] : []), ...checkers.map((item) => ({ ...item.location, phase: input.phase }))],
726
+ coverage: { domain: `${input.phase}-side-to-move-check-state`, exhaustive: true, covered: [kingSquare, ...checkerSquares], unresolved: [] },
727
+ replay: { fen: input.fen },
728
+ });
729
+ }
730
+
731
+ function pinRecord(input: {
732
+ board: Chess;
733
+ timeline: ReturnType<typeof createPocIdentityTimeline>;
734
+ contextHash: string;
735
+ fen: string;
736
+ }): CandidateRecord {
737
+ const color = input.board.turn();
738
+ const enemy = opposite(color);
739
+ const kingSquare = input.board.findPiece({ type: "k", color })[0];
740
+ if (!kingSquare) throw new Error("Mechanical fact provider requires both kings");
741
+ const pins: Array<Record<string, SemanticValue>> = [];
742
+ const participants: SemanticParticipant[] = [];
743
+ const pieceLocations: Record<string, SemanticValue>[] = [];
744
+ if (!input.board.isCheck()) {
745
+ for (const square of input.board.findPiece({ color, type: "p" })
746
+ .concat(input.board.findPiece({ color, type: "n" }), input.board.findPiece({ color, type: "b" }), input.board.findPiece({ color, type: "r" }), input.board.findPiece({ color, type: "q" }))) {
747
+ const removed = new Chess(input.fen);
748
+ removed.remove(square);
749
+ const pinners = removed.attackers(kingSquare, enemy).filter((origin) => {
750
+ const piece = removed.get(origin);
751
+ return piece && new Set<PieceSymbol>(["b", "r", "q"]).has(piece.type) && squaresBetween(origin, kingSquare).includes(square);
752
+ });
753
+ for (const pinnerSquare of pinners) {
754
+ const pinned = participantForSquare(input.timeline.after, square, PARTICIPANT_ROLES.TARGET);
755
+ const pinner = participantForSquare(input.timeline.after, pinnerSquare, PARTICIPANT_ROLES.ACTOR);
756
+ const king = participantForSquare(input.timeline.after, kingSquare, PARTICIPANT_ROLES.GOAL);
757
+ const piece = input.board.get(square);
758
+ if (!pinned || !pinner || !king || !piece) continue;
759
+ const moves = input.board.moves({ square, verbose: true });
760
+ pins.push({ pinnedSquare: square, pinnedType: piece.type, pinnerSquare, pinnerType: input.board.get(pinnerSquare)!.type, kingSquare, ray: [pinnerSquare, ...squaresBetween(pinnerSquare, kingSquare), kingSquare], legalMovesUci: moves.map((legalMove) => `${legalMove.from}${legalMove.to}${legalMove.promotion || ""}`) });
761
+ participants.push(pinner.participant, pinned.participant, king.participant);
762
+ pieceLocations.push({ ...pinner.location, phase: "after" }, { ...pinned.location, phase: "after" }, { ...king.location, phase: "after" });
763
+ }
764
+ }
765
+ }
766
+ const uniqueParticipants = participants.filter((item, index, all) => all.findIndex((candidate) => candidate.role === item.role && candidate.ref.domain === item.ref.domain && candidate.ref.id === item.ref.id) === index);
767
+ const uniqueLocations = pieceLocations.filter((item, index, all) => all.findIndex((candidate) => candidate.pieceId === item.pieceId && candidate.currentSquare === item.currentSquare) === index);
768
+ const exhaustive = !input.board.isCheck();
769
+ return createRecord({
770
+ contextHash: input.contextHash,
771
+ kind: "piece.absolute_pin",
772
+ phase: "after",
773
+ authority: "legal-exact",
774
+ priority: pins.length ? 89 : 45,
775
+ summary: exhaustive
776
+ ? pins.length
777
+ ? `${pins.length} absolute pin${pins.length === 1 ? " is" : "s are"} present for the side to move after the target move; the recorded legal moves show whether a pinned piece can capture its pinner or remain on the ray.`
778
+ : "No absolute pin is present for the side to move after the target move."
779
+ : "Absolute pins are not enumerated while the side to move is already in check.",
780
+ participants: uniqueParticipants,
781
+ data: { sideToMove: color, kingSquare, pins },
782
+ pieceLocations: uniqueLocations,
783
+ coverage: { domain: "side-to-move-absolute-pins-after", exhaustive, covered: pins.map((pin) => String(pin.pinnedSquare)), unresolved: exhaustive ? [] : ["side-is-in-check"] },
784
+ replay: { fen: input.fen },
785
+ });
786
+ }
787
+
788
+ export class MechanicalChessFactProvider implements ChessFactProvider {
789
+ readonly id = MECHANICAL_CHESS_FACT_PROVIDER_ID;
790
+ readonly version = MECHANICAL_CHESS_FACT_PROVIDER_VERSION;
791
+
792
+ capabilities(): readonly ChessFactCapability[] {
793
+ return CAPABILITIES;
794
+ }
795
+
796
+ collect(requestInput: ChessFactRequest, signal?: AbortSignal): ChessFactResult {
797
+ const request = validateChessFactRequest(requestInput);
798
+ if (signal?.aborted) throw signal.reason || new Error("Chess fact request cancelled");
799
+ validateReasoningPositionContext(request.context);
800
+ const startedAt = Date.now();
801
+ const contextHash = reasoningContextHash(request.context);
802
+ const before = new Chess(request.context.fenBefore);
803
+ const moverColor = before.turn();
804
+ const move = before.move({
805
+ from: request.context.moveUci.slice(0, 2),
806
+ to: request.context.moveUci.slice(2, 4),
807
+ promotion: request.context.moveUci.slice(4) || undefined,
808
+ });
809
+ if (!move || before.fen() !== new Chess(request.context.fenAfter).fen()) throw new Error("Mechanical fact provider context transition mismatch");
810
+ const after = before;
811
+ const beforeBoard = new Chess(request.context.fenBefore);
812
+ const timeline = createPocIdentityTimeline(request.context);
813
+ const from = move.from as Square;
814
+ const to = move.to as Square;
815
+ const target = targetMoveParticipants(timeline, from, to, request.context.moveUci);
816
+ let capturedPiece: { pieceId: string; color: Color; pieceType: PieceSymbol; square: Square } | null = null;
817
+ if (move.captured) {
818
+ const captureSquare = (move.isEnPassant() ? `${to[0]}${from[1]}` : to) as Square;
819
+ const captured = participantForSquare(timeline.before, captureSquare, PARTICIPANT_ROLES.TARGET);
820
+ if (captured) {
821
+ const identity = timeline.before.pieces[captured.participant.ref.id];
822
+ if (identity) capturedPiece = { pieceId: identity.id, color: identity.color, pieceType: identity.type, square: captureSquare };
823
+ target.participants.push(captured.participant);
824
+ target.locations.push({ ...captured.location, phase: "before" });
825
+ }
826
+ }
827
+ const candidates: CandidateRecord[] = [];
828
+ const push = (record: CandidateRecord) => candidates.push(record);
829
+
830
+ push(createRecord({
831
+ contextHash,
832
+ kind: "move.legal_transition",
833
+ phase: "transition",
834
+ authority: "legal-exact",
835
+ priority: 100,
836
+ summary: `${move.san} is legal and moves the ${PIECE_NAMES[move.piece]} from ${from} to ${to}${move.captured ? ` while capturing a ${PIECE_NAMES[move.captured]}` : ""}.`,
837
+ participants: target.participants,
838
+ data: { uci: request.context.moveUci, san: move.san, from, to, moverColor, pieceType: move.piece, capturedType: move.captured || null, capturedPiece, promotion: move.promotion || null },
839
+ pieceLocations: target.locations,
840
+ coverage: { domain: "target-move", exhaustive: true, covered: [request.context.moveUci], unresolved: [] },
841
+ replay: { fen: request.context.fenBefore, movesUci: [request.context.moveUci] },
842
+ }));
843
+
844
+ const beforeCheck = checkRecord({ board: beforeBoard, phase: "before", timeline, contextHash, fen: request.context.fenBefore });
845
+ const afterCheck = checkRecord({ board: after, phase: "after", timeline, contextHash, fen: request.context.fenAfter });
846
+ push(beforeCheck);
847
+ push(afterCheck);
848
+
849
+ const beforeKingSquare = beforeBoard.findPiece({ type: "k", color: moverColor })[0];
850
+ const beforeCheckers = beforeKingSquare ? beforeBoard.attackers(beforeKingSquare, opposite(moverColor)) : [];
851
+ if (move.piece !== "k" && beforeKingSquare && beforeCheckers.length && !after.isAttacked(after.findPiece({ type: "k", color: moverColor })[0], opposite(moverColor))) {
852
+ for (const checkerSquare of beforeCheckers.filter((square) => squaresBetween(square, beforeKingSquare).includes(to))) {
853
+ // This transition is performed by the moved piece. The checker is the
854
+ // piece whose checking line is affected by the block, so it is a
855
+ // target rather than a second actor sharing the mover's destination.
856
+ // Keeping that distinction here prevents consumers from inferring
857
+ // that both pieces travel to the single destination square.
858
+ const checker = participantForSquare(timeline.before, checkerSquare, PARTICIPANT_ROLES.TARGET);
859
+ const king = participantForSquare(timeline.before, beforeKingSquare, PARTICIPANT_ROLES.TARGET);
860
+ push(createRecord({
861
+ contextHash,
862
+ kind: "move.blocks_check_line",
863
+ phase: "transition",
864
+ authority: "legal-exact",
865
+ priority: 100,
866
+ summary: `${move.san} places the moved piece on ${to} between the checker on ${checkerSquare} and the king on ${beforeKingSquare}; the resulting position is not check.`,
867
+ participants: [...target.participants, ...(checker ? [checker.participant] : []), ...(king ? [king.participant] : [])],
868
+ data: { checkerSquare, kingSquare: beforeKingSquare, blockSquare: to, line: [checkerSquare, ...squaresBetween(checkerSquare, beforeKingSquare), beforeKingSquare], checkResolved: true },
869
+ pieceLocations: [...target.locations, ...(checker ? [{ ...checker.location, phase: "before" }] : []), ...(king ? [{ ...king.location, phase: "before" }] : [])],
870
+ coverage: { domain: "target-move-check-resolution", exhaustive: true, covered: [request.context.moveUci, checkerSquare, beforeKingSquare], unresolved: [] },
871
+ replay: { fen: request.context.fenBefore, movesUci: [request.context.moveUci] },
872
+ derivedFromIds: [beforeCheck.id, afterCheck.id],
873
+ }));
874
+ }
875
+ }
876
+
877
+ push(createRecord({
878
+ contextHash,
879
+ kind: "move.capture_check_delta",
880
+ phase: "transition",
881
+ authority: "legal-exact",
882
+ priority: after.isCheck() ? 98 : 88,
883
+ summary: `${move.san}${move.captured ? ` captures a ${PIECE_NAMES[move.captured]} on ${to}` : " does not capture"}${after.isCheck() ? " and gives check" : " and does not give check"}.`,
884
+ participants: target.participants,
885
+ data: { uci: request.context.moveUci, capture: Boolean(move.captured), capturedType: move.captured || null, capturedPiece, givesCheck: after.isCheck() },
886
+ pieceLocations: target.locations,
887
+ coverage: { domain: "target-move-capture-check", exhaustive: true, covered: [request.context.moveUci], unresolved: [] },
888
+ replay: { fen: request.context.fenBefore, movesUci: [request.context.moveUci] },
889
+ }));
890
+
891
+ // A target move's destination is the one square where an immediate
892
+ // capture/recapture question is intrinsically about the target move. Keep
893
+ // its complete attack map as a compact, deterministic fact even when the
894
+ // ordinary presentation budget is tight. This deliberately reports only
895
+ // geometry: chess.js attack maps include pinned pieces, so neither set is
896
+ // a conclusion about legal capture, SEE, or the safety of the mover.
897
+ const destinationFriendlyDefenders = stableAttackers(after, timeline.after, to, moverColor);
898
+ const destinationOpposingAttackers = stableAttackers(after, timeline.after, to, opposite(moverColor));
899
+ const movedPieceId = pieceIdAt(timeline.after, to);
900
+ push(createRecord({
901
+ contextHash,
902
+ kind: "square.target_destination_attackers_after",
903
+ phase: "after",
904
+ authority: "deterministic-derived",
905
+ priority: 101,
906
+ budgetProtected: true,
907
+ summary: `After ${move.san}, the post-move attack map on ${to} has friendly ${destinationFriendlyDefenders.map((item) => item.currentSquare).join(", ") || "none"} and opposing ${destinationOpposingAttackers.map((item) => item.currentSquare).join(", ") || "none"}. Geometry only; not a safety, exchange, or strategic conclusion.`,
908
+ participants: target.participants,
909
+ data: {
910
+ square: to,
911
+ occupant: movedPieceId ? { pieceId: movedPieceId, currentSquare: to, color: moverColor, pieceType: move.piece } : null,
912
+ friendlyDefenderSet: destinationFriendlyDefenders,
913
+ opposingAttackerSet: destinationOpposingAttackers,
914
+ attackMapScope: "geometry-only-including-pinned-pieces",
915
+ },
916
+ pieceLocations: target.locations,
917
+ coverage: {
918
+ domain: "target-move-destination-attack-map-after",
919
+ exhaustive: true,
920
+ covered: [to],
921
+ unresolved: [],
922
+ },
923
+ replay: { fen: request.context.fenAfter },
924
+ }));
925
+
926
+ const afterLegalMoves = after.moves({ verbose: true });
927
+ const movedPieceAfter = after.get(to);
928
+ const legalRecaptures = movedPieceAfter
929
+ ? afterLegalMoves.filter((reply) => reply.to === to && reply.captured === movedPieceAfter.type)
930
+ : [];
931
+ if (move.captured && movedPieceAfter) {
932
+ const captureResponse = deriveAdmittedCaptureResponseAvailabilityFact({
933
+ context: request.context,
934
+ contextHash,
935
+ start: "before",
936
+ movesUci: [request.context.moveUci],
937
+ });
938
+ const unavailable = captureResponse.data.unavailableGeometricDefenderPieceIds;
939
+ const blockedSliderCapturers = captureResponse.data.preCaptureBlockedSliderCapturers;
940
+ const replies = captureResponse.data.legalSameSquareRecaptures;
941
+ push({
942
+ ...captureResponse,
943
+ priority: Array.isArray(unavailable) && unavailable.length
944
+ || Array.isArray(blockedSliderCapturers) && blockedSliderCapturers.length
945
+ ? 95
946
+ : Array.isArray(replies) && replies.length
947
+ ? 84
948
+ : 72,
949
+ });
950
+ }
951
+ const captureOutcomes = immediateCaptureOutcomes(request.context.fenAfter, legalRecaptures);
952
+ const allCapturesLoseAfterImmediateSameSquareRecapture = captureOutcomes.length > 0 && captureOutcomes.every((outcome) => (
953
+ outcome.immediateSameSquareRecaptures.length > 0
954
+ && outcome.minimumNominalBalanceDeltaAfterImmediateSameSquareRecapture !== null
955
+ && outcome.minimumNominalBalanceDeltaAfterImmediateSameSquareRecapture < 0
956
+ ));
957
+ const recaptureParticipants = legalRecaptures.flatMap((reply) => {
958
+ const participant = participantForSquare(timeline.after, reply.from as Square, PARTICIPANT_ROLES.ACTOR);
959
+ return participant ? [participant] : [];
960
+ });
961
+ push(createRecord({
962
+ contextHash,
963
+ kind: "move.legal_recaptures_after",
964
+ phase: "after",
965
+ authority: "deterministic-derived",
966
+ // A legal capture of the moved piece which is immediately answered on
967
+ // the same square is direct target-move evidence. It stays near the
968
+ // top of normal deterministic ordering, while still making no SEE or
969
+ // longer-sequence safety claim.
970
+ priority: legalRecaptures.length && captureOutcomes.some((outcome) => outcome.immediateSameSquareRecaptures.length)
971
+ ? 100
972
+ : legalRecaptures.length ? (allCapturesLoseAfterImmediateSameSquareRecapture ? 72 : 96) : 42,
973
+ summary: legalRecaptures.length
974
+ ? `The piece that moved to ${to} can be captured immediately by ${legalRecaptures.map((reply) => reply.san).join(", ")}.${captureOutcomes.map((outcome) => outcome.immediateSameSquareRecaptures.length
975
+ ? ` After ${outcome.capture.san}, immediate same-square replies are ${outcome.immediateSameSquareRecaptures.map((reply) => `${reply.san} (capturing side nominal balance delta ${reply.nominalBalanceDeltaForCapturingSide})`).join(", ")}.`
976
+ : ` No immediate same-square reply follows ${outcome.capture.san}.`).join("")} This is nominal arithmetic, not SEE or proof of longer-term exchange soundness.`
977
+ : `The side to move has no immediate legal capture of the piece on ${to}. This does not establish that the piece is safe in a longer sequence.`,
978
+ participants: [...target.afterParticipants, ...recaptureParticipants.map((item) => item.participant)],
979
+ data: {
980
+ targetSquare: to,
981
+ targetPieceType: movedPieceAfter?.type || null,
982
+ targetPieceColor: movedPieceAfter?.color || null,
983
+ moves: legalRecaptures.map((reply) => ({
984
+ uci: moveUci(reply),
985
+ san: reply.san,
986
+ moverPieceId: pieceIdAt(timeline.after, reply.from as Square),
987
+ from: reply.from,
988
+ to: reply.to,
989
+ capturedType: reply.captured || null,
990
+ })),
991
+ captureOutcomes,
992
+ allCapturesLoseAfterImmediateSameSquareRecapture,
993
+ },
994
+ pieceLocations: [...target.locations.filter((location) => location.phase === "after"), ...recaptureParticipants.map((item) => ({ ...item.location, phase: "after" }))],
995
+ coverage: { domain: "legal-captures-of-target-mover-after", exhaustive: true, covered: legalRecaptures.map(moveUci), unresolved: [] },
996
+ replay: {
997
+ fen: request.context.fenAfter,
998
+ linesUci: captureOutcomes.flatMap((outcome) => outcome.immediateSameSquareRecaptures.length
999
+ ? outcome.immediateSameSquareRecaptures.map((reply) => [outcome.capture.uci, reply.uci])
1000
+ : [[outcome.capture.uci]]),
1001
+ },
1002
+ }));
1003
+
1004
+ if (after.isCheck()) {
1005
+ const checkedColor = after.turn();
1006
+ const kingSquare = after.findPiece({ type: "k", color: checkedColor })[0];
1007
+ const checkerSquares = after.attackers(kingSquare, opposite(checkedColor));
1008
+ const participants = checkerSquares.flatMap((square) => {
1009
+ const item = participantForSquare(timeline.after, square, PARTICIPANT_ROLES.ACTOR);
1010
+ return item ? [item] : [];
1011
+ });
1012
+ const king = participantForSquare(timeline.after, kingSquare, PARTICIPANT_ROLES.TARGET);
1013
+ push(createRecord({
1014
+ contextHash,
1015
+ kind: "king.checkers_after",
1016
+ phase: "after",
1017
+ authority: "legal-exact",
1018
+ priority: 97,
1019
+ summary: `After ${move.san}, ${checkerSquares.length} piece${checkerSquares.length === 1 ? " checks" : "s check"} the king on ${kingSquare}: ${checkerSquares.join(", ")}.`,
1020
+ participants: [...participants.map((item) => item.participant), ...(king ? [king.participant] : [])],
1021
+ data: { kingSquare, checkerSquares, checkingLines: checkerSquares.map((square) => ({ checker: square, between: squaresBetween(square, kingSquare) })) },
1022
+ pieceLocations: [...participants.map((item) => ({ ...item.location, phase: "after" })), ...(king ? [{ ...king.location, phase: "after" }] : [])],
1023
+ coverage: { domain: "checking-pieces-after", exhaustive: true, covered: [kingSquare, ...checkerSquares], unresolved: [] },
1024
+ replay: { fen: request.context.fenAfter },
1025
+ }));
1026
+ const kingMoves = after.moves({ square: kingSquare, verbose: true });
1027
+ push(createRecord({
1028
+ contextHash,
1029
+ kind: "king.legal_moves",
1030
+ phase: "after",
1031
+ authority: "legal-exact",
1032
+ priority: 94,
1033
+ summary: `The checked king on ${kingSquare} has ${kingMoves.length} legal king move${kingMoves.length === 1 ? "" : "s"}: ${kingMoves.map((item) => item.san).join(", ") || "none"}.`,
1034
+ participants: king ? [king.participant] : [],
1035
+ data: { kingSquare, movesUci: kingMoves.map((item) => `${item.from}${item.to}${item.promotion || ""}`), movesSan: kingMoves.map((item) => item.san) },
1036
+ pieceLocations: king ? [{ ...king.location, phase: "after" }] : [],
1037
+ coverage: { domain: "legal-king-moves-after", exhaustive: true, covered: kingMoves.map((item) => `${item.from}${item.to}${item.promotion || ""}`), unresolved: [] },
1038
+ replay: { fen: request.context.fenAfter },
1039
+ }));
1040
+ for (const checkerSquare of checkerSquares.filter((square) => square !== to && squaresBetween(square, kingSquare).includes(from))) {
1041
+ const checker = participantForSquare(timeline.after, checkerSquare, PARTICIPANT_ROLES.ACTOR);
1042
+ push(createRecord({
1043
+ contextHash,
1044
+ kind: "move.unblocks_line",
1045
+ phase: "transition",
1046
+ authority: "legal-exact",
1047
+ priority: 95,
1048
+ summary: `Moving from ${from} clears the line from the ${PIECE_NAMES[after.get(checkerSquare)!.type]} on ${checkerSquare} to the king on ${kingSquare}.`,
1049
+ participants: [...target.participants, ...(checker ? [checker.participant] : []), ...(king ? [king.participant] : [])],
1050
+ data: { vacatedSquare: from, checkerSquare, kingSquare, line: [checkerSquare, ...squaresBetween(checkerSquare, kingSquare), kingSquare] },
1051
+ pieceLocations: [...target.locations, ...(checker ? [{ ...checker.location, phase: "after" }] : []), ...(king ? [{ ...king.location, phase: "after" }] : [])],
1052
+ coverage: { domain: "target-move-slider-lines", exhaustive: true, covered: [from, checkerSquare, kingSquare], unresolved: [] },
1053
+ replay: { fen: request.context.fenBefore, movesUci: [request.context.moveUci] },
1054
+ }));
1055
+ }
1056
+ }
1057
+
1058
+ const whiteKingSquare = after.findPiece({ type: "k", color: "w" })[0];
1059
+ const blackKingSquare = after.findPiece({ type: "k", color: "b" })[0];
1060
+ if (!whiteKingSquare || !blackKingSquare) throw new Error("Mechanical fact provider requires both kings");
1061
+ const whiteKing = participantForSquare(timeline.after, whiteKingSquare, PARTICIPANT_ROLES.ACTOR);
1062
+ const blackKing = participantForSquare(timeline.after, blackKingSquare, PARTICIPANT_ROLES.TARGET);
1063
+ const distance = kingDistance(whiteKingSquare, blackKingSquare);
1064
+ const betweenKings = squaresBetween(whiteKingSquare, blackKingSquare);
1065
+ push(createRecord({
1066
+ contextHash,
1067
+ kind: "king.geometry_after",
1068
+ phase: "after",
1069
+ authority: "legal-exact",
1070
+ priority: distance.chebyshev <= 2 ? 90 : 24,
1071
+ summary: `After ${move.san}, the kings are on ${whiteKingSquare} and ${blackKingSquare}; their Chebyshev distance is ${distance.chebyshev}, it is ${colorName(after.turn())}'s turn, and aligned squares between them are ${betweenKings.join(", ") || "none"}.`,
1072
+ participants: [...(whiteKing ? [whiteKing.participant] : []), ...(blackKing ? [blackKing.participant] : [])],
1073
+ data: { whiteKingSquare, blackKingSquare, sideToMove: after.turn(), distance, alignedBetween: betweenKings },
1074
+ pieceLocations: [...(whiteKing ? [{ ...whiteKing.location, phase: "after" }] : []), ...(blackKing ? [{ ...blackKing.location, phase: "after" }] : [])],
1075
+ coverage: { domain: "king-square-geometry-after", exhaustive: true, covered: [whiteKingSquare, blackKingSquare], unresolved: [] },
1076
+ replay: { fen: request.context.fenAfter },
1077
+ }));
1078
+
1079
+ const castlingEvents = priorCastlingEvents(request.context);
1080
+ const castlingColors = new Set(castlingEvents.map((event) => event.color));
1081
+ push(createRecord({
1082
+ contextHash,
1083
+ kind: "king_rook.castling_history",
1084
+ phase: "path",
1085
+ authority: "legal-exact",
1086
+ priority: castlingEvents.length ? 88 : 18,
1087
+ summary: castlingEvents.length
1088
+ ? `Before ${move.san}, the recorded path contains ${castlingEvents.length} castling event${castlingEvents.length === 1 ? "" : "s"}: ${castlingEvents.map((event) => `${colorName(event.color)} ${event.san} on ply ${event.plyIndex + 1}`).join("; ")}.`
1089
+ : `The recorded path before ${move.san} contains no castling event.`,
1090
+ participants: [
1091
+ ...(castlingColors.has("w") && whiteKing ? [whiteKing.participant] : []),
1092
+ ...(castlingColors.has("b") && blackKing ? [blackKing.participant] : []),
1093
+ ],
1094
+ data: { events: castlingEvents },
1095
+ pieceLocations: [
1096
+ ...(castlingColors.has("w") && whiteKing ? [{ ...whiteKing.location, phase: "after" }] : []),
1097
+ ...(castlingColors.has("b") && blackKing ? [{ ...blackKing.location, phase: "after" }] : []),
1098
+ ],
1099
+ coverage: { domain: "prior-castling-events", exhaustive: true, covered: castlingEvents.map((event) => String(event.plyIndex)), unresolved: [] },
1100
+ replay: { fen: request.context.historyStartFen || new Chess().fen(), movesUci: request.context.historyUci },
1101
+ }));
1102
+
1103
+ const beforeAttacks = attackedSquares(beforeBoard, from, moverColor);
1104
+ const afterAttacks = attackedSquares(after, to, moverColor);
1105
+ const gained = afterAttacks.filter((square) => !beforeAttacks.includes(square));
1106
+ const lost = beforeAttacks.filter((square) => !afterAttacks.includes(square));
1107
+ const retained = afterAttacks.filter((square) => beforeAttacks.includes(square));
1108
+ const occupiedTargets = afterAttacks.filter((square) => after.get(square)?.color === opposite(moverColor));
1109
+ const targetPieces = occupiedTargets.flatMap((square) => {
1110
+ const item = participantForSquare(timeline.after, square, PARTICIPANT_ROLES.TARGET);
1111
+ return item ? [{ square, ...item }] : [];
1112
+ });
1113
+ push(createRecord({
1114
+ contextHash,
1115
+ kind: "piece.attack_delta",
1116
+ phase: "transition",
1117
+ authority: "deterministic-derived",
1118
+ priority: occupiedTargets.length ? 92 : 72,
1119
+ summary: `After ${move.san}, the moved ${PIECE_NAMES[move.piece]} attacks ${afterAttacks.join(", ") || "no squares"}; gained attacks are ${gained.join(", ") || "none"}, and occupied opponent targets are ${occupiedTargets.join(", ") || "none"}.`,
1120
+ participants: [...target.participants, ...targetPieces.map((item) => item.participant)],
1121
+ data: { from, to, beforeAttacks, afterAttacks, gained, lost, retained, occupiedOpponentTargets: occupiedTargets },
1122
+ pieceLocations: [...target.locations, ...targetPieces.map((item) => ({ ...item.location, phase: "after" }))],
1123
+ coverage: { domain: "moved-piece-geometric-attacks", exhaustive: true, covered: afterAttacks, unresolved: [] },
1124
+ replay: { fen: request.context.fenBefore, movesUci: [request.context.moveUci] },
1125
+ }));
1126
+
1127
+ const friendlyOrigins = after.attackers(to, moverColor).filter((origin) => after.get(origin)?.type !== "p");
1128
+ const friendlyAttackers = friendlyOrigins.flatMap((origin) => {
1129
+ const item = participantForSquare(timeline.after, origin, PARTICIPANT_ROLES.RESOURCE);
1130
+ const piece = after.get(origin);
1131
+ return item && piece ? [{ origin, pieceType: piece.type, ...item }] : [];
1132
+ });
1133
+ push(createRecord({
1134
+ contextHash,
1135
+ kind: "square.friendly_nonpawn_reachers_after",
1136
+ phase: "after",
1137
+ authority: "deterministic-derived",
1138
+ priority: friendlyAttackers.some((item) => item.pieceType === "n") ? 91 : friendlyAttackers.length ? 64 : 20,
1139
+ summary: `${to} is occupied by the moved ${colorName(moverColor).toLowerCase()} ${PIECE_NAMES[move.piece]} after ${move.san}; friendly non-pawn pieces whose movement geometry reaches ${to} are ${friendlyAttackers.map((item) => `${PIECE_NAMES[item.pieceType]} on ${item.origin}`).join(" and ") || "none"}. Friendly occupancy makes entry onto ${to} illegal while it remains, but does not establish whether that square is strategically desirable for the other piece.`,
1140
+ participants: [...target.participants, ...friendlyAttackers.map((item) => item.participant)],
1141
+ data: { square: to, occupantPieceId: pieceIdAt(timeline.after, to), occupantType: move.piece, color: moverColor, friendlyNonPawnReachers: friendlyAttackers.map((item) => ({ pieceId: item.participant.ref.id, currentSquare: item.origin, pieceType: item.pieceType })), entryIllegalWhileFriendlyOccupied: true },
1142
+ pieceLocations: [...target.locations, ...friendlyAttackers.map((item) => ({ ...item.location, phase: "after" }))],
1143
+ coverage: { domain: "target-destination-friendly-nonpawn-reachers", exhaustive: true, covered: [to, ...friendlyOrigins], unresolved: [] },
1144
+ replay: { fen: request.context.fenAfter },
1145
+ }));
1146
+
1147
+ const attackerChanges = attackerSetChanges(beforeBoard, after, timeline);
1148
+ const retainedAfterLoss = attackerChanges.filter((change) => change.lost.length && change.afterAttackers.length);
1149
+ const attackPieceLocations = attackerChanges
1150
+ .flatMap((change) => [...change.beforeAttackers.map((item) => ({ pieceId: item.pieceId, currentSquare: item.currentSquare, phase: "before" })), ...change.afterAttackers.map((item) => ({ pieceId: item.pieceId, currentSquare: item.currentSquare, phase: "after" }))])
1151
+ .filter((item, index, all) => all.findIndex((candidate) => candidate.pieceId === item.pieceId && candidate.currentSquare === item.currentSquare && candidate.phase === item.phase) === index);
1152
+ for (const change of attackerChanges) {
1153
+ const entries = afterLegalMoves.filter((reply) => reply.to === change.square);
1154
+ const entryResponseWitnesses = entries.map((entry) => {
1155
+ const afterEntry = new Chess(request.context.fenAfter);
1156
+ applyVerboseMove(afterEntry, entry);
1157
+ const captureMoves = afterEntry.moves({ verbose: true })
1158
+ .filter((reply) => reply.to === change.square && Boolean(reply.captured));
1159
+ const captureOutcomes = new Map(immediateCaptureOutcomes(afterEntry.fen(), captureMoves)
1160
+ .map((outcome) => [outcome.capture.uci, outcome]));
1161
+ const immediateSameSquareCaptures = captureMoves.map((reply) => {
1162
+ const outcome = captureOutcomes.get(moveUci(reply));
1163
+ return {
1164
+ uci: moveUci(reply),
1165
+ san: reply.san,
1166
+ from: reply.from,
1167
+ to: reply.to,
1168
+ moverPieceId: pieceIdAt(timeline.after, reply.from as Square),
1169
+ capturedPieceId: pieceIdAt(timeline.after, entry.from as Square),
1170
+ nominalBalanceDeltaAfterCapture: outcome?.nominalBalanceDeltaAfterCapture ?? null,
1171
+ immediateSameSquareRecaptures: outcome?.immediateSameSquareRecaptures || [],
1172
+ ...(outcome?.minimumNominalBalanceDeltaAfterImmediateSameSquareRecapture == null
1173
+ ? {}
1174
+ : { minimumNominalBalanceDeltaAfterImmediateSameSquareRecapture: outcome.minimumNominalBalanceDeltaAfterImmediateSameSquareRecapture }),
1175
+ };
1176
+ });
1177
+ return {
1178
+ entry: {
1179
+ uci: moveUci(entry),
1180
+ san: entry.san,
1181
+ from: entry.from,
1182
+ to: entry.to,
1183
+ moverPieceId: pieceIdAt(timeline.after, entry.from as Square),
1184
+ },
1185
+ immediateSameSquareCaptures,
1186
+ };
1187
+ });
1188
+ const entryResponseText = entryResponseWitnesses.flatMap((witness) => witness.immediateSameSquareCaptures
1189
+ .map((reply) => {
1190
+ const recaptures = reply.immediateSameSquareRecaptures.map((recapture) => `${recapture.san} (${recapture.nominalBalanceDeltaForCapturingSide})`);
1191
+ return `${witness.entry.san} ${reply.san}${recaptures.length ? ` ${recaptures.join(" or ")}` : ""} (first capturer nominal delta ${recaptures.length ? reply.minimumNominalBalanceDeltaAfterImmediateSameSquareRecapture : reply.nominalBalanceDeltaAfterCapture}; ${recaptures.length ? "after immediate recapture" : "no immediate recapture"})`;
1192
+ }));
1193
+ const entryResponseReplayLines: string[][] = entryResponseWitnesses.flatMap((witness) => witness.immediateSameSquareCaptures.length
1194
+ ? witness.immediateSameSquareCaptures.flatMap((reply) => reply.immediateSameSquareRecaptures.length
1195
+ ? reply.immediateSameSquareRecaptures.map((recapture) => [request.context.moveUci, witness.entry.uci, reply.uci, recapture.uci])
1196
+ : [[request.context.moveUci, witness.entry.uci, reply.uci]])
1197
+ : [[request.context.moveUci, witness.entry.uci]]);
1198
+ const beforeOccupant = beforeBoard.get(change.square);
1199
+ const afterOccupant = after.get(change.square);
1200
+ const beforeOccupantId = beforeOccupant ? pieceIdAt(timeline.before, change.square) : null;
1201
+ const afterOccupantId = afterOccupant ? pieceIdAt(timeline.after, change.square) : null;
1202
+ const occupiedOpponentLostAttack = change.lost.length > 0
1203
+ && beforeOccupant?.color === opposite(change.color)
1204
+ && afterOccupantId === beforeOccupantId;
1205
+ const newlyContestedLegalEntry = change.gained.length > 0 && entries.length > 0;
1206
+ const beforeText = change.beforeAttackers.map((item) => `${PIECE_NAMES[item.pieceType]} on ${item.currentSquare}`).join(" and ") || "none";
1207
+ const afterText = change.afterAttackers.map((item) => `${PIECE_NAMES[item.pieceType]} on ${item.currentSquare}`).join(" and ") || "none";
1208
+ const referenced: Array<{ participant: SemanticParticipant; location: Record<string, SemanticValue> }> = [
1209
+ ...change.beforeAttackers.flatMap((item) => {
1210
+ const participant = participantForSquare(timeline.before, item.currentSquare, PARTICIPANT_ROLES.RESOURCE);
1211
+ return participant ? [{ participant: participant.participant, location: { ...participant.location, phase: "before" } }] : [];
1212
+ }),
1213
+ ...change.afterAttackers.flatMap((item) => {
1214
+ const participant = participantForSquare(timeline.after, item.currentSquare, PARTICIPANT_ROLES.RESOURCE);
1215
+ return participant ? [{ participant: participant.participant, location: { ...participant.location, phase: "after" } }] : [];
1216
+ }),
1217
+ ];
1218
+ if (occupiedOpponentLostAttack) {
1219
+ const affectedBefore = participantForSquare(timeline.before, change.square, PARTICIPANT_ROLES.TARGET);
1220
+ const affectedAfter = participantForSquare(timeline.after, change.square, PARTICIPANT_ROLES.TARGET);
1221
+ if (affectedBefore) referenced.push({ participant: affectedBefore.participant, location: { ...affectedBefore.location, phase: "before" } });
1222
+ if (affectedAfter) referenced.push({ participant: affectedAfter.participant, location: { ...affectedAfter.location, phase: "after" } });
1223
+ }
1224
+ const participants = [...target.participants, ...referenced.map((item) => item.participant)]
1225
+ .filter((item, index, all) => all.findIndex((candidate) => candidate.role === item.role && candidate.ref.domain === item.ref.domain && candidate.ref.id === item.ref.id) === index);
1226
+ const locations = [...target.locations, ...referenced.map((item) => item.location)]
1227
+ .filter((item, index, all) => all.findIndex((candidate) => candidate.pieceId === item.pieceId && candidate.currentSquare === item.currentSquare && candidate.phase === item.phase) === index);
1228
+ const remainsAfterLoss = change.lost.length > 0 && change.afterAttackers.length > 0;
1229
+ push(createRecord({
1230
+ contextHash,
1231
+ kind: "square.attacker_delta",
1232
+ phase: "transition",
1233
+ authority: "deterministic-derived",
1234
+ priority: occupiedOpponentLostAttack
1235
+ ? 98
1236
+ : remainsAfterLoss && entries.length
1237
+ ? 94
1238
+ : newlyContestedLegalEntry
1239
+ // A newly available legal entry paired with an exact immediate
1240
+ // capture witness is direct, replayable board information. It
1241
+ // remains below occupied-piece protection, but ahead of broad
1242
+ // descriptive deltas so a tight budget can retain the concrete
1243
+ // entry/capture chain without inferring its strategic value.
1244
+ ? entryResponseWitnesses.some((witness) => witness.immediateSameSquareCaptures.length) ? 96 : 91
1245
+ : remainsAfterLoss
1246
+ ? 88
1247
+ : entries.length
1248
+ ? 82
1249
+ : 68,
1250
+ summary: `${colorName(change.color)} attackers of ${change.square} change from ${beforeText} to ${afterText}.${occupiedOpponentLostAttack && beforeOccupant ? ` The ${colorName(beforeOccupant.color).toLowerCase()} ${PIECE_NAMES[beforeOccupant.type]} remains on ${change.square} and is no longer attacked by the removed attacker set.` : ""}${entries.length ? ` ${colorName(after.turn())} has ${entries.length} legal move${entries.length === 1 ? "" : "s"} to ${change.square}: ${entries.map((reply) => reply.san).join(", ")}.` : ""}${entryResponseText.length ? ` Exact immediate captures back onto that entry square include ${entryResponseText.join(", ")}.` : ""}`,
1251
+ participants,
1252
+ data: {
1253
+ ...change,
1254
+ remainsAttackedAfterLoss: remainsAfterLoss,
1255
+ occupiedOpponentLostAttack,
1256
+ affectedOccupiedPiece: occupiedOpponentLostAttack && beforeOccupant ? {
1257
+ pieceId: beforeOccupantId,
1258
+ color: beforeOccupant.color,
1259
+ pieceType: beforeOccupant.type,
1260
+ square: change.square,
1261
+ } : null,
1262
+ newlyContestedLegalEntry,
1263
+ sideToMoveLegalEntries: entries.map((reply) => ({ uci: moveUci(reply), san: reply.san, from: reply.from, to: reply.to, moverPieceId: pieceIdAt(timeline.after, reply.from as Square) })),
1264
+ entryResponseWitnesses,
1265
+ entryResponseCoverage: {
1266
+ exhaustive: true,
1267
+ coveredEntryMoves: entries.map((reply) => moveUci(reply)),
1268
+ depth: "entry-capture-immediate-same-square-recapture",
1269
+ unresolved: ["later continuations", "off-square replies after the entry"],
1270
+ },
1271
+ },
1272
+ pieceLocations: locations,
1273
+ coverage: { domain: `attacker-set-${change.color}-${change.square}-transition`, exhaustive: true, covered: [change.square], unresolved: [] },
1274
+ replay: {
1275
+ fen: request.context.fenBefore,
1276
+ movesUci: [request.context.moveUci],
1277
+ ...(entryResponseReplayLines.length ? { linesUci: entryResponseReplayLines } : {}),
1278
+ },
1279
+ }));
1280
+ const exchangeWitnesses = sameSquareExchangeWitnesses(request.context.fenAfter, change.square);
1281
+ if (exchangeWitnesses.length) {
1282
+ const lineOutcomes = sameSquareLineOutcomes(request.context.fenAfter, exchangeWitnesses);
1283
+ push(createRecord({
1284
+ contextHash,
1285
+ kind: "square.same_square_exchange_witnesses",
1286
+ phase: "path",
1287
+ authority: "deterministic-derived",
1288
+ priority: lineOutcomes.every((outcome) => outcome.finalNominalBalanceDeltaForFirstCapturingSide !== null && outcome.finalNominalBalanceDeltaForFirstCapturingSide < 0)
1289
+ ? 70
1290
+ : exchangeWitnesses.some((line) => line.length >= 3) ? 93 : 78,
1291
+ summary: `Legal same-square exchange witnesses on ${change.square} after ${move.san} include ${exchangeWitnesses.map((line, index) => `${line.map((item) => item.san).join(" ")} (final nominal balance delta for the first capturing side ${lineOutcomes[index].finalNominalBalanceDeltaForFirstCapturingSide ?? "not applicable"})`).join("; ")}. These lines are not asserted to be forced or best, and nominal arithmetic is not SEE.`,
1292
+ participants: target.participants,
1293
+ data: {
1294
+ square: change.square,
1295
+ lines: exchangeWitnesses,
1296
+ lineOutcomes,
1297
+ attackerLossRetainedAndLegalEntry: remainsAfterLoss && entries.length > 0,
1298
+ },
1299
+ pieceLocations: target.locations,
1300
+ coverage: { domain: `same-square-exchange-witnesses-${change.square}`, exhaustive: true, covered: exchangeWitnesses.map((line) => line.map((item) => item.uci).join(" ")), unresolved: [] },
1301
+ replay: { fen: request.context.fenAfter, linesUci: exchangeWitnesses.map((line) => line.map((item) => item.uci)) },
1302
+ }));
1303
+ for (const witness of exchangeRayAlignmentWitnesses(request.context.fenAfter, timeline.after, exchangeWitnesses)) {
1304
+ push(createRecord({
1305
+ contextHash,
1306
+ kind: "line.occupied_ray_alignment_after_exchange",
1307
+ phase: "path",
1308
+ authority: "legal-exact",
1309
+ // Exact geometry in a hypothetical exchange line remains useful
1310
+ // on demand, but is less salient than the target move's direct
1311
+ // changes regardless of the rear piece's nominal value.
1312
+ priority: witness.rear.pieceType === "q" || witness.rear.pieceType === "k" ? 58 : 52,
1313
+ summary: `The legal witness ${witness.movesUci.join(" ")} newly creates an exact occupied ray from ${colorName(witness.slider.color)} ${PIECE_NAMES[witness.slider.pieceType]} on ${witness.slider.currentSquare} through ${colorName(witness.front.color)} ${PIECE_NAMES[witness.front.pieceType]} on ${witness.front.currentSquare} to ${colorName(witness.rear.color)} ${PIECE_NAMES[witness.rear.pieceType]} on ${witness.rear.currentSquare}. This geometry alone does not establish tactical meaning, force, or exchange quality.`,
1314
+ participants: [
1315
+ { role: PARTICIPANT_ROLES.ACTOR, ref: { domain: REFERENCE_DOMAINS.PIECE, id: witness.slider.pieceId } },
1316
+ { role: PARTICIPANT_ROLES.TARGET, ref: { domain: REFERENCE_DOMAINS.PIECE, id: witness.front.pieceId } },
1317
+ { role: PARTICIPANT_ROLES.TARGET, ref: { domain: REFERENCE_DOMAINS.PIECE, id: witness.rear.pieceId } },
1318
+ ],
1319
+ data: {
1320
+ movesUci: witness.movesUci,
1321
+ finalFen: witness.finalFen,
1322
+ sliderPieceId: witness.slider.pieceId,
1323
+ sliderSquare: witness.slider.currentSquare,
1324
+ sliderPieceType: witness.slider.pieceType,
1325
+ frontPieceId: witness.front.pieceId,
1326
+ frontSquare: witness.front.currentSquare,
1327
+ frontPieceType: witness.front.pieceType,
1328
+ rearPieceId: witness.rear.pieceId,
1329
+ rearSquare: witness.rear.currentSquare,
1330
+ rearPieceType: witness.rear.pieceType,
1331
+ ray: witness.ray,
1332
+ },
1333
+ pieceLocations: [
1334
+ { pieceId: witness.slider.pieceId, currentSquare: witness.slider.currentSquare, phase: "path-final" },
1335
+ { pieceId: witness.front.pieceId, currentSquare: witness.front.currentSquare, phase: "path-final" },
1336
+ { pieceId: witness.rear.pieceId, currentSquare: witness.rear.currentSquare, phase: "path-final" },
1337
+ ],
1338
+ coverage: {
1339
+ domain: `new-opposing-slider-ray-alignments-with-higher-value-rear-piece-after-${witness.movesUci.join("-")}`,
1340
+ exhaustive: true,
1341
+ covered: [`${witness.slider.currentSquare}:${witness.front.currentSquare}:${witness.rear.currentSquare}`],
1342
+ unresolved: [],
1343
+ },
1344
+ replay: { fen: request.context.fenAfter, movesUci: witness.movesUci },
1345
+ }));
1346
+ }
1347
+ }
1348
+ }
1349
+ push(createRecord({
1350
+ contextHash,
1351
+ kind: "square.attackers_delta_inventory",
1352
+ phase: "transition",
1353
+ authority: "deterministic-derived",
1354
+ priority: 22,
1355
+ summary: `Across all squares, ${attackerChanges.length} attacker set${attackerChanges.length === 1 ? " changes" : "s change"}; ${retainedAfterLoss.length} square${retainedAfterLoss.length === 1 ? " remains" : "s remain"} attacked after losing at least one attacker. This is geometry, not a claim of strategic importance.`,
1356
+ participants: target.participants,
1357
+ data: { moverColor, changes: attackerChanges, retainedAfterLoss: retainedAfterLoss.map((change) => change.square) },
1358
+ pieceLocations: [...target.locations, ...attackPieceLocations],
1359
+ coverage: { domain: "all-square-attacker-sets-transition", exhaustive: true, covered: [...SQUARES], unresolved: [] },
1360
+ replay: { fen: request.context.fenBefore, movesUci: [request.context.moveUci] },
1361
+ }));
1362
+
1363
+ for (const attackedSquare of occupiedTargets) {
1364
+ const attackedPiece = after.get(attackedSquare);
1365
+ const participant = participantForSquare(timeline.after, attackedSquare, PARTICIPANT_ROLES.TARGET);
1366
+ if (!attackedPiece || !participant || attackedPiece.color !== after.turn()) continue;
1367
+ const legalMoves = after.moves({ square: attackedSquare, verbose: true });
1368
+ push(createRecord({
1369
+ contextHash,
1370
+ kind: "piece.legal_moves",
1371
+ phase: "after",
1372
+ authority: "legal-exact",
1373
+ priority: 84,
1374
+ summary: `The attacked ${PIECE_NAMES[attackedPiece.type]} on ${attackedSquare} has ${legalMoves.length} legal move${legalMoves.length === 1 ? "" : "s"}: ${legalMoves.map((item) => item.san).join(", ") || "none"}.`,
1375
+ participants: [participant.participant],
1376
+ data: { origin: attackedSquare, pieceType: attackedPiece.type, movesUci: legalMoves.map((item) => `${item.from}${item.to}${item.promotion || ""}`), movesSan: legalMoves.map((item) => item.san) },
1377
+ pieceLocations: [{ ...participant.location, phase: "after" }],
1378
+ coverage: { domain: `legal-moves-from-${attackedSquare}`, exhaustive: true, covered: legalMoves.map((item) => `${item.from}${item.to}${item.promotion || ""}`), unresolved: [] },
1379
+ replay: { fen: request.context.fenAfter },
1380
+ }));
1381
+ }
1382
+
1383
+ if (move.isKingsideCastle() || move.isQueensideCastle()) {
1384
+ const rank = moverColor === "w" ? "1" : "8";
1385
+ const rookFrom = `${move.isKingsideCastle() ? "h" : "a"}${rank}` as Square;
1386
+ const rookTo = `${move.isKingsideCastle() ? "f" : "d"}${rank}` as Square;
1387
+ const rookBefore = participantForSquare(timeline.before, rookFrom, PARTICIPANT_ROLES.RESOURCE);
1388
+ const rookAfter = participantForSquare(timeline.after, rookTo, PARTICIPANT_ROLES.RESOURCE);
1389
+ push(createRecord({
1390
+ contextHash,
1391
+ kind: "castling.king_rook_transition",
1392
+ phase: "transition",
1393
+ authority: "legal-exact",
1394
+ priority: 98,
1395
+ summary: `${move.san} moves the king from ${from} to ${to} and the rook from ${rookFrom} to ${rookTo} in one legal move.`,
1396
+ participants: [...target.participants, ...(rookBefore ? [rookBefore.participant] : [])],
1397
+ data: { king: { from, to }, rook: { from: rookFrom, to: rookTo }, side: move.isKingsideCastle() ? "king" : "queen" },
1398
+ pieceLocations: [...target.locations, ...(rookBefore ? [{ pieceId: rookBefore.participant.ref.id, currentSquare: rookFrom, phase: "before" }, { pieceId: rookAfter?.participant.ref.id || rookBefore.participant.ref.id, currentSquare: rookTo, phase: "after" }] : [])],
1399
+ coverage: { domain: "target-move", exhaustive: true, covered: [request.context.moveUci, rookFrom, rookTo], unresolved: [] },
1400
+ replay: { fen: request.context.fenBefore, movesUci: [request.context.moveUci] },
1401
+ }));
1402
+ }
1403
+
1404
+ push(pinRecord({ board: after, timeline, contextHash, fen: request.context.fenAfter }));
1405
+
1406
+ if (move.piece === "p" && !move.isPromotion()) {
1407
+ const pawnState = isPassedPawn(after, to, moverColor);
1408
+ const toRank = Number(to[1]);
1409
+ const promotionSquare = `${to[0]}${moverColor === "w" ? "8" : "1"}` as Square;
1410
+ const ranksRemaining = moverColor === "w" ? 8 - toRank : toRank - 1;
1411
+ const forwardRank = toRank + (moverColor === "w" ? 1 : -1);
1412
+ const forwardSquare = forwardRank >= 1 && forwardRank <= 8 ? `${to[0]}${forwardRank}` as Square : null;
1413
+ const captureSquares = [-1, 1].flatMap((fileDelta) => {
1414
+ const file = to.charCodeAt(0) - 97 + fileDelta;
1415
+ return file >= 0 && file <= 7 && forwardRank >= 1 && forwardRank <= 8
1416
+ ? [`${String.fromCharCode(97 + file)}${forwardRank}` as Square]
1417
+ : [];
1418
+ });
1419
+ const squareOccupant = (square: Square | null) => {
1420
+ if (!square) return null;
1421
+ const piece = after.get(square);
1422
+ return piece ? { color: piece.color, pieceType: piece.type } : null;
1423
+ };
1424
+ const opponentKingSquare = moverColor === "w" ? blackKingSquare : whiteKingSquare;
1425
+ const opponentKingDistance = kingDistance(opponentKingSquare, promotionSquare).chebyshev;
1426
+ push(createRecord({
1427
+ contextHash,
1428
+ kind: "pawn.advance_state",
1429
+ phase: "transition",
1430
+ authority: "deterministic-derived",
1431
+ // A target pawn's immediate forward and capture geometry is direct
1432
+ // transition evidence. Keep it alongside other target-move facts in
1433
+ // constrained collections without treating the geometry as a future
1434
+ // plan or promotion result.
1435
+ priority: 97,
1436
+ summary: `After ${move.san}, the pawn is on ${to}. Its next forward square is ${forwardSquare || "none"} (${forwardSquare ? squareOccupant(forwardSquare) ? "occupied" : "empty" : "off board"}); its diagonal capture squares are ${captureSquares.map((square) => `${square} (${squareOccupant(square) ? "occupied" : "empty"})`).join(" and ") || "none"}. It has ${ranksRemaining} rank${ranksRemaining === 1 ? "" : "s"} remaining to ${promotionSquare}; ${pawnState.opposingPawnBlockers.length ? `opposing pawns ahead on its file or adjacent files are ${pawnState.opposingPawnBlockers.join(", ")}` : "no opposing pawn is ahead on its file or either adjacent file"}. This geometry does not assert that a future advance stays legal after the opponent replies.`,
1437
+ participants: target.participants,
1438
+ data: {
1439
+ from,
1440
+ to,
1441
+ color: moverColor,
1442
+ forwardSquare,
1443
+ forwardSquareOccupant: squareOccupant(forwardSquare),
1444
+ captureSquares,
1445
+ captureSquareOccupants: captureSquares.map((square) => ({ square, occupant: squareOccupant(square) })),
1446
+ promotionSquare,
1447
+ ranksRemaining,
1448
+ hasNoOpposingPawnAheadOnSameOrAdjacentFile: pawnState.passed,
1449
+ opposingPawnBlockers: pawnState.opposingPawnBlockers,
1450
+ opponentKingSquare,
1451
+ opponentKingDistanceToPromotionSquare: opponentKingDistance,
1452
+ sideToMove: after.turn(),
1453
+ },
1454
+ pieceLocations: target.locations,
1455
+ coverage: { domain: "target-pawn-advance", exhaustive: true, covered: [request.context.moveUci, to, promotionSquare], unresolved: [] },
1456
+ replay: { fen: request.context.fenBefore, movesUci: [request.context.moveUci] },
1457
+ }));
1458
+ }
1459
+
1460
+ push(createRecord({
1461
+ contextHash,
1462
+ kind: "material.inventory",
1463
+ phase: "after",
1464
+ authority: "legal-exact",
1465
+ priority: 30,
1466
+ summary: "The complete after-position piece inventory is recorded without assigning strategic meaning.",
1467
+ participants: target.afterParticipants,
1468
+ data: { pieces: inventory(after, timeline.after) },
1469
+ pieceLocations: inventory(after, timeline.after).flatMap((piece) => piece.pieceId ? [{ pieceId: piece.pieceId, currentSquare: piece.square, phase: "after" }] : []),
1470
+ coverage: { domain: "after-position-piece-inventory", exhaustive: true, covered: ["all-occupied-squares"], unresolved: [] },
1471
+ replay: { fen: request.context.fenAfter },
1472
+ }));
1473
+
1474
+ const beforeWhite = nominalMaterial(beforeBoard, "w");
1475
+ const beforeBlack = nominalMaterial(beforeBoard, "b");
1476
+ const afterWhite = nominalMaterial(after, "w");
1477
+ const afterBlack = nominalMaterial(after, "b");
1478
+ push(createRecord({
1479
+ contextHash,
1480
+ kind: "material.nominal_delta",
1481
+ phase: "transition",
1482
+ authority: "deterministic-derived",
1483
+ priority: move.captured || move.promotion ? 86 : 28,
1484
+ summary: `Nominal material changes from White ${beforeWhite}/Black ${beforeBlack} to White ${afterWhite}/Black ${afterBlack}; this arithmetic is not an engine evaluation.`,
1485
+ participants: target.participants,
1486
+ data: { values: NOMINAL_VALUES, before: { white: beforeWhite, black: beforeBlack }, after: { white: afterWhite, black: afterBlack }, delta: { white: afterWhite - beforeWhite, black: afterBlack - beforeBlack } },
1487
+ pieceLocations: target.locations,
1488
+ coverage: { domain: "target-move-material", exhaustive: true, covered: [request.context.moveUci], unresolved: [] },
1489
+ replay: { fen: request.context.fenBefore, movesUci: [request.context.moveUci] },
1490
+ }));
1491
+
1492
+ push(createRecord({
1493
+ contextHash,
1494
+ kind: "position.legal_move_count",
1495
+ phase: "after",
1496
+ authority: "legal-exact",
1497
+ priority: 20,
1498
+ summary: `The side to move after ${move.san} has ${afterLegalMoves.length} legal moves.`,
1499
+ participants: [],
1500
+ data: { sideToMove: after.turn(), legalMoveCount: afterLegalMoves.length },
1501
+ pieceLocations: [],
1502
+ coverage: { domain: "all-legal-moves-after", exhaustive: true, covered: ["all-legal-moves"], unresolved: [] },
1503
+ replay: { fen: request.context.fenAfter },
1504
+ }));
1505
+
1506
+ const replyPieceLocations = afterLegalMoves.map((reply) => ({
1507
+ pieceId: pieceIdAt(timeline.after, reply.from as Square),
1508
+ currentSquare: reply.from,
1509
+ phase: "after",
1510
+ })).filter((item): item is { pieceId: string; currentSquare: Square; phase: string } => Boolean(item.pieceId))
1511
+ .filter((item, index, all) => all.findIndex((candidate) => candidate.pieceId === item.pieceId && candidate.currentSquare === item.currentSquare) === index);
1512
+ push(createRecord({
1513
+ contextHash,
1514
+ kind: "position.legal_moves",
1515
+ phase: "after",
1516
+ authority: "legal-exact",
1517
+ priority: after.isCheck() || afterLegalMoves.length <= 4 ? 92 : 26,
1518
+ summary: `${colorName(after.turn())} has exactly ${afterLegalMoves.length} legal move${afterLegalMoves.length === 1 ? "" : "s"} after ${move.san}: ${afterLegalMoves.map((reply) => reply.san).join(", ") || "none"}.`,
1519
+ participants: [],
1520
+ data: { sideToMove: after.turn(), moves: afterLegalMoves.map((reply) => ({ uci: moveUci(reply), san: reply.san, moverPieceId: pieceIdAt(timeline.after, reply.from as Square), from: reply.from, to: reply.to, capture: reply.captured || null, promotion: reply.promotion || null })) },
1521
+ pieceLocations: replyPieceLocations,
1522
+ coverage: { domain: "all-legal-moves-after", exhaustive: true, covered: afterLegalMoves.map(moveUci), unresolved: [] },
1523
+ replay: { fen: request.context.fenAfter },
1524
+ }));
1525
+
1526
+ if (signal?.aborted) throw signal.reason || new Error("Chess fact request cancelled");
1527
+ if (Date.now() - startedAt > request.budgets.maxWallTimeMs) throw new Error("Chess fact request exceeded its wall-time budget");
1528
+ const requested = new Set(request.requestedKinds);
1529
+ const ordered = [...candidates].sort((left, right) => right.priority - left.priority || left.kind.localeCompare(right.kind) || left.id.localeCompare(right.id));
1530
+ const selected = requested.size ? ordered.filter((record) => requested.has(record.kind)) : ordered;
1531
+ const omitted: ChessFactResult["omitted"] = requested.size
1532
+ ? ordered.filter((record) => !requested.has(record.kind)).map((record) => ({ kind: record.kind, reason: "not-requested", recordId: record.id }))
1533
+ : [];
1534
+ const records: ChessFactRecord[] = [];
1535
+ let resultBytes = bytes({ schemaVersion: CHESS_FACT_RESULT_VERSION, contextHash, records: [], capabilities: CAPABILITIES, omitted: [], unavailableKinds: [], limitations: [] });
1536
+ // A caller asking for a named subset owns that subset. For the default
1537
+ // complete target-move collection, reserve the first ordinary budget slot
1538
+ // for the one protected destination geometry record. The protection never
1539
+ // expands maxRecords or maxBytes: if the caller's byte cap cannot express
1540
+ // that required target-move fact, fail explicitly rather than returning a
1541
+ // silently incomplete geometry set.
1542
+ const budgetProtected = requested.size ? [] : selected.filter((record) => record.budgetProtected);
1543
+ const ordinary = selected.filter((record) => !record.budgetProtected);
1544
+ for (const candidate of budgetProtected) {
1545
+ const record = { ...candidate } as Partial<CandidateRecord>;
1546
+ delete record.priority;
1547
+ delete record.budgetProtected;
1548
+ const fact = record as ChessFactRecord;
1549
+ const recordBytes = bytes(fact);
1550
+ if (records.length >= request.budgets.maxRecords) {
1551
+ throw new Error("target-destination-geometry-budget-invariant: maxRecords cannot reserve the required destination attack-map record");
1552
+ }
1553
+ if (resultBytes + recordBytes > request.budgets.maxBytes) {
1554
+ throw new Error("target-destination-geometry-budget-invariant: maxBytes cannot contain the required destination attack-map record");
1555
+ }
1556
+ records.push(fact);
1557
+ resultBytes += recordBytes;
1558
+ }
1559
+ for (const candidate of ordinary) {
1560
+ const record = { ...candidate } as Partial<CandidateRecord>;
1561
+ delete record.priority;
1562
+ delete record.budgetProtected;
1563
+ const fact = record as ChessFactRecord;
1564
+ const recordBytes = bytes(fact);
1565
+ if (records.length >= request.budgets.maxRecords || resultBytes + recordBytes > request.budgets.maxBytes) {
1566
+ omitted.push({ kind: fact.kind, reason: "budget", recordId: fact.id });
1567
+ } else {
1568
+ records.push(fact);
1569
+ resultBytes += recordBytes;
1570
+ }
1571
+ }
1572
+ const availableKinds = new Set(CAPABILITIES.map((capability) => capability.kind));
1573
+ return {
1574
+ schemaVersion: CHESS_FACT_RESULT_VERSION,
1575
+ contextHash,
1576
+ records,
1577
+ capabilities: [...CAPABILITIES],
1578
+ omitted,
1579
+ unavailableKinds: [...requested].filter((kind) => !availableKinds.has(kind)).map((kind) => ({ kind, reason: "The mechanical provider does not implement this registry kind." })),
1580
+ limitations: [
1581
+ "Mechanical browser provider slice; no semantic plans, classifications, engine evaluation, SEE, or exchange-safety claims are emitted.",
1582
+ "Geometric attack deltas do not establish that a target is tactically loose, hanging, or safely capturable.",
1583
+ ],
1584
+ };
1585
+ }
1586
+ }
1587
+
1588
+ export function createMechanicalFactRequest(
1589
+ context: ReasoningPositionContext,
1590
+ overrides: Partial<ChessFactRequest["budgets"]> = {},
1591
+ ): ChessFactRequest {
1592
+ return {
1593
+ schemaVersion: CHESS_FACT_REQUEST_VERSION,
1594
+ requestId: `mechanical-facts:${reasoningContextHash(context)}`,
1595
+ context,
1596
+ requestedKinds: [],
1597
+ // This helper is used by offline experiments and deterministic regression
1598
+ // fixtures. Live browser requests carry the run's explicit budget through
1599
+ // ChessFactObservationProvider instead of inheriting this default.
1600
+ budgets: { maxRecords: 64, maxBytes: 160_000, maxWallTimeMs: 5_000, ...overrides },
1601
+ };
1602
+ }