@dzhechkov/p-replicator 1.12.0 → 1.13.1

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 (106) hide show
  1. package/.dz-manifest.json +225 -61
  2. package/CHANGELOG.md +148 -1
  3. package/LICENSE +21 -0
  4. package/MULTIPLATFORM_ROADMAP.md +1 -1
  5. package/README/eng/01_quickstart.md +2 -2
  6. package/README/eng/02_user_guide.md +1 -1
  7. package/README/eng/03_admin_guide.md +2 -2
  8. package/README/eng/05_architecture.md +1 -1
  9. package/README/eng/README.md +2 -1
  10. package/README/ru/01_quickstart.md +2 -2
  11. package/README/ru/02_user_guide.md +1 -1
  12. package/README/ru/03_admin_guide.md +2 -2
  13. package/README/ru/05_architecture.md +1 -1
  14. package/README/ru/README.md +2 -1
  15. package/README/ru/html/index.html +8 -8
  16. package/README.md +132 -9
  17. package/bin/cli.js +0 -0
  18. package/package.json +10 -11
  19. package/sbom.json +470 -60
  20. package/scripts/check-pipeline-gaps.sh +0 -0
  21. package/src/commands/init.js +1 -1
  22. package/src/rule-components.json +5 -1
  23. package/src/utils.js +32 -3
  24. package/templates/.claude/agents/product-discoverer.md +38 -0
  25. package/templates/.claude/agents/replicate-coordinator.md +11 -1
  26. package/templates/.claude/commands/feature.md +29 -5
  27. package/templates/.claude/commands/go.md +6 -8
  28. package/templates/.claude/commands/harvest.md +5 -7
  29. package/templates/.claude/commands/replicate.md +169 -44
  30. package/templates/.claude/commands/start.md +28 -7
  31. package/templates/.claude/hooks/capture-source-path.cjs +795 -0
  32. package/templates/.claude/hooks/check-canon.cjs +493 -0
  33. package/templates/.claude/hooks/check-embed-contract.cjs +374 -0
  34. package/templates/.claude/hooks/check-external-deps.cjs +288 -0
  35. package/templates/.claude/hooks/check-file-ownership.cjs +424 -0
  36. package/templates/.claude/hooks/check-handoff-manifest.cjs +367 -0
  37. package/templates/.claude/hooks/check-job-contract.cjs +501 -0
  38. package/templates/.claude/hooks/check-look-origin.cjs +240 -0
  39. package/templates/.claude/hooks/check-look-trace.cjs +385 -0
  40. package/templates/.claude/hooks/check-metric-source.cjs +296 -0
  41. package/templates/.claude/hooks/check-model-cost.cjs +470 -0
  42. package/templates/.claude/hooks/check-ports.cjs +27 -6
  43. package/templates/.claude/hooks/check-source-version.cjs +312 -0
  44. package/templates/.claude/hooks/check-swarm-receipts.cjs +197 -0
  45. package/templates/.claude/hooks/check-webhook-contract.cjs +535 -0
  46. package/templates/.claude/hooks/statusline.cjs +2 -2
  47. package/templates/.claude/rules/embeddable-widget.md +73 -0
  48. package/templates/.claude/rules/feature-lifecycle.md +5 -6
  49. package/templates/.claude/rules/incoming-webhooks.md +99 -0
  50. package/templates/.claude/rules/long-running-job.md +73 -0
  51. package/templates/.claude/rules/model-call-cost.md +85 -0
  52. package/templates/.claude/rules/replicate-pipeline.md +121 -52
  53. package/templates/.claude/skills/brutal-honesty-review/SKILL.md +9 -0
  54. package/templates/.claude/skills/cc-toolkit-generator-enhanced/SKILL.md +4 -0
  55. package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/03-generate-p0.md +46 -1
  56. package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/04-generate-p1.md +7 -1
  57. package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/06-package-deliver.md +20 -2
  58. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/claude-md-strategy.md +7 -0
  59. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/automation-commands.md +17 -0
  60. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle-ent.md +43 -5
  61. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle.md +43 -7
  62. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/start-command.md +19 -1
  63. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/swarm-file-evidence.md +151 -0
  64. package/templates/.claude/skills/goap-research-ed25519/SKILL.md +37 -22
  65. package/templates/.claude/skills/goap-research-ed25519/references/negative-results.md +94 -0
  66. package/templates/.claude/skills/goap-research-ed25519/scripts/check_report_evidence.py +368 -4
  67. package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +122 -5
  68. package/templates/.claude/skills/goap-research-ed25519/scripts/evidence_fetch.py +33 -16
  69. package/templates/.claude/skills/goap-research-ed25519/scripts/quote_provenance.py +342 -0
  70. package/templates/.claude/skills/goap-research-ed25519/scripts/test_ed25519_verifier.py +60 -0
  71. package/templates/.claude/skills/goap-research-ed25519/scripts/test_evidence_provenance.py +139 -6
  72. package/templates/.claude/skills/goap-research-ed25519/scripts/test_quote_provenance.py +274 -0
  73. package/templates/.claude/skills/goap-research-ed25519/scripts/test_suite_completeness.py +2 -1
  74. package/templates/.claude/skills/knowledge-extractor/SKILL.md +4 -0
  75. package/templates/.claude/skills/pipeline-forge/SKILL.md +18 -23
  76. package/templates/.claude/skills/pipeline-forge/examples/replicate-analysis.md +7 -2
  77. package/templates/.claude/skills/pipeline-forge/references/patterns-catalog.md +19 -1
  78. package/templates/.claude/skills/pipeline-forge/references/self-extracted-patterns.md +17 -6
  79. package/templates/.claude/skills/pipeline-forge/references/skill-anatomy.md +0 -1
  80. package/templates/.claude/skills/reverse-engineering-unicorn/modules/025-cjm-prototype.md +21 -1
  81. package/templates/.claude/skills/sparc-prd-mini/SKILL.md +173 -725
  82. package/tests/snapshot/baseline.json +60 -38
  83. package/tests/unit/capture-source-path.test.js +492 -0
  84. package/tests/unit/check-canon.test.js +403 -0
  85. package/tests/unit/check-embed-contract.test.js +422 -0
  86. package/tests/unit/check-external-deps.test.js +363 -0
  87. package/tests/unit/check-file-ownership.test.js +388 -0
  88. package/tests/unit/check-handoff-manifest.test.js +410 -0
  89. package/tests/unit/check-job-contract.test.js +514 -0
  90. package/tests/unit/check-look-origin.test.js +180 -0
  91. package/tests/unit/check-look-trace.test.js +420 -0
  92. package/tests/unit/check-metric-source.test.js +325 -0
  93. package/tests/unit/check-model-cost.test.js +425 -0
  94. package/tests/unit/check-ports.test.js +46 -2
  95. package/tests/unit/check-source-version.test.js +344 -0
  96. package/tests/unit/check-swarm-receipts.test.js +231 -0
  97. package/tests/unit/check-webhook-contract.test.js +536 -0
  98. package/tests/unit/db-port-rule.test.js +8 -2
  99. package/tests/unit/detection-ladder-registry.test.js +2 -2
  100. package/tests/unit/generator-swarm-contract.test.js +287 -0
  101. package/tests/unit/guard-honest-input-meta.test.js +64 -0
  102. package/tests/unit/honest-failure-rules.test.js +91 -9
  103. package/tests/unit/look-phase-contract.test.js +231 -0
  104. package/tests/unit/negative-conclusion-gate.test.js +300 -0
  105. package/tests/unit/quote-provenance.test.js +122 -0
  106. package/tests/unit/utils.test.js +40 -3
@@ -0,0 +1,363 @@
1
+ 'use strict';
2
+
3
+ // The deterministic half of «инвентарь внешних зависимостей может ОТСУТСТВОВАТЬ». There is NO new
4
+ // always-loaded rule: the corpus budget is spent (measured 2026-09-01) and the owner refused to
5
+ // raise the threshold, so the doctrine lives in the checker's header, which is never loaded.
6
+ //
7
+ // WHAT IS ALREADY SHIPPED AND IS NOT RE-CREATED: the inventory (five columns), the three verdicts
8
+ // CONFIRMED / UNCONFIRMED / CONTRADICTED, the verbatim-quote requirement, the sixth validator lens
9
+ // and the Phase-2 output rows — all in skills/sparc-prd-mini/SKILL.md, pinned by
10
+ // tests/unit/external-dependency-check.test.js.
11
+ //
12
+ // THE MISSING THING: nobody ENFORCED it. The template states the rule in its own words — write
13
+ // literally "No external dependencies — this product calls no third-party service", because "an
14
+ // empty section and an absent section are indistinguishable" — and the only consumers were a prose
15
+ // lens read by a model and two output-table rows. Layer 3-4 both.
16
+ //
17
+ // THE VACUOUS TRUTH: the Phase-2 green line reads "no external dependency UNCONFIRMED or
18
+ // CONTRADICTED". Over an EMPTY SET that is true by itself. So the single outcome this file exists to
19
+ // make unreachable is a clean answer produced by absence — which is why an absent section is exit 2
20
+ // and can never be exit 0. P3 is that fixture; P16 re-measures the three green siblings.
21
+
22
+ const { test, describe } = require('node:test');
23
+ const assert = require('node:assert/strict');
24
+ const { spawnSync } = require('node:child_process');
25
+ const fs = require('node:fs');
26
+ const os = require('node:os');
27
+ const path = require('node:path');
28
+
29
+ const PKG = path.resolve(__dirname, '..', '..');
30
+ const TPL = path.join(PKG, 'templates', '.claude');
31
+ const CHECK = path.join(TPL, 'hooks', 'check-external-deps.cjs');
32
+ const ARCH = 'docs/Architecture.md';
33
+
34
+ const read = (rel) => fs.readFileSync(path.join(PKG, rel), 'utf8');
35
+
36
+ /** Build a throwaway project and run a REAL checker over it. */
37
+ function run(checker, files) {
38
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-extdeps-')));
39
+ try {
40
+ for (const [rel, body] of Object.entries(files || {})) {
41
+ const abs = path.join(dir, rel);
42
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
43
+ fs.writeFileSync(abs, body);
44
+ }
45
+ const r = spawnSync(process.execPath, [checker, dir], { encoding: 'utf8' });
46
+ return { code: r.status, out: (r.stdout || '') + (r.stderr || '') };
47
+ } finally { fs.rmSync(dir, { recursive: true, force: true }); }
48
+ }
49
+
50
+ const check = (files) => run(CHECK, files);
51
+
52
+ const GOOD_EVIDENCE = 'https://docs.example.test/email · checked 2026-08-30 · '
53
+ + '«The API reports hard and soft bounces via webhook»';
54
+
55
+ /** An Architecture.md whose inventory says what the case needs. */
56
+ function arch({ rows, before = '## Technology Stack\n\n| Layer | Technology | Rationale |\n|---|---|---|\n| Backend | Node | привычен команде |\n', section = true, prose = '' } = {}) {
57
+ const body = rows === undefined
58
+ ? [['отправка писем', 'Postmark', GOOD_EVIDENCE, 'CONFIRMED', 'FR-010']]
59
+ : rows;
60
+ let out = '# Architecture\n\n' + before + '\n';
61
+ if (section) {
62
+ out += '## External Dependencies\n\n' + (prose ? prose + '\n\n' : '');
63
+ if (body.length) {
64
+ out += '| Capability needed | Provider / API | Evidence | Verdict | Requirements relying on it |\n'
65
+ + '|---|---|---|---|---|\n'
66
+ + body.map((r) => '| ' + r.join(' | ') + ' |').join('\n') + '\n';
67
+ }
68
+ }
69
+ out += '\n## Data Architecture\n\nPostgres.\n';
70
+ return out;
71
+ }
72
+
73
+ describe('инвентарь внешних зависимостей — the deterministic half', () => {
74
+ test('P1 - a filled inventory whose rows carry verdicts is CLEAN', () => {
75
+ const r = check({ [ARCH]: arch() });
76
+ assert.equal(r.code, 0, r.out);
77
+ assert.match(r.out, /✅/);
78
+ // The boundary is part of the verdict: the checker cannot open the link.
79
+ assert.match(r.out, /НЕ ОТКРЫВАЕТ ссылку/, r.out);
80
+ });
81
+
82
+ test('P2 - no Architecture.md is «Phase 1 is unfinished», not «no dependencies»', () => {
83
+ const r = check({});
84
+ assert.equal(r.code, 2, r.out);
85
+ assert.match(r.out, /нет файла docs\/Architecture\.md/);
86
+ assert.doesNotMatch(r.out, /✅/);
87
+ });
88
+
89
+ test('P3 - THE fixture: an ABSENT section exits 2 with a named reason, never 0', () => {
90
+ const r = check({ [ARCH]: arch({ section: false }) });
91
+ assert.equal(r.code, 2, r.out);
92
+ assert.match(r.out, /нет раздела `## External Dependencies`/);
93
+ // The mechanism must be printed, or the code alone teaches nothing.
94
+ assert.match(r.out, /истинным САМО СОБОЙ|вакуумн/i, r.out);
95
+ assert.doesNotMatch(r.out, /✅/);
96
+ });
97
+
98
+ test('P4 - the LEGITIMATE «no dependencies» answer is a DIFFERENT fact and prints differently', () => {
99
+ // Absent section = the question was never asked.
100
+ // The sentence = the question was asked and answered.
101
+ // Both are exit 2; printing them identically would rebuild the confusion this file removes.
102
+ const declared = check({ [ARCH]: arch({
103
+ rows: [], prose: 'No external dependencies — this product calls no third-party service.',
104
+ }) });
105
+ assert.equal(declared.code, 2, declared.out);
106
+ assert.match(declared.out, /законный ответ/);
107
+ assert.match(declared.out, /вопрос БЫЛ задан/);
108
+
109
+ const absent = check({ [ARCH]: arch({ section: false }) });
110
+ assert.notEqual(declared.out, absent.out,
111
+ 'the two exit-2 reasons must not be printed the same way — they are opposite facts');
112
+ });
113
+
114
+ test('P5 - an EMPTY section is not the legitimate answer either', () => {
115
+ const r = check({ [ARCH]: arch({ rows: [] }) });
116
+ assert.equal(r.code, 2, r.out);
117
+ assert.match(r.out, /пуст/);
118
+ assert.match(r.out, /дословно/, 'and it must say what to write instead');
119
+ });
120
+
121
+ test('P6 - a section holding only the shipped TEMPLATE row is not filled in', () => {
122
+ const r = check({ [ARCH]: arch({ rows: [
123
+ ['[what the product needs it to DO]', '[service]', '[link] · checked [YYYY-MM-DD]', 'CONFIRMED', '[REQ ids]'],
124
+ ] }) });
125
+ assert.equal(r.code, 2, r.out);
126
+ assert.match(r.out, /шаблонная строка/);
127
+ });
128
+
129
+ test('P7 - a row with no verdict is a PROVEN defect, not an unknown', () => {
130
+ // The inventory's entire job is to carry a verdict; a row that carries none was written as if
131
+ // it had one, and that is exactly what makes the Phase-2 green line contentless.
132
+ const r = check({ [ARCH]: arch({ rows: [
133
+ ['отправка писем', 'Postmark', GOOD_EVIDENCE, '', 'FR-010'],
134
+ ] }) });
135
+ assert.equal(r.code, 1, r.out);
136
+ assert.match(r.out, /без вердикта/);
137
+ assert.match(r.out, /отправка писем/, 'the row must be NAMED');
138
+ });
139
+
140
+ test('P8 - an unrecognised verdict is refused with the closed three listed', () => {
141
+ const r = check({ [ARCH]: arch({ rows: [
142
+ ['отправка писем', 'Postmark', GOOD_EVIDENCE, 'PROBABLY', 'FR-010'],
143
+ ] }) });
144
+ assert.equal(r.code, 1, r.out);
145
+ assert.match(r.out, /CONFIRMED \| UNCONFIRMED \| CONTRADICTED/);
146
+ });
147
+
148
+ test('P9 - CONTRADICTED is a proven defect and names the requirements resting on it', () => {
149
+ const r = check({ [ARCH]: arch({ rows: [
150
+ ['вебхук о возвратах', 'Postmark', GOOD_EVIDENCE, 'CONTRADICTED', 'FR-011'],
151
+ ] }) });
152
+ assert.equal(r.code, 1, r.out);
153
+ assert.match(r.out, /CONTRADICTED/);
154
+ assert.match(r.out, /FR-011/, 'the dependent requirements must be named, not counted');
155
+ });
156
+
157
+ test('P10 - THE second fixture: CONFIRMED without a verbatim quote fails, WITH THE ROW NAMED', () => {
158
+ const r = check({ [ARCH]: arch({ rows: [
159
+ ['отправка писем', 'Postmark', 'https://docs.example.test/email · checked 2026-08-30', 'CONFIRMED', 'FR-010'],
160
+ ] }) });
161
+ assert.equal(r.code, 1, r.out);
162
+ assert.match(r.out, /без ДОСЛОВНОЙ ЦИТАТЫ/);
163
+ assert.match(r.out, /отправка писем/);
164
+ // The reason: a plausible link is the cheapest possible forgery.
165
+ assert.match(r.out, /самая дешёвая подделка/);
166
+ });
167
+
168
+ test('P11 - a missing link and a missing date are reported APART from the quote', () => {
169
+ // Three different repairs — «find the page», «say when you looked», «quote the sentence» — so
170
+ // collapsing them into one message would name the wrong fix two times out of three.
171
+ const noLink = check({ [ARCH]: arch({ rows: [
172
+ ['отправка писем', 'Postmark', 'checked 2026-08-30 · «The API reports hard and soft bounces»', 'CONFIRMED', 'FR-010'],
173
+ ] }) });
174
+ assert.equal(noLink.code, 1, noLink.out);
175
+ assert.match(noLink.out, /без ссылки/);
176
+
177
+ const noDate = check({ [ARCH]: arch({ rows: [
178
+ ['отправка писем', 'Postmark', 'https://docs.example.test/email · «The API reports hard and soft bounces»', 'CONFIRMED', 'FR-010'],
179
+ ] }) });
180
+ assert.equal(noDate.code, 1, noDate.out);
181
+ assert.match(noDate.out, /без даты проверки/);
182
+ assert.match(noDate.out, /ДРЕЙФУЕТ/, 'and it must say WHY a date matters');
183
+ });
184
+
185
+ test('P12 - UNCONFIRMED passes, and its requirements are NAMED as blocked from Phase 3', () => {
186
+ // The middle verdict is the honest state of a project on a machine with no web access. It must
187
+ // not fail — and it must not pass silently either, or external feasibility never has to be
188
+ // established at all while the gate looks mandatory on paper.
189
+ const r = check({ [ARCH]: arch({ rows: [
190
+ ['отправка писем', 'Postmark', GOOD_EVIDENCE, 'CONFIRMED', 'FR-010'],
191
+ ['отчёт о возвратах', 'Postmark', 'нет доступа к сети', 'UNCONFIRMED', 'FR-011'],
192
+ ] }) });
193
+ assert.equal(r.code, 0, r.out);
194
+ assert.match(r.out, /1 CONFIRMED, 1 UNCONFIRMED/);
195
+ assert.match(r.out, /НЕ входят в/);
196
+ assert.match(r.out, /FR-011/);
197
+ });
198
+
199
+ test('P13 - the Technology Stack table above is NOT read as inventory rows', () => {
200
+ // Read per section, never globally: the neighbouring table has three columns and no verdict, so
201
+ // a global scan would report a defect that is not there — an answer to a neighbouring question.
202
+ const r = check({ [ARCH]: arch() });
203
+ assert.equal(r.code, 0, r.out);
204
+ assert.match(r.out, /1 способност/, r.out);
205
+ });
206
+
207
+ test('P14 - a duplicated capability/provider pair stops the check', () => {
208
+ const r = check({ [ARCH]: arch({ rows: [
209
+ ['отправка писем', 'Postmark', GOOD_EVIDENCE, 'CONFIRMED', 'FR-010'],
210
+ ['отправка писем', 'Postmark', GOOD_EVIDENCE, 'UNCONFIRMED', 'FR-012'],
211
+ ] }) });
212
+ assert.equal(r.code, 2, r.out);
213
+ assert.match(r.out, /повторяются строки/);
214
+ });
215
+
216
+ test('P15 - no non-zero exit ever prints the clean marker', () => {
217
+ const cases = [
218
+ {},
219
+ { [ARCH]: arch({ section: false }) },
220
+ { [ARCH]: arch({ rows: [] }) },
221
+ { [ARCH]: arch({ rows: [['x', 'y', GOOD_EVIDENCE, '', 'FR-1']] }) },
222
+ { [ARCH]: arch({ rows: [['x', 'y', GOOD_EVIDENCE, 'CONTRADICTED', 'FR-1']] }) },
223
+ ];
224
+ for (const files of cases) {
225
+ const r = check(files);
226
+ assert.notEqual(r.code, 0, JSON.stringify(files));
227
+ assert.doesNotMatch(r.out, /✅/, r.out);
228
+ }
229
+ });
230
+
231
+ test('P16 - MEASURED: the three existing guards are all green over the missing inventory', () => {
232
+ // The justification, re-measured rather than quoted: a tree with the full Phase-1 set whose
233
+ // Architecture.md has NO inventory section. The siblings are not broken — nothing was asking
234
+ // this question.
235
+ const files = {
236
+ [ARCH]: arch({ section: false }),
237
+ 'docs/product-discovery-brief.md': [
238
+ '# Brief', '', '| Требование | Ось | Статус |', '|---|---|---|',
239
+ '| FR-GROWTH-001 | канал | ACCEPTED |', '',
240
+ ].join('\n'),
241
+ 'docs/source-product-profile.md': [
242
+ '# Source Product Profile', '', '**Статус съёмки:** СНЯТ', '',
243
+ '| Требование | Ось | Наблюдение | Источник | Статус |', '|---|---|---|---|---|',
244
+ '| FR-LOOK-001 | облик | палитра | https://example.test | СНЯТ |', '',
245
+ ].join('\n'),
246
+ 'docs/Specification.md': '# Specification\n\nFR-GROWTH-001 — реферальная программа.\n\nFR-LOOK-001 — палитра.\n',
247
+ 'docs/PRD.md': '# PRD\n\nПродукт для занятых людей, рассылающий еженедельный дайджест по почте.\n',
248
+ 'docs/Pseudocode.md': '# Pseudocode\n\nАлгоритм сборки дайджеста описан подробно и полно ниже.\n',
249
+ };
250
+ const growth = run(path.join(TPL, 'hooks', 'check-growth-trace.cjs'), files);
251
+ const look = run(path.join(TPL, 'hooks', 'check-look-trace.cjs'), files);
252
+ for (const [name, r] of [['growth', growth], ['look', look]]) {
253
+ assert.doesNotMatch(r.out, /External Dependencies/,
254
+ name + ' would have to name the missing inventory for this feature to be redundant: ' + r.out);
255
+ }
256
+ const mine = check(files);
257
+ assert.equal(mine.code, 2, mine.out);
258
+ assert.match(mine.out, /External Dependencies/, 'and this checker names it');
259
+ });
260
+ });
261
+
262
+ /** The seam's FORCE, separately from its vocabulary. */
263
+ function seamForceProblems(text) {
264
+ const problems = [];
265
+ if (!/Инвентарь внешних зависимостей ОБЯЗАН СУЩЕСТВОВАТЬ/.test(text)) {
266
+ problems.push('the existence duty is advisory, not imperative');
267
+ }
268
+ if (!/отсутствующий раздел — это НЕ «зависимостей\s*\n?нет»/.test(text)) {
269
+ problems.push('an absent section may read as «no dependencies» again — that IS the defect');
270
+ }
271
+ if (!/истинен САМ СОБОЙ/.test(text)) {
272
+ problems.push('the vacuous-truth mechanism is gone — without it the duty reads as paperwork');
273
+ }
274
+ if (!/node \.claude\/hooks\/check-external-deps\.cjs \./.test(text)) {
275
+ problems.push('the deterministic half is not invoked at the seam');
276
+ }
277
+ return problems;
278
+ }
279
+
280
+ describe('the seam still mandates, and every counter agrees', () => {
281
+ test('P17 - Phase 2 calls it as an acceptance criterion, and softening it fires', () => {
282
+ const rel = 'templates/.claude/commands/replicate.md';
283
+ const source = read(rel);
284
+ assert.deepEqual(seamForceProblems(source), [], rel);
285
+ // Placement matters: a check called after the swarm guards nothing this swarm would do.
286
+ const step = source.indexOf('Шаг 2.0');
287
+ const call = source.indexOf('check-external-deps.cjs');
288
+ assert.ok(step > 0 && call > step && call - step < 1200,
289
+ 'the call must live in Шаг 2.0, before the validation swarm launches');
290
+
291
+ const softened = source.replace('Инвентарь внешних зависимостей ОБЯЗАН СУЩЕСТВОВАТЬ',
292
+ 'Инвентарь внешних зависимостей желателен');
293
+ assert.notEqual(softened, source, 'mutation fixture did not apply — the sentence moved');
294
+ assert.deepEqual(seamForceProblems(softened), ['the existence duty is advisory, not imperative'],
295
+ 'softening the duty must fire EXACTLY the mandate predicate');
296
+
297
+ const noMechanism = source.replace('истинен САМ СОБОЙ', 'выглядит убедительно');
298
+ assert.ok(seamForceProblems(noMechanism).some((p) => p.includes('vacuous')), rel);
299
+ });
300
+
301
+ test('P18 - it is a hooks component wired to NO event, and every counter agrees', () => {
302
+ const { COMPONENTS } = require(path.join(PKG, 'src', 'utils.js'));
303
+ assert.ok(COMPONENTS.hooks.items['check-external-deps'],
304
+ 'it must be registered, or init/doctor/verify will not know it');
305
+ const settings = read('templates/.claude/settings.json');
306
+ assert.ok(!settings.includes('check-external-deps'),
307
+ 'it must not be wired to an event: this package\'s hooks are non-blocking by contract');
308
+
309
+ const statusline = read('templates/.claude/hooks/statusline.cjs');
310
+ const hooks = statusline.match(/hooksExpected:\s*(\d+)/);
311
+ assert.ok(hooks, 'statusline must declare hooksExpected');
312
+ const onDisk = fs.readdirSync(path.join(TPL, 'hooks')).filter((f) => f.endsWith('.cjs'));
313
+ assert.equal(Number(hooks[1]), Object.keys(COMPONENTS.hooks.items).length,
314
+ 'the status line would report a phantom missing hook: ' + hooks[1]);
315
+ assert.equal(onDisk.length, Object.keys(COMPONENTS.hooks.items).length,
316
+ 'the shipped directory and the declared contract disagree: ' + onDisk.join(', '));
317
+
318
+ const rule = read('templates/.claude/rules/replicate-pipeline.md');
319
+ assert.match(rule, /check-external-deps\.cjs/, 'the hook inventory must list it');
320
+ const inventory = rule.match(/\*\*Hooks \((\d+) files/);
321
+ assert.ok(inventory, 'the rule must state the hook count it is inventorying');
322
+ assert.equal(Number(inventory[1]), onDisk.length,
323
+ 'the rule prose and the shipped directory disagree — the exact split this family exists for');
324
+ });
325
+
326
+ test('P19 - the shipped rule it enforces is still there, and is not re-created here', () => {
327
+ // The boundary the backlog record draws: the inventory, the three verdicts and the quote
328
+ // requirement are ALREADY shipped. This feature adds enforcement and nothing else. If the rule
329
+ // were quietly rewritten here, the two copies would disagree and each would believe itself
330
+ // authoritative — which is worse than having one.
331
+ const skill = read('templates/.claude/skills/sparc-prd-mini/SKILL.md');
332
+ assert.match(skill, /No external dependencies —\s+this product calls no third-party service/,
333
+ 'the prescribed sentence must stay the SKILL\'s, and the checker must match it, not replace it');
334
+ assert.match(skill, /an empty section and an absent section are\s*\n?indistinguishable/i,
335
+ 'the reason the sentence exists must stay where the author reads it');
336
+ for (const verdict of ['CONFIRMED', 'UNCONFIRMED', 'CONTRADICTED']) {
337
+ assert.ok(skill.includes(verdict), 'the closed three must stay in the shipped rule: ' + verdict);
338
+ }
339
+ });
340
+
341
+ test('P20 - the mutation registry carries this feature\'s guards', () => {
342
+ const registry = JSON.parse(read('test/mutation-registry.json'));
343
+ const expected = {
344
+ 'extdeps-absent-section-is-never-clean': 'templates/.claude/hooks/check-external-deps.cjs',
345
+ 'extdeps-confirmed-needs-a-verbatim-quote': 'templates/.claude/hooks/check-external-deps.cjs',
346
+ 'extdeps-row-without-a-verdict-is-proven': 'templates/.claude/hooks/check-external-deps.cjs',
347
+ 'extdeps-seam-existence-is-mandatory': 'templates/.claude/commands/replicate.md',
348
+ };
349
+ for (const [id, file] of Object.entries(expected)) {
350
+ const entry = registry.entries.find((e) => e.id === id);
351
+ assert.ok(entry, 'missing targeted mutation id ' + id);
352
+ assert.equal(entry.file, file, id + ' mutates the wrong surface');
353
+ assert.ok(entry.property && entry.property.length > 40,
354
+ id + ' needs a behavioral property, not a cosmetic label');
355
+ assert.ok(entry.mutation && entry.mutation.find && entry.mutation.replace,
356
+ id + ' needs a real source mutation');
357
+ assert.ok(Number.isInteger(entry.minFailing) && entry.minFailing >= 1,
358
+ id + ' must require at least one failing test');
359
+ assert.equal(read(entry.file).split(entry.mutation.find).length - 1, 1,
360
+ id + ': mutation anchor must occur exactly once');
361
+ }
362
+ });
363
+ });