harness-everything 0.35.0 → 0.36.1
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/.github/workflows/ci.yml +2 -0
- package/AGENTS.md +3 -0
- package/CHANGELOG.md +44 -0
- package/ci/mechanism-335-baseline-evidence.test.js +2690 -0
- package/ci/mechanism-36-verify-timeout.test.js +534 -0
- package/ci/mechanism-37-portability-lint.test.js +71 -0
- package/ci/mechanism-38-verify-all.test.js +112 -0
- package/ci/mechanism-39-opencode-verify-outcomes.test.js +374 -0
- package/ci/portability-lint.js +288 -0
- package/docs/doc-evidence-baseline-binding.md +249 -0
- package/docs/document-lifecycle.md +2 -2
- package/docs/repository-contract.md +1 -1
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-evidence.js +452 -19
- package/harness-everything/scripts/doc-obligations.js +32 -5
- package/opencode-plugin/README.md +33 -2
- package/opencode-plugin/hooks/verify.js +87 -11
- package/opencode-plugin/index.mjs +216 -25
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +3 -1
- 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.js +452 -19
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-obligations.js +32 -5
- package/sbom.cdx.json +7 -7
- package/scripts/verify-all.js +314 -0
|
@@ -0,0 +1,2690 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Issue #335: baseline-bound evidence for deleted files and the source endpoint of a rename.
|
|
4
|
+
//
|
|
5
|
+
// These cases use the real deriveDocFacts -> deriveDocObligations -> checkDocObligations
|
|
6
|
+
// round trip on the existing deleted-adr and rename fixtures. Obligations are never
|
|
7
|
+
// hand-built, so the test exercises derivation and evidence together.
|
|
8
|
+
|
|
9
|
+
const assert = require('assert');
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const helper = require('./test-helper');
|
|
14
|
+
|
|
15
|
+
const sourceRoot = path.join(__dirname, '..', 'harness-everything', 'scripts');
|
|
16
|
+
const packageRoot = path.join(__dirname, '..', 'plugins', 'harness-everything', 'skills', 'harness-everything', 'scripts');
|
|
17
|
+
const fixtureRoot = path.join(__dirname, 'fixtures', 'doc-obligations', 'derive');
|
|
18
|
+
|
|
19
|
+
function loadModules(root) {
|
|
20
|
+
return {
|
|
21
|
+
deriveDocFacts: require(path.join(root, 'doc-obligations.js')).deriveDocFacts,
|
|
22
|
+
deriveDocObligations: require(path.join(root, 'doc-obligations.js')).deriveDocObligations,
|
|
23
|
+
checkDocObligations: require(path.join(root, 'doc-evidence.js')).checkDocObligations
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let modules;
|
|
28
|
+
let packageModules;
|
|
29
|
+
try {
|
|
30
|
+
modules = loadModules(sourceRoot);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
helper.check('335. canonical modules are available', false, error.message);
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
packageModules = loadModules(packageRoot);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
helper.check('335. packaged modules are available', false, error.message);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log('\n[335] baseline-bound document evidence...');
|
|
41
|
+
|
|
42
|
+
function sha256(text) {
|
|
43
|
+
return crypto.createHash('sha256').update(text).digest('hex');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readTree(root) {
|
|
47
|
+
const files = {};
|
|
48
|
+
if (!fs.existsSync(root)) return files;
|
|
49
|
+
const walk = (dir) => {
|
|
50
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
51
|
+
const full = path.join(dir, entry.name);
|
|
52
|
+
if (entry.isDirectory()) walk(full);
|
|
53
|
+
else {
|
|
54
|
+
const rel = path.relative(root, full).split(path.sep).join('/');
|
|
55
|
+
files[rel.endsWith('.fixture') ? rel.slice(0, -'.fixture'.length) : rel] = fs.readFileSync(full, 'utf8');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
walk(root);
|
|
60
|
+
return files;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function loadCase(name) {
|
|
64
|
+
const caseDir = path.join(fixtureRoot, name);
|
|
65
|
+
return {
|
|
66
|
+
base: { files: readTree(path.join(caseDir, 'base')) },
|
|
67
|
+
candidate: { files: readTree(path.join(caseDir, 'candidate')) }
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function caseConfig(name) {
|
|
72
|
+
const own = path.join(fixtureRoot, name, 'config.json');
|
|
73
|
+
return fs.existsSync(own) ? JSON.parse(fs.readFileSync(own, 'utf8')) : JSON.parse(fs.readFileSync(path.join(fixtureRoot, 'config.json'), 'utf8'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function treeDigest(files) {
|
|
77
|
+
return sha256(JSON.stringify(Object.keys(files).sort().map(file => [file, files[file]])));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function prepare(name, target = modules) {
|
|
81
|
+
const input = loadCase(name);
|
|
82
|
+
const { facts } = target.deriveDocFacts({ base: input.base, candidate: input.candidate, config: caseConfig(name) });
|
|
83
|
+
const obligations = target.deriveDocObligations(facts);
|
|
84
|
+
const artifacts = {};
|
|
85
|
+
for (const [file, text] of Object.entries(input.candidate.files)) artifacts[file] = { digest: sha256(text) };
|
|
86
|
+
const baseline = {
|
|
87
|
+
digest: treeDigest(input.base.files),
|
|
88
|
+
artifacts: Object.fromEntries(Object.entries(input.base.files).map(([file, text]) => [file, sha256(text)]))
|
|
89
|
+
};
|
|
90
|
+
return { input, facts, obligations, artifacts, baseline };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function obligationOf(ctx, id) {
|
|
94
|
+
return ctx.obligations.find(item => item.id === id);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resultOf(output, id) {
|
|
98
|
+
return output.results.find(item => item.obligationId === id);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function check(ctx, { executionRecords = [], reviewRecords = [], baseline = ctx.baseline, obligations = ctx.obligations, artifacts = ctx.artifacts } = {}, target = modules) {
|
|
102
|
+
return target.checkDocObligations({ obligations, artifacts, baseline, executionRecords, reviewRecords });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// P3 input-boundary helper: distinguish an omitted `artifacts` key from an explicit value,
|
|
106
|
+
// including an explicit `undefined` or `{}`.
|
|
107
|
+
const OMIT_ARTIFACTS = Symbol('omit artifacts');
|
|
108
|
+
function checkArtifacts(ctx, artifactsArg, { executionRecords = [], reviewRecords = [], baseline = ctx.baseline, obligations = ctx.obligations } = {}, target = modules) {
|
|
109
|
+
const args = { obligations, baseline, executionRecords, reviewRecords };
|
|
110
|
+
if (artifactsArg !== OMIT_ARTIFACTS) args.artifacts = artifactsArg;
|
|
111
|
+
return target.checkDocObligations(args);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function digestMap(ctx, paths) {
|
|
115
|
+
return Object.fromEntries(paths.map(file => [file, ctx.artifacts[file].digest]));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function baselineMapFor(ctx, paths) {
|
|
119
|
+
return Object.fromEntries(paths.map(file => [file, ctx.baseline.artifacts[file]]));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// A complete execution record. Candidate paths carry candidate digests; baseline-only
|
|
123
|
+
// paths carry trusted baseline digests.
|
|
124
|
+
function execRecord(ctx, id, obligationId, { paths, candidatePaths = paths, baselinePaths = [], artifactDigests, baselineArtifacts, ...overrides } = {}) {
|
|
125
|
+
return {
|
|
126
|
+
id,
|
|
127
|
+
obligationId,
|
|
128
|
+
command: `node ci/mechanism-335-baseline-evidence.test.js # ${obligationId}`,
|
|
129
|
+
exitCode: 0,
|
|
130
|
+
provenance: '335 fixture execution record bound to the trusted baseline',
|
|
131
|
+
baseDigest: ctx.baseline.digest,
|
|
132
|
+
paths: [...paths],
|
|
133
|
+
artifactDigests: artifactDigests === undefined ? digestMap(ctx, candidatePaths.filter(file => ctx.artifacts[file])) : artifactDigests,
|
|
134
|
+
baselineArtifacts: baselineArtifacts === undefined ? baselineMapFor(ctx, baselinePaths) : baselineArtifacts,
|
|
135
|
+
...overrides
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function reviewRecord(ctx, id, obligationId, { paths, candidatePaths = paths, baselinePaths = [], artifactDigests, baselineArtifacts, ...overrides } = {}) {
|
|
140
|
+
return {
|
|
141
|
+
...execRecord(ctx, id, obligationId, { paths, candidatePaths, baselinePaths, artifactDigests, baselineArtifacts }),
|
|
142
|
+
kind: 'review',
|
|
143
|
+
verdict: 'approve',
|
|
144
|
+
summary: 'independent reviewer approved the baseline-bound change',
|
|
145
|
+
...overrides
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function diagnosticCodes(output) {
|
|
150
|
+
return output.diagnostics.map(item => item.code);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function checkCase(name, run) {
|
|
154
|
+
try {
|
|
155
|
+
run();
|
|
156
|
+
helper.check(name, true);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
helper.check(name, false, error && error.message ? error.message : String(error));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const DELETED_ADR = 'docs/adr/GRPH-008.md';
|
|
163
|
+
const RENAME_FROM = 'docs/reference/old-guide.md';
|
|
164
|
+
const RENAME_TO = 'docs/reference/guide.md';
|
|
165
|
+
const GUIDE = 'docs/reference/guide.md';
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Derivation shape: scope.paths is preserved and baseline paths are annotated.
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
checkCase('335 derivation annotates the deleted ADR without dropping it from scope.paths', () => {
|
|
172
|
+
const ctx = prepare('deleted-adr');
|
|
173
|
+
const lint = obligationOf(ctx, 'doc-change:doc-lint');
|
|
174
|
+
assert.ok(lint.scope.paths.includes(DELETED_ADR), 'the deleted path stays in scope.paths');
|
|
175
|
+
assert.deepStrictEqual(lint.scope.baselinePaths,
|
|
176
|
+
[{ path: DELETED_ADR, reason: 'deleted' }], 'the deleted path is annotated as baseline-only');
|
|
177
|
+
assert.deepStrictEqual(obligationOf(ctx, 'doc-change:quality-review').scope.baselinePaths,
|
|
178
|
+
[{ path: DELETED_ADR, reason: 'deleted' }], 'normative obligations carry the same annotation');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
checkCase('335 derivation annotates a rename source with its destination and keeps both scope paths', () => {
|
|
182
|
+
const ctx = prepare('rename');
|
|
183
|
+
const lint = obligationOf(ctx, 'doc-change:doc-lint');
|
|
184
|
+
assert.ok(lint.scope.paths.includes(RENAME_FROM) && lint.scope.paths.includes(RENAME_TO),
|
|
185
|
+
`scope.paths keeps both endpoints: ${JSON.stringify(lint.scope.paths)}`);
|
|
186
|
+
assert.deepStrictEqual(lint.scope.baselinePaths,
|
|
187
|
+
[{ path: RENAME_FROM, reason: 'rename-from', renameTo: RENAME_TO }], 'the old endpoint is baseline-only');
|
|
188
|
+
assert.ok(!lint.scope.baselinePaths.some(entry => entry.path === RENAME_TO),
|
|
189
|
+
'the rename destination is not baseline-only');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// Positive controls: deletion tombstones and rename pairs pass.
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
checkCase('335 deleted ADR: baseline-bound lint, link, conflict and review evidence passes', () => {
|
|
197
|
+
const ctx = prepare('deleted-adr');
|
|
198
|
+
const output = check(ctx, {
|
|
199
|
+
executionRecords: [
|
|
200
|
+
execRecord(ctx, 'lint-run', 'doc-change:doc-lint', { paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR] }),
|
|
201
|
+
execRecord(ctx, 'links-run', 'doc-change:link-check', { paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR] }),
|
|
202
|
+
execRecord(ctx, 'conflict-run', 'doc-change:conflict-check', { paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR] })
|
|
203
|
+
],
|
|
204
|
+
reviewRecords: [
|
|
205
|
+
reviewRecord(ctx, 'review-run', 'doc-change:quality-review', { paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR] })
|
|
206
|
+
]
|
|
207
|
+
});
|
|
208
|
+
for (const id of ['doc-change:doc-lint', 'doc-change:link-check', 'doc-change:quality-review', 'doc-change:conflict-check']) {
|
|
209
|
+
const result = resultOf(output, id);
|
|
210
|
+
assert.ok(result, `${id} has a result`);
|
|
211
|
+
assert.strictEqual(result.observation, 'observed', `${id} observation`);
|
|
212
|
+
assert.strictEqual(result.outcome, 'pass', `${id} outcome`);
|
|
213
|
+
}
|
|
214
|
+
const lint = resultOf(output, 'doc-change:doc-lint');
|
|
215
|
+
assert.strictEqual(lint.baseDigest, ctx.baseline.digest, 'result records the whole-baseline identity');
|
|
216
|
+
assert.strictEqual(lint.baselineArtifacts[DELETED_ADR], ctx.baseline.artifacts[DELETED_ADR],
|
|
217
|
+
'result exposes the trusted per-path baseline digest');
|
|
218
|
+
assert.ok(lint.recordRefs.includes('lint-run'), 'the tombstone record is cited');
|
|
219
|
+
assert.ok(!output.diagnostics.some(item => item.code === 'missing-path-coverage'),
|
|
220
|
+
`no path-coverage diagnostic on a complete tombstone: ${JSON.stringify(output.diagnostics)}`);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
checkCase('335 identified rename: one record binding both endpoints satisfies lint and links', () => {
|
|
224
|
+
const ctx = prepare('rename');
|
|
225
|
+
const output = check(ctx, {
|
|
226
|
+
executionRecords: [
|
|
227
|
+
execRecord(ctx, 'move-run', 'doc-change:doc-lint', {
|
|
228
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM]
|
|
229
|
+
}),
|
|
230
|
+
execRecord(ctx, 'move-links', 'doc-change:link-check', {
|
|
231
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM]
|
|
232
|
+
})
|
|
233
|
+
]
|
|
234
|
+
});
|
|
235
|
+
for (const id of ['doc-change:doc-lint', 'doc-change:link-check']) {
|
|
236
|
+
const result = resultOf(output, id);
|
|
237
|
+
assert.strictEqual(result.outcome, 'pass', `${id} passes with a single pair record: ${JSON.stringify(result)}`);
|
|
238
|
+
assert.strictEqual(result.baselineArtifacts[RENAME_FROM], ctx.baseline.artifacts[RENAME_FROM],
|
|
239
|
+
`${id} records the rename source digest`);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// Deletion negative controls.
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
checkCase('335 deleted ADR with no evidence stays none', () => {
|
|
248
|
+
const ctx = prepare('deleted-adr');
|
|
249
|
+
const output = check(ctx);
|
|
250
|
+
for (const id of ['doc-change:doc-lint', 'doc-change:link-check', 'doc-change:quality-review', 'doc-change:conflict-check']) {
|
|
251
|
+
assert.notStrictEqual(resultOf(output, id).outcome, 'pass', `${id} cannot pass without evidence`);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
checkCase('335 deleted ADR stale whole-baseline identity is rejected', () => {
|
|
256
|
+
const ctx = prepare('deleted-adr');
|
|
257
|
+
const output = check(ctx, {
|
|
258
|
+
executionRecords: [execRecord(ctx, 'lint-run', 'doc-change:doc-lint', {
|
|
259
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR], baseDigest: sha256('another baseline')
|
|
260
|
+
})]
|
|
261
|
+
});
|
|
262
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass', 'a different baseline cannot pass');
|
|
263
|
+
assert.ok(diagnosticCodes(output).includes('stale-base-digest'), `stale base is diagnosed: ${diagnosticCodes(output)}`);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
checkCase('335 deleted ADR wrong per-path baseline digest is rejected', () => {
|
|
267
|
+
const ctx = prepare('deleted-adr');
|
|
268
|
+
const output = check(ctx, {
|
|
269
|
+
executionRecords: [execRecord(ctx, 'lint-run', 'doc-change:doc-lint', {
|
|
270
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [],
|
|
271
|
+
baselineArtifacts: { [DELETED_ADR]: sha256('another baseline file') }
|
|
272
|
+
})]
|
|
273
|
+
});
|
|
274
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass', 'a wrong old-path digest cannot pass');
|
|
275
|
+
assert.ok(diagnosticCodes(output).includes('stale-baseline-artifact-digest'),
|
|
276
|
+
`wrong baseline digest is diagnosed: ${diagnosticCodes(output)}`);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
checkCase('335 deleted ADR missing baseline.artifacts fails closed', () => {
|
|
280
|
+
const ctx = prepare('deleted-adr');
|
|
281
|
+
const output = check(ctx, {
|
|
282
|
+
executionRecords: [execRecord(ctx, 'lint-run', 'doc-change:doc-lint', {
|
|
283
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR]
|
|
284
|
+
})],
|
|
285
|
+
baseline: { digest: ctx.baseline.digest }
|
|
286
|
+
});
|
|
287
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass', 'absent baseline artifacts fail closed');
|
|
288
|
+
assert.ok(diagnosticCodes(output).includes('baseline-artifacts-missing'),
|
|
289
|
+
`missing baseline artifacts are diagnosed: ${diagnosticCodes(output)}`);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
checkCase('335 deleted ADR missing per-path baseline binding on the record is not a pass', () => {
|
|
293
|
+
const ctx = prepare('deleted-adr');
|
|
294
|
+
const output = check(ctx, {
|
|
295
|
+
executionRecords: [execRecord(ctx, 'lint-run', 'doc-change:doc-lint', {
|
|
296
|
+
paths: [DELETED_ADR], candidatePaths: [], baselineArtifacts: {}
|
|
297
|
+
})]
|
|
298
|
+
});
|
|
299
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass', 'a record without the old-path binding cannot pass');
|
|
300
|
+
assert.ok(diagnosticCodes(output).includes('missing-path-coverage'),
|
|
301
|
+
`unbound baseline path is diagnosed: ${diagnosticCodes(output)}`);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
checkCase('335 deleted path that still has a candidate artifact is a contradiction and fails closed', () => {
|
|
305
|
+
const ctx = prepare('deleted-adr');
|
|
306
|
+
ctx.artifacts[DELETED_ADR] = { digest: sha256('a candidate artifact for a deleted path') };
|
|
307
|
+
const output = check(ctx, {
|
|
308
|
+
executionRecords: [execRecord(ctx, 'lint-run', 'doc-change:doc-lint', {
|
|
309
|
+
paths: [DELETED_ADR], candidatePaths: [DELETED_ADR], baselinePaths: []
|
|
310
|
+
})]
|
|
311
|
+
});
|
|
312
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
313
|
+
'a candidate-present contradiction cannot pass');
|
|
314
|
+
assert.ok(diagnosticCodes(output).includes('baseline-path-candidate-present'),
|
|
315
|
+
`candidate-present contradiction is diagnosed: ${diagnosticCodes(output)}`);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
checkCase('335 absent whole-baseline identity fails closed for a declared baseline path', () => {
|
|
319
|
+
const ctx = prepare('deleted-adr');
|
|
320
|
+
const output = check(ctx, {
|
|
321
|
+
executionRecords: [execRecord(ctx, 'lint-run', 'doc-change:doc-lint', {
|
|
322
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR]
|
|
323
|
+
})],
|
|
324
|
+
baseline: { artifacts: ctx.baseline.artifacts }
|
|
325
|
+
});
|
|
326
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass', 'absent baseline identity cannot pass');
|
|
327
|
+
assert.ok(diagnosticCodes(output).includes('baseline-identity-missing'),
|
|
328
|
+
`absent baseline identity is diagnosed: ${diagnosticCodes(output)}`);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
checkCase('335 invalid per-path baseline digest fails closed', () => {
|
|
332
|
+
const ctx = prepare('deleted-adr');
|
|
333
|
+
const output = check(ctx, {
|
|
334
|
+
executionRecords: [execRecord(ctx, 'lint-run', 'doc-change:doc-lint', {
|
|
335
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: []
|
|
336
|
+
})],
|
|
337
|
+
baseline: { digest: ctx.baseline.digest, artifacts: { [DELETED_ADR]: 'not-a-sha256-digest' } }
|
|
338
|
+
});
|
|
339
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass', 'an invalid baseline digest cannot pass');
|
|
340
|
+
assert.ok(diagnosticCodes(output).includes('baseline-artifact-invalid'),
|
|
341
|
+
`invalid baseline digest is diagnosed: ${diagnosticCodes(output)}`);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
checkCase('335 malformed baselinePaths metadata fails closed', () => {
|
|
345
|
+
const ctx = prepare('deleted-adr');
|
|
346
|
+
const mutate = (baselinePaths) => check(ctx, {
|
|
347
|
+
obligations: ctx.obligations.map(item => item.capability === 'doc-lint'
|
|
348
|
+
? { ...item, scope: { ...item.scope, baselinePaths } }
|
|
349
|
+
: item),
|
|
350
|
+
executionRecords: [execRecord(ctx, 'lint-run', 'doc-change:doc-lint', {
|
|
351
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR]
|
|
352
|
+
})]
|
|
353
|
+
});
|
|
354
|
+
const cases = [
|
|
355
|
+
[{ path: DELETED_ADR, reason: 'deleted' }, { path: DELETED_ADR, reason: 'deleted' }],
|
|
356
|
+
[{ path: 'docs/adr/not-in-scope.md', reason: 'deleted' }],
|
|
357
|
+
[{ path: DELETED_ADR, reason: 'moved' }],
|
|
358
|
+
[{ path: DELETED_ADR, reason: 'rename-from' }],
|
|
359
|
+
[{ path: DELETED_ADR, reason: 'rename-from', renameTo: DELETED_ADR }]
|
|
360
|
+
];
|
|
361
|
+
for (const baselinePaths of cases) {
|
|
362
|
+
const output = mutate(baselinePaths);
|
|
363
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
364
|
+
`malformed metadata fails closed: ${JSON.stringify(baselinePaths)}`);
|
|
365
|
+
assert.ok(diagnosticCodes(output).includes('baseline-path-invalid'),
|
|
366
|
+
`malformed metadata is diagnosed: ${JSON.stringify(baselinePaths)} -> ${diagnosticCodes(output)}`);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
// Rename negative controls.
|
|
372
|
+
// ---------------------------------------------------------------------------
|
|
373
|
+
|
|
374
|
+
checkCase('335 rename destination-only evidence cannot satisfy the source endpoint', () => {
|
|
375
|
+
const ctx = prepare('rename');
|
|
376
|
+
const output = check(ctx, {
|
|
377
|
+
executionRecords: [execRecord(ctx, 'dest-run', 'doc-change:doc-lint', {
|
|
378
|
+
paths: [RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: []
|
|
379
|
+
})]
|
|
380
|
+
});
|
|
381
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass', 'the source endpoint stays unmet');
|
|
382
|
+
assert.ok(JSON.stringify(output.diagnostics).includes(RENAME_FROM),
|
|
383
|
+
`the diagnostic names the unmet source: ${JSON.stringify(output.diagnostics)}`);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
checkCase('335 rename source-only tombstone cannot satisfy the source endpoint', () => {
|
|
387
|
+
const ctx = prepare('rename');
|
|
388
|
+
const output = check(ctx, {
|
|
389
|
+
executionRecords: [execRecord(ctx, 'source-run', 'doc-change:doc-lint', {
|
|
390
|
+
paths: [RENAME_FROM], candidatePaths: [], baselinePaths: [RENAME_FROM]
|
|
391
|
+
})]
|
|
392
|
+
});
|
|
393
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
394
|
+
'a deletion tombstone alone cannot satisfy a known rename');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
checkCase('335 split endpoint records cannot satisfy the source endpoint', () => {
|
|
398
|
+
const ctx = prepare('rename');
|
|
399
|
+
const output = check(ctx, {
|
|
400
|
+
executionRecords: [
|
|
401
|
+
execRecord(ctx, 'source-run', 'doc-change:doc-lint', { paths: [RENAME_FROM], candidatePaths: [], baselinePaths: [RENAME_FROM] }),
|
|
402
|
+
execRecord(ctx, 'dest-run', 'doc-change:doc-lint', { paths: [RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [] })
|
|
403
|
+
]
|
|
404
|
+
});
|
|
405
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
406
|
+
'two records, one per endpoint, cannot satisfy a rename');
|
|
407
|
+
assert.ok(JSON.stringify(output.diagnostics).includes(RENAME_FROM),
|
|
408
|
+
`the diagnostic names the unmet source: ${JSON.stringify(output.diagnostics)}`);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
checkCase('335 rename pair with a stale destination digest cannot pass', () => {
|
|
412
|
+
const ctx = prepare('rename');
|
|
413
|
+
const output = check(ctx, {
|
|
414
|
+
executionRecords: [execRecord(ctx, 'move-run', 'doc-change:doc-lint', {
|
|
415
|
+
paths: [RENAME_FROM, RENAME_TO], baselinePaths: [RENAME_FROM],
|
|
416
|
+
artifactDigests: { [RENAME_TO]: sha256('stale destination content') }
|
|
417
|
+
})]
|
|
418
|
+
});
|
|
419
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass', 'a stale destination cannot pass');
|
|
420
|
+
assert.ok(diagnosticCodes(output).includes('stale-artifact-digest'),
|
|
421
|
+
`stale destination is diagnosed: ${diagnosticCodes(output)}`);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
checkCase('335 rename pair missing the source endpoint in paths cannot pass', () => {
|
|
425
|
+
const ctx = prepare('rename');
|
|
426
|
+
const output = check(ctx, {
|
|
427
|
+
executionRecords: [execRecord(ctx, 'move-run', 'doc-change:doc-lint', {
|
|
428
|
+
paths: [RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM]
|
|
429
|
+
})]
|
|
430
|
+
});
|
|
431
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
432
|
+
'a record must list the old path explicitly');
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
checkCase('335 rename pair missing the destination candidate digest cannot pass', () => {
|
|
436
|
+
const ctx = prepare('rename');
|
|
437
|
+
const output = check(ctx, {
|
|
438
|
+
executionRecords: [execRecord(ctx, 'move-run', 'doc-change:doc-lint', {
|
|
439
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [], baselinePaths: [RENAME_FROM]
|
|
440
|
+
})]
|
|
441
|
+
});
|
|
442
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
443
|
+
'the current destination digest is required');
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
checkCase('335 rename with the wrong old-path baseline digest cannot pass', () => {
|
|
447
|
+
const ctx = prepare('rename');
|
|
448
|
+
const output = check(ctx, {
|
|
449
|
+
executionRecords: [execRecord(ctx, 'move-run', 'doc-change:doc-lint', {
|
|
450
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO],
|
|
451
|
+
baselineArtifacts: { [RENAME_FROM]: sha256('wrong old content') }
|
|
452
|
+
})]
|
|
453
|
+
});
|
|
454
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
455
|
+
'a wrong old-path digest cannot satisfy the rename source');
|
|
456
|
+
assert.ok(diagnosticCodes(output).includes('stale-baseline-artifact-digest'),
|
|
457
|
+
`wrong old-path digest is diagnosed: ${diagnosticCodes(output)}`);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
checkCase('335 rename whose destination has no candidate artifact fails closed', () => {
|
|
461
|
+
const ctx = prepare('rename');
|
|
462
|
+
delete ctx.artifacts[RENAME_TO];
|
|
463
|
+
const output = check(ctx, {
|
|
464
|
+
executionRecords: [execRecord(ctx, 'move-run', 'doc-change:doc-lint', {
|
|
465
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [], baselinePaths: [RENAME_FROM]
|
|
466
|
+
})]
|
|
467
|
+
});
|
|
468
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
469
|
+
'a rename destination without a candidate artifact fails closed');
|
|
470
|
+
assert.ok(diagnosticCodes(output).includes('baseline-path-invalid'),
|
|
471
|
+
`absent rename destination is diagnosed: ${diagnosticCodes(output)}`);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// ---------------------------------------------------------------------------
|
|
475
|
+
// Normal candidate-bound edits and failing/unsupported evidence.
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
checkCase('335 normal candidate-bound edits still pass and baseline-only digests cannot substitute', () => {
|
|
479
|
+
const ctx = prepare('docs-only');
|
|
480
|
+
const bound = check(ctx, {
|
|
481
|
+
executionRecords: [
|
|
482
|
+
execRecord(ctx, 'lint-run', 'doc-change:doc-lint', { paths: [GUIDE], candidatePaths: [GUIDE] }),
|
|
483
|
+
execRecord(ctx, 'links-run', 'doc-change:link-check', { paths: [GUIDE], candidatePaths: [GUIDE] })
|
|
484
|
+
]
|
|
485
|
+
});
|
|
486
|
+
assert.strictEqual(resultOf(bound, 'doc-change:doc-lint').outcome, 'pass', 'a normal candidate-bound edit passes');
|
|
487
|
+
const baselineOnly = check(ctx, {
|
|
488
|
+
executionRecords: [execRecord(ctx, 'lint-run', 'doc-change:doc-lint', {
|
|
489
|
+
paths: [GUIDE], candidatePaths: [], baselineArtifacts: baselineMapFor(ctx, [GUIDE])
|
|
490
|
+
})]
|
|
491
|
+
});
|
|
492
|
+
assert.notStrictEqual(resultOf(baselineOnly, 'doc-change:doc-lint').outcome, 'pass',
|
|
493
|
+
'a baseline-only digest cannot satisfy a normal edited artifact');
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
checkCase('335 correctly bound failing command still fails (never pass, never none)', () => {
|
|
497
|
+
const ctx = prepare('deleted-adr');
|
|
498
|
+
const output = check(ctx, {
|
|
499
|
+
executionRecords: [execRecord(ctx, 'lint-run', 'doc-change:doc-lint', {
|
|
500
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR], exitCode: 1
|
|
501
|
+
})]
|
|
502
|
+
});
|
|
503
|
+
const result = resultOf(output, 'doc-change:doc-lint');
|
|
504
|
+
assert.strictEqual(result.observation, 'observed', 'a bound failing command is observed');
|
|
505
|
+
assert.strictEqual(result.outcome, 'fail', 'a bound failing command fails');
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
checkCase('335 unsupported review kind and unapproved review cannot satisfy a baseline-only review MUST', () => {
|
|
509
|
+
const ctx = prepare('deleted-adr');
|
|
510
|
+
for (const bad of [
|
|
511
|
+
reviewRecord(ctx, 'review-run', 'doc-change:quality-review', { paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR], kind: 'note' }),
|
|
512
|
+
reviewRecord(ctx, 'review-run', 'doc-change:quality-review', { paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR], verdict: 'reject' })
|
|
513
|
+
]) {
|
|
514
|
+
const output = check(ctx, { reviewRecords: [bad] });
|
|
515
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:quality-review').outcome, 'pass',
|
|
516
|
+
'only an approved review can satisfy quality-review');
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
checkCase('335 incomplete execution record on a baseline-only path is not a pass', () => {
|
|
521
|
+
const ctx = prepare('deleted-adr');
|
|
522
|
+
const output = check(ctx, {
|
|
523
|
+
executionRecords: [execRecord(ctx, 'lint-run', 'doc-change:doc-lint', {
|
|
524
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR], command: undefined, provenance: undefined
|
|
525
|
+
})]
|
|
526
|
+
});
|
|
527
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass', 'incomplete execution evidence cannot pass');
|
|
528
|
+
assert.ok(diagnosticCodes(output).includes('execution-record-incomplete'),
|
|
529
|
+
`incomplete execution is diagnosed: ${diagnosticCodes(output)}`);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
// Determinism, no input mutation, and source/package parity.
|
|
534
|
+
// ---------------------------------------------------------------------------
|
|
535
|
+
|
|
536
|
+
checkCase('335 baseline derivation, evidence and check are deterministic and do not mutate inputs', () => {
|
|
537
|
+
const first = prepare('deleted-adr');
|
|
538
|
+
const second = prepare('deleted-adr');
|
|
539
|
+
assert.deepStrictEqual(second.obligations, first.obligations, 'same inputs derive the same obligations');
|
|
540
|
+
const records = {
|
|
541
|
+
executionRecords: [
|
|
542
|
+
execRecord(first, 'lint-run', 'doc-change:doc-lint', { paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR] })
|
|
543
|
+
]
|
|
544
|
+
};
|
|
545
|
+
const snapshot = JSON.stringify({ obligations: first.obligations, artifacts: first.artifacts, baseline: first.baseline, records });
|
|
546
|
+
const outputA = check(first, records);
|
|
547
|
+
const outputB = check(second, records);
|
|
548
|
+
assert.deepStrictEqual(outputB, outputA, 'same inputs produce the same check output');
|
|
549
|
+
assert.strictEqual(JSON.stringify({ obligations: first.obligations, artifacts: first.artifacts, baseline: first.baseline, records }), snapshot,
|
|
550
|
+
'the check does not mutate its inputs');
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
checkCase('335 canonical and packaged modules produce identical baseline-bound results', () => {
|
|
554
|
+
for (const fixture of ['deleted-adr', 'rename']) {
|
|
555
|
+
const sourceCtx = prepare(fixture, modules);
|
|
556
|
+
const packageCtx = prepare(fixture, packageModules);
|
|
557
|
+
const sourceRecords = fixture === 'rename'
|
|
558
|
+
? { executionRecords: [
|
|
559
|
+
execRecord(sourceCtx, 'move-run', 'doc-change:doc-lint', { paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM] }),
|
|
560
|
+
execRecord(sourceCtx, 'move-links', 'doc-change:link-check', { paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM] })
|
|
561
|
+
] }
|
|
562
|
+
: { executionRecords: [
|
|
563
|
+
execRecord(sourceCtx, 'lint-run', 'doc-change:doc-lint', { paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR] }),
|
|
564
|
+
execRecord(sourceCtx, 'links-run', 'doc-change:link-check', { paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR] })
|
|
565
|
+
] };
|
|
566
|
+
const packageRecords = fixture === 'rename'
|
|
567
|
+
? { executionRecords: [
|
|
568
|
+
execRecord(packageCtx, 'move-run', 'doc-change:doc-lint', { paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM] }),
|
|
569
|
+
execRecord(packageCtx, 'move-links', 'doc-change:link-check', { paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM] })
|
|
570
|
+
] }
|
|
571
|
+
: { executionRecords: [
|
|
572
|
+
execRecord(packageCtx, 'lint-run', 'doc-change:doc-lint', { paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR] }),
|
|
573
|
+
execRecord(packageCtx, 'links-run', 'doc-change:link-check', { paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR] })
|
|
574
|
+
] };
|
|
575
|
+
const sourceOutput = check(sourceCtx, sourceRecords, modules);
|
|
576
|
+
const packageOutput = check(packageCtx, packageRecords, packageModules);
|
|
577
|
+
assert.deepStrictEqual(packageOutput.results, sourceOutput.results,
|
|
578
|
+
`${fixture}: packaged evidence results match the canonical module`);
|
|
579
|
+
assert.deepStrictEqual(packageOutput.diagnostics, sourceOutput.diagnostics,
|
|
580
|
+
`${fixture}: packaged evidence diagnostics match the canonical module`);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// ---------------------------------------------------------------------------
|
|
585
|
+
// Remediation (cold-review finding 1): a record that carries a candidate digest
|
|
586
|
+
// for a baseline-only path is contradictory and must not bind, even when the
|
|
587
|
+
// baseline fields are otherwise correct. A valid alternate record may still pass.
|
|
588
|
+
// ---------------------------------------------------------------------------
|
|
589
|
+
|
|
590
|
+
checkCase('335 deleted path: a record carrying a candidate digest for the deleted path is rejected', () => {
|
|
591
|
+
const ctx = prepare('deleted-adr');
|
|
592
|
+
const ghost = execRecord(ctx, 'ghost-run', 'doc-change:doc-lint', {
|
|
593
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR],
|
|
594
|
+
artifactDigests: { [DELETED_ADR]: sha256('ghost candidate content') }
|
|
595
|
+
});
|
|
596
|
+
const output = check(ctx, { executionRecords: [ghost] });
|
|
597
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
598
|
+
'a record with a candidate digest for a deleted path cannot bind');
|
|
599
|
+
assert.ok(diagnosticCodes(output).includes('baseline-path-candidate-digest'),
|
|
600
|
+
`the contradictory record is diagnosed: ${diagnosticCodes(output)}`);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
checkCase('335 deleted path: a contradictory ghost record does not block a valid alternate tombstone', () => {
|
|
604
|
+
const ctx = prepare('deleted-adr');
|
|
605
|
+
const output = check(ctx, {
|
|
606
|
+
executionRecords: [
|
|
607
|
+
execRecord(ctx, 'ghost-run', 'doc-change:doc-lint', {
|
|
608
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR],
|
|
609
|
+
artifactDigests: { [DELETED_ADR]: sha256('ghost candidate content') }
|
|
610
|
+
}),
|
|
611
|
+
execRecord(ctx, 'clean-run', 'doc-change:doc-lint', {
|
|
612
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR]
|
|
613
|
+
})
|
|
614
|
+
]
|
|
615
|
+
});
|
|
616
|
+
assert.strictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
617
|
+
'a valid alternate tombstone still satisfies the obligation');
|
|
618
|
+
assert.ok(resultOf(output, 'doc-change:doc-lint').recordRefs.includes('clean-run'),
|
|
619
|
+
'only the clean record is cited');
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
checkCase('335 deleted path: a review record carrying a candidate digest for the deleted path is rejected', () => {
|
|
623
|
+
const ctx = prepare('deleted-adr');
|
|
624
|
+
const output = check(ctx, {
|
|
625
|
+
reviewRecords: [reviewRecord(ctx, 'ghost-review', 'doc-change:quality-review', {
|
|
626
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR],
|
|
627
|
+
artifactDigests: { [DELETED_ADR]: sha256('ghost candidate content') }
|
|
628
|
+
})]
|
|
629
|
+
});
|
|
630
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:quality-review').outcome, 'pass',
|
|
631
|
+
'a review record with a candidate digest for a deleted path cannot bind');
|
|
632
|
+
assert.ok(diagnosticCodes(output).includes('baseline-path-candidate-digest'),
|
|
633
|
+
`the contradictory review record is diagnosed: ${diagnosticCodes(output)}`);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
checkCase('335 rename source: a single pair record carrying a candidate digest for the old path is rejected', () => {
|
|
637
|
+
const ctx = prepare('rename');
|
|
638
|
+
const output = check(ctx, {
|
|
639
|
+
executionRecords: [execRecord(ctx, 'move-run', 'doc-change:doc-lint', {
|
|
640
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM],
|
|
641
|
+
artifactDigests: {
|
|
642
|
+
[RENAME_FROM]: sha256('ghost old endpoint'),
|
|
643
|
+
[RENAME_TO]: ctx.artifacts[RENAME_TO].digest
|
|
644
|
+
}
|
|
645
|
+
})]
|
|
646
|
+
});
|
|
647
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
648
|
+
'a pair record with a ghost old-path digest cannot satisfy the rename source');
|
|
649
|
+
assert.ok(diagnosticCodes(output).includes('baseline-path-candidate-digest'),
|
|
650
|
+
`the contradictory pair record is diagnosed: ${diagnosticCodes(output)}`);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// ---------------------------------------------------------------------------
|
|
654
|
+
// Remediation (orchestrator probe 2): candidate artifact map OLD path present
|
|
655
|
+
// with an invalid/absent digest. Own-key presence is not deletion; it is
|
|
656
|
+
// contradictory path state and fails closed.
|
|
657
|
+
// ---------------------------------------------------------------------------
|
|
658
|
+
|
|
659
|
+
checkCase('335 deleted path: a present-but-invalid candidate entry fails closed (own-key presence)', () => {
|
|
660
|
+
for (const bad of [{}, { digest: '' }, null]) {
|
|
661
|
+
const ctx = prepare('deleted-adr');
|
|
662
|
+
ctx.artifacts[DELETED_ADR] = bad;
|
|
663
|
+
const output = check(ctx, {
|
|
664
|
+
executionRecords: [execRecord(ctx, 'tomb-run', 'doc-change:doc-lint', {
|
|
665
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR]
|
|
666
|
+
})]
|
|
667
|
+
});
|
|
668
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
669
|
+
`present candidate entry ${JSON.stringify(bad)} cannot be treated as a deletion`);
|
|
670
|
+
assert.ok(diagnosticCodes(output).includes('baseline-path-candidate-present'),
|
|
671
|
+
`present candidate entry is diagnosed: ${JSON.stringify(bad)} -> ${diagnosticCodes(output)}`);
|
|
672
|
+
}
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
checkCase('335 rename source: a present-but-invalid candidate entry fails closed (own-key presence)', () => {
|
|
676
|
+
for (const bad of [{}, { digest: '' }, null]) {
|
|
677
|
+
const ctx = prepare('rename');
|
|
678
|
+
ctx.artifacts[RENAME_FROM] = bad;
|
|
679
|
+
const output = check(ctx, {
|
|
680
|
+
executionRecords: [execRecord(ctx, 'move-run', 'doc-change:doc-lint', {
|
|
681
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM]
|
|
682
|
+
})]
|
|
683
|
+
});
|
|
684
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
685
|
+
`present candidate entry ${JSON.stringify(bad)} cannot be treated as a rename source`);
|
|
686
|
+
assert.ok(diagnosticCodes(output).includes('baseline-path-candidate-present'),
|
|
687
|
+
`present candidate entry is diagnosed: ${JSON.stringify(bad)} -> ${diagnosticCodes(output)}`);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
checkCase('335 missing per-path binding is visible as baseline-binding-missing', () => {
|
|
692
|
+
const ctx = prepare('deleted-adr');
|
|
693
|
+
const output = check(ctx, {
|
|
694
|
+
executionRecords: [execRecord(ctx, 'lint-run', 'doc-change:doc-lint', {
|
|
695
|
+
paths: [DELETED_ADR], candidatePaths: [], baselineArtifacts: {}
|
|
696
|
+
})]
|
|
697
|
+
});
|
|
698
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass');
|
|
699
|
+
assert.ok(diagnosticCodes(output).includes('baseline-binding-missing'),
|
|
700
|
+
`a record that names a baseline path without a binding is diagnosed: ${diagnosticCodes(output)}`);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// ---------------------------------------------------------------------------
|
|
704
|
+
// P3 input boundary: an absent or malformed top-level candidate artifact map is
|
|
705
|
+
// not a deletion inventory. It is unknown input and fails baseline-only
|
|
706
|
+
// obligations closed; an explicit {} remains a valid empty deletion inventory.
|
|
707
|
+
// ---------------------------------------------------------------------------
|
|
708
|
+
|
|
709
|
+
class CustomInventory {}
|
|
710
|
+
|
|
711
|
+
// Object-like values have no own enumerable keys, so the pre-fix provenance check treated
|
|
712
|
+
// them as an empty inventory. They are not plain objects and must fail baseline-only
|
|
713
|
+
// obligations closed; null-prototype dictionaries remain valid (tested separately).
|
|
714
|
+
const OBJECT_LIKE_INVALID_ARTIFACTS = [
|
|
715
|
+
['Map', new Map()],
|
|
716
|
+
['Date', new Date(0)],
|
|
717
|
+
['RegExp', /inventory/],
|
|
718
|
+
['class instance', new CustomInventory()],
|
|
719
|
+
['custom prototype', Object.create({ inherited: true })]
|
|
720
|
+
];
|
|
721
|
+
|
|
722
|
+
const INVALID_ARTIFACTS = [
|
|
723
|
+
['omitted', OMIT_ARTIFACTS],
|
|
724
|
+
['undefined', undefined],
|
|
725
|
+
['null', null],
|
|
726
|
+
['array', [DELETED_ADR]],
|
|
727
|
+
['string', 'not-a-map'],
|
|
728
|
+
['boolean', false],
|
|
729
|
+
...OBJECT_LIKE_INVALID_ARTIFACTS
|
|
730
|
+
];
|
|
731
|
+
|
|
732
|
+
checkCase('335 P3 deletion: absent or malformed top-level artifacts fails closed', () => {
|
|
733
|
+
const failures = [];
|
|
734
|
+
for (const [label, value] of INVALID_ARTIFACTS) {
|
|
735
|
+
const ctx = prepare('deleted-adr');
|
|
736
|
+
const output = checkArtifacts(ctx, value, {
|
|
737
|
+
executionRecords: [execRecord(ctx, 'tomb-run', 'doc-change:doc-lint', {
|
|
738
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR]
|
|
739
|
+
})]
|
|
740
|
+
});
|
|
741
|
+
const result = resultOf(output, 'doc-change:doc-lint');
|
|
742
|
+
if (result.outcome !== 'none') failures.push(`${label}: outcome=${result.outcome}`);
|
|
743
|
+
if (!diagnosticCodes(output).includes('candidate-artifacts-invalid')) {
|
|
744
|
+
failures.push(`${label}: diagnostics=${diagnosticCodes(output)}`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
assert.deepStrictEqual(failures, [],
|
|
748
|
+
`all absent/malformed candidate inventory variants must fail closed: ${failures.join('; ')}`);
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
checkCase('335 P3 deletion: an approving review cannot pass with absent top-level artifacts', () => {
|
|
752
|
+
const ctx = prepare('deleted-adr');
|
|
753
|
+
const output = checkArtifacts(ctx, OMIT_ARTIFACTS, {
|
|
754
|
+
reviewRecords: [reviewRecord(ctx, 'review-run', 'doc-change:quality-review', {
|
|
755
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR]
|
|
756
|
+
})]
|
|
757
|
+
});
|
|
758
|
+
assert.strictEqual(resultOf(output, 'doc-change:quality-review').outcome, 'none',
|
|
759
|
+
'an approving review cannot satisfy quality-review without a known candidate inventory');
|
|
760
|
+
assert.ok(diagnosticCodes(output).includes('candidate-artifacts-invalid'),
|
|
761
|
+
`candidate inventory is diagnosed: ${diagnosticCodes(output)}`);
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
checkCase('335 P3 deletion: object-like inventories fail closed for an approving review', () => {
|
|
765
|
+
const failures = [];
|
|
766
|
+
for (const [label, value] of OBJECT_LIKE_INVALID_ARTIFACTS) {
|
|
767
|
+
const ctx = prepare('deleted-adr');
|
|
768
|
+
const output = checkArtifacts(ctx, value, {
|
|
769
|
+
reviewRecords: [reviewRecord(ctx, 'review-run', 'doc-change:quality-review', {
|
|
770
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR]
|
|
771
|
+
})]
|
|
772
|
+
});
|
|
773
|
+
const result = resultOf(output, 'doc-change:quality-review');
|
|
774
|
+
if (result.outcome !== 'none') failures.push(`${label}: outcome=${result.outcome}`);
|
|
775
|
+
if (!diagnosticCodes(output).includes('candidate-artifacts-invalid')) {
|
|
776
|
+
failures.push(`${label}: diagnostics=${diagnosticCodes(output)}`);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
assert.deepStrictEqual(failures, [],
|
|
780
|
+
`every object-like inventory must fail the review lane closed: ${failures.join('; ')}`);
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
checkCase('335 P3 rename: absent or malformed top-level artifacts fails closed', () => {
|
|
784
|
+
const failures = [];
|
|
785
|
+
for (const [label, value] of INVALID_ARTIFACTS) {
|
|
786
|
+
const ctx = prepare('rename');
|
|
787
|
+
const output = checkArtifacts(ctx, value, {
|
|
788
|
+
executionRecords: [execRecord(ctx, 'move-run', 'doc-change:doc-lint', {
|
|
789
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM]
|
|
790
|
+
})]
|
|
791
|
+
});
|
|
792
|
+
const result = resultOf(output, 'doc-change:doc-lint');
|
|
793
|
+
if (result.outcome !== 'none') failures.push(`${label}: outcome=${result.outcome}`);
|
|
794
|
+
if (!diagnosticCodes(output).includes('candidate-artifacts-invalid')) {
|
|
795
|
+
failures.push(`${label}: diagnostics=${diagnosticCodes(output)}`);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
assert.deepStrictEqual(failures, [],
|
|
799
|
+
`rename must fail closed for every absent/malformed inventory: ${failures.join('; ')}`);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
checkCase('335 P3 deletion: explicit empty object remains a valid empty deletion inventory', () => {
|
|
803
|
+
const ctx = prepare('deleted-adr');
|
|
804
|
+
const output = checkArtifacts(ctx, {}, {
|
|
805
|
+
executionRecords: [execRecord(ctx, 'tomb-run', 'doc-change:doc-lint', {
|
|
806
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR]
|
|
807
|
+
})]
|
|
808
|
+
});
|
|
809
|
+
assert.strictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
810
|
+
'an explicit empty object is a legitimate empty deletion inventory');
|
|
811
|
+
assert.ok(!diagnosticCodes(output).includes('candidate-artifacts-invalid'),
|
|
812
|
+
'the legitimate empty inventory is not diagnosed as malformed');
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
checkCase('335 P3 null-prototype inventory is accepted for deletion and populated rename lanes', () => {
|
|
816
|
+
const deletion = prepare('deleted-adr');
|
|
817
|
+
const empty = Object.create(null);
|
|
818
|
+
const deletionOut = checkArtifacts(deletion, empty, {
|
|
819
|
+
executionRecords: [execRecord(deletion, 'tomb-run', 'doc-change:doc-lint', {
|
|
820
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR]
|
|
821
|
+
})]
|
|
822
|
+
});
|
|
823
|
+
assert.strictEqual(resultOf(deletionOut, 'doc-change:doc-lint').outcome, 'pass',
|
|
824
|
+
'an empty null-prototype dictionary is a legitimate empty deletion inventory');
|
|
825
|
+
assert.ok(!diagnosticCodes(deletionOut).includes('candidate-artifacts-invalid'),
|
|
826
|
+
'the null-prototype empty inventory is not diagnosed as malformed');
|
|
827
|
+
|
|
828
|
+
const rename = prepare('rename');
|
|
829
|
+
const populated = Object.create(null);
|
|
830
|
+
populated[RENAME_TO] = rename.artifacts[RENAME_TO];
|
|
831
|
+
const renameOut = checkArtifacts(rename, populated, {
|
|
832
|
+
executionRecords: [execRecord(rename, 'move-run', 'doc-change:doc-lint', {
|
|
833
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM]
|
|
834
|
+
})]
|
|
835
|
+
});
|
|
836
|
+
assert.strictEqual(resultOf(renameOut, 'doc-change:doc-lint').outcome, 'pass',
|
|
837
|
+
'a populated null-prototype inventory still satisfies the rename lane');
|
|
838
|
+
assert.ok(!diagnosticCodes(renameOut).includes('candidate-artifacts-invalid'),
|
|
839
|
+
'the populated null-prototype inventory is not diagnosed as malformed');
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
checkCase('335 P3 populated inventory and normal candidate binding stay valid', () => {
|
|
843
|
+
const deletion = prepare('deleted-adr');
|
|
844
|
+
const deletionOut = checkArtifacts(deletion, deletion.artifacts, {
|
|
845
|
+
executionRecords: [execRecord(deletion, 'tomb-run', 'doc-change:doc-lint', {
|
|
846
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR]
|
|
847
|
+
})]
|
|
848
|
+
});
|
|
849
|
+
assert.strictEqual(resultOf(deletionOut, 'doc-change:doc-lint').outcome, 'pass',
|
|
850
|
+
'a populated candidate inventory still satisfies the deletion lane');
|
|
851
|
+
const edited = prepare('docs-only');
|
|
852
|
+
const editedOut = checkArtifacts(edited, edited.artifacts, {
|
|
853
|
+
executionRecords: [execRecord(edited, 'edit-run', 'doc-change:doc-lint', {
|
|
854
|
+
paths: [GUIDE], candidatePaths: [GUIDE]
|
|
855
|
+
})]
|
|
856
|
+
});
|
|
857
|
+
assert.strictEqual(resultOf(editedOut, 'doc-change:doc-lint').outcome, 'pass',
|
|
858
|
+
'normal candidate-bound edits still pass with a populated inventory');
|
|
859
|
+
assert.ok(!diagnosticCodes(deletionOut).includes('candidate-artifacts-invalid'));
|
|
860
|
+
assert.ok(!diagnosticCodes(editedOut).includes('candidate-artifacts-invalid'));
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
checkCase('335 P3 non-baseline callers keep absent-map behavior', () => {
|
|
864
|
+
const ctx = prepare('docs-only');
|
|
865
|
+
const records = {
|
|
866
|
+
executionRecords: [execRecord(ctx, 'edit-run', 'doc-change:doc-lint', {
|
|
867
|
+
paths: [GUIDE], candidatePaths: [GUIDE]
|
|
868
|
+
})]
|
|
869
|
+
};
|
|
870
|
+
const omitted = checkArtifacts(ctx, OMIT_ARTIFACTS, records);
|
|
871
|
+
const empty = checkArtifacts(ctx, {}, records);
|
|
872
|
+
assert.deepStrictEqual(omitted, empty,
|
|
873
|
+
'non-baseline obligations treat an absent map and an explicit empty map identically');
|
|
874
|
+
assert.ok(diagnosticCodes(omitted).includes('missing-path-coverage'),
|
|
875
|
+
`the normal edited path still fails closed: ${diagnosticCodes(omitted)}`);
|
|
876
|
+
assert.ok(!diagnosticCodes(omitted).includes('candidate-artifacts-invalid'),
|
|
877
|
+
'the new input-boundary diagnostic does not appear for non-baseline obligations');
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
checkCase('335 P3 non-baseline callers keep the original artifactMap fallback for object-like values', () => {
|
|
881
|
+
const ctx = prepare('docs-only');
|
|
882
|
+
const records = {
|
|
883
|
+
executionRecords: [execRecord(ctx, 'edit-run', 'doc-change:doc-lint', {
|
|
884
|
+
paths: [GUIDE], candidatePaths: [GUIDE]
|
|
885
|
+
})]
|
|
886
|
+
};
|
|
887
|
+
const empty = checkArtifacts(ctx, {}, records);
|
|
888
|
+
const failures = [];
|
|
889
|
+
for (const [label, value] of INVALID_ARTIFACTS) {
|
|
890
|
+
const output = checkArtifacts(ctx, value, records);
|
|
891
|
+
try {
|
|
892
|
+
assert.deepStrictEqual(output, empty, `${label}: output differs from explicit {}`);
|
|
893
|
+
} catch (error) {
|
|
894
|
+
failures.push(error.message);
|
|
895
|
+
}
|
|
896
|
+
if (diagnosticCodes(output).includes('candidate-artifacts-invalid')) {
|
|
897
|
+
failures.push(`${label}: candidate-artifacts-invalid leaked into a non-baseline obligation`);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
assert.deepStrictEqual(failures, [],
|
|
901
|
+
`non-baseline fallback must stay unchanged: ${failures.join('; ')}`);
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
checkCase('335 P3 inventory guard is limited to applicable obligations with baseline paths', () => {
|
|
905
|
+
const ctx = prepare('deleted-adr');
|
|
906
|
+
const lint = obligationOf(ctx, 'doc-change:doc-lint');
|
|
907
|
+
const obligations = [
|
|
908
|
+
['N/A', { ...lint, applicability: 'N/A' }],
|
|
909
|
+
['unknown', { ...lint, applicability: 'unknown' }],
|
|
910
|
+
['empty baselinePaths', { ...lint, scope: { ...lint.scope, baselinePaths: [] } }]
|
|
911
|
+
];
|
|
912
|
+
for (const [label, obligation] of obligations) {
|
|
913
|
+
const output = checkArtifacts(ctx, new Map(), { obligations: [obligation] });
|
|
914
|
+
assert.ok(!diagnosticCodes(output).includes('candidate-artifacts-invalid'),
|
|
915
|
+
`${label}: the inventory guard must not apply (${diagnosticCodes(output)})`);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
// A Proxy can pass every value check yet abort the whole checker when its prototype trap
|
|
920
|
+
// throws. The prototype read must be confined to applicable baseline-only obligations, and a
|
|
921
|
+
// throwing trap must classify the inventory as unknown instead of escaping as an exception.
|
|
922
|
+
function throwingPrototypeInventory() {
|
|
923
|
+
return new Proxy({}, {
|
|
924
|
+
getPrototypeOf() { throw new TypeError('throwing prototype trap'); }
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
checkCase('335 P3 a throwing prototype trap cannot abort non-baseline or inapplicable obligations', () => {
|
|
929
|
+
const ctx = prepare('docs-only');
|
|
930
|
+
const records = {
|
|
931
|
+
executionRecords: [execRecord(ctx, 'edit-run', 'doc-change:doc-lint', {
|
|
932
|
+
paths: [GUIDE], candidatePaths: [GUIDE]
|
|
933
|
+
})]
|
|
934
|
+
};
|
|
935
|
+
const empty = checkArtifacts(ctx, {}, records);
|
|
936
|
+
let output;
|
|
937
|
+
try {
|
|
938
|
+
output = checkArtifacts(ctx, throwingPrototypeInventory(), records);
|
|
939
|
+
} catch (error) {
|
|
940
|
+
assert.fail(`a non-baseline obligation must not read the candidate prototype: ${error.message}`);
|
|
941
|
+
}
|
|
942
|
+
assert.deepStrictEqual(output, empty,
|
|
943
|
+
'a non-baseline caller sees the same result as an explicit empty map');
|
|
944
|
+
assert.ok(!diagnosticCodes(output).includes('candidate-artifacts-invalid'),
|
|
945
|
+
'the new input-boundary diagnostic does not appear for non-baseline obligations');
|
|
946
|
+
|
|
947
|
+
const deleted = prepare('deleted-adr');
|
|
948
|
+
const lint = obligationOf(deleted, 'doc-change:doc-lint');
|
|
949
|
+
const cases = [
|
|
950
|
+
['N/A', { ...lint, applicability: 'N/A' }],
|
|
951
|
+
['unknown', { ...lint, applicability: 'unknown' }],
|
|
952
|
+
['empty baselinePaths', { ...lint, scope: { ...lint.scope, baselinePaths: [] } }],
|
|
953
|
+
['no baselinePaths annotation', { ...lint, scope: { paths: lint.scope.paths } }]
|
|
954
|
+
];
|
|
955
|
+
for (const [label, obligation] of cases) {
|
|
956
|
+
let out;
|
|
957
|
+
try {
|
|
958
|
+
out = checkArtifacts(deleted, throwingPrototypeInventory(), { obligations: [obligation] });
|
|
959
|
+
} catch (error) {
|
|
960
|
+
assert.fail(`${label}: an inapplicable obligation must not read the candidate prototype: ${error.message}`);
|
|
961
|
+
}
|
|
962
|
+
assert.ok(!diagnosticCodes(out).includes('candidate-artifacts-invalid'),
|
|
963
|
+
`${label}: the inventory guard must not apply (${diagnosticCodes(out)})`);
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
checkCase('335 P3 a throwing prototype trap fails baseline-only evidence closed', () => {
|
|
968
|
+
const failures = [];
|
|
969
|
+
for (const scenario of ['deleted-adr', 'rename']) {
|
|
970
|
+
const ctx = prepare(scenario);
|
|
971
|
+
const isRename = scenario === 'rename';
|
|
972
|
+
const paths = isRename ? [RENAME_FROM, RENAME_TO] : [DELETED_ADR];
|
|
973
|
+
const candidatePaths = isRename ? [RENAME_TO] : [];
|
|
974
|
+
const baselinePaths = isRename ? [RENAME_FROM] : [DELETED_ADR];
|
|
975
|
+
const record = isRename
|
|
976
|
+
? execRecord(ctx, 'move-run', 'doc-change:doc-lint', { paths, candidatePaths, baselinePaths })
|
|
977
|
+
: execRecord(ctx, 'tomb-run', 'doc-change:doc-lint', { paths, candidatePaths, baselinePaths });
|
|
978
|
+
const outputs = [];
|
|
979
|
+
for (const target of [modules, packageModules]) {
|
|
980
|
+
let output;
|
|
981
|
+
try {
|
|
982
|
+
output = checkArtifacts(ctx, throwingPrototypeInventory(), { executionRecords: [record] }, target);
|
|
983
|
+
} catch (error) {
|
|
984
|
+
failures.push(`${scenario}: prototype trap escaped: ${error.message}`);
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
if (resultOf(output, 'doc-change:doc-lint').outcome !== 'none') {
|
|
988
|
+
failures.push(`${scenario}: outcome=${resultOf(output, 'doc-change:doc-lint').outcome}`);
|
|
989
|
+
}
|
|
990
|
+
if (!diagnosticCodes(output).includes('candidate-artifacts-invalid')) {
|
|
991
|
+
failures.push(`${scenario}: diagnostics=${diagnosticCodes(output)}`);
|
|
992
|
+
}
|
|
993
|
+
outputs.push(output);
|
|
994
|
+
}
|
|
995
|
+
if (outputs.length === 2) {
|
|
996
|
+
assert.deepStrictEqual(outputs[1].results, outputs[0].results,
|
|
997
|
+
`${scenario}: packaged results match the canonical module`);
|
|
998
|
+
assert.deepStrictEqual(outputs[1].diagnostics, outputs[0].diagnostics,
|
|
999
|
+
`${scenario}: packaged diagnostics match the canonical module`);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
assert.deepStrictEqual(failures, [],
|
|
1003
|
+
`a throwing prototype trap must fail closed in both modules: ${failures.join('; ')}`);
|
|
1004
|
+
|
|
1005
|
+
const ctx = prepare('deleted-adr');
|
|
1006
|
+
const lint = obligationOf(ctx, 'doc-change:doc-lint');
|
|
1007
|
+
const twin = { ...lint, id: 'doc-change:doc-lint-twin' };
|
|
1008
|
+
let reads = 0;
|
|
1009
|
+
const inventory = new Proxy({}, {
|
|
1010
|
+
getPrototypeOf() { reads += 1; throw new TypeError('throwing prototype trap'); }
|
|
1011
|
+
});
|
|
1012
|
+
checkArtifacts(ctx, inventory, { obligations: [lint, twin] });
|
|
1013
|
+
assert.strictEqual(reads, 1,
|
|
1014
|
+
'the inventory classification is resolved at most once for all obligations');
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
checkCase('335 P3 canonical and packaged modules agree on absent/malformed candidate inventory', () => {
|
|
1018
|
+
for (const [label, value] of INVALID_ARTIFACTS) {
|
|
1019
|
+
const sourceCtx = prepare('deleted-adr', modules);
|
|
1020
|
+
const packageCtx = prepare('deleted-adr', packageModules);
|
|
1021
|
+
const sourceOut = checkArtifacts(sourceCtx, value, {
|
|
1022
|
+
executionRecords: [execRecord(sourceCtx, 'tomb-run', 'doc-change:doc-lint', {
|
|
1023
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR]
|
|
1024
|
+
})]
|
|
1025
|
+
}, modules);
|
|
1026
|
+
const packageOut = checkArtifacts(packageCtx, value, {
|
|
1027
|
+
executionRecords: [execRecord(packageCtx, 'tomb-run', 'doc-change:doc-lint', {
|
|
1028
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR]
|
|
1029
|
+
})]
|
|
1030
|
+
}, packageModules);
|
|
1031
|
+
assert.deepStrictEqual(packageOut.results, sourceOut.results,
|
|
1032
|
+
`${label}: packaged results match the canonical module`);
|
|
1033
|
+
assert.deepStrictEqual(packageOut.diagnostics, sourceOut.diagnostics,
|
|
1034
|
+
`${label}: packaged diagnostics match the canonical module`);
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
// ---------------------------------------------------------------------------
|
|
1039
|
+
// P2 trust boundary: per-path evidence must be carried explicitly. Inherited
|
|
1040
|
+
// properties in baseline.artifacts, record.baselineArtifacts, the rename
|
|
1041
|
+
// destination digest, or the candidate destination inventory entry are not
|
|
1042
|
+
// own-key evidence and cannot satisfy a baseline-bound obligation.
|
|
1043
|
+
// ---------------------------------------------------------------------------
|
|
1044
|
+
|
|
1045
|
+
checkCase('335 P2 inherited baseline.artifacts entry is not trusted', () => {
|
|
1046
|
+
const ctx = prepare('deleted-adr');
|
|
1047
|
+
const inheritedBaseline = Object.create({ [DELETED_ADR]: ctx.baseline.artifacts[DELETED_ADR] });
|
|
1048
|
+
const output = check(ctx, {
|
|
1049
|
+
baseline: { digest: ctx.baseline.digest, artifacts: inheritedBaseline },
|
|
1050
|
+
executionRecords: [execRecord(ctx, 'tomb-run', 'doc-change:doc-lint', {
|
|
1051
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR]
|
|
1052
|
+
})]
|
|
1053
|
+
});
|
|
1054
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
1055
|
+
'an inherited-only baseline digest cannot satisfy a deletion');
|
|
1056
|
+
assert.ok(diagnosticCodes(output).includes('baseline-artifact-missing'),
|
|
1057
|
+
`the absent own baseline digest is diagnosed: ${diagnosticCodes(output)}`);
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
checkCase('335 P2 inherited record baselineArtifacts entry does not bind', () => {
|
|
1061
|
+
const ctx = prepare('deleted-adr');
|
|
1062
|
+
const inheritedRecordBinding = Object.create({ [DELETED_ADR]: ctx.baseline.artifacts[DELETED_ADR] });
|
|
1063
|
+
const output = check(ctx, {
|
|
1064
|
+
executionRecords: [execRecord(ctx, 'tomb-run', 'doc-change:doc-lint', {
|
|
1065
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR],
|
|
1066
|
+
baselineArtifacts: inheritedRecordBinding
|
|
1067
|
+
})]
|
|
1068
|
+
});
|
|
1069
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
1070
|
+
'an inherited-only record baseline digest cannot bind the path');
|
|
1071
|
+
assert.ok(diagnosticCodes(output).includes('baseline-binding-missing'),
|
|
1072
|
+
`the non-own record binding is diagnosed: ${diagnosticCodes(output)}`);
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
checkCase('335 P2 inherited rename destination digest does not bind the pair', () => {
|
|
1076
|
+
const ctx = prepare('rename');
|
|
1077
|
+
const inheritedDestination = Object.create({ [RENAME_TO]: ctx.artifacts[RENAME_TO].digest });
|
|
1078
|
+
const output = check(ctx, {
|
|
1079
|
+
executionRecords: [execRecord(ctx, 'move-run', 'doc-change:doc-lint', {
|
|
1080
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM],
|
|
1081
|
+
artifactDigests: inheritedDestination
|
|
1082
|
+
})]
|
|
1083
|
+
});
|
|
1084
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
1085
|
+
'an inherited-only destination digest cannot bind the rename pair');
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
checkCase('335 P2 inherited candidate destination entry is not an own inventory entry', () => {
|
|
1089
|
+
const ctx = prepare('rename');
|
|
1090
|
+
const inheritedCandidate = Object.create({ [RENAME_TO]: ctx.artifacts[RENAME_TO] });
|
|
1091
|
+
const output = checkArtifacts(ctx, inheritedCandidate, {
|
|
1092
|
+
executionRecords: [execRecord(ctx, 'move-run', 'doc-change:doc-lint', {
|
|
1093
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM]
|
|
1094
|
+
})]
|
|
1095
|
+
});
|
|
1096
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
1097
|
+
'an inherited-only candidate destination cannot satisfy the rename');
|
|
1098
|
+
assert.ok(diagnosticCodes(output).includes('baseline-path-invalid'),
|
|
1099
|
+
`the non-own candidate destination is diagnosed as a malformed annotation: ${diagnosticCodes(output)}`);
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
checkCase('335 P2 null-prototype baseline and record maps remain valid', () => {
|
|
1103
|
+
const ctx = prepare('deleted-adr');
|
|
1104
|
+
const baselineArtifacts = Object.assign(Object.create(null), baselineMapFor(ctx, [DELETED_ADR]));
|
|
1105
|
+
const recordBinding = Object.assign(Object.create(null), baselineMapFor(ctx, [DELETED_ADR]));
|
|
1106
|
+
const output = check(ctx, {
|
|
1107
|
+
baseline: { digest: ctx.baseline.digest, artifacts: baselineArtifacts },
|
|
1108
|
+
executionRecords: [execRecord(ctx, 'tomb-run', 'doc-change:doc-lint', {
|
|
1109
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR],
|
|
1110
|
+
baselineArtifacts: recordBinding
|
|
1111
|
+
})]
|
|
1112
|
+
});
|
|
1113
|
+
assert.strictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
1114
|
+
'own entries in null-prototype dictionaries remain valid baseline evidence');
|
|
1115
|
+
assert.ok(!diagnosticCodes(output).includes('baseline-artifact-missing'));
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
checkCase('335 P2 canonical and packaged modules agree on inherited-only evidence', () => {
|
|
1119
|
+
for (const target of [modules, packageModules]) {
|
|
1120
|
+
const ctx = prepare('deleted-adr', target);
|
|
1121
|
+
const inheritedBaseline = Object.create({ [DELETED_ADR]: ctx.baseline.artifacts[DELETED_ADR] });
|
|
1122
|
+
const inheritedRecord = Object.create({ [DELETED_ADR]: ctx.baseline.artifacts[DELETED_ADR] });
|
|
1123
|
+
const baselineOutput = target.checkDocObligations({
|
|
1124
|
+
obligations: ctx.obligations,
|
|
1125
|
+
artifacts: ctx.artifacts,
|
|
1126
|
+
baseline: { digest: ctx.baseline.digest, artifacts: inheritedBaseline },
|
|
1127
|
+
executionRecords: [execRecord(ctx, 'tomb-run', 'doc-change:doc-lint', {
|
|
1128
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR]
|
|
1129
|
+
})]
|
|
1130
|
+
});
|
|
1131
|
+
const recordOutput = target.checkDocObligations({
|
|
1132
|
+
obligations: ctx.obligations,
|
|
1133
|
+
artifacts: ctx.artifacts,
|
|
1134
|
+
baseline: ctx.baseline,
|
|
1135
|
+
executionRecords: [execRecord(ctx, 'tomb-run', 'doc-change:doc-lint', {
|
|
1136
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR],
|
|
1137
|
+
baselineArtifacts: inheritedRecord
|
|
1138
|
+
})]
|
|
1139
|
+
});
|
|
1140
|
+
for (const [label, output] of [['baseline', baselineOutput], ['record', recordOutput]]) {
|
|
1141
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
1142
|
+
`${label}: inherited-only evidence cannot pass in ${target === modules ? 'canonical' : 'packaged'} modules`);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
// ---------------------------------------------------------------------------
|
|
1148
|
+
// P3 declarations: metadata presence is own-key presence, and a rename's
|
|
1149
|
+
// destination must be a member of the same obligation scope.
|
|
1150
|
+
// ---------------------------------------------------------------------------
|
|
1151
|
+
|
|
1152
|
+
checkCase('335 P3 explicitly present undefined baselinePaths fails closed', () => {
|
|
1153
|
+
const ctx = prepare('docs-only');
|
|
1154
|
+
const lint = obligationOf(ctx, 'doc-change:doc-lint');
|
|
1155
|
+
const explicitUndefined = { ...lint, scope: { ...lint.scope, baselinePaths: undefined } };
|
|
1156
|
+
const output = check(ctx, {
|
|
1157
|
+
obligations: ctx.obligations.map(item => (item.id === lint.id ? explicitUndefined : item)),
|
|
1158
|
+
executionRecords: [execRecord(ctx, 'edit-run', 'doc-change:doc-lint', {
|
|
1159
|
+
paths: [GUIDE], candidatePaths: [GUIDE]
|
|
1160
|
+
})]
|
|
1161
|
+
});
|
|
1162
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
1163
|
+
'an explicitly present undefined baselinePaths annotation cannot pass');
|
|
1164
|
+
assert.ok(diagnosticCodes(output).includes('baseline-path-invalid'),
|
|
1165
|
+
`the present malformed annotation is diagnosed: ${diagnosticCodes(output)}`);
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
checkCase('335 P3 rename destination must belong to scope.paths', () => {
|
|
1169
|
+
const ctx = prepare('rename');
|
|
1170
|
+
const lint = obligationOf(ctx, 'doc-change:doc-lint');
|
|
1171
|
+
const outsideDestination = 'docs/reference/not-in-scope.md';
|
|
1172
|
+
// The out-of-scope destination has a real candidate artifact and a valid pair record, so an
|
|
1173
|
+
// implementation that only checks the destination digest would still pass it. Only the
|
|
1174
|
+
// scope-membership invariant makes this annotation fail closed.
|
|
1175
|
+
const artifacts = {
|
|
1176
|
+
...ctx.artifacts,
|
|
1177
|
+
[outsideDestination]: { digest: sha256('outside destination candidate') }
|
|
1178
|
+
};
|
|
1179
|
+
const mutated = {
|
|
1180
|
+
...lint,
|
|
1181
|
+
scope: {
|
|
1182
|
+
...lint.scope,
|
|
1183
|
+
baselinePaths: [{ path: RENAME_FROM, reason: 'rename-from', renameTo: outsideDestination }]
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
const output = check(ctx, {
|
|
1187
|
+
obligations: ctx.obligations.map(item => (item.id === lint.id ? mutated : item)),
|
|
1188
|
+
artifacts,
|
|
1189
|
+
executionRecords: [execRecord(ctx, 'move-run', 'doc-change:doc-lint', {
|
|
1190
|
+
paths: [RENAME_FROM, RENAME_TO, outsideDestination],
|
|
1191
|
+
candidatePaths: [],
|
|
1192
|
+
baselinePaths: [RENAME_FROM],
|
|
1193
|
+
artifactDigests: {
|
|
1194
|
+
[RENAME_TO]: artifacts[RENAME_TO].digest,
|
|
1195
|
+
[outsideDestination]: artifacts[outsideDestination].digest
|
|
1196
|
+
}
|
|
1197
|
+
})]
|
|
1198
|
+
});
|
|
1199
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
1200
|
+
'a rename destination outside scope.paths cannot pass');
|
|
1201
|
+
assert.ok(diagnosticCodes(output).includes('baseline-path-invalid'),
|
|
1202
|
+
`the out-of-scope destination is diagnosed: ${diagnosticCodes(output)}`);
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
// ---------------------------------------------------------------------------
|
|
1206
|
+
// P2 destination evidence: the rename destination artifact must carry its own
|
|
1207
|
+
// digest. An inherited nested digest is not explicit candidate evidence, while
|
|
1208
|
+
// own-digest and null-prototype entries remain valid.
|
|
1209
|
+
// ---------------------------------------------------------------------------
|
|
1210
|
+
|
|
1211
|
+
checkCase('335 P2 inherited destination artifact digest cannot bind the rename', () => {
|
|
1212
|
+
const ctx = prepare('rename');
|
|
1213
|
+
const artifacts = {
|
|
1214
|
+
...ctx.artifacts,
|
|
1215
|
+
[RENAME_TO]: Object.create({ digest: ctx.artifacts[RENAME_TO].digest })
|
|
1216
|
+
};
|
|
1217
|
+
const output = check(ctx, {
|
|
1218
|
+
artifacts,
|
|
1219
|
+
executionRecords: [execRecord(ctx, 'move-run', 'doc-change:doc-lint', {
|
|
1220
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM]
|
|
1221
|
+
})]
|
|
1222
|
+
});
|
|
1223
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
1224
|
+
'an inherited destination digest cannot satisfy the rename source');
|
|
1225
|
+
assert.ok(diagnosticCodes(output).includes('baseline-path-invalid'),
|
|
1226
|
+
`the destination without an own digest is diagnosed: ${diagnosticCodes(output)}`);
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
checkCase('335 P2 own and null-prototype destination digests remain valid', () => {
|
|
1230
|
+
const entryFactories = [
|
|
1231
|
+
['own digest', digest => ({ digest })],
|
|
1232
|
+
['null-prototype entry', digest => Object.assign(Object.create(null), { digest })]
|
|
1233
|
+
];
|
|
1234
|
+
for (const [label, makeEntry] of entryFactories) {
|
|
1235
|
+
const ctx = prepare('rename');
|
|
1236
|
+
const artifacts = {
|
|
1237
|
+
...ctx.artifacts,
|
|
1238
|
+
[RENAME_TO]: makeEntry(ctx.artifacts[RENAME_TO].digest)
|
|
1239
|
+
};
|
|
1240
|
+
const output = check(ctx, {
|
|
1241
|
+
artifacts,
|
|
1242
|
+
executionRecords: [execRecord(ctx, 'move-run', 'doc-change:doc-lint', {
|
|
1243
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM]
|
|
1244
|
+
})]
|
|
1245
|
+
});
|
|
1246
|
+
assert.strictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
1247
|
+
`${label}: an explicit destination digest remains valid`);
|
|
1248
|
+
}
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
// ---------------------------------------------------------------------------
|
|
1252
|
+
// P3 declaration presence: a deleted entry must not carry renameTo at all,
|
|
1253
|
+
// including an explicitly present undefined value.
|
|
1254
|
+
// ---------------------------------------------------------------------------
|
|
1255
|
+
|
|
1256
|
+
checkCase('335 P3 deleted entry with own renameTo undefined fails closed', () => {
|
|
1257
|
+
const ctx = prepare('deleted-adr');
|
|
1258
|
+
const lint = obligationOf(ctx, 'doc-change:doc-lint');
|
|
1259
|
+
const mutated = {
|
|
1260
|
+
...lint,
|
|
1261
|
+
scope: {
|
|
1262
|
+
...lint.scope,
|
|
1263
|
+
baselinePaths: [{ path: DELETED_ADR, reason: 'deleted', renameTo: undefined }]
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
const output = check(ctx, {
|
|
1267
|
+
obligations: ctx.obligations.map(item => (item.id === lint.id ? mutated : item)),
|
|
1268
|
+
executionRecords: [execRecord(ctx, 'tomb-run', 'doc-change:doc-lint', {
|
|
1269
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR]
|
|
1270
|
+
})]
|
|
1271
|
+
});
|
|
1272
|
+
assert.notStrictEqual(resultOf(output, 'doc-change:doc-lint').outcome, 'pass',
|
|
1273
|
+
'a present renameTo on a deletion cannot pass, even when undefined');
|
|
1274
|
+
assert.ok(diagnosticCodes(output).includes('baseline-path-invalid'),
|
|
1275
|
+
`the present renameTo is diagnosed: ${diagnosticCodes(output)}`);
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
// ---------------------------------------------------------------------------
|
|
1279
|
+
// P3 malformed-input safety: descriptor and digest accessor traps in the
|
|
1280
|
+
// candidate inventory must not abort the checker. Applicable baseline-only
|
|
1281
|
+
// obligations fail closed with candidate-artifacts-invalid; N/A and unknown
|
|
1282
|
+
// obligations keep their non-applicable result.
|
|
1283
|
+
// ---------------------------------------------------------------------------
|
|
1284
|
+
|
|
1285
|
+
function throwingDescriptorInventory() {
|
|
1286
|
+
return new Proxy({}, {
|
|
1287
|
+
getPrototypeOf() { return Object.prototype; },
|
|
1288
|
+
getOwnPropertyDescriptor() { throw new Error('descriptor trap'); }
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function throwingDigestArtifact() {
|
|
1293
|
+
const entry = {};
|
|
1294
|
+
Object.defineProperty(entry, 'digest', {
|
|
1295
|
+
enumerable: true,
|
|
1296
|
+
get() { throw new Error('digest trap'); }
|
|
1297
|
+
});
|
|
1298
|
+
return entry;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
checkCase('335 P3 a throwing property descriptor trap fails baseline obligations closed', () => {
|
|
1302
|
+
const failures = [];
|
|
1303
|
+
for (const scenario of ['deleted-adr', 'rename']) {
|
|
1304
|
+
const isRename = scenario === 'rename';
|
|
1305
|
+
for (const target of [modules, packageModules]) {
|
|
1306
|
+
const ctx = prepare(scenario, target);
|
|
1307
|
+
const paths = isRename ? [RENAME_FROM, RENAME_TO] : [DELETED_ADR];
|
|
1308
|
+
const candidatePaths = isRename ? [RENAME_TO] : [];
|
|
1309
|
+
const baselinePaths = isRename ? [RENAME_FROM] : [DELETED_ADR];
|
|
1310
|
+
const record = execRecord(ctx, isRename ? 'move-run' : 'tomb-run', 'doc-change:doc-lint', {
|
|
1311
|
+
paths, candidatePaths, baselinePaths
|
|
1312
|
+
});
|
|
1313
|
+
let output;
|
|
1314
|
+
try {
|
|
1315
|
+
output = checkArtifacts(ctx, throwingDescriptorInventory(), { executionRecords: [record] }, target);
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
failures.push(`${scenario}: descriptor trap escaped: ${error.message}`);
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
const result = resultOf(output, 'doc-change:doc-lint');
|
|
1321
|
+
if (result.outcome !== 'none') failures.push(`${scenario}: outcome=${result.outcome}`);
|
|
1322
|
+
if (!diagnosticCodes(output).includes('candidate-artifacts-invalid')) {
|
|
1323
|
+
failures.push(`${scenario}: diagnostics=${diagnosticCodes(output)}`);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
assert.deepStrictEqual(failures, [],
|
|
1328
|
+
`descriptor traps must fail closed in both modules: ${failures.join('; ')}`);
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
checkCase('335 P3 a throwing destination digest accessor fails the rename closed', () => {
|
|
1332
|
+
const failures = [];
|
|
1333
|
+
for (const target of [modules, packageModules]) {
|
|
1334
|
+
const ctx = prepare('rename', target);
|
|
1335
|
+
const artifacts = {
|
|
1336
|
+
...ctx.artifacts,
|
|
1337
|
+
[RENAME_TO]: throwingDigestArtifact()
|
|
1338
|
+
};
|
|
1339
|
+
const record = execRecord(ctx, 'move-run', 'doc-change:doc-lint', {
|
|
1340
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM]
|
|
1341
|
+
});
|
|
1342
|
+
let output;
|
|
1343
|
+
try {
|
|
1344
|
+
output = check(ctx, { artifacts, executionRecords: [record] }, target);
|
|
1345
|
+
} catch (error) {
|
|
1346
|
+
failures.push(`digest accessor trap escaped: ${error.message}`);
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
const result = resultOf(output, 'doc-change:doc-lint');
|
|
1350
|
+
if (result.outcome !== 'none') failures.push(`outcome=${result.outcome}`);
|
|
1351
|
+
if (!diagnosticCodes(output).includes('candidate-artifacts-invalid')) {
|
|
1352
|
+
failures.push(`diagnostics=${diagnosticCodes(output)}`);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
assert.deepStrictEqual(failures, [],
|
|
1356
|
+
`a throwing destination digest accessor must fail closed in both modules: ${failures.join('; ')}`);
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
checkCase('335 P3 descriptor and digest traps cannot abort N/A or unknown obligations', () => {
|
|
1360
|
+
const ctx = prepare('deleted-adr');
|
|
1361
|
+
const rename = prepare('rename');
|
|
1362
|
+
const lint = obligationOf(ctx, 'doc-change:doc-lint');
|
|
1363
|
+
const renameLint = obligationOf(rename, 'doc-change:doc-lint');
|
|
1364
|
+
const scenarios = [
|
|
1365
|
+
['deleted descriptor trap', ctx, lint, throwingDescriptorInventory()],
|
|
1366
|
+
['deleted digest trap', ctx, lint, { [DELETED_ADR]: throwingDigestArtifact() }],
|
|
1367
|
+
['rename descriptor trap', rename, renameLint, throwingDescriptorInventory()],
|
|
1368
|
+
['rename digest trap', rename, renameLint, {
|
|
1369
|
+
...rename.artifacts,
|
|
1370
|
+
[RENAME_TO]: throwingDigestArtifact()
|
|
1371
|
+
}]
|
|
1372
|
+
];
|
|
1373
|
+
for (const [label, scenarioCtx, obligation, artifacts] of scenarios) {
|
|
1374
|
+
for (const applicability of ['N/A', 'unknown']) {
|
|
1375
|
+
let output;
|
|
1376
|
+
try {
|
|
1377
|
+
output = checkArtifacts(scenarioCtx, artifacts, {
|
|
1378
|
+
obligations: [{ ...obligation, applicability }]
|
|
1379
|
+
});
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
assert.fail(`${label}/${applicability}: malformed inventory escaped: ${error.message}`);
|
|
1382
|
+
}
|
|
1383
|
+
const result = resultOf(output, obligation.id);
|
|
1384
|
+
assert.strictEqual(result.outcome, 'none',
|
|
1385
|
+
`${label}/${applicability}: non-applicable obligations stay none`);
|
|
1386
|
+
assert.ok(!diagnosticCodes(output).includes('candidate-artifacts-invalid'),
|
|
1387
|
+
`${label}/${applicability}: the inventory diagnostic is limited to applicable obligations`);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
// ---------------------------------------------------------------------------
|
|
1393
|
+
// P3 mixed scope: baseline-only and ordinary candidate paths in the same
|
|
1394
|
+
// applicable obligation. A throwing accessor on the ordinary path must fail
|
|
1395
|
+
// the inventory closed before the legacy candidate lane binds evidence.
|
|
1396
|
+
// ---------------------------------------------------------------------------
|
|
1397
|
+
|
|
1398
|
+
checkCase('335 P3 mixed delete+edit scope fails closed on a candidate digest accessor', () => {
|
|
1399
|
+
const failures = [];
|
|
1400
|
+
for (const target of [modules, packageModules]) {
|
|
1401
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
1402
|
+
const ctx = prepare('deleted-adr', target);
|
|
1403
|
+
const lint = obligationOf(ctx, 'doc-change:doc-lint');
|
|
1404
|
+
const mixed = {
|
|
1405
|
+
...lint,
|
|
1406
|
+
scope: {
|
|
1407
|
+
...lint.scope,
|
|
1408
|
+
paths: [DELETED_ADR, GUIDE],
|
|
1409
|
+
baselinePaths: [{ path: DELETED_ADR, reason: 'deleted' }]
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
const obligations = ctx.obligations.map(item => (item.id === lint.id ? mixed : item));
|
|
1413
|
+
const editedDigest = sha256('mixed edited candidate');
|
|
1414
|
+
const record = execRecord(ctx, 'mixed-run', 'doc-change:doc-lint', {
|
|
1415
|
+
paths: [DELETED_ADR, GUIDE],
|
|
1416
|
+
candidatePaths: [],
|
|
1417
|
+
baselinePaths: [DELETED_ADR],
|
|
1418
|
+
artifactDigests: { [GUIDE]: editedDigest }
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
const positiveArtifacts = { ...ctx.artifacts, [GUIDE]: { digest: editedDigest } };
|
|
1422
|
+
let positive;
|
|
1423
|
+
try {
|
|
1424
|
+
positive = check(ctx, { obligations, artifacts: positiveArtifacts, executionRecords: [record] }, target);
|
|
1425
|
+
} catch (error) {
|
|
1426
|
+
failures.push(`${label}: positive control threw: ${error.message}`);
|
|
1427
|
+
}
|
|
1428
|
+
if (positive && resultOf(positive, 'doc-change:doc-lint').outcome !== 'pass') {
|
|
1429
|
+
failures.push(`${label}: positive control outcome=${resultOf(positive, 'doc-change:doc-lint').outcome}`);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
const poisonedArtifacts = { ...ctx.artifacts, [GUIDE]: throwingDigestArtifact() };
|
|
1433
|
+
let output;
|
|
1434
|
+
try {
|
|
1435
|
+
output = check(ctx, { obligations, artifacts: poisonedArtifacts, executionRecords: [record] }, target);
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
failures.push(`${label}: candidate accessor escaped: ${error.message}`);
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
const result = resultOf(output, 'doc-change:doc-lint');
|
|
1441
|
+
if (result.outcome !== 'none') failures.push(`${label}: outcome=${result.outcome}`);
|
|
1442
|
+
if (!diagnosticCodes(output).includes('candidate-artifacts-invalid')) {
|
|
1443
|
+
failures.push(`${label}: diagnostics=${diagnosticCodes(output)}`);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
assert.deepStrictEqual(failures, [],
|
|
1447
|
+
`mixed scopes must fail closed in both modules: ${failures.join('; ')}`);
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1450
|
+
// ---------------------------------------------------------------------------
|
|
1451
|
+
// P3 cache consistency: for an applicable baseline-annotated obligation every
|
|
1452
|
+
// ordinary scoped candidate digest is captured once, before binding. The audit
|
|
1453
|
+
// identity and the stale-digest diagnostics must consult that same captured
|
|
1454
|
+
// snapshot, not re-read a nested accessor whose later read throws or changes.
|
|
1455
|
+
// ---------------------------------------------------------------------------
|
|
1456
|
+
|
|
1457
|
+
// A digest accessor that is valid for the first `validReads` property reads and
|
|
1458
|
+
// then throws or returns `laterValue`. `artifact()` reads `entry.digest` up to
|
|
1459
|
+
// three times per call, so the pre-binding capture consumes three valid reads.
|
|
1460
|
+
function statefulDigestEntry(digest, { validReads = 3, later = 'throw', laterValue } = {}) {
|
|
1461
|
+
const entry = {};
|
|
1462
|
+
let reads = 0;
|
|
1463
|
+
Object.defineProperty(entry, 'digest', {
|
|
1464
|
+
enumerable: true,
|
|
1465
|
+
get() {
|
|
1466
|
+
reads += 1;
|
|
1467
|
+
if (reads <= validReads) return digest;
|
|
1468
|
+
if (later === 'throw') throw new Error('stateful digest accessor');
|
|
1469
|
+
return laterValue;
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
return entry;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// The deleted-adr fixture with one extra ordinary candidate path (GUIDE) in the
|
|
1476
|
+
// doc-lint scope, so the same applicable obligation declares baseline-only and
|
|
1477
|
+
// ordinary candidate paths.
|
|
1478
|
+
function mixedCacheContext(target) {
|
|
1479
|
+
const ctx = prepare('deleted-adr', target);
|
|
1480
|
+
const lint = obligationOf(ctx, 'doc-change:doc-lint');
|
|
1481
|
+
const mixed = {
|
|
1482
|
+
...lint,
|
|
1483
|
+
scope: {
|
|
1484
|
+
...lint.scope,
|
|
1485
|
+
paths: [DELETED_ADR, GUIDE],
|
|
1486
|
+
baselinePaths: [{ path: DELETED_ADR, reason: 'deleted' }]
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
return { ctx, obligations: ctx.obligations.map(item => (item.id === lint.id ? mixed : item)) };
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
function mixedCacheRecord(ctx, id, guideDigest) {
|
|
1493
|
+
return execRecord(ctx, id, 'doc-change:doc-lint', {
|
|
1494
|
+
paths: [DELETED_ADR, GUIDE],
|
|
1495
|
+
candidatePaths: [],
|
|
1496
|
+
baselinePaths: [DELETED_ADR],
|
|
1497
|
+
artifactDigests: { [GUIDE]: guideDigest }
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
checkCase('335 P3 captured candidate snapshot drives the audit identity and stale diagnostics', () => {
|
|
1502
|
+
const failures = [];
|
|
1503
|
+
const capturedDigest = sha256('captured mixed candidate snapshot');
|
|
1504
|
+
const staleDigest = sha256('stale previous mixed candidate');
|
|
1505
|
+
const changedDigest = sha256('later mixed candidate read');
|
|
1506
|
+
|
|
1507
|
+
for (const target of [modules, packageModules]) {
|
|
1508
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
1509
|
+
const { ctx, obligations } = mixedCacheContext(target);
|
|
1510
|
+
const valid = mixedCacheRecord(ctx, 'snapshot-run', capturedDigest);
|
|
1511
|
+
|
|
1512
|
+
// Throwing after capture: the record bound to the captured digest must pass,
|
|
1513
|
+
// its audit identity must name that digest, and a stale record must still be
|
|
1514
|
+
// diagnosed as stale instead of silently losing its mismatch.
|
|
1515
|
+
let passing;
|
|
1516
|
+
try {
|
|
1517
|
+
passing = check(ctx, {
|
|
1518
|
+
obligations,
|
|
1519
|
+
artifacts: { ...ctx.artifacts, [GUIDE]: statefulDigestEntry(capturedDigest, { later: 'throw' }) },
|
|
1520
|
+
executionRecords: [valid]
|
|
1521
|
+
}, target);
|
|
1522
|
+
} catch (error) {
|
|
1523
|
+
failures.push(`${label}/throwing: valid check threw: ${error.message}`);
|
|
1524
|
+
}
|
|
1525
|
+
if (passing) {
|
|
1526
|
+
const result = resultOf(passing, 'doc-change:doc-lint');
|
|
1527
|
+
if (result.outcome !== 'pass') failures.push(`${label}/throwing: valid outcome=${result.outcome}`);
|
|
1528
|
+
if (!result.artifactDigest.includes(`${GUIDE}=${capturedDigest}`)) {
|
|
1529
|
+
failures.push(`${label}/throwing: audit identity is not the bound digest: ${result.artifactDigest}`);
|
|
1530
|
+
}
|
|
1531
|
+
if (diagnosticCodes(passing).includes('stale-artifact-digest')) {
|
|
1532
|
+
failures.push(`${label}/throwing: the captured-bound record was reported stale`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
let stale;
|
|
1537
|
+
try {
|
|
1538
|
+
stale = check(ctx, {
|
|
1539
|
+
obligations,
|
|
1540
|
+
artifacts: { ...ctx.artifacts, [GUIDE]: statefulDigestEntry(capturedDigest, { later: 'throw' }) },
|
|
1541
|
+
executionRecords: [mixedCacheRecord(ctx, 'stale-run', staleDigest)]
|
|
1542
|
+
}, target);
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
failures.push(`${label}/throwing: stale check threw: ${error.message}`);
|
|
1545
|
+
}
|
|
1546
|
+
if (stale) {
|
|
1547
|
+
if (resultOf(stale, 'doc-change:doc-lint').outcome === 'pass') {
|
|
1548
|
+
failures.push(`${label}/throwing: a stale record passed`);
|
|
1549
|
+
}
|
|
1550
|
+
if (!diagnosticCodes(stale).includes('stale-artifact-digest')) {
|
|
1551
|
+
failures.push(`${label}/throwing: a stale record was not diagnosed: ${diagnosticCodes(stale)}`);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
// Changing after capture: the captured-bound record must pass and must not be
|
|
1556
|
+
// misreported stale, and the audit identity must keep the captured digest.
|
|
1557
|
+
let changed;
|
|
1558
|
+
try {
|
|
1559
|
+
changed = check(ctx, {
|
|
1560
|
+
obligations,
|
|
1561
|
+
artifacts: {
|
|
1562
|
+
...ctx.artifacts,
|
|
1563
|
+
[GUIDE]: statefulDigestEntry(capturedDigest, { later: 'value', laterValue: changedDigest })
|
|
1564
|
+
},
|
|
1565
|
+
executionRecords: [valid]
|
|
1566
|
+
}, target);
|
|
1567
|
+
} catch (error) {
|
|
1568
|
+
failures.push(`${label}/changing: check threw: ${error.message}`);
|
|
1569
|
+
}
|
|
1570
|
+
if (changed) {
|
|
1571
|
+
const result = resultOf(changed, 'doc-change:doc-lint');
|
|
1572
|
+
if (result.outcome !== 'pass') failures.push(`${label}/changing: outcome=${result.outcome}`);
|
|
1573
|
+
if (!result.artifactDigest.includes(`${GUIDE}=${capturedDigest}`)) {
|
|
1574
|
+
failures.push(`${label}/changing: audit identity drifted to a later read: ${result.artifactDigest}`);
|
|
1575
|
+
}
|
|
1576
|
+
if (diagnosticCodes(changed).includes('stale-artifact-digest')) {
|
|
1577
|
+
failures.push(`${label}/changing: the captured-bound record was misreported stale`);
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
assert.deepStrictEqual(failures, [],
|
|
1583
|
+
`the captured candidate snapshot must stay consistent: ${failures.join('; ')}`);
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
// ---------------------------------------------------------------------------
|
|
1587
|
+
// P3 rename cache consistency: the rename destination must bind against the
|
|
1588
|
+
// same captured candidate digest that the ordinary candidate lane, the audit
|
|
1589
|
+
// identity, and the stale diagnostics use. Metadata validation still requires
|
|
1590
|
+
// the destination to be an own-key inventory entry with its own digest, so the
|
|
1591
|
+
// strict own-key boundary is unchanged; after capture the digest is used
|
|
1592
|
+
// exclusively, never re-read live.
|
|
1593
|
+
// ---------------------------------------------------------------------------
|
|
1594
|
+
|
|
1595
|
+
const RENAME_CAPTURED = sha256('rename captured destination');
|
|
1596
|
+
const RENAME_LATER = sha256('rename later destination');
|
|
1597
|
+
|
|
1598
|
+
function renameCacheContext(target) {
|
|
1599
|
+
const ctx = prepare('rename', target);
|
|
1600
|
+
return { ctx, obligations: [obligationOf(ctx, 'doc-change:doc-lint')] };
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
function renameCacheArtifacts(ctx, entry) {
|
|
1604
|
+
return { ...ctx.artifacts, [RENAME_TO]: entry };
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
function renamePairRecord(ctx, id, destinationDigest) {
|
|
1608
|
+
return execRecord(ctx, id, 'doc-change:doc-lint', {
|
|
1609
|
+
paths: [RENAME_FROM, RENAME_TO],
|
|
1610
|
+
candidatePaths: [],
|
|
1611
|
+
baselinePaths: [RENAME_FROM],
|
|
1612
|
+
artifactDigests: { [RENAME_TO]: destinationDigest }
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
function renameDestinationOnlyRecord(ctx, id, destinationDigest) {
|
|
1617
|
+
return execRecord(ctx, id, 'doc-change:doc-lint', {
|
|
1618
|
+
paths: [RENAME_TO],
|
|
1619
|
+
candidatePaths: [],
|
|
1620
|
+
baselinePaths: [],
|
|
1621
|
+
artifactDigests: { [RENAME_TO]: destinationDigest }
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
checkCase('335 P3 rename destination binds to the captured candidate snapshot', () => {
|
|
1626
|
+
const failures = [];
|
|
1627
|
+
for (const target of [modules, packageModules]) {
|
|
1628
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
1629
|
+
const { ctx, obligations } = renameCacheContext(target);
|
|
1630
|
+
const runWith = (artifacts, records) => target.checkDocObligations({
|
|
1631
|
+
obligations, artifacts, baseline: ctx.baseline, executionRecords: records, reviewRecords: []
|
|
1632
|
+
});
|
|
1633
|
+
// Metadata + capture read the destination accessor six times; every later
|
|
1634
|
+
// read must not be needed because binding uses the captured snapshot.
|
|
1635
|
+
const entry = (later, laterValue) => statefulDigestEntry(RENAME_CAPTURED, { validReads: 6, later, laterValue });
|
|
1636
|
+
|
|
1637
|
+
// 1) later-throw: the valid pair binds on the captured digest and reports it.
|
|
1638
|
+
let out;
|
|
1639
|
+
try {
|
|
1640
|
+
out = runWith(renameCacheArtifacts(ctx, entry('throw')),
|
|
1641
|
+
[renamePairRecord(ctx, 'pair-run', RENAME_CAPTURED)]);
|
|
1642
|
+
} catch (error) { failures.push(`${label}/throw: escaped: ${error.message}`); }
|
|
1643
|
+
if (out) {
|
|
1644
|
+
const result = resultOf(out, 'doc-change:doc-lint');
|
|
1645
|
+
if (result.outcome !== 'pass') failures.push(`${label}/throw: outcome=${result.outcome}`);
|
|
1646
|
+
if (!result.artifactDigest.includes(`${RENAME_TO}=${RENAME_CAPTURED}`)) {
|
|
1647
|
+
failures.push(`${label}/throw: audit identity=${result.artifactDigest}`);
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// 2) later-change: a valid pair bound to the captured digest still passes.
|
|
1652
|
+
out = undefined;
|
|
1653
|
+
try {
|
|
1654
|
+
out = runWith(renameCacheArtifacts(ctx, entry('value', RENAME_LATER)),
|
|
1655
|
+
[renamePairRecord(ctx, 'pair-run', RENAME_CAPTURED)]);
|
|
1656
|
+
} catch (error) { failures.push(`${label}/change: escaped: ${error.message}`); }
|
|
1657
|
+
if (out) {
|
|
1658
|
+
const result = resultOf(out, 'doc-change:doc-lint');
|
|
1659
|
+
if (result.outcome !== 'pass') failures.push(`${label}/change: outcome=${result.outcome}`);
|
|
1660
|
+
if (!result.artifactDigest.includes(`${RENAME_TO}=${RENAME_CAPTURED}`)) {
|
|
1661
|
+
failures.push(`${label}/change: audit identity=${result.artifactDigest}`);
|
|
1662
|
+
}
|
|
1663
|
+
if (diagnosticCodes(out).includes('stale-artifact-digest')) {
|
|
1664
|
+
failures.push(`${label}/change: the captured-bound pair was misreported stale`);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// 3) stale pair + destination-only record must not jointly pass, and the
|
|
1669
|
+
// stale pair must be diagnosed.
|
|
1670
|
+
out = undefined;
|
|
1671
|
+
try {
|
|
1672
|
+
out = runWith(renameCacheArtifacts(ctx, entry('value', RENAME_LATER)),
|
|
1673
|
+
[renamePairRecord(ctx, 'pair-run', RENAME_LATER),
|
|
1674
|
+
renameDestinationOnlyRecord(ctx, 'dest-only-run', RENAME_CAPTURED)]);
|
|
1675
|
+
} catch (error) { failures.push(`${label}/joint: escaped: ${error.message}`); }
|
|
1676
|
+
if (out) {
|
|
1677
|
+
if (resultOf(out, 'doc-change:doc-lint').outcome === 'pass') {
|
|
1678
|
+
failures.push(`${label}/joint: a stale pair plus a destination-only record jointly passed`);
|
|
1679
|
+
}
|
|
1680
|
+
if (!diagnosticCodes(out).includes('stale-artifact-digest')) {
|
|
1681
|
+
failures.push(`${label}/joint: the stale pair was not diagnosed: ${diagnosticCodes(out)}`);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
// 4) a stale pair alone cannot pass.
|
|
1686
|
+
out = undefined;
|
|
1687
|
+
try {
|
|
1688
|
+
out = runWith(renameCacheArtifacts(ctx, entry('value', RENAME_LATER)),
|
|
1689
|
+
[renamePairRecord(ctx, 'pair-run', RENAME_LATER)]);
|
|
1690
|
+
} catch (error) { failures.push(`${label}/stale: escaped: ${error.message}`); }
|
|
1691
|
+
if (out && resultOf(out, 'doc-change:doc-lint').outcome === 'pass') {
|
|
1692
|
+
failures.push(`${label}/stale: a stale pair passed`);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// 5) the strict own-key destination boundary is unchanged: static own and
|
|
1696
|
+
// null-prototype own digests pass, an inherited-only digest fails closed.
|
|
1697
|
+
const ownStatic = runWith(renameCacheArtifacts(ctx, { digest: RENAME_CAPTURED }),
|
|
1698
|
+
[renamePairRecord(ctx, 'pair-run', RENAME_CAPTURED)]);
|
|
1699
|
+
if (resultOf(ownStatic, 'doc-change:doc-lint').outcome !== 'pass') {
|
|
1700
|
+
failures.push(`${label}/static: a static own digest did not pass`);
|
|
1701
|
+
}
|
|
1702
|
+
const nullProto = Object.assign(Object.create(null), { digest: RENAME_CAPTURED });
|
|
1703
|
+
const ownNull = runWith(renameCacheArtifacts(ctx, nullProto),
|
|
1704
|
+
[renamePairRecord(ctx, 'pair-run', RENAME_CAPTURED)]);
|
|
1705
|
+
if (resultOf(ownNull, 'doc-change:doc-lint').outcome !== 'pass') {
|
|
1706
|
+
failures.push(`${label}/nullproto: a null-prototype own digest did not pass`);
|
|
1707
|
+
}
|
|
1708
|
+
const inherited = runWith(renameCacheArtifacts(ctx, Object.create({ digest: RENAME_CAPTURED })),
|
|
1709
|
+
[renamePairRecord(ctx, 'pair-run', RENAME_CAPTURED)]);
|
|
1710
|
+
if (resultOf(inherited, 'doc-change:doc-lint').outcome === 'pass') {
|
|
1711
|
+
failures.push(`${label}/inherited: an inherited-only digest bound the rename`);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
assert.deepStrictEqual(failures, [],
|
|
1716
|
+
`the rename destination must bind to the captured snapshot: ${failures.join('; ')}`);
|
|
1717
|
+
});
|
|
1718
|
+
|
|
1719
|
+
// ---------------------------------------------------------------------------
|
|
1720
|
+
// P2 candidate-snapshot consistency (review r4318074471): metadata validation is
|
|
1721
|
+
// the single candidate snapshot. A validated baseline-only path carries its
|
|
1722
|
+
// source-absence proof, and a validated rename destination carries its own
|
|
1723
|
+
// validated digest, so binding and the audit identity never re-inspect the
|
|
1724
|
+
// candidate map. A plain-prototype Proxy whose own-property inspection is
|
|
1725
|
+
// exhausted after metadata must not abort the check, while a trap that throws
|
|
1726
|
+
// during metadata still fails the obligation closed.
|
|
1727
|
+
// ---------------------------------------------------------------------------
|
|
1728
|
+
|
|
1729
|
+
function descriptorPatienceInventory(target, allowedDescriptorCalls) {
|
|
1730
|
+
let descriptorCalls = 0;
|
|
1731
|
+
const proxy = new Proxy(target, {
|
|
1732
|
+
getPrototypeOf() { return Object.prototype; },
|
|
1733
|
+
getOwnPropertyDescriptor(t, prop) {
|
|
1734
|
+
descriptorCalls += 1;
|
|
1735
|
+
if (descriptorCalls > allowedDescriptorCalls) {
|
|
1736
|
+
throw new Error(`descriptor trap call ${descriptorCalls}`);
|
|
1737
|
+
}
|
|
1738
|
+
return Reflect.getOwnPropertyDescriptor(t, prop);
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
return { proxy, descriptorCalls: () => descriptorCalls };
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
function countingDigestEntry(digest, { validReads = 3, later = 'throw', laterValue } = {}) {
|
|
1745
|
+
const entry = {};
|
|
1746
|
+
let reads = 0;
|
|
1747
|
+
Object.defineProperty(entry, 'digest', {
|
|
1748
|
+
enumerable: true,
|
|
1749
|
+
get() {
|
|
1750
|
+
reads += 1;
|
|
1751
|
+
if (reads <= validReads) return digest;
|
|
1752
|
+
if (later === 'throw') throw new Error('stateful digest accessor');
|
|
1753
|
+
return laterValue;
|
|
1754
|
+
}
|
|
1755
|
+
});
|
|
1756
|
+
return { entry, reads: () => reads };
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
function runDocLint(ctx, target, { artifacts = ctx.artifacts, records = [] }) {
|
|
1760
|
+
return target.checkDocObligations({
|
|
1761
|
+
obligations: [obligationOf(ctx, 'doc-change:doc-lint')],
|
|
1762
|
+
artifacts,
|
|
1763
|
+
baseline: ctx.baseline,
|
|
1764
|
+
executionRecords: records,
|
|
1765
|
+
reviewRecords: []
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
checkCase('335 P3 a validated rename pair is not re-inspected when the candidate descriptor trap is exhausted', () => {
|
|
1770
|
+
const failures = [];
|
|
1771
|
+
for (const target of [modules, packageModules]) {
|
|
1772
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
1773
|
+
const ctx = prepare('rename', target);
|
|
1774
|
+
const guarded = descriptorPatienceInventory(ctx.artifacts, 2);
|
|
1775
|
+
const record = execRecord(ctx, 'pair-run', 'doc-change:doc-lint', {
|
|
1776
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM]
|
|
1777
|
+
});
|
|
1778
|
+
let out;
|
|
1779
|
+
try {
|
|
1780
|
+
out = runDocLint(ctx, target, { artifacts: guarded.proxy, records: [record] });
|
|
1781
|
+
} catch (error) { failures.push(`${label}: escaped: ${error.message}`); continue; }
|
|
1782
|
+
if (resultOf(out, 'doc-change:doc-lint').outcome !== 'pass') {
|
|
1783
|
+
failures.push(`${label}: outcome=${resultOf(out, 'doc-change:doc-lint').outcome}`);
|
|
1784
|
+
}
|
|
1785
|
+
if (guarded.descriptorCalls() !== 2) failures.push(`${label}: descriptor calls=${guarded.descriptorCalls()}`);
|
|
1786
|
+
}
|
|
1787
|
+
assert.deepStrictEqual(failures, [],
|
|
1788
|
+
`a validated rename must not re-inspect the candidate: ${failures.join('; ')}`);
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
checkCase('335 P3 a validated deletion tombstone is not re-inspected when the candidate descriptor trap is exhausted', () => {
|
|
1792
|
+
const failures = [];
|
|
1793
|
+
for (const target of [modules, packageModules]) {
|
|
1794
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
1795
|
+
const ctx = prepare('deleted-adr', target);
|
|
1796
|
+
const guarded = descriptorPatienceInventory(ctx.artifacts, 1);
|
|
1797
|
+
const record = execRecord(ctx, 'tomb-run', 'doc-change:doc-lint', {
|
|
1798
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR]
|
|
1799
|
+
});
|
|
1800
|
+
let out;
|
|
1801
|
+
try {
|
|
1802
|
+
out = runDocLint(ctx, target, { artifacts: guarded.proxy, records: [record] });
|
|
1803
|
+
} catch (error) { failures.push(`${label}: escaped: ${error.message}`); continue; }
|
|
1804
|
+
if (resultOf(out, 'doc-change:doc-lint').outcome !== 'pass') {
|
|
1805
|
+
failures.push(`${label}: outcome=${resultOf(out, 'doc-change:doc-lint').outcome}`);
|
|
1806
|
+
}
|
|
1807
|
+
if (guarded.descriptorCalls() !== 1) failures.push(`${label}: descriptor calls=${guarded.descriptorCalls()}`);
|
|
1808
|
+
}
|
|
1809
|
+
assert.deepStrictEqual(failures, [],
|
|
1810
|
+
`a validated tombstone must not re-inspect the candidate: ${failures.join('; ')}`);
|
|
1811
|
+
});
|
|
1812
|
+
|
|
1813
|
+
checkCase('335 P3 a descriptor trap that throws during metadata still fails the baseline pair closed', () => {
|
|
1814
|
+
const failures = [];
|
|
1815
|
+
for (const target of [modules, packageModules]) {
|
|
1816
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
1817
|
+
for (const scenario of ['deleted-adr', 'rename']) {
|
|
1818
|
+
const ctx = prepare(scenario, target);
|
|
1819
|
+
const guarded = descriptorPatienceInventory(ctx.artifacts, 0);
|
|
1820
|
+
const isRename = scenario === 'rename';
|
|
1821
|
+
const record = execRecord(ctx, 'run', 'doc-change:doc-lint', {
|
|
1822
|
+
paths: isRename ? [RENAME_FROM, RENAME_TO] : [DELETED_ADR],
|
|
1823
|
+
candidatePaths: isRename ? [RENAME_TO] : [],
|
|
1824
|
+
baselinePaths: isRename ? [RENAME_FROM] : [DELETED_ADR]
|
|
1825
|
+
});
|
|
1826
|
+
let out;
|
|
1827
|
+
try {
|
|
1828
|
+
out = runDocLint(ctx, target, { artifacts: guarded.proxy, records: [record] });
|
|
1829
|
+
} catch (error) { failures.push(`${label}/${scenario}: escaped: ${error.message}`); continue; }
|
|
1830
|
+
const result = resultOf(out, 'doc-change:doc-lint');
|
|
1831
|
+
if (result.outcome !== 'none') failures.push(`${label}/${scenario}: outcome=${result.outcome}`);
|
|
1832
|
+
if (!diagnosticCodes(out).includes('candidate-artifacts-invalid')) {
|
|
1833
|
+
failures.push(`${label}/${scenario}: diagnostics=${diagnosticCodes(out)}`);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
assert.deepStrictEqual(failures, [],
|
|
1838
|
+
`a metadata descriptor trap must fail closed: ${failures.join('; ')}`);
|
|
1839
|
+
});
|
|
1840
|
+
|
|
1841
|
+
checkCase('335 P3 the validated source-absence proof is carried into the audit and binding', () => {
|
|
1842
|
+
const failures = [];
|
|
1843
|
+
for (const target of [modules, packageModules]) {
|
|
1844
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
1845
|
+
const ctx = prepare('rename', target);
|
|
1846
|
+
let sourceGets = 0;
|
|
1847
|
+
const inventory = new Proxy(ctx.artifacts, {
|
|
1848
|
+
getPrototypeOf() { return Object.prototype; },
|
|
1849
|
+
get(t, prop, receiver) {
|
|
1850
|
+
if (prop === RENAME_FROM) {
|
|
1851
|
+
sourceGets += 1;
|
|
1852
|
+
return { digest: sha256('spoofed source entry') };
|
|
1853
|
+
}
|
|
1854
|
+
return Reflect.get(t, prop, receiver);
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
const record = execRecord(ctx, 'pair-run', 'doc-change:doc-lint', {
|
|
1858
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM]
|
|
1859
|
+
});
|
|
1860
|
+
let out;
|
|
1861
|
+
try {
|
|
1862
|
+
out = runDocLint(ctx, target, { artifacts: inventory, records: [record] });
|
|
1863
|
+
} catch (error) { failures.push(`${label}: escaped: ${error.message}`); continue; }
|
|
1864
|
+
const result = resultOf(out, 'doc-change:doc-lint');
|
|
1865
|
+
if (result.outcome !== 'pass') failures.push(`${label}: outcome=${result.outcome}`);
|
|
1866
|
+
if (!result.artifactDigest.includes(`${RENAME_FROM}=missing`)) {
|
|
1867
|
+
failures.push(`${label}: audit=${result.artifactDigest}`);
|
|
1868
|
+
}
|
|
1869
|
+
if (sourceGets !== 0) failures.push(`${label}: source getter reads=${sourceGets}`);
|
|
1870
|
+
}
|
|
1871
|
+
assert.deepStrictEqual(failures, [],
|
|
1872
|
+
`the source-absence proof must be carried: ${failures.join('; ')}`);
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
checkCase('335 P3 the metadata-validated rename destination digest is carried without a second candidate read', () => {
|
|
1876
|
+
const failures = [];
|
|
1877
|
+
for (const target of [modules, packageModules]) {
|
|
1878
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
1879
|
+
const ctx = prepare('rename', target);
|
|
1880
|
+
const counted = countingDigestEntry(RENAME_CAPTURED, { validReads: 3, later: 'throw' });
|
|
1881
|
+
const artifacts = { ...ctx.artifacts, [RENAME_TO]: counted.entry };
|
|
1882
|
+
const record = execRecord(ctx, 'pair-run', 'doc-change:doc-lint', {
|
|
1883
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [], baselinePaths: [RENAME_FROM],
|
|
1884
|
+
artifactDigests: { [RENAME_TO]: RENAME_CAPTURED }
|
|
1885
|
+
});
|
|
1886
|
+
let out;
|
|
1887
|
+
try {
|
|
1888
|
+
out = runDocLint(ctx, target, { artifacts, records: [record] });
|
|
1889
|
+
} catch (error) { failures.push(`${label}: escaped: ${error.message}`); continue; }
|
|
1890
|
+
const result = resultOf(out, 'doc-change:doc-lint');
|
|
1891
|
+
if (result.outcome !== 'pass') failures.push(`${label}: outcome=${result.outcome}`);
|
|
1892
|
+
if (!result.artifactDigest.includes(`${RENAME_TO}=${RENAME_CAPTURED}`)) {
|
|
1893
|
+
failures.push(`${label}: audit=${result.artifactDigest}`);
|
|
1894
|
+
}
|
|
1895
|
+
if (counted.reads() !== 3) failures.push(`${label}: destination reads=${counted.reads()}`);
|
|
1896
|
+
}
|
|
1897
|
+
assert.deepStrictEqual(failures, [],
|
|
1898
|
+
`the validated destination digest must be carried: ${failures.join('; ')}`);
|
|
1899
|
+
});
|
|
1900
|
+
|
|
1901
|
+
checkCase('335 P3 a stale pair plus a destination-only record stays none under descriptor patience', () => {
|
|
1902
|
+
const failures = [];
|
|
1903
|
+
for (const target of [modules, packageModules]) {
|
|
1904
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
1905
|
+
const ctx = prepare('rename', target);
|
|
1906
|
+
const guarded = descriptorPatienceInventory(ctx.artifacts, 2);
|
|
1907
|
+
const stale = execRecord(ctx, 'pair-run', 'doc-change:doc-lint', {
|
|
1908
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [], baselinePaths: [RENAME_FROM],
|
|
1909
|
+
artifactDigests: { [RENAME_TO]: RENAME_LATER }
|
|
1910
|
+
});
|
|
1911
|
+
const destOnly = execRecord(ctx, 'dest-only-run', 'doc-change:doc-lint', {
|
|
1912
|
+
paths: [RENAME_TO], candidatePaths: [], baselinePaths: [],
|
|
1913
|
+
artifactDigests: { [RENAME_TO]: RENAME_CAPTURED }
|
|
1914
|
+
});
|
|
1915
|
+
let out;
|
|
1916
|
+
try {
|
|
1917
|
+
out = runDocLint(ctx, target, { artifacts: guarded.proxy, records: [stale, destOnly] });
|
|
1918
|
+
} catch (error) { failures.push(`${label}: escaped: ${error.message}`); continue; }
|
|
1919
|
+
if (resultOf(out, 'doc-change:doc-lint').outcome === 'pass') {
|
|
1920
|
+
failures.push(`${label}: a stale pair plus a destination-only record jointly passed`);
|
|
1921
|
+
}
|
|
1922
|
+
if (!diagnosticCodes(out).includes('stale-artifact-digest')) {
|
|
1923
|
+
failures.push(`${label}: diagnostics=${diagnosticCodes(out)}`);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
assert.deepStrictEqual(failures, [],
|
|
1927
|
+
`a stale pair must not jointly pass: ${failures.join('; ')}`);
|
|
1928
|
+
});
|
|
1929
|
+
|
|
1930
|
+
checkCase('335 P3 extra raw baselinePaths properties cannot forge the captured absence or destination digest', () => {
|
|
1931
|
+
const ctx = prepare('rename');
|
|
1932
|
+
const lint = obligationOf(ctx, 'doc-change:doc-lint');
|
|
1933
|
+
const forged = sha256('forged destination digest');
|
|
1934
|
+
const mutated = {
|
|
1935
|
+
...lint,
|
|
1936
|
+
scope: {
|
|
1937
|
+
...lint.scope,
|
|
1938
|
+
baselinePaths: [{
|
|
1939
|
+
path: RENAME_FROM, reason: 'rename-from', renameTo: RENAME_TO,
|
|
1940
|
+
candidatePathPresent: true, sourceAbsent: false, renameDestinationDigest: forged
|
|
1941
|
+
}]
|
|
1942
|
+
}
|
|
1943
|
+
};
|
|
1944
|
+
const output = check(ctx, {
|
|
1945
|
+
obligations: ctx.obligations.map(item => (item.id === lint.id ? mutated : item)),
|
|
1946
|
+
executionRecords: [execRecord(ctx, 'pair-run', 'doc-change:doc-lint', {
|
|
1947
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM]
|
|
1948
|
+
})]
|
|
1949
|
+
});
|
|
1950
|
+
const result = resultOf(output, 'doc-change:doc-lint');
|
|
1951
|
+
assert.strictEqual(result.outcome, 'pass',
|
|
1952
|
+
'extra raw fields must not override the validated destination digest');
|
|
1953
|
+
assert.ok(result.artifactDigest.includes(`${RENAME_TO}=${ctx.artifacts[RENAME_TO].digest}`),
|
|
1954
|
+
`the audit identity must keep the validated destination digest: ${result.artifactDigest}`);
|
|
1955
|
+
});
|
|
1956
|
+
|
|
1957
|
+
checkCase('335 P3 descriptor-patience inventories do not abort inapplicable or legacy obligations', () => {
|
|
1958
|
+
const ctx = prepare('deleted-adr');
|
|
1959
|
+
const lint = obligationOf(ctx, 'doc-change:doc-lint');
|
|
1960
|
+
for (const applicability of ['N/A', 'unknown']) {
|
|
1961
|
+
const guarded = descriptorPatienceInventory(ctx.artifacts, 0);
|
|
1962
|
+
let out;
|
|
1963
|
+
try {
|
|
1964
|
+
out = checkArtifacts(ctx, guarded.proxy, { obligations: [{ ...lint, applicability }] });
|
|
1965
|
+
} catch (error) {
|
|
1966
|
+
assert.fail(`${applicability}: descriptor-patience inventory escaped: ${error.message}`);
|
|
1967
|
+
}
|
|
1968
|
+
assert.strictEqual(resultOf(out, lint.id).outcome, 'none', `${applicability} stays none`);
|
|
1969
|
+
assert.ok(!diagnosticCodes(out).includes('candidate-artifacts-invalid'),
|
|
1970
|
+
`${applicability} emits no inventory diagnostic`);
|
|
1971
|
+
}
|
|
1972
|
+
const edited = prepare('docs-only');
|
|
1973
|
+
const guarded = descriptorPatienceInventory(edited.artifacts, 0);
|
|
1974
|
+
const records = {
|
|
1975
|
+
executionRecords: [execRecord(edited, 'edit-run', 'doc-change:doc-lint', { paths: [GUIDE], candidatePaths: [GUIDE] })]
|
|
1976
|
+
};
|
|
1977
|
+
const withProxy = checkArtifacts(edited, guarded.proxy, records);
|
|
1978
|
+
const withPlain = checkArtifacts(edited, { ...edited.artifacts }, records);
|
|
1979
|
+
assert.deepStrictEqual(withProxy, withPlain,
|
|
1980
|
+
'legacy unannotated callers read the candidate map through the legacy lane unchanged');
|
|
1981
|
+
assert.strictEqual(resultOf(withProxy, 'doc-change:doc-lint').outcome, 'pass',
|
|
1982
|
+
'a populated legacy inventory still satisfies a normal candidate path');
|
|
1983
|
+
});
|
|
1984
|
+
|
|
1985
|
+
// ---------------------------------------------------------------------------
|
|
1986
|
+
// P2 applicability + shared-destination consistency (reviews r4318354774 and
|
|
1987
|
+
// r4318354780 at 4800a75): the validated source-absence audit shortcut is for
|
|
1988
|
+
// applicable obligations only, and every accepted entry that shares a rename
|
|
1989
|
+
// destination must agree on the validated own digest.
|
|
1990
|
+
// ---------------------------------------------------------------------------
|
|
1991
|
+
|
|
1992
|
+
const RENAME_SOURCE_SPOOF = sha256('spoofed legacy source entry');
|
|
1993
|
+
const RENAME_SECOND_SOURCE = 'docs/reference/second-old.md';
|
|
1994
|
+
const SECOND_SOURCE_BASELINE = sha256('second source trusted baseline');
|
|
1995
|
+
|
|
1996
|
+
checkCase('335 P3 inapplicable baseline-annotated obligations keep the legacy candidate audit read', () => {
|
|
1997
|
+
const failures = [];
|
|
1998
|
+
for (const target of [modules, packageModules]) {
|
|
1999
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
2000
|
+
const ctx = prepare('rename', target);
|
|
2001
|
+
const lint = obligationOf(ctx, 'doc-change:doc-lint');
|
|
2002
|
+
let sourceGets = 0;
|
|
2003
|
+
const inventory = new Proxy(ctx.artifacts, {
|
|
2004
|
+
getPrototypeOf() { return Object.prototype; },
|
|
2005
|
+
get(t, prop, receiver) {
|
|
2006
|
+
if (prop === RENAME_FROM) { sourceGets += 1; return { digest: RENAME_SOURCE_SPOOF }; }
|
|
2007
|
+
return Reflect.get(t, prop, receiver);
|
|
2008
|
+
}
|
|
2009
|
+
});
|
|
2010
|
+
for (const applicability of ['N/A', 'unknown']) {
|
|
2011
|
+
const out = checkArtifacts(ctx, inventory, { obligations: [{ ...lint, applicability }] }, target);
|
|
2012
|
+
const result = resultOf(out, lint.id);
|
|
2013
|
+
if (result.outcome !== 'none') failures.push(`${label}/${applicability}: outcome=${result.outcome}`);
|
|
2014
|
+
if (!result.artifactDigest.includes(`${RENAME_FROM}=${RENAME_SOURCE_SPOOF}`)) {
|
|
2015
|
+
failures.push(`${label}/${applicability}: audit=${result.artifactDigest}`);
|
|
2016
|
+
}
|
|
2017
|
+
if (diagnosticCodes(out).includes('candidate-artifacts-invalid')) {
|
|
2018
|
+
failures.push(`${label}/${applicability}: candidate-artifacts-invalid emitted`);
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
if (sourceGets === 0) failures.push(`${label}: the legacy read was not performed`);
|
|
2022
|
+
|
|
2023
|
+
const record = execRecord(ctx, 'pair-run', 'doc-change:doc-lint', {
|
|
2024
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [RENAME_TO], baselinePaths: [RENAME_FROM]
|
|
2025
|
+
});
|
|
2026
|
+
const applicable = runDocLint(ctx, target, { artifacts: inventory, records: [record] });
|
|
2027
|
+
const applicableResult = resultOf(applicable, 'doc-change:doc-lint');
|
|
2028
|
+
if (applicableResult.outcome !== 'pass') failures.push(`${label}/applicable: outcome=${applicableResult.outcome}`);
|
|
2029
|
+
if (!applicableResult.artifactDigest.includes(`${RENAME_FROM}=missing`)) {
|
|
2030
|
+
failures.push(`${label}/applicable: audit=${applicableResult.artifactDigest}`);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
assert.deepStrictEqual(failures, [],
|
|
2034
|
+
`inapplicable obligations must keep the legacy audit read: ${failures.join('; ')}`);
|
|
2035
|
+
});
|
|
2036
|
+
|
|
2037
|
+
checkCase('335 P3 non-baseline callers keep the legacy candidate read for the audit identity', () => {
|
|
2038
|
+
const ctx = prepare('docs-only');
|
|
2039
|
+
const spoof = sha256('legacy non-baseline spoof');
|
|
2040
|
+
const inventory = new Proxy(ctx.artifacts, {
|
|
2041
|
+
getPrototypeOf() { return Object.prototype; },
|
|
2042
|
+
get(t, prop, receiver) {
|
|
2043
|
+
if (prop === GUIDE) return { digest: spoof };
|
|
2044
|
+
return Reflect.get(t, prop, receiver);
|
|
2045
|
+
}
|
|
2046
|
+
});
|
|
2047
|
+
const output = check(ctx, {
|
|
2048
|
+
artifacts: inventory,
|
|
2049
|
+
executionRecords: [execRecord(ctx, 'edit-run', 'doc-change:doc-lint', {
|
|
2050
|
+
paths: [GUIDE], candidatePaths: [GUIDE], artifactDigests: { [GUIDE]: spoof }
|
|
2051
|
+
})]
|
|
2052
|
+
});
|
|
2053
|
+
const result = resultOf(output, 'doc-change:doc-lint');
|
|
2054
|
+
assert.strictEqual(result.outcome, 'pass', 'a non-baseline edit still binds through the legacy lane');
|
|
2055
|
+
assert.ok(result.artifactDigest.includes(`${GUIDE}=${spoof}`),
|
|
2056
|
+
`the legacy non-baseline audit read is retained: ${result.artifactDigest}`);
|
|
2057
|
+
});
|
|
2058
|
+
|
|
2059
|
+
function sharedDestinationContext(target) {
|
|
2060
|
+
const ctx = prepare('rename', target);
|
|
2061
|
+
const lint = obligationOf(ctx, 'doc-change:doc-lint');
|
|
2062
|
+
const obligation = {
|
|
2063
|
+
...lint,
|
|
2064
|
+
scope: {
|
|
2065
|
+
...lint.scope,
|
|
2066
|
+
paths: [RENAME_FROM, RENAME_SECOND_SOURCE, RENAME_TO],
|
|
2067
|
+
baselinePaths: [
|
|
2068
|
+
{ path: RENAME_FROM, reason: 'rename-from', renameTo: RENAME_TO },
|
|
2069
|
+
{ path: RENAME_SECOND_SOURCE, reason: 'rename-from', renameTo: RENAME_TO }
|
|
2070
|
+
]
|
|
2071
|
+
}
|
|
2072
|
+
};
|
|
2073
|
+
return {
|
|
2074
|
+
ctx,
|
|
2075
|
+
obligations: ctx.obligations.map(item => (item.id === lint.id ? obligation : item)),
|
|
2076
|
+
baseline: {
|
|
2077
|
+
digest: ctx.baseline.digest,
|
|
2078
|
+
artifacts: { ...ctx.baseline.artifacts, [RENAME_SECOND_SOURCE]: SECOND_SOURCE_BASELINE }
|
|
2079
|
+
}
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
function sharedPairRecord(ctx, id, destinationDigest) {
|
|
2084
|
+
return execRecord(ctx, id, 'doc-change:doc-lint', {
|
|
2085
|
+
paths: [RENAME_FROM, RENAME_SECOND_SOURCE, RENAME_TO],
|
|
2086
|
+
candidatePaths: [],
|
|
2087
|
+
baselinePaths: [RENAME_FROM, RENAME_SECOND_SOURCE],
|
|
2088
|
+
artifactDigests: { [RENAME_TO]: destinationDigest },
|
|
2089
|
+
baselineArtifacts: {
|
|
2090
|
+
[RENAME_FROM]: ctx.baseline.artifacts[RENAME_FROM],
|
|
2091
|
+
[RENAME_SECOND_SOURCE]: SECOND_SOURCE_BASELINE
|
|
2092
|
+
}
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
checkCase('335 P3 two accepted entries sharing a rename destination must agree on the validated digest', () => {
|
|
2097
|
+
const failures = [];
|
|
2098
|
+
for (const target of [modules, packageModules]) {
|
|
2099
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
2100
|
+
const run = (artifacts, records) => {
|
|
2101
|
+
const { ctx, obligations, baseline } = sharedDestinationContext(target);
|
|
2102
|
+
return target.checkDocObligations({ obligations, artifacts, baseline, executionRecords: records, reviewRecords: [] });
|
|
2103
|
+
};
|
|
2104
|
+
|
|
2105
|
+
// Stable shared destination: matching validated values still pass.
|
|
2106
|
+
{
|
|
2107
|
+
const { ctx } = sharedDestinationContext(target);
|
|
2108
|
+
const out = run(
|
|
2109
|
+
{ ...ctx.artifacts, [RENAME_TO]: { digest: RENAME_CAPTURED } },
|
|
2110
|
+
[sharedPairRecord(ctx, 'pair-run', RENAME_CAPTURED)]
|
|
2111
|
+
);
|
|
2112
|
+
const result = resultOf(out, 'doc-change:doc-lint');
|
|
2113
|
+
if (result.outcome !== 'pass') failures.push(`${label}/stable: outcome=${result.outcome}`);
|
|
2114
|
+
if (!result.artifactDigest.includes(`${RENAME_TO}=${RENAME_CAPTURED}`)) {
|
|
2115
|
+
failures.push(`${label}/stable: audit=${result.artifactDigest}`);
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// Dynamic shared destination: D1 for the first entry's metadata, D2 for the second.
|
|
2120
|
+
{
|
|
2121
|
+
const { ctx } = sharedDestinationContext(target);
|
|
2122
|
+
const dynamic = countingDigestEntry(RENAME_CAPTURED, { validReads: 3, later: 'value', laterValue: RENAME_LATER });
|
|
2123
|
+
const artifacts = { ...ctx.artifacts, [RENAME_TO]: dynamic.entry };
|
|
2124
|
+
const out = run(artifacts, [
|
|
2125
|
+
sharedPairRecord(ctx, 'pair-run', RENAME_CAPTURED),
|
|
2126
|
+
sharedPairRecord(ctx, 'alternate-run', RENAME_CAPTURED)
|
|
2127
|
+
]);
|
|
2128
|
+
const result = resultOf(out, 'doc-change:doc-lint');
|
|
2129
|
+
if (result.outcome !== 'none') failures.push(`${label}/dynamic: outcome=${result.outcome}`);
|
|
2130
|
+
if (!diagnosticCodes(out).includes('baseline-path-invalid')) {
|
|
2131
|
+
failures.push(`${label}/dynamic: diagnostics=${diagnosticCodes(out)}`);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
// Inherited-only destination digest is still not an own validated value.
|
|
2136
|
+
{
|
|
2137
|
+
const { ctx } = sharedDestinationContext(target);
|
|
2138
|
+
const out = run(
|
|
2139
|
+
{ ...ctx.artifacts, [RENAME_TO]: Object.create({ digest: RENAME_CAPTURED }) },
|
|
2140
|
+
[sharedPairRecord(ctx, 'pair-run', RENAME_CAPTURED)]
|
|
2141
|
+
);
|
|
2142
|
+
if (resultOf(out, 'doc-change:doc-lint').outcome === 'pass') {
|
|
2143
|
+
failures.push(`${label}/inherited: an inherited-only shared destination passed`);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
// Caller-asserted alias fields are not authority: a raw diff digest does not override
|
|
2148
|
+
// the live validated value.
|
|
2149
|
+
{
|
|
2150
|
+
const { ctx, obligations, baseline } = sharedDestinationContext(target);
|
|
2151
|
+
const aliased = obligations.map(item => item.id === 'doc-change:doc-lint'
|
|
2152
|
+
? {
|
|
2153
|
+
...item,
|
|
2154
|
+
scope: {
|
|
2155
|
+
...item.scope,
|
|
2156
|
+
baselinePaths: item.scope.baselinePaths.map(entry => ({
|
|
2157
|
+
...entry, renameDestinationDigest: RENAME_LATER
|
|
2158
|
+
}))
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
: item);
|
|
2162
|
+
const out = target.checkDocObligations({
|
|
2163
|
+
obligations: aliased,
|
|
2164
|
+
artifacts: { ...ctx.artifacts, [RENAME_TO]: { digest: RENAME_CAPTURED } },
|
|
2165
|
+
baseline,
|
|
2166
|
+
executionRecords: [sharedPairRecord(ctx, 'pair-run', RENAME_CAPTURED)],
|
|
2167
|
+
reviewRecords: []
|
|
2168
|
+
});
|
|
2169
|
+
const result = resultOf(out, 'doc-change:doc-lint');
|
|
2170
|
+
if (result.outcome !== 'pass') failures.push(`${label}/alias: outcome=${result.outcome}`);
|
|
2171
|
+
if (!result.artifactDigest.includes(`${RENAME_TO}=${RENAME_CAPTURED}`)) {
|
|
2172
|
+
failures.push(`${label}/alias: audit=${result.artifactDigest}`);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
assert.deepStrictEqual(failures, [],
|
|
2177
|
+
`shared rename destinations must agree on the validated digest: ${failures.join('; ')}`);
|
|
2178
|
+
});
|
|
2179
|
+
|
|
2180
|
+
// ---------------------------------------------------------------------------
|
|
2181
|
+
// P2 applicability-gated destination snapshot (review r4318520818 at 56f3094):
|
|
2182
|
+
// N/A and unknown obligations never bind evidence, so their destination digest
|
|
2183
|
+
// getter must not be consumed during metadata validation; the audit identity
|
|
2184
|
+
// keeps the un-gated legacy candidate read (the parent value).
|
|
2185
|
+
// ---------------------------------------------------------------------------
|
|
2186
|
+
|
|
2187
|
+
checkCase('335 P2 an inapplicable rename never consumes the destination snapshot during metadata', () => {
|
|
2188
|
+
const failures = [];
|
|
2189
|
+
for (const target of [modules, packageModules]) {
|
|
2190
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
2191
|
+
for (const applicability of ['N/A', 'unknown', undefined]) {
|
|
2192
|
+
const ctx = prepare('rename', target);
|
|
2193
|
+
const lint = obligationOf(ctx, 'doc-change:doc-lint');
|
|
2194
|
+
const counted = countingDigestEntry(RENAME_CAPTURED, { validReads: 3, later: 'value', laterValue: RENAME_LATER });
|
|
2195
|
+
const artifacts = { ...ctx.artifacts, [RENAME_TO]: counted.entry };
|
|
2196
|
+
let obligation;
|
|
2197
|
+
if (applicability === undefined) {
|
|
2198
|
+
obligation = { ...lint };
|
|
2199
|
+
delete obligation.applicability;
|
|
2200
|
+
} else {
|
|
2201
|
+
obligation = { ...lint, applicability };
|
|
2202
|
+
}
|
|
2203
|
+
let out;
|
|
2204
|
+
try {
|
|
2205
|
+
out = checkArtifacts(ctx, artifacts, { obligations: [obligation] }, target);
|
|
2206
|
+
} catch (error) {
|
|
2207
|
+
failures.push(`${label}/${applicability}: escaped: ${error.message}`);
|
|
2208
|
+
continue;
|
|
2209
|
+
}
|
|
2210
|
+
const result = resultOf(out, lint.id);
|
|
2211
|
+
if (result.outcome !== 'none') failures.push(`${label}/${applicability}: outcome=${result.outcome}`);
|
|
2212
|
+
if (!result.artifactDigest.includes(`${RENAME_TO}=${RENAME_CAPTURED}`)) {
|
|
2213
|
+
failures.push(`${label}/${applicability}: audit=${result.artifactDigest}`);
|
|
2214
|
+
}
|
|
2215
|
+
if (counted.reads() !== 3) failures.push(`${label}/${applicability}: destination getter reads=${counted.reads()}`);
|
|
2216
|
+
const codes = diagnosticCodes(out);
|
|
2217
|
+
if (codes.includes('baseline-path-invalid') || codes.includes('candidate-artifacts-invalid')) {
|
|
2218
|
+
failures.push(`${label}/${applicability}: diagnostics=${codes.join(', ')}`);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
// Positive control: the applicable obligation still captures the destination snapshot
|
|
2223
|
+
// once during metadata and binds against it.
|
|
2224
|
+
{
|
|
2225
|
+
const ctx = prepare('rename', target);
|
|
2226
|
+
const counted = countingDigestEntry(RENAME_CAPTURED, { validReads: 3, later: 'value', laterValue: RENAME_LATER });
|
|
2227
|
+
const artifacts = { ...ctx.artifacts, [RENAME_TO]: counted.entry };
|
|
2228
|
+
const record = {
|
|
2229
|
+
...execRecord(ctx, 'pair-run', 'doc-change:doc-lint', {
|
|
2230
|
+
paths: [RENAME_FROM, RENAME_TO], candidatePaths: [], baselinePaths: [RENAME_FROM],
|
|
2231
|
+
artifactDigests: { [RENAME_TO]: RENAME_CAPTURED }
|
|
2232
|
+
})
|
|
2233
|
+
};
|
|
2234
|
+
const obligation = obligationOf(ctx, 'doc-change:doc-lint');
|
|
2235
|
+
const out = checkArtifacts(ctx, artifacts, { obligations: [obligation], executionRecords: [record] }, target);
|
|
2236
|
+
const result = resultOf(out, 'doc-change:doc-lint');
|
|
2237
|
+
if (result.outcome !== 'pass') failures.push(`${label}/applicable: outcome=${result.outcome}`);
|
|
2238
|
+
if (counted.reads() !== 3) failures.push(`${label}/applicable: destination getter reads=${counted.reads()}`);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
assert.deepStrictEqual(failures, [],
|
|
2242
|
+
`inapplicable obligations must not consume the destination snapshot: ${failures.join('; ')}`);
|
|
2243
|
+
});
|
|
2244
|
+
|
|
2245
|
+
// ---------------------------------------------------------------------------
|
|
2246
|
+
// P2 guarded baselinePaths entry inspection (review r4318520836 at 56f3094):
|
|
2247
|
+
// a Proxy entry whose path, reason, or renameTo field getter/descriptor trap
|
|
2248
|
+
// throws fails the entry closed as a metadata error instead of escaping the
|
|
2249
|
+
// checker, and a late-throwing entry accessor cannot abort the audit mirror.
|
|
2250
|
+
// ---------------------------------------------------------------------------
|
|
2251
|
+
|
|
2252
|
+
checkCase('335 P2 baselinePaths entry accessors that throw fail the entry closed without escaping', () => {
|
|
2253
|
+
const failures = [];
|
|
2254
|
+
for (const target of [modules, packageModules]) {
|
|
2255
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
2256
|
+
const lintObligation = (ctx, entry) => {
|
|
2257
|
+
const lint = obligationOf(ctx, 'doc-change:doc-lint');
|
|
2258
|
+
return { ...lint, scope: { ...lint.scope, baselinePaths: [entry] } };
|
|
2259
|
+
};
|
|
2260
|
+
|
|
2261
|
+
// A throwing path getter must not escape.
|
|
2262
|
+
{
|
|
2263
|
+
const ctx = prepare('deleted-adr', target);
|
|
2264
|
+
const entry = new Proxy({ path: DELETED_ADR, reason: 'deleted' }, {
|
|
2265
|
+
get(t, prop) {
|
|
2266
|
+
if (prop === 'path') throw new Error('entry path getter');
|
|
2267
|
+
return Reflect.get(t, prop);
|
|
2268
|
+
}
|
|
2269
|
+
});
|
|
2270
|
+
let out;
|
|
2271
|
+
try {
|
|
2272
|
+
out = checkArtifacts(ctx, ctx.artifacts, { obligations: [lintObligation(ctx, entry)] }, target);
|
|
2273
|
+
} catch (error) {
|
|
2274
|
+
failures.push(`${label}/path-getter: escaped: ${error.message}`);
|
|
2275
|
+
continue;
|
|
2276
|
+
}
|
|
2277
|
+
if (resultOf(out, 'doc-change:doc-lint').outcome !== 'none') {
|
|
2278
|
+
failures.push(`${label}/path-getter: outcome=${resultOf(out, 'doc-change:doc-lint').outcome}`);
|
|
2279
|
+
}
|
|
2280
|
+
if (!diagnosticCodes(out).includes('baseline-path-invalid')) {
|
|
2281
|
+
failures.push(`${label}/path-getter: diagnostics=${diagnosticCodes(out)}`);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
// A throwing reason getter must not escape.
|
|
2286
|
+
{
|
|
2287
|
+
const ctx = prepare('deleted-adr', target);
|
|
2288
|
+
const entry = new Proxy({ path: DELETED_ADR, reason: 'deleted' }, {
|
|
2289
|
+
get(t, prop) {
|
|
2290
|
+
if (prop === 'reason') throw new Error('entry reason getter');
|
|
2291
|
+
return Reflect.get(t, prop);
|
|
2292
|
+
}
|
|
2293
|
+
});
|
|
2294
|
+
let out;
|
|
2295
|
+
try {
|
|
2296
|
+
out = checkArtifacts(ctx, ctx.artifacts, { obligations: [lintObligation(ctx, entry)] }, target);
|
|
2297
|
+
} catch (error) {
|
|
2298
|
+
failures.push(`${label}/reason-getter: escaped: ${error.message}`);
|
|
2299
|
+
continue;
|
|
2300
|
+
}
|
|
2301
|
+
if (resultOf(out, 'doc-change:doc-lint').outcome !== 'none') {
|
|
2302
|
+
failures.push(`${label}/reason-getter: outcome=${resultOf(out, 'doc-change:doc-lint').outcome}`);
|
|
2303
|
+
}
|
|
2304
|
+
if (!diagnosticCodes(out).includes('baseline-path-invalid')) {
|
|
2305
|
+
failures.push(`${label}/reason-getter: diagnostics=${diagnosticCodes(out)}`);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// A descriptor trap on the renameTo own-presence check must not escape.
|
|
2310
|
+
{
|
|
2311
|
+
const ctx = prepare('deleted-adr', target);
|
|
2312
|
+
const entry = new Proxy({ path: DELETED_ADR, reason: 'deleted' }, {
|
|
2313
|
+
getOwnPropertyDescriptor(t, prop) {
|
|
2314
|
+
if (prop === 'renameTo') throw new Error('entry descriptor trap');
|
|
2315
|
+
return Reflect.getOwnPropertyDescriptor(t, prop);
|
|
2316
|
+
}
|
|
2317
|
+
});
|
|
2318
|
+
let out;
|
|
2319
|
+
try {
|
|
2320
|
+
out = checkArtifacts(ctx, ctx.artifacts, { obligations: [lintObligation(ctx, entry)] }, target);
|
|
2321
|
+
} catch (error) {
|
|
2322
|
+
failures.push(`${label}/descriptor: escaped: ${error.message}`);
|
|
2323
|
+
continue;
|
|
2324
|
+
}
|
|
2325
|
+
if (resultOf(out, 'doc-change:doc-lint').outcome !== 'none') {
|
|
2326
|
+
failures.push(`${label}/descriptor: outcome=${resultOf(out, 'doc-change:doc-lint').outcome}`);
|
|
2327
|
+
}
|
|
2328
|
+
if (!diagnosticCodes(out).includes('baseline-path-invalid')) {
|
|
2329
|
+
failures.push(`${label}/descriptor: diagnostics=${diagnosticCodes(out)}`);
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// A path getter that throws only on a later read cannot abort the audit mirror; a
|
|
2334
|
+
// valid tombstone still passes (the entry metadata itself was validated once).
|
|
2335
|
+
{
|
|
2336
|
+
const ctx = prepare('deleted-adr', target);
|
|
2337
|
+
let pathReads = 0;
|
|
2338
|
+
const entry = new Proxy({ path: DELETED_ADR, reason: 'deleted' }, {
|
|
2339
|
+
get(t, prop) {
|
|
2340
|
+
if (prop === 'path') {
|
|
2341
|
+
pathReads += 1;
|
|
2342
|
+
if (pathReads > 1) throw new Error('late path getter');
|
|
2343
|
+
return Reflect.get(t, prop);
|
|
2344
|
+
}
|
|
2345
|
+
return Reflect.get(t, prop);
|
|
2346
|
+
}
|
|
2347
|
+
});
|
|
2348
|
+
const record = execRecord(ctx, 'tomb-run', 'doc-change:doc-lint', {
|
|
2349
|
+
paths: [DELETED_ADR], candidatePaths: [], baselinePaths: [DELETED_ADR]
|
|
2350
|
+
});
|
|
2351
|
+
let out;
|
|
2352
|
+
try {
|
|
2353
|
+
out = checkArtifacts(ctx, ctx.artifacts, {
|
|
2354
|
+
obligations: [lintObligation(ctx, entry)], executionRecords: [record]
|
|
2355
|
+
}, target);
|
|
2356
|
+
} catch (error) {
|
|
2357
|
+
failures.push(`${label}/late-path: escaped: ${error.message}`);
|
|
2358
|
+
continue;
|
|
2359
|
+
}
|
|
2360
|
+
if (resultOf(out, 'doc-change:doc-lint').outcome !== 'pass') {
|
|
2361
|
+
failures.push(`${label}/late-path: outcome=${resultOf(out, 'doc-change:doc-lint').outcome}`);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
// Inapplicable obligations keep the fail-closed metadata diagnostic and the legacy
|
|
2366
|
+
// legacy outcome without escaping.
|
|
2367
|
+
{
|
|
2368
|
+
const ctx = prepare('deleted-adr', target);
|
|
2369
|
+
const entry = new Proxy({ path: DELETED_ADR, reason: 'deleted' }, {
|
|
2370
|
+
get(t, prop) {
|
|
2371
|
+
if (prop === 'path' || prop === 'reason') throw new Error('entry field getter');
|
|
2372
|
+
return Reflect.get(t, prop);
|
|
2373
|
+
}
|
|
2374
|
+
});
|
|
2375
|
+
for (const applicability of ['N/A', 'unknown']) {
|
|
2376
|
+
let out;
|
|
2377
|
+
try {
|
|
2378
|
+
out = checkArtifacts(ctx, ctx.artifacts, {
|
|
2379
|
+
obligations: [{ ...lintObligation(ctx, entry), applicability }]
|
|
2380
|
+
}, target);
|
|
2381
|
+
} catch (error) {
|
|
2382
|
+
failures.push(`${label}/${applicability}: escaped: ${error.message}`);
|
|
2383
|
+
continue;
|
|
2384
|
+
}
|
|
2385
|
+
const result = resultOf(out, 'doc-change:doc-lint');
|
|
2386
|
+
if (result.outcome !== 'none') failures.push(`${label}/${applicability}: outcome=${result.outcome}`);
|
|
2387
|
+
if (!diagnosticCodes(out).includes('baseline-path-invalid')) {
|
|
2388
|
+
failures.push(`${label}/${applicability}: diagnostics=${diagnosticCodes(out)}`);
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
assert.deepStrictEqual(failures, [],
|
|
2394
|
+
`baselinePaths entry accessor failures must fail closed: ${failures.join('; ')}`);
|
|
2395
|
+
});
|
|
2396
|
+
|
|
2397
|
+
// ---------------------------------------------------------------------------
|
|
2398
|
+
// P2 validated baseline digest snapshot (review r4320818749 at 4cdb97c): the
|
|
2399
|
+
// per-path baseline digest validated during metadata is captured once; the
|
|
2400
|
+
// audit mirror, the stale-baseline diagnostics, and binding reuse that
|
|
2401
|
+
// captured value instead of re-reading the caller-owned baseline map. A
|
|
2402
|
+
// stateful own getter cannot bind a record against a value it started
|
|
2403
|
+
// returning only after validation, and a throwing getter is a metadata error.
|
|
2404
|
+
// ---------------------------------------------------------------------------
|
|
2405
|
+
|
|
2406
|
+
const BASELINE_EXAMPLE = 'docs/adr/example.md';
|
|
2407
|
+
const BASELINE_EXAMPLE_VALUE = sha256('example baseline content');
|
|
2408
|
+
const BASELINE_EXAMPLE_LATER = sha256('example later baseline content');
|
|
2409
|
+
|
|
2410
|
+
function statefulBaselineArtifacts(pathPart) {
|
|
2411
|
+
let probeReads = 0;
|
|
2412
|
+
return {
|
|
2413
|
+
map: new Proxy({ [pathPart]: undefined }, {
|
|
2414
|
+
getPrototypeOf() { return null; },
|
|
2415
|
+
get(t, prop, receiver) {
|
|
2416
|
+
if (prop === pathPart) {
|
|
2417
|
+
probeReads += 1;
|
|
2418
|
+
return probeReads === 1 ? BASELINE_EXAMPLE_VALUE : BASELINE_EXAMPLE_LATER;
|
|
2419
|
+
}
|
|
2420
|
+
return Reflect.get(t, prop, receiver);
|
|
2421
|
+
}
|
|
2422
|
+
}),
|
|
2423
|
+
probeReads: () => probeReads
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
function singleBaselineObligation() {
|
|
2428
|
+
return [{
|
|
2429
|
+
id: 'doc-change:doc-lint',
|
|
2430
|
+
capability: 'doc-lint',
|
|
2431
|
+
level: 'MUST',
|
|
2432
|
+
applicability: 'applicable',
|
|
2433
|
+
scope: { paths: [BASELINE_EXAMPLE], baselinePaths: [{ path: BASELINE_EXAMPLE, reason: 'deleted' }] }
|
|
2434
|
+
}];
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
function singleBaselineRecord(baselineDigestValue, baselinePathValue, pathPart = BASELINE_EXAMPLE) {
|
|
2438
|
+
return [{
|
|
2439
|
+
id: 'tomb-run',
|
|
2440
|
+
obligationId: 'doc-change:doc-lint',
|
|
2441
|
+
command: `node ci/mechanism-335-baseline-evidence.test.js # tomb-run`,
|
|
2442
|
+
exitCode: 0,
|
|
2443
|
+
provenance: 'validated baseline snapshot regression',
|
|
2444
|
+
baseDigest: baselineDigestValue,
|
|
2445
|
+
paths: [pathPart],
|
|
2446
|
+
artifactDigests: {},
|
|
2447
|
+
baselineArtifacts: { [pathPart]: baselinePathValue }
|
|
2448
|
+
}];
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
checkCase('335 P2 a drifted live baseline digest cannot bind a record against the validated value', () => {
|
|
2452
|
+
const failures = [];
|
|
2453
|
+
for (const target of [modules, packageModules]) {
|
|
2454
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
2455
|
+
const baselineDigest = sha256('example whole baseline');
|
|
2456
|
+
const base = { digest: baselineDigest };
|
|
2457
|
+
|
|
2458
|
+
// The validated value is the first getter read; a later value must never bind.
|
|
2459
|
+
{
|
|
2460
|
+
const stateful = statefulBaselineArtifacts(BASELINE_EXAMPLE);
|
|
2461
|
+
const baseline = { ...base, artifacts: stateful.map };
|
|
2462
|
+
let out;
|
|
2463
|
+
try {
|
|
2464
|
+
out = target.checkDocObligations({
|
|
2465
|
+
obligations: singleBaselineObligation(),
|
|
2466
|
+
artifacts: {},
|
|
2467
|
+
baseline,
|
|
2468
|
+
executionRecords: singleBaselineRecord(baselineDigest, BASELINE_EXAMPLE_LATER),
|
|
2469
|
+
reviewRecords: []
|
|
2470
|
+
});
|
|
2471
|
+
} catch (error) {
|
|
2472
|
+
failures.push(`${label}/drifted-later: escaped: ${error.message}`);
|
|
2473
|
+
continue;
|
|
2474
|
+
}
|
|
2475
|
+
const result = out.results.find(item => item.obligationId === 'doc-change:doc-lint');
|
|
2476
|
+
if (result.outcome === 'pass') failures.push(`${label}/drifted-later: the later value bound a record (false PASS)`);
|
|
2477
|
+
if (!out.diagnostics.some(item => item.code === 'stale-baseline-artifact-digest')) {
|
|
2478
|
+
failures.push(`${label}/drifted-later: diagnostics=${out.diagnostics.map(item => item.code)}`);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
// Positive control: the record carrying the validated value still passes.
|
|
2483
|
+
{
|
|
2484
|
+
const stateful = statefulBaselineArtifacts(BASELINE_EXAMPLE);
|
|
2485
|
+
const baseline = { ...base, artifacts: stateful.map };
|
|
2486
|
+
let out;
|
|
2487
|
+
try {
|
|
2488
|
+
out = target.checkDocObligations({
|
|
2489
|
+
obligations: singleBaselineObligation(),
|
|
2490
|
+
artifacts: {},
|
|
2491
|
+
baseline,
|
|
2492
|
+
executionRecords: singleBaselineRecord(baselineDigest, BASELINE_EXAMPLE_VALUE),
|
|
2493
|
+
reviewRecords: []
|
|
2494
|
+
});
|
|
2495
|
+
} catch (error) {
|
|
2496
|
+
failures.push(`${label}/drifted-validated: escaped: ${error.message}`);
|
|
2497
|
+
continue;
|
|
2498
|
+
}
|
|
2499
|
+
const result = out.results.find(item => item.obligationId === 'doc-change:doc-lint');
|
|
2500
|
+
if (result.outcome !== 'pass') failures.push(`${label}/drifted-validated: outcome=${result.outcome}`);
|
|
2501
|
+
if (stateful.probeReads() !== 1) failures.push(`${label}/drifted-validated: live baseline reads=${stateful.probeReads()}`);
|
|
2502
|
+
if (result.baselineArtifacts[BASELINE_EXAMPLE] !== BASELINE_EXAMPLE_VALUE) {
|
|
2503
|
+
failures.push(`${label}/drifted-validated: audit=${JSON.stringify(result.baselineArtifacts)}`);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
|
|
2507
|
+
// Stable controls on a plain baseline map: valid tombstone passes, wrong value does not.
|
|
2508
|
+
{
|
|
2509
|
+
const baseline = { ...base, artifacts: { [BASELINE_EXAMPLE]: BASELINE_EXAMPLE_VALUE } };
|
|
2510
|
+
const stable = target.checkDocObligations({
|
|
2511
|
+
obligations: singleBaselineObligation(),
|
|
2512
|
+
artifacts: {},
|
|
2513
|
+
baseline,
|
|
2514
|
+
executionRecords: singleBaselineRecord(baselineDigest, BASELINE_EXAMPLE_VALUE),
|
|
2515
|
+
reviewRecords: []
|
|
2516
|
+
});
|
|
2517
|
+
const stableLater = target.checkDocObligations({
|
|
2518
|
+
obligations: singleBaselineObligation(),
|
|
2519
|
+
artifacts: {},
|
|
2520
|
+
baseline,
|
|
2521
|
+
executionRecords: singleBaselineRecord(baselineDigest, BASELINE_EXAMPLE_LATER),
|
|
2522
|
+
reviewRecords: []
|
|
2523
|
+
});
|
|
2524
|
+
if (stable.results.find(item => item.obligationId === 'doc-change:doc-lint').outcome !== 'pass') {
|
|
2525
|
+
failures.push(`${label}/stable: a stable validated tombstone did not pass`);
|
|
2526
|
+
}
|
|
2527
|
+
const stableLaterResult = stableLater.results.find(item => item.obligationId === 'doc-change:doc-lint');
|
|
2528
|
+
if (stableLaterResult.outcome !== 'none') failures.push(`${label}/stable: outcome=${stableLaterResult.outcome}`);
|
|
2529
|
+
if (!stableLater.diagnostics.some(item => item.code === 'stale-baseline-artifact-digest')) {
|
|
2530
|
+
failures.push(`${label}/stable: diagnostics=${stableLater.diagnostics.map(item => item.code)}`);
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
// A throwing baseline digest getter is a metadata error, not an escape.
|
|
2535
|
+
{
|
|
2536
|
+
const baseline = {
|
|
2537
|
+
digest: baselineDigest,
|
|
2538
|
+
artifacts: new Proxy({ [BASELINE_EXAMPLE]: undefined }, {
|
|
2539
|
+
getPrototypeOf() { return null; },
|
|
2540
|
+
get(t, prop, receiver) {
|
|
2541
|
+
if (prop === BASELINE_EXAMPLE) throw new Error('baseline digest getter');
|
|
2542
|
+
return Reflect.get(t, prop, receiver);
|
|
2543
|
+
}
|
|
2544
|
+
})
|
|
2545
|
+
};
|
|
2546
|
+
let out;
|
|
2547
|
+
try {
|
|
2548
|
+
out = target.checkDocObligations({
|
|
2549
|
+
obligations: singleBaselineObligation(),
|
|
2550
|
+
artifacts: {},
|
|
2551
|
+
baseline,
|
|
2552
|
+
executionRecords: singleBaselineRecord(baselineDigest, BASELINE_EXAMPLE_VALUE),
|
|
2553
|
+
reviewRecords: []
|
|
2554
|
+
});
|
|
2555
|
+
} catch (error) {
|
|
2556
|
+
failures.push(`${label}/throwing: escaped: ${error.message}`);
|
|
2557
|
+
continue;
|
|
2558
|
+
}
|
|
2559
|
+
const result = out.results.find(item => item.obligationId === 'doc-change:doc-lint');
|
|
2560
|
+
if (result.outcome === 'pass') failures.push(`${label}/throwing: a throwing getter produced a pass`);
|
|
2561
|
+
if (!out.diagnostics.some(item => item.code === 'baseline-artifact-invalid')) {
|
|
2562
|
+
failures.push(`${label}/throwing: diagnostics=${out.diagnostics.map(item => item.code)}`);
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
assert.deepStrictEqual(failures, [],
|
|
2567
|
+
`the validated baseline digest must be captured once: ${failures.join('; ')}`);
|
|
2568
|
+
});
|
|
2569
|
+
|
|
2570
|
+
checkCase('335 P2 the audit mirror reports the metadata-validated path snapshot, not a re-read', () => {
|
|
2571
|
+
const failures = [];
|
|
2572
|
+
for (const target of [modules, packageModules]) {
|
|
2573
|
+
const label = target === modules ? 'canonical' : 'packaged';
|
|
2574
|
+
const PATH_A = 'docs/adr/A.md';
|
|
2575
|
+
const PATH_B = 'docs/adr/B.md';
|
|
2576
|
+
const baselineDigest = sha256('audit-mirror whole baseline');
|
|
2577
|
+
const baseline = {
|
|
2578
|
+
digest: baselineDigest,
|
|
2579
|
+
artifacts: { [PATH_A]: BASELINE_EXAMPLE_VALUE, [PATH_B]: BASELINE_EXAMPLE_LATER }
|
|
2580
|
+
};
|
|
2581
|
+
|
|
2582
|
+
// Stateful path getter: metadata validates A (first read); the audit mirror must keep
|
|
2583
|
+
// describing A and must not re-read the getter.
|
|
2584
|
+
{
|
|
2585
|
+
let pathReads = 0;
|
|
2586
|
+
const entry = {
|
|
2587
|
+
get path() {
|
|
2588
|
+
pathReads += 1;
|
|
2589
|
+
return pathReads === 1 ? PATH_A : PATH_B;
|
|
2590
|
+
},
|
|
2591
|
+
reason: 'deleted'
|
|
2592
|
+
};
|
|
2593
|
+
const obligation = {
|
|
2594
|
+
id: 'doc-change:doc-lint',
|
|
2595
|
+
capability: 'doc-lint',
|
|
2596
|
+
level: 'MUST',
|
|
2597
|
+
applicability: 'applicable',
|
|
2598
|
+
scope: { paths: [PATH_A], baselinePaths: [entry] }
|
|
2599
|
+
};
|
|
2600
|
+
let out;
|
|
2601
|
+
try {
|
|
2602
|
+
out = target.checkDocObligations({
|
|
2603
|
+
obligations: [obligation],
|
|
2604
|
+
artifacts: {},
|
|
2605
|
+
baseline,
|
|
2606
|
+
executionRecords: singleBaselineRecord(baselineDigest, BASELINE_EXAMPLE_VALUE, PATH_A),
|
|
2607
|
+
reviewRecords: []
|
|
2608
|
+
});
|
|
2609
|
+
} catch (error) {
|
|
2610
|
+
failures.push(`${label}/stateful: escaped: ${error.message}`);
|
|
2611
|
+
continue;
|
|
2612
|
+
}
|
|
2613
|
+
const result = out.results.find(item => item.obligationId === 'doc-change:doc-lint');
|
|
2614
|
+
if (result.outcome !== 'pass') failures.push(`${label}/stateful: outcome=${result.outcome}`);
|
|
2615
|
+
if (result.baselineArtifacts[PATH_B] !== undefined) {
|
|
2616
|
+
failures.push(`${label}/stateful: audit ${JSON.stringify(result.baselineArtifacts)} describes the unvalidated path`);
|
|
2617
|
+
}
|
|
2618
|
+
if (result.baselineArtifacts[PATH_A] !== BASELINE_EXAMPLE_VALUE) {
|
|
2619
|
+
failures.push(`${label}/stateful: audit ${JSON.stringify(result.baselineArtifacts)} lost the validated digest`);
|
|
2620
|
+
}
|
|
2621
|
+
if (pathReads !== 1) failures.push(`${label}/stateful: path getter reads=${pathReads}`);
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
// Static-path positive control: a plain entry reports its validated snapshot.
|
|
2625
|
+
{
|
|
2626
|
+
const obligation = {
|
|
2627
|
+
id: 'doc-change:doc-lint',
|
|
2628
|
+
capability: 'doc-lint',
|
|
2629
|
+
level: 'MUST',
|
|
2630
|
+
applicability: 'applicable',
|
|
2631
|
+
scope: { paths: [PATH_A], baselinePaths: [{ path: PATH_A, reason: 'deleted' }] }
|
|
2632
|
+
};
|
|
2633
|
+
const out = target.checkDocObligations({
|
|
2634
|
+
obligations: [obligation],
|
|
2635
|
+
artifacts: {},
|
|
2636
|
+
baseline,
|
|
2637
|
+
executionRecords: singleBaselineRecord(baselineDigest, BASELINE_EXAMPLE_VALUE, PATH_A),
|
|
2638
|
+
reviewRecords: []
|
|
2639
|
+
});
|
|
2640
|
+
const result = out.results.find(item => item.obligationId === 'doc-change:doc-lint');
|
|
2641
|
+
if (result.outcome !== 'pass') failures.push(`${label}/static: outcome=${result.outcome}`);
|
|
2642
|
+
if (result.baselineArtifacts[PATH_A] !== BASELINE_EXAMPLE_VALUE) {
|
|
2643
|
+
failures.push(`${label}/static: audit=${JSON.stringify(result.baselineArtifacts)}`);
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
// Inapplicable obligations report the validated snapshot too and never re-read the entry.
|
|
2648
|
+
{
|
|
2649
|
+
let pathReads = 0;
|
|
2650
|
+
const entry = {
|
|
2651
|
+
get path() {
|
|
2652
|
+
pathReads += 1;
|
|
2653
|
+
return pathReads === 1 ? PATH_A : PATH_B;
|
|
2654
|
+
},
|
|
2655
|
+
reason: 'deleted'
|
|
2656
|
+
};
|
|
2657
|
+
const obligation = {
|
|
2658
|
+
id: 'doc-change:doc-lint',
|
|
2659
|
+
capability: 'doc-lint',
|
|
2660
|
+
level: 'MUST',
|
|
2661
|
+
applicability: 'N/A',
|
|
2662
|
+
scope: { paths: [PATH_A], baselinePaths: [entry] }
|
|
2663
|
+
};
|
|
2664
|
+
let out;
|
|
2665
|
+
try {
|
|
2666
|
+
out = target.checkDocObligations({
|
|
2667
|
+
obligations: [obligation],
|
|
2668
|
+
artifacts: {},
|
|
2669
|
+
baseline,
|
|
2670
|
+
executionRecords: [],
|
|
2671
|
+
reviewRecords: []
|
|
2672
|
+
});
|
|
2673
|
+
} catch (error) {
|
|
2674
|
+
failures.push(`${label}/N/A: escaped: ${error.message}`);
|
|
2675
|
+
continue;
|
|
2676
|
+
}
|
|
2677
|
+
const result = out.results.find(item => item.obligationId === 'doc-change:doc-lint');
|
|
2678
|
+
if (result.outcome !== 'none') failures.push(`${label}/N/A: outcome=${result.outcome}`);
|
|
2679
|
+
if (result.baselineArtifacts[PATH_B] !== undefined
|
|
2680
|
+
|| result.baselineArtifacts[PATH_A] !== BASELINE_EXAMPLE_VALUE) {
|
|
2681
|
+
failures.push(`${label}/N/A: audit=${JSON.stringify(result.baselineArtifacts)}`);
|
|
2682
|
+
}
|
|
2683
|
+
if (pathReads !== 1) failures.push(`${label}/N/A: path getter reads=${pathReads}`);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
assert.deepStrictEqual(failures, [],
|
|
2687
|
+
`the audit mirror must use the validated path snapshot: ${failures.join('; ')}`);
|
|
2688
|
+
});
|
|
2689
|
+
|
|
2690
|
+
helper.finish();
|