agentic-workflow-manager 9.22.0 → 9.22.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.
Files changed (53) hide show
  1. package/dist/src/commands/change/index.js +2 -2
  2. package/dist/src/commands/init.js +8 -4
  3. package/dist/src/commands/registry/add.js +9 -3
  4. package/dist/src/commands/sync.js +11 -5
  5. package/dist/src/commands/update.js +23 -12
  6. package/dist/src/core/changes/derive.js +6 -3
  7. package/dist/src/core/changes/git.js +1 -2
  8. package/dist/src/core/changes/worktree.js +15 -3
  9. package/dist/src/core/init/steps.js +3 -8
  10. package/dist/src/core/registries.js +20 -15
  11. package/dist/src/core/registry-git-errors.js +107 -0
  12. package/dist/src/core/registry-git.js +93 -0
  13. package/dist/src/core/registry-sync-report.js +74 -0
  14. package/dist/src/core/versioning.js +3 -6
  15. package/dist/src/index.js +5 -8
  16. package/dist/tests/commands/change/new.test.js +103 -57
  17. package/dist/tests/commands/change/validate.test.js +164 -108
  18. package/dist/tests/commands/changes/list.test.js +41 -22
  19. package/dist/tests/commands/job/gate-reconcile.test.js +13 -2
  20. package/dist/tests/commands/registry-sync-honesty.test.js +175 -0
  21. package/dist/tests/commands/sensors/exec.test.js +3 -1
  22. package/dist/tests/commands/update.test.js +8 -8
  23. package/dist/tests/commands/watch/supervisor-loop.test.js +21 -5
  24. package/dist/tests/core/changes/helpers.js +15 -3
  25. package/dist/tests/core/changes/list-paging.test.js +131 -110
  26. package/dist/tests/core/changes/list.test.js +133 -83
  27. package/dist/tests/core/changes/receipt.test.js +18 -6
  28. package/dist/tests/core/diagnostics/provider-checks.test.js +3 -0
  29. package/dist/tests/core/init/steps-registry-sync.test.js +6 -8
  30. package/dist/tests/core/registry-git.test.js +153 -0
  31. package/dist/tests/helpers/commit-seeded-registry.js +18 -0
  32. package/dist/tests/integration/codex-provider-isolated.test.js +7 -2
  33. package/dist/tests/integration/copilot-init-isolated.test.js +7 -4
  34. package/dist/tests/structural/active-documentation.test.js +0 -41
  35. package/dist/tests/structural/bundle-skill-reference-contract.test.js +0 -12
  36. package/dist/tests/structural/cli-reference-matches-commander.test.js +94 -0
  37. package/dist/tests/structural/native-release-artifacts.test.js +116 -107
  38. package/dist/tests/structural/sensor-documentation-contract.test.js +0 -47
  39. package/dist/tests/structural/sensor-portability-contract.test.js +0 -7
  40. package/dist/tests/structural/simple-git-v4-pin.test.js +49 -0
  41. package/package.json +2 -2
  42. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  43. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  44. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  45. package/prebuilds/win32-x64/secure_fs.node +0 -0
  46. package/dist/tests/structural/cloud-admission-docs-contract.test.js +0 -54
  47. package/dist/tests/structural/documentation-phase-is-mechanized.test.js +0 -29
  48. package/dist/tests/structural/operator-environments-docs-contract.test.js +0 -70
  49. package/dist/tests/structural/process-model-description-contract.test.js +0 -34
  50. package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +0 -104
  51. package/dist/tests/structural/r4-plan-currentness-docs.test.js +0 -37
  52. package/dist/tests/structural/sensor-execution-optimization-docs-contract.test.js +0 -102
  53. package/dist/tests/structural/sensor-slice-decision-contract.test.js +0 -63
@@ -83,6 +83,62 @@ function commitRecord(slug = 'rec', overall = 'pass', sensors = ['lint']) {
83
83
  repo.git('commit', '-q', '-m', `docs(changes): ${slug}`);
84
84
  return rel;
85
85
  }
86
+ // Prebuilt states. Each is built once, lazily, in its own clone of the template (the git work of `change new` and the commits
87
+ // happens there once); every test that starts from it gets a private file copy (no git process) that it may change freely.
88
+ const fixtures = new Map();
89
+ afterAll(() => { for (const f of fixtures.values())
90
+ f.cleanup(); });
91
+ /** The prebuilt state `key` (built by `build` on first use). */
92
+ function built(key, build) {
93
+ let state = fixtures.get(key);
94
+ if (state === undefined) {
95
+ const own = repo;
96
+ repo = (0, helpers_1.cloneRepo)(template);
97
+ try {
98
+ build();
99
+ state = repo;
100
+ fixtures.set(key, state);
101
+ }
102
+ catch (error) {
103
+ repo.cleanup();
104
+ throw error;
105
+ }
106
+ finally {
107
+ repo = own;
108
+ }
109
+ }
110
+ return state;
111
+ }
112
+ /** Starts the test from a private copy of the prebuilt state `key`. */
113
+ function fixture(key, build) {
114
+ const state = built(key, build);
115
+ repo.cleanup();
116
+ repo = (0, helpers_1.cloneRepo)(state);
117
+ }
118
+ const recordKey = (slug, overall, sensors) => `record ${slug} ${overall} ${sensors.join(',')}`;
119
+ /** The repository after `commitRecord(slug, overall, sensors)`, from a prebuilt state. Returns the record's path. */
120
+ function withRecord(slug = 'rec', overall = 'pass', sensors = ['lint']) {
121
+ fixture(recordKey(slug, overall, sensors), () => { commitRecord(slug, overall, sensors); });
122
+ return `docs/changes/2026-10-17-${slug}.md`;
123
+ }
124
+ const sharedRuns = new Map();
125
+ /**
126
+ * `runChangeValidate(root, target, opts)` on an untouched prebuilt state: `none` is the template (a branch with one commit and no
127
+ * record), `record` is the state of `withRecord()`. Validation is read-only (R4.6, asserted below), so each distinct run is made
128
+ * once and its result is shared by the tests that only read it.
129
+ */
130
+ function untouched(state, target, opts) {
131
+ const key = JSON.stringify([state, target ?? null, opts]);
132
+ let r = sharedRuns.get(key);
133
+ if (r === undefined) {
134
+ const root = state === 'none' ? template.root : built(recordKey('rec', 'pass', ['lint']), () => { commitRecord(); }).root;
135
+ r = (0, change_1.runChangeValidate)(root, target, opts);
136
+ sharedRuns.set(key, r);
137
+ }
138
+ return r;
139
+ }
140
+ /** The repository after `commitRecord('aaa')` and `commitRecord('bbb')`, from a prebuilt state. */
141
+ const withTwoRecords = () => fixture('records aaa bbb', () => { commitRecord('aaa'); commitRecord('bbb'); });
86
142
  /** Edits the working-tree copy of a record (nothing is committed). */
87
143
  function edit(rel, fn) {
88
144
  const before = fs_1.default.readFileSync(abs(rel), 'utf8');
@@ -112,22 +168,20 @@ function expectFailure(r, code) {
112
168
  const rawUnsafe = /[\u0000-\u0009\u000b-\u001f\u007f-\u009f\u2028\u2029]/;
113
169
  describe('R4.1 target selection', () => {
114
170
  test('without a path every record added in merge-base..HEAD is validated', () => {
115
- commitRecord('aaa');
116
- commitRecord('bbb');
171
+ withTwoRecords();
117
172
  const r = run();
118
173
  expect(report(r).records.map((x) => x.path)).toEqual(['docs/changes/2026-10-17-aaa.md', 'docs/changes/2026-10-17-bbb.md']);
119
174
  expect(r.code).toBe(0);
120
175
  });
121
176
  test('one invalid record among several makes the run invalid and is the one reported as such', () => {
122
- commitRecord('aaa');
123
- commitRecord('bbb');
177
+ withTwoRecords();
124
178
  fs_1.default.writeFileSync(abs('docs/changes/2026-10-17-bbb.md'), 'not a record');
125
179
  const out = report(run());
126
180
  expect(out.state).toBe('invalid');
127
181
  expect(out.records.map((x) => x.state)).toEqual(['valid', 'invalid']);
128
182
  });
129
183
  test('a record that already exists at the merge base and is only modified is not an added record', () => {
130
- const rel = commitRecord();
184
+ const rel = withRecord();
131
185
  repo.git('branch', '-f', 'main', 'HEAD');
132
186
  repo.commit({ [rel]: fs_1.default.readFileSync(abs(rel), 'utf8') + '\nmore\n' }, 'docs: touch the record');
133
187
  expect(codesOf(run())).toEqual(['record-missing']);
@@ -137,27 +191,26 @@ describe('R4.1 target selection', () => {
137
191
  expect(codesOf(run())).toEqual(['record-missing']);
138
192
  });
139
193
  test('with a path only that file is validated', () => {
140
- commitRecord('aaa');
141
- commitRecord('bbb');
194
+ withTwoRecords();
142
195
  fs_1.default.writeFileSync(abs('docs/changes/2026-10-17-bbb.md'), 'not a record');
143
196
  const r = run('docs/changes/2026-10-17-aaa.md');
144
197
  expect(report(r).records.map((x) => x.path)).toEqual(['docs/changes/2026-10-17-aaa.md']);
145
198
  expect(r.code).toBe(0);
146
199
  });
147
200
  test('a path is read relative to the working directory, from a subdirectory', () => {
148
- const rel = commitRecord();
201
+ const rel = withRecord();
149
202
  const r = (0, change_1.runChangeValidate)(path_1.default.join(repo.root, 'cli'), `..${path_1.default.sep}docs${path_1.default.sep}changes${path_1.default.sep}2026-10-17-rec.md`, { json: true });
150
203
  expect(report(r).records[0].path).toBe(rel);
151
204
  expect(r.code).toBe(0);
152
205
  });
153
206
  test('an absolute path inside the repository is accepted', () => {
154
- const rel = commitRecord();
207
+ const rel = withRecord();
155
208
  const r = (0, change_1.runChangeValidate)(repo.root, abs(rel), { json: true });
156
209
  expect(report(r).records[0].path).toBe(rel);
157
210
  expect(r.code).toBe(0);
158
211
  });
159
212
  test('with a path the record is read even when the branch range holds no commit (derivation failure is reported for it)', () => {
160
- const rel = commitRecord();
213
+ const rel = withRecord();
161
214
  repo.git('branch', '-f', 'main', 'HEAD');
162
215
  const r = run(rel);
163
216
  expect(r.code).toBe(2);
@@ -222,7 +275,7 @@ describe('target path safety', () => {
222
275
  diag(r, 'record-missing', undefined);
223
276
  });
224
277
  (0, helpers_1.posixOnly)('a record that is a symlink is record-missing and is never read', () => {
225
- commitRecord();
278
+ withRecord();
226
279
  const outside = path_1.default.join(fs_1.default.mkdtempSync(path_1.default.join(repo.home, 'outside-')), 'outside.md');
227
280
  fs_1.default.writeFileSync(outside, fs_1.default.readFileSync(abs(REL)));
228
281
  fs_1.default.rmSync(abs(REL));
@@ -232,7 +285,7 @@ describe('target path safety', () => {
232
285
  diag(r, 'record-missing', undefined);
233
286
  });
234
287
  (0, helpers_1.posixOnly)('a symlinked docs/changes directory is unsafe-path for every record', () => {
235
- commitRecord();
288
+ withRecord();
236
289
  const moved = path_1.default.join(fs_1.default.mkdtempSync(path_1.default.join(repo.home, 'moved-')), 'changes');
237
290
  fs_1.default.renameSync(path_1.default.join(repo.root, 'docs', 'changes'), moved);
238
291
  fs_1.default.symlinkSync(moved, path_1.default.join(repo.root, 'docs', 'changes'));
@@ -257,14 +310,14 @@ describe('target path safety', () => {
257
310
  expect(codesOf(run())).toEqual(['frontmatter-missing']);
258
311
  });
259
312
  test('a record that is not valid UTF-8 is record-invalid (not record-too-large)', () => {
260
- commitRecord();
313
+ withRecord();
261
314
  fs_1.default.writeFileSync(abs(REL), Buffer.from([0x2d, 0x2d, 0x2d, 0xff, 0xfe, 0x0a]));
262
315
  expect(codesOf(run(REL))).toEqual(['record-invalid']);
263
316
  });
264
317
  });
265
318
  describe('R4.2 missing record and the opt-out trailer', () => {
266
319
  test('no record and no trailer fails with record-missing, state invalid, exit 2', () => {
267
- const r = run();
320
+ const r = untouched('none', undefined, { json: true });
268
321
  expect(r.code).toBe(2);
269
322
  expect(report(r).state).toBe('invalid');
270
323
  diag(r, 'record-missing', undefined);
@@ -324,7 +377,7 @@ describe('R4.2 missing record and the opt-out trailer', () => {
324
377
  });
325
378
  describe('base resolution (R3.1, R3.2, R3.5)', () => {
326
379
  test('--base selects the base: a base at HEAD leaves no added record', () => {
327
- commitRecord();
380
+ withRecord();
328
381
  repo.git('branch', 'tip', 'HEAD');
329
382
  expect(run().code).toBe(0);
330
383
  diag(run(undefined, { base: 'tip' }), 'record-missing', undefined);
@@ -356,13 +409,12 @@ describe('base resolution (R3.1, R3.2, R3.5)', () => {
356
409
  });
357
410
  describe('R4.3 base checks', () => {
358
411
  test('a filled record that matches git passes', () => {
359
- commitRecord();
360
- const r = run();
412
+ const r = untouched('record', undefined, { json: true });
361
413
  expect(r.code).toBe(0);
362
414
  expect(report(r)).toMatchObject({ state: 'valid', records: [{ path: REL, state: 'valid', diagnostics: [] }] });
363
415
  });
364
416
  test('R1 shape errors are reported with their own code and field', () => {
365
- commitRecord();
417
+ withRecord();
366
418
  edit(REL, (t) => t.replace('schema: awm.change-record/v1', 'schema: other'));
367
419
  diag(run(REL), 'field-invalid', 'schema');
368
420
  });
@@ -374,7 +426,7 @@ describe('R4.3 base checks', () => {
374
426
  diag(run(), 'pending-content', undefined);
375
427
  });
376
428
  test('a record that fails R1 is not checked against git (no record to compare)', () => {
377
- commitRecord();
429
+ withRecord();
378
430
  edit(REL, (t) => t.replace('type: feat', 'type: fix').replace('schema: awm.change-record/v1', 'schema: other'));
379
431
  expect(codesOf(run(REL))).not.toContain('derived-mismatch');
380
432
  });
@@ -385,25 +437,25 @@ describe('R4.3 base checks', () => {
385
437
  ['scope', 'scope: changes', 'scope: other'],
386
438
  ['modules', ' - cli/src', ' - cli/src\n - docs'],
387
439
  ])('derived-mismatch names the field %s', (field, from, to) => {
388
- commitRecord();
440
+ withRecord();
389
441
  edit(REL, (t) => t.replace(from, to));
390
442
  const r = run(REL);
391
443
  expect(r.code).toBe(2);
392
444
  expect(diagsOf(r).filter((d) => d.code === 'derived-mismatch').map((d) => d.field)).toEqual([field]);
393
445
  });
394
446
  test('every mismatching derived field gets its own diagnostic', () => {
395
- commitRecord();
447
+ withRecord();
396
448
  edit(REL, (t) => t.replace('type: feat', 'type: fix').replace('semver: minor', 'semver: patch'));
397
449
  expect(diagsOf(run(REL)).filter((d) => d.code === 'derived-mismatch').map((d) => d.field).sort()).toEqual(['semver', 'type']);
398
450
  });
399
451
  test('a derived-mismatch message states the recorded and the derived value on one line', () => {
400
- commitRecord();
452
+ withRecord();
401
453
  edit(REL, (t) => t.replace('type: feat', 'type: fix'));
402
454
  const d = diag(run(REL), 'derived-mismatch', 'type');
403
455
  expect(d.message).toBe('type is "fix" but git derives "feat"');
404
456
  });
405
457
  test('a git-derivation failure is reported once with the derive code (no-conventional-commits)', () => {
406
- commitRecord();
458
+ withRecord();
407
459
  repo.git('reset', '-q', '--soft', 'main');
408
460
  repo.git('commit', '-q', '-m', 'work in progress');
409
461
  const r = run(REL);
@@ -411,12 +463,12 @@ describe('R4.3 base checks', () => {
411
463
  expect(codesOf(r)).not.toContain('derived-mismatch');
412
464
  });
413
465
  test('a verification commit that is not in the repository is verification-commit-foreign', () => {
414
- commitRecord();
466
+ withRecord();
415
467
  edit(REL, (t) => t.replace(/commit: [0-9a-f]{40}/, `commit: ${'e'.repeat(40)}`));
416
468
  diag(run(REL), 'verification-commit-foreign', 'verification.commit');
417
469
  });
418
470
  test('a verification commit on another branch is verification-commit-foreign', () => {
419
- commitRecord();
471
+ withRecord();
420
472
  const other = (() => {
421
473
  repo.git('checkout', '-q', '-b', 'side', 'main');
422
474
  const sha = repo.commit({ 'side.ts': '1' }, 'feat: side');
@@ -427,50 +479,51 @@ describe('R4.3 base checks', () => {
427
479
  diag(run(REL), 'verification-commit-foreign', 'verification.commit');
428
480
  });
429
481
  test('a verification commit equal to HEAD is accepted', () => {
430
- commitRecord();
482
+ withRecord();
431
483
  edit(REL, (t) => t.replace(/commit: [0-9a-f]{40}/, `commit: ${repo.git('rev-parse', 'HEAD')}`));
432
484
  const codes = codesOf(run(REL));
433
485
  expect(codes).not.toContain('verification-commit-foreign');
434
486
  expect(codes).not.toContain('code-changed-after-verification');
435
487
  });
436
488
  test('a foreign verification commit does not also report code-changed-after-verification', () => {
437
- commitRecord();
489
+ withRecord();
438
490
  edit(REL, (t) => t.replace(/commit: [0-9a-f]{40}/, `commit: ${'e'.repeat(40)}`));
439
491
  expect(codesOf(run(REL))).not.toContain('code-changed-after-verification');
440
492
  });
441
493
  test('code changed after the verified commit is code-changed-after-verification', () => {
442
- commitRecord();
494
+ withRecord();
443
495
  repo.commit({ 'cli/src/late.ts': '1' }, 'chore: late');
444
- const d = diag(run(), 'code-changed-after-verification', undefined);
496
+ const r = run();
497
+ const d = diag(r, 'code-changed-after-verification', undefined);
445
498
  expect(d.message).toContain('cli/src/late.ts');
446
- expect(codesOf(run())).not.toContain('derived-mismatch'); // same module, same type: only the late code is wrong
499
+ expect(codesOf(r)).not.toContain('derived-mismatch'); // same module, same type: only the late code is wrong
447
500
  });
448
501
  test('files under docs/changes changed after the verified commit are not code', () => {
449
- commitRecord();
502
+ withRecord();
450
503
  repo.commit({ 'docs/changes/notes.md': 'n' }, 'docs: notes');
451
504
  expect(codesOf(run())).not.toContain('code-changed-after-verification');
452
505
  });
453
506
  test('a path that merely starts like docs/changes is code', () => {
454
- commitRecord();
507
+ withRecord();
455
508
  repo.commit({ 'docs/changes-extra/x.md': 'n' }, 'chore: extra');
456
509
  expect(codesOf(run())).toContain('code-changed-after-verification');
457
510
  });
458
511
  test('late code and derived drift are both reported', () => {
459
- commitRecord();
512
+ withRecord();
460
513
  repo.commit({ 'cli/src/core/late.ts': '1' }, 'feat(core): late');
461
514
  const c = codesOf(run());
462
515
  expect(c).toContain('code-changed-after-verification');
463
516
  expect(c).toContain('derived-mismatch');
464
517
  });
465
518
  test('one late file is reported as "1 file" and two as "2 files"', () => {
466
- commitRecord();
519
+ withRecord();
467
520
  repo.commit({ 'cli/src/one.ts': '1' }, 'chore: one');
468
521
  expect(diag(run(), 'code-changed-after-verification', undefined).message).toMatch(/^1 file changed after [0-9a-f]{12}: cli\/src\/one\.ts$/);
469
522
  repo.commit({ 'cli/src/two.ts': '1' }, 'chore: two');
470
523
  expect(diag(run(), 'code-changed-after-verification', undefined).message).toMatch(/^2 files changed after [0-9a-f]{12}: cli\/src\/one\.ts, cli\/src\/two\.ts$/);
471
524
  });
472
525
  test('the late-file list names at most 20 paths and counts them all', () => {
473
- commitRecord();
526
+ withRecord();
474
527
  repo.commit(Object.fromEntries(Array.from({ length: 25 }, (_, i) => [`a${String(i).padStart(2, '0')}.ts`, '1'])), 'chore: late');
475
528
  const d = diag(run(), 'code-changed-after-verification', undefined);
476
529
  expect(d.message).toContain('25 files changed after');
@@ -479,20 +532,20 @@ describe('R4.3 base checks', () => {
479
532
  expect(d.message).not.toContain('a20.ts');
480
533
  });
481
534
  test('exactly 20 late files are all listed with no ellipsis', () => {
482
- commitRecord();
535
+ withRecord();
483
536
  repo.commit(Object.fromEntries(Array.from({ length: 20 }, (_, i) => [`a${String(i).padStart(2, '0')}.ts`, '1'])), 'chore: late');
484
537
  const d = diag(run(), 'code-changed-after-verification', undefined);
485
538
  expect(d.message.endsWith('a19.ts')).toBe(true);
486
539
  });
487
540
  test('a long late-file list stays one line of at most 300 units', () => {
488
- commitRecord();
541
+ withRecord();
489
542
  repo.commit(Object.fromEntries(Array.from({ length: 25 }, (_, i) => [`cli/src/late/${'d'.repeat(60)}${i}.ts`, '1'])), 'chore: late');
490
543
  const d = diag(run(), 'code-changed-after-verification', undefined);
491
544
  expect(d.message.length).toBeLessThanOrEqual(300);
492
545
  expect(d.message).not.toContain('\n');
493
546
  });
494
547
  (0, helpers_1.posixOnly)('a late file named with an escape sequence and a newline never reaches the output raw', () => {
495
- commitRecord();
548
+ withRecord();
496
549
  repo.commit({ 'cli/src/\u001b[31mred\nline.ts': '1' }, 'chore: hostile');
497
550
  const text = (0, change_1.runChangeValidate)(repo.root, undefined, {});
498
551
  const json = run();
@@ -503,7 +556,7 @@ describe('R4.3 base checks', () => {
503
556
  expect(diag(json, 'code-changed-after-verification', undefined).message).toContain('\\u001b');
504
557
  });
505
558
  test('a git failure while listing the late files is reported, not thrown', () => {
506
- commitRecord();
559
+ withRecord();
507
560
  const verified = repo.git('rev-parse', 'HEAD~1');
508
561
  const real = gitModule.changedFiles;
509
562
  // Only the late-file listing (which starts at the verified commit) fails; the derivation's own diff still works.
@@ -516,13 +569,13 @@ describe('R4.3 base checks', () => {
516
569
  });
517
570
  describe('R4.4 strict checks', () => {
518
571
  test('strict passes with a matching, passing receipt', () => {
519
- commitRecord();
572
+ withRecord();
520
573
  const r = run(undefined, { strict: true });
521
574
  expect(r.code).toBe(0);
522
575
  expect(report(r).state).toBe('valid');
523
576
  });
524
577
  test('R2.2c strict compares the sensors as sorted pairs: integer-like names and a record that lists them in another order still match', () => {
525
- commitRecord('rec', 'pass', ['tsc', '9', '10']);
578
+ withRecord('rec', 'pass', ['tsc', '9', '10']);
526
579
  expect(run(undefined, { strict: true }).code).toBe(0);
527
580
  // The same sensors, listed in another order in the record's YAML: the verdict is the same.
528
581
  edit(REL, (t) => t.replace(/ sensors:[\s\S]*?(?= receipt_digest)/, " sensors: { tsc: pass, '10': pass, '9': pass }\n"));
@@ -533,82 +586,82 @@ describe('R4.4 strict checks', () => {
533
586
  expect(r.code).toBe(0);
534
587
  });
535
588
  test('base mode never reads the receipt', () => {
536
- commitRecord();
589
+ withRecord();
537
590
  fs_1.default.rmSync(path_1.default.join(repo.root, '.awm', 'sensors', 'runs'), { recursive: true, force: true });
538
591
  expect(run().code).toBe(0);
539
592
  });
540
593
  test('strict: a missing receipt is receipt-missing', () => {
541
- commitRecord();
594
+ withRecord();
542
595
  fs_1.default.rmSync(path_1.default.join(repo.root, '.awm', 'sensors', 'runs'), { recursive: true, force: true });
543
596
  diag(run(undefined, { strict: true }), 'receipt-missing', 'verification.commit');
544
597
  });
545
598
  test('strict: a receipt that is not valid JSON is receipt-invalid', () => {
546
- commitRecord();
599
+ withRecord();
547
600
  const sha = /commit: ([0-9a-f]{40})/.exec(fs_1.default.readFileSync(abs(REL), 'utf8'))?.[1];
548
601
  fs_1.default.writeFileSync(path_1.default.join(repo.root, '.awm', 'sensors', 'runs', `${sha}.json`), '{ nope');
549
602
  diag(run(undefined, { strict: true }), 'receipt-invalid', 'verification.commit');
550
603
  });
551
604
  test('strict: a receipt whose content was edited (digest broken) is receipt-invalid', () => {
552
- commitRecord();
605
+ withRecord();
553
606
  const sha = /commit: ([0-9a-f]{40})/.exec(fs_1.default.readFileSync(abs(REL), 'utf8'))?.[1];
554
607
  const file = path_1.default.join(repo.root, '.awm', 'sensors', 'runs', `${sha}.json`);
555
608
  fs_1.default.writeFileSync(file, fs_1.default.readFileSync(file, 'utf8').replace('"pass"', '"fail"'));
556
609
  diag(run(undefined, { strict: true }), 'receipt-invalid', 'verification.commit');
557
610
  });
558
611
  test('without --strict a missing or failing receipt is not reported', () => {
559
- commitRecord('rec', 'fail');
612
+ withRecord('rec', 'fail');
560
613
  expect(run().code).toBe(0);
561
614
  });
562
615
  test('strict: a failing verdict is verification-not-passing', () => {
563
- commitRecord('rec', 'fail');
616
+ withRecord('rec', 'fail');
564
617
  const r = run(undefined, { strict: true });
565
618
  expect(r.code).toBe(2);
566
619
  diag(r, 'verification-not-passing', 'verification.overall');
567
620
  expect(codesOf(r)).not.toContain('verification-mismatch');
568
621
  });
569
622
  test('strict: a record digest that differs from the receipt is verification-mismatch on receipt_digest', () => {
570
- commitRecord();
623
+ withRecord();
571
624
  edit(REL, (t) => t.replace(/receipt_digest: sha256:[0-9a-f]{64}/, `receipt_digest: sha256:${'0'.repeat(64)}`));
572
625
  const r = run(REL, { strict: true });
573
626
  expect(diagsOf(r).filter((d) => d.code === 'verification-mismatch').map((d) => d.field)).toEqual(['verification.receipt_digest']);
574
627
  });
575
628
  test('strict: a record verdict that differs from the receipt is verification-mismatch on overall', () => {
576
- commitRecord();
629
+ withRecord();
577
630
  edit(REL, (t) => t.replace('overall: pass', 'overall: fail'));
578
631
  const r = run(REL, { strict: true });
579
632
  expect(diagsOf(r).filter((d) => d.code === 'verification-mismatch').map((d) => d.field)).toEqual(['verification.overall']);
580
633
  });
581
634
  test('strict: a record sensor status that differs from the receipt is verification-mismatch on sensors', () => {
582
- commitRecord();
635
+ withRecord();
583
636
  edit(REL, (t) => t.replace('lint: pass', 'lint: fail'));
584
637
  const r = run(REL, { strict: true });
585
638
  expect(diagsOf(r).filter((d) => d.code === 'verification-mismatch').map((d) => d.field)).toEqual(['verification.sensors']);
586
639
  });
587
640
  test('strict: an extra sensor in the record is verification-mismatch on sensors', () => {
588
- commitRecord();
641
+ withRecord();
589
642
  edit(REL, (t) => t.replace(' lint: pass', ' lint: pass\n extra: pass'));
590
643
  const r = run(REL, { strict: true });
591
644
  expect(diagsOf(r).filter((d) => d.code === 'verification-mismatch').map((d) => d.field)).toEqual(['verification.sensors']);
592
645
  });
593
646
  test('strict: a sensor the record omits is verification-mismatch on sensors', () => {
594
- commitRecord('rec', 'pass', ['lint', 'types']);
647
+ withRecord('rec', 'pass', ['lint', 'types']);
595
648
  expect(run(undefined, { strict: true }).code).toBe(0); // two sensors in the receipt and the record ...
596
649
  edit(REL, (t) => t.replace(' types: pass\n', '')); // ... and the record omits one
597
650
  const r = run(REL, { strict: true });
598
651
  expect(diagsOf(r).filter((d) => d.code === 'verification-mismatch').map((d) => d.field)).toEqual(['verification.sensors']);
599
652
  });
600
653
  test('strict: the sensor order in the record does not matter', () => {
601
- commitRecord('rec', 'pass', ['lint', 'types']);
654
+ withRecord('rec', 'pass', ['lint', 'types']);
602
655
  edit(REL, (t) => t.replace(' lint: pass\n types: pass\n', ' types: pass\n lint: pass\n'));
603
656
  expect(run(REL, { strict: true }).code).toBe(0);
604
657
  });
605
658
  test('without --strict a mismatching verification block is not reported', () => {
606
- commitRecord();
659
+ withRecord();
607
660
  edit(REL, (t) => t.replace('lint: pass', 'lint: fail'));
608
661
  expect(run(REL).code).toBe(0);
609
662
  });
610
663
  test('strict with a foreign verification commit asks for no receipt of it', () => {
611
- commitRecord();
664
+ withRecord();
612
665
  edit(REL, (t) => t.replace(/commit: [0-9a-f]{40}/, `commit: ${'e'.repeat(40)}`));
613
666
  const c = codesOf(run(REL, { strict: true }));
614
667
  expect(c).toContain('verification-commit-foreign');
@@ -617,7 +670,7 @@ describe('R4.4 strict checks', () => {
617
670
  });
618
671
  describe('R4.5 output and exit codes', () => {
619
672
  test('text report: header, one line per record, one indented line per diagnostic', () => {
620
- commitRecord();
673
+ withRecord();
621
674
  edit(REL, (t) => t.replace('type: feat', 'type: fix'));
622
675
  repo.git('commit', '-q', '-am', 'docs: edit the record');
623
676
  const r = (0, change_1.runChangeValidate)(repo.root, undefined, {});
@@ -626,7 +679,7 @@ describe('R4.5 output and exit codes', () => {
626
679
  expect(r.stdout).toBe(`Change Record validation: invalid\n- ${REL}: invalid\n - derived-mismatch (type): type is "fix" but git derives "feat"\n`);
627
680
  });
628
681
  test('text report of a valid run', () => {
629
- commitRecord();
682
+ withRecord();
630
683
  const r = (0, change_1.runChangeValidate)(repo.root, undefined, {});
631
684
  expect(r.code).toBe(0);
632
685
  expect(r.stdout).toBe(`Change Record validation: valid\n- ${REL}: valid\n`);
@@ -638,12 +691,12 @@ describe('R4.5 output and exit codes', () => {
638
691
  expect(r.stdout).toBe('Change Record validation: exempt (Change-Record: none)\n');
639
692
  });
640
693
  test('text report of a missing record names docs/changes', () => {
641
- const r = (0, change_1.runChangeValidate)(repo.root, undefined, {});
694
+ const r = untouched('none', undefined, {});
642
695
  expect(r.code).toBe(2);
643
696
  expect(r.stdout.startsWith('Change Record validation: invalid\n- docs/changes: invalid\n - record-missing: ')).toBe(true);
644
697
  });
645
698
  test('--json is one line with the documented shape, and a diagnostic without a field has no field key', () => {
646
- const r = run();
699
+ const r = untouched('none', undefined, { json: true });
647
700
  expect(r.stdout.endsWith('\n')).toBe(true);
648
701
  expect(r.stdout.slice(0, -1)).not.toContain('\n');
649
702
  const out = report(r);
@@ -652,13 +705,12 @@ describe('R4.5 output and exit codes', () => {
652
705
  expect(Object.keys(out.records[0].diagnostics[0]).sort()).toEqual(['code', 'message']);
653
706
  });
654
707
  test('--json carries the field when a diagnostic names one', () => {
655
- commitRecord();
708
+ withRecord();
656
709
  edit(REL, (t) => t.replace('type: feat', 'type: fix'));
657
710
  expect(Object.keys(diagsOf(run(REL))[0]).sort()).toEqual(['code', 'field', 'message']);
658
711
  });
659
712
  test('exit code 0 for a valid run', () => {
660
- commitRecord();
661
- expect(run().code).toBe(0);
713
+ expect(untouched('record', undefined, { json: true }).code).toBe(0);
662
714
  });
663
715
  test('exit code 0 for an exempt run', () => {
664
716
  repo.commit({ 'b.ts': '1' }, 'docs: b\n\nChange-Record: none');
@@ -666,20 +718,20 @@ describe('R4.5 output and exit codes', () => {
666
718
  expect((0, change_1.runChangeValidate)(repo.root, undefined, {}).code).toBe(0);
667
719
  });
668
720
  test('exit code 2 for a missing record', () => {
669
- expect(run().code).toBe(2);
721
+ expect(untouched('none', undefined, { json: true }).code).toBe(2);
670
722
  });
671
723
  test('exit code 2 for an invalid record', () => {
672
- commitRecord();
724
+ withRecord();
673
725
  repo.commit({ 'cli/src/more.ts': '1' }, 'chore: more');
674
726
  expect(run().code).toBe(2);
675
727
  });
676
728
  test('a report is never written to stderr', () => {
677
- expect(run().stderr).toBe('');
678
- expect((0, change_1.runChangeValidate)(repo.root, undefined, {}).stderr).toBe('');
729
+ expect(untouched('none', undefined, { json: true }).stderr).toBe('');
730
+ expect(untouched('none', undefined, {}).stderr).toBe('');
679
731
  });
680
732
  /** A committed record plus `count` unknown top-level keys in the working tree: `count` unknown-field diagnostics. */
681
733
  function withUnknownKeys(count) {
682
- commitRecord();
734
+ withRecord();
683
735
  const extra = Array.from({ length: count }, (_, i) => `extra${i}: 1`).join('\n');
684
736
  edit(REL, (t) => t.replace('schema: awm.change-record/v1', `schema: awm.change-record/v1\n${extra}`));
685
737
  return diagsOf(run(REL));
@@ -705,14 +757,15 @@ describe('R4.5 output and exit codes', () => {
705
757
  test('at most 50 records are validated; a 51st-and-later record yields too-many-records and an invalid run', () => {
706
758
  const files = Object.fromEntries(Array.from({ length: 53 }, (_, i) => [`docs/changes/2026-10-17-r${String(i).padStart(2, '0')}.md`, 'x']));
707
759
  repo.commit(files, 'docs: many');
708
- const out = report(run());
760
+ const r = run();
761
+ const out = report(r);
709
762
  expect(out.state).toBe('invalid');
710
763
  expect(out.records).toHaveLength(51);
711
764
  expect(out.records[0].path).toBe('docs/changes/2026-10-17-r00.md');
712
765
  expect(out.records[49].path).toBe('docs/changes/2026-10-17-r49.md');
713
766
  expect(out.records[50]).toEqual({ path: 'docs/changes', state: 'invalid', diagnostics: [{ code: 'too-many-records', message: '53 records were added; only the first 50 were validated' }] });
714
767
  expect(out.records.slice(0, 50).every((x) => x.diagnostics.every((d) => d.code !== 'too-many-records'))).toBe(true);
715
- expect(run().code).toBe(2);
768
+ expect(r.code).toBe(2);
716
769
  });
717
770
  test('exactly 50 records are all validated with no too-many-records marker', () => {
718
771
  const files = Object.fromEntries(Array.from({ length: 50 }, (_, i) => [`docs/changes/2026-10-17-r${String(i).padStart(2, '0')}.md`, 'x']));
@@ -722,7 +775,7 @@ describe('R4.5 output and exit codes', () => {
722
775
  expect(out.records.some((x) => x.diagnostics.some((d) => d.code === 'too-many-records'))).toBe(false);
723
776
  });
724
777
  test('every message is one line of at most 300 units, in both formats', () => {
725
- commitRecord();
778
+ withRecord();
726
779
  edit(REL, (t) => t.replace('type: feat', 'type: fix').replace('semver: minor', 'semver: patch'));
727
780
  repo.git('commit', '-q', '-am', 'docs: edit the record');
728
781
  repo.commit({ 'cli/src/late.ts': '1' }, 'chore: late');
@@ -734,7 +787,7 @@ describe('R4.5 output and exit codes', () => {
734
787
  expect(l.length).toBeLessThanOrEqual(300);
735
788
  });
736
789
  test('a long record value with quotes and backslashes in a derived-mismatch message stays one bounded, escaped line', () => {
737
- commitRecord();
790
+ withRecord();
738
791
  edit(REL, (t) => t.replace('scope: changes', `scope: ${JSON.stringify('"\\'.repeat(300))}`));
739
792
  const d = diag(run(REL), 'derived-mismatch', 'scope');
740
793
  expect(d.message.length).toBeLessThanOrEqual(300);
@@ -744,12 +797,12 @@ describe('R4.5 output and exit codes', () => {
744
797
  expect(l.length).toBeLessThanOrEqual(300);
745
798
  });
746
799
  test('the modules of a derived-mismatch message are listed as a bracketed list', () => {
747
- commitRecord();
800
+ withRecord();
748
801
  edit(REL, (t) => t.replace(' - cli/src', ' - cli/src\n - docs'));
749
802
  expect(diag(run(REL), 'derived-mismatch', 'modules').message).toBe('modules is ["cli/src", "docs"] but git derives ["cli/src"]');
750
803
  });
751
804
  test('a long recorded modules list does not hide the derived value (both halves stay visible)', () => {
752
- commitRecord();
805
+ withRecord();
753
806
  const many = Array.from({ length: 40 }, (_, i) => ` - mod/m${String(i).padStart(2, '0')}`).join('\n');
754
807
  edit(REL, (t) => t.replace(' - cli/src', ` - cli/src\n${many}`));
755
808
  const d = diag(run(REL), 'derived-mismatch', 'modules');
@@ -758,14 +811,14 @@ describe('R4.5 output and exit codes', () => {
758
811
  expect(d.message.endsWith('but git derives ["cli/src"]')).toBe(true);
759
812
  });
760
813
  test('a long derived modules list does not hide the recorded value', () => {
761
- commitRecord();
814
+ withRecord();
762
815
  repo.commit(Object.fromEntries(Array.from({ length: 40 }, (_, i) => [`wide/w${String(i).padStart(2, '0')}/x.ts`, '1'])), 'chore: wide');
763
816
  const d = diag(run(), 'derived-mismatch', 'modules');
764
817
  expect(d.message.length).toBeLessThanOrEqual(300);
765
818
  expect(d.message.startsWith('modules is ["cli/src"] but git derives ["cli/src", "wide/w00"')).toBe(true);
766
819
  });
767
820
  test('when both the recorded and the derived list are long, each is cut to its own share and the whole message stays under the limit', () => {
768
- commitRecord();
821
+ withRecord();
769
822
  repo.commit(Object.fromEntries(Array.from({ length: 40 }, (_, i) => [`wide/w${String(i).padStart(2, '0')}/x.ts`, '1'])), 'chore: wide');
770
823
  const many = Array.from({ length: 40 }, (_, i) => ` - mod/m${String(i).padStart(2, '0')}`).join('\n');
771
824
  repo.commit({ [REL]: fs_1.default.readFileSync(abs(REL), 'utf8').replace(' - cli/src', ` - cli/src\n${many}`) }, 'docs: record');
@@ -774,7 +827,7 @@ describe('R4.5 output and exit codes', () => {
774
827
  expect(d.message).toMatch(/^modules is \["cli\/src", "mod\/m00".*… but git derives \["cli\/src", "wide\/w00"/);
775
828
  });
776
829
  test('a null scope and a boolean are shown as themselves', () => {
777
- commitRecord();
830
+ withRecord();
778
831
  edit(REL, (t) => t.replace('scope: changes', 'scope: null').replace('breaking: false', 'breaking: true'));
779
832
  const ds = diagsOf(run(REL));
780
833
  expect(ds.find((d) => d.field === 'scope')?.message).toBe('scope is null but git derives "changes"');
@@ -832,55 +885,54 @@ describe('R4.5 operational failures under --json', () => {
832
885
  });
833
886
  describe('R4.3 committed-record verdict (record-uncommitted)', () => {
834
887
  test('a clean committed record has no record-uncommitted diagnostic', () => {
835
- commitRecord();
836
- expect(codesOf(run())).not.toContain('record-uncommitted');
837
- expect(run().code).toBe(0);
888
+ const r = untouched('record', undefined, { json: true });
889
+ expect(codesOf(r)).not.toContain('record-uncommitted');
890
+ expect(r.code).toBe(0);
838
891
  });
839
892
  test('an unstaged edit of a committed record is record-uncommitted', () => {
840
- commitRecord();
893
+ withRecord();
841
894
  edit(REL, (t) => `${t}\nmore\n`);
842
895
  const r = run();
843
896
  expect(r.code).toBe(2);
844
897
  diag(r, 'record-uncommitted', undefined);
845
898
  });
846
899
  test('the verdict is not silently computed from the edit: an edit that keeps the record valid is still invalid', () => {
847
- commitRecord();
900
+ withRecord();
848
901
  edit(REL, (t) => `${t}\nmore\n`);
849
902
  expect(run(REL).code).toBe(0); // the edited file on its own is fine ...
850
903
  expect(report(run()).state).toBe('invalid'); // ... but the branch's committed record is not what was validated
851
904
  });
852
905
  test('a staged but uncommitted edit is record-uncommitted', () => {
853
- commitRecord();
906
+ withRecord();
854
907
  edit(REL, (t) => `${t}\nmore\n`);
855
908
  repo.git('add', REL);
856
909
  diag(run(), 'record-uncommitted', undefined);
857
910
  });
858
911
  test('an edit that also breaks the shape reports both', () => {
859
- commitRecord();
912
+ withRecord();
860
913
  edit(REL, (t) => t.replace('schema: awm.change-record/v1', 'schema: other'));
861
914
  const c = codesOf(run());
862
915
  expect(c).toContain('record-uncommitted');
863
916
  expect(c).toContain('field-invalid');
864
917
  });
865
918
  test('uncommitted edit of another record does not taint a clean one', () => {
866
- commitRecord('aaa');
867
- commitRecord('bbb');
919
+ withTwoRecords();
868
920
  edit('docs/changes/2026-10-17-bbb.md', (t) => `${t}\nmore\n`);
869
921
  const out = report(run());
870
922
  expect(out.records.map((x) => x.state)).toEqual(['valid', 'invalid']);
871
923
  });
872
924
  test('an untracked unrelated file is not an uncommitted change of the record', () => {
873
- commitRecord();
925
+ withRecord();
874
926
  fs_1.default.writeFileSync(path_1.default.join(repo.root, 'docs', 'changes', 'notes.md'), 'x');
875
927
  expect(run().code).toBe(0);
876
928
  });
877
929
  test('with an explicit path the working-tree file is validated and no record-uncommitted is reported', () => {
878
- commitRecord();
930
+ withRecord();
879
931
  edit(REL, (t) => `${t}\nmore\n`);
880
932
  expect(codesOf(run(REL))).not.toContain('record-uncommitted');
881
933
  });
882
934
  test('a git failure while checking the record fails closed as record-uncommitted', () => {
883
- commitRecord();
935
+ withRecord();
884
936
  (0, helpers_1.spyOnGit)((args) => { if (args.includes('status'))
885
937
  throw Object.assign(new Error('boom'), { code: 'ETIMEDOUT' }); });
886
938
  const r = run();
@@ -891,9 +943,13 @@ describe('R4.3 committed-record verdict (record-uncommitted)', () => {
891
943
  describe('one derivation, one ancestry check and one late-file listing per verification commit', () => {
892
944
  /** Two valid records that both verify the same commit (the second is a copy of the first under another name). */
893
945
  function twoRecordsOneCommit() {
894
- commitRecord('aaa');
895
- const text = fs_1.default.readFileSync(abs('docs/changes/2026-10-17-aaa.md'), 'utf8').replace('id: 2026-10-17-aaa', 'id: 2026-10-17-bbb');
896
- repo.commit({ 'docs/changes/2026-10-17-bbb.md': text }, 'docs(changes): bbb');
946
+ fixture('records aaa + copy bbb', () => {
947
+ commitRecord('aaa');
948
+ const copy = fs_1.default.readFileSync(abs('docs/changes/2026-10-17-aaa.md'), 'utf8').replace('id: 2026-10-17-aaa', 'id: 2026-10-17-bbb');
949
+ repo.commit({ 'docs/changes/2026-10-17-bbb.md': copy }, 'docs(changes): bbb');
950
+ });
951
+ const text = fs_1.default.readFileSync(abs('docs/changes/2026-10-17-bbb.md'), 'utf8');
952
+ expect(text).toContain('id: 2026-10-17-bbb');
897
953
  return /commit: ([0-9a-f]{40})/.exec(text)?.[1];
898
954
  }
899
955
  function gitCalls(fn) {
@@ -929,7 +985,7 @@ describe('a path read through a symlinked working directory or repository root',
929
985
  return l;
930
986
  }
931
987
  (0, helpers_1.posixOnly)('a relative path is read from a cwd that is a symlink to the repository', () => {
932
- commitRecord();
988
+ withRecord();
933
989
  const r = (0, change_1.runChangeValidate)(link(repo.root), REL, { json: true });
934
990
  expect(r.code).toBe(0);
935
991
  expect(report(r).records[0].path).toBe(REL);
@@ -940,41 +996,41 @@ describe('a path read through a symlinked working directory or repository root',
940
996
  expect(report(r).records[0].diagnostics.map((d) => d.code)).toEqual(['record-missing']);
941
997
  });
942
998
  (0, helpers_1.posixOnly)('an absolute path through a symlink to the repository is accepted', () => {
943
- commitRecord();
999
+ withRecord();
944
1000
  const r = (0, change_1.runChangeValidate)(repo.root, path_1.default.join(link(repo.root), ...REL.split('/')), { json: true });
945
1001
  expect(r.code).toBe(0);
946
1002
  expect(report(r).records[0].path).toBe(REL);
947
1003
  });
948
1004
  (0, helpers_1.posixOnly)('a repository root reported through a symlink still resolves a path given from the real directory', () => {
949
- commitRecord();
1005
+ withRecord();
950
1006
  jest.spyOn(gitModule, 'repoRoot').mockReturnValue(link(repo.root));
951
1007
  const r = (0, change_1.runChangeValidate)(repo.root, abs(REL), { json: true });
952
1008
  expect(report(r).records[0].path).toBe(REL);
953
1009
  expect(r.code).toBe(0);
954
1010
  });
955
1011
  (0, helpers_1.posixOnly)('a symlink to somewhere else is still outside the repository', () => {
956
- commitRecord();
1012
+ withRecord();
957
1013
  const elsewhere = fs_1.default.mkdtempSync(path_1.default.join(repo.home, 'else-'));
958
1014
  expectFailure((0, change_1.runChangeValidate)(repo.root, path_1.default.join(link(elsewhere), '2026-10-17-rec.md'), {}), 'path-invalid');
959
1015
  });
960
1016
  });
961
1017
  describe('R4.3 shape first', () => {
962
1018
  test('an invalid-shape committed record with late code changes reports only the shape diagnostics', () => {
963
- commitRecord();
1019
+ withRecord();
964
1020
  edit(REL, (t) => t.replace('schema: awm.change-record/v1', 'schema: other').replace('type: feat', 'type: fix'));
965
1021
  repo.git('commit', '-q', '-am', 'docs: break the record');
966
1022
  repo.commit({ 'cli/src/core/late.ts': '1' }, 'feat(core): late');
967
1023
  expect(codesOf(run(undefined, { strict: true }))).toEqual(['field-invalid']);
968
1024
  });
969
1025
  test('the same record with an explicit path reports only the shape diagnostics', () => {
970
- commitRecord();
1026
+ withRecord();
971
1027
  edit(REL, (t) => t.replace('schema: awm.change-record/v1', 'schema: other'));
972
1028
  repo.git('commit', '-q', '-am', 'docs: break the record');
973
1029
  repo.commit({ 'cli/src/core/late.ts': '1' }, 'feat(core): late');
974
1030
  expect(codesOf(run(REL, { strict: true }))).toEqual(['field-invalid']);
975
1031
  });
976
1032
  test('once the shape is fixed the other diagnostics appear', () => {
977
- commitRecord();
1033
+ withRecord();
978
1034
  repo.commit({ 'cli/src/core/late.ts': '1' }, 'feat(core): late');
979
1035
  expect(codesOf(run())).toEqual(expect.arrayContaining(['code-changed-after-verification', 'derived-mismatch']));
980
1036
  });
@@ -1005,8 +1061,8 @@ describe('R4.6 read-only', () => {
1005
1061
  ['json', undefined, { json: true }],
1006
1062
  ['a path', REL, { strict: true }],
1007
1063
  ])('%s run changes no file, no directory and no git state', (_n, target, opts) => {
1008
- commitRecord();
1009
- repo.commit({ 'cli/src/late.ts': '1' }, 'chore: late');
1064
+ withRecord();
1065
+ repo.commit({ 'cli/src/late.ts': '1' }, 'chore: late'); // also leaves the copied index fresh, as a real checkout's would be
1010
1066
  const before = snapshot();
1011
1067
  const status = repo.git('status', '--porcelain', '--ignored');
1012
1068
  const r = (0, change_1.runChangeValidate)(repo.root, target, opts);
@@ -1022,7 +1078,7 @@ describe('R4.6 read-only', () => {
1022
1078
  expect(fs_1.default.existsSync(path_1.default.join(repo.root, 'docs'))).toBe(false);
1023
1079
  });
1024
1080
  test('a dirty working tree is not an error', () => {
1025
- commitRecord();
1081
+ withRecord();
1026
1082
  fs_1.default.writeFileSync(path_1.default.join(repo.root, 'scratch.txt'), 'x');
1027
1083
  expect(run().code).toBe(0);
1028
1084
  });
@@ -1045,11 +1101,11 @@ describe('robustness: never throws, always a CommandResult', () => {
1045
1101
  expect(r.stderr).toMatch(/^(invalid-input|flag-invalid|path-invalid): [^\n]*\n$/);
1046
1102
  });
1047
1103
  test('undefined opts are accepted as no flags', () => {
1048
- commitRecord();
1104
+ withRecord();
1049
1105
  expect((0, change_1.runChangeValidate)(repo.root, undefined, (0, helpers_1.bad)(undefined)).code).toBe(0);
1050
1106
  });
1051
1107
  test('an unexpected exception becomes internal-error carrying only the error name', () => {
1052
- commitRecord();
1108
+ withRecord();
1053
1109
  jest.spyOn(deriveModule, 'deriveFields').mockImplementation(() => { throw new TypeError('secret\u001b detail'); });
1054
1110
  const r = (0, change_1.runChangeValidate)(repo.root, undefined, {});
1055
1111
  expectFailure(r, 'internal-error');
@@ -1057,7 +1113,7 @@ describe('robustness: never throws, always a CommandResult', () => {
1057
1113
  expect(r.stderr).not.toContain('secret');
1058
1114
  });
1059
1115
  test('a HEAD that moves during validation is head-moved, not a verdict', () => {
1060
- commitRecord();
1116
+ withRecord();
1061
1117
  const real = gitModule.headSha;
1062
1118
  let calls = 0;
1063
1119
  jest.spyOn(gitModule, 'headSha').mockImplementation((root) => (++calls === 1 ? real(root) : 'f'.repeat(40)));
@@ -1073,12 +1129,12 @@ describe('robustness: never throws, always a CommandResult', () => {
1073
1129
  expect(r.stderr).toContain('HEAD moved');
1074
1130
  });
1075
1131
  test('an unreadable HEAD is git-failed', () => {
1076
- commitRecord();
1132
+ withRecord();
1077
1133
  jest.spyOn(gitModule, 'headSha').mockReturnValue(null);
1078
1134
  expectFailure((0, change_1.runChangeValidate)(repo.root, undefined, {}), 'git-failed');
1079
1135
  });
1080
1136
  test('a git failure while listing the added records is git-failed', () => {
1081
- commitRecord();
1137
+ withRecord();
1082
1138
  jest.spyOn(gitModule, 'addedFilesUnder').mockReturnValue(null);
1083
1139
  expectFailure((0, change_1.runChangeValidate)(repo.root, undefined, {}), 'git-failed');
1084
1140
  });
@@ -1106,13 +1162,13 @@ describe('registerChangeCommand: validate', () => {
1106
1162
  expect(sub()?.registeredArguments.map((a) => [a.name(), a.required])).toEqual([['path', false]]);
1107
1163
  });
1108
1164
  test('a valid run prints the report and leaves the exit code alone', async () => {
1109
- commitRecord();
1165
+ withRecord();
1110
1166
  await program().parseAsync(['node', 'awm', 'change', 'validate']);
1111
1167
  expect(out).toHaveBeenCalledWith(`Change Record validation: valid\n- ${REL}: valid\n`);
1112
1168
  expect(process.exitCode).toBe(savedExit);
1113
1169
  });
1114
1170
  test('flags and the path reach the command', async () => {
1115
- commitRecord('rec', 'fail');
1171
+ withRecord('rec', 'fail');
1116
1172
  await program().parseAsync(['node', 'awm', 'change', 'validate', REL, '--strict', '--json']);
1117
1173
  const printed = String(out.mock.calls[0][0]);
1118
1174
  expect(JSON.parse(printed).records[0].diagnostics.map((d) => d.code)).toContain('verification-not-passing');