harness-everything 0.14.0 → 0.15.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +17 -0
- package/ci/contract-integrity.test.js +155 -1
- package/contract-integrity/scripts/audit.js +225 -2
- 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/contract-integrity/scripts/audit.js +225 -2
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/verification-loop/SKILL.md +2 -2
- package/plugins/harness-everything/skills/verification-loop/references/verification-phases.md +9 -1
- package/verification-loop/SKILL.md +2 -2
- package/verification-loop/references/verification-phases.md +9 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"name": "harness-everything",
|
|
11
11
|
"source": "./",
|
|
12
12
|
"description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
|
|
13
|
-
"version": "0.
|
|
13
|
+
"version": "0.15.0",
|
|
14
14
|
"author": {
|
|
15
15
|
"name": "Miya Daniel"
|
|
16
16
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harness-everything",
|
|
3
3
|
"description": "Harness OS: an orchestrated agent operating system — tier routing, circuit breaker guardrails, verification gates, WAL session memory, and self-evolving workspace rules.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.15.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Miya Daniel"
|
|
7
7
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.15.0](https://github.com/dyphn1/Harness-everything/compare/v0.14.0...v0.15.0) (2026-09-20)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* **contract:** add freshness provenance helpers ([865fc0b](https://github.com/dyphn1/Harness-everything/commit/865fc0bdb111feff865c3a85767fc587aecbd11a))
|
|
10
|
+
* **contract:** emit requirement-scoped repair guidance ([cbd33ce](https://github.com/dyphn1/Harness-everything/commit/cbd33ce18fb8575671056bd4b2701bd0138baa96))
|
|
11
|
+
* **contract:** reject stale completion reports ([79ea49e](https://github.com/dyphn1/Harness-everything/commit/79ea49e40f110fe062ac714519597e8a23a6ab5b))
|
|
12
|
+
* **contract:** sync freshness audit runtime ([c5d0381](https://github.com/dyphn1/Harness-everything/commit/c5d038163b7ff2dc14e11bd4733e140e85e39f55))
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **contract:** harden freshness evidence boundaries ([33ebbda](https://github.com/dyphn1/Harness-everything/commit/33ebbdafb2b63c4ece86ee81c14c4fcc6d33907f))
|
|
17
|
+
* **contract:** launch npm probes cross-platform ([9ca5315](https://github.com/dyphn1/Harness-everything/commit/9ca5315cb30e8f03b08fb1f8789a141ea1179af4))
|
|
18
|
+
* **contract:** launch npm probes cross-platform ([2cad76e](https://github.com/dyphn1/Harness-everything/commit/2cad76e217af136d934946f05c740d816e051acd))
|
|
19
|
+
* **contract:** require complete freshness provenance ([5b6d717](https://github.com/dyphn1/Harness-everything/commit/5b6d717487fce37dcd1d1e8cbe8b037597035103))
|
|
20
|
+
* **contract:** require complete freshness provenance ([d94e256](https://github.com/dyphn1/Harness-everything/commit/d94e2562c54d615d223a8572fd4d468e017a5807))
|
|
21
|
+
|
|
5
22
|
## [0.14.0](https://github.com/dyphn1/Harness-everything/compare/v0.13.0...v0.14.0) (2026-09-20)
|
|
6
23
|
|
|
7
24
|
### Features
|
|
@@ -265,5 +265,159 @@ for (const relative of [
|
|
|
265
265
|
'Phase 2 OpenAI package mirror matches canonical: ' + relative);
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
-
|
|
268
|
+
// Phase 4: reports carry actionable requirement-scoped repair guidance and
|
|
269
|
+
// content fingerprints so stale completion evidence cannot be reused.
|
|
270
|
+
check(driftReport.repairGuidance.some(item =>
|
|
271
|
+
item.requirementId === 'REQ-001' && item.code === 'update-living-spec' &&
|
|
272
|
+
item.sourceRef === 'docs/specs/api.md#REQ-001'),
|
|
273
|
+
'stale-spec drift emits requirement-scoped living-spec repair guidance');
|
|
274
|
+
check(paddedReport.repairGuidance.some(item =>
|
|
275
|
+
item.requirementId === 'REQ-001' && item.code === 'strengthen-weak-oracle' && item.priority === 'BLOCKER'),
|
|
276
|
+
'surviving required probe emits BLOCKER weak-oracle guidance');
|
|
277
|
+
check(/Repair Guidance/.test(markdown(driftReport)) && /REQ-001/.test(markdown(driftReport)),
|
|
278
|
+
'Markdown keeps repair guidance concise and tied to requirement identity');
|
|
279
|
+
|
|
280
|
+
const freshRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-contract-freshness-'));
|
|
281
|
+
try {
|
|
282
|
+
const workspace = path.join(freshRoot, 'workspace');
|
|
283
|
+
fs.mkdirSync(workspace, { recursive: true });
|
|
284
|
+
for (const artifact of baseline.artifacts) {
|
|
285
|
+
const file = path.join(workspace, artifact.path);
|
|
286
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
287
|
+
fs.writeFileSync(file, artifact.id + ': fixture content\n', 'utf8');
|
|
288
|
+
}
|
|
289
|
+
const tracePath = path.join(freshRoot, 'trace.json');
|
|
290
|
+
const tddPath = path.join(freshRoot, 'tdd.json');
|
|
291
|
+
const reportPath = path.join(freshRoot, 'report.json');
|
|
292
|
+
fs.writeFileSync(tracePath, JSON.stringify(baseline), 'utf8');
|
|
293
|
+
fs.writeFileSync(tddPath, JSON.stringify(baselineTdd), 'utf8');
|
|
294
|
+
|
|
295
|
+
const unboundReportPath = path.join(freshRoot, 'report-unbound.json');
|
|
296
|
+
const unboundAudit = spawnSync(process.execPath, [
|
|
297
|
+
path.join(ROOT, 'contract-integrity/scripts/audit.js'),
|
|
298
|
+
tracePath,
|
|
299
|
+
'--tdd-evidence', tddPath,
|
|
300
|
+
'--output', unboundReportPath,
|
|
301
|
+
], { encoding: 'utf8' });
|
|
302
|
+
check(unboundAudit.status === 0, 'strict audit may emit a PASS report without workspace binding for immediate inspection');
|
|
303
|
+
const emptyWorkspace = path.join(freshRoot, 'empty-workspace');
|
|
304
|
+
fs.mkdirSync(emptyWorkspace, { recursive: true });
|
|
305
|
+
const unboundFreshness = spawnSync(process.execPath, [
|
|
306
|
+
path.join(ROOT, 'contract-integrity/scripts/audit.js'),
|
|
307
|
+
tracePath,
|
|
308
|
+
'--tdd-evidence', tddPath,
|
|
309
|
+
'--workspace', emptyWorkspace,
|
|
310
|
+
'--verify-fresh', unboundReportPath,
|
|
311
|
+
], { encoding: 'utf8' });
|
|
312
|
+
check(unboundFreshness.status === 1 &&
|
|
313
|
+
/report-workspace-unbound/.test(unboundFreshness.stderr) &&
|
|
314
|
+
/artifact-fingerprint-missing:/.test(unboundFreshness.stderr),
|
|
315
|
+
'report created without workspace fingerprints cannot later authorize freshness against a supplied workspace');
|
|
316
|
+
|
|
317
|
+
const audit = spawnSync(process.execPath, [
|
|
318
|
+
path.join(ROOT, 'contract-integrity/scripts/audit.js'),
|
|
319
|
+
tracePath,
|
|
320
|
+
'--tdd-evidence', tddPath,
|
|
321
|
+
'--workspace', workspace,
|
|
322
|
+
'--output', reportPath,
|
|
323
|
+
], { encoding: 'utf8' });
|
|
324
|
+
check(audit.status === 0, 'strict audit with existing workspace artifacts produces fresh PASS report', audit.stderr);
|
|
325
|
+
const freshReport = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
|
326
|
+
check(freshReport.provenance?.inputs?.trace?.sha256 &&
|
|
327
|
+
freshReport.provenance?.inputs?.tddEvidence?.sha256 &&
|
|
328
|
+
freshReport.provenance?.artifacts?.length === baseline.artifacts.length &&
|
|
329
|
+
freshReport.provenance.artifacts.every(item => item.sha256 && item.missing === false),
|
|
330
|
+
'report provenance records trace/TDD hashes plus current artifact hashes');
|
|
331
|
+
|
|
332
|
+
const verifyFresh = () => spawnSync(process.execPath, [
|
|
333
|
+
path.join(ROOT, 'contract-integrity/scripts/audit.js'),
|
|
334
|
+
tracePath,
|
|
335
|
+
'--tdd-evidence', tddPath,
|
|
336
|
+
'--workspace', workspace,
|
|
337
|
+
'--verify-fresh', reportPath,
|
|
338
|
+
], { encoding: 'utf8' });
|
|
339
|
+
|
|
340
|
+
const current = verifyFresh();
|
|
341
|
+
check(current.status === 0 && /Freshness: PASS/.test(current.stdout),
|
|
342
|
+
'unchanged PASS report can authorize completion');
|
|
343
|
+
|
|
344
|
+
const failingTracePath = path.join(freshRoot, 'trace-failing.json');
|
|
345
|
+
const failingReportPath = path.join(freshRoot, 'report-failing.json');
|
|
346
|
+
const failingTrace = clone(baseline);
|
|
347
|
+
failingTrace.probes[0].status = 'SURVIVED';
|
|
348
|
+
failingTrace.probes[0].failureClass = 'none';
|
|
349
|
+
fs.writeFileSync(failingTracePath, JSON.stringify(failingTrace), 'utf8');
|
|
350
|
+
const failingAudit = spawnSync(process.execPath, [
|
|
351
|
+
path.join(ROOT, 'contract-integrity/scripts/audit.js'),
|
|
352
|
+
failingTracePath,
|
|
353
|
+
'--tdd-evidence', tddPath,
|
|
354
|
+
'--workspace', workspace,
|
|
355
|
+
'--output', failingReportPath,
|
|
356
|
+
], { encoding: 'utf8' });
|
|
357
|
+
check(failingAudit.status === 1, 'surviving required probe produces a reusable FAIL report fixture');
|
|
358
|
+
const forgedReport = JSON.parse(fs.readFileSync(failingReportPath, 'utf8'));
|
|
359
|
+
forgedReport.result = 'PASS';
|
|
360
|
+
forgedReport.completionGate = 'PASS';
|
|
361
|
+
fs.writeFileSync(failingReportPath, JSON.stringify(forgedReport), 'utf8');
|
|
362
|
+
const forgedFreshness = spawnSync(process.execPath, [
|
|
363
|
+
path.join(ROOT, 'contract-integrity/scripts/audit.js'),
|
|
364
|
+
failingTracePath,
|
|
365
|
+
'--tdd-evidence', tddPath,
|
|
366
|
+
'--workspace', workspace,
|
|
367
|
+
'--verify-fresh', failingReportPath,
|
|
368
|
+
], { encoding: 'utf8' });
|
|
369
|
+
check(forgedFreshness.status === 1 && /current-audit-not-pass/.test(forgedFreshness.stderr),
|
|
370
|
+
'freshness verification re-derives the current gate instead of trusting forged PASS fields');
|
|
371
|
+
|
|
372
|
+
const linkedWorkspace = path.join(freshRoot, 'linked-workspace');
|
|
373
|
+
fs.mkdirSync(linkedWorkspace, { recursive: true });
|
|
374
|
+
for (const artifact of baseline.artifacts.filter(item => item.id !== 'IMPL-001')) {
|
|
375
|
+
const file = path.join(linkedWorkspace, artifact.path);
|
|
376
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
377
|
+
fs.writeFileSync(file, artifact.id + ': fixture content\n', 'utf8');
|
|
378
|
+
}
|
|
379
|
+
const outsideImpl = path.join(freshRoot, 'outside-implementation');
|
|
380
|
+
fs.mkdirSync(outsideImpl, { recursive: true });
|
|
381
|
+
fs.writeFileSync(path.join(outsideImpl, 'api.js'), 'IMPL-001: fixture content\n', 'utf8');
|
|
382
|
+
fs.symlinkSync(outsideImpl, path.join(linkedWorkspace, 'src'),
|
|
383
|
+
process.platform === 'win32' ? 'junction' : 'dir');
|
|
384
|
+
const linkedAudit = spawnSync(process.execPath, [
|
|
385
|
+
path.join(ROOT, 'contract-integrity/scripts/audit.js'),
|
|
386
|
+
tracePath,
|
|
387
|
+
'--tdd-evidence', tddPath,
|
|
388
|
+
'--workspace', linkedWorkspace,
|
|
389
|
+
], { encoding: 'utf8' });
|
|
390
|
+
check(linkedAudit.status === 2 && /linked-path: IMPL-001/.test(linkedAudit.stderr),
|
|
391
|
+
'workspace-aware audit rejects artifact paths that resolve through a symlink or junction');
|
|
392
|
+
|
|
393
|
+
const testArtifact = path.join(workspace, 'tests/api.test.js');
|
|
394
|
+
fs.appendFileSync(testArtifact, 'changed after audit\n', 'utf8');
|
|
395
|
+
const staleTestReport = verifyFresh();
|
|
396
|
+
check(staleTestReport.status === 1 &&
|
|
397
|
+
/artifact-changed-since-audit:TEST-001/.test(staleTestReport.stderr),
|
|
398
|
+
'test artifact change invalidates previously passing report');
|
|
399
|
+
fs.writeFileSync(testArtifact, 'TEST-001: fixture content\n', 'utf8');
|
|
400
|
+
|
|
401
|
+
fs.writeFileSync(tddPath, JSON.stringify({ ...baselineTdd, changedAfterAudit: true }), 'utf8');
|
|
402
|
+
const staleEvidence = verifyFresh();
|
|
403
|
+
check(staleEvidence.status === 1 &&
|
|
404
|
+
/tdd-evidence-changed-since-audit/.test(staleEvidence.stderr),
|
|
405
|
+
'#58 evidence change invalidates previously passing report');
|
|
406
|
+
|
|
407
|
+
fs.writeFileSync(tddPath, JSON.stringify(baselineTdd), 'utf8');
|
|
408
|
+
fs.rmSync(path.join(workspace, 'src/api.js'));
|
|
409
|
+
const missingArtifactAudit = spawnSync(process.execPath, [
|
|
410
|
+
path.join(ROOT, 'contract-integrity/scripts/audit.js'),
|
|
411
|
+
tracePath,
|
|
412
|
+
'--tdd-evidence', tddPath,
|
|
413
|
+
'--workspace', workspace,
|
|
414
|
+
], { encoding: 'utf8' });
|
|
415
|
+
check(missingArtifactAudit.status === 2 &&
|
|
416
|
+
/provenance artifact missing from workspace: IMPL-001/.test(missingArtifactAudit.stderr),
|
|
417
|
+
'strict workspace-aware audit fails closed when traced implementation evidence is missing');
|
|
418
|
+
} finally {
|
|
419
|
+
fs.rmSync(freshRoot, { recursive: true, force: true });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
console.log(`\n${failed === 0 ? 'PASS' : 'FAIL'}: #84 contract integrity phase 1-4 mechanism (${failed} failure${failed === 1 ? '' : 's'})`);
|
|
269
423
|
process.exit(failed === 0 ? 0 : 1);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const crypto = require('crypto');
|
|
4
5
|
const fs = require('fs');
|
|
5
6
|
const path = require('path');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
6
8
|
const { evaluateEvidence } = require('../../tdd/scripts/quality-gate');
|
|
7
9
|
|
|
8
10
|
const VERSION = '1.0.0';
|
|
@@ -57,6 +59,181 @@ function stable(value) {
|
|
|
57
59
|
return Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])]));
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
function sha256File(file) {
|
|
63
|
+
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function realpath(file) {
|
|
67
|
+
return fs.realpathSync.native ? fs.realpathSync.native(file) : fs.realpathSync(file);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function containedPath(root, target) {
|
|
71
|
+
const relative = path.relative(root, target);
|
|
72
|
+
return relative === '' ||
|
|
73
|
+
(relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function gitRevision(workspace) {
|
|
77
|
+
if (!workspace) return null;
|
|
78
|
+
const result = spawnSync('git', ['rev-parse', 'HEAD'], {
|
|
79
|
+
cwd: workspace,
|
|
80
|
+
encoding: 'utf8',
|
|
81
|
+
windowsHide: true,
|
|
82
|
+
});
|
|
83
|
+
return result.status === 0 ? String(result.stdout || '').trim() || null : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function artifactFingerprints(trace, workspace) {
|
|
87
|
+
if (!workspace) return [];
|
|
88
|
+
const root = path.resolve(workspace);
|
|
89
|
+
let realRoot;
|
|
90
|
+
try {
|
|
91
|
+
if (fs.lstatSync(root).isSymbolicLink()) throw new Error('linked-root');
|
|
92
|
+
realRoot = realpath(root);
|
|
93
|
+
} catch (_) {
|
|
94
|
+
return (trace?.artifacts || []).map(artifact => ({
|
|
95
|
+
id: artifact.id, path: artifact.path, sha256: null, missing: true, reason: 'workspace-unavailable',
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
return (trace?.artifacts || []).map(artifact => {
|
|
99
|
+
const full = path.resolve(root, artifact.path);
|
|
100
|
+
if (!containedPath(root, full)) {
|
|
101
|
+
return { id: artifact.id, path: artifact.path, sha256: null, missing: true, reason: 'escaped-path' };
|
|
102
|
+
}
|
|
103
|
+
let current = root;
|
|
104
|
+
for (const segment of path.relative(root, full).split(path.sep).filter(Boolean)) {
|
|
105
|
+
current = path.join(current, segment);
|
|
106
|
+
if (!fs.existsSync(current)) {
|
|
107
|
+
return { id: artifact.id, path: artifact.path, sha256: null, missing: true, reason: 'missing' };
|
|
108
|
+
}
|
|
109
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
110
|
+
return { id: artifact.id, path: artifact.path, sha256: null, missing: true, reason: 'linked-path' };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
let physical;
|
|
114
|
+
try { physical = realpath(full); }
|
|
115
|
+
catch (_) {
|
|
116
|
+
return { id: artifact.id, path: artifact.path, sha256: null, missing: true, reason: 'missing' };
|
|
117
|
+
}
|
|
118
|
+
if (!containedPath(realRoot, physical) || !fs.lstatSync(physical).isFile()) {
|
|
119
|
+
return { id: artifact.id, path: artifact.path, sha256: null, missing: true, reason: 'escaped-path' };
|
|
120
|
+
}
|
|
121
|
+
return { id: artifact.id, path: artifact.path, sha256: sha256File(physical), missing: false };
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildProvenance(args, trace) {
|
|
126
|
+
const workspace = args.workspace ? path.resolve(args.workspace) : null;
|
|
127
|
+
return {
|
|
128
|
+
schemaVersion: 1,
|
|
129
|
+
generatedAt: new Date().toISOString(),
|
|
130
|
+
auditVersion: VERSION,
|
|
131
|
+
nodeVersion: process.version,
|
|
132
|
+
command: 'node contract-integrity/scripts/audit.js <trace> [--tdd-evidence <evidence>] [--workspace <workspace>]',
|
|
133
|
+
gitRevision: gitRevision(workspace || process.cwd()),
|
|
134
|
+
workspaceIsolation: unique((trace?.probes || []).map(probe => probe.workspaceIsolation).filter(Boolean)),
|
|
135
|
+
workspace: workspace ? { root: workspace } : null,
|
|
136
|
+
inputs: {
|
|
137
|
+
trace: { sha256: sha256File(path.resolve(args.input)) },
|
|
138
|
+
tddEvidence: args.tddEvidence ? { sha256: sha256File(path.resolve(args.tddEvidence)) } : null,
|
|
139
|
+
},
|
|
140
|
+
artifacts: artifactFingerprints(trace, workspace),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function verifyFreshReport(report, args, trace, currentAudit = null) {
|
|
145
|
+
const reasons = [];
|
|
146
|
+
if (!isObject(report?.provenance) || !isObject(report.provenance.inputs)) {
|
|
147
|
+
return { fresh: false, authorizesCompletion: false, reasons: ['report-provenance-missing'] };
|
|
148
|
+
}
|
|
149
|
+
const currentTraceHash = sha256File(path.resolve(args.input));
|
|
150
|
+
if (report.provenance.inputs.trace?.sha256 !== currentTraceHash) reasons.push('trace-changed-since-audit');
|
|
151
|
+
const currentTddHash = args.tddEvidence ? sha256File(path.resolve(args.tddEvidence)) : null;
|
|
152
|
+
const reportedTddHash = report.provenance.inputs.tddEvidence?.sha256 || null;
|
|
153
|
+
if (currentTddHash !== reportedTddHash) reasons.push('tdd-evidence-changed-since-audit');
|
|
154
|
+
if (!currentAudit || currentAudit.result !== 'PASS' || currentAudit.completionGate !== 'PASS' || currentAudit.errors?.length) {
|
|
155
|
+
reasons.push('current-audit-not-pass');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const tracedArtifacts = Array.isArray(trace?.artifacts) ? trace.artifacts : [];
|
|
159
|
+
if (tracedArtifacts.length > 0) {
|
|
160
|
+
if (!args.workspace) {
|
|
161
|
+
reasons.push('workspace-required-for-artifact-freshness');
|
|
162
|
+
} else {
|
|
163
|
+
const reportedRoot = report.provenance.workspace?.root;
|
|
164
|
+
if (!reportedRoot) {
|
|
165
|
+
reasons.push('report-workspace-unbound');
|
|
166
|
+
} else if (path.resolve(reportedRoot) !== path.resolve(args.workspace)) {
|
|
167
|
+
reasons.push('workspace-changed-since-audit');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const priorArtifacts = Array.isArray(report.provenance.artifacts) ? report.provenance.artifacts : [];
|
|
171
|
+
const prior = new Map();
|
|
172
|
+
for (const item of priorArtifacts) {
|
|
173
|
+
if (item?.id && !prior.has(item.id)) prior.set(item.id, item);
|
|
174
|
+
else if (item?.id) reasons.push(`artifact-fingerprint-duplicate:${item.id}`);
|
|
175
|
+
}
|
|
176
|
+
const current = new Map(artifactFingerprints(trace, args.workspace).map(item => [item.id, item]));
|
|
177
|
+
for (const artifact of tracedArtifacts) {
|
|
178
|
+
const before = prior.get(artifact.id);
|
|
179
|
+
const now = current.get(artifact.id);
|
|
180
|
+
if (!before || before.path !== artifact.path || before.missing || !before.sha256) {
|
|
181
|
+
reasons.push(`artifact-fingerprint-missing:${artifact.id}`);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (!now || now.missing || now.sha256 !== before.sha256) {
|
|
185
|
+
reasons.push(`artifact-changed-since-audit:${artifact.id}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (priorArtifacts.length !== tracedArtifacts.length) {
|
|
189
|
+
reasons.push('artifact-fingerprint-set-incomplete');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
fresh: reasons.length === 0,
|
|
195
|
+
authorizesCompletion: reasons.length === 0 && report.completionGate === 'PASS' && report.result === 'PASS',
|
|
196
|
+
reasons: unique(reasons),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function repairGuidance(requirements) {
|
|
201
|
+
const priorityOrder = { BLOCKER: 0, HIGH: 1, MEDIUM: 2 };
|
|
202
|
+
const recommendations = [];
|
|
203
|
+
const add = (priority, req, code, action) => recommendations.push({
|
|
204
|
+
priority,
|
|
205
|
+
requirementId: req.requirementId,
|
|
206
|
+
sourceRef: req.sourceRef || null,
|
|
207
|
+
code,
|
|
208
|
+
action,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
for (const req of requirements) {
|
|
212
|
+
if (req.lifecycleStatus !== 'CURRENT') continue;
|
|
213
|
+
if (req.drift.includes('SOURCE_CONFLICT')) add('BLOCKER', req, 'resolve-source-conflict', 'Resolve the authoritative source conflict before changing tests or implementation.');
|
|
214
|
+
if (req.drift.includes('SOURCE_DEFECT')) add('BLOCKER', req, 'repair-source-defect', 'Repair or explicitly supersede the defective authoritative source before completion.');
|
|
215
|
+
if (req.protection.status === 'SURVIVED') add('BLOCKER', req, 'strengthen-weak-oracle', 'Strengthen the requirement-linked observable assertion so every required contract probe is killed for the expected reason.');
|
|
216
|
+
if (req.drift.includes('STALE_SPEC')) add('HIGH', req, 'update-living-spec', 'Update the living specification and requirement revision before accepting behavior that moved ahead of the contract.');
|
|
217
|
+
if (req.drift.includes('STALE_TEST')) add('HIGH', req, 'refresh-requirement-tests', 'Update requirement-linked tests and RED evidence against the current living specification.');
|
|
218
|
+
if (req.drift.includes('STALE_IMPLEMENTATION')) add('HIGH', req, 'refresh-implementation-evidence', 'Attach implementation evidence for the current requirement revision and rerun verification.');
|
|
219
|
+
if (req.drift.includes('UNTRACED_CHANGE')) add('HIGH', req, 'repair-contract-lineage', 'Restore ADR/spec/ticket/test/implementation lineage for this requirement before completion.');
|
|
220
|
+
if (req.protection.status === 'NOT_EVALUATED') add('HIGH', req, 'execute-required-probes', 'Execute every predeclared required contract probe in an isolated supported adapter or retain NOT_EVALUATED.');
|
|
221
|
+
if (req.completeness.result !== 'PASS' || req.completeness.score < 100) add('HIGH', req, 'complete-tdd-evidence', 'Repair the existing #58 completeness evidence; do not compensate with protection score.');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const seen = new Set();
|
|
225
|
+
return recommendations
|
|
226
|
+
.filter(item => {
|
|
227
|
+
const key = `${item.requirementId}:${item.code}`;
|
|
228
|
+
if (seen.has(key)) return false;
|
|
229
|
+
seen.add(key);
|
|
230
|
+
return true;
|
|
231
|
+
})
|
|
232
|
+
.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority] ||
|
|
233
|
+
String(a.requirementId).localeCompare(String(b.requirementId)) ||
|
|
234
|
+
a.code.localeCompare(b.code));
|
|
235
|
+
}
|
|
236
|
+
|
|
60
237
|
function validateTrace(input) {
|
|
61
238
|
const errors = [];
|
|
62
239
|
if (!isObject(input)) return ['trace root must be an object'];
|
|
@@ -385,6 +562,7 @@ function evaluateTrace(trace, options = {}) {
|
|
|
385
562
|
requirementId: requirement.requirementId,
|
|
386
563
|
revision: requirement.revision,
|
|
387
564
|
lifecycleStatus: requirement.status,
|
|
565
|
+
sourceRef: currentSpec ? `${currentSpec.path}#${requirement.requirementId}` : null,
|
|
388
566
|
drift: unique(statuses),
|
|
389
567
|
reasonCodes: lineage.reasons,
|
|
390
568
|
completeness: quality,
|
|
@@ -429,6 +607,7 @@ function evaluateTrace(trace, options = {}) {
|
|
|
429
607
|
},
|
|
430
608
|
driftCounts,
|
|
431
609
|
requirements: reportRequirements,
|
|
610
|
+
repairGuidance: repairGuidance(reportRequirements),
|
|
432
611
|
graph: {
|
|
433
612
|
nodes: [
|
|
434
613
|
...(trace?.artifacts || []).map(artifact => ({ id: artifact.id, kind: artifact.kind, status: artifact.status, path: artifact.path })),
|
|
@@ -458,9 +637,22 @@ function markdown(report) {
|
|
|
458
637
|
for (const req of report.requirements) {
|
|
459
638
|
lines.push(`| ${req.requirementId} | ${req.revision} | ${req.drift.join(', ')} | ${req.completeness.score} | ${req.protection.status} | ${req.gateEligible ? 'PASS' : 'FAIL'} |`);
|
|
460
639
|
}
|
|
640
|
+
if (report.repairGuidance?.length) {
|
|
641
|
+
lines.push('', '## Repair Guidance', '');
|
|
642
|
+
for (const item of report.repairGuidance) {
|
|
643
|
+
lines.push(`- **${item.priority}** ${item.requirementId}${item.sourceRef ? ` (${item.sourceRef})` : ''}: ${item.action} [${item.code}]`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
461
646
|
if (report.errors.length) {
|
|
462
647
|
lines.push('', '## Errors', '', ...report.errors.map(error => `- ${error}`));
|
|
463
648
|
}
|
|
649
|
+
if (report.provenance) {
|
|
650
|
+
lines.push('', '## Provenance', '');
|
|
651
|
+
lines.push(`- Audit version: ${report.provenance.auditVersion}`);
|
|
652
|
+
lines.push(`- Git revision: ${report.provenance.gitRevision || 'unknown'}`);
|
|
653
|
+
lines.push(`- Trace SHA-256: ${report.provenance.inputs?.trace?.sha256 || 'unknown'}`);
|
|
654
|
+
lines.push(`- TDD evidence SHA-256: ${report.provenance.inputs?.tddEvidence?.sha256 || 'none'}`);
|
|
655
|
+
}
|
|
464
656
|
lines.push('', '## Evidence boundary', '');
|
|
465
657
|
lines.push('- Completeness is derived by the existing #58 evaluator; aggregate scores supplied by agents are ignored.');
|
|
466
658
|
lines.push('- Protection counts only predeclared required probes. Extra/padded probes do not raise a requirement score.');
|
|
@@ -470,13 +662,15 @@ function markdown(report) {
|
|
|
470
662
|
}
|
|
471
663
|
|
|
472
664
|
function parseArgs(argv) {
|
|
473
|
-
const args = { input: null, tddEvidence: null, output: null, markdown: null };
|
|
665
|
+
const args = { input: null, tddEvidence: null, output: null, markdown: null, workspace: null, verifyFresh: null };
|
|
474
666
|
for (let i = 0; i < argv.length; i++) {
|
|
475
667
|
const token = argv[i];
|
|
476
668
|
if (!args.input && !token.startsWith('--')) args.input = token;
|
|
477
669
|
else if (token === '--tdd-evidence') args.tddEvidence = argv[++i];
|
|
478
670
|
else if (token === '--output') args.output = argv[++i];
|
|
479
671
|
else if (token === '--markdown') args.markdown = argv[++i];
|
|
672
|
+
else if (token === '--workspace') args.workspace = argv[++i];
|
|
673
|
+
else if (token === '--verify-fresh') args.verifyFresh = argv[++i];
|
|
480
674
|
else throw new Error(`unknown argument: ${token}`);
|
|
481
675
|
}
|
|
482
676
|
return args;
|
|
@@ -487,7 +681,7 @@ function runCli(argv) {
|
|
|
487
681
|
try { args = parseArgs(argv.slice(2)); }
|
|
488
682
|
catch (error) { console.error(`Contract Integrity: FAIL\n- ${error.message}`); return 2; }
|
|
489
683
|
if (!args.input) {
|
|
490
|
-
console.error('Usage: node contract-integrity/scripts/audit.js <trace.json> [--tdd-evidence <#58-evidence.json>] [--output report.json] [--markdown report.md]');
|
|
684
|
+
console.error('Usage: node contract-integrity/scripts/audit.js <trace.json> [--tdd-evidence <#58-evidence.json>] [--workspace <root>] [--output report.json] [--markdown report.md] [--verify-fresh prior-report.json]');
|
|
491
685
|
return 2;
|
|
492
686
|
}
|
|
493
687
|
|
|
@@ -501,7 +695,33 @@ function runCli(argv) {
|
|
|
501
695
|
return 2;
|
|
502
696
|
}
|
|
503
697
|
|
|
698
|
+
if (args.verifyFresh) {
|
|
699
|
+
let prior;
|
|
700
|
+
try { prior = readJson(args.verifyFresh); }
|
|
701
|
+
catch (error) {
|
|
702
|
+
console.error(`Contract Integrity Freshness: FAIL\n- ${error.message}`);
|
|
703
|
+
return 2;
|
|
704
|
+
}
|
|
705
|
+
const currentAudit = evaluateTrace(trace, { tddEvidence });
|
|
706
|
+
const freshness = verifyFreshReport(prior, args, trace, currentAudit);
|
|
707
|
+
console.log(`Contract Integrity Freshness: ${freshness.authorizesCompletion ? 'PASS' : 'FAIL'}`);
|
|
708
|
+
for (const reason of freshness.reasons) console.error(`- ${reason}`);
|
|
709
|
+
return freshness.authorizesCompletion ? 0 : 1;
|
|
710
|
+
}
|
|
711
|
+
|
|
504
712
|
const report = evaluateTrace(trace, { tddEvidence });
|
|
713
|
+
report.provenance = buildProvenance(args, trace);
|
|
714
|
+
if (args.workspace) {
|
|
715
|
+
const missing = report.provenance.artifacts.filter(item => item.missing);
|
|
716
|
+
if (trace.mode === 'strict' && missing.length) {
|
|
717
|
+
report.result = 'FAIL';
|
|
718
|
+
report.completionGate = 'FAIL';
|
|
719
|
+
report.gateEligible = false;
|
|
720
|
+
report.errors.push(...missing.map(item => item.reason === 'linked-path'
|
|
721
|
+
? `provenance artifact linked-path: ${item.id} -> ${item.path}`
|
|
722
|
+
: `provenance artifact missing from workspace: ${item.id} -> ${item.path}`));
|
|
723
|
+
}
|
|
724
|
+
}
|
|
505
725
|
console.log(`Contract Integrity: ${report.result}`);
|
|
506
726
|
console.log(`Scores: completeness=${report.scores.completenessScore} protection=${report.scores.protectionScore} integrity=${report.scores.contractIntegrityScore}`);
|
|
507
727
|
console.log(`Gate: ${report.completionGate}`);
|
|
@@ -527,10 +747,13 @@ if (require.main === module) process.exit(runCli(process.argv));
|
|
|
527
747
|
module.exports = {
|
|
528
748
|
DRIFT,
|
|
529
749
|
VERSION,
|
|
750
|
+
buildProvenance,
|
|
530
751
|
classifyLineage,
|
|
531
752
|
evaluateTrace,
|
|
532
753
|
markdown,
|
|
754
|
+
repairGuidance,
|
|
533
755
|
protectionForRequirement,
|
|
534
756
|
runCli,
|
|
535
757
|
validateTrace,
|
|
758
|
+
verifyFreshReport,
|
|
536
759
|
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const crypto = require('crypto');
|
|
4
5
|
const fs = require('fs');
|
|
5
6
|
const path = require('path');
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
6
8
|
const { evaluateEvidence } = require('../../skills/tdd/scripts/quality-gate');
|
|
7
9
|
|
|
8
10
|
const VERSION = '1.0.0';
|
|
@@ -57,6 +59,181 @@ function stable(value) {
|
|
|
57
59
|
return Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])]));
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
function sha256File(file) {
|
|
63
|
+
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function realpath(file) {
|
|
67
|
+
return fs.realpathSync.native ? fs.realpathSync.native(file) : fs.realpathSync(file);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function containedPath(root, target) {
|
|
71
|
+
const relative = path.relative(root, target);
|
|
72
|
+
return relative === '' ||
|
|
73
|
+
(relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function gitRevision(workspace) {
|
|
77
|
+
if (!workspace) return null;
|
|
78
|
+
const result = spawnSync('git', ['rev-parse', 'HEAD'], {
|
|
79
|
+
cwd: workspace,
|
|
80
|
+
encoding: 'utf8',
|
|
81
|
+
windowsHide: true,
|
|
82
|
+
});
|
|
83
|
+
return result.status === 0 ? String(result.stdout || '').trim() || null : null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function artifactFingerprints(trace, workspace) {
|
|
87
|
+
if (!workspace) return [];
|
|
88
|
+
const root = path.resolve(workspace);
|
|
89
|
+
let realRoot;
|
|
90
|
+
try {
|
|
91
|
+
if (fs.lstatSync(root).isSymbolicLink()) throw new Error('linked-root');
|
|
92
|
+
realRoot = realpath(root);
|
|
93
|
+
} catch (_) {
|
|
94
|
+
return (trace?.artifacts || []).map(artifact => ({
|
|
95
|
+
id: artifact.id, path: artifact.path, sha256: null, missing: true, reason: 'workspace-unavailable',
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
return (trace?.artifacts || []).map(artifact => {
|
|
99
|
+
const full = path.resolve(root, artifact.path);
|
|
100
|
+
if (!containedPath(root, full)) {
|
|
101
|
+
return { id: artifact.id, path: artifact.path, sha256: null, missing: true, reason: 'escaped-path' };
|
|
102
|
+
}
|
|
103
|
+
let current = root;
|
|
104
|
+
for (const segment of path.relative(root, full).split(path.sep).filter(Boolean)) {
|
|
105
|
+
current = path.join(current, segment);
|
|
106
|
+
if (!fs.existsSync(current)) {
|
|
107
|
+
return { id: artifact.id, path: artifact.path, sha256: null, missing: true, reason: 'missing' };
|
|
108
|
+
}
|
|
109
|
+
if (fs.lstatSync(current).isSymbolicLink()) {
|
|
110
|
+
return { id: artifact.id, path: artifact.path, sha256: null, missing: true, reason: 'linked-path' };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
let physical;
|
|
114
|
+
try { physical = realpath(full); }
|
|
115
|
+
catch (_) {
|
|
116
|
+
return { id: artifact.id, path: artifact.path, sha256: null, missing: true, reason: 'missing' };
|
|
117
|
+
}
|
|
118
|
+
if (!containedPath(realRoot, physical) || !fs.lstatSync(physical).isFile()) {
|
|
119
|
+
return { id: artifact.id, path: artifact.path, sha256: null, missing: true, reason: 'escaped-path' };
|
|
120
|
+
}
|
|
121
|
+
return { id: artifact.id, path: artifact.path, sha256: sha256File(physical), missing: false };
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildProvenance(args, trace) {
|
|
126
|
+
const workspace = args.workspace ? path.resolve(args.workspace) : null;
|
|
127
|
+
return {
|
|
128
|
+
schemaVersion: 1,
|
|
129
|
+
generatedAt: new Date().toISOString(),
|
|
130
|
+
auditVersion: VERSION,
|
|
131
|
+
nodeVersion: process.version,
|
|
132
|
+
command: 'node contract-integrity/scripts/audit.js <trace> [--tdd-evidence <evidence>] [--workspace <workspace>]',
|
|
133
|
+
gitRevision: gitRevision(workspace || process.cwd()),
|
|
134
|
+
workspaceIsolation: unique((trace?.probes || []).map(probe => probe.workspaceIsolation).filter(Boolean)),
|
|
135
|
+
workspace: workspace ? { root: workspace } : null,
|
|
136
|
+
inputs: {
|
|
137
|
+
trace: { sha256: sha256File(path.resolve(args.input)) },
|
|
138
|
+
tddEvidence: args.tddEvidence ? { sha256: sha256File(path.resolve(args.tddEvidence)) } : null,
|
|
139
|
+
},
|
|
140
|
+
artifacts: artifactFingerprints(trace, workspace),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function verifyFreshReport(report, args, trace, currentAudit = null) {
|
|
145
|
+
const reasons = [];
|
|
146
|
+
if (!isObject(report?.provenance) || !isObject(report.provenance.inputs)) {
|
|
147
|
+
return { fresh: false, authorizesCompletion: false, reasons: ['report-provenance-missing'] };
|
|
148
|
+
}
|
|
149
|
+
const currentTraceHash = sha256File(path.resolve(args.input));
|
|
150
|
+
if (report.provenance.inputs.trace?.sha256 !== currentTraceHash) reasons.push('trace-changed-since-audit');
|
|
151
|
+
const currentTddHash = args.tddEvidence ? sha256File(path.resolve(args.tddEvidence)) : null;
|
|
152
|
+
const reportedTddHash = report.provenance.inputs.tddEvidence?.sha256 || null;
|
|
153
|
+
if (currentTddHash !== reportedTddHash) reasons.push('tdd-evidence-changed-since-audit');
|
|
154
|
+
if (!currentAudit || currentAudit.result !== 'PASS' || currentAudit.completionGate !== 'PASS' || currentAudit.errors?.length) {
|
|
155
|
+
reasons.push('current-audit-not-pass');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const tracedArtifacts = Array.isArray(trace?.artifacts) ? trace.artifacts : [];
|
|
159
|
+
if (tracedArtifacts.length > 0) {
|
|
160
|
+
if (!args.workspace) {
|
|
161
|
+
reasons.push('workspace-required-for-artifact-freshness');
|
|
162
|
+
} else {
|
|
163
|
+
const reportedRoot = report.provenance.workspace?.root;
|
|
164
|
+
if (!reportedRoot) {
|
|
165
|
+
reasons.push('report-workspace-unbound');
|
|
166
|
+
} else if (path.resolve(reportedRoot) !== path.resolve(args.workspace)) {
|
|
167
|
+
reasons.push('workspace-changed-since-audit');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const priorArtifacts = Array.isArray(report.provenance.artifacts) ? report.provenance.artifacts : [];
|
|
171
|
+
const prior = new Map();
|
|
172
|
+
for (const item of priorArtifacts) {
|
|
173
|
+
if (item?.id && !prior.has(item.id)) prior.set(item.id, item);
|
|
174
|
+
else if (item?.id) reasons.push(`artifact-fingerprint-duplicate:${item.id}`);
|
|
175
|
+
}
|
|
176
|
+
const current = new Map(artifactFingerprints(trace, args.workspace).map(item => [item.id, item]));
|
|
177
|
+
for (const artifact of tracedArtifacts) {
|
|
178
|
+
const before = prior.get(artifact.id);
|
|
179
|
+
const now = current.get(artifact.id);
|
|
180
|
+
if (!before || before.path !== artifact.path || before.missing || !before.sha256) {
|
|
181
|
+
reasons.push(`artifact-fingerprint-missing:${artifact.id}`);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (!now || now.missing || now.sha256 !== before.sha256) {
|
|
185
|
+
reasons.push(`artifact-changed-since-audit:${artifact.id}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (priorArtifacts.length !== tracedArtifacts.length) {
|
|
189
|
+
reasons.push('artifact-fingerprint-set-incomplete');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
fresh: reasons.length === 0,
|
|
195
|
+
authorizesCompletion: reasons.length === 0 && report.completionGate === 'PASS' && report.result === 'PASS',
|
|
196
|
+
reasons: unique(reasons),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function repairGuidance(requirements) {
|
|
201
|
+
const priorityOrder = { BLOCKER: 0, HIGH: 1, MEDIUM: 2 };
|
|
202
|
+
const recommendations = [];
|
|
203
|
+
const add = (priority, req, code, action) => recommendations.push({
|
|
204
|
+
priority,
|
|
205
|
+
requirementId: req.requirementId,
|
|
206
|
+
sourceRef: req.sourceRef || null,
|
|
207
|
+
code,
|
|
208
|
+
action,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
for (const req of requirements) {
|
|
212
|
+
if (req.lifecycleStatus !== 'CURRENT') continue;
|
|
213
|
+
if (req.drift.includes('SOURCE_CONFLICT')) add('BLOCKER', req, 'resolve-source-conflict', 'Resolve the authoritative source conflict before changing tests or implementation.');
|
|
214
|
+
if (req.drift.includes('SOURCE_DEFECT')) add('BLOCKER', req, 'repair-source-defect', 'Repair or explicitly supersede the defective authoritative source before completion.');
|
|
215
|
+
if (req.protection.status === 'SURVIVED') add('BLOCKER', req, 'strengthen-weak-oracle', 'Strengthen the requirement-linked observable assertion so every required contract probe is killed for the expected reason.');
|
|
216
|
+
if (req.drift.includes('STALE_SPEC')) add('HIGH', req, 'update-living-spec', 'Update the living specification and requirement revision before accepting behavior that moved ahead of the contract.');
|
|
217
|
+
if (req.drift.includes('STALE_TEST')) add('HIGH', req, 'refresh-requirement-tests', 'Update requirement-linked tests and RED evidence against the current living specification.');
|
|
218
|
+
if (req.drift.includes('STALE_IMPLEMENTATION')) add('HIGH', req, 'refresh-implementation-evidence', 'Attach implementation evidence for the current requirement revision and rerun verification.');
|
|
219
|
+
if (req.drift.includes('UNTRACED_CHANGE')) add('HIGH', req, 'repair-contract-lineage', 'Restore ADR/spec/ticket/test/implementation lineage for this requirement before completion.');
|
|
220
|
+
if (req.protection.status === 'NOT_EVALUATED') add('HIGH', req, 'execute-required-probes', 'Execute every predeclared required contract probe in an isolated supported adapter or retain NOT_EVALUATED.');
|
|
221
|
+
if (req.completeness.result !== 'PASS' || req.completeness.score < 100) add('HIGH', req, 'complete-tdd-evidence', 'Repair the existing #58 completeness evidence; do not compensate with protection score.');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const seen = new Set();
|
|
225
|
+
return recommendations
|
|
226
|
+
.filter(item => {
|
|
227
|
+
const key = `${item.requirementId}:${item.code}`;
|
|
228
|
+
if (seen.has(key)) return false;
|
|
229
|
+
seen.add(key);
|
|
230
|
+
return true;
|
|
231
|
+
})
|
|
232
|
+
.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority] ||
|
|
233
|
+
String(a.requirementId).localeCompare(String(b.requirementId)) ||
|
|
234
|
+
a.code.localeCompare(b.code));
|
|
235
|
+
}
|
|
236
|
+
|
|
60
237
|
function validateTrace(input) {
|
|
61
238
|
const errors = [];
|
|
62
239
|
if (!isObject(input)) return ['trace root must be an object'];
|
|
@@ -385,6 +562,7 @@ function evaluateTrace(trace, options = {}) {
|
|
|
385
562
|
requirementId: requirement.requirementId,
|
|
386
563
|
revision: requirement.revision,
|
|
387
564
|
lifecycleStatus: requirement.status,
|
|
565
|
+
sourceRef: currentSpec ? `${currentSpec.path}#${requirement.requirementId}` : null,
|
|
388
566
|
drift: unique(statuses),
|
|
389
567
|
reasonCodes: lineage.reasons,
|
|
390
568
|
completeness: quality,
|
|
@@ -429,6 +607,7 @@ function evaluateTrace(trace, options = {}) {
|
|
|
429
607
|
},
|
|
430
608
|
driftCounts,
|
|
431
609
|
requirements: reportRequirements,
|
|
610
|
+
repairGuidance: repairGuidance(reportRequirements),
|
|
432
611
|
graph: {
|
|
433
612
|
nodes: [
|
|
434
613
|
...(trace?.artifacts || []).map(artifact => ({ id: artifact.id, kind: artifact.kind, status: artifact.status, path: artifact.path })),
|
|
@@ -458,9 +637,22 @@ function markdown(report) {
|
|
|
458
637
|
for (const req of report.requirements) {
|
|
459
638
|
lines.push(`| ${req.requirementId} | ${req.revision} | ${req.drift.join(', ')} | ${req.completeness.score} | ${req.protection.status} | ${req.gateEligible ? 'PASS' : 'FAIL'} |`);
|
|
460
639
|
}
|
|
640
|
+
if (report.repairGuidance?.length) {
|
|
641
|
+
lines.push('', '## Repair Guidance', '');
|
|
642
|
+
for (const item of report.repairGuidance) {
|
|
643
|
+
lines.push(`- **${item.priority}** ${item.requirementId}${item.sourceRef ? ` (${item.sourceRef})` : ''}: ${item.action} [${item.code}]`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
461
646
|
if (report.errors.length) {
|
|
462
647
|
lines.push('', '## Errors', '', ...report.errors.map(error => `- ${error}`));
|
|
463
648
|
}
|
|
649
|
+
if (report.provenance) {
|
|
650
|
+
lines.push('', '## Provenance', '');
|
|
651
|
+
lines.push(`- Audit version: ${report.provenance.auditVersion}`);
|
|
652
|
+
lines.push(`- Git revision: ${report.provenance.gitRevision || 'unknown'}`);
|
|
653
|
+
lines.push(`- Trace SHA-256: ${report.provenance.inputs?.trace?.sha256 || 'unknown'}`);
|
|
654
|
+
lines.push(`- TDD evidence SHA-256: ${report.provenance.inputs?.tddEvidence?.sha256 || 'none'}`);
|
|
655
|
+
}
|
|
464
656
|
lines.push('', '## Evidence boundary', '');
|
|
465
657
|
lines.push('- Completeness is derived by the existing #58 evaluator; aggregate scores supplied by agents are ignored.');
|
|
466
658
|
lines.push('- Protection counts only predeclared required probes. Extra/padded probes do not raise a requirement score.');
|
|
@@ -470,13 +662,15 @@ function markdown(report) {
|
|
|
470
662
|
}
|
|
471
663
|
|
|
472
664
|
function parseArgs(argv) {
|
|
473
|
-
const args = { input: null, tddEvidence: null, output: null, markdown: null };
|
|
665
|
+
const args = { input: null, tddEvidence: null, output: null, markdown: null, workspace: null, verifyFresh: null };
|
|
474
666
|
for (let i = 0; i < argv.length; i++) {
|
|
475
667
|
const token = argv[i];
|
|
476
668
|
if (!args.input && !token.startsWith('--')) args.input = token;
|
|
477
669
|
else if (token === '--tdd-evidence') args.tddEvidence = argv[++i];
|
|
478
670
|
else if (token === '--output') args.output = argv[++i];
|
|
479
671
|
else if (token === '--markdown') args.markdown = argv[++i];
|
|
672
|
+
else if (token === '--workspace') args.workspace = argv[++i];
|
|
673
|
+
else if (token === '--verify-fresh') args.verifyFresh = argv[++i];
|
|
480
674
|
else throw new Error(`unknown argument: ${token}`);
|
|
481
675
|
}
|
|
482
676
|
return args;
|
|
@@ -487,7 +681,7 @@ function runCli(argv) {
|
|
|
487
681
|
try { args = parseArgs(argv.slice(2)); }
|
|
488
682
|
catch (error) { console.error(`Contract Integrity: FAIL\n- ${error.message}`); return 2; }
|
|
489
683
|
if (!args.input) {
|
|
490
|
-
console.error('Usage: node contract-integrity/scripts/audit.js <trace.json> [--tdd-evidence <#58-evidence.json>] [--output report.json] [--markdown report.md]');
|
|
684
|
+
console.error('Usage: node contract-integrity/scripts/audit.js <trace.json> [--tdd-evidence <#58-evidence.json>] [--workspace <root>] [--output report.json] [--markdown report.md] [--verify-fresh prior-report.json]');
|
|
491
685
|
return 2;
|
|
492
686
|
}
|
|
493
687
|
|
|
@@ -501,7 +695,33 @@ function runCli(argv) {
|
|
|
501
695
|
return 2;
|
|
502
696
|
}
|
|
503
697
|
|
|
698
|
+
if (args.verifyFresh) {
|
|
699
|
+
let prior;
|
|
700
|
+
try { prior = readJson(args.verifyFresh); }
|
|
701
|
+
catch (error) {
|
|
702
|
+
console.error(`Contract Integrity Freshness: FAIL\n- ${error.message}`);
|
|
703
|
+
return 2;
|
|
704
|
+
}
|
|
705
|
+
const currentAudit = evaluateTrace(trace, { tddEvidence });
|
|
706
|
+
const freshness = verifyFreshReport(prior, args, trace, currentAudit);
|
|
707
|
+
console.log(`Contract Integrity Freshness: ${freshness.authorizesCompletion ? 'PASS' : 'FAIL'}`);
|
|
708
|
+
for (const reason of freshness.reasons) console.error(`- ${reason}`);
|
|
709
|
+
return freshness.authorizesCompletion ? 0 : 1;
|
|
710
|
+
}
|
|
711
|
+
|
|
504
712
|
const report = evaluateTrace(trace, { tddEvidence });
|
|
713
|
+
report.provenance = buildProvenance(args, trace);
|
|
714
|
+
if (args.workspace) {
|
|
715
|
+
const missing = report.provenance.artifacts.filter(item => item.missing);
|
|
716
|
+
if (trace.mode === 'strict' && missing.length) {
|
|
717
|
+
report.result = 'FAIL';
|
|
718
|
+
report.completionGate = 'FAIL';
|
|
719
|
+
report.gateEligible = false;
|
|
720
|
+
report.errors.push(...missing.map(item => item.reason === 'linked-path'
|
|
721
|
+
? `provenance artifact linked-path: ${item.id} -> ${item.path}`
|
|
722
|
+
: `provenance artifact missing from workspace: ${item.id} -> ${item.path}`));
|
|
723
|
+
}
|
|
724
|
+
}
|
|
505
725
|
console.log(`Contract Integrity: ${report.result}`);
|
|
506
726
|
console.log(`Scores: completeness=${report.scores.completenessScore} protection=${report.scores.protectionScore} integrity=${report.scores.contractIntegrityScore}`);
|
|
507
727
|
console.log(`Gate: ${report.completionGate}`);
|
|
@@ -527,10 +747,13 @@ if (require.main === module) process.exit(runCli(process.argv));
|
|
|
527
747
|
module.exports = {
|
|
528
748
|
DRIFT,
|
|
529
749
|
VERSION,
|
|
750
|
+
buildProvenance,
|
|
530
751
|
classifyLineage,
|
|
531
752
|
evaluateTrace,
|
|
532
753
|
markdown,
|
|
754
|
+
repairGuidance,
|
|
533
755
|
protectionForRequirement,
|
|
534
756
|
runCli,
|
|
535
757
|
validateTrace,
|
|
758
|
+
verifyFreshReport,
|
|
536
759
|
};
|
|
@@ -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.15.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",
|
|
@@ -4,7 +4,7 @@ description: "Run objective verification gates (build, types, lint, tests, secur
|
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
6
|
author: Miya Daniel
|
|
7
|
-
version: 0.
|
|
7
|
+
version: 0.15.0
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Verification Loop
|
|
@@ -41,7 +41,7 @@ and rerun from the first affected gate.
|
|
|
41
41
|
4. Tests and coverage: `npm run test -- --coverage`.
|
|
42
42
|
5. Security scan: secrets and stray logs.
|
|
43
43
|
6. Diff review: `git diff --stat` and unintended-change checks.
|
|
44
|
-
7. Contract reconciliation when the work cites `REQ-*` / ADR/spec/ticket lineage or a contract-integrity trace exists: run the strict `<this-skill-dir>/scripts/contract-integrity-audit.js` gate with current #58 evidence. `AUDIT`, `NOT_EVALUATED`, stale source binding, or drift is **not ready** for delivery.
|
|
44
|
+
7. Contract reconciliation when the work cites `REQ-*` / ADR/spec/ticket lineage or a contract-integrity trace exists: run the strict `<this-skill-dir>/scripts/contract-integrity-audit.js` gate with current #58 evidence and `--workspace <workspace>` so traced source/test/implementation artifacts are fingerprinted. `AUDIT`, `NOT_EVALUATED`, stale source binding, or drift is **not ready** for delivery.
|
|
45
45
|
8. Fill `<this-skill-dir>/templates/verification-report.template.md` with evidence.
|
|
46
46
|
|
|
47
47
|
Deep dive: `<this-skill-dir>/references/verification-phases.md`
|
package/plugins/harness-everything/skills/verification-loop/references/verification-phases.md
CHANGED
|
@@ -110,11 +110,19 @@ This phase is applicable when any of the following is true:
|
|
|
110
110
|
Use the current trace plus current #58 TDD evidence:
|
|
111
111
|
|
|
112
112
|
```bash
|
|
113
|
-
node "<this-skill-dir>/scripts/contract-integrity-audit.js" <trace.json> --tdd-evidence <tdd-evidence.json> --output <report.json> --markdown <report.md>
|
|
113
|
+
node "<this-skill-dir>/scripts/contract-integrity-audit.js" <trace.json> --tdd-evidence <tdd-evidence.json> --workspace <workspace> --output <report.json> --markdown <report.md>
|
|
114
114
|
```
|
|
115
115
|
|
|
116
116
|
Delivery requires strict **PASS**. Treat `FAIL`, `AUDIT`, `NOT_EVALUATED`, source conflicts/defects, stale spec/test/implementation lineage, surviving required probes, and audit-only output as **NOT READY**. Do not downgrade the gate merely because build/tests are otherwise green.
|
|
117
117
|
|
|
118
|
+
If an existing JSON report is being reused rather than regenerated immediately, verify its content fingerprints first:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
node "<this-skill-dir>/scripts/contract-integrity-audit.js" <trace.json> --tdd-evidence <tdd-evidence.json> --workspace <workspace> --verify-fresh <report.json>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Any trace, #58 evidence, spec, test, ticket, ADR, or implementation artifact change makes the prior report stale and unable to authorize completion.
|
|
125
|
+
|
|
118
126
|
If this phase is not applicable, record `NOT_APPLICABLE` with a one-line reason. Never use `NOT_APPLICABLE` to hide known requirement lineage.
|
|
119
127
|
|
|
120
128
|
## Output Format
|
|
@@ -4,7 +4,7 @@ description: "Run objective verification gates (build, types, lint, tests, secur
|
|
|
4
4
|
license: Apache-2.0
|
|
5
5
|
metadata:
|
|
6
6
|
author: Miya Daniel
|
|
7
|
-
version: 0.
|
|
7
|
+
version: 0.15.0
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# Verification Loop
|
|
@@ -41,7 +41,7 @@ and rerun from the first affected gate.
|
|
|
41
41
|
4. Tests and coverage: `npm run test -- --coverage`.
|
|
42
42
|
5. Security scan: secrets and stray logs.
|
|
43
43
|
6. Diff review: `git diff --stat` and unintended-change checks.
|
|
44
|
-
7. Contract reconciliation when the work cites `REQ-*` / ADR/spec/ticket lineage or a contract-integrity trace exists: run the strict `<this-skill-dir>/scripts/contract-integrity-audit.js` gate with current #58 evidence. `AUDIT`, `NOT_EVALUATED`, stale source binding, or drift is **not ready** for delivery.
|
|
44
|
+
7. Contract reconciliation when the work cites `REQ-*` / ADR/spec/ticket lineage or a contract-integrity trace exists: run the strict `<this-skill-dir>/scripts/contract-integrity-audit.js` gate with current #58 evidence and `--workspace <workspace>` so traced source/test/implementation artifacts are fingerprinted. `AUDIT`, `NOT_EVALUATED`, stale source binding, or drift is **not ready** for delivery.
|
|
45
45
|
8. Fill `<this-skill-dir>/templates/verification-report.template.md` with evidence.
|
|
46
46
|
|
|
47
47
|
Deep dive: `<this-skill-dir>/references/verification-phases.md`
|
|
@@ -110,11 +110,19 @@ This phase is applicable when any of the following is true:
|
|
|
110
110
|
Use the current trace plus current #58 TDD evidence:
|
|
111
111
|
|
|
112
112
|
```bash
|
|
113
|
-
node "<this-skill-dir>/scripts/contract-integrity-audit.js" <trace.json> --tdd-evidence <tdd-evidence.json> --output <report.json> --markdown <report.md>
|
|
113
|
+
node "<this-skill-dir>/scripts/contract-integrity-audit.js" <trace.json> --tdd-evidence <tdd-evidence.json> --workspace <workspace> --output <report.json> --markdown <report.md>
|
|
114
114
|
```
|
|
115
115
|
|
|
116
116
|
Delivery requires strict **PASS**. Treat `FAIL`, `AUDIT`, `NOT_EVALUATED`, source conflicts/defects, stale spec/test/implementation lineage, surviving required probes, and audit-only output as **NOT READY**. Do not downgrade the gate merely because build/tests are otherwise green.
|
|
117
117
|
|
|
118
|
+
If an existing JSON report is being reused rather than regenerated immediately, verify its content fingerprints first:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
node "<this-skill-dir>/scripts/contract-integrity-audit.js" <trace.json> --tdd-evidence <tdd-evidence.json> --workspace <workspace> --verify-fresh <report.json>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Any trace, #58 evidence, spec, test, ticket, ADR, or implementation artifact change makes the prior report stale and unable to authorize completion.
|
|
125
|
+
|
|
118
126
|
If this phase is not applicable, record `NOT_APPLICABLE` with a one-line reason. Never use `NOT_APPLICABLE` to hide known requirement lineage.
|
|
119
127
|
|
|
120
128
|
## Output Format
|