harness-everything 0.36.3 → 0.37.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +18 -0
- package/ci/fixtures/doc-obligations/providers-s3/cases.json +535 -0
- package/ci/fixtures/doc-obligations/read-boundary/boundary-controls.json +211 -0
- package/ci/fixtures/doc-obligations/read-boundary/boundary-policy.json +325 -0
- package/ci/fixtures/doc-obligations/read-boundary/cases.json +14058 -0
- package/ci/fixtures/doc-obligations/read-boundary/expected.json +1 -0
- package/ci/fixtures/doc-obligations/read-boundary/identity-cases.json +119 -0
- package/ci/fixtures/doc-obligations/read-boundary/identity-expected.json +1 -0
- package/ci/fixtures/doc-obligations/read-boundary/scope-cases.json +208 -0
- package/ci/fixtures/doc-obligations/read-boundary/scope-expected.json +1 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/cases.json +2751 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/flood-entrypoint.js +5 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/hang-entrypoint.js +5 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/pending-red.json +648 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/provider-contract.json +100 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/trust-contract.json +188 -0
- package/ci/lib/doc-evidence-boundary-check.js +1240 -0
- package/ci/lib/doc-evidence-fixtures.js +459 -0
- package/ci/lib/doc-evidence-trace.js +171 -0
- package/ci/lib/doc-reconciliation-prep-fixtures.js +502 -0
- package/ci/lib/snapshot-stdin.js +27 -0
- package/ci/mechanism-325-3b-preparation.test.js +382 -0
- package/ci/mechanism-325-3b-s3-providers.test.js +378 -0
- package/ci/mechanism-341-boundary-enforcement.test.js +660 -0
- package/ci/mechanism-341-capture-module.test.js +357 -0
- package/ci/mechanism-341-mutation-runner.test.js +413 -0
- package/ci/mechanism-341-read-boundary.test.js +388 -0
- package/ci/mechanism-341-snapshot-stdin.test.js +40 -0
- package/ci/mutation-check.js +805 -45
- package/ci/mutation-loader.js +544 -57
- package/ci/prep/doc-reconciliation-red.js +407 -0
- package/ci/stress-341-boundary-control.js +85 -0
- package/docs/doc-evidence-baseline-binding.md +105 -19
- package/docs/doc-evidence-boundary.md +135 -0
- package/docs/doc-workflow-reconciliation.md +338 -0
- package/docs/document-lifecycle.md +8 -1
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-evidence-capture.js +331 -0
- package/harness-everything/scripts/doc-evidence.js +72 -223
- package/harness-everything/scripts/doc-providers.js +113 -0
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +5 -3
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence-capture.js +331 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +72 -223
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-providers.js +113 -0
- package/sbom.cdx.json +43 -7
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// #325 Phase 3b preparation: strict and expected-RED replay over the real CLI, hook and Stop entry points.
|
|
5
|
+
//
|
|
6
|
+
// node ci/prep/doc-reconciliation-red.js --strict exits 1 on any desired-contract gap or control failure
|
|
7
|
+
// node ci/prep/doc-reconciliation-red.js --expect-red exits 0 only when every gap is qualified by the registry
|
|
8
|
+
// node ci/prep/doc-reconciliation-red.js --recapture-candidate <file> --baseline-sha <40-hex>
|
|
9
|
+
// writes a candidate registry for review; never the registry
|
|
10
|
+
//
|
|
11
|
+
// Optional: --case <id> (repeatable), --cases <file>, --registry <file>, --json (print the report), --log <file>.
|
|
12
|
+
// Exit 2 means infrastructure or registry error (replay crash, malformed registry, unreadable file, a spawn error,
|
|
13
|
+
// a signal, an exit outside the action's expectExit policy, or missing required output).
|
|
14
|
+
//
|
|
15
|
+
// This file is not discovered by filename. The preparation suite invokes --expect-red and parses the report.
|
|
16
|
+
// A qualified gap is an ON assertion that fails today and matches a registry entry exactly (case, assertion,
|
|
17
|
+
// distribution, actual and desired). Infrastructure faults are decided first and never qualify as gaps.
|
|
18
|
+
// Qualification is bound to the runtime fingerprint of the shared runtime files, not to the commit: a registry
|
|
19
|
+
// entry is valid only while those files are byte-identical to the capture.
|
|
20
|
+
|
|
21
|
+
const crypto = require('crypto');
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
const path = require('path');
|
|
24
|
+
const { spawnSync } = require('child_process');
|
|
25
|
+
const fx = require('../lib/doc-reconciliation-prep-fixtures');
|
|
26
|
+
|
|
27
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
28
|
+
const PREP_DIR = path.join(ROOT, 'ci', 'fixtures', 'doc-obligations', 'reconciliation-prep');
|
|
29
|
+
const DEFAULT_CASES = path.join(PREP_DIR, 'cases.json');
|
|
30
|
+
const DEFAULT_REGISTRY = path.join(PREP_DIR, 'pending-red.json');
|
|
31
|
+
const LEGS = ['source', 'packaged'];
|
|
32
|
+
const MODES = ['off', 'on'];
|
|
33
|
+
const BLOCKER = '#341 E0';
|
|
34
|
+
const ENTRY_KEYS = ['caseId', 'assertionId', 'distribution', 'baselineSha', 'actual', 'desired', 'blocker', 'reasonCode'];
|
|
35
|
+
// Shared runtime files whose bytes define what the registry describes. Every P01-P08 replay loads the first
|
|
36
|
+
// three (verified by a preload probe, see h3.md). doc-evidence.js is not loaded by any replay, so it is excluded:
|
|
37
|
+
// #341 0B refactors it, and including it would turn this suite red for an unrelated reason.
|
|
38
|
+
const FINGERPRINT_FILES = [
|
|
39
|
+
'hooks/scripts/lib/workflow-obligations.js',
|
|
40
|
+
'hooks/scripts/workflow-disposition.js',
|
|
41
|
+
'hooks/scripts/workflow-stop-gate.js'
|
|
42
|
+
];
|
|
43
|
+
const CANDIDATE_FILE = 'ci/fixtures/doc-obligations/reconciliation-prep/pending-red.candidate.json';
|
|
44
|
+
const UNREVIEWED_REASON = 'needs-reason';
|
|
45
|
+
|
|
46
|
+
class RegistryError extends Error {}
|
|
47
|
+
|
|
48
|
+
function fingerprint() {
|
|
49
|
+
const hash = crypto.createHash('sha256');
|
|
50
|
+
for (const rel of FINGERPRINT_FILES) {
|
|
51
|
+
hash.update(`${rel}\0`);
|
|
52
|
+
hash.update(fs.readFileSync(path.join(ROOT, rel)));
|
|
53
|
+
hash.update('\0');
|
|
54
|
+
}
|
|
55
|
+
return hash.digest('hex');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Read-only: the commit named in the re-capture command. Falls back to a visible placeholder.
|
|
59
|
+
function currentCommit() {
|
|
60
|
+
const result = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: ROOT, encoding: 'utf8' });
|
|
61
|
+
return result.status === 0 && /^[0-9a-f]{40}$/.test(result.stdout.trim()) ? result.stdout.trim() : '<commit>';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function recaptureCommand(commit) {
|
|
65
|
+
return `node ci/prep/doc-reconciliation-red.js --recapture-candidate ${CANDIDATE_FILE} --baseline-sha ${commit}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseArgs(argv) {
|
|
69
|
+
const args = {
|
|
70
|
+
mode: null, cases: [], casesFile: DEFAULT_CASES, registryFile: DEFAULT_REGISTRY,
|
|
71
|
+
json: false, log: null, candidateFile: null, baselineSha: null
|
|
72
|
+
};
|
|
73
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
74
|
+
const item = argv[i];
|
|
75
|
+
if (item === '--strict' || item === '--expect-red') args.mode = item.slice(2);
|
|
76
|
+
else if (item === '--json') args.json = true;
|
|
77
|
+
else if (item === '--case') args.cases.push(argv[++i]);
|
|
78
|
+
else if (item === '--cases') args.casesFile = argv[++i];
|
|
79
|
+
else if (item === '--registry') args.registryFile = argv[++i];
|
|
80
|
+
else if (item === '--log') args.log = argv[++i];
|
|
81
|
+
else if (item === '--recapture-candidate') { args.mode = 'recapture'; args.candidateFile = argv[++i]; }
|
|
82
|
+
else if (item === '--baseline-sha') args.baselineSha = argv[++i];
|
|
83
|
+
else throw new RegistryError(`unknown argument ${item}`);
|
|
84
|
+
}
|
|
85
|
+
if (!args.mode) throw new RegistryError('choose --strict, --expect-red, or --recapture-candidate <file>');
|
|
86
|
+
if (args.mode === 'recapture') {
|
|
87
|
+
if (!/^[0-9a-f]{40}$/.test(args.baselineSha || '')) throw new RegistryError('--recapture-candidate needs --baseline-sha <40-hex commit>');
|
|
88
|
+
if (!args.candidateFile) throw new RegistryError('--recapture-candidate needs a file path');
|
|
89
|
+
}
|
|
90
|
+
for (const value of [args.casesFile, args.registryFile]) {
|
|
91
|
+
if (value === undefined) throw new RegistryError('a file argument is missing its value');
|
|
92
|
+
}
|
|
93
|
+
return args;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Structural validation of the registry against the corpus. Any violation is a registry error (exit 2).
|
|
97
|
+
function validateRegistry(registry, corpus) {
|
|
98
|
+
if (!registry || typeof registry !== 'object') throw new RegistryError('registry must be a JSON object');
|
|
99
|
+
if (registry.schemaVersion !== 1) throw new RegistryError('registry schemaVersion must be 1');
|
|
100
|
+
if (!/^[0-9a-f]{40}$/.test(registry.baselineSha || '')) throw new RegistryError('registry baselineSha must be a 40-hex commit');
|
|
101
|
+
if (!/^[0-9a-f]{64}$/.test(registry.runtimeFingerprint || '')) throw new RegistryError('registry runtimeFingerprint must be a sha256');
|
|
102
|
+
if (!Array.isArray(registry.entries)) throw new RegistryError('registry entries must be an array');
|
|
103
|
+
const cases = new Map(corpus.cases.map(item => [item.id, item]));
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
for (const [index, entry] of registry.entries.entries()) {
|
|
106
|
+
const where = `entries[${index}]`;
|
|
107
|
+
if (!entry || typeof entry !== 'object') throw new RegistryError(`${where} must be an object`);
|
|
108
|
+
const keys = Object.keys(entry).sort();
|
|
109
|
+
if (JSON.stringify(keys) !== JSON.stringify([...ENTRY_KEYS].sort())) {
|
|
110
|
+
throw new RegistryError(`${where} must have exactly the keys ${ENTRY_KEYS.join(', ')}`);
|
|
111
|
+
}
|
|
112
|
+
const item = cases.get(entry.caseId);
|
|
113
|
+
if (!item) throw new RegistryError(`${where} names unknown case ${entry.caseId}`);
|
|
114
|
+
const onId = String(entry.assertionId || '').startsWith(`${entry.caseId}.on.`)
|
|
115
|
+
? entry.assertionId.slice(`${entry.caseId}.on.`.length)
|
|
116
|
+
: null;
|
|
117
|
+
if (!onId || !item.expectations.on.some(assertion => assertion.id === onId)) {
|
|
118
|
+
throw new RegistryError(`${where} names ${entry.assertionId}, which is not an ON assertion of ${entry.caseId}`);
|
|
119
|
+
}
|
|
120
|
+
if (!LEGS.includes(entry.distribution)) throw new RegistryError(`${where} distribution must be source or packaged`);
|
|
121
|
+
if (entry.baselineSha !== registry.baselineSha) throw new RegistryError(`${where} baselineSha differs from the registry capture`);
|
|
122
|
+
if (entry.blocker !== BLOCKER) throw new RegistryError(`${where} blocker must be ${BLOCKER}`);
|
|
123
|
+
if (typeof entry.reasonCode !== 'string' || !entry.reasonCode.trim()) throw new RegistryError(`${where} reasonCode must be non-empty`);
|
|
124
|
+
if (entry.reasonCode === UNREVIEWED_REASON) throw new RegistryError(`${where} reasonCode is ${UNREVIEWED_REASON}; a reviewer must replace it`);
|
|
125
|
+
if (entry.actual === undefined || entry.desired === undefined) throw new RegistryError(`${where} needs actual and desired values`);
|
|
126
|
+
const identity = `${entry.caseId}|${entry.assertionId}|${entry.distribution}`;
|
|
127
|
+
if (seen.has(identity)) throw new RegistryError(`${where} duplicates ${identity}`);
|
|
128
|
+
seen.add(identity);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const sameJson = (left, right) => JSON.stringify(fx.normalize(left)) === JSON.stringify(fx.normalize(right));
|
|
133
|
+
|
|
134
|
+
function comparable(root) {
|
|
135
|
+
return Object.fromEntries(Object.entries(root.actions).map(([id, action]) => [id, {
|
|
136
|
+
exitCode: action.exitCode,
|
|
137
|
+
signal: action.signal,
|
|
138
|
+
stdout: action.json === null ? action.stdout : null,
|
|
139
|
+
json: fx.normalize(action.json),
|
|
140
|
+
stderr: action.stderr,
|
|
141
|
+
state: fx.normalize(action.state)
|
|
142
|
+
}]).concat([['$final', fx.normalize(root.state)]]));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Classifies one case. `replays` holds the scrubbed roots: off/on for each leg.
|
|
146
|
+
function classifyCase(item, replays, registryEntries) {
|
|
147
|
+
// Infrastructure first: a faulted replay can never qualify as a gap, and parity is not meaningful for it. A replay
|
|
148
|
+
// that fail-fast did not run is absent from `replays`, so only the replays that ran are judged.
|
|
149
|
+
const found = [];
|
|
150
|
+
let actionsRun = 0;
|
|
151
|
+
for (const mode of MODES) {
|
|
152
|
+
for (const leg of LEGS) {
|
|
153
|
+
const root = replays[mode][leg];
|
|
154
|
+
if (!root) continue;
|
|
155
|
+
actionsRun += Object.keys(root.actions).length;
|
|
156
|
+
for (const { actionId, message } of fx.infrastructureFaults(item, root, mode)) {
|
|
157
|
+
found.push({ leg, mode, actionId, message, text: `${leg}.${mode}.${actionId}: ${message}` });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (found.length) {
|
|
162
|
+
const first = found[0];
|
|
163
|
+
return {
|
|
164
|
+
caseId: item.id,
|
|
165
|
+
status: 'INFRASTRUCTURE',
|
|
166
|
+
qualifiedGaps: 0,
|
|
167
|
+
parity: 'not-evaluated',
|
|
168
|
+
reason: first.text,
|
|
169
|
+
offending: { leg: first.leg, mode: first.mode, actionId: first.actionId, fault: first.message },
|
|
170
|
+
infrastructure: found.map(entry => entry.text),
|
|
171
|
+
actionsRun,
|
|
172
|
+
assertions: []
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const assertions = [];
|
|
176
|
+
let failed = false;
|
|
177
|
+
let qualifiedGaps = 0;
|
|
178
|
+
for (const leg of LEGS) {
|
|
179
|
+
for (const mode of ['off', 'on']) {
|
|
180
|
+
const root = replays[mode][leg];
|
|
181
|
+
for (const result of fx.evaluateAll(root, item.expectations[mode])) {
|
|
182
|
+
const assertionId = `${item.id}.${mode}.${result.id}`;
|
|
183
|
+
if (mode === 'off') {
|
|
184
|
+
if (!result.pass) failed = true;
|
|
185
|
+
assertions.push({ leg, mode, id: assertionId, status: result.pass ? 'pass' : 'control-fail', actual: result.actual });
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const entry = registryEntries.find(candidate => candidate.assertionId === assertionId && candidate.distribution === leg);
|
|
189
|
+
if (result.pass) {
|
|
190
|
+
if (entry) { failed = true; assertions.push({ leg, mode, id: assertionId, status: 'unexpected-pass', actual: result.actual }); }
|
|
191
|
+
else assertions.push({ leg, mode, id: assertionId, status: 'pass', actual: result.actual });
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (!entry) {
|
|
195
|
+
failed = true;
|
|
196
|
+
assertions.push({ leg, mode, id: assertionId, status: 'gap-unqualified', actual: result.actual, desired: result.expected });
|
|
197
|
+
} else if (sameJson(entry.actual, result.actual) && sameJson(entry.desired, result.expected)) {
|
|
198
|
+
qualifiedGaps += 1;
|
|
199
|
+
assertions.push({ leg, mode, id: assertionId, status: 'gap-qualified', actual: result.actual, desired: result.expected });
|
|
200
|
+
} else {
|
|
201
|
+
failed = true;
|
|
202
|
+
assertions.push({ leg, mode, id: assertionId, status: 'gap-mismatch', actual: result.actual, desired: result.expected, registered: entry.actual });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Parity: the two legs must agree on every normalized replay of this case.
|
|
208
|
+
let parity = 'match';
|
|
209
|
+
for (const mode of ['off', 'on']) {
|
|
210
|
+
if (!sameJson(comparable(replays[mode].source), comparable(replays[mode].packaged))) {
|
|
211
|
+
parity = 'mismatch';
|
|
212
|
+
failed = true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const status = failed ? 'FAIL' : qualifiedGaps > 0 ? 'EXPECTED_RED' : 'CONTROL_PASS';
|
|
216
|
+
return { caseId: item.id, status, qualifiedGaps, parity, assertions };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Both legs, both modes, for one case. Fresh replays every time; nothing is reused between cases or legs. The first
|
|
220
|
+
// replay with an infrastructure fault ends the case: the replays after it are not run and are absent from the result.
|
|
221
|
+
function replayAll(item) {
|
|
222
|
+
const replays = { off: {}, on: {} };
|
|
223
|
+
for (const mode of MODES) {
|
|
224
|
+
for (const leg of LEGS) {
|
|
225
|
+
replays[mode][leg] = fx.scrubReplay(fx.replayCase(item, leg, mode === 'on' ? { pilot: '1' } : undefined));
|
|
226
|
+
if (fx.infrastructureFaults(item, replays[mode][leg], mode).length) return replays;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return replays;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function countReplays(replays) {
|
|
233
|
+
return MODES.reduce((total, mode) => total + Object.keys(replays[mode]).length, 0);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// A case the corpus-level fail-fast did not reach. It is named, so the report never looks complete.
|
|
237
|
+
function notRun(item, infrastructureCaseId) {
|
|
238
|
+
return {
|
|
239
|
+
caseId: item.id,
|
|
240
|
+
status: 'NOT_RUN',
|
|
241
|
+
qualifiedGaps: 0,
|
|
242
|
+
parity: 'not-evaluated',
|
|
243
|
+
reason: `not run: ${infrastructureCaseId} is INFRASTRUCTURE`,
|
|
244
|
+
assertions: []
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// The one exit-code rule. Infrastructure (replay faults, policy and output violations) is 2, ahead of RED results.
|
|
249
|
+
function exitCodeFor(report, mode) {
|
|
250
|
+
if (report.cases.some(item => item.status === 'INFRASTRUCTURE')) return 2;
|
|
251
|
+
const failed = report.cases.some(item => item.status === 'FAIL');
|
|
252
|
+
const gaps = report.cases.some(item => item.assertions.some(entry => entry.status.startsWith('gap')));
|
|
253
|
+
if (mode === 'strict') return failed || gaps ? 1 : 0;
|
|
254
|
+
return failed ? 1 : 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function loadRegistry(file) {
|
|
258
|
+
let registry;
|
|
259
|
+
try {
|
|
260
|
+
registry = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
261
|
+
} catch (error) {
|
|
262
|
+
throw new RegistryError(`registry unreadable: ${error.message.split('\n')[0]}`);
|
|
263
|
+
}
|
|
264
|
+
return registry;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function run(args) {
|
|
268
|
+
const corpus = fx.readCaseCorpus(args.casesFile);
|
|
269
|
+
const registry = loadRegistry(args.registryFile);
|
|
270
|
+
validateRegistry(registry, corpus);
|
|
271
|
+
const runtime = fingerprint();
|
|
272
|
+
const cases = corpus.cases.filter(item => args.cases.length === 0 || args.cases.includes(item.id));
|
|
273
|
+
if (args.cases.length && cases.length !== args.cases.length) throw new RegistryError('a --case names no corpus case');
|
|
274
|
+
const report = {
|
|
275
|
+
schemaVersion: 1,
|
|
276
|
+
mode: args.mode,
|
|
277
|
+
baselineSha: registry.baselineSha,
|
|
278
|
+
runtimeFingerprintMatches: registry.runtimeFingerprint === runtime,
|
|
279
|
+
pilotCredit: false,
|
|
280
|
+
replaysRun: 0,
|
|
281
|
+
cases: []
|
|
282
|
+
};
|
|
283
|
+
for (const [index, item] of cases.entries()) {
|
|
284
|
+
const replays = replayAll(item);
|
|
285
|
+
report.replaysRun += countReplays(replays);
|
|
286
|
+
const result = classifyCase(item, replays, registry.entries.filter(entry => entry.caseId === item.id));
|
|
287
|
+
if (registry.runtimeFingerprint !== runtime && result.status !== 'INFRASTRUCTURE') {
|
|
288
|
+
result.status = 'FAIL';
|
|
289
|
+
result.reason = 'runtime fingerprint differs from the registry capture';
|
|
290
|
+
}
|
|
291
|
+
report.cases.push(result);
|
|
292
|
+
if (result.status === 'INFRASTRUCTURE') {
|
|
293
|
+
// Fail fast across the corpus: once a case is infrastructure, the remaining cases are not replayed.
|
|
294
|
+
for (const skipped of cases.slice(index + 1)) report.cases.push(notRun(skipped, item.id));
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (!report.runtimeFingerprintMatches) report.recaptureCommand = recaptureCommand(currentCommit());
|
|
299
|
+
return report;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Candidate registry from the observed gaps on the current runtime. The committed registry is never written.
|
|
303
|
+
// Reason codes carry over by identity; a gap that is new to the registry gets the unreviewed marker.
|
|
304
|
+
function recapture(args) {
|
|
305
|
+
const corpus = fx.readCaseCorpus(args.casesFile);
|
|
306
|
+
const existing = fs.existsSync(args.registryFile) ? loadRegistry(args.registryFile).entries || [] : [];
|
|
307
|
+
const known = new Map(existing.map(entry => [`${entry.assertionId}|${entry.distribution}`, entry.reasonCode]));
|
|
308
|
+
const entries = [];
|
|
309
|
+
const problems = [];
|
|
310
|
+
const cases = corpus.cases.filter(item => args.cases.length === 0 || args.cases.includes(item.id));
|
|
311
|
+
for (const item of cases) {
|
|
312
|
+
const result = classifyCase(item, replayAll(item), []);
|
|
313
|
+
if (result.status === 'INFRASTRUCTURE') {
|
|
314
|
+
// Refuse, and stop: no candidate is written once a replay is infrastructure, so later cases are not replayed.
|
|
315
|
+
problems.push(`${item.id} infrastructure: ${result.reason}`);
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
for (const assertion of result.assertions) {
|
|
319
|
+
if (assertion.status === 'control-fail') problems.push(`${item.id} control fails: ${assertion.id}`);
|
|
320
|
+
if (assertion.status === 'gap-unqualified') {
|
|
321
|
+
entries.push({
|
|
322
|
+
caseId: item.id,
|
|
323
|
+
assertionId: assertion.id,
|
|
324
|
+
distribution: assertion.leg,
|
|
325
|
+
baselineSha: args.baselineSha,
|
|
326
|
+
actual: assertion.actual,
|
|
327
|
+
desired: assertion.desired,
|
|
328
|
+
blocker: BLOCKER,
|
|
329
|
+
reasonCode: known.get(`${assertion.id}|${assertion.leg}`) || UNREVIEWED_REASON
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (result.parity === 'mismatch') problems.push(`${item.id} source and packaged legs disagree`);
|
|
334
|
+
}
|
|
335
|
+
if (problems.length) return { ok: false, problems };
|
|
336
|
+
return {
|
|
337
|
+
ok: true,
|
|
338
|
+
candidate: { schemaVersion: 1, phase: '325-3b-H2', baselineSha: args.baselineSha, runtimeFingerprint: fingerprint(), entries }
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function summarize(report) {
|
|
343
|
+
const lines = report.cases.map(item => {
|
|
344
|
+
const gaps = item.assertions.filter(entry => entry.status.startsWith('gap')).length;
|
|
345
|
+
const reason = item.reason ? ` (${item.reason})` : '';
|
|
346
|
+
return `${item.caseId.padEnd(30)} ${item.status.padEnd(13)} gaps=${gaps} parity=${item.parity}${reason}`;
|
|
347
|
+
});
|
|
348
|
+
return lines.join('\n');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function main() {
|
|
352
|
+
let args;
|
|
353
|
+
try {
|
|
354
|
+
args = parseArgs(process.argv.slice(2));
|
|
355
|
+
} catch (error) {
|
|
356
|
+
console.error(`[doc-reconciliation-red] ${error.message}`);
|
|
357
|
+
process.exitCode = 2;
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (args.mode === 'recapture') {
|
|
361
|
+
let outcome;
|
|
362
|
+
try {
|
|
363
|
+
outcome = recapture(args);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.error(`[doc-reconciliation-red] infrastructure: ${error.message.split('\n')[0]}`);
|
|
366
|
+
process.exitCode = 2;
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (!outcome.ok) {
|
|
370
|
+
console.error(`[doc-reconciliation-red] candidate not written:\n ${outcome.problems.join('\n ')}`);
|
|
371
|
+
process.exitCode = 1;
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const target = path.resolve(ROOT, args.candidateFile);
|
|
375
|
+
fs.writeFileSync(target, `${JSON.stringify(outcome.candidate, null, 2)}\n`, 'utf8');
|
|
376
|
+
const unreviewed = outcome.candidate.entries.filter(entry => entry.reasonCode === UNREVIEWED_REASON).length;
|
|
377
|
+
console.log(`candidate written: ${args.candidateFile} (${outcome.candidate.entries.length} entries, ${unreviewed} unreviewed)`);
|
|
378
|
+
console.log('review it against pending-red.json; replace the committed registry only after review, then delete the candidate.');
|
|
379
|
+
process.exitCode = 0;
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
let report;
|
|
383
|
+
try {
|
|
384
|
+
report = run(args);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
console.error(`[doc-reconciliation-red] infrastructure: ${error.message.split('\n')[0]}`);
|
|
387
|
+
process.exitCode = 2;
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (args.log) fs.writeFileSync(args.log, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
391
|
+
if (args.json) console.log(JSON.stringify(report, null, 2));
|
|
392
|
+
else console.log(summarize(report));
|
|
393
|
+
if (report.recaptureCommand) {
|
|
394
|
+
console.error(`[doc-reconciliation-red] runtime fingerprint changed since the registry capture. Re-capture deliberately; nothing is auto-accepted:\n` +
|
|
395
|
+
` 1. ${report.recaptureCommand}\n` +
|
|
396
|
+
` 2. review ${CANDIDATE_FILE} against ci/fixtures/doc-obligations/reconciliation-prep/pending-red.json (entries, reasonCode, actual, desired)\n` +
|
|
397
|
+
' 3. replace pending-red.json with the reviewed candidate and delete the candidate file');
|
|
398
|
+
}
|
|
399
|
+
process.exitCode = exitCodeFor(report, args.mode);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (require.main === module) main();
|
|
403
|
+
|
|
404
|
+
module.exports = {
|
|
405
|
+
fingerprint, validateRegistry, classifyCase, replayAll, run, recapture, exitCodeFor, parseArgs, recaptureCommand,
|
|
406
|
+
RegistryError, FINGERPRINT_FILES, CANDIDATE_FILE
|
|
407
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Local/on-demand stress lane: four real C00 controls plus four bounded CPU workers.
|
|
5
|
+
// Kept on-demand rather than in every gate: CPU saturation multiplies CI load.
|
|
6
|
+
// The preload makes an unrelated sensitivity child unavailable deterministically;
|
|
7
|
+
// selecting N01d must not run that child, even when the selected assertion passed.
|
|
8
|
+
const assert = require('node:assert/strict');
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const os = require('node:os');
|
|
11
|
+
const path = require('node:path');
|
|
12
|
+
const { spawn } = require('node:child_process');
|
|
13
|
+
|
|
14
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
15
|
+
|
|
16
|
+
if (process.argv.includes('--worker')) {
|
|
17
|
+
const { runChecks } = require('./mutation-check');
|
|
18
|
+
const [result] = runChecks({ root: ROOT, timeoutMs: 60000, legacyAssertions: {}, checks: [{
|
|
19
|
+
label: 'parallel selected boundary control', target: 'ci/lib/doc-evidence-boundary-check.js',
|
|
20
|
+
mutantId: 'C00-noop-boundary', test: 'ci/mechanism-341-boundary-enforcement.test.js',
|
|
21
|
+
caseId: 'C1-N01d-canonical',
|
|
22
|
+
assertionId: 'C1-N01d-canonical.boundary.intended-code-CONTEXT_ALIAS_OR_COMPUTED_USE',
|
|
23
|
+
}] });
|
|
24
|
+
console.log(JSON.stringify(result));
|
|
25
|
+
assert.equal(result.verdict, 'SURVIVED', result.reason);
|
|
26
|
+
} else {
|
|
27
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-341-control-stress-'));
|
|
28
|
+
const preload = path.join(temp, 'sensitivity-pressure.cjs');
|
|
29
|
+
fs.writeFileSync(preload, `
|
|
30
|
+
const cp = require('node:child_process');
|
|
31
|
+
const original = cp.spawnSync;
|
|
32
|
+
cp.spawnSync = function (exe, args, options) {
|
|
33
|
+
if (args && args.includes('--c1-isolation-child') && typeof options?.input === 'string') {
|
|
34
|
+
console.error('stress: pipe-fed isolation child reached');
|
|
35
|
+
return original(exe, ['-e', 'setTimeout(() => {}, 10000)'], { ...options, timeout: 25 });
|
|
36
|
+
}
|
|
37
|
+
if (args && /doc-evidence-boundary-check-baseline\\.js$/.test(args[0] || '')) {
|
|
38
|
+
// Simulate a starved, UNSELECTED child at the actual subprocess boundary.
|
|
39
|
+
console.error('stress: unselected sensitivity baseline reached');
|
|
40
|
+
return original(exe, ['-e', 'setTimeout(() => {}, 10000)'], { ...options, timeout: 25 });
|
|
41
|
+
}
|
|
42
|
+
return original(exe, args, options);
|
|
43
|
+
};
|
|
44
|
+
`);
|
|
45
|
+
const children = new Set();
|
|
46
|
+
const timer = setTimeout(() => {
|
|
47
|
+
for (const child of children) child.kill('SIGKILL');
|
|
48
|
+
}, 150000);
|
|
49
|
+
const env = { ...process.env, NODE_OPTIONS: `${process.env.NODE_OPTIONS || ''} --require=${JSON.stringify(preload)}`.trim() };
|
|
50
|
+
const run = args => new Promise((resolve, reject) => {
|
|
51
|
+
const child = spawn(process.execPath, args, { cwd: ROOT, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
52
|
+
children.add(child);
|
|
53
|
+
let output = '';
|
|
54
|
+
child.stdout.on('data', chunk => { output += chunk; });
|
|
55
|
+
child.stderr.on('data', chunk => { output += chunk; });
|
|
56
|
+
child.on('error', reject);
|
|
57
|
+
child.on('close', (status, signal) => {
|
|
58
|
+
children.delete(child);
|
|
59
|
+
process.stdout.write(output);
|
|
60
|
+
resolve({ status, signal });
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
const burners = [];
|
|
64
|
+
for (let i = 0; i < 4; i++) {
|
|
65
|
+
const child = spawn(process.execPath, ['-e', 'const end = Date.now() + 140000; while (Date.now() < end) {}'], {
|
|
66
|
+
stdio: 'ignore',
|
|
67
|
+
});
|
|
68
|
+
burners.push(child);
|
|
69
|
+
children.add(child);
|
|
70
|
+
child.on('error', () => {});
|
|
71
|
+
child.on('close', () => children.delete(child));
|
|
72
|
+
}
|
|
73
|
+
Promise.all(Array.from({ length: 4 }, () => run([__filename, '--worker'])))
|
|
74
|
+
.then(results => {
|
|
75
|
+
assert.ok(results.every(result => result.status === 0 && !result.signal),
|
|
76
|
+
`parallel C00 controls must survive: ${JSON.stringify(results)}`);
|
|
77
|
+
console.log('PASS four parallel C00 controls under CPU load and input/selection isolation');
|
|
78
|
+
})
|
|
79
|
+
.catch(error => { console.error(error.message); process.exitCode = 1; })
|
|
80
|
+
.finally(() => {
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
for (const child of children) child.kill('SIGKILL');
|
|
83
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -5,6 +5,90 @@ the phase 3a `doc-change` / `requirement-sync` producer in
|
|
|
5
5
|
[`document-lifecycle.md`](document-lifecycle.md). It does not reconcile evidence with workflow
|
|
6
6
|
disposition (#325 phase 3b/3c), add a lock, or add a router/provider path.
|
|
7
7
|
|
|
8
|
+
## Phase 0 read-boundary contract (#341)
|
|
9
|
+
|
|
10
|
+
Phase 0 preserves the public `checkDocObligations()` result and the current reads. A read role
|
|
11
|
+
captures facts at most once **per its existing role-specific identity and scope**; consumers of
|
|
12
|
+
that same role (validation, binding, stale diagnostics, and audit where applicable) reuse the
|
|
13
|
+
capture. Consumers are not roles, so separating consumers does not permit another caller-object
|
|
14
|
+
read. Detached immutable internal views are a Phase 0B implementation requirement; they must not
|
|
15
|
+
be obtained by freezing or retaining mutable caller-owned objects. An internally consistent
|
|
16
|
+
snapshot proves agreement with the supplied values, not their external truth or provenance.
|
|
17
|
+
|
|
18
|
+
The private `ReadResult<T>` contract for the later capture layer distinguishes:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
type ReadResult<T> =
|
|
22
|
+
| { kind: 'present'; value: T }
|
|
23
|
+
| { kind: 'absent' }
|
|
24
|
+
| { kind: 'inherited' }
|
|
25
|
+
| { kind: 'unknown'; reason: string };
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`present` includes an own property whose value is `undefined`; field validation decides whether
|
|
29
|
+
that value is valid, and it must not be collapsed into `absent`. `absent` means no own key exists.
|
|
30
|
+
`inherited` records that a value is reachable only through the prototype chain and cannot satisfy
|
|
31
|
+
an own-key evidence requirement. `unknown` covers throwing or otherwise uninspectable input. An
|
|
32
|
+
unknown read is not a completed obligation and cannot produce a pass; mandatory baseline evidence
|
|
33
|
+
that is invalid or unknown fails closed with a structured diagnostic. This representation does
|
|
34
|
+
not change existing field policy or the public output schema.
|
|
35
|
+
|
|
36
|
+
The read roles and compatibility traces are fixed to their existing identities:
|
|
37
|
+
|
|
38
|
+
| ID | Identity and read rule | Consumers / compatibility behavior |
|
|
39
|
+
| --- | --- | --- |
|
|
40
|
+
| R1 — whole-baseline digest | Invocation. Read once before the obligation loop only if `baseline` passes the plain-object check; otherwise zero reads. | Reused across obligations. This does not claim that the caller's baseline is externally attested. |
|
|
41
|
+
| R2 — candidate-inventory classification | Invocation, lazy. At most one prototype inspection, only when an applicable baseline-declaring obligation needs it. | The same classification is shared across obligations as today. Inapplicable and non-baseline lanes do not trigger it. |
|
|
42
|
+
| R3 — rename-destination validation | Obligation × declaring entry. One own-digest read for each declaring entry that reaches it; accepted entries must agree. | A rejected destination is poisoned: never reread, later declarations fail closed, and the failed snapshot takes precedence in audit over an earlier success. Distinct declaring entries remain separate observations. |
|
|
43
|
+
| R4 — baseline-entry metadata and source own-key absence | Obligation × entry. Capture `path`, `reason`, own `renameTo` presence/value, and source absence once for reuse. | The tested candidate descriptor budget combines R3 and R4: two inspections for a rename (destination presence and source absence), one for a deletion. |
|
|
44
|
+
| R5 — per-path baseline digest | Obligation × path. Capture once during metadata validation. | Audit mirror, stale diagnostics, and baseline binding reuse that validated value. |
|
|
45
|
+
| R6 — ordinary candidate capture | Obligation × path, applicable baseline-annotated lane. | Preserve today's `artifact()` access trace (up to three digest-property reads), seed from a successful R3 value, and reuse the capture for binding, audit, and stale diagnostics. |
|
|
46
|
+
| C1 — legacy audit lane | Obligation × path for N/A, unknown, or non-baseline cases. | Preserve today's `artifact()` reads; this is a compatibility trace, not a once-only guarantee. N/A and unknown do not consume R3. |
|
|
47
|
+
| C2 — evidence-record consumption | Obligation × record identity × field. | Preserve today's decision and provenance field read counts, including `paths`, `baseDigest`, `artifactDigests`, and `baselineArtifacts`; a single record must still bind both rename endpoints. |
|
|
48
|
+
| C3 — invocation-level ingestion | Invocation × record / obligation, before evaluation, including when there are no obligations. | Preserve `isRecord()` reads of `id` / `obligationId` (up to two each in the current guard/short-circuit order, twice each for an admitted stable record), `knownIds` reads of obligation `id` / `capability`, and the waiver scan reads of review-record `kind` / `obligationId`. |
|
|
49
|
+
| C4 — stale-diagnostic record paths | Obligation × record path. | Preserve iteration over `record.paths`; a path without a scoped capture falls back to the legacy `artifact()` read, including for applicable baseline-bound obligations. |
|
|
50
|
+
| C5 — requirement-sync PRD-set enumeration | Conditional obligation check. | Preserve own-key enumeration and `artifact()` reads for each `docs/prd/` path, including out-of-scope PRDs, only when envelope verification reaches that check. |
|
|
51
|
+
|
|
52
|
+
**Closure rule.** Any caller-object access not listed in R1–R6 remains a compatibility trace by
|
|
53
|
+
default: preserve its existing timing, count, and decision effect until a separately approved
|
|
54
|
+
change says otherwise. This does not permit a new access. A caller-object access absent from the
|
|
55
|
+
current implementation has a budget of zero in Phase 0. Phase 0A records ordered `get`, `ownKeys`,
|
|
56
|
+
`getOwnPropertyDescriptor`, and `getPrototypeOf` events across every factory-known caller object
|
|
57
|
+
and pins the complete trace per observed case and distribution, including symbol properties,
|
|
58
|
+
result category/identity, and thrown markers. Tracing must be transparent: the traced and unwrapped
|
|
59
|
+
controls must agree on outputs, exceptions, identity relationships, semantic read counts, and
|
|
60
|
+
caller-input integrity. The tracer must respect Proxy invariants for frozen objects and nested
|
|
61
|
+
non-configurable properties. A trace proves completeness for its observed case; it does not
|
|
62
|
+
establish branch coverage or replace Phase 0C's structural/API boundary and negative controls. Any
|
|
63
|
+
observed unclassified access is added to the contract before Phase 0B. The applicability gate
|
|
64
|
+
protects R2, R3, and scoped candidate capture; it does not gate every baseline, metadata, or
|
|
65
|
+
pre-evaluation ingestion read (R1 and C3 retain their stated timing). C2, C4, C5, and C3's
|
|
66
|
+
pre-evaluation ingestion are explicit compatibility lanes; their bounded reads must remain
|
|
67
|
+
isolated or be documented as narrow exceptions to the Phase 0C boundary.
|
|
68
|
+
|
|
69
|
+
### Phase limits
|
|
70
|
+
|
|
71
|
+
Phase 0A defines this contract and builds the RED replay corpus, exact read traces, and qualified
|
|
72
|
+
mutation controls. Each corpus case compares the full public result (`observation`, `outcome`,
|
|
73
|
+
diagnostics, `artifactDigest`, `baseDigest`, `baselineArtifacts`, provenance, and `recordRefs`),
|
|
74
|
+
role-specific counts/traces, and input integrity against canonical and packaged implementations.
|
|
75
|
+
Phase 0A does not change evaluator behavior. Phase 0B introduces the smallest private
|
|
76
|
+
capture layer as a pure refactor, preserving outputs, decision semantics, audit identity, read
|
|
77
|
+
order/counts, input integrity, and canonical/package parity. Phase 0C enforces the normalized
|
|
78
|
+
applicable baseline-bound boundary and qualifies mutation gates, including negative controls.
|
|
79
|
+
Those phases and relevant CI on the final merged SHA are required for Gate E0. Phase 0A alone is
|
|
80
|
+
not E0 and does not authorize runtime Evidence Reconciliation. Snapshot consistency is not external
|
|
81
|
+
VCS/provenance attestation; that remains in #325 Phase 3b/3c.
|
|
82
|
+
|
|
83
|
+
The C1 isolation trial rejected 9 of 11 weakened-boundary controls per distribution, while N05 and
|
|
84
|
+
N06 survived with complete coverage and no diagnostics. The C2 AST/source supplement now rejects
|
|
85
|
+
all 11 frozen controls in both distributions, including N05/N06 under `COMP-C2-BOUND`; its pinned
|
|
86
|
+
whole-module and named-target fingerprints fail closed on unreviewed structural changes. This is
|
|
87
|
+
deterministic mechanism evidence, not E0 qualification. C3 mutation/compatibility qualification and
|
|
88
|
+
the final merged-SHA gates remain required. See
|
|
89
|
+
[`doc-evidence-boundary.md`](doc-evidence-boundary.md) for the C1/C2 policy, trial results, and
|
|
90
|
+
evidence boundary.
|
|
91
|
+
|
|
8
92
|
## Data shape
|
|
9
93
|
|
|
10
94
|
The trusted baseline keeps its existing whole-tree identity and may add per-path SHA256 values:
|
|
@@ -39,9 +123,9 @@ scope = {
|
|
|
39
123
|
- `reason: 'rename-from'` describes the old endpoint of an identified content-identical move.
|
|
40
124
|
The destination is named by `renameTo` and must itself be a member of the same
|
|
41
125
|
`scope.paths`. A `renameTo` outside the scope is malformed and fails the obligation closed.
|
|
42
|
-
-
|
|
43
|
-
snapshotted through one guarded inspection
|
|
44
|
-
entry whose field getter or descriptor trap throws is a metadata error (`baseline-path-invalid`)
|
|
126
|
+
- Under R4, each `baselinePaths` entry's fields (`path`, `reason`, and the own-key `renameTo`
|
|
127
|
+
presence) are snapshotted through one guarded inspection per obligation/entry, then reused by
|
|
128
|
+
consumers. An entry whose field getter or descriptor trap throws is a metadata error (`baseline-path-invalid`)
|
|
45
129
|
and fails the entry closed instead of escaping the checker; a field accessor that throws only on
|
|
46
130
|
a later read cannot abort the audit mirror; the mirror is built from the metadata-validated
|
|
47
131
|
snapshot (validated path and captured digest) and is never a second read of the raw entry
|
|
@@ -74,7 +158,8 @@ A deleted path is satisfied only when one observed record:
|
|
|
74
158
|
4. carries **no** `artifactDigests` entry for that path, and the candidate artifact map has **no
|
|
75
159
|
own-key entry** for that path.
|
|
76
160
|
|
|
77
|
-
The trusted per-path value is validated once
|
|
161
|
+
The trusted per-path value is validated and captured once for its R5 obligation/path identity
|
|
162
|
+
during metadata validation.
|
|
78
163
|
The audit mirror, the stale-baseline diagnostics, and baseline binding compare against that
|
|
79
164
|
captured value and never re-read the caller-owned `baseline.artifacts` map during evidence
|
|
80
165
|
evaluation: a stateful own getter cannot satisfy a record with a value it started returning only
|
|
@@ -84,14 +169,14 @@ different or unusable value later is factored out entirely. A baseline per-path
|
|
|
84
169
|
throws during validation is a `baseline-artifact-invalid` metadata error and fails the path
|
|
85
170
|
closed instead of escaping the checker. The audit mirror is likewise keyed by the **validated**
|
|
86
171
|
path snapshot rather than a re-read of `baselinePaths[].path`, so a stateful `path` accessor is
|
|
87
|
-
read
|
|
88
|
-
bound the evidence.
|
|
172
|
+
read once within its R4 obligation/entry snapshot and a passing result always describes the
|
|
173
|
+
baseline identity that validated and bound the evidence.
|
|
89
174
|
|
|
90
|
-
The whole-baseline identity `baseline.digest` follows
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
diagnostics, and
|
|
175
|
+
The whole-baseline identity `baseline.digest` follows R1: the invocation-level read, when eligible,
|
|
176
|
+
is the scalar validated and reused by every obligation. A rename destination's own digest follows
|
|
177
|
+
R3: each declaring entry that reaches validation has its own read, with shared-destination
|
|
178
|
+
agreement and poisoned-snapshot precedence. Consumers of each accepted snapshot reuse its captured
|
|
179
|
+
value for metadata, binding, stale diagnostics, and audit identity.
|
|
95
180
|
|
|
96
181
|
Per-path evidence must be carried explicitly. Inherited properties in `baseline.artifacts` or a
|
|
97
182
|
record's `baselineArtifacts` are not own-key evidence and never satisfy a path; the trusted
|
|
@@ -116,11 +201,11 @@ candidate digest — the same one used by ordinary candidate binding, the audit
|
|
|
116
201
|
stale diagnostics. The destination is therefore never read again through the candidate map during
|
|
117
202
|
capture or binding: a nested `digest` that throws or changes after validation cannot make a stale
|
|
118
203
|
destination pair bind while the audit/stale verdicts still describe the captured value. The same
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
its only digest read, and the failed path contributes `path=missing` to
|
|
123
|
-
of a later, different value. The same
|
|
204
|
+
R3 also governs rejected destination snapshots: when a declaring entry's own-digest read throws or
|
|
205
|
+
returns an unusable value, that failed snapshot ('') is recorded and later declarations in the
|
|
206
|
+
same obligation's metadata loop do not reread the poisoned destination — the rejected entry's
|
|
207
|
+
metadata read remains its only digest read, and the failed path contributes `path=missing` to
|
|
208
|
+
`artifactDigest` instead of a later, different value. The same
|
|
124
209
|
record can cover both endpoints. Destination-only evidence, source-only tombstones, split endpoint
|
|
125
210
|
records, and stale destination digests do not satisfy the source. A deletion tombstone therefore
|
|
126
211
|
cannot stand in for a known rename. This destination validation and the snapshot it produces are
|
|
@@ -129,9 +214,10 @@ evidence, so they do not consume the destination digest getter during metadata v
|
|
|
129
214
|
audit identity keeps the un-gated legacy candidate read, and a stateful accessor therefore reports
|
|
130
215
|
the same first legacy value to them that it would report without baseline binding.
|
|
131
216
|
|
|
132
|
-
|
|
133
|
-
baseline path has been confirmed to have **no**
|
|
134
|
-
carried on the entry; binding and the
|
|
217
|
+
For an applicable baseline-only path, R4 metadata validation captures candidate own-key absence
|
|
218
|
+
once for that obligation/entry. Once a declared baseline path has been confirmed to have **no**
|
|
219
|
+
own-key candidate entry, that validated absence is carried on the entry; binding and the
|
|
220
|
+
`artifactDigest` audit identity reuse it instead of
|
|
135
221
|
re-inspecting the candidate map, so a `Proxy` that answers own-property or `get` inspection
|
|
136
222
|
consistently during validation cannot later abort the check or change the audit key. A
|
|
137
223
|
baseline-only path therefore contributes `path=missing` to `artifactDigest` from the validated
|