harness-everything 0.36.1 → 0.36.3
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/mechanism-2aw-project-docs-config.test.js +450 -10
- package/ci/mechanism-335-baseline-evidence.test.js +355 -7
- package/docs/doc-evidence-baseline-binding.md +28 -7
- package/docs/document-lifecycle.md +60 -8
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-evidence.js +69 -9
- 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 +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 +69 -9
- 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 +7 -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
|
@@ -72,8 +72,18 @@ function artifact(entry) {
|
|
|
72
72
|
|
|
73
73
|
// #335 P2: a baseline-bound rename destination must declare its own digest. An inherited
|
|
74
74
|
// `digest` (for example Object.create({ digest })) is not explicit candidate evidence.
|
|
75
|
-
|
|
76
|
-
|
|
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 : '';
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
function provenanceText(record) {
|
|
@@ -142,7 +152,20 @@ function checkDocObligations({ obligations, artifacts, baseline, executionRecord
|
|
|
142
152
|
}
|
|
143
153
|
return candidateInventoryKnown;
|
|
144
154
|
};
|
|
145
|
-
|
|
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
|
+
}
|
|
146
169
|
const executionList = Array.isArray(executionRecords) ? executionRecords.filter(isRecord) : [];
|
|
147
170
|
const reviewList = Array.isArray(reviewRecords) ? reviewRecords.filter(isRecord) : [];
|
|
148
171
|
|
|
@@ -184,6 +207,16 @@ function evaluateObligation(obligation, context) {
|
|
|
184
207
|
// #340: every accepted entry sharing a rename destination must validate the same own digest,
|
|
185
208
|
// or the shared-destination snapshot is contradictory and the obligation fails closed.
|
|
186
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();
|
|
187
220
|
// #340 P2(r4320818749): the validated per-path baseline digest is captured once during
|
|
188
221
|
// metadata validation. The audit mirror, the stale-baseline diagnostics, and baselineBound()
|
|
189
222
|
// reuse that captured value instead of re-reading the caller map, so a stateful own getter
|
|
@@ -289,19 +322,38 @@ function evaluateObligation(obligation, context) {
|
|
|
289
322
|
// keeps the un-gated legacy candidate read; only applicable obligations capture and
|
|
290
323
|
// validate the destination snapshot.
|
|
291
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
|
+
}
|
|
292
333
|
let destinationDigest = '';
|
|
293
334
|
try {
|
|
294
335
|
if (hasOwnEntry(artifactMap, entryRenameTo)) {
|
|
295
|
-
|
|
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]);
|
|
296
341
|
}
|
|
297
342
|
} catch {
|
|
298
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, '');
|
|
299
347
|
continue;
|
|
300
348
|
}
|
|
301
|
-
// #340: the captured own value must be a usable non-empty string;
|
|
302
|
-
//
|
|
349
|
+
// #340: the captured own value must be a usable non-empty string; an unusable result
|
|
350
|
+
// is not carried or seeded.
|
|
303
351
|
if (!nonEmptyString(destinationDigest)) {
|
|
304
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, '');
|
|
305
357
|
continue;
|
|
306
358
|
}
|
|
307
359
|
// #340: entries that share one rename destination must agree on the validated own
|
|
@@ -377,7 +429,9 @@ function evaluateObligation(obligation, context) {
|
|
|
377
429
|
// its own digest during metadata. Seed the captured snapshot from that validated value and
|
|
378
430
|
// never read the destination through the raw candidate map again, so a destination accessor
|
|
379
431
|
// that throws or changes after validation cannot make capture/binding/audit disagree. The
|
|
380
|
-
// seeded value is the validated own digest, never a caller-asserted rawEntry field.
|
|
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.
|
|
381
435
|
for (const entry of baselineEntries.values()) {
|
|
382
436
|
if (entry.reason === 'rename-from' && nonEmptyString(entry.renameDestinationDigest)
|
|
383
437
|
&& !candidateDigestCache.has(entry.renameTo)) {
|
|
@@ -385,7 +439,8 @@ function evaluateObligation(obligation, context) {
|
|
|
385
439
|
}
|
|
386
440
|
}
|
|
387
441
|
for (const path of scopePaths) {
|
|
388
|
-
if (baselineEntries.has(path) || candidateDigestCache.has(path)
|
|
442
|
+
if (baselineEntries.has(path) || candidateDigestCache.has(path)
|
|
443
|
+
|| failedDestinationDigests.has(path)) continue;
|
|
389
444
|
try {
|
|
390
445
|
candidateDigestCache.set(path, artifact(artifactMap[path]));
|
|
391
446
|
} catch {
|
|
@@ -428,6 +483,11 @@ function evaluateObligation(obligation, context) {
|
|
|
428
483
|
// stale verdict disagree with the digest that actually bound evidence. Untouched paths and
|
|
429
484
|
// unannotated callers keep the legacy read.
|
|
430
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 '';
|
|
431
491
|
if (candidateDigestCache.has(path)) return candidateDigestCache.get(path);
|
|
432
492
|
// #340: only an applicable baseline-annotated obligation validated the source absence as
|
|
433
493
|
// part of its captured snapshot. `N/A` and `unknown` obligations never bind evidence, so they
|
|
@@ -509,7 +569,7 @@ function evaluateObligation(obligation, context) {
|
|
|
509
569
|
// candidate lane, the audit identity, and the stale diagnostics use, so a stateful nested
|
|
510
570
|
// accessor cannot make binding disagree with them. Metadata validation already required the
|
|
511
571
|
// destination to be an own-key candidate inventory entry carrying its own digest
|
|
512
|
-
// (
|
|
572
|
+
// (ownDigestOnce), so a non-empty captured digest is required here and the accessor is
|
|
513
573
|
// never re-read; a missing destination skips capture and lands on this guard.
|
|
514
574
|
const capturedDestination = candidateDigestCache.get(entry.renameTo);
|
|
515
575
|
if (!nonEmptyString(capturedDestination)) return false;
|
|
@@ -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,
|
|
@@ -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,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "harness-everything",
|
|
4
|
-
"version": "0.36.
|
|
4
|
+
"version": "0.36.3",
|
|
5
5
|
"description": "Invariant-first engineering harness for routing, verification, and failure recovery while preserving agent autonomy.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Miya Daniel",
|
|
@@ -72,8 +72,18 @@ function artifact(entry) {
|
|
|
72
72
|
|
|
73
73
|
// #335 P2: a baseline-bound rename destination must declare its own digest. An inherited
|
|
74
74
|
// `digest` (for example Object.create({ digest })) is not explicit candidate evidence.
|
|
75
|
-
|
|
76
|
-
|
|
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 : '';
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
function provenanceText(record) {
|
|
@@ -142,7 +152,20 @@ function checkDocObligations({ obligations, artifacts, baseline, executionRecord
|
|
|
142
152
|
}
|
|
143
153
|
return candidateInventoryKnown;
|
|
144
154
|
};
|
|
145
|
-
|
|
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
|
+
}
|
|
146
169
|
const executionList = Array.isArray(executionRecords) ? executionRecords.filter(isRecord) : [];
|
|
147
170
|
const reviewList = Array.isArray(reviewRecords) ? reviewRecords.filter(isRecord) : [];
|
|
148
171
|
|
|
@@ -184,6 +207,16 @@ function evaluateObligation(obligation, context) {
|
|
|
184
207
|
// #340: every accepted entry sharing a rename destination must validate the same own digest,
|
|
185
208
|
// or the shared-destination snapshot is contradictory and the obligation fails closed.
|
|
186
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();
|
|
187
220
|
// #340 P2(r4320818749): the validated per-path baseline digest is captured once during
|
|
188
221
|
// metadata validation. The audit mirror, the stale-baseline diagnostics, and baselineBound()
|
|
189
222
|
// reuse that captured value instead of re-reading the caller map, so a stateful own getter
|
|
@@ -289,19 +322,38 @@ function evaluateObligation(obligation, context) {
|
|
|
289
322
|
// keeps the un-gated legacy candidate read; only applicable obligations capture and
|
|
290
323
|
// validate the destination snapshot.
|
|
291
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
|
+
}
|
|
292
333
|
let destinationDigest = '';
|
|
293
334
|
try {
|
|
294
335
|
if (hasOwnEntry(artifactMap, entryRenameTo)) {
|
|
295
|
-
|
|
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]);
|
|
296
341
|
}
|
|
297
342
|
} catch {
|
|
298
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, '');
|
|
299
347
|
continue;
|
|
300
348
|
}
|
|
301
|
-
// #340: the captured own value must be a usable non-empty string;
|
|
302
|
-
//
|
|
349
|
+
// #340: the captured own value must be a usable non-empty string; an unusable result
|
|
350
|
+
// is not carried or seeded.
|
|
303
351
|
if (!nonEmptyString(destinationDigest)) {
|
|
304
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, '');
|
|
305
357
|
continue;
|
|
306
358
|
}
|
|
307
359
|
// #340: entries that share one rename destination must agree on the validated own
|
|
@@ -377,7 +429,9 @@ function evaluateObligation(obligation, context) {
|
|
|
377
429
|
// its own digest during metadata. Seed the captured snapshot from that validated value and
|
|
378
430
|
// never read the destination through the raw candidate map again, so a destination accessor
|
|
379
431
|
// that throws or changes after validation cannot make capture/binding/audit disagree. The
|
|
380
|
-
// seeded value is the validated own digest, never a caller-asserted rawEntry field.
|
|
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.
|
|
381
435
|
for (const entry of baselineEntries.values()) {
|
|
382
436
|
if (entry.reason === 'rename-from' && nonEmptyString(entry.renameDestinationDigest)
|
|
383
437
|
&& !candidateDigestCache.has(entry.renameTo)) {
|
|
@@ -385,7 +439,8 @@ function evaluateObligation(obligation, context) {
|
|
|
385
439
|
}
|
|
386
440
|
}
|
|
387
441
|
for (const path of scopePaths) {
|
|
388
|
-
if (baselineEntries.has(path) || candidateDigestCache.has(path)
|
|
442
|
+
if (baselineEntries.has(path) || candidateDigestCache.has(path)
|
|
443
|
+
|| failedDestinationDigests.has(path)) continue;
|
|
389
444
|
try {
|
|
390
445
|
candidateDigestCache.set(path, artifact(artifactMap[path]));
|
|
391
446
|
} catch {
|
|
@@ -428,6 +483,11 @@ function evaluateObligation(obligation, context) {
|
|
|
428
483
|
// stale verdict disagree with the digest that actually bound evidence. Untouched paths and
|
|
429
484
|
// unannotated callers keep the legacy read.
|
|
430
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 '';
|
|
431
491
|
if (candidateDigestCache.has(path)) return candidateDigestCache.get(path);
|
|
432
492
|
// #340: only an applicable baseline-annotated obligation validated the source absence as
|
|
433
493
|
// part of its captured snapshot. `N/A` and `unknown` obligations never bind evidence, so they
|
|
@@ -509,7 +569,7 @@ function evaluateObligation(obligation, context) {
|
|
|
509
569
|
// candidate lane, the audit identity, and the stale diagnostics use, so a stateful nested
|
|
510
570
|
// accessor cannot make binding disagree with them. Metadata validation already required the
|
|
511
571
|
// destination to be an own-key candidate inventory entry carrying its own digest
|
|
512
|
-
// (
|
|
572
|
+
// (ownDigestOnce), so a non-empty captured digest is required here and the accessor is
|
|
513
573
|
// never re-read; a missing destination skips capture and lands on this guard.
|
|
514
574
|
const capturedDestination = candidateDigestCache.get(entry.renameTo);
|
|
515
575
|
if (!nonEmptyString(capturedDestination)) return false;
|
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,
|
package/plugins/harness-everything/skills/multi-agent-workspace/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,
|