@knights-eye-chess/ai-analysis 0.1.0-dev.18 → 0.1.0-dev.24
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/NPM-PUBLISHING.md +5 -5
- package/build.mjs +5 -1
- package/dist/chunk-AJJP7FYT.js +59 -0
- package/dist/engine-runtime-closure.json +6 -0
- package/dist/engine.js +6 -0
- package/dist/index.js +5 -1
- package/dist/owner-tests/engine-analysis-provider.test.mjs +169 -0
- package/dist/research.js +5 -1
- package/dist/runtime-closure.json +2 -1
- package/dist/types/chess-reasoning/answer-pipeline-exports.d.ts +2 -0
- package/dist/types/chess-reasoning/engine-analysis-provider.d.ts +42 -0
- package/docs/API.md +24 -0
- package/npm-release.json +1 -1
- package/package.json +6 -2
- package/src/chess-reasoning/answer-pipeline-exports.ts +2 -0
- package/src/chess-reasoning/engine-analysis-provider.ts +62 -0
- package/test/owner/engine-analysis-provider.test.mjs +70 -0
- package/test-types.mts +5 -0
- /package/dist/{chunk-TYQ5WIL2.js → chunk-V6NM6RB2.js} +0 -0
package/NPM-PUBLISHING.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
The owner authorized public npm prereleases on 10 October 2026. Use **`next`** and preserve any existing `latest` tag. Licensing is documented in [LICENSING.md](LICENSING.md).
|
|
4
4
|
|
|
5
|
-
The first registry prerelease
|
|
6
|
-
`0.1.0-dev.
|
|
7
|
-
This
|
|
5
|
+
The first registry prerelease was `0.1.0-dev.14`. This checkout prepares the
|
|
6
|
+
local `0.1.0-dev.24` candidate with Move Classifier `0.1.0-dev.16-board-offers.3`.
|
|
7
|
+
This candidate has not been published; check the registry before relying on `@next`.
|
|
8
8
|
|
|
9
9
|
## Prepare the exact candidate
|
|
10
10
|
|
|
@@ -15,9 +15,9 @@ npm ci
|
|
|
15
15
|
npm run publish:prepare
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
Review `artifacts/knights-eye-chess-ai-analysis-0.1.0-dev.
|
|
18
|
+
Review `artifacts/knights-eye-chess-ai-analysis-0.1.0-dev.24.tgz`, including its files, exports, declarations, licenses and exact registry dependencies. Reusable source and offline owner tests are intentional; application traces, credentials, vendor archives and source lockfiles are excluded. The checkout stays `private: true`; publish only the staged archive. Archive contents are immutable: use a new version for changed bytes.
|
|
19
19
|
|
|
20
|
-
Before every coordinated release, run `node tools/release-tooling/sync.mjs --check` in the reviewed app checkout
|
|
20
|
+
Before every coordinated release, run `node tools/release-tooling/sync.mjs --check` in the reviewed app checkout against its current component registry. Record the app and component commits and the result.
|
|
21
21
|
|
|
22
22
|
## Publish through GitHub Actions
|
|
23
23
|
|
package/build.mjs
CHANGED
|
@@ -4,7 +4,11 @@ import {readFileSync,writeFileSync,readdirSync,copyFileSync,rmSync} from 'node:f
|
|
|
4
4
|
import {createRequire} from 'node:module';
|
|
5
5
|
const require=createRequire(import.meta.url);
|
|
6
6
|
rmSync('dist',{recursive:true,force:true});
|
|
7
|
-
const result=await build({entryPoints:{index:'src/chess-reasoning/answer-pipeline-exports.ts',research:'src/chess-reasoning/research-exports.ts',shared:'src/shared-exports.ts'},bundle:true,format:'esm',platform:'neutral',target:'es2022',packages:'external',splitting:true,outdir:'dist',metafile:true});
|
|
7
|
+
const result=await build({entryPoints:{index:'src/chess-reasoning/answer-pipeline-exports.ts',research:'src/chess-reasoning/research-exports.ts',shared:'src/shared-exports.ts',engine:'src/chess-reasoning/engine-analysis-provider.ts'},bundle:true,format:'esm',platform:'neutral',target:'es2022',packages:'external',splitting:true,outdir:'dist',metafile:true});
|
|
8
|
+
const engine=await build({entryPoints:['src/chess-reasoning/engine-analysis-provider.ts'],bundle:true,format:'esm',platform:'neutral',target:'es2022',packages:'external',write:false,metafile:true});
|
|
9
|
+
const engineImports=Object.values(engine.metafile.outputs).flatMap(output=>output.imports);
|
|
10
|
+
if(engineImports.length)throw Error('Engine provider must have no runtime dependencies: '+engineImports.map(item=>item.path).join(', '));
|
|
11
|
+
writeFileSync('dist/engine-runtime-closure.json',JSON.stringify({runtimeInputs:Object.keys(engine.metafile.inputs).sort(),externalImports:[]},null,2)+'\n');
|
|
8
12
|
const shared=await build({entryPoints:['src/shared-exports.ts'],bundle:true,format:'esm',platform:'neutral',target:'es2022',packages:'external',write:false,metafile:true});
|
|
9
13
|
const sharedImports=[...new Set(Object.values(shared.metafile.outputs).flatMap(output=>output.imports.filter(item=>item.external).map(item=>item.path)))];
|
|
10
14
|
if(sharedImports.some(name=>name === 'ai' || name.startsWith('@ai-sdk/') || name.startsWith('@knights-eye-chess/move-classifier')))throw Error('Model or classifier runtime leaked into shared helpers: '+sharedImports.join(', '));
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/chess-reasoning/engine-analysis-provider.ts
|
|
2
|
+
function createEngineAnalysisProvider(client) {
|
|
3
|
+
return { async analyze(request) {
|
|
4
|
+
request.signal?.throwIfAborted();
|
|
5
|
+
const result = await client.search({
|
|
6
|
+
fen: request.fen,
|
|
7
|
+
limit: { depth: request.depth },
|
|
8
|
+
multiPv: request.multiPv,
|
|
9
|
+
rootMoves: request.rootMoves,
|
|
10
|
+
...request.rootMoves.length ? { requireRootCoverage: true } : {},
|
|
11
|
+
signal: request.signal
|
|
12
|
+
});
|
|
13
|
+
request.signal?.throwIfAborted();
|
|
14
|
+
const command = "go depth " + request.depth + (request.rootMoves.length ? " searchmoves " + request.rootMoves.join(" ") : "");
|
|
15
|
+
if (result.noMove === true) {
|
|
16
|
+
if (result.bestmove !== null || result.candidates.length !== 0 || !result.engineBuild) {
|
|
17
|
+
throw new Error("Invalid UCI no-move result.");
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
engine: result.engineBuild,
|
|
21
|
+
engineBuild: result.engineBuild,
|
|
22
|
+
command,
|
|
23
|
+
bestMove: null,
|
|
24
|
+
cacheProvenance: "engine",
|
|
25
|
+
lines: [],
|
|
26
|
+
...result.diagnostics ? { diagnostics: result.diagnostics } : {}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const build = result.candidates[0]?.scoreProvenance?.engineBuild;
|
|
30
|
+
if (typeof build !== "string" || !build) throw new Error("Engine returned no attributed build identity.");
|
|
31
|
+
if (!result.bestmove || result.bestmove !== result.candidates[0]?.pv[0] || result.engineBuild && result.engineBuild !== build || result.weightsId && result.weightsId !== result.candidates[0]?.scoreProvenance?.weightsId) {
|
|
32
|
+
throw new Error("Engine returned an incomplete or inconsistent best move.");
|
|
33
|
+
}
|
|
34
|
+
if (result.candidates.some((line) => line.scoreProvenance?.engineBuild !== build || line.scoreProvenance?.weightsId !== result.candidates[0]?.scoreProvenance?.weightsId)) {
|
|
35
|
+
throw new Error("Engine returned inconsistent build identity.");
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
engine: build,
|
|
39
|
+
engineBuild: build,
|
|
40
|
+
command,
|
|
41
|
+
bestMove: result.bestmove,
|
|
42
|
+
cacheProvenance: "engine",
|
|
43
|
+
...result.diagnostics ? { diagnostics: result.diagnostics } : {},
|
|
44
|
+
lines: result.candidates.map((candidate) => ({
|
|
45
|
+
rootMove: candidate.pv[0],
|
|
46
|
+
pv: candidate.pv,
|
|
47
|
+
depth: candidate.depth,
|
|
48
|
+
nodes: candidate.nodes ?? null,
|
|
49
|
+
scoreCp: candidate.scoreCp,
|
|
50
|
+
mateInMoves: candidate.mate,
|
|
51
|
+
scoreProvenance: candidate.scoreProvenance ? { ...candidate.scoreProvenance } : void 0
|
|
52
|
+
}))
|
|
53
|
+
};
|
|
54
|
+
} };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
createEngineAnalysisProvider
|
|
59
|
+
};
|
package/dist/engine.js
ADDED
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
simpleSystemPolicy,
|
|
18
18
|
simpleToolDescription,
|
|
19
19
|
validateSimpleToolInput
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-V6NM6RB2.js";
|
|
21
21
|
import {
|
|
22
22
|
DEFAULT_REASONING_RUN_CONTROLS,
|
|
23
23
|
EVIDENCE_SHARING_VERSION,
|
|
@@ -72,6 +72,9 @@ import {
|
|
|
72
72
|
validateReasoningPositionContext,
|
|
73
73
|
validateSimpleAgentConfig
|
|
74
74
|
} from "./chunk-6OXAG5H3.js";
|
|
75
|
+
import {
|
|
76
|
+
createEngineAnalysisProvider
|
|
77
|
+
} from "./chunk-AJJP7FYT.js";
|
|
75
78
|
export {
|
|
76
79
|
DEFAULT_REASONING_RUN_CONTROLS,
|
|
77
80
|
EVIDENCE_SHARING_VERSION,
|
|
@@ -97,6 +100,7 @@ export {
|
|
|
97
100
|
classifyReviewSubject,
|
|
98
101
|
createContextEvidence,
|
|
99
102
|
createDefaultSaliencePolicy,
|
|
103
|
+
createEngineAnalysisProvider,
|
|
100
104
|
createNativeAnswerConnector,
|
|
101
105
|
createPocToolHost,
|
|
102
106
|
createProductionObservationProvider,
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// test/owner/engine-analysis-provider.test.mjs
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
|
|
5
|
+
// src/chess-reasoning/engine-analysis-provider.ts
|
|
6
|
+
function createEngineAnalysisProvider(client) {
|
|
7
|
+
return { async analyze(request2) {
|
|
8
|
+
request2.signal?.throwIfAborted();
|
|
9
|
+
const result = await client.search({
|
|
10
|
+
fen: request2.fen,
|
|
11
|
+
limit: { depth: request2.depth },
|
|
12
|
+
multiPv: request2.multiPv,
|
|
13
|
+
rootMoves: request2.rootMoves,
|
|
14
|
+
...request2.rootMoves.length ? { requireRootCoverage: true } : {},
|
|
15
|
+
signal: request2.signal
|
|
16
|
+
});
|
|
17
|
+
request2.signal?.throwIfAborted();
|
|
18
|
+
const command = "go depth " + request2.depth + (request2.rootMoves.length ? " searchmoves " + request2.rootMoves.join(" ") : "");
|
|
19
|
+
if (result.noMove === true) {
|
|
20
|
+
if (result.bestmove !== null || result.candidates.length !== 0 || !result.engineBuild) {
|
|
21
|
+
throw new Error("Invalid UCI no-move result.");
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
engine: result.engineBuild,
|
|
25
|
+
engineBuild: result.engineBuild,
|
|
26
|
+
command,
|
|
27
|
+
bestMove: null,
|
|
28
|
+
cacheProvenance: "engine",
|
|
29
|
+
lines: [],
|
|
30
|
+
...result.diagnostics ? { diagnostics: result.diagnostics } : {}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const build = result.candidates[0]?.scoreProvenance?.engineBuild;
|
|
34
|
+
if (typeof build !== "string" || !build) throw new Error("Engine returned no attributed build identity.");
|
|
35
|
+
if (!result.bestmove || result.bestmove !== result.candidates[0]?.pv[0] || result.engineBuild && result.engineBuild !== build || result.weightsId && result.weightsId !== result.candidates[0]?.scoreProvenance?.weightsId) {
|
|
36
|
+
throw new Error("Engine returned an incomplete or inconsistent best move.");
|
|
37
|
+
}
|
|
38
|
+
if (result.candidates.some((line2) => line2.scoreProvenance?.engineBuild !== build || line2.scoreProvenance?.weightsId !== result.candidates[0]?.scoreProvenance?.weightsId)) {
|
|
39
|
+
throw new Error("Engine returned inconsistent build identity.");
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
engine: build,
|
|
43
|
+
engineBuild: build,
|
|
44
|
+
command,
|
|
45
|
+
bestMove: result.bestmove,
|
|
46
|
+
cacheProvenance: "engine",
|
|
47
|
+
...result.diagnostics ? { diagnostics: result.diagnostics } : {},
|
|
48
|
+
lines: result.candidates.map((candidate) => ({
|
|
49
|
+
rootMove: candidate.pv[0],
|
|
50
|
+
pv: candidate.pv,
|
|
51
|
+
depth: candidate.depth,
|
|
52
|
+
nodes: candidate.nodes ?? null,
|
|
53
|
+
scoreCp: candidate.scoreCp,
|
|
54
|
+
mateInMoves: candidate.mate,
|
|
55
|
+
scoreProvenance: candidate.scoreProvenance ? { ...candidate.scoreProvenance } : void 0
|
|
56
|
+
}))
|
|
57
|
+
};
|
|
58
|
+
} };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// test/owner/engine-analysis-provider.test.mjs
|
|
62
|
+
var request = { fen: "root", depth: 18, multiPv: 3, rootMoves: [], minimumPvPlies: 10 };
|
|
63
|
+
var line = (move, mate = null) => ({
|
|
64
|
+
pv: [move],
|
|
65
|
+
depth: 18,
|
|
66
|
+
nodes: 42,
|
|
67
|
+
scoreCp: mate === null ? 21 : null,
|
|
68
|
+
mate,
|
|
69
|
+
scoreProvenance: { kind: "root-search", engineBuild: "Stockfish test", fen: "root", originatingDepth: 18, reportedDepth: 18 }
|
|
70
|
+
});
|
|
71
|
+
test("maps one completed search without padding fewer lines or short PVs", async () => {
|
|
72
|
+
let calls = 0, received;
|
|
73
|
+
const candidates = [line("g7g8", -3), line("g7f7")];
|
|
74
|
+
const diagnostics = { processReused: true };
|
|
75
|
+
const provider = createEngineAnalysisProvider({ async search(input) {
|
|
76
|
+
calls++;
|
|
77
|
+
received = input;
|
|
78
|
+
return { bestmove: "g7g8", candidates, diagnostics };
|
|
79
|
+
} });
|
|
80
|
+
const result = await provider.analyze(request);
|
|
81
|
+
assert.equal(calls, 1);
|
|
82
|
+
assert.deepEqual(received, { fen: "root", limit: { depth: 18 }, multiPv: 3, rootMoves: [], signal: void 0 });
|
|
83
|
+
assert.equal(result.lines.length, 2);
|
|
84
|
+
assert.deepEqual(result.lines[0].pv, ["g7g8"]);
|
|
85
|
+
assert.equal(result.lines[0].mateInMoves, -3);
|
|
86
|
+
assert.equal(result.lines[0].scoreCp, null);
|
|
87
|
+
assert.equal(result.lines[0].nodes, 42);
|
|
88
|
+
assert.deepEqual(result.lines[0].scoreProvenance, candidates[0].scoreProvenance);
|
|
89
|
+
assert.equal(result.engineBuild, "Stockfish test");
|
|
90
|
+
assert.equal(result.cacheProvenance, "engine");
|
|
91
|
+
assert.equal(result.diagnostics, diagnostics);
|
|
92
|
+
});
|
|
93
|
+
test("preserves restricted roots and cancellation instead of changing search scope", async () => {
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
let received;
|
|
96
|
+
const provider = createEngineAnalysisProvider({ async search(input) {
|
|
97
|
+
received = input;
|
|
98
|
+
return { bestmove: "g7g8", candidates: [line("g7g8")] };
|
|
99
|
+
} });
|
|
100
|
+
const result = await provider.analyze({ ...request, multiPv: 1, rootMoves: ["g7g8"], signal: controller.signal });
|
|
101
|
+
assert.equal(received.signal, controller.signal);
|
|
102
|
+
assert.deepEqual(received.rootMoves, ["g7g8"]);
|
|
103
|
+
assert.equal(received.requireRootCoverage, true);
|
|
104
|
+
assert.equal(result.command, "go depth 18 searchmoves g7g8");
|
|
105
|
+
controller.abort();
|
|
106
|
+
await assert.rejects(provider.analyze({ ...request, signal: controller.signal }), { name: "AbortError" });
|
|
107
|
+
});
|
|
108
|
+
test("explicit comparison requires every requested root from the engine client", async () => {
|
|
109
|
+
const required = ["g7g8", "g7f7"];
|
|
110
|
+
let seen;
|
|
111
|
+
const client = { async search(input) {
|
|
112
|
+
seen = input;
|
|
113
|
+
const candidates = [line("g7g8")];
|
|
114
|
+
if (input.requireRootCoverage && input.rootMoves.some((root) => !candidates.some((candidate) => candidate.pv[0] === root)))
|
|
115
|
+
throw new Error("UCI engine did not complete requested score and root coverage.");
|
|
116
|
+
return { bestmove: "g7g8", candidates };
|
|
117
|
+
} };
|
|
118
|
+
const provider = createEngineAnalysisProvider(client);
|
|
119
|
+
await assert.rejects(provider.analyze({ ...request, multiPv: 2, rootMoves: required }), /root coverage/);
|
|
120
|
+
assert.deepEqual(seen.rootMoves, required);
|
|
121
|
+
assert.equal(seen.requireRootCoverage, true);
|
|
122
|
+
const unrestricted = await provider.analyze({ ...request, multiPv: 2, rootMoves: [] });
|
|
123
|
+
assert.equal(seen.requireRootCoverage, void 0);
|
|
124
|
+
assert.equal(unrestricted.lines.length, 1);
|
|
125
|
+
});
|
|
126
|
+
test("does not turn a cancelled or failed engine search into evidence", async () => {
|
|
127
|
+
const controller = new AbortController();
|
|
128
|
+
const provider = createEngineAnalysisProvider({ async search() {
|
|
129
|
+
controller.abort();
|
|
130
|
+
return { candidates: [line("g7g8")] };
|
|
131
|
+
} });
|
|
132
|
+
await assert.rejects(provider.analyze({ ...request, signal: controller.signal }), { name: "AbortError" });
|
|
133
|
+
const failure = new Error("engine failed");
|
|
134
|
+
await assert.rejects(createEngineAnalysisProvider({ async search() {
|
|
135
|
+
throw failure;
|
|
136
|
+
} }).analyze(request), (error) => error === failure);
|
|
137
|
+
});
|
|
138
|
+
test("requires an attributed single engine result", async () => {
|
|
139
|
+
for (const candidates of [[line("g7g8"), { ...line("g7f7"), scoreProvenance: { ...line("g7f7").scoreProvenance, weightsId: "different" } }], [{ ...line("g7g8"), scoreProvenance: void 0 }], [line("g7g8"), { ...line("g7f7"), scoreProvenance: { engineBuild: "other" } }]]) {
|
|
140
|
+
await assert.rejects(createEngineAnalysisProvider({ async search() {
|
|
141
|
+
return { bestmove: "g7g8", candidates };
|
|
142
|
+
} }).analyze(request), /identity/);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
test("explicit no-move result carries engine identity without inventing a score", async () => {
|
|
146
|
+
const provider = createEngineAnalysisProvider({ async search() {
|
|
147
|
+
return { noMove: true, bestmove: null, candidates: [], engineBuild: "Stockfish test" };
|
|
148
|
+
} });
|
|
149
|
+
const result = await provider.analyze(request);
|
|
150
|
+
assert.equal(result.bestMove, null);
|
|
151
|
+
assert.deepEqual(result.lines, []);
|
|
152
|
+
assert.equal(result.engineBuild, "Stockfish test");
|
|
153
|
+
for (const malformed of [
|
|
154
|
+
{ noMove: true, bestmove: null, candidates: [], engineBuild: "" },
|
|
155
|
+
{ noMove: true, bestmove: "e2e4", candidates: [], engineBuild: "Stockfish test" },
|
|
156
|
+
{ noMove: true, bestmove: null, candidates: [line("e2e4")], engineBuild: "Stockfish test" }
|
|
157
|
+
]) {
|
|
158
|
+
await assert.rejects(createEngineAnalysisProvider({ async search() {
|
|
159
|
+
return malformed;
|
|
160
|
+
} }).analyze(request), /no-move/);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
test("scored search requires an explicit completed best move", async () => {
|
|
164
|
+
for (const bestmove of [void 0, "d2d4"]) {
|
|
165
|
+
await assert.rejects(createEngineAnalysisProvider({ async search() {
|
|
166
|
+
return { bestmove, candidates: [line("e2e4")] };
|
|
167
|
+
} }).analyze(request), /best move/);
|
|
168
|
+
}
|
|
169
|
+
});
|
package/dist/research.js
CHANGED
|
@@ -211,7 +211,7 @@ import {
|
|
|
211
211
|
wikibooksTargetsForTool,
|
|
212
212
|
withBranchPreparation,
|
|
213
213
|
withRecordedPositionTools
|
|
214
|
-
} from "./chunk-
|
|
214
|
+
} from "./chunk-V6NM6RB2.js";
|
|
215
215
|
import {
|
|
216
216
|
CHESS_REASONING_POLICY_VERSION,
|
|
217
217
|
CHESS_REASONING_SCHEMA_VERSION,
|
|
@@ -317,6 +317,9 @@ import {
|
|
|
317
317
|
validateSimpleAgentConfig,
|
|
318
318
|
withEvidenceLicenses
|
|
319
319
|
} from "./chunk-6OXAG5H3.js";
|
|
320
|
+
import {
|
|
321
|
+
createEngineAnalysisProvider
|
|
322
|
+
} from "./chunk-AJJP7FYT.js";
|
|
320
323
|
|
|
321
324
|
// src/chess-reasoning/ablation-direct-synthesis.ts
|
|
322
325
|
import { SQUARES } from "chess.js";
|
|
@@ -2119,6 +2122,7 @@ export {
|
|
|
2119
2122
|
createContextEvidence,
|
|
2120
2123
|
createDefaultObservationProvider,
|
|
2121
2124
|
createDefaultSaliencePolicy,
|
|
2125
|
+
createEngineAnalysisProvider,
|
|
2122
2126
|
createIdeaGraph,
|
|
2123
2127
|
createIdentityState,
|
|
2124
2128
|
createMechanicalFactRequest,
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"src/chess-reasoning/compact-search-preparation.ts",
|
|
27
27
|
"src/chess-reasoning/concept-registry.ts",
|
|
28
28
|
"src/chess-reasoning/conversation-context.ts",
|
|
29
|
+
"src/chess-reasoning/engine-analysis-provider.ts",
|
|
29
30
|
"src/chess-reasoning/engine-search-failure.ts",
|
|
30
31
|
"src/chess-reasoning/evidence-citation-normalization.ts",
|
|
31
32
|
"src/chess-reasoning/evidence-licenses.ts",
|
|
@@ -99,5 +100,5 @@
|
|
|
99
100
|
"@ai-sdk/provider",
|
|
100
101
|
"@ai-sdk/provider-utils"
|
|
101
102
|
],
|
|
102
|
-
"runtimeBytes":
|
|
103
|
+
"runtimeBytes": 971929
|
|
103
104
|
}
|
|
@@ -9,6 +9,8 @@ export { createPocToolHost } from './answer-tool-host.js';
|
|
|
9
9
|
export type { AnswerToolHostOptions } from './answer-tool-host.js';
|
|
10
10
|
export type { PositionAnalysisProvider, PositionAnalysisRequest, PositionAnalysisResult, PositionAnalysisLine, PositionAnalysisSegment, CompatiblePositionAnalysisProvider, LegacyPositionAnalysisProvider, LegacyPositionAnalysisResult, LegacyPositionAnalysisLine } from './position-analysis-contracts.js';
|
|
11
11
|
export { toPositionAnalysis, toLegacyPositionAnalysis, toPositionAnalysisProvider, toLegacyPositionAnalysisProvider } from './position-analysis-contracts.js';
|
|
12
|
+
export { createEngineAnalysisProvider } from './engine-analysis-provider.js';
|
|
13
|
+
export type { AnalysisEngineClient } from './engine-analysis-provider.js';
|
|
12
14
|
export { createProductionObservationProvider } from './production-observation-provider.js';
|
|
13
15
|
export { validateReasoningPositionContext, reasoningContextHash, createContextEvidence, createQueryContract, DEFAULT_REASONING_RUN_CONTROLS, REASONING_POC_VERSION } from './poc-contracts.js';
|
|
14
16
|
export type { ReasoningPositionContext, ReasoningToolSet, ObservationProvider, ReasoningEvidenceRecord } from './poc-contracts.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { PositionAnalysisProvider } from './position-analysis-contracts.js';
|
|
2
|
+
/** Consumer-owned structural contract. No dependency on an engine implementation. */
|
|
3
|
+
export type AnalysisEngineClient = {
|
|
4
|
+
search(request: {
|
|
5
|
+
fen: string;
|
|
6
|
+
limit: {
|
|
7
|
+
depth: number;
|
|
8
|
+
};
|
|
9
|
+
multiPv: number;
|
|
10
|
+
rootMoves: string[];
|
|
11
|
+
requireRootCoverage?: boolean;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}): Promise<({
|
|
14
|
+
noMove?: false;
|
|
15
|
+
bestmove: string;
|
|
16
|
+
engineBuild?: string;
|
|
17
|
+
weightsId?: string;
|
|
18
|
+
candidates: Array<{
|
|
19
|
+
pv: string[];
|
|
20
|
+
depth: number;
|
|
21
|
+
nodes?: number | null;
|
|
22
|
+
scoreCp: number | null;
|
|
23
|
+
mate: number | null;
|
|
24
|
+
scoreProvenance?: {
|
|
25
|
+
engineBuild: string;
|
|
26
|
+
weightsId?: string;
|
|
27
|
+
};
|
|
28
|
+
}>;
|
|
29
|
+
} | {
|
|
30
|
+
noMove: true;
|
|
31
|
+
bestmove: null;
|
|
32
|
+
candidates: [];
|
|
33
|
+
engineBuild: string;
|
|
34
|
+
weightsId?: string;
|
|
35
|
+
}) & {
|
|
36
|
+
diagnostics?: unknown;
|
|
37
|
+
}>;
|
|
38
|
+
};
|
|
39
|
+
/** Adapt a host-owned depth-search client without acquiring or closing it.
|
|
40
|
+
* Short PVs remain short; evidence admission stays with the existing consumers.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createEngineAnalysisProvider(client: AnalysisEngineClient): PositionAnalysisProvider;
|
package/docs/API.md
CHANGED
|
@@ -89,3 +89,27 @@ An unrestricted completed search can return one through that many valid lines;
|
|
|
89
89
|
the original requested count remains in the witness even when fewer lines arrive.
|
|
90
90
|
Explicit `rootMoves` searches must still cover their entire requested root set.
|
|
91
91
|
Depth, score, provenance, distinct-root and legal-PV checks remain unchanged.
|
|
92
|
+
|
|
93
|
+
## Host-owned engine clients
|
|
94
|
+
|
|
95
|
+
`createEngineAnalysisProvider(client)` is exported from the package root and the
|
|
96
|
+
small `@knights-eye-chess/ai-analysis/engine` entry point. The latter imports no
|
|
97
|
+
model SDK or engine implementation. AI Analysis declares its own structural
|
|
98
|
+
`AnalysisEngineClient` contract; the host wires a compatible implementation.
|
|
99
|
+
|
|
100
|
+
The adapter translates a depth request to `limit: { depth }` and maps completed
|
|
101
|
+
engine candidates to AI lines without changing scores, mate units, achieved depth,
|
|
102
|
+
PV length or provenance. A valid result can contain fewer lines than requested.
|
|
103
|
+
Engine/weights identity remains attributed; different engines are not blended.
|
|
104
|
+
This is not evidence admission or a calibration of another engine's scores for
|
|
105
|
+
the Knight's Eye classifier. Nodes/time-limited human-likelihood evidence needs
|
|
106
|
+
its own consumer contract rather than masquerading as depth-search evidence.
|
|
107
|
+
|
|
108
|
+
The host owns lifecycle and closes the client. `minimumPvPlies` does not make this
|
|
109
|
+
adapter perform extra searches or pad PVs. Existing downstream admission and
|
|
110
|
+
continuation handling remain responsible for evidence use.
|
|
111
|
+
Scored results require an explicit completed `bestmove` matching the first scored
|
|
112
|
+
root. An explicit `noMove: true` result with empty candidates, `bestmove: null`,
|
|
113
|
+
and top-level `engineBuild` maps to no scored lines and a null best move. This is
|
|
114
|
+
only a UCI no-move report: the host checks legal moves at the FEN before claiming
|
|
115
|
+
checkmate, stalemate, or another chess terminal condition.
|
package/npm-release.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knights-eye-chess/ai-analysis",
|
|
3
|
-
"version": "0.1.0-dev.
|
|
3
|
+
"version": "0.1.0-dev.24",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"./shared": {
|
|
34
34
|
"types": "./dist/types/shared-exports.d.ts",
|
|
35
35
|
"import": "./dist/shared.js"
|
|
36
|
+
},
|
|
37
|
+
"./engine": {
|
|
38
|
+
"types": "./dist/types/chess-reasoning/engine-analysis-provider.d.ts",
|
|
39
|
+
"import": "./dist/engine.js"
|
|
36
40
|
}
|
|
37
41
|
},
|
|
38
42
|
"scripts": {
|
|
@@ -51,7 +55,7 @@
|
|
|
51
55
|
"dependencies": {
|
|
52
56
|
"@ai-sdk/provider": "4.0.9",
|
|
53
57
|
"@ai-sdk/provider-utils": "5.0.34",
|
|
54
|
-
"@knights-eye-chess/move-classifier": "0.1.0-dev.
|
|
58
|
+
"@knights-eye-chess/move-classifier": "0.1.0-dev.16-board-offers.3",
|
|
55
59
|
"@types/json-schema": "7.0.15",
|
|
56
60
|
"@types/node": "22.19.19",
|
|
57
61
|
"ai": "7.0.85",
|
|
@@ -9,6 +9,8 @@ export {createPocToolHost} from './answer-tool-host';
|
|
|
9
9
|
export type {AnswerToolHostOptions} from './answer-tool-host';
|
|
10
10
|
export type {PositionAnalysisProvider,PositionAnalysisRequest,PositionAnalysisResult,PositionAnalysisLine,PositionAnalysisSegment,CompatiblePositionAnalysisProvider,LegacyPositionAnalysisProvider,LegacyPositionAnalysisResult,LegacyPositionAnalysisLine} from './position-analysis-contracts';
|
|
11
11
|
export {toPositionAnalysis,toLegacyPositionAnalysis,toPositionAnalysisProvider,toLegacyPositionAnalysisProvider} from './position-analysis-contracts';
|
|
12
|
+
export {createEngineAnalysisProvider} from './engine-analysis-provider';
|
|
13
|
+
export type {AnalysisEngineClient} from './engine-analysis-provider';
|
|
12
14
|
export {createProductionObservationProvider} from './production-observation-provider';
|
|
13
15
|
export {validateReasoningPositionContext,reasoningContextHash,createContextEvidence,createQueryContract,DEFAULT_REASONING_RUN_CONTROLS,REASONING_POC_VERSION} from './poc-contracts';
|
|
14
16
|
export type {ReasoningPositionContext,ReasoningToolSet,ObservationProvider,ReasoningEvidenceRecord} from './poc-contracts';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { PositionAnalysisProvider, PositionAnalysisResult } from './position-analysis-contracts';
|
|
2
|
+
|
|
3
|
+
/** Consumer-owned structural contract. No dependency on an engine implementation. */
|
|
4
|
+
export type AnalysisEngineClient = {
|
|
5
|
+
search(request: {
|
|
6
|
+
fen: string; limit: { depth: number }; multiPv: number; rootMoves: string[];
|
|
7
|
+
requireRootCoverage?: boolean; signal?: AbortSignal;
|
|
8
|
+
}): Promise<({
|
|
9
|
+
noMove?: false; bestmove: string; engineBuild?: string; weightsId?: string;
|
|
10
|
+
candidates: Array<{
|
|
11
|
+
pv: string[]; depth: number; nodes?: number | null; scoreCp: number | null; mate: number | null;
|
|
12
|
+
scoreProvenance?: { engineBuild: string; weightsId?: string };
|
|
13
|
+
}>;
|
|
14
|
+
} | {
|
|
15
|
+
noMove: true; bestmove: null; candidates: []; engineBuild: string; weightsId?: string;
|
|
16
|
+
}) & { diagnostics?: unknown }>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Adapt a host-owned depth-search client without acquiring or closing it.
|
|
20
|
+
* Short PVs remain short; evidence admission stays with the existing consumers.
|
|
21
|
+
*/
|
|
22
|
+
export function createEngineAnalysisProvider(client: AnalysisEngineClient): PositionAnalysisProvider {
|
|
23
|
+
return { async analyze(request): Promise<PositionAnalysisResult> {
|
|
24
|
+
request.signal?.throwIfAborted();
|
|
25
|
+
const result = await client.search({ fen: request.fen, limit: { depth: request.depth },
|
|
26
|
+
multiPv: request.multiPv, rootMoves: request.rootMoves,
|
|
27
|
+
...(request.rootMoves.length ? { requireRootCoverage: true } : {}), signal: request.signal });
|
|
28
|
+
request.signal?.throwIfAborted();
|
|
29
|
+
const command = 'go depth ' + request.depth + (request.rootMoves.length ? ' searchmoves ' + request.rootMoves.join(' ') : '');
|
|
30
|
+
if (result.noMove === true) {
|
|
31
|
+
if (result.bestmove !== null || result.candidates.length !== 0 || !result.engineBuild) {
|
|
32
|
+
throw new Error('Invalid UCI no-move result.');
|
|
33
|
+
}
|
|
34
|
+
// A UCI no-move report is unscored; the host must check the legal board
|
|
35
|
+
// before interpreting it as checkmate, stalemate, or another terminal.
|
|
36
|
+
return { engine: result.engineBuild, engineBuild: result.engineBuild, command,
|
|
37
|
+
bestMove: null, cacheProvenance: 'engine', lines: [],
|
|
38
|
+
...(result.diagnostics ? { diagnostics: result.diagnostics } : {}) };
|
|
39
|
+
}
|
|
40
|
+
const build = result.candidates[0]?.scoreProvenance?.engineBuild;
|
|
41
|
+
if (typeof build !== 'string' || !build) throw new Error('Engine returned no attributed build identity.');
|
|
42
|
+
if (!result.bestmove || result.bestmove !== result.candidates[0]?.pv[0] ||
|
|
43
|
+
(result.engineBuild && result.engineBuild !== build) ||
|
|
44
|
+
(result.weightsId && result.weightsId !== result.candidates[0]?.scoreProvenance?.weightsId)) {
|
|
45
|
+
throw new Error('Engine returned an incomplete or inconsistent best move.');
|
|
46
|
+
}
|
|
47
|
+
// A provider result represents one engine search, not a blend of engines.
|
|
48
|
+
if (result.candidates.some(line => line.scoreProvenance?.engineBuild !== build ||
|
|
49
|
+
line.scoreProvenance?.weightsId !== result.candidates[0]?.scoreProvenance?.weightsId)) {
|
|
50
|
+
throw new Error('Engine returned inconsistent build identity.');
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
engine: build, engineBuild: build,
|
|
54
|
+
command, bestMove: result.bestmove,
|
|
55
|
+
cacheProvenance: 'engine',
|
|
56
|
+
...(result.diagnostics ? { diagnostics: result.diagnostics } : {}),
|
|
57
|
+
lines: result.candidates.map(candidate => ({ rootMove: candidate.pv[0], pv: candidate.pv,
|
|
58
|
+
depth: candidate.depth, nodes: candidate.nodes ?? null, scoreCp: candidate.scoreCp,
|
|
59
|
+
mateInMoves: candidate.mate, scoreProvenance: candidate.scoreProvenance ? { ...candidate.scoreProvenance } : undefined })),
|
|
60
|
+
};
|
|
61
|
+
} };
|
|
62
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {test} from 'node:test';
|
|
3
|
+
import {createEngineAnalysisProvider} from '../../src/chess-reasoning/engine-analysis-provider.ts';
|
|
4
|
+
const request={fen:'root',depth:18,multiPv:3,rootMoves:[],minimumPvPlies:10};
|
|
5
|
+
const line=(move,mate=null)=>({pv:[move],depth:18,nodes:42,scoreCp:mate===null?21:null,mate,
|
|
6
|
+
scoreProvenance:{kind:'root-search',engineBuild:'Stockfish test',fen:'root',originatingDepth:18,reportedDepth:18}});
|
|
7
|
+
test('maps one completed search without padding fewer lines or short PVs',async()=>{
|
|
8
|
+
let calls=0,received;
|
|
9
|
+
const candidates=[line('g7g8',-3),line('g7f7')];
|
|
10
|
+
const diagnostics={processReused:true};
|
|
11
|
+
const provider=createEngineAnalysisProvider({async search(input){calls++;received=input;return{bestmove:'g7g8',candidates,diagnostics};}});
|
|
12
|
+
const result=await provider.analyze(request);
|
|
13
|
+
assert.equal(calls,1);assert.deepEqual(received,{fen:'root',limit:{depth:18},multiPv:3,rootMoves:[],signal:undefined});
|
|
14
|
+
assert.equal(result.lines.length,2);assert.deepEqual(result.lines[0].pv,['g7g8']);
|
|
15
|
+
assert.equal(result.lines[0].mateInMoves,-3);assert.equal(result.lines[0].scoreCp,null);
|
|
16
|
+
assert.equal(result.lines[0].nodes,42);assert.deepEqual(result.lines[0].scoreProvenance,candidates[0].scoreProvenance);
|
|
17
|
+
assert.equal(result.engineBuild,'Stockfish test');assert.equal(result.cacheProvenance,'engine');assert.equal(result.diagnostics,diagnostics);
|
|
18
|
+
});
|
|
19
|
+
test('preserves restricted roots and cancellation instead of changing search scope',async()=>{
|
|
20
|
+
const controller=new AbortController();let received;
|
|
21
|
+
const provider=createEngineAnalysisProvider({async search(input){received=input;return{bestmove:'g7g8',candidates:[line('g7g8')]};}});
|
|
22
|
+
const result=await provider.analyze({...request,multiPv:1,rootMoves:['g7g8'],signal:controller.signal});
|
|
23
|
+
assert.equal(received.signal,controller.signal);assert.deepEqual(received.rootMoves,['g7g8']);
|
|
24
|
+
assert.equal(received.requireRootCoverage,true);
|
|
25
|
+
assert.equal(result.command,'go depth 18 searchmoves g7g8');
|
|
26
|
+
controller.abort();await assert.rejects(provider.analyze({...request,signal:controller.signal}),{name:'AbortError'});
|
|
27
|
+
});
|
|
28
|
+
test('explicit comparison requires every requested root from the engine client',async()=>{
|
|
29
|
+
const required=['g7g8','g7f7'];let seen;
|
|
30
|
+
const client={async search(input){
|
|
31
|
+
seen=input;
|
|
32
|
+
const candidates=[line('g7g8')];
|
|
33
|
+
if(input.requireRootCoverage && input.rootMoves.some(root=>!candidates.some(candidate=>candidate.pv[0]===root)))
|
|
34
|
+
throw new Error('UCI engine did not complete requested score and root coverage.');
|
|
35
|
+
return {bestmove:'g7g8',candidates};
|
|
36
|
+
}};
|
|
37
|
+
const provider=createEngineAnalysisProvider(client);
|
|
38
|
+
await assert.rejects(provider.analyze({...request,multiPv:2,rootMoves:required}),/root coverage/);
|
|
39
|
+
assert.deepEqual(seen.rootMoves,required);assert.equal(seen.requireRootCoverage,true);
|
|
40
|
+
const unrestricted=await provider.analyze({...request,multiPv:2,rootMoves:[]});
|
|
41
|
+
assert.equal(seen.requireRootCoverage,undefined);assert.equal(unrestricted.lines.length,1);
|
|
42
|
+
});
|
|
43
|
+
test('does not turn a cancelled or failed engine search into evidence',async()=>{
|
|
44
|
+
const controller=new AbortController();
|
|
45
|
+
const provider=createEngineAnalysisProvider({async search(){controller.abort();return{candidates:[line('g7g8')]};}});
|
|
46
|
+
await assert.rejects(provider.analyze({...request,signal:controller.signal}),{name:'AbortError'});
|
|
47
|
+
const failure=new Error('engine failed');
|
|
48
|
+
await assert.rejects(createEngineAnalysisProvider({async search(){throw failure;}}).analyze(request),error=>error===failure);
|
|
49
|
+
});
|
|
50
|
+
test('requires an attributed single engine result',async()=>{
|
|
51
|
+
for(const candidates of [[line('g7g8'),{...line('g7f7'),scoreProvenance:{...line('g7f7').scoreProvenance,weightsId:'different'}}],[{...line('g7g8'),scoreProvenance:undefined}],[line('g7g8'),{...line('g7f7'),scoreProvenance:{engineBuild:'other'}}]]){
|
|
52
|
+
await assert.rejects(createEngineAnalysisProvider({async search(){return{bestmove:'g7g8',candidates};}}).analyze(request),/identity/);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
test('explicit no-move result carries engine identity without inventing a score',async()=>{
|
|
56
|
+
const provider=createEngineAnalysisProvider({async search(){return{noMove:true,bestmove:null,candidates:[],engineBuild:'Stockfish test'};}});
|
|
57
|
+
const result=await provider.analyze(request);
|
|
58
|
+
assert.equal(result.bestMove,null);assert.deepEqual(result.lines,[]);
|
|
59
|
+
assert.equal(result.engineBuild,'Stockfish test');
|
|
60
|
+
for(const malformed of [{noMove:true,bestmove:null,candidates:[],engineBuild:''},
|
|
61
|
+
{noMove:true,bestmove:'e2e4',candidates:[],engineBuild:'Stockfish test'},
|
|
62
|
+
{noMove:true,bestmove:null,candidates:[line('e2e4')],engineBuild:'Stockfish test'}]) {
|
|
63
|
+
await assert.rejects(createEngineAnalysisProvider({async search(){return malformed;}}).analyze(request),/no-move/);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
test('scored search requires an explicit completed best move',async()=>{
|
|
67
|
+
for(const bestmove of [undefined,'d2d4']) {
|
|
68
|
+
await assert.rejects(createEngineAnalysisProvider({async search(){return{bestmove,candidates:[line('e2e4')]};}}).analyze(request),/best move/);
|
|
69
|
+
}
|
|
70
|
+
});
|
package/test-types.mts
CHANGED
|
@@ -21,3 +21,8 @@ const oldAsCurrent:PositionAnalysisResult=old;
|
|
|
21
21
|
const badMate:PositionAnalysisResult={...modern,lines:[{...modern.lines[0],mateInMoves:'3'}]};
|
|
22
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
23
|
void analyzer;void oldAsCurrent;void badMate;
|
|
24
|
+
|
|
25
|
+
import {createEngineAnalysisProvider,type AnalysisEngineClient} from '@knights-eye-chess/ai-analysis/engine';
|
|
26
|
+
const engineClient:AnalysisEngineClient={async search(request){return{bestmove:'e2e4',candidates:[{pv:['e2e4'],depth:request.limit.depth,scoreCp:20,mate:null,scoreProvenance:{engineBuild:'test'}}]};}};
|
|
27
|
+
const engineProvider:PositionAnalysisProvider=createEngineAnalysisProvider(engineClient);
|
|
28
|
+
void engineProvider;
|
|
File without changes
|