@dzhechkov/p-replicator 1.6.0 → 1.10.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.
- package/.dz-manifest.json +92 -32
- package/CHANGELOG.md +176 -0
- package/README.md +106 -4
- package/package.json +4 -4
- package/sbom.json +181 -31
- package/src/commands/doctor.js +43 -31
- package/src/commands/verify.js +27 -2
- package/src/utils.js +27 -0
- package/templates/.claude/commands/myinsights.md +22 -5
- package/templates/.claude/commands/replicate.md +57 -1
- package/templates/.claude/hooks/check-docs-complete.cjs +202 -0
- package/templates/.claude/hooks/check-growth-trace.cjs +191 -0
- package/templates/.claude/hooks/check-ports.cjs +36 -4
- package/templates/.claude/hooks/statusline.cjs +16 -5
- package/templates/.claude/rules/replicate-pipeline.md +14 -4
- package/templates/.claude/rules/skill-interface-protocol.md +9 -0
- package/templates/.claude/skills/brutal-honesty-review/scripts/assess-code.sh +40 -7
- package/templates/.claude/skills/brutal-honesty-review/scripts/assess-tests.sh +38 -11
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/03-generate-p0.md +6 -4
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/08-skill-composition.md +2 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-hooks-commands.md +40 -4
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle.md +2 -2
- package/templates/.claude/skills/requirements-validator/SKILL.md +52 -0
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/01-intelligence.md +4 -4
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/02-product-customers.md +2 -2
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/025-cjm-prototype.md +9 -1
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/03-market-competition.md +3 -3
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/04-business-finance.md +3 -3
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/05-growth-engine.md +132 -12
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/06-playbook-synthesis.md +1 -1
- package/templates/.claude/skills/sparc-prd-mini/SKILL.md +9 -9
- package/tests/snapshot/baseline.json +25 -23
- package/tests/unit/absence-is-not-emptiness.test.js +255 -0
- package/tests/unit/assess-scripts.test.js +150 -0
- package/tests/unit/check-docs-complete.test.js +292 -0
- package/tests/unit/check-growth-trace.test.js +188 -0
- package/tests/unit/check-ports.test.js +99 -0
- package/tests/unit/generated-guard-templates.test.js +134 -0
- package/tests/unit/growth-axes-and-compliance.test.js +169 -0
- package/tests/unit/growth-gate-conditional.test.js +122 -0
- package/tests/unit/growth-module-b2b-gate.test.js +20 -2
- package/tests/unit/growth-requirements-bridge.test.js +127 -0
- package/tests/unit/guard-forms.test.js +302 -0
- package/tests/unit/insights-docs-tell-the-truth.test.js +84 -0
- package/tests/unit/module-copy-identity.test.js +106 -0
- package/tests/unit/shipped-suite-context.test.js +142 -0
- package/tests/unit/skill-paths-prebaked.test.js +174 -0
- package/tests/unit/sync-templates-guard.test.js +31 -1
|
@@ -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
|
+
});
|
|
@@ -23,6 +23,35 @@ const fs = require('node:fs');
|
|
|
23
23
|
const os = require('node:os');
|
|
24
24
|
const path = require('node:path');
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Is this copy sitting inside the monorepo?
|
|
28
|
+
*
|
|
29
|
+
* A POSITIVE fact — the sibling packages EXIST — never the absence of something. An absence-based
|
|
30
|
+
* check would also fire on a broken checkout and quietly disable the guard exactly when something
|
|
31
|
+
* is wrong.
|
|
32
|
+
*
|
|
33
|
+
* MEASURED 2026-08-27: `npm test` from the published 1.9.0 tarball was 288/296. Both failures were
|
|
34
|
+
* monorepo-only BY CONSTRUCTION — one needs `scripts/`, which files[] does not ship; the other
|
|
35
|
+
* compares copies across sibling PACKAGES. Neither says anything about a user's installation, and
|
|
36
|
+
* shipping them red means a user who runs our tests is told their install is broken when it is not.
|
|
37
|
+
*
|
|
38
|
+
* The skip is only acceptable because tests/unit/shipped-suite-context.test.js asserts these files
|
|
39
|
+
* RUN — not skip — inside the monorepo. Without that the skip rots into permanent the day this
|
|
40
|
+
* detection breaks, and nothing would say so.
|
|
41
|
+
*/
|
|
42
|
+
function insideMonorepo() {
|
|
43
|
+
const siblings = path.resolve(__dirname, '..', '..', '..'); // packages/@dzhechkov
|
|
44
|
+
try {
|
|
45
|
+
return fs.statSync(path.join(siblings, 'harness-core', 'package.json')).isFile();
|
|
46
|
+
} catch { return false; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const MONOREPO_ONLY = !insideMonorepo();
|
|
50
|
+
if (MONOREPO_ONLY) {
|
|
51
|
+
console.log('# SKIP (monorepo-only): sibling package @dzhechkov/harness-core is not present, so '
|
|
52
|
+
+ 'this file cannot compare across packages. This says nothing about your installation.');
|
|
53
|
+
}
|
|
54
|
+
|
|
26
55
|
const PKG_DIR = path.resolve(__dirname, '..', '..');
|
|
27
56
|
const SCRIPT_SRC = path.join(PKG_DIR, 'scripts', 'sync-templates.js');
|
|
28
57
|
const MARKER = '.p-replicator-sync-source';
|
|
@@ -90,7 +119,8 @@ function runScript(pkgDir) {
|
|
|
90
119
|
const cleanup = (d) => fs.rmSync(d, { recursive: true, force: true });
|
|
91
120
|
const readPkg = (rel) => fs.readFileSync(path.join(PKG_DIR, rel), 'utf-8');
|
|
92
121
|
|
|
93
|
-
describe
|
|
122
|
+
describe.skip = describe.skip || (() => {});
|
|
123
|
+
(MONOREPO_ONLY ? describe.skip : describe)('a publish-time copy that was safe only because nobody called it', () => {
|
|
94
124
|
test('P1 — an unmarked root is REFUSED, and not one byte of templates/ changes', () => {
|
|
95
125
|
const f = fixture({});
|
|
96
126
|
try {
|