harness-everything 0.38.0 → 0.40.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/AGENTS.md +79 -63
- package/CHANGELOG.md +32 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/README.md +76 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/a-reviews-b.md +191 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/b-reviews-a.md +100 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/baseline-probes.log +47 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/bom-red.log +85 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/bom-transition.txt +25 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/coordinator-probes.txt +84 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/cross-suite.txt +18 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-bom.log +3 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-consistency.log.gz +0 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-core.log +198 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-probes.log +27 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-producer.log +83 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-review.md +89 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/final-transitions.log +5 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/implementation-a.md +166 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/implementation-b.md +264 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/logs.sha256 +15 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-a-probe.log +62 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-a-probe.txt +313 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-b-probe.log +51 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/peer-b-probe.txt +94 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/semantic-transitions.txt +25 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/source.sha256 +9 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-a-on-b-core.log +201 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-a-on-b-producer.log +82 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-b-on-a-core.log +197 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/tests-b-on-a-producer.log +82 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/verify-all.log.gz +0 -0
- package/benchmarks/results/reviews/pr353-r6-2026-10-22/verify-gate-summary.json +22 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2a/harness.js +489 -0
- package/ci/fixtures/doc-obligations/reconciliation-s2a/vectors.json +617 -0
- package/ci/mechanism-325-3b-s2a-core.test.js +840 -0
- package/ci/mechanism-325-3b-s2a-producer.test.js +564 -0
- package/code-review/SKILL.md +3 -3
- package/code-review/references/review-playbook.md +4 -0
- package/code-review/templates/finding.template.md +6 -0
- package/docs/doc-workflow-reconciliation.md +357 -0
- package/evals/zoom-out/tasks/negative-first-edge-case-review.yaml +16 -0
- package/evals/zoom-out/tasks/positive-repair-loop.yaml +15 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/project-docs-resolver.js +42 -8
- package/harness-everything/scripts/routing-keywords.json +4 -0
- package/hooks/scripts/lib/doc-reconciliation-core.js +280 -0
- package/hooks/scripts/lib/doc-reconciliation-events.js +141 -0
- package/hooks/scripts/lib/doc-reconciliation-producer.js +206 -0
- package/multi-agent-workspace/SKILL.md +1 -1
- package/multi-agent-workspace/scripts/project-docs-resolver.js +42 -8
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +2 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-core.js +280 -0
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-events.js +141 -0
- package/plugins/harness-everything/hooks/scripts/lib/doc-reconciliation-producer.js +206 -0
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/code-review/SKILL.md +3 -3
- package/plugins/harness-everything/skills/code-review/references/review-playbook.md +4 -0
- package/plugins/harness-everything/skills/code-review/templates/finding.template.md +6 -0
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js +42 -8
- package/plugins/harness-everything/skills/harness-everything/scripts/routing-keywords.json +4 -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 +42 -8
- package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-spec/scripts/project-docs-resolver.js +42 -8
- package/plugins/harness-everything/skills/zoom-out/SKILL.md +4 -3
- package/plugins/harness-everything/skills/zoom-out/references/circuit-breaker.md +7 -0
- package/plugins/harness-everything/skills/zoom-out/templates/zoom-out-report.template.md +4 -2
- package/sbom.cdx.json +7 -7
- package/scripts/errors-sync.js +90 -0
- package/to-spec/SKILL.md +1 -1
- package/to-spec/scripts/project-docs-resolver.js +42 -8
- package/zoom-out/SKILL.md +4 -3
- package/zoom-out/references/circuit-breaker.md +7 -0
- package/zoom-out/templates/zoom-out-report.template.md +4 -2
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Shared harness for the S2a RED suites. Test-only: fake origin providers, temporary Git repositories and event builders
|
|
4
|
+
// live here (a fixture file). Nothing here is production code, and nothing here grants origin, credit or trust.
|
|
5
|
+
// Every module under test is loaded per leg (canonical and packaged) and missing modules are reported per assertion.
|
|
6
|
+
|
|
7
|
+
const crypto = require('crypto');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { spawnSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
|
14
|
+
const FIXTURE_DIR = __dirname;
|
|
15
|
+
const VECTORS = JSON.parse(fs.readFileSync(path.join(FIXTURE_DIR, 'vectors.json'), 'utf8'));
|
|
16
|
+
const LEGS = [
|
|
17
|
+
{
|
|
18
|
+
name: 'source',
|
|
19
|
+
lib: path.join(ROOT, 'hooks', 'scripts', 'lib'),
|
|
20
|
+
scripts: path.join(ROOT, 'harness-everything', 'scripts')
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'packaged',
|
|
24
|
+
lib: path.join(ROOT, 'plugins', 'harness-everything', 'hooks', 'scripts', 'lib'),
|
|
25
|
+
scripts: path.join(ROOT, 'plugins', 'harness-everything', 'skills', 'harness-everything', 'scripts')
|
|
26
|
+
}
|
|
27
|
+
];
|
|
28
|
+
const DERIVE_CONFIG = JSON.parse(fs.readFileSync(path.join(ROOT, VECTORS.deriveConfig), 'utf8'));
|
|
29
|
+
const CONFIG_REL = '.agents/harness-everything/project-docs.json';
|
|
30
|
+
const PRINCIPALS = VECTORS.principals;
|
|
31
|
+
const FIXED_TASK = VECTORS.fixedTask;
|
|
32
|
+
const FIXED_WORKFLOW = VECTORS.fixedWorkflow;
|
|
33
|
+
const ZERO_DIGEST = '0'.repeat(64);
|
|
34
|
+
|
|
35
|
+
class InfraError extends Error {}
|
|
36
|
+
// A platform cannot observe the behaviour under test (e.g. no POSIX execute bit on Windows). Never a PASS.
|
|
37
|
+
class UncheckedError extends Error {}
|
|
38
|
+
|
|
39
|
+
function stable(value) {
|
|
40
|
+
if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
|
|
41
|
+
if (value && typeof value === 'object') {
|
|
42
|
+
return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stable(value[key])}`).join(',')}}`;
|
|
43
|
+
}
|
|
44
|
+
return JSON.stringify(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sha256(text) {
|
|
48
|
+
return crypto.createHash('sha256').update(text).digest('hex');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function clone(value) {
|
|
52
|
+
return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function sameStable(actual, expected, label) {
|
|
56
|
+
const a = stable(actual);
|
|
57
|
+
const e = stable(expected);
|
|
58
|
+
if (a !== e) throw new Error(`${label}: got ${a.slice(0, 240)} expected ${e.slice(0, 240)}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---- Module loading per leg -------------------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
const cache = new Map();
|
|
64
|
+
function loadModule(leg, where, file) {
|
|
65
|
+
const abs = path.join(leg[where], file);
|
|
66
|
+
const rel = path.relative(ROOT, abs);
|
|
67
|
+
if (!fs.existsSync(abs)) return { error: `module-missing: ${rel} does not exist` };
|
|
68
|
+
if (!cache.has(abs)) {
|
|
69
|
+
try {
|
|
70
|
+
cache.set(abs, { mod: require(abs) });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
cache.set(abs, { error: `module-load: ${rel}: ${error.message.split('\n')[0]}` });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return cache.get(abs);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Loads a fresh copy of a module after recording wrappers are installed, so destructured references in the module
|
|
79
|
+
// (for example const { execFileSync } = require('child_process')) bind to the recording wrappers.
|
|
80
|
+
function freshModule(abs) {
|
|
81
|
+
delete require.cache[require.resolve(abs)];
|
|
82
|
+
return require(abs);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Records file writes, spawned commands and network connects while fn runs. Spawned commands are forwarded to the real
|
|
86
|
+
// implementation so Git-backed capture still works; every other command is recorded and refused.
|
|
87
|
+
function recordingIo(fn) {
|
|
88
|
+
const childProcessModule = require('child_process');
|
|
89
|
+
const netModule = require('net');
|
|
90
|
+
const log = { writes: [], spawns: [], net: [] };
|
|
91
|
+
const original = {
|
|
92
|
+
writeFileSync: fs.writeFileSync,
|
|
93
|
+
appendFileSync: fs.appendFileSync,
|
|
94
|
+
mkdirSync: fs.mkdirSync,
|
|
95
|
+
rmSync: fs.rmSync,
|
|
96
|
+
renameSync: fs.renameSync,
|
|
97
|
+
unlinkSync: fs.unlinkSync,
|
|
98
|
+
execFileSync: childProcessModule.execFileSync,
|
|
99
|
+
spawnSync: childProcessModule.spawnSync,
|
|
100
|
+
execSync: childProcessModule.execSync,
|
|
101
|
+
connect: netModule.connect
|
|
102
|
+
};
|
|
103
|
+
for (const name of ['writeFileSync', 'appendFileSync', 'mkdirSync', 'rmSync', 'renameSync', 'unlinkSync']) {
|
|
104
|
+
fs[name] = (...args) => { log.writes.push(name); throw new Error(`forbidden file write ${name}`); };
|
|
105
|
+
}
|
|
106
|
+
childProcessModule.execFileSync = (file, args, options) => {
|
|
107
|
+
log.spawns.push(file);
|
|
108
|
+
if (file !== 'git') throw new Error(`forbidden spawn ${file}`);
|
|
109
|
+
return original.execFileSync(file, args, options);
|
|
110
|
+
};
|
|
111
|
+
childProcessModule.spawnSync = (file, args, options) => {
|
|
112
|
+
log.spawns.push(file);
|
|
113
|
+
if (file !== 'git') throw new Error(`forbidden spawn ${file}`);
|
|
114
|
+
return original.spawnSync(file, args, options);
|
|
115
|
+
};
|
|
116
|
+
childProcessModule.execSync = (command) => { log.spawns.push(String(command).split(' ')[0]); throw new Error('forbidden execSync'); };
|
|
117
|
+
netModule.connect = () => { log.net.push('connect'); throw new Error('forbidden network connect'); };
|
|
118
|
+
try {
|
|
119
|
+
return fn(log);
|
|
120
|
+
} finally {
|
|
121
|
+
for (const name of ['writeFileSync', 'appendFileSync', 'mkdirSync', 'rmSync', 'renameSync', 'unlinkSync']) fs[name] = original[name];
|
|
122
|
+
childProcessModule.execFileSync = original.execFileSync;
|
|
123
|
+
childProcessModule.spawnSync = original.spawnSync;
|
|
124
|
+
childProcessModule.execSync = original.execSync;
|
|
125
|
+
netModule.connect = original.connect;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---- Runner: one line per assertion and leg; INFRA is distinct from FAIL -----------------------------------------
|
|
130
|
+
|
|
131
|
+
function createRunner(label) {
|
|
132
|
+
const rows = [];
|
|
133
|
+
const counts = { PASS: 0, FAIL: 0, INFRA: 0, UNCHECKED: 0 };
|
|
134
|
+
function emit(status, id, leg, detail) {
|
|
135
|
+
counts[status] += 1;
|
|
136
|
+
rows.push({ status, id, leg, detail: detail || '' });
|
|
137
|
+
console.log(` ${status} ${id}${leg ? ` [${leg}]` : ''}${detail ? ` — ${detail}` : ''}`);
|
|
138
|
+
}
|
|
139
|
+
// requirements: [{ where: 'lib' | 'scripts', file, exports: [names] }]. fn(mods, leg) throws on failure.
|
|
140
|
+
function run(id, requirements, fn) {
|
|
141
|
+
for (const leg of LEGS) {
|
|
142
|
+
const mods = {};
|
|
143
|
+
let error = null;
|
|
144
|
+
for (const req of requirements) {
|
|
145
|
+
const loaded = loadModule(leg, req.where, req.file);
|
|
146
|
+
if (loaded.error) { error = loaded.error; break; }
|
|
147
|
+
for (const name of req.exports || []) {
|
|
148
|
+
if (typeof loaded.mod[name] !== 'function') { error = `export-missing: ${req.file} does not export ${name}`; break; }
|
|
149
|
+
}
|
|
150
|
+
if (error) break;
|
|
151
|
+
mods[req.file] = loaded.mod;
|
|
152
|
+
}
|
|
153
|
+
if (error) { emit('FAIL', id, leg.name, error); continue; }
|
|
154
|
+
try {
|
|
155
|
+
fn(mods, leg);
|
|
156
|
+
emit('PASS', id, leg.name);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error instanceof InfraError) emit('INFRA', id, leg.name, error.message.split('\n')[0]);
|
|
159
|
+
else if (error instanceof UncheckedError) emit('UNCHECKED', id, leg.name, error.message.split('\n')[0]);
|
|
160
|
+
else emit('FAIL', id, leg.name, error.message.split('\n')[0]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// One assertion over both legs at once (parity or cross-leg). Any missing leg reports its own reason.
|
|
165
|
+
function pair(id, requirements, fn) {
|
|
166
|
+
const mods = {};
|
|
167
|
+
for (const leg of LEGS) {
|
|
168
|
+
let error = null;
|
|
169
|
+
for (const req of requirements) {
|
|
170
|
+
const loaded = loadModule(leg, req.where, req.file);
|
|
171
|
+
if (loaded.error) { error = loaded.error; break; }
|
|
172
|
+
mods[`${leg.name}:${req.file}`] = loaded.mod;
|
|
173
|
+
}
|
|
174
|
+
if (error) {
|
|
175
|
+
for (const other of LEGS) emit('FAIL', id, other.name, error);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
fn(mods);
|
|
181
|
+
emit('PASS', id, 'both');
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (error instanceof InfraError) emit('INFRA', id, 'both', error.message.split('\n')[0]);
|
|
184
|
+
else emit('FAIL', id, 'both', error.message.split('\n')[0]);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function summary() {
|
|
188
|
+
const reasons = new Map();
|
|
189
|
+
for (const row of rows) {
|
|
190
|
+
if (row.status !== 'FAIL') continue;
|
|
191
|
+
const key = row.detail.replace(/ \(.*$/, '').split(' — ')[0].slice(0, 120);
|
|
192
|
+
reasons.set(key, (reasons.get(key) || 0) + 1);
|
|
193
|
+
}
|
|
194
|
+
console.log(`\n${label} summary: ${counts.PASS} PASS, ${counts.FAIL} FAIL, ${counts.INFRA} INFRA${counts.UNCHECKED ? `, ${counts.UNCHECKED} UNCHECKED` : ''} across ${rows.length} assertion-legs`);
|
|
195
|
+
if (reasons.size) {
|
|
196
|
+
console.log('FAIL reason classes:');
|
|
197
|
+
for (const [reason, count] of [...reasons.entries()].sort((a, b) => b[1] - a[1])) console.log(` ${count} × ${reason}`);
|
|
198
|
+
}
|
|
199
|
+
return counts;
|
|
200
|
+
}
|
|
201
|
+
return { run, pair, summary, counts, rows };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---- Temporary Git repositories ---------------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
const GIT_DATE = '2026-01-01T00:00:00Z';
|
|
207
|
+
function git(cwd, args) {
|
|
208
|
+
const env = { ...process.env };
|
|
209
|
+
for (const key of Object.keys(env)) if (key.startsWith('GIT_')) delete env[key];
|
|
210
|
+
env.GIT_AUTHOR_DATE = GIT_DATE;
|
|
211
|
+
env.GIT_COMMITTER_DATE = GIT_DATE;
|
|
212
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8', env, timeout: 20000, maxBuffer: 16 * 1024 * 1024 });
|
|
213
|
+
if (result.error || result.status !== 0) {
|
|
214
|
+
throw new InfraError(`git ${args[0]} failed in fixture repository: ${result.error ? result.error.message : String(result.stderr || '').split('\n')[0]}`);
|
|
215
|
+
}
|
|
216
|
+
return result.stdout;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function writeFiles(root, files) {
|
|
220
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
221
|
+
if (path.isAbsolute(rel) || rel.split(/[\\/]/).includes('..')) throw new InfraError(`fixture path must be relative: ${rel}`);
|
|
222
|
+
const target = path.join(root, rel);
|
|
223
|
+
if (content === null) { fs.rmSync(target, { force: true }); continue; }
|
|
224
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
225
|
+
fs.writeFileSync(target, content, 'utf8');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Writes stored bytes that the text writer cannot express (content that is not valid UTF-8).
|
|
230
|
+
function writeBinary(root, files) {
|
|
231
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
232
|
+
if (path.isAbsolute(rel) || rel.split(/[\\/]/).includes('..')) throw new InfraError(`fixture path must be relative: ${rel}`);
|
|
233
|
+
const target = path.join(root, rel);
|
|
234
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
235
|
+
fs.writeFileSync(target, Buffer.from(content));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Binary fixture content: bytes that are not valid UTF-8 (a PNG-like header with an invalid sequence).
|
|
240
|
+
const BINARY_BYTES = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0a, 0x1a, 0x0a, 0x00, 0xff, 0xfe, 0x80, 0x81]);
|
|
241
|
+
const BINARY_BYTES_CHANGED = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0a, 0x1a, 0x0a, 0x00, 0xff, 0xfe, 0x81, 0x82]);
|
|
242
|
+
|
|
243
|
+
function makeRepo(baseFiles, binaryFiles = {}) {
|
|
244
|
+
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'doc-s2a-')));
|
|
245
|
+
git(root, ['init', '-q']);
|
|
246
|
+
writeFiles(root, { [CONFIG_REL]: JSON.stringify(DERIVE_CONFIG, null, 2) + '\n', ...baseFiles });
|
|
247
|
+
writeBinary(root, binaryFiles);
|
|
248
|
+
git(root, ['add', '-A']);
|
|
249
|
+
git(root, ['-c', 'user.name=s2a-fixture', '-c', 'user.email=s2a-fixture@invalid', '-c', 'commit.gpgsign=false', 'commit', '-q', '--no-verify', '-m', 'baseline']);
|
|
250
|
+
const commit = git(root, ['rev-parse', 'HEAD']).trim();
|
|
251
|
+
return { root, commit };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function removeRepo(root) {
|
|
255
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function readTree(root) {
|
|
259
|
+
const files = {};
|
|
260
|
+
if (!fs.existsSync(root)) return files;
|
|
261
|
+
const walk = dir => {
|
|
262
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
263
|
+
const full = path.join(dir, entry.name);
|
|
264
|
+
if (entry.isDirectory()) walk(full);
|
|
265
|
+
else files[path.relative(root, full).split(path.sep).join('/')] = fs.readFileSync(full, 'utf8');
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
walk(root);
|
|
269
|
+
return files;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Base and candidate file maps for a named vector (derive fixture directory or inline files).
|
|
273
|
+
function vectorFiles(key) {
|
|
274
|
+
const vec = VECTORS.vectors[key];
|
|
275
|
+
if (!vec) throw new InfraError(`unknown vector ${key}`);
|
|
276
|
+
if (vec.deriveCase) {
|
|
277
|
+
const dir = path.join(ROOT, 'ci', 'fixtures', 'doc-obligations', 'derive', vec.deriveCase);
|
|
278
|
+
return { base: readTree(path.join(dir, 'base')), candidate: readTree(path.join(dir, 'candidate')) };
|
|
279
|
+
}
|
|
280
|
+
return { base: clone(vec.files.base), candidate: clone(vec.files.candidate) };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Per-path SHA256 of the file bytes actually present in the working tree (the stored bytes for the candidate).
|
|
284
|
+
function fileDigest(root, rel) {
|
|
285
|
+
const abs = path.join(root, rel);
|
|
286
|
+
if (!fs.existsSync(abs)) return null;
|
|
287
|
+
return sha256(fs.readFileSync(abs));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// PRD-set digest as the checker envelope defines it: SHA256 of sorted [path, artifactDigest] pairs under docs/prd/.
|
|
291
|
+
function prdSetDigest(root) {
|
|
292
|
+
const files = readTree(root);
|
|
293
|
+
const pairs = Object.keys(files).filter(p => p.startsWith('docs/prd/')).sort().map(p => [p, fileDigest(root, p)]);
|
|
294
|
+
return sha256(JSON.stringify(pairs));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ---- Fake origin provider (test-only; explicitly not production attestation) -----------------------------------
|
|
298
|
+
|
|
299
|
+
class FakeOrigin {
|
|
300
|
+
constructor({ context, mode = 'accept' }) {
|
|
301
|
+
this.context = clone(context);
|
|
302
|
+
this.mode = mode;
|
|
303
|
+
this.refs = new Map();
|
|
304
|
+
this.issued = new Set();
|
|
305
|
+
this.contextCalls = 0;
|
|
306
|
+
this.contextAt = null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
issueEvent(reference, event) {
|
|
310
|
+
this.refs.set(reference, clone(event));
|
|
311
|
+
this.issued.add(reference);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
resolveContext() {
|
|
315
|
+
this.contextCalls += 1;
|
|
316
|
+
if (this.mode === 'missing') return { status: 'unavailable', diagnostics: [{ code: 'origin-unavailable', message: 'no host context is correlated for this workspace' }] };
|
|
317
|
+
if (this.mode === 'expired') return { status: 'unavailable', diagnostics: [{ code: 'origin-unavailable', message: 'the host context has expired' }] };
|
|
318
|
+
const context = this.contextAt ? this.contextAt(this.contextCalls) : this.context;
|
|
319
|
+
return { status: 'accepted', context: clone(context), originRef: 'fake-origin:context' };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
resolveEvent({ reference }) {
|
|
323
|
+
if (this.mode === 'missing' || this.mode === 'expired') {
|
|
324
|
+
return { status: 'unavailable', diagnostics: [{ code: 'origin-unavailable', message: 'origin support is unavailable for this reference' }] };
|
|
325
|
+
}
|
|
326
|
+
if (!this.issued.has(reference)) {
|
|
327
|
+
return { status: 'rejected', diagnostics: [{ code: 'origin-untrusted', message: 'reference was not issued by the current provider' }] };
|
|
328
|
+
}
|
|
329
|
+
const event = clone(this.refs.get(reference));
|
|
330
|
+
if (event.providerStatus === 'stale') {
|
|
331
|
+
return { status: 'rejected', diagnostics: [{ code: 'event-stale', message: 'the provider reports this event as stale' }] };
|
|
332
|
+
}
|
|
333
|
+
return { status: 'accepted', event, originRef: event.originRef, correlation: { toolCorrelationId: event.toolCorrelationId || null } };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// With a repo, the fake provider binds the concrete workflow-start pin (the repo's original commit), as the
|
|
338
|
+
// S2a contract requires; omit the repo only to model a reference-only (unpinned) provider context.
|
|
339
|
+
function fakeContext(extra = {}, repo = null) {
|
|
340
|
+
const pin = repo && extra.baseline === undefined ? { baseline: { root: repo.root, commit: repo.commit } } : {};
|
|
341
|
+
return {
|
|
342
|
+
...pin,
|
|
343
|
+
workspaceId: 'fake-workspace',
|
|
344
|
+
sessionId: FIXED_WORKFLOW.sessionId,
|
|
345
|
+
workflowId: FIXED_WORKFLOW.workflowId,
|
|
346
|
+
workflowRevision: FIXED_WORKFLOW.workflowRevision,
|
|
347
|
+
taskId: FIXED_TASK.taskId,
|
|
348
|
+
principals: { author: PRINCIPALS.author, executors: [PRINCIPALS.executor] },
|
|
349
|
+
activeHost: { hostId: 'fake-host' },
|
|
350
|
+
baselineReference: 'fake-origin:workflow-start-baseline',
|
|
351
|
+
...extra
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function workflowInput(overrides = {}) {
|
|
356
|
+
return { workflowId: FIXED_WORKFLOW.workflowId, workflowRevision: FIXED_WORKFLOW.workflowRevision, sessionId: FIXED_WORKFLOW.sessionId, ...overrides };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function taskInput(overrides = {}) {
|
|
360
|
+
return clone({ taskId: FIXED_TASK.taskId, requirements: FIXED_TASK.requirements, ...overrides });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Positional producer input: the shape the S2a design names (baseline pin, workflow, task, scope, active host, PRD root).
|
|
364
|
+
function producerInput(repo, overrides = {}) {
|
|
365
|
+
return {
|
|
366
|
+
workspaceRoot: repo.root,
|
|
367
|
+
baseline: { root: repo.root, commit: repo.commit },
|
|
368
|
+
workflow: workflowInput(overrides.workflow),
|
|
369
|
+
task: taskInput(overrides.task),
|
|
370
|
+
scope: { prdRoot: 'docs/prd/' },
|
|
371
|
+
activeHost: { hostId: 'fake-host' },
|
|
372
|
+
prdRoot: 'docs/prd/',
|
|
373
|
+
...overrides.top
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Builds accepted-event payloads for every expected MUST of a vector from a captured snapshot. Digests are computed from
|
|
378
|
+
// the working-tree bytes (the stored bytes), not from snapshot field names.
|
|
379
|
+
function buildEvents(snapshot, repo, vec) {
|
|
380
|
+
const events = {};
|
|
381
|
+
const artifactDigests = {};
|
|
382
|
+
for (const obligation of snapshot.obligations || []) {
|
|
383
|
+
for (const rel of (obligation.scope && obligation.scope.paths) || []) {
|
|
384
|
+
artifactDigests[rel] = fileDigest(repo.root, rel);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const paths = mustId => (snapshot.obligations || []).find(item => item.id === mustId)?.scope?.paths || [];
|
|
388
|
+
for (const mustId of vec.expectedMustIds) {
|
|
389
|
+
const kind = (vec.eventKindByMust || {})[mustId];
|
|
390
|
+
if (!kind) continue;
|
|
391
|
+
const capability = mustId.replace(/^doc-change:/, '');
|
|
392
|
+
const ref = `ref-${capability}`;
|
|
393
|
+
const common = {
|
|
394
|
+
eventId: `evt-${capability}`,
|
|
395
|
+
producerId: 'fake-producer',
|
|
396
|
+
identity: clone(snapshot.identity),
|
|
397
|
+
paths: paths(mustId),
|
|
398
|
+
provenance: 'fake-origin:fixture',
|
|
399
|
+
capability,
|
|
400
|
+
originRef: `fake-origin:event:${ref}`
|
|
401
|
+
};
|
|
402
|
+
if (kind === 'execution') {
|
|
403
|
+
events[ref] = {
|
|
404
|
+
...common,
|
|
405
|
+
kind: 'execution',
|
|
406
|
+
principalId: PRINCIPALS.executor,
|
|
407
|
+
command: `fixture-check ${capability}`,
|
|
408
|
+
argv: ['fixture-check', capability],
|
|
409
|
+
cwd: '.',
|
|
410
|
+
exitCode: 0,
|
|
411
|
+
signal: null,
|
|
412
|
+
toolCorrelationId: `tool-${capability}`,
|
|
413
|
+
startIdentity: clone(snapshot.identity),
|
|
414
|
+
endIdentity: clone(snapshot.identity),
|
|
415
|
+
resultRef: `result-${capability}`,
|
|
416
|
+
artifactDigests: clone(artifactDigests)
|
|
417
|
+
};
|
|
418
|
+
} else {
|
|
419
|
+
events[ref] = {
|
|
420
|
+
...common,
|
|
421
|
+
kind: 'review',
|
|
422
|
+
principalId: PRINCIPALS.reviewer,
|
|
423
|
+
verdict: 'approve',
|
|
424
|
+
summary: `fixture review for ${capability}: approve`,
|
|
425
|
+
reviewedArtifactDigests: clone(artifactDigests),
|
|
426
|
+
independentOf: [PRINCIPALS.author, PRINCIPALS.executor]
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return events;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Reason codes found anywhere in a decision or acceptance result (diagnostics, rejections, unsatisfied reasons).
|
|
434
|
+
function codesOf(...results) {
|
|
435
|
+
const codes = new Set();
|
|
436
|
+
for (const result of results) {
|
|
437
|
+
if (!result || typeof result !== 'object') continue;
|
|
438
|
+
for (const item of [...(result.diagnostics || []), ...(result.rejected || []), ...(result.unsatisfied || [])]) {
|
|
439
|
+
if (item && typeof item.code === 'string') codes.add(item.code);
|
|
440
|
+
if (item && typeof item.reasonCode === 'string') codes.add(item.reasonCode);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return [...codes].sort();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function expectCodes(actualCodes, expected, label) {
|
|
447
|
+
const hit = expected.some(code => actualCodes.includes(code));
|
|
448
|
+
if (!hit) throw new Error(`${label}: expected one of ${JSON.stringify(expected)} in ${JSON.stringify(actualCodes)}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
module.exports = {
|
|
452
|
+
UncheckedError,
|
|
453
|
+
ROOT,
|
|
454
|
+
FIXTURE_DIR,
|
|
455
|
+
VECTORS,
|
|
456
|
+
LEGS,
|
|
457
|
+
DERIVE_CONFIG,
|
|
458
|
+
CONFIG_REL,
|
|
459
|
+
PRINCIPALS,
|
|
460
|
+
ZERO_DIGEST,
|
|
461
|
+
InfraError,
|
|
462
|
+
stable,
|
|
463
|
+
sha256,
|
|
464
|
+
clone,
|
|
465
|
+
sameStable,
|
|
466
|
+
createRunner,
|
|
467
|
+
loadModule,
|
|
468
|
+
freshModule,
|
|
469
|
+
recordingIo,
|
|
470
|
+
git,
|
|
471
|
+
writeFiles,
|
|
472
|
+
writeBinary,
|
|
473
|
+
BINARY_BYTES,
|
|
474
|
+
BINARY_BYTES_CHANGED,
|
|
475
|
+
makeRepo,
|
|
476
|
+
removeRepo,
|
|
477
|
+
readTree,
|
|
478
|
+
vectorFiles,
|
|
479
|
+
fileDigest,
|
|
480
|
+
prdSetDigest,
|
|
481
|
+
FakeOrigin,
|
|
482
|
+
fakeContext,
|
|
483
|
+
workflowInput,
|
|
484
|
+
taskInput,
|
|
485
|
+
producerInput,
|
|
486
|
+
buildEvents,
|
|
487
|
+
codesOf,
|
|
488
|
+
expectCodes
|
|
489
|
+
};
|