@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.
- package/LICENSE +235 -0
- package/LICENSING.md +26 -0
- package/NOTICE +5 -0
- package/NPM-PUBLISHING.md +24 -0
- package/README.md +141 -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 +72 -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,240 @@
|
|
|
1
|
+
// test/owner/score-comparison-notices.test.mjs
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
|
|
5
|
+
// src/chess-reasoning/matched-comparison.ts
|
|
6
|
+
import { Chess } from "chess.js";
|
|
7
|
+
function matchedRootComparison(result, fen2, requiredRoots = []) {
|
|
8
|
+
const lines = Array.isArray(result.lines) ? result.lines : [];
|
|
9
|
+
const rootMoves = lines.flatMap((line) => line && typeof line.rootMove === "string" ? [line.rootMove] : []);
|
|
10
|
+
const unmatched = (reason) => ({ status: "unmatched", actualDepth: null, rootMoves, reason });
|
|
11
|
+
if ((result.sourceCacheProvenance ?? result.cacheProvenance) !== "engine") return unmatched("Common engine search identity is unavailable.");
|
|
12
|
+
if (rootMoves.length !== lines.length) return unmatched("A compared root is malformed.");
|
|
13
|
+
if (lines.length < 2 || new Set(rootMoves).size !== rootMoves.length || requiredRoots.some((root) => !rootMoves.includes(root))) return unmatched("The search does not cover all compared moves.");
|
|
14
|
+
try {
|
|
15
|
+
const legal = new Set(new Chess(fen2).moves({ verbose: true }).map((move) => move.from + move.to + (move.promotion || "")));
|
|
16
|
+
if (rootMoves.some((root) => !legal.has(root))) return unmatched("A compared move is not legal at this root.");
|
|
17
|
+
} catch {
|
|
18
|
+
return unmatched("The comparison root is invalid.");
|
|
19
|
+
}
|
|
20
|
+
const depth = lines[0].depth, perspective = fen2.split(" ")[1] === "w" ? "white" : fen2.split(" ")[1] === "b" ? "black" : null;
|
|
21
|
+
if (!perspective || !Number.isSafeInteger(depth) || depth < 1 || lines.some((line) => line.depth !== depth)) return unmatched("The moves have no common achieved depth.");
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
if (line.scoreScope?.fen !== fen2 || line.scoreScope.perspective !== perspective || line.scoreScope.positiveMeans !== "advantage for perspective color") return unmatched("The score roots or perspectives differ.");
|
|
24
|
+
if (line.matePly == null ? !Number.isFinite(line.scoreCp) || Math.abs(line.scoreCp) >= 1e5 : !Number.isFinite(line.matePly)) return unmatched("A compared score is unavailable.");
|
|
25
|
+
if (line.matePly == null ? line.scoreScope.scoreCpUnit !== "centipawns" : line.scoreScope.matePlyUnit !== "signed mate-in-moves (legacy field name, not plies)") return unmatched("The score units are unknown.");
|
|
26
|
+
const origin = line.scoreProvenance;
|
|
27
|
+
if (origin && (origin.kind !== "root-search" || origin.fen !== fen2 || origin.originatingDepth !== depth)) return unmatched("A score is not a matching root-search measurement.");
|
|
28
|
+
if (origin?.engineBuild != null && origin.engineBuild !== result.engineBuild) return unmatched("The engine builds differ.");
|
|
29
|
+
if (origin?.command != null && result.command && origin.command !== result.command) return unmatched("The search commands differ.");
|
|
30
|
+
}
|
|
31
|
+
if (new Set(lines.flatMap((line) => typeof line.scoreProvenance?.command === "string" ? [line.scoreProvenance.command] : [])).size > 1) return unmatched("The search commands differ.");
|
|
32
|
+
return { status: "matched", actualDepth: depth, rootMoves, reason: "These root scores came from one engine search at a common achieved depth." };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/chess-reasoning/score-comparison-notices.ts
|
|
36
|
+
var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
37
|
+
var finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
38
|
+
var strings = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
|
|
39
|
+
function disagreementKind(observations) {
|
|
40
|
+
const mates = observations.filter((observation) => observation.score.unit === "mate-in-moves").map((observation) => observation.score.value);
|
|
41
|
+
if (mates.length !== observations.length) return mates.length ? "mate-status-disagreement" : "centipawn-variation";
|
|
42
|
+
if (mates.some((value) => value > 0) && mates.some((value) => value < 0)) return "opposite-mate-sign";
|
|
43
|
+
return "mate-distance-variation";
|
|
44
|
+
}
|
|
45
|
+
var sharedInterpretation = "Different search results for the same root move; equal requested depth does not make their search scope, actual depth, cache or search history equivalent. Preserve each scoped score. For relative move comparisons prefer alternatives covered together at the same actual depth in the identified comparison records; this supports a relative claim within that search, not independent confirmation or rejection of a mate result. Do not combine scores from separate searches into a move-loss estimate. This notice alone does not require another search.";
|
|
46
|
+
var interpretationFor = (kind) => {
|
|
47
|
+
const qualification = kind === "mate-status-disagreement" ? "Mate status differs: a centipawn score in another search does not refute its mating continuation, and mate distances cannot be converted to centipawns. " : kind === "opposite-mate-sign" ? "The mate signs oppose, indicating different reported mating sides; retain each search scope and inspect the underlying continuations. " : kind === "mate-distance-variation" ? "The searches report different mate distances; mate distances are not centipawns. " : "The searches report different centipawn evaluations. ";
|
|
48
|
+
return qualification + sharedInterpretation;
|
|
49
|
+
};
|
|
50
|
+
function originatingRootDepth(line, search, fen2) {
|
|
51
|
+
const provenance = object(line.scoreProvenance);
|
|
52
|
+
if (provenance) return provenance.kind === "root-search" && provenance.fen === fen2 && finite(provenance.originatingDepth) && Number.isInteger(provenance.originatingDepth) && provenance.originatingDepth > 0 && provenance.originatingDepth === line.depth ? provenance.originatingDepth : null;
|
|
53
|
+
const source = search.sourceCacheProvenance ?? search.cacheProvenance;
|
|
54
|
+
return source === "engine" && finite(line.depth) && Number.isInteger(line.depth) && line.depth > 0 ? line.depth : null;
|
|
55
|
+
}
|
|
56
|
+
function scoreComparisonNotices(records) {
|
|
57
|
+
const groups = /* @__PURE__ */ new Map();
|
|
58
|
+
for (const record of records) {
|
|
59
|
+
const wrapper = object(record.data);
|
|
60
|
+
const data = object(wrapper?.details) ?? wrapper;
|
|
61
|
+
const search = object(data?.searchScope);
|
|
62
|
+
if (!data || !search || typeof search.fen !== "string" || !Array.isArray(data.lines)) continue;
|
|
63
|
+
const fen2 = search.fen;
|
|
64
|
+
const perspective = fen2.split(" ")[1] === "w" ? "white" : fen2.split(" ")[1] === "b" ? "black" : null;
|
|
65
|
+
if (!perspective) continue;
|
|
66
|
+
const roots = strings(search.requestedRootMoves);
|
|
67
|
+
const lines = data.lines.map(object).filter((line) => !!line);
|
|
68
|
+
const scoped = (line) => {
|
|
69
|
+
const scope = object(line.scoreScope);
|
|
70
|
+
return scope?.fen === fen2 && scope.perspective === perspective && scope.positiveMeans === "advantage for perspective color";
|
|
71
|
+
};
|
|
72
|
+
const comparedRoots = roots.length ? roots : lines.map((line) => String(line.rootMove));
|
|
73
|
+
const matched = (!roots.length || record.coverage?.complete === true && roots.every((root) => record.coverage.covered.includes(root))) && matchedRootComparison({ ...data, command: search.command, cacheProvenance: search.cacheProvenance, sourceCacheProvenance: search.sourceCacheProvenance }, fen2, comparedRoots).status === "matched";
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
if (typeof line.rootMove !== "string" || !scoped(line)) continue;
|
|
76
|
+
const scope = object(line.scoreScope);
|
|
77
|
+
let score;
|
|
78
|
+
if (finite(line.matePly) && scope.matePlyUnit === "signed mate-in-moves (legacy field name, not plies)") {
|
|
79
|
+
score = { unit: "mate-in-moves", value: line.matePly };
|
|
80
|
+
} else if (line.matePly == null && finite(line.scoreCp) && Math.abs(line.scoreCp) < 1e5 && scope.scoreCpUnit === "centipawns") {
|
|
81
|
+
score = { unit: "centipawns", value: line.scoreCp };
|
|
82
|
+
} else continue;
|
|
83
|
+
const key = JSON.stringify([fen2, line.rootMove, perspective]);
|
|
84
|
+
let group = groups.get(key);
|
|
85
|
+
if (!group) {
|
|
86
|
+
group = { fen: fen2, rootMove: line.rootMove, perspective, observations: [], matched: /* @__PURE__ */ new Set() };
|
|
87
|
+
groups.set(key, group);
|
|
88
|
+
}
|
|
89
|
+
if (group.observations.some((observation) => observation.evidenceId === record.id)) continue;
|
|
90
|
+
group.observations.push({
|
|
91
|
+
evidenceId: record.id,
|
|
92
|
+
source: record.source,
|
|
93
|
+
score,
|
|
94
|
+
reportedDepth: finite(line.depth) ? line.depth : null,
|
|
95
|
+
actualDepth: originatingRootDepth(line, search, fen2),
|
|
96
|
+
requestedDepth: finite(search.requestedDepth) ? search.requestedDepth : null,
|
|
97
|
+
requestedMultiPv: finite(search.requestedMultiPv) ? search.requestedMultiPv : null,
|
|
98
|
+
rootScope: roots.length ? "restricted" : "unrestricted",
|
|
99
|
+
requestedRootMoves: roots,
|
|
100
|
+
cacheProvenance: typeof search.cacheProvenance === "string" ? search.cacheProvenance : "unreported",
|
|
101
|
+
sourceCacheProvenance: typeof search.sourceCacheProvenance === "string" ? search.sourceCacheProvenance : "unreported",
|
|
102
|
+
engine: typeof data.engine === "string" ? data.engine : null,
|
|
103
|
+
engineBuild: typeof data.engineBuild === "string" ? data.engineBuild : null,
|
|
104
|
+
...object(line.scoreProvenance) ? { scoreProvenance: line.scoreProvenance } : {}
|
|
105
|
+
});
|
|
106
|
+
if (matched && comparedRoots.includes(line.rootMove)) group.matched.add(record.id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return [...groups.values()].filter((group) => new Set(group.observations.map((observation) => JSON.stringify(observation.score))).size > 1).map((group) => {
|
|
110
|
+
const kind = disagreementKind(group.observations);
|
|
111
|
+
return {
|
|
112
|
+
fen: group.fen,
|
|
113
|
+
rootMove: group.rootMove,
|
|
114
|
+
perspective: group.perspective,
|
|
115
|
+
disagreementKind: kind,
|
|
116
|
+
observations: group.observations,
|
|
117
|
+
preferredWithinSearchComparisonEvidenceIds: [...group.matched],
|
|
118
|
+
interpretation: interpretationFor(kind)
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// test/owner/score-comparison-notices.test.mjs
|
|
124
|
+
var fen = "7k/8/8/8/8/8/8/KR6 w - - 0 1";
|
|
125
|
+
function evidence(id, cp, comparison = false) {
|
|
126
|
+
const roots = comparison ? ["b1b7", "b1b2"] : [];
|
|
127
|
+
const line = (rootMove, scoreCp) => ({ rootMove, scoreCp, matePly: null, depth: 18, scoreScope: { fen, perspective: "white", positiveMeans: "advantage for perspective color", scoreCpUnit: "centipawns", matePlyUnit: "signed mate-in-moves (legacy field name, not plies)" } });
|
|
128
|
+
return { id, source: `Stockfish/${comparison ? "compare_moves" : "explore-continuations"}`, coverage: { complete: comparison, covered: comparison ? roots : ["b1b7"], unresolved: comparison ? [] : ["non-enumerated alternatives"] }, data: { engine: "Stockfish", engineBuild: "18", searchScope: { fen, requestedDepth: 18, requestedMultiPv: comparison ? 2 : 3, requestedRootMoves: roots, cacheProvenance: comparison ? "engine" : "browser-completed-cache", sourceCacheProvenance: comparison ? "engine" : "widget-analysis-cache" }, lines: [line("b1b7", cp), ...comparison ? [line("b1b2", 1e3)] : []] } };
|
|
129
|
+
}
|
|
130
|
+
test("KE-REASON-016/021: qualify cached unrestricted versus matched comparison scores without changing evidence", () => {
|
|
131
|
+
const a = evidence("a", 1391), b = evidence("b", 1256, true), records = [a, b], saved = structuredClone(records);
|
|
132
|
+
const [notice] = scoreComparisonNotices(records);
|
|
133
|
+
assert.deepEqual(records, saved);
|
|
134
|
+
assert.equal(notice.rootMove, "b1b7");
|
|
135
|
+
assert.equal(notice.perspective, "white");
|
|
136
|
+
assert.deepEqual(notice.observations.map((o) => o.score), [{ unit: "centipawns", value: 1391 }, { unit: "centipawns", value: 1256 }]);
|
|
137
|
+
assert.deepEqual(notice.observations.map((o) => o.rootScope), ["unrestricted", "restricted"]);
|
|
138
|
+
assert.deepEqual(notice.observations.map((o) => [o.reportedDepth, o.actualDepth]), [[18, null], [18, 18]]);
|
|
139
|
+
assert.equal(notice.disagreementKind, "centipawn-variation");
|
|
140
|
+
assert.deepEqual(notice.preferredWithinSearchComparisonEvidenceIds, ["b"]);
|
|
141
|
+
assert.match(notice.interpretation, /does not require another search/);
|
|
142
|
+
});
|
|
143
|
+
test("KE-REASON-021: scope separates root positions and perspectives and excludes unscoped scores", () => {
|
|
144
|
+
const a = evidence("a", 1391), b = evidence("b", 1256, true);
|
|
145
|
+
b.data.searchScope.fen = fen.replace("0 1", "1 1");
|
|
146
|
+
assert.deepEqual(scoreComparisonNotices([a, b]), []);
|
|
147
|
+
b.data.searchScope.fen = fen;
|
|
148
|
+
b.data.lines[0].scoreScope.perspective = "black";
|
|
149
|
+
assert.deepEqual(scoreComparisonNotices([a, b]), []);
|
|
150
|
+
delete b.data.lines[0].scoreScope;
|
|
151
|
+
assert.deepEqual(scoreComparisonNotices([a, b]), []);
|
|
152
|
+
});
|
|
153
|
+
test("KE-REASON-021: historical nested results retain scope; duplicates and equal scores create no notice", () => {
|
|
154
|
+
const a = evidence("a", 1391), b = evidence("b", 1256, true);
|
|
155
|
+
b.data = { recordedPosition: { ply: 66 }, details: b.data };
|
|
156
|
+
assert.equal(scoreComparisonNotices([a, b, a]).length, 1);
|
|
157
|
+
assert.equal(scoreComparisonNotices([a, b, a])[0].observations.length, 2);
|
|
158
|
+
assert.deepEqual(scoreComparisonNotices([a, evidence("equal", 1391)]), []);
|
|
159
|
+
});
|
|
160
|
+
test("KE-REASON-021: mates remain typed distances and missing mate metadata cannot leak sentinel CP", () => {
|
|
161
|
+
const a = evidence("a", 1391), b = evidence("b", 1e5, true);
|
|
162
|
+
b.data.lines[0].matePly = 3;
|
|
163
|
+
const [notice] = scoreComparisonNotices([a, b]);
|
|
164
|
+
assert.deepEqual(notice.observations[1].score, { unit: "mate-in-moves", value: 3 });
|
|
165
|
+
assert.equal(notice.disagreementKind, "mate-status-disagreement");
|
|
166
|
+
assert.match(notice.interpretation, /centipawn score in another search does not refute its mating continuation/);
|
|
167
|
+
assert.match(notice.interpretation, /within that search, not independent confirmation or rejection/);
|
|
168
|
+
assert.deepEqual(notice.preferredWithinSearchComparisonEvidenceIds, ["b"]);
|
|
169
|
+
b.data.lines[0].matePly = null;
|
|
170
|
+
assert.deepEqual(scoreComparisonNotices([a, b]), []);
|
|
171
|
+
});
|
|
172
|
+
test("KE-REASON-016/021/026: distinguish opposing mate signs from distances while retaining reported scope and provenance", () => {
|
|
173
|
+
const a = evidence("a", 1e5), b = evidence("b", 1e5, true);
|
|
174
|
+
a.data.lines[0].matePly = 3;
|
|
175
|
+
b.data.lines[0].matePly = -5;
|
|
176
|
+
const reported = { kind: "root-search", fen, originatingDepth: 17, engineBuild: "18" };
|
|
177
|
+
b.data.lines[0].scoreProvenance = reported;
|
|
178
|
+
const saved = structuredClone([a, b]);
|
|
179
|
+
const [opposite] = scoreComparisonNotices([a, b]);
|
|
180
|
+
assert.equal(opposite.disagreementKind, "opposite-mate-sign");
|
|
181
|
+
assert.match(opposite.interpretation, /mate signs oppose/);
|
|
182
|
+
assert.deepEqual(opposite.observations[1].scoreProvenance, reported);
|
|
183
|
+
assert.equal("scoreProvenance" in opposite.observations[0], false);
|
|
184
|
+
assert.deepEqual(opposite.observations.map((o) => [o.reportedDepth, o.actualDepth, o.requestedDepth, o.rootScope, o.sourceCacheProvenance]), [[18, null, 18, "unrestricted", "widget-analysis-cache"], [18, null, 18, "restricted", "engine"]]);
|
|
185
|
+
assert.deepEqual([a, b], saved);
|
|
186
|
+
b.data.lines[0].matePly = 5;
|
|
187
|
+
const [distance] = scoreComparisonNotices([a, b]);
|
|
188
|
+
assert.equal(distance.disagreementKind, "mate-distance-variation");
|
|
189
|
+
assert.match(distance.interpretation, /different mate distances/);
|
|
190
|
+
assert.match(distance.interpretation, /does not require another search/);
|
|
191
|
+
});
|
|
192
|
+
test("KE-REASON-021: incomplete or mixed-depth comparisons do not receive comparison preference", () => {
|
|
193
|
+
const a = evidence("a", 1391), b = evidence("b", 1256, true);
|
|
194
|
+
b.data.lines[1].depth = 17;
|
|
195
|
+
assert.deepEqual(scoreComparisonNotices([a, b])[0].preferredWithinSearchComparisonEvidenceIds, []);
|
|
196
|
+
b.data.lines[1].depth = 18;
|
|
197
|
+
b.coverage.complete = false;
|
|
198
|
+
assert.deepEqual(scoreComparisonNotices([a, b])[0].preferredWithinSearchComparisonEvidenceIds, []);
|
|
199
|
+
});
|
|
200
|
+
test("KE-REASON-026: malformed candidates and inconsistent original commands cannot certify comparison preference", () => {
|
|
201
|
+
const a = evidence("a", 1391), b = evidence("b", 1256, true);
|
|
202
|
+
b.data.searchScope.command = "go depth 18";
|
|
203
|
+
b.data.lines.forEach((line) => line.scoreProvenance = { kind: "root-search", fen, originatingDepth: 18, command: "go depth 26" });
|
|
204
|
+
assert.deepEqual(scoreComparisonNotices([a, b])[0].preferredWithinSearchComparisonEvidenceIds, []);
|
|
205
|
+
b.data.lines.push(null);
|
|
206
|
+
assert.deepEqual(scoreComparisonNotices([a, b])[0].preferredWithinSearchComparisonEvidenceIds, []);
|
|
207
|
+
});
|
|
208
|
+
test("KE-REASON-016/021/026: cached reported depths and subsequent-position scores cannot certify a matched root comparison", () => {
|
|
209
|
+
const a = evidence("a", 1391), b = evidence("b", 1256, true);
|
|
210
|
+
b.data.searchScope.cacheProvenance = "browser-completed-cache";
|
|
211
|
+
b.data.searchScope.sourceCacheProvenance = "widget-analysis-cache";
|
|
212
|
+
assert.deepEqual(scoreComparisonNotices([a, b])[0].preferredWithinSearchComparisonEvidenceIds, []);
|
|
213
|
+
assert.deepEqual(scoreComparisonNotices([a, b])[0].observations[1].actualDepth, null);
|
|
214
|
+
const kinds = [
|
|
215
|
+
{ kind: "restored-fallback", originatingDepth: null, reportedDepth: 18, fallbackDepth: 18 },
|
|
216
|
+
{ kind: "legacy-unknown", originatingDepth: null, reportedDepth: 18 },
|
|
217
|
+
{ kind: "subsequent-position", fen: fen.replace("0 1", "1 1"), originatingDepth: 18, reportedDepth: 18 },
|
|
218
|
+
{ kind: "root-search", fen: fen.replace("0 1", "1 1"), originatingDepth: 18, engineBuild: "18" },
|
|
219
|
+
{ kind: "root-search", fen, originatingDepth: 0, engineBuild: "18" }
|
|
220
|
+
];
|
|
221
|
+
for (const provenance of kinds) {
|
|
222
|
+
b.data.lines[0].scoreProvenance = provenance;
|
|
223
|
+
b.data.lines[1].scoreProvenance = provenance;
|
|
224
|
+
const [notice] = scoreComparisonNotices([a, b]);
|
|
225
|
+
assert.equal(notice.observations[1].reportedDepth, 18);
|
|
226
|
+
assert.equal(notice.observations[1].actualDepth, null);
|
|
227
|
+
assert.deepEqual(notice.preferredWithinSearchComparisonEvidenceIds, []);
|
|
228
|
+
}
|
|
229
|
+
b.data.lines[0].scoreProvenance = { kind: "root-search", fen, originatingDepth: 17, engineBuild: "18" };
|
|
230
|
+
b.data.lines[1].scoreProvenance = { kind: "root-search", fen, originatingDepth: 18, engineBuild: "18" };
|
|
231
|
+
assert.deepEqual(scoreComparisonNotices([a, b])[0].preferredWithinSearchComparisonEvidenceIds, []);
|
|
232
|
+
b.data.lines[1].scoreProvenance = { kind: "root-search", fen, originatingDepth: 17, engineBuild: "18" };
|
|
233
|
+
assert.deepEqual(scoreComparisonNotices([a, b])[0].preferredWithinSearchComparisonEvidenceIds, [], "candidate provenance does not establish shared search identity");
|
|
234
|
+
b.data.searchScope.sourceCacheProvenance = "engine";
|
|
235
|
+
b.data.lines.forEach((line) => line.depth = 17);
|
|
236
|
+
const [verified] = scoreComparisonNotices([a, b]);
|
|
237
|
+
assert.deepEqual(verified.preferredWithinSearchComparisonEvidenceIds, ["b"]);
|
|
238
|
+
assert.deepEqual(verified.observations[1].scoreProvenance, b.data.lines[0].scoreProvenance);
|
|
239
|
+
assert.equal(verified.observations[1].actualDepth, 17);
|
|
240
|
+
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// test/owner/simple-engine-presentation.test.mjs
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
import { Chess as Chess2 } from "chess.js";
|
|
5
|
+
|
|
6
|
+
// src/chess-reasoning/mechanism-presentation.ts
|
|
7
|
+
var object = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8
|
+
var COLUMNS = {
|
|
9
|
+
mover: ["pieceId", "pieceIdAfter", "fromSquare", "toSquare", "color", "pieceTypeBefore", "pieceTypeAfter", "attackersBefore", "attackersAfter"],
|
|
10
|
+
unmoved: ["pieceId", "square", "pieceType", "attackersBefore", "attackersAfter"],
|
|
11
|
+
rays: ["pieceId", "fromSquare", "color", "pieceType", "direction", "gainedSquares", "lostSquares"],
|
|
12
|
+
control: ["square", "color", "attackersBefore", "attackersAfter"]
|
|
13
|
+
};
|
|
14
|
+
function exactKeys(value, columns) {
|
|
15
|
+
return object(value) && Object.keys(value).length === columns.length && columns.every((column) => Object.hasOwn(value, column));
|
|
16
|
+
}
|
|
17
|
+
function encodeRows(value, columns) {
|
|
18
|
+
return columns.map((column) => value[column]);
|
|
19
|
+
}
|
|
20
|
+
function compactMechanism(value, targetSquares) {
|
|
21
|
+
if (!object(value) || !Array.isArray(value.controlDelta)) return value;
|
|
22
|
+
const targets = [...new Set(targetSquares)].filter((s) => /^[a-h][1-8]$/.test(s));
|
|
23
|
+
const controlDelta = value.controlDelta.filter((d) => object(d) && targets.includes(String(d.square)));
|
|
24
|
+
const rays = Array.isArray(value.stationarySliderRays) ? value.stationarySliderRays.map((ray) => {
|
|
25
|
+
if (!object(ray)) return ray;
|
|
26
|
+
const { beforeSquares: _before, afterSquares: _after, ...changes } = ray;
|
|
27
|
+
void _before;
|
|
28
|
+
void _after;
|
|
29
|
+
return changes;
|
|
30
|
+
}) : value.stationarySliderRays;
|
|
31
|
+
const projected = {
|
|
32
|
+
...value,
|
|
33
|
+
stationarySliderRays: rays,
|
|
34
|
+
controlDelta,
|
|
35
|
+
presentation: {
|
|
36
|
+
controlSquares: targets,
|
|
37
|
+
omittedControlDeltaCount: value.controlDelta.length - controlDelta.length,
|
|
38
|
+
scope: "All changed stationary rays and mover/unmoved attack measurements retained. Control deltas shown only for controlSquares; an omitted square is not checked-negative in this view. Full ray extents and all control deltas are retrievable."
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
if (!exactKeys(value, ["mover", "attackedUnmovedFriendlyPieces", "stationarySliderRays", "controlDelta", "checksRun", "scope"]) || !exactKeys(projected.mover, COLUMNS.mover) || !Array.isArray(projected.attackedUnmovedFriendlyPieces) || !projected.attackedUnmovedFriendlyPieces.every((item) => exactKeys(item, COLUMNS.unmoved)) || !Array.isArray(rays) || !rays.every((item) => exactKeys(item, COLUMNS.rays)) || !controlDelta.every((item) => exactKeys(item, COLUMNS.control))) return projected;
|
|
42
|
+
return {
|
|
43
|
+
encoding: "move-mechanism/1",
|
|
44
|
+
columns: COLUMNS,
|
|
45
|
+
rows: {
|
|
46
|
+
mover: encodeRows(projected.mover, COLUMNS.mover),
|
|
47
|
+
unmoved: projected.attackedUnmovedFriendlyPieces.map((item) => encodeRows(item, COLUMNS.unmoved)),
|
|
48
|
+
rays: rays.map((item) => encodeRows(item, COLUMNS.rays)),
|
|
49
|
+
control: controlDelta.map((item) => encodeRows(item, COLUMNS.control))
|
|
50
|
+
},
|
|
51
|
+
checksRun: projected.checksRun,
|
|
52
|
+
scope: projected.scope,
|
|
53
|
+
presentation: projected.presentation
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function presentMoveMechanisms(moves, inspectPlies = [], inspectSquares = []) {
|
|
57
|
+
return moves.flatMap((fact, index) => {
|
|
58
|
+
if (!object(fact) || !object(fact.mechanism)) return [];
|
|
59
|
+
if (index >= 2 && !inspectPlies.includes(index + 1) && !inspectSquares.includes(String(fact.to))) return [];
|
|
60
|
+
const targets = [...inspectSquares, ...moves.slice(index, index + 3).flatMap((m) => object(m) && typeof m.to === "string" ? [m.to] : [])];
|
|
61
|
+
return [{ ply: index + 1, uci: fact.uci, san: fact.san, mechanism: compactMechanism(fact.mechanism, targets) }];
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/chess-reasoning/simple-engine-presentation.ts
|
|
66
|
+
import { Chess } from "chess.js";
|
|
67
|
+
var object2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
68
|
+
function projectEnginePresentation(value, fullGeometry = false) {
|
|
69
|
+
const visit = (v) => {
|
|
70
|
+
if (Array.isArray(v)) return v.map(visit);
|
|
71
|
+
if (!object2(v)) return v;
|
|
72
|
+
if (typeof v.id !== "string" || !object2(v.data)) {
|
|
73
|
+
return Object.fromEntries(Object.entries(v).map(([k, child]) => [k, visit(child)]));
|
|
74
|
+
}
|
|
75
|
+
const nested = object2(v.data.details) && (Array.isArray(v.data.details.lines) || Array.isArray(v.data.details.moveFacts));
|
|
76
|
+
const d = nested ? v.data.details : v.data;
|
|
77
|
+
if (Object.hasOwn(d, "modelPresentation")) return v;
|
|
78
|
+
if (object2(d.selectedMoveMechanism) && Array.isArray(d.selectedMoveMechanism.controlDelta) && !fullGeometry) {
|
|
79
|
+
const squares = d.selectedMoveMechanism.controlDelta.flatMap((delta) => object2(delta) && typeof delta.square === "string" ? [delta.square] : []);
|
|
80
|
+
const projected2 = { ...d, selectedMoveMechanism: compactMechanism(d.selectedMoveMechanism, squares) };
|
|
81
|
+
return { ...v, data: nested ? { ...v.data, details: projected2 } : projected2 };
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(d.moveFacts) && !fullGeometry) {
|
|
84
|
+
const inspection = object2(d.mechanismInspection) ? d.mechanismInspection : {};
|
|
85
|
+
const { moveFacts, ...rest } = d;
|
|
86
|
+
const projected2 = {
|
|
87
|
+
...rest,
|
|
88
|
+
mechanismFacts: presentMoveMechanisms(
|
|
89
|
+
moveFacts,
|
|
90
|
+
Array.isArray(inspection.inspectPlies) ? inspection.inspectPlies : [],
|
|
91
|
+
Array.isArray(inspection.inspectSquares) ? inspection.inspectSquares : []
|
|
92
|
+
),
|
|
93
|
+
modelPresentation: {
|
|
94
|
+
mechanismThroughPly: 2,
|
|
95
|
+
explicitInspection: inspection,
|
|
96
|
+
retrieveFullEvidenceId: v.id,
|
|
97
|
+
geometryNotice: "Mechanisms shown for first two plies, explicit inspectPlies and moves landing on inspectSquares. Other plies omitted, not absent; retrieve full evidence for all replayed move facts."
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
return { ...v, data: nested ? { ...v.data, details: projected2 } : projected2 };
|
|
101
|
+
}
|
|
102
|
+
if (!Array.isArray(d.lines)) return v;
|
|
103
|
+
const rootFen = object2(d.searchScope) && typeof d.searchScope.fen === "string" ? d.searchScope.fen : void 0;
|
|
104
|
+
let omittedGeometry = 0, omittedRaw = false, referencedFen = false;
|
|
105
|
+
const lines = d.lines.map((line) => {
|
|
106
|
+
if (!object2(line) || !Array.isArray(line.pv)) return line;
|
|
107
|
+
const next = { ...line };
|
|
108
|
+
if (!Object.hasOwn(line, "rawPvSameAsPv") && Array.isArray(line.rawPv) && JSON.stringify(line.rawPv) === JSON.stringify(line.pv)) {
|
|
109
|
+
delete next.rawPv;
|
|
110
|
+
next.rawPvSameAsPv = true;
|
|
111
|
+
omittedRaw = true;
|
|
112
|
+
}
|
|
113
|
+
if (rootFen && object2(line.pvCompletion) && Array.isArray(line.pvCompletion.segments)) {
|
|
114
|
+
const fens = [];
|
|
115
|
+
try {
|
|
116
|
+
const chess = new Chess(rootFen);
|
|
117
|
+
fens.push(chess.fen());
|
|
118
|
+
for (const move of line.pv) {
|
|
119
|
+
if (typeof move !== "string" || !/^[a-h][1-8][a-h][1-8][qrbn]?$/.test(move)) break;
|
|
120
|
+
chess.move({ from: move.slice(0, 2), to: move.slice(2, 4), promotion: move.slice(4) || void 0 });
|
|
121
|
+
fens.push(chess.fen());
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
}
|
|
125
|
+
next.pvCompletion = { ...line.pvCompletion, segments: line.pvCompletion.segments.map((segment) => {
|
|
126
|
+
if (!object2(segment) || !Number.isInteger(segment.startPly) || segment.startPly < 0 || typeof segment.fen !== "string" || fens[segment.startPly] !== segment.fen || Object.hasOwn(segment, "fenFromLinePvPly")) return segment;
|
|
127
|
+
const { fen: _fen, ...rest } = segment;
|
|
128
|
+
void _fen;
|
|
129
|
+
referencedFen = true;
|
|
130
|
+
return { ...rest, fenFromLinePvPly: segment.startPly };
|
|
131
|
+
}) };
|
|
132
|
+
}
|
|
133
|
+
return next;
|
|
134
|
+
});
|
|
135
|
+
const continuations = !fullGeometry && Array.isArray(d.continuationMoveFacts) ? d.continuationMoveFacts.map((c) => {
|
|
136
|
+
if (!object2(c) || !Array.isArray(c.moves)) return c;
|
|
137
|
+
const compact = presentMoveMechanisms(c.moves);
|
|
138
|
+
return { ...c, moves: c.moves.map((m, i) => {
|
|
139
|
+
if (!object2(m)) return m;
|
|
140
|
+
const next = { ...m };
|
|
141
|
+
if (i >= 2 && Object.hasOwn(next, "captureGeometry")) {
|
|
142
|
+
delete next.captureGeometry;
|
|
143
|
+
omittedGeometry++;
|
|
144
|
+
}
|
|
145
|
+
if (Object.hasOwn(next, "mechanism")) {
|
|
146
|
+
const delivered = compact.find((f) => object2(f) && f.ply === i + 1);
|
|
147
|
+
if (object2(delivered)) next.mechanism = delivered.mechanism;
|
|
148
|
+
else {
|
|
149
|
+
delete next.mechanism;
|
|
150
|
+
omittedGeometry++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return next;
|
|
154
|
+
}) };
|
|
155
|
+
}) : d.continuationMoveFacts;
|
|
156
|
+
const roots = !fullGeometry && Array.isArray(d.rootMoveFacts) ? d.rootMoveFacts.map((f) => !object2(f) || !f.mechanism ? f : { ...f, mechanism: compactMechanism(f.mechanism, [String(f.to)]) }) : d.rootMoveFacts;
|
|
157
|
+
if (!omittedGeometry && !omittedRaw && !referencedFen && !Array.isArray(d.rootMoveFacts)) return v;
|
|
158
|
+
const projected = {
|
|
159
|
+
...d,
|
|
160
|
+
lines,
|
|
161
|
+
...roots !== void 0 ? { rootMoveFacts: roots } : {},
|
|
162
|
+
...d.continuationMoveFacts !== void 0 ? { continuationMoveFacts: continuations } : {},
|
|
163
|
+
modelPresentation: {
|
|
164
|
+
...omittedGeometry ? {
|
|
165
|
+
captureGeometryThroughPly: 2,
|
|
166
|
+
omittedCaptureGeometry: omittedGeometry,
|
|
167
|
+
retrieveFullEvidenceId: v.id,
|
|
168
|
+
geometryNotice: "Deeper capture and mechanism geometry omitted, not absent. Retrieve this evidence ID for full geometry; basic move and capture facts remain complete."
|
|
169
|
+
} : {},
|
|
170
|
+
...referencedFen ? { segmentFenReference: "fenFromLinePvPly=N is the exact position after N moves of this line.pv, legally replayed from this engine result\u2019s searchScope.fen. Segment scores, depth and search provenance still apply only to that segment." } : {}
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
return { ...v, data: nested ? { ...v.data, details: projected } : projected };
|
|
174
|
+
};
|
|
175
|
+
return visit(value);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// test/owner/simple-engine-presentation.test.mjs
|
|
179
|
+
var pv = ["e2e4", "d7d5", "e4d5", "d8d5"];
|
|
180
|
+
function fixture() {
|
|
181
|
+
const b = new Chess2(), fens = [b.fen()];
|
|
182
|
+
for (const m of pv) {
|
|
183
|
+
b.move({ from: m.slice(0, 2), to: m.slice(2, 4) });
|
|
184
|
+
fens.push(b.fen());
|
|
185
|
+
}
|
|
186
|
+
const segments = [0, 2].map((startPly) => ({ fen: fens[startPly], startPly, depth: 18, pv: pv.slice(startPly), scoreCp: 15, matePly: null, command: "go depth 18", engine: "Stockfish", engineBuild: "18", cacheProvenance: "engine" }));
|
|
187
|
+
const moves = pv.map((uci, i) => ({ uci, capture: i > 1 ? { pieceType: "p" } : null, givesCheck: false, captureGeometry: { test: i } }));
|
|
188
|
+
return { evidence: [{ id: "e1", source: "Stockfish/compare_moves", data: { searchScope: { fen: fens[0] }, lines: [{ pv, rawPv: [...pv], depth: 18, pvCompletion: { conditional: true, status: "complete", segments } }], continuationMoveFacts: [{ rootMove: pv[0], moves }] } }] };
|
|
189
|
+
}
|
|
190
|
+
test("KE-REASON-016/021: default view removes only deep geometry and exact PV duplicates while preserving complete internal records", () => {
|
|
191
|
+
const raw = fixture(), copy = structuredClone(raw), v = projectEnginePresentation(raw), d = v.evidence[0].data;
|
|
192
|
+
assert.deepEqual(raw, copy);
|
|
193
|
+
assert.deepEqual(d.continuationMoveFacts[0].moves.map((m) => !!m.captureGeometry), [true, true, false, false]);
|
|
194
|
+
assert.deepEqual(d.continuationMoveFacts[0].moves.map((m) => m.capture), copy.evidence[0].data.continuationMoveFacts[0].moves.map((m) => m.capture));
|
|
195
|
+
assert.equal(d.lines[0].rawPv, void 0);
|
|
196
|
+
assert.deepEqual(d.lines[0].pv, pv);
|
|
197
|
+
assert.equal(d.modelPresentation.retrieveFullEvidenceId, "e1");
|
|
198
|
+
const segments = d.lines[0].pvCompletion.segments;
|
|
199
|
+
assert.deepEqual(segments.map((s) => s.fenFromLinePvPly), [0, 2]);
|
|
200
|
+
for (let i = 0; i < segments.length; i++) {
|
|
201
|
+
const { fen, ...original } = copy.evidence[0].data.lines[0].pvCompletion.segments[i];
|
|
202
|
+
const { fenFromLinePvPly, ...actual } = segments[i];
|
|
203
|
+
void fen;
|
|
204
|
+
void fenFromLinePvPly;
|
|
205
|
+
assert.deepEqual(actual, original);
|
|
206
|
+
}
|
|
207
|
+
const full = projectEnginePresentation(raw, true).evidence[0].data;
|
|
208
|
+
assert.deepEqual(full.continuationMoveFacts, copy.evidence[0].data.continuationMoveFacts);
|
|
209
|
+
assert.equal(full.modelPresentation.omittedCaptureGeometry, void 0);
|
|
210
|
+
});
|
|
211
|
+
test("KE-REASON-021: unequal original PV and mismatched segment FEN remain explicit", () => {
|
|
212
|
+
const raw = fixture(), line = raw.evidence[0].data.lines[0];
|
|
213
|
+
line.rawPv = pv.slice(0, 2);
|
|
214
|
+
line.pvCompletion.segments[1].fen = "unverified";
|
|
215
|
+
const d = projectEnginePresentation(raw).evidence[0].data;
|
|
216
|
+
assert.deepEqual(d.lines[0].rawPv, pv.slice(0, 2));
|
|
217
|
+
assert.equal(d.lines[0].pvCompletion.segments[1].fen, "unverified");
|
|
218
|
+
assert.equal(d.lines[0].pvCompletion.segments[1].fenFromLinePvPly, void 0);
|
|
219
|
+
});
|
|
220
|
+
test("KE-REASON-016: unrelated geometry and unknown record shapes stay intact", () => {
|
|
221
|
+
const raw = { id: "other", data: { captureGeometry: { detail: "preserve" }, continuationMoveFacts: [{ moves: [1, 2, { captureGeometry: { detail: "preserve" } }] }] } };
|
|
222
|
+
assert.deepEqual(projectEnginePresentation(raw), raw);
|
|
223
|
+
});
|
|
224
|
+
test("KE-REASON-016: recorded-position tools project nested details without losing historical scope", () => {
|
|
225
|
+
const raw = fixture(), data = raw.evidence[0].data;
|
|
226
|
+
raw.evidence[0].data = { recordedPosition: { ply: 10, fen: data.searchScope.fen }, details: data };
|
|
227
|
+
const v = projectEnginePresentation(raw).evidence[0];
|
|
228
|
+
assert.deepEqual(v.data.recordedPosition, raw.evidence[0].data.recordedPosition);
|
|
229
|
+
assert.equal(v.data.details.lines[0].rawPv, void 0);
|
|
230
|
+
assert.equal(v.data.details.continuationMoveFacts[0].moves[2].captureGeometry, void 0);
|
|
231
|
+
assert.equal(v.data.details.modelPresentation.retrieveFullEvidenceId, "e1");
|
|
232
|
+
assert.deepEqual(projectEnginePresentation(raw, true).evidence[0].data.details.continuationMoveFacts, data.continuationMoveFacts);
|
|
233
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// test/owner/tool-input-loop-guard.test.mjs
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
|
|
5
|
+
// src/chess-reasoning/tool-input-loop-guard.ts
|
|
6
|
+
function createToolInputLoopGuard() {
|
|
7
|
+
const failures = /* @__PURE__ */ new Map();
|
|
8
|
+
const stable = (value) => Array.isArray(value) ? value.map(stable) : value && typeof value === "object" ? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, stable(item)])) : value;
|
|
9
|
+
let repeatedFailures = 0;
|
|
10
|
+
return {
|
|
11
|
+
get repeatedFailures() {
|
|
12
|
+
return repeatedFailures;
|
|
13
|
+
},
|
|
14
|
+
validate(name, input, validate) {
|
|
15
|
+
const key = JSON.stringify([name, stable(input)]);
|
|
16
|
+
const previous = failures.get(key);
|
|
17
|
+
if (previous) {
|
|
18
|
+
repeatedFailures++;
|
|
19
|
+
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.`), {
|
|
20
|
+
code: "repeated-invalid-tool-input",
|
|
21
|
+
field: previous.field,
|
|
22
|
+
expected: previous.expected,
|
|
23
|
+
originalCode: previous.code,
|
|
24
|
+
repeatedInvalidInput: true
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
validate();
|
|
29
|
+
if (["test_line", "test_game_line"].includes(name) && input.compareFinalMovesUci !== void 0) {
|
|
30
|
+
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" });
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
const detail = error;
|
|
34
|
+
if (typeof detail.code === "string" && typeof detail.field === "string") failures.set(key, {
|
|
35
|
+
message: typeof detail.message === "string" ? detail.message.slice(0, 600) : "Invalid tool input.",
|
|
36
|
+
code: detail.code,
|
|
37
|
+
field: detail.field,
|
|
38
|
+
expected: typeof detail.expected === "string" ? detail.expected : void 0
|
|
39
|
+
});
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// test/owner/tool-input-loop-guard.test.mjs
|
|
47
|
+
test("KE-REASON-026: Nc8 repeated final comparison is blocked before dispatch; corrected request stays eligible", () => {
|
|
48
|
+
const guard = createToolInputLoopGuard();
|
|
49
|
+
let validations = 0;
|
|
50
|
+
const input = { phase: "after", movesUci: ["h2h3", "e8e4", "f1e1", "f8e8", "a2a4", "c8b6", "a4a5", "b6c4"], inspectSquares: ["c4", "e4", "d3", "f5"], inspectPlies: [6, 8], analyzeFinal: false, compareFinalMovesUci: ["d3c4"], minimumPvPlies: 10, depth: 18 };
|
|
51
|
+
assert.throws(() => guard.validate("test_line", input, () => validations++), /requires analyzeFinal=true/);
|
|
52
|
+
for (let n = 0; n < 3; n++) assert.throws(() => guard.validate("test_line", { ...input }, () => validations++), (e) => e.code === "repeated-invalid-tool-input" && e.repeatedInvalidInput === true);
|
|
53
|
+
assert.equal(validations, 1);
|
|
54
|
+
assert.equal(guard.repeatedFailures, 3);
|
|
55
|
+
guard.validate("test_line", { ...input, analyzeFinal: true }, () => validations++);
|
|
56
|
+
guard.validate("test_line", { phase: "after", movesUci: input.movesUci, analyzeFinal: false }, () => validations++);
|
|
57
|
+
assert.equal(validations, 3);
|
|
58
|
+
});
|
|
59
|
+
test("KE-REASON-026: canonical keys ignore property order but preserve array order, names and episode boundaries", () => {
|
|
60
|
+
const guard = createToolInputLoopGuard();
|
|
61
|
+
const invalid = () => {
|
|
62
|
+
throw Object.assign(new Error("array of 1..6 items"), { code: "invalid-tool-array", field: "compareFinalMovesUci", expected: "array of 1..6 items" });
|
|
63
|
+
};
|
|
64
|
+
assert.throws(() => guard.validate("test_line", { analyzeFinal: false, compareFinalMovesUci: [] }, invalid));
|
|
65
|
+
assert.throws(() => guard.validate("test_line", { compareFinalMovesUci: [], analyzeFinal: false }, invalid), (e) => e.code === "repeated-invalid-tool-input");
|
|
66
|
+
assert.throws(() => createToolInputLoopGuard().validate("test_line", { compareFinalMovesUci: [], analyzeFinal: false }, invalid), (e) => e.code === "invalid-tool-array");
|
|
67
|
+
assert.throws(() => guard.validate("test_game_line", { compareFinalMovesUci: [], analyzeFinal: false }, invalid), (e) => e.code === "invalid-tool-array");
|
|
68
|
+
});
|
|
69
|
+
test("KE-REASON-026: unstructured and transient failures are never remembered", () => {
|
|
70
|
+
const guard = createToolInputLoopGuard();
|
|
71
|
+
let attempts = 0;
|
|
72
|
+
for (let n = 0; n < 2; n++) assert.throws(() => guard.validate("analyze_position", {}, () => {
|
|
73
|
+
attempts++;
|
|
74
|
+
throw Error("network failed");
|
|
75
|
+
}), /network failed/);
|
|
76
|
+
assert.equal(attempts, 2);
|
|
77
|
+
assert.equal(guard.repeatedFailures, 0);
|
|
78
|
+
});
|