@dzhechkov/p-replicator 1.6.0 → 1.9.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 (33) hide show
  1. package/.dz-manifest.json +59 -23
  2. package/CHANGELOG.md +127 -0
  3. package/README.md +106 -4
  4. package/package.json +4 -4
  5. package/sbom.json +112 -22
  6. package/src/utils.js +2 -0
  7. package/templates/.claude/commands/replicate.md +57 -1
  8. package/templates/.claude/hooks/check-docs-complete.cjs +174 -0
  9. package/templates/.claude/hooks/check-growth-trace.cjs +191 -0
  10. package/templates/.claude/hooks/statusline.cjs +1 -1
  11. package/templates/.claude/rules/replicate-pipeline.md +14 -4
  12. package/templates/.claude/rules/skill-interface-protocol.md +9 -0
  13. package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/03-generate-p0.md +6 -4
  14. package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/08-skill-composition.md +2 -2
  15. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle.md +2 -2
  16. package/templates/.claude/skills/requirements-validator/SKILL.md +52 -0
  17. package/templates/.claude/skills/reverse-engineering-unicorn/modules/01-intelligence.md +4 -4
  18. package/templates/.claude/skills/reverse-engineering-unicorn/modules/02-product-customers.md +2 -2
  19. package/templates/.claude/skills/reverse-engineering-unicorn/modules/025-cjm-prototype.md +9 -1
  20. package/templates/.claude/skills/reverse-engineering-unicorn/modules/03-market-competition.md +3 -3
  21. package/templates/.claude/skills/reverse-engineering-unicorn/modules/04-business-finance.md +3 -3
  22. package/templates/.claude/skills/reverse-engineering-unicorn/modules/05-growth-engine.md +132 -12
  23. package/templates/.claude/skills/reverse-engineering-unicorn/modules/06-playbook-synthesis.md +1 -1
  24. package/templates/.claude/skills/sparc-prd-mini/SKILL.md +9 -9
  25. package/tests/snapshot/baseline.json +20 -18
  26. package/tests/unit/check-docs-complete.test.js +249 -0
  27. package/tests/unit/check-growth-trace.test.js +188 -0
  28. package/tests/unit/growth-axes-and-compliance.test.js +169 -0
  29. package/tests/unit/growth-gate-conditional.test.js +122 -0
  30. package/tests/unit/growth-module-b2b-gate.test.js +20 -2
  31. package/tests/unit/growth-requirements-bridge.test.js +127 -0
  32. package/tests/unit/module-copy-identity.test.js +76 -0
  33. package/tests/unit/skill-paths-prebaked.test.js +174 -0
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ // 05-growth-engine.md has FOUR live copies: the repo canonical .claude/ tree and templates/ of three
4
+ // PUBLISHED packages. Nothing tested that they agree.
5
+ //
6
+ // MEASURED before this test existed: tests/unit/sync-templates-guard.test.js guards the sync
7
+ // SCRIPT's choice of source root — a real defect, a different one. Cross-package copy identity had
8
+ // no guard at all, so a fix applied to one copy and forgotten in another would ship to npm in three
9
+ // packages that disagree, and nothing would go red.
10
+ //
11
+ // This feature edits all four in one change, which is exactly the moment to close it: the risk it
12
+ // guards is the risk this feature adds.
13
+
14
+ const { test, describe } = require('node:test');
15
+ const assert = require('node:assert/strict');
16
+ const crypto = require('node:crypto');
17
+ const fs = require('node:fs');
18
+ const path = require('node:path');
19
+
20
+ const PKG = path.resolve(__dirname, '..', '..');
21
+ const REPO = path.resolve(PKG, '..', '..', '..');
22
+ const REL = path.join('.claude', 'skills', 'reverse-engineering-unicorn', 'modules');
23
+ const TPL = path.join('templates', '.claude', 'skills', 'reverse-engineering-unicorn', 'modules');
24
+
25
+ /** The four copies that ship. Vendored sub-projects and .stryker-tmp sandboxes are deliberately
26
+ * excluded: they are separate checked-in projects, not publish targets of this monorepo. */
27
+ const COPIES = [
28
+ ['canonical', path.join(REPO, REL)],
29
+ ['skills-reverse-engineering', path.join(REPO, 'packages', '@dzhechkov', 'skills-reverse-engineering', TPL)],
30
+ ['p-replicator', path.join(REPO, 'packages', '@dzhechkov', 'p-replicator', TPL)],
31
+ ['keysarium', path.join(REPO, 'packages', '@dzhechkov', 'keysarium', TPL)],
32
+ ];
33
+
34
+ const sha = (f) => crypto.createHash('sha256').update(fs.readFileSync(f)).digest('hex');
35
+
36
+ describe('the four live copies of the growth module agree', () => {
37
+ test('P1 - all four live copies are byte-identical', () => {
38
+ const seen = COPIES.map(([name, dir]) => {
39
+ const f = path.join(dir, '05-growth-engine.md');
40
+ assert.ok(fs.existsSync(f), 'copy is missing entirely: ' + name + ' → ' + f);
41
+ return { name, hash: sha(f) };
42
+ });
43
+ const distinct = [...new Set(seen.map((s) => s.hash))];
44
+ assert.equal(distinct.length, 1,
45
+ 'copies diverged: ' + JSON.stringify(seen.map((s) => s.name + '=' + s.hash.slice(0, 8))));
46
+ });
47
+
48
+ test('P2 - a drifted copy is NAMED, not just counted', () => {
49
+ // A test that says "they differ" sends a reader to diff four files by hand. The failure message
50
+ // has to say WHICH. Proven by constructing the failure rather than by trusting the message above.
51
+ const fake = [
52
+ { name: 'canonical', hash: 'aaaa' },
53
+ { name: 'keysarium', hash: 'bbbb' },
54
+ ];
55
+ const msg = 'copies diverged: ' + JSON.stringify(fake.map((s) => s.name + '=' + s.hash.slice(0, 8)));
56
+ assert.match(msg, /keysarium/, 'the message must name the drifted copy');
57
+ assert.match(msg, /canonical/, 'and what it drifted from');
58
+ });
59
+
60
+ test('P3 - the whole module directory agrees, not only the file this feature touched', () => {
61
+ // Scoping the guard to one filename would let the NEXT edit, to a sibling module, drift silently
62
+ // — the same class of miss this test exists to close, one file over.
63
+ const [, canonDir] = COPIES[0];
64
+ const names = fs.readdirSync(canonDir).filter((n) => n.endsWith('.md')).sort();
65
+ assert.ok(names.length >= 6, 'the module directory should hold the M0-M6 modules: ' + names.length);
66
+ for (const name of names) {
67
+ const hashes = COPIES.map(([label, dir]) => {
68
+ const f = path.join(dir, name);
69
+ assert.ok(fs.existsSync(f), name + ' missing from ' + label);
70
+ return label + '=' + sha(f).slice(0, 8);
71
+ });
72
+ const distinct = [...new Set(hashes.map((h) => h.split('=')[1]))];
73
+ assert.equal(distinct.length, 1, name + ' diverged: ' + JSON.stringify(hashes));
74
+ }
75
+ });
76
+ });
@@ -0,0 +1,174 @@
1
+ 'use strict';
2
+
3
+ // The ten shipped skills referenced each other by claude.ai paths (`/mnt/skills/user/<name>/`) and
4
+ // worked only because two rules files tell the model to rewrite them at read time. That is layer 4
5
+ // on the cost-of-detection ladder: probabilistic, and silent when it lapses. MEASURED before this
6
+ // change: 83 occurrences across 19 files.
7
+ //
8
+ // A blind transform would have been worse than the defect, in two separate ways:
9
+ //
10
+ // 1. Seven of those occurrences are the GENERATOR'S OWN instructions to scan its output for
11
+ // unrewritten paths, and more are the rewrite tables themselves. Rewriting a MENTION destroys
12
+ // the mechanism that keeps generated skills clean.
13
+ // 2. Two referenced skills — frontend-design, idea2prd-manual — are NOT shipped here. Rewriting
14
+ // them yields `.claude/skills/frontend-design/`: a path that looks local, looks valid, and
15
+ // resolves to nothing. Worse than a foreign path, which at least announces itself.
16
+ //
17
+ // So the guard is an ALLOWLIST OF EXACT LOCATIONS, not a count. A count is defeated by adding one
18
+ // violation and deleting one mention; naming the survivors is not — and P4 asserts the second
19
+ // direction, because losing the generator's self-check is the failure this exists to prevent.
20
+
21
+ const { test, describe } = require('node:test');
22
+ const assert = require('node:assert/strict');
23
+ const crypto = require('node:crypto');
24
+ const fs = require('node:fs');
25
+ const path = require('node:path');
26
+
27
+ const TPL = path.join(__dirname, '..', '..', 'templates', '.claude');
28
+ const SKILLS = path.join(TPL, 'skills');
29
+
30
+ /**
31
+ * Files permitted to mention `/mnt/`, keyed by a fingerprint of THE MENTION LINES THEMSELVES.
32
+ *
33
+ * A per-file COUNT was the first version and cross-family review broke it in one move: in
34
+ * `03-generate-p0.md`, replace one rewrite-table row with an unrelated unrewritten path and the total
35
+ * stays 11, so the guard stays green over exactly the add/delete mutation the ADR says must fail.
36
+ * The fingerprint is over the sorted mention lines, so swapping one for another changes it.
37
+ */
38
+ const ALLOWED = {
39
+ 'pipeline-forge/SKILL.md': 'b2773e5b80c19885',
40
+ 'pipeline-forge/references/patterns-catalog.md': '9025717e78f2ead3',
41
+ 'pipeline-forge/references/self-extracted-patterns.md': '010eb3198d929b13',
42
+ 'cc-toolkit-generator-enhanced/SKILL.md': 'd7c97e6b6cae4831',
43
+ 'cc-toolkit-generator-enhanced/references/templates/feature-lifecycle-ent.md': '7826a204d7214a22',
44
+ 'cc-toolkit-generator-enhanced/references/templates/feature-lifecycle.md': 'c01aaa46eb670a1b',
45
+ 'cc-toolkit-generator-enhanced/modules/01-detect-parse.md': '70a68e025bd84997',
46
+ 'cc-toolkit-generator-enhanced/modules/03-generate-p0.md': '6410a1ea4107ee33',
47
+ 'cc-toolkit-generator-enhanced/modules/04-generate-p1.md': '634066fcf5a485bf',
48
+ 'cc-toolkit-generator-enhanced/modules/06-package-deliver.md': '12d0dd630ba3d992',
49
+ 'cc-toolkit-generator-enhanced/modules/08-skill-composition.md': 'd9b68fbe2cf1dbfc',
50
+ };
51
+
52
+ /** The fingerprint of one file's mention lines — the identity the allowlist is keyed on. */
53
+ function mentionPrint(rel) {
54
+ const lines = fs.readFileSync(path.join(SKILLS, rel), 'utf-8').split('\n')
55
+ .filter((l) => l.includes('/mnt/')).map((l) => l.trim());
56
+ return crypto.createHash('sha256').update(lines.join('\n')).digest('hex').slice(0, 16);
57
+ }
58
+
59
+ /** Every file under templates/.claude/skills, as repo-relative-to-SKILLS paths. */
60
+ function walk(dir, base) {
61
+ const out = [];
62
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
63
+ const abs = path.join(dir, e.name);
64
+ if (e.isDirectory()) out.push(...walk(abs, base));
65
+ else out.push(path.relative(base, abs).split(path.sep).join('/'));
66
+ }
67
+ return out;
68
+ }
69
+
70
+ const countMnt = (rel) => {
71
+ const body = fs.readFileSync(path.join(SKILLS, rel), 'utf-8');
72
+ return (body.match(/\/mnt\//g) || []).length;
73
+ };
74
+
75
+ describe('shipped skills resolve without a rewrite step', () => {
76
+ test('P1 - no unrewritten /mnt/ path outside the named allowlist', () => {
77
+ const offenders = [];
78
+ for (const rel of walk(SKILLS, SKILLS)) {
79
+ const n = countMnt(rel);
80
+ if (n > 0 && !(rel in ALLOWED)) offenders.push(rel + ' (' + n + ')');
81
+ }
82
+ assert.deepEqual(offenders, [],
83
+ 'these files carry claude.ai paths and are not on the mention allowlist: '
84
+ + JSON.stringify(offenders));
85
+ });
86
+
87
+ test('P2 - every rewritten .claude/skills/<name>/ path resolves to something that exists', () => {
88
+ // NARROWED after the first version failed loudly and was right to: a `.claude/skills/<name>`
89
+ // target does NOT have to be a skill this package ships. Phase 3 GENERATES several into the
90
+ // user's project (`project-context`, `coding-standards`, `security-patterns` — named in
91
+ // replicate-pipeline.md), the rewrite tables carry placeholder names, and two skills are
92
+ // declared optional externals. What this feature created is the class under test: a path that
93
+ // REPLACED a /mnt/ reference to a skill shipped here.
94
+ const shipped = new Set(fs.readdirSync(SKILLS));
95
+ const GENERATED = new Set(['project-context', 'coding-standards', 'security-patterns',
96
+ 'testing-patterns', 'aggregate-patterns', 'event-handlers', 'feature-navigator']);
97
+ const OPTIONAL_EXTERNAL = new Set(['frontend-design', 'idea2prd-manual']);
98
+ const bad = [];
99
+ for (const rel of walk(SKILLS, SKILLS)) {
100
+ const body = fs.readFileSync(path.join(SKILLS, rel), 'utf-8');
101
+ for (const line of body.split('\n')) {
102
+ // A line that shows BOTH sides of the mapping is describing the transform, not performing it.
103
+ if (line.includes('/mnt/')) continue;
104
+ const re = /\.claude\/skills\/([a-z0-9-]+)/g;
105
+ for (let m = re.exec(line); m !== null; m = re.exec(line)) {
106
+ const name = m[1];
107
+ if (shipped.has(name) || GENERATED.has(name) || OPTIONAL_EXTERNAL.has(name)) continue;
108
+ if (name === 'skills' || line.includes('{{') || line.includes('[name]')) continue;
109
+ bad.push(rel + ' → .claude/skills/' + name);
110
+ }
111
+ }
112
+ }
113
+ assert.deepEqual([...new Set(bad)], [],
114
+ 'a path points at a skill that is neither shipped, nor Phase-3 generated, nor a declared '
115
+ + 'optional external: ' + JSON.stringify([...new Set(bad)]));
116
+ });
117
+
118
+ test('P3 - absent skills are declared optional, not rewritten', () => {
119
+ // frontend-design is referenced but not shipped. It must NOT have become a local-looking path.
120
+ const cjm = fs.readFileSync(
121
+ path.join(SKILLS, 'reverse-engineering-unicorn', 'modules', '025-cjm-prototype.md'), 'utf-8');
122
+ assert.match(cjm, /`frontend-design` — OPTIONAL, ВНЕШНИЙ/,
123
+ 'an absent dependency must be declared optional');
124
+ assert.match(cjm, /\*\*Fallback, если его нет:\*\*/,
125
+ 'and carry a fallback — the shipped skill-interface-protocol §6 requires one for every OPTIONAL');
126
+ assert.match(cjm, /Молча пропускать нельзя/,
127
+ 'and must refuse the silent-skip reading');
128
+ assert.ok(!/\.claude\/skills\/frontend-design\/SKILL\.md/.test(cjm),
129
+ 'it must not be given a local path at all — that is the dangling-path failure this avoids');
130
+ });
131
+
132
+ test('P4 - each allowlisted mention still exists, by identity not by count', () => {
133
+ // Both directions, and neither expressible as a total. A NEW unrewritten path changes the
134
+ // fingerprint; so does a DELETED self-check — and losing the generator's `grep -r /mnt/` would
135
+ // silently stop it noticing unrewritten paths in the skills it GENERATES.
136
+ const drift = [];
137
+ for (const [rel, expected] of Object.entries(ALLOWED)) {
138
+ const abs = path.join(SKILLS, rel);
139
+ if (!fs.existsSync(abs)) { drift.push(rel + ' (file gone)'); continue; }
140
+ const got = mentionPrint(rel);
141
+ if (got !== expected) drift.push(rel + ' expected ' + expected + ', got ' + got);
142
+ }
143
+ assert.deepEqual(drift, [],
144
+ 'a mention changed: a new unrewritten path, a lost self-check, or one swapped for the other '
145
+ + '— a per-file count could not tell these apart: ' + JSON.stringify(drift));
146
+ });
147
+
148
+ test('P5 - the guard is an allowlist of locations, not a count', () => {
149
+ // Asserted on the guard itself. A total-count assertion passes after someone adds one violation
150
+ // and removes one mention, which is exactly the edit this feature makes plausible.
151
+ const self = fs.readFileSync(__filename, 'utf-8');
152
+ assert.match(self, /const ALLOWED = \{/, 'the guard must name locations');
153
+ assert.ok(Object.keys(ALLOWED).length >= 5, 'and enumerate them all');
154
+ // Keyed by fingerprint, not count — the property cross-family review found missing.
155
+ assert.match(self, /function mentionPrint/, 'identity, not arithmetic');
156
+ for (const v of Object.values(ALLOWED)) {
157
+ assert.match(String(v), /^[0-9a-f]{16}$/, 'every entry must be a fingerprint, not a number');
158
+ }
159
+ // A count over the whole tree would be a single number; the allowlist is per-file, and P1 keys
160
+ // on membership rather than on any total.
161
+ assert.match(self, /!\(rel in ALLOWED\)/,
162
+ 'P1 must decide by membership, not by comparing a sum');
163
+ });
164
+
165
+ test('P6 - the rewrite rules survive, for skills a user brings from claude.ai', () => {
166
+ // The fix removes the shipped skills' DEPENDENCE on the table, not the table. A user may still
167
+ // install a skill written against /mnt/ paths, and then the rule is what makes it work.
168
+ const proto = fs.readFileSync(path.join(TPL, 'rules', 'skill-interface-protocol.md'), 'utf-8');
169
+ assert.match(proto, /\/mnt\/skills\/user\/\[name\]\/`? \| `\.claude\/skills\/\[name\]\/`/,
170
+ 'the rewrite table must remain');
171
+ const pipeline = fs.readFileSync(path.join(TPL, 'rules', 'replicate-pipeline.md'), 'utf-8');
172
+ assert.match(pipeline, /\/mnt\/skills\/user\//, 'and the pipeline rule must keep its copy');
173
+ });
174
+ });