@dzhechkov/p-replicator 1.10.4 → 1.12.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 (68) hide show
  1. package/.dz-manifest.json +119 -47
  2. package/CHANGELOG.md +85 -0
  3. package/MULTIPLATFORM_ROADMAP.md +1 -1
  4. package/README/eng/01_quickstart.md +3 -3
  5. package/README/eng/02_user_guide.md +1 -1
  6. package/README/eng/03_admin_guide.md +2 -2
  7. package/README/eng/04_api_reference.md +11 -5
  8. package/README/eng/README.md +1 -1
  9. package/README/ru/01_quickstart.md +3 -3
  10. package/README/ru/02_user_guide.md +1 -1
  11. package/README/ru/03_admin_guide.md +2 -2
  12. package/README/ru/04_api_reference.md +11 -5
  13. package/README/ru/README.md +1 -1
  14. package/README/ru/html/index.html +7 -7
  15. package/README.md +154 -38
  16. package/bin/cli.js +0 -0
  17. package/package.json +12 -10
  18. package/sbom.json +226 -46
  19. package/scripts/check-pipeline-gaps.sh +413 -0
  20. package/src/commands/doctor.js +94 -4
  21. package/src/rule-components.json +11 -0
  22. package/src/utils.js +3 -8
  23. package/templates/.claude/agents/harvest-coordinator.md +10 -1
  24. package/templates/.claude/commands/feature.md +57 -9
  25. package/templates/.claude/commands/go.md +11 -0
  26. package/templates/.claude/commands/harvest.md +41 -3
  27. package/templates/.claude/commands/myinsights.md +21 -26
  28. package/templates/.claude/commands/replicate.md +10 -1
  29. package/templates/.claude/commands/start.md +8 -0
  30. package/templates/.claude/hooks/check-ports.cjs +409 -20
  31. package/templates/.claude/hooks/session-insights.cjs +158 -25
  32. package/templates/.claude/hooks/statusline.cjs +2 -2
  33. package/templates/.claude/hooks/write-insight.cjs +253 -0
  34. package/templates/.claude/rules/cost-of-detection-ladder.md +96 -0
  35. package/templates/.claude/rules/docker-ports.md +41 -19
  36. package/templates/.claude/rules/feature-lifecycle.md +14 -3
  37. package/templates/.claude/rules/honest-configuration.md +54 -0
  38. package/templates/.claude/rules/insights-capture.md +10 -5
  39. package/templates/.claude/rules/replicate-pipeline.md +4 -2
  40. package/templates/.claude/rules/skill-interface-protocol.md +1 -0
  41. package/templates/.claude/rules/swarm-file-evidence.md +46 -0
  42. package/templates/.claude/settings.json +13 -1
  43. package/templates/.claude/skills/knowledge-extractor/modules/01-agent-review.md +16 -5
  44. package/templates/.claude/skills/sparc-prd-mini/SKILL.md +86 -16
  45. package/tests/e2e/lifecycle.test.js +55 -9
  46. package/tests/e2e/packed-insights-writer.test.js +308 -0
  47. package/tests/fixtures/prep-traceability-fixture/docs/features/order-refund/01_specification.md +29 -0
  48. package/tests/fixtures/prep-traceability-fixture/docs/features/order-refund/02_pseudocode.md +57 -0
  49. package/tests/snapshot/baseline.json +24 -20
  50. package/tests/snapshot/templates.test.js +47 -0
  51. package/tests/unit/absence-is-not-emptiness.test.js +15 -1
  52. package/tests/unit/check-pipeline-gaps.test.js +94 -0
  53. package/tests/unit/check-ports.test.js +729 -2
  54. package/tests/unit/db-port-rule.test.js +36 -5
  55. package/tests/unit/detection-ladder-contract.test.js +302 -0
  56. package/tests/unit/detection-ladder-registry.test.js +52 -0
  57. package/tests/unit/doctor-insight-flow.test.js +315 -0
  58. package/tests/unit/external-dependency-check.test.js +19 -19
  59. package/tests/unit/honest-failure-rules.test.js +492 -0
  60. package/tests/unit/hooks-project-anchored.test.js +67 -3
  61. package/tests/unit/insights-docs-tell-the-truth.test.js +52 -31
  62. package/tests/unit/insights-dz-delegation.test.js +197 -0
  63. package/tests/unit/insights-writer.test.js +285 -0
  64. package/tests/unit/shipped-suite-context.test.js +3 -1
  65. package/tests/unit/traceability-machine-ids.test.js +413 -0
  66. package/tests/unit/traceability-negative-fixture.test.js +322 -0
  67. package/tests/unit/utils.test.js +3 -2
  68. package/LICENSE +0 -21
@@ -0,0 +1,413 @@
1
+ 'use strict';
2
+
3
+ const { describe, test } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('node:fs');
6
+ const os = require('node:os');
7
+ const path = require('node:path');
8
+ const { spawnSync } = require('node:child_process');
9
+
10
+ const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
11
+ const CHECKER = path.join(PACKAGE_ROOT, 'scripts', 'check-pipeline-gaps.sh');
12
+ const FEATURE_TEMPLATE = path.join(PACKAGE_ROOT, 'templates', '.claude', 'commands', 'feature.md');
13
+ const PROJECT_TEMPLATE = path.join(
14
+ PACKAGE_ROOT, 'templates', '.claude', 'skills', 'sparc-prd-mini', 'SKILL.md',
15
+ );
16
+
17
+ const DEFAULT_ROLES = {
18
+ specification: '01_specification.md',
19
+ pseudocode: '02_pseudocode.md',
20
+ architecture: '03_architecture.md',
21
+ refinement: '04_refinement.md',
22
+ completion: '05_completion.md',
23
+ };
24
+
25
+ function write(root, relative, body) {
26
+ const target = path.join(root, relative);
27
+ fs.mkdirSync(path.dirname(target), { recursive: true });
28
+ fs.writeFileSync(target, body);
29
+ return target;
30
+ }
31
+
32
+ function roleMap(root, name, heading, roles = DEFAULT_ROLES, extra = '') {
33
+ const rows = Object.entries(roles).map(([role, target]) => ` ${role}: ${target}`);
34
+ return write(root, name, `${heading}\n\n\`\`\`yaml\nDOCUMENT_ROLE_MAP:\n${rows.join('\n')}\n${extra}\`\`\`\n`);
35
+ }
36
+
37
+ function documents(root, slug, specification, pseudocode, roles = DEFAULT_ROLES) {
38
+ const prefix = path.join('docs', 'features', slug);
39
+ write(root, path.join(prefix, roles.specification), specification);
40
+ write(root, path.join(prefix, roles.pseudocode), pseudocode);
41
+ }
42
+
43
+ function algorithm(id, name = 'trace') {
44
+ return `### Algorithm: ${name}\n\nREQUIREMENT: \`${id}\`\n`;
45
+ }
46
+
47
+ function run(root, options = {}) {
48
+ const featureMap = options.featureMap || roleMap(
49
+ root, 'feature-map.md', '### Phase 1 document role map', options.roles || DEFAULT_ROLES,
50
+ );
51
+ const projectMap = options.projectMap || roleMap(
52
+ root, 'project-map.md', '### Project-level default',
53
+ options.projectRoles || options.roles || DEFAULT_ROLES,
54
+ );
55
+ const result = spawnSync('bash', [
56
+ options.checker || CHECKER, root, '--traceability', '--role-map-source', featureMap,
57
+ '--project-role-map-source', projectMap,
58
+ ], { encoding: 'utf8', timeout: options.timeout || 10000 });
59
+ return { status: result.status, signal: result.signal,
60
+ output: `${result.stdout || ''}${result.stderr || ''}` };
61
+ }
62
+
63
+ function temp(t, prefix = 'traceability-machine-ids-') {
64
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
65
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
66
+ return root;
67
+ }
68
+
69
+ describe('traceability machine IDs', () => {
70
+ test('P1 — source-only and target-only gaps independently fail with exact directions', (t) => {
71
+ const root = temp(t);
72
+ documents(root, 'orders', [
73
+ '### FR-orders-1 — declared and claimed',
74
+ '### FR-orders-2 — no algorithm',
75
+ '',
76
+ ].join('\n'), [
77
+ algorithm('FR-orders-1', 'known'),
78
+ algorithm('FR-orders-3', 'dangling'),
79
+ ].join('\n'));
80
+
81
+ const result = run(root);
82
+ assert.equal(result.status, 1, result.output);
83
+ assert.match(result.output, /GAP orders specification->pseudocode FR-orders-2/);
84
+ assert.match(result.output, /GAP orders pseudocode->specification FR-orders-3/);
85
+ assert.match(result.output, /missing-algorithm=1 orphan-algorithm=1/);
86
+ });
87
+
88
+ test('P2 — FR NFR and AC are distinct structural keys', (t) => {
89
+ const root = temp(t);
90
+ documents(root, 'billing', [
91
+ '### FR-billing-1 — functional',
92
+ '### NFR-billing-2 — quality',
93
+ '### AC-billing-3 — acceptance',
94
+ 'Prose FR-billing-9 is not a declaration.',
95
+ '### SC-FR-billing-1-1 — nested scenario',
96
+ '',
97
+ ].join('\n'), [
98
+ algorithm('FR-billing-1'),
99
+ 'Scenario table mentions NFR-billing-2 and AC-billing-3.',
100
+ algorithm('NFR-billing-4', 'dangling quality'),
101
+ ].join('\n'));
102
+
103
+ const result = run(root);
104
+ assert.equal(result.status, 1, result.output);
105
+ assert.match(result.output, /GAP billing specification->pseudocode AC-billing-3/);
106
+ assert.match(result.output, /GAP billing specification->pseudocode NFR-billing-2/);
107
+ assert.match(result.output, /GAP billing pseudocode->specification NFR-billing-4/);
108
+ assert.doesNotMatch(result.output, /GAP .*FR-billing-9/);
109
+ assert.doesNotMatch(result.output, /GAP .*SC-FR/);
110
+ });
111
+
112
+ test('P3 — renamed role targets work and malformed maps trigger inconclusive', (t) => {
113
+ const root = temp(t);
114
+ const roles = {
115
+ specification: 'requirements custom.md',
116
+ pseudocode: 'algorithm custom.md',
117
+ architecture: 'design custom.md',
118
+ refinement: 'risks custom.md',
119
+ completion: 'proof custom.md',
120
+ };
121
+ documents(root, 'renamed', '### FR-renamed-1 — mapped\n', algorithm('FR-renamed-1'), roles);
122
+ write(root, path.join('docs', roles.specification), '### FR-project-1 — root mapped\n');
123
+ write(root, path.join('docs', roles.pseudocode), algorithm('FR-project-1'));
124
+
125
+ const clean = run(root, { roles, projectRoles: roles });
126
+ assert.equal(clean.status, 0, clean.output);
127
+ assert.match(clean.output, /TRACE contour=project/);
128
+ assert.match(clean.output, /TRACE contour=renamed/);
129
+
130
+ const brokenRoles = { ...roles };
131
+ delete brokenRoles.pseudocode;
132
+ const brokenMap = roleMap(
133
+ root, 'broken-map.md', '### Phase 1 document role map', brokenRoles,
134
+ );
135
+ const broken = run(root, { roles, featureMap: brokenMap, projectRoles: roles });
136
+ assert.equal(broken.status, 2, broken.output);
137
+ assert.match(broken.output, /NOT-ESTABLISHED.*pseudocode/);
138
+
139
+ const invalidMaps = [
140
+ {
141
+ name: 'unknown-map.md',
142
+ map: roleMap(root, 'unknown-map.md', '### Phase 1 document role map', roles,
143
+ ' mystery: surprise.md\n'),
144
+ expected: /unknown role=mystery/,
145
+ },
146
+ {
147
+ name: 'empty-map.md',
148
+ map: roleMap(root, 'empty-map.md', '### Phase 1 document role map',
149
+ { ...roles, pseudocode: '' }),
150
+ expected: /role=pseudocode empty target/,
151
+ },
152
+ {
153
+ name: 'escaping-map.md',
154
+ map: roleMap(root, 'escaping-map.md', '### Phase 1 document role map',
155
+ { ...roles, specification: '../outside.md' }),
156
+ expected: /role=specification escaping target/,
157
+ },
158
+ ];
159
+ for (const fixture of invalidMaps) {
160
+ const rejected = run(root, { roles, featureMap: fixture.map, projectRoles: roles });
161
+ assert.equal(rejected.status, 2, `${fixture.name}: ${rejected.output}`);
162
+ assert.match(rejected.output, fixture.expected, fixture.name);
163
+ }
164
+
165
+ const renamedFeatureHeading = roleMap(
166
+ root, 'renamed-feature-heading.md', '### Phase 1 role map', roles,
167
+ );
168
+ const featureHeadingResult = run(root, {
169
+ roles, featureMap: renamedFeatureHeading, projectRoles: roles,
170
+ });
171
+ assert.equal(featureHeadingResult.status, 2, featureHeadingResult.output);
172
+ assert.match(featureHeadingResult.output,
173
+ /NOT-ESTABLISHED role-map=.* heading=### Phase 1 document role map has no DOCUMENT_ROLE_MAP/);
174
+
175
+ const renamedProjectHeading = roleMap(
176
+ root, 'renamed-project-heading.md', '### Project default', roles,
177
+ );
178
+ const projectHeadingResult = run(root, {
179
+ roles, projectMap: renamedProjectHeading, projectRoles: roles,
180
+ });
181
+ assert.equal(projectHeadingResult.status, 2, projectHeadingResult.output);
182
+ assert.match(projectHeadingResult.output,
183
+ /NOT-ESTABLISHED role-map=.* heading=### Project-level default has no DOCUMENT_ROLE_MAP/);
184
+ });
185
+
186
+ test('P4 — one executable preserves clean gap and inconclusive exit codes through the pipeline caller', (t) => {
187
+ const root = temp(t);
188
+ const caller = fs.readFileSync(FEATURE_TEMPLATE, 'utf8');
189
+ assert.match(caller,
190
+ /require\.resolve\('@dzhechkov\/p-replicator\/scripts\/check-pipeline-gaps\.sh'\)/);
191
+ assert.doesNotMatch(caller, /\.claude\/hooks\/check-pipeline-gaps\.sh/);
192
+ assert.match(caller, /Exit `0` is the only advancing result/);
193
+ documents(root, 'clean', '### FR-clean-1 — ok\n', algorithm('FR-clean-1'));
194
+ assert.equal(run(root).status, 0);
195
+
196
+ write(root, 'docs/features/clean/02_pseudocode.md', algorithm('FR-clean-2'));
197
+ const gap = run(root);
198
+ assert.equal(gap.status, 1, gap.output);
199
+ assert.match(gap.output, /VERDICT traceability=FAIL/);
200
+
201
+ fs.unlinkSync(path.join(root, 'docs', 'features', 'clean', '02_pseudocode.md'));
202
+ const inconclusive = run(root);
203
+ assert.equal(inconclusive.status, 2, inconclusive.output);
204
+ assert.match(inconclusive.output, /VERDICT traceability=NOT-ESTABLISHED/);
205
+ });
206
+
207
+ test('P5 — packed and freshly initialized consumer contains and runs the checker', (t) => {
208
+ const root = temp(t, 'p-replicator-consumer-');
209
+ write(root, 'package.json', '{"private":true}\n');
210
+ const packJson = path.join(root, 'npm-pack.json');
211
+ const packErr = path.join(root, 'npm-pack.stderr');
212
+ const packOutFd = fs.openSync(packJson, 'w');
213
+ const packErrFd = fs.openSync(packErr, 'w');
214
+ const packed = spawnSync('npm', ['pack', '--json', '--pack-destination', root], {
215
+ cwd: PACKAGE_ROOT,
216
+ timeout: 30000,
217
+ stdio: ['ignore', packOutFd, packErrFd],
218
+ env: { ...process.env, npm_config_cache: path.join(root, 'npm-cache') },
219
+ });
220
+ fs.closeSync(packOutFd);
221
+ fs.closeSync(packErrFd);
222
+ const packedOutput = fs.readFileSync(packJson, 'utf8');
223
+ const packedError = fs.readFileSync(packErr, 'utf8');
224
+ assert.equal(packed.status, 0, `${packed.error?.message ?? ''}\n${packedError}`);
225
+ const packReceipt = JSON.parse(packedOutput)[0];
226
+ const files = packReceipt.files.map((entry) => entry.path);
227
+ assert.ok(files.includes('scripts/check-pipeline-gaps.sh'), JSON.stringify(files));
228
+ assert.ok(!files.includes('scripts/sync-templates.js'), 'build-only sync script must stay excluded');
229
+
230
+ const tarball = path.join(root, packReceipt.filename);
231
+ const npmEnv = { ...process.env, npm_config_cache: path.join(root, 'npm-cache') };
232
+ const install = spawnSync('npm', [
233
+ 'install', '--ignore-scripts', '--no-package-lock', '--no-audit', '--no-fund',
234
+ '--omit=optional', tarball,
235
+ ], { cwd: root, encoding: 'utf8', timeout: 30000, env: npmEnv });
236
+ assert.equal(install.status, 0, `${install.stdout}\n${install.stderr}`);
237
+
238
+ const resolved = spawnSync(process.execPath, [
239
+ '-p', "require.resolve('@dzhechkov/p-replicator/scripts/check-pipeline-gaps.sh')",
240
+ ], { cwd: root, encoding: 'utf8', timeout: 10000 });
241
+ assert.equal(resolved.status, 0, `${resolved.stdout}\n${resolved.stderr}`);
242
+ const installedChecker = resolved.stdout.trim();
243
+ assert.equal(fs.existsSync(installedChecker), true, installedChecker);
244
+
245
+ const installedPackage = path.dirname(path.dirname(installedChecker));
246
+ const init = spawnSync(process.execPath, [path.join(installedPackage, 'bin', 'cli.js'), 'init'], {
247
+ cwd: root, encoding: 'utf8', timeout: 30000, env: npmEnv,
248
+ });
249
+ assert.equal(init.status, 0, `${init.stdout}\n${init.stderr}`);
250
+ const installedFeature = fs.readFileSync(path.join(root, '.claude', 'commands', 'feature.md'), 'utf8');
251
+ assert.match(installedFeature,
252
+ /require\.resolve\('@dzhechkov\/p-replicator\/scripts\/check-pipeline-gaps\.sh'\)/,
253
+ 'fresh init must retain a package-resolved checker invocation');
254
+ assert.equal(fs.existsSync(path.join(root, '.claude', 'hooks', 'check-pipeline-gaps.sh')), false,
255
+ 'the Node-only hook surface must not carry a shell projection');
256
+ documents(root, 'installed', '### FR-installed-1 — shipped\n', algorithm('FR-installed-1'));
257
+ const clean = spawnSync('bash', [installedChecker, root, '--traceability',
258
+ '--role-map-source', path.join(root, '.claude', 'commands', 'feature.md'),
259
+ '--project-role-map-source', path.join(root, '.claude', 'skills', 'sparc-prd-mini', 'SKILL.md'),
260
+ ], { encoding: 'utf8' });
261
+ assert.equal(clean.status, 0, `${clean.stdout}\n${clean.stderr}`);
262
+
263
+ write(root, 'docs/features/installed/02_pseudocode.md', algorithm('FR-installed-2'));
264
+ const orphan = spawnSync('bash', [installedChecker, root, '--traceability',
265
+ '--role-map-source', path.join(root, '.claude', 'commands', 'feature.md'),
266
+ '--project-role-map-source', path.join(root, '.claude', 'skills', 'sparc-prd-mini', 'SKILL.md'),
267
+ ], { encoding: 'utf8' });
268
+ assert.equal(orphan.status, 1, `${orphan.stdout}\n${orphan.stderr}`);
269
+ assert.match(`${orphan.stdout}${orphan.stderr}`,
270
+ /GAP installed pseudocode->specification FR-installed-2/);
271
+ });
272
+
273
+ test('P6 — every feature contour is checked without unsafe symlink traversal', (t) => {
274
+ const root = temp(t);
275
+ documents(root, 'alpha', '### FR-alpha-1 — clean\n', algorithm('FR-alpha-1'));
276
+ documents(root, 'middle-feature', '### FR-middle-feature-1 — gap\n', '');
277
+ documents(root, 'zulu', '### FR-zulu-1 — clean\n', algorithm('FR-zulu-1'));
278
+ const outside = temp(t, 'traceability-outside-');
279
+ fs.symlinkSync(outside, path.join(root, 'docs', 'features', 'escape'));
280
+
281
+ const result = run(root);
282
+ assert.equal(result.status, 1, result.output);
283
+ assert.match(result.output, /GAP middle-feature specification->pseudocode FR-middle-feature-1/);
284
+ assert.match(result.output, /NOT-ESTABLISHED.*escape.*symlink/);
285
+ assert.match(result.output, /TRACE contour=alpha/);
286
+ assert.match(result.output, /TRACE contour=zulu/);
287
+
288
+ const nestedRoot = temp(t, 'traceability-nested-symlink-');
289
+ const nestedRoles = {
290
+ ...DEFAULT_ROLES,
291
+ specification: 'mapped/requirements.md',
292
+ pseudocode: 'mapped/algorithms.md',
293
+ };
294
+ const contour = path.join(nestedRoot, 'docs', 'features', 'nested-symlink');
295
+ fs.mkdirSync(contour, { recursive: true });
296
+ fs.symlinkSync(outside, path.join(contour, 'mapped'));
297
+ const nested = run(nestedRoot, { roles: nestedRoles, projectRoles: nestedRoles });
298
+ assert.equal(nested.status, 2, nested.output);
299
+ assert.match(nested.output, /NOT-ESTABLISHED.*mapped.*symlink is not allowed/);
300
+ });
301
+
302
+ test('P7 — duplicate declarations are rejected as non-unique keys', (t) => {
303
+ const root = temp(t);
304
+ documents(root, 'duplicate', [
305
+ '### FR-duplicate-1 — first',
306
+ '### FR-duplicate-1 — second',
307
+ '',
308
+ ].join('\n'), [
309
+ '### Algorithm: first',
310
+ 'REQUIREMENT: `FR-duplicate-1`',
311
+ 'REQUIREMENT: `FR-duplicate-1`',
312
+ '',
313
+ ].join('\n'));
314
+
315
+ const result = run(root);
316
+ assert.equal(result.status, 1, result.output);
317
+ assert.match(result.output, /DUPLICATE duplicate specification FR-duplicate-1/);
318
+ assert.match(result.output, /DUPLICATE duplicate pseudocode FR-duplicate-1/);
319
+
320
+ write(root, 'docs/features/duplicate/01_specification.md',
321
+ '### FR-duplicate-X — malformed ordinal\n');
322
+ write(root, 'docs/features/duplicate/02_pseudocode.md', [
323
+ '### Algorithm: malformed claim',
324
+ 'REQUIREMENT: `FR-duplicate-X`',
325
+ '### Algorithm: missing claim',
326
+ 'STEPS:',
327
+ '1. Do work.',
328
+ '',
329
+ ].join('\n'));
330
+ const malformed = run(root);
331
+ assert.equal(malformed.status, 1, malformed.output);
332
+ assert.match(malformed.output, /MALFORMED duplicate specification/);
333
+ assert.match(malformed.output, /MALFORMED duplicate pseudocode/);
334
+ assert.match(malformed.output, /GAP duplicate pseudocode unkeyed-algorithm/);
335
+ });
336
+
337
+ test('P8 — supported formatting passes without decoy false positives', async (t) => {
338
+ const liveSpecification = [
339
+ '### FR-formatting-1',
340
+ '### NFR-formatting-2 — Windows line endings',
341
+ '### AC-formatting-3 - ASCII title separator',
342
+ 'Prose mentions FR-formatting-99.',
343
+ '| NFR-formatting-98 | table only |',
344
+ '<!-- ### AC-formatting-97 -->',
345
+ '### SC-FR-formatting-1-1 — nested scenario',
346
+ ];
347
+ const livePseudocode = [
348
+ '### Algorithm: formatter',
349
+ 'REQUIREMENT: `FR-formatting-1`',
350
+ 'REQUIREMENT: `NFR-formatting-2`',
351
+ 'REQUIREMENT: `AC-formatting-3`',
352
+ 'Comment FR-formatting-99 is not evidence.',
353
+ '| REQUIREMENT: `NFR-formatting-98` | table |',
354
+ '<!-- REQUIREMENT: `AC-formatting-97` -->',
355
+ ];
356
+
357
+ await t.test('fenced specification heading is not a declared requirement', (caseTest) => {
358
+ const root = temp(caseTest);
359
+ const specification = [...liveSpecification,
360
+ '```markdown',
361
+ '### FR-formatting-96 — fenced documentation example',
362
+ '```',
363
+ '',
364
+ ].join('\r\n');
365
+ documents(root, 'formatting', specification, [...livePseudocode, ''].join('\r\n'));
366
+
367
+ const result = run(root);
368
+ assert.equal(result.status, 0, result.output);
369
+ assert.match(result.output, /requirements=3 algorithms=3/);
370
+ assert.doesNotMatch(result.output, /FR-formatting-96/);
371
+ });
372
+
373
+ await t.test('fenced algorithm claims create neither orphan nor duplicate keys', (caseTest) => {
374
+ const root = temp(caseTest);
375
+ const pseudocode = [...livePseudocode,
376
+ '```text',
377
+ '### Algorithm: fenced decoy',
378
+ 'REQUIREMENT: `FR-formatting-95`',
379
+ 'REQUIREMENT: `FR-formatting-1`',
380
+ '```',
381
+ '',
382
+ ].join('\r\n');
383
+ documents(root, 'formatting', [...liveSpecification, ''].join('\r\n'), pseudocode);
384
+
385
+ const result = run(root);
386
+ assert.equal(result.status, 0, result.output);
387
+ assert.match(result.output, /requirements=3 algorithms=3/);
388
+ assert.doesNotMatch(result.output, /FR-formatting-95|DUPLICATE/);
389
+ });
390
+ });
391
+
392
+ test('P9 — large contour traversal stays within the measured budget', (t) => {
393
+ const root = temp(t);
394
+ const featureCount = 60;
395
+ const idsPerFeature = 20;
396
+ for (let featureIndex = 0; featureIndex < featureCount; featureIndex++) {
397
+ const slug = `scale-${String(featureIndex).padStart(3, '0')}`;
398
+ const ids = Array.from({ length: idsPerFeature }, (_, idIndex) =>
399
+ `FR-${slug}-${idIndex + 1}`);
400
+ const specification = ids.map((id) => `### ${id} — generated`).join('\n') + '\n';
401
+ const claimed = featureIndex === featureCount - 1 ? ids.slice(0, -1) : ids;
402
+ documents(root, slug, specification, claimed.map((id) => algorithm(id)).join('\n'));
403
+ }
404
+
405
+ const started = Date.now();
406
+ const result = run(root, { timeout: 20000 });
407
+ const elapsedMs = Date.now() - started;
408
+ assert.equal(result.status, 1, result.output);
409
+ assert.match(result.output, /GAP scale-059 specification->pseudocode FR-scale-059-20/);
410
+ assert.match(result.output, /VERDICT traceability=FAIL features=60 gaps=1 inconclusive=0/);
411
+ assert.ok(elapsedMs < 15000, `measured ${elapsedMs}ms exceeds the 15000ms contract budget`);
412
+ });
413
+ });