proofseal 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +13 -0
- package/README.md +210 -2
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +440 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +58 -0
- package/dist/config.js.map +1 -0
- package/dist/core/canonical.d.ts +16 -0
- package/dist/core/canonical.js +29 -0
- package/dist/core/canonical.js.map +1 -0
- package/dist/core/hash.d.ts +32 -0
- package/dist/core/hash.js +81 -0
- package/dist/core/hash.js.map +1 -0
- package/dist/core/marker-lint.d.ts +5 -0
- package/dist/core/marker-lint.js +55 -0
- package/dist/core/marker-lint.js.map +1 -0
- package/dist/core/paths.d.ts +10 -0
- package/dist/core/paths.js +13 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/harness/quantize.d.ts +38 -0
- package/dist/harness/quantize.js +76 -0
- package/dist/harness/quantize.js.map +1 -0
- package/dist/harness/run.d.ts +61 -0
- package/dist/harness/run.js +137 -0
- package/dist/harness/run.js.map +1 -0
- package/dist/history/gitinfo.d.ts +16 -0
- package/dist/history/gitinfo.js +69 -0
- package/dist/history/gitinfo.js.map +1 -0
- package/dist/history/jsonl.d.ts +28 -0
- package/dist/history/jsonl.js +71 -0
- package/dist/history/jsonl.js.map +1 -0
- package/dist/history/queries.d.ts +43 -0
- package/dist/history/queries.js +86 -0
- package/dist/history/queries.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/keys/derive.d.ts +28 -0
- package/dist/keys/derive.js +59 -0
- package/dist/keys/derive.js.map +1 -0
- package/dist/manifest/schema.d.ts +1068 -0
- package/dist/manifest/schema.js +102 -0
- package/dist/manifest/schema.js.map +1 -0
- package/dist/manifest/seal.d.ts +41 -0
- package/dist/manifest/seal.js +185 -0
- package/dist/manifest/seal.js.map +1 -0
- package/dist/manifest/verify.d.ts +102 -0
- package/dist/manifest/verify.js +246 -0
- package/dist/manifest/verify.js.map +1 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +138 -0
- package/dist/mcp/server.js.map +1 -0
- package/package.json +50 -3
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHA-256 helpers. Files are always read as raw bytes (never text mode)
|
|
3
|
+
* so line-ending normalization can never silently change a hash (R2).
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
/** sha256 of a string (utf8) or Buffer, lowercase hex (64 chars). */
|
|
8
|
+
export function sha256Hex(input) {
|
|
9
|
+
return createHash('sha256').update(input).digest('hex');
|
|
10
|
+
}
|
|
11
|
+
/** sha256 of a string (utf8) or Buffer, raw 32 bytes. */
|
|
12
|
+
export function sha256Bytes(input) {
|
|
13
|
+
return createHash('sha256').update(input).digest();
|
|
14
|
+
}
|
|
15
|
+
/** sha256 of a file's raw bytes, lowercase hex. */
|
|
16
|
+
export function fileSha256(absPath) {
|
|
17
|
+
return createHash('sha256').update(readFileSync(absPath)).digest('hex');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* sha256 of a file's bytes with CRLF→LF normalization (premortem #7:
|
|
21
|
+
* Windows insurance). Used ONLY for diagnostics: when a file-hash claim
|
|
22
|
+
* regresses but the CRLF-normalized hash still matches the sealed hash,
|
|
23
|
+
* the cause is almost certainly git autocrlf rewriting line endings —
|
|
24
|
+
* the regressed detail can then name the cause instead of crying tamper.
|
|
25
|
+
*/
|
|
26
|
+
export function fileSha256CrlfNormalized(absPath) {
|
|
27
|
+
const raw = readFileSync(absPath);
|
|
28
|
+
const out = Buffer.alloc(raw.length);
|
|
29
|
+
let n = 0;
|
|
30
|
+
for (let i = 0; i < raw.length; i++) {
|
|
31
|
+
if (raw[i] === 0x0d && raw[i + 1] === 0x0a)
|
|
32
|
+
continue; // drop CR of CRLF
|
|
33
|
+
out[n++] = raw[i];
|
|
34
|
+
}
|
|
35
|
+
return createHash('sha256').update(out.subarray(0, n)).digest('hex');
|
|
36
|
+
}
|
|
37
|
+
/** True if the file (decoded as utf8) contains the marker substring. */
|
|
38
|
+
export function fileContains(absPath, marker) {
|
|
39
|
+
return readFileSync(absPath, 'utf8').includes(marker);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Whitespace normalization for marker matching:
|
|
43
|
+
* 1. collapse every run of whitespace (spaces, tabs, newlines) to one space;
|
|
44
|
+
* 2. drop spaces that touch a non-word character (a space is only
|
|
45
|
+
* SIGNIFICANT between two identifier characters).
|
|
46
|
+
*
|
|
47
|
+
* Step 2 matters because formatters do not just reflow EXISTING whitespace —
|
|
48
|
+
* a Prettier line-wrap inserts whitespace where there was none (a newline
|
|
49
|
+
* after `(`, before `)`, around operators). Pure run-collapsing would still
|
|
50
|
+
* read `computeTotal(\n items\n)` as missing the marker
|
|
51
|
+
* `computeTotal(items)`. Keeping only word-adjacent spaces makes any
|
|
52
|
+
* whitespace-only rewrite match while `foo bar` can never match `foobar`
|
|
53
|
+
* (so a marker cannot pass via accidental token merging).
|
|
54
|
+
*/
|
|
55
|
+
function normalizeForMarker(s) {
|
|
56
|
+
return s.replace(/\s+/g, ' ').replace(/ ?([^A-Za-z0-9_ ]) ?/g, '$1').trim();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Whitespace-normalized marker presence (premortem #7: marker robustness).
|
|
60
|
+
*
|
|
61
|
+
* Markers exist to outlive REBUILDS of a file — but a plain substring check
|
|
62
|
+
* also breaks under routine reformatting (Prettier line-wraps, re-indents,
|
|
63
|
+
* tabs→spaces). That reads as a false "regressed" and teaches users that
|
|
64
|
+
* ProofSeal lies. Both the file text and the marker are normalized (see
|
|
65
|
+
* `normalizeForMarker`) before matching, so whitespace-only rewrites of the
|
|
66
|
+
* marker's surroundings still match while any non-whitespace edit to the
|
|
67
|
+
* marker text itself still (correctly) fails.
|
|
68
|
+
*
|
|
69
|
+
* Plain `fileContains` is kept for exact-substring use cases.
|
|
70
|
+
*/
|
|
71
|
+
export function markerPresent(text, marker) {
|
|
72
|
+
return normalizeForMarker(text).includes(normalizeForMarker(marker));
|
|
73
|
+
}
|
|
74
|
+
/** Whitespace-normalized count of (non-overlapping) marker occurrences. */
|
|
75
|
+
export function markerOccurrences(text, marker) {
|
|
76
|
+
const needle = normalizeForMarker(marker);
|
|
77
|
+
if (needle === '')
|
|
78
|
+
return 0;
|
|
79
|
+
return normalizeForMarker(text).split(needle).length - 1;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=hash.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash.js","sourceRoot":"","sources":["../../src/core/hash.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,qEAAqE;AACrE,MAAM,UAAU,SAAS,CAAC,KAAsB;IAC9C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,WAAW,CAAC,KAAsB;IAChD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,CAAC;AACrD,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,UAAU,CAAC,OAAe;IACxC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAe;IACtD,MAAM,GAAG,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IAClC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACrC,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI;YAAE,SAAS,CAAC,kBAAkB;QACxE,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IACD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACvE,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,MAAc;IAC1D,OAAO,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAS,kBAAkB,CAAC,CAAS;IACnC,OAAO,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,uBAAuB,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;AAC9E,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,MAAc;IACxD,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC;AACvE,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,MAAc;IAC5D,MAAM,MAAM,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC1C,IAAI,MAAM,KAAK,EAAE;QAAE,OAAO,CAAC,CAAC;IAC5B,OAAO,kBAAkB,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authoring-time marker lint (premortem #7: marker robustness).
|
|
3
|
+
*
|
|
4
|
+
* Heuristics that flag fragile markers at `claim add` time — the moment the
|
|
5
|
+
* user can still pick a better one. Lint results are ADVISORY ONLY: they are
|
|
6
|
+
* printed as warnings and never fail the command (a noisy hard failure here
|
|
7
|
+
* would push users back to file-hash claims, which are MORE fragile).
|
|
8
|
+
*/
|
|
9
|
+
import { markerOccurrences } from './hash.js';
|
|
10
|
+
const SUGGESTION = 'prefer a function/identifier name or a structural code fragment unique to the fix';
|
|
11
|
+
/**
|
|
12
|
+
* Lint a marker string against optional target-file text.
|
|
13
|
+
* Returns human-readable warning strings (empty array = no concerns).
|
|
14
|
+
*/
|
|
15
|
+
export function lintMarker(marker, fileText) {
|
|
16
|
+
const warnings = [];
|
|
17
|
+
// (i) Non-unique marker: a duplicate occurrence can mask removal of the
|
|
18
|
+
// real fix (the OTHER copy keeps the claim green). Whitespace-normalized,
|
|
19
|
+
// consistent with how presence is verified.
|
|
20
|
+
if (fileText !== undefined) {
|
|
21
|
+
const n = markerOccurrences(fileText, marker);
|
|
22
|
+
if (n > 1) {
|
|
23
|
+
warnings.push(`marker appears ${n} times in the target file (whitespace-normalized) — ` +
|
|
24
|
+
`duplicates can mask removal of the real fix; ${SUGGESTION}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// (ii) Looks like a log/exception message. These strings are routinely
|
|
28
|
+
// reworded during refactors, so they make poor long-lived markers.
|
|
29
|
+
const trimmed = marker.trim();
|
|
30
|
+
const quotedWhole = /^["'`].*["'`]$/s.test(trimmed);
|
|
31
|
+
const hasPlaceholders = /%[sdif]|\$\{|\{\}/.test(marker);
|
|
32
|
+
const hasLogWords = /\b(error|warn|fail|cannot|invalid|exception)\b/i.test(marker);
|
|
33
|
+
// "natural-language sentence": several space-separated words starting with a letter.
|
|
34
|
+
const isSentence = /^[A-Za-z]/.test(trimmed) && trimmed.split(/\s+/).length >= 4;
|
|
35
|
+
if (quotedWhole || hasPlaceholders || (hasLogWords && isSentence)) {
|
|
36
|
+
warnings.push('marker looks like a log/exception message — message text is routinely ' +
|
|
37
|
+
`reworded and will read as a false regression; ${SUGGESTION}`);
|
|
38
|
+
}
|
|
39
|
+
// (iii) Formatting-sensitive characters that formatters commonly rewrite.
|
|
40
|
+
const traits = [];
|
|
41
|
+
if (marker.includes('`'))
|
|
42
|
+
traits.push('backticks');
|
|
43
|
+
if (/ {2,}/.test(marker))
|
|
44
|
+
traits.push('multiple consecutive spaces');
|
|
45
|
+
if (/^\s|\s$/.test(marker))
|
|
46
|
+
traits.push('leading/trailing whitespace');
|
|
47
|
+
if (marker.length >= 2 && /^["'].*["']$/s.test(marker))
|
|
48
|
+
traits.push('quotes at both ends');
|
|
49
|
+
if (traits.length > 0) {
|
|
50
|
+
warnings.push(`marker contains formatting-sensitive characters (${traits.join(', ')}) — ` +
|
|
51
|
+
`formatters like Prettier may rewrite these; ${SUGGESTION}`);
|
|
52
|
+
}
|
|
53
|
+
return warnings;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=marker-lint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"marker-lint.js","sourceRoot":"","sources":["../../src/core/marker-lint.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAE9C,MAAM,UAAU,GACd,mFAAmF,CAAC;AAEtF;;;GAGG;AACH,MAAM,UAAU,UAAU,CAAC,MAAc,EAAE,QAAiB;IAC1D,MAAM,QAAQ,GAAa,EAAE,CAAC;IAE9B,wEAAwE;IACxE,0EAA0E;IAC1E,4CAA4C;IAC5C,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,MAAM,CAAC,GAAG,iBAAiB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC9C,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACV,QAAQ,CAAC,IAAI,CACX,kBAAkB,CAAC,sDAAsD;gBACvE,gDAAgD,UAAU,EAAE,CAC/D,CAAC;QACJ,CAAC;IACH,CAAC;IAED,uEAAuE;IACvE,mEAAmE;IACnE,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAC9B,MAAM,WAAW,GAAG,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACpD,MAAM,eAAe,GAAG,mBAAmB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACzD,MAAM,WAAW,GAAG,iDAAiD,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACnF,qFAAqF;IACrF,MAAM,UAAU,GAAG,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC;IACjF,IAAI,WAAW,IAAI,eAAe,IAAI,CAAC,WAAW,IAAI,UAAU,CAAC,EAAE,CAAC;QAClE,QAAQ,CAAC,IAAI,CACX,wEAAwE;YACtE,iDAAiD,UAAU,EAAE,CAChE,CAAC;IACJ,CAAC;IAED,0EAA0E;IAC1E,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACnD,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;IACrE,IAAI,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;IACvE,IAAI,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,eAAe,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,MAAM,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;IAC3F,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,QAAQ,CAAC,IAAI,CACX,oDAAoD,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM;YACzE,+CAA+C,UAAU,EAAE,CAC9D,CAAC;IACJ,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform claim-path normalization (premortem #7: Windows insurance).
|
|
3
|
+
*
|
|
4
|
+
* Claim file paths are stored and compared with forward slashes ONLY, at
|
|
5
|
+
* both seal and verify time: a claim authored on Windows with backslashes
|
|
6
|
+
* must match on POSIX and vice versa. `node:path.join` accepts forward
|
|
7
|
+
* slashes on every platform, so normalized paths resolve everywhere.
|
|
8
|
+
*/
|
|
9
|
+
/** Normalize a repo-relative claim path to forward slashes. */
|
|
10
|
+
export declare function normalizeClaimPath(p: string): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform claim-path normalization (premortem #7: Windows insurance).
|
|
3
|
+
*
|
|
4
|
+
* Claim file paths are stored and compared with forward slashes ONLY, at
|
|
5
|
+
* both seal and verify time: a claim authored on Windows with backslashes
|
|
6
|
+
* must match on POSIX and vice versa. `node:path.join` accepts forward
|
|
7
|
+
* slashes on every platform, so normalized paths resolve everywhere.
|
|
8
|
+
*/
|
|
9
|
+
/** Normalize a repo-relative claim path to forward slashes. */
|
|
10
|
+
export function normalizeClaimPath(p) {
|
|
11
|
+
return p.replace(/\\/g, '/');
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,+DAA+D;AAC/D,MAAM,UAAU,kBAAkB,CAAC,CAAS;IAC1C,OAAO,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAC/B,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const DEFAULT_DECIMALS = 6;
|
|
2
|
+
export declare const DEFAULT_TOLERANCE: {
|
|
3
|
+
readonly rtol: 0.0001;
|
|
4
|
+
readonly atol: 0.000001;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Round to N decimals with ties going to the even neighbor (banker's
|
|
8
|
+
* rounding — numpy semantics, NOT JS Math.round half-up; pitfall 7).
|
|
9
|
+
*/
|
|
10
|
+
export declare function roundHalfEven(value: number, decimals: number): number;
|
|
11
|
+
/** Quantize every value (round-half-even at N decimals). */
|
|
12
|
+
export declare function quantizeValues(values: readonly number[], decimals: number): number[];
|
|
13
|
+
/** Pack values as little-endian IEEE-754 float64 (struct.pack "<Nd"). */
|
|
14
|
+
export declare function packLEFloat64(values: readonly number[]): Buffer;
|
|
15
|
+
/**
|
|
16
|
+
* The full quantize → pack-LE-f64 → SHA-256 pipeline.
|
|
17
|
+
* Streams in chunks so large vectors never materialize one giant buffer.
|
|
18
|
+
*/
|
|
19
|
+
export declare function hashQuantized(values: readonly number[], decimals?: number): string;
|
|
20
|
+
export interface AllCloseResult {
|
|
21
|
+
ok: boolean;
|
|
22
|
+
/** Lengths matched? */
|
|
23
|
+
lengthMatch: boolean;
|
|
24
|
+
/** Count of out-of-tolerance elements. */
|
|
25
|
+
outOfTolerance: number;
|
|
26
|
+
/** Worst offender, for divergence forensics. */
|
|
27
|
+
worst?: {
|
|
28
|
+
index: number;
|
|
29
|
+
actual: number;
|
|
30
|
+
expected: number;
|
|
31
|
+
diff: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* numpy.allclose semantics: |a - b| <= atol + rtol * |b|, element-wise,
|
|
36
|
+
* with divergence forensics (per-element out-of-tolerance count + worst index).
|
|
37
|
+
*/
|
|
38
|
+
export declare function allClose(actual: readonly number[], expected: readonly number[], rtol?: number, atol?: number): AllCloseResult;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic numeric hashing (RuView Trust Kill Switch port):
|
|
3
|
+
* round-half-even quantization at N decimals → little-endian IEEE-754
|
|
4
|
+
* float64 packing → streamed SHA-256. Plus the rtol/atol tolerance gate
|
|
5
|
+
* with divergence forensics (issue #560 dual-gate verdict).
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
export const DEFAULT_DECIMALS = 6;
|
|
9
|
+
export const DEFAULT_TOLERANCE = { rtol: 1e-4, atol: 1e-6 };
|
|
10
|
+
/**
|
|
11
|
+
* Round to N decimals with ties going to the even neighbor (banker's
|
|
12
|
+
* rounding — numpy semantics, NOT JS Math.round half-up; pitfall 7).
|
|
13
|
+
*/
|
|
14
|
+
export function roundHalfEven(value, decimals) {
|
|
15
|
+
if (!Number.isFinite(value))
|
|
16
|
+
return value;
|
|
17
|
+
const factor = Math.pow(10, decimals);
|
|
18
|
+
const scaled = value * factor;
|
|
19
|
+
let rounded = Math.round(scaled); // half-toward-+Infinity
|
|
20
|
+
// Math.round on an exact .5 tie always lands on floor(scaled)+1; if that
|
|
21
|
+
// result is odd, the even neighbor is rounded-1 (works for both signs).
|
|
22
|
+
if (scaled - Math.floor(scaled) === 0.5 && rounded % 2 !== 0) {
|
|
23
|
+
rounded -= 1;
|
|
24
|
+
}
|
|
25
|
+
return rounded / factor;
|
|
26
|
+
}
|
|
27
|
+
/** Quantize every value (round-half-even at N decimals). */
|
|
28
|
+
export function quantizeValues(values, decimals) {
|
|
29
|
+
return values.map((v) => roundHalfEven(v, decimals));
|
|
30
|
+
}
|
|
31
|
+
/** Pack values as little-endian IEEE-754 float64 (struct.pack "<Nd"). */
|
|
32
|
+
export function packLEFloat64(values) {
|
|
33
|
+
const buf = Buffer.alloc(values.length * 8);
|
|
34
|
+
for (let i = 0; i < values.length; i++) {
|
|
35
|
+
buf.writeDoubleLE(values[i], i * 8);
|
|
36
|
+
}
|
|
37
|
+
return buf;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The full quantize → pack-LE-f64 → SHA-256 pipeline.
|
|
41
|
+
* Streams in chunks so large vectors never materialize one giant buffer.
|
|
42
|
+
*/
|
|
43
|
+
export function hashQuantized(values, decimals = DEFAULT_DECIMALS) {
|
|
44
|
+
const hasher = createHash('sha256');
|
|
45
|
+
const CHUNK = 4096;
|
|
46
|
+
for (let i = 0; i < values.length; i += CHUNK) {
|
|
47
|
+
const slice = values.slice(i, i + CHUNK).map((v) => roundHalfEven(v, decimals));
|
|
48
|
+
hasher.update(packLEFloat64(slice));
|
|
49
|
+
}
|
|
50
|
+
return hasher.digest('hex');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* numpy.allclose semantics: |a - b| <= atol + rtol * |b|, element-wise,
|
|
54
|
+
* with divergence forensics (per-element out-of-tolerance count + worst index).
|
|
55
|
+
*/
|
|
56
|
+
export function allClose(actual, expected, rtol = DEFAULT_TOLERANCE.rtol, atol = DEFAULT_TOLERANCE.atol) {
|
|
57
|
+
if (actual.length !== expected.length) {
|
|
58
|
+
return { ok: false, lengthMatch: false, outOfTolerance: Math.abs(actual.length - expected.length) };
|
|
59
|
+
}
|
|
60
|
+
let outOfTolerance = 0;
|
|
61
|
+
let worst;
|
|
62
|
+
for (let i = 0; i < actual.length; i++) {
|
|
63
|
+
const a = actual[i];
|
|
64
|
+
const b = expected[i];
|
|
65
|
+
const diff = Math.abs(a - b);
|
|
66
|
+
const bound = atol + rtol * Math.abs(b);
|
|
67
|
+
if (!(diff <= bound)) {
|
|
68
|
+
outOfTolerance += 1;
|
|
69
|
+
if (!worst || diff > worst.diff) {
|
|
70
|
+
worst = { index: i, actual: a, expected: b, diff };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { ok: outOfTolerance === 0, lengthMatch: true, outOfTolerance, worst };
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=quantize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quantize.js","sourceRoot":"","sources":["../../src/harness/quantize.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type AllCloseResult } from './quantize.js';
|
|
2
|
+
export interface HarnessDef {
|
|
3
|
+
name: string;
|
|
4
|
+
/** Shell command; spawned with PROOFSEAL_SEED set. */
|
|
5
|
+
cmd: string;
|
|
6
|
+
/** Working directory (defaults to process.cwd()). */
|
|
7
|
+
cwd?: string;
|
|
8
|
+
seed?: number;
|
|
9
|
+
quantizeDecimals?: number;
|
|
10
|
+
/** Named output blocks to skip when stdout is a JSON object (pitfall 6). */
|
|
11
|
+
exclude?: string[];
|
|
12
|
+
/** Committed expectation hash; absent = no expectation yet. */
|
|
13
|
+
expectedSha256?: string;
|
|
14
|
+
/** Path to committed JSON array of full-precision reference numbers. */
|
|
15
|
+
referenceVector?: string;
|
|
16
|
+
tolerance?: {
|
|
17
|
+
rtol: number;
|
|
18
|
+
atol: number;
|
|
19
|
+
};
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
}
|
|
22
|
+
export type HarnessStatus = 'pass' | 'drift' | 'regressed' | 'missing' | 'error';
|
|
23
|
+
export interface HarnessResult {
|
|
24
|
+
name: string;
|
|
25
|
+
status: HarnessStatus;
|
|
26
|
+
/** sha256 of quantized LE-f64 output (present when the run produced output). */
|
|
27
|
+
hash?: string;
|
|
28
|
+
expectedSha256?: string;
|
|
29
|
+
hashMatch?: boolean;
|
|
30
|
+
toleranceMatch?: boolean;
|
|
31
|
+
forensics?: AllCloseResult;
|
|
32
|
+
/** Full-precision parsed values (for --update reference regeneration). */
|
|
33
|
+
values?: number[];
|
|
34
|
+
quantized?: number[];
|
|
35
|
+
seed: number;
|
|
36
|
+
quantizeDecimals: number;
|
|
37
|
+
exitCode: number | null;
|
|
38
|
+
error?: string;
|
|
39
|
+
/**
|
|
40
|
+
* The harness command itself could not be found (spawn ENOENT or shell
|
|
41
|
+
* exit 127). CI footgun: a missing interpreter is an environment
|
|
42
|
+
* precondition, not a regression.
|
|
43
|
+
*/
|
|
44
|
+
commandNotFound?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* A referenceVector path is declared but the file is absent at run time.
|
|
47
|
+
* CI footgun: the seal outputs were probably never committed.
|
|
48
|
+
*/
|
|
49
|
+
referenceVectorMissing?: boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Parse numeric output from harness stdout.
|
|
53
|
+
* Accepted shapes:
|
|
54
|
+
* - JSON array of numbers (arbitrarily nested) → flattened in order
|
|
55
|
+
* - JSON object of named numeric blocks → keys sorted, excluded keys
|
|
56
|
+
* skipped, values flattened
|
|
57
|
+
* - plain whitespace/comma-separated numbers
|
|
58
|
+
*/
|
|
59
|
+
export declare function parseNumericOutput(stdout: string, exclude?: string[]): number[];
|
|
60
|
+
/** Run a harness and render the dual hash/tolerance verdict. */
|
|
61
|
+
export declare function runHarness(def: HarnessDef): Promise<HarnessResult>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic-output harness runner (ADR-0001 D9, RuView verify.py port).
|
|
3
|
+
*
|
|
4
|
+
* Spawns the harness command with PROOFSEAL_SEED in the environment,
|
|
5
|
+
* parses numeric output from stdout, quantizes (round-half-even, N
|
|
6
|
+
* decimals), packs LE float64, streams SHA-256, and renders a dual
|
|
7
|
+
* verdict: bit-exact hash match OR rtol/atol tolerance vs a committed
|
|
8
|
+
* reference vector (JSON array of numbers).
|
|
9
|
+
*/
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { resolve } from 'node:path';
|
|
13
|
+
import { allClose, hashQuantized, quantizeValues, DEFAULT_DECIMALS, DEFAULT_TOLERANCE, } from './quantize.js';
|
|
14
|
+
/**
|
|
15
|
+
* Parse numeric output from harness stdout.
|
|
16
|
+
* Accepted shapes:
|
|
17
|
+
* - JSON array of numbers (arbitrarily nested) → flattened in order
|
|
18
|
+
* - JSON object of named numeric blocks → keys sorted, excluded keys
|
|
19
|
+
* skipped, values flattened
|
|
20
|
+
* - plain whitespace/comma-separated numbers
|
|
21
|
+
*/
|
|
22
|
+
export function parseNumericOutput(stdout, exclude = []) {
|
|
23
|
+
const text = stdout.trim();
|
|
24
|
+
if (!text)
|
|
25
|
+
return [];
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(text);
|
|
28
|
+
return flattenNumbers(parsed, exclude);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return text
|
|
32
|
+
.split(/[\s,]+/)
|
|
33
|
+
.map(Number)
|
|
34
|
+
.filter((n) => Number.isFinite(n));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function flattenNumbers(value, exclude) {
|
|
38
|
+
if (typeof value === 'number')
|
|
39
|
+
return [value];
|
|
40
|
+
if (Array.isArray(value))
|
|
41
|
+
return value.flatMap((v) => flattenNumbers(v, exclude));
|
|
42
|
+
if (value !== null && typeof value === 'object') {
|
|
43
|
+
const obj = value;
|
|
44
|
+
return Object.keys(obj)
|
|
45
|
+
.sort()
|
|
46
|
+
.filter((k) => !exclude.includes(k))
|
|
47
|
+
.flatMap((k) => flattenNumbers(obj[k], exclude));
|
|
48
|
+
}
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
function execHarness(def, seed) {
|
|
52
|
+
return new Promise((resolvePromise) => {
|
|
53
|
+
const child = spawn(def.cmd, {
|
|
54
|
+
shell: true,
|
|
55
|
+
cwd: def.cwd ?? process.cwd(),
|
|
56
|
+
env: {
|
|
57
|
+
...process.env,
|
|
58
|
+
PROOFSEAL_SEED: String(seed),
|
|
59
|
+
// Legacy alias kept one release for harnesses written pre-rename
|
|
60
|
+
// (and the bench fixtures); PROOFSEAL_SEED is the documented name.
|
|
61
|
+
PROOFKIT_SEED: String(seed),
|
|
62
|
+
},
|
|
63
|
+
timeout: def.timeoutMs ?? 120_000,
|
|
64
|
+
});
|
|
65
|
+
let stdout = '';
|
|
66
|
+
child.stdout.on('data', (d) => {
|
|
67
|
+
stdout += d.toString('utf8');
|
|
68
|
+
});
|
|
69
|
+
child.on('error', (err) => resolvePromise({
|
|
70
|
+
stdout,
|
|
71
|
+
exitCode: null,
|
|
72
|
+
error: err.message,
|
|
73
|
+
commandNotFound: err.code === 'ENOENT',
|
|
74
|
+
}));
|
|
75
|
+
child.on('close', (code) =>
|
|
76
|
+
// shell:true means "command not found" surfaces as exit 127 on POSIX
|
|
77
|
+
// (cmd.exe uses other codes; the ENOENT path above covers direct spawn).
|
|
78
|
+
resolvePromise({ stdout, exitCode: code, commandNotFound: code === 127 }));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/** Run a harness and render the dual hash/tolerance verdict. */
|
|
82
|
+
export async function runHarness(def) {
|
|
83
|
+
const seed = def.seed ?? 42;
|
|
84
|
+
const decimals = def.quantizeDecimals ?? DEFAULT_DECIMALS;
|
|
85
|
+
const base = { name: def.name, seed, quantizeDecimals: decimals };
|
|
86
|
+
const run = await execHarness(def, seed);
|
|
87
|
+
if (run.error || run.exitCode !== 0) {
|
|
88
|
+
return {
|
|
89
|
+
...base,
|
|
90
|
+
status: 'error',
|
|
91
|
+
exitCode: run.exitCode,
|
|
92
|
+
error: run.error ?? `harness exited with code ${run.exitCode}`,
|
|
93
|
+
...(run.commandNotFound ? { commandNotFound: true } : {}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const values = parseNumericOutput(run.stdout, def.exclude ?? []);
|
|
97
|
+
const quantized = quantizeValues(values, decimals);
|
|
98
|
+
const hash = hashQuantized(values, decimals);
|
|
99
|
+
if (!def.expectedSha256) {
|
|
100
|
+
return { ...base, status: 'missing', exitCode: run.exitCode, hash, values, quantized, error: 'no committed expectedSha256 — run `proofseal harness run --update`' };
|
|
101
|
+
}
|
|
102
|
+
const hashMatch = hash === def.expectedSha256;
|
|
103
|
+
if (hashMatch) {
|
|
104
|
+
return { ...base, status: 'pass', exitCode: run.exitCode, hash, expectedSha256: def.expectedSha256, hashMatch, values, quantized };
|
|
105
|
+
}
|
|
106
|
+
// Tolerance fallback against the committed full-precision reference vector.
|
|
107
|
+
let toleranceMatch = false;
|
|
108
|
+
let forensics;
|
|
109
|
+
let referenceVectorMissing = false;
|
|
110
|
+
if (def.referenceVector) {
|
|
111
|
+
const refPath = resolve(def.cwd ?? process.cwd(), def.referenceVector);
|
|
112
|
+
if (existsSync(refPath)) {
|
|
113
|
+
const reference = JSON.parse(readFileSync(refPath, 'utf8'));
|
|
114
|
+
const tol = def.tolerance ?? DEFAULT_TOLERANCE;
|
|
115
|
+
forensics = allClose(values, reference, tol.rtol, tol.atol);
|
|
116
|
+
toleranceMatch = forensics.ok;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Declared but absent — almost always "seal outputs never committed".
|
|
120
|
+
referenceVectorMissing = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
...(referenceVectorMissing ? { referenceVectorMissing: true } : {}),
|
|
125
|
+
...base,
|
|
126
|
+
status: toleranceMatch ? 'drift' : 'regressed',
|
|
127
|
+
exitCode: run.exitCode,
|
|
128
|
+
hash,
|
|
129
|
+
expectedSha256: def.expectedSha256,
|
|
130
|
+
hashMatch,
|
|
131
|
+
toleranceMatch,
|
|
132
|
+
forensics,
|
|
133
|
+
values,
|
|
134
|
+
quantized,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/harness/run.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RegressionIntroduction } from './queries.js';
|
|
2
|
+
export interface RegressionGitInfo {
|
|
3
|
+
/** false = git rejects the SHA (rewritten history?); undefined = not checked. */
|
|
4
|
+
lastPassReachable?: boolean;
|
|
5
|
+
regressedAtReachable?: boolean;
|
|
6
|
+
/** `git rev-list --count lastPass..regressedAt` when both SHAs are reachable. */
|
|
7
|
+
rangeCommitCount?: number;
|
|
8
|
+
}
|
|
9
|
+
export type EnrichedRegression = RegressionIntroduction & RegressionGitInfo;
|
|
10
|
+
/** Tag appended to a SHA that git can no longer resolve. */
|
|
11
|
+
export declare const UNREACHABLE_TAG = "(unreachable \u2014 rewritten history?)";
|
|
12
|
+
/**
|
|
13
|
+
* Annotate regressions with reachability + range width. Fail-open: outside
|
|
14
|
+
* a git repo (or git missing) the regressions pass through untouched.
|
|
15
|
+
*/
|
|
16
|
+
export declare function enrichRegressionsWithGit(root: string, regressions: RegressionIntroduction[]): EnrichedRegression[];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheap git-side validation of bisect results (premortem hazards b/c/d):
|
|
3
|
+
* recorded SHAs can be orphaned by squash-merge, rebase, or force-push, and
|
|
4
|
+
* bisect granularity is seal frequency, not commits. We check reachability
|
|
5
|
+
* with `git cat-file -e <sha>^{commit}` and measure range width with
|
|
6
|
+
* `git rev-list --count`. Everything here is best-effort: git absent or
|
|
7
|
+
* not-a-repo → skip validation silently and return the input unchanged.
|
|
8
|
+
*/
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
/** Tag appended to a SHA that git can no longer resolve. */
|
|
11
|
+
export const UNREACHABLE_TAG = '(unreachable — rewritten history?)';
|
|
12
|
+
const SHA_RE = /^[0-9a-f]{7,40}$/i;
|
|
13
|
+
function git(root, args) {
|
|
14
|
+
return execFileSync('git', args, {
|
|
15
|
+
cwd: root,
|
|
16
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
17
|
+
timeout: 10_000,
|
|
18
|
+
}).toString();
|
|
19
|
+
}
|
|
20
|
+
function insideGitRepo(root) {
|
|
21
|
+
try {
|
|
22
|
+
git(root, ['rev-parse', '--git-dir']);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false; // git absent, or root is not a repo → skip validation
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function commitReachable(root, sha) {
|
|
30
|
+
try {
|
|
31
|
+
git(root, ['cat-file', '-e', `${sha}^{commit}`]);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function revListCount(root, from, to) {
|
|
39
|
+
try {
|
|
40
|
+
const n = Number(git(root, ['rev-list', '--count', `${from}..${to}`]).trim());
|
|
41
|
+
return Number.isFinite(n) ? n : undefined;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Annotate regressions with reachability + range width. Fail-open: outside
|
|
49
|
+
* a git repo (or git missing) the regressions pass through untouched.
|
|
50
|
+
*/
|
|
51
|
+
export function enrichRegressionsWithGit(root, regressions) {
|
|
52
|
+
if (regressions.length === 0 || !insideGitRepo(root)) {
|
|
53
|
+
return regressions.map((r) => ({ ...r }));
|
|
54
|
+
}
|
|
55
|
+
return regressions.map((r) => {
|
|
56
|
+
const info = {};
|
|
57
|
+
if (r.lastPassCommit && SHA_RE.test(r.lastPassCommit)) {
|
|
58
|
+
info.lastPassReachable = commitReachable(root, r.lastPassCommit);
|
|
59
|
+
}
|
|
60
|
+
if (SHA_RE.test(r.regressedAtCommit)) {
|
|
61
|
+
info.regressedAtReachable = commitReachable(root, r.regressedAtCommit);
|
|
62
|
+
}
|
|
63
|
+
if (info.lastPassReachable && info.regressedAtReachable) {
|
|
64
|
+
info.rangeCommitCount = revListCount(root, r.lastPassCommit, r.regressedAtCommit);
|
|
65
|
+
}
|
|
66
|
+
return { ...r, ...info };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=gitinfo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitinfo.js","sourceRoot":"","sources":["../../src/history/gitinfo.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAalD,4DAA4D;AAC5D,MAAM,CAAC,MAAM,eAAe,GAAG,oCAAoC,CAAC;AAEpE,MAAM,MAAM,GAAG,mBAAmB,CAAC;AAEnC,SAAS,GAAG,CAAC,IAAY,EAAE,IAAc;IACvC,OAAO,YAAY,CAAC,KAAK,EAAE,IAAI,EAAE;QAC/B,GAAG,EAAE,IAAI;QACT,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC;QACnC,OAAO,EAAE,MAAM;KAChB,CAAC,CAAC,QAAQ,EAAE,CAAC;AAChB,CAAC;AAED,SAAS,aAAa,CAAC,IAAY;IACjC,IAAI,CAAC;QACH,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC,CAAC,sDAAsD;IACtE,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,IAAY,EAAE,GAAW;IAChD,IAAI,CAAC;QACH,GAAG,CAAC,IAAI,EAAE,CAAC,UAAU,EAAE,IAAI,EAAE,GAAG,GAAG,WAAW,CAAC,CAAC,CAAC;QACjD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,SAAS,YAAY,CAAC,IAAY,EAAE,IAAY,EAAE,EAAU;IAC1D,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,UAAU,EAAE,SAAS,EAAE,GAAG,IAAI,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAC9E,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB,CACtC,IAAY,EACZ,WAAqC;IAErC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;QACrD,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5C,CAAC;IACD,OAAO,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC3B,MAAM,IAAI,GAAsB,EAAE,CAAC;QACnC,IAAI,CAAC,CAAC,cAAc,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,EAAE,CAAC;YACtD,IAAI,CAAC,iBAAiB,GAAG,eAAe,CAAC,IAAI,EAAE,CAAC,CAAC,cAAc,CAAC,CAAC;QACnE,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,iBAAiB,CAAC,EAAE,CAAC;YACrC,IAAI,CAAC,oBAAoB,GAAG,eAAe,CAAC,IAAI,EAAE,CAAC,CAAC,iBAAiB,CAAC,CAAC;QACzE,CAAC;QACD,IAAI,IAAI,CAAC,iBAAiB,IAAI,IAAI,CAAC,oBAAoB,EAAE,CAAC;YACxD,IAAI,CAAC,gBAAgB,GAAG,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC,cAAe,EAAE,CAAC,CAAC,iBAAiB,CAAC,CAAC;QACrF,CAAC;QACD,OAAO,EAAE,GAAG,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC;IAC3B,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Manifest } from '../manifest/schema.js';
|
|
2
|
+
export interface HistoryClaimState {
|
|
3
|
+
sha256: string;
|
|
4
|
+
verified: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface HistoryEntry {
|
|
7
|
+
v: 1;
|
|
8
|
+
commit: string;
|
|
9
|
+
issuedAt: string;
|
|
10
|
+
branch: string;
|
|
11
|
+
manifestHash: string;
|
|
12
|
+
summary: {
|
|
13
|
+
totalClaims: number;
|
|
14
|
+
verified: number;
|
|
15
|
+
missing: number;
|
|
16
|
+
};
|
|
17
|
+
claims: Record<string, HistoryClaimState>;
|
|
18
|
+
}
|
|
19
|
+
/** Was this claim verified at seal time? (per-type semantics) */
|
|
20
|
+
export declare function claimVerified(claim: Manifest['claims'][number]): boolean;
|
|
21
|
+
/** Append a compact snapshot of a sealed manifest. Exactly one '\n' per line. */
|
|
22
|
+
export declare function appendHistory(historyPath: string, manifest: Manifest, manifestHash: string): HistoryEntry;
|
|
23
|
+
/**
|
|
24
|
+
* Load JSONL history in FILE order. File order is not chronology (union
|
|
25
|
+
* merges can interleave branches) — queries sort by issuedAt via
|
|
26
|
+
* sortByIssuedAt(). Tolerates blank lines / no trailing newline.
|
|
27
|
+
*/
|
|
28
|
+
export declare function loadHistory(historyPath: string): HistoryEntry[];
|