@dzhechkov/p-replicator 1.12.0 → 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.
- package/.dz-manifest.json +281 -65
- package/CHANGELOG.md +148 -1
- package/LICENSE +21 -0
- package/MULTIPLATFORM_ROADMAP.md +1 -1
- package/README/eng/01_quickstart.md +2 -2
- package/README/eng/02_user_guide.md +1 -1
- package/README/eng/03_admin_guide.md +2 -2
- package/README/eng/05_architecture.md +1 -1
- package/README/eng/README.md +2 -1
- package/README/ru/01_quickstart.md +2 -2
- package/README/ru/02_user_guide.md +1 -1
- package/README/ru/03_admin_guide.md +2 -2
- package/README/ru/05_architecture.md +1 -1
- package/README/ru/README.md +2 -1
- package/README/ru/html/index.html +8 -8
- package/README.md +150 -9
- package/bin/cli.js +0 -0
- package/package.json +10 -11
- package/sbom.json +604 -64
- package/scripts/check-pipeline-gaps.sh +510 -21
- package/src/commands/init.js +1 -1
- package/src/rule-components.json +5 -1
- package/src/utils.js +33 -3
- package/templates/.claude/agents/product-discoverer.md +38 -0
- package/templates/.claude/agents/replicate-coordinator.md +11 -1
- package/templates/.claude/commands/feature.md +72 -6
- package/templates/.claude/commands/go.md +6 -8
- package/templates/.claude/commands/harvest.md +5 -7
- package/templates/.claude/commands/replicate.md +169 -44
- package/templates/.claude/commands/start.md +28 -7
- package/templates/.claude/hooks/capture-source-path.cjs +795 -0
- package/templates/.claude/hooks/check-canon.cjs +493 -0
- package/templates/.claude/hooks/check-embed-contract.cjs +374 -0
- package/templates/.claude/hooks/check-external-deps.cjs +288 -0
- package/templates/.claude/hooks/check-file-ownership.cjs +424 -0
- package/templates/.claude/hooks/check-handoff-manifest.cjs +367 -0
- package/templates/.claude/hooks/check-job-contract.cjs +501 -0
- package/templates/.claude/hooks/check-look-origin.cjs +240 -0
- package/templates/.claude/hooks/check-look-trace.cjs +385 -0
- package/templates/.claude/hooks/check-metric-source.cjs +296 -0
- package/templates/.claude/hooks/check-model-cost.cjs +470 -0
- package/templates/.claude/hooks/check-ports.cjs +27 -6
- package/templates/.claude/hooks/check-review-contract.cjs +205 -0
- package/templates/.claude/hooks/check-source-version.cjs +312 -0
- package/templates/.claude/hooks/check-swarm-receipts.cjs +197 -0
- package/templates/.claude/hooks/check-webhook-contract.cjs +535 -0
- package/templates/.claude/hooks/statusline.cjs +2 -2
- package/templates/.claude/rules/embeddable-widget.md +73 -0
- package/templates/.claude/rules/feature-lifecycle.md +5 -6
- package/templates/.claude/rules/incoming-webhooks.md +99 -0
- package/templates/.claude/rules/long-running-job.md +73 -0
- package/templates/.claude/rules/model-call-cost.md +85 -0
- package/templates/.claude/rules/replicate-pipeline.md +122 -52
- package/templates/.claude/skills/brutal-honesty-review/SKILL.md +9 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/SKILL.md +4 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/03-generate-p0.md +46 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/04-generate-p1.md +7 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/06-package-deliver.md +20 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/claude-md-strategy.md +7 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/automation-commands.md +17 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle-ent.md +43 -5
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle.md +43 -7
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/start-command.md +19 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/swarm-file-evidence.md +151 -0
- package/templates/.claude/skills/goap-research-ed25519/SKILL.md +37 -22
- package/templates/.claude/skills/goap-research-ed25519/references/negative-results.md +94 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/check_report_evidence.py +368 -4
- package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +122 -5
- package/templates/.claude/skills/goap-research-ed25519/scripts/evidence_fetch.py +33 -16
- package/templates/.claude/skills/goap-research-ed25519/scripts/quote_provenance.py +342 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_ed25519_verifier.py +60 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_evidence_provenance.py +139 -6
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_quote_provenance.py +274 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_suite_completeness.py +2 -1
- package/templates/.claude/skills/knowledge-extractor/SKILL.md +4 -0
- package/templates/.claude/skills/pipeline-forge/SKILL.md +18 -23
- package/templates/.claude/skills/pipeline-forge/examples/replicate-analysis.md +7 -2
- package/templates/.claude/skills/pipeline-forge/references/patterns-catalog.md +19 -1
- package/templates/.claude/skills/pipeline-forge/references/self-extracted-patterns.md +17 -6
- package/templates/.claude/skills/pipeline-forge/references/skill-anatomy.md +0 -1
- package/templates/.claude/skills/requirements-validator/SKILL.md +18 -5
- package/templates/.claude/skills/requirements-validator/references/feature-report-contracts.md +67 -0
- package/templates/.claude/skills/requirements-validator/references/scoring-system.md +15 -6
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/025-cjm-prototype.md +21 -1
- package/templates/.claude/skills/sparc-prd-mini/SKILL.md +173 -725
- package/tests/e2e/feature-contour.test.js +176 -0
- package/tests/fixtures/feature-contour/docs/features/demo-gate/01_specification.md +15 -0
- package/tests/fixtures/feature-contour/docs/features/demo-gate/02_pseudocode.md +19 -0
- package/tests/fixtures/feature-contour/docs/features/demo-gate/03_architecture.md +3 -0
- package/tests/fixtures/feature-contour/docs/features/demo-gate/04_refinement.md +3 -0
- package/tests/fixtures/feature-contour/docs/features/demo-gate/05_completion.md +7 -0
- package/tests/fixtures/feature-contour/docs/features/demo-gate/review-report.md +11 -0
- package/tests/fixtures/feature-contour/docs/features/demo-gate/validation-report.md +10 -0
- package/tests/fixtures/feature-contour/tests/demo.test.js +16 -0
- package/tests/snapshot/baseline.json +64 -40
- package/tests/unit/capture-source-path.test.js +492 -0
- package/tests/unit/check-canon.test.js +403 -0
- package/tests/unit/check-embed-contract.test.js +422 -0
- package/tests/unit/check-external-deps.test.js +363 -0
- package/tests/unit/check-file-ownership.test.js +388 -0
- package/tests/unit/check-handoff-manifest.test.js +410 -0
- package/tests/unit/check-job-contract.test.js +514 -0
- package/tests/unit/check-look-origin.test.js +180 -0
- package/tests/unit/check-look-trace.test.js +420 -0
- package/tests/unit/check-metric-source.test.js +325 -0
- package/tests/unit/check-model-cost.test.js +425 -0
- package/tests/unit/check-ports.test.js +46 -2
- package/tests/unit/check-review-contract.test.js +181 -0
- package/tests/unit/check-source-version.test.js +344 -0
- package/tests/unit/check-swarm-receipts.test.js +231 -0
- package/tests/unit/check-webhook-contract.test.js +536 -0
- package/tests/unit/db-port-rule.test.js +8 -2
- package/tests/unit/detection-ladder-registry.test.js +2 -2
- package/tests/unit/generator-swarm-contract.test.js +287 -0
- package/tests/unit/guard-honest-input-meta.test.js +64 -0
- package/tests/unit/honest-failure-rules.test.js +106 -9
- package/tests/unit/look-phase-contract.test.js +231 -0
- package/tests/unit/negative-conclusion-gate.test.js +300 -0
- package/tests/unit/quote-provenance.test.js +122 -0
- package/tests/unit/sync-templates-guard.test.js +46 -3
- package/tests/unit/traceability-completion-gate.test.js +267 -0
- package/tests/unit/utils.test.js +40 -3
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// scripts/sync-templates.js copies <found root>/.claude/{skills,commands,agents,rules,hooks} over
|
|
4
4
|
// templates/.claude/ — the directory that becomes the npm tarball. It finds its source by walking up
|
|
5
|
-
// to five parents
|
|
6
|
-
// MONOREPO, whose .claude/skills holds a whole
|
|
5
|
+
// to five parents, stopping at the package repository boundary, and taking the FIRST one containing
|
|
6
|
+
// .claude/skills, which inside a monorepo is the MONOREPO, whose .claude/skills holds a whole
|
|
7
|
+
// different toolkit.
|
|
7
8
|
//
|
|
8
9
|
// Nothing had fired only because package.json's prepublishOnly points at the publish gate, not here.
|
|
9
10
|
// "Safe because dead" is not a safety property: a dead script can be revived by anyone who does not
|
|
@@ -61,7 +62,11 @@ const DECLARATION = 'p-replicator-sync-source: v1';
|
|
|
61
62
|
* with its own templates/.claude tree. `marker` decides whether the root claims to be the source. */
|
|
62
63
|
function fixture(opts) {
|
|
63
64
|
const o = opts || {};
|
|
64
|
-
const
|
|
65
|
+
const parent = o.parent || os.tmpdir();
|
|
66
|
+
const root = fs.realpathSync(fs.mkdtempSync(path.join(parent, 'p-rep-sync-')));
|
|
67
|
+
const boundary = o.boundary === undefined ? 'dir' : o.boundary;
|
|
68
|
+
if (boundary === 'dir') fs.mkdirSync(path.join(root, '.git'));
|
|
69
|
+
if (boundary === 'file') fs.writeFileSync(path.join(root, '.git'), 'gitdir: /nonexistent\n');
|
|
65
70
|
if (o.rootHasClaude !== false) {
|
|
66
71
|
fs.mkdirSync(path.join(root, '.claude', 'skills', 'intruder'), { recursive: true });
|
|
67
72
|
fs.writeFileSync(path.join(root, '.claude', 'skills', 'intruder', 'SKILL.md'),
|
|
@@ -208,6 +213,44 @@ describe.skip = describe.skip || (() => {});
|
|
|
208
213
|
} finally { cleanup(f.root); }
|
|
209
214
|
});
|
|
210
215
|
|
|
216
|
+
test('P10 — a .claude/skills ABOVE the repository boundary is not a candidate root', () => {
|
|
217
|
+
const grandparent = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-intruder-')));
|
|
218
|
+
fs.mkdirSync(path.join(grandparent, '.claude', 'skills', 'intruder'), { recursive: true });
|
|
219
|
+
fs.writeFileSync(path.join(grandparent, '.claude', 'skills', 'intruder', 'SKILL.md'),
|
|
220
|
+
'# outside the fixture repository\n');
|
|
221
|
+
try {
|
|
222
|
+
const bounded = fixture({ parent: grandparent, rootHasClaude: false, boundary: 'dir' });
|
|
223
|
+
try {
|
|
224
|
+
const r = runScript(bounded.pkg);
|
|
225
|
+
assert.equal(r.code, 0, 'the repository boundary must stop the walk: ' + r.out);
|
|
226
|
+
assert.match(r.out, /Not in repo context — skipping sync/);
|
|
227
|
+
} finally { cleanup(bounded.root); }
|
|
228
|
+
|
|
229
|
+
const unbounded = fixture({ parent: grandparent, rootHasClaude: false, boundary: false });
|
|
230
|
+
try {
|
|
231
|
+
const r = runScript(unbounded.pkg);
|
|
232
|
+
assert.equal(r.code, 1, 'without a boundary the outside candidate must still be found');
|
|
233
|
+
assert.match(r.out, /REFUSING to sync/,
|
|
234
|
+
'the contrast proves the boundary, rather than the host /tmp state, caused the skip');
|
|
235
|
+
} finally { cleanup(unbounded.root); }
|
|
236
|
+
} finally { cleanup(grandparent); }
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test('P11 — a .git FILE (worktree) is a boundary too', () => {
|
|
240
|
+
const grandparent = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-intruder-')));
|
|
241
|
+
fs.mkdirSync(path.join(grandparent, '.claude', 'skills', 'intruder'), { recursive: true });
|
|
242
|
+
fs.writeFileSync(path.join(grandparent, '.claude', 'skills', 'intruder', 'SKILL.md'),
|
|
243
|
+
'# outside the fixture worktree\n');
|
|
244
|
+
try {
|
|
245
|
+
const f = fixture({ parent: grandparent, rootHasClaude: false, boundary: 'file' });
|
|
246
|
+
try {
|
|
247
|
+
const r = runScript(f.pkg);
|
|
248
|
+
assert.equal(r.code, 0, 'a worktree .git file must stop the walk: ' + r.out);
|
|
249
|
+
assert.match(r.out, /Not in repo context — skipping sync/);
|
|
250
|
+
} finally { cleanup(f.root); }
|
|
251
|
+
} finally { cleanup(grandparent); }
|
|
252
|
+
});
|
|
253
|
+
|
|
211
254
|
test('P5 — no helper tells a reader to run it', () => {
|
|
212
255
|
// The helper printed "Run prepublishOnly first: node scripts/sync-templates.js" when templates/
|
|
213
256
|
// was missing: a live invitation to the hazardous command, in the one situation where a reader
|
|
@@ -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
|
+
});
|
package/tests/unit/utils.test.js
CHANGED
|
@@ -341,12 +341,12 @@ describe('COMPONENTS.items SSOT', () => {
|
|
|
341
341
|
}
|
|
342
342
|
});
|
|
343
343
|
|
|
344
|
-
test('item counts: 10 skills, 11 commands, 4 agents,
|
|
344
|
+
test('item counts: 10 skills, 11 commands, 4 agents, 13 rules, 1 settings', () => {
|
|
345
345
|
assert.equal(Object.keys(utils.COMPONENTS.skills.items).length, 10, 'skills count');
|
|
346
346
|
assert.equal(Object.keys(utils.COMPONENTS.commands.items).length, 11, 'commands count (post v1.4: + 9 generic commands)');
|
|
347
347
|
assert.equal(Object.keys(utils.COMPONENTS.agents.items).length, 4, 'agents count');
|
|
348
|
-
assert.equal(Object.keys(utils.COMPONENTS.rules.items).length,
|
|
349
|
-
'rules count (+ honest failure rules)');
|
|
348
|
+
assert.equal(Object.keys(utils.COMPONENTS.rules.items).length, 13,
|
|
349
|
+
'rules count (+ honest failure rules, + embeddable-widget, + incoming-webhooks, + long-running-job, + model-call-cost)');
|
|
350
350
|
assert.ok(utils.COMPONENTS.settings, 'settings component group added in v1.4');
|
|
351
351
|
assert.equal(Object.keys(utils.COMPONENTS.settings.items).length, 1, 'settings.json count');
|
|
352
352
|
});
|
|
@@ -635,3 +635,40 @@ describe('COMPONENTS.hooks (v1.5.0)', () => {
|
|
|
635
635
|
`expected >= 6 hook scripts (post v1.5.0), got ${hookCount}`);
|
|
636
636
|
});
|
|
637
637
|
});
|
|
638
|
+
|
|
639
|
+
describe('mergeHookMatchers — identity survives the user respelling of a variable (a1ef0e41)', () => {
|
|
640
|
+
// MEASURED (backlog a1ef0e41): a user who hand-rewrote `$CLAUDE_PROJECT_DIR` into
|
|
641
|
+
// `${CLAUDE_PROJECT_DIR}` got every event hook DOUBLED on the next update, because the
|
|
642
|
+
// byte-equality Set saw two strings for one command. Both spellings expand identically in sh.
|
|
643
|
+
const shipped = (cmd) => [{ matcher: '*', hooks: [{ type: 'command', command: cmd }] }];
|
|
644
|
+
|
|
645
|
+
test('${VAR} respelling of a shipped $VAR hook does not double on merge', () => {
|
|
646
|
+
const user = shipped('node "${CLAUDE_PROJECT_DIR}/.claude/hooks/session-insights.cjs"');
|
|
647
|
+
const tpl = shipped('node "$CLAUDE_PROJECT_DIR/.claude/hooks/session-insights.cjs"');
|
|
648
|
+
const merged = utils.mergeHookMatchers(user, tpl);
|
|
649
|
+
assert.equal(merged[0].hooks.length, 1, 'one command, two spellings — must stay ONE hook');
|
|
650
|
+
// The USER's own spelling survives; the stored command is never rewritten.
|
|
651
|
+
assert.match(merged[0].hooks[0].command, /\$\{CLAUDE_PROJECT_DIR\}/);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test('whitespace-run respelling does not double either', () => {
|
|
655
|
+
const user = shipped('node a.cjs --flag');
|
|
656
|
+
const tpl = shipped('node a.cjs --flag');
|
|
657
|
+
assert.equal(utils.mergeHookMatchers(user, tpl)[0].hooks.length, 1);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test('genuinely different commands still both survive (dedup must not over-collapse)', () => {
|
|
661
|
+
const user = shipped('node a.cjs');
|
|
662
|
+
const tpl = shipped('node b.cjs');
|
|
663
|
+
assert.equal(utils.mergeHookMatchers(user, tpl)[0].hooks.length, 2);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
test('MUTATION: byte-equality dedup (the old code) goes red on the respelling case', () => {
|
|
667
|
+
// The RED half in miniature: with canonicalization stripped, the two spellings differ.
|
|
668
|
+
const oldDedup = (a, b) => new Set(a.map((h) => h.command)).has(b.command);
|
|
669
|
+
const userHook = { command: 'node "${X}/y.cjs"' };
|
|
670
|
+
const tplHook = { command: 'node "$X/y.cjs"' };
|
|
671
|
+
assert.equal(oldDedup([userHook], tplHook), false,
|
|
672
|
+
'byte equality cannot see through the respelling — this is the measured doubling');
|
|
673
|
+
});
|
|
674
|
+
});
|