harness-everything 0.36.3 → 0.36.4

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.
Files changed (47) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +10 -0
  4. package/ci/fixtures/doc-obligations/read-boundary/boundary-controls.json +211 -0
  5. package/ci/fixtures/doc-obligations/read-boundary/boundary-policy.json +325 -0
  6. package/ci/fixtures/doc-obligations/read-boundary/cases.json +14058 -0
  7. package/ci/fixtures/doc-obligations/read-boundary/expected.json +1 -0
  8. package/ci/fixtures/doc-obligations/read-boundary/identity-cases.json +119 -0
  9. package/ci/fixtures/doc-obligations/read-boundary/identity-expected.json +1 -0
  10. package/ci/fixtures/doc-obligations/read-boundary/scope-cases.json +208 -0
  11. package/ci/fixtures/doc-obligations/read-boundary/scope-expected.json +1 -0
  12. package/ci/fixtures/doc-obligations/reconciliation-prep/cases.json +2751 -0
  13. package/ci/fixtures/doc-obligations/reconciliation-prep/flood-entrypoint.js +5 -0
  14. package/ci/fixtures/doc-obligations/reconciliation-prep/hang-entrypoint.js +5 -0
  15. package/ci/fixtures/doc-obligations/reconciliation-prep/pending-red.json +648 -0
  16. package/ci/fixtures/doc-obligations/reconciliation-prep/provider-contract.json +100 -0
  17. package/ci/fixtures/doc-obligations/reconciliation-prep/trust-contract.json +188 -0
  18. package/ci/lib/doc-evidence-boundary-check.js +1240 -0
  19. package/ci/lib/doc-evidence-fixtures.js +459 -0
  20. package/ci/lib/doc-evidence-trace.js +171 -0
  21. package/ci/lib/doc-reconciliation-prep-fixtures.js +502 -0
  22. package/ci/lib/snapshot-stdin.js +27 -0
  23. package/ci/mechanism-325-3b-preparation.test.js +382 -0
  24. package/ci/mechanism-341-boundary-enforcement.test.js +660 -0
  25. package/ci/mechanism-341-capture-module.test.js +357 -0
  26. package/ci/mechanism-341-mutation-runner.test.js +413 -0
  27. package/ci/mechanism-341-read-boundary.test.js +388 -0
  28. package/ci/mechanism-341-snapshot-stdin.test.js +40 -0
  29. package/ci/mutation-check.js +805 -45
  30. package/ci/mutation-loader.js +544 -57
  31. package/ci/prep/doc-reconciliation-red.js +407 -0
  32. package/ci/stress-341-boundary-control.js +85 -0
  33. package/docs/doc-evidence-baseline-binding.md +105 -19
  34. package/docs/doc-evidence-boundary.md +135 -0
  35. package/docs/doc-workflow-reconciliation.md +224 -0
  36. package/docs/document-lifecycle.md +8 -1
  37. package/harness-everything/SKILL.md +1 -1
  38. package/harness-everything/scripts/doc-evidence-capture.js +331 -0
  39. package/harness-everything/scripts/doc-evidence.js +72 -223
  40. package/opencode-plugin/plugin.json +1 -1
  41. package/package.json +5 -3
  42. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  43. package/plugins/harness-everything/plugin.json +1 -1
  44. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  45. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence-capture.js +331 -0
  46. package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +72 -223
  47. package/sbom.cdx.json +43 -7
@@ -1,29 +1,13 @@
1
1
  'use strict';
2
2
 
3
- const crypto = require('crypto');
3
+ const {
4
+ createInvocationCapture
5
+ } = require('./doc-evidence-capture');
4
6
 
5
7
  function isPlainObject(value) {
6
8
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
7
9
  }
8
10
 
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
-
27
11
  function isRecord(value) {
28
12
  return isPlainObject(value)
29
13
  && typeof value.id === 'string' && value.id.length > 0
@@ -66,31 +50,10 @@ function scopePathsOf(obligation) {
66
50
  return [];
67
51
  }
68
52
 
69
- function artifact(entry) {
70
- return entry && typeof entry.digest === 'string' && entry.digest.length > 0 ? entry.digest : '';
71
- }
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
-
89
53
  function provenanceText(record) {
90
54
  return nonEmptyString(record.provenance) ? record.provenance : `record ${record.id}`;
91
55
  }
92
56
 
93
-
94
57
  function provenanceJoin(records, fallback) {
95
58
  return records.length ? uniqueSorted(records.map(provenanceText)).join('; ') : fallback;
96
59
  }
@@ -122,50 +85,25 @@ function incompleteFields(item) {
122
85
  return missing.join(', ');
123
86
  }
124
87
 
125
- // F2 (§11): the candidate PRD-set identity is the digest over the sorted [path, digest] pairs
126
- // of the artifacts under docs/prd/. The envelope must carry this exact digest to attest that
127
- // the inventory describes the candidate being checked.
128
- function prdSetDigest(artifactMap) {
129
- const pairs = Object.keys(artifactMap)
130
- .filter(path => path.startsWith('docs/prd/'))
131
- .sort()
132
- .map(path => [path, artifact(artifactMap[path])]);
133
- return crypto.createHash('sha256').update(JSON.stringify(pairs)).digest('hex');
134
- }
135
-
136
88
  function checkDocObligations({ obligations, artifacts, baseline, executionRecords, reviewRecords } = {}) {
137
89
  const diagnostics = [];
138
90
  const results = [];
91
+ const capture = createInvocationCapture({ artifacts, baseline });
139
92
  // P3: distinguish a caller-supplied plain-object or null-prototype inventory (including an
140
93
  // explicit {}) from an absent, malformed, or object-like value that the pre-existing
141
94
  // normalizer would coerce to (or treat as) an empty map. The provenance check is deferred to
142
95
  // applicable obligations that actually declare baseline paths, so non-baseline and
143
96
  // inapplicable callers never read the candidate inventory's prototype (a throwing Proxy trap
144
97
  // 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.
147
- const artifactMap = isPlainObject(artifacts) ? artifacts : {};
148
- let candidateInventoryKnown;
149
- const isCandidateInventoryKnown = () => {
150
- if (candidateInventoryKnown === undefined) {
151
- candidateInventoryKnown = isCandidateInventory(artifacts);
152
- }
153
- return candidateInventoryKnown;
154
- };
98
+ // sees the same classification. The capture module keeps the original `isPlainObject`
99
+ // fallback for non-baseline callers and exposes only narrow legacy read operations.
100
+ capture.initializeArtifactFallback();
155
101
  // P2(r4321077638): the whole-baseline identity is read exactly once and that same scalar is
156
102
  // validated, so a stateful getter cannot pass validation as A and have every later comparison
157
103
  // use B. A baseline identity whose inspection throws is unusable input: the captured digest
158
104
  // stays '', so baseline-bound evaluations fail closed through `baseline-identity-missing`
159
105
  // 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
- }
106
+ const baselineDigest = capture.captureBaselineIdentity();
169
107
  const executionList = Array.isArray(executionRecords) ? executionRecords.filter(isRecord) : [];
170
108
  const reviewList = Array.isArray(reviewRecords) ? reviewRecords.filter(isRecord) : [];
171
109
 
@@ -183,14 +121,14 @@ function checkDocObligations({ obligations, artifacts, baseline, executionRecord
183
121
 
184
122
  for (const obligation of obligationList) {
185
123
  results.push(evaluateObligation(obligation, {
186
- artifactMap, isCandidateInventoryKnown, baseline, baselineDigest, executionList, reviewList, diagnostics
124
+ capture, baselineDigest, executionList, reviewList, diagnostics
187
125
  }));
188
126
  }
189
127
  return { diagnostics, results };
190
128
  }
191
129
 
192
130
  function evaluateObligation(obligation, context) {
193
- const { artifactMap, isCandidateInventoryKnown, baseline, baselineDigest, executionList, reviewList, diagnostics } = context;
131
+ const { capture, baselineDigest, executionList, reviewList, diagnostics } = context;
194
132
  const capability = typeof obligation.capability === 'string' ? obligation.capability : '';
195
133
  const obligationId = nonEmptyString(obligation.id) ? obligation.id : capability;
196
134
  const scopePaths = scopePathsOf(obligation);
@@ -202,27 +140,8 @@ function evaluateObligation(obligation, context) {
202
140
  const scopeObject = isPlainObject(obligation.scope) ? obligation.scope : null;
203
141
  const declaresBaselinePaths = Boolean(scopeObject && Object.prototype.hasOwnProperty.call(scopeObject, 'baselinePaths'));
204
142
  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();
143
+ const obligationCapture = capture.beginObligation(applicability);
144
+ const baselineArtifactsAvailable = obligationCapture.prepareBaselineContainer();
226
145
  let baselineMetadataError = false;
227
146
  let candidateInventoryUnknown = false;
228
147
  // #335 P3: a candidate inventory can pass the prototype check and still throw from an
@@ -245,7 +164,7 @@ function evaluateObligation(obligation, context) {
245
164
  baselineMetadataDiagnostic('baseline-identity-missing', undefined,
246
165
  'baselinePaths are declared but the baseline carries no whole-baseline digest; baseline-bound evidence cannot be trusted');
247
166
  }
248
- if (!baselineArtifactsMap) {
167
+ if (!baselineArtifactsAvailable) {
249
168
  baselineMetadataDiagnostic('baseline-artifacts-missing', undefined,
250
169
  'baselinePaths are declared but the baseline carries no artifacts map; per-path baseline digests are unavailable');
251
170
  }
@@ -253,35 +172,20 @@ function evaluateObligation(obligation, context) {
253
172
  for (const rawEntry of declaredBaselinePaths) {
254
173
  const invalid = (path, reason) => baselineMetadataDiagnostic('baseline-path-invalid', path,
255
174
  `baselinePaths entry ${path ? JSON.stringify(path) : '(malformed)'} is invalid: ${reason}; the path fails closed`);
256
- if (!isPlainObject(rawEntry)) {
175
+ const entryCapture = obligationCapture.captureEntry(rawEntry);
176
+ if (entryCapture.kind === 'not-object') {
257
177
  invalid(undefined, 'it is not an object with a non-empty path');
258
178
  continue;
259
179
  }
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) {
180
+ if (entryCapture.kind === 'inspection-failed') {
277
181
  invalid(undefined, 'its entry fields could not be inspected safely (a getter or descriptor trap threw); the entry fails closed');
278
182
  continue;
279
183
  }
280
- if (!nonEmptyString(snapshot.path)) {
184
+ if (!nonEmptyString(entryCapture.path)) {
281
185
  invalid(undefined, 'it is not an object with a non-empty path');
282
186
  continue;
283
187
  }
284
- const entryPath = snapshot.path;
188
+ const entryPath = entryCapture.path;
285
189
  if (seen.has(entryPath)) {
286
190
  invalid(entryPath, 'the same path is declared more than once');
287
191
  continue;
@@ -291,12 +195,12 @@ function evaluateObligation(obligation, context) {
291
195
  invalid(entryPath, 'it is not a member of scope.paths');
292
196
  continue;
293
197
  }
294
- const entryReason = snapshot.reason;
198
+ const entryReason = entryCapture.reason;
295
199
  if (entryReason !== 'deleted' && entryReason !== 'rename-from') {
296
- invalid(entryPath, `reason ${JSON.stringify(entryReason)} is not deleted or rename-from`);
200
+ invalid(entryPath, `reason ${obligationCapture.formatInvalidReason(entryCapture.token)} is not deleted or rename-from`);
297
201
  continue;
298
202
  }
299
- if (entryReason === 'deleted' && snapshot.hasOwnRenameTo) {
203
+ if (entryReason === 'deleted' && entryCapture.hasOwnRenameTo) {
300
204
  invalid(entryPath, 'a deleted entry must not carry renameTo');
301
205
  continue;
302
206
  }
@@ -306,13 +210,13 @@ function evaluateObligation(obligation, context) {
306
210
  if (entryReason === 'rename-from') {
307
211
  // #340: the destination annotation is snapshotted once; a stateful getter cannot
308
212
  // make the two validation reads disagree.
309
- const entryRenameTo = snapshot.renameTo;
213
+ const entryRenameTo = entryCapture.renameTo;
310
214
  if (!nonEmptyString(entryRenameTo) || entryRenameTo === entryPath) {
311
215
  invalid(entryPath, 'a rename-from entry needs a distinct non-empty renameTo');
312
216
  continue;
313
217
  }
314
218
  if (!scopePaths.includes(entryRenameTo)) {
315
- invalid(entryPath, `its rename destination ${JSON.stringify(entryRenameTo)} is not a member of scope.paths`);
219
+ invalid(entryPath, `its rename destination ${obligationCapture.formatInvalidRenameTo(entryCapture.token)} is not a member of scope.paths`);
316
220
  continue;
317
221
  }
318
222
  // #340: destination candidate inspection stays applicability-gated. N/A and unknown
@@ -326,52 +230,41 @@ function evaluateObligation(obligation, context) {
326
230
  // honored inside the metadata loop; the failed visited snapshot is never read again
327
231
  // and this entry fails closed with it, so a stateful getter's later successful
328
232
  // 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`);
233
+ if (obligationCapture.isDestinationPoisoned(entryRenameTo)) {
234
+ invalid(entryPath, `its rename destination ${obligationCapture.formatInvalidRenameTo(entryCapture.token)} failed an earlier snapshot validation for this obligation; the failed destination is not read again and this entry fails closed with it`);
331
235
  continue;
332
236
  }
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 {
237
+ const destinationCapture = obligationCapture.captureDestination(entryCapture.token, entryRenameTo);
238
+ if (destinationCapture.kind === 'inspection-failed') {
343
239
  candidateInventoryInspectionFailed = true;
344
240
  // #342 P3: the destination is a visited snapshot even when its digest read threw;
345
241
  // record it so the capture and audit lanes never re-read it.
346
- failedDestinationDigests.set(entryRenameTo, '');
242
+ obligationCapture.poisonDestination(entryRenameTo);
347
243
  continue;
348
244
  }
349
245
  // #340: the captured own value must be a usable non-empty string; an unusable result
350
246
  // is not carried or seeded.
351
- if (!nonEmptyString(destinationDigest)) {
352
- invalid(entryPath, `its rename destination ${JSON.stringify(entryRenameTo)} has no candidate artifact`);
247
+ if (destinationCapture.kind === 'unusable') {
248
+ invalid(entryPath, `its rename destination ${obligationCapture.formatInvalidRenameTo(entryCapture.token)} has no candidate artifact`);
353
249
  // #342 P3: the destination is a visited snapshot even when its own digest is
354
250
  // unusable; record it so the capture and audit lanes never re-read it and cannot
355
251
  // describe a stateful getter's later non-empty value.
356
- failedDestinationDigests.set(entryRenameTo, '');
252
+ obligationCapture.poisonDestination(entryRenameTo);
357
253
  continue;
358
254
  }
359
255
  // #340: entries that share one rename destination must agree on the validated own
360
256
  // digest. A conflicting validated value is a contradictory snapshot and fails the
361
257
  // 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`);
258
+ if (destinationCapture.kind === 'conflict') {
259
+ invalid(entryPath, `its rename destination ${obligationCapture.formatInvalidRenameTo(entryCapture.token)} was validated to conflicting own digests by another accepted baseline entry; the shared destination snapshot is not consistent`);
367
260
  continue;
368
261
  }
369
- renameDestinationDigest = destinationDigest;
262
+ renameDestinationDigest = destinationCapture.digest;
370
263
  }
371
264
  }
372
265
  let candidatePathPresent = false;
373
266
  try {
374
- candidatePathPresent = hasOwnEntry(artifactMap, entryPath);
267
+ candidatePathPresent = obligationCapture.captureSourceAbsence(entryCapture.token, entryPath).kind === 'present';
375
268
  } catch {
376
269
  candidateInventoryInspectionFailed = true;
377
270
  continue;
@@ -381,71 +274,59 @@ function evaluateObligation(obligation, context) {
381
274
  `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
275
  continue;
383
276
  }
384
- if (baselineArtifactsMap) {
385
- let trusted;
386
- try {
387
- trusted = hasOwnEntry(baselineArtifactsMap, entryPath) ? baselineArtifactsMap[entryPath] : undefined;
388
- } catch {
277
+ let trustedBaselineDigest;
278
+ if (baselineArtifactsAvailable) {
279
+ const baselinePathRead = obligationCapture.captureBaselinePath(entryPath);
280
+ if (baselinePathRead.kind === 'unknown') {
389
281
  // #340 P2: a throwing baseline inspection is a metadata error, never an escape.
390
282
  baselineMetadataDiagnostic('baseline-artifact-invalid', entryPath,
391
283
  `the baseline digest for ${entryPath} could not be inspected safely (a getter or descriptor trap threw); the path fails closed`);
392
284
  continue;
393
285
  }
394
- if (trusted === undefined) {
286
+ if (baselinePathRead.kind === 'absent' || baselinePathRead.value === undefined) {
395
287
  baselineMetadataDiagnostic('baseline-artifact-missing', entryPath,
396
288
  `the baseline carries no per-path digest for declared baseline path ${entryPath}; the path fails closed`);
397
289
  continue;
398
290
  }
399
- if (!isSha256(trusted)) {
291
+ if (baselinePathRead.invalidValue || !isSha256(baselinePathRead.value)) {
400
292
  baselineMetadataDiagnostic('baseline-artifact-invalid', entryPath,
401
293
  `the baseline digest for ${entryPath} is not a SHA256 value; the path fails closed`);
402
294
  continue;
403
295
  }
404
- validatedBaselineDigests.set(entryPath, trusted);
296
+ trustedBaselineDigest = baselinePathRead.value;
405
297
  }
406
- baselineEntries.set(entryPath, {
298
+ obligationCapture.acceptBaselineEntry({
299
+ entryToken: entryCapture.token,
407
300
  path: entryPath,
408
301
  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
302
  // #340 P2: carry the exact validated SHA256 so binding never re-reads the
414
303
  // caller-owned baseline map.
415
- trustedBaselineDigest: validatedBaselineDigests.get(entryPath),
416
- ...(entryReason === 'rename-from' ? { renameTo: snapshot.renameTo, renameDestinationDigest } : {})
304
+ trustedBaselineDigest,
305
+ ...(entryReason === 'rename-from' ? { renameTo: entryCapture.renameTo, renameDestinationDigest } : {})
417
306
  });
418
307
  }
419
308
  }
420
309
  }
421
310
 
311
+ // These are fresh detached views from the obligation capture. The evaluator keeps its existing
312
+ // lookup shape while no raw entry or baseline map escapes the capture module.
313
+ const baselineEntries = new Map(obligationCapture.baselineEntries().map(entry => [entry.path, entry]));
314
+ const validatedBaselineDigests = new Map(obligationCapture.baselineAuditPairs());
315
+
422
316
  // #335 P3: a mixed scope combines baseline-only paths with ordinary candidate paths. Capture
423
317
  // every ordinary scoped candidate digest before evidence binding, so a throwing accessor in
424
318
  // the legacy candidate lane fails the inventory closed instead of aborting the obligation.
425
- const candidateDigestCache = new Map();
426
319
  if (applicability === 'applicable' && Array.isArray(declaredBaselinePaths)
427
320
  && declaredBaselinePaths.length > 0) {
428
321
  // #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
- }
322
+ // its own digest during metadata. The capture closure seeds the R6 snapshot from that
323
+ // validated value and never reads the destination through the raw candidate map again, so a
324
+ // destination accessor that throws or changes after validation cannot make capture/binding/
325
+ // audit disagree. The seeded value is the validated own digest, never a caller-asserted
326
+ // rawEntry field. A rejected destination's poisoned state suppresses the ordinary read, so no
327
+ // destination entry is ever read twice.
328
+ if (obligationCapture.captureOrdinaryCandidates(scopePaths)) {
329
+ candidateInventoryInspectionFailed = true;
449
330
  }
450
331
  }
451
332
 
@@ -455,7 +336,7 @@ function evaluateObligation(obligation, context) {
455
336
  // The prototype read happens here, not up front, so unrelated obligations never touch it.
456
337
  if (applicability === 'applicable' && Array.isArray(declaredBaselinePaths)
457
338
  && declaredBaselinePaths.length > 0
458
- && (candidateInventoryInspectionFailed || !isCandidateInventoryKnown())) {
339
+ && (candidateInventoryInspectionFailed || !capture.candidateInventoryKnown())) {
459
340
  candidateInventoryUnknown = true;
460
341
  diagnostics.push({
461
342
  code: 'candidate-artifacts-invalid',
@@ -477,30 +358,13 @@ function evaluateObligation(obligation, context) {
477
358
 
478
359
  // An uninspectable candidate entry must not abort an N/A or unknown obligation while the
479
360
  // 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
- };
361
+ // When a baseline-annotated obligation captured ordinary candidate digests before binding, the
362
+ // capture closure reuses those values for audit identity and stale diagnostics, so a nested
363
+ // digest accessor that only later throws or changes cannot make the audit key or stale verdict
364
+ // disagree with the digest that actually bound evidence. Untouched paths and unannotated
365
+ // callers keep the legacy read.
502
366
  const artifactDigestKey = scopePaths.length
503
- ? scopePaths.map(path => `${path}=${candidateDigestOf(path) || 'missing'}`).join('|')
367
+ ? scopePaths.map(path => `${path}=${obligationCapture.candidateAuditValue(path)}`).join('|')
504
368
  : (baselineDigest || 'no-scope-paths');
505
369
  const baseDigestKey = baselineDigest || 'no-baseline';
506
370
  const result = (observation, outcome, provenance, recordRefs) => ({
@@ -523,21 +387,7 @@ function evaluateObligation(obligation, context) {
523
387
  }
524
388
 
525
389
  const matching = [...executionList, ...reviewList].filter(item => item.obligationId === obligationId);
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
- };
390
+ const candidateBound = (item, path) => obligationCapture.compatibilityCandidateBound(item, path, baselineDigest);
541
391
 
542
392
  // A baseline-only path is satisfied only by a record bound to the trusted baseline digest.
543
393
  // A rename-from path additionally needs the same single record to carry the current
@@ -571,9 +421,9 @@ function evaluateObligation(obligation, context) {
571
421
  // destination to be an own-key candidate inventory entry carrying its own digest
572
422
  // (ownDigestOnce), so a non-empty captured digest is required here and the accessor is
573
423
  // 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;
424
+ const capturedDestination = obligationCapture.capturedCandidate(entry.renameTo);
425
+ if (capturedDestination.kind !== 'captured' || !nonEmptyString(capturedDestination.digest)) return false;
426
+ if (item.artifactDigests[entry.renameTo] !== capturedDestination.digest) return false;
577
427
  }
578
428
  return true;
579
429
  };
@@ -603,8 +453,7 @@ function evaluateObligation(obligation, context) {
603
453
  continue;
604
454
  }
605
455
  if (Array.isArray(item.paths) && isPlainObject(item.artifactDigests)) {
606
- const mismatch = item.paths.find(path => candidateDigestOf(path).length > 0
607
- && nonEmptyString(item.artifactDigests[path]) && item.artifactDigests[path] !== candidateDigestOf(path));
456
+ const mismatch = item.paths.find(path => obligationCapture.compatibilityStaleArtifactMismatch(item, path));
608
457
  if (mismatch) {
609
458
  diagnostics.push({
610
459
  code: 'stale-artifact-digest',
@@ -881,7 +730,7 @@ function evaluateObligation(obligation, context) {
881
730
  && envelope.complete === true
882
731
  && envelope.waiverInventoryComplete === true
883
732
  && nonEmptyString(envelope.baseDigest) && envelope.baseDigest === baselineDigest
884
- && nonEmptyString(envelope.candidateDigest) && envelope.candidateDigest === prdSetDigest(artifactMap)
733
+ && nonEmptyString(envelope.candidateDigest) && envelope.candidateDigest === capture.compatibilityPrdSetDigest()
885
734
  && nonEmptyString(envelope.configDigest);
886
735
  // Q2 (§11): the absence fail is per cited REQ. A REQ that is neither revised in this
887
736
  // change nor covered by a valid waiver that lists it (T3 per-REQ coverage; a waiver
package/sbom.cdx.json CHANGED
@@ -2,23 +2,23 @@
2
2
  "$schema": "https://cyclonedx.org/schema/bom-1.7.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.7",
5
- "serialNumber": "urn:uuid:f2d00cc7-b0ba-5fb7-aa66-45210b7b3234",
5
+ "serialNumber": "urn:uuid:8cda3c47-e4ef-58aa-b9a5-4b6cfd80f466",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-10-20T14:22:39.675Z",
8
+ "timestamp": "2026-10-21T04:48:52.670Z",
9
9
  "tools": [
10
10
  {
11
11
  "vendor": "Harness Everything",
12
12
  "name": "generate-sbom.js",
13
- "version": "0.36.3"
13
+ "version": "0.36.4"
14
14
  }
15
15
  ],
16
16
  "component": {
17
17
  "type": "application",
18
- "bom-ref": "pkg:npm/harness-everything@0.36.3",
18
+ "bom-ref": "pkg:npm/harness-everything@0.36.4",
19
19
  "name": "harness-everything",
20
- "version": "0.36.3",
21
- "purl": "pkg:npm/harness-everything@0.36.3"
20
+ "version": "0.36.4",
21
+ "purl": "pkg:npm/harness-everything@0.36.4"
22
22
  }
23
23
  },
24
24
  "components": [
@@ -596,6 +596,34 @@
596
596
  }
597
597
  ]
598
598
  },
599
+ {
600
+ "type": "library",
601
+ "bom-ref": "pkg:npm/acorn-walk@8.3.5",
602
+ "name": "acorn-walk",
603
+ "version": "8.3.5",
604
+ "scope": "excluded",
605
+ "purl": "pkg:npm/acorn-walk@8.3.5",
606
+ "hashes": [
607
+ {
608
+ "alg": "SHA-512",
609
+ "content": "HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw=="
610
+ }
611
+ ]
612
+ },
613
+ {
614
+ "type": "library",
615
+ "bom-ref": "pkg:npm/acorn@8.19.0",
616
+ "name": "acorn",
617
+ "version": "8.19.0",
618
+ "scope": "excluded",
619
+ "purl": "pkg:npm/acorn@8.19.0",
620
+ "hashes": [
621
+ {
622
+ "alg": "SHA-512",
623
+ "content": "oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw=="
624
+ }
625
+ ]
626
+ },
599
627
  {
600
628
  "type": "library",
601
629
  "bom-ref": "pkg:npm/ajv@8.20.0",
@@ -1892,6 +1920,12 @@
1892
1920
  "pkg:npm/undici-types@8.3.0"
1893
1921
  ]
1894
1922
  },
1923
+ {
1924
+ "ref": "pkg:npm/acorn-walk@8.3.5",
1925
+ "dependsOn": [
1926
+ "pkg:npm/acorn@8.19.0"
1927
+ ]
1928
+ },
1895
1929
  {
1896
1930
  "ref": "pkg:npm/ajv@8.20.0",
1897
1931
  "dependsOn": [
@@ -1999,10 +2033,12 @@
1999
2033
  ]
2000
2034
  },
2001
2035
  {
2002
- "ref": "pkg:npm/harness-everything@0.36.3",
2036
+ "ref": "pkg:npm/harness-everything@0.36.4",
2003
2037
  "dependsOn": [
2004
2038
  "pkg:npm/%40commitlint/cli@19.8.1",
2005
2039
  "pkg:npm/%40commitlint/config-conventional@19.8.1",
2040
+ "pkg:npm/acorn-walk@8.3.5",
2041
+ "pkg:npm/acorn@8.19.0",
2006
2042
  "pkg:npm/husky@9.1.7",
2007
2043
  "pkg:npm/js-yaml@5.4.1"
2008
2044
  ]