@knights-eye-chess/ai-analysis 0.1.0-dev.17 → 0.1.0-dev.21
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-XQDZJOYD.js → chunk-6OXAG5H3.js} +1 -1
- package/dist/chunk-AJJP7FYT.js +59 -0
- package/dist/{chunk-PLKNJRPF.js → chunk-V6NM6RB2.js} +1 -1
- package/dist/engine-runtime-closure.json +6 -0
- package/dist/engine.js +6 -0
- package/dist/index.js +6 -2
- package/dist/owner-tests/engine-analysis-provider.test.mjs +169 -0
- package/dist/owner-tests/pipeline-host-preparation.test.mjs +1 -1
- package/dist/owner-tests/prose-reference-admission.test.mjs +52 -1
- package/dist/research.js +6 -2
- package/dist/runtime-closure.json +2 -1
- package/dist/shared.js +1 -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 +33 -0
- package/npm-release.json +1 -1
- package/package.json +6 -2
- package/scripts/check-release-tooling.mjs +1 -1
- package/scripts/check-review-marker.mjs +79 -0
- package/scripts/publish-reviewed.mjs +5 -1
- package/scripts/release-tooling.json +4 -3
- package/src/chess-reasoning/answer-pipeline-exports.ts +2 -0
- package/src/chess-reasoning/engine-analysis-provider.ts +62 -0
- package/src/chess-reasoning/fresh-engine-pv-witnesses.ts +1 -1
- package/test/owner/engine-analysis-provider.test.mjs +70 -0
- package/test/owner/prose-reference-admission.test.mjs +28 -0
- package/test-types.mts +5 -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.21` candidate with Move Classifier `0.1.0-dev.15`.
|
|
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.21.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(', '));
|
|
@@ -1284,7 +1284,7 @@ function rejectionReason(context, hash, witness) {
|
|
|
1284
1284
|
if (witness.authority !== "bounded-engine" || !Array.isArray(witness.licenses) || witness.licenses.length !== 1 || witness.licenses[0] !== "engine.bounded-continuation") return "missing-authority-license";
|
|
1285
1285
|
if (witness.contextHash !== hash) return "context-mismatch";
|
|
1286
1286
|
if (!["before", "after"].includes(witness.phase) || witness.fen !== (witness.phase === "before" ? context.fenBefore : context.fenAfter)) return "root-mismatch";
|
|
1287
|
-
if (typeof witness.engineBuild !== "string" || !witness.engineBuild || typeof witness.command !== "string" || !witness.command || !positiveInteger(witness.requestedDepth) || !positiveInteger(witness.multiPv) || witness.multiPv > 32 || !Array.isArray(witness.lines) || witness.lines.length
|
|
1287
|
+
if (typeof witness.engineBuild !== "string" || !witness.engineBuild || typeof witness.command !== "string" || !witness.command || !positiveInteger(witness.requestedDepth) || !positiveInteger(witness.multiPv) || witness.multiPv > 32 || !Array.isArray(witness.lines) || witness.lines.length < 1 || witness.lines.length > witness.multiPv || !Array.isArray(witness.rootMoves) || witness.rootMoves.length > 32 || witness.rootMoves.some((root) => typeof root !== "string" || !UCI.test(root)) || new Set(witness.rootMoves).size !== witness.rootMoves.length) return "invalid-search-scope";
|
|
1288
1288
|
if (witness.lines.some((line) => !line || typeof line.rootMove !== "string")) return "invalid-pv";
|
|
1289
1289
|
const roots = witness.lines.map((line) => line.rootMove);
|
|
1290
1290
|
if (new Set(roots).size !== roots.length || witness.rootMoves.some((root) => !roots.includes(root)) || witness.rootMoves.length > 0 && roots.some((root) => !witness.rootMoves.includes(root))) return "root-set-mismatch";
|
|
@@ -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
|
+
};
|
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
validateEvidenceLicenseClaims,
|
|
51
51
|
validateReasoningPositionContext,
|
|
52
52
|
withEvidenceLicenses
|
|
53
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-6OXAG5H3.js";
|
|
54
54
|
|
|
55
55
|
// src/chess-reasoning/branch-expansion-policy.ts
|
|
56
56
|
var BRANCH_EXPANSION_OPTIONS = { maxNodes: 8, maxPlies: 4, maxLines: 2, depthFloor: 14, depthMode: "decreasing", maxElapsedMs: 2e4 };
|
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,
|
|
@@ -71,7 +71,10 @@ import {
|
|
|
71
71
|
toPositionAnalysisProvider,
|
|
72
72
|
validateReasoningPositionContext,
|
|
73
73
|
validateSimpleAgentConfig
|
|
74
|
-
} from "./chunk-
|
|
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
|
+
});
|
|
@@ -5879,7 +5879,7 @@ function rejectionReason(context2, hash, witness) {
|
|
|
5879
5879
|
if (witness.authority !== "bounded-engine" || !Array.isArray(witness.licenses) || witness.licenses.length !== 1 || witness.licenses[0] !== "engine.bounded-continuation") return "missing-authority-license";
|
|
5880
5880
|
if (witness.contextHash !== hash) return "context-mismatch";
|
|
5881
5881
|
if (!["before", "after"].includes(witness.phase) || witness.fen !== (witness.phase === "before" ? context2.fenBefore : context2.fenAfter)) return "root-mismatch";
|
|
5882
|
-
if (typeof witness.engineBuild !== "string" || !witness.engineBuild || typeof witness.command !== "string" || !witness.command || !positiveInteger(witness.requestedDepth) || !positiveInteger(witness.multiPv) || witness.multiPv > 32 || !Array.isArray(witness.lines) || witness.lines.length
|
|
5882
|
+
if (typeof witness.engineBuild !== "string" || !witness.engineBuild || typeof witness.command !== "string" || !witness.command || !positiveInteger(witness.requestedDepth) || !positiveInteger(witness.multiPv) || witness.multiPv > 32 || !Array.isArray(witness.lines) || witness.lines.length < 1 || witness.lines.length > witness.multiPv || !Array.isArray(witness.rootMoves) || witness.rootMoves.length > 32 || witness.rootMoves.some((root) => typeof root !== "string" || !UCI.test(root)) || new Set(witness.rootMoves).size !== witness.rootMoves.length) return "invalid-search-scope";
|
|
5883
5883
|
if (witness.lines.some((line) => !line || typeof line.rootMove !== "string")) return "invalid-pv";
|
|
5884
5884
|
const roots = witness.lines.map((line) => line.rootMove);
|
|
5885
5885
|
if (new Set(roots).size !== roots.length || witness.rootMoves.some((root) => !roots.includes(root)) || witness.rootMoves.length > 0 && roots.some((root) => !witness.rootMoves.includes(root))) return "root-set-mismatch";
|
|
@@ -1037,7 +1037,7 @@ function rejectionReason(context, hash, witness) {
|
|
|
1037
1037
|
if (witness.authority !== "bounded-engine" || !Array.isArray(witness.licenses) || witness.licenses.length !== 1 || witness.licenses[0] !== "engine.bounded-continuation") return "missing-authority-license";
|
|
1038
1038
|
if (witness.contextHash !== hash) return "context-mismatch";
|
|
1039
1039
|
if (!["before", "after"].includes(witness.phase) || witness.fen !== (witness.phase === "before" ? context.fenBefore : context.fenAfter)) return "root-mismatch";
|
|
1040
|
-
if (typeof witness.engineBuild !== "string" || !witness.engineBuild || typeof witness.command !== "string" || !witness.command || !positiveInteger(witness.requestedDepth) || !positiveInteger(witness.multiPv) || witness.multiPv > 32 || !Array.isArray(witness.lines) || witness.lines.length
|
|
1040
|
+
if (typeof witness.engineBuild !== "string" || !witness.engineBuild || typeof witness.command !== "string" || !witness.command || !positiveInteger(witness.requestedDepth) || !positiveInteger(witness.multiPv) || witness.multiPv > 32 || !Array.isArray(witness.lines) || witness.lines.length < 1 || witness.lines.length > witness.multiPv || !Array.isArray(witness.rootMoves) || witness.rootMoves.length > 32 || witness.rootMoves.some((root) => typeof root !== "string" || !UCI.test(root)) || new Set(witness.rootMoves).size !== witness.rootMoves.length) return "invalid-search-scope";
|
|
1041
1041
|
if (witness.lines.some((line) => !line || typeof line.rootMove !== "string")) return "invalid-pv";
|
|
1042
1042
|
const roots = witness.lines.map((line) => line.rootMove);
|
|
1043
1043
|
if (new Set(roots).size !== roots.length || witness.rootMoves.some((root) => !roots.includes(root)) || witness.rootMoves.length > 0 && roots.some((root) => !witness.rootMoves.includes(root))) return "root-set-mismatch";
|
|
@@ -1768,3 +1768,54 @@ test("fresh answer witnesses preserve shipped v22 reference geometry and reject
|
|
|
1768
1768
|
assert.equal(rejected.referenceGapCounts.unresolvedReferences, 1);
|
|
1769
1769
|
assert.equal(rejected.blocks[0].tokens[0].reference, void 0);
|
|
1770
1770
|
});
|
|
1771
|
+
test("completed unrestricted MultiPV witnesses accept fewer lines without changing requested scope", () => {
|
|
1772
|
+
const fen = "7r/p1p1p1kp/2p2rpQ/3q4/3P4/2P5/P1P2PP1/R3K2R b KQ - 5 18";
|
|
1773
|
+
const context = { kind: "position", nodeId: "two-legal", historyStartFen: fen, historyUci: [], moveUci: "", moveSan: "", fenBefore: fen, fenAfter: fen };
|
|
1774
|
+
const roots = ["g7g8", "g7f7"];
|
|
1775
|
+
assert.deepEqual(new Set(new Chess34(fen).moves({ verbose: true }).map((m) => m.from + m.to)), new Set(roots));
|
|
1776
|
+
const witness = {
|
|
1777
|
+
version: "fresh-engine-pv-witness/1",
|
|
1778
|
+
acquisition: "same-answer-engine",
|
|
1779
|
+
acquisitionId: 1,
|
|
1780
|
+
evidenceId: "server-engine-acquisition:two-legal",
|
|
1781
|
+
contextHash: reasoningContextHash(context),
|
|
1782
|
+
phase: "before",
|
|
1783
|
+
fen,
|
|
1784
|
+
engineBuild: "test-engine",
|
|
1785
|
+
command: "go depth 18",
|
|
1786
|
+
requestedDepth: 18,
|
|
1787
|
+
multiPv: 3,
|
|
1788
|
+
rootMoves: [],
|
|
1789
|
+
authority: "bounded-engine",
|
|
1790
|
+
licenses: ["engine.bounded-continuation"],
|
|
1791
|
+
lines: roots.map((rootMove, index) => ({
|
|
1792
|
+
rootMove,
|
|
1793
|
+
pv: [rootMove],
|
|
1794
|
+
depth: 18,
|
|
1795
|
+
scoreCp: -200 - index,
|
|
1796
|
+
mateInMoves: null,
|
|
1797
|
+
scoreProvenance: { kind: "root-search", fen, engineBuild: "test-engine", command: "go depth 18", originatingDepth: 18, reportedDepth: 18 }
|
|
1798
|
+
}))
|
|
1799
|
+
};
|
|
1800
|
+
const admitted = admitFreshEnginePvWitnesses(context, [witness]);
|
|
1801
|
+
assert.equal(admitted.audit.acceptedLines, 2);
|
|
1802
|
+
assert.equal(witness.multiPv, 3, "requested MultiPV remains provenance, not returned-line count");
|
|
1803
|
+
assert.equal(admitFreshEnginePvWitnesses(context, [{ ...witness, lines: [witness.lines[0]] }]).audit.acceptedLines, 1);
|
|
1804
|
+
for (const patch of [{ lines: [] }, { multiPv: 1 }, { rootMoves: roots, lines: [witness.lines[0]] }]) {
|
|
1805
|
+
assert.equal(admitFreshEnginePvWitnesses(context, [{ ...witness, ...patch }]).audit.acceptedLines, 0);
|
|
1806
|
+
}
|
|
1807
|
+
for (const change of [(line) => {
|
|
1808
|
+
line.depth = 17;
|
|
1809
|
+
}, (line) => {
|
|
1810
|
+
line.scoreCp = null;
|
|
1811
|
+
}, (line) => {
|
|
1812
|
+
line.pv = ["g7g6"];
|
|
1813
|
+
}]) {
|
|
1814
|
+
const invalid = structuredClone(witness);
|
|
1815
|
+
change(invalid.lines[1]);
|
|
1816
|
+
assert.equal(admitFreshEnginePvWitnesses(context, [invalid]).audit.acceptedLines, 0);
|
|
1817
|
+
}
|
|
1818
|
+
const rendered = simpleProseReferences("[Kg8](move:before:g7g8)", context, [], void 0, void 0, void 0, { freshEnginePvWitnesses: [witness] });
|
|
1819
|
+
assert.equal(rendered.referenceGapCounts.unresolvedReferences, 0);
|
|
1820
|
+
assert.equal(rendered.freshEnginePvAdmission.acceptedLines, 2);
|
|
1821
|
+
});
|
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,
|
|
@@ -316,7 +316,10 @@ import {
|
|
|
316
316
|
validateReasoningPositionContext,
|
|
317
317
|
validateSimpleAgentConfig,
|
|
318
318
|
withEvidenceLicenses
|
|
319
|
-
} from "./chunk-
|
|
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
|
}
|
package/dist/shared.js
CHANGED
|
@@ -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
|
@@ -80,3 +80,36 @@ remain in the canonical tool wrappers and host.
|
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
See the [pipeline source](../src/chess-reasoning/answer-pipeline.ts) and [connector contract](../src/chess-reasoning/answer-model-connector.ts).
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
### Completed MultiPV results
|
|
86
|
+
|
|
87
|
+
Fresh engine presentation witnesses interpret `multiPv` as the requested maximum.
|
|
88
|
+
An unrestricted completed search can return one through that many valid lines;
|
|
89
|
+
the original requested count remains in the witness even when fewer lines arrive.
|
|
90
|
+
Explicit `rootMoves` searches must still cover their entire requested root set.
|
|
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.21",
|
|
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.15",
|
|
55
59
|
"@types/json-schema": "7.0.15",
|
|
56
60
|
"@types/node": "22.19.19",
|
|
57
61
|
"ai": "7.0.85",
|
|
@@ -7,7 +7,7 @@ import path from 'node:path';
|
|
|
7
7
|
export function assertReleaseTooling(root = fileURLToPath(new URL('../', import.meta.url))) {
|
|
8
8
|
const manifest = JSON.parse(readFileSync(path.join(root, 'scripts/release-tooling.json'), 'utf8'));
|
|
9
9
|
if (manifest.schema !== 1 || manifest.canonical !== 'knightseye-net/tools/release-tooling') throw Error('Invalid release tooling stamp');
|
|
10
|
-
const required = ['scripts/pack.mjs', 'scripts/publish-reviewed.mjs', '.github/workflows/npm-publish.yml', 'scripts/verify-release.mjs', 'scripts/update-dependency.mjs', 'scripts/check-release-tooling.mjs', '.github/workflows/quality.yml'];
|
|
10
|
+
const required = ['scripts/check-review-marker.mjs', 'scripts/pack.mjs', 'scripts/publish-reviewed.mjs', '.github/workflows/npm-publish.yml', 'scripts/verify-release.mjs', 'scripts/update-dependency.mjs', 'scripts/check-release-tooling.mjs', '.github/workflows/quality.yml'];
|
|
11
11
|
if (!manifest.sha256 || required.some(relative => !/^[a-f0-9]{64}$/.test(manifest.sha256[relative] || ''))) throw Error('Incomplete release tooling stamp');
|
|
12
12
|
for (const [relative, expected] of Object.entries(manifest.sha256)) {
|
|
13
13
|
if (path.isAbsolute(relative) || relative.split('/').includes('..')) throw Error(`Invalid tooling path: ${relative}`);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Canonical source: knightseye-net/tools/release-tooling/templates/check-review-marker.mjs.
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const shaPattern = /^[a-f0-9]{40}$/;
|
|
7
|
+
export async function requireReview({ repository, sha, branch, reviewer, token, checkoutSha,
|
|
8
|
+
fetchImpl = fetch }) {
|
|
9
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(repository || '') || !shaPattern.test(sha || '') ||
|
|
10
|
+
checkoutSha !== sha || !['dev', 'production'].includes(branch) || reviewer !== 'Shedal' || !token) {
|
|
11
|
+
throw Error('Review gate requires repository, exact checkout SHA, release branch, Shedal and GitHub token');
|
|
12
|
+
}
|
|
13
|
+
const prefix = `/repos/${repository}`;
|
|
14
|
+
async function get(route) {
|
|
15
|
+
let response;
|
|
16
|
+
try {
|
|
17
|
+
response = await fetchImpl(`https://api.github.com${prefix}${route}`, {
|
|
18
|
+
headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json',
|
|
19
|
+
'X-GitHub-Api-Version': '2022-11-28' }, signal: AbortSignal.timeout(30000), redirect: 'error',
|
|
20
|
+
});
|
|
21
|
+
} catch { throw Error('Review evidence API unavailable'); }
|
|
22
|
+
if (!response.ok) throw Error(`Review evidence API failed: HTTP ${response.status}`);
|
|
23
|
+
try { return await response.json(); } catch { throw Error('Invalid review evidence JSON'); }
|
|
24
|
+
}
|
|
25
|
+
async function list(route) {
|
|
26
|
+
const all = [];
|
|
27
|
+
for (let page = 1; page <= 100; page++) {
|
|
28
|
+
const items = await get(`${route}?per_page=100&page=${page}`);
|
|
29
|
+
if (!Array.isArray(items)) throw Error('Invalid review evidence list');
|
|
30
|
+
all.push(...items);
|
|
31
|
+
if (items.length < 100) return all;
|
|
32
|
+
}
|
|
33
|
+
throw Error('Review evidence pagination limit reached');
|
|
34
|
+
}
|
|
35
|
+
const associated = await list(`/commits/${sha}/pulls`);
|
|
36
|
+
const matches = [];
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
for (const candidate of associated) {
|
|
39
|
+
if (!Number.isSafeInteger(candidate.number) || candidate.number <= 0) throw Error('Invalid associated PR');
|
|
40
|
+
if (seen.has(candidate.number)) continue;
|
|
41
|
+
seen.add(candidate.number);
|
|
42
|
+
// Resolve current PR metadata, not the commit-association response's snapshot.
|
|
43
|
+
const pr = await get(`/pulls/${candidate.number}`);
|
|
44
|
+
if (pr.number === candidate.number && pr.merged === true && pr.merged_at &&
|
|
45
|
+
pr.merge_commit_sha === sha && pr.base?.ref === branch &&
|
|
46
|
+
pr.base?.repo?.full_name === repository) matches.push(pr);
|
|
47
|
+
}
|
|
48
|
+
if (matches.length !== 1) throw Error('Expected one merged release PR for the exact checkout SHA');
|
|
49
|
+
const pr = matches[0];
|
|
50
|
+
const comments = await list(`/issues/${pr.number}/comments`);
|
|
51
|
+
const decisions = [];
|
|
52
|
+
for (const comment of comments) {
|
|
53
|
+
if (comment.user?.login !== reviewer || typeof comment.body !== 'string') continue;
|
|
54
|
+
// Standalone line permits Claude's normal generated footer after the decision.
|
|
55
|
+
const markers = comment.body.split(/\r?\n/).filter(line =>
|
|
56
|
+
new RegExp(`^claude-review: (approved|rejected) ${sha}$`).test(line));
|
|
57
|
+
if (!markers.length) continue;
|
|
58
|
+
const created = Date.parse(comment.created_at), updated = Date.parse(comment.updated_at);
|
|
59
|
+
if (!Number.isSafeInteger(comment.id) || !Number.isFinite(created) || !Number.isFinite(updated) || updated < created) {
|
|
60
|
+
throw Error('Invalid review decision metadata');
|
|
61
|
+
}
|
|
62
|
+
const edited = updated !== created;
|
|
63
|
+
decisions.push({ id: comment.id, time: updated, approved: !edited && markers.length === 1 &&
|
|
64
|
+
markers[0] === `claude-review: approved ${sha}` });
|
|
65
|
+
}
|
|
66
|
+
decisions.sort((a, b) => a.time - b.time || a.id - b.id);
|
|
67
|
+
const latest = decisions.at(-1);
|
|
68
|
+
if (!latest?.approved) throw Error('Exact commit has no current unedited approval; post a fresh review decision');
|
|
69
|
+
return { repository, sha, reviewer, pullRequest: pr.number,
|
|
70
|
+
reviewUrl: `https://github.com/${repository}/pull/${pr.number}#issuecomment-${latest.id}` };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
74
|
+
const checkoutSha = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
|
|
75
|
+
const receipt = await requireReview({ repository: process.env.GITHUB_REPOSITORY,
|
|
76
|
+
sha: process.env.GITHUB_SHA, branch: process.env.REVIEW_RELEASE_BRANCH,
|
|
77
|
+
reviewer: process.env.REVIEWER_LOGIN, token: process.env.GH_TOKEN, checkoutSha });
|
|
78
|
+
console.log(JSON.stringify(receipt));
|
|
79
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Generated from knightseye-net/tools/release-tooling; edit the canonical template and sync.
|
|
2
|
+
import { requireReview } from './check-review-marker.mjs';
|
|
2
3
|
import { assertReleaseTooling } from './check-release-tooling.mjs';
|
|
3
4
|
import { readFileSync } from 'node:fs';
|
|
4
5
|
import { createHash } from 'node:crypto';
|
|
5
|
-
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { spawnSync, execFileSync } from 'node:child_process';
|
|
6
7
|
import { fileURLToPath } from 'node:url';
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
|
|
@@ -81,5 +82,8 @@ export function publishReviewed({root, version, sha256, env = process.env, run =
|
|
|
81
82
|
}
|
|
82
83
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
83
84
|
assertReleaseTooling();
|
|
85
|
+
await requireReview({ repository: process.env.GITHUB_REPOSITORY, sha: process.env.GITHUB_SHA,
|
|
86
|
+
checkoutSha: execFileSync('git', ['rev-parse', 'HEAD'], { cwd: fileURLToPath(new URL('../', import.meta.url)), encoding: 'utf8' }).trim(),
|
|
87
|
+
branch: 'dev', reviewer: 'Shedal', token: process.env.GH_TOKEN });
|
|
84
88
|
console.log(JSON.stringify(publishReviewed({root: fileURLToPath(new URL('../', import.meta.url)), version: process.argv[2], sha256: process.argv[3]})));
|
|
85
89
|
}
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
"canonical": "knightseye-net/tools/release-tooling",
|
|
4
4
|
"sha256": {
|
|
5
5
|
"scripts/pack.mjs": "334aa4041e59b14fd2b072969f50beea64449136f7f9146d63d7277fc2e4808a",
|
|
6
|
-
"scripts/
|
|
7
|
-
"
|
|
6
|
+
"scripts/check-review-marker.mjs": "1d06c9139a835e47f78eef1bff221d5ccdc57c10f0731796ca6f5f0e3ca49e7f",
|
|
7
|
+
"scripts/publish-reviewed.mjs": "e046dda0c47cb4f7b2d044e22d7c719d5f1fda02762bef4d013125da6258fbd3",
|
|
8
|
+
".github/workflows/npm-publish.yml": "0bb481c699566136e271babf8bdb7860b545c0e789034c0e06f5c0556d84b250",
|
|
8
9
|
"scripts/verify-release.mjs": "d0f9baead779e553385bb7c26f1238ad4e60a189a818d4db4f721a1e5f88dedc",
|
|
9
10
|
"scripts/update-dependency.mjs": "9919f172be33973f124eabf56c1dc70e41e941e601783ef01aba1c47dd70db9d",
|
|
10
|
-
"scripts/check-release-tooling.mjs": "
|
|
11
|
+
"scripts/check-release-tooling.mjs": "667e9c096ef28cddaefb90be92601828e7268c39f8a285f6644d3784db6f5064",
|
|
11
12
|
".github/workflows/quality.yml": "a014920a9f89dc494a020bee5f69aec779cf58825f491690f9ed1ec089ed6fa0"
|
|
12
13
|
}
|
|
13
14
|
}
|
|
@@ -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
|
+
}
|
|
@@ -52,7 +52,7 @@ function rejectionReason(context: ReasoningPositionContext, hash: string, witnes
|
|
|
52
52
|
|| witness.fen !== (witness.phase === 'before' ? context.fenBefore : context.fenAfter)) return 'root-mismatch';
|
|
53
53
|
if (typeof witness.engineBuild !== 'string' || !witness.engineBuild || typeof witness.command !== 'string' || !witness.command
|
|
54
54
|
|| !positiveInteger(witness.requestedDepth) || !positiveInteger(witness.multiPv) || witness.multiPv > 32
|
|
55
|
-
|| !Array.isArray(witness.lines) || witness.lines.length
|
|
55
|
+
|| !Array.isArray(witness.lines) || witness.lines.length < 1 || witness.lines.length > witness.multiPv
|
|
56
56
|
|| !Array.isArray(witness.rootMoves) || witness.rootMoves.length > 32
|
|
57
57
|
|| witness.rootMoves.some(root => typeof root !== 'string' || !UCI.test(root))
|
|
58
58
|
|| new Set(witness.rootMoves).size !== witness.rootMoves.length) return 'invalid-search-scope';
|
|
@@ -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
|
+
});
|
|
@@ -24,3 +24,31 @@ test('fresh answer witnesses preserve shipped v22 reference geometry and reject
|
|
|
24
24
|
assert.equal(rejected.referenceGapCounts.unresolvedReferences,1);
|
|
25
25
|
assert.equal(rejected.blocks[0].tokens[0].reference,undefined);
|
|
26
26
|
});
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
test('completed unrestricted MultiPV witnesses accept fewer lines without changing requested scope', () => {
|
|
30
|
+
const fen='7r/p1p1p1kp/2p2rpQ/3q4/3P4/2P5/P1P2PP1/R3K2R b KQ - 5 18';
|
|
31
|
+
const context={kind:'position',nodeId:'two-legal',historyStartFen:fen,historyUci:[],moveUci:'',moveSan:'',fenBefore:fen,fenAfter:fen};
|
|
32
|
+
const roots=['g7g8','g7f7'];
|
|
33
|
+
assert.deepEqual(new Set(new Chess(fen).moves({verbose:true}).map(m=>m.from+m.to)),new Set(roots));
|
|
34
|
+
const witness={version:'fresh-engine-pv-witness/1',acquisition:'same-answer-engine',acquisitionId:1,
|
|
35
|
+
evidenceId:'server-engine-acquisition:two-legal',contextHash:reasoningContextHash(context),phase:'before',fen,
|
|
36
|
+
engineBuild:'test-engine',command:'go depth 18',requestedDepth:18,multiPv:3,rootMoves:[],
|
|
37
|
+
authority:'bounded-engine',licenses:['engine.bounded-continuation'],
|
|
38
|
+
lines:roots.map((rootMove,index)=>({rootMove,pv:[rootMove],depth:18,scoreCp:-200-index,mateInMoves:null,
|
|
39
|
+
scoreProvenance:{kind:'root-search',fen,engineBuild:'test-engine',command:'go depth 18',originatingDepth:18,reportedDepth:18}}))};
|
|
40
|
+
const admitted=admitFreshEnginePvWitnesses(context,[witness]);
|
|
41
|
+
assert.equal(admitted.audit.acceptedLines,2);
|
|
42
|
+
assert.equal(witness.multiPv,3,'requested MultiPV remains provenance, not returned-line count');
|
|
43
|
+
assert.equal(admitFreshEnginePvWitnesses(context,[{...witness,lines:[witness.lines[0]]}]).audit.acceptedLines,1);
|
|
44
|
+
for (const patch of [{lines:[]},{multiPv:1},{rootMoves:roots,lines:[witness.lines[0]]}]) {
|
|
45
|
+
assert.equal(admitFreshEnginePvWitnesses(context,[{...witness,...patch}]).audit.acceptedLines,0);
|
|
46
|
+
}
|
|
47
|
+
for (const change of [line=>{line.depth=17;},line=>{line.scoreCp=null;},line=>{line.pv=['g7g6'];}]) {
|
|
48
|
+
const invalid=structuredClone(witness);change(invalid.lines[1]);
|
|
49
|
+
assert.equal(admitFreshEnginePvWitnesses(context,[invalid]).audit.acceptedLines,0);
|
|
50
|
+
}
|
|
51
|
+
const rendered=simpleProseReferences('[Kg8](move:before:g7g8)',context,[],undefined,undefined,undefined,{freshEnginePvWitnesses:[witness]});
|
|
52
|
+
assert.equal(rendered.referenceGapCounts.unresolvedReferences,0);
|
|
53
|
+
assert.equal(rendered.freshEnginePvAdmission.acceptedLines,2);
|
|
54
|
+
});
|
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;
|