harness-everything 0.36.2 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +17 -0
- package/ci/fixtures/doc-obligations/read-boundary/boundary-controls.json +211 -0
- package/ci/fixtures/doc-obligations/read-boundary/boundary-policy.json +325 -0
- package/ci/fixtures/doc-obligations/read-boundary/cases.json +14058 -0
- package/ci/fixtures/doc-obligations/read-boundary/expected.json +1 -0
- package/ci/fixtures/doc-obligations/read-boundary/identity-cases.json +119 -0
- package/ci/fixtures/doc-obligations/read-boundary/identity-expected.json +1 -0
- package/ci/fixtures/doc-obligations/read-boundary/scope-cases.json +208 -0
- package/ci/fixtures/doc-obligations/read-boundary/scope-expected.json +1 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/cases.json +2751 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/flood-entrypoint.js +5 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/hang-entrypoint.js +5 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/pending-red.json +648 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/provider-contract.json +100 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/trust-contract.json +188 -0
- package/ci/lib/doc-evidence-boundary-check.js +1240 -0
- package/ci/lib/doc-evidence-fixtures.js +459 -0
- package/ci/lib/doc-evidence-trace.js +171 -0
- package/ci/lib/doc-reconciliation-prep-fixtures.js +502 -0
- package/ci/lib/snapshot-stdin.js +27 -0
- package/ci/mechanism-2aw-project-docs-config.test.js +450 -10
- package/ci/mechanism-325-3b-preparation.test.js +382 -0
- package/ci/mechanism-341-boundary-enforcement.test.js +660 -0
- package/ci/mechanism-341-capture-module.test.js +357 -0
- package/ci/mechanism-341-mutation-runner.test.js +413 -0
- package/ci/mechanism-341-read-boundary.test.js +388 -0
- package/ci/mechanism-341-snapshot-stdin.test.js +40 -0
- package/ci/mutation-check.js +805 -45
- package/ci/mutation-loader.js +544 -57
- package/ci/prep/doc-reconciliation-red.js +407 -0
- package/ci/stress-341-boundary-control.js +85 -0
- package/docs/doc-evidence-baseline-binding.md +105 -19
- package/docs/doc-evidence-boundary.md +135 -0
- package/docs/doc-workflow-reconciliation.md +224 -0
- package/docs/document-lifecycle.md +67 -8
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-evidence-capture.js +331 -0
- package/harness-everything/scripts/doc-evidence.js +72 -223
- package/harness-everything/scripts/project-docs-resolver.js +30 -13
- package/multi-agent-workspace/SKILL.md +1 -1
- package/multi-agent-workspace/scripts/project-docs-resolver.js +30 -13
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +5 -3
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence-capture.js +331 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +72 -223
- package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js +30 -13
- package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +1 -1
- package/plugins/harness-everything/skills/multi-agent-workspace/scripts/project-docs-resolver.js +30 -13
- package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-spec/scripts/check-project-docs.js +136 -4
- package/plugins/harness-everything/skills/to-spec/scripts/project-docs-resolver.js +30 -13
- package/sbom.cdx.json +43 -7
- package/to-spec/SKILL.md +1 -1
- package/to-spec/scripts/check-project-docs.js +136 -4
- package/to-spec/scripts/project-docs-resolver.js +30 -13
|
@@ -1,29 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
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.
|
|
146
|
-
// non-baseline callers.
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
206
|
-
const
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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 =
|
|
198
|
+
const entryReason = entryCapture.reason;
|
|
295
199
|
if (entryReason !== 'deleted' && entryReason !== 'rename-from') {
|
|
296
|
-
invalid(entryPath, `reason ${
|
|
200
|
+
invalid(entryPath, `reason ${obligationCapture.formatInvalidReason(entryCapture.token)} is not deleted or rename-from`);
|
|
297
201
|
continue;
|
|
298
202
|
}
|
|
299
|
-
if (entryReason === 'deleted' &&
|
|
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 =
|
|
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 ${
|
|
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 (
|
|
330
|
-
invalid(entryPath, `its rename destination ${
|
|
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
|
-
|
|
334
|
-
|
|
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
|
-
|
|
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 (
|
|
352
|
-
invalid(entryPath, `its rename destination ${
|
|
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
|
-
|
|
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
|
-
|
|
363
|
-
|
|
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 =
|
|
262
|
+
renameDestinationDigest = destinationCapture.digest;
|
|
370
263
|
}
|
|
371
264
|
}
|
|
372
265
|
let candidatePathPresent = false;
|
|
373
266
|
try {
|
|
374
|
-
candidatePathPresent =
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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 (
|
|
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(
|
|
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
|
-
|
|
296
|
+
trustedBaselineDigest = baselinePathRead.value;
|
|
405
297
|
}
|
|
406
|
-
|
|
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
|
|
416
|
-
...(entryReason === 'rename-from' ? { renameTo:
|
|
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.
|
|
430
|
-
// never
|
|
431
|
-
// that throws or changes after validation cannot make capture/binding/
|
|
432
|
-
// seeded value is the validated own digest, never a caller-asserted
|
|
433
|
-
// rejected destination
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
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 || !
|
|
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
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
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}=${
|
|
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 =
|
|
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 =>
|
|
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 ===
|
|
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/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js
CHANGED
|
@@ -78,6 +78,25 @@ function isPlainObject(value) {
|
|
|
78
78
|
return prototype === Object.prototype || prototype === null;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
function normalizeHostProjectDocs(projectDocs) {
|
|
82
|
+
if (!isPlainObject(projectDocs)) return { projectDocs, warnings: [] };
|
|
83
|
+
|
|
84
|
+
const normalized = { ...projectDocs };
|
|
85
|
+
const warnings = [];
|
|
86
|
+
if (typeof normalized.domains === 'string') {
|
|
87
|
+
if (typeof normalized.domainLocation !== 'string' || !normalized.domainLocation.trim()) {
|
|
88
|
+
normalized.domainLocation = normalized.domains;
|
|
89
|
+
}
|
|
90
|
+
delete normalized.domains;
|
|
91
|
+
warnings.push({
|
|
92
|
+
code: 'deprecated-domains-string',
|
|
93
|
+
severity: 'warning',
|
|
94
|
+
message: 'host projectDocs.domains as a string is deprecated; use domainLocation for the zone path and domains as the ID-prefix array'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return { projectDocs: normalized, warnings };
|
|
98
|
+
}
|
|
99
|
+
|
|
81
100
|
function projectDocsConfigPath(workspaceRoot) {
|
|
82
101
|
return path.join(workspaceRoot, PROJECT_DOCS_CONFIG);
|
|
83
102
|
}
|
|
@@ -148,22 +167,12 @@ function inspectHostProjectDocs(workspaceRoot) {
|
|
|
148
167
|
continue;
|
|
149
168
|
}
|
|
150
169
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
if (typeof projectDocs.domainLocation !== 'string' || !projectDocs.domainLocation.trim()) {
|
|
154
|
-
projectDocs.domainLocation = projectDocs.domains;
|
|
155
|
-
}
|
|
156
|
-
delete projectDocs.domains;
|
|
157
|
-
entry.warnings.push({
|
|
158
|
-
code: 'deprecated-domains-string',
|
|
159
|
-
severity: 'warning',
|
|
160
|
-
path: configSource(workspaceRoot, file),
|
|
161
|
-
message: 'host projectDocs.domains as a string is deprecated; use domainLocation for the zone path and domains as the ID-prefix array'
|
|
162
|
-
});
|
|
163
|
-
}
|
|
170
|
+
const normalizedHost = normalizeHostProjectDocs(manifest.projectDocs);
|
|
171
|
+
const projectDocs = normalizedHost.projectDocs;
|
|
164
172
|
|
|
165
173
|
const validation = validateDocsConfig(projectDocs);
|
|
166
174
|
const source = configSource(workspaceRoot, file);
|
|
175
|
+
entry.warnings.push(...normalizedHost.warnings.map(item => ({ ...item, path: source })));
|
|
167
176
|
entry.warnings.push(...validation.warnings.map(item => ({ ...item, path: source })));
|
|
168
177
|
if (!validation.valid) {
|
|
169
178
|
const details = validation.diagnostics.map(item => item.message).join('; ');
|
|
@@ -236,6 +245,13 @@ function validateDocsConfig(config) {
|
|
|
236
245
|
return { valid: false, diagnostics, warnings };
|
|
237
246
|
}
|
|
238
247
|
|
|
248
|
+
for (const field of ['docLocation', 'tracker', 'issueDefinition']) {
|
|
249
|
+
if (config[field] !== undefined && typeof config[field] !== 'string') {
|
|
250
|
+
const diagnosticField = field.replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
|
|
251
|
+
invalid(`invalid-${diagnosticField}-type`, `${field} must be a string when provided`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
239
255
|
for (const key of Object.keys(config).sort()) {
|
|
240
256
|
if (!PROJECT_DOC_CONFIG_KEYS.has(key)) {
|
|
241
257
|
warnings.push({
|
|
@@ -754,6 +770,7 @@ module.exports = {
|
|
|
754
770
|
readProjectDocs,
|
|
755
771
|
resolveDocsConfig,
|
|
756
772
|
validateDocsConfig,
|
|
773
|
+
normalizeHostProjectDocs,
|
|
757
774
|
parseContextMap,
|
|
758
775
|
resolveProjectDocs,
|
|
759
776
|
detectProjectDocsConventions,
|