@dzhechkov/p-replicator 1.5.18 → 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.
- package/.dz-manifest.json +173 -57
- package/CHANGELOG.md +262 -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 +6 -2
- package/README/eng/README.md +1 -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 +1 -1
- package/README/ru/html/index.html +7 -7
- package/README.md +139 -16
- package/bin/cli.js +0 -0
- package/package.json +11 -10
- package/sbom.json +346 -56
- package/src/utils.js +4 -0
- package/templates/.claude/agents/doc-validator.md +2 -1
- package/templates/.claude/agents/product-discoverer.md +1 -1
- package/templates/.claude/commands/next.md +16 -0
- package/templates/.claude/commands/replicate.md +183 -9
- package/templates/.claude/commands/start.md +19 -1
- package/templates/.claude/hooks/autocommit-insights.cjs +95 -10
- package/templates/.claude/hooks/autocommit-plans.cjs +95 -10
- package/templates/.claude/hooks/autocommit-roadmap.cjs +96 -13
- package/templates/.claude/hooks/check-docs-complete.cjs +174 -0
- package/templates/.claude/hooks/check-growth-trace.cjs +191 -0
- package/templates/.claude/hooks/check-ports.cjs +232 -0
- package/templates/.claude/hooks/session-insights.cjs +13 -1
- package/templates/.claude/hooks/state-update.cjs +13 -1
- package/templates/.claude/hooks/statusline.cjs +145 -18
- package/templates/.claude/rules/docker-ports.md +123 -0
- package/templates/.claude/rules/replicate-pipeline.md +19 -6
- package/templates/.claude/rules/skill-interface-protocol.md +9 -0
- package/templates/.claude/settings.json +5 -5
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/01-detect-parse.md +57 -14
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/02-analyze-map.md +9 -7
- 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/enhanced-recommendations.md +6 -4
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/extended-mapping.md +2 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-agents.md +2 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-skills.md +1 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/enhanced-claude-md.md +2 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle.md +2 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-suggestions.md +11 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/start-command.md +1 -1
- package/templates/.claude/skills/requirements-validator/SKILL.md +59 -0
- package/templates/.claude/skills/requirements-validator/references/scoring-system.md +28 -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 +152 -10
- package/tests/e2e/lifecycle.test.js +21 -10
- package/tests/snapshot/baseline.json +42 -38
- package/tests/snapshot/update-baseline.js +2 -1
- package/tests/unit/adr-decision-coverage.test.js +137 -0
- package/tests/unit/adr-scanner-contract.test.js +108 -0
- package/tests/unit/autocommit-deletion.test.js +242 -0
- package/tests/unit/check-docs-complete.test.js +249 -0
- package/tests/unit/check-growth-trace.test.js +188 -0
- package/tests/unit/check-ports.test.js +184 -0
- package/tests/unit/db-port-rule.test.js +216 -0
- package/tests/unit/detect-parse-anchor.test.js +109 -0
- package/tests/unit/external-dependency-check.test.js +209 -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 +122 -0
- package/tests/unit/growth-requirements-bridge.test.js +127 -0
- package/tests/unit/hooks-project-anchored.test.js +223 -0
- package/tests/unit/hooks-report-failures.test.js +207 -0
- package/tests/unit/module-copy-identity.test.js +76 -0
- package/tests/unit/pipeline-file-ownership.test.js +95 -0
- package/tests/unit/roadmap-one-schema.test.js +179 -0
- package/tests/unit/skill-paths-prebaked.test.js +174 -0
- package/tests/unit/sparc-reconciliation.test.js +117 -0
- package/tests/unit/spec-pseudocode-traceability.test.js +146 -0
- package/tests/unit/statusline-honest-labels.test.js +178 -0
- package/tests/unit/statusline-two-roots.test.js +237 -0
- package/tests/unit/sync-templates-guard.test.js +209 -0
- package/tests/unit/utils.test.js +2 -2
- package/tests/unit/validation-gate-teeth.test.js +158 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// All three autocommit hooks opened with `if (!fs.existsSync(TARGET)) process.exit(0);`, so a DELETED
|
|
4
|
+
// roadmap was "nothing to do" — though a deletion is exactly the change these hooks exist to record.
|
|
5
|
+
// The history showed the file being maintained and then, silently, not.
|
|
6
|
+
//
|
|
7
|
+
// THE TRAP, measured before a line was written: simply deleting that guard breaks the failure
|
|
8
|
+
// reporting that shipped the day before.
|
|
9
|
+
//
|
|
10
|
+
// git add -- <a path that never existed> → exit 128, "fatal: pathspec … did not match any files"
|
|
11
|
+
// git add -- <a tracked file, deleted> → exit 0, and diff --cached then reports a change
|
|
12
|
+
//
|
|
13
|
+
// A failing git call now prints one line, so the naive fix would print a failure on EVERY session
|
|
14
|
+
// stop in every project without this artifact — which is most of them. That is the noise the same
|
|
15
|
+
// feature's own comment warns trains people to ignore notices. P3 is the guard on it.
|
|
16
|
+
//
|
|
17
|
+
// The discriminator is `git ls-files -- <path>`: entries for a path git knows, nothing for one it
|
|
18
|
+
// does not. It must run BEFORE staging: staging a deletion removes the entry from the index, after
|
|
19
|
+
// which the same question answers "not tracked". (`--error-unmatch` discriminates just as well, for
|
|
20
|
+
// files and directories alike — an earlier note here said otherwise and was withdrawn after
|
|
21
|
+
// re-measuring. `ls-files --` is preferred only because it answers with data instead of throwing.)
|
|
22
|
+
|
|
23
|
+
const { test, describe } = require('node:test');
|
|
24
|
+
const assert = require('node:assert/strict');
|
|
25
|
+
const { execFileSync, spawnSync } = require('node:child_process');
|
|
26
|
+
const fs = require('node:fs');
|
|
27
|
+
const os = require('node:os');
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
|
|
30
|
+
const TPL = path.join(__dirname, '..', '..', 'templates', '.claude');
|
|
31
|
+
|
|
32
|
+
const HOOKS = [
|
|
33
|
+
{ name: 'autocommit-roadmap', remove: /auto-remove/,
|
|
34
|
+
make: (root) => {
|
|
35
|
+
fs.mkdirSync(path.join(root, '.claude'), { recursive: true });
|
|
36
|
+
const f = path.join(root, '.claude', 'feature-roadmap.json');
|
|
37
|
+
fs.writeFileSync(f, '{"v":1}\n');
|
|
38
|
+
return { drop: () => fs.rmSync(f), touch: () => fs.writeFileSync(f, '{"v":2}\n') };
|
|
39
|
+
} },
|
|
40
|
+
{ name: 'autocommit-insights', remove: /auto-remove/,
|
|
41
|
+
make: (root) => {
|
|
42
|
+
const d = path.join(root, '.claude', 'insights');
|
|
43
|
+
fs.mkdirSync(d, { recursive: true });
|
|
44
|
+
fs.writeFileSync(path.join(d, 'index.md'), '## one\n');
|
|
45
|
+
return { drop: () => fs.rmSync(d, { recursive: true }),
|
|
46
|
+
touch: () => fs.writeFileSync(path.join(d, 'index.md'), '## one\n## two\n') };
|
|
47
|
+
} },
|
|
48
|
+
{ name: 'autocommit-plans', remove: /auto-remove/,
|
|
49
|
+
make: (root) => {
|
|
50
|
+
const d = path.join(root, 'docs', 'plans');
|
|
51
|
+
fs.mkdirSync(d, { recursive: true });
|
|
52
|
+
fs.writeFileSync(path.join(d, 'a.md'), '# plan\n');
|
|
53
|
+
return { drop: () => fs.rmSync(d, { recursive: true }),
|
|
54
|
+
touch: () => fs.writeFileSync(path.join(d, 'a.md'), '# plan v2\n') };
|
|
55
|
+
} },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
function project(opts) {
|
|
59
|
+
const o = opts || {};
|
|
60
|
+
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-del-')));
|
|
61
|
+
fs.mkdirSync(path.join(root, '.claude', 'hooks'), { recursive: true });
|
|
62
|
+
for (const h of HOOKS) {
|
|
63
|
+
fs.copyFileSync(path.join(TPL, 'hooks', h.name + '.cjs'),
|
|
64
|
+
path.join(root, '.claude', 'hooks', h.name + '.cjs'));
|
|
65
|
+
}
|
|
66
|
+
if (o.git !== false) {
|
|
67
|
+
const git = (a) => execFileSync('git', a, { cwd: root, stdio: 'ignore' });
|
|
68
|
+
git(['init', '-q']);
|
|
69
|
+
git(['config', 'user.email', 'test@example.invalid']);
|
|
70
|
+
git(['config', 'user.name', 'test']);
|
|
71
|
+
}
|
|
72
|
+
return root;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const git = (root, args) =>
|
|
76
|
+
execFileSync('git', args, { cwd: root, encoding: 'utf8' }).trim();
|
|
77
|
+
const commitAll = (root) => {
|
|
78
|
+
execFileSync('git', ['add', '-A'], { cwd: root, stdio: 'ignore' });
|
|
79
|
+
execFileSync('git', ['commit', '-qm', 'base'], { cwd: root, stdio: 'ignore' });
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
function run(root, hookName) {
|
|
83
|
+
const r = spawnSync(process.execPath,
|
|
84
|
+
[path.join(root, '.claude', 'hooks', hookName + '.cjs')],
|
|
85
|
+
{ cwd: root, env: Object.assign({}, process.env, { CLAUDE_PROJECT_DIR: root }),
|
|
86
|
+
encoding: 'utf8' });
|
|
87
|
+
return { code: r.status, out: (r.stdout || '') + (r.stderr || '') };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const cleanup = (d) => fs.rmSync(d, { recursive: true, force: true });
|
|
91
|
+
|
|
92
|
+
describe('a deleted artifact is recorded, and an absent one is still silent', () => {
|
|
93
|
+
test('P1 — deleting a tracked target produces a commit and leaves the tree clean', () => {
|
|
94
|
+
const root = project({});
|
|
95
|
+
try {
|
|
96
|
+
const h = HOOKS[0];
|
|
97
|
+
const ctl = h.make(root);
|
|
98
|
+
commitAll(root);
|
|
99
|
+
const before = Number(git(root, ['rev-list', '--count', 'HEAD']));
|
|
100
|
+
ctl.drop();
|
|
101
|
+
|
|
102
|
+
const r = run(root, h.name);
|
|
103
|
+
assert.equal(r.code, 0, 'the hook must stay non-blocking: ' + r.out);
|
|
104
|
+
assert.equal(Number(git(root, ['rev-list', '--count', 'HEAD'])), before + 1,
|
|
105
|
+
'the deletion must be committed, not treated as nothing to do');
|
|
106
|
+
assert.equal(git(root, ['status', '--porcelain']), '',
|
|
107
|
+
'and the tree must be left clean, not merely staged');
|
|
108
|
+
} finally { cleanup(root); }
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('P2 — the commit names the removal, so the event is findable in the log', () => {
|
|
112
|
+
// The owner's stated purpose: be able to spot the event and undo it. A deletion recorded as
|
|
113
|
+
// "auto-update" is findable only by reading diffs.
|
|
114
|
+
const root = project({});
|
|
115
|
+
try {
|
|
116
|
+
const h = HOOKS[0];
|
|
117
|
+
const ctl = h.make(root);
|
|
118
|
+
commitAll(root);
|
|
119
|
+
ctl.drop();
|
|
120
|
+
run(root, h.name);
|
|
121
|
+
const subject = git(root, ['log', '-1', '--format=%s']);
|
|
122
|
+
assert.match(subject, h.remove,
|
|
123
|
+
'the subject must say the artifact was removed: ' + JSON.stringify(subject));
|
|
124
|
+
} finally { cleanup(root); }
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('P3 — a project that never had the artifact stays SILENT', () => {
|
|
128
|
+
// The guard on the naive fix. Deleting the existence check outright would make `git add` exit
|
|
129
|
+
// 128 here, and the failure reporting that shipped yesterday would print a line on every session
|
|
130
|
+
// stop in every project without this artifact.
|
|
131
|
+
const root = project({});
|
|
132
|
+
try {
|
|
133
|
+
fs.writeFileSync(path.join(root, 'unrelated.txt'), 'x\n');
|
|
134
|
+
commitAll(root);
|
|
135
|
+
for (const h of HOOKS) {
|
|
136
|
+
const r = run(root, h.name);
|
|
137
|
+
assert.equal(r.code, 0, h.name + ' must exit 0');
|
|
138
|
+
assert.equal(r.out.trim(), '',
|
|
139
|
+
h.name + ' printed something for an artifact this project never had: ' + r.out);
|
|
140
|
+
}
|
|
141
|
+
assert.equal(git(root, ['rev-list', '--count', 'HEAD']), '1',
|
|
142
|
+
'and nothing may be committed');
|
|
143
|
+
} finally { cleanup(root); }
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('P4 — not a git repository: silent, exit 0', () => {
|
|
147
|
+
const root = project({ git: false });
|
|
148
|
+
try {
|
|
149
|
+
const r = run(root, HOOKS[0].name);
|
|
150
|
+
assert.equal(r.code, 0);
|
|
151
|
+
assert.equal(r.out.trim(), '', 'not-a-repo is not a failure to report: ' + r.out);
|
|
152
|
+
} finally { cleanup(root); }
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('P5 — an existing, changed target still commits as before', () => {
|
|
156
|
+
const root = project({});
|
|
157
|
+
try {
|
|
158
|
+
const h = HOOKS[0];
|
|
159
|
+
const ctl = h.make(root);
|
|
160
|
+
commitAll(root);
|
|
161
|
+
ctl.touch();
|
|
162
|
+
const before = Number(git(root, ['rev-list', '--count', 'HEAD']));
|
|
163
|
+
assert.equal(run(root, h.name).code, 0);
|
|
164
|
+
assert.equal(Number(git(root, ['rev-list', '--count', 'HEAD'])), before + 1,
|
|
165
|
+
'the ordinary update path must be untouched');
|
|
166
|
+
assert.doesNotMatch(git(root, ['log', '-1', '--format=%s']), /auto-remove/,
|
|
167
|
+
'and an update must NOT be labelled a removal');
|
|
168
|
+
} finally { cleanup(root); }
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('P6 — an existing, unchanged target is silent', () => {
|
|
172
|
+
const root = project({});
|
|
173
|
+
try {
|
|
174
|
+
const h = HOOKS[0];
|
|
175
|
+
h.make(root);
|
|
176
|
+
commitAll(root);
|
|
177
|
+
const before = Number(git(root, ['rev-list', '--count', 'HEAD']));
|
|
178
|
+
const r = run(root, h.name);
|
|
179
|
+
assert.equal(r.code, 0);
|
|
180
|
+
assert.equal(r.out.trim(), '', 'nothing to commit must stay silent: ' + r.out);
|
|
181
|
+
assert.equal(Number(git(root, ['rev-list', '--count', 'HEAD'])), before,
|
|
182
|
+
'and must not create an empty commit');
|
|
183
|
+
} finally { cleanup(root); }
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('P8 — a PARTIAL deletion is still a removal, even though the directory remains', () => {
|
|
187
|
+
// Cross-family QE: classifying from `existsSync` on the DIRECTORY was wrong. Deleting one file
|
|
188
|
+
// inside it — or every tracked file while an ignored one keeps the directory present — left the
|
|
189
|
+
// path existing, so the commit said "auto-save" and the removal was unfindable by the very
|
|
190
|
+
// search this feature promises. The classification now comes from what git actually STAGED.
|
|
191
|
+
const root = project({});
|
|
192
|
+
try {
|
|
193
|
+
const d = path.join(root, 'docs', 'plans');
|
|
194
|
+
fs.mkdirSync(d, { recursive: true });
|
|
195
|
+
fs.writeFileSync(path.join(d, 'a.md'), 'a\n');
|
|
196
|
+
fs.writeFileSync(path.join(d, 'b.md'), 'b\n');
|
|
197
|
+
commitAll(root);
|
|
198
|
+
fs.rmSync(path.join(d, 'a.md')); // the directory still exists
|
|
199
|
+
|
|
200
|
+
const r = run(root, 'autocommit-plans');
|
|
201
|
+
assert.equal(r.code, 0, r.out);
|
|
202
|
+
assert.ok(fs.existsSync(d), 'the fixture must leave the directory in place');
|
|
203
|
+
assert.match(git(root, ['log', '-1', '--format=%s']), /auto-remove/,
|
|
204
|
+
'one deleted file inside a surviving directory is still a removal: '
|
|
205
|
+
+ git(root, ['log', '-1', '--format=%s']));
|
|
206
|
+
} finally { cleanup(root); }
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('P9 — a real git failure is not laundered into "never existed"', () => {
|
|
210
|
+
// The `ls-files` answer decides whether a deletion is recorded at all. A bare catch that turned
|
|
211
|
+
// every failure into an empty answer would silently skip the deletion — a bypass of the feature
|
|
212
|
+
// itself, wearing the shape of the ordinary case.
|
|
213
|
+
for (const h of HOOKS) {
|
|
214
|
+
const src = fs.readFileSync(path.join(TPL, 'hooks', h.name + '.cjs'), 'utf-8');
|
|
215
|
+
const at = src.indexOf("git(['ls-files'");
|
|
216
|
+
assert.ok(at > 0, h.name + ' must ask git whether the path is tracked');
|
|
217
|
+
const around = src.slice(at - 200, at + 200);
|
|
218
|
+
assert.ok(!/catch\s*\{\s*tracked\s*=\s*''/.test(around),
|
|
219
|
+
h.name + ' swallows a real ls-files failure as "not tracked": ' + around);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('P7 — all three hooks record their own deletion identically', () => {
|
|
224
|
+
// Two of the three watch a DIRECTORY rather than a file, so the deletion path has to work for
|
|
225
|
+
// both shapes — that is what this case proves, and it is why the fixtures differ.
|
|
226
|
+
for (const h of HOOKS) {
|
|
227
|
+
const root = project({});
|
|
228
|
+
try {
|
|
229
|
+
const ctl = h.make(root);
|
|
230
|
+
commitAll(root);
|
|
231
|
+
const before = Number(git(root, ['rev-list', '--count', 'HEAD']));
|
|
232
|
+
ctl.drop();
|
|
233
|
+
const r = run(root, h.name);
|
|
234
|
+
assert.equal(r.code, 0, h.name + ' must exit 0: ' + r.out);
|
|
235
|
+
assert.equal(Number(git(root, ['rev-list', '--count', 'HEAD'])), before + 1,
|
|
236
|
+
h.name + ' did not record its deletion');
|
|
237
|
+
assert.match(git(root, ['log', '-1', '--format=%s']), h.remove,
|
|
238
|
+
h.name + ' did not name the removal');
|
|
239
|
+
} finally { cleanup(root); }
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Phase 2 launched a swarm of validation agents over whatever Phase 1 produced, with nothing in
|
|
4
|
+
// between. Existence, emptiness and unfilled placeholders are decidable by forty lines of code, so
|
|
5
|
+
// sending a swarm to discover them is a layer-1 check living at layer 3.
|
|
6
|
+
//
|
|
7
|
+
// The trap this suite exists to hold: an eager placeholder pattern. A bracketed token is also how
|
|
8
|
+
// markdown writes a link and how a citation looks, so a naive /\[.*\]/ refuses legitimate documents
|
|
9
|
+
// — the same eager-gate failure this repo closed for the Measurable criterion and twice today for
|
|
10
|
+
// the growth criterion. P4 is the guard on it.
|
|
11
|
+
|
|
12
|
+
const { test, describe } = require('node:test');
|
|
13
|
+
const assert = require('node:assert/strict');
|
|
14
|
+
const { spawnSync } = require('node:child_process');
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
const os = require('node:os');
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
|
|
19
|
+
const PKG = path.resolve(__dirname, '..', '..');
|
|
20
|
+
const TPL = path.join(PKG, 'templates', '.claude');
|
|
21
|
+
const CHECK = path.join(TPL, 'hooks', 'check-docs-complete.cjs');
|
|
22
|
+
|
|
23
|
+
const REQUIRED = ['PRD.md', 'Solution_Strategy.md', 'Specification.md', 'Pseudocode.md',
|
|
24
|
+
'Architecture.md', 'Refinement.md', 'Completion.md', 'Research_Findings.md', 'Final_Summary.md'];
|
|
25
|
+
|
|
26
|
+
/** Real prose, comfortably over the emptiness threshold, with no placeholder in it. */
|
|
27
|
+
const REAL = '# Документ\n\n' + 'Содержательный абзац про предметную область проекта. '.repeat(12);
|
|
28
|
+
|
|
29
|
+
/** Build a project and run the real checker over it. */
|
|
30
|
+
function check(files, opts) {
|
|
31
|
+
const o = opts || {};
|
|
32
|
+
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-docs-')));
|
|
33
|
+
try {
|
|
34
|
+
if (!o.noDocs) {
|
|
35
|
+
fs.mkdirSync(path.join(dir, 'docs'), { recursive: true });
|
|
36
|
+
for (const [name, body] of Object.entries(files || {})) {
|
|
37
|
+
fs.writeFileSync(path.join(dir, 'docs', name), body);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const r = spawnSync(process.execPath, [CHECK, dir], { encoding: 'utf8' });
|
|
41
|
+
return { code: r.status, out: (r.stdout || '') + (r.stderr || '') };
|
|
42
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const complete = (over) => Object.assign(
|
|
46
|
+
Object.fromEntries(REQUIRED.map((f) => [f, REAL])), over || {});
|
|
47
|
+
|
|
48
|
+
describe('the cheap question is asked before the swarm is spent', () => {
|
|
49
|
+
test('P1 - a complete document set exits 0', () => {
|
|
50
|
+
const r = check(complete());
|
|
51
|
+
assert.equal(r.code, 0, 'a full set must pass: ' + r.out);
|
|
52
|
+
assert.match(r.out, /9 проверено/, r.out);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('P2 - a missing required document exits 1 and is NAMED', () => {
|
|
56
|
+
const files = complete();
|
|
57
|
+
delete files['Specification.md'];
|
|
58
|
+
const r = check(files);
|
|
59
|
+
assert.equal(r.code, 1, r.out);
|
|
60
|
+
assert.match(r.out, /Specification\.md: отсутствует/, 'the missing one must be named: ' + r.out);
|
|
61
|
+
assert.ok(!/PRD\.md/.test(r.out), 'and the present ones must not be: ' + r.out);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('P3 - a document that exists but says nothing exits 1', () => {
|
|
65
|
+
// The case `test -f` misses, and the reason this check is not a one-liner.
|
|
66
|
+
const r = check(complete({ 'Refinement.md': '# Refinement\n' }));
|
|
67
|
+
assert.equal(r.code, 1, r.out);
|
|
68
|
+
assert.match(r.out, /Refinement\.md: пуст или почти пуст/, r.out);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('P4 - markdown links and citations are not placeholders', () => {
|
|
72
|
+
// A naive bracket pattern refuses this document, which is entirely legitimate. An eager gate is
|
|
73
|
+
// not a stricter gate — it is a gate people turn off.
|
|
74
|
+
const withLinks = '# Архитектура\n\n'
|
|
75
|
+
+ 'Смотри [документацию Postgres](https://www.postgresql.org/docs/) и [ADR-001](docs/ADR.md). '
|
|
76
|
+
+ 'Подход описан в [Fowler 2019](https://martinfowler.com/articles/). '.repeat(6);
|
|
77
|
+
const r = check(complete({ 'Architecture.md': withLinks }));
|
|
78
|
+
assert.equal(r.code, 0,
|
|
79
|
+
'markdown links must not read as unfilled placeholders: ' + r.out);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('P5 - a project missing only optional documents passes', () => {
|
|
83
|
+
// ADR.md and C4_Diagrams.md are "(if applicable)" in replicate.md. Demanding them would refuse
|
|
84
|
+
// every project without DDD, forever — the trap already closed for Measurable and for growth.
|
|
85
|
+
const r = check(complete()); // neither optional file is present
|
|
86
|
+
assert.equal(r.code, 0, 'optional absence is a legitimate answer: ' + r.out);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('P13 - the counter-examples cross-family review supplied', () => {
|
|
90
|
+
// Both REPRODUCED as exit 1 before the fix, and both would have been catastrophic:
|
|
91
|
+
// the first blocks EVERY normally generated project, the second deadlocks a documented flow.
|
|
92
|
+
const mermaid = '# Architecture\n\n```mermaid\ngraph TD\n A[Web App] --> B[API]\n'
|
|
93
|
+
+ ' B --> F[(Database)]\n```\n\n' + REAL;
|
|
94
|
+
assert.equal(check(complete({ 'Architecture.md': mermaid })).code, 0,
|
|
95
|
+
'the BUNDLED sparc-prd-mini skill REQUIRES mermaid (SKILL.md:570-583) — blocking on its node '
|
|
96
|
+
+ 'labels would refuse every normally generated Architecture.md');
|
|
97
|
+
|
|
98
|
+
const gap = '# Specification\n\n[GAP: needs performance targets from the client]\n\n' + REAL;
|
|
99
|
+
assert.equal(check(complete({ 'Specification.md': gap })).code, 0,
|
|
100
|
+
'Phase 1 writes [GAP: ...] deliberately in --from-docs mode for Phase 2 to resolve '
|
|
101
|
+
+ '(replicate.md:82-94). Blocking deadlocks it: the only step that can clear the marker is '
|
|
102
|
+
+ 'the one this gate would refuse to start');
|
|
103
|
+
|
|
104
|
+
const citations = '# Research_Findings\n\nПо данным [1] и [^2] рынок растёт.\n\n' + REAL;
|
|
105
|
+
assert.equal(check(complete({ 'Research_Findings.md': citations })).code, 0,
|
|
106
|
+
'citations and footnotes are not placeholders');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('P14 - bracketed prose WARNS but never blocks', () => {
|
|
110
|
+
// The design change the review forced. This script cannot tell `[описание продукта]` from a
|
|
111
|
+
// diagram label without understanding the document, and a false block stops the whole pipeline —
|
|
112
|
+
// strictly worse than a missed placeholder the Phase-2 swarm would catch anyway. So the two
|
|
113
|
+
// confidence levels are separated, and the uncertain one is not given the power to refuse.
|
|
114
|
+
const r = check(complete({ 'PRD.md': '# PRD\n\n[описание продукта]\n\n' + REAL }));
|
|
115
|
+
assert.equal(r.code, 0, 'bracketed prose must not block: ' + r.out);
|
|
116
|
+
assert.match(r.out, /НЕ блокирует/, 'and must say plainly that it does not: ' + r.out);
|
|
117
|
+
assert.match(r.out, /PRD\.md: возможно незаполнено/, 'while still naming it: ' + r.out);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('P6 - an UNAMBIGUOUS placeholder is caught, in both blocking vocabularies', () => {
|
|
121
|
+
// Narrowed by the review: the bracketed-prose vocabulary moved to a warning (P14). What blocks
|
|
122
|
+
// is what cannot be anything else.
|
|
123
|
+
for (const [body, why] of [
|
|
124
|
+
['# PRD\n\n{{company_name}} строит продукт.\n\n' + REAL, 'a mustache placeholder'],
|
|
125
|
+
['# PRD\n\nTODO: дописать сегменты.\n\n' + REAL, 'a TODO marker'],
|
|
126
|
+
]) {
|
|
127
|
+
const r = check(complete({ 'PRD.md': body }));
|
|
128
|
+
assert.equal(r.code, 1, why + ' must block: ' + r.out);
|
|
129
|
+
assert.match(r.out, /PRD\.md: остались/, r.out);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('P7 - no docs directory exits 2, and an empty one does too', () => {
|
|
134
|
+
const none = check({}, { noDocs: true });
|
|
135
|
+
assert.equal(none.code, 2, 'nothing to check is not "complete": ' + none.out);
|
|
136
|
+
assert.match(none.out, /проверка НЕ выполнена/, none.out);
|
|
137
|
+
assert.match(none.out, /Фаза 1 ещё не отработала/, 'the reason must be named: ' + none.out);
|
|
138
|
+
|
|
139
|
+
const empty = check({});
|
|
140
|
+
assert.equal(empty.code, 2, 'an empty docs/ is not "complete" either: ' + empty.out);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('P10 - one binary, all three verdicts in a single run', () => {
|
|
144
|
+
const seen = [
|
|
145
|
+
check(complete()).code,
|
|
146
|
+
check(Object.assign(complete(), { 'PRD.md': '# PRD\n' })).code,
|
|
147
|
+
check({}, { noDocs: true }).code,
|
|
148
|
+
];
|
|
149
|
+
assert.deepEqual(seen, [0, 1, 2], 'expected complete/incomplete/could-not-check: '
|
|
150
|
+
+ JSON.stringify(seen));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('P11 - Phase 2 runs it BEFORE the swarm and refuses on exit 1', () => {
|
|
154
|
+
const src = fs.readFileSync(path.join(TPL, 'commands', 'replicate.md'), 'utf-8');
|
|
155
|
+
const start = src.indexOf('### Phase 2: VALIDATION');
|
|
156
|
+
const end = src.indexOf('\n### Phase 3', start + 1);
|
|
157
|
+
assert.ok(start > 0 && end > start, 'replicate.md must have a Phase 2');
|
|
158
|
+
const phase2 = src.slice(start, end);
|
|
159
|
+
|
|
160
|
+
const call = phase2.indexOf('check-docs-complete.cjs');
|
|
161
|
+
const swarm = phase2.indexOf('Swarm of Validation Agents');
|
|
162
|
+
assert.ok(call > 0, 'Phase 2 must invoke the check');
|
|
163
|
+
assert.ok(swarm > call,
|
|
164
|
+
'the check must precede the swarm, or it cannot save the swarm: call=' + call + ' swarm=' + swarm);
|
|
165
|
+
assert.match(phase2, /\*\*НЕ запускайте рой\.\*\*/,
|
|
166
|
+
'exit 1 must stop the swarm, or the check is advice');
|
|
167
|
+
assert.match(phase2, /это НЕ «всё в порядке»/,
|
|
168
|
+
'exit 2 must be refused as a pass, in the artifact a reader sees');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('P8 - prepublishOnly runs the snapshot test', () => {
|
|
172
|
+
// tests/ is in files[] (MEASURED: 29 test files in npm pack --dry-run), so baseline.json SHIPS.
|
|
173
|
+
// prepublishOnly ran only the signature gate, which signs whatever is present — dz sign prints
|
|
174
|
+
// the limit itself: "Ed25519 gives tamper-evidence, never truthfulness." A drifted baseline was
|
|
175
|
+
// therefore signed, shipped and wrong. The source backlog item claimed publish was already
|
|
176
|
+
// blocked; its own ground-check caught that this was false.
|
|
177
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PKG, 'package.json'), 'utf-8'));
|
|
178
|
+
const pre = pkg.scripts.prepublishOnly || '';
|
|
179
|
+
assert.match(pre, /tests\/snapshot\/templates\.test\.js/,
|
|
180
|
+
'a drifted baseline must not be publishable: ' + pre);
|
|
181
|
+
assert.ok(pre.indexOf('templates.test.js') < pre.indexOf('prepublish-gate'),
|
|
182
|
+
'the snapshot must run BEFORE signing, or a drifted baseline gets signed first: ' + pre);
|
|
183
|
+
assert.ok((pkg.files || []).includes('tests/'),
|
|
184
|
+
'this assertion only matters because tests/ ships — if that changes, revisit');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('P9 - it is a hooks component wired to NO event', () => {
|
|
188
|
+
const { COMPONENTS } = require(path.join(PKG, 'src', 'utils.js'));
|
|
189
|
+
assert.ok(COMPONENTS.hooks.items['check-docs-complete'], 'it must be registered');
|
|
190
|
+
const settings = fs.readFileSync(path.join(TPL, 'settings.json'), 'utf-8');
|
|
191
|
+
assert.ok(!settings.includes('check-docs-complete'),
|
|
192
|
+
'this packages hooks are non-blocking by contract; a hook could print but never refuse');
|
|
193
|
+
const statusline = fs.readFileSync(path.join(TPL, 'hooks', 'statusline.cjs'), 'utf-8');
|
|
194
|
+
const m = statusline.match(/hooksExpected:\s*(\d+)/);
|
|
195
|
+
assert.equal(Number(m[1]), Object.keys(COMPONENTS.hooks.items).length,
|
|
196
|
+
'the status line would report a phantom missing hook: ' + m[1]);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('P12 - the artifact states its own limit', () => {
|
|
200
|
+
const r = check(complete());
|
|
201
|
+
assert.match(r.out, /документы НАПИСАНЫ, а не что они верны/,
|
|
202
|
+
'a completeness pass looks like a correctness pass and must refuse the reading: ' + r.out);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// ── Appended: the resume half of backlog 58575b07 ─────────────────────────────────────────────
|
|
207
|
+
// The item asked for automated resume detection. Evidence gathered 2026-08-27 says the automation
|
|
208
|
+
// is not the valuable half: /replicate has FOUR interactive checkpoints (MEASURED: `grep -c
|
|
209
|
+
// 'Checkpoint:'`), commits after EVERY phase (replicate-pipeline.md:134-137), and — since this same
|
|
210
|
+
// feature — a deterministic docs-completeness answer. The item's own verifier said the payoff of
|
|
211
|
+
// automation is "modest even post-demo" for a pipeline where a human sits at every checkpoint.
|
|
212
|
+
//
|
|
213
|
+
// So what shipped is the documentation of the three signals that already exist, plus the recorded
|
|
214
|
+
// decision NOT to add branching logic. This test holds that record: a section that says "we decided
|
|
215
|
+
// not to" is worth nothing if the next reader cannot find the reasoning.
|
|
216
|
+
|
|
217
|
+
const { test: resumeTest, describe: resumeDescribe } = require('node:test');
|
|
218
|
+
|
|
219
|
+
resumeDescribe('an interrupted run can be resumed from what already exists', () => {
|
|
220
|
+
resumeTest('P13 - the three existing resume signals are named, with their commands', () => {
|
|
221
|
+
const src = fs.readFileSync(path.join(TPL, 'commands', 'replicate.md'), 'utf-8');
|
|
222
|
+
const start = src.indexOf('### Прерванный прогон');
|
|
223
|
+
assert.ok(start > 0, 'replicate.md must tell a user how to resume');
|
|
224
|
+
const end = src.indexOf('\n### ', start + 1);
|
|
225
|
+
const sec = src.slice(start, end > start ? end : undefined);
|
|
226
|
+
|
|
227
|
+
assert.match(sec, /git log --oneline/, 'the per-phase commits are the phase marker');
|
|
228
|
+
assert.match(sec, /check-docs-complete\.cjs/, 'the deterministic completeness answer');
|
|
229
|
+
assert.match(sec, /p-replicator verify/, 'and the Phase-3 toolkit signal');
|
|
230
|
+
assert.match(sec, /продолжай с Фазы 3/,
|
|
231
|
+
'and a concrete sentence to say, not just a list of probes');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
resumeTest('P14 - the decision NOT to automate is recorded with its reasoning', () => {
|
|
235
|
+
// A skipped item leaves no trace unless the skip is written down. Without this the next reader
|
|
236
|
+
// re-derives the question from scratch, or builds the thing that was deliberately not built.
|
|
237
|
+
const src = fs.readFileSync(path.join(TPL, 'commands', 'replicate.md'), 'utf-8');
|
|
238
|
+
const start = src.indexOf('### Прерванный прогон');
|
|
239
|
+
const end = src.indexOf('\n### ', start + 1);
|
|
240
|
+
const sec = src.slice(start, end > start ? end : undefined);
|
|
241
|
+
|
|
242
|
+
assert.match(sec, /сознательно НЕ реализовано/, 'the decision must be stated');
|
|
243
|
+
assert.match(sec, /бэклог `58575b07`/, 'with the item it answers');
|
|
244
|
+
assert.match(sec, /свежая логика ветвления в\s*\n?интерактивном конвейере/,
|
|
245
|
+
'and the reason, so a future reader can disagree with the reason rather than guess it');
|
|
246
|
+
assert.match(sec, /начинайте с\s*\n?того, что перечисленного выше оказалось недостаточно/,
|
|
247
|
+
'and the condition under which revisiting it is warranted');
|
|
248
|
+
});
|
|
249
|
+
});
|