@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,195 @@
1
+ import { classificationTargetAtSelection } from './classification-question-target';
2
+ import { DEFAULT_BRANCH_EXPANSION_MODE, BRANCH_EXPANSION_OPTIONS, withBranchPreparation, selectBranchHost } from './branch-expansion-policy';
3
+ import { planInitialSearches } from './initial-search-plan';
4
+ import { withRecordedPositionTools, memoizeToolResults } from './recorded-position-tools';
5
+ import type { AnswerRoutingPreparation, AnswerSelection, AnswerHistoryPort, AnswerOpeningSession } from './answer-host-contracts';
6
+ import { prepareOpponentThreats } from "./opponent-threat-preparation";
7
+ import { simpleBoardAnalysis } from "./simple-board-analysis";
8
+ import { createContextEvidence, REASONING_POC_VERSION, reasoningContextHash, createQueryContract, DEFAULT_REASONING_RUN_CONTROLS } from "./poc-contracts";
9
+ import type { PriorConversationMessage } from "./conversation-context";
10
+ import type { SimpleAgentConfig } from "./simple-agent-config";
11
+ import { captureAblationOriginalEvidenceBundle } from "./ablation-evidence-capture";
12
+ import { toLegacyPositionAnalysisProvider, type CompatiblePositionAnalysisProvider } from "./position-analysis-contracts";
13
+ import type { ObservationProvider } from "./poc-contracts";
14
+ import type { AnswerModelConnector } from "./answer-model-connector";
15
+ import type { WikibooksIndex } from "./wikibooks-index";
16
+ import { emitObservableTrace, type ObservableLlmTraceSink } from "./ai-sdk-runtime-support";
17
+ import { createDefaultSaliencePolicy } from "./poc-observations";
18
+ import { createPocToolHost } from "./poc-tools";
19
+ import { runPreparedAnswer, SIMPLE_MODELS, SIMPLE_BUDGETS, SIMPLE_EXPANDED_BUDGETS, SIMPLE_RECOVERY_BUDGETS } from "./portable-answer-runtime";
20
+ import type { PlayerPerspective } from "../player-perspective";
21
+
22
+ export type AnswerPipelineResult = Awaited<ReturnType<typeof runPreparedAnswer>> & { arm: "Simple"; bundleHash: string };
23
+
24
+ export async function runAnswerPipeline(input: {
25
+ connector: AnswerModelConnector;
26
+ policy?: Parameters<typeof runPreparedAnswer>[0]["policy"];
27
+ modelName?: string;
28
+ providerRoute?: string;
29
+ analyzer?: CompatiblePositionAnalysisProvider;
30
+ /** Optional analyzer for ordinary opponent preparation; later model tools still use analyzer. */
31
+ preparationAnalyzer?: CompatiblePositionAnalysisProvider;
32
+ /** Opt in only when the host can safely serve overlapping preparation and classification searches. */
33
+ parallelPreparation?: boolean;
34
+ observationProvider: ObservationProvider;
35
+ prepareRouting: () => Promise<AnswerRoutingPreparation>;
36
+ openingSession?: AnswerOpeningSession;
37
+ openingIndex?: () => Promise<WikibooksIndex>;
38
+ routingPreparation?: AnswerRoutingPreparation;
39
+ conversationHistory?: readonly PriorConversationMessage[];
40
+ simpleConfig?: SimpleAgentConfig;
41
+ simpleAllowance?: "default" | "expanded" | "glm-recovery";
42
+ requestId: string;
43
+ question: string;
44
+ playerPerspective?: PlayerPerspective | null;
45
+ selected: AnswerSelection;
46
+ bridge?: AnswerHistoryPort;
47
+ signal?: AbortSignal;
48
+ observableTraceSink?: ObservableLlmTraceSink;
49
+ onProgress?: (message: string) => void;
50
+ }): Promise<AnswerPipelineResult> {
51
+ const parentSignal = input.signal;
52
+ const controller = new AbortController();
53
+ const abort = () => controller.abort(parentSignal?.reason);
54
+ if (parentSignal?.aborted) abort(); else parentSignal?.addEventListener("abort", abort, { once: true });
55
+ // Only explicit cancellation aborts; the native loop owns soft completion.
56
+ input = { ...input, signal: controller.signal };
57
+ let sourceTimer: ReturnType<typeof setTimeout> | undefined;
58
+ try {
59
+ const preparationStarted = performance.now();
60
+ let phaseSequence = 0;
61
+ // Paired phase events measure work before the model loop. They do not carry
62
+ // chess payloads or errors that might contain host transport credentials.
63
+ const timed = async <T>(stage: string, operation: () => T | Promise<T>, details: Record<string, unknown> = {}, phaseSignal = input.signal): Promise<T> => {
64
+ const started = performance.now();
65
+ const invocationId = `${input.requestId}:pipeline-phase:${++phaseSequence}`;
66
+ const trace = (direction: "input" | "output" | "error", payload: Record<string, unknown>) => emitObservableTrace(input.observableTraceSink, {
67
+ stage, direction, model: "deterministic", providerRoute: "local-harness", reasoningEffort: "none",
68
+ payload: { invocationId, ...details, ...payload, elapsedMs: performance.now() - preparationStarted },
69
+ });
70
+ trace("input", {});
71
+ try {
72
+ const result = await operation();
73
+ trace("output", { status: "completed", durationMs: performance.now() - started });
74
+ return result;
75
+ } catch (error) {
76
+ trace("error", { status: phaseSignal?.aborted ? "cancelled" : "failed", durationMs: performance.now() - started });
77
+ throw error;
78
+ }
79
+ };
80
+ const routing = input.routingPreparation ?? await timed("answer-pipeline-routing", input.prepareRouting);
81
+ if(routing.question!==input.question||routing.selectionKey!==input.selected.selectionKey||routing.deliveryId!==input.selected.deliveryId)throw Error('stale-question-routing');
82
+ const omitSelectedContext=routing.omitSelectedContext;
83
+ if(input.signal?.aborted)throw input.signal.reason??Error('cancelled');
84
+ // Public MediaWiki traffic must never pass through the credentialed model relay.
85
+ const openingSession = input.openingSession;
86
+ const allowance = input.simpleAllowance === 'glm-recovery' ? SIMPLE_RECOVERY_BUDGETS
87
+ : input.simpleAllowance === 'expanded' ? SIMPLE_EXPANDED_BUDGETS : SIMPLE_BUDGETS;
88
+ const sourceDeadlineMs = Math.max(0, allowance.maxWallTimeMs - 60000);
89
+ let sourceTimedOut = false;
90
+ const sourceController = new AbortController();
91
+ const sourceSignal = AbortSignal.any([controller.signal, sourceController.signal]);
92
+ const prepareGameOpening = routing.decision.reviewGame && routing.mainLine;
93
+ const openingLookup = (prepareGameOpening || !omitSelectedContext) && openingSession ? (() => {
94
+ sourceTimer = setTimeout(() => { sourceTimedOut = true; sourceController.abort(); },
95
+ Math.max(0, sourceDeadlineMs - (input.routingPreparation?.elapsedMs ?? 0) - (performance.now() - preparationStarted)));
96
+ return (async () => {
97
+ const results = await Promise.all([
98
+ ...(prepareGameOpening ? [openingSession.gameReview(prepareGameOpening, sourceSignal)] : []),
99
+ ...(!omitSelectedContext ? [openingSession.initial(sourceSignal)] : []),
100
+ ]);
101
+ const pages = [...new Map(results.flatMap(result => result.pages).map(page => [`${page.title}@${page.revisionId}`, page])).values()];
102
+ return {...results[0], status:pages.length ? 'available' as const : results[0].status, pages,
103
+ requestCount:results.reduce((count, result) => count + result.requestCount, 0),
104
+ localIndexCandidateCount:results.reduce((count, result) => count + result.localIndexCandidateCount, 0),
105
+ localIndexPositionCheckCount:results.reduce((count, result) => count + result.localIndexPositionCheckCount, 0),
106
+ localIndexSelectedTitleCount:results.reduce((count, result) => count + result.localIndexSelectedTitleCount, 0),
107
+ cacheHitTitleCount:results.reduce((count, result) => count + result.cacheHitTitleCount, 0),
108
+ networkRequestCount:results.reduce((count, result) => count + result.networkRequestCount, 0),
109
+ networkRequestedTitles:results.flatMap(result => result.networkRequestedTitles),
110
+ timingMs:Math.max(...results.map(result => result.timingMs)),
111
+ limitations:[...new Set(results.flatMap(result => result.limitations))]};
112
+ })().finally(() => clearTimeout(sourceTimer));
113
+ })() : undefined;
114
+ const analyzer = input.analyzer ? toLegacyPositionAnalysisProvider(input.analyzer) : undefined;
115
+ const preparationAnalyzer = input.preparationAnalyzer ? toLegacyPositionAnalysisProvider(input.preparationAnalyzer) : analyzer;
116
+ const baseSearchPlan = planInitialSearches({question:input.question,context:input.selected.context,mainLine:routing.mainLine,reviewGame:routing.decision.reviewGame,reviewSubject:routing.decision.reviewSubject,answerScope:routing.decision.answers?.answer_scope?.choice,classificationQuestion:classificationTargetAtSelection(routing.decision,input.question,input.selected.context).applies,omitSelectedContext,playerPerspective:input.playerPerspective});
117
+ // Capture the selected chess context before refresh can replace host analysis.
118
+ // Classification refresh changes existingAnalysis, not the selected position.
119
+ const selectedContext = input.selected.context;
120
+ const prepare = preparationAnalyzer && !omitSelectedContext && !baseSearchPlan.requests.length
121
+ ? () => timed("answer-pipeline-preparation", () => prepareOpponentThreats(selectedContext, preparationAnalyzer, input.signal, input.onProgress, { parallel: input.parallelPreparation })) : undefined;
122
+ const refresh = () => timed("answer-pipeline-classification-refresh", () => input.bridge!.refreshContext!(input.selected, input.signal), { origin: "opponent-threat-preparation" });
123
+ let preparation: Awaited<ReturnType<typeof prepareOpponentThreats>>;
124
+ let refreshed: Awaited<ReturnType<NonNullable<AnswerHistoryPort["refreshContext"]>>> | undefined;
125
+ if (input.parallelPreparation && prepare && selectedContext.kind !== "position" && input.bridge?.refreshContext) {
126
+ input.onProgress?.("Refreshing move classification and candidate moves");
127
+ const preparationWork = prepare();
128
+ const classificationWork = refresh();
129
+ try {
130
+ [preparation, refreshed] = await Promise.all([preparationWork, classificationWork]);
131
+ input.signal?.throwIfAborted();
132
+ } catch (error) {
133
+ // Abort and drain the sibling before the host tears down answer resources.
134
+ // Promise.all observes both rejections; preserve the first operation's error.
135
+ controller.abort(error);
136
+ await Promise.allSettled([preparationWork, classificationWork]);
137
+ throw error;
138
+ }
139
+ } else {
140
+ preparation = prepare ? await prepare() : undefined;
141
+ input.onProgress?.("Refreshing move classification and candidate moves");
142
+ refreshed = preparation && input.bridge?.refreshContext ? await refresh() : undefined;
143
+ }
144
+ const initialSearchPlan=withBranchPreparation(baseSearchPlan,input.selected.context,omitSelectedContext,DEFAULT_BRANCH_EXPANSION_MODE);
145
+ // Never reuse a pre-search snapshot when no refresh capability is available.
146
+ const existingAnalysis = refreshed?.existingAnalysis ?? (preparation ? null : input.selected.existingAnalysis);
147
+ input.onProgress?.("Collecting compact chess evidence");
148
+ // Include attributed current board context; never include future recorded play.
149
+ const { boardAnalysis, observations } = await timed("answer-pipeline-observations", async () => {
150
+ const boardAnalysis = simpleBoardAnalysis(input.selected.context, existingAnalysis, input.selected.edgeId);
151
+ const provider = input.observationProvider;
152
+ const observations = input.selected.context.kind === "position" ? { schemaVersion: REASONING_POC_VERSION, contextHash: reasoningContextHash(input.selected.context), observations: [], evidence: [createContextEvidence(input.selected.context)], providers: [{ provider: "validated-starting-position", version: "1" }], limitations: [] } : await provider.observe(input.selected.context, DEFAULT_REASONING_RUN_CONTROLS, input.signal);
153
+ return { boardAnalysis, observations };
154
+ });
155
+ const { bundle, hostOptions, expandedOptions, host, getMainLine } = await timed("answer-pipeline-evidence-collection", () => {
156
+ const contract = createQueryContract({ requestId: input.requestId, question: input.question, context: input.selected.context });
157
+ const rankedObservations = createDefaultSaliencePolicy(DEFAULT_REASONING_RUN_CONTROLS.maxObservationsPresented).rank(observations, contract);
158
+ const bundle = captureAblationOriginalEvidenceBundle({ context: input.selected.context, question: input.question,
159
+ observations, rankedObservations, retainAllExactFacts: true });
160
+ const hostOptions = { ...(analyzer ? { analyzer } : {}), defaultDepth:18, minimumDepth:18, maxDepth:18, maxRootMoves:6, leadingAlternative:boardAnalysis?.leadingAlternative };
161
+ const baseHost = createPocToolHost(input.selected.context, hostOptions);
162
+ const expandedOptions={...hostOptions,branchExpansion:BRANCH_EXPANSION_OPTIONS};
163
+ const expandedHost=createPocToolHost(input.selected.context,expandedOptions);
164
+ // Freeze one validated record per answer; later navigation cannot replace it.
165
+ let mainLine = routing.mainLine;
166
+ const getMainLine = async (signal?:AbortSignal) => mainLine ?? (mainLine = await input.bridge!.getMainLine!(input.selected,signal));
167
+ const ordinaryHost = (initialSearchPlan && (mainLine || input.bridge?.getMainLine)
168
+ ? withRecordedPositionTools(input.selected.context,baseHost,hostOptions,getMainLine) : baseHost);
169
+ const expandedRecordedHost=initialSearchPlan && (mainLine || input.bridge?.getMainLine)
170
+ ? withRecordedPositionTools(input.selected.context,expandedHost,expandedOptions,getMainLine) : expandedHost;
171
+ const host=selectBranchHost(memoizeToolResults(ordinaryHost),memoizeToolResults(expandedRecordedHost),DEFAULT_BRANCH_EXPANSION_MODE);
172
+ return { bundle, hostOptions, expandedOptions, host, getMainLine };
173
+ });
174
+ const openingSourceContext = openingLookup ? await timed("answer-pipeline-opening-wait", () => openingLookup) : undefined;
175
+ const preModelElapsedMs = Math.max(sourceTimedOut ? sourceDeadlineMs : 0, performance.now() - preparationStarted + (input.routingPreparation?.elapsedMs??0));
176
+ const result = await runPreparedAnswer({ ...input, bundle, host, boardAnalysis, preparation, openingSourceContext, initialSearchPlan,
177
+ refreshPreparedBoardAnalysis: async signal => {
178
+ hostOptions.leadingAlternative=undefined;
179
+ expandedOptions.leadingAlternative=undefined;
180
+ const fresh=input.bridge?.refreshContext
181
+ ? await timed("answer-pipeline-classification-refresh", () => input.bridge!.refreshContext!(input.selected,signal), { origin: "initial-search-preparation" }, signal) : undefined;
182
+ const analysis=simpleBoardAnalysis(input.selected.context,fresh?.existingAnalysis??null,input.selected.edgeId);
183
+ hostOptions.leadingAlternative=analysis?.leadingAlternative;
184
+ expandedOptions.leadingAlternative=analysis?.leadingAlternative;
185
+ return analysis;
186
+ },
187
+ omitSelectedContext, initialMainLine:routing.mainLine, routingDecision:routing.decision, openingSourceLookup: openingSession ? (targets, signal, root) => openingSession.lookup(targets, signal, root) : undefined, preModelElapsedMs, analysisDepth: 18,
188
+ getMainLine: input.bridge?.getMainLine ? signal => getMainLine(signal) : undefined,
189
+ modelName: input.modelName ?? input.simpleConfig?.modelId ?? SIMPLE_MODELS["Simple-Sol"], effort: input.simpleConfig?.effort ?? "medium" });
190
+ return { ...result, arm: "Simple", bundleHash: bundle.bundleHash };
191
+ } finally {
192
+ clearTimeout(sourceTimer);
193
+ parentSignal?.removeEventListener("abort", abort);
194
+ }
195
+ }
@@ -0,0 +1,124 @@
1
+ import { checkCausalClaims } from './causal-claim-checks';
2
+ import { normalizedEvidenceCitations } from './evidence-citation-normalization';
3
+ import { reasoningContextHash } from './poc-contracts';
4
+ import { Chess } from 'chess.js';
5
+ import { historicalProseRoots } from './historical-prose';
6
+ import { simpleProseReferences, SIMPLE_REFERENCE_VERSION } from './simple-prose-references';
7
+ import { MAX_INTERACTIVE_LINE_PLIES } from './reference-limits';
8
+ import type { RecordedReferenceLine } from './historical-prose';
9
+ import type { AuthoredReferenceResult } from './simple-prose-references';
10
+ import type { ReasoningPositionContext, ReasoningEvidenceRecord } from './poc-contracts';
11
+
12
+ /** Deterministic presentation repair, never new analytical evidence. */
13
+ export function validateAnswerReferences(answer: string, context: ReasoningPositionContext,
14
+ records: readonly ReasoningEvidenceRecord[], recordedLine?: RecordedReferenceLine) {
15
+ const citationResult = normalizedEvidenceCitations(answer, records.filter(record => record.contextHash === reasoningContextHash(context)));
16
+ answer = citationResult.answer;
17
+ const historical = historicalProseRoots(context, records, recordedLine);
18
+ const repairs: Array<{ originalTarget: string; repairedTarget: string; label: string; evidenceIds: string[]; authority: string }> = [];
19
+ const repairedGaps = new Map<string,number>();
20
+ const repairedNotationCorrections: string[] = [];
21
+ const gameReferences: Array<{ target: string; status: 'resolved' | 'repaired' | 'unresolved'; reason?: string }> = [];
22
+ const admitted = (text: string) => {
23
+ const view = simpleProseReferences(text, context, records, recordedLine);
24
+ return { view, valid: view.coverage.resolved === 1 && view.notationCorrections.length === 0 && !view.blocks.some(block => block.tokens.some(token => token.reference?.kind === 'sequence')) && !view.gaps.some(g => !g.startsWith('Unbound text:')) };
25
+ };
26
+ const originalReferences = new Map<string,AuthoredReferenceResult>();
27
+ const originalView = simpleProseReferences(answer, context, records, recordedLine, undefined, r => originalReferences.set(JSON.stringify([r.label,r.target]),r));
28
+ const targetRepairedAnswer = answer.replace(/\[([^\]\n]+)\]\((game:[^)\n]*)\)/g, (original, label: string, target: string) => {
29
+ const initial = originalReferences.get(JSON.stringify([label,target]));
30
+ if (initial?.resolved) { gameReferences.push({target, status:'resolved'}); return original; }
31
+ try {
32
+ const parsed = /^game:(\d+):[^:\n]*$/.exec(target);
33
+ const root = parsed && historical.roots.get(Number(parsed[1]));
34
+ if (!parsed || !root) throw Error('Unknown recorded position');
35
+ // A cumulative path may repeat an earlier UCI accidentally. Delete at
36
+ // most one such occurrence, never insert/reorder moves or change roots.
37
+ // The complete repaired path must independently pass the same witnessed
38
+ // replay and SAN-label checks as rendering. Ambiguous repairs stay plain.
39
+ const authoredPath = target.split(':')[2].split(',');
40
+ const duplicateCandidates = new Map<string, ReturnType<typeof admitted>>();
41
+ if (authoredPath.length <= MAX_INTERACTIVE_LINE_PLIES + 1) {
42
+ for (let i = 1; i < authoredPath.length; i++) {
43
+ if (!authoredPath.slice(0, i).includes(authoredPath[i])) continue;
44
+ const path = authoredPath.filter((_, index) => index !== i);
45
+ const candidateTarget = `game:${parsed[1]}:${path.join(',')}`;
46
+ const checked = admitted(`[${label}](${candidateTarget})`);
47
+ const ids = checked.view.blocks.flatMap(b => b.tokens.flatMap(t => t.reference?.evidenceIds ?? []));
48
+ const actual = historical.recorded?.slice(Number(parsed[1]), Number(parsed[1]) + path.length);
49
+ const isActual = actual?.length === path.length && actual.every((uci, index) => uci === path[index]);
50
+ if (checked.valid && (isActual || ids.length)) duplicateCandidates.set(candidateTarget, checked);
51
+ }
52
+ }
53
+ if (duplicateCandidates.size === 1) {
54
+ const [repairedTarget, checked] = [...duplicateCandidates][0];
55
+ const evidenceIds = [...new Set(checked.view.blocks.flatMap(b => b.tokens.flatMap(t => t.reference?.evidenceIds ?? [])))];
56
+ repairs.push({originalTarget:target,repairedTarget,label,evidenceIds,authority:evidenceIds.length?'supplied-evidence':'recorded-play'});
57
+ if (initial?.reason) repairedGaps.set(initial.reason,(repairedGaps.get(initial.reason) ?? 0)+1);
58
+ repairedNotationCorrections.push(...checked.view.notationCorrections);
59
+ gameReferences.push({target,status:'repaired'});
60
+ return `[${label}](${repairedTarget})`;
61
+ }
62
+ if (duplicateCandidates.size > 1) throw Error('Ambiguous witnessed replacement');
63
+ // Parse the WHOLE label from its stated root; no suffix guessing, skipped
64
+ // words, root search, or joining independent engine paths.
65
+ const tokens = [...label.trim().matchAll(/(?:(\d+)(\.{1,3}|…)\s*)?([^\s.]+)/g)];
66
+ if (!tokens.length || tokens.length > MAX_INTERACTIVE_LINE_PLIES
67
+ || tokens.map(t => t[0]).join('').replace(/\s/g,'') !== label.replace(/\s/g,'')) throw Error('Incomplete SAN label');
68
+ const board = new Chess(root.context.fenAfter);
69
+ const path = tokens.map(t => {
70
+ const fields = board.fen().split(' ');
71
+ if (t[1] && (Number(t[1]) !== Number(fields[5]) || (t[2] === '.' ? 'w' : 'b') !== fields[1])) throw Error('Wrong move number');
72
+ const san = t[3].replace(/[!?]+$/,'');
73
+ const move = board.move(san, {strict:true});
74
+ if (move.san !== san) throw Error('Noncanonical SAN label');
75
+ return move.from + move.to + (move.promotion ?? '');
76
+ });
77
+ const repairedTarget = `game:${parsed[1]}:${path.join(',')}`;
78
+ const candidate = `[${label}](${repairedTarget})`;
79
+ const checked = admitted(candidate);
80
+ const evidenceIds = [...new Set(checked.view.blocks.flatMap(b => b.tokens.flatMap(t => t.reference?.evidenceIds ?? [])))];
81
+ const actual = historical.recorded?.slice(Number(parsed[1]), Number(parsed[1]) + path.length);
82
+ const isActual = actual?.length === path.length && actual.every((uci,i) => uci === path[i]);
83
+ if (!checked.valid || (!isActual && !evidenceIds.length)) throw Error('Replacement path is not witnessed');
84
+ repairs.push({originalTarget:target,repairedTarget,label,evidenceIds,authority:isActual?'recorded-play':'supplied-evidence'});
85
+ if(initial?.reason) repairedGaps.set(initial.reason,(repairedGaps.get(initial.reason) ?? 0)+1);
86
+ repairedNotationCorrections.push(...checked.view.notationCorrections);
87
+ gameReferences.push({target, status:'repaired'});
88
+ return candidate;
89
+ } catch {
90
+ gameReferences.push({target,status:'unresolved',reason:initial?.reason ?? 'Unresolved historical reference'});
91
+ return original; // Renderer retains its readable noninteractive fallback.
92
+ }
93
+ });
94
+ // Only a fully resolved original link can contribute a canonical label.
95
+ // Repaired historical targets already require complete strict canonical SAN.
96
+ // Keep the raw model output in its earlier trace.
97
+ const labelCorrections: Array<{originalLabel:string;canonicalLabel:string;target:string}> = [];
98
+ const repairedAnswer = targetRepairedAnswer.replace(/\[([^\]\n]+)\]\(([^)\n]+)\)/g, (original, label: string, target: string) => {
99
+ const reference = originalReferences.get(JSON.stringify([label,target]));
100
+ const canonicalLabel = reference?.resolved && reference.canonicalLabel;
101
+ if (!canonicalLabel || canonicalLabel === label) return original;
102
+ labelCorrections.push({originalLabel:label,canonicalLabel,target});
103
+ return `[${canonicalLabel}](${target})`;
104
+ });
105
+ // Target and label changes are already validated above. Reuse the full initial
106
+ // rendering pass instead of replaying every unaffected historical path twice.
107
+ const gaps = originalView.gaps.filter(gap => {
108
+ const repaired = repairedGaps.get(gap) ?? 0;
109
+ if (!repaired) return true;
110
+ repairedGaps.set(gap,repaired-1); return false;
111
+ });
112
+ let causalClaimChecks: ReturnType<typeof checkCausalClaims> = [];
113
+ let causalClaimCheckStatus: 'completed' | 'unavailable' = 'completed';
114
+ try { causalClaimChecks = checkCausalClaims(repairedAnswer,context,records,recordedLine); }
115
+ catch { causalClaimCheckStatus = 'unavailable'; } // A diagnostic failure never discards reference validation or prose.
116
+ return { answer: repairedAnswer, referenceValidation: {
117
+ version:SIMPLE_REFERENCE_VERSION, citationRepairs:citationResult.repairs, interactionCoverage:{...originalView.coverage,resolved:originalView.coverage.resolved+repairs.length},
118
+ unresolvedReferences:gaps.filter(g => !g.startsWith('Unbound text:')),
119
+ unboundMentions:gaps.filter(g => g.startsWith('Unbound text:')),
120
+ notationCorrections:[...new Set([...originalView.notationCorrections,...repairedNotationCorrections])], labelCorrections, gameReferences, repairs,
121
+ causalClaimChecks, causalClaimCheckStatus,
122
+ causalClaimCheckScope:'Witnessed single moves with explicit piece bindings or a direct move-as-subject escape clause only; ambiguous claims remain unresolved. Ranges are half-open UTF-16 offsets into the final reference-repaired answer; sentenceContext preserves surrounding text. Unlisted claims are not certified. Diagnostic only: no suppression or regeneration.',
123
+ } };
124
+ }
@@ -0,0 +1,10 @@
1
+ import { createPocToolHost as createInternalToolHost, type PocToolHostOptions } from './poc-tools';
2
+ import type { ReasoningPositionContext } from './poc-contracts';
3
+ import { toLegacyPositionAnalysisProvider, type CompatiblePositionAnalysisProvider } from './position-analysis-contracts';
4
+
5
+ export type AnswerToolHostOptions = Omit<PocToolHostOptions, 'analyzer'> & { analyzer?: CompatiblePositionAnalysisProvider };
6
+ /** Public analyzer contract; the existing evidence protocol retains its versioned legacy spelling. */
7
+ export function createPocToolHost(context: ReasoningPositionContext, options: AnswerToolHostOptions = {}) {
8
+ return createInternalToolHost(context, { ...options,
9
+ analyzer: options.analyzer ? toLegacyPositionAnalysisProvider(options.analyzer) : undefined });
10
+ }
@@ -0,0 +1,32 @@
1
+ import type { BranchExpansionConfig } from './branch-expansion';
2
+ import type { InitialSearchPlan } from './initial-search-plan';
3
+ import type { ReasoningPositionContext, ReasoningToolSet } from './poc-contracts';
4
+
5
+ export type BranchExpansionMode = 'off' | 'preparation' | 'tools' | 'both';
6
+ // Experimental until real-engine cost and answer qualification are complete.
7
+ export const BRANCH_EXPANSION_OPTIONS: BranchExpansionConfig = {maxNodes:8,maxPlies:4,maxLines:2,depthFloor:14,depthMode:'decreasing',maxElapsedMs:20000};
8
+ export const DEFAULT_BRANCH_EXPANSION_MODE: BranchExpansionMode = 'off';
9
+
10
+ /** Add a registered actual-after search without replacing hypothetical eager leads.
11
+ * Selected-context retention is the authority; omitted historical questions must
12
+ * acquire their explicit root through ordinary tools. No natural-language guesses. */
13
+ export function withBranchPreparation(plan: InitialSearchPlan, context: ReasoningPositionContext,
14
+ omitSelectedContext: boolean, mode: BranchExpansionMode): InitialSearchPlan {
15
+ if (!(mode === 'both' || mode === 'preparation') || omitSelectedContext || context.kind === 'position' || plan.requests.length) return plan;
16
+ return {...plan, requests:[{tool:'explore_continuations',input:{phase:'after',depth:18,multiPv:3},
17
+ reason:'Bounded actual-after continuation and response discovery; distinct from hypothetical skipped-turn leads.'}]};
18
+ }
19
+
20
+ /** Both hosts keep the same contracts. Only internally generated initial calls
21
+ * select the preparation arm; user/model tool schemas cannot alter this setting. */
22
+ export function selectBranchHost(ordinary: ReasoningToolSet, expanded: ReasoningToolSet,
23
+ mode: BranchExpansionMode): ReasoningToolSet {
24
+ return {reservesSearchAtDispatch:true, seedEvidence:records=>{const all=[...records];ordinary.seedEvidence?.(all);expanded.seedEvidence?.(all);}, descriptors:()=>ordinary.descriptors(), execute:async(name,input,signal,execution)=> {
25
+ const initial = typeof input.callId === 'string' && /^initial-search-\d+$/.test(input.callId);
26
+ const enabled = mode === 'both' || (initial ? mode === 'preparation' : mode === 'tools');
27
+ const selected = enabled ? expanded : ordinary;
28
+ const result = await selected.execute(name,input,signal,execution);
29
+ (enabled ? ordinary : expanded).seedEvidence?.(result.evidence);
30
+ return result;
31
+ }};
32
+ }
@@ -0,0 +1,80 @@
1
+ import { Chess } from 'chess.js';
2
+ import type { PositionAnalysisLine } from './poc-tools';
3
+
4
+ export type BranchExpansionConfig = {
5
+ maxNodes: number;
6
+ maxPlies: number;
7
+ maxLines: number;
8
+ depthFloor: number;
9
+ depthMode: 'fixed' | 'decreasing';
10
+ maxElapsedMs?: number;
11
+ };
12
+
13
+ export type BranchNode<T> = { prefix: string[]; finalFen: string; analysis: T };
14
+ export type BranchExpansionResult<T> = {
15
+ nodes: BranchNode<T>[];
16
+ summary: { requestedNodes: number; searchedNodes: number; returnedNodes: number; skippedDuplicate: number;
17
+ skippedInvalid: number; failedNodes: number; stopped: 'complete' | 'node-limit' | 'elapsed-limit';
18
+ scope: string };
19
+ };
20
+
21
+ /** Only the original PVs are traversed. New alternatives are retained, never
22
+ * recursively scheduled. Each node is an independent FEN-root search. */
23
+ export async function expandPrincipalBranches<T extends {lines: PositionAnalysisLine[]}>(input: {
24
+ rootFen: string;
25
+ lines: readonly PositionAnalysisLine[];
26
+ parentDepth: number;
27
+ config: BranchExpansionConfig;
28
+ signal?: AbortSignal;
29
+ search: (fen: string, depth: number) => Promise<T | null>;
30
+ }): Promise<BranchExpansionResult<T>> {
31
+ const started = performance.now();
32
+ const config = input.config;
33
+ const roots = input.lines.slice(0, Math.max(0,config.maxLines)).map(line => {
34
+ if (!Array.isArray(line.pv) || line.pv[0] !== line.rootMove) return [];
35
+ // Completed PV suffixes are independently searched from another root. They
36
+ // must not become the parent search's own branch-selection evidence.
37
+ return Array.isArray(line.rawPv) ? line.rawPv : line.pv;
38
+ });
39
+ const seen = new Set<string>();
40
+ const nodes: BranchNode<T>[] = [];
41
+ let requestedNodes = 0, searchedNodes = 0, skippedDuplicate = 0, skippedInvalid = 0, failedNodes = 0;
42
+ let stopped: BranchExpansionResult<T>['summary']['stopped'] = 'complete';
43
+ outer: for (let ply = 1; ply <= config.maxPlies; ply++) {
44
+ for (const pv of roots) {
45
+ if (pv.length < ply) continue;
46
+ input.signal?.throwIfAborted();
47
+ if (searchedNodes >= config.maxNodes) { stopped = 'node-limit'; break outer; }
48
+ if (config.maxElapsedMs != null && performance.now() - started >= config.maxElapsedMs) { stopped = 'elapsed-limit'; break outer; }
49
+ requestedNodes++;
50
+ const prefix = pv.slice(0,ply);
51
+ let finalFen: string;
52
+ try {
53
+ const chess = new Chess(input.rootFen);
54
+ for (const uci of prefix) {
55
+ if (!/^[a-h][1-8][a-h][1-8][qrbn]?$/.test(uci)
56
+ || !chess.move({from:uci.slice(0,2),to:uci.slice(2,4),promotion:uci.slice(4)||undefined})) throw Error('invalid-parent-pv');
57
+ }
58
+ if (chess.isGameOver()) continue;
59
+ finalFen = chess.fen();
60
+ } catch { skippedInvalid++; continue; }
61
+ if (seen.has(finalFen)) { skippedDuplicate++; continue; }
62
+ seen.add(finalFen);
63
+ const depth = config.depthMode === 'fixed' ? input.parentDepth
64
+ : Math.max(config.depthFloor,input.parentDepth-ply);
65
+ searchedNodes++;
66
+ try {
67
+ const analysis = await input.search(finalFen,depth);
68
+ input.signal?.throwIfAborted();
69
+ if (analysis?.lines.length) nodes.push({prefix,finalFen,analysis});
70
+ else failedNodes++;
71
+ } catch (error) {
72
+ input.signal?.throwIfAborted();
73
+ if ((error as {code?:string})?.code === 'search-budget-exhausted') { stopped='node-limit'; break outer; }
74
+ failedNodes++;
75
+ }
76
+ }
77
+ }
78
+ return {nodes,summary:{requestedNodes,searchedNodes,returnedNodes:nodes.length,skippedDuplicate,skippedInvalid,failedNodes,stopped,
79
+ scope:'Independent searches at exact legal PV-prefix positions. Child scores compare moves only within the same child search; no child score validates the parent move. No new branches recursively expanded.'}};
80
+ }