@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.
- package/.dz-manifest.json +312 -76
- package/CHANGELOG.md +232 -0
- package/MULTIPLATFORM_ROADMAP.md +1 -1
- package/README/eng/01_quickstart.md +3 -3
- package/README/eng/02_user_guide.md +1 -1
- package/README/eng/03_admin_guide.md +2 -2
- package/README/eng/04_api_reference.md +11 -5
- package/README/eng/05_architecture.md +1 -1
- package/README/eng/README.md +2 -1
- package/README/ru/01_quickstart.md +3 -3
- package/README/ru/02_user_guide.md +1 -1
- package/README/ru/03_admin_guide.md +2 -2
- package/README/ru/04_api_reference.md +11 -5
- package/README/ru/05_architecture.md +1 -1
- package/README/ru/README.md +2 -1
- package/README/ru/html/index.html +9 -9
- package/README.md +278 -39
- package/package.json +5 -4
- package/sbom.json +665 -75
- package/scripts/check-pipeline-gaps.sh +413 -0
- package/src/commands/doctor.js +94 -4
- package/src/commands/init.js +1 -1
- package/src/rule-components.json +15 -0
- package/src/utils.js +35 -11
- package/templates/.claude/agents/harvest-coordinator.md +10 -1
- package/templates/.claude/agents/product-discoverer.md +38 -0
- package/templates/.claude/agents/replicate-coordinator.md +11 -1
- package/templates/.claude/commands/feature.md +81 -9
- package/templates/.claude/commands/go.md +9 -0
- package/templates/.claude/commands/harvest.md +39 -3
- package/templates/.claude/commands/myinsights.md +21 -26
- package/templates/.claude/commands/replicate.md +171 -37
- package/templates/.claude/commands/start.md +29 -0
- package/templates/.claude/hooks/capture-source-path.cjs +795 -0
- package/templates/.claude/hooks/check-canon.cjs +493 -0
- package/templates/.claude/hooks/check-embed-contract.cjs +374 -0
- package/templates/.claude/hooks/check-external-deps.cjs +288 -0
- package/templates/.claude/hooks/check-file-ownership.cjs +424 -0
- package/templates/.claude/hooks/check-handoff-manifest.cjs +367 -0
- package/templates/.claude/hooks/check-job-contract.cjs +501 -0
- package/templates/.claude/hooks/check-look-origin.cjs +240 -0
- package/templates/.claude/hooks/check-look-trace.cjs +385 -0
- package/templates/.claude/hooks/check-metric-source.cjs +296 -0
- package/templates/.claude/hooks/check-model-cost.cjs +470 -0
- package/templates/.claude/hooks/check-ports.cjs +434 -24
- package/templates/.claude/hooks/check-source-version.cjs +312 -0
- package/templates/.claude/hooks/check-swarm-receipts.cjs +197 -0
- package/templates/.claude/hooks/check-webhook-contract.cjs +535 -0
- package/templates/.claude/hooks/session-insights.cjs +158 -25
- package/templates/.claude/hooks/statusline.cjs +2 -2
- package/templates/.claude/hooks/write-insight.cjs +253 -0
- package/templates/.claude/rules/cost-of-detection-ladder.md +96 -0
- package/templates/.claude/rules/docker-ports.md +41 -19
- package/templates/.claude/rules/embeddable-widget.md +73 -0
- package/templates/.claude/rules/feature-lifecycle.md +13 -3
- package/templates/.claude/rules/honest-configuration.md +54 -0
- package/templates/.claude/rules/incoming-webhooks.md +99 -0
- package/templates/.claude/rules/insights-capture.md +10 -5
- package/templates/.claude/rules/long-running-job.md +73 -0
- package/templates/.claude/rules/model-call-cost.md +85 -0
- package/templates/.claude/rules/replicate-pipeline.md +123 -52
- package/templates/.claude/rules/skill-interface-protocol.md +1 -0
- package/templates/.claude/rules/swarm-file-evidence.md +46 -0
- package/templates/.claude/settings.json +13 -1
- package/templates/.claude/skills/brutal-honesty-review/SKILL.md +9 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/SKILL.md +4 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/03-generate-p0.md +46 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/04-generate-p1.md +7 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/06-package-deliver.md +20 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/claude-md-strategy.md +7 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/automation-commands.md +17 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle-ent.md +43 -5
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle.md +43 -7
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/start-command.md +19 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/swarm-file-evidence.md +151 -0
- package/templates/.claude/skills/goap-research-ed25519/SKILL.md +37 -22
- package/templates/.claude/skills/goap-research-ed25519/references/negative-results.md +94 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/check_report_evidence.py +368 -4
- package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +122 -5
- package/templates/.claude/skills/goap-research-ed25519/scripts/evidence_fetch.py +33 -16
- package/templates/.claude/skills/goap-research-ed25519/scripts/quote_provenance.py +342 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_ed25519_verifier.py +60 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_evidence_provenance.py +139 -6
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_quote_provenance.py +274 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_suite_completeness.py +2 -1
- package/templates/.claude/skills/knowledge-extractor/SKILL.md +4 -0
- package/templates/.claude/skills/knowledge-extractor/modules/01-agent-review.md +16 -5
- package/templates/.claude/skills/pipeline-forge/SKILL.md +18 -23
- package/templates/.claude/skills/pipeline-forge/examples/replicate-analysis.md +7 -2
- package/templates/.claude/skills/pipeline-forge/references/patterns-catalog.md +19 -1
- package/templates/.claude/skills/pipeline-forge/references/self-extracted-patterns.md +17 -6
- package/templates/.claude/skills/pipeline-forge/references/skill-anatomy.md +0 -1
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/025-cjm-prototype.md +21 -1
- package/templates/.claude/skills/sparc-prd-mini/SKILL.md +234 -716
- package/tests/e2e/lifecycle.test.js +55 -9
- package/tests/e2e/packed-insights-writer.test.js +308 -0
- package/tests/fixtures/prep-traceability-fixture/docs/features/order-refund/01_specification.md +29 -0
- package/tests/fixtures/prep-traceability-fixture/docs/features/order-refund/02_pseudocode.md +57 -0
- package/tests/snapshot/baseline.json +72 -46
- package/tests/snapshot/templates.test.js +47 -0
- package/tests/unit/absence-is-not-emptiness.test.js +15 -1
- package/tests/unit/capture-source-path.test.js +492 -0
- package/tests/unit/check-canon.test.js +403 -0
- package/tests/unit/check-embed-contract.test.js +422 -0
- package/tests/unit/check-external-deps.test.js +363 -0
- package/tests/unit/check-file-ownership.test.js +388 -0
- package/tests/unit/check-handoff-manifest.test.js +410 -0
- package/tests/unit/check-job-contract.test.js +514 -0
- package/tests/unit/check-look-origin.test.js +180 -0
- package/tests/unit/check-look-trace.test.js +420 -0
- package/tests/unit/check-metric-source.test.js +325 -0
- package/tests/unit/check-model-cost.test.js +425 -0
- package/tests/unit/check-pipeline-gaps.test.js +94 -0
- package/tests/unit/check-ports.test.js +773 -2
- package/tests/unit/check-source-version.test.js +344 -0
- package/tests/unit/check-swarm-receipts.test.js +231 -0
- package/tests/unit/check-webhook-contract.test.js +536 -0
- package/tests/unit/db-port-rule.test.js +43 -6
- package/tests/unit/detection-ladder-contract.test.js +302 -0
- package/tests/unit/detection-ladder-registry.test.js +52 -0
- package/tests/unit/doctor-insight-flow.test.js +315 -0
- package/tests/unit/external-dependency-check.test.js +19 -19
- package/tests/unit/generator-swarm-contract.test.js +287 -0
- package/tests/unit/guard-honest-input-meta.test.js +64 -0
- package/tests/unit/honest-failure-rules.test.js +574 -0
- package/tests/unit/hooks-project-anchored.test.js +67 -3
- package/tests/unit/insights-docs-tell-the-truth.test.js +52 -31
- package/tests/unit/insights-dz-delegation.test.js +197 -0
- package/tests/unit/insights-writer.test.js +285 -0
- package/tests/unit/look-phase-contract.test.js +231 -0
- package/tests/unit/negative-conclusion-gate.test.js +300 -0
- package/tests/unit/quote-provenance.test.js +122 -0
- package/tests/unit/shipped-suite-context.test.js +3 -1
- package/tests/unit/traceability-machine-ids.test.js +413 -0
- package/tests/unit/traceability-negative-fixture.test.js +322 -0
- package/tests/unit/utils.test.js +40 -2
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The deterministic half of «передача Фазы 0 → Фазе 1 есть ЗАКРЫТЫЙ список полей». 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 and in the PRODUCER's agent
|
|
6
|
+
// file — neither of which the corpus measure counts.
|
|
7
|
+
//
|
|
8
|
+
// THE DEFECT, measured on this package's own templates. `commands/replicate.md` passes Product
|
|
9
|
+
// Context as exactly four fields — target_segments, key_competitors, differentiation, monetization.
|
|
10
|
+
// The producer's own declared output format, `agents/product-discoverer.md`, carries SIX sections,
|
|
11
|
+
// and the sixth is «Key Insights for PRD». There is no field for it. Growth Channels reaches the
|
|
12
|
+
// other side only through the separate FR-GROWTH seed.
|
|
13
|
+
//
|
|
14
|
+
// MECHANISM: the artifact IS written and IS on disk; what the next phase lacks is an INPUT through
|
|
15
|
+
// which it could reach a decision. So «used» and «silently dropped» are indistinguishable, because
|
|
16
|
+
// nobody is obliged to answer by list.
|
|
17
|
+
//
|
|
18
|
+
// MEASURED on a fixture tree (2026-09-01): a project whose brief line «Core Loop: еженедельный
|
|
19
|
+
// дайджест» appears nowhere in docs/Specification.md, with BOTH existing seeds fully traced, gives
|
|
20
|
+
// check-growth-trace 0, check-look-trace 0 and check-docs-complete 0 — three green guards over a
|
|
21
|
+
// proven loss. P16 below REBUILDS that tree and asserts exactly that, so the justification for this
|
|
22
|
+
// checker is a running measurement rather than a paragraph.
|
|
23
|
+
//
|
|
24
|
+
// THE CLAIM DELIBERATELY NOT MADE: «no Phase-0 artifact gets through» is FALSE. Two obligation
|
|
25
|
+
// families have layer 1 and work — FR-GROWTH and FR-LOOK. What was missing is a guard for
|
|
26
|
+
// everything ELSE the run produced.
|
|
27
|
+
|
|
28
|
+
const { test, describe } = require('node:test');
|
|
29
|
+
const assert = require('node:assert/strict');
|
|
30
|
+
const { spawnSync } = require('node:child_process');
|
|
31
|
+
const fs = require('node:fs');
|
|
32
|
+
const os = require('node:os');
|
|
33
|
+
const path = require('node:path');
|
|
34
|
+
|
|
35
|
+
const PKG = path.resolve(__dirname, '..', '..');
|
|
36
|
+
const TPL = path.join(PKG, 'templates', '.claude');
|
|
37
|
+
const CHECK = path.join(TPL, 'hooks', 'check-handoff-manifest.cjs');
|
|
38
|
+
const BRIEF = 'docs/product-discovery-brief.md';
|
|
39
|
+
const SPEC = 'docs/Specification.md';
|
|
40
|
+
|
|
41
|
+
const read = (rel) => fs.readFileSync(path.join(PKG, rel), 'utf8');
|
|
42
|
+
|
|
43
|
+
/** Build a throwaway project and run a REAL checker over it. */
|
|
44
|
+
function run(checker, files) {
|
|
45
|
+
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-handoff-')));
|
|
46
|
+
try {
|
|
47
|
+
for (const [rel, body] of Object.entries(files || {})) {
|
|
48
|
+
const abs = path.join(dir, rel);
|
|
49
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
50
|
+
fs.writeFileSync(abs, body);
|
|
51
|
+
}
|
|
52
|
+
const r = spawnSync(process.execPath, [checker, dir], { encoding: 'utf8' });
|
|
53
|
+
return { code: r.status, out: (r.stdout || '') + (r.stderr || '') };
|
|
54
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const check = (files) => run(CHECK, files);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A brief in whatever state the case needs.
|
|
61
|
+
*
|
|
62
|
+
* Defaults are the HEALTHY ones so that every case stays about the single thing it changes; a case
|
|
63
|
+
* that wants silence passes `null` for that field, and silence is asserted as its own outcome.
|
|
64
|
+
*/
|
|
65
|
+
function brief({ ran = 'да', run: status = 'ВЫПОЛНЕНА', reason = null, rows, tail = '' } = {}) {
|
|
66
|
+
const line = (label, value) => (value === null || value === undefined ? '' : '**' + label + ':** ' + value + '\n');
|
|
67
|
+
const body = rows === undefined
|
|
68
|
+
? [['Ключевые инсайты для PRD', 'PD-INSIGHT-001', 'M5'],
|
|
69
|
+
['Ядро продукта: еженедельный дайджест', 'PD-CORE-001', 'M2']]
|
|
70
|
+
: rows;
|
|
71
|
+
return '# Product Discovery Brief\n\n'
|
|
72
|
+
+ '### Key Insights for PRD\n\nПользователь платит за экономию времени.\n\n'
|
|
73
|
+
+ '## Манифест передачи\n\n'
|
|
74
|
+
+ line('Фаза 0 выполнена', ran)
|
|
75
|
+
+ line('Проверка манифеста', status)
|
|
76
|
+
+ line('Причина', reason)
|
|
77
|
+
+ '\n| Выход | Идентификатор | Модуль |\n|---|---|---|\n'
|
|
78
|
+
+ body.map((r) => '| ' + r.join(' | ') + ' |').join('\n') + '\n'
|
|
79
|
+
+ tail;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** A Specification that answers both default outputs. */
|
|
83
|
+
const SPEC_ANSWERS_BOTH = [
|
|
84
|
+
'# Specification', '',
|
|
85
|
+
'## FR-001 Дайджест', '',
|
|
86
|
+
'Ядро продукта — еженедельный дайджест (PD-CORE-001).', '',
|
|
87
|
+
'Экономия времени пользователя — PD-INSIGHT-001 — определяет приоритет MVP.', '',
|
|
88
|
+
].join('\n');
|
|
89
|
+
|
|
90
|
+
const healthy = (over = {}) => ({ [BRIEF]: brief(over), [SPEC]: SPEC_ANSWERS_BOTH });
|
|
91
|
+
|
|
92
|
+
describe('манифест передачи Фазы 0 → Фазы 1 — the deterministic half', () => {
|
|
93
|
+
test('P1 - every enumerated output answered by identifier is CLEAN', () => {
|
|
94
|
+
const r = check(healthy());
|
|
95
|
+
assert.equal(r.code, 0, r.out);
|
|
96
|
+
assert.match(r.out, /✅/);
|
|
97
|
+
// The boundary is part of the verdict: an insight can be cited and misused.
|
|
98
|
+
assert.match(r.out, /не что ответ хорош/, r.out);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('P2 - no brief means Phase 0 never ran, which is NOT «nothing was lost»', () => {
|
|
102
|
+
const r = check({ [SPEC]: SPEC_ANSWERS_BOTH });
|
|
103
|
+
assert.equal(r.code, 2, r.out);
|
|
104
|
+
assert.match(r.out, /--from-docs/);
|
|
105
|
+
assert.doesNotMatch(r.out, /✅/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('P3 - a brief with NO manifest section cannot be answered by list', () => {
|
|
109
|
+
// The state of the package before this feature: the artifact exists and there is nothing to
|
|
110
|
+
// answer against. Reporting that as clean is the exact substitution the checker refuses.
|
|
111
|
+
const r = check({
|
|
112
|
+
[BRIEF]: '# Product Discovery Brief\n\n### Key Insights for PRD\n\nтри инсайта\n',
|
|
113
|
+
[SPEC]: SPEC_ANSWERS_BOTH,
|
|
114
|
+
});
|
|
115
|
+
assert.equal(r.code, 2, r.out);
|
|
116
|
+
assert.match(r.out, /нет раздела `## Манифест передачи`/);
|
|
117
|
+
assert.match(r.out, /отвечать не по чему/);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('P4 - «Фаза 0 выполнена: нет» is legitimate, and it is 2 rather than 0', () => {
|
|
121
|
+
const r = check(healthy({ ran: 'нет' }));
|
|
122
|
+
assert.equal(r.code, 2, r.out);
|
|
123
|
+
assert.doesNotMatch(r.out, /✅/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('P5 - an unrecognised closed value stops the check instead of guessing', () => {
|
|
127
|
+
const r = check(healthy({ ran: 'частично' }));
|
|
128
|
+
assert.equal(r.code, 2, r.out);
|
|
129
|
+
assert.match(r.out, /нераспознанное значение/);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('P6 - «НЕ ВЫПОЛНЕНА» needs a reason from the closed list, and then it is honest', () => {
|
|
133
|
+
const noReason = check(healthy({ run: 'НЕ ВЫПОЛНЕНА' }));
|
|
134
|
+
assert.equal(noReason.code, 2, noReason.out);
|
|
135
|
+
assert.match(noReason.out, /без строки `\*\*Причина:\*\*`/);
|
|
136
|
+
|
|
137
|
+
const freeText = check(healthy({ run: 'НЕ ВЫПОЛНЕНА', reason: 'не успели' }));
|
|
138
|
+
assert.equal(freeText.code, 2, freeText.out);
|
|
139
|
+
assert.match(freeText.out, /не из закрытого списка/);
|
|
140
|
+
|
|
141
|
+
const named = check(healthy({ run: 'НЕ ВЫПОЛНЕНА', reason: 'фаза-1-не-завершена' }));
|
|
142
|
+
assert.equal(named.code, 2, named.out);
|
|
143
|
+
assert.match(named.out, /фаза-1-не-завершена/);
|
|
144
|
+
assert.doesNotMatch(named.out, /✅/);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('P7 - THE fixture: three outputs, one unanswered and unrejected, exits 1 WITH ITS NAME', () => {
|
|
148
|
+
// The shape the backlog record mandates, verbatim: the failing name must be printed.
|
|
149
|
+
const r = check({
|
|
150
|
+
[BRIEF]: brief({ rows: [
|
|
151
|
+
['Ключевые инсайты для PRD', 'PD-INSIGHT-001', 'M5'],
|
|
152
|
+
['Ядро продукта: еженедельный дайджест', 'PD-CORE-001', 'M2'],
|
|
153
|
+
['Каналы дистрибуции', 'PD-CHANNEL-001', 'M5'],
|
|
154
|
+
] }),
|
|
155
|
+
[SPEC]: SPEC_ANSWERS_BOTH,
|
|
156
|
+
});
|
|
157
|
+
assert.equal(r.code, 1, r.out);
|
|
158
|
+
assert.match(r.out, /PD-CHANNEL-001/, 'the lost output must be NAMED, not counted');
|
|
159
|
+
assert.match(r.out, /Каналы дистрибуции/, 'and named in words a person recognises');
|
|
160
|
+
assert.doesNotMatch(r.out, /PD-CORE-001/, 'an answered output must not be reported as lost');
|
|
161
|
+
assert.match(r.out, /молчание запрещено/);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('P8 - a rejection WITH a reason is an answer, and passes', () => {
|
|
165
|
+
const r = check({
|
|
166
|
+
[BRIEF]: brief({ rows: [
|
|
167
|
+
['Ключевые инсайты для PRD', 'PD-INSIGHT-001', 'M5'],
|
|
168
|
+
['Ядро продукта: еженедельный дайджест', 'PD-CORE-001', 'M2'],
|
|
169
|
+
['Каналы дистрибуции', 'PD-CHANNEL-001', 'M5'],
|
|
170
|
+
] }),
|
|
171
|
+
[SPEC]: SPEC_ANSWERS_BOTH
|
|
172
|
+
+ '\nPD-CHANNEL-001 отклонён — дистрибуция вне объёма MVP, вернёмся во второй версии.\n',
|
|
173
|
+
});
|
|
174
|
+
assert.equal(r.code, 0, r.out);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('P9 - a rejection WITHOUT a reason is its own defect, reported apart', () => {
|
|
178
|
+
// Different repairs: one needs a decision, the other needs the decision written down.
|
|
179
|
+
const r = check({
|
|
180
|
+
[BRIEF]: brief({ rows: [['Каналы дистрибуции', 'PD-CHANNEL-001', 'M5']] }),
|
|
181
|
+
[SPEC]: '# Specification\n\nPD-CHANNEL-001 отклонён.\n',
|
|
182
|
+
});
|
|
183
|
+
assert.equal(r.code, 1, r.out);
|
|
184
|
+
assert.match(r.out, /отклонён БЕЗ ПРИЧИНЫ/);
|
|
185
|
+
assert.match(r.out, /PD-CHANNEL-001/);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('P10 - a REFUSAL is not a citation, so the reason requirement is actually reached', () => {
|
|
189
|
+
// The measured hole in the sibling checker: a line containing the exact token read as «carried
|
|
190
|
+
// forward», so the rejection path was never entered and its reason was never demanded.
|
|
191
|
+
const withReason = check({
|
|
192
|
+
[BRIEF]: brief({ rows: [['Каналы', 'PD-CHANNEL-001', 'M5']] }),
|
|
193
|
+
[SPEC]: '# Specification\n\nPD-CHANNEL-001 rejected because the channel needs a partner we lack.\n',
|
|
194
|
+
});
|
|
195
|
+
assert.equal(withReason.code, 0, withReason.out);
|
|
196
|
+
|
|
197
|
+
const bare = check({
|
|
198
|
+
[BRIEF]: brief({ rows: [['Каналы', 'PD-CHANNEL-001', 'M5']] }),
|
|
199
|
+
[SPEC]: '# Specification\n\nPD-CHANNEL-001 rejected\n',
|
|
200
|
+
});
|
|
201
|
+
assert.equal(bare.code, 1, bare.out);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('P11 - an empty manifest under «да» is a PROVEN gap, not an unknown', () => {
|
|
205
|
+
const r = check({ [BRIEF]: brief({ rows: [] }), [SPEC]: SPEC_ANSWERS_BOTH });
|
|
206
|
+
assert.equal(r.code, 1, r.out);
|
|
207
|
+
assert.match(r.out, /ни один её выход не перечислен/);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('P12 - a template row does not count as a real output', () => {
|
|
211
|
+
// A manifest padded with the shipped example would make the check pass while describing work
|
|
212
|
+
// nobody did — worse than a short manifest, because it is a green verdict about fiction.
|
|
213
|
+
const r = check({
|
|
214
|
+
[BRIEF]: brief({ rows: [
|
|
215
|
+
['[what this run actually produced]', 'PD-INSIGHT-001', 'M5'],
|
|
216
|
+
] }),
|
|
217
|
+
[SPEC]: '# Specification\n\nничего\n',
|
|
218
|
+
});
|
|
219
|
+
assert.equal(r.code, 1, r.out);
|
|
220
|
+
assert.match(r.out, /ни один её выход не перечислен/,
|
|
221
|
+
'an untouched template must not look like a filled-in manifest');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('P13 - free prose instead of a stable identifier stops the check', () => {
|
|
225
|
+
// Prose cannot be answered BY LIST, and answering by list is the entire cure.
|
|
226
|
+
const r = check({
|
|
227
|
+
[BRIEF]: brief({ rows: [['Ключевые инсайты', 'три главных инсайта', 'M5']] }),
|
|
228
|
+
[SPEC]: SPEC_ANSWERS_BOTH,
|
|
229
|
+
});
|
|
230
|
+
assert.equal(r.code, 2, r.out);
|
|
231
|
+
assert.match(r.out, /без устойчивого идентификатора/);
|
|
232
|
+
assert.match(r.out, /ПО СПИСКУ/);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('P14 - a reused identifier stops the check: one citation would clear two outputs', () => {
|
|
236
|
+
const r = check({
|
|
237
|
+
[BRIEF]: brief({ rows: [
|
|
238
|
+
['Инсайты', 'PD-INSIGHT-001', 'M5'],
|
|
239
|
+
['Ядро продукта', 'PD-INSIGHT-001', 'M2'],
|
|
240
|
+
] }),
|
|
241
|
+
[SPEC]: SPEC_ANSWERS_BOTH,
|
|
242
|
+
});
|
|
243
|
+
assert.equal(r.code, 2, r.out);
|
|
244
|
+
assert.match(r.out, /повторяются идентификаторы/);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test('P15 - no Phase-1 documents at all is «nothing to answer WITH», never «all delivered»', () => {
|
|
248
|
+
const r = check({ [BRIEF]: brief() });
|
|
249
|
+
assert.equal(r.code, 2, r.out);
|
|
250
|
+
assert.match(r.out, /нет ни одного документа Фазы 1/);
|
|
251
|
+
assert.doesNotMatch(r.out, /✅/);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('P16 - MEASURED: the three existing guards are all green over the same proven loss', () => {
|
|
255
|
+
// The justification for this checker, re-measured rather than quoted. The fixture is the one
|
|
256
|
+
// from the backlog record: a brief whose Core Loop line reaches no Phase-1 document, with BOTH
|
|
257
|
+
// shipped seed families fully traced. The three siblings are not broken — they answer their own
|
|
258
|
+
// questions correctly, and nothing was asking this one.
|
|
259
|
+
const files = {
|
|
260
|
+
[BRIEF]: [
|
|
261
|
+
'# Product Discovery Brief', '',
|
|
262
|
+
'### Key Insights for PRD', '',
|
|
263
|
+
'Core Loop: еженедельный дайджест', '',
|
|
264
|
+
'| Требование | Ось | Статус |', '|---|---|---|',
|
|
265
|
+
'| FR-GROWTH-001 | канал | ACCEPTED |', '',
|
|
266
|
+
].join('\n') + '\n' + brief().slice(brief().indexOf('## Манифест передачи')),
|
|
267
|
+
[SPEC]: [
|
|
268
|
+
'# Specification', '',
|
|
269
|
+
'FR-GROWTH-001 — реферальная программа.', '',
|
|
270
|
+
'FR-LOOK-001 — палитра источника.', '',
|
|
271
|
+
'Экономия времени пользователя — PD-INSIGHT-001.', '',
|
|
272
|
+
].join('\n'),
|
|
273
|
+
'docs/source-product-profile.md': [
|
|
274
|
+
'# Source Product Profile', '',
|
|
275
|
+
'**Статус съёмки:** СНЯТ', '',
|
|
276
|
+
'| Требование | Ось | Наблюдение | Источник | Статус |', '|---|---|---|---|---|',
|
|
277
|
+
'| FR-LOOK-001 | облик | палитра | https://example.test | СНЯТ |', '',
|
|
278
|
+
].join('\n'),
|
|
279
|
+
'docs/PRD.md': '# PRD\n\nПродукт для занятых людей.\n',
|
|
280
|
+
'docs/Architecture.md': '# Architecture\n\nМонорепозиторий.\n',
|
|
281
|
+
'docs/Pseudocode.md': '# Pseudocode\n\nАлгоритм сборки дайджеста описан ниже подробно и полно.\n',
|
|
282
|
+
};
|
|
283
|
+
const growth = run(path.join(TPL, 'hooks', 'check-growth-trace.cjs'), files);
|
|
284
|
+
const look = run(path.join(TPL, 'hooks', 'check-look-trace.cjs'), files);
|
|
285
|
+
const docs = run(path.join(TPL, 'hooks', 'check-docs-complete.cjs'), files);
|
|
286
|
+
const mine = check(files);
|
|
287
|
+
|
|
288
|
+
// The seeds arrive; the third output does not. Whatever the two seed checkers answer, neither
|
|
289
|
+
// of them is capable of naming PD-CORE-001 — and that is the point being measured.
|
|
290
|
+
for (const [name, r] of [['growth', growth], ['look', look], ['docs', docs]]) {
|
|
291
|
+
assert.doesNotMatch(r.out, /PD-CORE-001/,
|
|
292
|
+
name + ' would have to name the lost output for this feature to be redundant: ' + r.out);
|
|
293
|
+
}
|
|
294
|
+
assert.equal(mine.code, 1, mine.out);
|
|
295
|
+
assert.match(mine.out, /PD-CORE-001/, 'and this checker names it');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test('P17 - no non-zero exit ever prints the clean marker', () => {
|
|
299
|
+
const cases = [
|
|
300
|
+
{},
|
|
301
|
+
{ [BRIEF]: brief({ ran: 'нет' }), [SPEC]: SPEC_ANSWERS_BOTH },
|
|
302
|
+
{ [BRIEF]: brief({ rows: [] }), [SPEC]: SPEC_ANSWERS_BOTH },
|
|
303
|
+
{ [BRIEF]: brief({ rows: [['Каналы', 'PD-CHANNEL-001', 'M5']] }), [SPEC]: '# Specification\n\nничего\n' },
|
|
304
|
+
{ [BRIEF]: brief() },
|
|
305
|
+
];
|
|
306
|
+
for (const files of cases) {
|
|
307
|
+
const r = check(files);
|
|
308
|
+
assert.notEqual(r.code, 0, JSON.stringify(Object.keys(files)));
|
|
309
|
+
assert.doesNotMatch(r.out, /✅/, r.out);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
/** The seam's FORCE, separately from its vocabulary. */
|
|
315
|
+
function seamForceProblems(text) {
|
|
316
|
+
const problems = [];
|
|
317
|
+
if (!/Phase 1 MUST answer for EVERY one/.test(text)) {
|
|
318
|
+
problems.push('the answer-by-list duty is advisory, not imperative');
|
|
319
|
+
}
|
|
320
|
+
if (!/reject it WITH A REASON/.test(text)) {
|
|
321
|
+
problems.push('a silent rejection is allowed again, which is indistinguishable from forgetting');
|
|
322
|
+
}
|
|
323
|
+
if (!/Silence is forbidden/.test(text)) {
|
|
324
|
+
problems.push('silence is no longer refused — and silence is the whole defect');
|
|
325
|
+
}
|
|
326
|
+
if (!/node \.claude\/hooks\/check-handoff-manifest\.cjs \./.test(text)) {
|
|
327
|
+
problems.push('the deterministic half is not invoked at the seam');
|
|
328
|
+
}
|
|
329
|
+
return problems;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
describe('the producer emits it, the seam demands it, and every counter agrees', () => {
|
|
333
|
+
test('P18 - /replicate demands the answer by list, and softening it fires', () => {
|
|
334
|
+
const rel = 'templates/.claude/commands/replicate.md';
|
|
335
|
+
const source = read(rel);
|
|
336
|
+
assert.deepEqual(seamForceProblems(source), [], rel);
|
|
337
|
+
|
|
338
|
+
const softened = source.replace('Phase 1 MUST answer for EVERY one',
|
|
339
|
+
'Phase 1 may take these into account');
|
|
340
|
+
assert.notEqual(softened, source, 'mutation fixture did not apply — the sentence moved');
|
|
341
|
+
assert.deepEqual(seamForceProblems(softened), ['the answer-by-list duty is advisory, not imperative'],
|
|
342
|
+
'softening the duty must fire EXACTLY the mandate predicate');
|
|
343
|
+
|
|
344
|
+
const silentDrop = source.replace('Silence is forbidden', 'Silence is discouraged');
|
|
345
|
+
assert.ok(seamForceProblems(silentDrop).some((p) => p.includes('silence')), rel);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('P19 - the PRODUCER emits the manifest, or Phase 1 has nothing to answer against', () => {
|
|
349
|
+
// The producer half lives in agents/, which the corpus measure does not count — so the long
|
|
350
|
+
// form is affordable exactly where it is needed and free where it is loaded.
|
|
351
|
+
const agent = read('templates/.claude/agents/product-discoverer.md');
|
|
352
|
+
assert.match(agent, /## Манифест передачи/, 'the output format must carry the manifest section');
|
|
353
|
+
assert.match(agent, /PD-INSIGHT-001/, 'with a concrete identifier shape');
|
|
354
|
+
assert.match(agent, /A module that did not run owes NOTHING/,
|
|
355
|
+
'a manifest padded to look complete is worse than a short one');
|
|
356
|
+
assert.match(agent, /check-handoff-manifest\.cjs/, 'and it must name its deterministic half');
|
|
357
|
+
// The closed four-field handoff is the REASON, and it must be stated where the producer reads it.
|
|
358
|
+
assert.match(agent, /CLOSED list of four fields/,
|
|
359
|
+
'without the mechanism the manifest reads as one more section to fill in');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('P20 - it is a hooks component wired to NO event, and every counter agrees', () => {
|
|
363
|
+
const { COMPONENTS } = require(path.join(PKG, 'src', 'utils.js'));
|
|
364
|
+
assert.ok(COMPONENTS.hooks.items['check-handoff-manifest'],
|
|
365
|
+
'it must be registered, or init/doctor/verify will not know it');
|
|
366
|
+
const settings = read('templates/.claude/settings.json');
|
|
367
|
+
assert.ok(!settings.includes('check-handoff-manifest'),
|
|
368
|
+
'it must not be wired to an event: this package\'s hooks are non-blocking by contract, so a '
|
|
369
|
+
+ 'hook could only print — the cure REQUIRES a callable utility that can refuse');
|
|
370
|
+
|
|
371
|
+
const statusline = read('templates/.claude/hooks/statusline.cjs');
|
|
372
|
+
const hooks = statusline.match(/hooksExpected:\s*(\d+)/);
|
|
373
|
+
assert.ok(hooks, 'statusline must declare hooksExpected');
|
|
374
|
+
const onDisk = fs.readdirSync(path.join(TPL, 'hooks')).filter((f) => f.endsWith('.cjs'));
|
|
375
|
+
assert.equal(Number(hooks[1]), Object.keys(COMPONENTS.hooks.items).length,
|
|
376
|
+
'the status line would report a phantom missing hook: ' + hooks[1]);
|
|
377
|
+
assert.equal(onDisk.length, Object.keys(COMPONENTS.hooks.items).length,
|
|
378
|
+
'the shipped directory and the declared contract disagree: ' + onDisk.join(', '));
|
|
379
|
+
|
|
380
|
+
const rule = read('templates/.claude/rules/replicate-pipeline.md');
|
|
381
|
+
assert.match(rule, /check-handoff-manifest\.cjs/, 'the hook inventory must list it');
|
|
382
|
+
const inventory = rule.match(/\*\*Hooks \((\d+) files/);
|
|
383
|
+
assert.ok(inventory, 'the rule must state the hook count it is inventorying');
|
|
384
|
+
assert.equal(Number(inventory[1]), onDisk.length,
|
|
385
|
+
'the rule prose and the shipped directory disagree — the exact split this family exists for');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('P21 - the mutation registry carries this feature\'s guards', () => {
|
|
389
|
+
const registry = JSON.parse(read('test/mutation-registry.json'));
|
|
390
|
+
const expected = {
|
|
391
|
+
'handoff-cannot-check-is-not-clean': 'templates/.claude/hooks/check-handoff-manifest.cjs',
|
|
392
|
+
'handoff-silence-is-not-an-answer': 'templates/.claude/hooks/check-handoff-manifest.cjs',
|
|
393
|
+
'handoff-a-refusal-needs-a-reason': 'templates/.claude/hooks/check-handoff-manifest.cjs',
|
|
394
|
+
'handoff-seam-answer-by-list-is-mandatory': 'templates/.claude/commands/replicate.md',
|
|
395
|
+
};
|
|
396
|
+
for (const [id, file] of Object.entries(expected)) {
|
|
397
|
+
const entry = registry.entries.find((e) => e.id === id);
|
|
398
|
+
assert.ok(entry, 'missing targeted mutation id ' + id);
|
|
399
|
+
assert.equal(entry.file, file, id + ' mutates the wrong surface');
|
|
400
|
+
assert.ok(entry.property && entry.property.length > 40,
|
|
401
|
+
id + ' needs a behavioral property, not a cosmetic label');
|
|
402
|
+
assert.ok(entry.mutation && entry.mutation.find && entry.mutation.replace,
|
|
403
|
+
id + ' needs a real source mutation');
|
|
404
|
+
assert.ok(Number.isInteger(entry.minFailing) && entry.minFailing >= 1,
|
|
405
|
+
id + ' must require at least one failing test');
|
|
406
|
+
assert.equal(read(entry.file).split(entry.mutation.find).length - 1, 1,
|
|
407
|
+
id + ': mutation anchor must occur exactly once');
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
});
|