@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,105 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { Chess } from 'chess.js';
|
|
4
|
+
import { moveMechanismFacts } from '../../src/chess-reasoning/move-mechanism-facts.ts';
|
|
5
|
+
import { applyIdentityMove, createIdentityState } from '../../src/chess-reasoning/piece-identities.ts';
|
|
6
|
+
|
|
7
|
+
function facts(fen, uci, suppliedIdentities = true) {
|
|
8
|
+
const before = new Chess(fen);
|
|
9
|
+
const beforeIds = createIdentityState(fen);
|
|
10
|
+
const { state: afterIds } = applyIdentityMove(beforeIds, {
|
|
11
|
+
from: uci.slice(0, 2), to: uci.slice(2, 4), promotion: uci.slice(4) || undefined,
|
|
12
|
+
});
|
|
13
|
+
const after = new Chess(afterIds.fen);
|
|
14
|
+
return moveMechanismFacts(before, after, uci.slice(0, 2), uci.slice(2, 4),
|
|
15
|
+
...(suppliedIdentities ? [beforeIds, afterIds] : []));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test('KE-REASON-016/021: Rb2+ mover is unattacked while the unmoved rook on d1 is attacked from a4', () => {
|
|
19
|
+
const observed = facts('1k1r3r/2p5/p2p1p2/2P2B2/b3PB1p/5P2/3R2PP/2KR4 w - - 1 25', 'd2b2');
|
|
20
|
+
assert.equal(observed.mover.pieceId, 'white-rook:d2:0');
|
|
21
|
+
assert.deepEqual(observed.mover.attackersBefore, []);
|
|
22
|
+
assert.deepEqual(observed.mover.attackersAfter, []);
|
|
23
|
+
assert.deepEqual(observed.attackedUnmovedFriendlyPieces.find((piece) => piece.square === 'd1'), {
|
|
24
|
+
pieceId: 'white-rook:d1:0', square: 'd1', pieceType: 'r',
|
|
25
|
+
attackersBefore: ['a4'], attackersAfter: ['a4'],
|
|
26
|
+
});
|
|
27
|
+
assert.ok(observed.checksRun.includes('unmoved-friendly-piece-geometric-attackers-before-after'));
|
|
28
|
+
assert.match(observed.scope, /geometric measurements/i);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('KE-REASON-016/021: after 7...b6, e5 frees the queen d4 horizontal ray through f4-h4, not a8', () => {
|
|
32
|
+
// Recorded main-line first 14 plies, exact position before White's eighth move.
|
|
33
|
+
const observed = facts('r1bqkb1r/p1pp1pp1/1p3n1p/8/2BQP3/2N5/PPP2PPP/R1B1K2R w KQkq - 0 8', 'e4e5');
|
|
34
|
+
const east = observed.stationarySliderRays.find((item) => item.fromSquare === 'd4' && item.direction === 'E');
|
|
35
|
+
assert.deepEqual(east?.beforeSquares, ['e4']);
|
|
36
|
+
assert.deepEqual(east?.afterSquares, ['e4', 'f4', 'g4', 'h4']);
|
|
37
|
+
assert.deepEqual(east?.gainedSquares, ['f4', 'g4', 'h4']);
|
|
38
|
+
assert.ok(!observed.stationarySliderRays.some((item) => item.fromSquare === 'd4' && item.gainedSquares.includes('a8')));
|
|
39
|
+
assert.deepEqual(observed.controlDelta.find((item) => item.square === 'g4' && item.color === 'w')?.attackersAfter, ['d4']);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('KE-REASON-016/021: control loss occurs at the actual knight move, not the prior pawn push', () => {
|
|
43
|
+
const rootFen = 'r1bqkb1r/p1pp1pp1/1p3n1p/8/2BQP3/2N5/PPP2PPP/R1B1K2R w KQkq - 0 8';
|
|
44
|
+
const pawnPush = facts(rootFen, 'e4e5');
|
|
45
|
+
assert.equal(pawnPush.controlDelta.find((item) => item.square === 'd5' && item.color === 'b'), undefined);
|
|
46
|
+
const afterPawnPush = new Chess(rootFen);
|
|
47
|
+
afterPawnPush.move({ from: 'e4', to: 'e5' });
|
|
48
|
+
const knightMove = facts(afterPawnPush.fen(), 'f6h7');
|
|
49
|
+
assert.deepEqual(knightMove.controlDelta.find((item) => item.square === 'd5' && item.color === 'b'), {
|
|
50
|
+
square: 'd5', color: 'b', attackersBefore: ['f6'], attackersAfter: [],
|
|
51
|
+
});
|
|
52
|
+
assert.deepEqual(knightMove.mover.attackersBefore, ['e5']);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('KE-REASON-016/021: moving slider is not reported as stationary opening, even along same file', () => {
|
|
56
|
+
const rootFen = 'k1B5/1R6/p2B1p2/8/4P2p/5P2/6PP/2K2b2 w - - 7 34';
|
|
57
|
+
const observed = facts(rootFen, 'b7b2');
|
|
58
|
+
assert.ok(!observed.stationarySliderRays.some((item) => item.pieceId === observed.mover.pieceId));
|
|
59
|
+
assert.ok(!observed.stationarySliderRays.some((item) => item.fromSquare === 'b7' || item.fromSquare === 'b2'));
|
|
60
|
+
const afterRook = new Chess(rootFen);
|
|
61
|
+
afterRook.move({ from: 'b7', to: 'b2' });
|
|
62
|
+
const response = facts(afterRook.fen(), 'f1b5');
|
|
63
|
+
const blocked = response.stationarySliderRays.find((item) => item.fromSquare === 'b2' && item.direction === 'N');
|
|
64
|
+
assert.deepEqual(blocked?.beforeSquares, ['b3', 'b4', 'b5', 'b6', 'b7', 'b8']);
|
|
65
|
+
assert.deepEqual(blocked?.afterSquares, ['b3', 'b4', 'b5']);
|
|
66
|
+
assert.deepEqual(blocked?.lostSquares, ['b6', 'b7', 'b8']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('KE-REASON-016/021: Bf7+, queen promotion and bishop underpromotion have exact mover identities and controls', () => {
|
|
70
|
+
const bishop = facts('8/4P3/1R2B3/6Bk/7P/8/8/2K5 w - - 3 50', 'e6f7');
|
|
71
|
+
assert.equal(bishop.mover.pieceId, bishop.mover.pieceIdAfter);
|
|
72
|
+
assert.equal(bishop.mover.pieceTypeBefore, 'b');
|
|
73
|
+
assert.deepEqual(bishop.controlDelta.find((item) => item.square === 'e8' && item.color === 'w')?.attackersAfter, ['f7']);
|
|
74
|
+
assert.deepEqual(bishop.controlDelta.find((item) => item.square === 'g4' && item.color === 'w'), {
|
|
75
|
+
square: 'g4', color: 'w', attackersBefore: ['e6'], attackersAfter: [],
|
|
76
|
+
});
|
|
77
|
+
const mateFen = '8/4P3/1R2B3/6Bk/7P/8/8/2K5 w - - 3 50';
|
|
78
|
+
for (const uci of ['e7e8q', 'e7e8b']) {
|
|
79
|
+
const promotionMate = new Chess(mateFen);
|
|
80
|
+
promotionMate.move({ from: 'e7', to: 'e8', promotion: uci.slice(4) });
|
|
81
|
+
assert.equal(promotionMate.isCheckmate(), true, `${uci} is a legal mate at the observed board`);
|
|
82
|
+
assert.equal(facts(mateFen, uci).mover.pieceTypeAfter, uci.slice(4));
|
|
83
|
+
}
|
|
84
|
+
const promotionFen = '8/4P3/1R2B3/6B1/7P/5k2/8/2K5 w - - 7 52';
|
|
85
|
+
const queen = facts(promotionFen, 'e7e8q');
|
|
86
|
+
const underpromotion = facts(promotionFen, 'e7e8b', false);
|
|
87
|
+
assert.equal(queen.mover.pieceTypeAfter, 'q');
|
|
88
|
+
assert.equal(underpromotion.mover.pieceTypeAfter, 'b');
|
|
89
|
+
assert.notEqual(queen.mover.pieceId, queen.mover.pieceIdAfter);
|
|
90
|
+
assert.match(queen.mover.pieceIdAfter, /promotion:e8/);
|
|
91
|
+
assert.ok(queen.controlDelta.some((entry) => entry.square === 'e7' && entry.color === 'w'));
|
|
92
|
+
assert.ok(!underpromotion.controlDelta.some((entry) => entry.square === 'e7' && entry.color === 'w'));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('KE-REASON-016/021: en passant removes the off-target blocker and castling excludes the moved rook', () => {
|
|
96
|
+
const ep = facts('7k/8/8/3pP3/8/8/8/K2R4 w - d6 0 1', 'e5d6');
|
|
97
|
+
assert.deepEqual(ep.controlDelta.find((item) => item.square === 'e4' && item.color === 'b'), {
|
|
98
|
+
square: 'e4', color: 'b', attackersBefore: ['d5'], attackersAfter: [],
|
|
99
|
+
});
|
|
100
|
+
assert.deepEqual(ep.stationarySliderRays.find((item) => item.fromSquare === 'd1' && item.direction === 'N')?.afterSquares,
|
|
101
|
+
['d2', 'd3', 'd4', 'd5', 'd6']);
|
|
102
|
+
const castle = facts('4k3/8/8/8/8/8/8/4K2R w K - 0 1', 'e1g1');
|
|
103
|
+
assert.ok(!castle.stationarySliderRays.some((item) => item.pieceId === 'white-rook:h1:0'));
|
|
104
|
+
assert.equal(castle.mover.toSquare, 'g1');
|
|
105
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {Chess} from 'chess.js';
|
|
4
|
+
import {runAnswerPipeline,createProductionObservationProvider,QUESTION_ROUTING_VERSION,simpleSystemPolicy,simpleToolDescription,toPositionAnalysis,toLegacyPositionAnalysis,toPositionAnalysisProvider,toLegacyPositionAnalysisProvider} from '../../src/chess-reasoning/answer-pipeline-exports.ts';
|
|
5
|
+
const fen=new Chess().fen();
|
|
6
|
+
const context={kind:'position',nodeId:'root',historyStartFen:fen,historyUci:[],moveUci:'',moveSan:'',fenBefore:fen,fenAfter:fen};
|
|
7
|
+
function setup(invoke,options={}) {
|
|
8
|
+
const selected={deliveryId:'answer-test',selectionKey:'root',edgeId:'root',context,existingAnalysis:null};
|
|
9
|
+
const question='Assess the current position';
|
|
10
|
+
return {requestId:'portable-test',question,selected,modelName:'deterministic-test',connector:{kind:'model-turn',invoke},
|
|
11
|
+
policy:{system:simpleSystemPolicy,description:simpleToolDescription},observationProvider:createProductionObservationProvider({context,question}),
|
|
12
|
+
prepareRouting:async()=>({question,selectionKey:'root',deliveryId:'answer-test',elapsedMs:0,omitSelectedContext:false,
|
|
13
|
+
decision:{version:QUESTION_ROUTING_VERSION,status:'fallback',omitSelectedContext:false,includeGameRecord:false,reviewGame:false,reason:'explicit-consumer-position',answers:{answer_scope:{choice:'position_analysis',confidence:1}}}}),...options};
|
|
14
|
+
}
|
|
15
|
+
function reply(text='',toolCalls=[]) {return {text,toolCalls,finishReason:toolCalls.length?'tool-calls':'stop',usage:{inputTokens:20,outputTokens:10},response:{messages:[{role:'assistant',content:toolCalls.length?toolCalls.map(c=>({type:'tool-call',...c})):text}]}};}
|
|
16
|
+
function analyzer(log) {return {async analyze(request){log.push(request);const board=new Chess(request.fen);const legal=board.moves({verbose:true});const roots=request.rootMoves.length?request.rootMoves:legal.slice(0,request.multiPv).map(m=>m.from+m.to+(m.promotion??''));return {engine:'deterministic-legal-test',engineBuild:'test-only',command:'test',bestMove:roots[0]??null,lines:roots.map(root=>{const b=new Chess(request.fen);b.move({from:root.slice(0,2),to:root.slice(2,4),promotion:root.slice(4)||undefined});const pv=[root];while(pv.length<(request.minimumPvPlies??1)&&!b.isGameOver()){const m=b.moves({verbose:true})[0];pv.push(m.from+m.to+(m.promotion??''));b.move(m);}return {rootMove:root,pv,depth:request.depth,nodes:1,scoreCp:0,matePly:null};})};}};}
|
|
17
|
+
test('KE-AI-PORT-001: existing preparation, genuine legal tools, resumable evidence and composition',async()=>{
|
|
18
|
+
let turn=0;const searches=[];const events=[];
|
|
19
|
+
const result=await runAnswerPipeline(setup(async request=>{
|
|
20
|
+
turn++;assert.match(request.system,/Knight/);assert.ok(request.tools.every(t=>t.inputSchema&&typeof t.inputSchema==='object'));
|
|
21
|
+
if(turn===1){assert.ok(searches.length>0);assert.match(JSON.stringify(request.messages),/initial-search/);return reply('',[{toolCallId:'inspect-1',toolName:'inspect_position',input:{phase:'before',squares:['e2']}}]);}
|
|
22
|
+
if(turn===2){assert.match(JSON.stringify(request.messages),/tool-result/);assert.match(JSON.stringify(request.messages),/inspect-1/);return reply('',[{toolCallId:'line-1',toolName:'test_line',input:{phase:'before',movesUci:['e2e4','e7e5'],analyzeFinal:false}}]);}
|
|
23
|
+
assert.match(JSON.stringify(request.messages),/line-1/);return reply('Both sides can develop their pieces.');
|
|
24
|
+
},{analyzer:analyzer(searches),observableTraceSink:e=>events.push(e)}));
|
|
25
|
+
assert.equal(result.status,'diagnostic');assert.equal(result.answer,'Both sides can develop their pieces.');assert.equal(turn,3);
|
|
26
|
+
assert.ok(result.initialSearches.some(s=>s.status==='completed'));assert.equal(result.toolRuns.filter(s=>s.status==='completed').length,2);assert.ok(result.presentedEvidence.length>1);assert.ok(events.some(e=>e.stage==='simple-model'));
|
|
27
|
+
});
|
|
28
|
+
test('KE-AI-PORT-001: canonical six-turn reserve forces tool-free composition',async()=>{
|
|
29
|
+
let turn=0;
|
|
30
|
+
const result=await runAnswerPipeline(setup(async request=>{turn++;if(request.toolChoice==='none'){assert.equal(turn,6);assert.equal(request.tools.length,0);return reply('The acquired evidence is limited.');}return reply('',[{toolCallId:'inspect-'+turn,toolName:'inspect_position',input:{phase:'before',squares:['e2']}}]);}));
|
|
31
|
+
assert.equal(turn,6);assert.equal(result.completionReason,'model-calls');assert.equal(result.toolCallsUsed,5);
|
|
32
|
+
});
|
|
33
|
+
test('KE-AI-PORT-001: search batch budget rejects nine requests before host execution',async()=>{
|
|
34
|
+
let turn=0;
|
|
35
|
+
const result=await runAnswerPipeline(setup(async request=>{turn++;if(turn===1)return reply('',[{toolCallId:'oversized',toolName:'search_batch',input:{requests:Array.from({length:9},(_,i)=>({queryId:'q'+i,tool:'test_line',input:{phase:'before',movesUci:['e2e4'],analyzeFinal:false}}))}}]);assert.match(JSON.stringify(request.messages),/invalid-tool-array/);return reply('The requested batch was too large.');}));
|
|
36
|
+
assert.equal(result.toolRuns[0].status,'failed');assert.equal(result.searchesUsed,0);
|
|
37
|
+
});
|
|
38
|
+
test('KE-AI-PORT-001: cancellation after model response prevents tool execution',async()=>{
|
|
39
|
+
const controller=new AbortController();
|
|
40
|
+
const result=await runAnswerPipeline(setup(async()=>{controller.abort();return reply('',[{toolCallId:'cancelled',toolName:'inspect_position',input:{phase:'before',squares:['e2']}}]);},{signal:controller.signal}));
|
|
41
|
+
assert.equal(result.status,'failure');assert.equal(result.toolRuns.length,0);assert.equal(result.answer,'');
|
|
42
|
+
});
|
|
43
|
+
test('KE-AI-PORT-001: unknown usage reserves output allowance without claiming measurement',async()=>{
|
|
44
|
+
const result=await runAnswerPipeline(setup(async()=>({...reply('A bounded answer.'),usage:{}})));
|
|
45
|
+
assert.equal(result.status,'diagnostic');assert.equal(result.usage.metering,'partly-unavailable');assert.equal(result.usage.reportedOutputTokens,null);assert.equal(result.usage.reportedInputTokens,null);assert.equal(result.usage.outputBudgetReservation,6000);assert.equal(result.usage.unmeteredCalls,1);
|
|
46
|
+
});
|
|
47
|
+
test('KE-AI-PORT-001: exact recorded history is frozen and retrieved through host port',async()=>{
|
|
48
|
+
let reads=0,turn=0;
|
|
49
|
+
const line={startFen:fen,moves:[{uci:'e2e4',san:'e4',edgeId:'e1'},{uci:'e7e5',san:'e5',edgeId:'e2'}],selectedIndex:-1,selectedAtStart:true,totalPlies:2,truncated:false};
|
|
50
|
+
const result=await runAnswerPipeline(setup(async request=>{turn++;if(turn<3)return reply('',[{toolCallId:'history-'+turn,toolName:'get_main_line',input:{}}]);assert.match(JSON.stringify(request.messages),/e2e4/);assert.match(JSON.stringify(request.messages),/recordedMainLine/);return reply('The recorded game began with central pawn moves.');},{bridge:{getMainLine:async()=>{reads++;return line;}}}));
|
|
51
|
+
assert.equal(reads,1);assert.equal(result.acquiredMainLines.length,2);assert.equal(result.acquiredMainLines[0].moves[1].san,'e5');
|
|
52
|
+
});
|
|
53
|
+
test('KE-AI-PORT-001: malformed connector batches fail closed before any valid prefix executes',async()=>{
|
|
54
|
+
const valid={toolCallId:'first',toolName:'inspect_position',input:{phase:'before',squares:['e2']}};
|
|
55
|
+
for(const invalid of [{...valid},{...valid,toolCallId:''},{...valid,toolCallId:'second',toolName:'unknown'},{...valid,toolCallId:'second',input:[]},{...valid,toolCallId:'second',input:null}]) {
|
|
56
|
+
const result=await runAnswerPipeline(setup(async()=>reply('',[valid,invalid])));
|
|
57
|
+
assert.equal(result.status,'failure');assert.equal(result.toolRuns.length,0);assert.equal(result.toolCallsUsed,0);
|
|
58
|
+
}
|
|
59
|
+
const result=await runAnswerPipeline(setup(async()=>reply('',Array.from({length:33},(_,i)=>({...valid,toolCallId:'call-'+i})))));
|
|
60
|
+
assert.equal(result.status,'failure');assert.equal(result.toolRuns.length,0);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('KE-REASON-034: signed-moves adapters preserve attributed legacy records and conditional segments',async()=>{
|
|
64
|
+
const provenance={kind:'restored-fallback',engineBuild:'historical-build',originatingDepth:11};
|
|
65
|
+
const scope={fen,perspective:'white',positiveMeans:'advantage for perspective color',scoreCpUnit:'centipawns',matePlyUnit:'signed mate-in-moves (legacy field name, not plies)'};
|
|
66
|
+
const legacy={engine:'historical-engine',engineBuild:'historical-build',command:'historical',bestMove:'e2e4',cacheProvenance:'browser-completed-cache',sourceCacheProvenance:'widget-analysis-cache',
|
|
67
|
+
lines:[{rootMove:'e2e4',pv:['e2e4'],rawPv:['e2e4'],depth:11,nodes:42,scoreCp:null,matePly:-3,scoreScope:scope,scoreProvenance:provenance,
|
|
68
|
+
pvCompletion:{requestedPlies:10,actualPlies:1,status:'short-budget',conditional:true,segments:[{fen,startPly:1,depth:8,pv:['e7e5'],scoreCp:null,matePly:2,scoreScope:{...scope,perspective:'black'},scoreProvenance:provenance,command:'historical-endpoint',engine:'historical-engine',engineBuild:'historical-build',cacheProvenance:'restored-fallback'}]}}]};
|
|
69
|
+
const frozen=JSON.stringify(legacy);
|
|
70
|
+
const preferred=toPositionAnalysis(legacy);
|
|
71
|
+
assert.equal(preferred.lines[0].mateInMoves,-3);assert.equal('matePly' in preferred.lines[0],false);
|
|
72
|
+
assert.equal(preferred.lines[0].pvCompletion.segments[0].mateInMoves,2);
|
|
73
|
+
assert.equal(preferred.lines[0].scoreScope.mateInMovesUnit,'signed mate-in-moves');
|
|
74
|
+
assert.equal('matePlyUnit' in preferred.lines[0].scoreScope,false);
|
|
75
|
+
assert.deepEqual(preferred.lines[0].scoreProvenance,provenance);
|
|
76
|
+
assert.deepEqual(toLegacyPositionAnalysis(preferred),legacy);
|
|
77
|
+
assert.equal(JSON.stringify(legacy),frozen);
|
|
78
|
+
for(const mate of [null,0,3,-5]){
|
|
79
|
+
const result={...legacy,lines:[{...legacy.lines[0],matePly:mate}]};
|
|
80
|
+
assert.deepEqual(toLegacyPositionAnalysis(toPositionAnalysis(result)),result);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('KE-REASON-034: conflicting aliases, invalid distances and units fail closed including segments',async()=>{
|
|
85
|
+
const result={engine:'test',engineBuild:'test',command:'test',bestMove:null,lines:[{rootMove:'e2e4',pv:['e2e4'],depth:1,nodes:null,scoreCp:null,mateInMoves:2}]};
|
|
86
|
+
for(const convert of [toPositionAnalysis,toLegacyPositionAnalysis]){
|
|
87
|
+
for(const value of [{matePly:3},{matePly:null},{mateInMoves:1.5},{mateInMoves:Infinity},{mateInMoves:undefined},{mateInMoves:'2'},{scoreScope:{mateInMovesUnit:'plies'}},{scoreScope:{matePlyUnit:'plies'}}]){
|
|
88
|
+
assert.throws(()=>convert({...result,lines:[{...result.lines[0],...value}]}),TypeError);
|
|
89
|
+
}
|
|
90
|
+
assert.throws(()=>convert({...result,lines:[{...result.lines[0],pvCompletion:{segments:[{matePly:1,mateInMoves:2}]}}]}),/Conflicting/);
|
|
91
|
+
const {mateInMoves:_,...missing}=result.lines[0];void _;
|
|
92
|
+
assert.throws(()=>convert({...result,lines:[missing]}),/Missing/);
|
|
93
|
+
assert.equal(convert({...result,lines:[{...result.lines[0],matePly:2}]}).lines.length,1);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('KE-REASON-034: preferred analyzer and explicit legacy analyzer produce identical answer evidence',async()=>{
|
|
98
|
+
const oldSearches=[],newSearches=[];
|
|
99
|
+
const old=await runAnswerPipeline(setup(async()=>reply('The evidence is bounded.'),{analyzer:analyzer(oldSearches)}));
|
|
100
|
+
const modern=await runAnswerPipeline(setup(async()=>reply('The evidence is bounded.'),{analyzer:toPositionAnalysisProvider(analyzer(newSearches))}));
|
|
101
|
+
assert.equal(modern.answer,old.answer);assert.equal(modern.status,old.status);
|
|
102
|
+
assert.deepEqual(modern.presentedEvidence,old.presentedEvidence);
|
|
103
|
+
assert.equal(newSearches.length,oldSearches.length);
|
|
104
|
+
assert.ok(newSearches.length>0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('KE-REASON-034: analyzer adapters forward exact requests and reject pre/post completion cancellation',async()=>{
|
|
108
|
+
for(const wrap of [toPositionAnalysisProvider,toLegacyPositionAnalysisProvider]){
|
|
109
|
+
let calls=0;const controller=new AbortController();
|
|
110
|
+
const request={fen,depth:18,multiPv:3,rootMoves:[],signal:controller.signal};
|
|
111
|
+
const provider=wrap({async analyze(received){calls++;assert.equal(received,request);controller.abort(new Error('stop-adapter'));return {engine:'test',engineBuild:'test',command:'test',bestMove:null,lines:[]};}});
|
|
112
|
+
await assert.rejects(provider.analyze(request),/stop-adapter/);assert.equal(calls,1);
|
|
113
|
+
await assert.rejects(provider.analyze(request),/stop-adapter/);assert.equal(calls,1);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {Chess} from 'chess.js';
|
|
4
|
+
import {planInitialSearches} from '../../src/chess-reasoning/initial-search-plan.ts';
|
|
5
|
+
const context={kind:'position',nodeId:'root',historyStartFen:'rnbq1rk1/p1p1bpp1/1p3n1p/3p4/3P3B/2N1PN2/PP3PPP/R2QKB1R w KQ - 0 9',historyUci:[],moveUci:'',moveSan:''};
|
|
6
|
+
context.fenBefore=context.fenAfter=context.historyStartFen;
|
|
7
|
+
const input={context,question:'Assess the current position: what are the main strengths, weaknesses and plans for both sides?',answerScope:'position_analysis',omitSelectedContext:false};
|
|
8
|
+
test('KE-REASON-021: prepare same actual position tool before first answering call',()=>{
|
|
9
|
+
const plan=planInitialSearches(input);assert.equal(plan.requests.length,1);assert.equal(plan.requests[0].tool,'explore_continuations');
|
|
10
|
+
assert.deepEqual(plan.requests[0].input,{phase:'before',depth:18,multiPv:3,minimumPvPlies:24});
|
|
11
|
+
});
|
|
12
|
+
test('retrospective, omitted, general, terminal and explicit ablation do not search an unrelated board',()=>{
|
|
13
|
+
for(const change of [{omitSelectedContext:true},{reviewGame:true},{answerScope:'general'},{positionPreparation:false}])assert.equal(planInitialSearches({...input,...change}).requests.length,0);
|
|
14
|
+
const terminal='7k/5Q2/6K1/8/8/8/8/8 b - - 0 1';assert.ok(new Chess(terminal).isGameOver());
|
|
15
|
+
assert.equal(planInitialSearches({...input,context:{...context,fenBefore:terminal,fenAfter:terminal}}).requests.length,0);
|
|
16
|
+
});
|
|
17
|
+
test('explicit current-node comparison takes precedence over broad preparation',()=>{
|
|
18
|
+
const p=planInitialSearches({...input,question:'Compare Bd3 and Be2 instead.'});assert.equal(p.requests.length,1);assert.equal(p.requests[0].tool,'compare_moves');
|
|
19
|
+
});
|
|
20
|
+
test('retained selected-move context assessment searches the after position',()=>{
|
|
21
|
+
const c={...context,kind:'move',moveUci:'e6d5',moveSan:'exd5'};assert.equal(planInitialSearches({...input,context:c}).requests[0].input.phase,'after');
|
|
22
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {consolidatePreparedSearches} from '../../src/chess-reasoning/preparation-consolidation.ts';
|
|
4
|
+
import {compactPreparedSearchResult} from '../../src/chess-reasoning/compact-search-preparation.ts';
|
|
5
|
+
import {scoreComparisonNotices} from '../../src/chess-reasoning/score-comparison-notices.ts';
|
|
6
|
+
const fen='rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
|
|
7
|
+
const scoreScope={fen,perspective:'white',positiveMeans:'advantage for perspective color',scoreCpUnit:'centipawns',matePlyUnit:'signed mate-in-moves (legacy field name, not plies)'};
|
|
8
|
+
function record(id,restricted=false){
|
|
9
|
+
const roots=restricted?['d2d4','e2e4','b1c3']:['e2e4','d2d4'];
|
|
10
|
+
return {id,contextHash:'game',source:'Stockfish/'+(restricted?'compare_moves':'explore-continuations'),
|
|
11
|
+
data:{recordedPosition:{recordHash:'record',ply:0,startFen:fen,fen,historyUci:[],rootContextHash:'root'},details:{phase:'after',engine:'Stockfish',engineBuild:'test',searchScope:{fen,requestedDepth:18,requestedMultiPv:restricted?null:2,requestedRootMoves:restricted?roots:[],cacheProvenance:'engine'},rootMoveFacts:[],continuationMoveFacts:[],lines:roots.map((rootMove,i)=>({rootMove,pv:[rootMove],depth:18,scoreCp:50-i*10,matePly:null,scoreScope,pvCompletion:{requestedPlies:1,actualPlies:1,status:'complete',conditional:false,segments:[]}}))}},
|
|
12
|
+
coverage:{complete:restricted,covered:roots,unresolved:[]},licenses:['engine.bounded-comparison'],replay:{fen,linesUci:roots.map(x=>[x])}};
|
|
13
|
+
}
|
|
14
|
+
function entry(callId,record,discoveryCallId){
|
|
15
|
+
const result=compactPreparedSearchResult({contextHash:'game',evidence:[record],limitations:[]},'b1c3');
|
|
16
|
+
return {callId,records:[record],playedMove:'b1c3',discoveryCallId,modelRun:{callId,status:'completed',result}};
|
|
17
|
+
}
|
|
18
|
+
const pair=()=>[entry('discovery',record('evidence:discovery')),entry('comparison',record('evidence:comparison',true),'discovery')];
|
|
19
|
+
test('KE-REASON-016/021/026: one primary comparison keeps original scores, order changes, dependency and retrieval identity',()=>{
|
|
20
|
+
const input=pair(),copy=structuredClone(input),notices=scoreComparisonNotices(input.flatMap(x=>x.records));
|
|
21
|
+
const out=consolidatePreparedSearches(input);
|
|
22
|
+
assert.equal(out.results.length,1);assert.deepEqual(out.results[0].result,input[1].modelRun.result);
|
|
23
|
+
assert.equal(out.results[0].discovery.evidenceId,'evidence:discovery');
|
|
24
|
+
assert.equal(out.results[0].discovery.retrieveFullEvidenceId,'evidence:discovery');
|
|
25
|
+
assert.deepEqual(out.results[0].discovery.scores.map(x=>[x.rootMove,x.scoreCp]),[['e2e4',50],['d2d4',40]]);
|
|
26
|
+
assert.deepEqual(out.results[0].discovery.comparisonOrderForDiscoveredMoves,['d2d4','e2e4']);
|
|
27
|
+
assert.equal(out.results[0].discovery.reportedOrderChanged,true);
|
|
28
|
+
assert.equal(out.diagnostics.dependencies[0].relation,'candidate-selection');
|
|
29
|
+
assert.equal(out.diagnostics.dependencies[0].primaryEvidenceId,'evidence:comparison');
|
|
30
|
+
assert.deepEqual(scoreComparisonNotices(input.flatMap(x=>x.records)),notices);assert.equal(notices.length,2);
|
|
31
|
+
assert.deepEqual(input,copy);
|
|
32
|
+
});
|
|
33
|
+
test('KE-REASON-016/021/026: distinct PVs and conditional segments remain explicitly discoverable by original ID',()=>{
|
|
34
|
+
const input=pair();const line=input[0].records[0].data.details.lines[0];line.pv.push('e7e5');line.pvCompletion.conditional=true;
|
|
35
|
+
input[0].modelRun.result=compactPreparedSearchResult({contextHash:'game',evidence:input[0].records,limitations:[]},'b1c3');
|
|
36
|
+
const out=consolidatePreparedSearches(input);assert.deepEqual(out.results[0].discovery.distinctContinuationRootMoves,['e2e4']);
|
|
37
|
+
assert.deepEqual(input[0].records[0].data.details.lines[0].pv,['e2e4','e7e5']);
|
|
38
|
+
});
|
|
39
|
+
test('KE-REASON-023/026: discovery-only delivered opening commentary survives consolidation',()=>{
|
|
40
|
+
const input=pair(),source={pages:[{title:'source',prose:'opening',matchedTargets:['original']}]};
|
|
41
|
+
input[0].modelRun.result={chessResult:input[0].modelRun.result,openingSourceContext:source};
|
|
42
|
+
assert.deepEqual(consolidatePreparedSearches(input).results[0].discoveryOpeningSourceContext,source);
|
|
43
|
+
});
|
|
44
|
+
test('KE-REASON-016/026: no omission for incomplete, failed, foreign, unrecognized, mixed-depth or unrelated results',()=>{
|
|
45
|
+
const changes=[
|
|
46
|
+
a=>a[1].modelRun.status='failed',a=>a[1].records[0].coverage.complete=false,
|
|
47
|
+
a=>a[1].records[0].coverage.covered.pop(),a=>a[1].records[0].data.details.lines.pop(),
|
|
48
|
+
a=>a[1].records[0].data.recordedPosition.ply=1,a=>a[1].records[0].contextHash='foreign',
|
|
49
|
+
a=>a[1].records[0].data.details.lines[0].depth=19,a=>a[1].records[0].data.details.lines[0].scoreScope={...scoreScope,perspective:'black'},
|
|
50
|
+
a=>a[0].records[0].data.details.newProducerField='retain',a=>a[1].discoveryCallId=undefined,
|
|
51
|
+
a=>a[1].playedMove=undefined,a=>a[0].records[0].data.details.lines[0].pv.push('a1a8'),
|
|
52
|
+
];
|
|
53
|
+
for(const change of changes){const input=pair();change(input);const out=consolidatePreparedSearches(input);assert.deepEqual(out.results,input.map(e=>e.modelRun));assert.equal(out.diagnostics.summarizedDiscoveryCount,0);}
|
|
54
|
+
const only=[pair()[0]];assert.deepEqual(consolidatePreparedSearches(only).results,only.map(e=>e.modelRun));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('KE-REASON-016/021/026: unverified cached comparison depth cannot authorize consolidation',()=>{
|
|
58
|
+
const input=pair();input[1].records[0].data.details.searchScope.sourceCacheProvenance='widget-analysis-cache';
|
|
59
|
+
assert.equal(consolidatePreparedSearches(input).results.length,2);
|
|
60
|
+
for(const line of input[1].records[0].data.details.lines)line.scoreProvenance={kind:'root-search',fen,originatingDepth:18};
|
|
61
|
+
input[0].records[0].data.details.lines[0].scoreProvenance={kind:'legacy-unknown',originatingDepth:null,reportedDepth:18};
|
|
62
|
+
assert.equal(consolidatePreparedSearches(input).results.length,2, 'known origins alone do not establish one search');
|
|
63
|
+
input[1].records[0].data.details.searchScope.sourceCacheProvenance='engine';
|
|
64
|
+
const out=consolidatePreparedSearches(input);
|
|
65
|
+
assert.equal(out.results.length,1);
|
|
66
|
+
assert.equal(out.results[0].discovery.scores[0].scoreProvenance.kind,'legacy-unknown');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('KE-REASON-026: the original command remains authoritative when origins share a different command',()=>{
|
|
70
|
+
const input=pair(),details=input[1].records[0].data.details;
|
|
71
|
+
details.searchScope.command='go depth 18 searchmoves d2d4 e2e4 b1c3';
|
|
72
|
+
for(const line of details.lines)line.scoreProvenance={kind:'root-search',fen,originatingDepth:18,command:'go depth 26'};
|
|
73
|
+
assert.equal(consolidatePreparedSearches(input).results.length,2);
|
|
74
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {Chess} from 'chess.js';
|
|
4
|
+
import {simpleProseReferences,reasoningContextHash,admitFreshEnginePvWitnesses} from '../../src/chess-reasoning/answer-pipeline-exports.ts';
|
|
5
|
+
|
|
6
|
+
test('fresh answer witnesses preserve shipped v22 reference geometry and reject foreign engine evidence',()=>{
|
|
7
|
+
const board=new Chess(),fen=board.fen();
|
|
8
|
+
const context={kind:'position',nodeId:'start',historyStartFen:fen,historyUci:[],moveUci:'',moveSan:'',fenBefore:fen,fenAfter:fen};
|
|
9
|
+
const witness={version:'fresh-engine-pv-witness/1',acquisition:'same-answer-engine',acquisitionId:1,
|
|
10
|
+
evidenceId:'server-engine-acquisition:1',contextHash:reasoningContextHash(context),phase:'before',fen,
|
|
11
|
+
engineBuild:'test-engine',command:'go depth 18',requestedDepth:18,multiPv:1,rootMoves:[],
|
|
12
|
+
authority:'bounded-engine',licenses:['engine.bounded-continuation'],
|
|
13
|
+
lines:[{rootMove:'e2e4',pv:['e2e4','e7e5'],depth:18,scoreCp:20,mateInMoves:null,
|
|
14
|
+
scoreProvenance:{kind:'root-search',fen,engineBuild:'test-engine',command:'go depth 18',originatingDepth:18,reportedDepth:18}}]};
|
|
15
|
+
const answer='[e5](move:before:e2e4,e7e5)';
|
|
16
|
+
const rendered=simpleProseReferences(answer,context,[],undefined,undefined,undefined,{freshEnginePvWitnesses:[witness]});
|
|
17
|
+
assert.equal(rendered.freshEnginePvAdmission.acceptedLines,1);
|
|
18
|
+
assert.equal(rendered.blocks[0].tokens[0].reference.position.moves.length,2);
|
|
19
|
+
assert.equal(rendered.blocks[0].tokens[0].reference.engineEvidence[0].evidenceId,witness.evidenceId);
|
|
20
|
+
assert.equal(rendered.referenceGapCounts.unresolvedReferences,0);
|
|
21
|
+
const foreign=structuredClone(witness);foreign.contextHash='foreign';
|
|
22
|
+
assert.equal(admitFreshEnginePvWitnesses(context,[foreign]).audit.acceptedLines,0);
|
|
23
|
+
const rejected=simpleProseReferences(answer,context,[],undefined,undefined,undefined,{freshEnginePvWitnesses:[foreign]});
|
|
24
|
+
assert.equal(rejected.referenceGapCounts.unresolvedReferences,1);
|
|
25
|
+
assert.equal(rejected.blocks[0].tokens[0].reference,undefined);
|
|
26
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { scoreComparisonNotices } from '../../src/chess-reasoning/score-comparison-notices.ts';
|
|
4
|
+
const fen='7k/8/8/8/8/8/8/KR6 w - - 0 1';
|
|
5
|
+
function evidence(id,cp,comparison=false){
|
|
6
|
+
const roots=comparison?['b1b7','b1b2']:[];
|
|
7
|
+
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)'}});
|
|
8
|
+
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',1000)]:[])]}};
|
|
9
|
+
}
|
|
10
|
+
test('KE-REASON-016/021: qualify cached unrestricted versus matched comparison scores without changing evidence',()=>{
|
|
11
|
+
const a=evidence('a',1391),b=evidence('b',1256,true),records=[a,b],saved=structuredClone(records);
|
|
12
|
+
const [notice]=scoreComparisonNotices(records);
|
|
13
|
+
assert.deepEqual(records,saved);
|
|
14
|
+
assert.equal(notice.rootMove,'b1b7');assert.equal(notice.perspective,'white');
|
|
15
|
+
assert.deepEqual(notice.observations.map(o=>o.score),[{unit:'centipawns',value:1391},{unit:'centipawns',value:1256}]);
|
|
16
|
+
assert.deepEqual(notice.observations.map(o=>o.rootScope),['unrestricted','restricted']);
|
|
17
|
+
assert.deepEqual(notice.observations.map(o=>[o.reportedDepth,o.actualDepth]),[[18,null],[18,18]]);
|
|
18
|
+
assert.equal(notice.disagreementKind,'centipawn-variation');
|
|
19
|
+
assert.deepEqual(notice.preferredWithinSearchComparisonEvidenceIds,['b']);
|
|
20
|
+
assert.match(notice.interpretation,/does not require another search/);
|
|
21
|
+
});
|
|
22
|
+
test('KE-REASON-021: scope separates root positions and perspectives and excludes unscoped scores',()=>{
|
|
23
|
+
const a=evidence('a',1391),b=evidence('b',1256,true);
|
|
24
|
+
b.data.searchScope.fen=fen.replace('0 1','1 1');
|
|
25
|
+
assert.deepEqual(scoreComparisonNotices([a,b]),[]);
|
|
26
|
+
b.data.searchScope.fen=fen;b.data.lines[0].scoreScope.perspective='black';
|
|
27
|
+
assert.deepEqual(scoreComparisonNotices([a,b]),[]);
|
|
28
|
+
delete b.data.lines[0].scoreScope;
|
|
29
|
+
assert.deepEqual(scoreComparisonNotices([a,b]),[]);
|
|
30
|
+
});
|
|
31
|
+
test('KE-REASON-021: historical nested results retain scope; duplicates and equal scores create no notice',()=>{
|
|
32
|
+
const a=evidence('a',1391),b=evidence('b',1256,true);
|
|
33
|
+
b.data={recordedPosition:{ply:66},details:b.data};
|
|
34
|
+
assert.equal(scoreComparisonNotices([a,b,a]).length,1);
|
|
35
|
+
assert.equal(scoreComparisonNotices([a,b,a])[0].observations.length,2);
|
|
36
|
+
assert.deepEqual(scoreComparisonNotices([a,evidence('equal',1391)]),[]);
|
|
37
|
+
});
|
|
38
|
+
test('KE-REASON-021: mates remain typed distances and missing mate metadata cannot leak sentinel CP',()=>{
|
|
39
|
+
const a=evidence('a',1391),b=evidence('b',100000,true);
|
|
40
|
+
b.data.lines[0].matePly=3;
|
|
41
|
+
const [notice]=scoreComparisonNotices([a,b]);
|
|
42
|
+
assert.deepEqual(notice.observations[1].score,{unit:'mate-in-moves',value:3});
|
|
43
|
+
assert.equal(notice.disagreementKind,'mate-status-disagreement');
|
|
44
|
+
assert.match(notice.interpretation,/centipawn score in another search does not refute its mating continuation/);
|
|
45
|
+
assert.match(notice.interpretation,/within that search, not independent confirmation or rejection/);
|
|
46
|
+
assert.deepEqual(notice.preferredWithinSearchComparisonEvidenceIds,['b']);
|
|
47
|
+
b.data.lines[0].matePly=null;
|
|
48
|
+
assert.deepEqual(scoreComparisonNotices([a,b]),[]);
|
|
49
|
+
});
|
|
50
|
+
test('KE-REASON-016/021/026: distinguish opposing mate signs from distances while retaining reported scope and provenance',()=>{
|
|
51
|
+
const a=evidence('a',100000),b=evidence('b',100000,true);
|
|
52
|
+
a.data.lines[0].matePly=3;b.data.lines[0].matePly=-5;
|
|
53
|
+
const reported={kind:'root-search',fen,originatingDepth:17,engineBuild:'18'};
|
|
54
|
+
b.data.lines[0].scoreProvenance=reported;
|
|
55
|
+
const saved=structuredClone([a,b]);
|
|
56
|
+
const [opposite]=scoreComparisonNotices([a,b]);
|
|
57
|
+
assert.equal(opposite.disagreementKind,'opposite-mate-sign');
|
|
58
|
+
assert.match(opposite.interpretation,/mate signs oppose/);
|
|
59
|
+
assert.deepEqual(opposite.observations[1].scoreProvenance,reported);
|
|
60
|
+
assert.equal('scoreProvenance' in opposite.observations[0],false);
|
|
61
|
+
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']]);
|
|
62
|
+
assert.deepEqual([a,b],saved);
|
|
63
|
+
b.data.lines[0].matePly=5;
|
|
64
|
+
const [distance]=scoreComparisonNotices([a,b]);
|
|
65
|
+
assert.equal(distance.disagreementKind,'mate-distance-variation');
|
|
66
|
+
assert.match(distance.interpretation,/different mate distances/);
|
|
67
|
+
assert.match(distance.interpretation,/does not require another search/);
|
|
68
|
+
});
|
|
69
|
+
test('KE-REASON-021: incomplete or mixed-depth comparisons do not receive comparison preference',()=>{
|
|
70
|
+
const a=evidence('a',1391),b=evidence('b',1256,true);
|
|
71
|
+
b.data.lines[1].depth=17;
|
|
72
|
+
assert.deepEqual(scoreComparisonNotices([a,b])[0].preferredWithinSearchComparisonEvidenceIds,[]);
|
|
73
|
+
b.data.lines[1].depth=18;b.coverage.complete=false;
|
|
74
|
+
assert.deepEqual(scoreComparisonNotices([a,b])[0].preferredWithinSearchComparisonEvidenceIds,[]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('KE-REASON-026: malformed candidates and inconsistent original commands cannot certify comparison preference',()=>{
|
|
78
|
+
const a=evidence('a',1391),b=evidence('b',1256,true);
|
|
79
|
+
b.data.searchScope.command='go depth 18';
|
|
80
|
+
b.data.lines.forEach(line=>line.scoreProvenance={kind:'root-search',fen,originatingDepth:18,command:'go depth 26'});
|
|
81
|
+
assert.deepEqual(scoreComparisonNotices([a,b])[0].preferredWithinSearchComparisonEvidenceIds,[]);
|
|
82
|
+
b.data.lines.push(null);
|
|
83
|
+
assert.deepEqual(scoreComparisonNotices([a,b])[0].preferredWithinSearchComparisonEvidenceIds,[]);
|
|
84
|
+
});
|
|
85
|
+
test('KE-REASON-016/021/026: cached reported depths and subsequent-position scores cannot certify a matched root comparison',()=>{
|
|
86
|
+
const a=evidence('a',1391),b=evidence('b',1256,true);
|
|
87
|
+
b.data.searchScope.cacheProvenance='browser-completed-cache';
|
|
88
|
+
b.data.searchScope.sourceCacheProvenance='widget-analysis-cache';
|
|
89
|
+
assert.deepEqual(scoreComparisonNotices([a,b])[0].preferredWithinSearchComparisonEvidenceIds,[]);
|
|
90
|
+
assert.deepEqual(scoreComparisonNotices([a,b])[0].observations[1].actualDepth,null);
|
|
91
|
+
const kinds=[
|
|
92
|
+
{kind:'restored-fallback',originatingDepth:null,reportedDepth:18,fallbackDepth:18},
|
|
93
|
+
{kind:'legacy-unknown',originatingDepth:null,reportedDepth:18},
|
|
94
|
+
{kind:'subsequent-position',fen:fen.replace('0 1','1 1'),originatingDepth:18,reportedDepth:18},
|
|
95
|
+
{kind:'root-search',fen:fen.replace('0 1','1 1'),originatingDepth:18,engineBuild:'18'},
|
|
96
|
+
{kind:'root-search',fen,originatingDepth:0,engineBuild:'18'},
|
|
97
|
+
];
|
|
98
|
+
for(const provenance of kinds){
|
|
99
|
+
b.data.lines[0].scoreProvenance=provenance;b.data.lines[1].scoreProvenance=provenance;
|
|
100
|
+
const [notice]=scoreComparisonNotices([a,b]);
|
|
101
|
+
assert.equal(notice.observations[1].reportedDepth,18);
|
|
102
|
+
assert.equal(notice.observations[1].actualDepth,null);
|
|
103
|
+
assert.deepEqual(notice.preferredWithinSearchComparisonEvidenceIds,[]);
|
|
104
|
+
}
|
|
105
|
+
b.data.lines[0].scoreProvenance={kind:'root-search',fen,originatingDepth:17,engineBuild:'18'};
|
|
106
|
+
b.data.lines[1].scoreProvenance={kind:'root-search',fen,originatingDepth:18,engineBuild:'18'};
|
|
107
|
+
assert.deepEqual(scoreComparisonNotices([a,b])[0].preferredWithinSearchComparisonEvidenceIds,[]);
|
|
108
|
+
b.data.lines[1].scoreProvenance={kind:'root-search',fen,originatingDepth:17,engineBuild:'18'};
|
|
109
|
+
assert.deepEqual(scoreComparisonNotices([a,b])[0].preferredWithinSearchComparisonEvidenceIds,[], 'candidate provenance does not establish shared search identity');
|
|
110
|
+
b.data.searchScope.sourceCacheProvenance='engine';
|
|
111
|
+
b.data.lines.forEach(line=>line.depth=17);
|
|
112
|
+
const [verified]=scoreComparisonNotices([a,b]);
|
|
113
|
+
assert.deepEqual(verified.preferredWithinSearchComparisonEvidenceIds,['b']);
|
|
114
|
+
assert.deepEqual(verified.observations[1].scoreProvenance,b.data.lines[0].scoreProvenance);
|
|
115
|
+
assert.equal(verified.observations[1].actualDepth,17);
|
|
116
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {Chess} from 'chess.js';
|
|
4
|
+
import {projectEnginePresentation} from '../../src/chess-reasoning/simple-engine-presentation.ts';
|
|
5
|
+
|
|
6
|
+
const pv=['e2e4','d7d5','e4d5','d8d5'];
|
|
7
|
+
function fixture() {
|
|
8
|
+
const b=new Chess(),fens=[b.fen()];
|
|
9
|
+
for(const m of pv){b.move({from:m.slice(0,2),to:m.slice(2,4)});fens.push(b.fen());}
|
|
10
|
+
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'}));
|
|
11
|
+
const moves=pv.map((uci,i)=>({uci,capture:i>1?{pieceType:'p'}:null,givesCheck:false,captureGeometry:{test:i}}));
|
|
12
|
+
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}]}}]};
|
|
13
|
+
}
|
|
14
|
+
test('KE-REASON-016/021: default view removes only deep geometry and exact PV duplicates while preserving complete internal records',()=>{
|
|
15
|
+
const raw=fixture(),copy=structuredClone(raw),v=projectEnginePresentation(raw),d=v.evidence[0].data;
|
|
16
|
+
assert.deepEqual(raw,copy);
|
|
17
|
+
assert.deepEqual(d.continuationMoveFacts[0].moves.map(m=>!!m.captureGeometry),[true,true,false,false]);
|
|
18
|
+
assert.deepEqual(d.continuationMoveFacts[0].moves.map(m=>m.capture),copy.evidence[0].data.continuationMoveFacts[0].moves.map(m=>m.capture));
|
|
19
|
+
assert.equal(d.lines[0].rawPv,undefined);assert.deepEqual(d.lines[0].pv,pv);
|
|
20
|
+
assert.equal(d.modelPresentation.retrieveFullEvidenceId,'e1');
|
|
21
|
+
const segments=d.lines[0].pvCompletion.segments;
|
|
22
|
+
assert.deepEqual(segments.map(s=>s.fenFromLinePvPly),[0,2]);
|
|
23
|
+
for(let i=0;i<segments.length;i++){const {fen,...original}=copy.evidence[0].data.lines[0].pvCompletion.segments[i];const {fenFromLinePvPly,...actual}=segments[i];void fen;void fenFromLinePvPly;assert.deepEqual(actual,original);}
|
|
24
|
+
const full=projectEnginePresentation(raw,true).evidence[0].data;
|
|
25
|
+
assert.deepEqual(full.continuationMoveFacts,copy.evidence[0].data.continuationMoveFacts);
|
|
26
|
+
assert.equal(full.modelPresentation.omittedCaptureGeometry,undefined);
|
|
27
|
+
});
|
|
28
|
+
test('KE-REASON-021: unequal original PV and mismatched segment FEN remain explicit',()=>{
|
|
29
|
+
const raw=fixture(),line=raw.evidence[0].data.lines[0];
|
|
30
|
+
line.rawPv=pv.slice(0,2);line.pvCompletion.segments[1].fen='unverified';
|
|
31
|
+
const d=projectEnginePresentation(raw).evidence[0].data;
|
|
32
|
+
assert.deepEqual(d.lines[0].rawPv,pv.slice(0,2));
|
|
33
|
+
assert.equal(d.lines[0].pvCompletion.segments[1].fen,'unverified');
|
|
34
|
+
assert.equal(d.lines[0].pvCompletion.segments[1].fenFromLinePvPly,undefined);
|
|
35
|
+
});
|
|
36
|
+
test('KE-REASON-016: unrelated geometry and unknown record shapes stay intact',()=>{
|
|
37
|
+
const raw={id:'other',data:{captureGeometry:{detail:'preserve'},continuationMoveFacts:[{moves:[1,2,{captureGeometry:{detail:'preserve'}}]}]}};
|
|
38
|
+
assert.deepEqual(projectEnginePresentation(raw),raw);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('KE-REASON-016: recorded-position tools project nested details without losing historical scope',()=>{
|
|
42
|
+
const raw=fixture(),data=raw.evidence[0].data;
|
|
43
|
+
raw.evidence[0].data={recordedPosition:{ply:10,fen:data.searchScope.fen},details:data};
|
|
44
|
+
const v=projectEnginePresentation(raw).evidence[0];
|
|
45
|
+
assert.deepEqual(v.data.recordedPosition,raw.evidence[0].data.recordedPosition);
|
|
46
|
+
assert.equal(v.data.details.lines[0].rawPv,undefined);
|
|
47
|
+
assert.equal(v.data.details.continuationMoveFacts[0].moves[2].captureGeometry,undefined);
|
|
48
|
+
assert.equal(v.data.details.modelPresentation.retrieveFullEvidenceId,'e1');
|
|
49
|
+
assert.deepEqual(projectEnginePresentation(raw,true).evidence[0].data.details.continuationMoveFacts,data.continuationMoveFacts);
|
|
50
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {createToolInputLoopGuard} from '../../src/chess-reasoning/tool-input-loop-guard.ts';
|
|
4
|
+
test('KE-REASON-026: Nc8 repeated final comparison is blocked before dispatch; corrected request stays eligible',()=>{
|
|
5
|
+
const guard=createToolInputLoopGuard(); let validations=0;
|
|
6
|
+
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};
|
|
7
|
+
assert.throws(()=>guard.validate('test_line',input,()=>validations++),/requires analyzeFinal=true/);
|
|
8
|
+
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);
|
|
9
|
+
assert.equal(validations,1); assert.equal(guard.repeatedFailures,3);
|
|
10
|
+
guard.validate('test_line',{...input,analyzeFinal:true},()=>validations++);
|
|
11
|
+
guard.validate('test_line',{phase:'after',movesUci:input.movesUci,analyzeFinal:false},()=>validations++);
|
|
12
|
+
assert.equal(validations,3);
|
|
13
|
+
});
|
|
14
|
+
test('KE-REASON-026: canonical keys ignore property order but preserve array order, names and episode boundaries',()=>{
|
|
15
|
+
const guard=createToolInputLoopGuard(); const invalid=()=>{throw Object.assign(new Error('array of 1..6 items'),{code:'invalid-tool-array',field:'compareFinalMovesUci',expected:'array of 1..6 items'});};
|
|
16
|
+
assert.throws(()=>guard.validate('test_line',{analyzeFinal:false,compareFinalMovesUci:[]},invalid));
|
|
17
|
+
assert.throws(()=>guard.validate('test_line',{compareFinalMovesUci:[],analyzeFinal:false},invalid),e=>e.code==='repeated-invalid-tool-input');
|
|
18
|
+
assert.throws(()=>createToolInputLoopGuard().validate('test_line',{compareFinalMovesUci:[],analyzeFinal:false},invalid),e=>e.code==='invalid-tool-array');
|
|
19
|
+
assert.throws(()=>guard.validate('test_game_line',{compareFinalMovesUci:[],analyzeFinal:false},invalid),e=>e.code==='invalid-tool-array');
|
|
20
|
+
});
|
|
21
|
+
test('KE-REASON-026: unstructured and transient failures are never remembered',()=>{
|
|
22
|
+
const guard=createToolInputLoopGuard(); let attempts=0;
|
|
23
|
+
for(let n=0;n<2;n++)assert.throws(()=>guard.validate('analyze_position',{},()=>{attempts++;throw Error('network failed');}),/network failed/);
|
|
24
|
+
assert.equal(attempts,2);assert.equal(guard.repeatedFailures,0);
|
|
25
|
+
});
|
package/test-types.mts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {runAnswerPipeline,createProductionObservationProvider,QUESTION_ROUTING_VERSION,type AnswerModelConnector,type AnswerSelection,type AnswerHistoryPort} from '@knights-eye-chess/ai-analysis';
|
|
2
|
+
const connector:AnswerModelConnector={kind:'model-turn',async invoke(request){return {text:request.toolChoice,toolCalls:[],finishReason:'stop',usage:{},response:{messages:[]}};}};
|
|
3
|
+
const selection:AnswerSelection={deliveryId:'x',selectionKey:'x',edgeId:'root',existingAnalysis:null,context:{kind:'position',nodeId:'root',fenBefore:'x',fenAfter:'x',historyUci:[],moveUci:'',moveSan:''}};
|
|
4
|
+
const history:AnswerHistoryPort={};
|
|
5
|
+
void runAnswerPipeline({connector,requestId:'x',question:'x',selected:selection,bridge:history,observationProvider:createProductionObservationProvider({context:selection.context}),prepareRouting:async()=>({question:'x',selectionKey:'x',deliveryId:'x',elapsedMs:0,omitSelectedContext:false,decision:{version:QUESTION_ROUTING_VERSION,status:'fallback',omitSelectedContext:false,includeGameRecord:false,reviewGame:false,reason:'explicit'}})});
|
|
6
|
+
// @ts-expect-error model turn cannot be replaced by an unrelated delegated agent executor
|
|
7
|
+
const wrong:AnswerModelConnector={kind:'agent',execute:async()=>''};
|
|
8
|
+
// @ts-expect-error exact history is required in a selected position context
|
|
9
|
+
const noHistory:AnswerSelection={...selection,context:{nodeId:'x',fenBefore:'x',fenAfter:'x',moveUci:'',moveSan:''}};
|
|
10
|
+
void wrong;void noHistory;
|
|
11
|
+
|
|
12
|
+
// The public analyzer distance is signed mate-in-moves; legacy reads are explicit.
|
|
13
|
+
import {toPositionAnalysis,toLegacyPositionAnalysis,type PositionAnalysisResult,type LegacyPositionAnalysisResult,type PositionAnalysisProvider} from '@knights-eye-chess/ai-analysis';
|
|
14
|
+
const modern:PositionAnalysisResult={engine:'synthetic',engineBuild:'test',command:'test',bestMove:'e2e4',lines:[{rootMove:'e2e4',pv:['e2e4'],depth:18,nodes:null,scoreCp:null,mateInMoves:-3}]};
|
|
15
|
+
const old:LegacyPositionAnalysisResult=toLegacyPositionAnalysis(modern);
|
|
16
|
+
const current:PositionAnalysisResult=toPositionAnalysis(old);
|
|
17
|
+
const analyzer:PositionAnalysisProvider={analyze:async()=>current};
|
|
18
|
+
// @ts-expect-error matePly alone is not the preferred public result contract
|
|
19
|
+
const oldAsCurrent:PositionAnalysisResult=old;
|
|
20
|
+
// @ts-expect-error mate distance is numeric, never a UCI or SAN string
|
|
21
|
+
const badMate:PositionAnalysisResult={...modern,lines:[{...modern.lines[0],mateInMoves:'3'}]};
|
|
22
|
+
void runAnswerPipeline({connector,requestId:'parallel',question:'x',selected:selection,bridge:history,analyzer,preparationAnalyzer:analyzer,parallelPreparation:true,observationProvider:createProductionObservationProvider({context:selection.context}),prepareRouting:async()=>({question:'x',selectionKey:'x',deliveryId:'x',elapsedMs:0,omitSelectedContext:false,decision:{version:QUESTION_ROUTING_VERSION,status:'fallback',omitSelectedContext:false,includeGameRecord:false,reviewGame:false,reason:'explicit'}})});
|
|
23
|
+
void analyzer;void oldAsCurrent;void badMate;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"strict": true,
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"lib": [
|
|
8
|
+
"ES2022",
|
|
9
|
+
"DOM",
|
|
10
|
+
"DOM.Iterable"
|
|
11
|
+
],
|
|
12
|
+
"types": [],
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"emitDeclarationOnly": true,
|
|
16
|
+
"rootDir": "src",
|
|
17
|
+
"outDir": "dist/types"
|
|
18
|
+
},
|
|
19
|
+
"include": [
|
|
20
|
+
"src/**/*.ts"
|
|
21
|
+
]
|
|
22
|
+
}
|