@knights-eye-chess/ai-analysis 0.0.0-stage → 0.1.0-dev.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +235 -0
- package/LICENSING.md +26 -0
- package/NOTICE +5 -0
- package/NPM-PUBLISHING.md +28 -0
- package/README.md +139 -2
- package/SOURCE-ORIGIN.json +16 -0
- package/THIRD_PARTY_NOTICES.md +18 -0
- package/build.mjs +22 -0
- package/dist/answer-pipeline.test.mjs +375 -0
- package/dist/chunk-PLKNJRPF.js +10662 -0
- package/dist/chunk-XQDZJOYD.js +2515 -0
- package/dist/index.js +146 -0
- package/dist/owner-tests/chess-reasoning-compact-search-preparation.test.mjs +963 -0
- package/dist/owner-tests/mechanism-encoding.test.mjs +529 -0
- package/dist/owner-tests/move-mechanism-facts.test.mjs +396 -0
- package/dist/owner-tests/pipeline-host-preparation.test.mjs +10556 -0
- package/dist/owner-tests/position-preparation.test.mjs +625 -0
- package/dist/owner-tests/preparation-consolidation.test.mjs +1052 -0
- package/dist/owner-tests/prose-reference-admission.test.mjs +1770 -0
- package/dist/owner-tests/score-comparison-notices.test.mjs +240 -0
- package/dist/owner-tests/simple-engine-presentation.test.mjs +233 -0
- package/dist/owner-tests/tool-input-loop-guard.test.mjs +78 -0
- package/dist/research.js +2274 -0
- package/dist/runtime-closure.json +103 -0
- package/dist/shared-runtime-closure.json +39 -0
- package/dist/shared.js +152 -0
- package/dist/types/ai-request-failure.d.ts +14 -0
- package/dist/types/ai-request-limits.d.ts +4 -0
- package/dist/types/ai-routing-diagnostics.d.ts +14 -0
- package/dist/types/chess-reasoning/ablation-adaptive-evidence-loop.d.ts +127 -0
- package/dist/types/chess-reasoning/ablation-adaptive-runtime.d.ts +178 -0
- package/dist/types/chess-reasoning/ablation-direct-synthesis.d.ts +1030 -0
- package/dist/types/chess-reasoning/ablation-evidence-bundle.d.ts +185 -0
- package/dist/types/chess-reasoning/ablation-evidence-capture.d.ts +37 -0
- package/dist/types/chess-reasoning/ablation-lean-direct-runtime.d.ts +193 -0
- package/dist/types/chess-reasoning/ablation-model-facing-evidence.d.ts +38 -0
- package/dist/types/chess-reasoning/ablation-transport-correction.d.ts +13 -0
- package/dist/types/chess-reasoning/ai-sdk-runtime-support.d.ts +93 -0
- package/dist/types/chess-reasoning/answer-host-contracts.d.ts +59 -0
- package/dist/types/chess-reasoning/answer-model-connector.d.ts +68 -0
- package/dist/types/chess-reasoning/answer-pipeline-exports.d.ts +98 -0
- package/dist/types/chess-reasoning/answer-pipeline.d.ts +41 -0
- package/dist/types/chess-reasoning/answer-reference-validation.d.ts +40 -0
- package/dist/types/chess-reasoning/answer-tool-host.d.ts +8 -0
- package/dist/types/chess-reasoning/branch-expansion-policy.d.ts +13 -0
- package/dist/types/chess-reasoning/branch-expansion.d.ts +39 -0
- package/dist/types/chess-reasoning/causal-claim-checks.d.ts +21 -0
- package/dist/types/chess-reasoning/chess-reference-dictionary.d.ts +13 -0
- package/dist/types/chess-reasoning/classification-question-target.d.ts +22 -0
- package/dist/types/chess-reasoning/classification-reassessment.d.ts +31 -0
- package/dist/types/chess-reasoning/compact-search-preparation.d.ts +7 -0
- package/dist/types/chess-reasoning/concept-registry.d.ts +99 -0
- package/dist/types/chess-reasoning/conversation-context.d.ts +12 -0
- package/dist/types/chess-reasoning/engine-search-failure.d.ts +7 -0
- package/dist/types/chess-reasoning/evidence-citation-normalization.d.ts +9 -0
- package/dist/types/chess-reasoning/evidence-licenses.d.ts +57 -0
- package/dist/types/chess-reasoning/evidence-selector-contract.d.ts +5 -0
- package/dist/types/chess-reasoning/existing-analysis-context.d.ts +104 -0
- package/dist/types/chess-reasoning/fact-observation-provider.d.ts +15 -0
- package/dist/types/chess-reasoning/fact-provider.d.ts +111 -0
- package/dist/types/chess-reasoning/fresh-engine-pv-witnesses.d.ts +67 -0
- package/dist/types/chess-reasoning/historical-prose.d.ts +23 -0
- package/dist/types/chess-reasoning/idea-graph.d.ts +34 -0
- package/dist/types/chess-reasoning/initial-search-plan.d.ts +37 -0
- package/dist/types/chess-reasoning/lossless-evidence-sharing.d.ts +18 -0
- package/dist/types/chess-reasoning/matched-comparison.d.ts +10 -0
- package/dist/types/chess-reasoning/material-accounting.d.ts +80 -0
- package/dist/types/chess-reasoning/material-format.d.ts +4 -0
- package/dist/types/chess-reasoning/mechanical-fact-provider.d.ts +33 -0
- package/dist/types/chess-reasoning/mechanical-ray-facts.d.ts +19 -0
- package/dist/types/chess-reasoning/mechanism-presentation.d.ts +9 -0
- package/dist/types/chess-reasoning/move-mechanism-facts.d.ts +45 -0
- package/dist/types/chess-reasoning/native-answer-connector.d.ts +4 -0
- package/dist/types/chess-reasoning/opening-source-contracts.d.ts +54 -0
- package/dist/types/chess-reasoning/opening-source-credits.d.ts +16 -0
- package/dist/types/chess-reasoning/opponent-threat-preparation.d.ts +7 -0
- package/dist/types/chess-reasoning/piece-identities.d.ts +11 -0
- package/dist/types/chess-reasoning/poc-contracts.d.ts +1026 -0
- package/dist/types/chess-reasoning/poc-hypotheses.d.ts +74 -0
- package/dist/types/chess-reasoning/poc-move-witnesses.d.ts +13 -0
- package/dist/types/chess-reasoning/poc-observations.d.ts +23 -0
- package/dist/types/chess-reasoning/poc-piece-refs.d.ts +12 -0
- package/dist/types/chess-reasoning/poc-plan-model.d.ts +45 -0
- package/dist/types/chess-reasoning/poc-semantic-text.d.ts +8 -0
- package/dist/types/chess-reasoning/poc-tools.d.ts +81 -0
- package/dist/types/chess-reasoning/portable-answer-runtime.d.ts +291 -0
- package/dist/types/chess-reasoning/position-analysis-contracts.d.ts +31 -0
- package/dist/types/chess-reasoning/position-analysis-scope.d.ts +3 -0
- package/dist/types/chess-reasoning/preparation-consolidation.d.ts +21 -0
- package/dist/types/chess-reasoning/production-observation-provider.d.ts +16 -0
- package/dist/types/chess-reasoning/question-routing.d.ts +326 -0
- package/dist/types/chess-reasoning/recorded-main-line.d.ts +37 -0
- package/dist/types/chess-reasoning/recorded-position-tools.d.ts +11 -0
- package/dist/types/chess-reasoning/recorded-reference-context.d.ts +13 -0
- package/dist/types/chess-reasoning/recorded-selected-classification.d.ts +36 -0
- package/dist/types/chess-reasoning/reference-limits.d.ts +2 -0
- package/dist/types/chess-reasoning/research-exports.d.ts +197 -0
- package/dist/types/chess-reasoning/saved-score-presentation.d.ts +14 -0
- package/dist/types/chess-reasoning/score-comparison-notices.d.ts +33 -0
- package/dist/types/chess-reasoning/server-simple-policy.d.ts +31 -0
- package/dist/types/chess-reasoning/simple-agent-config.d.ts +73 -0
- package/dist/types/chess-reasoning/simple-board-analysis.d.ts +85 -0
- package/dist/types/chess-reasoning/simple-compact-evidence.d.ts +7 -0
- package/dist/types/chess-reasoning/simple-completion.d.ts +23 -0
- package/dist/types/chess-reasoning/simple-engine-presentation.d.ts +3 -0
- package/dist/types/chess-reasoning/simple-evidence-preparation.d.ts +17 -0
- package/dist/types/chess-reasoning/simple-evidence-view.d.ts +17 -0
- package/dist/types/chess-reasoning/simple-explanation-coverage.d.ts +90 -0
- package/dist/types/chess-reasoning/simple-fact-delivery.d.ts +43 -0
- package/dist/types/chess-reasoning/simple-fallback-premise.d.ts +5 -0
- package/dist/types/chess-reasoning/simple-prose-references.d.ts +108 -0
- package/dist/types/chess-reasoning/simple-runtime-version.d.ts +1 -0
- package/dist/types/chess-reasoning/stable.d.ts +2 -0
- package/dist/types/chess-reasoning/tool-input-loop-guard.d.ts +5 -0
- package/dist/types/chess-reasoning/types.d.ts +180 -0
- package/dist/types/chess-reasoning/visible-support-lines.d.ts +12 -0
- package/dist/types/chess-reasoning/wikibooks-index.d.ts +30 -0
- package/dist/types/chess-reasoning/wikibooks-tool-context.d.ts +10 -0
- package/dist/types/player-perspective.d.ts +16 -0
- package/dist/types/shared-exports.d.ts +22 -0
- package/docs/API.md +82 -0
- package/docs/OWNERSHIP-ADOPTION.md +35 -0
- package/docs/OWNERSHIP-DRIFT.json +55 -0
- package/docs/RESEARCH-API.md +28 -0
- package/licenses/third-party/INVENTORY.json +202 -0
- package/licenses/third-party/ai-7.0.85/LICENSE +13 -0
- package/licenses/third-party/ai-sdk--gateway-4.0.69/LICENSE +13 -0
- package/licenses/third-party/ai-sdk--provider-4.0.9/LICENSE +13 -0
- package/licenses/third-party/ai-sdk--provider-utils-5.0.34/APACHE-2.0.txt +73 -0
- package/licenses/third-party/ai-sdk--provider-utils-5.0.34/src/to-json-schema/zod3-to-json-schema/LICENSE +16 -0
- package/licenses/third-party/chess.js-1.4.0/LICENSE +23 -0
- package/licenses/third-party/eventsource-parser-3.1.1/LICENSE +21 -0
- package/licenses/third-party/json-schema-0.4.0/LICENSE +195 -0
- package/licenses/third-party/standard-schema--spec-1.1.0/LICENSE +21 -0
- package/licenses/third-party/types--json-schema-7.0.15/LICENSE +21 -0
- package/licenses/third-party/types--node-22.19.19/LICENSE +21 -0
- package/licenses/third-party/undici-7.30.0/LICENSE +21 -0
- package/licenses/third-party/undici-7.30.0/lib/web/fetch/LICENSE +21 -0
- package/licenses/third-party/undici-types-6.21.0/LICENSE +21 -0
- package/licenses/third-party/vercel--oidc-3.2.0/LICENSE +202 -0
- package/licenses/third-party/workflow--serde-4.1.0/APACHE-2.0.txt +73 -0
- package/licenses/third-party/workflow--serde-4.1.0/LICENSE.md +3 -0
- package/licenses/third-party/zod-4.6.5/LICENSE +21 -0
- package/npm-release.json +5 -0
- package/package.json +72 -4
- package/scripts/check-release-tooling.mjs +22 -0
- package/scripts/pack.mjs +68 -0
- package/scripts/publish-reviewed.mjs +85 -0
- package/scripts/release-tooling.json +13 -0
- package/scripts/update-dependency.mjs +44 -0
- package/scripts/verify-release.mjs +52 -0
- package/src/ai-request-failure.ts +34 -0
- package/src/ai-request-limits.ts +4 -0
- package/src/ai-routing-diagnostics.ts +33 -0
- package/src/chess-reasoning/ablation-adaptive-evidence-loop.ts +515 -0
- package/src/chess-reasoning/ablation-adaptive-runtime.ts +924 -0
- package/src/chess-reasoning/ablation-direct-synthesis.ts +1128 -0
- package/src/chess-reasoning/ablation-evidence-bundle.ts +726 -0
- package/src/chess-reasoning/ablation-evidence-capture.ts +513 -0
- package/src/chess-reasoning/ablation-lean-direct-runtime.ts +670 -0
- package/src/chess-reasoning/ablation-model-facing-evidence.ts +212 -0
- package/src/chess-reasoning/ablation-transport-correction.ts +106 -0
- package/src/chess-reasoning/ai-sdk-runtime-support.ts +381 -0
- package/src/chess-reasoning/answer-host-contracts.ts +10 -0
- package/src/chess-reasoning/answer-model-connector.ts +83 -0
- package/src/chess-reasoning/answer-pipeline-exports.ts +98 -0
- package/src/chess-reasoning/answer-pipeline.ts +195 -0
- package/src/chess-reasoning/answer-reference-validation.ts +124 -0
- package/src/chess-reasoning/answer-tool-host.ts +10 -0
- package/src/chess-reasoning/branch-expansion-policy.ts +32 -0
- package/src/chess-reasoning/branch-expansion.ts +80 -0
- package/src/chess-reasoning/causal-claim-checks.ts +256 -0
- package/src/chess-reasoning/chess-reference-dictionary.ts +21 -0
- package/src/chess-reasoning/classification-question-target.ts +30 -0
- package/src/chess-reasoning/classification-reassessment.ts +34 -0
- package/src/chess-reasoning/compact-search-preparation.ts +125 -0
- package/src/chess-reasoning/concept-registry.ts +275 -0
- package/src/chess-reasoning/conversation-context.ts +29 -0
- package/src/chess-reasoning/engine-search-failure.ts +11 -0
- package/src/chess-reasoning/evidence-citation-normalization.ts +15 -0
- package/src/chess-reasoning/evidence-licenses.ts +166 -0
- package/src/chess-reasoning/evidence-selector-contract.ts +5 -0
- package/src/chess-reasoning/existing-analysis-context.ts +544 -0
- package/src/chess-reasoning/fact-observation-provider.ts +134 -0
- package/src/chess-reasoning/fact-provider.ts +250 -0
- package/src/chess-reasoning/fresh-engine-pv-witnesses.ts +128 -0
- package/src/chess-reasoning/historical-prose.ts +116 -0
- package/src/chess-reasoning/idea-graph.ts +224 -0
- package/src/chess-reasoning/initial-search-plan.ts +115 -0
- package/src/chess-reasoning/lossless-evidence-sharing.ts +135 -0
- package/src/chess-reasoning/matched-comparison.ts +40 -0
- package/src/chess-reasoning/material-accounting.ts +55 -0
- package/src/chess-reasoning/material-format.ts +47 -0
- package/src/chess-reasoning/mechanical-fact-provider.ts +1602 -0
- package/src/chess-reasoning/mechanical-ray-facts.ts +123 -0
- package/src/chess-reasoning/mechanism-presentation.ts +94 -0
- package/src/chess-reasoning/move-mechanism-facts.ts +185 -0
- package/src/chess-reasoning/native-answer-connector.ts +13 -0
- package/src/chess-reasoning/opening-source-contracts.ts +47 -0
- package/src/chess-reasoning/opening-source-credits.ts +53 -0
- package/src/chess-reasoning/opponent-threat-preparation.ts +98 -0
- package/src/chess-reasoning/piece-identities.ts +159 -0
- package/src/chess-reasoning/poc-contracts.ts +1097 -0
- package/src/chess-reasoning/poc-hypotheses.ts +759 -0
- package/src/chess-reasoning/poc-move-witnesses.ts +170 -0
- package/src/chess-reasoning/poc-observations.ts +611 -0
- package/src/chess-reasoning/poc-piece-refs.ts +62 -0
- package/src/chess-reasoning/poc-plan-model.ts +345 -0
- package/src/chess-reasoning/poc-semantic-text.ts +10 -0
- package/src/chess-reasoning/poc-tools.ts +978 -0
- package/src/chess-reasoning/portable-answer-runtime.ts +684 -0
- package/src/chess-reasoning/position-analysis-contracts.ts +88 -0
- package/src/chess-reasoning/position-analysis-scope.ts +20 -0
- package/src/chess-reasoning/preparation-consolidation.ts +91 -0
- package/src/chess-reasoning/production-observation-provider.ts +41 -0
- package/src/chess-reasoning/question-routing.ts +332 -0
- package/src/chess-reasoning/recorded-main-line.ts +55 -0
- package/src/chess-reasoning/recorded-position-tools.ts +78 -0
- package/src/chess-reasoning/recorded-reference-context.ts +44 -0
- package/src/chess-reasoning/recorded-selected-classification.ts +54 -0
- package/src/chess-reasoning/reference-limits.ts +2 -0
- package/src/chess-reasoning/research-exports.ts +197 -0
- package/src/chess-reasoning/saved-score-presentation.ts +6 -0
- package/src/chess-reasoning/score-comparison-notices.ts +115 -0
- package/src/chess-reasoning/server-simple-policy.ts +75 -0
- package/src/chess-reasoning/simple-agent-config.ts +58 -0
- package/src/chess-reasoning/simple-board-analysis.ts +57 -0
- package/src/chess-reasoning/simple-compact-evidence.ts +79 -0
- package/src/chess-reasoning/simple-completion.ts +37 -0
- package/src/chess-reasoning/simple-engine-presentation.ts +95 -0
- package/src/chess-reasoning/simple-evidence-preparation.ts +32 -0
- package/src/chess-reasoning/simple-evidence-view.ts +69 -0
- package/src/chess-reasoning/simple-explanation-coverage.ts +118 -0
- package/src/chess-reasoning/simple-fact-delivery.ts +52 -0
- package/src/chess-reasoning/simple-fallback-premise.ts +65 -0
- package/src/chess-reasoning/simple-prose-references.ts +528 -0
- package/src/chess-reasoning/simple-runtime-version.ts +2 -0
- package/src/chess-reasoning/stable.ts +18 -0
- package/src/chess-reasoning/tool-input-loop-guard.ts +38 -0
- package/src/chess-reasoning/types.ts +195 -0
- package/src/chess-reasoning/visible-support-lines.ts +44 -0
- package/src/chess-reasoning/wikibooks-index.ts +56 -0
- package/src/chess-reasoning/wikibooks-tool-context.ts +91 -0
- package/src/player-perspective.ts +41 -0
- package/src/shared-exports.ts +23 -0
- package/test/answer-pipeline.test.mjs +375 -0
- package/test/owner/chess-reasoning-compact-search-preparation.test.mjs +111 -0
- package/test/owner/mechanism-encoding.test.mjs +78 -0
- package/test/owner/move-mechanism-facts.test.mjs +105 -0
- package/test/owner/pipeline-host-preparation.test.mjs +115 -0
- package/test/owner/position-preparation.test.mjs +22 -0
- package/test/owner/preparation-consolidation.test.mjs +74 -0
- package/test/owner/prose-reference-admission.test.mjs +26 -0
- package/test/owner/score-comparison-notices.test.mjs +116 -0
- package/test/owner/simple-engine-presentation.test.mjs +50 -0
- package/test/owner/tool-input-loop-guard.test.mjs +25 -0
- package/test-types.mts +23 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
import { Chess } from 'chess.js';
|
|
2
|
+
import { historicalProseRoots } from './historical-prose';
|
|
3
|
+
import { normalizedEvidenceCitations } from './evidence-citation-normalization';
|
|
4
|
+
import type { RecordedReferenceLine } from './historical-prose';
|
|
5
|
+
export type AuthoredReferenceResult = {label:string;target:string;resolved:boolean;reason?:string;canonicalLabel?:string};
|
|
6
|
+
import { MAX_INTERACTIVE_LINE_PLIES } from "./reference-limits";
|
|
7
|
+
import type { Square } from "chess.js";
|
|
8
|
+
import { createPocIdentityTimeline } from "./poc-piece-refs";
|
|
9
|
+
import { applyIdentityMove } from "./piece-identities";
|
|
10
|
+
import { reasoningEvidenceReplayStartPhase } from "./poc-move-witnesses";
|
|
11
|
+
import { reasoningContextHash, validateReasoningPositionContext, type ReasoningPositionContext, type ReasoningEvidenceRecord } from "./poc-contracts";
|
|
12
|
+
import { effectiveLicensesForRecord } from "./evidence-licenses";
|
|
13
|
+
import { admitFreshEnginePvWitnesses, type FreshEnginePvWitness, type FreshEnginePvReferenceEvidence } from './fresh-engine-pv-witnesses';
|
|
14
|
+
export type { FreshEnginePvWitness } from './fresh-engine-pv-witnesses';
|
|
15
|
+
export type SimpleProseReferenceOptions = { freshEnginePvWitnesses?: readonly FreshEnginePvWitness[] };
|
|
16
|
+
export type SimpleReferenceGapCounts = {
|
|
17
|
+
version: 'simple-reference-gap-counts/1'; unboundProseSpans: number; unboundMentions: number;
|
|
18
|
+
unresolvedReferences: number; unknownCitations: number; formattingWarnings: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const SIMPLE_REFERENCE_VERSION = "simple-references/22";
|
|
22
|
+
const MAX_EVIDENCE_REPLAY_PLIES = 256;
|
|
23
|
+
const INTERNAL_WITNESS = Symbol('internal-evidence-witness');
|
|
24
|
+
const MAX_AUTHORED_REFERENCE_PLIES = MAX_INTERACTIVE_LINE_PLIES;
|
|
25
|
+
type Phase = "before" | "after";
|
|
26
|
+
export type SimplePosition = { recordedContext?: ReasoningPositionContext; start: Phase; moves: { uci: string; san: string; pieceId: string }[]; fen: string };
|
|
27
|
+
export type SimpleReference = { kind: "move" | "piece" | "square" | "sequence"; label: string; displayLabel?: string; position: SimplePosition;
|
|
28
|
+
evidenceIds: string[]; engineEvidence?: FreshEnginePvReferenceEvidence[]; square?: string; pieceId?: string; san?: string; uci?: string; copyText?: string; pieceCode?: string };
|
|
29
|
+
export type SimpleToken = { text: string; reference?: SimpleReference; sourceLink?: { href: string }; gap?: string; variationIndex?: number; variationStart?: boolean; variationEnd?: boolean; variationPlayFrom?: number; variationMoveIndices?: number[] };
|
|
30
|
+
const UCI = /^[a-h][1-8][a-h][1-8][qrbn]?$/;
|
|
31
|
+
const names = { p: "pawn", n: "knight", b: "bishop", r: "rook", q: "queen", k: "king" };
|
|
32
|
+
|
|
33
|
+
/** An authored piece SAN may omit only a canonical source file/rank. */
|
|
34
|
+
function compatibleMoveSan(authored: string, canonical: string) {
|
|
35
|
+
if (authored.replace(/[+#]$/, '') === canonical.replace(/[+#]$/, '')) return true;
|
|
36
|
+
const pattern = /^([KQRBN])([a-h]?)([1-8]?)(x?)([a-h][1-8])([+#]?)$/;
|
|
37
|
+
const given = pattern.exec(authored), actual = pattern.exec(canonical);
|
|
38
|
+
return Boolean(given && actual && (actual[2] || actual[3])
|
|
39
|
+
&& given[1] === actual[1] && given[4] === actual[4] && given[5] === actual[5]
|
|
40
|
+
&& (!given[2] || given[2] === actual[2]) && (!given[3] || given[3] === actual[3])
|
|
41
|
+
&& Number(Boolean(given[2])) + Number(Boolean(given[3])) < Number(Boolean(actual[2])) + Number(Boolean(actual[3])));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function canonicalMoveLabel(label: string, refs: readonly SimpleReference[]) {
|
|
45
|
+
const parts = [...label.trim().matchAll(/(?:(\d+)?(\.{1,3}|…)[ \t]*)?([^\s.]+)/g)];
|
|
46
|
+
if (parts.length !== refs.length || parts.map(part => part[0]).join('').replace(/\s/g, '') !== label.replace(/\s/g, '')) return label;
|
|
47
|
+
let index = 0;
|
|
48
|
+
return label.replace(/(?:(\d+)?(\.{1,3}|…)[ \t]*)?([^\s.]+)/g, (whole, _number, _dots, san: string) => {
|
|
49
|
+
const ref = refs[index++];
|
|
50
|
+
const canonical = ref.san;
|
|
51
|
+
if (!canonical || !compatibleMoveSan(san.replace(/[!?]+$/, ''), canonical)) return whole;
|
|
52
|
+
return whole.slice(0, whole.length - san.length) + canonical + (san.match(/[!?]+$/)?.[0] ?? '');
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Source attribution links are presentation-only; they never confer chess evidence. */
|
|
57
|
+
export function safeSimpleSourceLink(target: string): { href: string } | null {
|
|
58
|
+
let url: URL;
|
|
59
|
+
try { url = new URL(target); } catch { return null; }
|
|
60
|
+
if (url.protocol !== "https:" || url.username || url.password || url.port || url.hash) return null;
|
|
61
|
+
if (url.hostname === "creativecommons.org") {
|
|
62
|
+
return url.pathname === "/licenses/by-sa/4.0/" && !url.search ? { href: url.href } : null;
|
|
63
|
+
}
|
|
64
|
+
if (url.hostname !== "en.wikibooks.org" || url.pathname !== "/w/index.php") return null;
|
|
65
|
+
const title = url.searchParams.getAll("title");
|
|
66
|
+
if (title.length !== 1 || !title[0].trim()) return null;
|
|
67
|
+
const keys = [...url.searchParams.keys()].sort();
|
|
68
|
+
const revision = url.searchParams.get("oldid");
|
|
69
|
+
if (keys.join(",") === "oldid,title" && revision && /^[1-9]\d*$/.test(revision) && Number.isSafeInteger(Number(revision))) return { href: url.href };
|
|
70
|
+
if (keys.join(",") === "action,title" && url.searchParams.get("action") === "history") return { href: url.href };
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Preserve the model's prose while removing presentation-only fence markers. */
|
|
75
|
+
export function stripSimpleMarkdownFences(value: string) {
|
|
76
|
+
return value.replace(/^[ \t]*```(?:text|plaintext|markdown|md|pgn|chess)?[ \t]*$/gim, "");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const internalCitationLink = /\[([^\]\n]+)\]\(\s*(?:evidence|recorded-main-line):[\w:-]+\s*\)/g;
|
|
80
|
+
/** Internal citation labels are not user prose or navigation. Raw output is retained. */
|
|
81
|
+
export function stripSimpleCitationLinks(value: string) {
|
|
82
|
+
return value.replace(internalCitationLink, "");
|
|
83
|
+
}
|
|
84
|
+
/** Repair only trailing legacy labels attested as internal links in the raw answer. */
|
|
85
|
+
export function repairFrozenCitationLabels(value: string, rawParagraph: string) {
|
|
86
|
+
let result = stripSimpleCitationLinks(value);
|
|
87
|
+
const suffix = rawParagraph.match(/(?:\s*\[[^\]\n]+\]\(\s*(?:evidence|recorded-main-line):[\w:-]+\s*\))+\s*$/)?.[0];
|
|
88
|
+
if (!suffix) return result;
|
|
89
|
+
const labels = [...suffix.matchAll(internalCitationLink)].map(match => `[${match[1]}]`).reverse();
|
|
90
|
+
for (const label of labels) {
|
|
91
|
+
const trimmed = result.trimEnd();
|
|
92
|
+
if (!trimmed.endsWith(label)) break;
|
|
93
|
+
result = trimmed.slice(0, -label.length).trimEnd();
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Presentation admission only: no claim-entailment or strategic validator. */
|
|
99
|
+
export function simpleProseReferences(answer: string, context: ReasoningPositionContext, records: readonly ReasoningEvidenceRecord[], recordedLine?: RecordedReferenceLine, recordedPath?: string[], onReference?: (reference:AuthoredReferenceResult) => void, internalWitness?: typeof INTERNAL_WITNESS | SimpleProseReferenceOptions) {
|
|
100
|
+
const authoredPathLimit = internalWitness === INTERNAL_WITNESS ? MAX_EVIDENCE_REPLAY_PLIES : MAX_AUTHORED_REFERENCE_PLIES;
|
|
101
|
+
validateReasoningPositionContext(context);
|
|
102
|
+
answer = normalizedEvidenceCitations(answer, records.filter(record => record.contextHash === reasoningContextHash(context))).answer;
|
|
103
|
+
answer = stripSimpleMarkdownFences(answer);
|
|
104
|
+
const historical = historicalProseRoots(context, records, recordedLine);
|
|
105
|
+
const historicalVariations: Array<{start:Phase;ucis:string[];evidenceIds:string[];authority:string;recordedContext:ReasoningPositionContext}> = [];
|
|
106
|
+
const historicalRefs = new Map<string, SimpleReference[]>();
|
|
107
|
+
const timeline = createPocIdentityTimeline(context), hash = reasoningContextHash(context);
|
|
108
|
+
const evidence = records.filter(record => {
|
|
109
|
+
if (record.contextHash !== hash) return false;
|
|
110
|
+
try { effectiveLicensesForRecord(record); return true; } catch { return false; }
|
|
111
|
+
});
|
|
112
|
+
// Request-local only: exact root and ordered path, never shared across answers.
|
|
113
|
+
const replayCache = new Map<string, { state: typeof timeline.before; position: SimplePosition; copyText: string }>();
|
|
114
|
+
const replay = (start: Phase, ucis: string[]) => {
|
|
115
|
+
// Evidence can have a long PV; authored targets retain their separate 32-ply cap.
|
|
116
|
+
if (ucis.length > MAX_EVIDENCE_REPLAY_PLIES || ucis.some(uci => !UCI.test(uci))) throw Error("Invalid or overlong path");
|
|
117
|
+
const key = `${start}:${ucis.join(",")}`;
|
|
118
|
+
const cached = replayCache.get(key);
|
|
119
|
+
if (cached) return cached;
|
|
120
|
+
let state = timeline[start];
|
|
121
|
+
const moves: SimplePosition["moves"] = [];
|
|
122
|
+
let copyText = "";
|
|
123
|
+
for (const uci of ucis) {
|
|
124
|
+
const fen = state.fen.split(" ");
|
|
125
|
+
const applied = applyIdentityMove(state, { from: uci.slice(0, 2), to: uci.slice(2, 4), promotion: uci.slice(4) || undefined });
|
|
126
|
+
moves.push({ uci, san: applied.delta.san, pieceId: applied.delta.mover });
|
|
127
|
+
copyText = `${fen[5]}${fen[1] === "b" ? "..." : "."} ${applied.delta.san}`;
|
|
128
|
+
state = applied.state;
|
|
129
|
+
}
|
|
130
|
+
const result = { state, position: { start, moves, fen: state.fen } as SimplePosition, copyText };
|
|
131
|
+
replayCache.set(key, result);
|
|
132
|
+
return result;
|
|
133
|
+
};
|
|
134
|
+
const freshEnginePvAdmission = admitFreshEnginePvWitnesses(context, typeof internalWitness === 'object' ? internalWitness.freshEnginePvWitnesses ?? [] : []);
|
|
135
|
+
const lines: { start: Phase; ucis: string[]; evidenceIds: string[]; authority: string; engineEvidence?: FreshEnginePvReferenceEvidence[] }[] = freshEnginePvAdmission.lines;
|
|
136
|
+
if (recordedPath?.length) {
|
|
137
|
+
try { replay('after',recordedPath); lines.push({start:'after',ucis:recordedPath,evidenceIds:[],authority:'Recorded game play'}); } catch { /* Invalid record path is not admitted. */ }
|
|
138
|
+
}
|
|
139
|
+
const object = (value: unknown): Record<string, unknown> | null => value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
140
|
+
for (const record of evidence) {
|
|
141
|
+
if (record.data.recordedPosition) continue;
|
|
142
|
+
const replayPhase = reasoningEvidenceReplayStartPhase(record, context);
|
|
143
|
+
const start = replayPhase && context.kind === "position" ? "after" : replayPhase, r = record.replay;
|
|
144
|
+
// test_line's separate engine record starts at the tested line's endpoint.
|
|
145
|
+
// Its explicit phase + played prefix must reproduce BOTH declared FENs;
|
|
146
|
+
// never infer a root by searching the board or joining unrelated records.
|
|
147
|
+
const phase = record.data.phase, played = record.data.played, scope = object(record.data.searchScope);
|
|
148
|
+
if ((phase === "before" || phase === "after") && Array.isArray(played) && Array.isArray(record.data.lines)
|
|
149
|
+
&& effectiveLicensesForRecord(record).includes("engine.bounded-continuation")) {
|
|
150
|
+
try {
|
|
151
|
+
const prefix = played.map(move => object(move)?.uci);
|
|
152
|
+
if (prefix.some(uci => typeof uci !== "string")) throw Error("Invalid tested prefix");
|
|
153
|
+
const tested = replay(phase, prefix as string[]);
|
|
154
|
+
if (tested.position.fen !== r?.fen || tested.position.fen !== scope?.fen || tested.position.fen !== record.data.finalFen) throw Error("Tested endpoint mismatch");
|
|
155
|
+
if (tested.position.moves.some((move, index) => move.san !== object(played[index])?.san)) throw Error("Tested SAN mismatch");
|
|
156
|
+
for (const candidate of record.data.lines) {
|
|
157
|
+
const line = object(candidate), pv = line?.pv;
|
|
158
|
+
if (!Array.isArray(pv) || !pv.length || pv.some(uci => typeof uci !== "string") || line?.rootMove !== pv[0]) continue;
|
|
159
|
+
try {
|
|
160
|
+
const ucis = [...prefix, ...pv] as string[];
|
|
161
|
+
replay(phase, ucis);
|
|
162
|
+
lines.push({ start: phase, ucis, evidenceIds: [record.id], authority: "Bounded engine line" });
|
|
163
|
+
} catch { /* Never admit an invalid tail. */ }
|
|
164
|
+
}
|
|
165
|
+
} catch { /* A malformed continuation record contributes no path. */ }
|
|
166
|
+
}
|
|
167
|
+
if (!start || !r) continue;
|
|
168
|
+
const sequence = r.movesUci ?? r.moves ?? r.line ?? (r.move ? [r.move] : []);
|
|
169
|
+
const candidates = [sequence, ...(Array.isArray(r.linesUci) ? r.linesUci : [])];
|
|
170
|
+
// Older producers retained only a replay prefix while delivering the full
|
|
171
|
+
// engine PV. Admit its extension only in the same exact licensed scope,
|
|
172
|
+
// with an existing matching witness and a completely legal tail.
|
|
173
|
+
if ((phase === start || context.kind === "position" && phase === replayPhase) && scope?.fen === r.fen && r.fen === timeline[start].fen
|
|
174
|
+
&& Array.isArray(record.data.lines) && effectiveLicensesForRecord(record).some(license => license === "engine.bounded-continuation" || license === "engine.bounded-comparison")) {
|
|
175
|
+
for (const candidate of record.data.lines) {
|
|
176
|
+
const line = object(candidate), pv = line?.pv;
|
|
177
|
+
if (!Array.isArray(pv) || !pv.length || pv.some(uci => typeof uci !== "string")
|
|
178
|
+
|| line?.rootMove !== pv[0] || object(line?.scoreScope)?.fen !== r.fen) continue;
|
|
179
|
+
if (!candidates.some(prefix => Array.isArray(prefix) && prefix.length > 0
|
|
180
|
+
&& prefix.length <= pv.length && prefix.every((uci, index) => uci === pv[index]))) continue;
|
|
181
|
+
try {
|
|
182
|
+
replay(start, pv as string[]);
|
|
183
|
+
lines.push({ start, ucis: pv as string[], evidenceIds: [record.id], authority: "Bounded engine line" });
|
|
184
|
+
} catch { /* Invalid full PVs never supply a partial extension. */ }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const value of candidates) {
|
|
188
|
+
if (!Array.isArray(value) || !value.length || value.some(x => typeof x !== "string")) continue;
|
|
189
|
+
try {
|
|
190
|
+
replay(start, value as string[]); // Full line must be legal, including omitted tail.
|
|
191
|
+
lines.push({ start, ucis: value as string[], evidenceIds: [record.id], authority: effectiveLicensesForRecord(record).some(license => license.startsWith("engine.bounded-")) ? "Bounded engine line" : "Illustrative legal line" });
|
|
192
|
+
} catch { /* Invalid evidence replay is never partially admitted. */ }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const normalizedPath = (start: Phase, moves: string[]) => context.kind === "position" ? { start: "after", moves } : start === "before" && moves[0] === context.moveUci
|
|
196
|
+
? { start: "after", moves: moves.slice(1) } : { start, moves };
|
|
197
|
+
const matchingLines = (start: Phase, moves: string[]) => {
|
|
198
|
+
if (!moves.length) return [];
|
|
199
|
+
const path = normalizedPath(start, moves);
|
|
200
|
+
if (!path.moves.length) return [];
|
|
201
|
+
return lines.filter(line => { const candidate = normalizedPath(line.start, line.ucis); return candidate.start === path.start && path.moves.every((uci, i) => candidate.moves[i] === uci); });
|
|
202
|
+
};
|
|
203
|
+
// Aliases are declared in this answer only, after exact-context evidence
|
|
204
|
+
// admission. No global registry, previous-answer lookup, or persisted pointer.
|
|
205
|
+
const registered = new Map<string, { start: Phase; ucis: string[] }>();
|
|
206
|
+
const rejected = new Set<string>();
|
|
207
|
+
for (const match of answer.matchAll(/\[([^\]\n]+)\]\(line:([a-zA-Z][a-zA-Z0-9_-]{0,15}):(before|after):([^)\n]+)\)/g)) {
|
|
208
|
+
const [, label, id, start, path] = match;
|
|
209
|
+
if (registered.has(id) || rejected.has(id)) { registered.delete(id); rejected.add(id); continue; }
|
|
210
|
+
try {
|
|
211
|
+
const ucis = path.split(",");
|
|
212
|
+
if (!ucis.length || ucis.length > authoredPathLimit || id === "before" || id === "after") throw Error("Unsupported registered line");
|
|
213
|
+
const terminal = replay(start as Phase, ucis); // Validate the entire tail, not just a cited ply.
|
|
214
|
+
const authored = label.replace(/^(?:\d+)?(?:\.{1,3}|…)\s*/, "").replace(/[!?]+$/, "");
|
|
215
|
+
if (ucis.length === 1 && !compatibleMoveSan(authored, terminal.position.moves.at(-1)?.san ?? '')) throw Error("Registered SAN mismatch");
|
|
216
|
+
if (!matchingLines(start as Phase, ucis).length) throw Error("Unwitnessed registered line");
|
|
217
|
+
registered.set(id, { start: start as Phase, ucis });
|
|
218
|
+
} catch { rejected.add(id); }
|
|
219
|
+
}
|
|
220
|
+
const canonicalTarget = (target: string) => {
|
|
221
|
+
const [kind, id, ply, square, ...extra] = target.split(":");
|
|
222
|
+
if (id === "before" || id === "after") return target;
|
|
223
|
+
if (!["line", "move", "piece", "square"].includes(kind)) return target;
|
|
224
|
+
const line = registered.get(id);
|
|
225
|
+
if (!line || rejected.has(id)) throw Error("Unknown or invalid answer-local variation");
|
|
226
|
+
if (kind === "line") {
|
|
227
|
+
if (ply !== line.start || square !== line.ucis.join(",") || extra.length) throw Error("Invalid variation declaration");
|
|
228
|
+
return `move:${line.start}:${line.ucis.join(",")}`;
|
|
229
|
+
}
|
|
230
|
+
if (!/^(0|[1-9]\d*)$/.test(ply) || Number(ply) > line.ucis.length || extra.length
|
|
231
|
+
|| (kind === "move" && (square !== undefined || Number(ply) === 0))
|
|
232
|
+
|| (kind !== "move" && !/^[a-h][1-8]$/.test(square || ""))) throw Error("Invalid variation ply");
|
|
233
|
+
const prefix = line.ucis.slice(0, Number(ply)).join(",");
|
|
234
|
+
return `${kind}:${line.start}:${prefix}${kind === "move" ? "" : `:${square}`}`;
|
|
235
|
+
};
|
|
236
|
+
const notationCorrections: string[] = [];
|
|
237
|
+
const resolve = (label: string, target: string): SimpleReference => {
|
|
238
|
+
const parts = target.split(":"), kind = parts.shift(), start = parts.shift();
|
|
239
|
+
if (!["move", "piece", "square"].includes(kind || "") || (start !== "before" && start !== "after")) throw Error("Missing exact position");
|
|
240
|
+
// A square-only label is an unambiguous coordinate, not natural-language
|
|
241
|
+
// inference. Tolerate the omitted terminal square only for a valid UCI list.
|
|
242
|
+
const implicitSquare = kind === "square" && /^[a-h][1-8]$/.test(label)
|
|
243
|
+
&& parts.length === 1 && parts[0].split(",").every(uci => UCI.test(uci));
|
|
244
|
+
const square = kind === "move" ? undefined : implicitSquare ? label : parts.pop();
|
|
245
|
+
const ucis = parts.length === 1 && parts[0] ? parts[0].split(",") : [];
|
|
246
|
+
if (parts.length > 1 || ucis.length > authoredPathLimit) throw Error("Unsupported replay path");
|
|
247
|
+
const result = replay(start, ucis), matching = matchingLines(start, ucis);
|
|
248
|
+
if (ucis.length > 1 && !matching.length) throw Error("Path not witnessed in acquired evidence");
|
|
249
|
+
const evidenceIds = [...new Set(matching.flatMap(line => line.evidenceIds))];
|
|
250
|
+
const witnessedEngineEvidence = matching.flatMap(line => line.engineEvidence ?? []);
|
|
251
|
+
const engineEvidence = witnessedEngineEvidence.length ? { engineEvidence: witnessedEngineEvidence } : {};
|
|
252
|
+
if (kind === "move") {
|
|
253
|
+
const last = result.position.moves.at(-1);
|
|
254
|
+
const authored = label.replace(/^(?:\d+)?(?:\.{1,3}|…)\s*/, "").replace(/[!?]+$/, "");
|
|
255
|
+
if (!last || !compatibleMoveSan(authored, last.san)) throw Error("Move label does not match legal SAN");
|
|
256
|
+
const mover = result.state.pieces[last.pieceId];
|
|
257
|
+
return { kind, label: last.san, ...last, position: result.position, copyText: result.copyText, evidenceIds, ...engineEvidence, pieceCode: mover.color + mover.type };
|
|
258
|
+
}
|
|
259
|
+
if (!square || !/^[a-h][1-8]$/.test(square)) throw Error("Invalid square");
|
|
260
|
+
if (kind === "square") {
|
|
261
|
+
if (/^[a-h][1-8]$/.test(label) && label !== square) throw Error("Square label mismatch");
|
|
262
|
+
return { kind, label: square, displayLabel: label, square, position: result.position, evidenceIds, ...engineEvidence };
|
|
263
|
+
}
|
|
264
|
+
const pieceId = result.state.squares[square as Square], piece = pieceId && result.state.pieces[pieceId];
|
|
265
|
+
if (!piece) throw Error("No piece at the scoped square");
|
|
266
|
+
// Labels are arbitrary localized prose. Only the replayed board supplies
|
|
267
|
+
// identity and the accessible name; visible wording remains model-authored.
|
|
268
|
+
const canonicalLabel = `${piece.color === "w" ? "White" : "Black"} ${names[piece.type]} on ${square}`;
|
|
269
|
+
return { kind: "piece", label: canonicalLabel, displayLabel: label, square, pieceId: piece.id, pieceCode: piece.color + piece.type, position: result.position, evidenceIds, ...engineEvidence };
|
|
270
|
+
};
|
|
271
|
+
// A labelled move link can show the terminal move or a contiguous SAN suffix.
|
|
272
|
+
// Preserve valid SAN labels; prose labels remain one witnessed playback link.
|
|
273
|
+
const resolveLabel = (label: string, target: string): SimpleReference[] => {
|
|
274
|
+
if (!target.startsWith("move:")) return [resolve(label, target)];
|
|
275
|
+
const [kind, phase, path, ...extra] = target.split(":");
|
|
276
|
+
if (extra.length || !path || (phase !== "before" && phase !== "after")) throw Error("Missing exact position");
|
|
277
|
+
const ucis = path.split(",");
|
|
278
|
+
try {
|
|
279
|
+
// Remove numbering separately so both "2. d4" and "2.d4" are accepted.
|
|
280
|
+
const labels = [...label.trim().matchAll(/(?:(\d+)?(\.{1,3}|…)\s*)?([^\s.]+)/g)];
|
|
281
|
+
if (!labels.length || labels.length > ucis.length
|
|
282
|
+
|| labels.map(match => match[0]).join("").replace(/\s/g, "") !== label.replace(/\s/g, "")) throw Error("Move label does not match legal SAN");
|
|
283
|
+
const offset = ucis.length - labels.length;
|
|
284
|
+
return labels.map((match, index) => {
|
|
285
|
+
const ref = resolve(match[3], `${kind}:${phase}:${ucis.slice(0, offset + index + 1).join(",")}`);
|
|
286
|
+
if (match[1]) {
|
|
287
|
+
const prefix = ref.copyText!.split(" ")[0];
|
|
288
|
+
const expected = `${Number(match[1])}${match[2] === "." ? "." : "..."}`;
|
|
289
|
+
if (expected !== prefix) throw Error("Move number does not match legal position");
|
|
290
|
+
}
|
|
291
|
+
return ref;
|
|
292
|
+
});
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (ucis.length < 2) throw error;
|
|
295
|
+
// The target is navigation authority; prose labels cannot supply a path.
|
|
296
|
+
// Validate the entire path and witness before deriving any replacement.
|
|
297
|
+
const result = replay(phase, ucis);
|
|
298
|
+
if (!matchingLines(phase, ucis).length) throw Error("Path not witnessed in acquired evidence");
|
|
299
|
+
const terminal = result.position.moves.at(-1)!;
|
|
300
|
+
const validated = resolve(terminal.san, target);
|
|
301
|
+
return [{ kind: 'sequence', label, position: validated.position, evidenceIds: validated.evidenceIds,
|
|
302
|
+
...(validated.engineEvidence ? { engineEvidence: validated.engineEvidence } : {}) }];
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
const gaps: string[] = [], usedLines = new Map<string, typeof lines[number]>();
|
|
306
|
+
const referenceGapCounts: SimpleReferenceGapCounts = { version: 'simple-reference-gap-counts/1',
|
|
307
|
+
unboundProseSpans: 0, unboundMentions: 0, unresolvedReferences: 0, unknownCitations: 0, formattingWarnings: 0 };
|
|
308
|
+
let resolved = 0, total = 0;
|
|
309
|
+
const blocks = answer.split(/\n\s*\n|\n(?=\s*(?:[-*]|\d+\.)\s)/).filter(Boolean).map(raw => {
|
|
310
|
+
const cited = [...raw.matchAll(/(?:evidence|recorded-main-line):[\w:-]+/g)].map(match => match[0]);
|
|
311
|
+
for (const id of cited) if (!evidence.some(record => record.id === id)) {
|
|
312
|
+
referenceGapCounts.unknownCitations++;
|
|
313
|
+
gaps.push(`Unknown citation: ${id}`);
|
|
314
|
+
}
|
|
315
|
+
const withoutCitationControls = raw.replace(/\uE200cite[^\uE201]*\uE201/g, "");
|
|
316
|
+
if (withoutCitationControls !== raw) {
|
|
317
|
+
referenceGapCounts.formattingWarnings++;
|
|
318
|
+
gaps.push("Provider citation formatting hidden; raw output remains in the trace.");
|
|
319
|
+
}
|
|
320
|
+
const text = historical.annotate(stripSimpleCitationLinks(withoutCitationControls).replace(/\[(?:\s*(?:evidence|recorded-main-line):[\w:-]+\s*[,;]?\s*)+\]/g, "").replace(/(?:evidence|recorded-main-line):[\w:-]+/g, "").replace(/\([\s,;]*\)/g, "").trim());
|
|
321
|
+
const tokens: SimpleToken[] = [];
|
|
322
|
+
const plain = (value: string) => {
|
|
323
|
+
if (!value) return;
|
|
324
|
+
// Count unbound concrete chess mentions without guessing their context.
|
|
325
|
+
const unbound = value.match(/\b(?:[KQRBN](?:[a-h1-8])?x?[a-h][1-8]|[a-h]x[a-h][1-8](?:=[QRBN])?|[a-h][1-8](?:=[QRBN])?|O-O(?:-O)?)[+#]?\b|\b(?:the|White(?:’s|'s)?|Black(?:’s|'s)?) (?:knight|bishop|rook|queen|king|pawn)\b/g) || [];
|
|
326
|
+
total += unbound.length;
|
|
327
|
+
if (unbound.length) {
|
|
328
|
+
referenceGapCounts.unboundProseSpans++;
|
|
329
|
+
referenceGapCounts.unboundMentions += unbound.length;
|
|
330
|
+
gaps.push(`Unbound text: ${unbound.join(", ")}`);
|
|
331
|
+
}
|
|
332
|
+
tokens.push({ text: value });
|
|
333
|
+
};
|
|
334
|
+
let end = 0;
|
|
335
|
+
for (const match of text.matchAll(/\[([^\]\n]+)\]\(([^)\n]+)\)/g)) {
|
|
336
|
+
plain(text.slice(end, match.index));
|
|
337
|
+
const sourceLink = safeSimpleSourceLink(match[2]);
|
|
338
|
+
if (sourceLink) {
|
|
339
|
+
tokens.push({ text: match[1], sourceLink });
|
|
340
|
+
end = match.index! + match[0].length;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
total++;
|
|
344
|
+
try {
|
|
345
|
+
let refs: SimpleReference[];
|
|
346
|
+
if (match[2].startsWith('game:')) {
|
|
347
|
+
const parsed = /^game:(\d+):([a-h1-8qrbn,]+)$/.exec(match[2]);
|
|
348
|
+
const root = parsed && historical.roots.get(Number(parsed[1]));
|
|
349
|
+
if (!parsed || !root) throw Error('Unknown recorded position');
|
|
350
|
+
const path = parsed[2].split(',');
|
|
351
|
+
// Recorded actual paths are authoritative for navigation, not chess conclusions.
|
|
352
|
+
const actual = historical.recorded?.slice(Number(parsed[1]),Number(parsed[1])+path.length);
|
|
353
|
+
const isActual = actual?.length === path.length && actual.every((uci,i)=>uci===path[i]);
|
|
354
|
+
let v=simpleProseReferences(`[${match[1]}](move:after:${parsed[2]})`,root.context,root.records,undefined,isActual?path:undefined);
|
|
355
|
+
let witnessedRefs=v.blocks.flatMap(b=>b.tokens.flatMap(t=>t.reference?[t.reference]:[]));
|
|
356
|
+
let suffixWitness: {evidenceIds:string[];authority:string}|undefined;
|
|
357
|
+
if(!witnessedRefs.length || v.gaps.some(g=>!g.startsWith('Unbound'))) {
|
|
358
|
+
// An engine witness rooted before this game link may contain the
|
|
359
|
+
// exact suffix. The omitted moves must be actual recorded play;
|
|
360
|
+
// validate the whole original source path with the usual evidence
|
|
361
|
+
// admission before projecting navigation to this later root.
|
|
362
|
+
for(const candidate of historical.witnessedSuffixes(Number(parsed[1]),path)) {
|
|
363
|
+
const board=new Chess(candidate.root.context.fenAfter);
|
|
364
|
+
const fullLabel=candidate.path.map(uci=>{
|
|
365
|
+
const f=board.fen().split(' '),san=board.move({from:uci.slice(0,2),to:uci.slice(2,4),promotion:uci[4]}).san;
|
|
366
|
+
return `${f[5]}${f[1]==='w'?'.':'...'}${san}`;
|
|
367
|
+
}).join(' ');
|
|
368
|
+
const source=simpleProseReferences(`[${fullLabel}](move:after:${candidate.path.join(',')})`,candidate.root.context,candidate.root.records,undefined,undefined,undefined,INTERNAL_WITNESS);
|
|
369
|
+
const sourceRefs=source.blocks.flatMap(b=>b.tokens.flatMap(t=>t.reference?[t.reference]:[]));
|
|
370
|
+
if(source.coverage.resolved!==1 || source.gaps.some(g=>!g.startsWith('Unbound')) || sourceRefs.length!==candidate.path.length)continue;
|
|
371
|
+
const evidenceIds=sourceRefs.at(-1)?.evidenceIds ?? [];
|
|
372
|
+
if(!evidenceIds.length)continue;
|
|
373
|
+
const sourceLine=source.variations.find(line=>line.ucis.join(',')===candidate.path.join(','));
|
|
374
|
+
if(!sourceLine)continue;
|
|
375
|
+
const projected=simpleProseReferences(`[${match[1]}](move:after:${parsed[2]})`,root.context,[],undefined,path);
|
|
376
|
+
const projectedRefs=projected.blocks.flatMap(b=>b.tokens.flatMap(t=>t.reference?[t.reference]:[]));
|
|
377
|
+
if(projected.coverage.resolved!==1 || projected.gaps.some(g=>!g.startsWith('Unbound')) || !projectedRefs.length)continue;
|
|
378
|
+
suffixWitness={evidenceIds,authority:sourceLine.authority};
|
|
379
|
+
v={...projected,variations:projected.variations.map(line=>({...line,evidenceIds,authority:sourceLine.authority,
|
|
380
|
+
references:line.references.map(ref=>({...ref,evidenceIds}))}))};
|
|
381
|
+
witnessedRefs=projectedRefs.map(ref=>({...ref,evidenceIds}));
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
refs=witnessedRefs.map(ref=>({...ref,position:{...ref.position,recordedContext:root.context}}));
|
|
386
|
+
if(!refs.length || v.gaps.some(g=>!g.startsWith('Unbound')))throw Error('Unwitnessed historical path');
|
|
387
|
+
let witnessedVariations = v.variations;
|
|
388
|
+
if (!witnessedVariations.length && path.length > 1) {
|
|
389
|
+
const board = new Chess(root.context.fenAfter);
|
|
390
|
+
const sans = path.map(uci=>board.move({from:uci.slice(0,2),to:uci.slice(2,4),promotion:uci[4]}).san).join(' ');
|
|
391
|
+
const expanded = simpleProseReferences(`[${sans}](move:after:${parsed[2]})`,root.context,suffixWitness?[]:root.records,undefined,suffixWitness?path:isActual?path:undefined).variations;
|
|
392
|
+
witnessedVariations = suffixWitness ? expanded.map(line=>({...line,evidenceIds:suffixWitness.evidenceIds,authority:suffixWitness.authority,
|
|
393
|
+
references:line.references.map(ref=>({...ref,evidenceIds:suffixWitness.evidenceIds}))})) : expanded;
|
|
394
|
+
}
|
|
395
|
+
for(const line of witnessedVariations) {
|
|
396
|
+
historicalVariations.push({...line,recordedContext:root.context});
|
|
397
|
+
historicalRefs.set(`${reasoningContextHash(root.context)}:${line.ucis.join(',')}`,line.references.map(r=>({...r,position:{...r.position,recordedContext:root.context}})));
|
|
398
|
+
}
|
|
399
|
+
} else refs = resolveLabel(match[1], canonicalTarget(match[2]));
|
|
400
|
+
const canonicalLabel = refs.every(ref => ref.kind === 'move') ? canonicalMoveLabel(match[1], refs) : match[1];
|
|
401
|
+
if (canonicalLabel !== match[1]) notationCorrections.push(`${match[1]} → ${canonicalLabel} (derived from legal replay)`);
|
|
402
|
+
onReference?.({label:match[1],target:match[2],resolved:true,canonicalLabel});
|
|
403
|
+
resolved++;
|
|
404
|
+
for (const [index, ref] of refs.entries()) {
|
|
405
|
+
if (index) tokens.push({ text: " " });
|
|
406
|
+
tokens.push({ text: ref.label, reference: ref });
|
|
407
|
+
const ucis = ref.position.moves.map(move => move.uci);
|
|
408
|
+
if (!ref.position.recordedContext && ref.kind === "move" && ucis.length) for (const line of matchingLines(ref.position.start, ucis)) {
|
|
409
|
+
// Offer exactly the cited/witnessed prefix, never an unmentioned tail.
|
|
410
|
+
if (ucis.length > 1) {
|
|
411
|
+
const key = `${ref.position.start}:${ucis.join(",")}`, previous = usedLines.get(key);
|
|
412
|
+
usedLines.set(key, { ...line, start: ref.position.start, ucis,
|
|
413
|
+
evidenceIds: [...new Set([...(previous?.evidenceIds ?? []), ...line.evidenceIds])],
|
|
414
|
+
...((previous?.engineEvidence?.length || line.engineEvidence?.length) ? {
|
|
415
|
+
engineEvidence: [...(previous?.engineEvidence ?? []), ...(line.engineEvidence ?? [])].filter((item, index, all) =>
|
|
416
|
+
all.findIndex(other => other.evidenceId === item.evidenceId && other.rootMove === item.rootMove) === index),
|
|
417
|
+
} : {}),
|
|
418
|
+
authority: previous?.authority === "Bounded engine line" ? previous.authority : line.authority });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} catch (error) {
|
|
423
|
+
referenceGapCounts.unresolvedReferences++;
|
|
424
|
+
const gap = `${match[1]}: ${error instanceof Error ? error.message : "Unresolved reference"}`;
|
|
425
|
+
onReference?.({label:match[1],target:match[2],resolved:false,reason:gap});
|
|
426
|
+
gaps.push(gap); tokens.push({ text: match[1], gap });
|
|
427
|
+
}
|
|
428
|
+
end = match.index! + match[0].length;
|
|
429
|
+
}
|
|
430
|
+
plain(text.slice(end));
|
|
431
|
+
return { tokens, evidenceIds: cited.filter(id => evidence.some(record => record.id === id)), leadingVariationIndex: undefined as number | undefined };
|
|
432
|
+
});
|
|
433
|
+
const variations: Array<typeof lines[number] & {recordedContext?:ReasoningPositionContext}> = [...usedLines.values(), ...historicalVariations];
|
|
434
|
+
decorateSimpleVariations(blocks, variations);
|
|
435
|
+
const visibleVariations = new Set(blocks.flatMap(block => block.tokens.flatMap(token => token.variationIndex === undefined ? [] : [token.variationIndex])));
|
|
436
|
+
const visibleIndices = [...visibleVariations].sort((a, b) => a - b);
|
|
437
|
+
for (const block of blocks) for (const token of block.tokens) if (token.variationIndex !== undefined)
|
|
438
|
+
token.variationIndex = visibleIndices.indexOf(token.variationIndex);
|
|
439
|
+
for (const block of blocks) if (block.leadingVariationIndex !== undefined)
|
|
440
|
+
block.leadingVariationIndex = visibleIndices.indexOf(block.leadingVariationIndex);
|
|
441
|
+
return { blocks, evidence, gaps, notationCorrections, coverage: { resolved, total },
|
|
442
|
+
...(typeof internalWitness === 'object' ? { freshEnginePvAdmission: freshEnginePvAdmission.audit, referenceGapCounts } : {}),
|
|
443
|
+
variations: visibleIndices.map(index => variations[index]).map(line => ({
|
|
444
|
+
...line, position: line.recordedContext ? historicalRefs.get(`${reasoningContextHash(line.recordedContext)}:${line.ucis.join(',')}`)!.at(-1)!.position : replay(line.start, line.ucis).position,
|
|
445
|
+
references: line.recordedContext ? historicalRefs.get(`${reasoningContextHash(line.recordedContext)}:${line.ucis.join(',')}`)! : replay(line.start, line.ucis).position.moves.map((move, index) =>
|
|
446
|
+
resolve(move.san, `move:${line.start}:${line.ucis.slice(0, index + 1).join(",")}`)),
|
|
447
|
+
})) };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Formatting only: use already validated paths, including persisted answer views. */
|
|
451
|
+
export function decorateSimpleVariations(
|
|
452
|
+
blocks: { tokens: SimpleToken[]; leadingVariationIndex?: number }[],
|
|
453
|
+
variations: readonly { start: Phase; ucis: string[]; recordedContext?:ReasoningPositionContext }[],
|
|
454
|
+
) {
|
|
455
|
+
const sameRoot = (a?:ReasoningPositionContext,b?:ReasoningPositionContext) => (!a&&!b) || Boolean(a&&b&&a.historyStartFen===b.historyStartFen&&a.moveUci===b.moveUci&&a.historyUci.join(',')===b.historyUci.join(','));
|
|
456
|
+
for (const block of blocks) {
|
|
457
|
+
block.leadingVariationIndex = undefined;
|
|
458
|
+
for (const token of block.tokens) { delete token.variationIndex; delete token.variationStart; delete token.variationEnd; delete token.variationPlayFrom; delete token.variationMoveIndices; }
|
|
459
|
+
}
|
|
460
|
+
// Number-only text is notation, not intervening prose. Normalize only
|
|
461
|
+
// between consecutive legally resolved occurrences of the same path.
|
|
462
|
+
for (const block of blocks) for (let i = 1; i < block.tokens.length - 1; i++) {
|
|
463
|
+
const token = block.tokens[i], previous = block.tokens[i - 1].reference, next = block.tokens[i + 1].reference;
|
|
464
|
+
if (token.reference || !/^[\s,;]*(?:\d+\s*(?:\.{1,3}|…)\s*)+$/.test(token.text)
|
|
465
|
+
|| previous?.kind !== "move" || next?.kind !== "move" || !sameRoot(previous.position.recordedContext,next.position.recordedContext) || previous.position.start !== next.position.start
|
|
466
|
+
|| next.position.moves.length !== previous.position.moves.length + 1
|
|
467
|
+
|| !previous.position.moves.every((move, index) => move.uci === next.position.moves[index].uci)) continue;
|
|
468
|
+
token.text = " ";
|
|
469
|
+
}
|
|
470
|
+
// Attach only at an exact witnessed occurrence, never by matching prose SAN.
|
|
471
|
+
// Suppress shorter prefixes only within this paragraph, preserving earlier explanations.
|
|
472
|
+
for (const block of blocks) {
|
|
473
|
+
const mentioned = variations.map((line, variationIndex) => ({ line, variationIndex,
|
|
474
|
+
candidates: block.tokens.filter(token => token.reference?.kind === "move"
|
|
475
|
+
&& sameRoot(token.reference.position.recordedContext,line.recordedContext)
|
|
476
|
+
&& token.reference.position.start === line.start
|
|
477
|
+
&& token.reference.position.moves.length === line.ucis.length
|
|
478
|
+
&& token.reference.position.moves.every((move, i) => move.uci === line.ucis[i]))
|
|
479
|
+
})).filter(item => item.candidates.length);
|
|
480
|
+
for (const item of mentioned) {
|
|
481
|
+
if (mentioned.some(other => sameRoot(other.line.recordedContext,item.line.recordedContext) && other.line.start === item.line.start && other.line.ucis.length > item.line.ucis.length
|
|
482
|
+
&& item.line.ucis.every((uci, i) => other.line.ucis[i] === uci))) continue;
|
|
483
|
+
const target = item.candidates.find(token => token.reference?.kind === "move") ?? item.candidates[0];
|
|
484
|
+
if (target.reference?.kind === "move") {
|
|
485
|
+
const belongs = (token: SimpleToken) => token.reference?.kind === "move"
|
|
486
|
+
&& sameRoot(token.reference.position.recordedContext,item.line.recordedContext)
|
|
487
|
+
&& token.reference.position.start === item.line.start
|
|
488
|
+
&& token.reference.position.moves.length <= item.line.ucis.length
|
|
489
|
+
&& token.reference.position.moves.every((move, i) => move.uci === item.line.ucis[i]);
|
|
490
|
+
const end = block.tokens.indexOf(target);
|
|
491
|
+
const mentions = block.tokens.slice(0, end + 1).map((token, index) => ({ token, index })).filter(({ token }) => belongs(token));
|
|
492
|
+
// A navigation path is not a displayed combination. A lone endpoint
|
|
493
|
+
// (or repeated mention of it) keeps its move link, without replay.
|
|
494
|
+
if (new Set(mentions.map(({ token }) => token.reference!.position.moves.length)).size < 2) continue;
|
|
495
|
+
target.variationIndex = item.variationIndex;
|
|
496
|
+
const interrupted = mentions.length > 1 && block.tokens.slice(mentions[0].index, end)
|
|
497
|
+
.some(token => !belongs(token) && (token.reference || !/^[\s,;*]*$/.test(token.text)));
|
|
498
|
+
if (interrupted) {
|
|
499
|
+
// Replay only witnessed mentions; the board positions omitted prerequisites.
|
|
500
|
+
target.variationMoveIndices = [...new Set(mentions.map(({ token }) => token.reference!.position.moves.length - 1))].sort((a, b) => a - b);
|
|
501
|
+
// Only a single coherent line owns a paragraph-leading action. Mixed
|
|
502
|
+
// comparisons retain their per-line action at the exact endpoint.
|
|
503
|
+
const distinctMoves = new Set(mentions.map(({ token }) => token.reference!.position.moves.length));
|
|
504
|
+
if (distinctMoves.size > 1 && block.tokens.filter(token => token.reference?.kind === "move").every(belongs)
|
|
505
|
+
&& !/^\s*(?:#{1,3} |[-*] |\d+\. )/.test(block.tokens[0]?.text || "")) block.leadingVariationIndex = item.variationIndex;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
let first = block.tokens.indexOf(target), nextLength = target.reference.position.moves.length;
|
|
509
|
+
for (let cursor = first - 1; cursor >= 0; cursor--) {
|
|
510
|
+
const prior = block.tokens[cursor];
|
|
511
|
+
if (!prior.reference && /^[\s,;]*$/.test(prior.text)) continue;
|
|
512
|
+
const ref = prior.reference;
|
|
513
|
+
if (ref?.kind !== "move" || !sameRoot(ref.position.recordedContext,item.line.recordedContext) || ref.position.start !== item.line.start
|
|
514
|
+
|| ref.position.moves.length !== nextLength - 1
|
|
515
|
+
|| !ref.position.moves.every((move, i) => move.uci === item.line.ucis[i])) break;
|
|
516
|
+
first = cursor; nextLength--;
|
|
517
|
+
}
|
|
518
|
+
if (first !== block.tokens.indexOf(target)) {
|
|
519
|
+
const prefix = block.tokens[first - 1];
|
|
520
|
+
if (prefix && !prefix.reference) prefix.text = prefix.text.replace(/\b\d+\s*(?:\.{1,3}|…)\s*$/, "");
|
|
521
|
+
block.tokens[first].variationStart = true;
|
|
522
|
+
target.variationEnd = true;
|
|
523
|
+
target.variationPlayFrom = nextLength - 1;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function stableHash(value: unknown): string {
|
|
2
|
+
const text = stableJson(value);
|
|
3
|
+
let hash = 0x811c9dc5;
|
|
4
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
5
|
+
hash ^= text.charCodeAt(index);
|
|
6
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
7
|
+
}
|
|
8
|
+
return hash.toString(16).padStart(8, "0");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function stableJson(value: unknown): string {
|
|
12
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
13
|
+
if (value && typeof value === "object") {
|
|
14
|
+
const record = value as Record<string, unknown>;
|
|
15
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
|
|
16
|
+
}
|
|
17
|
+
return JSON.stringify(value);
|
|
18
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Per answering episode; only pre-dispatch validation errors enter this cache. */
|
|
2
|
+
export function createToolInputLoopGuard() {
|
|
3
|
+
const failures = new Map<string, {message:string; code:string; field?:string; expected?:string}>();
|
|
4
|
+
const stable = (value: unknown): unknown => Array.isArray(value) ? value.map(stable)
|
|
5
|
+
: value && typeof value === 'object' ? Object.fromEntries(Object.entries(value).sort(([a],[b])=>a.localeCompare(b)).map(([key,item])=>[key,stable(item)])) : value;
|
|
6
|
+
let repeatedFailures = 0;
|
|
7
|
+
return {
|
|
8
|
+
get repeatedFailures() { return repeatedFailures; },
|
|
9
|
+
validate(name: string, input: Record<string, unknown>, validate: () => void) {
|
|
10
|
+
// Runtime IDs are not tool arguments: invoke this before adding callId or
|
|
11
|
+
// contextHash. Keep every actual argument in the key, including paths.
|
|
12
|
+
const key = JSON.stringify([name, stable(input)]);
|
|
13
|
+
const previous = failures.get(key);
|
|
14
|
+
if (previous) {
|
|
15
|
+
repeatedFailures++;
|
|
16
|
+
throw Object.assign(new Error(`Repeated invalid request was not executed. ${previous.message} Correct the named field; do not repeat unchanged arguments. If unnecessary, summarize the acquired evidence.`), {
|
|
17
|
+
code:'repeated-invalid-tool-input', field:previous.field, expected:previous.expected,
|
|
18
|
+
originalCode:previous.code, repeatedInvalidInput:true,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
validate();
|
|
23
|
+
if (['test_line','test_game_line'].includes(name) && input.compareFinalMovesUci !== undefined) {
|
|
24
|
+
if (input.analyzeFinal !== true) throw Object.assign(new Error('compareFinalMovesUci requires analyzeFinal=true. Set analyzeFinal=true to compare the final position, or omit compareFinalMovesUci for replay only.'), {code:'invalid-final-comparison',field:'analyzeFinal',expected:'true when compareFinalMovesUci is supplied'});
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
// Only structured, deterministic validation failures. Never cache host,
|
|
28
|
+
// engine, provider, cancellation, evidence-contract or transient errors.
|
|
29
|
+
const detail = error as {message?:unknown;code?:unknown;field?:unknown;expected?:unknown};
|
|
30
|
+
if (typeof detail.code === 'string' && typeof detail.field === 'string') failures.set(key, {
|
|
31
|
+
message:typeof detail.message === 'string' ? detail.message.slice(0,600) : 'Invalid tool input.',
|
|
32
|
+
code:detail.code,field:detail.field,expected:typeof detail.expected === 'string' ? detail.expected : undefined,
|
|
33
|
+
});
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|