harness-everything 0.36.0 → 0.36.2
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 +37 -0
- package/ci/mechanism-335-baseline-evidence.test.js +3038 -0
- package/docs/doc-evidence-baseline-binding.md +270 -0
- package/docs/document-lifecycle.md +2 -2
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-evidence.js +513 -20
- 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 +513 -20
- 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,22 @@ 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
|
+
// P2(r4321077631): reads the own digest exactly once. `artifact()` inspects `entry.digest` up
|
|
76
|
+
// to three times (typeof, length, value), so a stateful accessor could validate one value and
|
|
77
|
+
// have the captured destination snapshot carry another. Only the applicable rename-destination
|
|
78
|
+
// snapshot uses this helper; non-baseline and inapplicable lanes keep the legacy `artifact()`
|
|
79
|
+
// reads for their audit identity. A throwing digest getter propagates to the caller's guarded
|
|
80
|
+
// block and fails the inspection closed there. This is the single authoritative own-digest
|
|
81
|
+
// helper; nothing else in this module reads a destination's own digest.
|
|
82
|
+
function ownDigestOnce(entry) {
|
|
83
|
+
if (!isPlainObject(entry)) return '';
|
|
84
|
+
if (!hasOwnEntry(entry, 'digest')) return '';
|
|
85
|
+
const digest = entry.digest;
|
|
86
|
+
return typeof digest === 'string' && digest.length > 0 ? digest : '';
|
|
87
|
+
}
|
|
88
|
+
|
|
39
89
|
function provenanceText(record) {
|
|
40
90
|
return nonEmptyString(record.provenance) ? record.provenance : `record ${record.id}`;
|
|
41
91
|
}
|
|
@@ -86,8 +136,36 @@ function prdSetDigest(artifactMap) {
|
|
|
86
136
|
function checkDocObligations({ obligations, artifacts, baseline, executionRecords, reviewRecords } = {}) {
|
|
87
137
|
const diagnostics = [];
|
|
88
138
|
const results = [];
|
|
139
|
+
// P3: distinguish a caller-supplied plain-object or null-prototype inventory (including an
|
|
140
|
+
// explicit {}) from an absent, malformed, or object-like value that the pre-existing
|
|
141
|
+
// normalizer would coerce to (or treat as) an empty map. The provenance check is deferred to
|
|
142
|
+
// applicable obligations that actually declare baseline paths, so non-baseline and
|
|
143
|
+
// inapplicable callers never read the candidate inventory's prototype (a throwing Proxy trap
|
|
144
|
+
// must not abort an unrelated obligation). It is resolved at most once so every obligation
|
|
145
|
+
// sees the same classification. `artifactMap` keeps the original `isPlainObject` fallback for
|
|
146
|
+
// non-baseline callers.
|
|
89
147
|
const artifactMap = isPlainObject(artifacts) ? artifacts : {};
|
|
90
|
-
|
|
148
|
+
let candidateInventoryKnown;
|
|
149
|
+
const isCandidateInventoryKnown = () => {
|
|
150
|
+
if (candidateInventoryKnown === undefined) {
|
|
151
|
+
candidateInventoryKnown = isCandidateInventory(artifacts);
|
|
152
|
+
}
|
|
153
|
+
return candidateInventoryKnown;
|
|
154
|
+
};
|
|
155
|
+
// P2(r4321077638): the whole-baseline identity is read exactly once and that same scalar is
|
|
156
|
+
// validated, so a stateful getter cannot pass validation as A and have every later comparison
|
|
157
|
+
// use B. A baseline identity whose inspection throws is unusable input: the captured digest
|
|
158
|
+
// stays '', so baseline-bound evaluations fail closed through `baseline-identity-missing`
|
|
159
|
+
// instead of escaping the checker, and non-baseline callers keep the legacy '' shape.
|
|
160
|
+
let baselineDigest = '';
|
|
161
|
+
if (isPlainObject(baseline)) {
|
|
162
|
+
try {
|
|
163
|
+
const declaredBaselineDigest = baseline.digest;
|
|
164
|
+
if (nonEmptyString(declaredBaselineDigest)) baselineDigest = declaredBaselineDigest;
|
|
165
|
+
} catch {
|
|
166
|
+
baselineDigest = '';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
91
169
|
const executionList = Array.isArray(executionRecords) ? executionRecords.filter(isRecord) : [];
|
|
92
170
|
const reviewList = Array.isArray(reviewRecords) ? reviewRecords.filter(isRecord) : [];
|
|
93
171
|
|
|
@@ -105,21 +183,324 @@ function checkDocObligations({ obligations, artifacts, baseline, executionRecord
|
|
|
105
183
|
|
|
106
184
|
for (const obligation of obligationList) {
|
|
107
185
|
results.push(evaluateObligation(obligation, {
|
|
108
|
-
artifactMap, baselineDigest, executionList, reviewList, diagnostics
|
|
186
|
+
artifactMap, isCandidateInventoryKnown, baseline, baselineDigest, executionList, reviewList, diagnostics
|
|
109
187
|
}));
|
|
110
188
|
}
|
|
111
189
|
return { diagnostics, results };
|
|
112
190
|
}
|
|
113
191
|
|
|
114
192
|
function evaluateObligation(obligation, context) {
|
|
115
|
-
const { artifactMap, baselineDigest, executionList, reviewList, diagnostics } = context;
|
|
193
|
+
const { artifactMap, isCandidateInventoryKnown, baseline, baselineDigest, executionList, reviewList, diagnostics } = context;
|
|
116
194
|
const capability = typeof obligation.capability === 'string' ? obligation.capability : '';
|
|
117
195
|
const obligationId = nonEmptyString(obligation.id) ? obligation.id : capability;
|
|
118
196
|
const scopePaths = scopePathsOf(obligation);
|
|
119
197
|
const applicability = obligation.applicability;
|
|
120
198
|
|
|
199
|
+
// #335: baseline-only paths are declared by the obligation. Every declared entry is validated
|
|
200
|
+
// before any outcome is decided; a malformed or contradictory annotation fails the whole
|
|
201
|
+
// obligation closed instead of being silently ignored.
|
|
202
|
+
const scopeObject = isPlainObject(obligation.scope) ? obligation.scope : null;
|
|
203
|
+
const declaresBaselinePaths = Boolean(scopeObject && Object.prototype.hasOwnProperty.call(scopeObject, 'baselinePaths'));
|
|
204
|
+
const declaredBaselinePaths = declaresBaselinePaths ? scopeObject.baselinePaths : undefined;
|
|
205
|
+
const baselineArtifactsMap = isPlainObject(baseline) && isPlainObject(baseline.artifacts) ? baseline.artifacts : null;
|
|
206
|
+
const baselineEntries = new Map();
|
|
207
|
+
// #340: every accepted entry sharing a rename destination must validate the same own digest,
|
|
208
|
+
// or the shared-destination snapshot is contradictory and the obligation fails closed.
|
|
209
|
+
const validatedDestinationDigestMap = new Map();
|
|
210
|
+
// #342 P3: a rename destination whose own-digest read throws or returns an unusable value is
|
|
211
|
+
// still a visited snapshot. Its captured ('') value is recorded here so the ordinary capture
|
|
212
|
+
// and the audit identity never re-read that destination entry: the single metadata read stays
|
|
213
|
+
// the only digest read for that declaring entry, and a stateful getter cannot ship a later
|
|
214
|
+
// non-empty value into the audit key even though its entry was rejected. A poisoned
|
|
215
|
+
// destination is honored inside the metadata loop (a later declaring entry fails closed
|
|
216
|
+
// without reading it) and keeps precedence over a previously seeded successful snapshot;
|
|
217
|
+
// multiple accepted entries still read the destination once each by design, because their
|
|
218
|
+
// agreement check detects shared-destination contradictions.
|
|
219
|
+
const failedDestinationDigests = new Map();
|
|
220
|
+
// #340 P2(r4320818749): the validated per-path baseline digest is captured once during
|
|
221
|
+
// metadata validation. The audit mirror, the stale-baseline diagnostics, and baselineBound()
|
|
222
|
+
// reuse that captured value instead of re-reading the caller map, so a stateful own getter
|
|
223
|
+
// cannot pass evidence against a value it started returning only after validation, and a
|
|
224
|
+
// later-throwing read cannot escape the checker.
|
|
225
|
+
const validatedBaselineDigests = new Map();
|
|
226
|
+
let baselineMetadataError = false;
|
|
227
|
+
let candidateInventoryUnknown = false;
|
|
228
|
+
// #335 P3: a candidate inventory can pass the prototype check and still throw from an
|
|
229
|
+
// own-property descriptor trap or a nested digest accessor. Any such failure is treated as
|
|
230
|
+
// unknown inventory rather than escaping the checker; the diagnostic is emitted only for
|
|
231
|
+
// applicable obligations that declare baseline-only paths.
|
|
232
|
+
let candidateInventoryInspectionFailed = false;
|
|
233
|
+
|
|
234
|
+
const baselineMetadataDiagnostic = (code, path, message) => {
|
|
235
|
+
baselineMetadataError = true;
|
|
236
|
+
diagnostics.push({ code, obligationId, ...(path ? { path } : {}), message });
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
if (declaresBaselinePaths) {
|
|
240
|
+
if (!Array.isArray(declaredBaselinePaths)) {
|
|
241
|
+
baselineMetadataDiagnostic('baseline-path-invalid', undefined,
|
|
242
|
+
'scope.baselinePaths is present but not an array; baseline-bound evidence cannot be evaluated');
|
|
243
|
+
} else if (declaredBaselinePaths.length) {
|
|
244
|
+
if (!nonEmptyString(baselineDigest)) {
|
|
245
|
+
baselineMetadataDiagnostic('baseline-identity-missing', undefined,
|
|
246
|
+
'baselinePaths are declared but the baseline carries no whole-baseline digest; baseline-bound evidence cannot be trusted');
|
|
247
|
+
}
|
|
248
|
+
if (!baselineArtifactsMap) {
|
|
249
|
+
baselineMetadataDiagnostic('baseline-artifacts-missing', undefined,
|
|
250
|
+
'baselinePaths are declared but the baseline carries no artifacts map; per-path baseline digests are unavailable');
|
|
251
|
+
}
|
|
252
|
+
const seen = new Set();
|
|
253
|
+
for (const rawEntry of declaredBaselinePaths) {
|
|
254
|
+
const invalid = (path, reason) => baselineMetadataDiagnostic('baseline-path-invalid', path,
|
|
255
|
+
`baselinePaths entry ${path ? JSON.stringify(path) : '(malformed)'} is invalid: ${reason}; the path fails closed`);
|
|
256
|
+
if (!isPlainObject(rawEntry)) {
|
|
257
|
+
invalid(undefined, 'it is not an object with a non-empty path');
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
// #340: entry fields are snapshotted through one guarded inspection. A Proxy entry
|
|
261
|
+
// whose `path`, `reason`, or `renameTo` getter or descriptor trap throws must not
|
|
262
|
+
// escape the checker; the inspection failure itself fails the entry closed as a
|
|
263
|
+
// metadata error, and each field is carried exactly once so a stateful accessor
|
|
264
|
+
// cannot make validation and capture disagree.
|
|
265
|
+
let snapshot = null;
|
|
266
|
+
try {
|
|
267
|
+
snapshot = {
|
|
268
|
+
path: rawEntry.path,
|
|
269
|
+
reason: rawEntry.reason,
|
|
270
|
+
hasOwnRenameTo: Object.prototype.hasOwnProperty.call(rawEntry, 'renameTo'),
|
|
271
|
+
renameTo: rawEntry.renameTo
|
|
272
|
+
};
|
|
273
|
+
} catch {
|
|
274
|
+
snapshot = null;
|
|
275
|
+
}
|
|
276
|
+
if (!snapshot) {
|
|
277
|
+
invalid(undefined, 'its entry fields could not be inspected safely (a getter or descriptor trap threw); the entry fails closed');
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (!nonEmptyString(snapshot.path)) {
|
|
281
|
+
invalid(undefined, 'it is not an object with a non-empty path');
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const entryPath = snapshot.path;
|
|
285
|
+
if (seen.has(entryPath)) {
|
|
286
|
+
invalid(entryPath, 'the same path is declared more than once');
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
seen.add(entryPath);
|
|
290
|
+
if (!scopePaths.includes(entryPath)) {
|
|
291
|
+
invalid(entryPath, 'it is not a member of scope.paths');
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const entryReason = snapshot.reason;
|
|
295
|
+
if (entryReason !== 'deleted' && entryReason !== 'rename-from') {
|
|
296
|
+
invalid(entryPath, `reason ${JSON.stringify(entryReason)} is not deleted or rename-from`);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (entryReason === 'deleted' && snapshot.hasOwnRenameTo) {
|
|
300
|
+
invalid(entryPath, 'a deleted entry must not carry renameTo');
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
// #340: the validated rename destination digest is carried on the entry; the
|
|
304
|
+
// destination candidate map is not read again after metadata validation.
|
|
305
|
+
let renameDestinationDigest = '';
|
|
306
|
+
if (entryReason === 'rename-from') {
|
|
307
|
+
// #340: the destination annotation is snapshotted once; a stateful getter cannot
|
|
308
|
+
// make the two validation reads disagree.
|
|
309
|
+
const entryRenameTo = snapshot.renameTo;
|
|
310
|
+
if (!nonEmptyString(entryRenameTo) || entryRenameTo === entryPath) {
|
|
311
|
+
invalid(entryPath, 'a rename-from entry needs a distinct non-empty renameTo');
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (!scopePaths.includes(entryRenameTo)) {
|
|
315
|
+
invalid(entryPath, `its rename destination ${JSON.stringify(entryRenameTo)} is not a member of scope.paths`);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
// #340: destination candidate inspection stays applicability-gated. N/A and unknown
|
|
319
|
+
// obligations never bind evidence, so consuming their destination digest getter
|
|
320
|
+
// during metadata validation would shift the legacy audit identity (a stateful
|
|
321
|
+
// getter reports a later value on the second artifact read). Their audit identity
|
|
322
|
+
// keeps the un-gated legacy candidate read; only applicable obligations capture and
|
|
323
|
+
// validate the destination snapshot.
|
|
324
|
+
if (applicability === 'applicable') {
|
|
325
|
+
// #342 review P3: a destination poisoned by an earlier entry's failed snapshot is
|
|
326
|
+
// honored inside the metadata loop; the failed visited snapshot is never read again
|
|
327
|
+
// and this entry fails closed with it, so a stateful getter's later successful
|
|
328
|
+
// value cannot repair a shared destination or enter the audit.
|
|
329
|
+
if (failedDestinationDigests.has(entryRenameTo)) {
|
|
330
|
+
invalid(entryPath, `its rename destination ${JSON.stringify(entryRenameTo)} failed an earlier snapshot validation for this obligation; the failed destination is not read again and this entry fails closed with it`);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
let destinationDigest = '';
|
|
334
|
+
try {
|
|
335
|
+
if (hasOwnEntry(artifactMap, entryRenameTo)) {
|
|
336
|
+
// P2(r4321077631): read the destination's own digest exactly once; the captured
|
|
337
|
+
// value is the one validated below and the one that flows to the metadata cache,
|
|
338
|
+
// binding, the stale diagnostics, and the audit identity. A stateful destination
|
|
339
|
+
// getter cannot be validated as B while a record carrying only its later C passes.
|
|
340
|
+
destinationDigest = ownDigestOnce(artifactMap[entryRenameTo]);
|
|
341
|
+
}
|
|
342
|
+
} catch {
|
|
343
|
+
candidateInventoryInspectionFailed = true;
|
|
344
|
+
// #342 P3: the destination is a visited snapshot even when its digest read threw;
|
|
345
|
+
// record it so the capture and audit lanes never re-read it.
|
|
346
|
+
failedDestinationDigests.set(entryRenameTo, '');
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
// #340: the captured own value must be a usable non-empty string; an unusable result
|
|
350
|
+
// is not carried or seeded.
|
|
351
|
+
if (!nonEmptyString(destinationDigest)) {
|
|
352
|
+
invalid(entryPath, `its rename destination ${JSON.stringify(entryRenameTo)} has no candidate artifact`);
|
|
353
|
+
// #342 P3: the destination is a visited snapshot even when its own digest is
|
|
354
|
+
// unusable; record it so the capture and audit lanes never re-read it and cannot
|
|
355
|
+
// describe a stateful getter's later non-empty value.
|
|
356
|
+
failedDestinationDigests.set(entryRenameTo, '');
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
// #340: entries that share one rename destination must agree on the validated own
|
|
360
|
+
// digest. A conflicting validated value is a contradictory snapshot and fails the
|
|
361
|
+
// whole obligation closed before any binding; equal values are allowed.
|
|
362
|
+
const sharedDestinationDigest = validatedDestinationDigestMap.get(entryRenameTo);
|
|
363
|
+
if (sharedDestinationDigest === undefined) {
|
|
364
|
+
validatedDestinationDigestMap.set(entryRenameTo, destinationDigest);
|
|
365
|
+
} else if (sharedDestinationDigest !== destinationDigest) {
|
|
366
|
+
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`);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
renameDestinationDigest = destinationDigest;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
let candidatePathPresent = false;
|
|
373
|
+
try {
|
|
374
|
+
candidatePathPresent = hasOwnEntry(artifactMap, entryPath);
|
|
375
|
+
} catch {
|
|
376
|
+
candidateInventoryInspectionFailed = true;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (candidatePathPresent) {
|
|
380
|
+
baselineMetadataDiagnostic('baseline-path-candidate-present', entryPath,
|
|
381
|
+
`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`);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
if (baselineArtifactsMap) {
|
|
385
|
+
let trusted;
|
|
386
|
+
try {
|
|
387
|
+
trusted = hasOwnEntry(baselineArtifactsMap, entryPath) ? baselineArtifactsMap[entryPath] : undefined;
|
|
388
|
+
} catch {
|
|
389
|
+
// #340 P2: a throwing baseline inspection is a metadata error, never an escape.
|
|
390
|
+
baselineMetadataDiagnostic('baseline-artifact-invalid', entryPath,
|
|
391
|
+
`the baseline digest for ${entryPath} could not be inspected safely (a getter or descriptor trap threw); the path fails closed`);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (trusted === undefined) {
|
|
395
|
+
baselineMetadataDiagnostic('baseline-artifact-missing', entryPath,
|
|
396
|
+
`the baseline carries no per-path digest for declared baseline path ${entryPath}; the path fails closed`);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (!isSha256(trusted)) {
|
|
400
|
+
baselineMetadataDiagnostic('baseline-artifact-invalid', entryPath,
|
|
401
|
+
`the baseline digest for ${entryPath} is not a SHA256 value; the path fails closed`);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
validatedBaselineDigests.set(entryPath, trusted);
|
|
405
|
+
}
|
|
406
|
+
baselineEntries.set(entryPath, {
|
|
407
|
+
path: entryPath,
|
|
408
|
+
reason: entryReason,
|
|
409
|
+
// #340: the metadata check above confirmed this path has no own-key candidate entry;
|
|
410
|
+
// carry that validated source-absence proof explicitly (never spread from rawEntry) so
|
|
411
|
+
// binding and the audit identity reuse it instead of re-inspecting the candidate map.
|
|
412
|
+
candidatePathPresent: false,
|
|
413
|
+
// #340 P2: carry the exact validated SHA256 so binding never re-reads the
|
|
414
|
+
// caller-owned baseline map.
|
|
415
|
+
trustedBaselineDigest: validatedBaselineDigests.get(entryPath),
|
|
416
|
+
...(entryReason === 'rename-from' ? { renameTo: snapshot.renameTo, renameDestinationDigest } : {})
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// #335 P3: a mixed scope combines baseline-only paths with ordinary candidate paths. Capture
|
|
423
|
+
// every ordinary scoped candidate digest before evidence binding, so a throwing accessor in
|
|
424
|
+
// the legacy candidate lane fails the inventory closed instead of aborting the obligation.
|
|
425
|
+
const candidateDigestCache = new Map();
|
|
426
|
+
if (applicability === 'applicable' && Array.isArray(declaredBaselinePaths)
|
|
427
|
+
&& declaredBaselinePaths.length > 0) {
|
|
428
|
+
// #340: a rename destination was already validated as an own-key candidate entry carrying
|
|
429
|
+
// its own digest during metadata. Seed the captured snapshot from that validated value and
|
|
430
|
+
// never read the destination through the raw candidate map again, so a destination accessor
|
|
431
|
+
// that throws or changes after validation cannot make capture/binding/audit disagree. The
|
|
432
|
+
// seeded value is the validated own digest, never a caller-asserted rawEntry field. A
|
|
433
|
+
// rejected destination is still visited: its failed capture (see failedDestinationDigests)
|
|
434
|
+
// suppresses the ordinary read below, so no destination entry is ever read twice.
|
|
435
|
+
for (const entry of baselineEntries.values()) {
|
|
436
|
+
if (entry.reason === 'rename-from' && nonEmptyString(entry.renameDestinationDigest)
|
|
437
|
+
&& !candidateDigestCache.has(entry.renameTo)) {
|
|
438
|
+
candidateDigestCache.set(entry.renameTo, entry.renameDestinationDigest);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
for (const path of scopePaths) {
|
|
442
|
+
if (baselineEntries.has(path) || candidateDigestCache.has(path)
|
|
443
|
+
|| failedDestinationDigests.has(path)) continue;
|
|
444
|
+
try {
|
|
445
|
+
candidateDigestCache.set(path, artifact(artifactMap[path]));
|
|
446
|
+
} catch {
|
|
447
|
+
candidateInventoryInspectionFailed = true;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// P3: an applicable obligation that actually declares baseline-only paths needs a known
|
|
453
|
+
// candidate inventory. Explicit {} is known and remains the legitimate empty deletion
|
|
454
|
+
// inventory; omitted/undefined/null/array/string/other malformed values are unknown input.
|
|
455
|
+
// The prototype read happens here, not up front, so unrelated obligations never touch it.
|
|
456
|
+
if (applicability === 'applicable' && Array.isArray(declaredBaselinePaths)
|
|
457
|
+
&& declaredBaselinePaths.length > 0
|
|
458
|
+
&& (candidateInventoryInspectionFailed || !isCandidateInventoryKnown())) {
|
|
459
|
+
candidateInventoryUnknown = true;
|
|
460
|
+
diagnostics.push({
|
|
461
|
+
code: 'candidate-artifacts-invalid',
|
|
462
|
+
obligationId,
|
|
463
|
+
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'
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const baselineAudit = {};
|
|
468
|
+
// #340 P2(r4320941809): the audit mirror is built from the metadata-validated snapshot
|
|
469
|
+
// (`validatedBaselineDigests`, populated during the per-entry guard), never from a re-read
|
|
470
|
+
// of the raw entry path or a fallback inspection of the live baseline map. A stateful
|
|
471
|
+
// `path` accessor therefore cannot make a passing result describe a different baseline
|
|
472
|
+
// identity than the one that validated and bound the evidence, and each declared path
|
|
473
|
+
// contributes its snapshot exactly once in validation order.
|
|
474
|
+
for (const [baselineAuditPath, baselineAuditDigest] of validatedBaselineDigests) {
|
|
475
|
+
baselineAudit[baselineAuditPath] = baselineAuditDigest;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// An uninspectable candidate entry must not abort an N/A or unknown obligation while the
|
|
479
|
+
// audit key is built; the baseline-bound lanes fail closed through the inventory diagnostic.
|
|
480
|
+
// When a baseline-annotated obligation captured the ordinary candidate digests before binding
|
|
481
|
+
// (candidateDigestCache), the audit identity and stale diagnostics use that same snapshot, so a
|
|
482
|
+
// nested digest accessor that only later throws or changes can never make the audit key or the
|
|
483
|
+
// stale verdict disagree with the digest that actually bound evidence. Untouched paths and
|
|
484
|
+
// unannotated callers keep the legacy read.
|
|
485
|
+
const candidateDigestOf = (path) => {
|
|
486
|
+
// #342 P3: poisoned state takes precedence over the capture cache. A rejected rename
|
|
487
|
+
// destination keeps its visited '' capture even when an earlier accepted entry seeded a
|
|
488
|
+
// successful value, so the audit never describes a value the getter shipped after a failed
|
|
489
|
+
// snapshot made the obligation fail closed; the caller map is never read for it again.
|
|
490
|
+
if (failedDestinationDigests.has(path)) return '';
|
|
491
|
+
if (candidateDigestCache.has(path)) return candidateDigestCache.get(path);
|
|
492
|
+
// #340: only an applicable baseline-annotated obligation validated the source absence as
|
|
493
|
+
// part of its captured snapshot. `N/A` and `unknown` obligations never bind evidence, so they
|
|
494
|
+
// keep the legacy candidate read for their audit identity and stay `outcome: 'none'`.
|
|
495
|
+
if (applicability === 'applicable' && baselineEntries.has(path)) return '';
|
|
496
|
+
try {
|
|
497
|
+
return artifact(artifactMap[path]);
|
|
498
|
+
} catch {
|
|
499
|
+
return '';
|
|
500
|
+
}
|
|
501
|
+
};
|
|
121
502
|
const artifactDigestKey = scopePaths.length
|
|
122
|
-
? scopePaths.map(path => `${path}=${
|
|
503
|
+
? scopePaths.map(path => `${path}=${candidateDigestOf(path) || 'missing'}`).join('|')
|
|
123
504
|
: (baselineDigest || 'no-scope-paths');
|
|
124
505
|
const baseDigestKey = baselineDigest || 'no-baseline';
|
|
125
506
|
const result = (observation, outcome, provenance, recordRefs) => ({
|
|
@@ -128,6 +509,7 @@ function evaluateObligation(obligation, context) {
|
|
|
128
509
|
outcome,
|
|
129
510
|
artifactDigest: artifactDigestKey,
|
|
130
511
|
baseDigest: baseDigestKey,
|
|
512
|
+
...(declaresBaselinePaths ? { baselineArtifacts: { ...baselineAudit } } : {}),
|
|
131
513
|
provenance,
|
|
132
514
|
recordRefs: [...recordRefs]
|
|
133
515
|
});
|
|
@@ -141,10 +523,65 @@ function evaluateObligation(obligation, context) {
|
|
|
141
523
|
}
|
|
142
524
|
|
|
143
525
|
const matching = [...executionList, ...reviewList].filter(item => item.obligationId === obligationId);
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
526
|
+
const candidateBound = (item, path) => {
|
|
527
|
+
if (!nonEmptyString(item.baseDigest) || item.baseDigest !== baselineDigest) return false;
|
|
528
|
+
if (!isPlainObject(item.artifactDigests)) return false;
|
|
529
|
+
if (candidateDigestCache.has(path)) {
|
|
530
|
+
// Baseline-annotated obligations use the digest captured before binding, so a mixed
|
|
531
|
+
// delete+edit scope never re-reads a throwing accessor from the legacy lane.
|
|
532
|
+
const digest = candidateDigestCache.get(path);
|
|
533
|
+
return digest.length > 0
|
|
534
|
+
&& item.artifactDigests[path] === digest
|
|
535
|
+
&& Array.isArray(item.paths) && item.paths.includes(path);
|
|
536
|
+
}
|
|
537
|
+
return artifact(artifactMap[path]).length > 0
|
|
538
|
+
&& item.artifactDigests[path] === artifactMap[path].digest
|
|
539
|
+
&& Array.isArray(item.paths) && item.paths.includes(path);
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// A baseline-only path is satisfied only by a record bound to the trusted baseline digest.
|
|
543
|
+
// A rename-from path additionally needs the same single record to carry the current
|
|
544
|
+
// destination candidate digest, so destination-only, source-only and split records fail.
|
|
545
|
+
const baselineBound = (item, entry) => {
|
|
546
|
+
if (!nonEmptyString(item.baseDigest) || item.baseDigest !== baselineDigest) return false;
|
|
547
|
+
if (!Array.isArray(item.paths) || !item.paths.includes(entry.path)) return false;
|
|
548
|
+
// #340: metadata validation confirmed this baseline-only path has no own-key candidate
|
|
549
|
+
// entry. Reuse that carried absence proof instead of re-inspecting the candidate map, so a
|
|
550
|
+
// descriptor trap that only throws on a later inspection cannot abort the check. A path
|
|
551
|
+
// whose metadata was malformed never reaches here; it fails closed earlier.
|
|
552
|
+
if (entry.candidatePathPresent !== false) return false;
|
|
553
|
+
if (isPlainObject(item.artifactDigests)
|
|
554
|
+
&& Object.prototype.hasOwnProperty.call(item.artifactDigests, entry.path)) return false;
|
|
555
|
+
// #340 P2: binding compares the validated baseline digest captured during metadata, never
|
|
556
|
+
// a fresh read of the caller-owned baseline map; a stateful getter that started returning
|
|
557
|
+
// a different value after validation cannot make a stale record bind.
|
|
558
|
+
const trusted = entry.trustedBaselineDigest;
|
|
559
|
+
if (!isSha256(trusted)) return false;
|
|
560
|
+
if (!isPlainObject(item.baselineArtifacts)
|
|
561
|
+
|| !hasOwnEntry(item.baselineArtifacts, entry.path)
|
|
562
|
+
|| item.baselineArtifacts[entry.path] !== trusted) return false;
|
|
563
|
+
if (entry.reason === 'rename-from') {
|
|
564
|
+
if (!item.paths.includes(entry.renameTo)) return false;
|
|
565
|
+
if (!isPlainObject(item.artifactDigests)
|
|
566
|
+
|| !hasOwnEntry(item.artifactDigests, entry.renameTo)
|
|
567
|
+
|| !nonEmptyString(item.artifactDigests[entry.renameTo])) return false;
|
|
568
|
+
// #340 P3: the destination binds to the same captured candidate digest that the ordinary
|
|
569
|
+
// candidate lane, the audit identity, and the stale diagnostics use, so a stateful nested
|
|
570
|
+
// accessor cannot make binding disagree with them. Metadata validation already required the
|
|
571
|
+
// destination to be an own-key candidate inventory entry carrying its own digest
|
|
572
|
+
// (ownDigestOnce), so a non-empty captured digest is required here and the accessor is
|
|
573
|
+
// never re-read; a missing destination skips capture and lands on this guard.
|
|
574
|
+
const capturedDestination = candidateDigestCache.get(entry.renameTo);
|
|
575
|
+
if (!nonEmptyString(capturedDestination)) return false;
|
|
576
|
+
if (item.artifactDigests[entry.renameTo] !== capturedDestination) return false;
|
|
577
|
+
}
|
|
578
|
+
return true;
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const boundToScopePath = (item, path) => {
|
|
582
|
+
const entry = baselineEntries.get(path);
|
|
583
|
+
return entry ? baselineBound(item, entry) : candidateBound(item, path);
|
|
584
|
+
};
|
|
148
585
|
|
|
149
586
|
for (const item of matching) {
|
|
150
587
|
if (executionList.includes(item) && !completeExecution(item)) {
|
|
@@ -166,8 +603,8 @@ function evaluateObligation(obligation, context) {
|
|
|
166
603
|
continue;
|
|
167
604
|
}
|
|
168
605
|
if (Array.isArray(item.paths) && isPlainObject(item.artifactDigests)) {
|
|
169
|
-
const mismatch = item.paths.find(path =>
|
|
170
|
-
&& nonEmptyString(item.artifactDigests[path]) && item.artifactDigests[path] !==
|
|
606
|
+
const mismatch = item.paths.find(path => candidateDigestOf(path).length > 0
|
|
607
|
+
&& nonEmptyString(item.artifactDigests[path]) && item.artifactDigests[path] !== candidateDigestOf(path));
|
|
171
608
|
if (mismatch) {
|
|
172
609
|
diagnostics.push({
|
|
173
610
|
code: 'stale-artifact-digest',
|
|
@@ -178,12 +615,65 @@ function evaluateObligation(obligation, context) {
|
|
|
178
615
|
}
|
|
179
616
|
}
|
|
180
617
|
|
|
618
|
+
// #335: a record that names a baseline-only path but carries no per-path baseline digest is
|
|
619
|
+
// visible as an attempt, and a record whose per-path digest disagrees with the trusted value
|
|
620
|
+
// is stale. Both fail closed through missing-path-coverage and stale-baseline-artifact-digest.
|
|
621
|
+
for (const item of matching) {
|
|
622
|
+
const baselineNamed = Array.isArray(item.paths) ? item.paths.filter(path => baselineEntries.has(path)) : [];
|
|
623
|
+
if (isPlainObject(item.baselineArtifacts)) {
|
|
624
|
+
for (const [path, value] of Object.entries(item.baselineArtifacts)) {
|
|
625
|
+
if (!baselineEntries.has(path)) continue;
|
|
626
|
+
// #340 P2: the stale comparison uses the baseline digest captured during metadata,
|
|
627
|
+
// never a fresh read of the caller-owned map.
|
|
628
|
+
const trusted = validatedBaselineDigests.has(path)
|
|
629
|
+
? validatedBaselineDigests.get(path)
|
|
630
|
+
: undefined;
|
|
631
|
+
if (!isSha256(trusted)) continue;
|
|
632
|
+
if (nonEmptyString(value) && value !== trusted) {
|
|
633
|
+
diagnostics.push({
|
|
634
|
+
code: 'stale-baseline-artifact-digest',
|
|
635
|
+
obligationId,
|
|
636
|
+
message: `record ${item.id} carries a baseline digest for path ${path} that differs from the trusted baseline value`
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
for (const path of baselineNamed) {
|
|
642
|
+
const bound = isPlainObject(item.baselineArtifacts)
|
|
643
|
+
&& hasOwnEntry(item.baselineArtifacts, path)
|
|
644
|
+
&& nonEmptyString(item.baselineArtifacts[path]);
|
|
645
|
+
if (!bound) {
|
|
646
|
+
diagnostics.push({
|
|
647
|
+
code: 'baseline-binding-missing',
|
|
648
|
+
obligationId,
|
|
649
|
+
message: `record ${item.id} names baseline path ${path} but carries no trusted baseline digest for it`
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
if (isPlainObject(item.artifactDigests)
|
|
653
|
+
&& Object.prototype.hasOwnProperty.call(item.artifactDigests, path)) {
|
|
654
|
+
diagnostics.push({
|
|
655
|
+
code: 'baseline-path-candidate-digest',
|
|
656
|
+
obligationId,
|
|
657
|
+
message: `record ${item.id} carries a candidate artifactDigests entry for baseline-only path ${path}; the contradictory record cannot bind that path`
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (baselineMetadataError || candidateInventoryUnknown) {
|
|
664
|
+
return result('unresolved', 'none',
|
|
665
|
+
candidateInventoryUnknown && !baselineMetadataError
|
|
666
|
+
? 'the top-level candidate artifacts inventory is absent or malformed; baseline-bound evidence fails closed'
|
|
667
|
+
: 'baseline path metadata is missing, invalid, or contradictory; baseline-bound evidence fails closed',
|
|
668
|
+
[]);
|
|
669
|
+
}
|
|
670
|
+
|
|
181
671
|
const usedExec = matching.filter(item => executionList.includes(item)
|
|
182
672
|
&& completeExecution(item)
|
|
183
|
-
&& scopePaths.some(path =>
|
|
673
|
+
&& scopePaths.some(path => boundToScopePath(item, path)));
|
|
184
674
|
const failingExec = usedExec.filter(item => item.exitCode !== 0);
|
|
185
675
|
const boundReviews = matching.filter(item => reviewList.includes(item) && item.kind === 'review'
|
|
186
|
-
&& scopePaths.some(path =>
|
|
676
|
+
&& scopePaths.some(path => boundToScopePath(item, path)));
|
|
187
677
|
const validReviews = boundReviews.filter(item => item.verdict === 'approve' && nonEmptyString(item.summary));
|
|
188
678
|
const invalidReviews = boundReviews.filter(item => !validReviews.includes(item));
|
|
189
679
|
const oddKindRecords = matching.filter(item => reviewList.includes(item)
|
|
@@ -204,13 +694,16 @@ function evaluateObligation(obligation, context) {
|
|
|
204
694
|
});
|
|
205
695
|
}
|
|
206
696
|
|
|
207
|
-
const boundAny = (path) => matching.some(item =>
|
|
697
|
+
const boundAny = (path) => matching.some(item => boundToScopePath(item, path));
|
|
208
698
|
const unmetPaths = scopePaths.filter(path => !boundAny(path));
|
|
209
699
|
for (const path of unmetPaths) {
|
|
700
|
+
const entry = baselineEntries.get(path);
|
|
210
701
|
diagnostics.push({
|
|
211
702
|
code: 'missing-path-coverage',
|
|
212
703
|
obligationId,
|
|
213
|
-
message:
|
|
704
|
+
message: entry
|
|
705
|
+
? `no observed record is bound to baseline path ${path} (${entry.reason}${entry.renameTo ? ` -> ${entry.renameTo}` : ''}) with the trusted baseline digest`
|
|
706
|
+
: `no observed record is bound to path ${path} with the current candidate and baseline digests`
|
|
214
707
|
});
|
|
215
708
|
}
|
|
216
709
|
|
|
@@ -219,7 +712,7 @@ function evaluateObligation(obligation, context) {
|
|
|
219
712
|
const target = recordIndex.get(item);
|
|
220
713
|
return Boolean(target) && target.obligationId === obligationId
|
|
221
714
|
&& executionList.includes(target) && completeExecution(target) && target.exitCode === 0
|
|
222
|
-
&& scopePaths.some(path =>
|
|
715
|
+
&& scopePaths.some(path => boundToScopePath(target, path));
|
|
223
716
|
};
|
|
224
717
|
const waiverCandidates = reviewList.filter(item => item.kind === 'waiver'
|
|
225
718
|
&& item.obligationId === obligationId);
|
|
@@ -321,14 +814,14 @@ function evaluateObligation(obligation, context) {
|
|
|
321
814
|
[]);
|
|
322
815
|
}
|
|
323
816
|
|
|
324
|
-
const waiverSelfBound = item => scopePaths.length > 0 && scopePaths.every(path =>
|
|
817
|
+
const waiverSelfBound = item => scopePaths.length > 0 && scopePaths.every(path => boundToScopePath(item, path));
|
|
325
818
|
const citationsCoverScope = item => {
|
|
326
819
|
const covered = new Set();
|
|
327
820
|
for (const id of Array.isArray(item.citedRecordIds) ? item.citedRecordIds : []) {
|
|
328
821
|
const target = recordIndex.get(id);
|
|
329
822
|
if (target && target.obligationId === obligationId && target.exitCode === 0
|
|
330
823
|
&& completeExecution(target) && !failingExec.includes(target)) {
|
|
331
|
-
for (const path of scopePaths) if (
|
|
824
|
+
for (const path of scopePaths) if (boundToScopePath(target, path)) covered.add(path);
|
|
332
825
|
}
|
|
333
826
|
}
|
|
334
827
|
return covered.size === scopePaths.length;
|
|
@@ -362,7 +855,7 @@ function evaluateObligation(obligation, context) {
|
|
|
362
855
|
|
|
363
856
|
const passingExec = usedExec.filter(item => item.exitCode === 0);
|
|
364
857
|
const execCoveredAll = scopePaths.length > 0
|
|
365
|
-
&& scopePaths.every(path => passingExec.some(item =>
|
|
858
|
+
&& scopePaths.every(path => passingExec.some(item => boundToScopePath(item, path)));
|
|
366
859
|
// S3 (§11): identity data gates every per-REQ coverage claim. A waiver lane pass requires a
|
|
367
860
|
// known non-empty cited identity list, known update data, and coverage of every uncovered
|
|
368
861
|
// missing REQ by a listing valid waiver. Absent or empty cited data, or an absent
|
|
@@ -439,7 +932,7 @@ function evaluateObligation(obligation, context) {
|
|
|
439
932
|
const reviewsRequired = capability === 'quality-review';
|
|
440
933
|
if (reviewsRequired) {
|
|
441
934
|
const reviewCovers = (path) => matching.some(item => reviewList.includes(item) && item.kind === 'review'
|
|
442
|
-
&& item.verdict === 'approve' && nonEmptyString(item.summary) &&
|
|
935
|
+
&& item.verdict === 'approve' && nonEmptyString(item.summary) && boundToScopePath(item, path));
|
|
443
936
|
if (!invalidReviews.length && !oddKindRecords.length
|
|
444
937
|
&& validReviews.length && scopePaths.every(reviewCovers)) {
|
|
445
938
|
const contributors = [...validReviews, ...usedExec.filter(item => !failingExec.includes(item))];
|
|
@@ -454,7 +947,7 @@ function evaluateObligation(obligation, context) {
|
|
|
454
947
|
|
|
455
948
|
const execCovers = (path) => matching.some(item => executionList.includes(item)
|
|
456
949
|
&& completeExecution(item)
|
|
457
|
-
&& !failingExec.includes(item) &&
|
|
950
|
+
&& !failingExec.includes(item) && boundToScopePath(item, path));
|
|
458
951
|
const execCovered = scopePaths.every(execCovers);
|
|
459
952
|
if (usedExec.length && execCovered) {
|
|
460
953
|
const contributors = [...usedExec.filter(item => !failingExec.includes(item)), ...validWaivers];
|