harness-everything 0.36.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/CHANGELOG.md +27 -0
- package/ci/mechanism-335-baseline-evidence.test.js +2690 -0
- package/docs/doc-evidence-baseline-binding.md +249 -0
- package/docs/document-lifecycle.md +2 -2
- 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/plugin.json +1 -1
- package/package.json +1 -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
|
@@ -6,6 +6,24 @@ function isPlainObject(value) {
|
|
|
6
6
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
// #335 P3: the candidate inventory provenance check is deliberately stricter than the shared
|
|
10
|
+
// `isPlainObject` helper. Baseline-only evidence requires a real plain object or a
|
|
11
|
+
// null-prototype dictionary; object-like values (Map, Date, RegExp, class instances) must not
|
|
12
|
+
// masquerade as an empty inventory. The shared normalizer and `artifactMap` fallback remain
|
|
13
|
+
// unchanged for non-baseline callers.
|
|
14
|
+
// A Proxy (or host object) whose `getPrototypeOf` trap throws cannot be proven to be a plain
|
|
15
|
+
// object, so it is classified as unknown inventory rather than aborting the whole check.
|
|
16
|
+
function isCandidateInventory(value) {
|
|
17
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return false;
|
|
18
|
+
let prototype;
|
|
19
|
+
try {
|
|
20
|
+
prototype = Object.getPrototypeOf(value);
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return prototype === Object.prototype || prototype === null;
|
|
25
|
+
}
|
|
26
|
+
|
|
9
27
|
function isRecord(value) {
|
|
10
28
|
return isPlainObject(value)
|
|
11
29
|
&& typeof value.id === 'string' && value.id.length > 0
|
|
@@ -16,6 +34,22 @@ function nonEmptyString(value) {
|
|
|
16
34
|
return typeof value === 'string' && value.trim().length > 0;
|
|
17
35
|
}
|
|
18
36
|
|
|
37
|
+
// #335: per-path baseline digests are SHA256 values. Trust still follows the whole-baseline
|
|
38
|
+
// identity, so an invalid per-path value fails the baseline path closed.
|
|
39
|
+
const SHA256_DIGEST = /^[a-f0-9]{64}$/i;
|
|
40
|
+
|
|
41
|
+
function isSha256(value) {
|
|
42
|
+
return typeof value === 'string' && SHA256_DIGEST.test(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// #335 P2: per-path baseline and rename evidence must be carried explicitly. Inherited
|
|
46
|
+
// properties (including prototype-injected digests) are not declared evidence, so every
|
|
47
|
+
// trust-boundary map lookup uses own-key membership rather than bracket lookup alone.
|
|
48
|
+
function hasOwnEntry(map, key) {
|
|
49
|
+
return Boolean(map) && typeof map === 'object' && typeof key === 'string'
|
|
50
|
+
&& Object.prototype.hasOwnProperty.call(map, key);
|
|
51
|
+
}
|
|
52
|
+
|
|
19
53
|
function compareText(left, right) {
|
|
20
54
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
21
55
|
}
|
|
@@ -36,6 +70,12 @@ function artifact(entry) {
|
|
|
36
70
|
return entry && typeof entry.digest === 'string' && entry.digest.length > 0 ? entry.digest : '';
|
|
37
71
|
}
|
|
38
72
|
|
|
73
|
+
// #335 P2: a baseline-bound rename destination must declare its own digest. An inherited
|
|
74
|
+
// `digest` (for example Object.create({ digest })) is not explicit candidate evidence.
|
|
75
|
+
function ownArtifactDigest(entry) {
|
|
76
|
+
return isPlainObject(entry) && hasOwnEntry(entry, 'digest') ? artifact(entry) : '';
|
|
77
|
+
}
|
|
78
|
+
|
|
39
79
|
function provenanceText(record) {
|
|
40
80
|
return nonEmptyString(record.provenance) ? record.provenance : `record ${record.id}`;
|
|
41
81
|
}
|
|
@@ -86,7 +126,22 @@ function prdSetDigest(artifactMap) {
|
|
|
86
126
|
function checkDocObligations({ obligations, artifacts, baseline, executionRecords, reviewRecords } = {}) {
|
|
87
127
|
const diagnostics = [];
|
|
88
128
|
const results = [];
|
|
129
|
+
// P3: distinguish a caller-supplied plain-object or null-prototype inventory (including an
|
|
130
|
+
// explicit {}) from an absent, malformed, or object-like value that the pre-existing
|
|
131
|
+
// normalizer would coerce to (or treat as) an empty map. The provenance check is deferred to
|
|
132
|
+
// applicable obligations that actually declare baseline paths, so non-baseline and
|
|
133
|
+
// inapplicable callers never read the candidate inventory's prototype (a throwing Proxy trap
|
|
134
|
+
// must not abort an unrelated obligation). It is resolved at most once so every obligation
|
|
135
|
+
// sees the same classification. `artifactMap` keeps the original `isPlainObject` fallback for
|
|
136
|
+
// non-baseline callers.
|
|
89
137
|
const artifactMap = isPlainObject(artifacts) ? artifacts : {};
|
|
138
|
+
let candidateInventoryKnown;
|
|
139
|
+
const isCandidateInventoryKnown = () => {
|
|
140
|
+
if (candidateInventoryKnown === undefined) {
|
|
141
|
+
candidateInventoryKnown = isCandidateInventory(artifacts);
|
|
142
|
+
}
|
|
143
|
+
return candidateInventoryKnown;
|
|
144
|
+
};
|
|
90
145
|
const baselineDigest = isPlainObject(baseline) && nonEmptyString(baseline.digest) ? baseline.digest : '';
|
|
91
146
|
const executionList = Array.isArray(executionRecords) ? executionRecords.filter(isRecord) : [];
|
|
92
147
|
const reviewList = Array.isArray(reviewRecords) ? reviewRecords.filter(isRecord) : [];
|
|
@@ -105,21 +160,287 @@ function checkDocObligations({ obligations, artifacts, baseline, executionRecord
|
|
|
105
160
|
|
|
106
161
|
for (const obligation of obligationList) {
|
|
107
162
|
results.push(evaluateObligation(obligation, {
|
|
108
|
-
artifactMap, baselineDigest, executionList, reviewList, diagnostics
|
|
163
|
+
artifactMap, isCandidateInventoryKnown, baseline, baselineDigest, executionList, reviewList, diagnostics
|
|
109
164
|
}));
|
|
110
165
|
}
|
|
111
166
|
return { diagnostics, results };
|
|
112
167
|
}
|
|
113
168
|
|
|
114
169
|
function evaluateObligation(obligation, context) {
|
|
115
|
-
const { artifactMap, baselineDigest, executionList, reviewList, diagnostics } = context;
|
|
170
|
+
const { artifactMap, isCandidateInventoryKnown, baseline, baselineDigest, executionList, reviewList, diagnostics } = context;
|
|
116
171
|
const capability = typeof obligation.capability === 'string' ? obligation.capability : '';
|
|
117
172
|
const obligationId = nonEmptyString(obligation.id) ? obligation.id : capability;
|
|
118
173
|
const scopePaths = scopePathsOf(obligation);
|
|
119
174
|
const applicability = obligation.applicability;
|
|
120
175
|
|
|
176
|
+
// #335: baseline-only paths are declared by the obligation. Every declared entry is validated
|
|
177
|
+
// before any outcome is decided; a malformed or contradictory annotation fails the whole
|
|
178
|
+
// obligation closed instead of being silently ignored.
|
|
179
|
+
const scopeObject = isPlainObject(obligation.scope) ? obligation.scope : null;
|
|
180
|
+
const declaresBaselinePaths = Boolean(scopeObject && Object.prototype.hasOwnProperty.call(scopeObject, 'baselinePaths'));
|
|
181
|
+
const declaredBaselinePaths = declaresBaselinePaths ? scopeObject.baselinePaths : undefined;
|
|
182
|
+
const baselineArtifactsMap = isPlainObject(baseline) && isPlainObject(baseline.artifacts) ? baseline.artifacts : null;
|
|
183
|
+
const baselineEntries = new Map();
|
|
184
|
+
// #340: every accepted entry sharing a rename destination must validate the same own digest,
|
|
185
|
+
// or the shared-destination snapshot is contradictory and the obligation fails closed.
|
|
186
|
+
const validatedDestinationDigestMap = new Map();
|
|
187
|
+
// #340 P2(r4320818749): the validated per-path baseline digest is captured once during
|
|
188
|
+
// metadata validation. The audit mirror, the stale-baseline diagnostics, and baselineBound()
|
|
189
|
+
// reuse that captured value instead of re-reading the caller map, so a stateful own getter
|
|
190
|
+
// cannot pass evidence against a value it started returning only after validation, and a
|
|
191
|
+
// later-throwing read cannot escape the checker.
|
|
192
|
+
const validatedBaselineDigests = new Map();
|
|
193
|
+
let baselineMetadataError = false;
|
|
194
|
+
let candidateInventoryUnknown = false;
|
|
195
|
+
// #335 P3: a candidate inventory can pass the prototype check and still throw from an
|
|
196
|
+
// own-property descriptor trap or a nested digest accessor. Any such failure is treated as
|
|
197
|
+
// unknown inventory rather than escaping the checker; the diagnostic is emitted only for
|
|
198
|
+
// applicable obligations that declare baseline-only paths.
|
|
199
|
+
let candidateInventoryInspectionFailed = false;
|
|
200
|
+
|
|
201
|
+
const baselineMetadataDiagnostic = (code, path, message) => {
|
|
202
|
+
baselineMetadataError = true;
|
|
203
|
+
diagnostics.push({ code, obligationId, ...(path ? { path } : {}), message });
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
if (declaresBaselinePaths) {
|
|
207
|
+
if (!Array.isArray(declaredBaselinePaths)) {
|
|
208
|
+
baselineMetadataDiagnostic('baseline-path-invalid', undefined,
|
|
209
|
+
'scope.baselinePaths is present but not an array; baseline-bound evidence cannot be evaluated');
|
|
210
|
+
} else if (declaredBaselinePaths.length) {
|
|
211
|
+
if (!nonEmptyString(baselineDigest)) {
|
|
212
|
+
baselineMetadataDiagnostic('baseline-identity-missing', undefined,
|
|
213
|
+
'baselinePaths are declared but the baseline carries no whole-baseline digest; baseline-bound evidence cannot be trusted');
|
|
214
|
+
}
|
|
215
|
+
if (!baselineArtifactsMap) {
|
|
216
|
+
baselineMetadataDiagnostic('baseline-artifacts-missing', undefined,
|
|
217
|
+
'baselinePaths are declared but the baseline carries no artifacts map; per-path baseline digests are unavailable');
|
|
218
|
+
}
|
|
219
|
+
const seen = new Set();
|
|
220
|
+
for (const rawEntry of declaredBaselinePaths) {
|
|
221
|
+
const invalid = (path, reason) => baselineMetadataDiagnostic('baseline-path-invalid', path,
|
|
222
|
+
`baselinePaths entry ${path ? JSON.stringify(path) : '(malformed)'} is invalid: ${reason}; the path fails closed`);
|
|
223
|
+
if (!isPlainObject(rawEntry)) {
|
|
224
|
+
invalid(undefined, 'it is not an object with a non-empty path');
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
// #340: entry fields are snapshotted through one guarded inspection. A Proxy entry
|
|
228
|
+
// whose `path`, `reason`, or `renameTo` getter or descriptor trap throws must not
|
|
229
|
+
// escape the checker; the inspection failure itself fails the entry closed as a
|
|
230
|
+
// metadata error, and each field is carried exactly once so a stateful accessor
|
|
231
|
+
// cannot make validation and capture disagree.
|
|
232
|
+
let snapshot = null;
|
|
233
|
+
try {
|
|
234
|
+
snapshot = {
|
|
235
|
+
path: rawEntry.path,
|
|
236
|
+
reason: rawEntry.reason,
|
|
237
|
+
hasOwnRenameTo: Object.prototype.hasOwnProperty.call(rawEntry, 'renameTo'),
|
|
238
|
+
renameTo: rawEntry.renameTo
|
|
239
|
+
};
|
|
240
|
+
} catch {
|
|
241
|
+
snapshot = null;
|
|
242
|
+
}
|
|
243
|
+
if (!snapshot) {
|
|
244
|
+
invalid(undefined, 'its entry fields could not be inspected safely (a getter or descriptor trap threw); the entry fails closed');
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (!nonEmptyString(snapshot.path)) {
|
|
248
|
+
invalid(undefined, 'it is not an object with a non-empty path');
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const entryPath = snapshot.path;
|
|
252
|
+
if (seen.has(entryPath)) {
|
|
253
|
+
invalid(entryPath, 'the same path is declared more than once');
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
seen.add(entryPath);
|
|
257
|
+
if (!scopePaths.includes(entryPath)) {
|
|
258
|
+
invalid(entryPath, 'it is not a member of scope.paths');
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const entryReason = snapshot.reason;
|
|
262
|
+
if (entryReason !== 'deleted' && entryReason !== 'rename-from') {
|
|
263
|
+
invalid(entryPath, `reason ${JSON.stringify(entryReason)} is not deleted or rename-from`);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (entryReason === 'deleted' && snapshot.hasOwnRenameTo) {
|
|
267
|
+
invalid(entryPath, 'a deleted entry must not carry renameTo');
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
// #340: the validated rename destination digest is carried on the entry; the
|
|
271
|
+
// destination candidate map is not read again after metadata validation.
|
|
272
|
+
let renameDestinationDigest = '';
|
|
273
|
+
if (entryReason === 'rename-from') {
|
|
274
|
+
// #340: the destination annotation is snapshotted once; a stateful getter cannot
|
|
275
|
+
// make the two validation reads disagree.
|
|
276
|
+
const entryRenameTo = snapshot.renameTo;
|
|
277
|
+
if (!nonEmptyString(entryRenameTo) || entryRenameTo === entryPath) {
|
|
278
|
+
invalid(entryPath, 'a rename-from entry needs a distinct non-empty renameTo');
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (!scopePaths.includes(entryRenameTo)) {
|
|
282
|
+
invalid(entryPath, `its rename destination ${JSON.stringify(entryRenameTo)} is not a member of scope.paths`);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
// #340: destination candidate inspection stays applicability-gated. N/A and unknown
|
|
286
|
+
// obligations never bind evidence, so consuming their destination digest getter
|
|
287
|
+
// during metadata validation would shift the legacy audit identity (a stateful
|
|
288
|
+
// getter reports a later value on the second artifact read). Their audit identity
|
|
289
|
+
// keeps the un-gated legacy candidate read; only applicable obligations capture and
|
|
290
|
+
// validate the destination snapshot.
|
|
291
|
+
if (applicability === 'applicable') {
|
|
292
|
+
let destinationDigest = '';
|
|
293
|
+
try {
|
|
294
|
+
if (hasOwnEntry(artifactMap, entryRenameTo)) {
|
|
295
|
+
destinationDigest = ownArtifactDigest(artifactMap[entryRenameTo]);
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
candidateInventoryInspectionFailed = true;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
// #340: the captured own value must be a usable non-empty string; `artifact()` can read
|
|
302
|
+
// a stateful accessor three times, so a non-string result is not carried or seeded.
|
|
303
|
+
if (!nonEmptyString(destinationDigest)) {
|
|
304
|
+
invalid(entryPath, `its rename destination ${JSON.stringify(entryRenameTo)} has no candidate artifact`);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
// #340: entries that share one rename destination must agree on the validated own
|
|
308
|
+
// digest. A conflicting validated value is a contradictory snapshot and fails the
|
|
309
|
+
// whole obligation closed before any binding; equal values are allowed.
|
|
310
|
+
const sharedDestinationDigest = validatedDestinationDigestMap.get(entryRenameTo);
|
|
311
|
+
if (sharedDestinationDigest === undefined) {
|
|
312
|
+
validatedDestinationDigestMap.set(entryRenameTo, destinationDigest);
|
|
313
|
+
} else if (sharedDestinationDigest !== destinationDigest) {
|
|
314
|
+
invalid(entryPath, `its rename destination ${JSON.stringify(entryRenameTo)} was validated to conflicting own digests by another accepted baseline entry; the shared destination snapshot is not consistent`);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
renameDestinationDigest = destinationDigest;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
let candidatePathPresent = false;
|
|
321
|
+
try {
|
|
322
|
+
candidatePathPresent = hasOwnEntry(artifactMap, entryPath);
|
|
323
|
+
} catch {
|
|
324
|
+
candidateInventoryInspectionFailed = true;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (candidatePathPresent) {
|
|
328
|
+
baselineMetadataDiagnostic('baseline-path-candidate-present', entryPath,
|
|
329
|
+
`baseline path ${entryPath} is declared baseline-only but the candidate artifact map has an entry for it (own key, regardless of digest validity); the contradictory path state fails closed`);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (baselineArtifactsMap) {
|
|
333
|
+
let trusted;
|
|
334
|
+
try {
|
|
335
|
+
trusted = hasOwnEntry(baselineArtifactsMap, entryPath) ? baselineArtifactsMap[entryPath] : undefined;
|
|
336
|
+
} catch {
|
|
337
|
+
// #340 P2: a throwing baseline inspection is a metadata error, never an escape.
|
|
338
|
+
baselineMetadataDiagnostic('baseline-artifact-invalid', entryPath,
|
|
339
|
+
`the baseline digest for ${entryPath} could not be inspected safely (a getter or descriptor trap threw); the path fails closed`);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (trusted === undefined) {
|
|
343
|
+
baselineMetadataDiagnostic('baseline-artifact-missing', entryPath,
|
|
344
|
+
`the baseline carries no per-path digest for declared baseline path ${entryPath}; the path fails closed`);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (!isSha256(trusted)) {
|
|
348
|
+
baselineMetadataDiagnostic('baseline-artifact-invalid', entryPath,
|
|
349
|
+
`the baseline digest for ${entryPath} is not a SHA256 value; the path fails closed`);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
validatedBaselineDigests.set(entryPath, trusted);
|
|
353
|
+
}
|
|
354
|
+
baselineEntries.set(entryPath, {
|
|
355
|
+
path: entryPath,
|
|
356
|
+
reason: entryReason,
|
|
357
|
+
// #340: the metadata check above confirmed this path has no own-key candidate entry;
|
|
358
|
+
// carry that validated source-absence proof explicitly (never spread from rawEntry) so
|
|
359
|
+
// binding and the audit identity reuse it instead of re-inspecting the candidate map.
|
|
360
|
+
candidatePathPresent: false,
|
|
361
|
+
// #340 P2: carry the exact validated SHA256 so binding never re-reads the
|
|
362
|
+
// caller-owned baseline map.
|
|
363
|
+
trustedBaselineDigest: validatedBaselineDigests.get(entryPath),
|
|
364
|
+
...(entryReason === 'rename-from' ? { renameTo: snapshot.renameTo, renameDestinationDigest } : {})
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// #335 P3: a mixed scope combines baseline-only paths with ordinary candidate paths. Capture
|
|
371
|
+
// every ordinary scoped candidate digest before evidence binding, so a throwing accessor in
|
|
372
|
+
// the legacy candidate lane fails the inventory closed instead of aborting the obligation.
|
|
373
|
+
const candidateDigestCache = new Map();
|
|
374
|
+
if (applicability === 'applicable' && Array.isArray(declaredBaselinePaths)
|
|
375
|
+
&& declaredBaselinePaths.length > 0) {
|
|
376
|
+
// #340: a rename destination was already validated as an own-key candidate entry carrying
|
|
377
|
+
// its own digest during metadata. Seed the captured snapshot from that validated value and
|
|
378
|
+
// never read the destination through the raw candidate map again, so a destination accessor
|
|
379
|
+
// that throws or changes after validation cannot make capture/binding/audit disagree. The
|
|
380
|
+
// seeded value is the validated own digest, never a caller-asserted rawEntry field.
|
|
381
|
+
for (const entry of baselineEntries.values()) {
|
|
382
|
+
if (entry.reason === 'rename-from' && nonEmptyString(entry.renameDestinationDigest)
|
|
383
|
+
&& !candidateDigestCache.has(entry.renameTo)) {
|
|
384
|
+
candidateDigestCache.set(entry.renameTo, entry.renameDestinationDigest);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
for (const path of scopePaths) {
|
|
388
|
+
if (baselineEntries.has(path) || candidateDigestCache.has(path)) continue;
|
|
389
|
+
try {
|
|
390
|
+
candidateDigestCache.set(path, artifact(artifactMap[path]));
|
|
391
|
+
} catch {
|
|
392
|
+
candidateInventoryInspectionFailed = true;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// P3: an applicable obligation that actually declares baseline-only paths needs a known
|
|
398
|
+
// candidate inventory. Explicit {} is known and remains the legitimate empty deletion
|
|
399
|
+
// inventory; omitted/undefined/null/array/string/other malformed values are unknown input.
|
|
400
|
+
// The prototype read happens here, not up front, so unrelated obligations never touch it.
|
|
401
|
+
if (applicability === 'applicable' && Array.isArray(declaredBaselinePaths)
|
|
402
|
+
&& declaredBaselinePaths.length > 0
|
|
403
|
+
&& (candidateInventoryInspectionFailed || !isCandidateInventoryKnown())) {
|
|
404
|
+
candidateInventoryUnknown = true;
|
|
405
|
+
diagnostics.push({
|
|
406
|
+
code: 'candidate-artifacts-invalid',
|
|
407
|
+
obligationId,
|
|
408
|
+
message: 'this obligation declares baseline-only paths, but the top-level artifacts argument is not a plain object or cannot be inspected safely; the candidate inventory is unknown, so baseline-bound evidence fails closed'
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const baselineAudit = {};
|
|
413
|
+
// #340 P2(r4320941809): the audit mirror is built from the metadata-validated snapshot
|
|
414
|
+
// (`validatedBaselineDigests`, populated during the per-entry guard), never from a re-read
|
|
415
|
+
// of the raw entry path or a fallback inspection of the live baseline map. A stateful
|
|
416
|
+
// `path` accessor therefore cannot make a passing result describe a different baseline
|
|
417
|
+
// identity than the one that validated and bound the evidence, and each declared path
|
|
418
|
+
// contributes its snapshot exactly once in validation order.
|
|
419
|
+
for (const [baselineAuditPath, baselineAuditDigest] of validatedBaselineDigests) {
|
|
420
|
+
baselineAudit[baselineAuditPath] = baselineAuditDigest;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// An uninspectable candidate entry must not abort an N/A or unknown obligation while the
|
|
424
|
+
// audit key is built; the baseline-bound lanes fail closed through the inventory diagnostic.
|
|
425
|
+
// When a baseline-annotated obligation captured the ordinary candidate digests before binding
|
|
426
|
+
// (candidateDigestCache), the audit identity and stale diagnostics use that same snapshot, so a
|
|
427
|
+
// nested digest accessor that only later throws or changes can never make the audit key or the
|
|
428
|
+
// stale verdict disagree with the digest that actually bound evidence. Untouched paths and
|
|
429
|
+
// unannotated callers keep the legacy read.
|
|
430
|
+
const candidateDigestOf = (path) => {
|
|
431
|
+
if (candidateDigestCache.has(path)) return candidateDigestCache.get(path);
|
|
432
|
+
// #340: only an applicable baseline-annotated obligation validated the source absence as
|
|
433
|
+
// part of its captured snapshot. `N/A` and `unknown` obligations never bind evidence, so they
|
|
434
|
+
// keep the legacy candidate read for their audit identity and stay `outcome: 'none'`.
|
|
435
|
+
if (applicability === 'applicable' && baselineEntries.has(path)) return '';
|
|
436
|
+
try {
|
|
437
|
+
return artifact(artifactMap[path]);
|
|
438
|
+
} catch {
|
|
439
|
+
return '';
|
|
440
|
+
}
|
|
441
|
+
};
|
|
121
442
|
const artifactDigestKey = scopePaths.length
|
|
122
|
-
? scopePaths.map(path => `${path}=${
|
|
443
|
+
? scopePaths.map(path => `${path}=${candidateDigestOf(path) || 'missing'}`).join('|')
|
|
123
444
|
: (baselineDigest || 'no-scope-paths');
|
|
124
445
|
const baseDigestKey = baselineDigest || 'no-baseline';
|
|
125
446
|
const result = (observation, outcome, provenance, recordRefs) => ({
|
|
@@ -128,6 +449,7 @@ function evaluateObligation(obligation, context) {
|
|
|
128
449
|
outcome,
|
|
129
450
|
artifactDigest: artifactDigestKey,
|
|
130
451
|
baseDigest: baseDigestKey,
|
|
452
|
+
...(declaresBaselinePaths ? { baselineArtifacts: { ...baselineAudit } } : {}),
|
|
131
453
|
provenance,
|
|
132
454
|
recordRefs: [...recordRefs]
|
|
133
455
|
});
|
|
@@ -141,10 +463,65 @@ function evaluateObligation(obligation, context) {
|
|
|
141
463
|
}
|
|
142
464
|
|
|
143
465
|
const matching = [...executionList, ...reviewList].filter(item => item.obligationId === obligationId);
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
466
|
+
const candidateBound = (item, path) => {
|
|
467
|
+
if (!nonEmptyString(item.baseDigest) || item.baseDigest !== baselineDigest) return false;
|
|
468
|
+
if (!isPlainObject(item.artifactDigests)) return false;
|
|
469
|
+
if (candidateDigestCache.has(path)) {
|
|
470
|
+
// Baseline-annotated obligations use the digest captured before binding, so a mixed
|
|
471
|
+
// delete+edit scope never re-reads a throwing accessor from the legacy lane.
|
|
472
|
+
const digest = candidateDigestCache.get(path);
|
|
473
|
+
return digest.length > 0
|
|
474
|
+
&& item.artifactDigests[path] === digest
|
|
475
|
+
&& Array.isArray(item.paths) && item.paths.includes(path);
|
|
476
|
+
}
|
|
477
|
+
return artifact(artifactMap[path]).length > 0
|
|
478
|
+
&& item.artifactDigests[path] === artifactMap[path].digest
|
|
479
|
+
&& Array.isArray(item.paths) && item.paths.includes(path);
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
// A baseline-only path is satisfied only by a record bound to the trusted baseline digest.
|
|
483
|
+
// A rename-from path additionally needs the same single record to carry the current
|
|
484
|
+
// destination candidate digest, so destination-only, source-only and split records fail.
|
|
485
|
+
const baselineBound = (item, entry) => {
|
|
486
|
+
if (!nonEmptyString(item.baseDigest) || item.baseDigest !== baselineDigest) return false;
|
|
487
|
+
if (!Array.isArray(item.paths) || !item.paths.includes(entry.path)) return false;
|
|
488
|
+
// #340: metadata validation confirmed this baseline-only path has no own-key candidate
|
|
489
|
+
// entry. Reuse that carried absence proof instead of re-inspecting the candidate map, so a
|
|
490
|
+
// descriptor trap that only throws on a later inspection cannot abort the check. A path
|
|
491
|
+
// whose metadata was malformed never reaches here; it fails closed earlier.
|
|
492
|
+
if (entry.candidatePathPresent !== false) return false;
|
|
493
|
+
if (isPlainObject(item.artifactDigests)
|
|
494
|
+
&& Object.prototype.hasOwnProperty.call(item.artifactDigests, entry.path)) return false;
|
|
495
|
+
// #340 P2: binding compares the validated baseline digest captured during metadata, never
|
|
496
|
+
// a fresh read of the caller-owned baseline map; a stateful getter that started returning
|
|
497
|
+
// a different value after validation cannot make a stale record bind.
|
|
498
|
+
const trusted = entry.trustedBaselineDigest;
|
|
499
|
+
if (!isSha256(trusted)) return false;
|
|
500
|
+
if (!isPlainObject(item.baselineArtifacts)
|
|
501
|
+
|| !hasOwnEntry(item.baselineArtifacts, entry.path)
|
|
502
|
+
|| item.baselineArtifacts[entry.path] !== trusted) return false;
|
|
503
|
+
if (entry.reason === 'rename-from') {
|
|
504
|
+
if (!item.paths.includes(entry.renameTo)) return false;
|
|
505
|
+
if (!isPlainObject(item.artifactDigests)
|
|
506
|
+
|| !hasOwnEntry(item.artifactDigests, entry.renameTo)
|
|
507
|
+
|| !nonEmptyString(item.artifactDigests[entry.renameTo])) return false;
|
|
508
|
+
// #340 P3: the destination binds to the same captured candidate digest that the ordinary
|
|
509
|
+
// candidate lane, the audit identity, and the stale diagnostics use, so a stateful nested
|
|
510
|
+
// accessor cannot make binding disagree with them. Metadata validation already required the
|
|
511
|
+
// destination to be an own-key candidate inventory entry carrying its own digest
|
|
512
|
+
// (ownArtifactDigest), so a non-empty captured digest is required here and the accessor is
|
|
513
|
+
// never re-read; a missing destination skips capture and lands on this guard.
|
|
514
|
+
const capturedDestination = candidateDigestCache.get(entry.renameTo);
|
|
515
|
+
if (!nonEmptyString(capturedDestination)) return false;
|
|
516
|
+
if (item.artifactDigests[entry.renameTo] !== capturedDestination) return false;
|
|
517
|
+
}
|
|
518
|
+
return true;
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const boundToScopePath = (item, path) => {
|
|
522
|
+
const entry = baselineEntries.get(path);
|
|
523
|
+
return entry ? baselineBound(item, entry) : candidateBound(item, path);
|
|
524
|
+
};
|
|
148
525
|
|
|
149
526
|
for (const item of matching) {
|
|
150
527
|
if (executionList.includes(item) && !completeExecution(item)) {
|
|
@@ -166,8 +543,8 @@ function evaluateObligation(obligation, context) {
|
|
|
166
543
|
continue;
|
|
167
544
|
}
|
|
168
545
|
if (Array.isArray(item.paths) && isPlainObject(item.artifactDigests)) {
|
|
169
|
-
const mismatch = item.paths.find(path =>
|
|
170
|
-
&& nonEmptyString(item.artifactDigests[path]) && item.artifactDigests[path] !==
|
|
546
|
+
const mismatch = item.paths.find(path => candidateDigestOf(path).length > 0
|
|
547
|
+
&& nonEmptyString(item.artifactDigests[path]) && item.artifactDigests[path] !== candidateDigestOf(path));
|
|
171
548
|
if (mismatch) {
|
|
172
549
|
diagnostics.push({
|
|
173
550
|
code: 'stale-artifact-digest',
|
|
@@ -178,12 +555,65 @@ function evaluateObligation(obligation, context) {
|
|
|
178
555
|
}
|
|
179
556
|
}
|
|
180
557
|
|
|
558
|
+
// #335: a record that names a baseline-only path but carries no per-path baseline digest is
|
|
559
|
+
// visible as an attempt, and a record whose per-path digest disagrees with the trusted value
|
|
560
|
+
// is stale. Both fail closed through missing-path-coverage and stale-baseline-artifact-digest.
|
|
561
|
+
for (const item of matching) {
|
|
562
|
+
const baselineNamed = Array.isArray(item.paths) ? item.paths.filter(path => baselineEntries.has(path)) : [];
|
|
563
|
+
if (isPlainObject(item.baselineArtifacts)) {
|
|
564
|
+
for (const [path, value] of Object.entries(item.baselineArtifacts)) {
|
|
565
|
+
if (!baselineEntries.has(path)) continue;
|
|
566
|
+
// #340 P2: the stale comparison uses the baseline digest captured during metadata,
|
|
567
|
+
// never a fresh read of the caller-owned map.
|
|
568
|
+
const trusted = validatedBaselineDigests.has(path)
|
|
569
|
+
? validatedBaselineDigests.get(path)
|
|
570
|
+
: undefined;
|
|
571
|
+
if (!isSha256(trusted)) continue;
|
|
572
|
+
if (nonEmptyString(value) && value !== trusted) {
|
|
573
|
+
diagnostics.push({
|
|
574
|
+
code: 'stale-baseline-artifact-digest',
|
|
575
|
+
obligationId,
|
|
576
|
+
message: `record ${item.id} carries a baseline digest for path ${path} that differs from the trusted baseline value`
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
for (const path of baselineNamed) {
|
|
582
|
+
const bound = isPlainObject(item.baselineArtifacts)
|
|
583
|
+
&& hasOwnEntry(item.baselineArtifacts, path)
|
|
584
|
+
&& nonEmptyString(item.baselineArtifacts[path]);
|
|
585
|
+
if (!bound) {
|
|
586
|
+
diagnostics.push({
|
|
587
|
+
code: 'baseline-binding-missing',
|
|
588
|
+
obligationId,
|
|
589
|
+
message: `record ${item.id} names baseline path ${path} but carries no trusted baseline digest for it`
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
if (isPlainObject(item.artifactDigests)
|
|
593
|
+
&& Object.prototype.hasOwnProperty.call(item.artifactDigests, path)) {
|
|
594
|
+
diagnostics.push({
|
|
595
|
+
code: 'baseline-path-candidate-digest',
|
|
596
|
+
obligationId,
|
|
597
|
+
message: `record ${item.id} carries a candidate artifactDigests entry for baseline-only path ${path}; the contradictory record cannot bind that path`
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (baselineMetadataError || candidateInventoryUnknown) {
|
|
604
|
+
return result('unresolved', 'none',
|
|
605
|
+
candidateInventoryUnknown && !baselineMetadataError
|
|
606
|
+
? 'the top-level candidate artifacts inventory is absent or malformed; baseline-bound evidence fails closed'
|
|
607
|
+
: 'baseline path metadata is missing, invalid, or contradictory; baseline-bound evidence fails closed',
|
|
608
|
+
[]);
|
|
609
|
+
}
|
|
610
|
+
|
|
181
611
|
const usedExec = matching.filter(item => executionList.includes(item)
|
|
182
612
|
&& completeExecution(item)
|
|
183
|
-
&& scopePaths.some(path =>
|
|
613
|
+
&& scopePaths.some(path => boundToScopePath(item, path)));
|
|
184
614
|
const failingExec = usedExec.filter(item => item.exitCode !== 0);
|
|
185
615
|
const boundReviews = matching.filter(item => reviewList.includes(item) && item.kind === 'review'
|
|
186
|
-
&& scopePaths.some(path =>
|
|
616
|
+
&& scopePaths.some(path => boundToScopePath(item, path)));
|
|
187
617
|
const validReviews = boundReviews.filter(item => item.verdict === 'approve' && nonEmptyString(item.summary));
|
|
188
618
|
const invalidReviews = boundReviews.filter(item => !validReviews.includes(item));
|
|
189
619
|
const oddKindRecords = matching.filter(item => reviewList.includes(item)
|
|
@@ -204,13 +634,16 @@ function evaluateObligation(obligation, context) {
|
|
|
204
634
|
});
|
|
205
635
|
}
|
|
206
636
|
|
|
207
|
-
const boundAny = (path) => matching.some(item =>
|
|
637
|
+
const boundAny = (path) => matching.some(item => boundToScopePath(item, path));
|
|
208
638
|
const unmetPaths = scopePaths.filter(path => !boundAny(path));
|
|
209
639
|
for (const path of unmetPaths) {
|
|
640
|
+
const entry = baselineEntries.get(path);
|
|
210
641
|
diagnostics.push({
|
|
211
642
|
code: 'missing-path-coverage',
|
|
212
643
|
obligationId,
|
|
213
|
-
message:
|
|
644
|
+
message: entry
|
|
645
|
+
? `no observed record is bound to baseline path ${path} (${entry.reason}${entry.renameTo ? ` -> ${entry.renameTo}` : ''}) with the trusted baseline digest`
|
|
646
|
+
: `no observed record is bound to path ${path} with the current candidate and baseline digests`
|
|
214
647
|
});
|
|
215
648
|
}
|
|
216
649
|
|
|
@@ -219,7 +652,7 @@ function evaluateObligation(obligation, context) {
|
|
|
219
652
|
const target = recordIndex.get(item);
|
|
220
653
|
return Boolean(target) && target.obligationId === obligationId
|
|
221
654
|
&& executionList.includes(target) && completeExecution(target) && target.exitCode === 0
|
|
222
|
-
&& scopePaths.some(path =>
|
|
655
|
+
&& scopePaths.some(path => boundToScopePath(target, path));
|
|
223
656
|
};
|
|
224
657
|
const waiverCandidates = reviewList.filter(item => item.kind === 'waiver'
|
|
225
658
|
&& item.obligationId === obligationId);
|
|
@@ -321,14 +754,14 @@ function evaluateObligation(obligation, context) {
|
|
|
321
754
|
[]);
|
|
322
755
|
}
|
|
323
756
|
|
|
324
|
-
const waiverSelfBound = item => scopePaths.length > 0 && scopePaths.every(path =>
|
|
757
|
+
const waiverSelfBound = item => scopePaths.length > 0 && scopePaths.every(path => boundToScopePath(item, path));
|
|
325
758
|
const citationsCoverScope = item => {
|
|
326
759
|
const covered = new Set();
|
|
327
760
|
for (const id of Array.isArray(item.citedRecordIds) ? item.citedRecordIds : []) {
|
|
328
761
|
const target = recordIndex.get(id);
|
|
329
762
|
if (target && target.obligationId === obligationId && target.exitCode === 0
|
|
330
763
|
&& completeExecution(target) && !failingExec.includes(target)) {
|
|
331
|
-
for (const path of scopePaths) if (
|
|
764
|
+
for (const path of scopePaths) if (boundToScopePath(target, path)) covered.add(path);
|
|
332
765
|
}
|
|
333
766
|
}
|
|
334
767
|
return covered.size === scopePaths.length;
|
|
@@ -362,7 +795,7 @@ function evaluateObligation(obligation, context) {
|
|
|
362
795
|
|
|
363
796
|
const passingExec = usedExec.filter(item => item.exitCode === 0);
|
|
364
797
|
const execCoveredAll = scopePaths.length > 0
|
|
365
|
-
&& scopePaths.every(path => passingExec.some(item =>
|
|
798
|
+
&& scopePaths.every(path => passingExec.some(item => boundToScopePath(item, path)));
|
|
366
799
|
// S3 (§11): identity data gates every per-REQ coverage claim. A waiver lane pass requires a
|
|
367
800
|
// known non-empty cited identity list, known update data, and coverage of every uncovered
|
|
368
801
|
// missing REQ by a listing valid waiver. Absent or empty cited data, or an absent
|
|
@@ -439,7 +872,7 @@ function evaluateObligation(obligation, context) {
|
|
|
439
872
|
const reviewsRequired = capability === 'quality-review';
|
|
440
873
|
if (reviewsRequired) {
|
|
441
874
|
const reviewCovers = (path) => matching.some(item => reviewList.includes(item) && item.kind === 'review'
|
|
442
|
-
&& item.verdict === 'approve' && nonEmptyString(item.summary) &&
|
|
875
|
+
&& item.verdict === 'approve' && nonEmptyString(item.summary) && boundToScopePath(item, path));
|
|
443
876
|
if (!invalidReviews.length && !oddKindRecords.length
|
|
444
877
|
&& validReviews.length && scopePaths.every(reviewCovers)) {
|
|
445
878
|
const contributors = [...validReviews, ...usedExec.filter(item => !failingExec.includes(item))];
|
|
@@ -454,7 +887,7 @@ function evaluateObligation(obligation, context) {
|
|
|
454
887
|
|
|
455
888
|
const execCovers = (path) => matching.some(item => executionList.includes(item)
|
|
456
889
|
&& completeExecution(item)
|
|
457
|
-
&& !failingExec.includes(item) &&
|
|
890
|
+
&& !failingExec.includes(item) && boundToScopePath(item, path));
|
|
458
891
|
const execCovered = scopePaths.every(execCovers);
|
|
459
892
|
if (usedExec.length && execCovered) {
|
|
460
893
|
const contributors = [...usedExec.filter(item => !failingExec.includes(item)), ...validWaivers];
|
|
@@ -163,6 +163,20 @@ function changePaths(change) {
|
|
|
163
163
|
return change.kind === 'rename' ? [change.from, change.to] : [change.path];
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
// #335: a path with no candidate artifact. The old endpoint of an identified rename and a
|
|
167
|
+
// deleted path are both baseline-only. Derivation annotates them; it never removes them from
|
|
168
|
+
// scope.paths, so an unannotated caller keeps the pre-existing fail-closed behavior.
|
|
169
|
+
function baselinePathFacts(changes) {
|
|
170
|
+
const entries = [];
|
|
171
|
+
for (const change of changes) {
|
|
172
|
+
if (change.kind === 'deleted') entries.push({ path: change.path, reason: 'deleted' });
|
|
173
|
+
else if (change.kind === 'rename') {
|
|
174
|
+
entries.push({ path: change.from, reason: 'rename-from', renameTo: change.to });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return entries.sort((left, right) => compareText(left.path, right.path));
|
|
178
|
+
}
|
|
179
|
+
|
|
166
180
|
function uniqueSorted(values) {
|
|
167
181
|
return [...new Set(values.filter(value => typeof value === 'string'))].sort(compareText);
|
|
168
182
|
}
|
|
@@ -1465,6 +1479,7 @@ function deriveDocFacts({ base, candidate, config, inventory } = {}) {
|
|
|
1465
1479
|
governedTouched,
|
|
1466
1480
|
governedPaths: uniqueSorted(contextualChanges.filter(change => change.oldGoverned || change.newGoverned)
|
|
1467
1481
|
.flatMap(changePaths)),
|
|
1482
|
+
baselinePaths: baselinePathFacts(contextualChanges),
|
|
1468
1483
|
newGovernedPaths: newGovernedFiles.map(change => change.newPath).sort(compareText),
|
|
1469
1484
|
derivedAffected,
|
|
1470
1485
|
derivedPaths,
|
|
@@ -1540,6 +1555,13 @@ function deriveDocObligations(facts) {
|
|
|
1540
1555
|
...(facts.citedReqs || []).map(item => item && item.file)]) : []
|
|
1541
1556
|
};
|
|
1542
1557
|
const inventoryEnvelope = validFacts ? copyInventoryEnvelope(facts.inventory) : undefined;
|
|
1558
|
+
const baselineByPath = new Map((validFacts && Array.isArray(facts.baselinePaths) ? facts.baselinePaths : [])
|
|
1559
|
+
.filter(entry => entry && typeof entry.path === 'string')
|
|
1560
|
+
.map(entry => [entry.path, {
|
|
1561
|
+
path: entry.path,
|
|
1562
|
+
reason: entry.reason,
|
|
1563
|
+
...(typeof entry.renameTo === 'string' ? { renameTo: entry.renameTo } : {})
|
|
1564
|
+
}]));
|
|
1543
1565
|
const requirementFacts = {
|
|
1544
1566
|
citedReqs: validFacts && Array.isArray(facts.citedReqs) ? facts.citedReqs : [],
|
|
1545
1567
|
reqUpdates: validFacts && Array.isArray(facts.reqUpdates) ? facts.reqUpdates : [],
|
|
@@ -1558,11 +1580,16 @@ function deriveDocObligations(facts) {
|
|
|
1558
1580
|
capability: item.capability,
|
|
1559
1581
|
strength: strengths.get(item.capability) || item.strength,
|
|
1560
1582
|
applicability: applicable.has(item.capability) ? 'applicable' : unknown.has(item.capability) ? 'unknown' : 'N/A',
|
|
1561
|
-
scope: {
|
|
1562
|
-
paths
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1583
|
+
scope: (() => {
|
|
1584
|
+
const paths = uniqueSorted(scopePaths[item.capability] || []);
|
|
1585
|
+
const baselinePaths = paths.filter(path => baselineByPath.has(path)).map(path => baselineByPath.get(path));
|
|
1586
|
+
return {
|
|
1587
|
+
paths,
|
|
1588
|
+
...(baselinePaths.length ? { baselinePaths } : {}),
|
|
1589
|
+
...(item.capability === 'requirement-sync' && requirementFacts.requirementScopeUnknown
|
|
1590
|
+
? { requirementIds: 'unknown' } : {})
|
|
1591
|
+
};
|
|
1592
|
+
})(),
|
|
1566
1593
|
acceptance: item.acceptance,
|
|
1567
1594
|
...(item.capability === 'requirement-sync' ? requirementFacts : {})
|
|
1568
1595
|
}));
|