@knights-eye-chess/ai-analysis 0.1.0-dev.14 → 0.1.0-dev.18
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 +6 -2
- package/README.md +7 -9
- package/dist/{chunk-XQDZJOYD.js → chunk-6OXAG5H3.js} +1 -1
- package/dist/{chunk-PLKNJRPF.js → chunk-TYQ5WIL2.js} +1 -1
- package/dist/index.js +2 -2
- 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 +2 -2
- package/dist/runtime-closure.json +1 -1
- package/dist/shared.js +1 -1
- package/docs/API.md +9 -0
- package/npm-release.json +1 -1
- package/package.json +2 -2
- package/scripts/check-release-tooling.mjs +1 -1
- package/scripts/check-review-marker.mjs +79 -0
- package/scripts/publish-reviewed.mjs +31 -14
- package/scripts/release-tooling.json +4 -3
- package/src/chess-reasoning/fresh-engine-pv-witnesses.ts +1 -1
- package/test/owner/prose-reference-admission.test.mjs +28 -0
package/NPM-PUBLISHING.md
CHANGED
|
@@ -2,16 +2,20 @@
|
|
|
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 is `0.1.0-dev.14`. The source checkout now prepares
|
|
6
|
+
`0.1.0-dev.17` with Move Classifier `0.1.0-dev.13`.
|
|
7
|
+
This next candidate has not been published; `@next` remains the verified registry release.
|
|
8
|
+
|
|
5
9
|
## Prepare the exact candidate
|
|
6
10
|
|
|
7
|
-
Use Node 24 and npm 11:
|
|
11
|
+
Use Node 24.19.0 and npm 11.9.0:
|
|
8
12
|
|
|
9
13
|
```sh
|
|
10
14
|
npm ci
|
|
11
15
|
npm run publish:prepare
|
|
12
16
|
```
|
|
13
17
|
|
|
14
|
-
Review `artifacts/knights-eye-chess-ai-analysis-0.1.0-dev.
|
|
18
|
+
Review `artifacts/knights-eye-chess-ai-analysis-0.1.0-dev.17.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.
|
|
15
19
|
|
|
16
20
|
Before every coordinated release, run `node tools/release-tooling/sync.mjs --check` in the reviewed app checkout alongside all six component revisions. Record those commits and the result.
|
|
17
21
|
|
package/README.md
CHANGED
|
@@ -22,19 +22,17 @@ engine hosting, persistence and privacy settings for chess data and traces.
|
|
|
22
22
|
|
|
23
23
|
## Get started
|
|
24
24
|
|
|
25
|
-
Use Node 24 or newer.
|
|
26
|
-
|
|
25
|
+
Use Node 24 or newer. Install the published prerelease with the chess library
|
|
26
|
+
imported by the example below:
|
|
27
27
|
|
|
28
28
|
```sh
|
|
29
|
-
npm
|
|
30
|
-
npm run pack:checked
|
|
29
|
+
npm install @knights-eye-chess/ai-analysis@next chess.js@1.4.0
|
|
31
30
|
```
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
```
|
|
32
|
+
The first reviewed registry candidate is `0.1.0-dev.14`; pin the exact version
|
|
33
|
+
for a reproducible install. Source developers can use `npm ci` and
|
|
34
|
+
`npm run pack:checked` in this checkout.
|
|
35
|
+
[SOURCE-ORIGIN.json](SOURCE-ORIGIN.json) records historical archive provenance.
|
|
38
36
|
|
|
39
37
|
This runnable example uses a local connector with a fixed answer. It needs no
|
|
40
38
|
provider credentials or engine, and demonstrates host wiring rather than model
|
|
@@ -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";
|
|
@@ -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/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
simpleSystemPolicy,
|
|
18
18
|
simpleToolDescription,
|
|
19
19
|
validateSimpleToolInput
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-TYQ5WIL2.js";
|
|
21
21
|
import {
|
|
22
22
|
DEFAULT_REASONING_RUN_CONTROLS,
|
|
23
23
|
EVIDENCE_SHARING_VERSION,
|
|
@@ -71,7 +71,7 @@ import {
|
|
|
71
71
|
toPositionAnalysisProvider,
|
|
72
72
|
validateReasoningPositionContext,
|
|
73
73
|
validateSimpleAgentConfig
|
|
74
|
-
} from "./chunk-
|
|
74
|
+
} from "./chunk-6OXAG5H3.js";
|
|
75
75
|
export {
|
|
76
76
|
DEFAULT_REASONING_RUN_CONTROLS,
|
|
77
77
|
EVIDENCE_SHARING_VERSION,
|
|
@@ -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-TYQ5WIL2.js";
|
|
215
215
|
import {
|
|
216
216
|
CHESS_REASONING_POLICY_VERSION,
|
|
217
217
|
CHESS_REASONING_SCHEMA_VERSION,
|
|
@@ -316,7 +316,7 @@ import {
|
|
|
316
316
|
validateReasoningPositionContext,
|
|
317
317
|
validateSimpleAgentConfig,
|
|
318
318
|
withEvidenceLicenses
|
|
319
|
-
} from "./chunk-
|
|
319
|
+
} from "./chunk-6OXAG5H3.js";
|
|
320
320
|
|
|
321
321
|
// src/chess-reasoning/ablation-direct-synthesis.ts
|
|
322
322
|
import { SQUARES } from "chess.js";
|
package/dist/shared.js
CHANGED
package/docs/API.md
CHANGED
|
@@ -80,3 +80,12 @@ 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.
|
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.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@ai-sdk/provider": "4.0.9",
|
|
53
53
|
"@ai-sdk/provider-utils": "5.0.34",
|
|
54
|
-
"@knights-eye-chess/move-classifier": "0.1.0-dev.
|
|
54
|
+
"@knights-eye-chess/move-classifier": "0.1.0-dev.14",
|
|
55
55
|
"@types/json-schema": "7.0.15",
|
|
56
56
|
"@types/node": "22.19.19",
|
|
57
57
|
"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
|
|
|
@@ -11,15 +12,15 @@ export function npmFailure(result) {
|
|
|
11
12
|
for (const output of [result.stdout, result.stderr]) {
|
|
12
13
|
try { const parsed = JSON.parse(output || '{}'); error ||= parsed.error; } catch {}
|
|
13
14
|
}
|
|
14
|
-
const allowed = new Set(['EOTP', 'E401', 'E403', 'E404', 'E409', 'E422', 'E429', 'E500', 'E502', 'E503', 'E504', 'EPROVENANCE', 'EUSAGE', 'ENEEDAUTH', 'ENOTP', 'EPRIVATE', 'EPUBLISHCONFLICT', 'EUNSCOPED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND']);
|
|
15
|
-
const candidate = typeof error?.code === 'string' ? error.code : (result.stderr || '').match(/^npm (?:error|ERR!) code ([A-Z][A-Z0-
|
|
15
|
+
const allowed = new Set(['EOTP', 'E401', 'E403', 'E404', 'E408', 'E409', 'E422', 'E429', 'E500', 'E502', 'E503', 'E504', 'EPROVENANCE', 'EUSAGE', 'ENEEDAUTH', 'ENOTP', 'EPRIVATE', 'EPUBLISHCONFLICT', 'EUNSCOPED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED']);
|
|
16
|
+
const candidate = typeof error?.code === 'string' ? error.code : result.error?.code || (result.stderr || '').match(/^npm (?:error|ERR!) code ([A-Z][A-Z0-9_]+)\s*$/m)?.[1];
|
|
16
17
|
const code = allowed.has(candidate) ? candidate : 'UNKNOWN';
|
|
17
18
|
const rawStatus = error?.statusCode ?? error?.status ?? (/^E[45]\d\d$/.test(code) ? Number(code.slice(1)) : undefined);
|
|
18
19
|
const httpStatus = Number.isInteger(rawStatus) && rawStatus >= 400 && rawStatus <= 599 ? rawStatus : 'unknown';
|
|
19
20
|
const exitStatus = Number.isInteger(result.status) && result.status >= 0 && result.status <= 255 ? result.status : 'unknown';
|
|
20
21
|
return {code, httpStatus, exitStatus};
|
|
21
22
|
}
|
|
22
|
-
export function publishReviewed({root, version, sha256, env = process.env, run = spawnSync, wait =
|
|
23
|
+
export function publishReviewed({root, version, sha256, env = process.env, run = spawnSync, wait = milliseconds => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds), now = Date.now}) {
|
|
23
24
|
if (env.GITHUB_ACTIONS !== 'true' || env.GITHUB_EVENT_NAME !== 'workflow_dispatch' || env.GITHUB_REF !== 'refs/heads/dev') throw Error('Publishing requires manual GitHub Actions dispatch on dev');
|
|
24
25
|
if (env.GITHUB_REPOSITORY_VISIBILITY !== 'public') throw Error('Provenance publishing requires a public GitHub repository');
|
|
25
26
|
if (!env.NODE_AUTH_TOKEN?.trim()) throw Error('Missing NODE_AUTH_TOKEN; publication has no authentication fallback');
|
|
@@ -35,26 +36,31 @@ export function publishReviewed({root, version, sha256, env = process.env, run =
|
|
|
35
36
|
const pkg = JSON.parse(metadataResult.stdout);
|
|
36
37
|
if (pkg.name !== source.name || pkg.version !== version || pkg.private || pkg.publishConfig?.access !== 'public' || pkg.publishConfig?.registry !== 'https://registry.npmjs.org/' || pkg.publishConfig?.tag !== 'next') throw Error('Expected reviewed staged registry candidate');
|
|
37
38
|
const integrity = 'sha512-' + createHash('sha512').update(bytes).digest('base64');
|
|
38
|
-
const lookupArgs = ['view', `${pkg.name}@${version}`, '--json', '--registry', 'https://registry.npmjs.org/'];
|
|
39
|
+
const lookupArgs = ['view', `${pkg.name}@${version}`, '--json', '--registry', 'https://registry.npmjs.org/', '--prefer-online', '--fetch-retries=0', '--fetch-timeout=15000'];
|
|
39
40
|
const lookupOptions = { ...options, timeout: 15000 };
|
|
40
41
|
const lookup = run('npm', lookupArgs, lookupOptions);
|
|
41
42
|
if (lookup.status === 0) {
|
|
42
|
-
|
|
43
|
+
let existing;
|
|
44
|
+
try { existing = JSON.parse(lookup.stdout); } catch { throw Error('Registry lookup returned invalid JSON; refusing publication'); }
|
|
43
45
|
if (existing.name !== pkg.name || existing.version !== version || existing.dist?.integrity !== integrity) throw Error('Published version exists with different identity or integrity');
|
|
44
46
|
return { name: pkg.name, version, sha256, integrity, published: false, alreadyPublished: true, registryConfirmed: true };
|
|
45
47
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (error?.code !== 'E404') throw Error('Registry lookup failed; refusing publication');
|
|
48
|
+
const absent = npmFailure(lookup);
|
|
49
|
+
if (absent.code !== 'E404') throw Error(`Registry lookup failed; refusing publication: code=${absent.code}, httpStatus=${absent.httpStatus}, exitStatus=${absent.exitStatus}`);
|
|
49
50
|
const result = run('npm', ['publish', archive, '--ignore-scripts', '--access', 'public', '--tag', 'next', '--registry', 'https://registry.npmjs.org/', '--provenance', '--json'], options);
|
|
50
51
|
if (result.status !== 0) {
|
|
51
52
|
const {code, httpStatus, exitStatus} = npmFailure(result);
|
|
52
53
|
throw Error(`npm publish failed for ${pkg.name}@${version}: code=${code}, httpStatus=${httpStatus}, exitStatus=${exitStatus}`);
|
|
53
54
|
}
|
|
54
|
-
let visible = false;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
let visible = false, attempts = 0, lastFailure;
|
|
56
|
+
const started = now(), deadline = started + 240000;
|
|
57
|
+
const transientCodes = new Set(['E404', 'E408', 'E429', 'E500', 'E502', 'E503', 'E504', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'EAI_AGAIN', 'ECONNREFUSED']);
|
|
58
|
+
for (let attempt = 0; attempt < 25; attempt++) {
|
|
59
|
+
if (attempt) wait(Math.min(10000, Math.max(0, deadline - now())));
|
|
60
|
+
const remaining = deadline - now();
|
|
61
|
+
if (remaining <= 0) break;
|
|
62
|
+
const confirmation = run('npm', lookupArgs, { ...lookupOptions, timeout: Math.min(15000, remaining) });
|
|
63
|
+
attempts++;
|
|
58
64
|
if (confirmation.status === 0) {
|
|
59
65
|
let existing;
|
|
60
66
|
try { existing = JSON.parse(confirmation.stdout); } catch {}
|
|
@@ -62,11 +68,22 @@ export function publishReviewed({root, version, sha256, env = process.env, run =
|
|
|
62
68
|
visible = true;
|
|
63
69
|
break;
|
|
64
70
|
}
|
|
71
|
+
lastFailure = npmFailure(confirmation);
|
|
72
|
+
const {code, httpStatus, exitStatus} = lastFailure;
|
|
73
|
+
if (['E401', 'E403', 'ENEEDAUTH', 'EOTP', 'ENOTP'].includes(code) || [401, 403].includes(httpStatus) || !(transientCodes.has(code) || [408, 429, 500, 502, 503, 504].includes(httpStatus))) {
|
|
74
|
+
throw Error(`Postpublication registry confirmation failed for ${pkg.name}@${version}: code=${code}, httpStatus=${httpStatus}, exitStatus=${exitStatus}; publication was not retried`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!visible) {
|
|
78
|
+
const {code = 'UNKNOWN', httpStatus = 'unknown', exitStatus = 'unknown'} = lastFailure || {};
|
|
79
|
+
throw Error(`npm publication returned success but registry confirmation pending for ${pkg.name}@${version}: attempts=${attempts}, elapsedMs=${Math.max(0, Math.min(240000, now() - started))}, code=${code}, httpStatus=${httpStatus}, exitStatus=${exitStatus}; bounded confirmation ended, publication was not retried`);
|
|
65
80
|
}
|
|
66
|
-
if (!visible) throw Error(`npm publication returned success but registry confirmation failed for ${pkg.name}@${version}; publication was not retried`);
|
|
67
81
|
return { name: pkg.name, version, sha256, integrity, published: true, alreadyPublished: false, registryConfirmed: true };
|
|
68
82
|
}
|
|
69
83
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
70
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 });
|
|
71
88
|
console.log(JSON.stringify(publishReviewed({root: fileURLToPath(new URL('../', import.meta.url)), version: process.argv[2], sha256: process.argv[3]})));
|
|
72
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
|
}
|
|
@@ -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';
|
|
@@ -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
|
+
});
|