harness-everything 0.42.2 → 0.43.1
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/document-inventory.json +167 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.github/workflows/ci.yml +82 -0
- package/CHANGELOG.md +23 -0
- package/ci/mechanism-2ax-doc-lifecycle.test.js +942 -0
- package/ci/mechanism-325-f1-doc-check.test.js +178 -0
- package/docs/own-repo-governance-e1-plan-2026-10-25-2139.md +253 -0
- package/docs/platform-capabilities.md +8 -0
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-lifecycle.js +296 -3
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +296 -3
- package/sbom.cdx.json +7 -7
- package/scripts/doc-governance-guard.js +344 -0
|
@@ -9,6 +9,9 @@ const { spawnSync } = require('child_process');
|
|
|
9
9
|
const helper = require('./test-helper');
|
|
10
10
|
|
|
11
11
|
const modulePath = path.join(__dirname, '..', 'harness-everything', 'scripts', 'doc-lifecycle.js');
|
|
12
|
+
const governanceGuardPath = path.join(__dirname, '..', 'scripts', 'doc-governance-guard.js');
|
|
13
|
+
const REPOSITORY_ROOT = path.resolve(__dirname, '..');
|
|
14
|
+
const P2_BASE_SHA = 'b733cadec17542b9ae30aabb50c1ba3481a58d74';
|
|
12
15
|
let lifecycle;
|
|
13
16
|
try {
|
|
14
17
|
lifecycle = require(modulePath);
|
|
@@ -146,6 +149,219 @@ function gitBytesAt(root, revision, relativePath) {
|
|
|
146
149
|
return Buffer.from(result.stdout);
|
|
147
150
|
}
|
|
148
151
|
|
|
152
|
+
function gitRawAt(root, args) {
|
|
153
|
+
const result = spawnSync('git', ['-C', root, ...args], {
|
|
154
|
+
encoding: 'buffer',
|
|
155
|
+
env: { ...process.env, GIT_CONFIG_NOSYSTEM: '1' }
|
|
156
|
+
});
|
|
157
|
+
if (result.status !== 0) throw new Error(`git ${args.join(' ')} failed: ${String(result.stderr || result.stdout)}`);
|
|
158
|
+
return Buffer.from(result.stdout);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function nulFields(bytes) {
|
|
162
|
+
if (bytes.length && bytes[bytes.length - 1] !== 0) throw new Error('Git oracle output is not NUL terminated');
|
|
163
|
+
return bytes.length ? bytes.subarray(0, -1).toString('binary').split('\0').map(value => Buffer.from(value, 'binary')) : [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseTreeOracle(bytes) {
|
|
167
|
+
const entries = new Map();
|
|
168
|
+
for (const field of nulFields(bytes)) {
|
|
169
|
+
const tab = field.indexOf(9);
|
|
170
|
+
if (tab < 0) throw new Error('malformed raw Git tree entry');
|
|
171
|
+
const [mode, type, oid] = field.subarray(0, tab).toString('ascii').split(' ');
|
|
172
|
+
const pathBytes = Buffer.from(field.subarray(tab + 1));
|
|
173
|
+
entries.set(pathBytes.toString('base64'), { mode, type, oid, pathBytes });
|
|
174
|
+
}
|
|
175
|
+
return entries;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parseStageOracle(bytes) {
|
|
179
|
+
const entries = new Map();
|
|
180
|
+
for (const field of nulFields(bytes)) {
|
|
181
|
+
const tab = field.indexOf(9);
|
|
182
|
+
if (tab < 0) throw new Error('malformed raw Git index entry');
|
|
183
|
+
const [mode, oid, stage] = field.subarray(0, tab).toString('ascii').split(' ');
|
|
184
|
+
if (stage !== '0') throw new Error(`unmerged Git index stage ${stage}`);
|
|
185
|
+
const pathBytes = Buffer.from(field.subarray(tab + 1));
|
|
186
|
+
entries.set(pathBytes.toString('base64'), { mode, oid, type: mode === '160000' ? 'commit' : 'blob', pathBytes });
|
|
187
|
+
}
|
|
188
|
+
return entries;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function parseNameStatusOracle(bytes) {
|
|
192
|
+
const fields = nulFields(bytes);
|
|
193
|
+
const changes = new Map();
|
|
194
|
+
for (let index = 0; index < fields.length;) {
|
|
195
|
+
const statusText = fields[index++].toString('ascii');
|
|
196
|
+
const kind = statusText[0];
|
|
197
|
+
const first = fields[index++];
|
|
198
|
+
if (!first) throw new Error('truncated Git name-status entry');
|
|
199
|
+
if (kind === 'R' || kind === 'C') {
|
|
200
|
+
const second = fields[index++];
|
|
201
|
+
if (!second) throw new Error('truncated Git rename/copy entry');
|
|
202
|
+
changes.set(first.toString('base64'), { pathBytes: first, status: kind === 'R' ? 'renamed-from' : 'copied-from', endpoint: 'source', partner: second, statusText });
|
|
203
|
+
changes.set(second.toString('base64'), { pathBytes: second, status: kind === 'R' ? 'renamed-to' : 'copied-to', endpoint: 'target', partner: first, statusText });
|
|
204
|
+
} else {
|
|
205
|
+
changes.set(first.toString('base64'), {
|
|
206
|
+
pathBytes: first,
|
|
207
|
+
status: kind === 'A' ? 'added' : kind === 'D' ? 'deleted' : kind === 'T' ? 'type-changed' : kind === 'U' ? 'unmerged' : 'modified',
|
|
208
|
+
statusText
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return changes;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function parseStatusOracle(bytes) {
|
|
216
|
+
const fields = nulFields(bytes);
|
|
217
|
+
const paths = new Map();
|
|
218
|
+
for (let index = 0; index < fields.length;) {
|
|
219
|
+
const field = fields[index++];
|
|
220
|
+
if (field.length < 4) throw new Error('malformed Git porcelain status entry');
|
|
221
|
+
const status = field.subarray(0, 2).toString('ascii');
|
|
222
|
+
const pathBytes = Buffer.from(field.subarray(3));
|
|
223
|
+
paths.set(pathBytes.toString('base64'), status);
|
|
224
|
+
if (status.includes('R') || status.includes('C')) {
|
|
225
|
+
const partner = fields[index++];
|
|
226
|
+
if (!partner) throw new Error('truncated Git porcelain rename/copy entry');
|
|
227
|
+
paths.set(partner.toString('base64'), status);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return paths;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isDocumentPath(pathBytes) {
|
|
234
|
+
const name = pathBytes.toString('latin1');
|
|
235
|
+
const lower = name.toLowerCase();
|
|
236
|
+
return /\.(md|mdx|markdown|rst|adoc|txt)$/.test(lower)
|
|
237
|
+
|| /(?:^|\/)(?:readme|license|notice|contributing|changelog|code_of_conduct|security)$/.test(lower)
|
|
238
|
+
|| /(?:^|\/)(?:readme|changelog)[^/]*$/.test(lower);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function inventoryOracle(root, base) {
|
|
242
|
+
const commit = gitAt(root, ['rev-parse', '--verify', '--quiet', '--end-of-options', `${base}^{commit}`]);
|
|
243
|
+
const baseEntries = parseTreeOracle(gitRawAt(root, ['ls-tree', '-r', '-z', '--full-tree', commit]));
|
|
244
|
+
const stageEntries = parseStageOracle(gitRawAt(root, ['ls-files', '-z', '--stage']));
|
|
245
|
+
const changes = parseNameStatusOracle(gitRawAt(root, ['diff', '--name-status', '-z', '--find-renames', commit, '--']));
|
|
246
|
+
const statusPaths = parseStatusOracle(gitRawAt(root, ['status', '--porcelain=v1', '-z']));
|
|
247
|
+
const entries = new Map([...baseEntries, ...stageEntries]);
|
|
248
|
+
for (const [key, change] of changes) {
|
|
249
|
+
if (!entries.has(key)) entries.set(key, { pathBytes: change.pathBytes });
|
|
250
|
+
if (change.partner) {
|
|
251
|
+
const partnerKey = change.partner.toString('base64');
|
|
252
|
+
if (!entries.has(partnerKey)) entries.set(partnerKey, { pathBytes: change.partner });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const documentKeys = new Set([...entries].filter(([, entry]) => isDocumentPath(entry.pathBytes)).map(([key]) => key));
|
|
256
|
+
return { root, commit, baseEntries, stageEntries, changes, statusPaths, entries, documentKeys };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function rawWorktreeIdentity(root, pathBytes, stageEntry) {
|
|
260
|
+
const decoded = pathBytes.toString('utf8');
|
|
261
|
+
const absolute = Buffer.from(decoded, 'utf8').equals(pathBytes)
|
|
262
|
+
? path.join(path.resolve(root), ...decoded.split('/'))
|
|
263
|
+
: process.platform === 'win32' ? null : Buffer.concat([Buffer.from(`${path.resolve(root)}${path.sep}`), pathBytes]);
|
|
264
|
+
if (!absolute) return null;
|
|
265
|
+
let stat;
|
|
266
|
+
try { stat = fs.lstatSync(absolute); } catch (error) {
|
|
267
|
+
if (error.code === 'ENOENT') return null;
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
if (stageEntry && stageEntry.mode === '160000' && stat.isDirectory()) {
|
|
271
|
+
return { mode: stageEntry.mode, type: 'commit', oid: stageEntry.oid };
|
|
272
|
+
}
|
|
273
|
+
let mode;
|
|
274
|
+
let bytes;
|
|
275
|
+
if (stat.isSymbolicLink()) {
|
|
276
|
+
mode = '120000';
|
|
277
|
+
bytes = fs.readlinkSync(absolute, { encoding: 'buffer' });
|
|
278
|
+
} else if (stat.isFile()) {
|
|
279
|
+
mode = stat.mode & 0o111 ? '100755' : '100644';
|
|
280
|
+
bytes = fs.readFileSync(absolute);
|
|
281
|
+
} else return null;
|
|
282
|
+
const result = spawnSync('git', ['-C', root, 'hash-object', '--stdin'], {
|
|
283
|
+
input: bytes,
|
|
284
|
+
encoding: 'buffer',
|
|
285
|
+
env: { ...process.env, GIT_CONFIG_NOSYSTEM: '1' }
|
|
286
|
+
});
|
|
287
|
+
if (result.status !== 0) throw new Error(`git hash-object failed: ${String(result.stderr || result.stdout)}`);
|
|
288
|
+
return { mode, type: 'blob', oid: result.stdout.toString('ascii').trim() };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function assertInventoryAgainstOracle(report, oracle, expectedDocumentKeys = oracle.documentKeys) {
|
|
292
|
+
assert.strictEqual(report.base.commit, oracle.commit, 'inventory base is the pinned oracle commit');
|
|
293
|
+
const expectedKeys = new Set(expectedDocumentKeys);
|
|
294
|
+
for (const [key, entry] of oracle.entries) {
|
|
295
|
+
const base = oracle.baseEntries.get(key);
|
|
296
|
+
const staged = oracle.stageEntries.get(key);
|
|
297
|
+
const change = oracle.changes.get(key);
|
|
298
|
+
if ((base && ['120000', '160000'].includes(base.mode))
|
|
299
|
+
|| (staged && ['120000', '160000'].includes(staged.mode))) expectedKeys.add(key);
|
|
300
|
+
if (change && change.partner && (expectedDocumentKeys.has(key) || expectedDocumentKeys.has(change.partner.toString('base64')))) {
|
|
301
|
+
expectedKeys.add(key);
|
|
302
|
+
expectedKeys.add(change.partner.toString('base64'));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const records = new Map();
|
|
306
|
+
for (const record of report.records || []) {
|
|
307
|
+
assert.ok(Object.prototype.hasOwnProperty.call(record, 'path'), 'report path is present');
|
|
308
|
+
assert.ok(typeof record.path === 'string' && typeof record.pathEncoding === 'string', 'report has a display path and encoding');
|
|
309
|
+
assert.ok(typeof record.pathBytesBase64 === 'string' && record.pathBytesBase64, 'raw path identity is present');
|
|
310
|
+
assert.ok(!records.has(record.pathBytesBase64), `duplicate report identity ${record.pathBytesBase64}`);
|
|
311
|
+
records.set(record.pathBytesBase64, record);
|
|
312
|
+
}
|
|
313
|
+
assert.deepStrictEqual([...records.keys()].sort(), [...expectedKeys].sort(), 'report identity paths equal raw Git oracle paths');
|
|
314
|
+
for (const key of expectedKeys) {
|
|
315
|
+
const record = records.get(key);
|
|
316
|
+
const base = oracle.baseEntries.get(key) || null;
|
|
317
|
+
const staged = oracle.stageEntries.get(key) || null;
|
|
318
|
+
const change = oracle.changes.get(key) || null;
|
|
319
|
+
const localStatus = oracle.statusPaths.get(key);
|
|
320
|
+
assert.deepStrictEqual(record.base, base ? { mode: base.mode, type: base.type, oid: base.oid } : null, `base identity ${key}`);
|
|
321
|
+
const deleted = change && ['deleted', 'renamed-from'].includes(change.status);
|
|
322
|
+
const current = deleted ? null : change ? rawWorktreeIdentity(oracle.root, recordPathBytes(record), staged) : base ? base : staged;
|
|
323
|
+
if (current) {
|
|
324
|
+
assert.ok(record.current, `current identity is present for ${key}`);
|
|
325
|
+
for (const field of ['mode', 'type', 'oid']) assert.strictEqual(record.current[field], current[field], `current ${field} ${key}`);
|
|
326
|
+
if (staged && (!localStatus || localStatus[1] === ' ')) {
|
|
327
|
+
for (const field of ['mode', 'type', 'oid']) assert.strictEqual(record.current[field], staged[field], `clean worktree matches staged ${field} ${key}`);
|
|
328
|
+
}
|
|
329
|
+
} else if (record.current !== null) {
|
|
330
|
+
throw new Error(`deleted/unavailable current identity should be null for ${key}`);
|
|
331
|
+
} else if (!deleted) {
|
|
332
|
+
assert.ok(typeof record.currentReason === 'string' && record.currentReason, `unavailable identity reason ${key}`);
|
|
333
|
+
}
|
|
334
|
+
const expectedStatus = change ? change.status : base ? 'unchanged' : 'added';
|
|
335
|
+
assert.strictEqual(record.status, expectedStatus, `Git status ${key}`);
|
|
336
|
+
if (change && change.partner) {
|
|
337
|
+
assert.strictEqual(record.rename.endpoint, change.endpoint, `rename endpoint ${key}`);
|
|
338
|
+
assert.strictEqual(record.rename.partnerPathBytesBase64, change.partner.toString('base64'), `rename partner bytes ${key}`);
|
|
339
|
+
} else assert.strictEqual(record.rename, undefined, `no rename metadata ${key}`);
|
|
340
|
+
if (expectedDocumentKeys.has(key)) {
|
|
341
|
+
assert.strictEqual(record.document, true, `identified document ${key}`);
|
|
342
|
+
for (const axis of ['role', 'lifecycle', 'disposition']) {
|
|
343
|
+
assert.ok(Object.prototype.hasOwnProperty.call(record, axis) && typeof record[axis] === 'string' && record[axis], `${axis} axis ${key}`);
|
|
344
|
+
}
|
|
345
|
+
assert.ok(typeof record.reason === 'string' && record.reason, `disposition reason ${key}`);
|
|
346
|
+
assert.strictEqual(record.validation && record.validation.outcome, 'unchecked', `semantic validation stays separate and unchecked ${key}`);
|
|
347
|
+
assert.ok(typeof record.validation.reason === 'string' && record.validation.reason, `validation reason ${key}`);
|
|
348
|
+
if (record.disposition === 'unknown') assert.ok(record.reason, `unknown disposition remains visible with a reason ${key}`);
|
|
349
|
+
if (oracle.statusPaths.has(key)) assert.ok(change || !base, `raw status path is represented by a base-relative change ${key}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
assert.deepStrictEqual(report.coverage, {
|
|
353
|
+
identifiedDocumentPaths: expectedDocumentKeys.size,
|
|
354
|
+
explicitDispositionPaths: expectedDocumentKeys.size,
|
|
355
|
+
complete: true,
|
|
356
|
+
claim: 'disposition coverage only; semantic validation was not performed'
|
|
357
|
+
}, 'coverage counts all oracle documents without promoting semantics');
|
|
358
|
+
return records;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function recordPathBytes(record) {
|
|
362
|
+
return Buffer.from(record.pathBytesBase64, 'base64');
|
|
363
|
+
}
|
|
364
|
+
|
|
149
365
|
function legacyHistory(name, {
|
|
150
366
|
frontmatter = { id: 'GRAPH-900', status: 'accepted', aliases: [], supersedes: [] },
|
|
151
367
|
reference = false,
|
|
@@ -1058,4 +1274,730 @@ checkCase('2ax. scenario fixtures enforce the full inventory and required files'
|
|
|
1058
1274
|
assert.ok(runScenarioSet(unexpected).includes('unexpected scenario: incomplete'), JSON.stringify(runScenarioSet(unexpected)));
|
|
1059
1275
|
});
|
|
1060
1276
|
|
|
1277
|
+
checkCase('2ax. Git document inventory preserves dispositions and base/current identity', () => {
|
|
1278
|
+
const root = workspace('git-inventory');
|
|
1279
|
+
const write = (relative, bytes = `${relative}\n`) => {
|
|
1280
|
+
const absolute = path.join(root, ...relative.split('/'));
|
|
1281
|
+
fs.mkdirSync(path.dirname(absolute), { recursive: true });
|
|
1282
|
+
fs.writeFileSync(absolute, bytes);
|
|
1283
|
+
return absolute;
|
|
1284
|
+
};
|
|
1285
|
+
const manifestRelative = '.agents/harness-everything/document-inventory.json';
|
|
1286
|
+
write(manifestRelative, `${JSON.stringify({
|
|
1287
|
+
schemaVersion: 1,
|
|
1288
|
+
documentGlobs: ['**/*.md', '**/*.txt'],
|
|
1289
|
+
rules: [
|
|
1290
|
+
{ match: 'docs/reference/**', role: 'reference', lifecycle: 'current', disposition: 'applicable', reason: 'fixture positive control' },
|
|
1291
|
+
{ match: '**/archive/**', role: 'legacy/archive', lifecycle: 'historical', disposition: 'inventory-only', reason: 'retained historical fixture' },
|
|
1292
|
+
{ match: '**/generated/**', role: 'generated-view', lifecycle: 'generated', disposition: 'inventory-only', reason: 'generator is not run by inventory' },
|
|
1293
|
+
{ match: '**/fixtures/**', role: 'fixture', lifecycle: 'retained-test-input', disposition: 'excluded', reason: 'fixture purpose is preserved' },
|
|
1294
|
+
{ match: '**/vendor/**', role: 'vendor-document', lifecycle: 'external-content', disposition: 'excluded', reason: 'vendor purpose is preserved' }
|
|
1295
|
+
]
|
|
1296
|
+
}, null, 2)}\n`);
|
|
1297
|
+
for (const relative of [
|
|
1298
|
+
'README.md', 'docs/reference/positive.md', 'docs/prd/plan.md', 'docs/adr/decision.md',
|
|
1299
|
+
'docs/archive/old.md', 'docs/generated/view.md', 'docs/fixtures/setup.md',
|
|
1300
|
+
'benchmarks/results/evidence.md', 'vendor/pkg/README.md', 'skills/sample/SKILL.md',
|
|
1301
|
+
'references/source.md', 'templates/example.md', 'nested/deep/unknown.md',
|
|
1302
|
+
'docs/delete.md', 'docs/rename-old.md', 'docs/mode.md', ...(process.platform === 'win32' ? [] : ['docs/odd\tline\nname.md'])
|
|
1303
|
+
]) write(relative);
|
|
1304
|
+
const symlinks = [];
|
|
1305
|
+
const trackedSymlink = path.join(root, 'docs', 'fixtures', 'external.md');
|
|
1306
|
+
fs.mkdirSync(path.dirname(trackedSymlink), { recursive: true });
|
|
1307
|
+
try {
|
|
1308
|
+
fs.symlinkSync(path.join(os.tmpdir(), 'outside-document-not-read.md'), trackedSymlink, process.platform === 'win32' ? 'file' : undefined);
|
|
1309
|
+
symlinks.push('docs/fixtures/external.md');
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
if (!['EPERM', 'EACCES', 'ENOTSUP'].includes(error.code)) throw error;
|
|
1312
|
+
console.log(` SKIP 2ax. tracked symlink inventory subcase: symlink creation unavailable (${error.code})`);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
gitAt(root, ['init', '-q']);
|
|
1316
|
+
for (const [key, value] of [['user.name', 'Harness Test'], ['user.email', 'harness@example.invalid'], ['commit.gpgsign', 'false'], ['core.filemode', 'false']]) gitAt(root, ['config', key, value]);
|
|
1317
|
+
gitAt(root, ['add', '-A']);
|
|
1318
|
+
gitAt(root, ['update-index', '--chmod=+x', '--', 'docs/mode.md']);
|
|
1319
|
+
gitAt(root, ['commit', '-qm', 'inventory fixture']);
|
|
1320
|
+
const gitlinkTarget = gitAt(root, ['rev-parse', 'HEAD']);
|
|
1321
|
+
gitAt(root, ['update-index', '--add', '--cacheinfo', `160000,${gitlinkTarget},vendor/submodule`]);
|
|
1322
|
+
gitAt(root, ['commit', '-qm', 'add gitlink fixture']);
|
|
1323
|
+
const base = gitAt(root, ['rev-parse', 'HEAD']);
|
|
1324
|
+
|
|
1325
|
+
const untrackedSymlink = path.join(root, 'docs', 'fixtures', 'untracked-external.md');
|
|
1326
|
+
try {
|
|
1327
|
+
fs.symlinkSync(path.join(os.tmpdir(), 'outside-document-not-read.md'), untrackedSymlink, process.platform === 'win32' ? 'file' : undefined);
|
|
1328
|
+
symlinks.push('docs/fixtures/untracked-external.md');
|
|
1329
|
+
} catch (error) {
|
|
1330
|
+
if (!['EPERM', 'EACCES', 'ENOTSUP'].includes(error.code)) throw error;
|
|
1331
|
+
console.log(` SKIP 2ax. proposed symlink inventory subcase: symlink creation unavailable (${error.code})`);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
fs.rmSync(path.join(root, 'docs', 'delete.md'));
|
|
1335
|
+
fs.renameSync(path.join(root, 'docs', 'rename-old.md'), path.join(root, 'docs', 'rename-new.md'));
|
|
1336
|
+
write('docs/untracked.md');
|
|
1337
|
+
write('docs/staged.md');
|
|
1338
|
+
write('local-session/private-notes.md', 'private sentinel: first value\n');
|
|
1339
|
+
gitAt(root, ['add', '-A', '--', 'docs/rename-old.md', 'docs/rename-new.md', 'docs/staged.md']);
|
|
1340
|
+
write('docs/reference/positive.md', 'modified positive control\n');
|
|
1341
|
+
if (process.platform !== 'win32') write('docs/odd\tline\nname.md', 'modified unusual path\n');
|
|
1342
|
+
|
|
1343
|
+
const cli = (script, extraArgs = []) => {
|
|
1344
|
+
const result = spawnSync(process.execPath, [script, 'inventory', '--workspace', root, '--manifest', manifestRelative, '--base', base, ...extraArgs], { cwd: tempRoot, encoding: 'utf8' });
|
|
1345
|
+
assert.ifError(result.error);
|
|
1346
|
+
assert.strictEqual(result.status, 0, `inventory ${extraArgs.join(' ')}: ${result.stderr}`);
|
|
1347
|
+
return { report: JSON.parse(result.stdout), stdout: result.stdout };
|
|
1348
|
+
};
|
|
1349
|
+
const first = cli(modulePath);
|
|
1350
|
+
write('local-session/private-notes.md', 'private sentinel: changed value\n');
|
|
1351
|
+
const second = cli(modulePath);
|
|
1352
|
+
const report = first.report;
|
|
1353
|
+
assert.strictEqual(first.stdout, second.stdout, 'changes to unproposed scratch files do not affect inventory output');
|
|
1354
|
+
assert.ok(!first.stdout.includes('local-session/private-notes.md'), 'unproposed local session path is not inventoried');
|
|
1355
|
+
assert.ok(!first.stdout.includes('private sentinel'), 'unproposed local session contents do not enter output');
|
|
1356
|
+
assert.strictEqual(report.base.commit, base, 'the requested base resolves to a pinned commit');
|
|
1357
|
+
assert.strictEqual(report.candidate.kind, 'working-tree', 'the current worktree is not mislabeled as the pinned base');
|
|
1358
|
+
assert.ok(report.candidate.sha256, 'the document candidate identity has a digest');
|
|
1359
|
+
assert.deepStrictEqual(report.coverage, {
|
|
1360
|
+
identifiedDocumentPaths: report.records.filter(item => item.document).length,
|
|
1361
|
+
explicitDispositionPaths: report.records.filter(item => item.document && item.reason).length,
|
|
1362
|
+
complete: true,
|
|
1363
|
+
claim: 'disposition coverage only; semantic validation was not performed'
|
|
1364
|
+
});
|
|
1365
|
+
assert.ok(report.records.some(item => item.path === 'README.md'), 'root README is inventoried');
|
|
1366
|
+
for (const relative of ['docs/prd/plan.md', 'docs/adr/decision.md', 'docs/archive/old.md', 'docs/generated/view.md', 'docs/fixtures/setup.md', 'benchmarks/results/evidence.md', 'vendor/pkg/README.md', 'skills/sample/SKILL.md', 'references/source.md', 'templates/example.md', 'nested/deep/unknown.md']) {
|
|
1367
|
+
assert.ok(report.records.some(item => item.path === relative), `nested document is inventoried: ${relative}`);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
const byPath = new Map(report.records.map(item => [item.path, item]));
|
|
1371
|
+
const positive = byPath.get('docs/reference/positive.md');
|
|
1372
|
+
assert.strictEqual(positive.role, 'reference');
|
|
1373
|
+
assert.strictEqual(positive.lifecycle, 'current');
|
|
1374
|
+
assert.strictEqual(positive.disposition, 'applicable');
|
|
1375
|
+
assert.ok(positive.reason, 'disposition reason is explicit');
|
|
1376
|
+
assert.strictEqual(positive.status, 'modified');
|
|
1377
|
+
assert.strictEqual(positive.validation.outcome, 'unchecked');
|
|
1378
|
+
assert.ok(positive.validation.reason, 'validation outcome has its own reason');
|
|
1379
|
+
assert.strictEqual(positive.current.oid, gitAt(root, ['hash-object', 'docs/reference/positive.md']));
|
|
1380
|
+
|
|
1381
|
+
assert.strictEqual(byPath.get('docs/delete.md').status, 'deleted');
|
|
1382
|
+
assert.strictEqual(byPath.get('docs/delete.md').current, null);
|
|
1383
|
+
assert.strictEqual(byPath.get('docs/delete.md').base.oid, gitAt(root, ['rev-parse', `${base}:docs/delete.md`]));
|
|
1384
|
+
assert.strictEqual(byPath.get('docs/rename-old.md').status, 'renamed-from');
|
|
1385
|
+
assert.strictEqual(byPath.get('docs/rename-new.md').status, 'renamed-to');
|
|
1386
|
+
assert.strictEqual(byPath.get('docs/rename-old.md').rename.partnerPath, 'docs/rename-new.md');
|
|
1387
|
+
assert.strictEqual(byPath.get('docs/rename-new.md').rename.partnerPath, 'docs/rename-old.md');
|
|
1388
|
+
assert.ok(!byPath.has('docs/untracked.md'), 'unproposed untracked docs are excluded');
|
|
1389
|
+
assert.strictEqual(byPath.get('docs/staged.md').status, 'added');
|
|
1390
|
+
assert.strictEqual(byPath.get('docs/mode.md').base.mode, '100755', 'Git index executable mode is preserved across hosts');
|
|
1391
|
+
assert.strictEqual(byPath.get('docs/mode.md').current.mode, '100755');
|
|
1392
|
+
|
|
1393
|
+
if (process.platform !== 'win32') {
|
|
1394
|
+
const unusualPath = 'docs/odd\tline\nname.md';
|
|
1395
|
+
const unusual = report.records.find(item => item.pathBytesBase64 === Buffer.from(unusualPath).toString('base64'));
|
|
1396
|
+
assert.ok(unusual, 'NUL-delimited Git plumbing preserves tabs and newlines in raw path bytes');
|
|
1397
|
+
assert.strictEqual(unusual.status, 'modified');
|
|
1398
|
+
assert.strictEqual(unusual.path, unusualPath);
|
|
1399
|
+
} else console.log(' SKIP 2ax. tab/newline filename subcase: Windows forbids control characters in filenames');
|
|
1400
|
+
assert.strictEqual(byPath.get('nested/deep/unknown.md').disposition, 'unknown');
|
|
1401
|
+
assert.ok(byPath.get('nested/deep/unknown.md').reason, 'unknown classification retains a reason');
|
|
1402
|
+
if (symlinks.includes('docs/fixtures/external.md')) {
|
|
1403
|
+
assert.strictEqual(byPath.get('docs/fixtures/external.md').kind, 'symlink');
|
|
1404
|
+
assert.strictEqual(byPath.get('docs/fixtures/external.md').base.mode, '120000');
|
|
1405
|
+
}
|
|
1406
|
+
const proposed = cli(modulePath, ['--proposed-untracked', 'docs/untracked.md', ...(symlinks.includes('docs/fixtures/untracked-external.md') ? ['--proposed-untracked', 'docs/fixtures/untracked-external.md'] : [])]).report;
|
|
1407
|
+
const proposedByPath = new Map(proposed.records.map(item => [item.path, item]));
|
|
1408
|
+
assert.strictEqual(proposedByPath.get('docs/untracked.md').status, 'added', 'explicitly proposed untracked document is inventoried');
|
|
1409
|
+
if (symlinks.includes('docs/fixtures/untracked-external.md')) {
|
|
1410
|
+
assert.strictEqual(proposedByPath.get('docs/fixtures/untracked-external.md').kind, 'symlink');
|
|
1411
|
+
assert.strictEqual(proposedByPath.get('docs/fixtures/untracked-external.md').current.mode, '120000');
|
|
1412
|
+
assert.strictEqual(proposedByPath.get('docs/fixtures/untracked-external.md').current.oidBasis, 'raw worktree bytes; no Git filters applied');
|
|
1413
|
+
const linkOid = spawnSync('git', ['-C', root, 'hash-object', '--stdin'], { encoding: 'utf8', input: fs.readlinkSync(path.join(root, 'docs', 'fixtures', 'untracked-external.md'), { encoding: 'buffer' }) });
|
|
1414
|
+
assert.ifError(linkOid.error);
|
|
1415
|
+
assert.strictEqual(proposedByPath.get('docs/fixtures/untracked-external.md').current.oid, linkOid.stdout.trim());
|
|
1416
|
+
}
|
|
1417
|
+
assert.strictEqual(byPath.get('vendor/submodule').kind, 'gitlink');
|
|
1418
|
+
assert.strictEqual(byPath.get('vendor/submodule').base.mode, '160000');
|
|
1419
|
+
assert.strictEqual(byPath.get('vendor/submodule').base.oid, gitAt(root, ['rev-parse', `${base}:vendor/submodule`]));
|
|
1420
|
+
assert.ok(!report.records.some(item => item.path.startsWith('vendor/submodule/')), 'gitlink contents are not recursed');
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
let pinnedBaseAvailable = false;
|
|
1424
|
+
try { gitAt(REPOSITORY_ROOT, ['cat-file', '-e', `${P2_BASE_SHA}^{commit}`]); pinnedBaseAvailable = true; } catch { /* reported below as unchecked */ }
|
|
1425
|
+
if (!pinnedBaseAvailable) helper.check('2ax P2. actual repository inventory matches the raw Git oracle at a pinned base', false, `UNCHECKED: pinned baseline ${P2_BASE_SHA} unavailable; required repository ratchet not verified`);
|
|
1426
|
+
else checkCase('2ax P2. actual repository inventory matches the raw Git oracle at a pinned base', () => {
|
|
1427
|
+
const result = spawnSync(process.execPath, [modulePath, 'inventory', '--workspace', REPOSITORY_ROOT, '--manifest', '.agents/harness-everything/document-inventory.json', '--base', P2_BASE_SHA], { cwd: tempRoot, encoding: 'utf8' });
|
|
1428
|
+
assert.strictEqual(result.status, 0, `actual repository inventory: ${result.stderr}`);
|
|
1429
|
+
const report = JSON.parse(result.stdout);
|
|
1430
|
+
const oracle = inventoryOracle(REPOSITORY_ROOT, P2_BASE_SHA);
|
|
1431
|
+
const records = assertInventoryAgainstOracle(report, oracle);
|
|
1432
|
+
assert.ok(records.has(Buffer.from('README.md').toString('base64')), 'actual repository positive control is present');
|
|
1433
|
+
assert.ok(records.has(Buffer.from('docs/architecture.md').toString('base64')), 'governed repository path is not dropped');
|
|
1434
|
+
console.log(` P2 repo oracle: ${report.coverage.identifiedDocumentPaths} document paths, ${oracle.changes.size} changed endpoints, ${oracle.stageEntries.size} index entries, ${oracle.statusPaths.size} status paths matched from raw NUL Git commands`);
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
checkCase('2ax P2. baseline-to-candidate ratchet detects scope and identity drift', () => {
|
|
1438
|
+
const root = workspace('baseline-ratchet');
|
|
1439
|
+
const manifestPath = '.agents/harness-everything/document-inventory.json';
|
|
1440
|
+
const rule = match => ({ match, role: 'reference', lifecycle: 'current', disposition: 'applicable', reason: `fixture rule for ${match}` });
|
|
1441
|
+
// This expected policy is separate from the candidate manifest under test.
|
|
1442
|
+
const baselineMusts = Object.freeze([
|
|
1443
|
+
Object.freeze({ path: 'docs/reference/keep.md', role: 'reference', lifecycle: 'current', disposition: 'applicable' })
|
|
1444
|
+
]);
|
|
1445
|
+
const assertBaselineMusts = candidate => {
|
|
1446
|
+
const records = new Map(candidate.records.map(item => [item.pathBytesBase64, item]));
|
|
1447
|
+
for (const must of baselineMusts) {
|
|
1448
|
+
const key = Buffer.from(must.path).toString('base64');
|
|
1449
|
+
const record = records.get(key);
|
|
1450
|
+
assert.ok(record, `baseline MUST document is missing: ${must.path}`);
|
|
1451
|
+
for (const field of ['role', 'lifecycle', 'disposition']) {
|
|
1452
|
+
assert.strictEqual(record[field], must[field], `baseline MUST ${must.path} ${field} changed`);
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
};
|
|
1456
|
+
const baselineManifest = {
|
|
1457
|
+
schemaVersion: 1,
|
|
1458
|
+
documentGlobs: ['**/*.md'],
|
|
1459
|
+
rules: [
|
|
1460
|
+
rule('docs/reference/current.md'), rule('docs/reference/keep.md'), rule('docs/reference/deleted.md'),
|
|
1461
|
+
rule('docs/reference/rename-old.md'), rule('docs/reference/new.md'), rule('docs/reference/staged.md'),
|
|
1462
|
+
rule('docs/reference/working.md'), rule('docs/reference/mode.md')
|
|
1463
|
+
]
|
|
1464
|
+
};
|
|
1465
|
+
const write = (relative, contents) => {
|
|
1466
|
+
const absolute = path.join(root, ...relative.split('/'));
|
|
1467
|
+
fs.mkdirSync(path.dirname(absolute), { recursive: true });
|
|
1468
|
+
fs.writeFileSync(absolute, contents);
|
|
1469
|
+
return absolute;
|
|
1470
|
+
};
|
|
1471
|
+
const writeManifest = value => write(manifestPath, `${JSON.stringify(value, null, 2)}\n`);
|
|
1472
|
+
writeManifest(baselineManifest);
|
|
1473
|
+
for (const relative of [
|
|
1474
|
+
'docs/reference/current.md', 'docs/reference/keep.md', 'docs/reference/deleted.md',
|
|
1475
|
+
'docs/reference/rename-old.md', 'docs/reference/working.md', 'docs/reference/staged.md',
|
|
1476
|
+
'docs/reference/mode.md'
|
|
1477
|
+
]) write(relative, `${relative}\n`);
|
|
1478
|
+
gitAt(root, ['init', '-q']);
|
|
1479
|
+
for (const [key, value] of [['user.name', 'Harness P2 fixture'], ['user.email', 'harness-p2@example.invalid'], ['commit.gpgsign', 'false'], ['core.filemode', 'false']]) gitAt(root, ['config', key, value]);
|
|
1480
|
+
gitAt(root, ['add', '-A']);
|
|
1481
|
+
gitAt(root, ['update-index', '--chmod=+x', '--', 'docs/reference/mode.md']);
|
|
1482
|
+
gitAt(root, ['commit', '-qm', 'trusted inventory baseline']);
|
|
1483
|
+
const base = gitAt(root, ['rev-parse', 'HEAD']);
|
|
1484
|
+
|
|
1485
|
+
fs.rmSync(path.join(root, 'docs/reference/deleted.md'));
|
|
1486
|
+
fs.renameSync(path.join(root, 'docs/reference/rename-old.md'), path.join(root, 'docs/reference/rename-new.md'));
|
|
1487
|
+
write('docs/reference/new.md', 'new tracked document\n');
|
|
1488
|
+
write('docs/reference/staged.md', 'staged tracked document\n');
|
|
1489
|
+
write('docs/reference/working.md', 'staged contents\n');
|
|
1490
|
+
write('docs/unknown/unclassified.md', 'unknown path remains visible\n');
|
|
1491
|
+
const candidateManifest = { ...baselineManifest, rules: baselineManifest.rules.filter(item => item.match !== 'docs/reference/keep.md').concat(rule('docs/reference/rename-new.md')) };
|
|
1492
|
+
writeManifest(candidateManifest);
|
|
1493
|
+
gitAt(root, ['add', '-A']);
|
|
1494
|
+
write('docs/reference/working.md', 'unstaged worktree contents\n');
|
|
1495
|
+
|
|
1496
|
+
const inventory = () => {
|
|
1497
|
+
const result = spawnSync(process.execPath, [modulePath, 'inventory', '--workspace', root, '--manifest', manifestPath, '--base', base], { cwd: tempRoot, encoding: 'utf8' });
|
|
1498
|
+
assert.ifError(result.error);
|
|
1499
|
+
assert.strictEqual(result.status, 0, `fixture inventory: ${result.stderr}`);
|
|
1500
|
+
return JSON.parse(result.stdout);
|
|
1501
|
+
};
|
|
1502
|
+
const docPaths = [
|
|
1503
|
+
'docs/reference/current.md', 'docs/reference/keep.md', 'docs/reference/deleted.md',
|
|
1504
|
+
'docs/reference/rename-old.md', 'docs/reference/rename-new.md', 'docs/reference/new.md',
|
|
1505
|
+
'docs/reference/staged.md', 'docs/reference/working.md', 'docs/reference/mode.md',
|
|
1506
|
+
'docs/unknown/unclassified.md'
|
|
1507
|
+
];
|
|
1508
|
+
const expectedDocs = new Set(docPaths.map(value => Buffer.from(value).toString('base64')));
|
|
1509
|
+
const report = inventory();
|
|
1510
|
+
assert.throws(() => assertBaselineMusts(report), /baseline MUST docs\/reference\/keep\.md role changed/, 'dropping a baseline MUST rule is a policy scope regression');
|
|
1511
|
+
const oracle = inventoryOracle(root, base);
|
|
1512
|
+
const records = assertInventoryAgainstOracle(report, oracle, expectedDocs);
|
|
1513
|
+
const get = value => records.get(Buffer.from(value).toString('base64'));
|
|
1514
|
+
assert.strictEqual(get('docs/reference/current.md').disposition, 'applicable', 'unchanged governed path is a positive control');
|
|
1515
|
+
assert.strictEqual(get('docs/reference/new.md').status, 'added', 'new tracked document is visible');
|
|
1516
|
+
assert.ok(oracle.stageEntries.has(Buffer.from('docs/reference/staged.md').toString('base64')), 'staged positive control is in the independent index oracle');
|
|
1517
|
+
assert.ok(oracle.statusPaths.has(Buffer.from('docs/reference/working.md').toString('base64')), 'unstaged positive control is in porcelain status');
|
|
1518
|
+
assert.strictEqual(get('docs/reference/working.md').current.oid, gitAt(root, ['hash-object', 'docs/reference/working.md']), 'working-tree OID is checked independently');
|
|
1519
|
+
const stagedWorkingOid = oracle.stageEntries.get(Buffer.from('docs/reference/working.md').toString('base64')).oid;
|
|
1520
|
+
const stagedWorkingHash = spawnSync('git', ['-C', root, 'hash-object', '--stdin'], { input: Buffer.from('staged contents\n'), encoding: 'utf8' });
|
|
1521
|
+
assert.ifError(stagedWorkingHash.error);
|
|
1522
|
+
assert.strictEqual(stagedWorkingOid, stagedWorkingHash.stdout.trim(), 'index OID comes from its staged bytes');
|
|
1523
|
+
assert.notStrictEqual(get('docs/reference/working.md').current.oid, stagedWorkingOid, 'worktree OID remains distinct from the staged OID');
|
|
1524
|
+
assert.strictEqual(get('docs/reference/mode.md').base.mode, '100755', 'base executable mode is preserved');
|
|
1525
|
+
assert.strictEqual(get('docs/reference/deleted.md').status, 'deleted', 'deleted baseline endpoint remains visible');
|
|
1526
|
+
assert.strictEqual(get('docs/reference/rename-old.md').rename.endpoint, 'source', 'rename source is visible');
|
|
1527
|
+
assert.strictEqual(get('docs/reference/rename-new.md').rename.endpoint, 'target', 'rename target is visible');
|
|
1528
|
+
for (const value of ['docs/reference/keep.md', 'docs/unknown/unclassified.md']) {
|
|
1529
|
+
assert.strictEqual(get(value).disposition, 'unknown', `unknown or dropped entry remains visible: ${value}`);
|
|
1530
|
+
assert.ok(get(value).reason, `unknown or dropped entry keeps a reason: ${value}`);
|
|
1531
|
+
assert.strictEqual(get(value).validation.outcome, 'unchecked', `unknown is not semantic PASS: ${value}`);
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
const retainedMustManifest = { ...candidateManifest, rules: [rule('docs/reference/keep.md'), ...candidateManifest.rules] };
|
|
1535
|
+
writeManifest(retainedMustManifest);
|
|
1536
|
+
gitAt(root, ['add', '--', manifestPath]);
|
|
1537
|
+
const retainedMustReport = inventory();
|
|
1538
|
+
assert.doesNotThrow(() => assertBaselineMusts(retainedMustReport), 'retaining the baseline MUST passes the policy ratchet');
|
|
1539
|
+
const retainedByPath = new Map(retainedMustReport.records.map(item => [item.path, item]));
|
|
1540
|
+
assert.strictEqual(retainedByPath.get('docs/reference/current.md').disposition, 'applicable', 'unchanged baseline path remains a positive control');
|
|
1541
|
+
assert.strictEqual(retainedByPath.get('docs/unknown/unclassified.md').disposition, 'unknown', 'a new unknown path remains visible without shrinking baseline MUSTs');
|
|
1542
|
+
|
|
1543
|
+
const weakenedMustManifest = {
|
|
1544
|
+
...retainedMustManifest,
|
|
1545
|
+
rules: retainedMustManifest.rules.map(item => item.match === 'docs/reference/keep.md' ? { ...item, disposition: 'inventory-only' } : item)
|
|
1546
|
+
};
|
|
1547
|
+
writeManifest(weakenedMustManifest);
|
|
1548
|
+
gitAt(root, ['add', '--', manifestPath]);
|
|
1549
|
+
const weakenedMustReport = inventory();
|
|
1550
|
+
assert.throws(() => assertBaselineMusts(weakenedMustReport), /baseline MUST docs\/reference\/keep\.md disposition changed/, 'weakening a baseline MUST rule is a policy scope regression');
|
|
1551
|
+
|
|
1552
|
+
function getFrom(candidate, relative) {
|
|
1553
|
+
return candidate.records.find(item => item.pathBytesBase64 === Buffer.from(relative).toString('base64'));
|
|
1554
|
+
}
|
|
1555
|
+
const rejects = (mutate, label) => {
|
|
1556
|
+
const copy = JSON.parse(JSON.stringify(report));
|
|
1557
|
+
mutate(copy);
|
|
1558
|
+
assert.throws(() => assertInventoryAgainstOracle(copy, oracle, expectedDocs), undefined, label);
|
|
1559
|
+
};
|
|
1560
|
+
rejects(copy => { copy.records.pop(); }, 'ratchet rejects an omitted identity');
|
|
1561
|
+
rejects(copy => { delete copy.records[0].path; }, 'ratchet rejects a missing report path');
|
|
1562
|
+
rejects(copy => { delete getFrom(copy, 'docs/reference/new.md').reason; }, 'ratchet rejects a missing disposition reason');
|
|
1563
|
+
rejects(copy => { const item = getFrom(copy, 'docs/reference/new.md'); delete item.lifecycle; delete item.disposition; delete item.validation; }, 'ratchet rejects collapsed axes');
|
|
1564
|
+
rejects(copy => { delete getFrom(copy, 'docs/reference/new.md').current.oid; }, 'ratchet rejects missing object identity');
|
|
1565
|
+
|
|
1566
|
+
const narrowed = { ...candidateManifest, documentGlobs: ['**/*.pdf'] };
|
|
1567
|
+
writeManifest(narrowed);
|
|
1568
|
+
gitAt(root, ['add', '--', manifestPath]);
|
|
1569
|
+
write('docs/reference/scope-gap.md', 'candidate path hidden by narrowed globs\n');
|
|
1570
|
+
gitAt(root, ['add', '--', 'docs/reference/scope-gap.md']);
|
|
1571
|
+
const narrowedReport = inventory();
|
|
1572
|
+
const narrowedOracle = inventoryOracle(root, base);
|
|
1573
|
+
const scopeDocs = new Set([...expectedDocs, Buffer.from('docs/reference/scope-gap.md').toString('base64')]);
|
|
1574
|
+
assert.ok(narrowedOracle.documentKeys.has(Buffer.from('docs/reference/scope-gap.md').toString('base64')));
|
|
1575
|
+
assert.strictEqual(narrowedReport.coverage.identifiedDocumentPaths, 0, 'narrowed globs cannot hide the full expected denominator');
|
|
1576
|
+
assert.strictEqual(narrowedReport.coverage.complete, true, 'the producer-only zero-document claim is deliberately insufficient');
|
|
1577
|
+
assert.throws(() => assertInventoryAgainstOracle(narrowedReport, narrowedOracle, scopeDocs), undefined, 'ratchet rejects a narrowed document glob and visible scope gap');
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
const symlinkProbe = path.join(tempRoot, 'manifest-ancestor-link-probe');
|
|
1581
|
+
try {
|
|
1582
|
+
fs.symlinkSync(tempRoot, symlinkProbe, process.platform === 'win32' ? 'junction' : 'dir');
|
|
1583
|
+
fs.rmSync(symlinkProbe, { recursive: true, force: true });
|
|
1584
|
+
checkCase('2ax. inventory rejects a manifest reached through a workspace symlink ancestor', () => {
|
|
1585
|
+
const root = workspace('manifest-ancestor-symlink');
|
|
1586
|
+
const manifestRelative = '.agents/harness-everything/document-inventory.json';
|
|
1587
|
+
const outside = path.join(tempRoot, 'outside-manifest-ancestor');
|
|
1588
|
+
const manifest = { schemaVersion: 1, documentGlobs: ['**/*.md'], rules: [{ match: '**/*.md', role: 'inside-control', lifecycle: 'current', disposition: 'applicable', reason: 'inside manifest' }] };
|
|
1589
|
+
const write = (target, content) => { fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, content); };
|
|
1590
|
+
write(path.join(root, manifestRelative), `${JSON.stringify(manifest)}\n`);
|
|
1591
|
+
write(path.join(root, 'docs', 'tracked.md'), 'tracked\n');
|
|
1592
|
+
const externalManifest = { ...manifest, rules: [{ ...manifest.rules[0], role: 'external-sentinel-secret', reason: 'must never be read' }] };
|
|
1593
|
+
write(path.join(outside, 'harness-everything', 'document-inventory.json'), `${JSON.stringify(externalManifest)}\n`);
|
|
1594
|
+
gitAt(root, ['init', '-q']);
|
|
1595
|
+
for (const [key, value] of [['user.name', 'Harness Test'], ['user.email', 'harness@example.invalid']]) gitAt(root, ['config', key, value]);
|
|
1596
|
+
gitAt(root, ['add', '-A']);
|
|
1597
|
+
gitAt(root, ['commit', '-qm', 'manifest symlink fixture']);
|
|
1598
|
+
const base = gitAt(root, ['rev-parse', 'HEAD']);
|
|
1599
|
+
const run = () => spawnSync(process.execPath, [modulePath, 'inventory', '--workspace', root, '--manifest', manifestRelative, '--base', base], { cwd: tempRoot, encoding: 'utf8' });
|
|
1600
|
+
const inside = run();
|
|
1601
|
+
assert.strictEqual(inside.status, 0, inside.stderr);
|
|
1602
|
+
assert.strictEqual(JSON.parse(inside.stdout).records.find(item => item.path === 'docs/tracked.md').role, 'inside-control');
|
|
1603
|
+
fs.rmSync(path.join(root, '.agents'), { recursive: true, force: true });
|
|
1604
|
+
fs.symlinkSync(outside, path.join(root, '.agents'), process.platform === 'win32' ? 'junction' : 'dir');
|
|
1605
|
+
const escaped = run();
|
|
1606
|
+
assert.strictEqual(escaped.status, 2, 'manifest reached through an ancestor symlink fails closed');
|
|
1607
|
+
assert.match(escaped.stderr, /symbolic link|symlink/i);
|
|
1608
|
+
assert.ok(!escaped.stdout.includes('external-sentinel-secret'), 'external manifest contents are not accepted');
|
|
1609
|
+
});
|
|
1610
|
+
} catch (error) {
|
|
1611
|
+
if (!['EPERM', 'EACCES', 'ENOTSUP'].includes(error.code)) throw error;
|
|
1612
|
+
console.log(`SKIP 2ax. manifest ancestor symlink guard: symlink creation unavailable (${error.code})`);
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
checkCase('2ax. frozen Git-object guard with bootstrap and tamper controls', () => {
|
|
1616
|
+
const trustedPaths = [
|
|
1617
|
+
'scripts/doc-governance-guard.js',
|
|
1618
|
+
'ci/mechanism-2ax-doc-lifecycle.test.js',
|
|
1619
|
+
'ci/mechanism-325-f1-doc-check.test.js',
|
|
1620
|
+
'harness-everything/scripts/doc-check.js',
|
|
1621
|
+
'harness-everything/scripts/doc-lifecycle.js',
|
|
1622
|
+
'harness-everything/scripts/project-docs-resolver.js',
|
|
1623
|
+
'plugins/harness-everything/skills/harness-everything/scripts/doc-check.js',
|
|
1624
|
+
'plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js',
|
|
1625
|
+
'plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js',
|
|
1626
|
+
'.agents/harness-everything/project-docs.json',
|
|
1627
|
+
'.agents/harness-everything/document-inventory.json',
|
|
1628
|
+
'.github/workflows/ci.yml',
|
|
1629
|
+
'package.json',
|
|
1630
|
+
'self-evolve/scripts/self-regression.js',
|
|
1631
|
+
'ci/mechanism-test.js',
|
|
1632
|
+
'ci/mutation-check.js',
|
|
1633
|
+
'ci/lib/isolated-test-process.js',
|
|
1634
|
+
'ci/test-helper.js',
|
|
1635
|
+
'scripts/verify-all.js'
|
|
1636
|
+
];
|
|
1637
|
+
const frozenGuard = fs.readFileSync(governanceGuardPath);
|
|
1638
|
+
const frozenTest = fs.readFileSync(__filename);
|
|
1639
|
+
const frozenGuardPath = path.join(tempRoot, 'frozen-doc-governance-guard.js');
|
|
1640
|
+
const frozenTestPath = path.join(tempRoot, 'frozen-2ax-test.js');
|
|
1641
|
+
fs.writeFileSync(frozenGuardPath, frozenGuard);
|
|
1642
|
+
fs.writeFileSync(frozenTestPath, frozenTest);
|
|
1643
|
+
assert.ok(fs.readFileSync(frozenGuardPath).equals(frozenGuard), 'trusted verifier bytes are frozen before fixture mutations');
|
|
1644
|
+
assert.ok(fs.readFileSync(frozenTestPath).equals(frozenTest), 'trusted test/oracle bytes are frozen before fixture mutations');
|
|
1645
|
+
|
|
1646
|
+
function fixtureGit(root, args, input) {
|
|
1647
|
+
const result = spawnSync('git', ['-C', root, ...args], {
|
|
1648
|
+
encoding: 'buffer',
|
|
1649
|
+
input,
|
|
1650
|
+
env: { ...process.env, GIT_CONFIG_NOSYSTEM: '1', GIT_NO_REPLACE_OBJECTS: '1' }
|
|
1651
|
+
});
|
|
1652
|
+
assert.ifError(result.error);
|
|
1653
|
+
assert.strictEqual(result.status, 0, `git ${args.join(' ')}: ${Buffer.from(result.stderr || '').toString('utf8')}`);
|
|
1654
|
+
return Buffer.from(result.stdout || '');
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function writeRelative(root, relativePath, bytes) {
|
|
1658
|
+
const target = path.join(root, ...relativePath.split('/'));
|
|
1659
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
1660
|
+
fs.writeFileSync(target, bytes);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
function commitFixture(root, message, executablePath) {
|
|
1664
|
+
fixtureGit(root, ['add', '-A']);
|
|
1665
|
+
if (executablePath) fixtureGit(root, ['update-index', '--chmod=+x', '--', executablePath]);
|
|
1666
|
+
fixtureGit(root, ['commit', '--quiet', '-m', message]);
|
|
1667
|
+
return fixtureGit(root, ['rev-parse', 'HEAD']).toString('ascii').trim();
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function createFixture() {
|
|
1671
|
+
const root = workspace('governance-guard-git');
|
|
1672
|
+
fs.mkdirSync(path.join(root, '.fixture-hooks'), { recursive: true });
|
|
1673
|
+
fixtureGit(root, ['init', '--quiet']);
|
|
1674
|
+
fixtureGit(root, ['config', 'user.name', 'Harness Guard Test']);
|
|
1675
|
+
fixtureGit(root, ['config', 'user.email', 'guard@example.invalid']);
|
|
1676
|
+
fixtureGit(root, ['config', 'core.hooksPath', path.join(root, '.fixture-hooks')]);
|
|
1677
|
+
for (const relativePath of trustedPaths) {
|
|
1678
|
+
const bytes = relativePath === 'scripts/doc-governance-guard.js' ? frozenGuard
|
|
1679
|
+
: relativePath === 'ci/mechanism-2ax-doc-lifecycle.test.js' ? frozenTest
|
|
1680
|
+
: fs.readFileSync(path.join(REPOSITORY_ROOT, ...relativePath.split('/')));
|
|
1681
|
+
writeRelative(root, relativePath, bytes);
|
|
1682
|
+
}
|
|
1683
|
+
const copyScenarioTree = (from, into) => {
|
|
1684
|
+
fs.mkdirSync(into, { recursive: true });
|
|
1685
|
+
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
1686
|
+
const source = path.join(from, entry.name);
|
|
1687
|
+
const target = path.join(into, entry.name);
|
|
1688
|
+
if (entry.isDirectory()) copyScenarioTree(source, target);
|
|
1689
|
+
else fs.copyFileSync(source, target);
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
copyScenarioTree(
|
|
1693
|
+
path.join(REPOSITORY_ROOT, 'ci/fixtures/doc-lifecycle/scenarios'),
|
|
1694
|
+
path.join(root, 'ci/fixtures/doc-lifecycle/scenarios')
|
|
1695
|
+
);
|
|
1696
|
+
const policy = JSON.parse(fs.readFileSync(path.join(REPOSITORY_ROOT, '.agents/harness-everything/project-docs.json'), 'utf8'));
|
|
1697
|
+
for (const record of [...policy.governed, ...policy.derived]) writeRelative(root, record.path, `baseline ${record.path}\n`);
|
|
1698
|
+
writeRelative(root, 'docs/ordinary.md', 'baseline ordinary document\n');
|
|
1699
|
+
return { root, base: commitFixture(root, 'fixture baseline') };
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function resetFixture(fixture) {
|
|
1703
|
+
fixtureGit(fixture.root, ['reset', '--hard', fixture.base]);
|
|
1704
|
+
fixtureGit(fixture.root, ['clean', '-fd']);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
function runFrozenGuard(fixture, base, candidate, extraArgs = []) {
|
|
1708
|
+
const args = [frozenGuardPath, '--repo', fixture.root, '--base', base, ...extraArgs];
|
|
1709
|
+
if (candidate) args.push('--candidate', candidate);
|
|
1710
|
+
const result = spawnSync(process.execPath, args, { cwd: tempRoot, encoding: 'utf8', timeout: 20000 });
|
|
1711
|
+
assert.ifError(result.error);
|
|
1712
|
+
assert.ok(result.stdout, result.stderr || 'guard returned no JSON');
|
|
1713
|
+
return { status: result.status, report: JSON.parse(result.stdout), stderr: result.stderr };
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
const fixture = createFixture();
|
|
1717
|
+
const ordinaryPath = path.join(fixture.root, 'docs', 'ordinary.md');
|
|
1718
|
+
fs.writeFileSync(ordinaryPath, 'ordinary document edit\n');
|
|
1719
|
+
// Git for Windows rejects LF paths even when they are added directly to the index.
|
|
1720
|
+
const newlinePath = 'docs/new\nunknown.md';
|
|
1721
|
+
const linkOid = fixtureGit(fixture.root, ['hash-object', '-w', '--stdin'], Buffer.from('../outside-target.md\n')).toString('ascii').trim();
|
|
1722
|
+
fixtureGit(fixture.root, ['add', '-A']);
|
|
1723
|
+
if (process.platform !== 'win32') {
|
|
1724
|
+
const newlineOid = fixtureGit(fixture.root, ['hash-object', '-w', '--stdin'], Buffer.from('new document\n')).toString('ascii').trim();
|
|
1725
|
+
fixtureGit(fixture.root, ['update-index', '--add', '--cacheinfo', `100644,${newlineOid},${newlinePath}`]);
|
|
1726
|
+
}
|
|
1727
|
+
fixtureGit(fixture.root, ['update-index', '--add', '--cacheinfo', `120000,${linkOid},docs/link.md`]);
|
|
1728
|
+
fixtureGit(fixture.root, ['update-index', '--add', '--cacheinfo', `160000,${fixture.base},docs/submodule.md`]);
|
|
1729
|
+
fixtureGit(fixture.root, ['commit', '--quiet', '-m', 'ordinary document edits']);
|
|
1730
|
+
const positiveCandidate = fixtureGit(fixture.root, ['rev-parse', 'HEAD']).toString('ascii').trim();
|
|
1731
|
+
const positive = runFrozenGuard(fixture, fixture.base, positiveCandidate);
|
|
1732
|
+
assert.strictEqual(positive.status, 0, positive.stderr);
|
|
1733
|
+
assert.strictEqual(positive.report.verdict, 'PASS', 'ordinary document edits remain permitted');
|
|
1734
|
+
assert.strictEqual(positive.report.inputs.base.commit, fixture.base, 'base SHA is exact in JSON');
|
|
1735
|
+
assert.strictEqual(positive.report.inputs.candidate.commit, positiveCandidate, 'candidate SHA is exact in JSON');
|
|
1736
|
+
assert.strictEqual(positive.report.verifier.sha256, crypto.createHash('sha256').update(frozenGuard).digest('hex'));
|
|
1737
|
+
assert.strictEqual(positive.report.verifier.matchesBaseObject, true, 'running verifier bytes match the baseline guard blob');
|
|
1738
|
+
assert.deepStrictEqual(positive.report.trustSurface.paths.slice(0, trustedPaths.length).map(item => item.path), trustedPaths, 'fixed trust surface is explicit');
|
|
1739
|
+
const scenarioExpectedPath = 'ci/fixtures/doc-lifecycle/scenarios/active-to-active/expected.json';
|
|
1740
|
+
const scenarioOracle = positive.report.trustSurface.paths.find(item => item.path === scenarioExpectedPath);
|
|
1741
|
+
assert.ok(scenarioOracle, 'tracked scenario oracle is part of the baseline trust surface');
|
|
1742
|
+
assert.deepStrictEqual(scenarioOracle.base, scenarioOracle.candidate, 'unchanged oracle identity is a positive control');
|
|
1743
|
+
const newDocs = new Map(positive.report.newDocuments.map(item => [item.path, item]));
|
|
1744
|
+
const specialDocumentPaths = [...(process.platform === 'win32' ? [] : [newlinePath]), 'docs/link.md', 'docs/submodule.md'];
|
|
1745
|
+
for (const relativePath of specialDocumentPaths) {
|
|
1746
|
+
const record = newDocs.get(relativePath);
|
|
1747
|
+
assert.ok(record, `new document is inventoried even with a special path or Git mode: ${relativePath}`);
|
|
1748
|
+
assert.strictEqual(record.role, 'unknown');
|
|
1749
|
+
assert.strictEqual(record.disposition, 'unknown');
|
|
1750
|
+
assert.strictEqual(record.validation, 'unchecked');
|
|
1751
|
+
}
|
|
1752
|
+
assert.strictEqual(newDocs.get('docs/link.md').mode, '120000', 'symlink mode is retained without following the target');
|
|
1753
|
+
assert.strictEqual(newDocs.get('docs/submodule.md').mode, '160000', 'gitlink mode is retained without recursing');
|
|
1754
|
+
assert.strictEqual(newDocs.get('docs/submodule.md').type, 'commit');
|
|
1755
|
+
|
|
1756
|
+
const scenarioExpectedFile = path.join(fixture.root, ...scenarioExpectedPath.split('/'));
|
|
1757
|
+
for (const mutation of ['content', 'delete', 'mode', 'add']) {
|
|
1758
|
+
resetFixture(fixture);
|
|
1759
|
+
if (mutation === 'content') {
|
|
1760
|
+
const expected = JSON.parse(fs.readFileSync(scenarioExpectedFile, 'utf8'));
|
|
1761
|
+
delete expected.resolve['docs/adr/GRPH-002.md'];
|
|
1762
|
+
fs.writeFileSync(scenarioExpectedFile, `${JSON.stringify(expected, null, 2)}\n`);
|
|
1763
|
+
} else if (mutation === 'delete') {
|
|
1764
|
+
fixtureGit(fixture.root, ['rm', '--quiet', '--', scenarioExpectedPath]);
|
|
1765
|
+
} else if (mutation === 'add') {
|
|
1766
|
+
writeRelative(fixture.root, 'ci/fixtures/doc-lifecycle/scenarios/new/expected.json', '{}\n');
|
|
1767
|
+
}
|
|
1768
|
+
const candidate = commitFixture(fixture.root, `mutate scenario oracle ${mutation}`, mutation === 'mode' ? scenarioExpectedPath : undefined);
|
|
1769
|
+
const result = runFrozenGuard(fixture, fixture.base, candidate);
|
|
1770
|
+
assert.strictEqual(result.status, 2, `scenario fixture ${mutation} requires review: ${result.stderr}`);
|
|
1771
|
+
assert.strictEqual(result.report.verdict, 'REVIEW_REQUIRED');
|
|
1772
|
+
const item = result.report.trustSurface.changed.find(entry => entry.path === (mutation === 'add'
|
|
1773
|
+
? 'ci/fixtures/doc-lifecycle/scenarios/new/expected.json'
|
|
1774
|
+
: scenarioExpectedPath));
|
|
1775
|
+
assert.ok(item, `scenario fixture ${mutation} identity change is reported`);
|
|
1776
|
+
if (mutation === 'delete') assert.strictEqual(item.candidate, null, 'deleted oracle retains its baseline identity');
|
|
1777
|
+
if (mutation === 'add') assert.strictEqual(item.base, null, 'new oracle has no baseline identity');
|
|
1778
|
+
if (mutation === 'content') assert.notStrictEqual(item.base.oid, item.candidate.oid, 'oracle content change is reported by blob OID');
|
|
1779
|
+
if (mutation === 'mode') assert.notStrictEqual(item.base.mode, item.candidate.mode, 'oracle mode change is reported');
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
const unchanged = runFrozenGuard(fixture, fixture.base, fixture.base);
|
|
1783
|
+
assert.strictEqual(unchanged.status, 0, 'an unchanged tree passes');
|
|
1784
|
+
assert.strictEqual(unchanged.report.verdict, 'PASS');
|
|
1785
|
+
|
|
1786
|
+
const rawIdentity = entry => ({ mode: entry.mode, type: entry.type, oid: entry.oid });
|
|
1787
|
+
const configuredIdentity = (commit, relativePath) => {
|
|
1788
|
+
const tree = parseTreeOracle(fixtureGit(fixture.root, ['ls-tree', '-r', '-z', '--full-tree', commit]));
|
|
1789
|
+
return rawIdentity(tree.get(Buffer.from(relativePath).toString('base64')));
|
|
1790
|
+
};
|
|
1791
|
+
resetFixture(fixture);
|
|
1792
|
+
writeRelative(fixture.root, 'docs/architecture.md', 'changed governed document content\n');
|
|
1793
|
+
const revisedGovernedCandidate = commitFixture(fixture.root, 'revise governed document');
|
|
1794
|
+
const revisedGoverned = runFrozenGuard(fixture, fixture.base, revisedGovernedCandidate);
|
|
1795
|
+
const fixtureStatusBeforeRepeat = fixtureGit(fixture.root, ['status', '--porcelain']);
|
|
1796
|
+
const revisedGovernedRepeat = runFrozenGuard(fixture, fixture.base, revisedGovernedCandidate);
|
|
1797
|
+
const fixtureStatusAfterRepeat = fixtureGit(fixture.root, ['status', '--porcelain']);
|
|
1798
|
+
assert.deepStrictEqual(revisedGovernedRepeat, revisedGoverned, 'identical Git inputs produce an identical report');
|
|
1799
|
+
assert.deepStrictEqual(fixtureStatusAfterRepeat, fixtureStatusBeforeRepeat, 'guard execution does not mutate the fixture repository');
|
|
1800
|
+
assert.strictEqual(revisedGoverned.status, 0, 'regular governed content revision passes');
|
|
1801
|
+
assert.strictEqual(revisedGoverned.report.verdict, 'PASS');
|
|
1802
|
+
const revisedEntry = revisedGoverned.report.policyPreservation.configuredPaths.paths.find(item => item.path === 'docs/architecture.md');
|
|
1803
|
+
assert.strictEqual(revisedEntry.state, 'revised', 'governed content revision is informational');
|
|
1804
|
+
assert.deepStrictEqual(revisedEntry.base, configuredIdentity(fixture.base, 'docs/architecture.md'));
|
|
1805
|
+
assert.deepStrictEqual(revisedEntry.candidate, configuredIdentity(revisedGovernedCandidate, 'docs/architecture.md'));
|
|
1806
|
+
assert.match(revisedEntry.reason, /not semantically validated by the guard/i);
|
|
1807
|
+
assert.ok(revisedGoverned.report.limits.some(limit => /content revisions.*not semantically validated by the guard/i.test(limit)));
|
|
1808
|
+
assert.strictEqual(revisedGoverned.report.policyPreservation.configuredPaths.state, 'pass');
|
|
1809
|
+
assert.deepStrictEqual(revisedGoverned.report.policyPreservation.configuredPaths.missingOrChanged, []);
|
|
1810
|
+
|
|
1811
|
+
resetFixture(fixture);
|
|
1812
|
+
writeRelative(fixture.root, 'docs/governed-index.md', 'changed derived output\n');
|
|
1813
|
+
const derivedCandidate = commitFixture(fixture.root, 'change derived document');
|
|
1814
|
+
const derivedChange = runFrozenGuard(fixture, fixture.base, derivedCandidate);
|
|
1815
|
+
assert.strictEqual(derivedChange.status, 2, 'derived content change still requires review');
|
|
1816
|
+
assert.strictEqual(derivedChange.report.verdict, 'REVIEW_REQUIRED');
|
|
1817
|
+
const derivedEntry = derivedChange.report.policyPreservation.configuredPaths.missingOrChanged.find(item => item.path === 'docs/governed-index.md');
|
|
1818
|
+
assert.ok(derivedEntry, 'derived content change remains in review-required paths');
|
|
1819
|
+
assert.strictEqual(derivedEntry.state, 'review_required');
|
|
1820
|
+
assert.deepStrictEqual(derivedEntry.base, configuredIdentity(fixture.base, 'docs/governed-index.md'));
|
|
1821
|
+
assert.deepStrictEqual(derivedEntry.candidate, configuredIdentity(derivedCandidate, 'docs/governed-index.md'));
|
|
1822
|
+
|
|
1823
|
+
for (const mutation of ['rename', 'mode', 'symlink']) {
|
|
1824
|
+
resetFixture(fixture);
|
|
1825
|
+
const relativePath = 'docs/architecture.md';
|
|
1826
|
+
if (mutation === 'rename') {
|
|
1827
|
+
fixtureGit(fixture.root, ['mv', '--', relativePath, 'docs/renamed-architecture.md']);
|
|
1828
|
+
} else if (mutation === 'symlink') {
|
|
1829
|
+
fs.rmSync(path.join(fixture.root, ...relativePath.split('/')));
|
|
1830
|
+
const oid = fixtureGit(fixture.root, ['hash-object', '-w', '--stdin'], Buffer.from('../outside-target.md\n')).toString('ascii').trim();
|
|
1831
|
+
fixtureGit(fixture.root, ['add', '-A']);
|
|
1832
|
+
fixtureGit(fixture.root, ['update-index', '--add', '--cacheinfo', `120000,${oid},${relativePath}`]);
|
|
1833
|
+
fixtureGit(fixture.root, ['commit', '--quiet', '-m', 'replace governed document with symlink']);
|
|
1834
|
+
}
|
|
1835
|
+
const candidate = mutation === 'symlink'
|
|
1836
|
+
? fixtureGit(fixture.root, ['rev-parse', 'HEAD']).toString('ascii').trim()
|
|
1837
|
+
: commitFixture(fixture.root, `${mutation} governed document`, mutation === 'mode' ? relativePath : undefined);
|
|
1838
|
+
const result = runFrozenGuard(fixture, fixture.base, candidate);
|
|
1839
|
+
assert.strictEqual(result.status, 2, `${mutation} of a governed entry requires review`);
|
|
1840
|
+
assert.strictEqual(result.report.verdict, 'REVIEW_REQUIRED');
|
|
1841
|
+
const item = result.report.policyPreservation.configuredPaths.missingOrChanged.find(entry => entry.path === relativePath);
|
|
1842
|
+
assert.ok(item, `${mutation} is reported for the original configured path`);
|
|
1843
|
+
assert.strictEqual(item.state, 'review_required');
|
|
1844
|
+
assert.deepStrictEqual(item.base, configuredIdentity(fixture.base, relativePath));
|
|
1845
|
+
if (mutation === 'rename') assert.strictEqual(item.candidate, null);
|
|
1846
|
+
if (mutation === 'mode') assert.notStrictEqual(item.base.mode, item.candidate.mode);
|
|
1847
|
+
if (mutation === 'symlink') assert.strictEqual(item.candidate.mode, '120000');
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
resetFixture(fixture);
|
|
1851
|
+
fixtureGit(fixture.root, ['rm', '--quiet', '--', 'docs/architecture.md']);
|
|
1852
|
+
const deletedGovernedCandidate = commitFixture(fixture.root, 'delete configured document');
|
|
1853
|
+
const deletedGoverned = runFrozenGuard(fixture, fixture.base, deletedGovernedCandidate);
|
|
1854
|
+
assert.strictEqual(deletedGoverned.status, 2);
|
|
1855
|
+
assert.strictEqual(deletedGoverned.report.verdict, 'REVIEW_REQUIRED');
|
|
1856
|
+
assert.ok(deletedGoverned.report.policyPreservation.configuredPaths.missingOrChanged.some(item => item.path === 'docs/architecture.md'));
|
|
1857
|
+
|
|
1858
|
+
resetFixture(fixture);
|
|
1859
|
+
const registeredPolicy = JSON.parse(fs.readFileSync(path.join(fixture.root, '.agents/harness-everything/project-docs.json'), 'utf8'));
|
|
1860
|
+
registeredPolicy.governed.push({ path: 'docs/new-governed.md', role: 'reference' });
|
|
1861
|
+
writeRelative(fixture.root, '.agents/harness-everything/project-docs.json', `${JSON.stringify(registeredPolicy, null, 2)}\n`);
|
|
1862
|
+
writeRelative(fixture.root, 'docs/new-governed.md', 'newly registered governed member\n');
|
|
1863
|
+
const registrationCandidate = commitFixture(fixture.root, 'register governed document');
|
|
1864
|
+
const registration = runFrozenGuard(fixture, fixture.base, registrationCandidate);
|
|
1865
|
+
assert.strictEqual(registration.status, 2, 'new-member registration requires review');
|
|
1866
|
+
assert.strictEqual(registration.report.verdict, 'REVIEW_REQUIRED');
|
|
1867
|
+
assert.ok(registration.report.policyPreservation.governed.added.some(item => item.path === 'docs/new-governed.md'));
|
|
1868
|
+
|
|
1869
|
+
for (const relativePath of trustedPaths) {
|
|
1870
|
+
resetFixture(fixture);
|
|
1871
|
+
if (relativePath === '.agents/harness-everything/project-docs.json') {
|
|
1872
|
+
const configPath = path.join(fixture.root, ...relativePath.split('/'));
|
|
1873
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
1874
|
+
config.governed.pop();
|
|
1875
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
1876
|
+
} else if (relativePath === '.agents/harness-everything/document-inventory.json') {
|
|
1877
|
+
const configPath = path.join(fixture.root, ...relativePath.split('/'));
|
|
1878
|
+
fs.appendFileSync(configPath, '\n');
|
|
1879
|
+
} else if (relativePath === 'scripts/doc-governance-guard.js') {
|
|
1880
|
+
writeRelative(fixture.root, relativePath, 'process.exit(0);\n');
|
|
1881
|
+
} else if (relativePath === 'ci/mechanism-2ax-doc-lifecycle.test.js' || relativePath === '.github/workflows/ci.yml') {
|
|
1882
|
+
fixtureGit(fixture.root, ['rm', '--quiet', '--', relativePath]);
|
|
1883
|
+
} else {
|
|
1884
|
+
const filePath = path.join(fixture.root, ...relativePath.split('/'));
|
|
1885
|
+
fs.appendFileSync(filePath, '\n// candidate mutation\n');
|
|
1886
|
+
}
|
|
1887
|
+
if (relativePath === 'scripts/doc-governance-guard.js') {
|
|
1888
|
+
writeRelative(fixture.root, '.harness-review-approval.json', '{"approved":true,"candidate":"self"}\n');
|
|
1889
|
+
}
|
|
1890
|
+
const candidate = commitFixture(fixture.root, `mutate ${relativePath}`);
|
|
1891
|
+
const result = runFrozenGuard(fixture, fixture.base, candidate);
|
|
1892
|
+
assert.strictEqual(result.status, 2, `${relativePath} mutation must require review: ${result.stderr}`);
|
|
1893
|
+
assert.strictEqual(result.report.verdict, 'REVIEW_REQUIRED', `${relativePath} cannot self-approve`);
|
|
1894
|
+
assert.ok(result.report.trustSurface.changed.some(item => item.path === relativePath), `${relativePath} identity change is reported`);
|
|
1895
|
+
assert.ok(!Object.prototype.hasOwnProperty.call(result.report, 'approval'), 'candidate approval material is not an input');
|
|
1896
|
+
if (relativePath === '.agents/harness-everything/project-docs.json') {
|
|
1897
|
+
assert.ok(result.report.policyPreservation.governed.missingOrChanged.length, 'removed baseline governed record remains visible');
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
for (const relativePath of ['.agents/harness-everything/project-docs.json', '.agents/harness-everything/document-inventory.json']) {
|
|
1902
|
+
resetFixture(fixture);
|
|
1903
|
+
writeRelative(fixture.root, relativePath, '{malformed\n');
|
|
1904
|
+
const malformedCandidate = commitFixture(fixture.root, `malformed candidate ${relativePath}`);
|
|
1905
|
+
const malformed = runFrozenGuard(fixture, fixture.base, malformedCandidate);
|
|
1906
|
+
assert.strictEqual(malformed.status, 1);
|
|
1907
|
+
assert.strictEqual(malformed.report.verdict, 'FAIL');
|
|
1908
|
+
assert.match(malformed.report.errors[0].message, /malformed JSON/i);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
resetFixture(fixture);
|
|
1912
|
+
fs.writeFileSync(path.join(fixture.root, '.agents/harness-everything/project-docs.json'), '{malformed\n');
|
|
1913
|
+
const malformedBase = commitFixture(fixture.root, 'malformed baseline policy');
|
|
1914
|
+
fs.writeFileSync(ordinaryPath, 'candidate follows malformed baseline\n');
|
|
1915
|
+
const afterMalformedBase = commitFixture(fixture.root, 'candidate after malformed baseline');
|
|
1916
|
+
const malformedBaseline = runFrozenGuard(fixture, malformedBase, afterMalformedBase);
|
|
1917
|
+
assert.strictEqual(malformedBaseline.status, 1);
|
|
1918
|
+
assert.strictEqual(malformedBaseline.report.verdict, 'FAIL');
|
|
1919
|
+
|
|
1920
|
+
resetFixture(fixture);
|
|
1921
|
+
fixtureGit(fixture.root, ['rm', '--quiet', '--', 'scripts/doc-governance-guard.js']);
|
|
1922
|
+
const noGuardBase = commitFixture(fixture.root, 'baseline predates verifier');
|
|
1923
|
+
fs.writeFileSync(ordinaryPath, 'candidate without baseline verifier\n');
|
|
1924
|
+
const noGuardCandidate = commitFixture(fixture.root, 'candidate before verifier bootstrap');
|
|
1925
|
+
const bootstrap = runFrozenGuard(fixture, noGuardBase, noGuardCandidate);
|
|
1926
|
+
assert.strictEqual(bootstrap.status, 3);
|
|
1927
|
+
assert.strictEqual(bootstrap.report.verdict, 'BOOTSTRAP_UNCHECKED');
|
|
1928
|
+
assert.strictEqual(bootstrap.report.verifier.matchesBaseObject, false);
|
|
1929
|
+
|
|
1930
|
+
const missingCandidate = runFrozenGuard(fixture, fixture.base, null);
|
|
1931
|
+
assert.strictEqual(missingCandidate.status, 1);
|
|
1932
|
+
assert.strictEqual(missingCandidate.report.verdict, 'FAIL');
|
|
1933
|
+
const missingCommit = runFrozenGuard(fixture, fixture.base, '0'.repeat(40));
|
|
1934
|
+
assert.notStrictEqual(missingCommit.status, 0);
|
|
1935
|
+
assert.notStrictEqual(missingCommit.report.verdict, 'PASS');
|
|
1936
|
+
|
|
1937
|
+
checkCase('2ax. CI workflow keeps the base-owned guard in a failing standalone job', () => {
|
|
1938
|
+
const workflowPath = path.join(REPOSITORY_ROOT, '.github/workflows/ci.yml');
|
|
1939
|
+
const workflow = require('js-yaml').load(fs.readFileSync(workflowPath, 'utf8'));
|
|
1940
|
+
const job = workflow.jobs['doc-governance'];
|
|
1941
|
+
assert.ok(job, 'standalone doc-governance job exists');
|
|
1942
|
+
assert.strictEqual(job['runs-on'], 'ubuntu-latest');
|
|
1943
|
+
assert.notStrictEqual(job['continue-on-error'], true, 'the job does not mask guard failures');
|
|
1944
|
+
assert.ok(job.steps.some(step => step.uses === 'actions/checkout@v7' && step.with['fetch-depth'] === 0), 'job fetches base Git objects');
|
|
1945
|
+
assert.ok(job.steps.some(step => step.uses === 'actions/setup-node@v7' && step.with['node-version'] === 24), 'job provides Node 24');
|
|
1946
|
+
const guardStep = job.steps.find(step => step.name === 'Run base-owned document governance guard');
|
|
1947
|
+
assert.ok(guardStep, 'standalone job owns the guard step');
|
|
1948
|
+
assert.notStrictEqual(guardStep['continue-on-error'], true, 'the guard step propagates nonzero exits');
|
|
1949
|
+
assert.ok(!workflow.jobs['self-regression'].steps.some(step => step.name === guardStep.name), 'self-regression no longer runs the guard');
|
|
1950
|
+
const command = String(guardStep.run || '').trimEnd();
|
|
1951
|
+
const inline = command.match(/^node -e '([\s\S]*)'$/);
|
|
1952
|
+
assert.ok(inline, 'guard step remains a directly executable Node script');
|
|
1953
|
+
|
|
1954
|
+
function simulate(event, base, candidate) {
|
|
1955
|
+
const summaryPath = path.join(tempRoot, `guard-summary-${process.pid}-${Math.random().toString(16).slice(2)}.md`);
|
|
1956
|
+
const result = spawnSync(process.execPath, ['-e', inline[1]], {
|
|
1957
|
+
cwd: fixture.root,
|
|
1958
|
+
encoding: 'utf8',
|
|
1959
|
+
timeout: 20000,
|
|
1960
|
+
env: {
|
|
1961
|
+
...process.env,
|
|
1962
|
+
HARNESS_GUARD_EVENT: event,
|
|
1963
|
+
HARNESS_GUARD_BASE_SHA: base || '',
|
|
1964
|
+
HARNESS_GUARD_CANDIDATE_SHA: candidate || '',
|
|
1965
|
+
GITHUB_SHA: 'checkout-control',
|
|
1966
|
+
GITHUB_STEP_SUMMARY: summaryPath
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
const summary = fs.existsSync(summaryPath) ? fs.readFileSync(summaryPath, 'utf8') : '';
|
|
1970
|
+
fs.rmSync(summaryPath, { force: true });
|
|
1971
|
+
assert.ifError(result.error);
|
|
1972
|
+
return { status: result.status, output: result.stdout, summary };
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
const uncheckedEvent = simulate('push');
|
|
1976
|
+
assert.strictEqual(uncheckedEvent.status, 0);
|
|
1977
|
+
assert.match(uncheckedEvent.output, /UNCHECKED/);
|
|
1978
|
+
const bootstrapEvent = simulate('pull_request', noGuardBase, noGuardCandidate);
|
|
1979
|
+
assert.strictEqual(bootstrapEvent.status, 0, 'missing base verifier remains bootstrap-warning only');
|
|
1980
|
+
assert.match(bootstrapEvent.output, /BOOTSTRAP_UNCHECKED/);
|
|
1981
|
+
assert.match(bootstrapEvent.summary, /BOOTSTRAP_UNCHECKED/);
|
|
1982
|
+
const passEvent = simulate('pull_request', fixture.base, fixture.base);
|
|
1983
|
+
assert.strictEqual(passEvent.status, 0);
|
|
1984
|
+
assert.match(passEvent.output, /"verdict": "PASS"/);
|
|
1985
|
+
|
|
1986
|
+
resetFixture(fixture);
|
|
1987
|
+
writeRelative(fixture.root, 'docs/governed-index.md', 'changed derived output for CI simulation\n');
|
|
1988
|
+
const reviewCandidate = commitFixture(fixture.root, 'simulate guard review failure');
|
|
1989
|
+
const reviewEvent = simulate('pull_request', fixture.base, reviewCandidate);
|
|
1990
|
+
assert.strictEqual(reviewEvent.status, 2, 'REVIEW_REQUIRED stays a failing workflow job');
|
|
1991
|
+
assert.match(reviewEvent.output, /"verdict": "REVIEW_REQUIRED"/);
|
|
1992
|
+
|
|
1993
|
+
for (const exitCode of [1, 3]) {
|
|
1994
|
+
resetFixture(fixture);
|
|
1995
|
+
writeRelative(fixture.root, 'scripts/doc-governance-guard.js', `process.exit(${exitCode});\n`);
|
|
1996
|
+
const base = commitFixture(fixture.root, `simulate base guard exit ${exitCode}`);
|
|
1997
|
+
const result = simulate('pull_request', base, base);
|
|
1998
|
+
assert.strictEqual(result.status, exitCode, `the standalone job propagates guard exit ${exitCode}`);
|
|
1999
|
+
}
|
|
2000
|
+
});
|
|
2001
|
+
});
|
|
2002
|
+
|
|
1061
2003
|
helper.finish();
|