@dzhechkov/p-replicator 1.10.4 → 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 (136) hide show
  1. package/.dz-manifest.json +312 -76
  2. package/CHANGELOG.md +232 -0
  3. package/MULTIPLATFORM_ROADMAP.md +1 -1
  4. package/README/eng/01_quickstart.md +3 -3
  5. package/README/eng/02_user_guide.md +1 -1
  6. package/README/eng/03_admin_guide.md +2 -2
  7. package/README/eng/04_api_reference.md +11 -5
  8. package/README/eng/05_architecture.md +1 -1
  9. package/README/eng/README.md +2 -1
  10. package/README/ru/01_quickstart.md +3 -3
  11. package/README/ru/02_user_guide.md +1 -1
  12. package/README/ru/03_admin_guide.md +2 -2
  13. package/README/ru/04_api_reference.md +11 -5
  14. package/README/ru/05_architecture.md +1 -1
  15. package/README/ru/README.md +2 -1
  16. package/README/ru/html/index.html +9 -9
  17. package/README.md +278 -39
  18. package/package.json +5 -4
  19. package/sbom.json +665 -75
  20. package/scripts/check-pipeline-gaps.sh +413 -0
  21. package/src/commands/doctor.js +94 -4
  22. package/src/commands/init.js +1 -1
  23. package/src/rule-components.json +15 -0
  24. package/src/utils.js +35 -11
  25. package/templates/.claude/agents/harvest-coordinator.md +10 -1
  26. package/templates/.claude/agents/product-discoverer.md +38 -0
  27. package/templates/.claude/agents/replicate-coordinator.md +11 -1
  28. package/templates/.claude/commands/feature.md +81 -9
  29. package/templates/.claude/commands/go.md +9 -0
  30. package/templates/.claude/commands/harvest.md +39 -3
  31. package/templates/.claude/commands/myinsights.md +21 -26
  32. package/templates/.claude/commands/replicate.md +171 -37
  33. package/templates/.claude/commands/start.md +29 -0
  34. package/templates/.claude/hooks/capture-source-path.cjs +795 -0
  35. package/templates/.claude/hooks/check-canon.cjs +493 -0
  36. package/templates/.claude/hooks/check-embed-contract.cjs +374 -0
  37. package/templates/.claude/hooks/check-external-deps.cjs +288 -0
  38. package/templates/.claude/hooks/check-file-ownership.cjs +424 -0
  39. package/templates/.claude/hooks/check-handoff-manifest.cjs +367 -0
  40. package/templates/.claude/hooks/check-job-contract.cjs +501 -0
  41. package/templates/.claude/hooks/check-look-origin.cjs +240 -0
  42. package/templates/.claude/hooks/check-look-trace.cjs +385 -0
  43. package/templates/.claude/hooks/check-metric-source.cjs +296 -0
  44. package/templates/.claude/hooks/check-model-cost.cjs +470 -0
  45. package/templates/.claude/hooks/check-ports.cjs +434 -24
  46. package/templates/.claude/hooks/check-source-version.cjs +312 -0
  47. package/templates/.claude/hooks/check-swarm-receipts.cjs +197 -0
  48. package/templates/.claude/hooks/check-webhook-contract.cjs +535 -0
  49. package/templates/.claude/hooks/session-insights.cjs +158 -25
  50. package/templates/.claude/hooks/statusline.cjs +2 -2
  51. package/templates/.claude/hooks/write-insight.cjs +253 -0
  52. package/templates/.claude/rules/cost-of-detection-ladder.md +96 -0
  53. package/templates/.claude/rules/docker-ports.md +41 -19
  54. package/templates/.claude/rules/embeddable-widget.md +73 -0
  55. package/templates/.claude/rules/feature-lifecycle.md +13 -3
  56. package/templates/.claude/rules/honest-configuration.md +54 -0
  57. package/templates/.claude/rules/incoming-webhooks.md +99 -0
  58. package/templates/.claude/rules/insights-capture.md +10 -5
  59. package/templates/.claude/rules/long-running-job.md +73 -0
  60. package/templates/.claude/rules/model-call-cost.md +85 -0
  61. package/templates/.claude/rules/replicate-pipeline.md +123 -52
  62. package/templates/.claude/rules/skill-interface-protocol.md +1 -0
  63. package/templates/.claude/rules/swarm-file-evidence.md +46 -0
  64. package/templates/.claude/settings.json +13 -1
  65. package/templates/.claude/skills/brutal-honesty-review/SKILL.md +9 -0
  66. package/templates/.claude/skills/cc-toolkit-generator-enhanced/SKILL.md +4 -0
  67. package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/03-generate-p0.md +46 -1
  68. package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/04-generate-p1.md +7 -1
  69. package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/06-package-deliver.md +20 -2
  70. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/claude-md-strategy.md +7 -0
  71. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/automation-commands.md +17 -0
  72. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle-ent.md +43 -5
  73. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle.md +43 -7
  74. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/start-command.md +19 -1
  75. package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/swarm-file-evidence.md +151 -0
  76. package/templates/.claude/skills/goap-research-ed25519/SKILL.md +37 -22
  77. package/templates/.claude/skills/goap-research-ed25519/references/negative-results.md +94 -0
  78. package/templates/.claude/skills/goap-research-ed25519/scripts/check_report_evidence.py +368 -4
  79. package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +122 -5
  80. package/templates/.claude/skills/goap-research-ed25519/scripts/evidence_fetch.py +33 -16
  81. package/templates/.claude/skills/goap-research-ed25519/scripts/quote_provenance.py +342 -0
  82. package/templates/.claude/skills/goap-research-ed25519/scripts/test_ed25519_verifier.py +60 -0
  83. package/templates/.claude/skills/goap-research-ed25519/scripts/test_evidence_provenance.py +139 -6
  84. package/templates/.claude/skills/goap-research-ed25519/scripts/test_quote_provenance.py +274 -0
  85. package/templates/.claude/skills/goap-research-ed25519/scripts/test_suite_completeness.py +2 -1
  86. package/templates/.claude/skills/knowledge-extractor/SKILL.md +4 -0
  87. package/templates/.claude/skills/knowledge-extractor/modules/01-agent-review.md +16 -5
  88. package/templates/.claude/skills/pipeline-forge/SKILL.md +18 -23
  89. package/templates/.claude/skills/pipeline-forge/examples/replicate-analysis.md +7 -2
  90. package/templates/.claude/skills/pipeline-forge/references/patterns-catalog.md +19 -1
  91. package/templates/.claude/skills/pipeline-forge/references/self-extracted-patterns.md +17 -6
  92. package/templates/.claude/skills/pipeline-forge/references/skill-anatomy.md +0 -1
  93. package/templates/.claude/skills/reverse-engineering-unicorn/modules/025-cjm-prototype.md +21 -1
  94. package/templates/.claude/skills/sparc-prd-mini/SKILL.md +234 -716
  95. package/tests/e2e/lifecycle.test.js +55 -9
  96. package/tests/e2e/packed-insights-writer.test.js +308 -0
  97. package/tests/fixtures/prep-traceability-fixture/docs/features/order-refund/01_specification.md +29 -0
  98. package/tests/fixtures/prep-traceability-fixture/docs/features/order-refund/02_pseudocode.md +57 -0
  99. package/tests/snapshot/baseline.json +72 -46
  100. package/tests/snapshot/templates.test.js +47 -0
  101. package/tests/unit/absence-is-not-emptiness.test.js +15 -1
  102. package/tests/unit/capture-source-path.test.js +492 -0
  103. package/tests/unit/check-canon.test.js +403 -0
  104. package/tests/unit/check-embed-contract.test.js +422 -0
  105. package/tests/unit/check-external-deps.test.js +363 -0
  106. package/tests/unit/check-file-ownership.test.js +388 -0
  107. package/tests/unit/check-handoff-manifest.test.js +410 -0
  108. package/tests/unit/check-job-contract.test.js +514 -0
  109. package/tests/unit/check-look-origin.test.js +180 -0
  110. package/tests/unit/check-look-trace.test.js +420 -0
  111. package/tests/unit/check-metric-source.test.js +325 -0
  112. package/tests/unit/check-model-cost.test.js +425 -0
  113. package/tests/unit/check-pipeline-gaps.test.js +94 -0
  114. package/tests/unit/check-ports.test.js +773 -2
  115. package/tests/unit/check-source-version.test.js +344 -0
  116. package/tests/unit/check-swarm-receipts.test.js +231 -0
  117. package/tests/unit/check-webhook-contract.test.js +536 -0
  118. package/tests/unit/db-port-rule.test.js +43 -6
  119. package/tests/unit/detection-ladder-contract.test.js +302 -0
  120. package/tests/unit/detection-ladder-registry.test.js +52 -0
  121. package/tests/unit/doctor-insight-flow.test.js +315 -0
  122. package/tests/unit/external-dependency-check.test.js +19 -19
  123. package/tests/unit/generator-swarm-contract.test.js +287 -0
  124. package/tests/unit/guard-honest-input-meta.test.js +64 -0
  125. package/tests/unit/honest-failure-rules.test.js +574 -0
  126. package/tests/unit/hooks-project-anchored.test.js +67 -3
  127. package/tests/unit/insights-docs-tell-the-truth.test.js +52 -31
  128. package/tests/unit/insights-dz-delegation.test.js +197 -0
  129. package/tests/unit/insights-writer.test.js +285 -0
  130. package/tests/unit/look-phase-contract.test.js +231 -0
  131. package/tests/unit/negative-conclusion-gate.test.js +300 -0
  132. package/tests/unit/quote-provenance.test.js +122 -0
  133. package/tests/unit/shipped-suite-context.test.js +3 -1
  134. package/tests/unit/traceability-machine-ids.test.js +413 -0
  135. package/tests/unit/traceability-negative-fixture.test.js +322 -0
  136. package/tests/unit/utils.test.js +40 -2
@@ -0,0 +1,325 @@
1
+ 'use strict';
2
+
3
+ // The deterministic half of «метрика не называет источник значения». There is NO new always-loaded
4
+ // rule; the fourth column and one paragraph live in the SKILL that already owns the metrics table,
5
+ // and the long doctrine lives in the checker's header, which the corpus measure does not count.
6
+ //
7
+ // THE DEFECT, measured on this package's own templates. The Success Metrics table has three columns
8
+ // — Metric, Target, Timeline — and no column for where the number comes from. The completion
9
+ // checklist asks only whether «метрики успеха измеримы», i.e. whether a NUMBER is present. And the
10
+ // single validator lens that looks OUTSIDE the documents reads exactly one thing, Architecture.md →
11
+ // ## External Dependencies, so a metric in the PRD never reaches it: there is no route at all.
12
+ //
13
+ // MECHANISM: measurability is decided by the SHAPE OF A NUMBER, never by the system's ability to
14
+ // OBTAIN it. «Доля дошедших до публикации отзыва — 40% — неделя 1» passes completeness,
15
+ // measurability and consistency on a platform whose API exposes no reviews. P14 rebuilds that tree
16
+ // and re-measures the seven shipped guards rather than quoting the claim.
17
+ //
18
+ // WHY THE FOURTH COLUMN IS A CLOSED LIST: an open Source column collects «из аналитики», which names
19
+ // a genre of place rather than a place — the same defect as a spend ceiling named «разумный» or
20
+ // evidence that is a URL nobody opened.
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-metric-source.cjs');
32
+ const PRD = 'docs/PRD.md';
33
+ const ARCH = 'docs/Architecture.md';
34
+
35
+ const read = (rel) => fs.readFileSync(path.join(PKG, rel), 'utf8');
36
+
37
+ /** Build a throwaway project and run a REAL checker over it. */
38
+ function run(checker, files) {
39
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-metric-')));
40
+ try {
41
+ for (const [rel, body] of Object.entries(files || {})) {
42
+ const abs = path.join(dir, rel);
43
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
44
+ fs.writeFileSync(abs, body);
45
+ }
46
+ const r = spawnSync(process.execPath, [checker, dir], { encoding: 'utf8' });
47
+ return { code: r.status, out: (r.stdout || '') + (r.stderr || '') };
48
+ } finally { fs.rmSync(dir, { recursive: true, force: true }); }
49
+ }
50
+
51
+ const check = (files) => run(CHECK, files);
52
+
53
+ /** A PRD whose Success Metrics table says what the case needs. `columns: 3` drops the fourth. */
54
+ function prd({ rows, columns = 4, heading = true } = {}) {
55
+ const body = rows === undefined
56
+ ? [['доля активных на 7-й день', '35%', 'неделя 4', 'наш журнал']]
57
+ : rows;
58
+ const head = columns === 4
59
+ ? '| Metric | Target | Timeline | Источник значения |\n|---|---|---|---|\n'
60
+ : '| Metric | Target | Timeline |\n|---|---|---|\n';
61
+ return '# PRD\n\n## Key Features\n\nДайджест.\n\n'
62
+ + (heading ? '## Success Metrics\n' + head
63
+ + body.map((r) => '| ' + r.slice(0, columns).join(' | ') + ' |').join('\n') + '\n\n' : '')
64
+ + '## Timeline & Phases\n| Phase | Features | Timeline |\n|---|---|---|\n| MVP | дайджест | неделя 4 |\n';
65
+ }
66
+
67
+ /** An Architecture.md whose inventory does or does not carry the named method. */
68
+ function arch({ methods = [] } = {}) {
69
+ return '# Architecture\n\n## External Dependencies\n\n'
70
+ + '| Capability needed | Provider / API | Evidence | Verdict | Requirements relying on it |\n'
71
+ + '|---|---|---|---|---|\n'
72
+ + methods.map((m) => '| ' + m + ' | Example | https://docs.example.test · checked 2026-08-30 · '
73
+ + '«the endpoint returns published reviews» | CONFIRMED | FR-042 |').join('\n')
74
+ + '\n\n## Data Architecture\n\nPostgres.\n';
75
+ }
76
+
77
+ describe('источник значения метрики — the deterministic half', () => {
78
+ test('P1 - metrics naming a source from the closed list are CLEAN', () => {
79
+ const r = check({ [PRD]: prd() });
80
+ assert.equal(r.code, 0, r.out);
81
+ assert.match(r.out, /✅/);
82
+ assert.match(r.out, /НЕ ЗОВЁТ API/, r.out);
83
+ });
84
+
85
+ test('P2 - no Success Metrics section anywhere is NOT clean', () => {
86
+ const r = check({ [PRD]: prd({ heading: false }) });
87
+ assert.equal(r.code, 2, r.out);
88
+ assert.match(r.out, /нет раздела `## Success Metrics`/);
89
+ assert.doesNotMatch(r.out, /✅/);
90
+ });
91
+
92
+ test('P3 - THE core defect: a table with no fourth column at all', () => {
93
+ // Reported as its own case, because the repair is a COLUMN, not a value — telling an author to
94
+ // fill a cell that does not exist is the kind of message people ignore.
95
+ const r = check({ [PRD]: prd({ columns: 3 }) });
96
+ assert.equal(r.code, 1, r.out);
97
+ assert.match(r.out, /нет колонки «Источник значения»/);
98
+ assert.match(r.out, /доля активных на 7-й день/, 'the metric must be NAMED');
99
+ // The mechanism, in words: the shape of a number, not the ability to obtain it.
100
+ assert.match(r.out, /формой ЧИСЛА/);
101
+ });
102
+
103
+ test('P4 - an empty source cell is a blocker, not a note', () => {
104
+ const r = check({ [PRD]: prd({ rows: [['доля отзывов', '40%', 'неделя 1', '']] }) });
105
+ assert.equal(r.code, 1, r.out);
106
+ assert.match(r.out, /источник значения не назван/);
107
+ assert.match(r.out, /не задавали/);
108
+ });
109
+
110
+ test('P5 - «из аналитики» is a GENRE of place, not a place', () => {
111
+ const r = check({ [PRD]: prd({ rows: [['доля отзывов', '40%', 'неделя 1', 'из аналитики']] }) });
112
+ assert.equal(r.code, 1, r.out);
113
+ assert.match(r.out, /вне закрытого списка/);
114
+ assert.match(r.out, /жанр места/);
115
+ // The kinship is stated, because it is what makes the closed list obviously right.
116
+ assert.match(r.out, /разумный|не открывал/);
117
+ });
118
+
119
+ test('P6 - an external API must name a METHOD, and manual measurement must say HOW', () => {
120
+ // A vendor is not a capability: the same service has one and lacks its neighbour.
121
+ const noMethod = check({
122
+ [PRD]: prd({ rows: [['доля отзывов', '40%', 'неделя 1', 'внешний API']] }),
123
+ [ARCH]: arch({ methods: ['reviews.publish'] }),
124
+ });
125
+ assert.equal(noMethod.code, 1, noMethod.out);
126
+ assert.match(noMethod.out, /не сказано ЧЕМ именно/);
127
+ assert.match(noMethod.out, /МЕТОД/);
128
+
129
+ const bareManual = check({ [PRD]: prd({ rows: [['NPS', '40', 'неделя 8', 'ручное измерение']] }) });
130
+ assert.equal(bareManual.code, 1, bareManual.out);
131
+ assert.match(bareManual.out, /не сказано ЧЕМ именно/);
132
+ });
133
+
134
+ test('P7 - manual measurement, NAMED, is a legitimate answer and passes', () => {
135
+ // Refusing it would push people to dress a hand count as instrumentation, which is worse than
136
+ // the hand count. What is refused is manual measurement PRESENTED AS instrumented.
137
+ const r = check({ [PRD]: prd({
138
+ rows: [['NPS', '40', 'неделя 8', 'ручное измерение: сверка пяти карточек глазами']],
139
+ }) });
140
+ assert.equal(r.code, 0, r.out);
141
+ assert.match(r.out, /вручную 1/);
142
+ });
143
+
144
+ test('P8 - THE mandated fixture: an external-API metric with NO inventory row fails BY NAME', () => {
145
+ const r = check({
146
+ [PRD]: prd({ rows: [['доля дошедших до публикации отзыва', '40%', 'неделя 1',
147
+ 'внешний API: reviews.publish']] }),
148
+ [ARCH]: arch({ methods: ['email.send'] }),
149
+ });
150
+ assert.equal(r.code, 1, r.out);
151
+ assert.match(r.out, /нет в инвентаре внешних зависимостей/);
152
+ assert.match(r.out, /доля дошедших до публикации отзыва/, 'the metric must be NAMED');
153
+ assert.match(r.out, /шестую линзу|линзу/);
154
+ });
155
+
156
+ test('P9 - and the SAME metric with its inventory row passes', () => {
157
+ const r = check({
158
+ [PRD]: prd({ rows: [['доля дошедших до публикации отзыва', '40%', 'неделя 1',
159
+ 'внешний API: reviews.publish']] }),
160
+ [ARCH]: arch({ methods: ['reviews.publish'] }),
161
+ });
162
+ assert.equal(r.code, 0, r.out);
163
+ assert.match(r.out, /строку инвентаря/);
164
+ });
165
+
166
+ test('P10 - no inventory section at all is reported with its OWN reason', () => {
167
+ // Two different repairs — «write the inventory» vs «add the missing row» — so the message must
168
+ // differ. Merging them would name the wrong fix half the time.
169
+ const noSection = check({
170
+ [PRD]: prd({ rows: [['доля отзывов', '40%', 'неделя 1', 'внешний API: reviews.publish']] }),
171
+ [ARCH]: '# Architecture\n\n## Data Architecture\n\nPostgres.\n',
172
+ });
173
+ assert.equal(noSection.code, 1, noSection.out);
174
+ assert.match(noSection.out, /нет раздела `## External Dependencies` вовсе/);
175
+
176
+ const wrongRow = check({
177
+ [PRD]: prd({ rows: [['доля отзывов', '40%', 'неделя 1', 'внешний API: reviews.publish']] }),
178
+ [ARCH]: arch({ methods: ['email.send'] }),
179
+ });
180
+ assert.match(wrongRow.out, /инвентарь есть, но названного метода в нём нет/);
181
+ assert.notEqual(noSection.out, wrongRow.out, 'two different repairs must not print the same way');
182
+ });
183
+
184
+ test('P11 - the Timeline & Phases table below is NOT read as metrics', () => {
185
+ // Read per section, never globally: the neighbouring table has exactly three columns and the
186
+ // same shape, so a global scan would report every phase as a metric with no source.
187
+ const r = check({ [PRD]: prd() });
188
+ assert.equal(r.code, 0, r.out);
189
+ assert.match(r.out, /1 метрик/, r.out);
190
+ });
191
+
192
+ test('P12 - a template row does not count, and duplicates stop the check', () => {
193
+ const template = check({ [PRD]: prd({ rows: [['[metric]', '[target]', '[timeline]', '[источник]']] }) });
194
+ assert.equal(template.code, 2, template.out);
195
+ assert.match(template.out, /шаблонные строки/);
196
+
197
+ const dupe = check({ [PRD]: prd({ rows: [
198
+ ['доля активных', '35%', 'неделя 4', 'наш журнал'],
199
+ ['доля активных', '40%', 'неделя 8', 'наша БД'],
200
+ ] }) });
201
+ assert.equal(dupe.code, 2, dupe.out);
202
+ assert.match(dupe.out, /повторяются строки/);
203
+ });
204
+
205
+ test('P13 - no non-zero exit ever prints the clean marker', () => {
206
+ const cases = [
207
+ { [PRD]: prd({ heading: false }) },
208
+ { [PRD]: prd({ columns: 3 }) },
209
+ { [PRD]: prd({ rows: [['m', '1', 'w1', '']] }) },
210
+ { [PRD]: prd({ rows: [['m', '1', 'w1', 'из аналитики']] }) },
211
+ { [PRD]: prd({ rows: [['m', '1', 'w1', 'внешний API: reviews.publish']] }) },
212
+ ];
213
+ for (const files of cases) {
214
+ const r = check(files);
215
+ assert.notEqual(r.code, 0, JSON.stringify(files));
216
+ assert.doesNotMatch(r.out, /✅/, r.out);
217
+ }
218
+ });
219
+
220
+ test('P14 - MEASURED: the undeliverable metric passes every OTHER shipped guard', () => {
221
+ // The justification, re-measured rather than quoted: the fixture from the backlog record — a
222
+ // metric «доля дошедших до публикации отзыва» resting on a non-existent reviews.publish. The
223
+ // siblings are not broken; nothing was asking this question.
224
+ const files = {
225
+ [PRD]: prd({ rows: [['доля дошедших до публикации отзыва', '40%', 'неделя 1',
226
+ 'внешний API: reviews.publish']] }),
227
+ 'docs/Specification.md': '# Specification\n\n### FR-042\n\nПубликация отзыва через reviews.publish.\n',
228
+ 'docs/Pseudocode.md': '# Pseudocode\n\nАлгоритм публикации отзыва описан подробно и полно ниже.\n',
229
+ [ARCH]: arch({ methods: ['email.send'] }),
230
+ };
231
+ for (const sibling of ['check-docs-complete.cjs', 'check-external-deps.cjs']) {
232
+ const r = run(path.join(TPL, 'hooks', sibling), files);
233
+ assert.doesNotMatch(r.out, /доля дошедших до публикации отзыва/,
234
+ sibling + ' would have to name the metric for this feature to be redundant: ' + r.out);
235
+ }
236
+ // check-external-deps is deliberately independent: its inventory here is well-formed, so it is
237
+ // GREEN over the same tree. The two mechanisms have independent counterexamples, which is why
238
+ // they are two records and two checkers.
239
+ const deps = run(path.join(TPL, 'hooks', 'check-external-deps.cjs'), files);
240
+ assert.equal(deps.code, 0, 'the inventory itself is fine — only the metric is unobtainable: ' + deps.out);
241
+
242
+ const mine = check(files);
243
+ assert.equal(mine.code, 1, mine.out);
244
+ assert.match(mine.out, /доля дошедших до публикации отзыва/, 'and this checker names it');
245
+ });
246
+ });
247
+
248
+ describe('the template demands it, and every counter agrees', () => {
249
+ test('P15 - the SKILL carries the fourth column, the closed list and the consequence', () => {
250
+ const skill = read('templates/.claude/skills/sparc-prd-mini/SKILL.md');
251
+ assert.match(skill, /\| Metric \| Target \| Timeline \| Источник значения \|/,
252
+ 'the table shipped to every project must have the column, or nobody fills it');
253
+ assert.match(skill, /Every metric MUST name where its value comes from/,
254
+ 'and the duty must read as a duty, not a suggestion');
255
+ for (const value of ['наш журнал', 'наша БД', 'внешний API', 'ручное измерение']) {
256
+ assert.ok(skill.includes(value), 'the closed list must be spelled out: ' + value);
257
+ }
258
+ assert.match(skill, /MUST produce a row in `## External Dependencies`/,
259
+ 'the only route by which a metric reaches a lens looking outside the documents');
260
+ // The completion checklist asked only for a number; that is the line that let it through.
261
+ assert.match(skill, /Метрики успеха измеримы И называют источник значения/,
262
+ 'the checklist must ask the new question too, or the old one still passes alone');
263
+ });
264
+
265
+ test('P16 - the validator 100-point table is untouched, deliberately', () => {
266
+ // The backlog record forbids putting weight on the validator's scoring table: doing so would
267
+ // turn tests/unit/validation-gate-teeth.test.js red, and the same prohibition already applied to
268
+ // Phase 0.5. The enforcement lives in a callable utility instead — which is also the stronger
269
+ // layer, so the constraint and the design agree rather than merely coexisting.
270
+ const cmd = read('templates/.claude/commands/replicate.md');
271
+ const skill = read('templates/.claude/skills/sparc-prd-mini/SKILL.md');
272
+ for (const [name, text] of [['replicate.md', cmd], ['SKILL.md', skill]]) {
273
+ assert.ok(!/источник значения[^\n]{0,80}\b(балл|points?|вес|weight)/i.test(text),
274
+ name + ': metric provenance must not be given a score in the validator table');
275
+ }
276
+ });
277
+
278
+ test('P17 - it is a hooks component wired to NO event, and every counter agrees', () => {
279
+ const { COMPONENTS } = require(path.join(PKG, 'src', 'utils.js'));
280
+ assert.ok(COMPONENTS.hooks.items['check-metric-source'],
281
+ 'it must be registered, or init/doctor/verify will not know it');
282
+ const settings = read('templates/.claude/settings.json');
283
+ assert.ok(!settings.includes('check-metric-source'),
284
+ 'it must not be wired to an event: this package\'s hooks are non-blocking by contract');
285
+
286
+ const statusline = read('templates/.claude/hooks/statusline.cjs');
287
+ const hooks = statusline.match(/hooksExpected:\s*(\d+)/);
288
+ assert.ok(hooks, 'statusline must declare hooksExpected');
289
+ const onDisk = fs.readdirSync(path.join(TPL, 'hooks')).filter((f) => f.endsWith('.cjs'));
290
+ assert.equal(Number(hooks[1]), Object.keys(COMPONENTS.hooks.items).length,
291
+ 'the status line would report a phantom missing hook: ' + hooks[1]);
292
+ assert.equal(onDisk.length, Object.keys(COMPONENTS.hooks.items).length,
293
+ 'the shipped directory and the declared contract disagree: ' + onDisk.join(', '));
294
+
295
+ const rule = read('templates/.claude/rules/replicate-pipeline.md');
296
+ assert.match(rule, /check-metric-source\.cjs/, 'the hook inventory must list it');
297
+ const inventory = rule.match(/\*\*Hooks \((\d+) files/);
298
+ assert.ok(inventory, 'the rule must state the hook count it is inventorying');
299
+ assert.equal(Number(inventory[1]), onDisk.length,
300
+ 'the rule prose and the shipped directory disagree — the exact split this family exists for');
301
+ });
302
+
303
+ test('P18 - the mutation registry carries this feature\'s guards', () => {
304
+ const registry = JSON.parse(read('test/mutation-registry.json'));
305
+ const expected = {
306
+ 'metric-source-column-must-exist': 'templates/.claude/hooks/check-metric-source.cjs',
307
+ 'metric-source-external-api-needs-its-inventory-row': 'templates/.claude/hooks/check-metric-source.cjs',
308
+ 'metric-source-list-is-closed': 'templates/.claude/hooks/check-metric-source.cjs',
309
+ 'metric-source-template-demands-the-column': 'templates/.claude/skills/sparc-prd-mini/SKILL.md',
310
+ };
311
+ for (const [id, file] of Object.entries(expected)) {
312
+ const entry = registry.entries.find((e) => e.id === id);
313
+ assert.ok(entry, 'missing targeted mutation id ' + id);
314
+ assert.equal(entry.file, file, id + ' mutates the wrong surface');
315
+ assert.ok(entry.property && entry.property.length > 40,
316
+ id + ' needs a behavioral property, not a cosmetic label');
317
+ assert.ok(entry.mutation && entry.mutation.find && entry.mutation.replace,
318
+ id + ' needs a real source mutation');
319
+ assert.ok(Number.isInteger(entry.minFailing) && entry.minFailing >= 1,
320
+ id + ' must require at least one failing test');
321
+ assert.equal(read(entry.file).split(entry.mutation.find).length - 1, 1,
322
+ id + ': mutation anchor must occur exactly once');
323
+ }
324
+ });
325
+ });