harness-everything 0.47.4 → 0.48.0
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/.agents/harness-everything/project-docs.json +2 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +33 -0
- package/behavioral-evals/README.md +17 -0
- package/behavioral-evals/case-validator.js +15 -1
- package/behavioral-evals/cases/baseline-tdd-workflow.yaml +4 -0
- package/behavioral-evals/run.js +172 -0
- package/behavioral-evals/transcript-parser.js +43 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2a/run13-workflow-requirements.json +42 -0
- package/ci/mechanism-2s-tdd-quality.test.js +149 -6
- package/ci/mechanism-e1-dependency-artifact-scope.test.js +657 -0
- package/ci/mechanism-prb-outline-review.test.js +1 -1
- package/ci/test-behavioral-case-validation.js +215 -1
- package/code-review/SKILL.md +1 -1
- package/code-review/references/review-playbook.md +9 -0
- package/docs/doc-workflow-reconciliation.md +3 -1
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-lifecycle.js +11 -3
- package/harness-everything/scripts/project-docs-resolver.js +12 -0
- package/hooks/scripts/lib/doc-reconciliation-core.js +56 -0
- package/hooks/scripts/lib/doc-reconciliation-producer.js +274 -12
- package/hooks/scripts/lib/doc-reconciliation-provider.js +1 -1
- package/multi-agent-workspace/SKILL.md +1 -1
- package/multi-agent-workspace/scripts/project-docs-resolver.js +12 -0
- 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/hooks/scripts/lib/doc-reconciliation-core.js +56 -0
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-producer.js +274 -12
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-provider.js +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/code-review/SKILL.md +1 -1
- package/plugins/harness-everything/skills/code-review/references/review-playbook.md +9 -0
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +11 -3
- package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js +12 -0
- 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 +12 -0
- package/plugins/harness-everything/skills/tdd/SKILL.md +1 -1
- package/plugins/harness-everything/skills/tdd/fixtures/quality-scenarios.json +10 -2
- package/plugins/harness-everything/skills/tdd/references/quality-model.md +2 -0
- package/plugins/harness-everything/skills/tdd/schemas/quality-evidence.schema.json +21 -0
- package/plugins/harness-everything/skills/tdd/schemas/quality-report.schema.json +2 -0
- package/plugins/harness-everything/skills/tdd/scripts/quality-gate.js +165 -1
- package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-spec/scripts/project-docs-resolver.js +12 -0
- package/plugins/harness-everything/skills/zoom-out/SKILL.md +1 -1
- package/plugins/harness-everything/skills/zoom-out/references/circuit-breaker.md +1 -0
- package/sbom.cdx.json +7 -7
- package/tdd/SKILL.md +1 -1
- package/tdd/fixtures/quality-scenarios.json +10 -2
- package/tdd/references/quality-model.md +2 -0
- package/tdd/schemas/quality-evidence.schema.json +21 -0
- package/tdd/schemas/quality-report.schema.json +2 -0
- package/tdd/scripts/quality-gate.js +165 -1
- package/to-spec/SKILL.md +1 -1
- package/to-spec/scripts/project-docs-resolver.js +12 -0
- package/zoom-out/SKILL.md +1 -1
- package/zoom-out/references/circuit-breaker.md +1 -0
|
@@ -4,14 +4,17 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { execFileSync } = require('child_process');
|
|
6
6
|
const { _internals: U } = require('./doc-reconciliation-core');
|
|
7
|
-
const {
|
|
8
|
-
const { resolveDocsConfigCaptured } = U.runtimeModule('project-docs-resolver.js');
|
|
7
|
+
const { convertWorkspaceFiles } = U.runtimeModule('doc-lifecycle.js');
|
|
8
|
+
const { resolveDocsConfigCaptured, validateDocsConfig } = U.runtimeModule('project-docs-resolver.js');
|
|
9
9
|
const { deriveDocFacts, deriveDocObligations } = U.runtimeModule('doc-obligations.js');
|
|
10
10
|
|
|
11
11
|
const MAX_FILES = 10000;
|
|
12
12
|
const MAX_BYTES = 64 * 1024 * 1024;
|
|
13
13
|
const POSIX_MODES = process.platform !== 'win32';
|
|
14
|
-
|
|
14
|
+
const PROJECT_CONFIG_PATH = '.agents/harness-everything/project-docs.json';
|
|
15
|
+
const SUPPORTED_ARTIFACT_ROOTS = new Set(['node_modules', '.husky/_']);
|
|
16
|
+
const POLICY_VERSION = 'dependency-artifact-scope/1';
|
|
17
|
+
function fail(code, reasonCode) { throw Object.assign(new Error(code), reasonCode ? { reasonCode } : {}); }
|
|
15
18
|
// The JS realpath keeps Windows 8.3 spellings (RUNNER~1) while Git reports the long name, so
|
|
16
19
|
// directory identity uses native resolution and is case-insensitive on Windows (as workflow-isolation).
|
|
17
20
|
function canonicalPath(value) { return fs.realpathSync.native(path.resolve(value)); }
|
|
@@ -64,7 +67,249 @@ function captureBaseline(root, pin) {
|
|
|
64
67
|
validatePaths(records);
|
|
65
68
|
return { commit, treeRef, records };
|
|
66
69
|
}
|
|
67
|
-
|
|
70
|
+
|
|
71
|
+
function pathUnderRoot(relative, root) {
|
|
72
|
+
const candidate = String(relative).replace(/\\/g, '/').normalize('NFC').toLowerCase();
|
|
73
|
+
const expected = root.normalize('NFC').toLowerCase();
|
|
74
|
+
return candidate === expected || candidate.startsWith(`${expected}/`);
|
|
75
|
+
}
|
|
76
|
+
function rootMention(value, root) {
|
|
77
|
+
const normalized = String(value).replace(/\\/g, '/').normalize('NFC').toLowerCase();
|
|
78
|
+
const needle = root.normalize('NFC').toLowerCase();
|
|
79
|
+
let offset = 0;
|
|
80
|
+
while ((offset = normalized.indexOf(needle, offset)) >= 0) {
|
|
81
|
+
const before = offset === 0 || !/[a-z0-9_.-]/.test(normalized[offset - 1]);
|
|
82
|
+
const after = offset + needle.length;
|
|
83
|
+
const boundary = after === normalized.length || /[/*?\s"'`:#),;\]]/.test(normalized[after]);
|
|
84
|
+
if (before && boundary) return true;
|
|
85
|
+
offset++;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
function globSegmentMatches(rawPattern, rawSegment) {
|
|
90
|
+
// Same NFC + lowercase folding as rootMention()/pathUnderRoot(), so a wildcard spelling cannot dodge it.
|
|
91
|
+
const pattern = rawPattern.normalize('NFC').toLowerCase();
|
|
92
|
+
const segment = rawSegment.normalize('NFC').toLowerCase();
|
|
93
|
+
if (/[\[\]{}]/.test(pattern)) return true;
|
|
94
|
+
const escaped = pattern.split(/([*?])/).map(part => part === '*' ? '[\\s\\S]*' : part === '?' ? '[\\s\\S]'
|
|
95
|
+
: part.replace(/[|\\{}()[\]^$+?.]/g, '\\$&')).join('');
|
|
96
|
+
return new RegExp(`^${escaped}$`).test(segment);
|
|
97
|
+
}
|
|
98
|
+
function relativeSelectorMayIntersectRoot(selector, root) {
|
|
99
|
+
const normalized = selector.replace(/\\/g, '/');
|
|
100
|
+
if (/[\[\]{}]/.test(normalized) || normalized.split('/').includes('..')) return true;
|
|
101
|
+
// `.` and empty segments are redundant anywhere in a workspace-relative selector.
|
|
102
|
+
const pattern = normalized.split('/').filter(segment => segment !== '' && segment !== '.');
|
|
103
|
+
const target = root.split('/');
|
|
104
|
+
let states = new Set([0]);
|
|
105
|
+
const closure = values => {
|
|
106
|
+
const expanded = new Set(values);
|
|
107
|
+
let changed = true;
|
|
108
|
+
while (changed) {
|
|
109
|
+
changed = false;
|
|
110
|
+
for (const index of expanded) {
|
|
111
|
+
if (pattern[index] === '**' && !expanded.has(index + 1)) {
|
|
112
|
+
expanded.add(index + 1);
|
|
113
|
+
changed = true;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return expanded;
|
|
118
|
+
};
|
|
119
|
+
states = closure(states);
|
|
120
|
+
for (const segment of target) {
|
|
121
|
+
const next = new Set();
|
|
122
|
+
for (const index of states) {
|
|
123
|
+
if (index >= pattern.length) continue;
|
|
124
|
+
if (pattern[index] === '**') next.add(index);
|
|
125
|
+
else if (globSegmentMatches(pattern[index], segment)) next.add(index + 1);
|
|
126
|
+
}
|
|
127
|
+
states = closure(next);
|
|
128
|
+
if (!states.size) return false;
|
|
129
|
+
}
|
|
130
|
+
return states.size > 0;
|
|
131
|
+
}
|
|
132
|
+
function relativePathWithin(workspace, candidate, pathApi) {
|
|
133
|
+
const relative = pathApi.relative(workspace, candidate);
|
|
134
|
+
if (!relative) return '';
|
|
135
|
+
const normalized = relative.replace(/\\/g, '/');
|
|
136
|
+
// Case and filesystem aliases that cannot be resolved exactly must fail closed.
|
|
137
|
+
const workspaceParts = workspace.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
138
|
+
const candidateParts = candidate.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
139
|
+
const samePrefix = workspaceParts.length <= candidateParts.length
|
|
140
|
+
&& workspaceParts.every((part, index) => part.normalize('NFC').toLowerCase()
|
|
141
|
+
=== candidateParts[index].normalize('NFC').toLowerCase());
|
|
142
|
+
if (samePrefix) return candidateParts.slice(workspaceParts.length).join('/');
|
|
143
|
+
if (pathApi.isAbsolute(relative) || normalized === '..' || normalized.startsWith('../')) return null;
|
|
144
|
+
return normalized;
|
|
145
|
+
}
|
|
146
|
+
function realpathWithMissingSuffix(candidate, pathApi) {
|
|
147
|
+
let current = candidate;
|
|
148
|
+
const suffix = [];
|
|
149
|
+
while (true) {
|
|
150
|
+
try { return pathApi.resolve(fs.realpathSync.native(current), ...suffix); }
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (!['ENOENT', 'ENOTDIR'].includes(error.code)) return null;
|
|
153
|
+
const parent = pathApi.dirname(current);
|
|
154
|
+
if (parent === current) return null;
|
|
155
|
+
suffix.unshift(pathApi.basename(current));
|
|
156
|
+
current = parent;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function absoluteSelectorRelatives(selector, workspaceRoot) {
|
|
161
|
+
const windowsDrive = /^[A-Za-z]:/.test(selector);
|
|
162
|
+
if ((windowsDrive || selector.startsWith('\\') || selector.startsWith('//')) && process.platform !== 'win32') return true;
|
|
163
|
+
const pathApi = process.platform === 'win32' ? path.win32 : path;
|
|
164
|
+
if (windowsDrive && !pathApi.isAbsolute(selector)) return true;
|
|
165
|
+
if (!pathApi.isAbsolute(selector)) return null;
|
|
166
|
+
|
|
167
|
+
const workspace = canonicalPath(workspaceRoot);
|
|
168
|
+
const absolute = pathApi.resolve(selector);
|
|
169
|
+
const lexicalRelative = relativePathWithin(workspace, absolute, pathApi);
|
|
170
|
+
const resolved = realpathWithMissingSuffix(absolute, pathApi);
|
|
171
|
+
if (!resolved) return true;
|
|
172
|
+
const realRelative = relativePathWithin(workspace, resolved, pathApi);
|
|
173
|
+
if (lexicalRelative !== null && realRelative !== null) return [lexicalRelative, realRelative];
|
|
174
|
+
if (lexicalRelative !== null || realRelative !== null) return true;
|
|
175
|
+
// A glob outside the workspace can still expand into it (via a parent such as /tmp/*/node_modules/**
|
|
176
|
+
// or a symlinked entry under an unrelated directory); only literal outside paths are provably disjoint.
|
|
177
|
+
if (/[*?\[\]{}]/.test(selector)) return true;
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
function selectorMayIntersectRoot(selector, root, workspaceRoot) {
|
|
181
|
+
if (typeof selector !== 'string' || !selector.trim()) return true;
|
|
182
|
+
const absolute = absoluteSelectorRelatives(selector, workspaceRoot);
|
|
183
|
+
if (absolute === true) return true;
|
|
184
|
+
if (absolute === false) return false;
|
|
185
|
+
if (absolute !== null) return absolute.some(relative => relativeSelectorMayIntersectRoot(relative || '.', root));
|
|
186
|
+
return relativeSelectorMayIntersectRoot(selector, root);
|
|
187
|
+
}
|
|
188
|
+
function valueMayIntersectRoots(value, roots, workspaceRoot) {
|
|
189
|
+
if (typeof value === 'string') {
|
|
190
|
+
const absolute = absoluteSelectorRelatives(value, workspaceRoot);
|
|
191
|
+
if (absolute === true) return true;
|
|
192
|
+
if (absolute === false) return false;
|
|
193
|
+
if (absolute !== null) return roots.some(root => absolute.some(relative => {
|
|
194
|
+
const selector = relative || '.';
|
|
195
|
+
return rootMention(selector, root)
|
|
196
|
+
|| ((selector.includes('/') || /[*?\[\]{}]/.test(selector)) && selectorMayIntersectRoot(selector, root, workspaceRoot));
|
|
197
|
+
}));
|
|
198
|
+
return roots.some(root => rootMention(value, root)
|
|
199
|
+
|| ((!value.trim() || value.includes('/') || /[*?\[\]{}]/.test(value))
|
|
200
|
+
&& selectorMayIntersectRoot(value, root, workspaceRoot)));
|
|
201
|
+
}
|
|
202
|
+
if (Array.isArray(value)) return value.some(item => valueMayIntersectRoots(item, roots, workspaceRoot));
|
|
203
|
+
if (U.record(value)) return Object.entries(value).some(([key, item]) => valueMayIntersectRoots(key, roots, workspaceRoot)
|
|
204
|
+
|| valueMayIntersectRoots(item, roots, workspaceRoot));
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
function withoutTaskPinIdentity(value) {
|
|
208
|
+
if (Array.isArray(value)) return value.map(withoutTaskPinIdentity);
|
|
209
|
+
if (!U.record(value)) return value;
|
|
210
|
+
return Object.fromEntries(Object.entries(value)
|
|
211
|
+
.filter(([key]) => !/ids?$/i.test(key))
|
|
212
|
+
.map(([key, item]) => [key, withoutTaskPinIdentity(item)]));
|
|
213
|
+
}
|
|
214
|
+
function projectConfigFromBytes(bytes) {
|
|
215
|
+
try {
|
|
216
|
+
const config = JSON.parse(Buffer.from(bytes).toString('utf8'));
|
|
217
|
+
return { config, valid: validateDocsConfig(config).valid };
|
|
218
|
+
} catch { return { config: null, valid: false }; }
|
|
219
|
+
}
|
|
220
|
+
function baselineProjectConfig(baseline) {
|
|
221
|
+
const source = baseline.records.find(item => item.path === PROJECT_CONFIG_PATH && item.presence === 'present');
|
|
222
|
+
return source ? { source, ...projectConfigFromBytes(source.storedBytes) } : { source: null, config: null, valid: false };
|
|
223
|
+
}
|
|
224
|
+
function capturedProjectConfig(config) {
|
|
225
|
+
const source = config.capture.sources.find(item => item.path === PROJECT_CONFIG_PATH);
|
|
226
|
+
return source && source.presence === 'present'
|
|
227
|
+
? { source, ...projectConfigFromBytes(source.bytes) }
|
|
228
|
+
: { source: source || null, config: null, valid: false };
|
|
229
|
+
}
|
|
230
|
+
function dependencyRoots(config) {
|
|
231
|
+
return U.record(config) && U.record(config.inventory)
|
|
232
|
+
&& Object.prototype.hasOwnProperty.call(config.inventory, 'dependencyArtifactRoots')
|
|
233
|
+
? config.inventory.dependencyArtifactRoots : undefined;
|
|
234
|
+
}
|
|
235
|
+
function sameRootField(left, right) {
|
|
236
|
+
if (left === undefined || right === undefined) return left === right;
|
|
237
|
+
return U.stable([...left].sort()) === U.stable([...right].sort());
|
|
238
|
+
}
|
|
239
|
+
function validateExcludedRoots(workspaceRoot, roots) {
|
|
240
|
+
const workspace = canonicalPath(workspaceRoot);
|
|
241
|
+
const states = [];
|
|
242
|
+
for (const root of roots) {
|
|
243
|
+
let parent = workspace;
|
|
244
|
+
let presence = 'present-directory';
|
|
245
|
+
for (const component of root.split('/')) {
|
|
246
|
+
let entries;
|
|
247
|
+
try { entries = fs.readdirSync(parent, { withFileTypes: true }); }
|
|
248
|
+
catch { fail('inventory-incomplete', 'dependency-root-parent-unreadable'); }
|
|
249
|
+
const aliases = entries.filter(entry => entry.name.normalize('NFC').toLowerCase() === component.normalize('NFC').toLowerCase());
|
|
250
|
+
if (aliases.length > 1 || aliases.some(entry => entry.name !== component)) fail('inventory-unsupported', 'dependency-root-alias');
|
|
251
|
+
const current = path.join(parent, component);
|
|
252
|
+
let stat;
|
|
253
|
+
try { stat = fs.lstatSync(current); }
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (error.code === 'ENOENT') { presence = 'absent'; break; }
|
|
256
|
+
fail('inventory-incomplete', 'dependency-root-unreadable');
|
|
257
|
+
}
|
|
258
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) fail('inventory-unsupported', 'dependency-root-not-plain-directory');
|
|
259
|
+
let real;
|
|
260
|
+
try { real = fs.realpathSync.native(current); }
|
|
261
|
+
catch { fail('inventory-incomplete', 'dependency-root-unreadable'); }
|
|
262
|
+
const within = process.platform === 'win32'
|
|
263
|
+
? real.toLowerCase().startsWith(`${workspace.toLowerCase()}${path.sep}`)
|
|
264
|
+
: real.startsWith(`${workspace}${path.sep}`);
|
|
265
|
+
if (!within) fail('inventory-unsupported', 'dependency-root-outside-workspace');
|
|
266
|
+
parent = current;
|
|
267
|
+
}
|
|
268
|
+
states.push({ path: root, presence });
|
|
269
|
+
}
|
|
270
|
+
return states;
|
|
271
|
+
}
|
|
272
|
+
function guardedPolicy(root, baseline, capturedConfig, input) {
|
|
273
|
+
const base = baselineProjectConfig(baseline);
|
|
274
|
+
const current = capturedProjectConfig(capturedConfig);
|
|
275
|
+
const baselineRoots = base.valid ? dependencyRoots(base.config) : undefined;
|
|
276
|
+
const candidateRoots = current.valid ? dependencyRoots(current.config) : undefined;
|
|
277
|
+
if (current.valid && candidateRoots !== undefined && (!base.valid || !sameRootField(baselineRoots, candidateRoots))) {
|
|
278
|
+
fail('inventory-unsupported', 'dependency-artifact-policy-changed-or-unpinned');
|
|
279
|
+
}
|
|
280
|
+
if (!base.valid || baselineRoots === undefined || baselineRoots.length === 0) return null;
|
|
281
|
+
if (!current.valid || !sameRootField(baselineRoots, candidateRoots)) fail('inventory-unsupported', 'dependency-artifact-policy-changed-or-unpinned');
|
|
282
|
+
const roots = [...baselineRoots].sort();
|
|
283
|
+
const indexed = git(root, ['ls-files', '-z']).toString('utf8').split('\0').filter(Boolean);
|
|
284
|
+
if (baseline.records.some(item => roots.some(value => pathUnderRoot(item.path, value)))) fail('inventory-unsupported', 'baseline-path-overlap');
|
|
285
|
+
if (indexed.some(item => roots.some(value => pathUnderRoot(item, value)))) fail('inventory-unsupported', 'index-path-overlap');
|
|
286
|
+
if (capturedConfig.capture.sources.some(item => roots.some(value => pathUnderRoot(item.path, value)))) {
|
|
287
|
+
fail('inventory-unsupported', 'config-source-overlap');
|
|
288
|
+
}
|
|
289
|
+
const configs = [base.config, capturedConfig.resolution.config];
|
|
290
|
+
if (configs.some(config => {
|
|
291
|
+
if (!U.record(config)) return false;
|
|
292
|
+
const selectors = Object.fromEntries(Object.entries(config).filter(([key]) => key !== 'inventory'));
|
|
293
|
+
return valueMayIntersectRoots(selectors, roots, root);
|
|
294
|
+
})) fail('inventory-unsupported', 'config-selector-overlap');
|
|
295
|
+
if (valueMayIntersectRoots(withoutTaskPinIdentity(input.task), roots, root) || valueMayIntersectRoots(input.scope, roots, root)) {
|
|
296
|
+
fail('inventory-unsupported', 'task-input-overlap');
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
roots,
|
|
300
|
+
baselineCommit: baseline.commit,
|
|
301
|
+
storedBytesDigest: U.sha(base.source.storedBytes),
|
|
302
|
+
guardOutcomes: [
|
|
303
|
+
{ guard: 'baseline-paths', state: 'clear', reason: 'No baseline path intersects an approved root.' },
|
|
304
|
+
{ guard: 'index-paths', state: 'clear', reason: 'No current index path intersects an approved root.' },
|
|
305
|
+
{ guard: 'config-sources', state: 'clear', reason: 'No baseline or current config source intersects an approved root.' },
|
|
306
|
+
{ guard: 'config-selectors', state: 'clear', reason: 'Baseline and current config declarations are disjoint from approved roots.' },
|
|
307
|
+
{ guard: 'task-inputs', state: 'clear', reason: 'No explicit task or requirement input intersects an approved root.' }
|
|
308
|
+
]
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function captureCandidate(root, baseline, excludedRoots = []) {
|
|
312
|
+
const excludedRootStates = excludedRoots.length ? validateExcludedRoots(root, excludedRoots) : [];
|
|
68
313
|
const raw = [];
|
|
69
314
|
let total = 0;
|
|
70
315
|
let visited = 0;
|
|
@@ -72,8 +317,9 @@ function captureCandidate(root, baseline) {
|
|
|
72
317
|
const entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => U.compare(a.name, b.name));
|
|
73
318
|
for (const entry of entries) {
|
|
74
319
|
if (!prefix && entry.name === '.git') continue;
|
|
75
|
-
if (++visited > MAX_FILES) fail('inventory-unsupported');
|
|
76
320
|
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
321
|
+
if (excludedRoots.includes(relative)) continue;
|
|
322
|
+
if (++visited > MAX_FILES) fail('inventory-unsupported');
|
|
77
323
|
const absolute = path.join(dir, entry.name);
|
|
78
324
|
const stat = fs.lstatSync(absolute);
|
|
79
325
|
if (stat.isDirectory()) {
|
|
@@ -102,7 +348,8 @@ function captureCandidate(root, baseline) {
|
|
|
102
348
|
}
|
|
103
349
|
}
|
|
104
350
|
// Lifecycle owns text/eol/autocrlf conversion. A completeness walk above detects its skipped paths.
|
|
105
|
-
const stored = new Map(
|
|
351
|
+
const stored = new Map(convertWorkspaceFiles(root, raw.map(item => ({ path: item.path, bytes: item.raw })))
|
|
352
|
+
.map(item => [item.path, item.bytes]));
|
|
106
353
|
const records = raw.map(item => {
|
|
107
354
|
const converted = stored.get(item.path);
|
|
108
355
|
if (!converted) fail('inventory-unsupported');
|
|
@@ -111,7 +358,7 @@ function captureCandidate(root, baseline) {
|
|
|
111
358
|
return fileRecord(item.path, item.mode, converted);
|
|
112
359
|
});
|
|
113
360
|
for (const item of baseline.records) if (!stored.has(item.path)) records.push(fileRecord(item.path, '', Buffer.alloc(0), 'deleted'));
|
|
114
|
-
return records;
|
|
361
|
+
return { records, excludedRootStates };
|
|
115
362
|
}
|
|
116
363
|
const progressKeys = new Set(['status', 'state', 'evidence', 'loaded', 'loadedAt', 'updatedAt', 'createdAt', 'completedAt', 'timestamp', 'report', 'reports']);
|
|
117
364
|
// Progress fields are excluded only at their defined record locations (top-level task/workflow, each
|
|
@@ -169,8 +416,20 @@ function produceDocSnapshot(input, dependencies = {}) {
|
|
|
169
416
|
const baseline = captureBaseline(root, detached.baseline);
|
|
170
417
|
const config = resolveDocsConfigCaptured(root, { activeHost: detached.activeHost });
|
|
171
418
|
if (!['project', 'host'].includes(config.resolution.status) || config.capture.sources.some(s => s.presence === 'unreadable')) fail('config-unavailable');
|
|
172
|
-
const
|
|
173
|
-
const
|
|
419
|
+
const policy = guardedPolicy(root, baseline, config, detached);
|
|
420
|
+
const firstCapture = captureCandidate(root, baseline, policy?.roots || []);
|
|
421
|
+
const coverage = policy ? {
|
|
422
|
+
kind: 'project-with-dependency-artifact-exclusions',
|
|
423
|
+
policyVersion: POLICY_VERSION,
|
|
424
|
+
policySource: { path: PROJECT_CONFIG_PATH, baselineCommit: policy.baselineCommit, storedBytesDigest: policy.storedBytesDigest },
|
|
425
|
+
dependencyArtifactRoots: policy.roots,
|
|
426
|
+
excluded: ['.git', ...policy.roots],
|
|
427
|
+
excludedRootStates: firstCapture.excludedRootStates,
|
|
428
|
+
guardOutcomes: [...policy.guardOutcomes,
|
|
429
|
+
{ guard: 'excluded-root-types', state: 'clear', reason: 'Each approved root is an in-workspace directory or absent.', roots: firstCapture.excludedRootStates }],
|
|
430
|
+
scope: U.clone(detached.scope)
|
|
431
|
+
} : { kind: 'whole-project', excluded: ['.git'], scope: U.clone(detached.scope) };
|
|
432
|
+
const candidate = { records: firstCapture.records, coverage };
|
|
174
433
|
// Same owned config bytes must be the bytes covered by the working-tree capture.
|
|
175
434
|
for (const source of config.capture.sources) {
|
|
176
435
|
const file = candidate.records.find(r => r.path === source.path && r.presence === 'present');
|
|
@@ -189,18 +448,21 @@ function produceDocSnapshot(input, dependencies = {}) {
|
|
|
189
448
|
const facts = derived.facts;
|
|
190
449
|
const obligations = deriveDocObligations(facts);
|
|
191
450
|
const derivationReasons = Object.fromEntries(obligations.map(o => [o.id, { applicability: o.applicability, reason: `captured facts derive ${o.capability}: ${o.applicability}`, factRefs: ['facts', ...(o.scope?.paths || []).map(p => `inventory:${p}`)] }]));
|
|
192
|
-
const again = captureCandidate(root, baseline);
|
|
451
|
+
const again = captureCandidate(root, baseline, policy?.roots || []);
|
|
193
452
|
const configAgain = resolveDocsConfigCaptured(root, { activeHost: detached.activeHost });
|
|
453
|
+
const policyAgain = guardedPolicy(root, baseline, configAgain, detached);
|
|
194
454
|
let finalContext;
|
|
195
455
|
try { finalContext = contextFor(detached, originProvider); }
|
|
196
456
|
catch { fail('snapshot-changed'); }
|
|
197
|
-
if (U.snapshotDigest('candidate', baseline, coverage, again) !== candidate.digest
|
|
457
|
+
if (U.snapshotDigest('candidate', baseline, coverage, again.records) !== candidate.digest
|
|
458
|
+
|| U.stable(again.excludedRootStates) !== U.stable(firstCapture.excludedRootStates)
|
|
459
|
+
|| U.stable(policyAgain) !== U.stable(policy) || U.configDigest(configAgain) !== config.digest
|
|
198
460
|
|| U.stable(originContext) !== U.stable(finalContext)) fail('snapshot-changed');
|
|
199
461
|
return { state: 'ready', snapshot: { identity, baseline, candidate, config, originContext, taskSemantics: planIdentity, facts, obligations, derivationReasons }, diagnostics: [] };
|
|
200
462
|
} catch (error) {
|
|
201
463
|
const codes = ['invalid-input', 'baseline-unavailable', 'inventory-incomplete', 'inventory-unsupported', 'snapshot-changed', 'config-unavailable', 'unsupported-prd-root', 'identity-mismatch', 'origin-unavailable', 'origin-untrusted'];
|
|
202
464
|
const code = codes.includes(error.message) ? error.message : 'inventory-unsupported';
|
|
203
|
-
return { state: 'unavailable', snapshot: null, diagnostics: [U.diagnostic(code)] };
|
|
465
|
+
return { state: 'unavailable', snapshot: null, diagnostics: [U.diagnostic(code, error.reasonCode ? { reasonCode: error.reasonCode } : {})] };
|
|
204
466
|
}
|
|
205
467
|
}
|
|
206
468
|
module.exports = { produceDocSnapshot };
|
|
@@ -77,7 +77,7 @@ const OUTLINE_INVENTORY_PIN = Object.freeze({
|
|
|
77
77
|
inputSha: '0ef0541c6cae550e8269ba712e03797e0e987dfb',
|
|
78
78
|
approvalRef: { issue: 325, commentId: '6436207729', decision: 1 },
|
|
79
79
|
rowDomainMappings: Object.freeze([Object.freeze({ rowIds: Object.freeze(['15', '16', '17', '18']),
|
|
80
|
-
sourceDomain: 'VERI', domain: 'EVID', approvalRef: '
|
|
80
|
+
sourceDomain: 'VERI', domain: 'EVID', approvalRef: 'https://github.com/dyphn1/Harness-everything/issues/325#issuecomment-6706805112' })]),
|
|
81
81
|
retainedArtifactPath: `inventory/${OUTLINE_INVENTORY_SHA256}.md`,
|
|
82
82
|
});
|
|
83
83
|
const OUTLINE_MARKER = 'HARNESS_OUTLINE_DISPATCH_V1';
|
|
@@ -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.
|
|
4
|
+
"version": "0.48.0",
|
|
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",
|
|
@@ -19,6 +19,15 @@ Use this checklist for the applicable risks; do not force irrelevant categories.
|
|
|
19
19
|
- [ ] Follow each important diagnostic/fact through every consumer and ancestor. Check whether required facts are computed before derived authority, indexes, or output.
|
|
20
20
|
- [ ] Compare new rules with previously accepted rules and controls. Separate display severity from safety prerequisites.
|
|
21
21
|
|
|
22
|
+
## Check evidence a gate trusts
|
|
23
|
+
|
|
24
|
+
Apply these when the change grades, gates or labels evidence. Authors SHOULD run them as a self-review before push.
|
|
25
|
+
|
|
26
|
+
- [ ] **Claimed vs observed.** Separate what the subject reports about itself (a tool's `completed` status, a result marker, an agent-supplied expected value, a status label) from what the checker observes independently (exit code, output, a recomputed result, the quoted source). A self-report alone never passes; when nothing independent is observable, the result is inconclusive.
|
|
27
|
+
- [ ] **Select, then judge.** When the last, first or any item of a sequence decides (last run, final message, latest attempt), select from every attempt in order before filtering by outcome. Test both orders: failure then success, and success then failure or no result.
|
|
28
|
+
- [ ] **One witness per rule.** Give each detection rule a negative case that triggers only that rule; a case that also trips an exit code or another pattern cannot show the rule works. Probe positive cases at structural boundaries (inside a token, string or quote), not only at safe separators.
|
|
29
|
+
- [ ] **Comparable subjects, honest labels.** A comparison or conflict needs both sides to describe the same target. A status name claims only what was checked: a consistent quote is not a validated diagnosis.
|
|
30
|
+
|
|
22
31
|
## Check tests and output
|
|
23
32
|
|
|
24
33
|
- [ ] Ask whether each test still passes if the behavior is disabled, bypassed, or broken (mutation thought experiment). Confirm the test is included in CI.
|
|
@@ -1701,6 +1701,15 @@ function parseInventoryDoc(relativePath, bytes) {
|
|
|
1701
1701
|
};
|
|
1702
1702
|
}
|
|
1703
1703
|
|
|
1704
|
+
function convertWorkspaceFiles(root, inventory) {
|
|
1705
|
+
const files = (Array.isArray(inventory) ? inventory : []).map(item => ({
|
|
1706
|
+
relative: item.path,
|
|
1707
|
+
bytes: Buffer.from(item.bytes)
|
|
1708
|
+
}));
|
|
1709
|
+
const rules = readWorkingTreeRules(root, files.filter(item => item.bytes.includes(0x0d)).map(item => item.relative));
|
|
1710
|
+
return files.map(item => ({ path: item.relative, bytes: workingStoredBytes(root, item.relative, item.bytes, rules) }));
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1704
1713
|
function scanWorkspaceFiles(root, scope = null) {
|
|
1705
1714
|
if (!root) return [];
|
|
1706
1715
|
const workspace = path.resolve(root);
|
|
@@ -1728,8 +1737,7 @@ function scanWorkspaceFiles(root, scope = null) {
|
|
|
1728
1737
|
}
|
|
1729
1738
|
}
|
|
1730
1739
|
walk(workspace, '');
|
|
1731
|
-
|
|
1732
|
-
return files.map(item => ({ path: item.relative, bytes: workingStoredBytes(workspace, item.relative, item.bytes, rules) }));
|
|
1740
|
+
return convertWorkspaceFiles(workspace, files.map(item => ({ path: item.relative, bytes: item.bytes })));
|
|
1733
1741
|
}
|
|
1734
1742
|
|
|
1735
1743
|
function scanWorkspaceInventory(root) {
|
|
@@ -2785,6 +2793,6 @@ function main(argv = process.argv.slice(2)) {
|
|
|
2785
2793
|
return analysis.diagnostics.some(item => item.severity === 'error') ? 1 : 0;
|
|
2786
2794
|
}
|
|
2787
2795
|
|
|
2788
|
-
module.exports = { readGitBaseInventory, scanWorkspaceFiles, parseFrontmatter, parseRequirementBlocks, collectDocs, documentCorpusPaths, analyzeDocs, resolveRef, lintDocs, buildIndex, buildIndexSnapshot, configSourceInventory, diffRevisions, buildGitDocumentInventory };
|
|
2796
|
+
module.exports = { readGitBaseInventory, scanWorkspaceFiles, convertWorkspaceFiles, parseFrontmatter, parseRequirementBlocks, collectDocs, documentCorpusPaths, analyzeDocs, resolveRef, lintDocs, buildIndex, buildIndexSnapshot, configSourceInventory, diffRevisions, buildGitDocumentInventory };
|
|
2789
2797
|
|
|
2790
2798
|
if (require.main === module) process.exitCode = main();
|
package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js
CHANGED
|
@@ -21,6 +21,7 @@ const PROJECT_DOCS_CONFIG = path.join('.agents', 'harness-everything', 'project-
|
|
|
21
21
|
const PROJECT_DOC_ROLES = Object.freeze(['prd', 'adr', 'ticket', 'reference', 'log', 'report']);
|
|
22
22
|
const PROJECT_DOC_CONFIG_KEYS = new Set([
|
|
23
23
|
'governed', 'domains', 'legacy', 'derived', 'tickets',
|
|
24
|
+
'inventory',
|
|
24
25
|
'docLocation', 'tracker', 'issueDefinition', 'contextMap', 'contextMapPath',
|
|
25
26
|
'decisionLocation', 'decisions', 'adrLocation', 'adr',
|
|
26
27
|
'domainLocation', 'contextLocation', 'contexts', 'domain',
|
|
@@ -327,6 +328,17 @@ function validateDocsConfig(config) {
|
|
|
327
328
|
}
|
|
328
329
|
}
|
|
329
330
|
|
|
331
|
+
if (Object.prototype.hasOwnProperty.call(config, 'inventory')) {
|
|
332
|
+
const inventory = config.inventory;
|
|
333
|
+
const roots = inventory && inventory.dependencyArtifactRoots;
|
|
334
|
+
const supportedRoots = new Set(['node_modules', '.husky/_']);
|
|
335
|
+
if (!isPlainObject(inventory) || Object.keys(inventory).some(key => key !== 'dependencyArtifactRoots')
|
|
336
|
+
|| !Array.isArray(roots) || roots.some(root => typeof root !== 'string' || !supportedRoots.has(root))
|
|
337
|
+
|| new Set(roots).size !== roots.length) {
|
|
338
|
+
invalid('invalid-dependency-artifact-roots', 'inventory.dependencyArtifactRoots must contain distinct exact roots from node_modules and .husky/_');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
330
342
|
if (Object.prototype.hasOwnProperty.call(config, 'tickets') &&
|
|
331
343
|
config.tickets !== 'tracker' && !isRepoRelativePath(config.tickets)) {
|
|
332
344
|
invalid('invalid-tickets', 'tickets must be "tracker" or a repo-relative path');
|
package/plugins/harness-everything/skills/multi-agent-workspace/scripts/project-docs-resolver.js
CHANGED
|
@@ -21,6 +21,7 @@ const PROJECT_DOCS_CONFIG = path.join('.agents', 'harness-everything', 'project-
|
|
|
21
21
|
const PROJECT_DOC_ROLES = Object.freeze(['prd', 'adr', 'ticket', 'reference', 'log', 'report']);
|
|
22
22
|
const PROJECT_DOC_CONFIG_KEYS = new Set([
|
|
23
23
|
'governed', 'domains', 'legacy', 'derived', 'tickets',
|
|
24
|
+
'inventory',
|
|
24
25
|
'docLocation', 'tracker', 'issueDefinition', 'contextMap', 'contextMapPath',
|
|
25
26
|
'decisionLocation', 'decisions', 'adrLocation', 'adr',
|
|
26
27
|
'domainLocation', 'contextLocation', 'contexts', 'domain',
|
|
@@ -327,6 +328,17 @@ function validateDocsConfig(config) {
|
|
|
327
328
|
}
|
|
328
329
|
}
|
|
329
330
|
|
|
331
|
+
if (Object.prototype.hasOwnProperty.call(config, 'inventory')) {
|
|
332
|
+
const inventory = config.inventory;
|
|
333
|
+
const roots = inventory && inventory.dependencyArtifactRoots;
|
|
334
|
+
const supportedRoots = new Set(['node_modules', '.husky/_']);
|
|
335
|
+
if (!isPlainObject(inventory) || Object.keys(inventory).some(key => key !== 'dependencyArtifactRoots')
|
|
336
|
+
|| !Array.isArray(roots) || roots.some(root => typeof root !== 'string' || !supportedRoots.has(root))
|
|
337
|
+
|| new Set(roots).size !== roots.length) {
|
|
338
|
+
invalid('invalid-dependency-artifact-roots', 'inventory.dependencyArtifactRoots must contain distinct exact roots from node_modules and .husky/_');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
330
342
|
if (Object.prototype.hasOwnProperty.call(config, 'tickets') &&
|
|
331
343
|
config.tickets !== 'tracker' && !isRepoRelativePath(config.tickets)) {
|
|
332
344
|
invalid('invalid-tickets', 'tickets must be "tracker" or a repo-relative path');
|
|
@@ -8,8 +8,16 @@
|
|
|
8
8
|
{ "name": "weighted integration failure", "mutation": "the 0.15 positive dimension scores zero", "expectedScore": 85, "expectedResult": "FAIL" },
|
|
9
9
|
{ "name": "weighted unit failure", "mutation": "unit determinism is N/A and the 0.15 positive dimension scores zero", "expectedScore": 83.33, "expectedResult": "FAIL" },
|
|
10
10
|
{ "name": "NON_CONFORMANT", "mutation": "sourceConformance = NON_CONFORMANT", "expectedResult": "FAIL" },
|
|
11
|
-
{ "name": "SOURCE_DEFECT", "mutation": "sourceValidity = SOURCE_DEFECT and
|
|
12
|
-
{ "name": "
|
|
11
|
+
{ "name": "SOURCE_DEFECT", "mutation": "sourceValidity = SOURCE_DEFECT, sourceNotes is present, and sourceEvidence quotes the defective assertion against the observed value; sourceEvidenceStatus is REVIEWER_REQUIRED, never VERIFIED, because a single-source mismatch does not prove the source is wrong", "expectedResult": "PASS" },
|
|
12
|
+
{ "name": "SOURCE_DEFECT without sourceEvidence", "mutation": "sourceValidity = SOURCE_DEFECT with sourceNotes but no sourceEvidence", "expectedResult": "FAIL" },
|
|
13
|
+
{ "name": "SOURCE_CONFLICT", "mutation": "sourceValidity = SOURCE_CONFLICT with a traced additional source and sourceEvidence quoting both sources", "expectedResult": "FAIL" },
|
|
14
|
+
{ "name": "SOURCE_CONFLICT without sourceEvidence", "mutation": "sourceValidity = SOURCE_CONFLICT with two authoritative sources but no sourceEvidence", "expectedResult": "FAIL" },
|
|
15
|
+
{ "name": "SOURCE_CONFLICT node-versus-value", "mutation": "sourceValidity = SOURCE_CONFLICT citing only expect(list.head.value).toEqual(1) from one spec, observed value is an Element object", "expectedEvidence": "REJECTED", "expectedResult": "FAIL" },
|
|
16
|
+
{ "name": "SOURCE_CONFLICT agreeing expectations", "mutation": "sourceValidity = SOURCE_CONFLICT with two sources expecting the same value and a failing observed value; the mismatch is implementation behaviour", "expectedEvidence": "REJECTED", "expectedResult": "FAIL" },
|
|
17
|
+
{ "name": "SOURCE_CONFLICT two-source evidence", "mutation": "sourceValidity = SOURCE_CONFLICT with two authoritative sources quoting disagreeing expectations for the same subject list.head and a failing observed value; one quote (toBeInstanceOf) is not mechanically checkable, so sourceEvidenceStatus is REVIEWER_REQUIRED and the unresolved conflict gate stays closed", "expectedEvidence": "ACCEPTED", "expectedResult": "FAIL" },
|
|
18
|
+
{ "name": "SOURCE_CONFLICT relabelled expected", "mutation": "sourceValidity = SOURCE_CONFLICT with two authoritative sources quoting the identical assertion expect(list.head).toEqual(1) but expected values 1 and 3; each expected must match its quoted literal, so the second entry is rejected", "expectedEvidence": "REJECTED", "expectedResult": "FAIL" },
|
|
19
|
+
{ "name": "SOURCE_CONFLICT verified literal disagreement", "mutation": "sourceValidity = SOURCE_CONFLICT with two authoritative sources quoting expect(list.head.value).toBe(1) and expect(list.head.value).toBe('1') with matching expected values 1 and '1' and a failing observed value; sourceEvidenceStatus is VERIFIED and the unresolved conflict gate stays closed", "expectedEvidence": "ACCEPTED", "expectedResult": "FAIL" },
|
|
20
|
+
{ "name": "SOURCE_CONFLICT unrelated assertions", "mutation": "sourceValidity = SOURCE_CONFLICT quoting expect(foo).toEqual(1) from one source and expect(bar).toEqual(3) from another; unequal expectations about different subjects", "expectedEvidence": "REJECTED", "expectedResult": "FAIL" },
|
|
13
21
|
{ "name": "malformed input", "mutation": "requirements is not an array", "expectedResult": "FAIL" },
|
|
14
22
|
{ "name": "unexpected input", "mutation": "one unexpectedInput check is FAIL", "expectedResult": "FAIL" },
|
|
15
23
|
{ "name": "error handling", "mutation": "one errorHandling check is SKIPPED", "expectedResult": "FAIL" },
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
This model turns each source requirement into auditable evidence. The authoritative source remains the contract even when it appears wrong. Record a suspected documentation problem as `SOURCE_DEFECT`; record disagreement between traced sources as `SOURCE_CONFLICT`. Never reinterpret either into a passing implementation. If no authoritative source exists, use `INFERRED` and state the inference basis.
|
|
4
4
|
|
|
5
|
+
Before declaring either outcome, `sourceEvidence` quotes the exact conflicting assertions, names each declared source by path, and compares each `expected` value with the `observed` value from the run. A `SOURCE_CONFLICT` needs at least two distinct sources whose expected values disagree about the same `subject` — the contract target each quoted assertion constrains, written as it appears in the quote (`list.head` is not `list.head.value`). Unequal expectations about unrelated targets are not a conflict. If the quoted expectations agree and the run still fails, the mismatch is in the implementation: record `NON_CONFORMANT`, not `SOURCE_CONFLICT`. Each `expected` must match a mechanically readable literal in the quoted assertion (`toEqual`, `toStrictEqual`, `toBe`, or `assert.*` with a JSON or single-quoted literal); otherwise the claim is marked `REVIEWER_REQUIRED` and the gate does not treat it as an established disagreement. `VERIFIED` applies only to a `SOURCE_CONFLICT` whose two quoted literals disagree. A `SOURCE_DEFECT` is always `REVIEWER_REQUIRED`, because a mismatch with the observed value does not prove the source is wrong; the implementation may be the one at fault.
|
|
6
|
+
|
|
5
7
|
## Profile contract
|
|
6
8
|
|
|
7
9
|
Every requirement declares exactly one `testProfile`: `unit` or `integration`. The selected profile defines which case classes are applicable; the common scoring and source rules do not make every test use identical evidence.
|
|
@@ -149,6 +149,18 @@
|
|
|
149
149
|
}
|
|
150
150
|
]
|
|
151
151
|
},
|
|
152
|
+
"sourceEvidence": {
|
|
153
|
+
"type": "object",
|
|
154
|
+
"additionalProperties": false,
|
|
155
|
+
"required": ["source", "assertion", "expected", "observed"],
|
|
156
|
+
"properties": {
|
|
157
|
+
"source": { "type": "string", "minLength": 1 },
|
|
158
|
+
"subject": { "type": "string", "minLength": 1 },
|
|
159
|
+
"assertion": { "type": "string", "minLength": 1 },
|
|
160
|
+
"expected": {},
|
|
161
|
+
"observed": {}
|
|
162
|
+
}
|
|
163
|
+
},
|
|
152
164
|
"requirement": {
|
|
153
165
|
"type": "object",
|
|
154
166
|
"additionalProperties": false,
|
|
@@ -161,6 +173,7 @@
|
|
|
161
173
|
"sourceConformance": { "enum": ["PASS", "NON_CONFORMANT"] },
|
|
162
174
|
"sourceValidity": { "enum": ["VALID", "SOURCE_DEFECT", "SOURCE_CONFLICT"] },
|
|
163
175
|
"sourceNotes": { "type": "string", "minLength": 1 },
|
|
176
|
+
"sourceEvidence": { "type": "array", "items": { "$ref": "#/$defs/sourceEvidence" } },
|
|
164
177
|
"skippedTests": { "type": "array" },
|
|
165
178
|
"flakyTests": { "type": "array" },
|
|
166
179
|
"naDeclarations": { "type": "array" },
|
|
@@ -186,6 +199,14 @@
|
|
|
186
199
|
}
|
|
187
200
|
},
|
|
188
201
|
"allOf": [
|
|
202
|
+
{
|
|
203
|
+
"if": { "required": ["sourceValidity"], "properties": { "sourceValidity": { "enum": ["SOURCE_DEFECT", "SOURCE_CONFLICT"] } } },
|
|
204
|
+
"then": { "required": ["sourceEvidence"], "properties": { "sourceEvidence": { "minItems": 1 } } }
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"if": { "required": ["sourceValidity"], "properties": { "sourceValidity": { "const": "SOURCE_CONFLICT" } } },
|
|
208
|
+
"then": { "properties": { "sourceEvidence": { "items": { "required": ["subject"] } } } }
|
|
209
|
+
},
|
|
189
210
|
{
|
|
190
211
|
"if": {
|
|
191
212
|
"required": ["testProfile"],
|
|
@@ -96,6 +96,8 @@
|
|
|
96
96
|
"sourceConformance": { "enum": ["PASS", "NON_CONFORMANT", null] },
|
|
97
97
|
"sourceValidity": { "enum": ["VALID", "SOURCE_DEFECT", "SOURCE_CONFLICT", null] },
|
|
98
98
|
"sourceNotes": { "type": "string" },
|
|
99
|
+
"sourceEvidence": { "type": "array" },
|
|
100
|
+
"sourceEvidenceStatus": { "enum": ["VERIFIED", "REVIEWER_REQUIRED"], "description": "VERIFIED only for a SOURCE_CONFLICT whose two quoted literals disagree; SOURCE_DEFECT is always REVIEWER_REQUIRED." },
|
|
99
101
|
"dimensions": { "$ref": "#/$defs/dimensions" },
|
|
100
102
|
"skippedTests": { "type": "array" },
|
|
101
103
|
"flakyTests": { "type": "array" },
|