@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,375 @@
|
|
|
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 '@knights-eye-chess/ai-analysis';
|
|
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
|
+
function moveSetup(invoke,options={}) {
|
|
18
|
+
const board=new Chess();board.move('e4');
|
|
19
|
+
const moveContext={...context,kind:undefined,nodeId:'e1',moveUci:'e2e4',moveSan:'e4',fenAfter:board.fen()};
|
|
20
|
+
const question='What is the idea behind this move?';
|
|
21
|
+
return setup(invoke,{question,selected:{deliveryId:'answer-test',selectionKey:'e1',edgeId:'e1',context:moveContext,existingAnalysis:null},
|
|
22
|
+
observationProvider:createProductionObservationProvider({context:moveContext,question}),
|
|
23
|
+
prepareRouting:async()=>({question,selectionKey:'e1',deliveryId:'answer-test',elapsedMs:0,omitSelectedContext:false,
|
|
24
|
+
decision:{version:QUESTION_ROUTING_VERSION,status:'fallback',omitSelectedContext:false,includeGameRecord:false,reviewGame:false,reason:'test-selected-move',answers:{answer_scope:{choice:'move_explanation',confidence:1}}}}),...options});
|
|
25
|
+
}
|
|
26
|
+
test('KE-AI-PORT-003: preparation, classification and observations have separate measured intervals',async t=>{
|
|
27
|
+
let clock=100,turns=0;const events=[],searches=[];
|
|
28
|
+
t.mock.method(performance,'now',()=>clock);
|
|
29
|
+
const successful=analyzer(searches);
|
|
30
|
+
const input=moveSetup(async()=>{turns++;return reply('The pawn occupies the centre.');});
|
|
31
|
+
const route=input.prepareRouting,provider=input.observationProvider;
|
|
32
|
+
input.prepareRouting=async()=>{clock+=7;return route();};
|
|
33
|
+
input.analyzer={async analyze(request){clock+=searches.length?17:11;return successful.analyze(request);}};
|
|
34
|
+
input.bridge={async refreshContext(){clock+=42;return {existingAnalysis:null};}};
|
|
35
|
+
input.observationProvider={async observe(...args){clock+=13;return provider.observe(...args);}};
|
|
36
|
+
input.observableTraceSink=event=>events.push(event);
|
|
37
|
+
const result=await runAnswerPipeline(input);
|
|
38
|
+
assert.equal(result.answer,'The pawn occupies the centre.');assert.equal(turns,1);assert.equal(searches.length,2);
|
|
39
|
+
const phases=events.filter(event=>event.stage.startsWith('answer-pipeline-'));
|
|
40
|
+
assert.deepEqual(phases.filter(e=>e.direction==='output').map(e=>[e.stage,e.payload.durationMs]),[
|
|
41
|
+
['answer-pipeline-routing',7],['answer-pipeline-preparation',28],
|
|
42
|
+
['answer-pipeline-classification-refresh',42],['answer-pipeline-observations',13],
|
|
43
|
+
['answer-pipeline-evidence-collection',0],
|
|
44
|
+
]);
|
|
45
|
+
const ids=new Set();
|
|
46
|
+
for(let i=0;i<phases.length;i+=2){
|
|
47
|
+
const [start,end]=phases.slice(i,i+2);
|
|
48
|
+
for(const event of [start,end]){
|
|
49
|
+
assert.equal(event.model,'deterministic');assert.equal(event.providerRoute,'local-harness');assert.equal(event.reasoningEffort,'none');
|
|
50
|
+
}
|
|
51
|
+
assert.equal(start.direction,'input');assert.equal(end.direction,'output');assert.equal(end.payload.status,'completed');
|
|
52
|
+
assert.equal(start.payload.invocationId,end.payload.invocationId);assert.equal(start.stage,end.stage);
|
|
53
|
+
assert.equal(end.payload.elapsedMs-start.payload.elapsedMs,end.payload.durationMs);
|
|
54
|
+
assert.equal(ids.has(start.payload.invocationId),false);ids.add(start.payload.invocationId);
|
|
55
|
+
}
|
|
56
|
+
assert.equal(events.find(e=>e.stage==='simple-preparation').payload.elapsedMs,28);
|
|
57
|
+
});
|
|
58
|
+
test('KE-AI-PORT-003: classification failure and cancellation retain their original error and a terminal duration',async t=>{
|
|
59
|
+
let clock=0;t.mock.method(performance,'now',()=>clock);
|
|
60
|
+
for(const cancelled of [false,true]){
|
|
61
|
+
const controller=new AbortController(),events=[];let turns=0;
|
|
62
|
+
const failure=new Error('host-private-error-message');
|
|
63
|
+
const input=moveSetup(async()=>{turns++;return reply('Must not run.');},{analyzer:analyzer([]),signal:controller.signal,
|
|
64
|
+
bridge:{async refreshContext(){clock+=23;if(cancelled)controller.abort(failure);throw failure;}},
|
|
65
|
+
observableTraceSink:event=>events.push(event)});
|
|
66
|
+
await assert.rejects(runAnswerPipeline(input),error=>error===failure);assert.equal(turns,0);
|
|
67
|
+
const phase=events.filter(event=>event.stage==='answer-pipeline-classification-refresh');
|
|
68
|
+
assert.equal(phase.length,2);assert.equal(phase[0].direction,'input');assert.equal(phase[1].direction,'error');
|
|
69
|
+
assert.equal(phase[1].payload.status,cancelled?'cancelled':'failed');assert.equal(phase[1].payload.durationMs,23);
|
|
70
|
+
assert.equal(phase[0].payload.invocationId,phase[1].payload.invocationId);
|
|
71
|
+
assert.doesNotMatch(JSON.stringify(phase),/host-private-error-message/);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
test('KE-AI-PORT-003: initial-search refresh failure remains recoverable and supplied routing is not remeasured',async()=>{
|
|
75
|
+
const events=[],input=setup(async()=>reply('The position permits development.'),{analyzer:analyzer([]),
|
|
76
|
+
parallelPreparation:true,bridge:{async refreshContext(){throw Error('refresh unavailable');}},observableTraceSink:event=>events.push(event)});
|
|
77
|
+
input.routingPreparation=await input.prepareRouting();input.prepareRouting=()=>{throw Error('supplied routing must be reused');};
|
|
78
|
+
const result=await runAnswerPipeline(input);
|
|
79
|
+
assert.equal(result.answer,'The position permits development.');
|
|
80
|
+
assert.equal(events.some(event=>event.stage==='answer-pipeline-routing'),false);
|
|
81
|
+
const phase=events.find(event=>event.stage==='answer-pipeline-classification-refresh'&&event.direction==='error');
|
|
82
|
+
assert.equal(phase.payload.origin,'initial-search-preparation');assert.equal(phase.payload.status,'failed');
|
|
83
|
+
assert.ok(Number.isFinite(phase.payload.durationMs));assert.ok(phase.payload.durationMs>=0);
|
|
84
|
+
});
|
|
85
|
+
test('KE-AI-PORT-003: overlapping opening lookup measures only its remaining wait',async t=>{
|
|
86
|
+
let clock=0,finishOpening;const events=[];t.mock.method(performance,'now',()=>clock);
|
|
87
|
+
const pendingOpening=new Promise(resolve=>{finishOpening=resolve;});
|
|
88
|
+
const source={status:'unavailable',contextHash:'no-source',pages:[],requestCount:0,networkRequestCount:0,
|
|
89
|
+
networkRequestedTitles:[],localIndexCandidateCount:0,localIndexPositionCheckCount:0,
|
|
90
|
+
localIndexSelectedTitleCount:0,cacheHitTitleCount:0,timingMs:41,limitations:[]};
|
|
91
|
+
const input=moveSetup(async()=>reply('The pawn occupies the centre.'),{
|
|
92
|
+
analyzer:analyzer([]),openingSession:{initial:()=>pendingOpening},
|
|
93
|
+
bridge:{async refreshContext(){clock+=8;return {existingAnalysis:null};}},
|
|
94
|
+
observableTraceSink:event=>{events.push(event);if(event.stage==='answer-pipeline-opening-wait'&&event.direction==='input'){clock+=33;finishOpening(source);}},
|
|
95
|
+
});
|
|
96
|
+
const result=await runAnswerPipeline(input);
|
|
97
|
+
assert.equal(result.answer,'The pawn occupies the centre.');
|
|
98
|
+
const phase=events.filter(event=>event.stage==='answer-pipeline-opening-wait');
|
|
99
|
+
assert.equal(phase.length,2);assert.equal(phase[0].payload.elapsedMs,8);
|
|
100
|
+
assert.equal(phase[1].payload.durationMs,33);assert.equal(phase[1].payload.elapsedMs,41);
|
|
101
|
+
});
|
|
102
|
+
function deferred() {let resolve,reject;const promise=new Promise((yes,no)=>{resolve=yes;reject=no;});return {promise,resolve,reject};}
|
|
103
|
+
function tick() {return new Promise(resolve=>setImmediate(resolve));}
|
|
104
|
+
function untilAborted(signal) {return new Promise((_,reject)=>{if(signal.aborted)reject(signal.reason);else signal.addEventListener('abort',()=>reject(signal.reason),{once:true});});}
|
|
105
|
+
test('KE-AI-PORT-004: ordinary preparation and classification overlap only when requested and model waits for both',async()=>{
|
|
106
|
+
for(const first of ['preparation','classification']){
|
|
107
|
+
const prepGate=deferred(),classificationGate=deferred(),events=[],searches=[];let classificationStarted=false,turns=0;
|
|
108
|
+
const legal=analyzer(searches);
|
|
109
|
+
const input=moveSetup(async()=>{turns++;return reply('The pawn occupies the centre.');},{parallelPreparation:true,
|
|
110
|
+
analyzer:{async analyze(request){if(!searches.length)await prepGate.promise;return legal.analyze(request);}},
|
|
111
|
+
bridge:{async refreshContext(selected){classificationStarted=true;await classificationGate.promise;selected.existingAnalysis=null;return {existingAnalysis:null};}},
|
|
112
|
+
observableTraceSink:event=>events.push(event)});
|
|
113
|
+
const running=runAnswerPipeline(input);
|
|
114
|
+
await tick();
|
|
115
|
+
assert.equal(classificationStarted,true);assert.equal(turns,0);
|
|
116
|
+
for(const stage of ['answer-pipeline-preparation','answer-pipeline-classification-refresh'])assert.ok(events.some(event=>event.stage===stage&&event.direction==='input'));
|
|
117
|
+
(first==='preparation'?prepGate:classificationGate).resolve();await tick();assert.equal(turns,0);
|
|
118
|
+
(first==='preparation'?classificationGate:prepGate).resolve();const result=await running;
|
|
119
|
+
assert.equal(result.answer,'The pawn occupies the centre.');assert.equal(turns,1);assert.equal(searches.length,2);
|
|
120
|
+
const phases=events.filter(event=>event.stage==='answer-pipeline-preparation'||event.stage==='answer-pipeline-classification-refresh');
|
|
121
|
+
for(const start of phases.filter(event=>event.direction==='input')){
|
|
122
|
+
const end=phases.find(event=>event.payload.invocationId===start.payload.invocationId&&event.direction==='output');
|
|
123
|
+
assert.ok(end);assert.equal(end.payload.status,'completed');assert.ok(Number.isFinite(end.payload.durationMs));
|
|
124
|
+
assert.equal(start.model,'deterministic');assert.equal(start.providerRoute,'local-harness');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
test('KE-AI-PORT-004: opted-in independent opponent probes overlap with classification and retain original evidence order',async()=>{
|
|
129
|
+
const beforeGate=deferred(),afterGate=deferred(),classificationGate=deferred(),requests=[],completed=[];let classificationStarted=false,turns=0;
|
|
130
|
+
const legal=analyzer([]);
|
|
131
|
+
const running=runAnswerPipeline(moveSetup(async()=>{turns++;return reply('The pawn occupies the centre.');},{parallelPreparation:true,
|
|
132
|
+
analyzer:{async analyze(request){requests.push(request);await (request.depth===18?beforeGate:afterGate).promise;completed.push(request.depth);return legal.analyze(request);}},
|
|
133
|
+
bridge:{async refreshContext(){classificationStarted=true;await classificationGate.promise;return {existingAnalysis:null};}},
|
|
134
|
+
}));
|
|
135
|
+
await tick();assert.deepEqual(requests.map(request=>request.depth),[18,20]);assert.equal(classificationStarted,true);assert.equal(turns,0);
|
|
136
|
+
afterGate.resolve();classificationGate.resolve();await tick();assert.deepEqual(completed,[20]);assert.equal(turns,0);
|
|
137
|
+
beforeGate.resolve();const result=await running;assert.equal(turns,1);assert.deepEqual(completed,[20,18]);
|
|
138
|
+
assert.deepEqual(result.preparation.rawRecords.map(record=>record.scope),['hypothetical-opponent-turn-before','actual-after']);
|
|
139
|
+
assert.deepEqual(result.preparation.rawRecords.map(record=>record.request.depth),[18,20]);
|
|
140
|
+
assert.deepEqual(result.preparation.investigationLeads.probes.map(probe=>probe.scope),['hypothetical-opponent-turn-before','actual-after']);
|
|
141
|
+
assert.equal(result.preparation.searchCount,2);assert.equal(result.preparation.searchesSucceeded,2);assert.equal(result.preparation.searchesFailed,0);
|
|
142
|
+
});
|
|
143
|
+
test('KE-AI-PORT-004: ordinary preparation retains serial host behavior by default',async()=>{
|
|
144
|
+
const gate=deferred(),searches=[];let refreshing=false,turns=0;
|
|
145
|
+
const legal=analyzer(searches);
|
|
146
|
+
const running=runAnswerPipeline(moveSetup(async()=>{turns++;return reply('The pawn occupies the centre.');},{
|
|
147
|
+
analyzer:{async analyze(request){if(!searches.length)await gate.promise;return legal.analyze(request);}},
|
|
148
|
+
bridge:{async refreshContext(){refreshing=true;assert.equal(searches.length,2);return {existingAnalysis:null};}},
|
|
149
|
+
}));
|
|
150
|
+
await tick();assert.equal(refreshing,false);assert.equal(turns,0);gate.resolve();await running;assert.equal(refreshing,true);assert.equal(turns,1);
|
|
151
|
+
});
|
|
152
|
+
test('KE-AI-PORT-004: failed parallel classification cancels and drains preparation while retaining the original failure',async()=>{
|
|
153
|
+
const failure=new Error('private-classification-failure'),events=[];let turns=0,observations=0,drained=false;
|
|
154
|
+
const input=moveSetup(async()=>{turns++;return reply('Must not run.');},{parallelPreparation:true,
|
|
155
|
+
analyzer:{async analyze(request){try{await untilAborted(request.signal);}finally{await tick();drained=true;}}},
|
|
156
|
+
bridge:{async refreshContext(){await tick();throw failure;}},
|
|
157
|
+
observationProvider:{async observe(){observations++;throw Error('Must not observe.');}},
|
|
158
|
+
observableTraceSink:event=>events.push(event)});
|
|
159
|
+
await assert.rejects(runAnswerPipeline(input),error=>error===failure);
|
|
160
|
+
assert.equal(drained,true);assert.equal(turns,0);assert.equal(observations,0);
|
|
161
|
+
assert.equal(events.find(event=>event.stage==='answer-pipeline-classification-refresh'&&event.direction==='error').payload.status,'failed');
|
|
162
|
+
assert.equal(events.find(event=>event.stage==='answer-pipeline-preparation'&&event.direction==='error').payload.status,'cancelled');
|
|
163
|
+
assert.doesNotMatch(JSON.stringify(events.filter(event=>event.stage.startsWith('answer-pipeline-'))),/private-classification-failure/);
|
|
164
|
+
});
|
|
165
|
+
test('KE-AI-PORT-004: parent cancellation during overlapping preparation prevents observations and model dispatch',async()=>{
|
|
166
|
+
const controller=new AbortController(),reason=new Error('explicit-parent-cancellation'),events=[];let turns=0,refreshes=0;
|
|
167
|
+
const running=runAnswerPipeline(moveSetup(async()=>{turns++;return reply('Must not run.');},{parallelPreparation:true,signal:controller.signal,
|
|
168
|
+
analyzer:{async analyze(request){await untilAborted(request.signal);}},
|
|
169
|
+
bridge:{async refreshContext(_selected,signal){refreshes++;await untilAborted(signal);}},
|
|
170
|
+
observableTraceSink:event=>events.push(event)}));
|
|
171
|
+
const failed=assert.rejects(running,error=>error===reason);
|
|
172
|
+
await tick();assert.equal(refreshes,1);controller.abort(reason);await failed;assert.equal(turns,0);
|
|
173
|
+
const errors=events.filter(event=>event.stage.startsWith('answer-pipeline-')&&event.direction==='error');
|
|
174
|
+
assert.equal(errors.length,2);assert.ok(errors.every(event=>event.payload.status==='cancelled'));
|
|
175
|
+
assert.equal(events.some(event=>event.stage==='answer-pipeline-observations'),false);
|
|
176
|
+
});
|
|
177
|
+
test('KE-AI-PORT-004: cancellation remains effective when overlapping host operations ignore the signal and return',async()=>{
|
|
178
|
+
const controller=new AbortController(),reason=new Error('cancelled'),gate=deferred(),searches=[];let turns=0;
|
|
179
|
+
const legal=analyzer(searches);
|
|
180
|
+
const running=runAnswerPipeline(moveSetup(async()=>{turns++;return reply('Must not run.');},{parallelPreparation:true,signal:controller.signal,
|
|
181
|
+
analyzer:legal,bridge:{async refreshContext(){await gate.promise;return {existingAnalysis:null};}},
|
|
182
|
+
}));
|
|
183
|
+
const failed=assert.rejects(running,error=>error===reason);await tick();controller.abort(reason);gate.resolve();await failed;assert.equal(turns,0);
|
|
184
|
+
});
|
|
185
|
+
test('KE-AI-PORT-004: preparation analyzer affects upfront opponent probes only while model tools retain the normal analyzer',async()=>{
|
|
186
|
+
const preparationSearches=[],toolSearches=[];let turn=0;
|
|
187
|
+
const result=await runAnswerPipeline(moveSetup(async()=>++turn===1?reply('',[{toolCallId:'compare',toolName:'compare_moves',input:{phase:'before',movesUci:['e2e4','d2d4']}}]):reply('Both central pawn moves are available.'),{
|
|
188
|
+
parallelPreparation:true,analyzer:analyzer(toolSearches),preparationAnalyzer:analyzer(preparationSearches),
|
|
189
|
+
bridge:{async refreshContext(){return {existingAnalysis:null};}},
|
|
190
|
+
}));
|
|
191
|
+
assert.equal(result.answer,'Both central pawn moves are available.');assert.deepEqual(preparationSearches.map(request=>request.depth),[18,20]);
|
|
192
|
+
assert.ok(toolSearches.length>0);assert.ok(toolSearches.every(request=>request.depth===18));
|
|
193
|
+
});
|
|
194
|
+
test('KE-AI-PORT-002: all failed preparation and tool searches retain causes and disclose degraded answers',async()=>{
|
|
195
|
+
let turn=0,calls=0;const events=[];
|
|
196
|
+
const result=await runAnswerPipeline(moveSetup(async request=>{
|
|
197
|
+
turn++;
|
|
198
|
+
if(turn===1)return reply('',[{toolCallId:'compare-before',toolName:'compare_moves',input:{phase:'before',movesUci:['e2e4','d2d4']}}]);
|
|
199
|
+
if(turn===2){assert.match(JSON.stringify(request.messages),/engine-503/);return reply('',[{toolCallId:'compare-after',toolName:'compare_moves',input:{phase:'after',movesUci:['e7e5','c7c5']}}]);}
|
|
200
|
+
return reply('The pawn occupies the centre.');
|
|
201
|
+
},{analyzer:{async analyze(){calls++;throw Object.assign(new Error('engine-503: unavailable'),{code:'engine-unavailable',status:503,cause:new Error('upstream unreachable'),stack:'private stack',headers:{Authorization:'private header'}});}},observableTraceSink:e=>events.push(e)}));
|
|
202
|
+
assert.equal(result.status,'diagnostic');assert.equal(calls,4);assert.equal(result.searchesUsed,4);
|
|
203
|
+
assert.equal(result.searchesSucceeded,0);assert.equal(result.searchesFailed,4);assert.equal(result.engineEvidenceStatus,'unavailable');
|
|
204
|
+
assert.match(result.answer,/^Engine analysis was unavailable.*not been verified with Stockfish\.\n\nThe pawn occupies/);
|
|
205
|
+
assert.equal(result.preparation.searchesFailed,2);assert.deepEqual(result.preparation.rawRecords,[]);
|
|
206
|
+
for(const entry of result.preparation.investigationLeads.unavailable){assert.equal(entry.error,'engine-503: unavailable');assert.equal(entry.code,'engine-unavailable');assert.equal(entry.status,503);assert.equal(entry.cause,'upstream unreachable');assert.equal('stack' in entry,false);assert.equal('headers' in entry,false);}
|
|
207
|
+
assert.ok(events.some(e=>e.stage==='simple-engine-evidence'&&e.payload.status==='degraded'&&e.payload.searchesFailed===4));
|
|
208
|
+
assert.doesNotMatch(JSON.stringify(result.preparation),/private stack|private header/);
|
|
209
|
+
});
|
|
210
|
+
test('KE-AI-PORT-002: final causal claim ranges include outage disclosure and UTF-16 paragraph offsets',async()=>{
|
|
211
|
+
const prose='A quiet plan ♞ 🧩.\n\n[1. e4](game:0:e2e4) opens the bishop on f1 toward a6.\n\nA second view ♟: [1. e4](game:0:e2e4) clears the bishop on f1 toward a6.';
|
|
212
|
+
for(const outage of [true,false]) {
|
|
213
|
+
const events=[];
|
|
214
|
+
const result=await runAnswerPipeline(moveSetup(async()=>reply(prose),{
|
|
215
|
+
analyzer:outage?{async analyze(){throw Error('deterministic engine outage');}}:analyzer([]),
|
|
216
|
+
observableTraceSink:event=>events.push(event),
|
|
217
|
+
}));
|
|
218
|
+
assert.equal(result.engineEvidenceStatus,outage?'unavailable':'available');
|
|
219
|
+
assert.equal(result.referenceValidation.causalClaimCheckStatus,'completed');
|
|
220
|
+
const checks=result.referenceValidation.causalClaimChecks;
|
|
221
|
+
assert.equal(checks.length,2);
|
|
222
|
+
assert.ok(prose.length>[...prose].length,'fixture includes a UTF-16 surrogate pair');
|
|
223
|
+
for(const check of checks) {
|
|
224
|
+
assert.equal(result.answer.slice(check.claimRange.start,check.claimRange.end),check.claim);
|
|
225
|
+
assert.equal(result.answer.slice(check.sentenceContext.start,check.sentenceContext.end),check.sentenceContext.text);
|
|
226
|
+
assert.match(check.claim,/bishop on f1 toward a6/);
|
|
227
|
+
}
|
|
228
|
+
assert.deepEqual(events.find(event=>event.stage==='simple-reference-validation').payload.causalClaimChecks,checks);
|
|
229
|
+
if(outage)assert.match(result.answer,/^Engine analysis was unavailable/);
|
|
230
|
+
else assert.equal(result.answer,prose);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
test('KE-AI-PORT-002: endpoint comparison descriptions explicitly require final analysis',()=>{
|
|
234
|
+
for(const name of ['test_line','test_game_line'])assert.match(simpleToolDescription(name),/compareFinalMovesUci requires analyzeFinal=true/);
|
|
235
|
+
});
|
|
236
|
+
test('KE-AI-PORT-002: recovery after failed preparation counts real successes and clears all-failed disclosure',async()=>{
|
|
237
|
+
let turn=0,calls=0;const successful=analyzer([]);
|
|
238
|
+
const result=await runAnswerPipeline(moveSetup(async()=>++turn===1?reply('',[{toolCallId:'recovered',toolName:'compare_moves',input:{phase:'before',movesUci:['e2e4','d2d4']}}]):reply('Both central pawn moves are available.'),
|
|
239
|
+
{analyzer:{async analyze(request){calls++;if(calls<=2)throw Error('offline');return successful.analyze(request);}}}));
|
|
240
|
+
assert.equal(result.status,'diagnostic');assert.equal(result.searchesUsed,3);assert.equal(result.searchesSucceeded,1);assert.equal(result.searchesFailed,2);
|
|
241
|
+
assert.equal(result.engineEvidenceStatus,'available');assert.equal(result.answer,'Both central pawn moves are available.');
|
|
242
|
+
});
|
|
243
|
+
test('KE-AI-PORT-002: initial search failures and later successes count analyzer dispatches, not tools',async()=>{
|
|
244
|
+
let turn=0,calls=0;const successful=analyzer([]);
|
|
245
|
+
const result=await runAnswerPipeline(setup(async()=>++turn===1?reply('',[{toolCallId:'recover',toolName:'compare_moves',input:{phase:'before',movesUci:['e2e4','d2d4']}}]):reply('A bounded assessment.'),
|
|
246
|
+
{analyzer:{async analyze(request){calls++;if(calls===1)throw Error('initial engine unavailable');return successful.analyze(request);}}}));
|
|
247
|
+
assert.equal(result.initialSearches[0].status,'failed');assert.equal(result.searchesUsed,2);assert.equal(result.searchesSucceeded,1);assert.equal(result.searchesFailed,1);assert.equal(result.engineEvidenceStatus,'available');
|
|
248
|
+
});
|
|
249
|
+
test('KE-AI-PORT-002: endpoint failures within one successful tool remain failed searches',async()=>{
|
|
250
|
+
let calls=0;const successful=analyzer([]);
|
|
251
|
+
const result=await runAnswerPipeline(setup(async()=>reply('Only the initial engine lines are available.'),
|
|
252
|
+
{analyzer:{async analyze(request){calls++;if(calls>1)throw Error('endpoint unavailable');const value=await successful.analyze(request);return {...value,lines:value.lines.map(line=>({...line,pv:[line.rootMove]}))};}}}));
|
|
253
|
+
assert.equal(result.initialSearches[0].status,'completed');assert.equal(calls,4);assert.equal(result.searchesUsed,4);
|
|
254
|
+
assert.equal(result.searchesSucceeded,1);assert.equal(result.searchesFailed,3);assert.equal(result.engineEvidenceStatus,'available');
|
|
255
|
+
assert.equal(result.answer,'Only the initial engine lines are available.');
|
|
256
|
+
});
|
|
257
|
+
test('KE-AI-PORT-002: a partial search batch counts failed dispatches separately from successful queries',async()=>{
|
|
258
|
+
let turn=0,calls=0;const successful=analyzer([]);
|
|
259
|
+
const result=await runAnswerPipeline(setup(async request=>{
|
|
260
|
+
if(++turn===1)return reply('',[{toolCallId:'partial-batch',toolName:'search_batch',input:{requests:[
|
|
261
|
+
{queryId:'central',tool:'compare_moves',input:{phase:'before',movesUci:['e2e4','d2d4']}},
|
|
262
|
+
{queryId:'knights',tool:'compare_moves',input:{phase:'before',movesUci:['g1f3','b1c3']}}]}}]);
|
|
263
|
+
assert.match(JSON.stringify(request.messages),/batch search unavailable/);return reply('The central pawn comparison is available.');
|
|
264
|
+
},{analyzer:{async analyze(request){calls++;if(calls===2)return successful.analyze(request);throw Error('batch search unavailable');}}}));
|
|
265
|
+
assert.equal(result.toolRuns[0].status,'completed');assert.deepEqual(result.toolRuns[0].result.searches.map(s=>s.status),['completed','failed']);
|
|
266
|
+
assert.equal(calls,3);assert.equal(result.searchesUsed,3);assert.equal(result.searchesSucceeded,1);assert.equal(result.searchesFailed,2);assert.equal(result.engineEvidenceStatus,'available');
|
|
267
|
+
assert.equal(result.answer,'The central pawn comparison is available.');
|
|
268
|
+
});
|
|
269
|
+
test('KE-AI-PORT-002: no engine requested and cancelled searches do not become outage disclosures',async()=>{
|
|
270
|
+
const noEngine=await runAnswerPipeline(setup(async()=>reply('A positional explanation.')));
|
|
271
|
+
assert.equal(noEngine.engineEvidenceStatus,'not-requested');assert.equal(noEngine.searchesFailed,0);assert.equal(noEngine.answer,'A positional explanation.');
|
|
272
|
+
const controller=new AbortController();let calls=0,turn=0;const successful=analyzer([]);
|
|
273
|
+
const cancelled=await runAnswerPipeline(moveSetup(async()=>++turn===1?reply('',[{toolCallId:'cancel-search',toolName:'compare_moves',input:{phase:'before',movesUci:['e2e4','d2d4']}}]):reply('This must not run.'),
|
|
274
|
+
{signal:controller.signal,analyzer:{async analyze(request){calls++;if(calls<=2)return successful.analyze(request);controller.abort(new Error('explicit cancellation'));throw Error('stopped');}}}));
|
|
275
|
+
assert.equal(cancelled.status,'failure');assert.equal(cancelled.answer,'');assert.equal(cancelled.searchesUsed,3);assert.equal(cancelled.searchesFailed,0);
|
|
276
|
+
});
|
|
277
|
+
test('KE-AI-PORT-001: existing preparation, genuine legal tools, resumable evidence and composition',async()=>{
|
|
278
|
+
let turn=0;const searches=[];const events=[];
|
|
279
|
+
const result=await runAnswerPipeline(setup(async request=>{
|
|
280
|
+
turn++;assert.match(request.system,/Knight/);assert.ok(request.tools.every(t=>t.inputSchema&&typeof t.inputSchema==='object'));
|
|
281
|
+
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']}}]);}
|
|
282
|
+
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}}]);}
|
|
283
|
+
assert.match(JSON.stringify(request.messages),/line-1/);return reply('Both sides can develop their pieces.');
|
|
284
|
+
},{analyzer:analyzer(searches),observableTraceSink:e=>events.push(e)}));
|
|
285
|
+
assert.equal(result.status,'diagnostic');assert.equal(result.answer,'Both sides can develop their pieces.');assert.equal(turn,3);
|
|
286
|
+
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'));
|
|
287
|
+
});
|
|
288
|
+
test('KE-AI-PORT-001: canonical six-turn reserve forces tool-free composition',async()=>{
|
|
289
|
+
let turn=0;
|
|
290
|
+
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']}}]);}));
|
|
291
|
+
assert.equal(turn,6);assert.equal(result.completionReason,'model-calls');assert.equal(result.toolCallsUsed,5);
|
|
292
|
+
});
|
|
293
|
+
test('KE-AI-PORT-001: search batch budget rejects nine requests before host execution',async()=>{
|
|
294
|
+
let turn=0;
|
|
295
|
+
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.');}));
|
|
296
|
+
assert.equal(result.toolRuns[0].status,'failed');assert.equal(result.searchesUsed,0);
|
|
297
|
+
});
|
|
298
|
+
test('KE-AI-PORT-001: cancellation after model response prevents tool execution',async()=>{
|
|
299
|
+
const controller=new AbortController();
|
|
300
|
+
const result=await runAnswerPipeline(setup(async()=>{controller.abort();return reply('',[{toolCallId:'cancelled',toolName:'inspect_position',input:{phase:'before',squares:['e2']}}]);},{signal:controller.signal}));
|
|
301
|
+
assert.equal(result.status,'failure');assert.equal(result.toolRuns.length,0);assert.equal(result.answer,'');
|
|
302
|
+
});
|
|
303
|
+
test('KE-AI-PORT-001: unknown usage reserves output allowance without claiming measurement',async()=>{
|
|
304
|
+
const result=await runAnswerPipeline(setup(async()=>({...reply('A bounded answer.'),usage:{}})));
|
|
305
|
+
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);
|
|
306
|
+
});
|
|
307
|
+
test('KE-AI-PORT-001: exact recorded history is frozen and retrieved through host port',async()=>{
|
|
308
|
+
let reads=0,turn=0;
|
|
309
|
+
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};
|
|
310
|
+
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;}}}));
|
|
311
|
+
assert.equal(reads,1);assert.equal(result.acquiredMainLines.length,2);assert.equal(result.acquiredMainLines[0].moves[1].san,'e5');
|
|
312
|
+
});
|
|
313
|
+
test('KE-AI-PORT-001: malformed connector batches fail closed before any valid prefix executes',async()=>{
|
|
314
|
+
const valid={toolCallId:'first',toolName:'inspect_position',input:{phase:'before',squares:['e2']}};
|
|
315
|
+
for(const invalid of [{...valid},{...valid,toolCallId:''},{...valid,toolCallId:'second',toolName:'unknown'},{...valid,toolCallId:'second',input:[]},{...valid,toolCallId:'second',input:null}]) {
|
|
316
|
+
const result=await runAnswerPipeline(setup(async()=>reply('',[valid,invalid])));
|
|
317
|
+
assert.equal(result.status,'failure');assert.equal(result.toolRuns.length,0);assert.equal(result.toolCallsUsed,0);
|
|
318
|
+
}
|
|
319
|
+
const result=await runAnswerPipeline(setup(async()=>reply('',Array.from({length:33},(_,i)=>({...valid,toolCallId:'call-'+i})))));
|
|
320
|
+
assert.equal(result.status,'failure');assert.equal(result.toolRuns.length,0);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test('KE-REASON-034: signed-moves adapters preserve attributed legacy records and conditional segments',async()=>{
|
|
324
|
+
const provenance={kind:'restored-fallback',engineBuild:'historical-build',originatingDepth:11};
|
|
325
|
+
const scope={fen,perspective:'white',positiveMeans:'advantage for perspective color',scoreCpUnit:'centipawns',matePlyUnit:'signed mate-in-moves (legacy field name, not plies)'};
|
|
326
|
+
const legacy={engine:'historical-engine',engineBuild:'historical-build',command:'historical',bestMove:'e2e4',cacheProvenance:'browser-completed-cache',sourceCacheProvenance:'widget-analysis-cache',
|
|
327
|
+
lines:[{rootMove:'e2e4',pv:['e2e4'],rawPv:['e2e4'],depth:11,nodes:42,scoreCp:null,matePly:-3,scoreScope:scope,scoreProvenance:provenance,
|
|
328
|
+
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'}]}}]};
|
|
329
|
+
const frozen=JSON.stringify(legacy);
|
|
330
|
+
const preferred=toPositionAnalysis(legacy);
|
|
331
|
+
assert.equal(preferred.lines[0].mateInMoves,-3);assert.equal('matePly' in preferred.lines[0],false);
|
|
332
|
+
assert.equal(preferred.lines[0].pvCompletion.segments[0].mateInMoves,2);
|
|
333
|
+
assert.equal(preferred.lines[0].scoreScope.mateInMovesUnit,'signed mate-in-moves');
|
|
334
|
+
assert.equal('matePlyUnit' in preferred.lines[0].scoreScope,false);
|
|
335
|
+
assert.deepEqual(preferred.lines[0].scoreProvenance,provenance);
|
|
336
|
+
assert.deepEqual(toLegacyPositionAnalysis(preferred),legacy);
|
|
337
|
+
assert.equal(JSON.stringify(legacy),frozen);
|
|
338
|
+
for(const mate of [null,0,3,-5]){
|
|
339
|
+
const result={...legacy,lines:[{...legacy.lines[0],matePly:mate}]};
|
|
340
|
+
assert.deepEqual(toLegacyPositionAnalysis(toPositionAnalysis(result)),result);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test('KE-REASON-034: conflicting aliases, invalid distances and units fail closed including segments',async()=>{
|
|
345
|
+
const result={engine:'test',engineBuild:'test',command:'test',bestMove:null,lines:[{rootMove:'e2e4',pv:['e2e4'],depth:1,nodes:null,scoreCp:null,mateInMoves:2}]};
|
|
346
|
+
for(const convert of [toPositionAnalysis,toLegacyPositionAnalysis]){
|
|
347
|
+
for(const value of [{matePly:3},{matePly:null},{mateInMoves:1.5},{mateInMoves:Infinity},{mateInMoves:undefined},{mateInMoves:'2'},{scoreScope:{mateInMovesUnit:'plies'}},{scoreScope:{matePlyUnit:'plies'}}]){
|
|
348
|
+
assert.throws(()=>convert({...result,lines:[{...result.lines[0],...value}]}),TypeError);
|
|
349
|
+
}
|
|
350
|
+
assert.throws(()=>convert({...result,lines:[{...result.lines[0],pvCompletion:{segments:[{matePly:1,mateInMoves:2}]}}]}),/Conflicting/);
|
|
351
|
+
const {mateInMoves:_,...missing}=result.lines[0];void _;
|
|
352
|
+
assert.throws(()=>convert({...result,lines:[missing]}),/Missing/);
|
|
353
|
+
assert.equal(convert({...result,lines:[{...result.lines[0],matePly:2}]}).lines.length,1);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test('KE-REASON-034: preferred analyzer and explicit legacy analyzer produce identical answer evidence',async()=>{
|
|
358
|
+
const oldSearches=[],newSearches=[];
|
|
359
|
+
const old=await runAnswerPipeline(setup(async()=>reply('The evidence is bounded.'),{analyzer:analyzer(oldSearches)}));
|
|
360
|
+
const modern=await runAnswerPipeline(setup(async()=>reply('The evidence is bounded.'),{analyzer:toPositionAnalysisProvider(analyzer(newSearches))}));
|
|
361
|
+
assert.equal(modern.answer,old.answer);assert.equal(modern.status,old.status);
|
|
362
|
+
assert.deepEqual(modern.presentedEvidence,old.presentedEvidence);
|
|
363
|
+
assert.equal(newSearches.length,oldSearches.length);
|
|
364
|
+
assert.ok(newSearches.length>0);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test('KE-REASON-034: analyzer adapters forward exact requests and reject pre/post completion cancellation',async()=>{
|
|
368
|
+
for(const wrap of [toPositionAnalysisProvider,toLegacyPositionAnalysisProvider]){
|
|
369
|
+
let calls=0;const controller=new AbortController();
|
|
370
|
+
const request={fen,depth:18,multiPv:3,rootMoves:[],signal:controller.signal};
|
|
371
|
+
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:[]};}});
|
|
372
|
+
await assert.rejects(provider.analyze(request),/stop-adapter/);assert.equal(calls,1);
|
|
373
|
+
await assert.rejects(provider.analyze(request),/stop-adapter/);assert.equal(calls,1);
|
|
374
|
+
}
|
|
375
|
+
});
|