harness-everything 0.42.2 → 0.43.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.
@@ -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,604 @@ 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
+ for (const relativePath of ['docs/architecture.md', 'docs/governed-index.md']) {
1783
+ resetFixture(fixture);
1784
+ writeRelative(fixture.root, relativePath, `changed configured document ${relativePath}\n`);
1785
+ const candidate = commitFixture(fixture.root, `edit configured document ${relativePath}`);
1786
+ const result = runFrozenGuard(fixture, fixture.base, candidate);
1787
+ assert.strictEqual(result.status, 2, `${relativePath} content change requires review: ${result.stderr}`);
1788
+ assert.strictEqual(result.report.verdict, 'REVIEW_REQUIRED');
1789
+ const item = result.report.policyPreservation.configuredPaths.missingOrChanged.find(entry => entry.path === relativePath);
1790
+ assert.ok(item, `${relativePath} identity change is reported`);
1791
+ const baseTree = parseTreeOracle(fixtureGit(fixture.root, ['ls-tree', '-r', '-z', '--full-tree', fixture.base]));
1792
+ const candidateTree = parseTreeOracle(fixtureGit(fixture.root, ['ls-tree', '-r', '-z', '--full-tree', candidate]));
1793
+ const key = Buffer.from(relativePath).toString('base64');
1794
+ const rawIdentity = entry => ({ mode: entry.mode, type: entry.type, oid: entry.oid });
1795
+ assert.deepStrictEqual(item.base, rawIdentity(baseTree.get(key)), 'report includes exact baseline Git identity');
1796
+ assert.deepStrictEqual(item.candidate, rawIdentity(candidateTree.get(key)), 'report includes exact candidate Git identity');
1797
+ assert.notStrictEqual(item.base.oid, item.candidate.oid, 'content change is identified by blob OID');
1798
+ }
1799
+
1800
+ resetFixture(fixture);
1801
+ fixtureGit(fixture.root, ['rm', '--quiet', '--', 'docs/architecture.md']);
1802
+ const deletedGovernedCandidate = commitFixture(fixture.root, 'delete configured document');
1803
+ const deletedGoverned = runFrozenGuard(fixture, fixture.base, deletedGovernedCandidate);
1804
+ assert.strictEqual(deletedGoverned.status, 2);
1805
+ assert.strictEqual(deletedGoverned.report.verdict, 'REVIEW_REQUIRED');
1806
+ assert.ok(deletedGoverned.report.policyPreservation.configuredPaths.missingOrChanged.some(item => item.path === 'docs/architecture.md'));
1807
+
1808
+ for (const relativePath of trustedPaths) {
1809
+ resetFixture(fixture);
1810
+ if (relativePath === '.agents/harness-everything/project-docs.json') {
1811
+ const configPath = path.join(fixture.root, ...relativePath.split('/'));
1812
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
1813
+ config.governed.pop();
1814
+ fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
1815
+ } else if (relativePath === '.agents/harness-everything/document-inventory.json') {
1816
+ const configPath = path.join(fixture.root, ...relativePath.split('/'));
1817
+ fs.appendFileSync(configPath, '\n');
1818
+ } else if (relativePath === 'scripts/doc-governance-guard.js') {
1819
+ writeRelative(fixture.root, relativePath, 'process.exit(0);\n');
1820
+ } else if (relativePath === 'ci/mechanism-2ax-doc-lifecycle.test.js' || relativePath === '.github/workflows/ci.yml') {
1821
+ fixtureGit(fixture.root, ['rm', '--quiet', '--', relativePath]);
1822
+ } else {
1823
+ const filePath = path.join(fixture.root, ...relativePath.split('/'));
1824
+ fs.appendFileSync(filePath, '\n// candidate mutation\n');
1825
+ }
1826
+ if (relativePath === 'scripts/doc-governance-guard.js') {
1827
+ writeRelative(fixture.root, '.harness-review-approval.json', '{"approved":true,"candidate":"self"}\n');
1828
+ }
1829
+ const candidate = commitFixture(fixture.root, `mutate ${relativePath}`);
1830
+ const result = runFrozenGuard(fixture, fixture.base, candidate);
1831
+ assert.strictEqual(result.status, 2, `${relativePath} mutation must require review: ${result.stderr}`);
1832
+ assert.strictEqual(result.report.verdict, 'REVIEW_REQUIRED', `${relativePath} cannot self-approve`);
1833
+ assert.ok(result.report.trustSurface.changed.some(item => item.path === relativePath), `${relativePath} identity change is reported`);
1834
+ assert.ok(!Object.prototype.hasOwnProperty.call(result.report, 'approval'), 'candidate approval material is not an input');
1835
+ if (relativePath === '.agents/harness-everything/project-docs.json') {
1836
+ assert.ok(result.report.policyPreservation.governed.missingOrChanged.length, 'removed baseline governed record remains visible');
1837
+ }
1838
+ }
1839
+
1840
+ for (const relativePath of ['.agents/harness-everything/project-docs.json', '.agents/harness-everything/document-inventory.json']) {
1841
+ resetFixture(fixture);
1842
+ writeRelative(fixture.root, relativePath, '{malformed\n');
1843
+ const malformedCandidate = commitFixture(fixture.root, `malformed candidate ${relativePath}`);
1844
+ const malformed = runFrozenGuard(fixture, fixture.base, malformedCandidate);
1845
+ assert.strictEqual(malformed.status, 1);
1846
+ assert.strictEqual(malformed.report.verdict, 'FAIL');
1847
+ assert.match(malformed.report.errors[0].message, /malformed JSON/i);
1848
+ }
1849
+
1850
+ resetFixture(fixture);
1851
+ fs.writeFileSync(path.join(fixture.root, '.agents/harness-everything/project-docs.json'), '{malformed\n');
1852
+ const malformedBase = commitFixture(fixture.root, 'malformed baseline policy');
1853
+ fs.writeFileSync(ordinaryPath, 'candidate follows malformed baseline\n');
1854
+ const afterMalformedBase = commitFixture(fixture.root, 'candidate after malformed baseline');
1855
+ const malformedBaseline = runFrozenGuard(fixture, malformedBase, afterMalformedBase);
1856
+ assert.strictEqual(malformedBaseline.status, 1);
1857
+ assert.strictEqual(malformedBaseline.report.verdict, 'FAIL');
1858
+
1859
+ resetFixture(fixture);
1860
+ fixtureGit(fixture.root, ['rm', '--quiet', '--', 'scripts/doc-governance-guard.js']);
1861
+ const noGuardBase = commitFixture(fixture.root, 'baseline predates verifier');
1862
+ fs.writeFileSync(ordinaryPath, 'candidate without baseline verifier\n');
1863
+ const noGuardCandidate = commitFixture(fixture.root, 'candidate before verifier bootstrap');
1864
+ const bootstrap = runFrozenGuard(fixture, noGuardBase, noGuardCandidate);
1865
+ assert.strictEqual(bootstrap.status, 3);
1866
+ assert.strictEqual(bootstrap.report.verdict, 'BOOTSTRAP_UNCHECKED');
1867
+ assert.strictEqual(bootstrap.report.verifier.matchesBaseObject, false);
1868
+
1869
+ const missingCandidate = runFrozenGuard(fixture, fixture.base, null);
1870
+ assert.strictEqual(missingCandidate.status, 1);
1871
+ assert.strictEqual(missingCandidate.report.verdict, 'FAIL');
1872
+ const missingCommit = runFrozenGuard(fixture, fixture.base, '0'.repeat(40));
1873
+ assert.notStrictEqual(missingCommit.status, 0);
1874
+ assert.notStrictEqual(missingCommit.report.verdict, 'PASS');
1875
+ });
1876
+
1061
1877
  helper.finish();