@dzhechkov/p-replicator 1.13.1 → 1.13.2

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 (27) hide show
  1. package/.dz-manifest.json +65 -13
  2. package/README.md +19 -1
  3. package/package.json +3 -3
  4. package/sbom.json +142 -12
  5. package/scripts/check-pipeline-gaps.sh +510 -21
  6. package/src/utils.js +1 -0
  7. package/templates/.claude/commands/feature.md +43 -1
  8. package/templates/.claude/hooks/check-review-contract.cjs +205 -0
  9. package/templates/.claude/hooks/statusline.cjs +1 -1
  10. package/templates/.claude/rules/replicate-pipeline.md +4 -3
  11. package/templates/.claude/skills/requirements-validator/SKILL.md +18 -5
  12. package/templates/.claude/skills/requirements-validator/references/feature-report-contracts.md +67 -0
  13. package/templates/.claude/skills/requirements-validator/references/scoring-system.md +15 -6
  14. package/tests/e2e/feature-contour.test.js +176 -0
  15. package/tests/fixtures/feature-contour/docs/features/demo-gate/01_specification.md +15 -0
  16. package/tests/fixtures/feature-contour/docs/features/demo-gate/02_pseudocode.md +19 -0
  17. package/tests/fixtures/feature-contour/docs/features/demo-gate/03_architecture.md +3 -0
  18. package/tests/fixtures/feature-contour/docs/features/demo-gate/04_refinement.md +3 -0
  19. package/tests/fixtures/feature-contour/docs/features/demo-gate/05_completion.md +7 -0
  20. package/tests/fixtures/feature-contour/docs/features/demo-gate/review-report.md +11 -0
  21. package/tests/fixtures/feature-contour/docs/features/demo-gate/validation-report.md +10 -0
  22. package/tests/fixtures/feature-contour/tests/demo.test.js +16 -0
  23. package/tests/snapshot/baseline.json +9 -7
  24. package/tests/unit/check-review-contract.test.js +181 -0
  25. package/tests/unit/honest-failure-rules.test.js +18 -3
  26. package/tests/unit/sync-templates-guard.test.js +46 -3
  27. package/tests/unit/traceability-completion-gate.test.js +267 -0
@@ -0,0 +1,267 @@
1
+ 'use strict';
2
+
3
+ const { describe, test } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const crypto = require('node:crypto');
6
+ const fs = require('node:fs');
7
+ const os = require('node:os');
8
+ const path = require('node:path');
9
+ const { spawnSync } = require('node:child_process');
10
+
11
+ const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
12
+ const CHECKER = path.join(PACKAGE_ROOT, 'scripts', 'check-pipeline-gaps.sh');
13
+ const TRACEABILITY_FIXTURE = path.join(PACKAGE_ROOT, 'tests', 'fixtures', 'prep-traceability-fixture');
14
+ const FEATURE_TEMPLATE = path.join(PACKAGE_ROOT, 'templates', '.claude', 'commands', 'feature.md');
15
+ const PROJECT_TEMPLATE = path.join(
16
+ PACKAGE_ROOT, 'templates', '.claude', 'skills', 'sparc-prd-mini', 'SKILL.md',
17
+ );
18
+
19
+ const ROLES = {
20
+ specification: '01_specification.md',
21
+ pseudocode: '02_pseudocode.md',
22
+ architecture: '03_architecture.md',
23
+ refinement: '04_refinement.md',
24
+ completion: '05_completion.md',
25
+ };
26
+
27
+ function write(root, relative, body) {
28
+ const target = path.join(root, relative);
29
+ fs.mkdirSync(path.dirname(target), { recursive: true });
30
+ fs.writeFileSync(target, body);
31
+ return target;
32
+ }
33
+
34
+ function roleMap(root, relative, heading) {
35
+ const rows = Object.entries(ROLES).map(([role, target]) => ` ${role}: ${target}`);
36
+ return write(root, relative,
37
+ `${heading}\n\n\`\`\`yaml\nDOCUMENT_ROLE_MAP:\n${rows.join('\n')}\n\`\`\`\n`);
38
+ }
39
+
40
+ function temp(t, prefix = 'traceability-completion-') {
41
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
42
+ t.after(() => fs.rmSync(root, { recursive: true, force: true }));
43
+ return root;
44
+ }
45
+
46
+ function createContour(root, options = {}) {
47
+ const slug = options.slug || 'demo';
48
+ const prefix = path.join('docs', 'features', slug);
49
+ const specification = options.specification || [
50
+ `# ${slug} specification`,
51
+ '',
52
+ `### FR-${slug}-1 — feature requirement`,
53
+ '',
54
+ `### AC-${slug}-1 — accepts a valid request`,
55
+ '',
56
+ `### AC-${slug}-2 — rejects an invalid request`,
57
+ '',
58
+ ].join('\n');
59
+ const digest = crypto.createHash('sha256').update(specification).digest('hex');
60
+ write(root, path.join(prefix, ROLES.specification), specification);
61
+ write(root, path.join(prefix, ROLES.pseudocode), [
62
+ `### Algorithm: ${slug}`,
63
+ `REQUIREMENT: \`FR-${slug}-1\``,
64
+ `REQUIREMENT: \`AC-${slug}-1\``,
65
+ `REQUIREMENT: \`AC-${slug}-2\``,
66
+ '',
67
+ ].join('\n'));
68
+ write(root, path.join(prefix, ROLES.architecture), '# Architecture\n');
69
+ write(root, path.join(prefix, ROLES.refinement), '# Refinement\n');
70
+ write(root, path.join(prefix, ROLES.completion), options.completion || [
71
+ '# Completion',
72
+ '',
73
+ '## Criterion coverage',
74
+ '| Criterion | Test file | Test title |',
75
+ '|-----------|-----------|------------|',
76
+ `| AC-${slug}-1 | tests/${slug}.test.js | accepts a valid request |`,
77
+ `| AC-${slug}-2 | tests/${slug}.test.js | rejects an invalid request |`,
78
+ '',
79
+ ].join('\n'));
80
+ write(root, path.join(prefix, 'validation-report.md'), options.validation || [
81
+ '# Validation report',
82
+ `Spec revision: sha256:${digest}`,
83
+ '',
84
+ '## Criterion scenarios',
85
+ '| Criterion | Scenario |',
86
+ '|-----------|----------|',
87
+ `| AC-${slug}-1 | accepts a valid request |`,
88
+ `| AC-${slug}-2 | rejects an invalid request |`,
89
+ '',
90
+ ].join('\n'));
91
+ write(root, path.join('tests', `${slug}.test.js`), options.testBody || [
92
+ "test('accepts a valid request', () => {});",
93
+ "test('rejects an invalid request', () => {});",
94
+ '',
95
+ ].join('\n'));
96
+ return { slug, prefix, digest, specification };
97
+ }
98
+
99
+ function maps(root) {
100
+ return {
101
+ feature: roleMap(root, 'feature-map.md', '### Phase 1 document role map'),
102
+ project: roleMap(root, 'project-map.md', '### Project-level default'),
103
+ };
104
+ }
105
+
106
+ function runChecker(root, flags, options = {}) {
107
+ const roleMaps = options.maps || maps(root);
108
+ const result = spawnSync('bash', [options.checker || CHECKER, root, ...flags,
109
+ '--role-map-source', roleMaps.feature,
110
+ '--project-role-map-source', roleMaps.project,
111
+ ], { cwd: options.cwd || PACKAGE_ROOT, encoding: 'utf8', timeout: 10000 });
112
+ return {
113
+ status: result.status,
114
+ signal: result.signal,
115
+ stdout: result.stdout || '',
116
+ stderr: result.stderr || '',
117
+ output: `${result.stdout || ''}${result.stderr || ''}`,
118
+ };
119
+ }
120
+
121
+ describe('traceability completion and report gates', () => {
122
+ test('P1 — clean contour: --completion and --report-revision exit 0 with PASS verdicts', (t) => {
123
+ const root = temp(t);
124
+ createContour(root);
125
+ const result = runChecker(root, ['--completion', '--report-revision']);
126
+ assert.equal(result.status, 0, result.output);
127
+ assert.match(result.output, /VERDICT completion=PASS features=1 gaps=0 inconclusive=0/);
128
+ assert.match(result.output, /VERDICT report-revision=PASS features=1 gaps=0 inconclusive=0/);
129
+ });
130
+
131
+ test('P2 — an AC with no coverage row is a named gap (exit 1)', (t) => {
132
+ const root = temp(t);
133
+ const contour = createContour(root);
134
+ const completion = path.join(root, contour.prefix, ROLES.completion);
135
+ fs.writeFileSync(completion, fs.readFileSync(completion, 'utf8')
136
+ .replace(/^\| AC-demo-2 .*\n/m, ''));
137
+ const result = runChecker(root, ['--completion']);
138
+ assert.equal(result.status, 1, result.output);
139
+ assert.match(result.output,
140
+ /GAP contour=demo completion AC-demo-2 has no row in Criterion coverage/);
141
+ assert.match(result.output, /VERDICT completion=FAIL/);
142
+ });
143
+
144
+ test('P3 — a row whose id is not in the specification is a named gap', (t) => {
145
+ const root = temp(t);
146
+ const contour = createContour(root);
147
+ const completion = path.join(root, contour.prefix, ROLES.completion);
148
+ fs.appendFileSync(completion,
149
+ '| AC-demo-9 | tests/demo.test.js | accepts a valid request |\n');
150
+ const result = runChecker(root, ['--completion']);
151
+ assert.equal(result.status, 1, result.output);
152
+ assert.match(result.output,
153
+ /GAP contour=demo completion row AC-demo-9 is not declared in the specification/);
154
+ });
155
+
156
+ test('P4 — a row whose test file lacks the title is a named gap; escaping and symlinked paths are refused', (t) => {
157
+ const root = temp(t);
158
+ const contour = createContour(root, { testBody: "test('another title', () => {});\n" });
159
+ const completion = path.join(root, contour.prefix, ROLES.completion);
160
+ let result = runChecker(root, ['--completion']);
161
+ assert.equal(result.status, 1, result.output);
162
+ assert.match(result.output,
163
+ /AC-demo-1 test file tests\/demo\.test\.js does not contain title "accepts a valid request"/);
164
+
165
+ fs.writeFileSync(completion, fs.readFileSync(completion, 'utf8')
166
+ .replace('tests/demo.test.js | accepts a valid request',
167
+ '../outside.test.js | accepts a valid request'));
168
+ result = runChecker(root, ['--completion']);
169
+ assert.equal(result.status, 1, result.output);
170
+ assert.match(result.output, /test file \.\.\/outside\.test\.js escapes the project root/);
171
+
172
+ const outside = temp(t, 'traceability-completion-outside-');
173
+ write(outside, 'linked.test.js', "test('accepts a valid request', () => {});\n");
174
+ fs.symlinkSync(path.join(outside, 'linked.test.js'), path.join(root, 'tests', 'linked.test.js'));
175
+ fs.writeFileSync(completion, fs.readFileSync(completion, 'utf8')
176
+ .replace('../outside.test.js', 'tests/linked.test.js'));
177
+ result = runChecker(root, ['--completion']);
178
+ assert.equal(result.status, 1, result.output);
179
+ assert.match(result.output, /test file tests\/linked\.test\.js uses a symlink/);
180
+ });
181
+
182
+ test('P5 — a stale revision line exits 1 naming both prefixes; a missing line exits 2', (t) => {
183
+ const root = temp(t);
184
+ const contour = createContour(root);
185
+ const report = path.join(root, contour.prefix, 'validation-report.md');
186
+ const stale = 'a'.repeat(64);
187
+ fs.writeFileSync(report, fs.readFileSync(report, 'utf8')
188
+ .replace(contour.digest, stale));
189
+ let result = runChecker(root, ['--report-revision']);
190
+ assert.equal(result.status, 1, result.output);
191
+ assert.match(result.output,
192
+ new RegExp(`sha256:${stale.slice(0, 12)}… != specification sha256:${contour.digest.slice(0, 12)}…`));
193
+
194
+ fs.writeFileSync(report, fs.readFileSync(report, 'utf8')
195
+ .replace(/^Spec revision:.*\n/m, ''));
196
+ result = runChecker(root, ['--report-revision']);
197
+ assert.equal(result.status, 2, result.output);
198
+ assert.match(result.output,
199
+ /NOT-ESTABLISHED contour=demo report-revision line missing in validation-report\.md/);
200
+ });
201
+
202
+ test('P5b — --criterion-scenarios names missing and undeclared rows; a missing table exits 2', (t) => {
203
+ const root = temp(t);
204
+ const contour = createContour(root);
205
+ const report = path.join(root, contour.prefix, 'validation-report.md');
206
+ fs.writeFileSync(report, fs.readFileSync(report, 'utf8')
207
+ .replace(/^\| AC-demo-2 .*\n/m, '')
208
+ .replace('| AC-demo-1 | accepts a valid request |', [
209
+ '| AC-demo-1 | accepts a valid request |',
210
+ '| AC-demo-9 | undeclared scenario |',
211
+ ].join('\n')));
212
+ let result = runChecker(root, ['--criterion-scenarios']);
213
+ assert.equal(result.status, 1, result.output);
214
+ assert.match(result.output,
215
+ /GAP contour=demo criterion-scenarios AC-demo-2 has no scenario row/);
216
+ assert.match(result.output,
217
+ /GAP contour=demo criterion-scenarios row AC-demo-9 is not declared in the specification/);
218
+
219
+ fs.writeFileSync(report, [
220
+ '# Validation report',
221
+ `Spec revision: sha256:${contour.digest}`,
222
+ '',
223
+ '## Different section',
224
+ '',
225
+ ].join('\n'));
226
+ result = runChecker(root, ['--criterion-scenarios']);
227
+ assert.equal(result.status, 2, result.output);
228
+ assert.match(result.output, /NOT-ESTABLISHED.*criterion-scenarios table missing or malformed/);
229
+ });
230
+
231
+ test('P6 — flags compose and the worst status wins', (t) => {
232
+ const root = temp(t);
233
+ const contour = createContour(root);
234
+ const completion = path.join(root, contour.prefix, ROLES.completion);
235
+ const report = path.join(root, contour.prefix, 'validation-report.md');
236
+ fs.writeFileSync(completion, fs.readFileSync(completion, 'utf8')
237
+ .replace(/^\| AC-demo-2 .*\n/m, ''));
238
+ fs.writeFileSync(report, fs.readFileSync(report, 'utf8')
239
+ .replace(/^Spec revision:.*\n/m, ''));
240
+ const result = runChecker(root, [
241
+ '--traceability', '--completion', '--report-revision', '--criterion-scenarios',
242
+ ]);
243
+ assert.equal(result.status, 2, result.output);
244
+ assert.match(result.output, /VERDICT traceability=PASS/);
245
+ assert.match(result.output, /VERDICT completion=FAIL/);
246
+ assert.match(result.output, /VERDICT report-revision=NOT-ESTABLISHED/);
247
+ assert.match(result.output, /VERDICT criterion-scenarios=PASS/);
248
+ });
249
+
250
+ test('P7 — --traceability output is byte-identical to before on the existing fixture', (t) => {
251
+ const root = temp(t, 'traceability-legacy-checker-');
252
+ const oldScript = path.join(root, 'check-pipeline-gaps.sh');
253
+ const shown = spawnSync('git', ['show', 'HEAD:./scripts/check-pipeline-gaps.sh'], {
254
+ cwd: PACKAGE_ROOT, encoding: 'utf8', timeout: 10000,
255
+ });
256
+ assert.equal(shown.status, 0, `${shown.stdout}\n${shown.stderr}`);
257
+ fs.writeFileSync(oldScript, shown.stdout, { mode: 0o755 });
258
+ const roleMaps = { feature: FEATURE_TEMPLATE, project: PROJECT_TEMPLATE };
259
+ const before = runChecker(TRACEABILITY_FIXTURE, ['--traceability'], {
260
+ checker: oldScript, maps: roleMaps,
261
+ });
262
+ const after = runChecker(TRACEABILITY_FIXTURE, ['--traceability'], { maps: roleMaps });
263
+ assert.equal(after.status, before.status, after.output);
264
+ assert.equal(after.stdout, before.stdout);
265
+ assert.equal(after.stderr, before.stderr);
266
+ });
267
+ });