@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,88 @@
1
+ import type {
2
+ PositionAnalysisLine as LegacyPositionAnalysisLine,
3
+ PositionAnalysisResult as LegacyPositionAnalysisResult,
4
+ PositionAnalysisProvider as LegacyPositionAnalysisProvider,
5
+ PositionAnalysisRequest,
6
+ } from './poc-tools';
7
+
8
+ export type { PositionAnalysisRequest } from './poc-tools';
9
+ /** Historical evidence uses matePly, whose values have always been signed moves. */
10
+ export type { LegacyPositionAnalysisLine, LegacyPositionAnalysisResult, LegacyPositionAnalysisProvider };
11
+
12
+ type LegacyCompletion = NonNullable<LegacyPositionAnalysisLine['pvCompletion']>;
13
+ type LegacySegment = LegacyCompletion['segments'][number];
14
+ /** Signed mate-in-moves relative to the declared score perspective; null is no mate score. */
15
+ export type PositionAnalysisSegment = Omit<LegacySegment, 'matePly'> & { mateInMoves: number | null };
16
+ export type PositionAnalysisLine = Omit<LegacyPositionAnalysisLine, 'matePly' | 'pvCompletion'> & {
17
+ mateInMoves: number | null;
18
+ pvCompletion?: Omit<LegacyCompletion, 'segments'> & { segments: PositionAnalysisSegment[] };
19
+ };
20
+ export type PositionAnalysisResult = Omit<LegacyPositionAnalysisResult, 'lines'> & { lines: PositionAnalysisLine[] };
21
+ export interface PositionAnalysisProvider {
22
+ analyze(request: PositionAnalysisRequest): Promise<PositionAnalysisResult>;
23
+ }
24
+ export type CompatiblePositionAnalysisProvider = PositionAnalysisProvider | LegacyPositionAnalysisProvider;
25
+
26
+ const LEGACY_UNIT = 'signed mate-in-moves (legacy field name, not plies)';
27
+ const UNIT = 'signed mate-in-moves';
28
+ type ScoreAliases = { matePly?: number | null; mateInMoves?: number | null; scoreScope?: Record<string, string> };
29
+ function mateValue(score: ScoreAliases): number | null {
30
+ const old = Object.hasOwn(score, 'matePly');
31
+ const current = Object.hasOwn(score, 'mateInMoves');
32
+ if (!old && !current) throw new TypeError('Missing mateInMoves (or legacy matePly).');
33
+ for (const key of ['matePly', 'mateInMoves'] as const) {
34
+ if (Object.hasOwn(score, key) && score[key] !== null && !Number.isSafeInteger(score[key])) {
35
+ throw new TypeError(`Invalid ${key}: expected signed integer moves or null.`);
36
+ }
37
+ }
38
+ if (old && current && score.matePly !== score.mateInMoves) throw new TypeError('Conflicting matePly and mateInMoves.');
39
+ return (current ? score.mateInMoves : score.matePly) as number | null;
40
+ }
41
+ function scope(value: ScoreAliases['scoreScope'], legacy: boolean): ScoreAliases['scoreScope'] {
42
+ if (!value) return value;
43
+ const { matePlyUnit, mateInMovesUnit, ...rest } = value;
44
+ if (matePlyUnit !== undefined && matePlyUnit !== LEGACY_UNIT) throw new TypeError('Invalid legacy mate score unit.');
45
+ if (mateInMovesUnit !== undefined && mateInMovesUnit !== UNIT) throw new TypeError('Invalid mate score unit.');
46
+ // Conversion does not invent missing scope or attest that scores are comparable.
47
+ if (matePlyUnit === undefined && mateInMovesUnit === undefined) return { ...rest };
48
+ return { ...rest, ...(legacy ? { matePlyUnit: LEGACY_UNIT } : { mateInMovesUnit: UNIT }) };
49
+ }
50
+ function convertScore<T extends ScoreAliases>(value: T, legacy: boolean) {
51
+ const mate = mateValue(value);
52
+ const { matePly: _old, mateInMoves: _current, scoreScope, ...rest } = value;
53
+ void _old; void _current;
54
+ return { ...rest, ...(legacy ? { matePly: mate } : { mateInMoves: mate }),
55
+ ...(scoreScope !== undefined ? { scoreScope: scope(scoreScope, legacy) } : {}) };
56
+ }
57
+ function convert(result: PositionAnalysisResult | LegacyPositionAnalysisResult, legacy: boolean) {
58
+ return { ...result, lines: result.lines.map(line => ({ ...convertScore(line, legacy),
59
+ ...(line.pvCompletion ? { pvCompletion: { ...line.pvCompletion,
60
+ segments: line.pvCompletion.segments.map(segment => convertScore(segment, legacy)) } } : {}) })) };
61
+ }
62
+
63
+ /** Rename only: no search, score reinterpretation, schema migration or evidence admission. */
64
+ export function toPositionAnalysis(result: PositionAnalysisResult | LegacyPositionAnalysisResult): PositionAnalysisResult {
65
+ return convert(result, false) as PositionAnalysisResult;
66
+ }
67
+ /** Explicit compatibility conversion for current internal tool/trace wire schemas. */
68
+ export function toLegacyPositionAnalysis(result: PositionAnalysisResult | LegacyPositionAnalysisResult): LegacyPositionAnalysisResult {
69
+ return convert(result, true) as LegacyPositionAnalysisResult;
70
+ }
71
+ /** Public/legacy input normalization; delegates the original request and cancellation signal. */
72
+ export function toLegacyPositionAnalysisProvider(provider: CompatiblePositionAnalysisProvider): LegacyPositionAnalysisProvider {
73
+ return { async analyze(request) {
74
+ request.signal?.throwIfAborted();
75
+ const result = await provider.analyze(request);
76
+ request.signal?.throwIfAborted();
77
+ return toLegacyPositionAnalysis(result);
78
+ } };
79
+ }
80
+ /** Adapt an existing legacy provider to the preferred public signed-moves contract. */
81
+ export function toPositionAnalysisProvider(provider: CompatiblePositionAnalysisProvider): PositionAnalysisProvider {
82
+ return { async analyze(request) {
83
+ request.signal?.throwIfAborted();
84
+ const result = await provider.analyze(request);
85
+ request.signal?.throwIfAborted();
86
+ return toPositionAnalysis(result);
87
+ } };
88
+ }
@@ -0,0 +1,20 @@
1
+ import type { PositionAnalysisRequest, PositionAnalysisResult } from "./poc-tools";
2
+
3
+ /** Both Stockfish adapters retain UCI scores relative to the search root's turn. */
4
+ export function scopePositionAnalysis(result: PositionAnalysisResult, request: PositionAnalysisRequest): PositionAnalysisResult {
5
+ const turn = request.fen.split(" ")[1];
6
+ if (turn !== "w" && turn !== "b") throw new Error("Invalid score root turn");
7
+ const scoreScope = {
8
+ fen: request.fen,
9
+ perspective: turn === "w" ? "white" : "black",
10
+ positiveMeans: "advantage for perspective color",
11
+ scoreCpUnit: "centipawns",
12
+ matePlyUnit: "signed mate-in-moves (legacy field name, not plies)",
13
+ };
14
+ return { ...result, lines: result.lines.map(line => {
15
+ if (line.scoreScope && Object.entries(scoreScope).some(([key, value]) => line.scoreScope?.[key] !== value)) {
16
+ throw new Error("Engine score scope mismatch");
17
+ }
18
+ return { ...line, scoreScope };
19
+ }) };
20
+ }
@@ -0,0 +1,91 @@
1
+ import { compactSearchRecord } from './compact-search-preparation';
2
+ import type { ReasoningEvidenceRecord } from './poc-contracts';
3
+ import { matchedRootComparison } from './matched-comparison';
4
+ import type { PositionAnalysisResult } from './poc-tools';
5
+
6
+ export type PreparedPresentation = {
7
+ callId:string; modelRun:Record<string,unknown>; records:ReasoningEvidenceRecord[];
8
+ playedMove?:string; discoveryCallId?:string; limitations?:string[];
9
+ };
10
+ type Obj=Record<string,unknown>;
11
+ const object=(v:unknown):v is Obj=>!!v && typeof v==='object' && !Array.isArray(v);
12
+ const strings=(v:unknown):v is string[]=>Array.isArray(v)&&v.every(x=>typeof x==='string');
13
+ const finite=(v:unknown):v is number=>typeof v==='number'&&Number.isFinite(v);
14
+ const equal=(a:unknown,b:unknown)=>JSON.stringify(a)===JSON.stringify(b);
15
+
16
+ function search(record:ReasoningEvidenceRecord) {
17
+ const data=record.data;
18
+ if(!object(data.recordedPosition)||!object(data.details))return;
19
+ const root=data.recordedPosition,details=data.details;
20
+ if(!object(details.searchScope)||!Array.isArray(details.lines)||!details.lines.length||!details.lines.every(object))return;
21
+ const scope=details.searchScope,lines=details.lines as Obj[];
22
+ const perspective=typeof root.fen==='string'&&root.fen.split(' ')[1]==='w'?'white':'black';
23
+ if(typeof root.fen!=='string'||scope.fen!==root.fen||!Number.isInteger(root.ply)
24
+ ||!strings(scope.requestedRootMoves)||new Set(lines.map(l=>l.rootMove)).size!==lines.length)return;
25
+ for(const line of lines){
26
+ if(typeof line.rootMove!=='string'||!strings(line.pv)||line.pv[0]!==line.rootMove||!finite(line.depth)
27
+ ||!object(line.scoreScope)||line.scoreScope.fen!==root.fen||line.scoreScope.perspective!==perspective
28
+ ||line.scoreScope.positiveMeans!=='advantage for perspective color')return;
29
+ if(finite(line.matePly)){
30
+ if(line.scoreScope.matePlyUnit!=='signed mate-in-moves (legacy field name, not plies)')return;
31
+ }else if(line.matePly!=null||!finite(line.scoreCp)||Math.abs(line.scoreCp)>=100000||line.scoreScope.scoreCpUnit!=='centipawns')return;
32
+ }
33
+ return {root,details,scope,lines,requestedRootMoves:scope.requestedRootMoves as string[]};
34
+ }
35
+
36
+ /** Consolidates presentation only. Raw searches, evidence and numeric scores are never merged. */
37
+ export function consolidatePreparedSearches(entries:readonly PreparedPresentation[]) {
38
+ const replacements=new Map<string,Record<string,unknown>>(),omitted=new Set<string>();
39
+ const dependencies:Array<Record<string,unknown>>=[];
40
+ for(const primary of entries){
41
+ if(!primary.discoveryCallId)continue;
42
+ const discovery=entries.find(e=>e.callId===primary.discoveryCallId);
43
+ const dependency:Record<string,unknown>={relation:'candidate-selection',discoveryCallId:primary.discoveryCallId,
44
+ comparisonCallId:primary.callId,discoveryEvidenceIds:discovery?.records.map(r=>r.id)??[],comparisonEvidenceIds:primary.records.map(r=>r.id),
45
+ presentation:'separate',reason:'Comparison is not a complete compatible replacement.'};
46
+ dependencies.push(dependency);
47
+ if(!discovery||discovery.modelRun.status!=='completed'||primary.modelRun.status!=='completed'
48
+ ||discovery.records.length!==1||primary.records.length!==1||omitted.has(discovery.callId))continue;
49
+ const original=discovery.records[0],comparison=primary.records[0],a=search(original),b=search(comparison);
50
+ if(!a||!b||!original.source.endsWith('/explore-continuations')||!comparison.source.endsWith('/compare_moves')
51
+ ||original.contextHash!==comparison.contextHash||!equal(a.root,b.root)||a.requestedRootMoves.length
52
+ ||comparison.coverage?.complete!==true||!primary.playedMove||!b.lines.some(l=>l.rootMove===primary.playedMove)
53
+ ||new Set(b.lines.map(l=>l.depth)).size!==1)continue;
54
+ // Only the primary comparison needs a verified common root-search depth.
55
+ // A legacy discovery may remain a separately qualified historical summary.
56
+ if(matchedRootComparison({...b.details,command:b.scope.command,cacheProvenance:b.scope.cacheProvenance,sourceCacheProvenance:b.scope.sourceCacheProvenance} as unknown as PositionAnalysisResult,String(b.root.fen),b.requestedRootMoves).status!=='matched')continue;
57
+ const roots=a.lines.map(l=>String(l.rootMove)),compared=b.requestedRootMoves;
58
+ if(![...roots,primary.playedMove,...compared].every(move=>compared.includes(move)&&comparison.coverage!.covered.includes(move)&&b.lines.some(l=>l.rootMove===move)))continue;
59
+ // A compact projection marker confirms that the schema was recognized. Unknown
60
+ // producer fields fall back to the original presentation rather than disappearing.
61
+ if(!compactSearchRecord(original,primary.playedMove)||!compactSearchRecord(comparison,primary.playedMove))continue;
62
+ const commonComparisonOrder=b.lines.map(l=>String(l.rootMove)).filter(move=>roots.includes(move));
63
+ const differentContinuations=a.lines.filter(line=>{
64
+ const other=b.lines.find(l=>l.rootMove===line.rootMove)!;
65
+ return !equal(line.pv,other.pv)||!equal(line.pvCompletion,other.pvCompletion);
66
+ }).map(l=>l.rootMove);
67
+ const discoverySummary={callId:discovery.callId,evidenceId:original.id,source:original.source,
68
+ engine:a.details.engine,engineBuild:a.details.engineBuild,searchScope:a.scope,
69
+ coverage:original.coverage,summary:original.summary,limitations:discovery.limitations??[],
70
+ scores:a.lines.map(l=>({rootMove:l.rootMove,depth:l.depth,scoreCp:l.scoreCp,matePly:l.matePly,scoreScope:l.scoreScope,
71
+ ...(l.scoreProvenance?{scoreProvenance:l.scoreProvenance}:{}),
72
+ ...(object(l.pvCompletion)?{continuation:{status:l.pvCompletion.status,conditional:l.pvCompletion.conditional,actualPlies:l.pvCompletion.actualPlies,requestedPlies:l.pvCompletion.requestedPlies}}:{})})),
73
+ discoveryOrder:roots,comparisonOrderForDiscoveredMoves:commonComparisonOrder,
74
+ reportedOrderChanged:!equal(roots,commonComparisonOrder),distinctContinuationRootMoves:differentContinuations,
75
+ retrieveFullEvidenceId:original.id,
76
+ interpretation:'Discovery supplied candidates for this comparison. Its scores and reported ordering belong to the discovery search, not the primary comparison. Reported order changes may include tied moves. Full discovery continuations and details remain retrievable; use retrieve_evidence before relying on an omitted discovery line. Score disagreements remain in scoreComparisonNotices. This dependency does not establish that the answering model used or cited either record.'};
77
+ // Keep opening commentary delivered during discovery, including its original
78
+ // associations. It may not be repeated by the subsequent comparison lookup.
79
+ const result=discovery.modelRun.result;
80
+ const discoveryOpeningSourceContext=object(result)?result.openingSourceContext:undefined;
81
+ replacements.set(primary.callId,{...primary.modelRun,discovery:discoverySummary,
82
+ ...(discoveryOpeningSourceContext?{discoveryOpeningSourceContext}:{})});
83
+ omitted.add(discovery.callId);
84
+ Object.assign(dependency,{presentation:'comparison-with-discovery-summary',reason:'Complete same-root comparison covers discovered alternatives and played move.',
85
+ primaryEvidenceId:comparison.id,reportedOrderChanged:discoverySummary.reportedOrderChanged,distinctContinuationRootMoves:differentContinuations});
86
+ }
87
+ return {results:entries.filter(e=>!omitted.has(e.callId)).map(e=>replacements.get(e.callId)??e.modelRun),
88
+ diagnostics:{version:'preparation-consolidation/1',searchCount:entries.length,primaryEntryCount:entries.length-omitted.size,
89
+ summarizedDiscoveryCount:omitted.size,dependencies,
90
+ interpretation:'Candidate-selection and presentation decisions are observable. They do not establish whether uncited records influenced the answer. All authoritative records remain available for retrieval.'}};
91
+ }
@@ -0,0 +1,41 @@
1
+ import {
2
+ EXISTING_ANALYSIS_CONTEXT_MODE,
3
+ ExistingAnalysisObservationProvider,
4
+ normalizeExistingAnalysisContext,
5
+ type ExistingAnalysisContextMode,
6
+ } from "./existing-analysis-context";
7
+ import { ChessFactObservationProvider } from "./fact-observation-provider";
8
+ import { MechanicalChessFactProvider } from "./mechanical-fact-provider";
9
+ import { CompositeObservationProvider, DeterministicObservationProvider } from "./poc-observations";
10
+ import type { ReasoningPositionContext } from "./poc-contracts";
11
+
12
+ /**
13
+ * The production pre-semantic observation composition. It is deliberately
14
+ * environment-neutral: browser and evaluation callers share exactly the same
15
+ * deterministic, mechanical-fact, and attributed-existing-analysis inputs.
16
+ */
17
+ export const PRODUCTION_OBSERVATION_PROVIDER_VERSION = "production-observation-provider/1" as const;
18
+
19
+ export type ProductionObservationProviderInput = {
20
+ existingAnalysis?: unknown;
21
+ context?: ReasoningPositionContext;
22
+ question?: string;
23
+ existingAnalysisContextMode?: ExistingAnalysisContextMode;
24
+ };
25
+
26
+ export function createProductionObservationProvider(input: ProductionObservationProviderInput = {}) {
27
+ const context = input.context;
28
+ const question = input.question || "";
29
+ const mode = input.existingAnalysisContextMode || EXISTING_ANALYSIS_CONTEXT_MODE.FULL;
30
+ return new CompositeObservationProvider([
31
+ new DeterministicObservationProvider(),
32
+ new ChessFactObservationProvider(new MechanicalChessFactProvider(), {
33
+ maxRecords: 24,
34
+ maxBytes: 20_000,
35
+ }),
36
+ new ExistingAnalysisObservationProvider(
37
+ context ? normalizeExistingAnalysisContext(input.existingAnalysis ?? null, context, { question }) : null,
38
+ mode,
39
+ ),
40
+ ]);
41
+ }
@@ -0,0 +1,332 @@
1
+ /** KE-REASON-024: question-only Jev routing contract. */
2
+ export const QUESTION_ROUTING_VERSION = "jev-r17-move-classification-20261003";
3
+
4
+ export const REVIEW_SUBJECTS = ['self','opponent','white','black','both','unspecified'] as const;
5
+ export const ANSWER_SCOPES = ['move_explanation','position_analysis','game_review','general','mixed'] as const;
6
+ export type AnswerScope = typeof ANSWER_SCOPES[number];
7
+ export type AnswerScopeAnswer = {type:'choice';choice:AnswerScope;confidence?:number;probabilities?:Partial<Record<AnswerScope,number>>};
8
+ export type ReviewSubject = typeof REVIEW_SUBJECTS[number];
9
+ export type ReviewSubjectAnswer = {type:'choice';choice:ReviewSubject;confidence?:number;probabilities?:Partial<Record<ReviewSubject,number>>};
10
+ const reviewSubjectQuestion = {
11
+ type:'choice',
12
+ instructions:"Whose play is the user asking to assess across a game or phase? Find the subject of that assessment, even if expressed as past decisions to improve rather than the word review. Ignore separate local-position requests and quoted or excluded instructions. Ownership of a game alone is not a player restriction. If the question explicitly identifies the reviewed player's color, choose white or black rather than self or opponent. Conflicting active review subjects must not select one player.",
13
+ criteria:{
14
+ self:"Assessment of the user's own performance or decisions: their errors, what they did wrong, or what they should have done differently across the game. This personal assessment can accompany a request to go through their game. No assessment of the other player's decisions is requested, and the question does not explicitly identify the user's color.",
15
+ opponent:"The user asks to assess their opponent's play, mistakes or decisions, without requesting review of their own play. The question does not explicitly identify which color that opponent played.",
16
+ white:"The requested review concerns White's decisions only, whether named directly or identified as the user/opponent by an explicit statement of who played which color. Color mentions that merely describe the opponent or another local question do not count.",
17
+ black:"The requested review concerns Black's decisions only, whether named directly or identified as the user/opponent by an explicit statement of who played which color. Color mentions that merely describe the opponent or another local question do not count.",
18
+ both:"The across-game/phase review explicitly covers both players, or contains conflicting review subjects that cannot be resolved to one player. A separate question about a single move or piece does not add a player to this review's scope.",
19
+ unspecified:"No assessment across multiple played decisions is requested, or no player's decisions are singled out. An overall review of the game, my game or our opening alone has no player restriction. An additional request to assess the user's own decisions DOES specify self instead.",
20
+ },
21
+ } as const;
22
+
23
+ const independentCriteria = {
24
+ yes: "Every active request is independent of the board selection: game-record facts (including final moves), review of a game or phase (including endgames), general knowledge or language, or an explicitly numbered historical move and its alternatives. Quoted or excluded current-position references do not count.",
25
+ no: "At least one active request is about the current board, selected move, next reply, or continuation relative to it; or refers to an unidentified move or prior answer. A mixed game-review/current-position question belongs here.",
26
+ };
27
+
28
+ const questions = {
29
+ independent: {
30
+ type: "choice", instructions: "Can all active requests be answered using the game record or general knowledge, without knowing which turn the viewer selected?", criteria: independentCriteria,
31
+ },
32
+ independent_noul: {
33
+ type: "noul", instructions: "Is the user asking only about subjects identifiable independently of the selected move?",
34
+ criteria: { true: independentCriteria.yes, false: independentCriteria.no },
35
+ },
36
+ board_guard: {
37
+ type: "choice", instructions: "Does the user actively ask about the board position currently displayed?",
38
+ criteria: {
39
+ yes: "An active request for its evaluation, material, threats, legal moves, plans, or another property of the current position. Excluding discussion of the selected move does not exclude the board.",
40
+ no: "Only game history, phase review, an explicitly numbered historical turn, general knowledge, or discussion/translation of quoted text. No active request about the current board.",
41
+ },
42
+ },
43
+ review_only: {
44
+ "type": "choice",
45
+ "instructions": "Is the user's entire request a review of how they played across a game or a phase of that game?",
46
+ "criteria": {
47
+ "yes": "Assess their play across the opening, middlegame, endgame or whole game. Evaluate performance, identify mistakes or missed chances, or give lessons from that play. No additional request about the selected move, the current board, or what to play next.",
48
+ "no": "A specific move or current position is being discussed, including a mixed request combining a game review with a selected-move explanation, current-position assessment, next move or continuation. General chess advice without reviewing their played game is also no."
49
+ }
50
+ },
51
+ local_reference: {
52
+ "type": "choice",
53
+ "instructions": "Does answering the user's active question require identifying a move or position referred to without an explicit game move number?",
54
+ "criteria": {
55
+ "yes": "The question asks about this move, this position, here, now, a next move, an unnamed alternative, or a reference to a previous answer. Include mixed requests even if they also mention game history or reviewing the opening. A question like why this is losing needs the referenced position.",
56
+ "no": "Every active request is about the whole game, a game phase, general knowledge, or an explicitly numbered historical move. Mentions of the current board that are quoted or explicitly excluded are not active requests."
57
+ }
58
+ },
59
+ game_record: {
60
+ "type": "choice",
61
+ "instructions": "Does the user ask what actually happened in this game, including which moves were played?",
62
+ "criteria": {
63
+ "yes": "The user asks for actual game events, actual subsequent play, or whether/when a move was played.",
64
+ "no": "The user is not asking to retrieve actual game events or played moves."
65
+ }
66
+ },
67
+ game_context: {
68
+ "type": "choice",
69
+ "instructions": "Does answering the user require their recorded game, beyond the selected move and its immediately surrounding positions?",
70
+ "criteria": {
71
+ "yes": "The answer depends on played game history: actual events or later play, review of a game or phase, an explicitly numbered historical move, or advice and variations specific to the opening played in this game. Include coaching requests grounded in their game even when they do not explicitly ask to retrieve moves.",
72
+ "no": "The current position or selected move alone is sufficient, or the request is general chess knowledge, translation, or unrelated to the played game. A named opening alone without a reference to their game does not require game history. Comparing possible moves in the selected position is a local question, unless another part of the request needs game history."
73
+ }
74
+ },
75
+ game_review: {
76
+ type: "choice", instructions: "Is a review of play across the game or a phase explicitly requested?",
77
+ criteria: {
78
+ yes: "Review multiple decisions, find mistakes across a phase or the game, or recommend lessons from that play. An explicit question about how an earlier phase went still counts when paired with next-move advice. A comprehensive review of the full game counts even without mentioning mistakes or personal performance.",
79
+ no: "No broad review is requested. Mentioning past mistakes while asking what to do now, or asking about just one move, is not a broad review. Retrieving facts, statistics or played moves without evaluating decisions is not a review. Evaluating only one hypothetical alternative is not a phase review. Opening identification or general planning advice without assessing played decisions is not a review.",
80
+ },
81
+ },
82
+ selected_anchor: {
83
+ type: "choice",
84
+ instructions: "Is any part of the request about the currently selected chess position or an unidentified local move? Classify the active request, not quoted or explicitly excluded text.",
85
+ criteria: {
86
+ yes: "The displayed board, this move, an unnumbered move judged against the engine, an unnamed alternative, what to play from here, or an unresolved reference to previous analysis. General advice alongside a local question still counts.",
87
+ no: "Only a whole game, a game phase, a move identified by its turn number, actual game events, or a self-contained general topic. A numbered move anchors its consequences and replies too. This includes numbered historical moves written with non-English piece notation.",
88
+ },
89
+ },
90
+ numbered_move_target: {
91
+ type: "choice",
92
+ instructions: "Is an actual game turn number attached to the target move, separately from its destination square? Ignore quoted or excluded requests.",
93
+ criteria: {
94
+ yes: "A turn number precedes a period and move, as in 5.Nxe5 or 12...Nd4, or prose explicitly says bishop move on turn twelve. The user asks about that identified move, its alternatives or consequences.",
95
+ no: "A square rank is NOT a turn number: Ne6, Na5, a5 and d4 are unnumbered moves. Quantities of plies and ranges of turns are not numbered target moves. Numbered text merely quoted for translation does not count.",
96
+ },
97
+ },
98
+ played_opening: {
99
+ type: "choice",
100
+ instructions: "Does the user actively ask to identify or teach the opening system or sequence played in their game? Ignore quoted or excluded requests.",
101
+ criteria: {
102
+ yes: "Identify the played opening, teach this opening or our opening, give its overall main lines, or coach the opening phase of their game. The request must actually refer to an opening or opening phase.",
103
+ no: "Only current-position advice, best replies or continuations from the current point. An opening name merely framing a particular move is insufficient. Generic named-opening theory, or opening words inside text being translated, also do not count.",
104
+ },
105
+ },
106
+ retrospective_review: {
107
+ type: "choice",
108
+ instructions: "Does the user request feedback on how they played across a game or a phase? Classify the active request, not quoted or explicitly excluded text.",
109
+ criteria: {
110
+ yes: "Review actual play, diagnose recurring mistakes, or explain how their decisions across the opening, middlegame, endgame or game could have been better. This can be part of a mixed request.",
111
+ no: "Only one move or position; theory, prospective continuations, hypothetical traps or typical future mistakes. A walkthrough of possible replies is not feedback on played decisions.",
112
+ },
113
+ },
114
+ } as const;
115
+
116
+ const answerScopeQuestion = {
117
+ type:'choice',
118
+ instructions:"What is the main task of the user's active request? Classify its requested answer, not the selected board, available game history, routing needs, or quoted/excluded text. A move explanation requires a specific move actually played, including an identified historical game move; a hypothetical candidate from the current position is position analysis. Choose mixed if a move explanation is combined with a substantial whole-game, position, or other independent task.",
119
+ criteria:{
120
+ move_explanation:'Explain or evaluate one specific played move, selected now or identified from the recorded game, and its pertinent alternatives or continuations.',
121
+ position_analysis:'Assess a current or starting position, legal candidates, plans or next moves without a specific played target move.',
122
+ game_review:'Review decisions or play across the whole recorded game or a phase, even when a selected move happens to be available.',
123
+ general:'Answer a factual, rules, opening theory, translation or other question that is not a played-move explanation, position analysis or game review.',
124
+ mixed:'Multiple substantial tasks have different scopes; do not force one played-move structure over the complete answer.',
125
+ },
126
+ } as const;
127
+
128
+ const moveClassificationQuestion = {
129
+ type:'choice',
130
+ instructions:"Does ANY active part of the question ask to explain, justify, challenge or reassess the quality classification of a PLAYED move? Include mixed questions and played moves identified by SAN, game move number, this move, or previous move. You have only the question: do not infer whether a named or numbered move is selected now or historical. Ignore quoted or expressly excluded requests.",
131
+ criteria:{
132
+ yes:"At least one active request asks why or whether a played move deserves a quality label: forced, brilliant, great, interesting, best, good, okay, dubious, mistake, missed opportunity or blunder. Why is this the best move in this position, why is 14.Qg4 great, and why was my opponent's previous move a blunder all count. The request can be one part of a mixed question.",
133
+ no:"No active played-move quality-classification request. Only move intention, plans, threats, position evaluation, prospective hypothetical candidates, opening lines, general label definitions, translation of quoted classification questions, expressly excluded label explanations, or a label mentioned merely as background.",
134
+ },
135
+ } as const;
136
+ const classificationReferenceQuestion = {
137
+ type:'choice',
138
+ instructions:"How is the target of the ACTIVE PLAYED-MOVE CLASSIFICATION request expressed? Describe only the wording, not game facts or which move is selected. Examine classification requests separately from unrelated requests in a mixed question. If ANY active classification request explicitly refers to this/current/selected move or an unnamed local played move, choose selection_relative even alongside named-move questions. Unrelated local plans or next-move requests do not count as selection-relative classification requests. Ignore quoted and expressly excluded text.",
139
+ criteria:{
140
+ selection_relative:"An active classification request says this move, the current/selected move, this was a mistake, or otherwise concerns an unnamed local played move. Takes precedence if there is also a named or previous-move classification request. Compare 14.Qg4 with 14.Qh5, and explain why this selected move is marked a mistake is selection_relative.",
141
+ named_move:"Active played-move classification requests name a move by SAN, optionally with a game move number, and no separate selection-relative classification request exists. Why is 14.Qg4 great, and what should I play next here is named_move: only the plans request is local. The number does not tell you whether that move is currently selected.",
142
+ other_relative:"Active played-move classification requests identify a previous move, opponent's last move or another relative played move without explicit SAN, with no selection-relative or named-move classification request. This describes the reference wording, not its resolved game position.",
143
+ none:"No active played-move classification request: only definitions, quoted or excluded label questions, prospective hypothetical moves, move intention, position evaluation, plans, opening lines or labels mentioned as background.",
144
+ },
145
+ } as const;
146
+ export const CLASSIFICATION_REFERENCES=['selection_relative','named_move','other_relative','none'] as const;
147
+ export type ClassificationReference=typeof CLASSIFICATION_REFERENCES[number];
148
+ export type ClassificationReferenceAnswer={type:'choice';choice:ClassificationReference;confidence?:number;probabilities?:Partial<Record<ClassificationReference,number>>};
149
+ /** Saved r16 responses remain readable; these legacy heads are never requested or used. */
150
+ export const CLASSIFICATION_TARGETS=['selected_move','other_move','none'] as const;
151
+ export type ClassificationTarget=typeof CLASSIFICATION_TARGETS[number];
152
+ export type ClassificationTargetAnswer={type:'choice';choice:ClassificationTarget;confidence?:number;probabilities?:Partial<Record<ClassificationTarget,number>>};
153
+
154
+ export type QuestionRoutingAnswer = {type:"choice";choice:"yes"|"no";confidence?:number;probabilities?:Partial<Record<"yes"|"no",number>>} | {type:"noul";noul:number};
155
+ export type QuestionRoutingAnswers = Record<keyof typeof questions,QuestionRoutingAnswer> & {review_subject?:ReviewSubjectAnswer;answer_scope?:AnswerScopeAnswer;current_classification?:QuestionRoutingAnswer;classification_target?:ClassificationTargetAnswer;move_classification?:QuestionRoutingAnswer;classification_reference?:ClassificationReferenceAnswer};
156
+ export type QuestionRoutingDecision = {
157
+ version:string;
158
+ status:"classified"|"fallback";
159
+ omitSelectedContext:boolean;
160
+ includeGameRecord:boolean;
161
+ gameRecordTrigger?:'starting-position';
162
+ reviewGame:boolean;
163
+ reviewSubject?:ReviewSubject;
164
+ reason?:string;
165
+ answers?:QuestionRoutingAnswers;
166
+ usage?:{cost?:number|null;prompt_tokens?:number;completion_tokens?:number;total_tokens?:number;input_tokens?:number;output_tokens?:number};
167
+ providerLatencyMs?:number;
168
+ };
169
+
170
+ export function makeQuestionRoutingRequest(question:string) {
171
+ if(typeof question!=="string" || !question.trim() || question.length>8000) throw new Error("Invalid routing question.");
172
+ // New objects per request prevent a caller from modifying the frozen template.
173
+ return {model:"typesafe/jev-1.13",state:{question},questions:structuredClone({...questions,review_subject:reviewSubjectQuestion,answer_scope:answerScopeQuestion,move_classification:moveClassificationQuestion,classification_reference:classificationReferenceQuestion})};
174
+ }
175
+
176
+ const record=(value:unknown):value is Record<string,unknown> => value!==null && typeof value==="object" && !Array.isArray(value);
177
+ const probability=(value:unknown):value is number => typeof value==="number" && Number.isFinite(value) && value>=0 && value<=1;
178
+ const keysWithin=(value:Record<string,unknown>,keys:readonly string[])=>Object.keys(value).every(key=>keys.includes(key));
179
+ /** Preserve validated legacy heads even if the additive subject head is unavailable. */
180
+ export function parseQuestionRoutingAnswers(value:unknown):QuestionRoutingAnswers|null {
181
+ if(!record(value) || !record(value.answers) || !keysWithin(value.answers,[...Object.keys(questions),'review_subject','answer_scope','current_classification','classification_target','move_classification','classification_reference'])) return null;
182
+ const parsed:Record<string,QuestionRoutingAnswer>={};
183
+ for(const id of Object.keys(questions) as Array<keyof typeof questions>) {
184
+ const answer=value.answers[id];
185
+ if(!record(answer)) return null;
186
+ if(id==="independent_noul") {
187
+ if(answer.type!=="noul" || !probability(answer.noul) || !keysWithin(answer,["type","noul"])) return null;
188
+ parsed[id]={type:"noul",noul:answer.noul};
189
+ } else {
190
+ if(answer.type!=="choice" || (answer.choice!=="yes" && answer.choice!=="no") || !keysWithin(answer,["type","choice","confidence","probabilities"])) return null;
191
+ if(answer.confidence!==undefined && !probability(answer.confidence)) return null;
192
+ if(answer.probabilities!==undefined && (!record(answer.probabilities) || !keysWithin(answer.probabilities,["yes","no"])
193
+ || Object.values(answer.probabilities).some(value=>!probability(value)))) return null;
194
+ parsed[id]={type:"choice",choice:answer.choice,
195
+ ...(answer.confidence===undefined?{}:{confidence:answer.confidence as number}),
196
+ ...(answer.probabilities===undefined?{}:{probabilities:answer.probabilities as Partial<Record<"yes"|"no",number>>})};
197
+ }
198
+ }
199
+ const result=parsed as QuestionRoutingAnswers;
200
+ const subject=value.answers.review_subject;
201
+ if(record(subject) && subject.type==='choice' && REVIEW_SUBJECTS.includes(subject.choice as ReviewSubject)
202
+ && keysWithin(subject,['type','choice','confidence','probabilities'])
203
+ && (subject.confidence===undefined||probability(subject.confidence))
204
+ && (subject.probabilities===undefined||record(subject.probabilities)&&keysWithin(subject.probabilities,REVIEW_SUBJECTS)
205
+ && Object.values(subject.probabilities).every(probability))) {
206
+ result.review_subject={type:'choice',choice:subject.choice as ReviewSubject,
207
+ ...(subject.confidence===undefined?{}:{confidence:subject.confidence as number}),
208
+ ...(subject.probabilities===undefined?{}:{probabilities:subject.probabilities as Partial<Record<ReviewSubject,number>>})};
209
+ }
210
+ const scope=value.answers.answer_scope;
211
+ if(record(scope) && scope.type==='choice' && ANSWER_SCOPES.includes(scope.choice as AnswerScope)
212
+ && keysWithin(scope,['type','choice','confidence','probabilities'])
213
+ && (scope.confidence===undefined||probability(scope.confidence))
214
+ && (scope.probabilities===undefined||record(scope.probabilities)&&keysWithin(scope.probabilities,ANSWER_SCOPES)
215
+ && Object.values(scope.probabilities).every(probability))) {
216
+ result.answer_scope={type:'choice',choice:scope.choice as AnswerScope,
217
+ ...(scope.confidence===undefined?{}:{confidence:scope.confidence as number}),
218
+ ...(scope.probabilities===undefined?{}:{probabilities:scope.probabilities as Partial<Record<AnswerScope,number>>})};
219
+ }
220
+ const classification=value.answers.current_classification;
221
+ if(record(classification) && classification.type==='choice'
222
+ && (classification.choice==='yes'||classification.choice==='no')
223
+ && keysWithin(classification,['type','choice','confidence','probabilities'])
224
+ && (classification.confidence===undefined||probability(classification.confidence))
225
+ && (classification.probabilities===undefined||record(classification.probabilities)
226
+ && keysWithin(classification.probabilities,['yes','no'])
227
+ && Object.values(classification.probabilities).every(probability))) {
228
+ result.current_classification={type:'choice',choice:classification.choice,
229
+ ...(classification.confidence===undefined?{}:{confidence:classification.confidence as number}),
230
+ ...(classification.probabilities===undefined?{}:{probabilities:classification.probabilities as Partial<Record<'yes'|'no',number>>})};
231
+ }
232
+ const target=value.answers.classification_target;
233
+ if(record(target) && target.type==='choice' && CLASSIFICATION_TARGETS.includes(target.choice as ClassificationTarget)
234
+ && keysWithin(target,['type','choice','confidence','probabilities'])
235
+ && (target.confidence===undefined||probability(target.confidence))
236
+ && (target.probabilities===undefined||record(target.probabilities)
237
+ && keysWithin(target.probabilities,CLASSIFICATION_TARGETS)
238
+ && Object.values(target.probabilities).every(probability))) {
239
+ result.classification_target={type:'choice',choice:target.choice as ClassificationTarget,
240
+ ...(target.confidence===undefined?{}:{confidence:target.confidence as number}),
241
+ ...(target.probabilities===undefined?{}:{probabilities:target.probabilities as Partial<Record<ClassificationTarget,number>>})};
242
+ }
243
+ const moveClassification=value.answers.move_classification;
244
+ if(record(moveClassification) && moveClassification.type==='choice'
245
+ && (moveClassification.choice==='yes'||moveClassification.choice==='no')
246
+ && keysWithin(moveClassification,['type','choice','confidence','probabilities'])
247
+ && (moveClassification.confidence===undefined||probability(moveClassification.confidence))
248
+ && (moveClassification.probabilities===undefined||record(moveClassification.probabilities)
249
+ && keysWithin(moveClassification.probabilities,['yes','no'])
250
+ && Object.values(moveClassification.probabilities).every(probability))) {
251
+ result.move_classification={type:'choice',choice:moveClassification.choice,
252
+ ...(moveClassification.confidence===undefined?{}:{confidence:moveClassification.confidence as number}),
253
+ ...(moveClassification.probabilities===undefined?{}:{probabilities:moveClassification.probabilities as Partial<Record<'yes'|'no',number>>})};
254
+ }
255
+ const reference=value.answers.classification_reference;
256
+ if(record(reference) && reference.type==='choice' && CLASSIFICATION_REFERENCES.includes(reference.choice as ClassificationReference)
257
+ && keysWithin(reference,['type','choice','confidence','probabilities'])
258
+ && (reference.confidence===undefined||probability(reference.confidence))
259
+ && (reference.probabilities===undefined||record(reference.probabilities)
260
+ && keysWithin(reference.probabilities,CLASSIFICATION_REFERENCES)
261
+ && Object.values(reference.probabilities).every(probability))) {
262
+ result.classification_reference={type:'choice',choice:reference.choice as ClassificationReference,
263
+ ...(reference.confidence===undefined?{}:{confidence:reference.confidence as number}),
264
+ ...(reference.probabilities===undefined?{}:{probabilities:reference.probabilities as Partial<Record<ClassificationReference,number>>})};
265
+ }
266
+ return result;
267
+ }
268
+
269
+ /** Unmeasured, ambiguous, legacy and fallback routes never enable the move-only suffix. */
270
+ export function moveSectionScope(decision:QuestionRoutingDecision):'move-explanation'|'none' {
271
+ if(decision.status!=='classified')return 'none';
272
+ const scope=decision.answers?.answer_scope;
273
+ const score=scope?.probabilities?.move_explanation;
274
+ return scope?.choice==='move_explanation' && typeof score==='number' && score>=0.75
275
+ && Object.entries(scope.probabilities??{}).every(([key,p])=>key==='move_explanation'||p<score)
276
+ ? 'move-explanation' : 'none';
277
+ }
278
+
279
+ export function classifyReviewSubject(answers:QuestionRoutingAnswers):ReviewSubject {
280
+ const answer=answers.review_subject;
281
+ const selected=answer?.probabilities?.[answer.choice]??0;
282
+ return answer && selected>=0.75 && Object.entries(answer.probabilities??{})
283
+ .every(([key,p])=>key===answer.choice||p<selected) ? answer.choice : 'unspecified';
284
+ }
285
+
286
+ export function classifyQuestionRoutingAnswers(answers:QuestionRoutingAnswers):Pick<QuestionRoutingDecision,"omitSelectedContext"|"includeGameRecord"|"reviewGame"> {
287
+ const local=answers.local_reference;
288
+ // Preserve the historical r8 omission and review signals as the baseline.
289
+ const retainLocal=local.type!=="choice" || local.probabilities?.yes===undefined
290
+ || (local.choice==="yes" && local.probabilities.yes>=0.85);
291
+ const reviewOnly=answers.review_only;
292
+ const independentReview=reviewOnly.type==="choice" && reviewOnly.choice==="yes" && (reviewOnly.probabilities?.yes??0)>=0.85;
293
+ const independence=independentReview || (answers.independent as {choice:"yes"|"no"}).choice==="yes"
294
+ || (answers.independent_noul as {noul:number}).noul>0.5;
295
+ const board=answers.board_guard as {choice:"yes"|"no";probabilities?:{yes?:number}};
296
+ const historicalOmission=!retainLocal && independence && board.choice==="no";
297
+ const selected=answers.selected_anchor as {choice:"yes"|"no";probabilities?:{yes?:number;no?:number}};
298
+ const strong=(id:"numbered_move_target"|"played_opening"|"review_only"|"game_context"|"game_review"|"retrospective_review",threshold:number)=>{
299
+ const answer=answers[id] as {choice:"yes"|"no";probabilities?:{yes?:number}};
300
+ return answer.choice==="yes" && (answer.probabilities?.yes??0)>=threshold;
301
+ };
302
+ const selectedGuard=(answers.move_classification?.type==='choice' && answers.move_classification.choice==='yes' && answers.classification_reference?.choice==='selection_relative') || selected.probabilities?.no===undefined
303
+ || (selected.choice==="yes" && (selected.probabilities?.yes??0)>=0.85);
304
+ const numbered=strong("numbered_move_target",0.9),opening=strong("played_opening",0.9);
305
+ // Numbered historical moves need a nonlocal anchor and weak board/reference
306
+ // veto scores. Unknown scores cannot authorize omission.
307
+ const numberedExtra=numbered && selected.choice==="no" && (board.probabilities?.yes??1)<0.7
308
+ && (local.type==="choice" ? local.probabilities?.yes??1 : 1)<0.85;
309
+ const openingExtra=opening && selected.choice==="no" && board.choice==="no";
310
+ // Three concordant review heads may outweigh a weak board classification,
311
+ // but never an identified selected anchor or an unmeasured/strong local veto.
312
+ const reviewExtra=independentReview && strong("game_review",0.85) && strong("retrospective_review",0.85)
313
+ && selected.choice==="no" && (board.probabilities?.yes??1)<0.7
314
+ && (local.type==="choice" ? local.probabilities?.yes??1 : 1)<0.85;
315
+ const omitSelectedContext=!selectedGuard && (historicalOmission||numberedExtra||openingExtra||reviewExtra);
316
+ const retrospective=answers.retrospective_review as {choice:"yes"|"no";probabilities?:{no?:number}};
317
+ const historicalReview=(answers.game_review as {choice:"yes"|"no"}).choice==="yes" || strong("review_only",0.85);
318
+ const reviewGame=historicalReview && !(selected.choice==="yes" && retrospective.choice==="no"
319
+ && (retrospective.probabilities?.no??0)>=0.85);
320
+ const explicitRecord=strong("game_context",0.75);
321
+ const includeGameRecord=omitSelectedContext || numbered || opening || reviewGame || explicitRecord
322
+ || (answers.game_record as {choice:"yes"|"no"}).choice==="yes";
323
+ return {omitSelectedContext,includeGameRecord,reviewGame};
324
+ }
325
+
326
+ /** KE-REASON-024: question-only intent; callers resolve the move using frozen context. */
327
+ export function hasMoveClassificationQuestion(decision:QuestionRoutingDecision):boolean {
328
+ const answer=decision.answers?.move_classification;
329
+ return decision.status==='classified' && answer?.type==='choice' && answer.choice==='yes';
330
+ }
331
+ /** Transitional alias: this indicates intent, not whether its target is the selected move. */
332
+ export const shouldInspectCurrentClassification=hasMoveClassificationQuestion;