harness-everything 0.32.0 → 0.34.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.
Files changed (103) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -1
  3. package/CHANGELOG.md +33 -0
  4. package/README.md +3 -2
  5. package/ci/fixtures/doc-lifecycle/prd-revisions/config.json +9 -0
  6. package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/config.json +20 -0
  7. package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/expected.json +12 -0
  8. package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/invalid-fork/docs/prd/graph/r001.md +20 -0
  9. package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/invalid-fork/docs/prd/graph/r002.md +26 -0
  10. package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/invalid-fork/docs/prd/graph/r003.md +26 -0
  11. package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/valid/docs/prd/graph/r001.md +20 -0
  12. package/ci/fixtures/doc-lifecycle/prd-revisions/packaged/valid/docs/prd/graph/r002.md +26 -0
  13. package/ci/fixtures/doc-lifecycle/prd-revisions/ten-reqs/r001.body.md +39 -0
  14. package/ci/fixtures/doc-lifecycle/prd-revisions/ten-reqs/r002-dropped.body.md +35 -0
  15. package/ci/fixtures/doc-lifecycle/prd-revisions/ten-reqs/r002-incomplete.body.md +37 -0
  16. package/ci/fixtures/doc-lifecycle/prd-revisions/ten-reqs/r002-one-replaced.body.md +39 -0
  17. package/ci/fixtures/doc-lifecycle/prd-revisions/ten-reqs/r002-overlap.body.md +39 -0
  18. package/ci/fixtures/doc-lifecycle/scenarios/active-to-active/config.json +1 -0
  19. package/ci/fixtures/doc-lifecycle/scenarios/active-to-active/docs/adr/GRPH-001.md +8 -0
  20. package/ci/fixtures/doc-lifecycle/scenarios/active-to-active/docs/adr/GRPH-002.md +9 -0
  21. package/ci/fixtures/doc-lifecycle/scenarios/active-to-active/expected.json +1 -0
  22. package/ci/fixtures/doc-lifecycle/scenarios/active-to-active-mismatch/config.json +1 -0
  23. package/ci/fixtures/doc-lifecycle/scenarios/active-to-active-mismatch/docs/adr/GRPH-001.md +8 -0
  24. package/ci/fixtures/doc-lifecycle/scenarios/active-to-active-mismatch/docs/adr/GRPH-002.md +9 -0
  25. package/ci/fixtures/doc-lifecycle/scenarios/active-to-active-mismatch/expected.json +1 -0
  26. package/ci/fixtures/doc-lifecycle/scenarios/active-to-legacy/config.json +1 -0
  27. package/ci/fixtures/doc-lifecycle/scenarios/active-to-legacy/docs/adr/GRPH-010.md +9 -0
  28. package/ci/fixtures/doc-lifecycle/scenarios/active-to-legacy/docs/adr/legacy/GRPH-007.md +8 -0
  29. package/ci/fixtures/doc-lifecycle/scenarios/active-to-legacy/expected.json +1 -0
  30. package/ci/fixtures/doc-lifecycle/scenarios/alias-rename/config.json +1 -0
  31. package/ci/fixtures/doc-lifecycle/scenarios/alias-rename/docs/adr/GRAPH-007.md +9 -0
  32. package/ci/fixtures/doc-lifecycle/scenarios/alias-rename/docs/adr/legacy/GRPH-007.md +8 -0
  33. package/ci/fixtures/doc-lifecycle/scenarios/alias-rename/expected.json +1 -0
  34. package/ci/fixtures/doc-lifecycle/scenarios/duplicate-legacy/config.json +1 -0
  35. package/ci/fixtures/doc-lifecycle/scenarios/duplicate-legacy/docs/adr/legacy/v1/GRPH-007.md +8 -0
  36. package/ci/fixtures/doc-lifecycle/scenarios/duplicate-legacy/docs/adr/legacy/v2/GRPH-007.md +8 -0
  37. package/ci/fixtures/doc-lifecycle/scenarios/duplicate-legacy/expected.json +1 -0
  38. package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/config.json +1 -0
  39. package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/docs/adr/GRPH-003.md +9 -0
  40. package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/docs/adr/GRPH-004.md +9 -0
  41. package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/docs/adr/GRPH-005.md +9 -0
  42. package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/docs/adr/GRPH-006.md +9 -0
  43. package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/docs/adr/GRPH-007.md +9 -0
  44. package/ci/fixtures/doc-lifecycle/scenarios/invalid-refs/expected.json +1 -0
  45. package/ci/fixtures/doc-lifecycle/scenarios/pending-edge/config.json +1 -0
  46. package/ci/fixtures/doc-lifecycle/scenarios/pending-edge/docs/adr/GRPH-020.md +8 -0
  47. package/ci/fixtures/doc-lifecycle/scenarios/pending-edge/docs/adr/GRPH-021.md +9 -0
  48. package/ci/fixtures/doc-lifecycle/scenarios/pending-edge/expected.json +1 -0
  49. package/ci/fixtures/doc-lifecycle/scenarios/recorded-conflict/config.json +1 -0
  50. package/ci/fixtures/doc-lifecycle/scenarios/recorded-conflict/docs/adr/GRPH-003.md +8 -0
  51. package/ci/fixtures/doc-lifecycle/scenarios/recorded-conflict/docs/prd/graph.md +10 -0
  52. package/ci/fixtures/doc-lifecycle/scenarios/recorded-conflict/expected.json +1 -0
  53. package/ci/mechanism-2aw-project-docs-config.test.js +759 -0
  54. package/ci/mechanism-2ax-doc-lifecycle.test.js +1061 -0
  55. package/ci/mechanism-2ay-prd-revisions.test.js +2891 -0
  56. package/ci/mechanism-2az-step-type-registry.test.js +86 -0
  57. package/ci/openai-plugin-package.test.js +1 -1
  58. package/ci/release-consistency-check.js +26 -9
  59. package/ci/release-consistency-check.test.js +30 -0
  60. package/ci/router-workflow-plan.test.js +36 -0
  61. package/code-review/SKILL.md +41 -0
  62. package/code-review/references/review-playbook.md +40 -0
  63. package/code-review/templates/finding.template.md +33 -0
  64. package/docs/document-lifecycle.md +206 -0
  65. package/docs/openai-plugin.md +1 -1
  66. package/docs/workflows/code-review.md +58 -0
  67. package/evals/code-review/eval.yaml +26 -0
  68. package/evals/code-review/tasks/negative-grill-me.yaml +16 -0
  69. package/evals/code-review/tasks/negative-security-review.yaml +16 -0
  70. package/evals/code-review/tasks/negative-tdd.yaml +16 -0
  71. package/evals/code-review/tasks/negative-verification-loop.yaml +16 -0
  72. package/evals/code-review/tasks/positive-pr-review.yaml +16 -0
  73. package/evals/code-review/tasks/positive-revised-head-rereview.yaml +16 -0
  74. package/harness-everything/SKILL.md +1 -1
  75. package/harness-everything/references/skill-registry.md +1 -0
  76. package/harness-everything/scripts/doc-lifecycle.js +2401 -0
  77. package/harness-everything/scripts/project-docs-resolver.js +764 -0
  78. package/harness-everything/scripts/routing-keywords.json +7 -0
  79. package/hooks/scripts/lib/binding-resolver.js +1 -0
  80. package/multi-agent-workspace/SKILL.md +1 -1
  81. package/multi-agent-workspace/scripts/project-docs-resolver.js +437 -30
  82. package/opencode-plugin/plugin.json +1 -1
  83. package/package.json +2 -2
  84. package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
  85. package/plugins/harness-everything/hooks/scripts/lib/binding-resolver.js +1 -0
  86. package/plugins/harness-everything/plugin.json +1 -1
  87. package/plugins/harness-everything/skills/code-review/SKILL.md +41 -0
  88. package/plugins/harness-everything/skills/code-review/references/review-playbook.md +40 -0
  89. package/plugins/harness-everything/skills/code-review/templates/finding.template.md +33 -0
  90. package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
  91. package/plugins/harness-everything/skills/harness-everything/references/skill-registry.md +1 -0
  92. package/plugins/harness-everything/skills/harness-everything/scripts/doc-lifecycle.js +2401 -0
  93. package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js +764 -0
  94. package/plugins/harness-everything/skills/harness-everything/scripts/routing-keywords.json +7 -0
  95. package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +1 -1
  96. package/plugins/harness-everything/skills/multi-agent-workspace/scripts/project-docs-resolver.js +437 -30
  97. package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
  98. package/plugins/harness-everything/skills/to-spec/scripts/check-project-docs.js +168 -54
  99. package/plugins/harness-everything/skills/to-spec/scripts/project-docs-resolver.js +749 -96
  100. package/sbom.cdx.json +7 -7
  101. package/to-spec/SKILL.md +1 -1
  102. package/to-spec/scripts/check-project-docs.js +168 -54
  103. package/to-spec/scripts/project-docs-resolver.js +749 -96
@@ -0,0 +1,2891 @@
1
+ 'use strict';
2
+
3
+ // testProfile: "integration"
4
+ // D6.9 contract source: docs/document-lifecycle.md, section "Contract-style
5
+ // PRD revisions". API assumptions kept here are explicit and guarded by
6
+ // assertions so a missing API is reported as expected RED, never as TypeError.
7
+ // `storedBytes` is the pure-API byte input used by these fixtures; Git-backed
8
+ // tests exercise collected file bytes separately. diffRevisions' JSON shape
9
+ // and changed.reviewRequired are INFERRED from the D6.9 owner fixture review.
10
+
11
+ const assert = require('assert');
12
+ const crypto = require('crypto');
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const path = require('path');
16
+ const { execFileSync, spawnSync } = require('child_process');
17
+ const helper = require('./test-helper');
18
+
19
+ const modulePath = path.join(__dirname, '..', 'harness-everything', 'scripts', 'doc-lifecycle.js');
20
+ const fixtureRoot = path.join(__dirname, 'fixtures', 'doc-lifecycle', 'prd-revisions');
21
+ const CONFIG = JSON.parse(fs.readFileSync(path.join(fixtureRoot, 'config.json'), 'utf8'));
22
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-2ay revisions-測試-'));
23
+ const gitGlobalConfig = path.join(tempRoot, 'empty global git config');
24
+ const gitHooksDir = path.join(tempRoot, 'empty hooks');
25
+ const gitTemplateDir = path.join(tempRoot, 'empty templates');
26
+ const gitAttributesFile = path.join(tempRoot, 'empty attributes');
27
+ fs.writeFileSync(gitGlobalConfig, '', 'utf8');
28
+ fs.writeFileSync(gitAttributesFile, '', 'utf8');
29
+ fs.mkdirSync(gitHooksDir, { recursive: true });
30
+ fs.mkdirSync(gitTemplateDir, { recursive: true });
31
+
32
+ function gitEnvironment() {
33
+ const env = { ...process.env };
34
+ delete env.GIT_CONFIG_GLOBAL;
35
+ delete env.GIT_CONFIG_SYSTEM;
36
+ env.GIT_CONFIG_GLOBAL = gitGlobalConfig;
37
+ env.GIT_CONFIG_NOSYSTEM = '1';
38
+ for (const key of ['GIT_DIR', 'GIT_INDEX_FILE', 'GIT_WORK_TREE', 'GIT_COMMON_DIR', 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_CONFIG_PARAMETERS', 'GIT_CONFIG_COUNT']) delete env[key];
39
+ for (const key of Object.keys(env)) if (/^GIT_CONFIG_(KEY|VALUE)_\d+$/.test(key)) delete env[key];
40
+ return env;
41
+ }
42
+
43
+ function sha256(bytes) {
44
+ return `sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`;
45
+ }
46
+
47
+ function sortedJson(value) {
48
+ if (Array.isArray(value)) return `[${value.map(sortedJson).join(',')}]`;
49
+ if (value && typeof value === 'object') {
50
+ return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${sortedJson(value[key])}`).join(',')}}`;
51
+ }
52
+ return JSON.stringify(value);
53
+ }
54
+
55
+ // Independent oracle from the D6.9 digest contract: SHA-256 of sorted-key
56
+ // JSON(frontmatter minus status/effective_at/approval), LF, then stored body;
57
+ // artifact digest is SHA-256 over the complete stored file bytes.
58
+ function approvalContentDigest(frontmatter, body) {
59
+ const projected = Object.fromEntries(Object.entries(frontmatter)
60
+ .filter(([key]) => !['status', 'effective_at', 'approval'].includes(key)));
61
+ return sha256(Buffer.concat([Buffer.from(`${sortedJson(projected)}\n`, 'utf8'), Buffer.from(body, 'utf8')]));
62
+ }
63
+
64
+ let lifecycle;
65
+ try {
66
+ lifecycle = require(modulePath);
67
+ } catch (error) {
68
+ helper.check('2ay. D6.9 lifecycle module is available', false, error.message);
69
+ helper.finish();
70
+ }
71
+
72
+ process.on('exit', () => fs.rmSync(tempRoot, { recursive: true, force: true }));
73
+ console.log('\n[2ay] D6.9 contract-style PRD revisions (RED fixtures)...');
74
+
75
+ function checkCase(name, run) {
76
+ try {
77
+ run();
78
+ helper.check(name, true);
79
+ } catch (error) {
80
+ helper.check(name, false, error && error.stack ? error.stack : String(error));
81
+ }
82
+ }
83
+
84
+ const xreviewRetirementCases = [];
85
+ function rememberXreviewRetirementCase(name, docs, unresolvedConflictPaths) {
86
+ xreviewRetirementCases.push({ name, docs, unresolvedConflictPaths: unresolvedConflictPaths.slice().sort() });
87
+ }
88
+
89
+ function scalar(value) {
90
+ if (value === null) return 'null';
91
+ if (typeof value === 'string') return JSON.stringify(value);
92
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
93
+ if (Array.isArray(value)) return `[${value.map(item => scalar(item)).join(', ')}]`;
94
+ throw new TypeError(`unsupported fixture scalar: ${typeof value}`);
95
+ }
96
+
97
+ function fieldLines(key, value, indent = 0) {
98
+ const prefix = ' '.repeat(indent);
99
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
100
+ return [`${prefix}${key}:`, ...Object.entries(value).flatMap(([child, nested]) => fieldLines(child, nested, indent + 2))];
101
+ }
102
+ return [`${prefix}${key}: ${scalar(value)}`];
103
+ }
104
+
105
+ function yaml(frontmatter) {
106
+ return Object.entries(frontmatter).flatMap(([key, value]) => fieldLines(key, value)).join('\n');
107
+ }
108
+
109
+ function renderSource(frontmatter, body = '', eol = '\n') {
110
+ return `---\n${yaml(frontmatter)}\n---\n${body}`.replace(/\n/g, eol);
111
+ }
112
+
113
+ function writeDoc(root, relativePath, frontmatter, body = '', eol = '\n') {
114
+ const absolute = path.join(root, ...relativePath.split('/'));
115
+ fs.mkdirSync(path.dirname(absolute), { recursive: true });
116
+ fs.writeFileSync(absolute, renderSource(frontmatter, body, eol), 'utf8');
117
+ return absolute;
118
+ }
119
+
120
+ function parseGeneratedSource(source) {
121
+ const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
122
+ assert.ok(match, 'generated fixture keeps opening and closing frontmatter delimiters');
123
+ const data = {};
124
+ let nested = null;
125
+ const parseValue = value => {
126
+ const token = value.trim();
127
+ if (token.startsWith('"') || token.startsWith('[')) return JSON.parse(token);
128
+ if (token === 'true') return true;
129
+ if (token === 'false') return false;
130
+ if (token === 'null') return null;
131
+ if (/^-?\d+(?:\.\d+)?$/.test(token)) return Number(token);
132
+ return token;
133
+ };
134
+ for (const line of match[1].split(/\r?\n/)) {
135
+ const child = line.match(/^ ([A-Za-z0-9_-]+):(?:[ \t]*(.*))?$/);
136
+ if (child) {
137
+ assert.ok(nested, `nested field has a declared parent: ${line}`);
138
+ nested[child[1]] = parseValue(child[2] || '');
139
+ continue;
140
+ }
141
+ const field = line.match(/^([A-Za-z0-9_-]+):(?:[ \t]*(.*))?$/);
142
+ assert.ok(field, `generated frontmatter field parses: ${line}`);
143
+ const value = field[2] || '';
144
+ if (!value) {
145
+ data[field[1]] = {};
146
+ nested = data[field[1]];
147
+ } else {
148
+ data[field[1]] = parseValue(value);
149
+ nested = null;
150
+ }
151
+ }
152
+ return { data, body: match[2] };
153
+ }
154
+
155
+ function docObject(relativePath, frontmatter, body = '') {
156
+ const storedBytes = Buffer.from(renderSource(frontmatter, body), 'utf8');
157
+ const doc = {
158
+ path: relativePath,
159
+ role: relativePath.startsWith('docs/prd/') ? 'prd' : 'report',
160
+ legacy: relativePath.includes('/legacy/'),
161
+ frontmatter: { ...frontmatter },
162
+ body,
163
+ error: null
164
+ };
165
+ Object.defineProperty(doc, 'storedBytes', { value: storedBytes, enumerable: false, writable: true });
166
+ return doc;
167
+ }
168
+
169
+ function pin(id, revision, requirement) {
170
+ return `${id}@r${revision}${requirement ? `#${requirement}` : ''}`;
171
+ }
172
+
173
+ function revisionDoc(options, priorDoc) {
174
+ const {
175
+ id = 'PRD-GRPH-001',
176
+ revision,
177
+ status = 'draft',
178
+ body = '# Contract\n\nComplete acceptance snapshot.\n',
179
+ relativePath = `docs/prd/sample/r${String(revision).padStart(3, '0')}.md`,
180
+ previousRevision,
181
+ previousDigest,
182
+ draftedBy = 'Alex Drafting Agent',
183
+ ...extra
184
+ } = options;
185
+ const frontmatter = { id, revision, status, drafted_by: draftedBy, ...extra };
186
+ if (previousRevision !== undefined) frontmatter.previous_revision = previousRevision;
187
+ if (previousDigest !== undefined) frontmatter.previous_digest = previousDigest;
188
+ else if (previousRevision !== undefined && priorDoc) frontmatter.previous_digest = sha256(priorDoc.storedBytes);
189
+ if (status === 'effective') frontmatter.effective_at = frontmatter.effective_at || '2026-10-17T00:01:00Z';
190
+ if (status === 'approved' || status === 'effective') {
191
+ frontmatter.approval = {
192
+ by: 'Independent cold verifier',
193
+ at: '2026-10-17T00:00:00Z',
194
+ ref: 'https://approvals.example.invalid/prd-review/001',
195
+ role: 'human',
196
+ content_digest: ''
197
+ };
198
+ frontmatter.approval.content_digest = approvalContentDigest(frontmatter, body);
199
+ }
200
+ return docObject(relativePath, frontmatter, body);
201
+ }
202
+
203
+ function reviseFixture(doc, changes, { rebindApproval = true } = {}) {
204
+ const frontmatter = { ...doc.frontmatter, ...changes };
205
+ const approval = changes.approval || doc.frontmatter.approval;
206
+ if (approval) {
207
+ frontmatter.approval = { ...approval };
208
+ if (rebindApproval) frontmatter.approval.content_digest = approvalContentDigest(frontmatter, doc.body);
209
+ }
210
+ return docObject(doc.path, frontmatter, doc.body);
211
+ }
212
+
213
+ function lintDocs(docs, options = { all: true }) {
214
+ return analyzeDocs(docs, options).diagnostics;
215
+ }
216
+
217
+ function analyzeDocs(docs, options = { all: true }) {
218
+ assert.strictEqual(typeof lifecycle.analyzeDocs, 'function', 'D6.9 exports analyzeDocs(docs, config, opts)');
219
+ return lifecycle.analyzeDocs(docs, CONFIG, options);
220
+ }
221
+
222
+ function assertEnvelope(result, disposition) {
223
+ assert.ok(result && typeof result === 'object' && !Array.isArray(result), 'analysis is an object envelope');
224
+ assert.ok(Array.isArray(result.diagnostics), 'analysis.diagnostics is an array');
225
+ assert.ok(result.evidence && typeof result.evidence === 'object', 'analysis.evidence is present');
226
+ assert.strictEqual(result.evidence.immutability, disposition);
227
+ const roundTrip = JSON.parse(JSON.stringify(result));
228
+ assert.strictEqual(roundTrip.evidence.immutability, disposition, 'JSON serialization preserves evidence disposition');
229
+ return result;
230
+ }
231
+
232
+ function codes(diagnostics) {
233
+ return diagnostics.map(item => item.code);
234
+ }
235
+
236
+ function matching(diagnostics, code) {
237
+ return diagnostics.filter(item => item.code === code);
238
+ }
239
+
240
+ function expectCode(diagnostics, code, detail = '') {
241
+ assert.ok(matching(diagnostics, code).length > 0, `${code} expected${detail ? `: ${detail}` : ''}; got ${JSON.stringify(diagnostics)}`);
242
+ return matching(diagnostics, code);
243
+ }
244
+
245
+ function expectOnlyCodes(diagnostics, expected) {
246
+ const actual = [...new Set(diagnostics.map(item => item.code))].sort();
247
+ assert.deepStrictEqual(actual, [...expected].sort(), `unexpected diagnostics: ${JSON.stringify(diagnostics)}`);
248
+ }
249
+
250
+ function forbidCode(diagnostics, code) {
251
+ assert.strictEqual(matching(diagnostics, code).length, 0, `${code} unexpected: ${JSON.stringify(diagnostics)}`);
252
+ }
253
+
254
+ function revisionIds(body) {
255
+ return [...body.matchAll(/^### (REQ-[A-Z0-9-]+)(?: \([^)]*\))?:/gm)].map(match => match[1]).sort();
256
+ }
257
+
258
+ function fixtureBody(name) {
259
+ return fs.readFileSync(path.join(fixtureRoot, 'ten-reqs', name), 'utf8');
260
+ }
261
+
262
+ function git(root, args, input) {
263
+ return execFileSync('git', [
264
+ '-c', `core.hooksPath=${gitHooksDir}`,
265
+ '-c', `core.attributesFile=${gitAttributesFile}`,
266
+ '-c', 'core.safecrlf=false',
267
+ '-c', 'commit.gpgsign=false',
268
+ ...args
269
+ ], {
270
+ cwd: root,
271
+ encoding: 'utf8',
272
+ input,
273
+ env: gitEnvironment()
274
+ }).trim();
275
+ }
276
+
277
+ function gitBytes(root, args, input) {
278
+ return execFileSync('git', [
279
+ '-c', `core.hooksPath=${gitHooksDir}`,
280
+ '-c', `core.attributesFile=${gitAttributesFile}`,
281
+ '-c', 'commit.gpgsign=false',
282
+ ...args
283
+ ], { cwd: root, encoding: 'buffer', input, env: gitEnvironment() });
284
+ }
285
+
286
+ function gitRepo(name, autocrlf = 'false') {
287
+ const root = path.join(tempRoot, `repo with spaces – 臺灣 ${name}`);
288
+ fs.mkdirSync(root, { recursive: true });
289
+ git(root, ['-c', `init.templateDir=${gitTemplateDir}`, 'init', '--quiet']);
290
+ git(root, ['config', 'core.autocrlf', autocrlf]);
291
+ git(root, ['config', 'core.attributesFile', gitAttributesFile]);
292
+ git(root, ['config', 'core.safecrlf', 'false']);
293
+ git(root, ['config', 'core.hooksPath', gitHooksDir]);
294
+ git(root, ['config', 'commit.gpgsign', 'false']);
295
+ git(root, ['config', 'user.name', 'D6.9 Test']);
296
+ git(root, ['config', 'user.email', 'd69-tests@example.invalid']);
297
+ fs.writeFileSync(path.join(root, '.gitattributes'), `*.md text eol=${autocrlf === 'true' ? 'crlf' : 'lf'}\n`, 'utf8');
298
+ fs.writeFileSync(path.join(root, 'lifecycle-config.json'), JSON.stringify(CONFIG, null, 2), 'utf8');
299
+ return root;
300
+ }
301
+
302
+ function commitAll(root, message) {
303
+ git(root, ['add', '--all']);
304
+ git(root, ['commit', '--quiet', '-m', message]);
305
+ return git(root, ['rev-parse', 'HEAD']);
306
+ }
307
+
308
+ function baseLint(root, baseRevision) {
309
+ const configPath = path.join(root, 'lifecycle-config.json');
310
+ const result = spawnSync(process.execPath, [
311
+ modulePath,
312
+ 'lint',
313
+ '--workspace', root,
314
+ '--config', configPath,
315
+ '--all',
316
+ '--base',
317
+ baseRevision
318
+ ], { cwd: root, encoding: 'utf8', env: gitEnvironment() });
319
+ assert.ifError(result.error);
320
+ assert.ok([0, 1].includes(result.status), `--base CLI must return a behavior result, not argument/setup failure (status ${result.status}; stderr: ${result.stderr}; stdout: ${result.stdout})`);
321
+ const output = JSON.parse(result.stdout);
322
+ assert.ok(output && Array.isArray(output.diagnostics) && output.evidence, `CLI JSON must be an analysis envelope: ${result.stdout}`);
323
+ return output;
324
+ }
325
+
326
+ function lifecycleCommand(root, command, { baseRevision, changedPath, all = false } = {}) {
327
+ const args = [modulePath, command, '--workspace', root, '--config', path.join(root, 'lifecycle-config.json')];
328
+ if (all) args.push('--all');
329
+ else args.push('--changed', changedPath || '');
330
+ if (baseRevision) args.push('--base', baseRevision);
331
+ const result = spawnSync(process.execPath, args, { cwd: root, encoding: 'utf8', env: gitEnvironment() });
332
+ assert.ifError(result.error);
333
+ assert.ok([0, 1].includes(result.status), `${command} CLI must return a behavior result, not argument/setup failure (status ${result.status}; stderr: ${result.stderr}; stdout: ${result.stdout})`);
334
+ return command === 'lint' ? JSON.parse(result.stdout) : result.stdout;
335
+ }
336
+
337
+ function inventoryAt(root, baseRevision) {
338
+ const paths = gitBytes(root, ['ls-tree', '-r', '--name-only', '-z', '--full-tree', baseRevision])
339
+ .toString('utf8').split('\0').filter(Boolean);
340
+ return paths.map(relativePath => {
341
+ let bytes;
342
+ try { bytes = gitBytes(root, ['show', `${baseRevision}:${relativePath}`]); } catch { return null; }
343
+ const parsed = lifecycle.parseFrontmatter(bytes.toString('utf8'));
344
+ if (parsed.error || !parsed.data || typeof parsed.data.id !== 'string') return null;
345
+ return {
346
+ id: parsed.data.id,
347
+ revision: parsed.data.revision,
348
+ path: relativePath,
349
+ status: parsed.data.status,
350
+ bytes
351
+ };
352
+ }).filter(Boolean);
353
+ }
354
+
355
+ function seedRevisionRepo(name, status, autocrlf = 'false', beforeCommit) {
356
+ const root = gitRepo(name, autocrlf);
357
+ const body = '# Immutable baseline\n\nAcceptance: preserve this exact contract.\n';
358
+ const seed = revisionDoc({ id: 'PRD-GRPH-001', revision: 1, status, body, relativePath: 'docs/prd/sample/r001.md' });
359
+ const doc = reviseFixture(seed, {
360
+ approval: seed.frontmatter.approval
361
+ ? { ...seed.frontmatter.approval, ref: 'docs/approvals/prd-review.md' }
362
+ : undefined
363
+ });
364
+ const digest = sha256(doc.storedBytes);
365
+ const file = path.join(root, 'docs/prd/sample/r001.md');
366
+ writeDoc(root, 'docs/prd/sample/r001.md', doc.frontmatter, body);
367
+ writeDoc(root, 'docs/approvals/prd-review.md', { id: 'APPROVAL-001', status: 'final' }, '# Independent review record\n');
368
+ if (typeof beforeCommit === 'function') beforeCommit(root, digest);
369
+ return { root, body, digest, base: commitAll(root, `${status} PRD baseline`) };
370
+ }
371
+
372
+ function seedEffectiveRepo(name, autocrlf = 'false', beforeCommit) {
373
+ return seedRevisionRepo(name, 'effective', autocrlf, beforeCommit);
374
+ }
375
+
376
+ function seedApprovedRepo(name, autocrlf = 'false') {
377
+ return seedRevisionRepo(name, 'approved', autocrlf);
378
+ }
379
+
380
+ function mutateRevision(root, fields, body, relativePath = 'docs/prd/sample/r001.md') {
381
+ const file = path.join(root, ...relativePath.split('/'));
382
+ const originalSource = fs.readFileSync(file, 'utf8');
383
+ let source = originalSource;
384
+ const firstLine = source.indexOf('\n') + 1;
385
+ const delimiter = source.slice(firstLine).match(/\r?\n---[ \t]*\r?\n/);
386
+ const closing = delimiter ? firstLine + delimiter.index : -1;
387
+ assert.notStrictEqual(closing, -1, 'test fixture must contain a closing frontmatter delimiter');
388
+ let header = source.slice(firstLine, closing);
389
+ for (const [key, value] of Object.entries(fields)) {
390
+ const pattern = new RegExp(`^${key}:.*$`, 'm');
391
+ if (pattern.test(header)) header = header.replace(pattern, `${key}: ${scalar(value)}`);
392
+ else header += `${header ? '\n' : ''}${key}: ${scalar(value)}`;
393
+ }
394
+ const originalBody = source.slice(closing + delimiter[0].length);
395
+ const nextBody = body === undefined ? originalBody : body;
396
+ source = `---\n${header}\n---\n${nextBody}`;
397
+ fs.writeFileSync(file, source, 'utf8');
398
+ const parsed = parseGeneratedSource(source);
399
+ assert.strictEqual(parsed.body, nextBody, 'mutation must preserve or replace exactly the requested body');
400
+ const before = parseGeneratedSource(originalSource);
401
+ for (const [key, value] of Object.entries(fields)) assert.deepStrictEqual(parsed.data[key], value, `mutated ${key} field`);
402
+ for (const [key, value] of Object.entries(before.data)) {
403
+ if (!Object.prototype.hasOwnProperty.call(fields, key)) assert.deepStrictEqual(parsed.data[key], value, `unrequested ${key} field remains unchanged`);
404
+ }
405
+ return file;
406
+ }
407
+
408
+ function diffRevisions(docs, refA, refB) {
409
+ assert.strictEqual(typeof lifecycle.diffRevisions, 'function', 'D6.9 exports diffRevisions(docs, refA, refB)');
410
+ return lifecycle.diffRevisions(docs, refA, refB);
411
+ }
412
+
413
+ function assertResolve(docs, ref, expected) {
414
+ assert.strictEqual(typeof lifecycle.resolveRef, 'function', 'D6.9 reference resolver is exported');
415
+ const result = lifecycle.resolveRef(docs, ref);
416
+ assert.ok(result && result.doc, `${ref} should resolve; got ${JSON.stringify(result)}`);
417
+ if (expected.path) assert.strictEqual(result.doc.path, expected.path, `${ref} path`);
418
+ if (expected.revision !== undefined) assert.strictEqual(result.revision, expected.revision, `${ref} revision`);
419
+ if (expected.status !== undefined) assert.strictEqual(result.status, expected.status, `${ref} status`);
420
+ if (expected.digest) {
421
+ const sourceDoc = docs.find(item => item.path === result.doc.path);
422
+ assert.ok(sourceDoc && Buffer.isBuffer(sourceDoc.storedBytes), `${ref} fixture includes complete stored artifact bytes`);
423
+ const wanted = expected.digest === true ? sha256(sourceDoc.storedBytes) : expected.digest;
424
+ assert.strictEqual(result.digest, wanted, `${ref} independently computed artifact digest`);
425
+ }
426
+ if (expected.requirement) assert.strictEqual(result.requirement && result.requirement.id, expected.requirement, `${ref} requirement`);
427
+ if (expected.provenance) assert.ok(result.provenance, `${ref} returns revision provenance`);
428
+ return result;
429
+ }
430
+
431
+ checkCase('2ay digest oracle matches fixed approval-projection and complete-artifact vectors', () => {
432
+ const doc = revisionDoc({ revision: 1, status: 'effective' });
433
+ assert.strictEqual(doc.frontmatter.approval.content_digest, 'sha256:67adaca2af0a1c3e946fc21dfc6902aa87664745ad663972f0b0ba88ece33946', 'projection hash is independently pinned');
434
+ assert.strictEqual(sha256(doc.storedBytes), 'sha256:be4d183e4a6e4ffd786202de1d2e7a733fc0e308db395c437bfeba28a2092ec1', 'complete stored-file hash is independently pinned');
435
+ assert.notStrictEqual(doc.frontmatter.approval.content_digest, sha256(doc.storedBytes), 'approval and artifact identities remain distinct');
436
+ });
437
+
438
+ checkCase('2ay golden valid chain is clean and one-rule digest mutations are discriminating', () => {
439
+ const first = revisionDoc({ revision: 1, status: 'effective', body: fixtureBody('r001.body.md') });
440
+ const second = revisionDoc({ revision: 2, status: 'approved', previousRevision: 1, body: fixtureBody('r002-one-replaced.body.md') }, first);
441
+ const golden = assertEnvelope(analyzeDocs([first, second]), 'not-observed');
442
+ assert.deepStrictEqual(golden.diagnostics, [], 'independently valid linked revisions produce no structural diagnostics');
443
+
444
+ const wrongArtifactLink = reviseFixture(second, {
445
+ previous_digest: sha256(Buffer.from('not the parent artifact', 'utf8'))
446
+ });
447
+ expectOnlyCodes(lintDocs([first, wrongArtifactLink]), ['revision-digest-mismatch']);
448
+
449
+ const wrongApproval = reviseFixture(second, {}, { rebindApproval: false });
450
+ wrongApproval.frontmatter.approval.content_digest = sha256(Buffer.from('not the approved projection', 'utf8'));
451
+ wrongApproval.storedBytes = Buffer.from(renderSource(wrongApproval.frontmatter, wrongApproval.body), 'utf8');
452
+ expectOnlyCodes(lintDocs([first, wrongApproval]), ['approval-content-mismatch']);
453
+ });
454
+
455
+ checkCase('2ay approval requires a complete independent reviewer record and a resolvable evidence ref', () => {
456
+ const first = revisionDoc({ revision: 1, status: 'effective' });
457
+ const approved = revisionDoc({ revision: 2, status: 'approved', previousRevision: 1 }, first);
458
+ for (const field of ['by', 'at', 'ref', 'content_digest', 'role']) {
459
+ const incompleteFm = { ...approved.frontmatter, approval: { ...approved.frontmatter.approval } };
460
+ delete incompleteFm.approval[field];
461
+ const incomplete = docObject(approved.path, incompleteFm, approved.body);
462
+ expectCode(lintDocs([first, incomplete]), 'approval-missing', `approval.${field}`);
463
+ }
464
+
465
+ const selfApproved = reviseFixture(approved, {
466
+ approval: { ...approved.frontmatter.approval, by: approved.frontmatter.drafted_by }
467
+ });
468
+ expectOnlyCodes(lintDocs([first, selfApproved]), ['approval-not-independent']);
469
+
470
+ const root = gitRepo('approval-evidence-ref');
471
+ const approvedSeed = revisionDoc({ revision: 1, status: 'approved', relativePath: 'docs/prd/sample/r001.md' });
472
+ const approvedOnDisk = reviseFixture(approvedSeed, {
473
+ approval: { ...approvedSeed.frontmatter.approval, ref: 'docs/approvals/prd-review.md' }
474
+ });
475
+ writeDoc(root, approvedOnDisk.path, approvedOnDisk.frontmatter, approvedOnDisk.body);
476
+ writeDoc(root, approvedOnDisk.frontmatter.approval.ref, { id: 'APPROVAL-001', status: 'final' }, '# Review record\n');
477
+ const validRecord = analyzeDocs(lifecycle.collectDocs(root, CONFIG), { all: true });
478
+ assert.deepStrictEqual(validRecord.diagnostics.filter(item => item.severity === 'error'), [], 'an in-repo approval ref resolves offline');
479
+ fs.unlinkSync(path.join(root, ...approvedOnDisk.frontmatter.approval.ref.split('/')));
480
+ const missingRecord = analyzeDocs(lifecycle.collectDocs(root, CONFIG), { all: true });
481
+ assert.ok(missingRecord.diagnostics.some(item => item.severity === 'error'), 'a missing in-repo approval record is not accepted as evidence');
482
+ });
483
+
484
+ // Owner case 1 — compare against a trusted --base commit, not only a
485
+ // self-consistent previous_digest. Each mutation starts from a clean repo.
486
+ checkCase('2ay case 1. effective revision body edit is rejected against --base', () => {
487
+ const { root, base } = seedEffectiveRepo('immutable-edit');
488
+ mutateRevision(root, {}, '# Edited baseline\n\nAcceptance: silently changed.\n');
489
+ expectCode(baseLint(root, base).diagnostics, 'immutable-revision-modified');
490
+ });
491
+
492
+ checkCase('2ay case 1. status downgrade cannot make a base-effective revision mutable', () => {
493
+ const { root, base } = seedEffectiveRepo('immutable-status');
494
+ mutateRevision(root, { status: 'draft' });
495
+ expectCode(baseLint(root, base).diagnostics, 'immutable-revision-modified');
496
+ });
497
+
498
+ checkCase('2ay case 1. deleting a base-effective revision is rejected', () => {
499
+ const { root, base } = seedEffectiveRepo('immutable-delete');
500
+ fs.unlinkSync(path.join(root, 'docs/prd/sample/r001.md'));
501
+ expectCode(baseLint(root, base).diagnostics, 'immutable-revision-deleted');
502
+ });
503
+
504
+ checkCase('2ay case 1. moving a base-effective revision outside governed scope is rejected', () => {
505
+ const { root, base } = seedEffectiveRepo('immutable-move');
506
+ const source = path.join(root, 'docs/prd/sample/r001.md');
507
+ const target = path.join(root, 'docs/archive/r001.md');
508
+ fs.mkdirSync(path.dirname(target), { recursive: true });
509
+ fs.renameSync(source, target);
510
+ const diagnostics = baseLint(root, base).diagnostics;
511
+ assert.ok(
512
+ diagnostics.some(item => ['immutable-revision-modified', 'immutable-revision-deleted'].includes(item.code)),
513
+ `move should retain immutable provenance: ${JSON.stringify(diagnostics)}`
514
+ );
515
+ });
516
+
517
+ checkCase('2ay case 1. co-editing the parent and previous_digest still fails the trusted-base comparison', () => {
518
+ const { root, base } = seedEffectiveRepo('immutable-coedit', 'false', (repoRoot, parentDigest) => {
519
+ writeDoc(repoRoot, 'docs/prd/sample/r002.md', {
520
+ id: 'PRD-GRPH-001', revision: 2, previous_revision: 1,
521
+ previous_digest: parentDigest, status: 'draft'
522
+ }, '# Child revision\n\nAcceptance: follow the current parent.\n');
523
+ });
524
+ const parentFile = path.join(root, 'docs/prd/sample/r001.md');
525
+ const parentSource = fs.readFileSync(parentFile, 'utf8');
526
+ const closing = parentSource.indexOf('\n---\n', parentSource.indexOf('\n') + 1);
527
+ const parentBody = parentSource.slice(closing + 5);
528
+ const changedBody = `${parentBody}\nA coordinated parent edit.\n`;
529
+ mutateRevision(root, {}, changedBody);
530
+ const childFile = path.join(root, 'docs/prd/sample/r002.md');
531
+ const candidate = lifecycle.collectDocs(root, CONFIG);
532
+ const editedParentDigest = sha256(Buffer.from(fs.readFileSync(parentFile, 'utf8'), 'utf8'));
533
+ const childSource = fs.readFileSync(childFile, 'utf8').replace(/^previous_digest:.*$/m, `previous_digest: ${JSON.stringify(editedParentDigest)}`);
534
+ fs.writeFileSync(childFile, childSource, 'utf8');
535
+ expectCode(baseLint(root, base).diagnostics, 'immutable-revision-modified');
536
+ });
537
+
538
+ checkCase('2ay case 1. absent trusted baseline is explicitly not-observed, never a pass', () => {
539
+ const onlyRevision = revisionDoc({ revision: 1, status: 'effective' });
540
+ const result = assertEnvelope(analyzeDocs([onlyRevision], { all: true }), 'not-observed');
541
+ assert.deepStrictEqual(result.diagnostics, [], 'a well-formed effective snapshot can be structurally valid without a baseline');
542
+ const projected = lifecycle.lintDocs([onlyRevision], CONFIG, { all: true });
543
+ assert.ok(Array.isArray(projected), 'lintDocs remains the diagnostics-only compatibility projection');
544
+ assert.deepStrictEqual(projected, result.diagnostics);
545
+ assert.strictEqual(Object.prototype.hasOwnProperty.call(projected, 'evidence'), false, 'callers obtain evidence from analyzeDocs, not hidden array metadata');
546
+ });
547
+
548
+ checkCase('2ay case 1. supplied stored-byte inventory is equivalent to a trusted Git base', () => {
549
+ const effective = revisionDoc({ revision: 1, status: 'effective' });
550
+ // INFERRED inventory option shape: the contract requires an offline snapshot
551
+ // inventory but does not prescribe its JS representation.
552
+ const inventory = [{
553
+ id: effective.frontmatter.id,
554
+ revision: effective.frontmatter.revision,
555
+ path: effective.path,
556
+ status: effective.frontmatter.status,
557
+ bytes: effective.storedBytes
558
+ }];
559
+ const fromInventory = assertEnvelope(analyzeDocs([effective], { all: true, baseInventory: inventory }), 'observed');
560
+ assert.deepStrictEqual(fromInventory.diagnostics, []);
561
+
562
+ const root = gitRepo('inventory-equivalence');
563
+ writeDoc(root, effective.path, effective.frontmatter, effective.body);
564
+ const base = commitAll(root, 'inventory equivalence baseline');
565
+ const fromGit = baseLint(root, base);
566
+ assert.strictEqual(fromGit.evidence.immutability, 'observed');
567
+ assert.deepStrictEqual(fromInventory.diagnostics, fromGit.diagnostics, 'offline byte inventory and Git blob base produce equal diagnostics');
568
+ });
569
+
570
+ checkCase('2ay case 1. CLI emits not-observed evidence without a trusted baseline', () => {
571
+ const root = gitRepo('cli-no-base-evidence');
572
+ const effective = revisionDoc({ revision: 1, status: 'effective', relativePath: 'docs/prd/sample/r001.md' });
573
+ writeDoc(root, effective.path, effective.frontmatter, effective.body);
574
+ commitAll(root, 'valid unbaselined effective snapshot');
575
+ const result = spawnSync(process.execPath, [modulePath, 'lint', '--workspace', root, '--config', path.join(root, 'lifecycle-config.json'), '--all'], {
576
+ cwd: tempRoot, encoding: 'utf8', env: gitEnvironment()
577
+ });
578
+ assert.ifError(result.error);
579
+ assert.strictEqual(result.status, 0, `not-observed is evidence, not an error diagnostic: ${result.stderr} ${result.stdout}`);
580
+ const output = JSON.parse(result.stdout);
581
+ assert.strictEqual(output.evidence && output.evidence.immutability, 'not-observed');
582
+ assert.deepStrictEqual(output.diagnostics, []);
583
+ });
584
+
585
+ checkCase('2ay case 1. missing trusted history is unavailable and is never fetched', () => {
586
+ const remote = path.join(tempRoot, 'remote with spaces – 臺灣.git');
587
+ fs.mkdirSync(remote, { recursive: true });
588
+ git(remote, ['-c', `init.templateDir=${gitTemplateDir}`, 'init', '--bare', '--quiet']);
589
+ const publisher = gitRepo('unavailable-base-publisher');
590
+ writeDoc(publisher, 'README.md', { id: 'IGNORED' }, 'Remote-only commit.\n');
591
+ const remoteOnly = commitAll(publisher, 'remote-only history');
592
+ git(publisher, ['remote', 'add', 'origin', remote]);
593
+ git(publisher, ['push', '--quiet', 'origin', 'HEAD:refs/heads/main']);
594
+
595
+ const { root } = seedEffectiveRepo('unavailable-base-candidate');
596
+ git(root, ['remote', 'add', 'origin', remote]);
597
+ const result = spawnSync(process.execPath, [modulePath, 'lint', '--workspace', root, '--config', path.join(root, 'lifecycle-config.json'), '--all', '--base', 'refs/remotes/origin/main'], {
598
+ cwd: tempRoot, encoding: 'utf8', env: gitEnvironment()
599
+ });
600
+ assert.ifError(result.error);
601
+ assert.strictEqual(result.status, 0, `unavailable evidence alone does not fail lint: ${result.stderr} ${result.stdout}`);
602
+ const output = JSON.parse(result.stdout);
603
+ assert.strictEqual(output.evidence && output.evidence.immutability, 'unavailable');
604
+ const remoteTrackingRef = spawnSync('git', ['rev-parse', '--verify', '--quiet', 'refs/remotes/origin/main'], { cwd: root, encoding: 'utf8', env: gitEnvironment() });
605
+ assert.strictEqual(remoteTrackingRef.status, 1, `checker must not fetch a missing base ref; remote contains ${remoteOnly}`);
606
+ });
607
+
608
+ checkCase('2ay case 1. candidate config exclusion cannot bypass immutable base inventory', () => {
609
+ const { root, base } = seedEffectiveRepo('config-exclusion-bypass');
610
+ mutateRevision(root, {}, '# Changed while excluded from current config.\n');
611
+ fs.writeFileSync(path.join(root, 'lifecycle-config.json'), JSON.stringify({ governed: [], domains: ['GRPH', 'IFCE'], legacy: [] }, null, 2), 'utf8');
612
+ expectCode(baseLint(root, base).diagnostics, 'immutable-revision-modified');
613
+ });
614
+
615
+ // Owner case 2 — drafts remain editable but never displace the current
616
+ // effective revision; rejected drafts are never selectable.
617
+ checkCase('2ay case 2. a complete draft stays provisional while the previous effective revision remains current', () => {
618
+ const first = revisionDoc({ revision: 1, status: 'effective', body: fixtureBody('r001.body.md') });
619
+ const second = revisionDoc({ revision: 2, status: 'draft', previousRevision: 1, body: fixtureBody('r002-one-replaced.body.md') }, first);
620
+ const docs = [first, second];
621
+ assertResolve(docs, 'PRD-GRPH-001', { path: first.path, revision: 1, status: 'effective' });
622
+ assertResolve(docs, pin('PRD-GRPH-001', 2), { path: second.path, revision: 2, status: 'draft', digest: true });
623
+ });
624
+
625
+ checkCase('2ay case 2. rejected revisions never become current', () => {
626
+ const first = revisionDoc({ revision: 1, status: 'effective' });
627
+ const rejected = revisionDoc({ revision: 2, status: 'rejected', previousRevision: 1 }, first);
628
+ assertResolve([first, rejected], 'PRD-GRPH-001', { path: first.path, revision: 1, status: 'effective' });
629
+ });
630
+
631
+ checkCase('2ay case 2. withdrawn revisions and pre-activation snapshots never become current', () => {
632
+ const draftBase = revisionDoc({ revision: 1, status: 'draft' });
633
+ const withdrawn = revisionDoc({ revision: 2, status: 'withdrawn', previousRevision: 1 }, draftBase);
634
+ const docs = [draftBase, withdrawn];
635
+ const current = lifecycle.resolveRef(docs, 'PRD-GRPH-001');
636
+ assert.ok(!current.doc, `zero-effective state is explicit and has no current: ${JSON.stringify(current)}`);
637
+
638
+ const effective = revisionDoc({ revision: 1, status: 'effective' });
639
+ const withdrawnChild = revisionDoc({ revision: 2, status: 'withdrawn', previousRevision: 1 }, effective);
640
+ const onlyEffective = lifecycle.resolveRef([effective, withdrawnChild], 'PRD-GRPH-001');
641
+ assert.strictEqual(onlyEffective.doc && onlyEffective.doc.path, effective.path, 'withdrawn child cannot displace the current effective revision');
642
+ });
643
+
644
+ checkCase('2ay case 2. draft-local snapshot validation still reports an empty acceptance block', () => {
645
+ const first = revisionDoc({ revision: 1, status: 'effective', body: fixtureBody('r001.body.md') });
646
+ const incomplete = revisionDoc({ revision: 2, status: 'draft', previousRevision: 1, body: fixtureBody('r002-incomplete.body.md') }, first);
647
+ expectCode(lintDocs([first, incomplete]), 'incomplete-snapshot');
648
+ });
649
+
650
+ checkCase('2ay fix. a draft revision declares its own REQs without producing missing-req references', () => {
651
+ const draft = revisionDoc({
652
+ revision: 1,
653
+ status: 'draft',
654
+ body: '### REQ-GRPH-055: Draft-local behavior\n\nAcceptance: keep this declaration visible to reviewers.\n'
655
+ });
656
+ const diagnostics = lintDocs([draft]);
657
+ forbidCode(diagnostics, 'missing-req');
658
+ forbidCode(diagnostics, 'unresolved-ref');
659
+ });
660
+
661
+ // Owner case 3 — approval is content-bound; activation is a separate,
662
+ // allowlisted transition and a stale parent cannot be activated.
663
+ checkCase('2ay case 3. approval alone does not change the current effective revision', () => {
664
+ const first = revisionDoc({ revision: 1, status: 'effective' });
665
+ const approved = revisionDoc({ revision: 2, status: 'approved', previousRevision: 1 }, first);
666
+ assertResolve([first, approved], 'PRD-GRPH-001', { path: first.path, revision: 1, status: 'effective' });
667
+ assertResolve([first, approved], pin('PRD-GRPH-001', 2), { path: approved.path, revision: 2, status: 'approved', digest: true });
668
+ });
669
+
670
+ checkCase('2ay case 3. approved-to-effective activation changes only allowlisted fields against a trusted Git base', () => {
671
+ const root = gitRepo('approved-to-effective-activation');
672
+ const first = revisionDoc({ revision: 1, status: 'effective', relativePath: 'docs/prd/sample/r001.md' });
673
+ const second = revisionDoc({ revision: 2, status: 'approved', previousRevision: 1, relativePath: 'docs/prd/sample/r002.md' }, first);
674
+ writeDoc(root, first.path, first.frontmatter, first.body);
675
+ writeDoc(root, second.path, second.frontmatter, second.body);
676
+ writeDoc(root, 'docs/approvals/prd-review.md', { id: 'APPROVAL-001', status: 'final' }, '# Independent review record\n');
677
+ const base = commitAll(root, 'effective r1 and approved r2');
678
+ const r1BlobBefore = git(root, ['rev-parse', `${base}:${first.path}`]);
679
+ mutateRevision(root, { status: 'effective', effective_at: '2026-10-17T00:02:00Z' }, undefined, second.path);
680
+ const output = baseLint(root, base);
681
+ assert.strictEqual(output.evidence.immutability, 'observed');
682
+ assert.deepStrictEqual(output.diagnostics.filter(item => item.severity === 'error'), [], `valid activation must lint cleanly: ${JSON.stringify(output)}`);
683
+ const r1BlobAfter = git(root, ['hash-object', `--path=${first.path}`, first.path]);
684
+ assert.strictEqual(r1BlobAfter, r1BlobBefore, 'the previous effective artifact retains the same Git blob identity');
685
+ const current = lifecycle.resolveRef(lifecycle.collectDocs(root, CONFIG), 'PRD-GRPH-001');
686
+ assert.strictEqual(current.doc && current.doc.path, second.path, 'the new effective revision becomes the unique current');
687
+ assert.strictEqual(current.revision, 2);
688
+ assert.strictEqual(current.status, 'effective');
689
+ assert.strictEqual(current.digest, sha256(fs.readFileSync(path.join(root, ...second.path.split('/')))), 'resolver reports complete candidate artifact bytes');
690
+ });
691
+
692
+ checkCase('2ay review2 P1. approved revision cannot move while activating or remain approved at a new path', () => {
693
+ const makeApprovedRepo = name => {
694
+ const root = gitRepo(name);
695
+ const first = revisionDoc({ revision: 1, status: 'effective', relativePath: 'docs/prd/sample/r001.md' });
696
+ const second = revisionDoc({ revision: 2, status: 'approved', previousRevision: 1, relativePath: 'docs/prd/sample/r002.md' }, first);
697
+ writeDoc(root, first.path, first.frontmatter, first.body);
698
+ writeDoc(root, second.path, second.frontmatter, second.body);
699
+ writeDoc(root, 'docs/approvals/prd-review.md', { id: 'APPROVAL-001', status: 'final' }, '# Independent review record\n');
700
+ return { root, second, base: commitAll(root, 'r1 effective and r2 approved') };
701
+ };
702
+
703
+ const activating = makeApprovedRepo('approved-path-move-on-activation');
704
+ const movedPath = 'docs/prd/sample/moved-r002.md';
705
+ fs.mkdirSync(path.dirname(path.join(activating.root, movedPath)), { recursive: true });
706
+ fs.renameSync(path.join(activating.root, ...activating.second.path.split('/')), path.join(activating.root, ...movedPath.split('/')));
707
+ mutateRevision(activating.root, { status: 'effective', effective_at: '2026-10-17T00:02:00Z' }, undefined, movedPath);
708
+ expectCode(baseLint(activating.root, activating.base).diagnostics, 'invalid-transition', 'activation preserves the trusted approved baseline path');
709
+
710
+ const remainingApproved = makeApprovedRepo('approved-path-move-still-approved');
711
+ const approvedMovedPath = 'docs/prd/sample/moved-r002.md';
712
+ fs.mkdirSync(path.dirname(path.join(remainingApproved.root, approvedMovedPath)), { recursive: true });
713
+ fs.renameSync(path.join(remainingApproved.root, ...remainingApproved.second.path.split('/')), path.join(remainingApproved.root, ...approvedMovedPath.split('/')));
714
+ expectCode(baseLint(remainingApproved.root, remainingApproved.base).diagnostics, 'invalid-transition', 'an approved revision cannot move while retaining approved status');
715
+ });
716
+
717
+ checkCase('2ay case 3. missing approval is diagnosed independently from digest mismatch', () => {
718
+ const missing = revisionDoc({ revision: 1, status: 'effective', approval: undefined });
719
+ delete missing.frontmatter.approval;
720
+ missing.storedBytes = Buffer.from(renderSource(missing.frontmatter, missing.body), 'utf8');
721
+ expectOnlyCodes(lintDocs([missing]), ['approval-missing']);
722
+ });
723
+
724
+ checkCase('2ay case 3. content changes between approval and activation violate the activation field allowlist', () => {
725
+ const { root, base } = seedApprovedRepo('activation-allowlist');
726
+ mutateRevision(root, { status: 'effective', effective_at: '2026-10-17T00:02:00Z' }, '# Changed while activating\n\nAcceptance: this was not approved.\n');
727
+ const diagnostics = baseLint(root, base).diagnostics;
728
+ expectCode(diagnostics, 'invalid-transition');
729
+ expectCode(diagnostics, 'approval-content-mismatch');
730
+ });
731
+
732
+ checkCase('2ay case 3. metadata-only approved-to-effective changes violate the activation allowlist', () => {
733
+ const root = gitRepo('activation-metadata-only-change');
734
+ const first = revisionDoc({ revision: 1, status: 'effective', relativePath: 'docs/prd/sample/r001.md' });
735
+ const second = revisionDoc({ revision: 2, status: 'approved', previousRevision: 1, release_channel: 'stable', relativePath: 'docs/prd/sample/r002.md' }, first);
736
+ writeDoc(root, first.path, first.frontmatter, first.body);
737
+ writeDoc(root, second.path, second.frontmatter, second.body);
738
+ writeDoc(root, 'docs/approvals/prd-review.md', { id: 'APPROVAL-001', status: 'final' }, '# Independent review record\n');
739
+ const base = commitAll(root, 'r1 effective and r2 approved');
740
+ mutateRevision(root, { status: 'effective', effective_at: '2026-10-17T00:02:00Z', release_channel: 'beta' }, undefined, second.path);
741
+ const diagnostics = baseLint(root, base).diagnostics;
742
+ expectCode(diagnostics, 'invalid-transition');
743
+ expectCode(diagnostics, 'approval-content-mismatch');
744
+ });
745
+
746
+ checkCase('2ay case 3. replacing approval evidence and rebinding its digest cannot authorize an unapproved metadata edit', () => {
747
+ const root = gitRepo('activation-forged-approval');
748
+ const first = revisionDoc({ revision: 1, status: 'effective', relativePath: 'docs/prd/sample/r001.md' });
749
+ const second = revisionDoc({ revision: 2, status: 'approved', previousRevision: 1, release_channel: 'stable', relativePath: 'docs/prd/sample/r002.md' }, first);
750
+ writeDoc(root, first.path, first.frontmatter, first.body);
751
+ writeDoc(root, second.path, second.frontmatter, second.body);
752
+ writeDoc(root, 'docs/approvals/prd-review.md', { id: 'APPROVAL-001', status: 'final' }, '# Independent review record\n');
753
+ const base = commitAll(root, 'r1 effective and r2 approved');
754
+ const candidate = { ...second.frontmatter, status: 'effective', effective_at: '2026-10-17T00:02:00Z', release_channel: 'beta' };
755
+ candidate.approval = { ...second.frontmatter.approval, by: 'Replacement Reviewer' };
756
+ candidate.approval.content_digest = approvalContentDigest(candidate, second.body);
757
+ writeDoc(root, second.path, candidate, second.body);
758
+ const diagnostics = baseLint(root, base).diagnostics;
759
+ expectOnlyCodes(diagnostics.filter(item => ['invalid-transition', 'approval-content-mismatch'].includes(item.code)), ['invalid-transition']);
760
+ });
761
+
762
+ checkCase('2ay case 3. approved draft based on a stale current revision cannot activate from trusted base', () => {
763
+ const root = gitRepo('stale-draft-base');
764
+ const first = revisionDoc({ revision: 1, status: 'effective', relativePath: 'docs/prd/sample/r001.md' });
765
+ const current = revisionDoc({ revision: 3, status: 'effective', previousRevision: 1, relativePath: 'docs/prd/sample/r003.md' }, first);
766
+ const staleApproved = revisionDoc({ revision: 2, status: 'approved', previousRevision: 1, relativePath: 'docs/prd/sample/r002.md' }, first);
767
+ for (const doc of [first, current, staleApproved]) writeDoc(root, doc.path, doc.frontmatter, doc.body);
768
+ writeDoc(root, 'docs/approvals/prd-review.md', { id: 'APPROVAL-001', status: 'final' }, '# Independent review record\n');
769
+ const base = commitAll(root, 'current r3 and approved stale r2');
770
+ const baseCurrent = lifecycle.resolveRef(lifecycle.collectDocs(root, CONFIG), 'PRD-GRPH-001');
771
+ assert.strictEqual(baseCurrent.doc && baseCurrent.doc.path, current.path, 'valid base selects r3 while r2 is only approved');
772
+ mutateRevision(root, { status: 'effective', effective_at: '2026-10-17T00:02:00Z' }, undefined, staleApproved.path);
773
+ const result = baseLint(root, base);
774
+ const stale = expectCode(result.diagnostics, 'stale-draft-base');
775
+ assert.ok(stale.some(item => item.path === staleApproved.path && item.id === 'PRD-GRPH-001'), 'stale activation is anchored to its declaring revision');
776
+ expectCode(result.diagnostics, 'revision-fork');
777
+ const invalidCurrent = lifecycle.resolveRef(lifecycle.collectDocs(root, CONFIG), 'PRD-GRPH-001');
778
+ assert.ok(!invalidCurrent.doc, `invalid candidate graph has no current; resolver must not choose a sibling: ${JSON.stringify(invalidCurrent)}`);
779
+ });
780
+
781
+ // Owner case 4 — complete snapshots preserve nine REQs, explicitly replace
782
+ // one, and reject dropped/incomplete or malformed replacement declarations.
783
+ checkCase('2ay case 4. activated ten-REQ revision preserves, replaces, and resolves requirement continuity', () => {
784
+ const parentBody = fixtureBody('r001.body.md');
785
+ const childBody = fixtureBody('r002-one-replaced.body.md');
786
+ const parentIds = revisionIds(parentBody);
787
+ const childIds = revisionIds(childBody);
788
+ assert.strictEqual(parentIds.length, 10);
789
+ assert.strictEqual(childIds.length, 10);
790
+ assert.deepStrictEqual(childIds.filter(id => parentIds.includes(id)), parentIds.slice(0, 9));
791
+ const parent = revisionDoc({ revision: 1, status: 'effective', body: parentBody });
792
+ const child = revisionDoc({ revision: 2, status: 'effective', previousRevision: 1, body: childBody }, parent);
793
+ assert.deepStrictEqual(analyzeDocs([parent, child]).diagnostics.filter(item => item.severity === 'error'), [], 'the complete replacement chain is structurally valid');
794
+ const diff = diffRevisions([parent, child], pin('PRD-GRPH-001', 1), pin('PRD-GRPH-001', 2));
795
+ assert.deepStrictEqual(diff.requirements.kept, parentIds.slice(0, 9));
796
+ assert.deepStrictEqual(diff.requirements.replaced, [{ from: 'REQ-GRPH-019', to: 'REQ-GRPH-020' }]);
797
+ assert.deepStrictEqual(diff.requirements.changed, []);
798
+ assert.deepStrictEqual(diff.requirements.removed, []);
799
+ assert.deepStrictEqual(diff.requirements.added, []);
800
+ assert.ok(!JSON.stringify(diff).includes('supersedes'), 'REQ replacement must not become whole-PRD supersession');
801
+ for (const id of parentIds.slice(0, 9)) {
802
+ const resolved = lifecycle.resolveRef([parent, child], id);
803
+ assert.strictEqual(resolved.doc && resolved.doc.path, child.path, `${id} plain ref resolves in the current revision`);
804
+ assert.strictEqual(resolved.requirement && resolved.requirement.id, id);
805
+ }
806
+ const pinnedReplaced = lifecycle.resolveRef([parent, child], pin('PRD-GRPH-001', 1, 'REQ-GRPH-019'));
807
+ assert.strictEqual(pinnedReplaced.doc && pinnedReplaced.doc.path, parent.path, 'revision-pinned replaced REQ remains addressable historically');
808
+ assert.strictEqual(pinnedReplaced.requirement && pinnedReplaced.requirement.id, 'REQ-GRPH-019');
809
+ const plainReplaced = lifecycle.resolveRef([parent, child], 'REQ-GRPH-019');
810
+ assert.ok(!plainReplaced.doc, `plain ref to replaced REQ does not auto-follow: ${JSON.stringify(plainReplaced)}`);
811
+ assert.match(plainReplaced.message || '', /REQ-GRPH-020|replac/i, 'unresolved plain ref carries a historical replacement hint');
812
+ });
813
+
814
+ checkCase('2ay case 4. a missing parent REQ is req-dropped unless removed_reqs accounts for it', () => {
815
+ const parent = revisionDoc({ revision: 1, status: 'draft', body: fixtureBody('r001.body.md') });
816
+ const dropped = revisionDoc({ revision: 2, status: 'draft', previousRevision: 1, body: fixtureBody('r002-dropped.body.md') }, parent);
817
+ expectCode(lintDocs([parent, dropped]), 'req-dropped');
818
+ const removed = revisionDoc({ revision: 2, status: 'draft', previousRevision: 1, removed_reqs: ['REQ-GRPH-019'], body: fixtureBody('r002-dropped.body.md') }, parent);
819
+ forbidCode(lintDocs([parent, removed]), 'req-dropped');
820
+ const removalDiff = diffRevisions([parent, removed], pin('PRD-GRPH-001', 1), pin('PRD-GRPH-001', 2));
821
+ assert.deepStrictEqual(removalDiff.requirements.kept, ['REQ-GRPH-010', 'REQ-GRPH-011', 'REQ-GRPH-012', 'REQ-GRPH-013', 'REQ-GRPH-014', 'REQ-GRPH-015', 'REQ-GRPH-016', 'REQ-GRPH-017', 'REQ-GRPH-018']);
822
+ assert.deepStrictEqual(removalDiff.requirements.removed, ['REQ-GRPH-019']);
823
+ assert.deepStrictEqual(removalDiff.requirements.added, []);
824
+ assert.deepStrictEqual(removalDiff.requirements.changed, []);
825
+ assert.deepStrictEqual(removalDiff.requirements.replaced, []);
826
+ });
827
+
828
+ checkCase('2ay case 4. each REQ block requires non-empty acceptance content', () => {
829
+ const parent = revisionDoc({ revision: 1, status: 'draft', body: fixtureBody('r001.body.md') });
830
+ const child = revisionDoc({ revision: 2, status: 'draft', previousRevision: 1, body: fixtureBody('r002-incomplete.body.md') }, parent);
831
+ expectCode(lintDocs([parent, child]), 'incomplete-snapshot');
832
+ });
833
+
834
+ checkCase('2ay case 4. replacement grammar supports split and merge edges', () => {
835
+ const parentBody = fixtureBody('r001.body.md');
836
+ const splitBody = `${parentBody.replace(/### REQ-GRPH-019:[\s\S]*$/, '')}### REQ-GRPH-020 (replaces: REQ-GRPH-019): Error category\n\nAcceptance: classify each failure.\n\n### REQ-GRPH-021 (replaces: REQ-GRPH-019): Recovery action\n\nAcceptance: provide a recovery action.\n`;
837
+ const parent = revisionDoc({ revision: 1, status: 'draft', body: parentBody });
838
+ const child = revisionDoc({ revision: 2, status: 'draft', previousRevision: 1, body: splitBody }, parent);
839
+ const diagnostics = lintDocs([parent, child]);
840
+ assert.deepStrictEqual(diagnostics.filter(item => item.severity === 'error'), [], 'valid split snapshot has no structural errors');
841
+ const diff = diffRevisions([parent, child], pin('PRD-GRPH-001', 1), pin('PRD-GRPH-001', 2));
842
+ assert.deepStrictEqual(diff.requirements.kept, ['REQ-GRPH-010', 'REQ-GRPH-011', 'REQ-GRPH-012', 'REQ-GRPH-013', 'REQ-GRPH-014', 'REQ-GRPH-015', 'REQ-GRPH-016', 'REQ-GRPH-017', 'REQ-GRPH-018']);
843
+ assert.deepStrictEqual(diff.requirements.replaced, [
844
+ { from: 'REQ-GRPH-019', to: 'REQ-GRPH-020' },
845
+ { from: 'REQ-GRPH-019', to: 'REQ-GRPH-021' }
846
+ ]);
847
+ assert.deepStrictEqual(diff.requirements.changed, []);
848
+ assert.deepStrictEqual(diff.requirements.removed, []);
849
+ assert.deepStrictEqual(diff.requirements.added, []);
850
+
851
+ const mergeBody = `${parentBody.replace(/### REQ-GRPH-018:[\s\S]*$/, '')}### REQ-GRPH-020 (replaces: REQ-GRPH-018, REQ-GRPH-019): Combined export\n\nAcceptance: preserve export and recovery behavior.\n`;
852
+ const merge = revisionDoc({ revision: 2, status: 'draft', previousRevision: 1, body: mergeBody }, parent);
853
+ assert.deepStrictEqual(lintDocs([parent, merge]).filter(item => item.severity === 'error'), [], 'valid merge snapshot has no structural errors');
854
+ const merged = diffRevisions([parent, merge], pin('PRD-GRPH-001', 1), pin('PRD-GRPH-001', 2));
855
+ assert.deepStrictEqual(merged.requirements.kept, ['REQ-GRPH-010', 'REQ-GRPH-011', 'REQ-GRPH-012', 'REQ-GRPH-013', 'REQ-GRPH-014', 'REQ-GRPH-015', 'REQ-GRPH-016', 'REQ-GRPH-017']);
856
+ assert.deepStrictEqual(merged.requirements.replaced, [
857
+ { from: 'REQ-GRPH-018', to: 'REQ-GRPH-020' },
858
+ { from: 'REQ-GRPH-019', to: 'REQ-GRPH-020' }
859
+ ]);
860
+ assert.deepStrictEqual(merged.requirements.changed, []);
861
+ assert.deepStrictEqual(merged.requirements.removed, []);
862
+ assert.deepStrictEqual(merged.requirements.added, []);
863
+ });
864
+
865
+ checkCase('2ay case 4. replacement cannot also claim the replaced REQ as an alias', () => {
866
+ const parent = revisionDoc({ revision: 1, status: 'draft', body: fixtureBody('r001.body.md') });
867
+ const child = revisionDoc({ revision: 2, status: 'draft', previousRevision: 1, body: fixtureBody('r002-overlap.body.md') }, parent);
868
+ expectCode(lintDocs([parent, child]), 'replacement-alias-overlap');
869
+ });
870
+
871
+ checkCase('2ay case 4. duplicate REQs within one revision and across distinct current PRDs are anchored on every involved document', () => {
872
+ const duplicateWithinOne = revisionDoc({
873
+ revision: 1,
874
+ status: 'draft',
875
+ body: `${fixtureBody('r001.body.md')}\n### REQ-GRPH-010: Duplicate in same snapshot\n\nAcceptance: expose duplicate identity.\n`
876
+ });
877
+ const withinDiagnostics = lintDocs([duplicateWithinOne]);
878
+ const within = expectCode(withinDiagnostics, 'duplicate-req');
879
+ assert.ok(within.some(item => item.path === duplicateWithinOne.path && item.id === 'REQ-GRPH-010'), 'same-revision duplicate points at its PRD');
880
+
881
+ const graph = revisionDoc({ id: 'PRD-GRPH-001', revision: 1, status: 'effective', body: '### REQ-GRPH-099: Shared current requirement\n\nAcceptance: one owner.\n' });
882
+ const interfaceDoc = revisionDoc({ id: 'PRD-IFCE-001', revision: 1, status: 'effective', relativePath: 'docs/prd/interface/r001.md', body: '### REQ-GRPH-099: Shared current requirement\n\nAcceptance: conflicting owner.\n' });
883
+ const cross = expectCode(lintDocs([graph, interfaceDoc]), 'duplicate-req');
884
+ assert.deepStrictEqual(cross.map(item => item.path), [graph.path, interfaceDoc.path].sort());
885
+ const candidatesByPath = Object.fromEntries(cross.map(item => [item.path, item.candidates]));
886
+ assert.deepStrictEqual(candidatesByPath[graph.path], [interfaceDoc.path]);
887
+ assert.deepStrictEqual(candidatesByPath[interfaceDoc.path], [graph.path]);
888
+ assert.ok(cross.every(item => item.id === 'REQ-GRPH-099'), 'each document diagnostic identifies the colliding REQ');
889
+ });
890
+
891
+ checkCase('2ay case 4. repeated REQ identity across revisions of one logical PRD is valid history', () => {
892
+ const first = revisionDoc({ revision: 1, status: 'effective', body: fixtureBody('r001.body.md') });
893
+ const second = revisionDoc({ revision: 2, status: 'effective', previousRevision: 1, body: fixtureBody('r001.body.md') }, first);
894
+ const diagnostics = lintDocs([first, second]);
895
+ forbidCode(diagnostics, 'duplicate-req');
896
+ forbidCode(diagnostics, 'duplicate-id');
897
+ forbidCode(diagnostics, 'alias-collision');
898
+ });
899
+
900
+ checkCase('2ay case 4. cross-PRD alias collision is emitted on every involved document with counterpart candidates', () => {
901
+ const first = revisionDoc({ id: 'PRD-GRPH-001', revision: 1, status: 'effective', aliases: ['PRD-IFCE-002'] });
902
+ const second = revisionDoc({ id: 'PRD-IFCE-002', revision: 1, status: 'effective', relativePath: 'docs/prd/interface/r001.md' });
903
+ const collisions = expectCode(lintDocs([first, second]), 'alias-collision');
904
+ assert.deepStrictEqual(collisions.map(item => item.path), [first.path, second.path].sort());
905
+ const candidatesByPath = Object.fromEntries(collisions.map(item => [item.path, item.candidates]));
906
+ assert.deepStrictEqual(candidatesByPath[first.path], [second.path]);
907
+ assert.deepStrictEqual(candidatesByPath[second.path], [first.path]);
908
+ assert.ok(collisions.every(item => item.id === 'PRD-IFCE-002'), 'each collision diagnostic identifies the conflicting name');
909
+ });
910
+
911
+ checkCase('2ay case 4. replacement cycles across valid parent revisions are diagnosed', () => {
912
+ const firstBody = '### REQ-GRPH-010: Original A\n\nAcceptance: preserve A.\n\n### REQ-GRPH-011: Stable\n\nAcceptance: preserve stable.\n';
913
+ const secondBody = '### REQ-GRPH-011: Stable\n\nAcceptance: preserve stable.\n\n### REQ-GRPH-020 (replaces: REQ-GRPH-010): Replacement B\n\nAcceptance: replace A.\n';
914
+ const thirdBody = '### REQ-GRPH-011: Stable\n\nAcceptance: preserve stable.\n\n### REQ-GRPH-010 (replaces: REQ-GRPH-020): Reused A\n\nAcceptance: form a replacement cycle.\n';
915
+ const first = revisionDoc({ revision: 1, status: 'draft', body: firstBody });
916
+ const second = revisionDoc({ revision: 2, status: 'draft', previousRevision: 1, body: secondBody }, first);
917
+ const third = revisionDoc({ revision: 3, status: 'draft', previousRevision: 2, body: thirdBody }, second);
918
+ expectCode(lintDocs([first, second, third]), 'replacement-cycle');
919
+ });
920
+
921
+ checkCase('2ay case 4. duplicate, unknown, malformed, and empty clauses fail independently', () => {
922
+ const parent = revisionDoc({ revision: 1, status: 'draft', body: fixtureBody('r001.body.md') });
923
+ const invalidBodies = [
924
+ `${fixtureBody('r001.body.md')}\n### REQ-GRPH-020 (replaces: REQ-GRPH-019; replaces: REQ-GRPH-018): Duplicate clause\n\nAcceptance: reject duplicate clauses.\n`,
925
+ `${fixtureBody('r001.body.md')}\n### REQ-GRPH-020 (mystery: yes): Unknown clause\n\nAcceptance: reject unknown clauses.\n`,
926
+ `${fixtureBody('r001.body.md')}\n### REQ-GRPH-020 (replaces REQ-GRPH-019): Malformed clause\n\nAcceptance: reject malformed clauses.\n`,
927
+ `${fixtureBody('r001.body.md')}\n### REQ-GRPH-020 (replaces: ): Empty clause\n\nAcceptance: reject empty clauses.\n`
928
+ ];
929
+ for (const [index, body] of invalidBodies.entries()) {
930
+ const child = revisionDoc({ revision: 2, status: 'draft', previousRevision: 1, body, relativePath: `docs/prd/sample/invalid-clause-${index}.md` }, parent);
931
+ expectOnlyCodes(lintDocs([parent, child]), ['invalid-req-clause']);
932
+ }
933
+ });
934
+
935
+ checkCase('2ay case 4. changed normative text under a stable REQ ID requires review classification', () => {
936
+ const firstBody = fixtureBody('r001.body.md');
937
+ const revisedBody = firstBody.replace('Acceptance: every declared vertex is drawn.', 'Acceptance: hidden vertices are always drawn.');
938
+ const first = revisionDoc({ revision: 1, status: 'effective', body: firstBody });
939
+ const second = revisionDoc({ revision: 2, status: 'draft', previousRevision: 1, body: revisedBody }, first);
940
+ const diff = diffRevisions([first, second], pin('PRD-GRPH-001', 1), pin('PRD-GRPH-001', 2));
941
+ const changed = diff.requirements.changed.find(item => item.id === 'REQ-GRPH-010');
942
+ assert.ok(changed, 'the same ID with changed normative acceptance must be visible in the diff');
943
+ assert.strictEqual(changed.reviewRequired, true, 'ID continuity is structural evidence, not semantic approval');
944
+ assert.deepStrictEqual(diff.requirements.kept, ['REQ-GRPH-011', 'REQ-GRPH-012', 'REQ-GRPH-013', 'REQ-GRPH-014', 'REQ-GRPH-015', 'REQ-GRPH-016', 'REQ-GRPH-017', 'REQ-GRPH-018', 'REQ-GRPH-019']);
945
+ assert.deepStrictEqual(diff.requirements.changed.map(item => item.id), ['REQ-GRPH-010']);
946
+ assert.deepStrictEqual(diff.requirements.added, []);
947
+ assert.deepStrictEqual(diff.requirements.removed, []);
948
+ assert.deepStrictEqual(diff.requirements.replaced, []);
949
+ });
950
+
951
+ // Owner case 5 — graph validation must finish before current selection.
952
+ checkCase('2ay case 5. duplicate logical revision pairs are diagnosed', () => {
953
+ const one = revisionDoc({ revision: 1, status: 'draft', relativePath: 'docs/prd/sample/a.md' });
954
+ const two = revisionDoc({ revision: 1, status: 'draft', relativePath: 'docs/prd/sample/b.md' });
955
+ expectCode(lintDocs([one, two]), 'duplicate-revision');
956
+ const invalidCurrent = lifecycle.resolveRef([one, two], 'PRD-GRPH-001');
957
+ assert.ok(!invalidCurrent.doc, 'duplicate revision identity prevents arbitrary current selection');
958
+ });
959
+
960
+ checkCase('2ay case 5. missing and cross-logical parents are diagnosed', () => {
961
+ const missing = revisionDoc({ revision: 2, status: 'draft', previousRevision: 1 });
962
+ expectCode(lintDocs([missing]), 'missing-parent');
963
+ assert.ok(!lifecycle.resolveRef([missing], 'PRD-GRPH-001').doc, 'missing parent leaves current undefined');
964
+ const otherRoot = revisionDoc({ id: 'PRD-IFCE-001', revision: 1, status: 'draft' });
965
+ const wrongParent = revisionDoc({ id: 'PRD-GRPH-001', revision: 2, status: 'draft', previousRevision: 1 });
966
+ expectCode(lintDocs([otherRoot, wrongParent]), 'missing-parent');
967
+ assert.ok(!lifecycle.resolveRef([otherRoot, wrongParent], 'PRD-GRPH-001').doc, 'cross-logical parent cannot supply a current chain');
968
+ });
969
+
970
+ checkCase('2ay case 5. multiple baselines and revision cycles are diagnosed', () => {
971
+ const baselineOne = revisionDoc({ revision: 1, status: 'draft', relativePath: 'docs/prd/sample/r001.md' });
972
+ const baselineTwo = revisionDoc({ revision: 2, status: 'draft', relativePath: 'docs/prd/sample/r002.md' });
973
+ expectCode(lintDocs([baselineOne, baselineTwo]), 'multiple-baselines');
974
+ assert.ok(!lifecycle.resolveRef([baselineOne, baselineTwo], 'PRD-GRPH-001').doc, 'multiple baselines prevent current selection');
975
+ const cycleOne = revisionDoc({ revision: 1, status: 'draft', previousRevision: 2, relativePath: 'docs/prd/sample/cycle-1.md' });
976
+ const cycleTwo = revisionDoc({ revision: 2, status: 'draft', previousRevision: 1, relativePath: 'docs/prd/sample/cycle-2.md' });
977
+ expectCode(lintDocs([cycleOne, cycleTwo]), 'revision-cycle');
978
+ assert.ok(!lifecycle.resolveRef([cycleOne, cycleTwo], 'PRD-GRPH-001').doc, 'cyclic graph prevents current selection');
979
+ });
980
+
981
+ checkCase('2ay case 5. parent revision must be lower and an invalid effective chain has no current', () => {
982
+ const later = revisionDoc({ revision: 2, status: 'effective' });
983
+ const lowerChild = revisionDoc({ revision: 1, status: 'effective', previousRevision: 2, relativePath: 'docs/prd/sample/r001-child.md' }, later);
984
+ expectCode(lintDocs([later, lowerChild]), 'missing-parent');
985
+ assert.ok(!lifecycle.resolveRef([later, lowerChild], 'PRD-GRPH-001').doc, 'decreasing parent edge does not pick either effective node');
986
+ });
987
+
988
+ checkCase('2ay case 5. effective sibling revisions form a diagnosed fork, with no arbitrary current', () => {
989
+ const first = revisionDoc({ revision: 1, status: 'effective' });
990
+ const left = revisionDoc({ revision: 2, status: 'effective', previousRevision: 1, relativePath: 'docs/prd/sample/left.md' }, first);
991
+ const right = revisionDoc({ revision: 3, status: 'effective', previousRevision: 1, relativePath: 'docs/prd/sample/right.md' }, first);
992
+ const diagnostics = lintDocs([first, left, right]);
993
+ expectCode(diagnostics, 'revision-fork');
994
+ const current = lifecycle.resolveRef([first, left, right], 'PRD-GRPH-001');
995
+ assert.ok(!current.doc, `invalid graph must not choose a current revision: ${JSON.stringify(current)}`);
996
+ });
997
+
998
+ checkCase('2ay case 5. zero, leading-zero, fractional, and string revisions are invalid', () => {
999
+ const docs = [
1000
+ revisionDoc({ revision: 0, status: 'draft', relativePath: 'docs/prd/sample/zero.md' }),
1001
+ revisionDoc({ revision: '01', status: 'draft', relativePath: 'docs/prd/sample/leading-zero.md' }),
1002
+ revisionDoc({ revision: 1.5, status: 'draft', relativePath: 'docs/prd/sample/fraction.md' }),
1003
+ revisionDoc({ revision: '2', status: 'draft', relativePath: 'docs/prd/sample/string.md' })
1004
+ ];
1005
+ assert.ok(matching(lintDocs(docs), 'invalid-revision').length >= 4);
1006
+ });
1007
+
1008
+ // Owner case 6 — namespace-first, whole-token reference parsing. Legacy path
1009
+ // selectors retain @ and # literally.
1010
+ checkCase('2ay case 6. current, revision-pinned, and pinned-REQ references resolve to exact provenance', () => {
1011
+ const firstBody = '### REQ-GRPH-012 (aliases: REQ-GRAPH-012): Stable requirement\n\nAcceptance: preserve the behavior.\n';
1012
+ const secondBody = '### REQ-GRPH-012 (aliases: REQ-GRAPH-012, REQ-GRPH-099): Stable requirement\n\nAcceptance: preserve the behavior.\n';
1013
+ const first = revisionDoc({ revision: 1, status: 'effective', aliases: ['PRD-GRPH-009'], body: firstBody });
1014
+ const second = revisionDoc({ revision: 2, status: 'effective', previousRevision: 1, aliases: ['PRD-GRPH-009'], body: secondBody }, first);
1015
+ const docs = [first, second];
1016
+ assertResolve(docs, 'PRD-GRPH-001', { path: second.path, revision: 2, status: 'effective', digest: true, provenance: true });
1017
+ assertResolve(docs, pin('PRD-GRPH-001', 1), { path: first.path, revision: 1, status: 'effective', digest: true, provenance: true });
1018
+ assertResolve(docs, pin('PRD-GRPH-001', 1, 'REQ-GRAPH-012'), { path: first.path, revision: 1, status: 'effective', requirement: 'REQ-GRPH-012', digest: true, provenance: true });
1019
+ assertResolve(docs, pin('PRD-GRPH-001', 2, 'REQ-GRPH-099'), { path: second.path, revision: 2, status: 'effective', requirement: 'REQ-GRPH-012', digest: true, provenance: true });
1020
+ const historicalAlias = lifecycle.resolveRef(docs, pin('PRD-GRPH-001', 1, 'REQ-GRPH-099'));
1021
+ assert.ok(!historicalAlias.doc, 'an alias declared only in r2 does not resolve as of r1');
1022
+ const diagnostics = lintDocs(docs);
1023
+ forbidCode(diagnostics, 'alias-collision');
1024
+ forbidCode(diagnostics, 'duplicate-req');
1025
+ });
1026
+
1027
+ checkCase('2ay case 6. draft pins are provisional and malformed suffixes never resolve their valid prefix', () => {
1028
+ const first = revisionDoc({ revision: 1, status: 'effective' });
1029
+ const draft = revisionDoc({ revision: 2, status: 'draft', previousRevision: 1, });
1030
+ const docs = [first, draft];
1031
+ assertResolve(docs, pin('PRD-GRPH-001', 2), { path: draft.path, revision: 2, status: 'draft' });
1032
+ for (const ref of ['PRD-GRPH-001@r2junk', 'PRD-GRPH-001@r02', 'PRD-GRPH-001@r2#REQ-GRPH-012suffix']) {
1033
+ const result = lifecycle.resolveRef(docs, ref);
1034
+ assert.ok(!result.doc, `${ref} must not fall through to a valid prefix: ${JSON.stringify(result)}`);
1035
+ assert.ok(['invalid-ref', 'unresolved-ref'].includes(result.error), `${ref} error category: ${JSON.stringify(result)}`);
1036
+ }
1037
+ });
1038
+
1039
+ checkCase('2ay case 6. legacy selectors preserve a path containing @ and #', () => {
1040
+ const legacy = docObject('docs/adr/legacy/GRAPH-009@old#record.md', { id: 'GRAPH-009', status: 'accepted' }, '# Frozen legacy record\n');
1041
+ legacy.legacy = true;
1042
+ const result = lifecycle.resolveRef([legacy], 'legacy:GRAPH-009@docs/adr/legacy/GRAPH-009@old#record.md');
1043
+ assert.strictEqual(result.doc && result.doc.path, legacy.path);
1044
+ });
1045
+
1046
+ // Owner case 7 — cross-PRD supersedes is an external retirement projection:
1047
+ // source and target artifacts stay unchanged, and splits/merges are addressable.
1048
+ checkCase('2ay case 7. cross-PRD retirement records exact pinned targets without mutating them', () => {
1049
+ const oldGraph = revisionDoc({ revision: 1, status: 'effective', relativePath: 'docs/prd/graph/r001.md' });
1050
+ const oldInterface = revisionDoc({ id: 'PRD-IFCE-001', revision: 1, status: 'effective', relativePath: 'docs/prd/interface/r001.md' });
1051
+ const newPlatform = revisionDoc({ id: 'PRD-GRPH-002', revision: 1, status: 'effective', relativePath: 'docs/prd/platform/r001.md', supersedes: ['PRD-GRPH-001@r1', 'PRD-IFCE-001@r1'] });
1052
+ const graphSplit = revisionDoc({ id: 'PRD-GRPH-003', revision: 1, status: 'effective', relativePath: 'docs/prd/graph-next/r001.md', supersedes: ['PRD-GRPH-001@r1'] });
1053
+ const root = path.join(tempRoot, 'retirement source target files');
1054
+ fs.mkdirSync(root, { recursive: true });
1055
+ for (const doc of [oldGraph, oldInterface, newPlatform, graphSplit]) writeDoc(root, doc.path, doc.frontmatter, doc.body);
1056
+ const before = new Map([oldGraph, oldInterface, newPlatform, graphSplit].map(doc => [doc.path, fs.readFileSync(path.join(root, ...doc.path.split('/')))]));
1057
+ const docs = lifecycle.collectDocs(root, CONFIG);
1058
+ const analysis = analyzeDocs(docs);
1059
+ assert.deepStrictEqual(analysis.diagnostics.filter(item => item.severity === 'error'), [], `cross-PRD revision retirement is valid: ${JSON.stringify(analysis.diagnostics)}`);
1060
+ const index = lifecycle.buildIndex(docs, CONFIG);
1061
+ const graphLine = index.split('\n').find(line => line.includes(oldGraph.path)) || '';
1062
+ const interfaceLine = index.split('\n').find(line => line.includes(oldInterface.path)) || '';
1063
+ assert.ok(graphLine.includes('PRD-GRPH-002') && graphLine.includes('PRD-GRPH-003'), `split successors for the exact retired graph revision are listed: ${graphLine}`);
1064
+ assert.ok(interfaceLine.includes('PRD-GRPH-002'), `merge successor for the exact retired interface revision is listed: ${interfaceLine}`);
1065
+ const declaredEdges = [
1066
+ ['PRD-GRPH-002', 'PRD-GRPH-001@r1'],
1067
+ ['PRD-GRPH-002', 'PRD-IFCE-001@r1'],
1068
+ ['PRD-GRPH-003', 'PRD-GRPH-001@r1']
1069
+ ];
1070
+ for (const [source, target] of declaredEdges) {
1071
+ const sourceDoc = docs.find(doc => doc.frontmatter.id === source);
1072
+ assert.ok(sourceDoc.frontmatter.supersedes.includes(target), `${source} stores exact edge to ${target}`);
1073
+ assert.ok(index.includes(source) && index.includes(target), `index preserves exact edge endpoints ${source} -> ${target}`);
1074
+ }
1075
+ assertResolve(docs, 'PRD-GRPH-001', { path: oldGraph.path, revision: 1, status: 'effective', digest: true });
1076
+ for (const [relative, bytes] of before) {
1077
+ assert.deepStrictEqual(fs.readFileSync(path.join(root, ...relative.split('/'))), bytes, `lint/index left stored target/source bytes unchanged for ${relative}`);
1078
+ }
1079
+ });
1080
+
1081
+ checkCase('2ay xreview T1. valid historical retirement remains issued by the effective prefix', () => {
1082
+ const target = revisionDoc({
1083
+ id: 'PRD-GRPH-041', revision: 1, status: 'effective',
1084
+ relativePath: 'docs/prd/retirement-target/r001.md'
1085
+ });
1086
+ const targetPin = pin(target.frontmatter.id, target.frontmatter.revision);
1087
+ const issuerR1 = revisionDoc({
1088
+ id: 'PRD-IFCE-041', revision: 1, status: 'effective',
1089
+ relativePath: 'docs/prd/retirement-issuer/r001.md', supersedes: [targetPin], conflicts_with: [targetPin]
1090
+ });
1091
+ const issuerR2 = revisionDoc({
1092
+ id: 'PRD-IFCE-041', revision: 2, previousRevision: 1, status: 'effective',
1093
+ relativePath: 'docs/prd/retirement-issuer/r002.md'
1094
+ }, issuerR1);
1095
+ const docs = [target, issuerR1, issuerR2];
1096
+ rememberXreviewRetirementCase('T1', docs, []);
1097
+ const analysis = analyzeDocs(docs);
1098
+ forbidCode(analysis.diagnostics, 'recorded-conflict', 'the valid pinned retirement resolves the owner-recorded pair');
1099
+ const index = lifecycle.buildIndex(docs, CONFIG, { all: true });
1100
+ const targetLine = index.split('\n').find(line => line.includes(target.path)) || '';
1101
+ const issuerLine = index.split('\n').find(line => line.includes(issuerR1.path)) || '';
1102
+ assert.ok(targetLine.includes('superseded by:'), `a historical effective retirement edge keeps the target retired: ${targetLine}`);
1103
+ assert.ok(targetLine.includes(`${issuerR1.frontmatter.id}@r1`), `the index identifies the revision that issued retirement: ${targetLine}`);
1104
+ assert.ok(issuerLine.includes(`retires: \`${targetPin}\``), `the historical issuer retains its outgoing retirement provenance: ${issuerLine}`);
1105
+ const latestLine = index.split('\n').find(line => line.includes(issuerR2.path)) || '';
1106
+ assert.ok(!latestLine.includes('pending supersedes:'), 'an omitted edge on the current revision does not create a new pending edge');
1107
+ assert.ok(issuerLine.includes('derived state: historical'), `r1 remains historical: ${issuerLine}`);
1108
+ assert.ok(latestLine.includes('derived state: current'), `r2 remains current: ${latestLine}`);
1109
+ });
1110
+
1111
+ checkCase('2ay xreview T2. invalid later draft preserves r1 retirement while current stays none', () => {
1112
+ const targetA = revisionDoc({ id: 'PRD-GRPH-042', revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t2-a/r001.md' });
1113
+ const targetB = revisionDoc({ id: 'PRD-GRPH-043', revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t2-b/r001.md' });
1114
+ const pinA = pin(targetA.frontmatter.id, 1);
1115
+ const pinB = pin(targetB.frontmatter.id, 1);
1116
+ const r1 = revisionDoc({ id: 'PRD-IFCE-042', revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t2-issuer/r001.md', supersedes: [pinA], conflicts_with: [pinA] });
1117
+ const r2 = revisionDoc({ id: 'PRD-IFCE-042', revision: 2, status: 'effective', previousRevision: 1, relativePath: 'docs/prd/xreview/t2-issuer/r002.md' }, r1);
1118
+ const r3 = revisionDoc({ id: 'PRD-IFCE-042', revision: 3, status: 'draft', previousRevision: 999, previousDigest: sha256(r2.storedBytes), relativePath: 'docs/prd/xreview/t2-issuer/r003.md', supersedes: [pinB], conflicts_with: [pinB] }, r2);
1119
+ const docs = [targetA, targetB, r1, r2, r3];
1120
+ rememberXreviewRetirementCase('T2', docs, [r3.path]);
1121
+ const analysis = analyzeDocs(docs);
1122
+ expectCode(analysis.diagnostics, 'missing-parent', 'the later draft has an absent parent');
1123
+ expectCode(analysis.diagnostics, 'recorded-conflict', 'the draft cannot resolve its recorded conflict');
1124
+ assert.ok(!lifecycle.resolveRef(docs, 'PRD-IFCE-042').doc, 'whole-graph validation still reports no current revision');
1125
+ const index = lifecycle.buildIndex(docs, CONFIG, { all: true });
1126
+ const lineA = index.split('\n').find(line => line.includes(targetA.path)) || '';
1127
+ const lineB = index.split('\n').find(line => line.includes(targetB.path)) || '';
1128
+ const issuerLine = index.split('\n').find(line => line.includes(r1.path)) || '';
1129
+ const draftLine = index.split('\n').find(line => line.includes(r3.path)) || '';
1130
+ assert.ok(lineA.includes('retirement provenance:') && lineA.includes(r1.frontmatter.id + '@r1'), 'the exact r1 event survives: ' + lineA);
1131
+ assert.ok(!lineB.includes('superseded by:'), 'the invalid draft edge does not retire target B: ' + lineB);
1132
+ assert.ok(issuerLine.includes('retires:'), 'r1 retains its outgoing event: ' + issuerLine);
1133
+ assert.ok(draftLine.includes('pending supersedes:'), 'draft edge remains visibly pending: ' + draftLine);
1134
+ });
1135
+
1136
+ checkCase('2ay xreview T3. invalid effective r2 cannot issue its edge or erase r1-prefix eligibility', () => {
1137
+ const target = revisionDoc({ id: 'PRD-GRPH-044', revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t3-target/r001.md' });
1138
+ const targetPin = pin(target.frontmatter.id, 1);
1139
+ const r1 = revisionDoc({ id: 'PRD-IFCE-044', revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t3-issuer/r001.md' });
1140
+ const validR2 = revisionDoc({ id: 'PRD-IFCE-044', revision: 2, status: 'effective', previousRevision: 1, relativePath: 'docs/prd/xreview/t3-issuer/r002.md', supersedes: [targetPin], conflicts_with: [targetPin] }, r1);
1141
+ const invalidFrontmatter = { ...validR2.frontmatter };
1142
+ delete invalidFrontmatter.approval;
1143
+ const r2 = docObject(validR2.path, invalidFrontmatter, validR2.body);
1144
+ const docs = [target, r1, r2];
1145
+ rememberXreviewRetirementCase('T3', docs, [r2.path]);
1146
+ const diagnostics = analyzeDocs(docs).diagnostics;
1147
+ expectCode(diagnostics, 'approval-missing', 'r2 has no independent approval evidence');
1148
+ expectCode(diagnostics, 'recorded-conflict', 'the invalid edge remains unresolved');
1149
+ const index = lifecycle.buildIndex(docs, CONFIG, { all: true });
1150
+ const targetLine = index.split('\n').find(line => line.includes(target.path)) || '';
1151
+ const sourceLine = index.split('\n').find(line => line.includes(r2.path)) || '';
1152
+ assert.ok(!targetLine.includes('superseded by:'), 'unapproved r2 does not retire the target: ' + targetLine);
1153
+ assert.ok(sourceLine.includes('pending supersedes:'), 'unapproved r2 edge stays pending: ' + sourceLine);
1154
+ });
1155
+
1156
+ checkCase('2ay xreview T4. invalid effective descendants preserve valid events and cannot add new ones', () => {
1157
+ const cases = [];
1158
+ for (const variant of ['independence', 'digest']) {
1159
+ const suffix = variant === 'independence' ? 'a' : 'b';
1160
+ const domainNumber = variant === 'independence' ? '045' : '046';
1161
+ const targetA = revisionDoc({ id: 'PRD-GRPH-' + domainNumber, revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t4-' + suffix + '-a/r001.md' });
1162
+ const targetB = revisionDoc({ id: 'PRD-GRPH-' + (variant === 'independence' ? '047' : '048'), revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t4-' + suffix + '-b/r001.md' });
1163
+ const pinA = pin(targetA.frontmatter.id, 1);
1164
+ const pinB = pin(targetB.frontmatter.id, 1);
1165
+ const r1 = revisionDoc({ id: 'PRD-IFCE-' + domainNumber, revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t4-' + suffix + '-issuer/r001.md', supersedes: [pinA], conflicts_with: [pinA] });
1166
+ let r2 = revisionDoc({ id: r1.frontmatter.id, revision: 2, status: 'effective', previousRevision: 1, ...(variant === 'digest' ? { previousDigest: sha256(Buffer.from('wrong predecessor')) } : {}), relativePath: 'docs/prd/xreview/t4-' + suffix + '-issuer/r002.md', supersedes: [pinB], conflicts_with: [pinB] }, r1);
1167
+ if (variant === 'independence') r2 = reviseFixture(r2, { approval: { ...r2.frontmatter.approval, by: r2.frontmatter.drafted_by } });
1168
+ const docs = [targetA, targetB, r1, r2];
1169
+ rememberXreviewRetirementCase('T4-' + variant, docs, [r2.path]);
1170
+ cases.push({ variant, targetA, targetB, r1, r2, docs });
1171
+ }
1172
+ for (const { variant, targetA, targetB, r1, r2, docs } of cases) {
1173
+ const diagnostics = analyzeDocs(docs).diagnostics;
1174
+ expectCode(diagnostics, variant === 'independence' ? 'approval-not-independent' : 'revision-digest-mismatch', variant + ' invalidates only r2');
1175
+ expectCode(diagnostics, 'recorded-conflict', 'r2 cannot resolve target B conflict');
1176
+ const index = lifecycle.buildIndex(docs, CONFIG, { all: true });
1177
+ const lineA = index.split('\n').find(line => line.includes(targetA.path)) || '';
1178
+ const lineB = index.split('\n').find(line => line.includes(targetB.path)) || '';
1179
+ assert.ok(lineA.includes('retirement provenance:') && lineA.includes(r1.frontmatter.id + '@r1'), 'prior retirement survives ' + variant + ': ' + lineA);
1180
+ assert.ok(!lineB.includes('superseded by:'), 'invalid ' + variant + ' child cannot retire target B: ' + lineB);
1181
+ }
1182
+ });
1183
+
1184
+ checkCase('2ay xreview T5. effective fork keeps r1 event but neither ambiguous child issues', () => {
1185
+ const targetA = revisionDoc({ id: 'PRD-GRPH-049', revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t5-a/r001.md' });
1186
+ const targetB = revisionDoc({ id: 'PRD-GRPH-050', revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t5-b/r001.md' });
1187
+ const targetC = revisionDoc({ id: 'PRD-GRPH-051', revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t5-c/r001.md' });
1188
+ const pinA = pin(targetA.frontmatter.id, 1);
1189
+ const pinB = pin(targetB.frontmatter.id, 1);
1190
+ const pinC = pin(targetC.frontmatter.id, 1);
1191
+ const r1 = revisionDoc({ id: 'PRD-IFCE-049', revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t5-issuer/r001.md', supersedes: [pinA], conflicts_with: [pinA] });
1192
+ const r2 = revisionDoc({ id: 'PRD-IFCE-049', revision: 2, status: 'effective', previousRevision: 1, relativePath: 'docs/prd/xreview/t5-issuer/r002.md', supersedes: [pinB], conflicts_with: [pinB] }, r1);
1193
+ const r3 = revisionDoc({ id: 'PRD-IFCE-049', revision: 3, status: 'effective', previousRevision: 1, relativePath: 'docs/prd/xreview/t5-issuer/r003.md', supersedes: [pinC], conflicts_with: [pinC] }, r1);
1194
+ const docs = [targetA, targetB, targetC, r1, r2, r3];
1195
+ rememberXreviewRetirementCase('T5', docs, [r2.path, r3.path]);
1196
+ const diagnostics = analyzeDocs(docs).diagnostics;
1197
+ expectCode(diagnostics, 'revision-fork');
1198
+ expectCode(diagnostics, 'recorded-conflict');
1199
+ assert.ok(!lifecycle.resolveRef(docs, 'PRD-IFCE-049').doc, 'a fork has no arbitrarily selected current revision');
1200
+ const index = lifecycle.buildIndex(docs, CONFIG, { all: true });
1201
+ const lineA = index.split('\n').find(line => line.includes(targetA.path)) || '';
1202
+ const lineB = index.split('\n').find(line => line.includes(targetB.path)) || '';
1203
+ const lineC = index.split('\n').find(line => line.includes(targetC.path)) || '';
1204
+ assert.ok(lineA.includes('retirement provenance:') && lineA.includes(r1.frontmatter.id + '@r1'), 'unambiguous r1 event remains: ' + lineA);
1205
+ assert.ok(!lineB.includes('superseded by:') && !lineC.includes('superseded by:'), 'neither ambiguous effective child retires its target');
1206
+ });
1207
+
1208
+ checkCase('2ay xreview T6. disconnected effective and pending statuses cannot issue or erase prefix events', () => {
1209
+ const targets = ['052', '053', '054', '055', '056'].map((number, index) => revisionDoc({ id: 'PRD-GRPH-' + number, revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t6-target-' + index + '/r001.md' }));
1210
+ const pins = targets.map(target => pin(target.frontmatter.id, 1));
1211
+ const r1 = revisionDoc({ id: 'PRD-IFCE-052', revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t6-issuer/r001.md', supersedes: [pins[0]], conflicts_with: [pins[0]] });
1212
+ const disconnected = revisionDoc({ id: 'PRD-IFCE-052', revision: 3, status: 'effective', previousRevision: 2, previousDigest: sha256(Buffer.from('missing parent')), relativePath: 'docs/prd/xreview/t6-issuer/r003.md', supersedes: [pins[1]], conflicts_with: [pins[1]] }, r1);
1213
+ const pending = ['draft', 'approved', 'rejected'].map((status, index) => revisionDoc({
1214
+ id: 'PRD-IFCE-05' + (3 + index), revision: 1, status,
1215
+ relativePath: 'docs/prd/xreview/t6-pending-' + status + '/r001.md',
1216
+ supersedes: [pins[index + 2]], conflicts_with: [pins[index + 2]]
1217
+ }));
1218
+ const docs = [...targets, r1, disconnected, ...pending];
1219
+ const unresolvedPaths = [disconnected, ...pending].map(doc => doc.path);
1220
+ rememberXreviewRetirementCase('T6', docs, unresolvedPaths);
1221
+ const diagnostics = analyzeDocs(docs).diagnostics;
1222
+ expectCode(diagnostics, 'missing-parent', 'effective node is disconnected from its declared parent');
1223
+ expectCode(diagnostics, 'recorded-conflict', 'disconnected and non-effective edges leave recorded conflicts unresolved');
1224
+ const index = lifecycle.buildIndex(docs, CONFIG, { all: true });
1225
+ const lineA = index.split('\n').find(line => line.includes(targets[0].path)) || '';
1226
+ assert.ok(lineA.includes('retirement provenance:') && lineA.includes(r1.frontmatter.id + '@r1'), 'the accepted prefix event remains: ' + lineA);
1227
+ for (const target of targets.slice(1)) {
1228
+ const line = index.split('\n').find(item => item.includes(target.path)) || '';
1229
+ assert.ok(!line.includes('superseded by:'), 'no disconnected or pending issuer retires ' + target.path + ': ' + line);
1230
+ }
1231
+ for (const issuer of [disconnected, ...pending]) {
1232
+ const line = index.split('\n').find(item => item.includes(issuer.path)) || '';
1233
+ assert.ok(line.includes('pending supersedes:'), 'non-eligible edge remains pending for ' + issuer.frontmatter.status + ': ' + line);
1234
+ }
1235
+ });
1236
+
1237
+ checkCase('2ay xreview T7. activation, independence and REQ-continuity evidence gate child issuers', () => {
1238
+ const root = gitRepo('xreview-t7-invalid-activation');
1239
+ const targetA = revisionDoc({ id: 'PRD-GRPH-057', revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t7-a/r001.md' });
1240
+ const targetB = revisionDoc({ id: 'PRD-GRPH-058', revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t7-b/r001.md' });
1241
+ const pinA = pin(targetA.frontmatter.id, 1);
1242
+ const pinB = pin(targetB.frontmatter.id, 1);
1243
+ const r1 = revisionDoc({ id: 'PRD-IFCE-057', revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t7-issuer/r001.md', supersedes: [pinA], conflicts_with: [pinA] });
1244
+ const approved = revisionDoc({ id: 'PRD-IFCE-057', revision: 2, status: 'approved', previousRevision: 1, release_channel: 'stable', relativePath: 'docs/prd/xreview/t7-issuer/r002.md', supersedes: [pinB], conflicts_with: [pinB] }, r1);
1245
+ for (const doc of [targetA, targetB, r1, approved]) writeDoc(root, doc.path, doc.frontmatter, doc.body);
1246
+ const base = commitAll(root, 'xreview activation base');
1247
+ const forgedActivation = reviseFixture(approved, {
1248
+ status: 'effective', effective_at: '2026-10-17T00:02:00Z', release_channel: 'beta'
1249
+ });
1250
+ writeDoc(root, forgedActivation.path, forgedActivation.frontmatter, forgedActivation.body);
1251
+ const activatedDocs = lifecycle.collectDocs(root, CONFIG);
1252
+ const activationAnalysis = analyzeDocs(activatedDocs, { workspace: root, base, configPath: path.join(root, 'lifecycle-config.json'), all: true });
1253
+ expectCode(activationAnalysis.diagnostics, 'invalid-transition', 'the trusted-base activation changed a frozen field');
1254
+ expectCode(activationAnalysis.diagnostics, 'recorded-conflict', 'the forged activation does not resolve the conflict');
1255
+ const activationIndex = lifecycle.buildIndex(activatedDocs, CONFIG, {
1256
+ all: true,
1257
+ diagnostics: activationAnalysis.diagnostics,
1258
+ _graph: activationAnalysis._graph,
1259
+ _retirementEligibleSources: activationAnalysis._retirementEligibleSources
1260
+ });
1261
+ const targetALine = activationIndex.split('\n').find(line => line.includes(targetA.path)) || '';
1262
+ const targetBLine = activationIndex.split('\n').find(line => line.includes(targetB.path)) || '';
1263
+ assert.ok(targetALine.includes('retirement provenance:') && targetALine.includes(r1.frontmatter.id + '@r1'), 'validated prefix event remains after invalid activation: ' + targetALine);
1264
+ assert.ok(!targetBLine.includes('superseded by:'), 'transition-invalid r2 cannot retire target B: ' + targetBLine);
1265
+
1266
+ const independentBase = revisionDoc({ id: 'PRD-IFCE-058', revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t7-independent/r001.md' });
1267
+ const independentTarget = revisionDoc({ id: 'PRD-GRPH-059', revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t7-independent-target/r001.md' });
1268
+ const independentPin = pin(independentTarget.frontmatter.id, 1);
1269
+ const selfApproved = revisionDoc({ id: independentBase.frontmatter.id, revision: 2, status: 'effective', previousRevision: 1, relativePath: 'docs/prd/xreview/t7-independent/r002.md', supersedes: [independentPin], conflicts_with: [independentPin] }, independentBase);
1270
+ const invalidIndependent = reviseFixture(selfApproved, { approval: { ...selfApproved.frontmatter.approval, by: selfApproved.frontmatter.drafted_by } });
1271
+ const independentDocs = [independentTarget, independentBase, invalidIndependent];
1272
+ const independentDiagnostics = analyzeDocs(independentDocs).diagnostics;
1273
+ expectCode(independentDiagnostics, 'approval-not-independent', 'self-review is not independent approval evidence');
1274
+ expectCode(independentDiagnostics, 'recorded-conflict', 'self-reviewed edge remains unresolved');
1275
+ const independentIndex = lifecycle.buildIndex(independentDocs, CONFIG, { all: true });
1276
+ assert.ok(!independentIndex.split('\n').find(line => line.includes(independentTarget.path))?.includes('superseded by:'), 'self-reviewed child does not retire its target');
1277
+
1278
+ const reqA = revisionDoc({ id: 'PRD-GRPH-060', revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t7-req-target-a/r001.md' });
1279
+ const reqB = revisionDoc({ id: 'PRD-GRPH-061', revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t7-req-target-b/r001.md' });
1280
+ const reqPinA = pin(reqA.frontmatter.id, 1);
1281
+ const reqPinB = pin(reqB.frontmatter.id, 1);
1282
+ const reqR1 = revisionDoc({ id: 'PRD-IFCE-060', revision: 1, status: 'effective', relativePath: 'docs/prd/xreview/t7-req-issuer/r001.md', supersedes: [reqPinA], conflicts_with: [reqPinA], body: '### REQ-GRPH-160: Stable behavior\n\nAcceptance: retain the stable behavior.\n' });
1283
+ const droppedReqR2 = revisionDoc({ id: 'PRD-IFCE-060', revision: 2, status: 'effective', previousRevision: 1, relativePath: 'docs/prd/xreview/t7-req-issuer/r002.md', supersedes: [reqPinB], conflicts_with: [reqPinB], body: '### REQ-GRPH-161: New behavior\n\nAcceptance: add the new behavior without silently dropping the old requirement.\n' }, reqR1);
1284
+ const reqDocs = [reqA, reqB, reqR1, droppedReqR2];
1285
+ const reqDiagnostics = analyzeDocs(reqDocs).diagnostics;
1286
+ expectCode(reqDiagnostics, 'req-dropped', 'required REQ continuity fails on the proposed child');
1287
+ expectCode(reqDiagnostics, 'recorded-conflict', 'REQ-invalid child cannot resolve its conflict');
1288
+ const reqIndex = lifecycle.buildIndex(reqDocs, CONFIG, { all: true });
1289
+ const reqALine = reqIndex.split('\n').find(line => line.includes(reqA.path)) || '';
1290
+ const reqBLine = reqIndex.split('\n').find(line => line.includes(reqB.path)) || '';
1291
+ assert.ok(reqALine.includes('retirement provenance:') && reqALine.includes(reqR1.frontmatter.id + '@r1'), 'prior event remains under REQ continuity error: ' + reqALine);
1292
+ assert.ok(!reqBLine.includes('superseded by:'), 'REQ-invalid r2 cannot retire target B: ' + reqBLine);
1293
+ });
1294
+
1295
+ checkCase('2ay xreview C4. trusted base governance controls immutable enforcement, not conventional paths', () => {
1296
+ const scopedConfig = {
1297
+ governed: [{ path: 'specifications/contracts/**', role: 'prd' }],
1298
+ domains: ['GRPH', 'IFCE'],
1299
+ legacy: []
1300
+ };
1301
+ const writeConfig = root => fs.writeFileSync(path.join(root, 'lifecycle-config.json'), JSON.stringify(scopedConfig, null, 2), 'utf8');
1302
+
1303
+ const ungovernedRoot = gitRepo('xreview-c4-ungoverned-conventional-path');
1304
+ writeConfig(ungovernedRoot);
1305
+ const ungoverned = revisionDoc({ id: 'PRD-GRPH-062', revision: 1, status: 'effective', relativePath: 'docs/prd/unconfigured/r001.md' });
1306
+ writeDoc(ungovernedRoot, ungoverned.path, ungoverned.frontmatter, ungoverned.body);
1307
+ const ungovernedBase = commitAll(ungovernedRoot, 'unconfigured conventional-path artifact');
1308
+ writeDoc(ungovernedRoot, ungoverned.path, ungoverned.frontmatter, ungoverned.body + '\nAn ungoverned edit.\n');
1309
+ const ungovernedResult = baseLint(ungovernedRoot, ungovernedBase);
1310
+ forbidCode(ungovernedResult.diagnostics, 'immutable-revision-modified', 'docs/prd/ discovery must not confer trusted-base authority');
1311
+ forbidCode(ungovernedResult.diagnostics, 'immutable-revision-deleted');
1312
+
1313
+ const narrowedRoot = gitRepo('xreview-c4-governed-then-narrowed');
1314
+ writeConfig(narrowedRoot);
1315
+ const governed = revisionDoc({ id: 'PRD-GRPH-063', revision: 1, status: 'effective', relativePath: 'specifications/contracts/graph/r001.md' });
1316
+ writeDoc(narrowedRoot, governed.path, governed.frontmatter, governed.body);
1317
+ const governedBase = commitAll(narrowedRoot, 'base-governed effective artifact');
1318
+ fs.writeFileSync(path.join(narrowedRoot, 'lifecycle-config.json'), JSON.stringify({ governed: [], domains: ['GRPH', 'IFCE'], legacy: [] }, null, 2), 'utf8');
1319
+ writeDoc(narrowedRoot, governed.path, governed.frontmatter, governed.body + '\nA governed edit after candidate narrowing.\n');
1320
+ const narrowedResult = baseLint(narrowedRoot, governedBase);
1321
+ expectCode(narrowedResult.diagnostics, 'immutable-revision-modified', 'trusted base governance survives candidate narrowing');
1322
+ });
1323
+
1324
+ checkCase('2ay xreview F1. base-config legacy exclusions stay scoped to their own source for freeze and transition checks', () => {
1325
+ const governedConfig = {
1326
+ governed: [{ path: 'specifications/contracts/**', role: 'prd' }],
1327
+ domains: ['GRPH', 'IFCE'],
1328
+ legacy: []
1329
+ };
1330
+ const narrowedConfig = { governed: [], domains: ['GRPH', 'IFCE'], legacy: [] };
1331
+ const writeLifecycleConfig = (root, config) => fs.writeFileSync(
1332
+ path.join(root, 'lifecycle-config.json'), JSON.stringify(config, null, 2), 'utf8'
1333
+ );
1334
+ const addCompetingLegacySource = root => {
1335
+ const manifest = path.join(root, '.claude', 'harness-everything', 'manifest.json');
1336
+ fs.mkdirSync(path.dirname(manifest), { recursive: true });
1337
+ fs.writeFileSync(manifest, JSON.stringify({ projectDocs: {
1338
+ governed: [], domains: ['GRPH'], legacy: ['specifications/contracts/**']
1339
+ } }, null, 2), 'utf8');
1340
+ };
1341
+
1342
+ const deletedRoot = gitRepo('xreview-f1-delete-narrow-with-competing-legacy');
1343
+ writeLifecycleConfig(deletedRoot, governedConfig);
1344
+ addCompetingLegacySource(deletedRoot);
1345
+ const deleted = revisionDoc({ id: 'PRD-GRPH-064', revision: 1, status: 'effective', relativePath: 'specifications/contracts/graph/r001.md' });
1346
+ writeDoc(deletedRoot, deleted.path, deleted.frontmatter, deleted.body);
1347
+ const deletedBase = commitAll(deletedRoot, 'trusted governed effective PRD plus unrelated host legacy exclusion');
1348
+ fs.rmSync(path.join(deletedRoot, ...deleted.path.split('/')));
1349
+ writeLifecycleConfig(deletedRoot, narrowedConfig);
1350
+ const deletedResult = baseLint(deletedRoot, deletedBase);
1351
+ expectCode(deletedResult.diagnostics, 'immutable-revision-deleted', 'another base source legacy exclusion cannot cancel trusted governance');
1352
+
1353
+ const controlRoot = gitRepo('xreview-f1-delete-narrow-control');
1354
+ writeLifecycleConfig(controlRoot, governedConfig);
1355
+ const controlDoc = revisionDoc({ id: 'PRD-GRPH-065', revision: 1, status: 'effective', relativePath: 'specifications/contracts/graph/r001.md' });
1356
+ writeDoc(controlRoot, controlDoc.path, controlDoc.frontmatter, controlDoc.body);
1357
+ const controlBase = commitAll(controlRoot, 'trusted governed effective PRD without competing host config');
1358
+ fs.rmSync(path.join(controlRoot, ...controlDoc.path.split('/')));
1359
+ writeLifecycleConfig(controlRoot, narrowedConfig);
1360
+ expectCode(baseLint(controlRoot, controlBase).diagnostics, 'immutable-revision-deleted', 'control without a competing legacy source remains protected');
1361
+
1362
+ const editedRoot = gitRepo('xreview-f1-edit-transition-with-competing-legacy');
1363
+ writeLifecycleConfig(editedRoot, governedConfig);
1364
+ addCompetingLegacySource(editedRoot);
1365
+ const approved = revisionDoc({ id: 'PRD-GRPH-066', revision: 1, status: 'approved', relativePath: 'specifications/contracts/graph/r001.md' });
1366
+ writeDoc(editedRoot, approved.path, approved.frontmatter, approved.body);
1367
+ const editedBase = commitAll(editedRoot, 'trusted governed approved PRD plus unrelated host legacy exclusion');
1368
+ writeLifecycleConfig(editedRoot, narrowedConfig);
1369
+ writeDoc(editedRoot, approved.path, approved.frontmatter, `${approved.body}\nAn edit to the approved candidate.\n`);
1370
+ const editedResult = baseLint(editedRoot, editedBase);
1371
+ expectCode(editedResult.diagnostics, 'invalid-transition', 'base-governed approved candidate remains in transition checks after narrowing');
1372
+
1373
+ const ownLegacyRoot = gitRepo('xreview-f1-own-config-legacy-exemption');
1374
+ writeLifecycleConfig(ownLegacyRoot, {
1375
+ ...governedConfig,
1376
+ legacy: ['specifications/contracts/**']
1377
+ });
1378
+ const exempt = revisionDoc({ id: 'PRD-GRPH-067', revision: 1, status: 'effective', relativePath: 'specifications/contracts/graph/r001.md' });
1379
+ writeDoc(ownLegacyRoot, exempt.path, exempt.frontmatter, exempt.body);
1380
+ const ownLegacyBase = commitAll(ownLegacyRoot, 'source config explicitly exempts its governed path as legacy');
1381
+ writeLifecycleConfig(ownLegacyRoot, narrowedConfig);
1382
+ writeDoc(ownLegacyRoot, exempt.path, exempt.frontmatter, `${exempt.body}\nAn edit remains exempt by its own base source.\n`);
1383
+ const ownLegacyResult = baseLint(ownLegacyRoot, ownLegacyBase);
1384
+ forbidCode(ownLegacyResult.diagnostics, 'immutable-revision-modified');
1385
+ forbidCode(ownLegacyResult.diagnostics, 'immutable-revision-deleted');
1386
+ });
1387
+
1388
+ checkCase('2ay xreview T8. lint, index, input order and packaged module agree for T1-T6', () => {
1389
+ const packagePath = path.join(__dirname, '..', 'plugins', 'harness-everything', 'skills', 'harness-everything', 'scripts', 'doc-lifecycle.js');
1390
+ const packagedLifecycle = require(packagePath);
1391
+ assert.strictEqual(xreviewRetirementCases.length, 7, 'T1, T2, T3, both T4 negatives, T5 and T6 are all represented');
1392
+ for (const scenario of xreviewRetirementCases) {
1393
+ const forward = {
1394
+ diagnostics: lifecycle.lintDocs(scenario.docs, CONFIG, { all: true }),
1395
+ index: lifecycle.buildIndex(scenario.docs, CONFIG, { all: true })
1396
+ };
1397
+ const reverse = {
1398
+ diagnostics: lifecycle.lintDocs([...scenario.docs].reverse(), CONFIG, { all: true }),
1399
+ index: lifecycle.buildIndex([...scenario.docs].reverse(), CONFIG, { all: true })
1400
+ };
1401
+ assert.deepStrictEqual(reverse, forward, scenario.name + ' source output is deterministic under reversed input');
1402
+ const unresolvedPaths = forward.diagnostics.filter(item => item.code === 'recorded-conflict').map(item => item.path).sort();
1403
+ assert.deepStrictEqual(unresolvedPaths, scenario.unresolvedConflictPaths, scenario.name + ' leaves only unissued pairs unresolved');
1404
+ const packageForward = {
1405
+ diagnostics: packagedLifecycle.lintDocs(scenario.docs, CONFIG, { all: true }),
1406
+ index: packagedLifecycle.buildIndex(scenario.docs, CONFIG, { all: true })
1407
+ };
1408
+ const packageReverse = {
1409
+ diagnostics: packagedLifecycle.lintDocs([...scenario.docs].reverse(), CONFIG, { all: true }),
1410
+ index: packagedLifecycle.buildIndex([...scenario.docs].reverse(), CONFIG, { all: true })
1411
+ };
1412
+ assert.deepStrictEqual(packageReverse, packageForward, scenario.name + ' package output is deterministic');
1413
+ assert.deepStrictEqual(packageForward, forward, scenario.name + ' packaged and source consumers agree');
1414
+ }
1415
+ });
1416
+
1417
+ // Owner case 8 — explicit migration, portable byte identity, deterministic
1418
+ // index/diff independent of input order, and package parity.
1419
+ function prepareMigrationRepo(name) {
1420
+ const root = gitRepo(name);
1421
+ const livingBody = '# Existing living contract\n\nAcceptance: preserve existing users.\n';
1422
+ const livingPath = writeDoc(root, 'docs/prd/living.md', { id: 'PRD-GRPH-001', status: 'active' }, livingBody);
1423
+ const legacyPath = writeDoc(root, 'docs/prd/legacy/PRD-GRPH-009.md', { id: 'PRD-GRPH-009', status: 'active' }, '# Frozen imported record\n');
1424
+ const beforeLegacy = fs.readFileSync(legacyPath);
1425
+ const oldRevision = commitAll(root, 'pre-migration living PRD');
1426
+ const oldBlob = gitBytes(root, ['show', `${oldRevision}:docs/prd/living.md`]);
1427
+ const migratedFromDigest = sha256(oldBlob);
1428
+ const approvedSeed = revisionDoc({
1429
+ id: 'PRD-GRPH-001', revision: 1, status: 'effective', draftedBy: 'Legacy Contract Owner',
1430
+ migrated_from_digest: migratedFromDigest,
1431
+ migration_record: 'docs/migrations/prd-graph-001.json',
1432
+ relativePath: 'docs/prd/living.md', body: livingBody
1433
+ });
1434
+ const migrated = reviseFixture(approvedSeed, {
1435
+ approval: { ...approvedSeed.frontmatter.approval, ref: 'docs/approvals/prd-migration-review.md' }
1436
+ });
1437
+ writeDoc(root, 'docs/prd/living.md', migrated.frontmatter, livingBody);
1438
+ writeDoc(root, migrated.frontmatter.approval.ref, { id: 'APPROVAL-MIGRATION-001', status: 'final' }, '# Baseline migration review\n');
1439
+ const migrationRecordPath = path.join(root, 'docs/migrations/prd-graph-001.json');
1440
+ fs.mkdirSync(path.dirname(migrationRecordPath), { recursive: true });
1441
+ const migrationRecord = {
1442
+ record: 'prd-baseline-migration', id: 'PRD-GRPH-001', revision: 1,
1443
+ source: 'docs/prd/living.md', migrated_from_digest: migratedFromDigest,
1444
+ approved_by: 'Independent cold verifier'
1445
+ };
1446
+ fs.writeFileSync(migrationRecordPath, JSON.stringify(migrationRecord, null, 2) + '\n', 'utf8');
1447
+ const migrationCommit = commitAll(root, 'metadata-only PRD baseline adoption');
1448
+ return { root, livingPath, livingBody, legacyPath, beforeLegacy, oldRevision, oldBlob, migratedFromDigest, migrationRecordPath, migrationRecord, migrationCommit };
1449
+ }
1450
+
1451
+ function addEffectiveR2(root, r1Path = 'docs/prd/living.md', r2Path = 'docs/prd/r002.md') {
1452
+ const r1 = lifecycle.collectDocs(root, CONFIG).find(doc => doc.path === r1Path);
1453
+ assert.ok(r1, `effective r1 exists at ${r1Path}`);
1454
+ const r2 = revisionDoc({
1455
+ id: r1.frontmatter.id,
1456
+ revision: 2,
1457
+ status: 'effective',
1458
+ previousRevision: 1,
1459
+ relativePath: r2Path,
1460
+ body: r1.body
1461
+ }, r1);
1462
+ writeDoc(root, r2Path, r2.frontmatter, r2.body);
1463
+ return { r1, r2 };
1464
+ }
1465
+
1466
+ checkCase('2ay case 8. baseline adoption binds prior stored bytes to a recorded metadata-only migration', () => {
1467
+ const state = prepareMigrationRepo('baseline-migration-valid');
1468
+ const result = baseLint(state.root, state.oldRevision);
1469
+ assert.strictEqual(result.evidence.immutability, 'observed');
1470
+ assert.deepStrictEqual(result.diagnostics.filter(item => item.severity === 'error'), [], `valid baseline adoption is accepted: ${JSON.stringify(result.diagnostics)}`);
1471
+ assert.strictEqual(sha256(state.oldBlob), state.migratedFromDigest, 'migration digest names the old Git blob bytes');
1472
+ assert.strictEqual(fs.readFileSync(state.livingPath, 'utf8').includes(state.livingBody), true, 'baseline migration preserves the exact body bytes');
1473
+ assert.deepStrictEqual(fs.readFileSync(state.legacyPath), state.beforeLegacy, 'legacy contract remains byte-identical');
1474
+ const record = JSON.parse(fs.readFileSync(state.migrationRecordPath, 'utf8'));
1475
+ assert.deepStrictEqual(record, state.migrationRecord, 'migration record is present and binds the old digest');
1476
+
1477
+ const migratedDoc = lifecycle.collectDocs(state.root, CONFIG).find(doc => doc.path === 'docs/prd/living.md');
1478
+ assert.strictEqual(migratedDoc.frontmatter.revision, 1);
1479
+ assert.strictEqual(migratedDoc.frontmatter.migrated_from_digest, state.migratedFromDigest);
1480
+ const migratedSource = lifecycle.parseFrontmatter(fs.readFileSync(state.livingPath, 'utf8'));
1481
+ assert.strictEqual(migratedSource.body, state.livingBody, 'migration changes metadata only');
1482
+ });
1483
+
1484
+ checkCase('2ay index authority. missing and mismatched baseline migration records are unresolved and invalidate current state', () => {
1485
+ const cases = [
1486
+ {
1487
+ name: 'missing',
1488
+ code: 'migration-record-missing',
1489
+ prepare(state) { fs.unlinkSync(state.migrationRecordPath); }
1490
+ },
1491
+ {
1492
+ name: 'mismatched',
1493
+ code: 'migration-record-mismatch',
1494
+ prepare(state) {
1495
+ const wrong = { ...state.migrationRecord, source: 'docs/prd/a-different-source.md' };
1496
+ fs.writeFileSync(state.migrationRecordPath, JSON.stringify(wrong, null, 2) + '\n', 'utf8');
1497
+ }
1498
+ }
1499
+ ];
1500
+ for (const scenario of cases) {
1501
+ const state = prepareMigrationRepo(`index-authority-${scenario.name}`);
1502
+ scenario.prepare(state);
1503
+ const allLint = lifecycleCommand(state.root, 'lint', { baseRevision: state.oldRevision, all: true });
1504
+ const changedLint = lifecycleCommand(state.root, 'lint', { baseRevision: state.oldRevision, changedPath: 'docs/prd/living.md' });
1505
+ expectCode(allLint.diagnostics, scenario.code, `${scenario.name} migration record is a lint error`);
1506
+ expectCode(changedLint.diagnostics, scenario.code, `${scenario.name} migration record is also an error in changed mode`);
1507
+ assert.deepStrictEqual(changedLint.diagnostics, allLint.diagnostics, `${scenario.name} lint diagnostics are --changed/--all equivalent`);
1508
+
1509
+ const allIndex = lifecycleCommand(state.root, 'index', { baseRevision: state.oldRevision, all: true });
1510
+ const changedIndex = lifecycleCommand(state.root, 'index', { baseRevision: state.oldRevision, changedPath: 'docs/prd/living.md' });
1511
+ assert.ok(allIndex.includes(`- \`${scenario.code}\` (error)`), `${scenario.name} authority error is unresolved in index:\n${allIndex}`);
1512
+ assert.match(allIndex, /PRD-GRPH-001` — state: invalid; current: none/, `${scenario.name} index cannot call the adopted revision current:\n${allIndex}`);
1513
+ assert.match(allIndex, /`docs\/prd\/living\.md` \(role: prd; stored status: effective; derived state: invalid\)/, `${scenario.name} active document state is invalid:\n${allIndex}`);
1514
+ assert.strictEqual(changedIndex, allIndex, `${scenario.name} index output is --changed/--all equivalent`);
1515
+ }
1516
+ });
1517
+
1518
+ checkCase('2ay index authority. valid metadata-only adoption stays current with and without a trusted base', () => {
1519
+ const state = prepareMigrationRepo('index-authority-valid-adoption');
1520
+ for (const mode of [
1521
+ { label: 'trusted base, all', baseRevision: state.oldRevision, all: true },
1522
+ { label: 'trusted base, changed', baseRevision: state.oldRevision, changedPath: 'docs/prd/living.md' },
1523
+ { label: 'no base, all', all: true },
1524
+ { label: 'no base, changed', changedPath: 'docs/prd/living.md' }
1525
+ ]) {
1526
+ const lint = lifecycleCommand(state.root, 'lint', mode);
1527
+ assert.deepStrictEqual(lint.diagnostics.filter(item => item.severity === 'error'), [], `${mode.label}: valid metadata-only adoption has no errors`);
1528
+ const index = lifecycleCommand(state.root, 'index', mode);
1529
+ assert.match(index, /PRD-GRPH-001` — state: valid; current: `PRD-GRPH-001@r1`/, `${mode.label}: valid adoption stays current`);
1530
+ assert.match(index, /`docs\/prd\/living\.md` \(role: prd; stored status: effective; derived state: current\)/, `${mode.label}: current document stays valid`);
1531
+ }
1532
+ });
1533
+
1534
+ function assertChildRejectedByTrustedBase(state, expectedCode) {
1535
+ const lint = baseLint(state.root, state.oldRevision);
1536
+ expectCode(lint.diagnostics, expectedCode);
1537
+ const docs = lifecycle.collectDocs(state.root, CONFIG);
1538
+ const options = {
1539
+ workspace: state.root,
1540
+ configPath: path.join(state.root, 'lifecycle-config.json'),
1541
+ base: state.oldRevision,
1542
+ all: true
1543
+ };
1544
+ const analysis = lifecycle.analyzeDocs(docs, CONFIG, options);
1545
+ expectCode(analysis.diagnostics, expectedCode);
1546
+ assert.strictEqual(analysis._retirementEligibleSources.size, 0, `${expectedCode} prevents an error-free effective prefix`);
1547
+ const index = lifecycle.buildIndex(docs, CONFIG, {
1548
+ ...options,
1549
+ diagnostics: analysis.diagnostics,
1550
+ _graph: analysis._graph,
1551
+ _retirementEligibleSources: analysis._retirementEligibleSources
1552
+ });
1553
+ const cli = lifecycleCommand(state.root, 'index', { baseRevision: state.oldRevision, all: true });
1554
+ assert.strictEqual(index, cli, 'pre-analyzed API and CLI render the same authority result');
1555
+ assert.match(index, /PRD-GRPH-001` — state: invalid; current: none/, `${expectedCode} in r1 invalidates effective r2 authority:\n${index}`);
1556
+ assert.ok(index.includes(`- \`${expectedCode}\` (error)`), `${expectedCode} remains visible under Unresolved`);
1557
+ }
1558
+
1559
+ checkCase('2ay review4 P2. migration errors on r1 invalidate an otherwise-valid effective r2', () => {
1560
+ const missing = prepareMigrationRepo('index-invalid-effective-prefix-missing-r1');
1561
+ fs.unlinkSync(missing.migrationRecordPath);
1562
+ addEffectiveR2(missing.root);
1563
+ assertChildRejectedByTrustedBase(missing, 'migration-record-missing');
1564
+
1565
+ const mismatched = prepareMigrationRepo('index-invalid-effective-prefix-mismatch-r1');
1566
+ const wrongRecord = { ...mismatched.migrationRecord, source: 'docs/prd/wrong.md' };
1567
+ fs.writeFileSync(mismatched.migrationRecordPath, JSON.stringify(wrongRecord, null, 2) + '\n', 'utf8');
1568
+ addEffectiveR2(mismatched.root);
1569
+ assertChildRejectedByTrustedBase(mismatched, 'migration-record-mismatch');
1570
+ });
1571
+
1572
+ checkCase('2ay review4 P2. immutable effective ancestor keeps otherwise-valid r2 out of current authority', () => {
1573
+ const state = seedRevisionRepo('index-invalid-effective-prefix-immutable-ancestor', 'effective');
1574
+ const baseR1 = lifecycle.collectDocs(state.root, CONFIG).find(doc => doc.path === 'docs/prd/sample/r001.md');
1575
+ const changedBody = '# Edited immutable baseline\n\nAcceptance: this edit is forbidden.\n';
1576
+ const changedFrontmatter = {
1577
+ ...baseR1.frontmatter,
1578
+ approval: { ...baseR1.frontmatter.approval }
1579
+ };
1580
+ changedFrontmatter.approval.content_digest = approvalContentDigest(changedFrontmatter, changedBody);
1581
+ const changedR1 = docObject(baseR1.path, changedFrontmatter, changedBody);
1582
+ writeDoc(state.root, changedR1.path, changedFrontmatter, changedBody);
1583
+ const r2 = revisionDoc({
1584
+ id: changedFrontmatter.id,
1585
+ revision: 2,
1586
+ status: 'effective',
1587
+ previousRevision: 1,
1588
+ relativePath: 'docs/prd/sample/r002.md',
1589
+ body: changedBody
1590
+ }, changedR1);
1591
+ writeDoc(state.root, r2.path, r2.frontmatter, r2.body);
1592
+
1593
+ const docs = lifecycle.collectDocs(state.root, CONFIG);
1594
+ const options = { workspace: state.root, base: state.base, all: true };
1595
+ const analysis = lifecycle.analyzeDocs(docs, CONFIG, options);
1596
+ expectCode(analysis.diagnostics, 'immutable-revision-modified');
1597
+ assert.strictEqual(analysis._retirementEligibleSources.size, 0, 'the immutable error blocks the effective prefix at r1');
1598
+ const index = lifecycle.buildIndex(docs, CONFIG, {
1599
+ ...options,
1600
+ diagnostics: analysis.diagnostics,
1601
+ _graph: analysis._graph,
1602
+ _retirementEligibleSources: analysis._retirementEligibleSources
1603
+ });
1604
+ assert.match(index, /PRD-GRPH-001` — state: invalid; current: none/, `immutable ancestor cannot authorize r2:\n${index}`);
1605
+ });
1606
+
1607
+ checkCase('2ay review4 P2. direct buildIndex with trusted base matches the CLI authority view', () => {
1608
+ const state = prepareMigrationRepo('index-direct-api-base-parity');
1609
+ fs.unlinkSync(state.migrationRecordPath);
1610
+ const docs = lifecycle.collectDocs(state.root, CONFIG);
1611
+ const options = {
1612
+ workspace: state.root,
1613
+ configPath: path.join(state.root, 'lifecycle-config.json'),
1614
+ base: state.oldRevision,
1615
+ all: true
1616
+ };
1617
+ const direct = lifecycle.buildIndex(docs, CONFIG, options);
1618
+ const cli = lifecycleCommand(state.root, 'index', { baseRevision: state.oldRevision, all: true });
1619
+ assert.strictEqual(direct, cli, 'direct API analyzes supplied trusted-base context just like CLI');
1620
+ assert.match(direct, /PRD-GRPH-001` — state: invalid; current: none/);
1621
+ assert.ok(direct.includes('- `migration-record-missing` (error)'));
1622
+ });
1623
+
1624
+ checkCase('2ay review4 P2. warning-only authority diagnostics preserve current but block retirement eligibility', () => {
1625
+ const target = revisionDoc({ id: 'PRD-IFCE-102', revision: 1, status: 'effective', relativePath: 'docs/prd/target/r001.md' });
1626
+ const source = revisionDoc({
1627
+ id: 'PRD-GRPH-101', revision: 1, status: 'effective', relativePath: 'docs/prd/source/r001.md',
1628
+ supersedes: ['PRD-IFCE-102@r1']
1629
+ });
1630
+ const warning = {
1631
+ code: 'migration-record-missing', severity: 'warning', path: source.path,
1632
+ message: 'severity matrix warning must not revoke a validated prefix'
1633
+ };
1634
+ const index = lifecycle.buildIndex([source, target], CONFIG, { all: true, diagnostics: [warning] });
1635
+ assert.match(index, /PRD-GRPH-101` — state: valid; current: `PRD-GRPH-101@r1`/);
1636
+ assert.ok(index.includes('pending supersedes: `PRD-IFCE-102@r1`'), `warning-only blocker keeps the edge pending:\n${index}`);
1637
+ assert.ok(!index.includes('superseded by: `PRD-GRPH-101@r1` -> `PRD-IFCE-102@r1`'),
1638
+ `warning-only authority finding must not project retirement:\n${index}`);
1639
+ assert.ok(index.includes('- `migration-record-missing` (warning)'));
1640
+ });
1641
+
1642
+ function runCollisionAuthorityFixture(name, code, createDocs) {
1643
+ const root = gitRepo(`index-collision-${name}`);
1644
+ const config = {
1645
+ ...CONFIG,
1646
+ governed: [...CONFIG.governed, { path: 'docs/reports/**', role: 'report' }]
1647
+ };
1648
+ const configPath = path.join(root, 'lifecycle-config.json');
1649
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
1650
+ const fixture = createDocs();
1651
+ for (const doc of fixture.docs) writeDoc(root, doc.path, doc.frontmatter, doc.body);
1652
+ commitAll(root, `${name} collision retirement authority fixture`);
1653
+ const entries = lifecycle.collectDocs(root, config);
1654
+ const targetPath = fixture.target.path;
1655
+ const sourcePath = fixture.source.path;
1656
+
1657
+ for (const mode of [
1658
+ { name: 'unchanged', options: { changedPaths: [] }, cli: { changedPath: '' }, severity: 'warning', current: 'current' },
1659
+ { name: 'all', options: { all: true }, cli: { all: true }, severity: 'error', current: 'invalid' }
1660
+ ]) {
1661
+ const analysisOptions = { workspace: root, configPath, ...mode.options };
1662
+ const analysis = lifecycle.analyzeDocs(entries, config, analysisOptions);
1663
+ const collision = expectCode(analysis.diagnostics, code, `${name}/${mode.name} collision is reported`)
1664
+ .find(item => item.path === sourcePath);
1665
+ assert.strictEqual(collision.severity, mode.severity, `${name}/${mode.name} collision severity`);
1666
+ const conflict = expectCode(analysis.diagnostics, 'recorded-conflict', `${name}/${mode.name} retirement remains unauthorized`)
1667
+ .find(item => item.path === sourcePath);
1668
+ assert.strictEqual(conflict.severity, mode.severity, `${name}/${mode.name} conflict remains at the ratchet severity`);
1669
+
1670
+ const cliIndex = lifecycleCommand(root, 'index', mode.cli);
1671
+ const directIndex = lifecycle.buildIndex(entries, config, analysisOptions);
1672
+ const suppliedDiagnosticsIndex = lifecycle.buildIndex(entries, config, {
1673
+ ...analysisOptions,
1674
+ diagnostics: analysis.diagnostics
1675
+ });
1676
+ const suppliedAnalysisIndex = lifecycle.buildIndex(entries, config, {
1677
+ ...analysisOptions,
1678
+ diagnostics: analysis.diagnostics,
1679
+ _graph: analysis._graph,
1680
+ _retirementEligibleSources: analysis._retirementEligibleSources
1681
+ });
1682
+ assert.strictEqual(directIndex, cliIndex, `${name}/${mode.name}: direct API equals CLI`);
1683
+ assert.strictEqual(suppliedDiagnosticsIndex, cliIndex, `${name}/${mode.name}: supplied diagnostics API equals CLI`);
1684
+ assert.strictEqual(suppliedAnalysisIndex, cliIndex, `${name}/${mode.name}: supplied analysis API equals CLI`);
1685
+ const sourceRow = cliIndex.split('\n').find(line => line.includes(`\`${sourcePath}\``)) || '';
1686
+ const targetRow = cliIndex.split('\n').find(line => line.includes(`\`${targetPath}\``)) || '';
1687
+ assert.ok(sourceRow.includes(`derived state: ${mode.current}`),
1688
+ `${name}/${mode.name}: current state follows error-level authority only: ${sourceRow}`);
1689
+ assert.ok(sourceRow.includes(`pending supersedes: \`${fixture.target.frontmatter.id}@r1\``),
1690
+ `${name}/${mode.name}: collided source's edge stays pending: ${sourceRow}`);
1691
+ assert.ok(!targetRow.includes('superseded by:'),
1692
+ `${name}/${mode.name}: target is not projected as retired: ${targetRow}`);
1693
+ assert.ok(cliIndex.includes(`- \`${code}\` (${mode.severity})`), `${name}/${mode.name}: collision stays visible in Unresolved`);
1694
+ assert.ok(cliIndex.includes(`- \`recorded-conflict\` (${mode.severity})`), `${name}/${mode.name}: conflict stays unresolved`);
1695
+ }
1696
+ }
1697
+
1698
+ checkCase('2ay review5 P2. real duplicate-id collision blocks retirement at warning and error severity across APIs', () => {
1699
+ runCollisionAuthorityFixture('duplicate-id', 'duplicate-id', () => {
1700
+ const target = revisionDoc({ id: 'PRD-GRPH-700', revision: 1, status: 'effective', relativePath: 'docs/prd/target/r001.md' });
1701
+ const source = revisionDoc({
1702
+ id: 'PRD-IFCE-700', revision: 1, status: 'effective', relativePath: 'docs/prd/source/r001.md',
1703
+ supersedes: ['PRD-GRPH-700@r1'], conflicts_with: ['PRD-GRPH-700@r1']
1704
+ });
1705
+ const collision = docObject('docs/reports/duplicate.md', { id: source.frontmatter.id, status: 'final' }, 'Duplicate active identity.\n');
1706
+ return { source, target, docs: [source, target, collision] };
1707
+ });
1708
+ });
1709
+
1710
+ checkCase('2ay review5 P2. real alias-collision blocks retirement at warning and error severity across APIs', () => {
1711
+ runCollisionAuthorityFixture('alias-collision', 'alias-collision', () => {
1712
+ const target = revisionDoc({ id: 'PRD-GRPH-700', revision: 1, status: 'effective', relativePath: 'docs/prd/target/r001.md' });
1713
+ const source = revisionDoc({
1714
+ id: 'PRD-IFCE-700', revision: 1, status: 'effective', relativePath: 'docs/prd/source/r001.md',
1715
+ aliases: ['PRD-IFCE-799'], supersedes: ['PRD-GRPH-700@r1'], conflicts_with: ['PRD-GRPH-700@r1']
1716
+ });
1717
+ const collision = docObject('docs/reports/alias.md', { id: 'PRD-IFCE-799', status: 'final' }, 'Claimed alias as canonical.\n');
1718
+ return { source, target, docs: [source, target, collision] };
1719
+ });
1720
+ });
1721
+
1722
+ checkCase('2ay review5 P2. real duplicate-req collision blocks retirement at warning and error severity across APIs', () => {
1723
+ runCollisionAuthorityFixture('duplicate-req', 'duplicate-req', () => {
1724
+ const body = '### REQ-IFCE-777: Shared\n\nAcceptance: same requirement identity.\n';
1725
+ const target = revisionDoc({ id: 'PRD-GRPH-700', revision: 1, status: 'effective', relativePath: 'docs/prd/target/r001.md' });
1726
+ const source = revisionDoc({
1727
+ id: 'PRD-IFCE-700', revision: 1, status: 'effective', relativePath: 'docs/prd/source/r001.md', body,
1728
+ supersedes: ['PRD-GRPH-700@r1'], conflicts_with: ['PRD-GRPH-700@r1']
1729
+ });
1730
+ const other = revisionDoc({ id: 'PRD-IFCE-701', revision: 1, status: 'effective', relativePath: 'docs/prd/other/r001.md', body });
1731
+ return { source, target, docs: [source, other, target] };
1732
+ });
1733
+ });
1734
+
1735
+ checkCase('2ay case 8. missing or mismatched migration records and digests are rejected', () => {
1736
+ const missing = prepareMigrationRepo('baseline-migration-missing-record');
1737
+ fs.unlinkSync(missing.migrationRecordPath);
1738
+ const missingResult = baseLint(missing.root, missing.oldRevision);
1739
+ assert.ok(missingResult.diagnostics.some(item => item.severity === 'error'), 'missing migration record is an error');
1740
+
1741
+ const wrongRecord = prepareMigrationRepo('baseline-migration-wrong-record');
1742
+ const alteredRecord = { ...wrongRecord.migrationRecord, migrated_from_digest: sha256(Buffer.from('wrong prior artifact', 'utf8')) };
1743
+ fs.writeFileSync(wrongRecord.migrationRecordPath, JSON.stringify(alteredRecord, null, 2) + '\n', 'utf8');
1744
+ const wrongRecordResult = baseLint(wrongRecord.root, wrongRecord.oldRevision);
1745
+ assert.ok(wrongRecordResult.diagnostics.some(item => item.severity === 'error'), 'record digest must agree with the old stored artifact');
1746
+
1747
+ const wrongSourceDigest = prepareMigrationRepo('baseline-migration-wrong-source-digest');
1748
+ const current = lifecycle.collectDocs(wrongSourceDigest.root, CONFIG).find(doc => doc.path === 'docs/prd/living.md');
1749
+ const changed = reviseFixture(current, { migrated_from_digest: sha256(Buffer.from('forged prior artifact', 'utf8')) });
1750
+ writeDoc(wrongSourceDigest.root, changed.path, changed.frontmatter, changed.body);
1751
+ const changedRecord = { ...wrongSourceDigest.migrationRecord, migrated_from_digest: changed.frontmatter.migrated_from_digest };
1752
+ fs.writeFileSync(wrongSourceDigest.migrationRecordPath, JSON.stringify(changedRecord, null, 2) + '\n', 'utf8');
1753
+ const wrongSourceResult = baseLint(wrongSourceDigest.root, wrongSourceDigest.oldRevision);
1754
+ assert.ok(wrongSourceResult.diagnostics.some(item => item.severity === 'error'), 'matching forged metadata cannot replace the actual old-blob digest');
1755
+ });
1756
+
1757
+ checkCase('2ay fix. trusted base ID-only refs escalate when the old target path is changed', () => {
1758
+ const target = revisionDoc({ revision: 1, status: 'effective', relativePath: 'docs/prd/sample/r001.md' });
1759
+ const source = docObject('docs/reports/consumer.md', { id: 'REPORT-001', status: 'final' }, 'See PRD-GRPH-001 for the contract.\n');
1760
+ const withoutBase = analyzeDocs([source], { changedPaths: [target.path] });
1761
+ assert.ok(withoutBase.diagnostics.some(item => item.code === 'unresolved-ref' && item.severity === 'warning'), 'without a trusted ID-to-path baseline, the unchanged source remains a warning');
1762
+ const withBase = analyzeDocs([source], {
1763
+ changedPaths: [target.path],
1764
+ baseInventory: [{
1765
+ id: target.frontmatter.id,
1766
+ revision: target.frontmatter.revision,
1767
+ path: target.path,
1768
+ status: target.frontmatter.status,
1769
+ bytes: target.storedBytes
1770
+ }]
1771
+ });
1772
+ assert.ok(withBase.diagnostics.some(item => item.code === 'unresolved-ref' && item.severity === 'error'), 'the trusted base maps the ID to its changed/deleted former path');
1773
+ });
1774
+
1775
+ checkCase('2ay case 8. migration metadata cannot exempt later content edits from effective immutability', () => {
1776
+ const state = prepareMigrationRepo('baseline-migration-later-edit');
1777
+ mutateRevision(state.root, {}, `${state.livingBody}\nA later edit with migration metadata still present.\n`, 'docs/prd/living.md');
1778
+ const current = lifecycle.collectDocs(state.root, CONFIG).find(doc => doc.path === 'docs/prd/living.md');
1779
+ const parsed = lifecycle.parseFrontmatter(fs.readFileSync(state.livingPath, 'utf8'));
1780
+ const edited = reviseFixture({ ...current, body: parsed.body }, {});
1781
+ writeDoc(state.root, edited.path, edited.frontmatter, edited.body);
1782
+ expectCode(baseLint(state.root, state.migrationCommit).diagnostics, 'immutable-revision-modified');
1783
+ });
1784
+
1785
+ checkCase('2ay case 8. identical trees yield byte-identical index and revision diff in reversed order', () => {
1786
+ const first = revisionDoc({ revision: 1, status: 'effective', body: fixtureBody('r001.body.md') });
1787
+ const second = revisionDoc({ revision: 2, status: 'draft', previousRevision: 1, body: fixtureBody('r002-one-replaced.body.md') }, first);
1788
+ const docs = [first, second];
1789
+ const forwardIndex = lifecycle.buildIndex(docs, CONFIG);
1790
+ const reverseIndex = lifecycle.buildIndex([...docs].reverse(), CONFIG);
1791
+ assert.strictEqual(forwardIndex, reverseIndex, 'index bytes must not depend on collection order');
1792
+ const forwardDiff = diffRevisions(docs, pin('PRD-GRPH-001', 1), pin('PRD-GRPH-001', 2));
1793
+ const reverseDiff = diffRevisions([...docs].reverse(), pin('PRD-GRPH-001', 1), pin('PRD-GRPH-001', 2));
1794
+ assert.strictEqual(JSON.stringify(forwardDiff), JSON.stringify(reverseDiff), 'diff bytes must not depend on collection order');
1795
+ });
1796
+
1797
+ checkCase('2ay case 8. CRLF working copy retains LF Git-blob identity for a trusted baseline', () => {
1798
+ const { root, base } = seedEffectiveRepo('crlf-blob-identity', 'true');
1799
+ const file = path.join(root, 'docs/prd/sample/r001.md');
1800
+ const source = fs.readFileSync(file, 'utf8').replace(/\r?\n/g, '\r\n');
1801
+ fs.writeFileSync(file, source, 'utf8');
1802
+ assert.ok(root.includes('spaces') && root.includes('臺灣'), 'Git fixture path includes spaces and non-ASCII characters');
1803
+ const blob = gitBytes(root, ['show', `${base}:docs/prd/sample/r001.md`]);
1804
+ assert.ok(!blob.includes(0x0d), 'committed repository blob uses LF bytes');
1805
+ assert.ok(fs.readFileSync(file, 'utf8').includes('\r\n'), 'working copy uses CRLF bytes');
1806
+ const expectedBlobId = git(root, ['rev-parse', `${base}:docs/prd/sample/r001.md`]);
1807
+ const normalizedWorkingBlobId = git(root, ['hash-object', `--path=docs/prd/sample/r001.md`, 'docs/prd/sample/r001.md']);
1808
+ assert.strictEqual(normalizedWorkingBlobId, expectedBlobId, 'expected and actual Git blob identities match after EOL normalization');
1809
+ const result = spawnSync(process.execPath, [
1810
+ modulePath, 'lint', '--workspace', root,
1811
+ '--config', path.join(root, 'lifecycle-config.json'), '--all', '--base', base
1812
+ ], { cwd: root, encoding: 'utf8', env: gitEnvironment() });
1813
+ assert.ifError(result.error);
1814
+ assert.strictEqual(result.status, 0, `line-ending conversion must not change immutable identity: ${result.stderr} ${result.stdout}`);
1815
+ const output = JSON.parse(result.stdout);
1816
+ assert.strictEqual(output.evidence.immutability, 'observed');
1817
+ assert.deepStrictEqual(output.diagnostics.filter(item => item.severity === 'error'), [], 'same Git blob identity is an unchanged effective artifact');
1818
+
1819
+ const changed = fs.readFileSync(file, 'utf8').replace('Acceptance: preserve this exact contract.', 'Acceptance: this contract has changed.');
1820
+ fs.writeFileSync(file, changed, 'utf8');
1821
+ const changedBlobId = git(root, ['hash-object', '--path=docs/prd/sample/r001.md', 'docs/prd/sample/r001.md']);
1822
+ assert.notStrictEqual(changedBlobId, expectedBlobId, 'real content mutation changes the normalized Git blob');
1823
+ expectCode(baseLint(root, base).diagnostics, 'immutable-revision-modified');
1824
+ });
1825
+
1826
+ checkCase('2ay case 8. isolated source, Claude, and Codex package trees agree on file-backed D6.9 behavior', () => {
1827
+ const packagedRoot = path.join(__dirname, '..', 'plugins', 'harness-everything', 'skills', 'harness-everything');
1828
+ const sourceSkillRoot = path.dirname(path.dirname(modulePath));
1829
+ assert.ok(fs.existsSync(path.join(packagedRoot, 'scripts', 'doc-lifecycle.js')), 'Codex package lifecycle runtime exists');
1830
+ const claudeCopyRoot = path.join(tempRoot, 'isolated Claude plugin', 'harness-everything');
1831
+ const codexCopyRoot = path.join(tempRoot, 'isolated Codex plugin', 'harness-everything');
1832
+ fs.cpSync(sourceSkillRoot, claudeCopyRoot, { recursive: true });
1833
+ fs.cpSync(packagedRoot, codexCopyRoot, { recursive: true });
1834
+ const resolverCopies = [
1835
+ [path.join(__dirname, '..', 'multi-agent-workspace', 'scripts', 'project-docs-resolver.js'), path.join(path.dirname(claudeCopyRoot), 'multi-agent-workspace', 'scripts', 'project-docs-resolver.js')],
1836
+ [path.join(path.dirname(packagedRoot), 'multi-agent-workspace', 'scripts', 'project-docs-resolver.js'), path.join(path.dirname(codexCopyRoot), 'multi-agent-workspace', 'scripts', 'project-docs-resolver.js')]
1837
+ ];
1838
+ for (const [source, target] of resolverCopies) {
1839
+ fs.mkdirSync(path.dirname(target), { recursive: true });
1840
+ fs.copyFileSync(source, target);
1841
+ }
1842
+
1843
+ const packageFixtureRoot = path.join(fixtureRoot, 'packaged');
1844
+ const expected = JSON.parse(fs.readFileSync(path.join(packageFixtureRoot, 'expected.json'), 'utf8'));
1845
+ const validRoot = path.join(packageFixtureRoot, 'valid');
1846
+ const invalidRoot = path.join(packageFixtureRoot, 'invalid-fork');
1847
+ const r1Bytes = fs.readFileSync(path.join(validRoot, 'docs/prd/graph/r001.md'));
1848
+ const r2Bytes = fs.readFileSync(path.join(validRoot, 'docs/prd/graph/r002.md'));
1849
+ assert.strictEqual(sha256(r1Bytes), expected.valid.r001ArtifactDigest, 'static r1 fixture bytes match independent golden artifact hash');
1850
+ assert.strictEqual(sha256(r2Bytes), expected.valid.r002ArtifactDigest, 'static r2 fixture bytes match independent golden artifact hash');
1851
+ const body1 = '### REQ-GRPH-010 (aliases: REQ-GRPH-100): Initial requirement\n\nAcceptance: resolve the original alias.\n\n### REQ-GRPH-011: Stable requirement\n\nAcceptance: preserve the stable behavior.\n';
1852
+ const body2 = '### REQ-GRPH-010 (aliases: REQ-GRPH-100, REQ-GRPH-101): Initial requirement\n\nAcceptance: resolve the original alias.\n\n### REQ-GRPH-011: Stable requirement\n\nAcceptance: preserve the stable behavior.\n\n### REQ-GRPH-012: Added requirement\n\nAcceptance: include the new behavior.\n';
1853
+ const expectedR1Approval = approvalContentDigest({ id: 'PRD-GRPH-001', revision: 1, status: 'effective', drafted_by: 'Alice Example', effective_at: '2026-10-17T00:01:00Z', approval: {} }, body1);
1854
+ const expectedR2Approval = approvalContentDigest({ id: 'PRD-GRPH-001', revision: 2, status: 'effective', drafted_by: 'Alice Example', previous_revision: 1, previous_digest: expected.valid.r001ArtifactDigest, effective_at: '2026-10-17T00:02:00Z', approval: {} }, body2);
1855
+ assert.strictEqual(expected.valid.r001ApprovalDigest, expectedR1Approval, 'static r1 approval digest matches the independent projection oracle');
1856
+ assert.strictEqual(expected.valid.r002ApprovalDigest, expectedR2Approval, 'static r2 approval digest matches the independent projection oracle');
1857
+ assert.ok(r1Bytes.toString('utf8').includes(expected.valid.r001ApprovalDigest));
1858
+ assert.ok(r2Bytes.toString('utf8').includes(expected.valid.r002ApprovalDigest));
1859
+
1860
+ const inspectScript = `
1861
+ const fs = require('fs');
1862
+ const lifecycle = require(process.argv[1]);
1863
+ const fixtureRoot = process.argv[2];
1864
+ const config = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
1865
+ const missing = ['collectDocs', 'analyzeDocs', 'lintDocs', 'resolveRef', 'buildIndex', 'diffRevisions'].filter(name => typeof lifecycle[name] !== 'function');
1866
+ if (missing.length) { process.stdout.write(JSON.stringify({ missing })); process.exit(0); }
1867
+ try {
1868
+ const validDocs = lifecycle.collectDocs(fixtureRoot + '/valid', config);
1869
+ const valid = lifecycle.analyzeDocs(validDocs, config, { all: true });
1870
+ const current = lifecycle.resolveRef(validDocs, 'PRD-GRPH-001');
1871
+ const pinned = lifecycle.resolveRef(validDocs, 'PRD-GRPH-001@r1#REQ-GRPH-100');
1872
+ const introducedAlias = lifecycle.resolveRef(validDocs, 'PRD-GRPH-001@r2#REQ-GRPH-101');
1873
+ const oldAliasAtR2 = lifecycle.resolveRef(validDocs, 'PRD-GRPH-001@r1#REQ-GRPH-101');
1874
+ const index = lifecycle.buildIndex(validDocs, config);
1875
+ const diff = lifecycle.diffRevisions(validDocs, 'PRD-GRPH-001@r1', 'PRD-GRPH-001@r2');
1876
+ const reverseIndex = lifecycle.buildIndex([...validDocs].reverse(), config);
1877
+ const reverseDiff = lifecycle.diffRevisions([...validDocs].reverse(), 'PRD-GRPH-001@r1', 'PRD-GRPH-001@r2');
1878
+ const invalidDocs = lifecycle.collectDocs(fixtureRoot + '/invalid-fork', config);
1879
+ const invalid = lifecycle.analyzeDocs(invalidDocs, config, { all: true });
1880
+ const invalidCurrent = lifecycle.resolveRef(invalidDocs, 'PRD-GRPH-001');
1881
+ process.stdout.write(JSON.stringify({
1882
+ valid: { diagnostics: valid.diagnostics, evidence: valid.evidence, projectedDiagnostics: lifecycle.lintDocs(validDocs, config, { all: true }), current: { path: current.doc && current.doc.path, revision: current.revision, status: current.status, digest: current.digest, provenance: current.provenance }, pinned: { path: pinned.doc && pinned.doc.path, revision: pinned.revision, status: pinned.status, digest: pinned.digest, requirement: pinned.requirement, provenance: pinned.provenance }, introducedAlias: { path: introducedAlias.doc && introducedAlias.doc.path, requirement: introducedAlias.requirement }, oldAliasAtR2: { error: oldAliasAtR2.error }, index, diff, reverseIndex, reverseDiff },
1883
+ invalid: { diagnostics: invalid.diagnostics, evidence: invalid.evidence, current: { path: invalidCurrent.doc && invalidCurrent.doc.path, error: invalidCurrent.error } }
1884
+ }));
1885
+ } catch (error) {
1886
+ process.stdout.write(JSON.stringify({ runtimeError: error && error.name || 'Error', message: String(error && error.message || error) }));
1887
+ }
1888
+ `;
1889
+ const configPath = path.join(packageFixtureRoot, 'config.json');
1890
+ const runtimes = [
1891
+ ['source', modulePath],
1892
+ ['Claude isolated package', path.join(claudeCopyRoot, 'scripts', 'doc-lifecycle.js')],
1893
+ ['Codex isolated package', path.join(codexCopyRoot, 'scripts', 'doc-lifecycle.js')]
1894
+ ];
1895
+ const results = [];
1896
+ for (const [name, runtimePath] of runtimes) {
1897
+ const repeated = [];
1898
+ for (let run = 0; run < 2; run++) {
1899
+ const child = spawnSync(process.execPath, ['-e', inspectScript, runtimePath, packageFixtureRoot, configPath], {
1900
+ cwd: tempRoot, encoding: 'utf8', env: gitEnvironment()
1901
+ });
1902
+ assert.ifError(child.error);
1903
+ assert.strictEqual(child.status, 0, `${name} process exit: ${child.stderr}`);
1904
+ const output = JSON.parse(child.stdout);
1905
+ assert.deepStrictEqual(output.missing || [], [], `${name} exports the complete D6.9 surface`);
1906
+ assert.strictEqual(output.runtimeError, undefined, `${name} executes without runtime/setup errors: ${JSON.stringify(output)}`);
1907
+ repeated.push({ output, bytes: child.stdout });
1908
+ }
1909
+ assert.strictEqual(repeated[0].bytes, repeated[1].bytes, `${name} repeated clean runs are byte-deterministic`);
1910
+ results.push(repeated[0].output);
1911
+ }
1912
+ assert.strictEqual(JSON.stringify(results[0]), JSON.stringify(results[1]), 'source and Claude package serialized output bytes agree');
1913
+ assert.strictEqual(JSON.stringify(results[0]), JSON.stringify(results[2]), 'source and Codex package serialized output bytes agree');
1914
+
1915
+ const valid = results[0].valid;
1916
+ assert.deepStrictEqual(valid.diagnostics, [], 'file-backed valid chain is clean');
1917
+ assert.deepStrictEqual(valid.projectedDiagnostics, valid.diagnostics, 'lintDocs remains the diagnostics-only projection');
1918
+ assert.strictEqual(valid.evidence.immutability, 'not-observed');
1919
+ assert.strictEqual(valid.current.path, 'docs/prd/graph/r002.md');
1920
+ assert.strictEqual(valid.current.revision, 2);
1921
+ assert.strictEqual(valid.current.status, 'effective');
1922
+ assert.strictEqual(valid.current.digest, expected.valid.r002ArtifactDigest);
1923
+ assert.ok(valid.current.provenance, 'current resolution exposes its revision provenance');
1924
+ assert.strictEqual(valid.pinned.path, 'docs/prd/graph/r001.md');
1925
+ assert.strictEqual(valid.pinned.revision, 1);
1926
+ assert.strictEqual(valid.pinned.status, 'effective');
1927
+ assert.strictEqual(valid.pinned.digest, expected.valid.r001ArtifactDigest);
1928
+ assert.strictEqual(valid.pinned.requirement.id, 'REQ-GRPH-010');
1929
+ assert.ok(valid.pinned.provenance, 'pinned resolution exposes its revision provenance');
1930
+ assert.strictEqual(valid.introducedAlias.path, 'docs/prd/graph/r002.md');
1931
+ assert.strictEqual(valid.introducedAlias.requirement.id, 'REQ-GRPH-010');
1932
+ assert.ok(valid.oldAliasAtR2.error, 'r2-only alias never resolves in historical r1');
1933
+ assert.ok(valid.index.includes('docs/prd/graph/r001.md') && valid.index.includes('docs/prd/graph/r002.md'), 'index contains both real revision files');
1934
+ assert.deepStrictEqual(valid.diff.requirements.kept, ['REQ-GRPH-010', 'REQ-GRPH-011']);
1935
+ assert.deepStrictEqual(valid.diff.requirements.added, ['REQ-GRPH-012']);
1936
+ assert.deepStrictEqual(valid.diff.requirements.changed, []);
1937
+ assert.deepStrictEqual(valid.diff.requirements.removed, []);
1938
+ assert.deepStrictEqual(valid.diff.requirements.replaced, []);
1939
+ assert.strictEqual(valid.index, valid.reverseIndex, 'index output is deterministic under reversed history input');
1940
+ assert.strictEqual(JSON.stringify(valid.diff), JSON.stringify(valid.reverseDiff), 'diff output is deterministic under reversed history input');
1941
+ assert.ok(results[0].invalid.diagnostics.some(item => item.code === 'revision-fork'), 'packaged diagnostic fixture produces its specified fork finding');
1942
+ assert.strictEqual(results[0].invalid.current.path, null, 'forked graph has no arbitrary current revision');
1943
+
1944
+ const invalidConfigRoot = path.join(tempRoot, 'package-cli-invalid-config');
1945
+ fs.mkdirSync(invalidConfigRoot, { recursive: true });
1946
+ const invalidConfigPath = path.join(invalidConfigRoot, 'config.json');
1947
+ fs.writeFileSync(invalidConfigPath, JSON.stringify({ governed: 'docs/adr/**' }), 'utf8');
1948
+ for (const [name, runtimePath] of runtimes) {
1949
+ const child = spawnSync(process.execPath, [runtimePath, 'lint', '--workspace', invalidConfigRoot, '--config', invalidConfigPath, '--all'], {
1950
+ cwd: tempRoot, encoding: 'utf8', env: gitEnvironment()
1951
+ });
1952
+ assert.ifError(child.error);
1953
+ assert.strictEqual(child.status, 2, `${name} CLI rejects malformed explicit config: ${child.stdout} ${child.stderr}`);
1954
+ assert.match(child.stderr, /invalid project-docs config.*invalid-governed/i, `${name} CLI uses the shared config validator`);
1955
+ }
1956
+ });
1957
+
1958
+ // Codex review fix round 1 — close parity, authorization, continuity and
1959
+ // attribution gaps while keeping each regression independently observable.
1960
+ checkCase('2ay fix1. offline inventory matches Git for activation transition diagnostics', () => {
1961
+ const root = gitRepo('inventory-transition-parity');
1962
+ const first = revisionDoc({ revision: 1, status: 'effective', relativePath: 'docs/prd/sample/r001.md' });
1963
+ const second = revisionDoc({ revision: 2, status: 'approved', previousRevision: 1, release_channel: 'stable', relativePath: 'docs/prd/sample/r002.md' }, first);
1964
+ writeDoc(root, first.path, first.frontmatter, first.body);
1965
+ writeDoc(root, second.path, second.frontmatter, second.body);
1966
+ const base = commitAll(root, 'effective and approved baseline');
1967
+ const forgedActivation = reviseFixture(second, {
1968
+ status: 'effective', effective_at: '2026-10-17T00:02:00Z', release_channel: 'beta'
1969
+ });
1970
+ writeDoc(root, forgedActivation.path, forgedActivation.frontmatter, forgedActivation.body);
1971
+
1972
+ const fromGit = baseLint(root, base);
1973
+ const fromInventory = analyzeDocs(lifecycle.collectDocs(root, CONFIG), {
1974
+ workspace: root,
1975
+ baseInventory: inventoryAt(root, base)
1976
+ });
1977
+ expectCode(fromGit.diagnostics, 'invalid-transition');
1978
+ assert.deepStrictEqual(fromInventory.diagnostics, fromGit.diagnostics, 'stored-byte inventory parses to the same transition model as the Git base');
1979
+ });
1980
+
1981
+ checkCase('2ay fix1. offline inventory matches Git for migration diagnostics', () => {
1982
+ const state = prepareMigrationRepo('inventory-migration-parity');
1983
+ const altered = { ...state.migrationRecord, migrated_from_digest: sha256(Buffer.from('forged migration source', 'utf8')) };
1984
+ fs.writeFileSync(state.migrationRecordPath, JSON.stringify(altered, null, 2) + '\n', 'utf8');
1985
+
1986
+ const fromGit = baseLint(state.root, state.oldRevision);
1987
+ const fromInventory = analyzeDocs(lifecycle.collectDocs(state.root, CONFIG), {
1988
+ workspace: state.root,
1989
+ baseInventory: inventoryAt(state.root, state.oldRevision)
1990
+ });
1991
+ expectCode(fromGit.diagnostics, 'migration-record-mismatch');
1992
+ assert.deepStrictEqual(fromInventory.diagnostics, fromGit.diagnostics, 'inventory bytes provide the migration source without reopening Git');
1993
+
1994
+ const detachedDocs = lifecycle.collectDocs(state.root, CONFIG).map(doc => {
1995
+ const detached = { ...doc };
1996
+ Object.defineProperty(detached, 'storedBytes', { value: doc.storedBytes, enumerable: false });
1997
+ return detached;
1998
+ });
1999
+ expectCode(analyzeDocs(detachedDocs, { baseInventory: inventoryAt(state.root, state.oldRevision) }).diagnostics, 'migration-record-missing', 'pure API mode reports that current migration evidence cannot be checked');
2000
+ });
2001
+
2002
+ checkCase('2ay fix1. draft, approved and rejected cross-PRD retirement edges stay pending', () => {
2003
+ for (const [index, status] of ['draft', 'approved', 'rejected'].entries()) {
2004
+ const target = revisionDoc({ revision: 1, status: 'effective', relativePath: `docs/prd/target-${index}/r001.md` });
2005
+ const targetPin = `${target.frontmatter.id}@r1`;
2006
+ const source = revisionDoc({
2007
+ id: `PRD-IFCE-00${index + 1}`, revision: 1, status,
2008
+ relativePath: `docs/prd/source-${index}/r001.md`,
2009
+ supersedes: [targetPin], conflicts_with: [targetPin]
2010
+ });
2011
+ const docs = [target, source];
2012
+ const diagnostics = analyzeDocs(docs).diagnostics;
2013
+ expectCode(diagnostics, 'recorded-conflict', `${status} source is not eligible to resolve the recorded conflict`);
2014
+ const indexText = lifecycle.buildIndex(docs, CONFIG, { all: true });
2015
+ const sourceLine = indexText.split('\n').find(line => line.includes(source.path)) || '';
2016
+ const targetLine = indexText.split('\n').find(line => line.includes(target.path)) || '';
2017
+ assert.ok(sourceLine.includes('pending supersedes: `' + targetPin + '`'), `${status} source edge is rendered as pending: ${sourceLine}`);
2018
+ assert.ok(!targetLine.includes('superseded by:'), `${status} source does not retire the target: ${targetLine}`);
2019
+ }
2020
+
2021
+ const target = revisionDoc({ revision: 1, status: 'effective', relativePath: 'docs/prd/invalid-source-target/r001.md' });
2022
+ const targetPin = `${target.frontmatter.id}@r1`;
2023
+ const invalidEffective = revisionDoc({
2024
+ id: 'PRD-IFCE-009', revision: 1, status: 'effective',
2025
+ relativePath: 'docs/prd/invalid-source/r001.md',
2026
+ supersedes: [targetPin], conflicts_with: [targetPin]
2027
+ });
2028
+ invalidEffective.frontmatter.approval.content_digest = sha256(Buffer.from('invalid effective source', 'utf8'));
2029
+ const docs = [target, invalidEffective];
2030
+ expectCode(analyzeDocs(docs).diagnostics, 'recorded-conflict', 'an invalid effective revision cannot resolve the conflict');
2031
+ const indexText = lifecycle.buildIndex(docs, CONFIG, { all: true });
2032
+ const sourceLine = indexText.split('\n').find(line => line.includes(invalidEffective.path)) || '';
2033
+ assert.ok(sourceLine.includes('pending supersedes:'), 'an invalid effective revision edge stays pending in the index');
2034
+ });
2035
+
2036
+ checkCase('2ay fix1. approved snapshots reject metadata, body and approval edits until returned to draft', () => {
2037
+ for (const [name, change] of [
2038
+ ['metadata', (doc, fm, body) => { fm.release_channel = 'beta'; }],
2039
+ ['body', (doc, fm, body) => { body = `${body}\nUnapproved addition.\n`; return body; }],
2040
+ ['approval', (doc, fm) => { fm.approval = { ...fm.approval, by: 'Replacement Reviewer' }; }]
2041
+ ]) {
2042
+ const { root, base } = seedApprovedRepo(`approved-freeze-${name}`);
2043
+ const original = lifecycle.collectDocs(root, CONFIG).find(doc => doc.path === 'docs/prd/sample/r001.md');
2044
+ const frontmatter = { ...original.frontmatter, approval: { ...original.frontmatter.approval } };
2045
+ const body = change(original, frontmatter, original.body) || original.body;
2046
+ frontmatter.approval.content_digest = approvalContentDigest(frontmatter, body);
2047
+ writeDoc(root, original.path, frontmatter, body);
2048
+ expectCode(baseLint(root, base).diagnostics, 'invalid-transition', `${name} edit while still approved`);
2049
+ }
2050
+
2051
+ const { root, base } = seedApprovedRepo('approved-freeze-return-to-draft');
2052
+ const original = lifecycle.collectDocs(root, CONFIG).find(doc => doc.path === 'docs/prd/sample/r001.md');
2053
+ writeDoc(root, original.path, { ...original.frontmatter, status: 'draft' }, `${original.body}\nDraft edit.\n`);
2054
+ forbidCode(baseLint(root, base).diagnostics, 'invalid-transition');
2055
+ });
2056
+
2057
+ checkCase('2ay fix1. removed REQ IDs cannot return with a different meaning', () => {
2058
+ const firstBody = '### REQ-GRPH-010: Original behavior\n\nAcceptance: keep the original behavior.\n\n### REQ-GRPH-011: Stable behavior\n\nAcceptance: keep stable behavior.\n';
2059
+ const secondBody = '### REQ-GRPH-011: Stable behavior\n\nAcceptance: keep stable behavior.\n';
2060
+ const thirdBody = '### REQ-GRPH-010: Unrelated behavior\n\nAcceptance: introduce an unrelated behavior under the retired identity.\n\n### REQ-GRPH-011: Stable behavior\n\nAcceptance: keep stable behavior.\n';
2061
+ const first = revisionDoc({ revision: 1, status: 'draft', body: firstBody });
2062
+ const second = revisionDoc({ revision: 2, status: 'draft', previousRevision: 1, removed_reqs: ['REQ-GRPH-010'], body: secondBody }, first);
2063
+ const third = revisionDoc({ revision: 3, status: 'draft', previousRevision: 2, body: thirdBody }, second);
2064
+ expectCode(lintDocs([first, second, third]), 'invalid-req-clause', 'tombstoned ID cannot be reused for changed acceptance');
2065
+ });
2066
+
2067
+ checkCase('2ay fix1. replaces targets resolve through historical aliases and reject missing or ambiguous names', () => {
2068
+ const parentByAlias = revisionDoc({
2069
+ revision: 1, status: 'draft',
2070
+ body: '### REQ-GRPH-010 (aliases: REQ-GRPH-100): Original behavior\n\nAcceptance: keep the original behavior.\n'
2071
+ });
2072
+ const childByAlias = revisionDoc({
2073
+ revision: 2, status: 'draft', previousRevision: 1,
2074
+ body: '### REQ-GRPH-020 (replaces: REQ-GRPH-100): Replacement behavior\n\nAcceptance: replace the original behavior.\n'
2075
+ }, parentByAlias);
2076
+ forbidCode(lintDocs([parentByAlias, childByAlias]), 'req-dropped');
2077
+ forbidCode(lintDocs([parentByAlias, childByAlias]), 'invalid-req-clause');
2078
+ assert.deepStrictEqual(
2079
+ diffRevisions([parentByAlias, childByAlias], pin('PRD-GRPH-001', 1), pin('PRD-GRPH-001', 2)).requirements.replaced,
2080
+ [{ from: 'REQ-GRPH-010', to: 'REQ-GRPH-020' }],
2081
+ 'diff resolves a historical alias to its canonical replaced REQ'
2082
+ );
2083
+
2084
+ const parent = revisionDoc({ revision: 1, status: 'draft', body: fixtureBody('r001.body.md') });
2085
+ const phantom = revisionDoc({
2086
+ revision: 2, status: 'draft', previousRevision: 1,
2087
+ body: `${fixtureBody('r001.body.md')}\n### REQ-GRPH-020 (replaces: REQ-GRPH-999): Phantom target\n\nAcceptance: reject nonexistent predecessor references.\n`
2088
+ }, parent);
2089
+ expectCode(lintDocs([parent, phantom]), 'invalid-req-clause', 'replacement target is absent from history');
2090
+
2091
+ const ambiguousParent = revisionDoc({
2092
+ revision: 1, status: 'draft',
2093
+ body: '### REQ-GRPH-010 (aliases: REQ-GRPH-090): First behavior\n\nAcceptance: keep first behavior.\n\n### REQ-GRPH-011 (aliases: REQ-GRPH-090): Second behavior\n\nAcceptance: keep second behavior.\n'
2094
+ });
2095
+ const ambiguousChild = revisionDoc({
2096
+ revision: 2, status: 'draft', previousRevision: 1,
2097
+ body: '### REQ-GRPH-020 (replaces: REQ-GRPH-090): Ambiguous replacement\n\nAcceptance: reject an ambiguous alias.\n'
2098
+ }, ambiguousParent);
2099
+ expectCode(lintDocs([ambiguousParent, ambiguousChild]), 'invalid-req-clause', 'ambiguous historical alias is not selected arbitrarily');
2100
+ });
2101
+
2102
+ checkCase('2ay fix1. approved and effective revisions require drafted_by and ordered timezone timestamps', () => {
2103
+ const missingAuthor = revisionDoc({ revision: 1, status: 'approved' });
2104
+ const missingAuthorFm = { ...missingAuthor.frontmatter };
2105
+ delete missingAuthorFm.drafted_by;
2106
+ missingAuthorFm.approval = { ...missingAuthorFm.approval };
2107
+ missingAuthorFm.approval.content_digest = approvalContentDigest(missingAuthorFm, missingAuthor.body);
2108
+ expectCode(lintDocs([docObject(missingAuthor.path, missingAuthorFm, missingAuthor.body)]), 'approval-missing', 'drafted_by is required');
2109
+
2110
+ const badApprovalTime = revisionDoc({ revision: 1, status: 'effective', effective_at: '2026-10-17T00:01:00Z' });
2111
+ badApprovalTime.frontmatter.approval.at = '2026-10-17T00:00:00';
2112
+ expectCode(lintDocs([docObject(badApprovalTime.path, badApprovalTime.frontmatter, badApprovalTime.body)]), 'invalid-transition', 'approval timestamp requires an ISO 8601 timezone');
2113
+
2114
+ const invertedTime = revisionDoc({ revision: 1, status: 'effective', effective_at: '2026-10-17T00:01:00Z' });
2115
+ invertedTime.frontmatter.approval.at = '2026-10-17T00:02:00Z';
2116
+ expectCode(lintDocs([docObject(invertedTime.path, invertedTime.frontmatter, invertedTime.body)]), 'invalid-transition', 'effective_at cannot precede approval.at');
2117
+ });
2118
+
2119
+ // Codex review fix round 2 — aliases, byte acquisition, trusted identity
2120
+ // ratcheting, index state and bounded Git process use.
2121
+ checkCase('2ay fix2. PRD aliases preserve logical identity across a domain rename and inherited pinned lookup', () => {
2122
+ const first = revisionDoc({
2123
+ id: 'PRD-GRPH-001', revision: 1, status: 'effective', aliases: ['PRD-GRPH-009'],
2124
+ relativePath: 'docs/prd/graph/r001.md'
2125
+ });
2126
+ const renamed = revisionDoc({
2127
+ id: 'PRD-IFCE-002', revision: 2, status: 'effective', previousRevision: 1,
2128
+ aliases: ['PRD-GRPH-001'], relativePath: 'docs/prd/interface/r002.md'
2129
+ }, first);
2130
+ const docs = [first, renamed];
2131
+ forbidCode(lintDocs(docs), 'invalid-alias');
2132
+ forbidCode(lintDocs(docs), 'missing-parent');
2133
+ forbidCode(lintDocs(docs), 'multiple-baselines');
2134
+ assert.strictEqual(lifecycle.resolveRef(docs, 'PRD-GRPH-001').doc.path, renamed.path, 'the historical canonical spelling resolves the current renamed PRD');
2135
+ assert.strictEqual(lifecycle.resolveRef(docs, 'PRD-IFCE-002').doc.path, renamed.path, 'the new canonical spelling resolves the current PRD');
2136
+ assert.strictEqual(lifecycle.resolveRef(docs, 'PRD-GRPH-001@r1').doc.path, first.path, 'the old canonical spelling remains pinned to its historical snapshot');
2137
+ assert.strictEqual(lifecycle.resolveRef(docs, 'PRD-GRPH-009@r2').doc.path, renamed.path, 'an inherited alias resolves against the requested later snapshot');
2138
+ assert.ok(!lifecycle.resolveRef(docs, 'PRD-IFCE-002@r1').doc, 'a future canonical spelling is not retroactively valid at r1');
2139
+
2140
+ const unrelated = revisionDoc({ id: 'PRD-IFCE-003', revision: 1, status: 'effective', aliases: ['PRD-GRPH-009'], relativePath: 'docs/prd/other/r001.md' });
2141
+ const collision = expectCode(lintDocs([first, unrelated]), 'alias-collision');
2142
+ assert.deepStrictEqual(collision.map(item => item.path), [first.path, unrelated.path].sort());
2143
+ });
2144
+
2145
+ checkCase('2ay fix2. Unicode paths use Git stored-byte semantics across text, eol and autocrlf settings', () => {
2146
+ const cases = [
2147
+ { autocrlf: 'false', attributes: '*.md text eol=lf\n' },
2148
+ { autocrlf: 'false', attributes: '*.md text eol=crlf\n' },
2149
+ { autocrlf: 'true', attributes: '*.md text\n' },
2150
+ { autocrlf: 'input', attributes: '*.md text=auto\n' }
2151
+ ];
2152
+ const relativePath = 'docs/prd/契約 signed [v1].md';
2153
+ for (const [index, variant] of cases.entries()) {
2154
+ const root = gitRepo(`unicode-stored-bytes-${index}`, variant.autocrlf);
2155
+ fs.writeFileSync(path.join(root, '.gitattributes'), variant.attributes, 'utf8');
2156
+ const seed = revisionDoc({ revision: 1, status: 'effective', relativePath });
2157
+ writeDoc(root, relativePath, seed.frontmatter, seed.body);
2158
+ const base = commitAll(root, `unicode stored bytes ${index}`);
2159
+ const working = fs.readFileSync(path.join(root, ...relativePath.split('/'))).toString('utf8');
2160
+ fs.writeFileSync(path.join(root, ...relativePath.split('/')), working.replace(/\r?\n/g, '\r\n'), 'utf8');
2161
+ const hashObject = git(root, ['hash-object', '--path', relativePath, relativePath]);
2162
+ const baselineBlob = git(root, ['rev-parse', `${base}:${relativePath}`]);
2163
+ assert.strictEqual(hashObject, baselineBlob, `Git independently normalizes ${relativePath} under ${JSON.stringify(variant)}`);
2164
+ const result = baseLint(root, base);
2165
+ forbidCode(result.diagnostics, 'approval-content-mismatch');
2166
+ forbidCode(result.diagnostics, 'immutable-revision-modified');
2167
+ }
2168
+
2169
+ const supplied = revisionDoc({ revision: 1, status: 'effective', relativePath });
2170
+ supplied.storedBytes = Buffer.from(supplied.storedBytes.toString('utf8').replace(/\n/g, '\r\n'), 'utf8');
2171
+ const direct = lifecycle.resolveRef([supplied], 'PRD-GRPH-001@r1');
2172
+ assert.strictEqual(direct.digest, sha256(supplied.storedBytes), 'pure API storedBytes are already stored bytes and are not normalized as a checkout');
2173
+ });
2174
+
2175
+ checkCase('2ay fix2. trusted base IDs include bare legacy, document aliases, REQ IDs and REQ aliases', () => {
2176
+ const legacy = docObject('docs/adr/legacy/GRPH-001.md', { id: 'GRPH-001', status: 'accepted' }, '# Archived decision\n');
2177
+ legacy.legacy = true;
2178
+ const owner = revisionDoc({
2179
+ revision: 1, status: 'effective', aliases: ['PRD-GRPH-009'],
2180
+ body: '### REQ-GRPH-010 (aliases: REQ-GRPH-100): Stable behavior\n\nAcceptance: preserve the existing behavior.\n'
2181
+ });
2182
+ const report = docObject('docs/reports/untouched.md', { id: 'REPORT-001', status: 'final' }, [
2183
+ 'Reference archived `legacy:GRPH-001`.',
2184
+ 'The renamed plan is `PRD-GRPH-009`.',
2185
+ 'The requirement identity is `REQ-GRPH-100`.'
2186
+ ].join('\n'));
2187
+ const baseInventory = [legacy, owner].map(doc => ({
2188
+ id: doc.frontmatter.id,
2189
+ revision: doc.frontmatter.revision,
2190
+ path: doc.path,
2191
+ status: doc.frontmatter.status,
2192
+ bytes: doc.storedBytes
2193
+ }));
2194
+ const analysis = analyzeDocs([report], {
2195
+ baseInventory,
2196
+ changedPaths: [legacy.path, owner.path]
2197
+ });
2198
+ for (const ref of ['legacy:GRPH-001', 'PRD-GRPH-009', 'REQ-GRPH-100']) {
2199
+ const item = analysis.diagnostics.find(diagnostic => diagnostic.id === ref);
2200
+ assert.ok(item, `missing-reference diagnostic exists for ${ref}: ${JSON.stringify(analysis.diagnostics)}`);
2201
+ assert.strictEqual(item.severity, 'error', `${ref} is escalated through its trusted base owner path`);
2202
+ assert.strictEqual(item.path, report.path, 'the diagnostic remains attached to the untouched declaring document');
2203
+ }
2204
+ });
2205
+
2206
+ checkCase('2ay fix2. index exposes exact current, historical, pending, invalid and pre-activation PRD state', () => {
2207
+ const first = revisionDoc({ revision: 1, status: 'effective', relativePath: 'docs/prd/graph/r001.md' });
2208
+ const current = revisionDoc({ revision: 2, status: 'effective', previousRevision: 1, relativePath: 'docs/prd/graph/r002.md' }, first);
2209
+ const pending = revisionDoc({ revision: 3, status: 'draft', previousRevision: 2, relativePath: 'docs/prd/graph/r003.md' }, current);
2210
+ const preActivation = revisionDoc({ id: 'PRD-IFCE-010', revision: 1, status: 'draft', relativePath: 'docs/prd/pre-activation/r001.md' });
2211
+ const invalidBase = revisionDoc({ id: 'PRD-IFCE-011', revision: 1, status: 'effective', relativePath: 'docs/prd/fork/r001.md' });
2212
+ const invalidLeft = revisionDoc({ id: 'PRD-IFCE-011', revision: 2, status: 'effective', previousRevision: 1, relativePath: 'docs/prd/fork/r002.md' }, invalidBase);
2213
+ const invalidRight = revisionDoc({ id: 'PRD-IFCE-011', revision: 3, status: 'effective', previousRevision: 1, relativePath: 'docs/prd/fork/r003.md' }, invalidBase);
2214
+ const retiring = revisionDoc({ id: 'PRD-IFCE-020', revision: 1, status: 'effective', relativePath: 'docs/prd/retiring/r001.md', supersedes: ['PRD-GRPH-001@r2'] });
2215
+ const docs = [first, current, pending, preActivation, invalidBase, invalidLeft, invalidRight, retiring];
2216
+ const index = lifecycle.buildIndex(docs, CONFIG, { all: true });
2217
+ const expected = [
2218
+ `- \`PRD-GRPH-001\` — state: valid; current: \`PRD-GRPH-001@r2\` (\`${current.path}\`; digest: \`${sha256(current.storedBytes)}\`); history: \`PRD-GRPH-001@r1\` -> \`PRD-GRPH-001@r2\`; pending: \`PRD-GRPH-001@r3\` (draft)`,
2219
+ '- `PRD-IFCE-010` — state: pre-activation; current: none; history: none; pending: `PRD-IFCE-010@r1` (draft)',
2220
+ '- `PRD-IFCE-011` — state: invalid; current: none; history: `PRD-IFCE-011@r1` -> `PRD-IFCE-011@r2`; `PRD-IFCE-011@r1` -> `PRD-IFCE-011@r3`; pending: none',
2221
+ `- \`PRD-IFCE-020\` — state: valid; current: \`PRD-IFCE-020@r1\` (\`${retiring.path}\`; digest: \`${sha256(retiring.storedBytes)}\`); history: \`PRD-IFCE-020@r1\`; pending: none`
2222
+ ].join('\n');
2223
+ const section = index.split('## PRD revision chains\n\n')[1]?.split('\n\n## Legacy documents')[0];
2224
+ assert.strictEqual(section, expected, 'index publishes exact logical PRD identity, current digest, history and non-current state');
2225
+ const oldRow = index.split('\n').find(line => line.includes(first.path)) || '';
2226
+ assert.ok(oldRow.includes('stored status: effective; derived state: historical'), `stored and derived state remain distinct: ${oldRow}`);
2227
+ const newRow = index.split('\n').find(line => line.includes(current.path)) || '';
2228
+ assert.ok(newRow.includes('stored status: effective; derived state: current'), `current revision state is explicit: ${newRow}`);
2229
+ const targetRow = index.split('\n').find(line => line.includes(current.path)) || '';
2230
+ assert.ok(targetRow.includes('retirement provenance: `PRD-IFCE-020@r1` -> `PRD-GRPH-001@r2`'), `retirement keeps exact source and target pins: ${targetRow}`);
2231
+ });
2232
+
2233
+ checkCase('2ay fix2. trusted base config retains effective identities outside current governed paths', () => {
2234
+ const root = gitRepo('base-configured-outside-current-scope');
2235
+ const oldConfig = { ...CONFIG, governed: [{ path: 'specifications/prd/**', role: 'prd' }], legacy: [] };
2236
+ fs.writeFileSync(path.join(root, 'lifecycle-config.json'), JSON.stringify(oldConfig, null, 2), 'utf8');
2237
+ const effective = revisionDoc({ revision: 1, status: 'effective', relativePath: 'specifications/prd/living.md' });
2238
+ writeDoc(root, effective.path, effective.frontmatter, effective.body);
2239
+ const base = commitAll(root, 'base config governs external PRD path');
2240
+ writeDoc(root, effective.path, effective.frontmatter, `${effective.body}\nUnapproved change.\n`);
2241
+ const currentConfig = { ...oldConfig, governed: [] };
2242
+ fs.writeFileSync(path.join(root, 'lifecycle-config.json'), JSON.stringify(currentConfig, null, 2), 'utf8');
2243
+ const result = lifecycle.analyzeDocs([], currentConfig, { workspace: root, base, all: true });
2244
+ assert.strictEqual(result.evidence.immutability, 'observed', `base config and effective identity are available: ${JSON.stringify(result)}`);
2245
+ expectCode(result.diagnostics, 'immutable-revision-modified', 'base config path remains part of the trusted identity inventory after candidate exclusion');
2246
+ });
2247
+
2248
+ checkCase('2ay fix2. base tree loading uses a bounded small number of Git processes with unrelated files', () => {
2249
+ const root = gitRepo('base-process-count');
2250
+ for (let index = 0; index < 40; index++) {
2251
+ const source = path.join(root, 'src', `unrelated-${String(index).padStart(2, '0')}.txt`);
2252
+ fs.mkdirSync(path.dirname(source), { recursive: true });
2253
+ fs.writeFileSync(source, `unrelated source ${index}\n`, 'utf8');
2254
+ }
2255
+ const base = commitAll(root, 'many unrelated files');
2256
+ const script = `
2257
+ const childProcess = require('child_process');
2258
+ const original = childProcess.execFileSync;
2259
+ let gitCalls = 0;
2260
+ childProcess.execFileSync = function(command, args, options) {
2261
+ if (command === 'git') gitCalls++;
2262
+ return original.call(this, command, args, options);
2263
+ };
2264
+ const lifecycle = require(process.argv[1]);
2265
+ const config = JSON.parse(require('fs').readFileSync(process.argv[3], 'utf8'));
2266
+ const result = lifecycle.analyzeDocs([], config, { workspace: process.argv[2], base: process.argv[4], all: true });
2267
+ process.stdout.write(JSON.stringify({ gitCalls, evidence: result.evidence }));
2268
+ `;
2269
+ const child = spawnSync(process.execPath, ['-e', script, modulePath, root, path.join(root, 'lifecycle-config.json'), base], {
2270
+ cwd: root,
2271
+ encoding: 'utf8',
2272
+ env: gitEnvironment()
2273
+ });
2274
+ assert.ifError(child.error);
2275
+ assert.strictEqual(child.status, 0, `instrumented analysis exits cleanly: ${child.stderr}`);
2276
+ const output = JSON.parse(child.stdout);
2277
+ assert.strictEqual(output.evidence.immutability, 'observed');
2278
+ assert.strictEqual(output.gitCalls, 3, `40 unrelated files require only rev-parse, ls-tree and one config-object batch (observed ${output.gitCalls})`);
2279
+ });
2280
+
2281
+ // Codex review fix round 3 — union historical config sources and Git's
2282
+ // autocrlf binary decision.
2283
+ checkCase('2ay fix3 P1. historical host-manifest governance protects excluded effective PRDs', () => {
2284
+ const root = gitRepo('historical-host-governance');
2285
+ const externalPath = 'specifications/contracts/r1.md';
2286
+ const historicalConfig = {
2287
+ governed: [{ path: 'specifications/contracts/**', role: 'prd' }],
2288
+ legacy: []
2289
+ };
2290
+ const hostManifest = path.join(root, '.claude', 'harness-everything', 'manifest.json');
2291
+ fs.mkdirSync(path.dirname(hostManifest), { recursive: true });
2292
+ fs.writeFileSync(hostManifest, JSON.stringify({ projectDocs: historicalConfig }, null, 2), 'utf8');
2293
+ const effective = revisionDoc({ revision: 1, status: 'effective', relativePath: externalPath });
2294
+ writeDoc(root, externalPath, effective.frontmatter, effective.body);
2295
+ const base = commitAll(root, 'host manifest governs external effective PRD');
2296
+
2297
+ writeDoc(root, externalPath, effective.frontmatter, `${effective.body}\nUnapproved historical change.\n`);
2298
+ fs.rmSync(hostManifest);
2299
+ const currentConfig = { ...CONFIG, governed: [] };
2300
+ const result = lifecycle.analyzeDocs([], currentConfig, { workspace: root, base, all: true });
2301
+ assert.strictEqual(result.evidence.immutability, 'observed', 'the trusted base remains available after host config removal');
2302
+ expectCode(result.diagnostics, 'immutable-revision-modified', 'base host projectDocs scope remains protected when current governed paths are empty');
2303
+ });
2304
+
2305
+ checkCase('2ay fix3 P1. the base copy of an explicit nonstandard --config path retains its governance', () => {
2306
+ const root = gitRepo('historical-explicit-config-path');
2307
+ const relativeConfigPath = 'settings/release-docs.json';
2308
+ const configPath = path.join(root, ...relativeConfigPath.split('/'));
2309
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
2310
+ fs.writeFileSync(configPath, JSON.stringify({ governed: [{ path: 'specifications/contracts/**', role: 'prd' }], legacy: [] }, null, 2), 'utf8');
2311
+ const relativePath = 'specifications/contracts/r1.md';
2312
+ const effective = revisionDoc({ revision: 1, status: 'effective', relativePath });
2313
+ writeDoc(root, relativePath, effective.frontmatter, effective.body);
2314
+ const base = commitAll(root, 'explicit config governs external effective PRD');
2315
+
2316
+ writeDoc(root, relativePath, effective.frontmatter, `${effective.body}\nUnapproved config-scoped change.\n`);
2317
+ fs.writeFileSync(configPath, JSON.stringify({ governed: [], legacy: [] }, null, 2), 'utf8');
2318
+ const result = spawnSync(process.execPath, [
2319
+ modulePath, 'lint', '--workspace', root, '--config', configPath, '--all', '--base', base
2320
+ ], { cwd: root, encoding: 'utf8', env: gitEnvironment() });
2321
+ assert.ifError(result.error);
2322
+ assert.ok([0, 1].includes(result.status), `explicit config CLI completes analysis (status ${result.status}; stderr: ${result.stderr})`);
2323
+ const output = JSON.parse(result.stdout);
2324
+ assert.strictEqual(output.evidence.immutability, 'observed', 'the trusted base is read with the explicit config path provenance');
2325
+ expectCode(output.diagnostics, 'immutable-revision-modified', 'base copy of the nonstandard explicit config keeps its historical PRD glob active');
2326
+ });
2327
+
2328
+ function assertAutocrlfBinaryIdentity(autocrlf) {
2329
+ const root = gitRepo(`autocrlf-binary-${autocrlf}`, autocrlf);
2330
+ fs.writeFileSync(path.join(root, '.gitattributes'), '', 'utf8');
2331
+ const relativePath = 'docs/prd/sample/binary-effective.md';
2332
+ const body = '# Binary-safe immutable contract\r\n\r\nAcceptance: preserve this byte.\0\r\n';
2333
+ const effective = revisionDoc({ revision: 1, status: 'effective', relativePath, body });
2334
+ writeDoc(root, relativePath, effective.frontmatter, body);
2335
+ const workingBytes = fs.readFileSync(path.join(root, ...relativePath.split('/')));
2336
+ const base = commitAll(root, `NUL binary PRD with autocrlf ${autocrlf}`);
2337
+ const storedBytes = gitBytes(root, ['show', `${base}:${relativePath}`]);
2338
+ const baseBlob = git(root, ['rev-parse', `${base}:${relativePath}`]);
2339
+ const independentlyHashedBlob = git(root, ['hash-object', `--path=${relativePath}`, relativePath]);
2340
+ assert.deepStrictEqual(storedBytes, workingBytes, `Git stores NUL-containing bytes unchanged with core.autocrlf=${autocrlf}`);
2341
+ assert.strictEqual(independentlyHashedBlob, baseBlob, `Git's own path hash confirms the blob identity under core.autocrlf=${autocrlf}`);
2342
+
2343
+ const result = baseLint(root, base);
2344
+ forbidCode(result.diagnostics, 'approval-content-mismatch', `unchanged binary body retains its bound digest with core.autocrlf=${autocrlf}`);
2345
+ forbidCode(result.diagnostics, 'immutable-revision-modified', `unchanged binary body retains its effective identity with core.autocrlf=${autocrlf}`);
2346
+ }
2347
+
2348
+ checkCase('2ay fix3 P2. autocrlf=true preserves NUL-containing binary bodies byte-for-byte without attributes', () => {
2349
+ assertAutocrlfBinaryIdentity('true');
2350
+ });
2351
+
2352
+ checkCase('2ay fix3 P2. autocrlf=input preserves NUL-containing binary bodies byte-for-byte without attributes', () => {
2353
+ assertAutocrlfBinaryIdentity('input');
2354
+ });
2355
+
2356
+ // Owner P1 regressions: path-role PRDs must not use the old mutable living
2357
+ // status vocabulary as an alternate route around D6.9.
2358
+ checkCase('2ay owner P1. changed active or superseded PRDs require D6.9 schema, while unchanged living files warn', () => {
2359
+ const cases = [
2360
+ docObject('docs/prd/no-id-active.md', { status: 'active' }, '# Old living text\n'),
2361
+ docObject('docs/prd/active-with-id.md', { id: 'PRD-GRPH-020', status: 'active' }, '# Old living text\n'),
2362
+ docObject('docs/prd/superseded.md', { id: 'PRD-GRPH-021', status: 'superseded' }, '# Old living text\n'),
2363
+ docObject('docs/prd/active-with-revision.md', { id: 'PRD-GRPH-022', revision: 1, status: 'active' }, '# Invalid D6.9 active revision\n')
2364
+ ];
2365
+ for (const doc of cases) {
2366
+ const diagnostics = analyzeDocs([doc], { changedPaths: [doc.path] }).diagnostics;
2367
+ const finding = doc.frontmatter.revision === undefined
2368
+ ? expectCode(diagnostics, 'prd-migration-required', `changed compatibility PRD ${doc.path}`)[0]
2369
+ : expectCode(diagnostics, 'invalid-status', `revisioned PRD cannot use active status ${doc.path}`)[0];
2370
+ assert.strictEqual(finding.severity, 'error');
2371
+ }
2372
+
2373
+ const old = cases[1];
2374
+ const unchanged = analyzeDocs([old], {
2375
+ baseInventory: [{ id: old.frontmatter.id, path: old.path, status: old.frontmatter.status, bytes: old.storedBytes }],
2376
+ changedPaths: []
2377
+ }).diagnostics;
2378
+ const warning = expectCode(unchanged, 'prd-migration-required', 'unchanged living PRD stays visible')[0];
2379
+ assert.strictEqual(warning.severity, 'warning');
2380
+ const unbased = expectCode(analyzeDocs([old], { changedPaths: [] }).diagnostics, 'prd-migration-required')[0];
2381
+ assert.match(unbased.message, /no trusted base proves it is unchanged/i, 'an unbased warning states the evidence limit');
2382
+
2383
+ const validDraft = revisionDoc({ revision: 1, status: 'draft' });
2384
+ const draftDiagnostics = analyzeDocs([validDraft], { all: true }).diagnostics;
2385
+ for (const code of ['invalid-prd-id', 'invalid-revision', 'invalid-status', 'prd-migration-required']) forbidCode(draftDiagnostics, code);
2386
+ const missingIdAndRevision = docObject('docs/prd/new-no-id.md', { status: 'active' }, '# New active PRD\n');
2387
+ assert.ok(analyzeDocs([missingIdAndRevision], { changedPaths: [missingIdAndRevision.path] }).diagnostics.some(item => item.severity === 'error'), 'new PRDs without an ID or revision are rejected');
2388
+ });
2389
+
2390
+ checkCase('2ay owner P1. trusted-base byte differences and newly governed paths join the ratchet input', () => {
2391
+ const root = gitRepo('owner-p1-objective-ratchet');
2392
+ const relativePath = 'docs/prd/living.md';
2393
+ const originalBody = '# Existing living contract\n\nAcceptance: keep current behavior.\n';
2394
+ writeDoc(root, relativePath, { id: 'PRD-GRPH-031', status: 'active' }, originalBody);
2395
+ const base = commitAll(root, 'old living PRD');
2396
+ writeDoc(root, relativePath, { id: 'PRD-GRPH-031', status: 'active' }, `${originalBody}\nChanged behavior.\n`);
2397
+ const changedDocs = lifecycle.collectDocs(root, CONFIG);
2398
+ const changed = lifecycle.analyzeDocs(changedDocs, CONFIG, { workspace: root, base, changedPaths: [] });
2399
+ const changedFinding = expectCode(changed.diagnostics, 'prd-migration-required', 'base-visible content edit cannot be omitted from changedPaths')[0];
2400
+ assert.strictEqual(changedFinding.severity, 'error');
2401
+
2402
+ const adoptedRoot = gitRepo('owner-p1-newly-governed');
2403
+ const adoptedPath = 'specifications/prd/living.md';
2404
+ const adoptedBody = '# Existing contract outside prior governance\n';
2405
+ const priorConfig = { governed: [], legacy: [] };
2406
+ fs.writeFileSync(path.join(adoptedRoot, 'lifecycle-config.json'), JSON.stringify(priorConfig), 'utf8');
2407
+ writeDoc(adoptedRoot, adoptedPath, { id: 'PRD-GRPH-032', status: 'active' }, adoptedBody);
2408
+ const prior = commitAll(adoptedRoot, 'path before PRD governance');
2409
+ const currentConfig = { ...CONFIG, governed: [{ path: 'specifications/prd/**', role: 'prd' }] };
2410
+ const adoptedDocs = lifecycle.collectDocs(adoptedRoot, currentConfig);
2411
+ const adopted = lifecycle.analyzeDocs(adoptedDocs, currentConfig, { workspace: adoptedRoot, base: prior, changedPaths: [] });
2412
+ const adoptionFinding = expectCode(adopted.diagnostics, 'prd-migration-required', 'newly governed old living PRD requires schema adoption')[0];
2413
+ assert.strictEqual(adoptionFinding.severity, 'error');
2414
+ });
2415
+
2416
+ checkCase('2ay owner P1. revision-one stamping cannot hide a changed living baseline', () => {
2417
+ const root = gitRepo('owner-p1-forged-migration');
2418
+ const relativePath = 'docs/prd/living.md';
2419
+ const originalBody = '# Existing living contract\n\nAcceptance: keep old behavior.\n';
2420
+ writeDoc(root, relativePath, { id: 'PRD-GRPH-033', status: 'active' }, originalBody);
2421
+ const base = commitAll(root, 'living PRD before migration');
2422
+ const forged = revisionDoc({
2423
+ id: 'PRD-GRPH-033', revision: 1, status: 'effective', relativePath,
2424
+ body: `${originalBody}\nUnreviewed new behavior.\n`
2425
+ });
2426
+ writeDoc(root, relativePath, forged.frontmatter, forged.body);
2427
+ const result = lifecycle.analyzeDocs(lifecycle.collectDocs(root, CONFIG), CONFIG, {
2428
+ workspace: root, base, changedPaths: []
2429
+ });
2430
+ expectCode(result.diagnostics, 'migration-record-missing', 'baseline content change requires recorded metadata-only adoption');
2431
+
2432
+ const renamedRoot = gitRepo('owner-p1-renamed-forged-migration');
2433
+ const oldPath = 'docs/prd/living.md';
2434
+ const oldBody = '# Existing living contract\n\nAcceptance: keep old behavior.\n';
2435
+ writeDoc(renamedRoot, oldPath, { id: 'PRD-GRPH-034', status: 'active' }, oldBody);
2436
+ const renamedBase = commitAll(renamedRoot, 'living PRD before rename and revision stamp');
2437
+ const moved = revisionDoc({
2438
+ id: 'PRD-GRPH-034', revision: 1, status: 'effective',
2439
+ relativePath: 'docs/prd/renamed/r001.md', body: `${oldBody}\nUnreviewed edit.\n`
2440
+ });
2441
+ writeDoc(renamedRoot, moved.path, moved.frontmatter, moved.body);
2442
+ const renamedResult = lifecycle.analyzeDocs(lifecycle.collectDocs(renamedRoot, CONFIG), CONFIG, {
2443
+ workspace: renamedRoot, base: renamedBase, changedPaths: []
2444
+ });
2445
+ expectCode(renamedResult.diagnostics, 'migration-record-missing', 'moving a living PRD cannot bypass migration evidence');
2446
+ });
2447
+
2448
+ checkCase('2ay review2 P2. lifecycle file reads stay within document scope and skip unrelated subtrees', () => {
2449
+ const root = gitRepo('bounded-lifecycle-file-reads');
2450
+ const effective = revisionDoc({ revision: 1, status: 'effective', relativePath: 'docs/prd/sample/r001.md' });
2451
+ writeDoc(root, effective.path, effective.frontmatter, effective.body);
2452
+ const base = commitAll(root, 'bounded scan baseline');
2453
+ const unrelatedRoot = path.join(root, 'unrelated-generated-tree');
2454
+ const nodeModulesRoot = path.join(root, 'node_modules');
2455
+ fs.mkdirSync(path.join(unrelatedRoot, 'large-binary-batch'), { recursive: true });
2456
+ fs.mkdirSync(path.join(nodeModulesRoot, 'unrelated-package'), { recursive: true });
2457
+ const unrelatedFileCount = 40;
2458
+ const unrelatedBytesPerFile = 128 * 1024;
2459
+ for (let index = 0; index < unrelatedFileCount; index++) {
2460
+ fs.writeFileSync(path.join(unrelatedRoot, 'large-binary-batch', `chunk-${index}.bin`), Buffer.alloc(unrelatedBytesPerFile, index));
2461
+ }
2462
+ fs.writeFileSync(path.join(nodeModulesRoot, 'unrelated-package', 'payload.bin'), Buffer.alloc(unrelatedBytesPerFile, 0xff));
2463
+
2464
+ const originalReadFileSync = fs.readFileSync;
2465
+ const originalReaddirSync = fs.readdirSync;
2466
+ const reads = [];
2467
+ const visitedDirectories = [];
2468
+ const rootPrefix = `${path.resolve(root)}${path.sep}`;
2469
+ fs.readFileSync = function instrumentedReadFileSync(file, ...args) {
2470
+ if (typeof file === 'string') {
2471
+ const absolute = path.resolve(file);
2472
+ if (absolute.startsWith(rootPrefix)) reads.push(path.relative(root, absolute).split(path.sep).join('/'));
2473
+ }
2474
+ return originalReadFileSync.call(this, file, ...args);
2475
+ };
2476
+ fs.readdirSync = function instrumentedReaddirSync(directory, ...args) {
2477
+ if (typeof directory === 'string') {
2478
+ const absolute = path.resolve(directory);
2479
+ if (absolute === path.resolve(root) || absolute.startsWith(rootPrefix)) {
2480
+ visitedDirectories.push(path.relative(root, absolute).split(path.sep).join('/'));
2481
+ }
2482
+ }
2483
+ return originalReaddirSync.call(this, directory, ...args);
2484
+ };
2485
+
2486
+ let measured;
2487
+ try {
2488
+ const docs = lifecycle.collectDocs(root, CONFIG);
2489
+ const collectReads = reads.length;
2490
+ const collectVisited = [...visitedDirectories];
2491
+
2492
+ const noBaseStart = reads.length;
2493
+ lifecycle.analyzeDocs(docs, CONFIG, { workspace: root, all: true });
2494
+ const noBaseReads = reads.length - noBaseStart;
2495
+
2496
+ const withBaseStart = reads.length;
2497
+ lifecycle.analyzeDocs(docs, CONFIG, { workspace: root, base, all: true });
2498
+ const baselineReads = reads.slice(withBaseStart);
2499
+ measured = { collectReads, noBaseReads, baselineReads, collectVisited, visitedDirectories: [...visitedDirectories] };
2500
+ } finally {
2501
+ fs.readFileSync = originalReadFileSync;
2502
+ fs.readdirSync = originalReaddirSync;
2503
+ }
2504
+
2505
+ assert.ok(measured.collectReads <= 1, `collectDocs reads only its one governed document: ${measured.collectReads} reads (${reads.join(', ')})`);
2506
+ assert.strictEqual(measured.noBaseReads, 0, 'analysis without a trusted baseline reuses collected docs instead of inventorying the checkout');
2507
+ assert.ok(measured.baselineReads.length <= 2, `baseline candidate reads stay bounded to governed/provenance paths: ${measured.baselineReads.length} reads (${measured.baselineReads.join(', ')})`);
2508
+ for (const ignored of ['.git', 'node_modules', 'unrelated-generated-tree']) {
2509
+ assert.ok(!measured.visitedDirectories.some(directory => directory === ignored || directory.startsWith(`${ignored}/`)), `traversal prunes ${ignored} before descending`);
2510
+ }
2511
+ console.log(`[2ay] review2 measured reads: ${unrelatedFileCount} unrelated binary files (${unrelatedFileCount * unrelatedBytesPerFile} bytes); collectDocs=${measured.collectReads}, no-baseline analyze=${measured.noBaseReads}, baseline analyze=${measured.baselineReads.length}`);
2512
+ });
2513
+
2514
+ checkCase('2ay owner P1. graph diagnostic severity includes changed candidate paths', () => {
2515
+ const parent = revisionDoc({ revision: 1, status: 'effective', relativePath: 'docs/prd/graph/r001.md' });
2516
+ const child = revisionDoc({
2517
+ revision: 2, status: 'draft', previousRevision: 1,
2518
+ previousDigest: sha256(Buffer.from('wrong parent bytes', 'utf8')),
2519
+ relativePath: 'docs/prd/graph/r002.md'
2520
+ }, parent);
2521
+ const diagnostics = analyzeDocs([parent, child], { changedPaths: [parent.path] }).diagnostics;
2522
+ const digestMismatch = expectCode(diagnostics, 'revision-digest-mismatch')[0];
2523
+ assert.strictEqual(digestMismatch.path, child.path, 'the graph diagnostic remains attached to its declaring child');
2524
+ assert.strictEqual(digestMismatch.severity, 'error', 'a changed parent candidate escalates the child diagnostic');
2525
+ });
2526
+
2527
+ checkCase('2ay owner P1. source and packaged lifecycle implementations agree on the active-status rejection', () => {
2528
+ const sourceSkillRoot = path.dirname(path.dirname(modulePath));
2529
+ const codexSkillRoot = path.join(__dirname, '..', 'plugins', 'harness-everything', 'skills', 'harness-everything');
2530
+ const claudeCopyRoot = path.join(tempRoot, 'owner P1 isolated Claude', 'harness-everything');
2531
+ const codexCopyRoot = path.join(tempRoot, 'owner P1 isolated Codex', 'harness-everything');
2532
+ fs.cpSync(sourceSkillRoot, claudeCopyRoot, { recursive: true });
2533
+ fs.cpSync(codexSkillRoot, codexCopyRoot, { recursive: true });
2534
+ const packageRoots = [
2535
+ modulePath,
2536
+ path.join(claudeCopyRoot, 'scripts', 'doc-lifecycle.js'),
2537
+ path.join(codexCopyRoot, 'scripts', 'doc-lifecycle.js')
2538
+ ];
2539
+ const oldLiving = docObject('docs/prd/parity.md', { id: 'PRD-GRPH-023', status: 'active' }, '# Compatibility only.\n');
2540
+ const outputs = packageRoots.map(runtimePath => {
2541
+ const implementation = require(runtimePath);
2542
+ return implementation.analyzeDocs([oldLiving], CONFIG, { changedPaths: [oldLiving.path] }).diagnostics;
2543
+ });
2544
+ assert.deepStrictEqual(outputs[0], outputs[1], 'source and isolated Claude tree return identical lifecycle diagnostics');
2545
+ assert.deepStrictEqual(outputs[0], outputs[2], 'source and Codex package return identical lifecycle diagnostics');
2546
+ assert.strictEqual(outputs[0][0].code, 'prd-migration-required');
2547
+ assert.strictEqual(outputs[0][0].severity, 'error');
2548
+ });
2549
+
2550
+
2551
+ // PR #327 owner re-review: preserve each trusted config source's authority.
2552
+ function competingMigrationRepo(name) {
2553
+ const root = gitRepo(name);
2554
+ const config = { ...CONFIG, governed: [{ path: 'specifications/contracts/**', role: 'prd' }] };
2555
+ fs.writeFileSync(path.join(root, 'lifecycle-config.json'), JSON.stringify(config));
2556
+ const manifest = path.join(root, '.claude/harness-everything/manifest.json');
2557
+ fs.mkdirSync(path.dirname(manifest), { recursive: true });
2558
+ fs.writeFileSync(manifest, JSON.stringify({ projectDocs: {
2559
+ governed: [], legacy: ['specifications/contracts/**']
2560
+ } }));
2561
+ const relativePath = 'specifications/contracts/living.md';
2562
+ const body = '# Existing contract\n\nAcceptance: retain behavior.\n';
2563
+ writeDoc(root, relativePath, { id: 'PRD-GRPH-080', status: 'active' }, body);
2564
+ const base = commitAll(root, 'independent PRD governance and competing legacy source');
2565
+ return { root, config, relativePath, body, base };
2566
+ }
2567
+
2568
+ checkCase('2ay review3 P1. competing legacy cannot hide changed-body baseline adoption without a record', () => {
2569
+ const { root, config, relativePath, body, base } = competingMigrationRepo('review3-migration-missing');
2570
+ const migrated = revisionDoc({ id: 'PRD-GRPH-080', revision: 1, status: 'effective', relativePath, body: body + '\nChanged behavior.\n' });
2571
+ writeDoc(root, relativePath, migrated.frontmatter, migrated.body);
2572
+ const result = lifecycle.analyzeDocs(lifecycle.collectDocs(root, config), config, { workspace: root, base, changedPaths: [] });
2573
+ expectCode(result.diagnostics, 'migration-record-missing');
2574
+ });
2575
+
2576
+ checkCase('2ay review3 P1. competing legacy accepts recorded metadata-only baseline adoption', () => {
2577
+ const { root, config, relativePath, body, base } = competingMigrationRepo('review3-migration-valid');
2578
+ const digest = sha256(gitBytes(root, ['show', `${base}:${relativePath}`]));
2579
+ const recordPath = 'migration.json';
2580
+ const migrated = revisionDoc({ id: 'PRD-GRPH-080', revision: 1, status: 'effective', relativePath, body,
2581
+ migrated_from_digest: digest, migration_record: recordPath });
2582
+ writeDoc(root, relativePath, migrated.frontmatter, body);
2583
+ fs.writeFileSync(path.join(root, recordPath), JSON.stringify({
2584
+ record: 'prd-baseline-migration', id: migrated.frontmatter.id, revision: 1, source: relativePath,
2585
+ migrated_from_digest: digest, approved_by: migrated.frontmatter.approval.by
2586
+ }));
2587
+ const result = lifecycle.analyzeDocs(lifecycle.collectDocs(root, config), config, { workspace: root, base, all: true });
2588
+ assert.deepStrictEqual(result.diagnostics.filter(item => item.severity === 'error'), []);
2589
+ });
2590
+
2591
+ checkCase('2ay review3 P1. competing legacy does not mark an unchanged governed living PRD newly adopted', () => {
2592
+ const { root, config, relativePath, base } = competingMigrationRepo('review3-unchanged-ratchet');
2593
+ const result = lifecycle.analyzeDocs(lifecycle.collectDocs(root, config), config, { workspace: root, base, changedPaths: [] });
2594
+ const finding = expectCode(result.diagnostics, 'prd-migration-required').find(item => item.path === relativePath);
2595
+ assert.strictEqual(finding.severity, 'warning', 'unchanged previously governed bytes remain a compatibility warning');
2596
+ });
2597
+
2598
+ function newlyGovernedRoleCli({ includeCandidateHost }) {
2599
+ const root = gitRepo(`dogfood-newly-governed-role-${includeCandidateHost ? 'with-host' : 'without-host'}`);
2600
+ fs.rmSync(path.join(root, 'lifecycle-config.json'));
2601
+ const configPath = path.join(root, 'lifecycle.json');
2602
+ const baseConfig = { governed: [{ path: 'specifications/contracts/**', role: 'reference' }], domains: ['IFCE'] };
2603
+ const candidateConfig = { governed: [{ path: 'specifications/contracts/**', role: 'prd' }], domains: ['IFCE'] };
2604
+ fs.writeFileSync(configPath, JSON.stringify(baseConfig), 'utf8');
2605
+ const hostPath = path.join(root, '.claude/harness-everything/manifest.json');
2606
+ fs.mkdirSync(path.dirname(hostPath), { recursive: true });
2607
+ fs.writeFileSync(hostPath, JSON.stringify({ projectDocs: {
2608
+ governed: [{ path: 'specifications/**', role: 'reference' }], domains: ['IFCE']
2609
+ } }), 'utf8');
2610
+ const relativePath = 'specifications/contracts/living.md';
2611
+ writeDoc(root, relativePath, { id: 'PRD-IFCE-001', status: 'active' }, '# Living contract\n');
2612
+ const base = commitAll(root, 'reference-governed living document baseline');
2613
+ fs.writeFileSync(configPath, JSON.stringify(candidateConfig), 'utf8');
2614
+ if (!includeCandidateHost) fs.rmSync(hostPath);
2615
+ const child = spawnSync(process.execPath, [
2616
+ modulePath, 'lint', '--workspace', root, '--config', configPath, '--changed', '', '--base', base
2617
+ ], { cwd: root, encoding: 'utf8', env: gitEnvironment() });
2618
+ assert.ifError(child.error);
2619
+ return { child, relativePath };
2620
+ }
2621
+
2622
+ checkCase('2ay dogfood. positive PRD source is not masked by a broader host reference source', () => {
2623
+ const { child, relativePath } = newlyGovernedRoleCli({ includeCandidateHost: true });
2624
+ assert.strictEqual(child.status, 1, `newly governed active PRD must fail lint: ${child.stdout} ${child.stderr}`);
2625
+ const result = JSON.parse(child.stdout);
2626
+ const finding = expectCode(result.diagnostics, 'prd-migration-required').find(item => item.path === relativePath);
2627
+ assert.strictEqual(finding.severity, 'error');
2628
+ assert.match(finding.message, /changed or newly governed/);
2629
+ });
2630
+
2631
+ checkCase('2ay dogfood. newly governed PRD remains an error when its historical host source is removed', () => {
2632
+ const { child, relativePath } = newlyGovernedRoleCli({ includeCandidateHost: false });
2633
+ assert.strictEqual(child.status, 1, `newly governed active PRD must fail lint: ${child.stdout} ${child.stderr}`);
2634
+ const result = JSON.parse(child.stdout);
2635
+ const finding = expectCode(result.diagnostics, 'prd-migration-required').find(item => item.path === relativePath);
2636
+ assert.strictEqual(finding.severity, 'error');
2637
+ });
2638
+
2639
+ checkCase('2ay dogfood. harness-everything skill archive lints without optional sibling skills and still validates config', () => {
2640
+ const repositoryRoot = path.resolve(__dirname, '..');
2641
+ const archiveSource = path.join(tempRoot, 'harness-only-archive-source');
2642
+ const archiveDestination = path.join(tempRoot, 'harness-only-archive-layout');
2643
+ fs.mkdirSync(archiveSource, { recursive: true });
2644
+ fs.cpSync(path.join(repositoryRoot, 'harness-everything'), path.join(archiveSource, 'harness-everything'), { recursive: true });
2645
+ git(archiveSource, ['init', '--quiet']);
2646
+ git(archiveSource, ['config', 'user.name', 'Selective Install Test']);
2647
+ git(archiveSource, ['config', 'user.email', 'selective-install@example.invalid']);
2648
+ git(archiveSource, ['add', 'harness-everything']);
2649
+ git(archiveSource, ['commit', '--quiet', '-m', 'harness-only skill snapshot']);
2650
+ const archivePath = path.join(tempRoot, 'harness-only.tar');
2651
+ fs.writeFileSync(archivePath, gitBytes(archiveSource, ['archive', '--format=tar', 'HEAD', 'harness-everything']));
2652
+ fs.mkdirSync(archiveDestination, { recursive: true });
2653
+ execFileSync('tar', ['-xf', 'harness-only.tar', '-C', 'harness-only-archive-layout'], { cwd: tempRoot });
2654
+
2655
+ const runtime = path.join(archiveDestination, 'harness-everything', 'scripts', 'doc-lifecycle.js');
2656
+ assert.ok(fs.existsSync(runtime), 'the archive contains the harness-everything lifecycle runtime');
2657
+ assert.ok(!fs.existsSync(path.join(archiveDestination, 'multi-agent-workspace')));
2658
+ assert.ok(!fs.existsSync(path.join(archiveDestination, 'to-spec')));
2659
+ const workspace = path.join(tempRoot, 'harness-only-workspace');
2660
+ fs.mkdirSync(workspace, { recursive: true });
2661
+ fs.cpSync(path.join(fixtureRoot, 'packaged', 'valid'), workspace, { recursive: true });
2662
+ const configPath = path.join(workspace, 'lifecycle-config.json');
2663
+ fs.writeFileSync(configPath, JSON.stringify(CONFIG, null, 2), 'utf8');
2664
+
2665
+ const valid = spawnSync(process.execPath, [runtime, 'lint', '--workspace', workspace, '--config', configPath, '--all'], {
2666
+ cwd: tempRoot, encoding: 'utf8', env: gitEnvironment()
2667
+ });
2668
+ assert.ifError(valid.error);
2669
+ assert.strictEqual(valid.status, 0, `valid config in the isolated skill archive lints normally: ${valid.stderr} ${valid.stdout}`);
2670
+ assert.deepStrictEqual(JSON.parse(valid.stdout).diagnostics.filter(item => item.severity === 'error'), []);
2671
+
2672
+ fs.writeFileSync(configPath, JSON.stringify({ governed: 'not-an-array', domains: ['IFCE'] }), 'utf8');
2673
+ const invalid = spawnSync(process.execPath, [runtime, 'lint', '--workspace', workspace, '--config', configPath, '--all'], {
2674
+ cwd: tempRoot, encoding: 'utf8', env: gitEnvironment()
2675
+ });
2676
+ assert.ifError(invalid.error);
2677
+ assert.strictEqual(invalid.status, 2, 'invalid project-docs config keeps the CLI setup error exit code');
2678
+ assert.match(invalid.stderr, /invalid project-docs config.*invalid-governed/);
2679
+ });
2680
+
2681
+ checkCase('2ay review3 P2a. deleting approved r2 is invalid', () => {
2682
+ const root = gitRepo('review3-approved-deleted');
2683
+ const r1 = revisionDoc({ revision: 1, status: 'effective' });
2684
+ const r2 = revisionDoc({ revision: 2, status: 'approved', previousRevision: 1 }, r1);
2685
+ writeDoc(root, r1.path, r1.frontmatter, r1.body);
2686
+ writeDoc(root, r2.path, r2.frontmatter, r2.body);
2687
+ const base = commitAll(root, 'effective r1 and approved r2');
2688
+ fs.rmSync(path.join(root, r2.path));
2689
+ expectCode(baseLint(root, base).diagnostics, 'invalid-transition', 'approved revision cannot be deleted');
2690
+ });
2691
+
2692
+ checkCase('2ay review3 P2a. moving approved r2 outside governance is invalid', () => {
2693
+ const root = gitRepo('review3-approved-outside');
2694
+ const r1 = revisionDoc({ revision: 1, status: 'effective' });
2695
+ const r2 = revisionDoc({ revision: 2, status: 'approved', previousRevision: 1 }, r1);
2696
+ writeDoc(root, r1.path, r1.frontmatter, r1.body);
2697
+ writeDoc(root, r2.path, r2.frontmatter, r2.body);
2698
+ const base = commitAll(root, 'effective r1 and approved r2');
2699
+ fs.mkdirSync(path.join(root, 'outside'), { recursive: true });
2700
+ fs.renameSync(path.join(root, r2.path), path.join(root, 'outside/r2.md'));
2701
+ expectCode(baseLint(root, base).diagnostics, 'invalid-transition', 'approved revision cannot move outside governance');
2702
+ });
2703
+
2704
+ checkCase('2ay review3 P2a. approved r2 cannot be renumbered to r3 with a rebound approval', () => {
2705
+ const root = gitRepo('review3-approved-renumber-control');
2706
+ const r1 = revisionDoc({ revision: 1, status: 'effective' });
2707
+ const r2 = revisionDoc({ revision: 2, status: 'approved', previousRevision: 1 }, r1);
2708
+ writeDoc(root, r1.path, r1.frontmatter, r1.body);
2709
+ writeDoc(root, r2.path, r2.frontmatter, r2.body);
2710
+ const base = commitAll(root, 'effective r1 and approved r2');
2711
+ const r3 = reviseFixture(r2, { revision: 3 });
2712
+ writeDoc(root, r3.path, r3.frontmatter, r3.body);
2713
+ expectCode(baseLint(root, base).diagnostics, 'invalid-transition', 'approved r2 cannot disappear through renumbering');
2714
+ });
2715
+
2716
+ checkCase('2ay review3 P2a. approved r2 may return to draft and effective deletion retains its diagnostic', () => {
2717
+ const root = gitRepo('review3-approved-draft-control');
2718
+ const r1 = revisionDoc({ revision: 1, status: 'effective' });
2719
+ const r2 = revisionDoc({ revision: 2, status: 'approved', previousRevision: 1 }, r1);
2720
+ writeDoc(root, r1.path, r1.frontmatter, r1.body);
2721
+ writeDoc(root, r2.path, r2.frontmatter, r2.body);
2722
+ const base = commitAll(root, 'approved r2 control');
2723
+ writeDoc(root, r2.path, { ...r2.frontmatter, status: 'draft' }, r2.body);
2724
+ const draft = baseLint(root, base);
2725
+ assert.deepStrictEqual(draft.diagnostics.filter(item => item.severity === 'error'), []);
2726
+ fs.rmSync(path.join(root, r1.path));
2727
+ expectCode(baseLint(root, base).diagnostics, 'immutable-revision-deleted');
2728
+ });
2729
+
2730
+
2731
+ checkCase('2ay review3 P1. independent PRD authority retains deleted living requirement identities', () => {
2732
+ const { root, config, relativePath } = competingMigrationRepo('review3-base-req-paths');
2733
+ // The earlier host source calls the same path a report; it cannot mask
2734
+ // the independent lifecycle config's PRD authority.
2735
+ const manifest = path.join(root, '.claude/harness-everything/manifest.json');
2736
+ fs.writeFileSync(manifest, JSON.stringify({ projectDocs: {
2737
+ governed: [{ path: 'specifications/contracts/**', role: 'report' }], legacy: []
2738
+ } }));
2739
+ writeDoc(root, relativePath, { id: 'PRD-GRPH-080', status: 'active' },
2740
+ '### REQ-GRPH-080: Existing requirement\n\nAcceptance: retain behavior.\n');
2741
+ const consumer = docObject('docs/reports/consumer.md', { id: 'REPORT-080', status: 'final', supersedes: ['REQ-GRPH-080'] }, '# Consumer\n');
2742
+ writeDoc(root, consumer.path, consumer.frontmatter, consumer.body);
2743
+ const base = commitAll(root, 'overlapping historical report and PRD authority');
2744
+ fs.rmSync(path.join(root, relativePath));
2745
+ const currentConfig = { ...config, governed: [{ path: 'docs/reports/**', role: 'report' }] };
2746
+ const result = lifecycle.analyzeDocs(lifecycle.collectDocs(root, currentConfig), currentConfig, { workspace: root, base, changedPaths: [] });
2747
+ const finding = expectCode(result.diagnostics, 'unresolved-ref').find(item => item.id === 'REQ-GRPH-080');
2748
+ assert.ok(finding, 'consumer has an unresolved requirement reference');
2749
+ assert.strictEqual(finding.severity, 'error', 'deleted governed requirement contributes its changed base path');
2750
+ assert.ok(finding.candidates.includes(relativePath), 'diagnostic retains the trusted requirement path');
2751
+ });
2752
+
2753
+ checkCase('2ay dogfood A. offline baseInventory shares historical sources across migration, REQ identity and objective change checks', () => {
2754
+ const governedSource = {
2755
+ governed: [
2756
+ { path: 'specifications/contracts/**', role: 'prd' },
2757
+ { path: 'docs/reports/**', role: 'report' }
2758
+ ],
2759
+ domains: ['GRPH'], legacy: []
2760
+ };
2761
+ const competingLegacySource = {
2762
+ governed: [], domains: ['GRPH'], legacy: ['specifications/contracts/**']
2763
+ };
2764
+ const configSources = [governedSource, competingLegacySource];
2765
+ const config = governedSource;
2766
+ const offlineRun = (root, inventory, extras = {}) => {
2767
+ const {
2768
+ config: candidateConfig = config,
2769
+ configSources: candidateConfigSources = configSources,
2770
+ baseConfigSources = configSources,
2771
+ ...analysisExtras
2772
+ } = extras;
2773
+ const inputPath = path.join(tempRoot, `offline-authority-${crypto.randomBytes(6).toString('hex')}.json`);
2774
+ fs.writeFileSync(inputPath, JSON.stringify({
2775
+ root,
2776
+ config: candidateConfig,
2777
+ inventory: inventory.map(item => ({ ...item, bytes: Buffer.from(item.bytes || []).toString('base64') })),
2778
+ configSources: candidateConfigSources,
2779
+ baseConfigSources,
2780
+ extras: analysisExtras
2781
+ }), 'utf8');
2782
+ const script = `
2783
+ const fs = require('fs');
2784
+ const lifecycle = require(process.argv[2]);
2785
+ const input = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
2786
+ const inventory = input.inventory.map(item => ({ ...item, bytes: Buffer.from(item.bytes, 'base64') }));
2787
+ const docs = lifecycle.collectDocs(input.root, input.config);
2788
+ const result = lifecycle.analyzeDocs(docs, input.config, {
2789
+ workspace: input.root,
2790
+ baseInventory: inventory,
2791
+ baseConfigSources: input.baseConfigSources,
2792
+ configSources: input.configSources,
2793
+ all: true,
2794
+ ...input.extras
2795
+ });
2796
+ process.stdout.write(JSON.stringify(result));
2797
+ `;
2798
+ const child = spawnSync(process.execPath, ['-e', script, inputPath, modulePath], { cwd: tempRoot, encoding: 'utf8' });
2799
+ assert.ifError(child.error);
2800
+ assert.strictEqual(child.status, 0, `offline analyzer subprocess exits cleanly: ${child.stderr}`);
2801
+ return JSON.parse(child.stdout);
2802
+ };
2803
+ const writeAuthoritySources = root => {
2804
+ fs.writeFileSync(path.join(root, 'lifecycle-config.json'), JSON.stringify(config, null, 2), 'utf8');
2805
+ const manifest = path.join(root, '.claude/harness-everything/manifest.json');
2806
+ fs.mkdirSync(path.dirname(manifest), { recursive: true });
2807
+ fs.writeFileSync(manifest, JSON.stringify({ projectDocs: competingLegacySource }, null, 2), 'utf8');
2808
+ };
2809
+
2810
+ const migrationRoot = gitRepo('dogfood-offline-custom-migration');
2811
+ writeAuthoritySources(migrationRoot);
2812
+ const livingPath = 'specifications/contracts/graph/living.md';
2813
+ const livingBody = '# Living contract\n\nAcceptance: keep this behavior.\n';
2814
+ writeDoc(migrationRoot, livingPath, { id: 'PRD-GRPH-081', status: 'active' }, livingBody);
2815
+ const base = commitAll(migrationRoot, 'custom-scope living baseline');
2816
+ const oldBytes = gitBytes(migrationRoot, ['show', `${base}:${livingPath}`]);
2817
+ const migratedFromDigest = sha256(oldBytes);
2818
+ const migrationRecordPath = 'docs/migrations/prd-graph-081.json';
2819
+ const approvalPath = 'docs/approvals/prd-migration-review.md';
2820
+ const adoptedSeed = revisionDoc({
2821
+ id: 'PRD-GRPH-081', revision: 1, status: 'effective', draftedBy: 'Legacy Contract Owner',
2822
+ relativePath: livingPath, body: livingBody
2823
+ });
2824
+ const unrecordedAdoption = reviseFixture(adoptedSeed, {
2825
+ approval: { ...adoptedSeed.frontmatter.approval, ref: approvalPath }
2826
+ });
2827
+ writeDoc(migrationRoot, livingPath, unrecordedAdoption.frontmatter, livingBody);
2828
+ writeDoc(migrationRoot, approvalPath, { id: 'APPROVAL-081', status: 'final' }, '# Migration review\n');
2829
+
2830
+ const inventory = inventoryAt(migrationRoot, base);
2831
+ const gitMissing = baseLint(migrationRoot, base);
2832
+ const offlineMissing = offlineRun(migrationRoot, inventory);
2833
+ expectCode(gitMissing.diagnostics, 'migration-record-missing');
2834
+ expectCode(offlineMissing.diagnostics, 'migration-record-missing');
2835
+ assert.deepStrictEqual(offlineMissing.diagnostics, gitMissing.diagnostics, 'custom-scope offline migration matches trusted Git analysis');
2836
+
2837
+ const adoptedSeedWithMetadata = revisionDoc({
2838
+ id: 'PRD-GRPH-081', revision: 1, status: 'effective', draftedBy: 'Legacy Contract Owner',
2839
+ migrated_from_digest: migratedFromDigest, migration_record: migrationRecordPath,
2840
+ relativePath: livingPath, body: livingBody
2841
+ });
2842
+ const adopted = reviseFixture(adoptedSeedWithMetadata, {
2843
+ approval: { ...adoptedSeedWithMetadata.frontmatter.approval, ref: approvalPath }
2844
+ });
2845
+ writeDoc(migrationRoot, livingPath, adopted.frontmatter, livingBody);
2846
+ const migrationRecord = {
2847
+ record: 'prd-baseline-migration', id: 'PRD-GRPH-081', revision: 1,
2848
+ source: livingPath, migrated_from_digest: migratedFromDigest,
2849
+ approved_by: adopted.frontmatter.approval.by
2850
+ };
2851
+ const recordPath = path.join(migrationRoot, ...migrationRecordPath.split('/'));
2852
+ fs.mkdirSync(path.dirname(recordPath), { recursive: true });
2853
+ fs.writeFileSync(recordPath, `${JSON.stringify(migrationRecord, null, 2)}\n`, 'utf8');
2854
+ const gitValid = baseLint(migrationRoot, base);
2855
+ const offlineValid = offlineRun(migrationRoot, inventory);
2856
+ assert.deepStrictEqual(offlineValid.diagnostics, gitValid.diagnostics, 'valid metadata-only custom-scope adoption matches Git analysis');
2857
+ forbidCode(offlineValid.diagnostics, 'migration-record-missing');
2858
+ forbidCode(offlineValid.diagnostics, 'migration-record-mismatch');
2859
+
2860
+ const unchangedRoot = gitRepo('dogfood-offline-objective-unchanged');
2861
+ writeAuthoritySources(unchangedRoot);
2862
+ writeDoc(unchangedRoot, livingPath, { id: 'PRD-GRPH-082', status: 'active' }, livingBody);
2863
+ const unchangedBase = commitAll(unchangedRoot, 'unchanged custom living PRD');
2864
+ const unchangedResult = offlineRun(unchangedRoot, inventoryAt(unchangedRoot, unchangedBase));
2865
+ const unchangedMigration = expectCode(unchangedResult.diagnostics, 'prd-migration-required');
2866
+ assert.ok(unchangedMigration.every(item => item.severity === 'warning'), `competing historical legacy source cannot turn unchanged governed PRD into an error: ${JSON.stringify(unchangedMigration)}`);
2867
+
2868
+ const reqRoot = gitRepo('dogfood-offline-base-req-path');
2869
+ writeAuthoritySources(reqRoot);
2870
+ const reqPath = 'specifications/contracts/graph/requirements.md';
2871
+ writeDoc(reqRoot, reqPath, { id: 'PRD-GRPH-083', status: 'active' }, '### REQ-GRPH-083: Retained requirement\n\nAcceptance: keep identity.\n');
2872
+ const consumer = docObject('docs/reports/consumer.md', {
2873
+ id: 'GRPH-084', status: 'final', supersedes: ['REQ-GRPH-083']
2874
+ }, '# Consumer\n');
2875
+ writeDoc(reqRoot, consumer.path, consumer.frontmatter, consumer.body);
2876
+ const reqBase = commitAll(reqRoot, 'custom governed requirement baseline');
2877
+ fs.rmSync(path.join(reqRoot, ...reqPath.split('/')));
2878
+ const reportOnlyConfig = { governed: [{ path: 'docs/reports/**', role: 'report' }], domains: ['GRPH'], legacy: [] };
2879
+ const reqResult = offlineRun(reqRoot, inventoryAt(reqRoot, reqBase), {
2880
+ config: reportOnlyConfig,
2881
+ configSources: [reportOnlyConfig],
2882
+ all: false,
2883
+ changedPaths: []
2884
+ });
2885
+ const unresolved = expectCode(reqResult.diagnostics, 'unresolved-ref').find(item => item.id === 'REQ-GRPH-083');
2886
+ assert.ok(unresolved, `removed REQ resolves to the base identity entry: ${JSON.stringify(reqResult.diagnostics)}`);
2887
+ assert.strictEqual(unresolved.severity, 'error', 'deleted custom governed requirement contributes its changed base path');
2888
+ assert.ok(unresolved.candidates.includes(reqPath), 'unresolved ref retains the historical target path');
2889
+ });
2890
+
2891
+ helper.finish();