@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,367 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * check-handoff-manifest.cjs — всё, что Фаза 0 ПРОИЗВЕЛА, получило от Фазы 1 ответ по списку?
6
+ *
7
+ * NOT an event hook — and here that is not a stylistic note but a requirement of the cure. The
8
+ * deliberate shape is a CALLABLE UTILITY in the form of the two that already exist
9
+ * (`check-growth-trace.cjs`, `check-look-trace.cjs`), never an event hook: this package's hooks are
10
+ * NON-BLOCKING by contract, pinned by tests/unit/hooks-project-anchored.test.js, which requires exit
11
+ * 0 — a hook could only print, it could never refuse. Invoke it:
12
+ *
13
+ * node .claude/hooks/check-handoff-manifest.cjs [path-to-project]
14
+ *
15
+ * WHY IT EXISTS — the failure, before the technology.
16
+ *
17
+ * The Phase 0 → Phase 1 handoff is a CLOSED LIST OF FOUR FIELDS. `commands/replicate.md` passes
18
+ * Product Context as exactly `target_segments`, `key_competitors`, `differentiation`,
19
+ * `monetization`. The producer's own declared output format, `agents/product-discoverer.md`, carries
20
+ * SIX sections, and the sixth is «Key Insights for PRD — Top 3-5 insights that should inform product
21
+ * planning». There is no field for it. None. (Growth Channels reaches the other side only through
22
+ * the `FR-GROWTH` seed, which is a different mechanism with its own checker.)
23
+ *
24
+ * THE MECHANISM OF THE LOSS: the artifact IS written and it IS on disk, in
25
+ * `docs/product-discovery-brief.md`. What the next phase lacks is an INPUT through which it could
26
+ * reach a decision. So «used» and «silently dropped» look identical from every side — because
27
+ * nobody is obliged to answer by list.
28
+ *
29
+ * MEASURED on a fixture tree (2026-09-01): a project whose brief line «Core Loop: еженедельный
30
+ * дайджест» appears nowhere in `docs/Specification.md`, with BOTH existing seeds fully traced, gives
31
+ * `check-growth-trace` 0, `check-look-trace` 0 and `check-docs-complete` 0 — three green guards over
32
+ * a proven loss. They are not broken; they are answering their own questions correctly. Nothing was
33
+ * asking this one.
34
+ *
35
+ * THE CLAIM THIS FILE DELIBERATELY DOES NOT MAKE. «No Phase 0 artifact gets through» is FALSE, and
36
+ * repeating it would be the easy version of the story. Exactly two obligation families have a layer-1
37
+ * guard and they work: `FR-GROWTH` and `FR-LOOK`. What is missing is a guard for everything ELSE the
38
+ * run produced — which is why the manifest enumerates the run's OWN outputs rather than a fixed list
39
+ * somebody wrote in advance.
40
+ *
41
+ * WHAT AN ENTRY MAY BE. Only ENUMERABLE outputs of the REAL run, each with a STABLE IDENTIFIER —
42
+ * never free prose. Two reasons, both load-bearing: prose cannot be answered by list (which is the
43
+ * defect), and a module that did not run must not owe anything (which is what keeps the manifest
44
+ * honest instead of ceremonial). A module that did not run contributes no rows.
45
+ *
46
+ * WHAT THIS FILE CAN AND CANNOT DECIDE — read before trusting exit 0.
47
+ *
48
+ * It settles that every enumerated output got an ANSWER: it is named by identifier somewhere in the
49
+ * Phase-1 documents, or it is REJECTED with a reason. Silence is neither. It does NOT settle whether
50
+ * the answer is a good one — an insight can be cited and misused, and no deterministic check can see
51
+ * that. That stays layer 3.
52
+ *
53
+ * THE EXACT FORM — appended to `docs/product-discovery-brief.md` by Phase 0 as its last section:
54
+ *
55
+ * ## Манифест передачи
56
+ *
57
+ * **Фаза 0 выполнена:** да (да | нет — `--from-docs` legitimately skips Phase 0)
58
+ * **Проверка манифеста:** ВЫПОЛНЕНА (ВЫПОЛНЕНА | НЕ ВЫПОЛНЕНА)
59
+ * **Причина:** — (required when НЕ ВЫПОЛНЕНА; one of the closed REASONS)
60
+ *
61
+ * | Выход | Идентификатор | Модуль |
62
+ * |---|---|---|
63
+ * | Ключевые инсайты для PRD | PD-INSIGHT-001 | M5 |
64
+ * | Ядро продукта: еженедельный дайджест | PD-CORE-001 | M2 |
65
+ *
66
+ * Exit codes — three, and the third is the point:
67
+ * 0 every enumerated output is answered — cited by identifier in a Phase-1 document, or rejected
68
+ * with a reason
69
+ * 1 a defect is PROVEN and NAMED: an output nobody answered for, a rejection with no reason, or
70
+ * an empty manifest under «Фаза 0 выполнена: да»
71
+ * 2 THE CHECK DID NOT RUN — no brief (Phase 0 never ran; `--from-docs` skips it), no manifest
72
+ * section, no Phase-1 documents to answer with, a malformed or duplicated identifier, or the
73
+ * legitimate answers «Фаза 0 не выполнялась» and «проверка НЕ ВЫПОЛНЕНА, причина такая-то»
74
+ *
75
+ * A checker that answers "clean" when it could not look converts an unknown into a reassurance.
76
+ * "Phase 0 did not run" is not "nothing was lost".
77
+ */
78
+
79
+ const fs = require('node:fs');
80
+ const path = require('node:path');
81
+
82
+ const BRIEF = path.join('docs', 'product-discovery-brief.md');
83
+
84
+ /**
85
+ * The Phase-1 documents an answer may live in. The SAME list the status line calls the SPARC set, so
86
+ * the two cannot disagree about what Phase 1 produced. An answer counts wherever it is written: the
87
+ * obligation is to ANSWER, not to answer in a particular file.
88
+ */
89
+ const PHASE1 = ['PRD.md', 'Solution_Strategy.md', 'Specification.md', 'Pseudocode.md',
90
+ 'Architecture.md', 'Refinement.md', 'Completion.md', 'Research_Findings.md',
91
+ 'Final_Summary.md', 'C4_Diagrams.md', 'ADR.md'];
92
+
93
+ /** Did Phase 0 run at all? A CLOSED set — `--from-docs` skips it, and that is legitimate. */
94
+ const RAN = { 'ДА': true, 'НЕТ': false };
95
+
96
+ /** Was the handoff review performed? CLOSED, and the negative answer is honest, not a failure. */
97
+ const RUN_STATUS = { 'ВЫПОЛНЕНА': 'done', 'НЕ ВЫПОЛНЕНА': 'not-done' };
98
+
99
+ /**
100
+ * Why the handoff was not settled. CLOSED list — each entry names a DIFFERENT repair:
101
+ * выходы-не-перечислены — Phase 0 produced things but never enumerated them; enumerate them
102
+ * фаза-1-не-завершена — there is nothing to answer WITH yet; finish Phase 1, then re-check
103
+ * решение-отложено — take the decision (this is the one that becomes a silent drop)
104
+ * вне-объёма — decide it is out of scope and record the decision
105
+ */
106
+ const REASONS = ['выходы-не-перечислены', 'фаза-1-не-завершена', 'решение-отложено', 'вне-объёма'];
107
+
108
+ /** A stable identifier: `PD-<WORD>-<nnn>`. Case-sensitive and exact — a title is not an identifier,
109
+ * and a paraphrase cannot be answered by list, which is the whole defect. */
110
+ const ID = /\bPD-[A-Z][A-Z0-9]{1,11}-\d{3}\b/g;
111
+ const ID_EXACT = /^PD-[A-Z][A-Z0-9]{1,11}-\d{3}$/;
112
+
113
+ /** A line that REFUSES an output. Same vocabulary as `check-growth-trace.cjs`, deliberately, so the
114
+ * two checkers cannot disagree about what a refusal looks like. */
115
+ const REJECT_WORD = /(отклон\w*|не берём|не беремся|не берем|rejected|declined|out of scope|вне области)/i;
116
+
117
+ function say(s) { process.stdout.write(s + '\n'); }
118
+
119
+ /** Exit 2 with a reason. Never merged with "clean": not-run and not-violated are different facts. */
120
+ function cannotCheck(reason, hint) {
121
+ say('⚠️ проверка НЕ выполнена: ' + reason);
122
+ if (hint) say(' ' + hint);
123
+ process.exit(2);
124
+ }
125
+
126
+ /** The value of a `**Label:** value` header line, or null when the label is absent entirely. */
127
+ function header(text, label) {
128
+ const re = new RegExp('^\\s*\\*\\*' + label + ':?\\*\\*\\s*:?(.*)$', 'im');
129
+ const m = re.exec(text);
130
+ if (!m) return null;
131
+ return m[1].trim().replace(/^[«"`]|[»"`]$/g, '').trim();
132
+ }
133
+
134
+ /** A header value read against a CLOSED map, with both failure modes kept apart. */
135
+ function closedHeader(text, label, map, what) {
136
+ const raw = header(text, label);
137
+ if (raw === null) {
138
+ cannotCheck('в манифесте нет строки `**' + label + ':**`',
139
+ what + ' — допустимы ровно: ' + Object.keys(map).join(' | '));
140
+ }
141
+ const key = raw.toUpperCase().replace(/\s+/g, ' ').trim();
142
+ if (!Object.prototype.hasOwnProperty.call(map, key)) {
143
+ cannotCheck('нераспознанное значение `' + label + '`: ' + (key === '' ? '(пусто)' : key),
144
+ 'допустимы ровно: ' + Object.keys(map).join(' | '));
145
+ }
146
+ return map[key];
147
+ }
148
+
149
+ /**
150
+ * The body of the `## Манифест передачи` section.
151
+ *
152
+ * Read PER SECTION, never from the whole brief: the brief carries other tables (the competitor
153
+ * matrix, the FR-GROWTH seed), and a global scan would read their rows as manifest entries — an
154
+ * answer to a neighbouring question, delivered as a defect.
155
+ */
156
+ function manifestSection(brief) {
157
+ const lines = brief.split('\n');
158
+ const start = lines.findIndex((l) => /^#{2,6}\s+Манифест передачи\s*$/i.test(l.trim()));
159
+ if (start < 0) return null;
160
+ const level = /^(#+)/.exec(lines[start].trim())[1].length;
161
+ let end = lines.length;
162
+ for (let i = start + 1; i < lines.length; i++) {
163
+ const m = /^(#{1,6})\s+\S/.exec(lines[i].trim());
164
+ if (m && m[1].length <= level) { end = i; break; }
165
+ }
166
+ return lines.slice(start + 1, end).join('\n');
167
+ }
168
+
169
+ /**
170
+ * The enumerated outputs.
171
+ *
172
+ * A row is a markdown table row whose SECOND cell is an identifier. A cell holding a bracketed
173
+ * placeholder is a TEMPLATE row, not a real output — counting it would let an untouched template
174
+ * look like a filled-in manifest, which is the same substitution the seed checkers already refuse.
175
+ */
176
+ function manifestRows(body) {
177
+ const rows = [];
178
+ const bad = [];
179
+ for (const raw of body.split('\n')) {
180
+ const line = raw.trim();
181
+ if (!line.startsWith('|')) continue;
182
+ const cells = line.split('|').map((c) => c.trim());
183
+ if (cells.length < 4) continue;
184
+ const what = cells[1];
185
+ const id = (cells[2] || '').replace(/^[`«"]|[`»"]$/g, '').trim();
186
+ if (!what || /^:?-+:?$/.test(what) || what.toLowerCase() === 'выход') continue;
187
+ if (/^\[.*\]$/.test(what) || what === '...') continue; // template row
188
+ if (!id || /^\[.*\]$/.test(id)) { bad.push(what + ' → (нет идентификатора)'); continue; }
189
+ if (!ID_EXACT.test(id)) { bad.push(what + ' → ' + id); continue; }
190
+ rows.push({ what, id, module: cells[3] || '' });
191
+ }
192
+ return { rows, bad };
193
+ }
194
+
195
+ /** Identifiers cited by the Phase-1 documents. A REFUSAL IS NOT A CITATION — it is decided by
196
+ * `rejected()`, which demands a reason; otherwise `PD-INSIGHT-001 отклонён` would read as used. */
197
+ function cited(docs) {
198
+ const seen = new Set();
199
+ for (const text of docs.values()) {
200
+ for (const line of text.split('\n')) {
201
+ if (REJECT_WORD.test(line)) continue;
202
+ ID.lastIndex = 0;
203
+ for (let m = ID.exec(line); m !== null; m = ID.exec(line)) seen.add(m[0]);
204
+ }
205
+ }
206
+ return seen;
207
+ }
208
+
209
+ /**
210
+ * An output may be REFUSED instead of used — but only ON THE RECORD, with a reason.
211
+ *
212
+ * The reason must live AFTER the rejection word, and it must be WORDS. Both constraints are
213
+ * inherited from `check-growth-trace.cjs`, where scanning the whole line was a measured false-clean:
214
+ * the identifier itself contains hyphens, so a reason pattern that accepted punctuation matched the
215
+ * id and passed `FR-GROWTH-001 rejected` with nothing after it.
216
+ */
217
+ function rejected(haystacks, id) {
218
+ const re = new RegExp('^.*\\b' + id + '\\b.*$', 'gm');
219
+ for (const hay of haystacks) {
220
+ for (const line of hay.match(re) || []) {
221
+ const m = REJECT_WORD.exec(line);
222
+ if (!m) continue;
223
+ const tail = line.slice(m.index + m[0].length);
224
+ if (/[\p{L}\p{N}][\p{L}\p{N}\s]{6,}/u.test(tail.replace(/^[\s:—–-]+/, ''))) return true;
225
+ }
226
+ }
227
+ return false;
228
+ }
229
+
230
+ /** A rejection line that names the id but gives no reason. Reported apart from «no answer at all»
231
+ * because the repairs differ: one needs a decision, the other needs the decision WRITTEN DOWN. */
232
+ function refusedWithoutReason(haystacks, id) {
233
+ const re = new RegExp('^.*\\b' + id + '\\b.*$', 'gm');
234
+ for (const hay of haystacks) {
235
+ for (const line of hay.match(re) || []) {
236
+ if (REJECT_WORD.test(line)) return true;
237
+ }
238
+ }
239
+ return false;
240
+ }
241
+
242
+ function main() {
243
+ const root = process.argv[2] || '.';
244
+ try { if (!fs.statSync(root).isDirectory()) cannotCheck('это не каталог: ' + root); }
245
+ catch { cannotCheck('путь не существует: ' + root); }
246
+
247
+ const briefAbs = path.join(root, BRIEF);
248
+ let brief;
249
+ try {
250
+ if (!fs.statSync(briefAbs).isFile()) cannotCheck(BRIEF + ' существует, но это не файл');
251
+ brief = fs.readFileSync(briefAbs, 'utf-8');
252
+ } catch (e) {
253
+ if (e && e.code === 'ENOENT') {
254
+ cannotCheck('нет файла ' + BRIEF,
255
+ 'это значит, что Фаза 0 не запускалась (вход `--from-docs` её пропускает) — а НЕ что '
256
+ + 'ничего не потеряно');
257
+ }
258
+ cannotCheck('не читается ' + BRIEF + ': ' + ((e && e.message) || e));
259
+ }
260
+
261
+ const body = manifestSection(brief);
262
+ if (body === null) {
263
+ cannotCheck('в брифе нет раздела `## Манифест передачи`',
264
+ 'без перечня произведённого «использовано» и «молча выпало» неотличимы: отвечать не по чему. '
265
+ + 'Отсутствие раздела — это НЕ «Фаза 0 ничего не произвела».');
266
+ }
267
+
268
+ const ran = closedHeader(body, 'Фаза 0 выполнена', RAN,
269
+ 'без этой строки «Фаза 0 пропущена» неотличимо от «Фаза 0 ничего не дала»');
270
+ if (!ran) {
271
+ cannotCheck('манифест говорит «Фаза 0 выполнена: нет» — передавать нечего',
272
+ 'это законный ответ, а не нарушение: вход `--from-docs` пропускает Фазу 0 целиком');
273
+ }
274
+
275
+ const run = closedHeader(body, 'Проверка манифеста', RUN_STATUS,
276
+ 'без этой строки «не сверяли» неотличимо от «сверили»');
277
+ if (run === 'not-done') {
278
+ const raw = header(body, 'Причина');
279
+ if (raw === null || raw === '') {
280
+ cannotCheck('проверка манифеста НЕ ВЫПОЛНЕНА без строки `**Причина:**`',
281
+ 'причина обязательна и берётся из закрытого списка: ' + REASONS.join(' | ')
282
+ + ' — каждая означает СВОЙ ремонт');
283
+ }
284
+ const picked = REASONS.filter((r) => raw.includes(r));
285
+ if (picked.length !== 1) {
286
+ cannotCheck('причина «' + raw + '» не из закрытого списка (или названо сразу несколько)',
287
+ 'допустимы ровно: ' + REASONS.join(' | '));
288
+ }
289
+ cannotCheck('проверка манифеста НЕ ВЫПОЛНЕНА, причина: ' + picked[0],
290
+ 'честное «неизвестно», а не «всё доехало»; пока причина не закрыта, выход Фазы 0 лежит на '
291
+ + 'диске, а входа, через который он попал бы в решения, у Фазы 1 нет');
292
+ }
293
+
294
+ const { rows, bad } = manifestRows(body);
295
+ if (bad.length) {
296
+ cannotCheck('в манифесте есть выходы без устойчивого идентификатора: ' + bad.join(', '),
297
+ 'идентификатор имеет вид `PD-СЛОВО-nnn`. Произвольная проза не может быть отвечена ПО СПИСКУ '
298
+ + '— а отвечать по списку и есть лечение');
299
+ }
300
+ if (rows.length === 0) {
301
+ say('❌ Фаза 0 объявлена выполненной, но ни один её выход не перечислен');
302
+ say(' • раздел `## Манифест передачи` не содержит ни одной заполненной строки');
303
+ say(' Перечень произведённого — единственное, по чему Фаза 1 может ОТВЕТИТЬ. Без него '
304
+ + '«использовано» и «молча выпало» пишутся одинаково.');
305
+ process.exit(1);
306
+ }
307
+ const ids = rows.map((r) => r.id);
308
+ const dupes = [...new Set(ids.filter((id, i) => ids.indexOf(id) !== i))];
309
+ if (dupes.length) {
310
+ cannotCheck('в манифесте повторяются идентификаторы: ' + dupes.join(', '),
311
+ 'номер не переиспользуется — пока дубли не разведены, ОДНО упоминание в документах Фазы 1 '
312
+ + 'зачло бы сразу два разных выхода');
313
+ }
314
+
315
+ // The documents an answer may live in. NONE of them present is «could not check», never «clean»:
316
+ // there is nothing to answer WITH.
317
+ const docs = new Map();
318
+ for (const name of PHASE1) {
319
+ const p = path.join(root, 'docs', name);
320
+ try { if (fs.statSync(p).isFile()) docs.set(name, fs.readFileSync(p, 'utf-8')); } catch { /* absent */ }
321
+ }
322
+ if (docs.size === 0) {
323
+ cannotCheck('в docs/ нет ни одного документа Фазы 1',
324
+ 'отвечать по манифесту не в чем — это не «всё доехало»; ожидались любые из: '
325
+ + PHASE1.join(', '));
326
+ }
327
+
328
+ const haystacks = [brief, ...docs.values()];
329
+ const seen = cited(docs);
330
+ const silent = [];
331
+ const unreasoned = [];
332
+ for (const row of rows) {
333
+ if (seen.has(row.id)) continue;
334
+ if (rejected(haystacks, row.id)) continue;
335
+ if (refusedWithoutReason(haystacks, row.id)) unreasoned.push(row);
336
+ else silent.push(row);
337
+ }
338
+
339
+ if (unreasoned.length) {
340
+ say('❌ выход отклонён БЕЗ ПРИЧИНЫ (' + unreasoned.length + ' из ' + rows.length + '):');
341
+ for (const r of unreasoned) say(' • ' + r.id + ' — ' + r.what);
342
+ say(' «Отклонено» без причины неотличимо от «забыли»: и то и другое оставляет читателя без '
343
+ + 'основания решения. Назовите причину в той же строке.');
344
+ process.exit(1);
345
+ }
346
+ if (silent.length) {
347
+ say('❌ выход Фазы 0 не получил ответа от Фазы 1 (' + silent.length + ' из ' + rows.length + '):');
348
+ for (const r of silent) say(' • ' + r.id + ' — ' + r.what + (r.module ? ' [' + r.module + ']' : ''));
349
+ say(' Артефакт написан и лежит в ' + BRIEF + ', а входа, через который он попал бы в решения, '
350
+ + 'нет — поэтому «использовано» и «молча выпало» выглядят одинаково. Каждый выход обязан '
351
+ + 'быть либо назван в документе Фазы 1, либо отклонён С ПРИЧИНОЙ; молчание запрещено.');
352
+ process.exit(1);
353
+ }
354
+
355
+ say('✅ все ' + rows.length + ' выход(ов) Фазы 0 получили ответ по списку в ' + docs.size
356
+ + ' документ(ах) Фазы 1 — использованы либо отклонены с причиной');
357
+ say(' Ограничение: это доказывает, что выход ОТВЕЧЕН, а не что ответ хорош. Инсайт можно '
358
+ + 'процитировать и применить неверно — этого не видит ни одна детерминированная проверка (слой 3).');
359
+ process.exit(0);
360
+ }
361
+
362
+ try {
363
+ main();
364
+ } catch (err) {
365
+ // Even an unexpected failure must not read as "clean".
366
+ cannotCheck('внутренняя ошибка проверки: ' + String((err && err.message) || err));
367
+ }