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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/LICENSE +235 -0
  2. package/LICENSING.md +26 -0
  3. package/NOTICE +5 -0
  4. package/NPM-PUBLISHING.md +28 -0
  5. package/README.md +139 -2
  6. package/SOURCE-ORIGIN.json +16 -0
  7. package/THIRD_PARTY_NOTICES.md +18 -0
  8. package/build.mjs +22 -0
  9. package/dist/answer-pipeline.test.mjs +375 -0
  10. package/dist/chunk-PLKNJRPF.js +10662 -0
  11. package/dist/chunk-XQDZJOYD.js +2515 -0
  12. package/dist/index.js +146 -0
  13. package/dist/owner-tests/chess-reasoning-compact-search-preparation.test.mjs +963 -0
  14. package/dist/owner-tests/mechanism-encoding.test.mjs +529 -0
  15. package/dist/owner-tests/move-mechanism-facts.test.mjs +396 -0
  16. package/dist/owner-tests/pipeline-host-preparation.test.mjs +10556 -0
  17. package/dist/owner-tests/position-preparation.test.mjs +625 -0
  18. package/dist/owner-tests/preparation-consolidation.test.mjs +1052 -0
  19. package/dist/owner-tests/prose-reference-admission.test.mjs +1770 -0
  20. package/dist/owner-tests/score-comparison-notices.test.mjs +240 -0
  21. package/dist/owner-tests/simple-engine-presentation.test.mjs +233 -0
  22. package/dist/owner-tests/tool-input-loop-guard.test.mjs +78 -0
  23. package/dist/research.js +2274 -0
  24. package/dist/runtime-closure.json +103 -0
  25. package/dist/shared-runtime-closure.json +39 -0
  26. package/dist/shared.js +152 -0
  27. package/dist/types/ai-request-failure.d.ts +14 -0
  28. package/dist/types/ai-request-limits.d.ts +4 -0
  29. package/dist/types/ai-routing-diagnostics.d.ts +14 -0
  30. package/dist/types/chess-reasoning/ablation-adaptive-evidence-loop.d.ts +127 -0
  31. package/dist/types/chess-reasoning/ablation-adaptive-runtime.d.ts +178 -0
  32. package/dist/types/chess-reasoning/ablation-direct-synthesis.d.ts +1030 -0
  33. package/dist/types/chess-reasoning/ablation-evidence-bundle.d.ts +185 -0
  34. package/dist/types/chess-reasoning/ablation-evidence-capture.d.ts +37 -0
  35. package/dist/types/chess-reasoning/ablation-lean-direct-runtime.d.ts +193 -0
  36. package/dist/types/chess-reasoning/ablation-model-facing-evidence.d.ts +38 -0
  37. package/dist/types/chess-reasoning/ablation-transport-correction.d.ts +13 -0
  38. package/dist/types/chess-reasoning/ai-sdk-runtime-support.d.ts +93 -0
  39. package/dist/types/chess-reasoning/answer-host-contracts.d.ts +59 -0
  40. package/dist/types/chess-reasoning/answer-model-connector.d.ts +68 -0
  41. package/dist/types/chess-reasoning/answer-pipeline-exports.d.ts +98 -0
  42. package/dist/types/chess-reasoning/answer-pipeline.d.ts +41 -0
  43. package/dist/types/chess-reasoning/answer-reference-validation.d.ts +40 -0
  44. package/dist/types/chess-reasoning/answer-tool-host.d.ts +8 -0
  45. package/dist/types/chess-reasoning/branch-expansion-policy.d.ts +13 -0
  46. package/dist/types/chess-reasoning/branch-expansion.d.ts +39 -0
  47. package/dist/types/chess-reasoning/causal-claim-checks.d.ts +21 -0
  48. package/dist/types/chess-reasoning/chess-reference-dictionary.d.ts +13 -0
  49. package/dist/types/chess-reasoning/classification-question-target.d.ts +22 -0
  50. package/dist/types/chess-reasoning/classification-reassessment.d.ts +31 -0
  51. package/dist/types/chess-reasoning/compact-search-preparation.d.ts +7 -0
  52. package/dist/types/chess-reasoning/concept-registry.d.ts +99 -0
  53. package/dist/types/chess-reasoning/conversation-context.d.ts +12 -0
  54. package/dist/types/chess-reasoning/engine-search-failure.d.ts +7 -0
  55. package/dist/types/chess-reasoning/evidence-citation-normalization.d.ts +9 -0
  56. package/dist/types/chess-reasoning/evidence-licenses.d.ts +57 -0
  57. package/dist/types/chess-reasoning/evidence-selector-contract.d.ts +5 -0
  58. package/dist/types/chess-reasoning/existing-analysis-context.d.ts +104 -0
  59. package/dist/types/chess-reasoning/fact-observation-provider.d.ts +15 -0
  60. package/dist/types/chess-reasoning/fact-provider.d.ts +111 -0
  61. package/dist/types/chess-reasoning/fresh-engine-pv-witnesses.d.ts +67 -0
  62. package/dist/types/chess-reasoning/historical-prose.d.ts +23 -0
  63. package/dist/types/chess-reasoning/idea-graph.d.ts +34 -0
  64. package/dist/types/chess-reasoning/initial-search-plan.d.ts +37 -0
  65. package/dist/types/chess-reasoning/lossless-evidence-sharing.d.ts +18 -0
  66. package/dist/types/chess-reasoning/matched-comparison.d.ts +10 -0
  67. package/dist/types/chess-reasoning/material-accounting.d.ts +80 -0
  68. package/dist/types/chess-reasoning/material-format.d.ts +4 -0
  69. package/dist/types/chess-reasoning/mechanical-fact-provider.d.ts +33 -0
  70. package/dist/types/chess-reasoning/mechanical-ray-facts.d.ts +19 -0
  71. package/dist/types/chess-reasoning/mechanism-presentation.d.ts +9 -0
  72. package/dist/types/chess-reasoning/move-mechanism-facts.d.ts +45 -0
  73. package/dist/types/chess-reasoning/native-answer-connector.d.ts +4 -0
  74. package/dist/types/chess-reasoning/opening-source-contracts.d.ts +54 -0
  75. package/dist/types/chess-reasoning/opening-source-credits.d.ts +16 -0
  76. package/dist/types/chess-reasoning/opponent-threat-preparation.d.ts +7 -0
  77. package/dist/types/chess-reasoning/piece-identities.d.ts +11 -0
  78. package/dist/types/chess-reasoning/poc-contracts.d.ts +1026 -0
  79. package/dist/types/chess-reasoning/poc-hypotheses.d.ts +74 -0
  80. package/dist/types/chess-reasoning/poc-move-witnesses.d.ts +13 -0
  81. package/dist/types/chess-reasoning/poc-observations.d.ts +23 -0
  82. package/dist/types/chess-reasoning/poc-piece-refs.d.ts +12 -0
  83. package/dist/types/chess-reasoning/poc-plan-model.d.ts +45 -0
  84. package/dist/types/chess-reasoning/poc-semantic-text.d.ts +8 -0
  85. package/dist/types/chess-reasoning/poc-tools.d.ts +81 -0
  86. package/dist/types/chess-reasoning/portable-answer-runtime.d.ts +291 -0
  87. package/dist/types/chess-reasoning/position-analysis-contracts.d.ts +31 -0
  88. package/dist/types/chess-reasoning/position-analysis-scope.d.ts +3 -0
  89. package/dist/types/chess-reasoning/preparation-consolidation.d.ts +21 -0
  90. package/dist/types/chess-reasoning/production-observation-provider.d.ts +16 -0
  91. package/dist/types/chess-reasoning/question-routing.d.ts +326 -0
  92. package/dist/types/chess-reasoning/recorded-main-line.d.ts +37 -0
  93. package/dist/types/chess-reasoning/recorded-position-tools.d.ts +11 -0
  94. package/dist/types/chess-reasoning/recorded-reference-context.d.ts +13 -0
  95. package/dist/types/chess-reasoning/recorded-selected-classification.d.ts +36 -0
  96. package/dist/types/chess-reasoning/reference-limits.d.ts +2 -0
  97. package/dist/types/chess-reasoning/research-exports.d.ts +197 -0
  98. package/dist/types/chess-reasoning/saved-score-presentation.d.ts +14 -0
  99. package/dist/types/chess-reasoning/score-comparison-notices.d.ts +33 -0
  100. package/dist/types/chess-reasoning/server-simple-policy.d.ts +31 -0
  101. package/dist/types/chess-reasoning/simple-agent-config.d.ts +73 -0
  102. package/dist/types/chess-reasoning/simple-board-analysis.d.ts +85 -0
  103. package/dist/types/chess-reasoning/simple-compact-evidence.d.ts +7 -0
  104. package/dist/types/chess-reasoning/simple-completion.d.ts +23 -0
  105. package/dist/types/chess-reasoning/simple-engine-presentation.d.ts +3 -0
  106. package/dist/types/chess-reasoning/simple-evidence-preparation.d.ts +17 -0
  107. package/dist/types/chess-reasoning/simple-evidence-view.d.ts +17 -0
  108. package/dist/types/chess-reasoning/simple-explanation-coverage.d.ts +90 -0
  109. package/dist/types/chess-reasoning/simple-fact-delivery.d.ts +43 -0
  110. package/dist/types/chess-reasoning/simple-fallback-premise.d.ts +5 -0
  111. package/dist/types/chess-reasoning/simple-prose-references.d.ts +108 -0
  112. package/dist/types/chess-reasoning/simple-runtime-version.d.ts +1 -0
  113. package/dist/types/chess-reasoning/stable.d.ts +2 -0
  114. package/dist/types/chess-reasoning/tool-input-loop-guard.d.ts +5 -0
  115. package/dist/types/chess-reasoning/types.d.ts +180 -0
  116. package/dist/types/chess-reasoning/visible-support-lines.d.ts +12 -0
  117. package/dist/types/chess-reasoning/wikibooks-index.d.ts +30 -0
  118. package/dist/types/chess-reasoning/wikibooks-tool-context.d.ts +10 -0
  119. package/dist/types/player-perspective.d.ts +16 -0
  120. package/dist/types/shared-exports.d.ts +22 -0
  121. package/docs/API.md +82 -0
  122. package/docs/OWNERSHIP-ADOPTION.md +35 -0
  123. package/docs/OWNERSHIP-DRIFT.json +55 -0
  124. package/docs/RESEARCH-API.md +28 -0
  125. package/licenses/third-party/INVENTORY.json +202 -0
  126. package/licenses/third-party/ai-7.0.85/LICENSE +13 -0
  127. package/licenses/third-party/ai-sdk--gateway-4.0.69/LICENSE +13 -0
  128. package/licenses/third-party/ai-sdk--provider-4.0.9/LICENSE +13 -0
  129. package/licenses/third-party/ai-sdk--provider-utils-5.0.34/APACHE-2.0.txt +73 -0
  130. package/licenses/third-party/ai-sdk--provider-utils-5.0.34/src/to-json-schema/zod3-to-json-schema/LICENSE +16 -0
  131. package/licenses/third-party/chess.js-1.4.0/LICENSE +23 -0
  132. package/licenses/third-party/eventsource-parser-3.1.1/LICENSE +21 -0
  133. package/licenses/third-party/json-schema-0.4.0/LICENSE +195 -0
  134. package/licenses/third-party/standard-schema--spec-1.1.0/LICENSE +21 -0
  135. package/licenses/third-party/types--json-schema-7.0.15/LICENSE +21 -0
  136. package/licenses/third-party/types--node-22.19.19/LICENSE +21 -0
  137. package/licenses/third-party/undici-7.30.0/LICENSE +21 -0
  138. package/licenses/third-party/undici-7.30.0/lib/web/fetch/LICENSE +21 -0
  139. package/licenses/third-party/undici-types-6.21.0/LICENSE +21 -0
  140. package/licenses/third-party/vercel--oidc-3.2.0/LICENSE +202 -0
  141. package/licenses/third-party/workflow--serde-4.1.0/APACHE-2.0.txt +73 -0
  142. package/licenses/third-party/workflow--serde-4.1.0/LICENSE.md +3 -0
  143. package/licenses/third-party/zod-4.6.5/LICENSE +21 -0
  144. package/npm-release.json +5 -0
  145. package/package.json +72 -4
  146. package/scripts/check-release-tooling.mjs +22 -0
  147. package/scripts/pack.mjs +68 -0
  148. package/scripts/publish-reviewed.mjs +85 -0
  149. package/scripts/release-tooling.json +13 -0
  150. package/scripts/update-dependency.mjs +44 -0
  151. package/scripts/verify-release.mjs +52 -0
  152. package/src/ai-request-failure.ts +34 -0
  153. package/src/ai-request-limits.ts +4 -0
  154. package/src/ai-routing-diagnostics.ts +33 -0
  155. package/src/chess-reasoning/ablation-adaptive-evidence-loop.ts +515 -0
  156. package/src/chess-reasoning/ablation-adaptive-runtime.ts +924 -0
  157. package/src/chess-reasoning/ablation-direct-synthesis.ts +1128 -0
  158. package/src/chess-reasoning/ablation-evidence-bundle.ts +726 -0
  159. package/src/chess-reasoning/ablation-evidence-capture.ts +513 -0
  160. package/src/chess-reasoning/ablation-lean-direct-runtime.ts +670 -0
  161. package/src/chess-reasoning/ablation-model-facing-evidence.ts +212 -0
  162. package/src/chess-reasoning/ablation-transport-correction.ts +106 -0
  163. package/src/chess-reasoning/ai-sdk-runtime-support.ts +381 -0
  164. package/src/chess-reasoning/answer-host-contracts.ts +10 -0
  165. package/src/chess-reasoning/answer-model-connector.ts +83 -0
  166. package/src/chess-reasoning/answer-pipeline-exports.ts +98 -0
  167. package/src/chess-reasoning/answer-pipeline.ts +195 -0
  168. package/src/chess-reasoning/answer-reference-validation.ts +124 -0
  169. package/src/chess-reasoning/answer-tool-host.ts +10 -0
  170. package/src/chess-reasoning/branch-expansion-policy.ts +32 -0
  171. package/src/chess-reasoning/branch-expansion.ts +80 -0
  172. package/src/chess-reasoning/causal-claim-checks.ts +256 -0
  173. package/src/chess-reasoning/chess-reference-dictionary.ts +21 -0
  174. package/src/chess-reasoning/classification-question-target.ts +30 -0
  175. package/src/chess-reasoning/classification-reassessment.ts +34 -0
  176. package/src/chess-reasoning/compact-search-preparation.ts +125 -0
  177. package/src/chess-reasoning/concept-registry.ts +275 -0
  178. package/src/chess-reasoning/conversation-context.ts +29 -0
  179. package/src/chess-reasoning/engine-search-failure.ts +11 -0
  180. package/src/chess-reasoning/evidence-citation-normalization.ts +15 -0
  181. package/src/chess-reasoning/evidence-licenses.ts +166 -0
  182. package/src/chess-reasoning/evidence-selector-contract.ts +5 -0
  183. package/src/chess-reasoning/existing-analysis-context.ts +544 -0
  184. package/src/chess-reasoning/fact-observation-provider.ts +134 -0
  185. package/src/chess-reasoning/fact-provider.ts +250 -0
  186. package/src/chess-reasoning/fresh-engine-pv-witnesses.ts +128 -0
  187. package/src/chess-reasoning/historical-prose.ts +116 -0
  188. package/src/chess-reasoning/idea-graph.ts +224 -0
  189. package/src/chess-reasoning/initial-search-plan.ts +115 -0
  190. package/src/chess-reasoning/lossless-evidence-sharing.ts +135 -0
  191. package/src/chess-reasoning/matched-comparison.ts +40 -0
  192. package/src/chess-reasoning/material-accounting.ts +55 -0
  193. package/src/chess-reasoning/material-format.ts +47 -0
  194. package/src/chess-reasoning/mechanical-fact-provider.ts +1602 -0
  195. package/src/chess-reasoning/mechanical-ray-facts.ts +123 -0
  196. package/src/chess-reasoning/mechanism-presentation.ts +94 -0
  197. package/src/chess-reasoning/move-mechanism-facts.ts +185 -0
  198. package/src/chess-reasoning/native-answer-connector.ts +13 -0
  199. package/src/chess-reasoning/opening-source-contracts.ts +47 -0
  200. package/src/chess-reasoning/opening-source-credits.ts +53 -0
  201. package/src/chess-reasoning/opponent-threat-preparation.ts +98 -0
  202. package/src/chess-reasoning/piece-identities.ts +159 -0
  203. package/src/chess-reasoning/poc-contracts.ts +1097 -0
  204. package/src/chess-reasoning/poc-hypotheses.ts +759 -0
  205. package/src/chess-reasoning/poc-move-witnesses.ts +170 -0
  206. package/src/chess-reasoning/poc-observations.ts +611 -0
  207. package/src/chess-reasoning/poc-piece-refs.ts +62 -0
  208. package/src/chess-reasoning/poc-plan-model.ts +345 -0
  209. package/src/chess-reasoning/poc-semantic-text.ts +10 -0
  210. package/src/chess-reasoning/poc-tools.ts +978 -0
  211. package/src/chess-reasoning/portable-answer-runtime.ts +684 -0
  212. package/src/chess-reasoning/position-analysis-contracts.ts +88 -0
  213. package/src/chess-reasoning/position-analysis-scope.ts +20 -0
  214. package/src/chess-reasoning/preparation-consolidation.ts +91 -0
  215. package/src/chess-reasoning/production-observation-provider.ts +41 -0
  216. package/src/chess-reasoning/question-routing.ts +332 -0
  217. package/src/chess-reasoning/recorded-main-line.ts +55 -0
  218. package/src/chess-reasoning/recorded-position-tools.ts +78 -0
  219. package/src/chess-reasoning/recorded-reference-context.ts +44 -0
  220. package/src/chess-reasoning/recorded-selected-classification.ts +54 -0
  221. package/src/chess-reasoning/reference-limits.ts +2 -0
  222. package/src/chess-reasoning/research-exports.ts +197 -0
  223. package/src/chess-reasoning/saved-score-presentation.ts +6 -0
  224. package/src/chess-reasoning/score-comparison-notices.ts +115 -0
  225. package/src/chess-reasoning/server-simple-policy.ts +75 -0
  226. package/src/chess-reasoning/simple-agent-config.ts +58 -0
  227. package/src/chess-reasoning/simple-board-analysis.ts +57 -0
  228. package/src/chess-reasoning/simple-compact-evidence.ts +79 -0
  229. package/src/chess-reasoning/simple-completion.ts +37 -0
  230. package/src/chess-reasoning/simple-engine-presentation.ts +95 -0
  231. package/src/chess-reasoning/simple-evidence-preparation.ts +32 -0
  232. package/src/chess-reasoning/simple-evidence-view.ts +69 -0
  233. package/src/chess-reasoning/simple-explanation-coverage.ts +118 -0
  234. package/src/chess-reasoning/simple-fact-delivery.ts +52 -0
  235. package/src/chess-reasoning/simple-fallback-premise.ts +65 -0
  236. package/src/chess-reasoning/simple-prose-references.ts +528 -0
  237. package/src/chess-reasoning/simple-runtime-version.ts +2 -0
  238. package/src/chess-reasoning/stable.ts +18 -0
  239. package/src/chess-reasoning/tool-input-loop-guard.ts +38 -0
  240. package/src/chess-reasoning/types.ts +195 -0
  241. package/src/chess-reasoning/visible-support-lines.ts +44 -0
  242. package/src/chess-reasoning/wikibooks-index.ts +56 -0
  243. package/src/chess-reasoning/wikibooks-tool-context.ts +91 -0
  244. package/src/player-perspective.ts +41 -0
  245. package/src/shared-exports.ts +23 -0
  246. package/test/answer-pipeline.test.mjs +375 -0
  247. package/test/owner/chess-reasoning-compact-search-preparation.test.mjs +111 -0
  248. package/test/owner/mechanism-encoding.test.mjs +78 -0
  249. package/test/owner/move-mechanism-facts.test.mjs +105 -0
  250. package/test/owner/pipeline-host-preparation.test.mjs +115 -0
  251. package/test/owner/position-preparation.test.mjs +22 -0
  252. package/test/owner/preparation-consolidation.test.mjs +74 -0
  253. package/test/owner/prose-reference-admission.test.mjs +26 -0
  254. package/test/owner/score-comparison-notices.test.mjs +116 -0
  255. package/test/owner/simple-engine-presentation.test.mjs +50 -0
  256. package/test/owner/tool-input-loop-guard.test.mjs +25 -0
  257. package/test-types.mts +23 -0
  258. package/tsconfig.json +22 -0
@@ -0,0 +1,611 @@
1
+ import { Chess, SQUARES, type Color, type Move, type Square } from "chess.js";
2
+ import { PARTICIPANT_ROLES, REFERENCE_DOMAINS, participant } from "./concept-registry";
3
+ import {
4
+ REASONING_POC_VERSION,
5
+ reasoningContextHash,
6
+ type ObservationProvider,
7
+ type QueryContract,
8
+ type ReasoningEvidenceRecord,
9
+ type ReasoningObservation,
10
+ type ReasoningObservationSet,
11
+ type ReasoningPositionContext,
12
+ type ReasoningProbeRecipe,
13
+ type ReasoningRankedObservationSet,
14
+ type ReasoningRunControls,
15
+ type SaliencePolicy,
16
+ } from "./poc-contracts";
17
+ import { createPocIdentityTimeline, pieceIdAt } from "./poc-piece-refs";
18
+ import { stableHash } from "./stable";
19
+ import { withEvidenceLicenses } from "./evidence-licenses";
20
+ import type { IdentityState, SemanticParticipant, SemanticValue } from "./types";
21
+ import { factProofStrength } from "./fact-provider";
22
+
23
+ export const DETERMINISTIC_OBSERVATION_PROVIDER_VERSION = "deterministic-position-observations/5" as const;
24
+ export const QUESTION_SALIENCE_POLICY_VERSION = "question-conditioned-observation-salience/4" as const;
25
+
26
+ const PROVIDER_ID = "knights-eye/deterministic-position-observations";
27
+ const CENTRAL_SQUARES = new Set(["c3", "d3", "e3", "f3", "c4", "d4", "e4", "f4", "c5", "d5", "e5", "f5", "c6", "d6", "e6", "f6"]);
28
+
29
+ type ObservationSeed = {
30
+ observationType: string;
31
+ participants: SemanticParticipant[];
32
+ attributes: Record<string, SemanticValue>;
33
+ exactness: "exact" | "heuristic";
34
+ methods: string[];
35
+ probeRecipes?: ReasoningProbeRecipe[];
36
+ summary: string;
37
+ replay: Record<string, SemanticValue>;
38
+ };
39
+
40
+ type CoverageAffordance = { observation: ReasoningObservation; square: Square; redundantSquares: Square[]; entryMoves: Move[] };
41
+
42
+ function opposite(color: Color): Color {
43
+ return color === "w" ? "b" : "w";
44
+ }
45
+
46
+ function uci(move: Move): string {
47
+ return `${move.from}${move.to}${move.promotion || ""}`;
48
+ }
49
+
50
+ function play(chess: Chess, moveUci: string): Move | null {
51
+ return chess.move({ from: moveUci.slice(0, 2), to: moveUci.slice(2, 4), promotion: moveUci.slice(4) || undefined });
52
+ }
53
+
54
+ function stablePieceRef(state: IdentityState, square: Square): string | null {
55
+ return pieceIdAt(state, square);
56
+ }
57
+
58
+ function attackedSquares(chess: Chess, origin: Square, color: Color): Square[] {
59
+ return SQUARES.filter((square) => chess.attackers(square, color).includes(origin));
60
+ }
61
+
62
+ function isCheck(move: Move | null): boolean {
63
+ return Boolean(move?.san.includes("+") || move?.san.includes("#"));
64
+ }
65
+
66
+ function kingSquare(chess: Chess, color: Color): Square | null {
67
+ return SQUARES.find((square) => {
68
+ const piece = chess.get(square);
69
+ return piece?.color === color && piece.type === "k";
70
+ }) || null;
71
+ }
72
+
73
+ function distance(left: Square, right: Square): number {
74
+ return Math.max(Math.abs(left.charCodeAt(0) - right.charCodeAt(0)), Math.abs(Number(left[1]) - Number(right[1])));
75
+ }
76
+
77
+ function quietMoves(chess: Chess, excludedSquares: Set<string> = new Set()): Move[] {
78
+ return chess.moves({ verbose: true })
79
+ .filter((move) => !move.isCapture() && !move.isPromotion() && !isCheck(move) && !excludedSquares.has(move.from) && !excludedSquares.has(move.to))
80
+ .sort((left, right) => uci(left).localeCompare(uci(right)));
81
+ }
82
+
83
+ function makeObservation(contextHash: string, seed: ObservationSeed): { observation: ReasoningObservation; evidence: ReasoningEvidenceRecord[] } {
84
+ const identity = {
85
+ observationType: seed.observationType,
86
+ participants: [...seed.participants].sort((left, right) => `${left.role}:${left.ref.domain}:${left.ref.id}`.localeCompare(`${right.role}:${right.ref.domain}:${right.ref.id}`)),
87
+ attributes: seed.attributes,
88
+ exactness: seed.exactness,
89
+ methods: [...seed.methods].sort(),
90
+ };
91
+ const id = `observation:${stableHash({ contextHash, identity })}`;
92
+ const evidenceId = `evidence:observation-fact:${stableHash({ contextHash, id, replay: seed.replay })}`;
93
+ const authority = seed.exactness === "heuristic" ? "heuristic-affordance" : "deterministic-derived";
94
+ const witnessEvidenceId = seed.exactness === "heuristic"
95
+ ? `evidence:observation-witness:${stableHash({ contextHash, id, replay: seed.replay })}`
96
+ : null;
97
+ const observation: ReasoningObservation = {
98
+ schemaVersion: REASONING_POC_VERSION,
99
+ id,
100
+ contextHash,
101
+ observationType: seed.observationType,
102
+ participants: identity.participants,
103
+ attributes: seed.attributes,
104
+ exactness: seed.exactness,
105
+ provenance: {
106
+ provider: PROVIDER_ID,
107
+ version: DETERMINISTIC_OBSERVATION_PROVIDER_VERSION,
108
+ trust: "deterministic",
109
+ methods: [...new Set(seed.methods)].sort(),
110
+ },
111
+ factEvidenceIds: [evidenceId, ...(witnessEvidenceId ? [witnessEvidenceId] : [])],
112
+ probeRecipes: seed.probeRecipes || [],
113
+ };
114
+ const evidence: ReasoningEvidenceRecord = withEvidenceLicenses({
115
+ schemaVersion: REASONING_POC_VERSION,
116
+ id: evidenceId,
117
+ contextHash,
118
+ kind: "fact",
119
+ source: `${PROVIDER_ID}/${DETERMINISTIC_OBSERVATION_PROVIDER_VERSION}`,
120
+ proofStrength: factProofStrength(authority),
121
+ summary: seed.summary,
122
+ data: {
123
+ observationId: id,
124
+ observationType: seed.observationType,
125
+ participants: identity.participants as unknown as SemanticValue,
126
+ attributes: seed.attributes,
127
+ exactness: seed.exactness,
128
+ authority,
129
+ ...(witnessEvidenceId ? { derivedFromEvidenceIds: [witnessEvidenceId] } : {}),
130
+ },
131
+ coverage: null,
132
+ replay: seed.exactness === "heuristic" ? null : seed.replay,
133
+ });
134
+ const witnessEvidence: ReasoningEvidenceRecord[] = witnessEvidenceId ? [withEvidenceLicenses({
135
+ schemaVersion: REASONING_POC_VERSION,
136
+ id: witnessEvidenceId,
137
+ contextHash,
138
+ kind: "fact",
139
+ source: `${PROVIDER_ID}/${DETERMINISTIC_OBSERVATION_PROVIDER_VERSION}/legal-witness`,
140
+ proofStrength: factProofStrength("legal-exact"),
141
+ summary: `A complete legal witness associated with ${seed.observationType} exists; it establishes sequence legality and identity only, not strategic relevance, purpose, or plan.`,
142
+ data: {
143
+ authority: "legal-exact",
144
+ witnessForObservationId: id,
145
+ scope: "sequence-legality-and-identity",
146
+ },
147
+ coverage: null,
148
+ replay: seed.replay,
149
+ })] : [];
150
+ return { observation, evidence: [evidence, ...witnessEvidence] };
151
+ }
152
+
153
+ function findOccupationWitness(after: Chess, square: Square, entries: Move[], redundantSquares: Square[]) {
154
+ for (const entry of entries) {
155
+ const entered = new Chess(after.fen());
156
+ if (!play(entered, uci(entry))) continue;
157
+ const exchanges = entered.moves({ verbose: true })
158
+ .filter((move) => move.to === square && move.isCapture() && redundantSquares.includes(move.from as Square))
159
+ .sort((left, right) => uci(left).localeCompare(uci(right)));
160
+ for (const exchange of exchanges) {
161
+ const exchanged = new Chess(entered.fen());
162
+ if (!play(exchanged, uci(exchange))) continue;
163
+ const recapture = exchanged.moves({ verbose: true })
164
+ .filter((move) => move.to === square && move.isCapture())
165
+ .sort((left, right) => Number(left.piece !== "p") - Number(right.piece !== "p") || uci(left).localeCompare(uci(right)))
166
+ .find((move) => move.piece !== entry.piece);
167
+ if (!recapture) continue;
168
+ return { line: [uci(entry), uci(exchange), uci(recapture)], entry, exchange, recapture };
169
+ }
170
+ }
171
+ return null;
172
+ }
173
+
174
+ function findQueenRoute(chess: Chess, owner: Color, enemyKing: Square, weakenedPawnOrigin: Square) {
175
+ const routes: Array<{ first: Move; spacer: Move; second: Move; score: number }> = [];
176
+ const firstMoves = chess.moves({ verbose: true }).filter((move) => move.color === owner && move.piece === "q").sort((a, b) => uci(a).localeCompare(uci(b)));
177
+ for (const first of firstMoves) {
178
+ const afterFirst = new Chess(chess.fen());
179
+ if (!play(afterFirst, uci(first))) continue;
180
+ for (const spacer of quietMoves(afterFirst)) {
181
+ const afterSpacer = new Chess(afterFirst.fen());
182
+ if (!play(afterSpacer, uci(spacer))) continue;
183
+ const secondMoves = afterSpacer.moves({ square: first.to as Square, verbose: true })
184
+ .filter((move) => move.piece === "q" && distance(move.to as Square, enemyKing) <= 2);
185
+ for (const second of secondMoves) {
186
+ const score = (second.isCapture() ? 4 : 0) + (3 - distance(second.to as Square, enemyKing))
187
+ + (distance(second.to as Square, weakenedPawnOrigin) <= 1 ? 6 : 0)
188
+ + (CENTRAL_SQUARES.has(first.to) ? 0.25 : 0);
189
+ routes.push({ first, spacer, second, score });
190
+ }
191
+ }
192
+ }
193
+ return routes.sort((left, right) => right.score - left.score || `${uci(left.first)}:${uci(left.second)}`.localeCompare(`${uci(right.first)}:${uci(right.second)}`))[0] || null;
194
+ }
195
+
196
+ function findRemovalAffordance(after: Chess, attackerSquare: Square, targetSquare: Square, owner: Color) {
197
+ const opponent = opposite(owner);
198
+ const targetControl = attackedSquares(after, targetSquare, opponent);
199
+ const waitingMoves = after.moves({ verbose: true })
200
+ .filter((move) => move.from !== targetSquare && !move.isCapture() && !move.isPromotion() && !isCheck(move))
201
+ .map((move) => {
202
+ const position = new Chess(after.fen());
203
+ play(position, uci(move));
204
+ return { move, score: position.attackers(attackerSquare, opponent).includes(move.to as Square) ? 10 + (move.piece === "p" ? 3 : 0) : 0 };
205
+ })
206
+ .sort((left, right) => right.score - left.score || uci(left.move).localeCompare(uci(right.move)));
207
+
208
+ for (const waiting of waitingMoves) {
209
+ const afterWaiting = new Chess(after.fen());
210
+ if (!play(afterWaiting, uci(waiting.move))) continue;
211
+ const capture = afterWaiting.moves({ square: attackerSquare, verbose: true }).find((move) => move.to === targetSquare && move.isCapture());
212
+ if (!capture) continue;
213
+ const afterCapture = new Chess(afterWaiting.fen());
214
+ if (!play(afterCapture, uci(capture))) continue;
215
+ const recaptures = afterCapture.moves({ verbose: true }).filter((move) => move.to === targetSquare && move.isCapture())
216
+ .sort((left, right) => Number(left.piece !== "p") - Number(right.piece !== "p") || uci(left).localeCompare(uci(right)));
217
+ for (const recapture of recaptures) {
218
+ const afterRecapture = new Chess(afterCapture.fen());
219
+ if (!play(afterRecapture, uci(recapture))) continue;
220
+ const followUps = afterRecapture.moves({ verbose: true })
221
+ .filter((move) => targetControl.includes(move.to as Square) && !["p", "k", "q"].includes(move.piece))
222
+ .sort((left, right) => Number(!CENTRAL_SQUARES.has(left.to)) - Number(!CENTRAL_SQUARES.has(right.to)) || uci(left).localeCompare(uci(right)));
223
+ for (const followUp of followUps) {
224
+ const afterFollowUp = new Chess(afterRecapture.fen());
225
+ if (!play(afterFollowUp, uci(followUp))) continue;
226
+ const firstSpacer = quietMoves(afterFollowUp, new Set([targetSquare]))[0];
227
+ if (!firstSpacer) continue;
228
+ const beforeQueen = new Chess(afterFollowUp.fen());
229
+ if (!play(beforeQueen, uci(firstSpacer))) continue;
230
+ const enemyKing = kingSquare(beforeQueen, opponent);
231
+ if (!enemyKing) continue;
232
+ const queenRoute = findQueenRoute(beforeQueen, owner, enemyKing, recapture.from as Square);
233
+ if (!queenRoute) continue;
234
+ return {
235
+ defenseRoots: after.moves({ square: targetSquare, verbose: true }).map(uci).sort(),
236
+ exchangeLine: [uci(waiting.move), uci(capture), uci(recapture)],
237
+ followUp: uci(followUp),
238
+ queenRoute: [uci(queenRoute.first), uci(queenRoute.second)],
239
+ queenWaypoints: [queenRoute.first.from, queenRoute.first.to, queenRoute.second.to],
240
+ fullWitnessLine: [uci(waiting.move), uci(capture), uci(recapture), uci(followUp), uci(firstSpacer), uci(queenRoute.first), uci(queenRoute.spacer), uci(queenRoute.second)],
241
+ followUpSquare: followUp.to as Square,
242
+ goalSquare: queenRoute.second.to as Square,
243
+ supportingPieceSquare: followUp.from as Square,
244
+ queenSquare: queenRoute.first.from as Square,
245
+ };
246
+ }
247
+ }
248
+ }
249
+ return null;
250
+ }
251
+
252
+ export class DeterministicObservationProvider implements ObservationProvider {
253
+ readonly id = PROVIDER_ID;
254
+ readonly version = DETERMINISTIC_OBSERVATION_PROVIDER_VERSION;
255
+
256
+ observe(context: ReasoningPositionContext, _controls: ReasoningRunControls, signal?: AbortSignal): ReasoningObservationSet {
257
+ if (signal?.aborted) throw signal.reason || new Error("Observation extraction cancelled");
258
+ const contextHash = reasoningContextHash(context);
259
+ const before = new Chess(context.fenBefore);
260
+ const after = new Chess(context.fenAfter);
261
+ const owner = before.turn();
262
+ const opponent = opposite(owner);
263
+ const from = context.moveUci.slice(0, 2) as Square;
264
+ const to = context.moveUci.slice(2, 4) as Square;
265
+ const moverBefore = before.get(from);
266
+ const identities = createPocIdentityTimeline(context);
267
+ const moverId = stablePieceRef(identities.before, from);
268
+ const observations: ReasoningObservation[] = [];
269
+ const evidence: ReasoningEvidenceRecord[] = [];
270
+ const coverageAffordances: CoverageAffordance[] = [];
271
+ const add = (seed: ObservationSeed) => {
272
+ const made = makeObservation(contextHash, seed);
273
+ observations.push(made.observation);
274
+ evidence.push(...made.evidence);
275
+ return made.observation;
276
+ };
277
+
278
+ if (moverBefore && moverId) {
279
+ const beforeAttacks = attackedSquares(before, from, owner);
280
+ const afterAttacks = attackedSquares(after, to, owner);
281
+ const newlyAttackedSquares = afterAttacks.filter((square) => !beforeAttacks.includes(square));
282
+ const retainedAttacks = afterAttacks.filter((square) => beforeAttacks.includes(square));
283
+ const lostAttacks = beforeAttacks.filter((square) => !afterAttacks.includes(square));
284
+ const newlyAttackedOccupiedTargets = newlyAttackedSquares.flatMap((square) => {
285
+ const target = after.get(square);
286
+ const pieceId = target?.color === opponent ? stablePieceRef(identities.after, square) : null;
287
+ return target?.color === opponent && pieceId
288
+ ? [{ square, pieceId, pieceType: target.type }]
289
+ : [];
290
+ });
291
+ const newlyDefendedOccupiedAllies = newlyAttackedSquares.flatMap((square) => {
292
+ const target = after.get(square);
293
+ const pieceId = target?.color === owner ? stablePieceRef(identities.after, square) : null;
294
+ return target?.color === owner && pieceId
295
+ ? [{ square, pieceId, pieceType: target.type }]
296
+ : [];
297
+ });
298
+ const salientNewAttacks = newlyAttackedSquares.filter((square) => {
299
+ const target = after.get(square);
300
+ return target?.color === opponent && target.type !== "p" && target.type !== "k";
301
+ });
302
+ add({
303
+ observationType: "target-move-delta",
304
+ participants: [
305
+ participant(PARTICIPANT_ROLES.ACTOR, REFERENCE_DOMAINS.PIECE, moverId),
306
+ ...newlyAttackedOccupiedTargets.flatMap((target) => [
307
+ participant(PARTICIPANT_ROLES.TARGET, REFERENCE_DOMAINS.PIECE, target.pieceId),
308
+ participant(PARTICIPANT_ROLES.TARGET, REFERENCE_DOMAINS.SQUARE, target.square),
309
+ ]),
310
+ ...newlyDefendedOccupiedAllies.flatMap((target) => [
311
+ participant(PARTICIPANT_ROLES.RESOURCE, REFERENCE_DOMAINS.PIECE, target.pieceId),
312
+ participant(PARTICIPANT_ROLES.TARGET, REFERENCE_DOMAINS.SQUARE, target.square),
313
+ ]),
314
+ ],
315
+ attributes: { moveUci: context.moveUci, from, to, newlyAttackedSquares, retainedAttacks, lostAttacks, salientNewAttacks, newlyAttackedOccupiedTargets, newlyDefendedOccupiedAllies },
316
+ exactness: "exact",
317
+ methods: ["legal-move-replay", "before-after-attack-delta", "stable-piece-identity", "occupied-target-resolution"],
318
+ summary: `Exact move and attack-map delta for ${context.moveUci}, including stable identities for newly attacked opponents and newly defended allies; no tactical or strategic meaning is assigned.`,
319
+ replay: { fen: context.fenBefore, move: context.moveUci },
320
+ });
321
+
322
+ for (const square of lostAttacks) {
323
+ const redundantSquares = after.attackers(square, owner).filter((origin) => origin !== to);
324
+ const entryMoves = after.moves({ verbose: true }).filter((move) => move.color === opponent && move.to === square && move.piece !== "p");
325
+ if (!redundantSquares.length || !entryMoves.length) continue;
326
+ const redundantIds = redundantSquares.map((origin) => stablePieceRef(identities.after, origin)).filter((id): id is string => Boolean(id));
327
+ const observation = add({
328
+ observationType: "coverage-handoff-affordance",
329
+ participants: [
330
+ participant(PARTICIPANT_ROLES.ACTOR, REFERENCE_DOMAINS.PIECE, moverId),
331
+ participant(PARTICIPANT_ROLES.TARGET, REFERENCE_DOMAINS.SQUARE, square),
332
+ ...redundantIds.map((id) => participant(PARTICIPANT_ROLES.RESOURCE, REFERENCE_DOMAINS.PIECE, id)),
333
+ ],
334
+ attributes: { square, movedPieceNoLongerAttacks: true, retainedFriendlyAttackers: redundantIds, opponentNonPawnEntryMoves: entryMoves.map(uci).sort() },
335
+ exactness: "exact",
336
+ methods: ["before-after-attack-delta", "legal-entry-enumeration"],
337
+ summary: `The moved piece no longer attacks ${square}; another friendly piece still does, and a non-pawn entry to ${square} is legal.`,
338
+ replay: { fenBefore: context.fenBefore, fenAfter: context.fenAfter, square },
339
+ probeRecipes: [{ tool: "inspect_position", input: { phase: "after", squares: [square, from, to] }, purpose: `Inspect exact occupancy and attack relations around ${square}.` }],
340
+ });
341
+ coverageAffordances.push({ observation, square, redundantSquares, entryMoves });
342
+ }
343
+ }
344
+
345
+ for (const coverage of coverageAffordances) {
346
+ const witness = findOccupationWitness(after, coverage.square, coverage.entryMoves, coverage.redundantSquares);
347
+ if (!witness) continue;
348
+ add({
349
+ observationType: "occupation-change-legal-witness",
350
+ participants: [
351
+ participant(PARTICIPANT_ROLES.SUBJECT, REFERENCE_DOMAINS.SQUARE, coverage.square),
352
+ ...(stablePieceRef(identities.after, witness.entry.from as Square) ? [participant(PARTICIPANT_ROLES.ACTOR, REFERENCE_DOMAINS.PIECE, stablePieceRef(identities.after, witness.entry.from as Square)!)] : []),
353
+ ...(stablePieceRef(identities.after, witness.exchange.from as Square) ? [participant(PARTICIPANT_ROLES.RESOURCE, REFERENCE_DOMAINS.PIECE, stablePieceRef(identities.after, witness.exchange.from as Square)!)] : []),
354
+ ],
355
+ attributes: { square: coverage.square, legalLine: witness.line, initialOccupantType: witness.entry.piece, finalOccupantType: witness.recapture.piece },
356
+ exactness: "exact",
357
+ methods: ["legal-line-enumeration", "occupant-type-comparison"],
358
+ summary: `A legal exchange sequence changes the type of piece occupying ${coverage.square}; the observation does not decide its strategic meaning.`,
359
+ replay: { fen: context.fenAfter, moves: witness.line },
360
+ probeRecipes: [{ tool: "test_line", input: { phase: "after", movesUci: witness.line, inspectSquares: [coverage.square] }, purpose: `Replay the exact occupation change on ${coverage.square}.` }],
361
+ });
362
+ }
363
+
364
+ if (moverBefore && moverId) {
365
+ const opposingKing = kingSquare(after, opponent);
366
+ const targetSquares = SQUARES.filter((square) => {
367
+ const target = after.get(square);
368
+ return target?.color === opponent
369
+ && target.type !== "p"
370
+ && target.type !== "k"
371
+ && Boolean(opposingKing)
372
+ && distance(square, opposingKing!) <= 2
373
+ && after.attackers(square, owner).includes(to)
374
+ && !before.attackers(square, owner).includes(from);
375
+ });
376
+ for (const targetSquare of targetSquares) {
377
+ const targetId = stablePieceRef(identities.after, targetSquare);
378
+ if (!targetId) continue;
379
+ const removal = findRemovalAffordance(after, to, targetSquare, owner);
380
+ if (!removal || !removal.defenseRoots.length) continue;
381
+ const supportId = stablePieceRef(identities.after, removal.supportingPieceSquare);
382
+ const queenId = stablePieceRef(identities.after, removal.queenSquare);
383
+ add({
384
+ observationType: "exchange-follow-up-legal-affordance",
385
+ participants: [
386
+ participant(PARTICIPANT_ROLES.ACTOR, REFERENCE_DOMAINS.PIECE, moverId),
387
+ participant(PARTICIPANT_ROLES.TARGET, REFERENCE_DOMAINS.PIECE, targetId),
388
+ ...(supportId ? [participant(PARTICIPANT_ROLES.RESOURCE, REFERENCE_DOMAINS.PIECE, supportId)] : []),
389
+ ],
390
+ attributes: { targetSquare, legalTargetMoves: removal.defenseRoots, exchangeLine: removal.exchangeLine, followUpMove: removal.followUp, followUpSquare: removal.followUpSquare, conditional: true },
391
+ exactness: "heuristic",
392
+ methods: ["new-attack-delta", "legal-exchange-enumeration", "central-follow-up-search"],
393
+ summary: `A legal conditional exchange-and-follow-up witness exists after the newly created pressure; it is not a forced plan.`,
394
+ replay: { fen: context.fenAfter, moves: [...removal.exchangeLine, removal.followUp] },
395
+ probeRecipes: [
396
+ { tool: "enumerate_defenses", input: { rootMoves: removal.defenseRoots, depth: 12 }, purpose: "Compare the complete enumerated legal-move class for the pressured piece." },
397
+ { tool: "test_line", input: { phase: "after", movesUci: removal.fullWitnessLine, inspectSquares: [targetSquare, removal.followUpSquare, removal.goalSquare] }, purpose: "Replay the complete legal witness without presenting it as a forced variation." },
398
+ ],
399
+ });
400
+ if (queenId) {
401
+ add({
402
+ observationType: "conditional-route-affordance",
403
+ participants: [
404
+ participant(PARTICIPANT_ROLES.ACTOR, REFERENCE_DOMAINS.PIECE, queenId),
405
+ participant(PARTICIPANT_ROLES.GOAL, REFERENCE_DOMAINS.SQUARE, removal.goalSquare),
406
+ ],
407
+ attributes: { pieceId: queenId, waypoints: removal.queenWaypoints, routeMoves: removal.queenRoute, prerequisitesWitness: [...removal.exchangeLine, removal.followUp], fullLegalWitness: removal.fullWitnessLine, routeKind: "conditional-route" },
408
+ exactness: "heuristic",
409
+ methods: ["legal-route-search", "king-proximity-ranking", "full-line-witness"],
410
+ summary: `A queen route through the listed waypoints appears inside a legal witness line. The route is conditional and is not a principal variation or proof of intent.`,
411
+ replay: { fen: context.fenAfter, moves: removal.fullWitnessLine },
412
+ probeRecipes: [{ tool: "test_line", input: { phase: "after", movesUci: removal.fullWitnessLine, inspectSquares: removal.queenWaypoints }, purpose: "Replay the legal witness containing the conditional queen route." }],
413
+ });
414
+ }
415
+ break;
416
+ }
417
+ }
418
+
419
+ const deduped = [...new Map(observations.map((observation) => [observation.id, observation])).values()].sort((a, b) => a.id.localeCompare(b.id));
420
+ const usedEvidence = new Set(deduped.flatMap((observation) => observation.factEvidenceIds));
421
+ return {
422
+ schemaVersion: REASONING_POC_VERSION,
423
+ contextHash,
424
+ observations: deduped,
425
+ evidence: evidence.filter((record) => usedEvidence.has(record.id)),
426
+ providers: [{ provider: this.id, version: this.version }],
427
+ limitations: [
428
+ "Deterministic observations describe facts and affordances only; they do not name plans, roles, changed constraints, or causal intent.",
429
+ ],
430
+ };
431
+ }
432
+ }
433
+
434
+ export class CompositeObservationProvider implements ObservationProvider {
435
+ readonly id = "knights-eye/composite-observation-provider";
436
+ readonly version: string;
437
+
438
+ constructor(private readonly providers: ObservationProvider[]) {
439
+ if (!providers.length) throw new Error("CompositeObservationProvider requires at least one provider");
440
+ this.version = stableHash(providers.map((provider) => ({ id: provider.id, version: provider.version })));
441
+ }
442
+
443
+ async observe(context: ReasoningPositionContext, controls: ReasoningRunControls, signal?: AbortSignal): Promise<ReasoningObservationSet> {
444
+ const contextHash = reasoningContextHash(context);
445
+ const sets = await Promise.all(this.providers.map((provider) => provider.observe(context, controls, signal)));
446
+ for (const set of sets) if (set.contextHash !== contextHash) throw new Error("Observation provider returned stale context");
447
+ const observations = [...new Map(sets.flatMap((set) => set.observations).map((item) => [item.id, item])).values()];
448
+ const evidence = [...new Map(sets.flatMap((set) => set.evidence).map((item) => [item.id, item])).values()];
449
+ return {
450
+ schemaVersion: REASONING_POC_VERSION,
451
+ contextHash,
452
+ observations: observations.sort((a, b) => a.id.localeCompare(b.id)),
453
+ evidence,
454
+ providers: [...new Map(sets.flatMap((set) => set.providers).map((provider) => [`${provider.provider}\u0000${provider.version}`, provider])).values()],
455
+ limitations: [...new Set(sets.flatMap((set) => set.limitations))],
456
+ };
457
+ }
458
+ }
459
+
460
+ const BASE_SALIENCE: Record<string, number> = {
461
+ "target-move-delta": 0.92,
462
+ "analysis-context:material-offer-set": 0.96,
463
+ "analysis-context:engine.bounded-assessment": 0.88,
464
+ "analysis-context:classification.label": 0.28,
465
+ "analysis-context:classifier.missed-opportunity": 0.7,
466
+ "analysis-context:semantic.lifecycle.material-exchange": 0.58,
467
+ "analysis-context:path.semantic.lifecycle.material-exchange": 0.42,
468
+ "coverage-handoff-affordance": 0.86,
469
+ // Preserve salience for older composable records; provider /5 never emits it.
470
+ "recent-castling-and-check-response": 0.82,
471
+ "occupation-change-legal-witness": 0.84,
472
+ "exchange-follow-up-legal-affordance": 0.9,
473
+ "conditional-route-affordance": 0.32,
474
+ };
475
+
476
+ function semanticRecord(value: SemanticValue | undefined): Record<string, SemanticValue> | null {
477
+ return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, SemanticValue> : null;
478
+ }
479
+
480
+ function semanticList(value: SemanticValue | undefined): SemanticValue[] {
481
+ return Array.isArray(value) ? value : [];
482
+ }
483
+
484
+ function mechanicalObservationBase(observation: ReasoningObservation): number | null {
485
+ if (!observation.observationType.startsWith("chess-fact:")) return null;
486
+ const kind = String(observation.attributes.factKind || observation.observationType.slice("chess-fact:".length));
487
+ const data = semanticRecord(observation.attributes.data) || {};
488
+ switch (kind) {
489
+ case "move.blocks_check_line": return 0.98;
490
+ case "castling.king_rook_transition": return 0.95;
491
+ case "move.unblocks_line": return 0.93;
492
+ case "king.checkers_after": return 0.92;
493
+ case "king.legal_moves": return 0.88;
494
+ case "king.geometry_after": return semanticRecord(data.distance)?.chebyshev === 2 ? 0.85 : 0.48;
495
+ case "king_rook.castling_history": return semanticList(data.events).length ? 0.82 : 0.35;
496
+ case "line.occupied_ray_alignment_after_exchange": return data.rearPieceType === "q" || data.rearPieceType === "k" ? 0.52 : 0.44;
497
+ case "piece.absolute_pin": return semanticList(data.pins).length ? 0.9 : 0.45;
498
+ case "move.legal_recaptures_after": return semanticList(data.moves).length
499
+ ? data.allCapturesLoseAfterImmediateSameSquareRecapture === true ? 0.64 : 0.91
500
+ : 0.47;
501
+ case "move.capture_response_availability_after": return semanticList(data.preCaptureBlockedSliderCapturers).length
502
+ || semanticList(data.unavailableGeometricDefenderPieceIds).length
503
+ ? 0.98
504
+ : semanticList(data.legalSameSquareRecaptures).length ? 0.9 : 0.32;
505
+ case "move.capture_check_delta": return data.capture === true || data.givesCheck === true ? 0.94 : 0.5;
506
+ case "piece.attack_delta": return semanticList(data.occupiedOpponentTargets).length ? 0.9 : semanticList(data.gained).length ? 0.8 : 0.55;
507
+ case "piece.legal_moves": return data.pieceType === "q" ? 0.9 : data.pieceType === "r" ? 0.87 : data.pieceType === "b" || data.pieceType === "n" ? 0.84 : 0.68;
508
+ case "square.attacker_delta": return data.occupiedOpponentLostAttack === true
509
+ ? 0.96
510
+ : data.remainsAttackedAfterLoss === true && semanticList(data.sideToMoveLegalEntries).length
511
+ ? 0.88
512
+ : data.newlyContestedLegalEntry === true
513
+ ? 0.86
514
+ : data.remainsAttackedAfterLoss === true
515
+ ? 0.76
516
+ : 0.58;
517
+ case "square.friendly_nonpawn_reachers_after": return semanticList(data.friendlyNonPawnReachers).some((item) => semanticRecord(item)?.pieceType === "n") ? 0.84 : 0.54;
518
+ case "square.same_square_exchange_witnesses": {
519
+ const outcomes = semanticList(data.lineOutcomes).map(semanticRecord).filter(Boolean);
520
+ const allFirstCapturesLose = outcomes.length > 0 && outcomes.every((outcome) => typeof outcome!.finalNominalBalanceDeltaForFirstCapturingSide === "number" && outcome!.finalNominalBalanceDeltaForFirstCapturingSide < 0);
521
+ return allFirstCapturesLose ? 0.6 : data.attackerLossRetainedAndLegalEntry === true ? 0.87 : 0.57;
522
+ }
523
+ case "pawn.advance_state": return data.ranksRemaining === 1 ? 0.97 : 0.94;
524
+ case "position.legal_moves": return semanticList(data.moves).length <= 5 ? 0.92 : 0.52;
525
+ case "king.check_state": return data.inCheck === true ? observation.attributes.phase === "before" ? 0.97 : 0.93 : 0.46;
526
+ case "material.nominal_delta": return semanticRecord(data.delta) && Object.values(semanticRecord(data.delta)!).some((value) => typeof value === "number" && value !== 0) ? 0.82 : 0.38;
527
+ case "move.legal_transition": return 0.74;
528
+ case "square.attackers_delta_inventory": return 0.3;
529
+ case "material.inventory": return 0.28;
530
+ case "position.legal_move_count": return 0.25;
531
+ default: return 0.5;
532
+ }
533
+ }
534
+
535
+ function existingAnalysisObservationBase(observation: ReasoningObservation, question: string): { base: number; reason: string } | null {
536
+ if (!observation.observationType.startsWith("analysis-context:")) return null;
537
+ const asksAboutClassification = /\b(classif(?:y|ied|ication)?|label(?:led|ed)?|call(?:ed|ing)?|grade(?:d)?|rating)\b/i.test(question);
538
+ const asksForComparison = /\b(best|better|worse|prefer(?:able|red)?|instead|alternative|compar(?:e|ed|ison)|score|evaluation|engine|support(?:s|ed)?)\b/i.test(question);
539
+ const asksAboutMaterial = /\b(material|offer(?:ed|ing)?|sacrific(?:e|ed|ial)?|captur(?:e|ed|ing)|exchange|pawn|piece)\b/i.test(question);
540
+ const asksAboutHistory = /\b(earlier|later|previous|prior|subsequent|persist(?:s|ed|ent)?|retain(?:s|ed)?|redirect(?:s|ed)?|abandon(?:s|ed)?|settle(?:s|d)?|convert(?:s|ed)?|throughout|over the game)\b/i.test(question);
541
+ switch (observation.observationType) {
542
+ case "analysis-context:material-offer-set": {
543
+ const offers = semanticList(observation.attributes.offers);
544
+ const screened = semanticList(observation.attributes.screenedCandidates);
545
+ if (offers.length) return { base: 0.96, reason: "context-content:qualifying-offer" };
546
+ if (asksAboutMaterial) return { base: screened.length ? 0.78 : 0.68, reason: "question-relevance:material" };
547
+ return { base: screened.length ? 0.38 : 0.16, reason: screened.length ? "context-content:screened-only" : "context-content:empty-offer-set" };
548
+ }
549
+ case "analysis-context:classification.label":
550
+ return asksAboutClassification
551
+ ? { base: 0.92, reason: "question-relevance:reported-classification" }
552
+ : { base: 0.28, reason: "context-authority:label-only" };
553
+ case "analysis-context:engine.bounded-assessment":
554
+ return asksForComparison || asksAboutClassification
555
+ ? { base: 0.94, reason: "question-relevance:bounded-assessment" }
556
+ : { base: 0.88, reason: "context-content:bounded-assessment" };
557
+ case "analysis-context:classifier.missed-opportunity":
558
+ return asksForComparison
559
+ ? { base: 0.82, reason: "question-relevance:missed-opportunity" }
560
+ : { base: 0.4, reason: "context-authority:diagnostic-affordance" };
561
+ case "analysis-context:semantic.lifecycle.material-exchange":
562
+ case "analysis-context:path.semantic.lifecycle.material-exchange":
563
+ return asksAboutMaterial || asksAboutHistory
564
+ ? { base: observation.observationType.startsWith("analysis-context:path.") ? 0.72 : 0.8, reason: "question-relevance:material-lifecycle" }
565
+ : { base: observation.observationType.startsWith("analysis-context:path.") ? 0.24 : 0.32, reason: "context-authority:lifecycle-affordance" };
566
+ default:
567
+ return { base: 0.5, reason: "context-content:generic" };
568
+ }
569
+ }
570
+
571
+ export class QuestionConditionedSaliencePolicy implements SaliencePolicy {
572
+ readonly id = QUESTION_SALIENCE_POLICY_VERSION;
573
+
574
+ constructor(private readonly maxPresented = 16) {}
575
+
576
+ rank(observations: ReasoningObservationSet, contract: QueryContract): ReasoningRankedObservationSet {
577
+ const moveIdeaQuestion = /\b(idea|plan|why|purpose|point|prepare|threat)\b/i.test(contract.question);
578
+ const ranked = observations.observations.map((observation) => {
579
+ const mechanicalBase = mechanicalObservationBase(observation);
580
+ const analysisContext = existingAnalysisObservationBase(observation, contract.question);
581
+ const base = mechanicalBase ?? analysisContext?.base ?? BASE_SALIENCE[observation.observationType] ?? 0.5;
582
+ const questionBoost = moveIdeaQuestion
583
+ && observation.observationType !== "target-move-delta"
584
+ && !analysisContext
585
+ ? 0.06
586
+ : 0;
587
+ const exactnessBoost = observation.exactness === "exact" ? 0.02 : 0;
588
+ return {
589
+ ...observation,
590
+ salience: Math.min(1, Math.round((base + questionBoost + exactnessBoost) * 1_000) / 1_000),
591
+ salienceReasons: [
592
+ moveIdeaQuestion ? "question-shape:move-idea" : "question-shape:general",
593
+ `observation-type:${observation.observationType}`,
594
+ ...(analysisContext ? [analysisContext.reason] : []),
595
+ `exactness:${observation.exactness}`,
596
+ ],
597
+ presentedToModel: false,
598
+ };
599
+ }).sort((a, b) => b.salience - a.salience || a.id.localeCompare(b.id));
600
+ ranked.forEach((observation, index) => { observation.presentedToModel = index < this.maxPresented; });
601
+ return { ...observations, observations: ranked, saliencePolicy: this.id };
602
+ }
603
+ }
604
+
605
+ export function createDefaultObservationProvider(): ObservationProvider {
606
+ return new CompositeObservationProvider([new DeterministicObservationProvider()]);
607
+ }
608
+
609
+ export function createDefaultSaliencePolicy(maxPresented = 16): SaliencePolicy {
610
+ return new QuestionConditionedSaliencePolicy(maxPresented);
611
+ }
@@ -0,0 +1,62 @@
1
+ import { Chess, SQUARES, type PieceSymbol, type Square } from "chess.js";
2
+ import { applyIdentityMove, createIdentityState } from "./piece-identities";
3
+ import type { IdentityState, PieceId, PieceIdentity } from "./types";
4
+ import type { ReasoningPositionContext } from "./poc-contracts";
5
+
6
+ const PIECE_LABELS: Record<PieceSymbol, string> = {
7
+ p: "pawn",
8
+ n: "knight",
9
+ b: "bishop",
10
+ r: "rook",
11
+ q: "queen",
12
+ k: "king",
13
+ };
14
+
15
+ function moveObject(uci: string) {
16
+ return {
17
+ from: uci.slice(0, 2),
18
+ to: uci.slice(2, 4),
19
+ promotion: uci.slice(4) || undefined,
20
+ };
21
+ }
22
+
23
+ function replayFromStart(historyUci: string[], historyStartFen?: string): IdentityState | null {
24
+ let state = createIdentityState(historyStartFen);
25
+ try {
26
+ for (const uci of historyUci) state = applyIdentityMove(state, moveObject(uci)).state;
27
+ return state;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ export type PocIdentityTimeline = {
34
+ before: IdentityState;
35
+ after: IdentityState;
36
+ };
37
+
38
+ export function createPocIdentityTimeline(context: ReasoningPositionContext): PocIdentityTimeline {
39
+ const replayed = replayFromStart(context.historyUci, context.historyStartFen);
40
+ const before = replayed?.fen === new Chess(context.fenBefore).fen()
41
+ ? replayed
42
+ : createIdentityState(context.fenBefore, `reasoning:${context.nodeId}`, context.historyUci.length);
43
+ const after = context.kind === "position" ? before : applyIdentityMove(before, moveObject(context.moveUci)).state;
44
+ if (after.fen !== new Chess(context.fenAfter).fen()) throw new Error("Stable piece-identity replay does not match fenAfter");
45
+ return { before, after };
46
+ }
47
+
48
+ export function pieceIdAt(state: IdentityState, square: Square): PieceId | null {
49
+ return state.squares[square] || null;
50
+ }
51
+
52
+ export function squareForPiece(state: IdentityState, pieceId: PieceId): Square | null {
53
+ return (SQUARES.find((square) => state.squares[square] === pieceId) as Square | undefined) || null;
54
+ }
55
+
56
+ export function pieceIdentity(state: IdentityState, pieceId: PieceId): PieceIdentity | null {
57
+ return state.pieces[pieceId] || null;
58
+ }
59
+
60
+ export function canonicalPieceLabel(piece: PieceIdentity): string {
61
+ return `${piece.color === "w" ? "White" : "Black"} ${PIECE_LABELS[piece.type]}`;
62
+ }