@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,574 @@
1
+ 'use strict';
2
+
3
+ const { test } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const { spawnSync } = require('node:child_process');
6
+ const crypto = require('node:crypto');
7
+ const fs = require('node:fs');
8
+ const os = require('node:os');
9
+ const path = require('node:path');
10
+
11
+ const PKG = path.resolve(__dirname, '..', '..');
12
+ const ROOT = path.join(PKG, 'templates', '.claude');
13
+ const CLI = path.join(PKG, 'bin', 'cli.js');
14
+ const RULES = ['honest-configuration', 'swarm-file-evidence'];
15
+ /**
16
+ * The PREIMAGE of the current change: the always-loaded corpus (rules + commands + every SKILL.md)
17
+ * as it stood BEFORE this change, measured by measureCorpus() below.
18
+ *
19
+ * It must be RE-PINNED with each deliberate corpus growth, and here is why: the guard measures a
20
+ * DELTA against a "before". Left pinned at an older change's preimage, "before" silently stops
21
+ * meaning "before this change" and the trigger degenerates into a permanent ceiling — the package
22
+ * could never add another rule or command again, however small, because the previous change had
23
+ * already spent most of the budget. That is not the property the guard names ("context delta uses
24
+ * fresh measurement"), and it would be a ceiling nobody chose.
25
+ *
26
+ * `ESTIMATED_TRIGGER` is deliberately NOT touched when the preimage moves. Re-pinning the preimage
27
+ * restores the measurement; changing the threshold would relax it, and those are different acts.
28
+ *
29
+ * Re-pinned 2026-09-01 by the LEAD agent after merging THREE parallel branches — `incoming-webhooks`
30
+ * (2658f5a7), `long-running-job` (22885752) and `model-call-cost` (1f4ea077). Each branch measured
31
+ * its own delta against the SAME preimage and each fitted the trigger ALONE; none of them could see
32
+ * the other two. The preimage is therefore pinned to the state BEFORE ALL THREE, not to the last of
33
+ * them: pinning to the last would let the gate weigh one contribution and stay silent about two,
34
+ * which is the exact form of dishonesty this gate exists to prevent. The owner instructed that
35
+ * `ESTIMATED_TRIGGER` NOT be raised under any result — a red gate telling the truth is worth more
36
+ * than a green one bought by moving the line.
37
+ *
38
+ * The previous preimage was the `embeddable-widget` pin (backlog 23ed5f5c), the last state before
39
+ * those three merged rules. Feature `corpus-budget-compression` earned a fresh preimage on
40
+ * 2026-09-01/02: P5 passed against that OLD pin on the settled tree with live
41
+ * {files:34, bytes:356321, estimatedTokens:95940.35}, a measured delta of 3988.50 below the
42
+ * unchanged 4037.75 trigger.
43
+ * Reproducer: measureCorpus() over templates/.claude/{rules,commands}/*.md plus every SKILL.md.
44
+ * `ESTIMATED_TRIGGER` was not touched.
45
+ */
46
+ const FRESH_BEFORE = { files: 34, bytes: 356321, estimatedTokens: 95940.35 };
47
+ const ESTIMATED_TRIGGER = 4037.75;
48
+ // Measured 2026-09-01 before corpus-budget-compression. Modules and references are outside
49
+ // measureCorpus(); a 0-byte allowance makes any relocation growth visible instead of budget-free.
50
+ const UNMEASURED_SKILL_SURFACES_BEFORE = { files: 59, bytes: 638309 };
51
+ const UNMEASURED_SKILL_SURFACES_GROWTH_ALLOWANCE = 0;
52
+ const SEAMS = [
53
+ ['commands/feature.md', 'both'],
54
+ ['rules/feature-lifecycle.md', 'both'],
55
+ ['commands/harvest.md', 'both'],
56
+ ['commands/replicate.md', 'both'],
57
+ ['commands/go.md', 'both'],
58
+ ['commands/start.md', 'both'],
59
+ ['skills/knowledge-extractor/modules/01-agent-review.md', 'both'],
60
+ ['agents/harvest-coordinator.md', 'both'],
61
+ ];
62
+
63
+ const read = (rel) => fs.readFileSync(path.join(PKG, rel), 'utf8');
64
+ const hash = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
65
+ const cleanup = (dir) => fs.rmSync(dir, { recursive: true, force: true });
66
+
67
+ function swarmContractProblems(text, profile = 'both') {
68
+ const problems = [];
69
+ const dispatch = profile === 'both' || profile === 'dispatch';
70
+ const receipt = profile === 'both' || profile === 'receipt';
71
+ if (dispatch) {
72
+ if (!/WORK_UNIT_ID/.test(text)) problems.push('dispatch does not name WORK_UNIT_ID');
73
+ if (!/TRACE_PATH/.test(text)) problems.push('dispatch does not name TRACE_PATH');
74
+ if (!/absolute/i.test(text)) problems.push('TRACE_PATH is not resolved as absolute');
75
+ if (!/(?:unique|different|distinct)[\s\S]{0,100}(?:WORK_UNIT_ID|TRACE_PATH)|(?:WORK_UNIT_ID|TRACE_PATH)[\s\S]{0,100}(?:unique|different|distinct)/i.test(text)) {
76
+ problems.push('trace path is not unique per work unit');
77
+ }
78
+ const write = text.search(/write[\s\S]{0,100}TRACE_PATH/i);
79
+ const reportAfterWrite = write < 0 ? -1 : text.slice(write)
80
+ .search(/one-line[\s\S]{0,40}(?:pointer|report)|(?:pointer|report)[\s\S]{0,40}one-line/i);
81
+ if (write < 0 || reportAfterWrite < 0) problems.push('worker does not write before report');
82
+ }
83
+ if (receipt) {
84
+ const required = [
85
+ [/regular/i, 'regular-file check missing'],
86
+ [/non-symlink|not a symlink/i, 'symlink refusal missing'],
87
+ [/non-(?:empty|whitespace)|substantive/i, 'substantive-body check missing'],
88
+ [/post-launch|after[\s\S]{0,40}launch|pre-dispatch/i, 'freshness boundary missing'],
89
+ [/Status: completed/, 'completed terminal marker missing'],
90
+ [/Status: failed/, 'failed terminal marker missing'],
91
+ [/(?:narrative|chat)[\s\S]{0,80}(?:not|never)[\s\S]{0,40}(?:receipt|result)|silence[\s\S]{0,80}(?:not|never)/i,
92
+ 'narrative or silence can still act as a receipt'],
93
+ [/(?:refuse|block|must not)[\s\S]{0,100}(?:merge|synthesis|aggregation|completion)/i,
94
+ 'invalid receipt does not block aggregation'],
95
+ ];
96
+ for (const [pattern, message] of required) if (!pattern.test(text)) problems.push(message);
97
+ }
98
+ return problems;
99
+ }
100
+
101
+ function inspectReceipt({ workUnitId, tracePath, launchMs, liveness = 'unknown', probeError = false,
102
+ forceReadError = false }) {
103
+ if (!workUnitId || !path.isAbsolute(tracePath)) return { state: 'undelivered', reason: 'assignment' };
104
+ if (probeError) return { state: 'inconclusive', reason: 'liveness-probe-error' };
105
+ let stat;
106
+ try { stat = fs.lstatSync(tracePath); } catch (error) {
107
+ if (error.code !== 'ENOENT') return { state: 'inconclusive', reason: 'unreadable' };
108
+ if (liveness === 'live') return { state: 'waiting', reason: 'positive-liveness-only' };
109
+ return { state: 'undelivered', reason: liveness === 'dead' ? 'dead-worker' : 'missing' };
110
+ }
111
+ if (stat.isSymbolicLink() || !stat.isFile()) return { state: 'undelivered', reason: 'not-regular' };
112
+ if (stat.mtimeMs <= launchMs) return { state: 'undelivered', reason: 'stale' };
113
+ let body;
114
+ try {
115
+ if (forceReadError) throw new Error('fixture read failure');
116
+ body = fs.readFileSync(tracePath, 'utf8');
117
+ } catch { return { state: 'inconclusive', reason: 'unreadable' }; }
118
+ if (!body.trim()) return { state: 'undelivered', reason: 'empty' };
119
+ const lines = body.trimEnd().split(/\r?\n/);
120
+ const terminal = lines.at(-1);
121
+ if (!/^Status: (completed|failed)$/.test(terminal)) return { state: 'undelivered', reason: 'non-terminal' };
122
+ if (!lines.slice(0, -1).join('\n').trim()) return { state: 'undelivered', reason: 'empty-payload' };
123
+ return terminal === 'Status: completed'
124
+ ? { state: 'completed', reason: 'terminal-receipt' }
125
+ : { state: 'failed', reason: 'delivered-failure' };
126
+ }
127
+
128
+ function decideConfiguration(fixture) {
129
+ if (fixture.optionalDependency && fixture.fallbackCannotAlterExternalOutput) return 'OPTIONAL_FALLBACK';
130
+ if (fixture.phase === 'build' && fixture.cannotEmitExternalOutput) return 'BOUNDED_BUILD_SUBSTITUTE';
131
+ if (fixture.declared && !fixture.readByDecision) return 'REFUSE';
132
+ if (fixture.authority === 'unreachable') return 'UNKNOWN';
133
+ if (fixture.ratio && fixture.ratio.denominator === 0) return 'UNAVAILABLE';
134
+ if (fixture.required && fixture.value === undefined) return 'REFUSE';
135
+ if (fixture.required && fixture.value === '') return 'REFUSE';
136
+ if (fixture.value === '/' && fixture.kind === 'base-url') return 'REFUSE';
137
+ if (fixture.kind === 'cidr' && fixture.value === '/0' && fixture.derivedFromEmpty) return 'REFUSE';
138
+ if (fixture.kind === 'allowlist' && Array.isArray(fixture.value) && fixture.value.length === 0) return 'REFUSE';
139
+ if (fixture.recognized === false) return 'REFUSE';
140
+ return 'ACCEPT';
141
+ }
142
+
143
+ function parseDecisionRows(text) {
144
+ const rows = new Map();
145
+ for (const line of text.split(/\r?\n/)) {
146
+ const match = line.match(/^\|\s*(CFG-[SI]\d+)\s*\|\s*([^|]+)\|\s*([^|]+)\|/);
147
+ if (match) rows.set(match[1], { signal: match[2].trim(), response: match[3].trim() });
148
+ }
149
+ return rows;
150
+ }
151
+
152
+ function configurationContractProblems(text) {
153
+ const problems = [];
154
+ if (!/^### Substitution axis$/m.test(text)) problems.push('substitution axis missing');
155
+ if (!/^### Interpretation axis$/m.test(text)) problems.push('interpretation axis missing');
156
+ const rows = parseDecisionRows(text);
157
+ const required = {
158
+ 'CFG-S1': /REFUSE/,
159
+ 'CFG-S2': /REFUSE/,
160
+ 'CFG-I1': /REFUSE|UNKNOWN/,
161
+ 'CFG-I2': /REFUSE/,
162
+ 'CFG-I3': /REFUSE/,
163
+ 'CFG-I4': /REFUSE|UNKNOWN/,
164
+ 'CFG-I5': /REFUSE/,
165
+ 'CFG-I6': /REFUSE/,
166
+ 'CFG-I7': /UNAVAILABLE|UNKNOWN/,
167
+ 'CFG-I8': /CODE-OWNED/,
168
+ };
169
+ for (const [id, allowed] of Object.entries(required)) {
170
+ const row = rows.get(id);
171
+ if (!row) problems.push(`${id} missing`);
172
+ else if (!allowed.test(row.response)) problems.push(`${id} is fail-open: ${row.response}`);
173
+ }
174
+ const collapsed = rows.get('CFG-I1')?.signal === rows.get('CFG-I2')?.signal;
175
+ if (collapsed) problems.push('undefined and empty string are collapsed');
176
+ return problems;
177
+ }
178
+
179
+ function ruleShapeProblems(text) {
180
+ const problems = [];
181
+ for (const heading of ['Rule', 'Mechanics', 'Bounded exception', 'Observable violation → replacement', 'Self-check']) {
182
+ if (!new RegExp(`^## ${heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm').test(text)) {
183
+ problems.push(`${heading} section missing`);
184
+ }
185
+ }
186
+ if (/be careful|будьте внимательны/i.test(text)) problems.push('non-actionable admonition');
187
+ if (/\/home\/|dz brain|dz-harness-hub/.test(text)) problems.push('private runtime dependency');
188
+ return problems;
189
+ }
190
+
191
+ function walkSkills(dir, out) {
192
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
193
+ const file = path.join(dir, entry.name);
194
+ if (entry.isDirectory()) walkSkills(file, out);
195
+ else if (entry.name === 'SKILL.md') out.push(file);
196
+ }
197
+ }
198
+
199
+ function walkAllFiles(dir, out) {
200
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
201
+ const file = path.join(dir, entry.name);
202
+ if (entry.isDirectory()) walkAllFiles(file, out);
203
+ else out.push(file);
204
+ }
205
+ }
206
+
207
+ function measureUnmeasuredSkillSurfaces(root) {
208
+ const files = [];
209
+ const skills = path.join(root, 'skills');
210
+ for (const skill of fs.readdirSync(skills, { withFileTypes: true }).filter((entry) => entry.isDirectory())) {
211
+ for (const surface of ['modules', 'references']) {
212
+ const dir = path.join(skills, skill.name, surface);
213
+ if (fs.existsSync(dir)) walkAllFiles(dir, files);
214
+ }
215
+ }
216
+ return files.reduce((total, file) => ({
217
+ files: total.files + 1,
218
+ bytes: total.bytes + fs.readFileSync(file).length,
219
+ }), { files: 0, bytes: 0 });
220
+ }
221
+
222
+ function measureCorpus(root) {
223
+ const files = [];
224
+ for (const dir of ['rules', 'commands']) {
225
+ for (const name of fs.readdirSync(path.join(root, dir)).sort()) {
226
+ if (name.endsWith('.md')) files.push(path.join(root, dir, name));
227
+ }
228
+ }
229
+ walkSkills(path.join(root, 'skills'), files);
230
+ const rows = files.map((file) => {
231
+ const bytes = fs.readFileSync(file);
232
+ const cyrillic = (bytes.toString('utf8').match(/[\u0400-\u04ff]/g) || []).length;
233
+ const divisor = (cyrillic * 2) / bytes.length > 0.5 ? 2.5 : 4;
234
+ return { file, bytes: bytes.length, estimatedTokens: bytes.length / divisor };
235
+ });
236
+ return rows.reduce((total, row) => ({
237
+ files: total.files + 1,
238
+ bytes: total.bytes + row.bytes,
239
+ estimatedTokens: total.estimatedTokens + row.estimatedTokens,
240
+ }), { files: 0, bytes: 0, estimatedTokens: 0 });
241
+ }
242
+
243
+ function deltaProblems({ before, after, labels, complete = true }) {
244
+ const problems = [];
245
+ if (!complete) problems.push('measurement input unreadable');
246
+ if (!labels.includes('unconditional rules+commands')) problems.push('unconditional load surface unlabeled');
247
+ if (!labels.includes('invocation-dependent skills')) problems.push('skills mislabeled as unconditional');
248
+ if (after.estimatedTokens - before.estimatedTokens > ESTIMATED_TRIGGER) problems.push('compression required');
249
+ return problems;
250
+ }
251
+
252
+ function boundaryProblems(slugs) {
253
+ const actual = [...slugs].sort();
254
+ return actual.length === 2 && RULES.every((slug) => actual.includes(slug))
255
+ ? [] : [`expected exactly ${RULES.join(' + ')}, got ${actual.join(' + ')}`];
256
+ }
257
+
258
+ function seamInventoryProblems(paths) {
259
+ const expected = SEAMS.map(([file]) => file).sort();
260
+ const actual = [...new Set(paths)].sort();
261
+ return actual.length === expected.length && expected.every((file) => actual.includes(file))
262
+ ? [] : [`owned seam inventory mismatch: ${actual.join(', ')}`];
263
+ }
264
+
265
+ function countClaimProblems(text, expected) {
266
+ const claim = /(\d+)\s+rules\b|(\d+)\s+правил|Rules[ \t]+(?:●|\()?(\d+)|\*\*(\d+) rules\*\*/g;
267
+ return [...text.matchAll(claim)]
268
+ .map((match) => Number(match[1] || match[2] || match[3] || match[4]))
269
+ .filter((number) => number === expected || number === 7)
270
+ .filter((number) => number !== expected)
271
+ .map((number) => `current rule count says ${number}, expected ${expected}`);
272
+ }
273
+
274
+ function packageExportProblems(files) {
275
+ const required = ['templates', 'tests', '.dz-manifest.json', 'sbom.json'];
276
+ const normalized = files.map((entry) => entry.replace(/\/$/, ''));
277
+ return required.filter((entry) => !normalized.includes(entry)).map((entry) => `package export omits ${entry}`);
278
+ }
279
+
280
+ function assignmentProblems(assignments) {
281
+ const problems = [];
282
+ const paths = assignments.map((entry) => entry.tracePath);
283
+ if (paths.some((tracePath) => !path.isAbsolute(tracePath))) problems.push('TRACE_PATH is not absolute');
284
+ if (new Set(paths).size !== paths.length) problems.push('duplicate TRACE_PATH');
285
+ if (new Set(assignments.map((entry) => entry.workUnitId)).size !== assignments.length) {
286
+ problems.push('duplicate WORK_UNIT_ID');
287
+ }
288
+ return problems;
289
+ }
290
+
291
+ function runCli(dir, command, extra = []) {
292
+ const result = spawnSync(process.execPath, [CLI, command, ...extra], {
293
+ cwd: dir, encoding: 'utf8', timeout: 30000,
294
+ });
295
+ return { code: result.status, out: `${result.stdout || ''}${result.stderr || ''}` };
296
+ }
297
+
298
+ test('P0 - ADR boundary is one feature with exactly two non-duplicated rules', () => {
299
+ assert.deepEqual(boundaryProblems(RULES), []);
300
+ assert.equal(boundaryProblems(['honest-failure']).length, 1,
301
+ 'one catch-all rule must not erase the two reader decisions');
302
+ assert.equal(boundaryProblems(['swarm-file-evidence', 'silent-substitution', 'fail-closed']).length, 1,
303
+ 'three intake labels must not become three always-loaded files');
304
+ for (const slug of RULES) {
305
+ assert.ok(fs.existsSync(path.join(ROOT, 'rules', `${slug}.md`)), `missing chosen rule ${slug}`);
306
+ }
307
+ });
308
+
309
+ test('P1 - registry and rule filesystem agree on the thirteen-rule contract', () => {
310
+ const registry = JSON.parse(read('src/rule-components.json'));
311
+ const files = fs.readdirSync(path.join(ROOT, 'rules'))
312
+ .filter((name) => name.endsWith('.md')).map((name) => name.slice(0, -3)).sort();
313
+ assert.equal(Object.keys(registry).length, 13, 'the canonical registry must carry thirteen rules');
314
+ assert.deepEqual(Object.keys(registry).sort(), files, 'registry and canonical rule files diverged');
315
+ for (const slug of RULES) assert.ok(registry[slug], `registry missing ${slug}`);
316
+ });
317
+
318
+ test('P2 - receipt matrix refuses missing empty stale partial and non-terminal traces', () => {
319
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-receipt-')));
320
+ const launchMs = Date.now() - 1000;
321
+ const trace = (name) => path.join(dir, `${name}.md`);
322
+ try {
323
+ assert.deepEqual(inspectReceipt({ workUnitId: 'missing', tracePath: trace('missing'), launchMs }),
324
+ { state: 'undelivered', reason: 'missing' });
325
+ assert.equal(inspectReceipt({ workUnitId: 'live', tracePath: trace('live'), launchMs,
326
+ liveness: 'live' }).state, 'waiting', 'a live PID may extend waiting but is not delivery');
327
+ assert.deepEqual(inspectReceipt({ workUnitId: 'dead', tracePath: trace('dead'), launchMs,
328
+ liveness: 'dead' }), { state: 'undelivered', reason: 'dead-worker' });
329
+ assert.equal(inspectReceipt({ workUnitId: 'probe', tracePath: trace('probe'), launchMs,
330
+ probeError: true }).state, 'inconclusive');
331
+
332
+ fs.writeFileSync(trace('empty'), ' \n');
333
+ assert.equal(inspectReceipt({ workUnitId: 'empty', tracePath: trace('empty'), launchMs }).reason, 'empty');
334
+ fs.writeFileSync(trace('stale'), 'payload\nStatus: completed\n');
335
+ fs.utimesSync(trace('stale'), new Date(launchMs - 5000), new Date(launchMs - 5000));
336
+ assert.equal(inspectReceipt({ workUnitId: 'stale', tracePath: trace('stale'), launchMs }).reason, 'stale');
337
+ fs.writeFileSync(trace('partial'), 'payload without terminal marker\n');
338
+ assert.equal(inspectReceipt({ workUnitId: 'partial', tracePath: trace('partial'), launchMs }).reason,
339
+ 'non-terminal');
340
+ fs.writeFileSync(trace('failed'), 'diagnostic\nStatus: failed\n');
341
+ assert.equal(inspectReceipt({ workUnitId: 'failed', tracePath: trace('failed'), launchMs }).state, 'failed');
342
+ fs.writeFileSync(trace('complete'), 'substantive result\nStatus: completed\n');
343
+ assert.equal(inspectReceipt({ workUnitId: 'complete', tracePath: trace('complete'), launchMs }).state,
344
+ 'completed');
345
+ assert.equal(inspectReceipt({ workUnitId: 'unreadable', tracePath: trace('complete'), launchMs,
346
+ forceReadError: true }).state, 'inconclusive');
347
+ fs.symlinkSync(trace('complete'), trace('link'));
348
+ assert.equal(inspectReceipt({ workUnitId: 'link', tracePath: trace('link'), launchMs }).reason,
349
+ 'not-regular');
350
+ assert.deepEqual(assignmentProblems([
351
+ { workUnitId: 'a', tracePath: trace('shared') },
352
+ { workUnitId: 'b', tracePath: trace('shared') },
353
+ ]), ['duplicate TRACE_PATH']);
354
+ assert.deepEqual(assignmentProblems([
355
+ { workUnitId: 'a', tracePath: trace('a') },
356
+ { workUnitId: 'b', tracePath: trace('b') },
357
+ ]), []);
358
+ } finally { cleanup(dir); }
359
+ });
360
+
361
+ test('P3 - absent malformed and unreachable external inputs refuse instead of becoming plausible output', () => {
362
+ const cases = [
363
+ [{ required: true, value: undefined }, 'REFUSE', 'absent required value'],
364
+ [{ required: true, value: '' }, 'REFUSE', 'empty differs from undefined'],
365
+ [{ required: true, value: '/', kind: 'base-url' }, 'REFUSE', "BASE_URL='/'"],
366
+ [{ required: true, value: '/0', kind: 'cidr', derivedFromEmpty: true }, 'REFUSE', 'empty CIDR'],
367
+ [{ required: true, value: [], kind: 'allowlist' }, 'REFUSE', 'empty allowlist'],
368
+ [{ required: true, value: 'tyop', recognized: false }, 'REFUSE', 'unknown tariff'],
369
+ [{ required: true, value: 'cached', authority: 'unreachable' }, 'UNKNOWN', 'unreachable authority'],
370
+ [{ required: true, value: 'declared', declared: true, readByDecision: false }, 'REFUSE',
371
+ 'declared-but-unread input'],
372
+ [{ ratio: { numerator: 0, denominator: 0 } }, 'UNAVAILABLE', '0/0'],
373
+ [{ required: true, value: '/', kind: 'base-url', failureSentinel: false }, 'REFUSE',
374
+ 'invalid obtained value bypassed the failure sentinel'],
375
+ [{ required: true, value: 'https://example.test', recognized: true }, 'ACCEPT', 'explicit valid value'],
376
+ [{ optionalDependency: true, fallbackCannotAlterExternalOutput: true }, 'OPTIONAL_FALLBACK',
377
+ 'legitimate optional dependency'],
378
+ [{ phase: 'build', cannotEmitExternalOutput: true }, 'BOUNDED_BUILD_SUBSTITUTE',
379
+ 'named non-emitting build phase'],
380
+ ];
381
+ for (const [fixture, expected, label] of cases) {
382
+ assert.equal(decideConfiguration(fixture), expected, label);
383
+ }
384
+
385
+ const source = read('templates/.claude/rules/honest-configuration.md');
386
+ assert.deepEqual(configurationContractProblems(source), []);
387
+ const substitutionMutant = source.replace(/(\|\s*CFG-S1\s*\|[^|]+\|)\s*REFUSE/, '$1 DEFAULT');
388
+ assert.ok(configurationContractProblems(substitutionMutant).some((p) => p.includes('CFG-S1')),
389
+ 'weakening substitution must fire independently');
390
+ const interpretationMutant = source.replace(/(\|\s*CFG-I2\s*\|[^|]+\|)\s*REFUSE/, '$1 ALLOW');
391
+ assert.ok(configurationContractProblems(interpretationMutant).some((p) => p.includes('CFG-I2')),
392
+ 'weakening interpretation must fire independently');
393
+ });
394
+
395
+ test('P4 - both rules use actionable rule mechanics exception and self-check sections', () => {
396
+ const clean = [
397
+ '# Fixture',
398
+ '## Rule',
399
+ 'Refuse the operation.',
400
+ '## Mechanics',
401
+ 'Inspect the value.',
402
+ '## Bounded exception',
403
+ 'None.',
404
+ '## Observable violation → replacement',
405
+ 'DEFAULT is the violation; REFUSE instead.',
406
+ '## Self-check',
407
+ 'Run the bad and good fixtures.',
408
+ ].join('\n');
409
+ assert.deepEqual(ruleShapeProblems(clean), []);
410
+ for (const heading of ['Rule', 'Mechanics', 'Bounded exception', 'Observable violation → replacement',
411
+ 'Self-check']) {
412
+ const malformed = clean.replace(`## ${heading}`, `## Missing ${heading}`);
413
+ assert.ok(ruleShapeProblems(malformed).some((problem) => problem.includes(heading)),
414
+ `malformed fixture did not detect missing ${heading}`);
415
+ }
416
+ for (const slug of RULES) {
417
+ assert.deepEqual(ruleShapeProblems(read(`templates/.claude/rules/${slug}.md`)), [], slug);
418
+ }
419
+ });
420
+
421
+ test('P5 - context delta uses fresh measurement and honest load-surface labels', () => {
422
+ const live = measureCorpus(ROOT);
423
+ assert.ok(live.files >= FRESH_BEFORE.files, 'adding rules cannot reduce the measured same-set file count');
424
+ assert.ok(live.bytes >= FRESH_BEFORE.bytes, 'fresh preimage must remain the lower attribution boundary');
425
+ assert.deepEqual(deltaProblems({
426
+ before: FRESH_BEFORE,
427
+ after: live,
428
+ labels: ['unconditional rules+commands', 'invocation-dependent skills'],
429
+ }), []);
430
+ assert.deepEqual(deltaProblems({
431
+ before: FRESH_BEFORE,
432
+ after: { ...FRESH_BEFORE, estimatedTokens: FRESH_BEFORE.estimatedTokens + ESTIMATED_TRIGGER + 1 },
433
+ labels: ['unconditional rules+commands', 'invocation-dependent skills'],
434
+ }), ['compression required']);
435
+ assert.ok(deltaProblems({ before: FRESH_BEFORE, after: FRESH_BEFORE,
436
+ labels: ['unconditional rules+commands'], complete: false }).length >= 2,
437
+ 'unreadable or mislabeled measurement must not become a zero delta');
438
+ });
439
+
440
+ test('P6 - the owned inventory is eight first-party seams, and the generators have their own', () => {
441
+ const files = SEAMS.map(([file]) => file);
442
+ assert.deepEqual(seamInventoryProblems(files), []);
443
+ assert.equal(seamInventoryProblems(files.slice(0, -1)).length, 1,
444
+ 'omitting an owned seam must be observable');
445
+ assert.equal(seamInventoryProblems([...files, 'skills/generator/example.md']).length, 1,
446
+ 'this list is FIRST-PARTY only; a generator seam belongs to the generator inventory');
447
+
448
+ // Until 2026-09-01 this test asserted that generator coverage was DEFERRED, and said so honestly.
449
+ // It is no longer deferred: the contract now travels with the toolkits this package generates,
450
+ // and its inventory, its twelve-predicate check, its discovery net and its mutation proof live in
451
+ // tests/unit/generator-swarm-contract.test.js. Leaving the old wording here would have turned an
452
+ // honest deferral into a false one — the same substitution the whole feature refuses.
453
+ const generatorSuite = path.join(PKG, 'tests', 'unit', 'generator-swarm-contract.test.js');
454
+ assert.ok(fs.existsSync(generatorSuite),
455
+ 'the generator half must be proved somewhere, or this file is silently the only claim');
456
+ const generatorSource = fs.readFileSync(generatorSuite, 'utf8');
457
+ assert.match(generatorSource, /const GENERATOR_SEAMS = \[/,
458
+ 'the generator inventory must be enumerated, not implied');
459
+ assert.match(generatorSource, /uncoveredDispatchers/,
460
+ 'an inventory without a discovery net cannot notice the NEXT uncured generator file');
461
+ });
462
+
463
+ test('P7 - current package inventories derive the thirteen-rule count', () => {
464
+ assert.deepEqual(countClaimProblems('Pre-shipped: 12 rules. Rules ●11+2. **12 rules**', 11), []);
465
+ assert.equal(countClaimProblems('Pre-shipped: 7 rules.', 11).length, 1,
466
+ 'the deliberately stale count fixture must fire');
467
+ const docs = [
468
+ 'README.md',
469
+ 'MULTIPLATFORM_ROADMAP.md',
470
+ 'README/eng/01_quickstart.md',
471
+ 'README/eng/02_user_guide.md',
472
+ 'README/eng/03_admin_guide.md',
473
+ 'README/eng/README.md',
474
+ 'README/ru/01_quickstart.md',
475
+ 'README/ru/02_user_guide.md',
476
+ 'README/ru/03_admin_guide.md',
477
+ 'README/ru/README.md',
478
+ 'README/ru/html/index.html',
479
+ 'templates/.claude/commands/replicate.md',
480
+ 'templates/.claude/rules/replicate-pipeline.md',
481
+ ];
482
+ const wrong = docs.flatMap((file) => countClaimProblems(read(file), 11).map((problem) => `${file}: ${problem}`));
483
+ assert.deepEqual(wrong, []);
484
+ });
485
+
486
+ test('P8 - every owned group-A swarm seam requires a named trace before synthesis', () => {
487
+ for (const [file, profile] of SEAMS) {
488
+ const source = fs.readFileSync(path.join(ROOT, file), 'utf8');
489
+ assert.deepEqual(swarmContractProblems(source, profile), [], file);
490
+ assert.ok(swarmContractProblems(source.replace(/WORK_UNIT_ID/g, 'UNIT'), profile)
491
+ .some((problem) => problem.includes('WORK_UNIT_ID')), `${file}: dispatch mutant survived`);
492
+ assert.ok(swarmContractProblems(source.replace(/Status: completed/g, 'Status: done'), profile)
493
+ .some((problem) => problem.includes('completed terminal')), `${file}: receipt mutant survived`);
494
+ }
495
+
496
+ const project = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-honest-init-')));
497
+ try {
498
+ const init = runCli(project, 'init');
499
+ assert.equal(init.code, 0, init.out);
500
+ for (const [file, profile] of SEAMS) {
501
+ const installed = fs.readFileSync(path.join(project, '.claude', file), 'utf8');
502
+ assert.deepEqual(swarmContractProblems(installed, profile), [], `fresh init: ${file}`);
503
+ }
504
+ } finally { cleanup(project); }
505
+ });
506
+
507
+ test('P9 - targeted honest-failure mutation and runner wiring are discriminating', () => {
508
+ const packageJson = JSON.parse(read('package.json'));
509
+ for (const runner of ['test', 'test:unit']) {
510
+ assert.match(packageJson.scripts[runner], /tests\/unit\/honest-failure-rules\.test\.js/,
511
+ `${runner} omits the focused oracle`);
512
+ }
513
+ const registry = JSON.parse(read('test/mutation-registry.json'));
514
+ const entry = registry.entries.find((candidate) =>
515
+ candidate.id === 'honest-failure-swarm-receipt-required');
516
+ assert.ok(entry, 'focused mutation id is not registered');
517
+ assert.equal(entry.file, 'templates/.claude/commands/feature.md');
518
+ assert.ok(entry.mutation.find.includes('\n'), 'the production anchor must be multiline');
519
+ assert.ok(entry.mutation.find.length > 100, 'the anchor must be specific enough to stay unique');
520
+ assert.ok(/P2/.test(entry.property) && /P8/.test(entry.property) && /P9/.test(entry.property),
521
+ 'the property must name the real receipt, live-seam, and wiring oracles');
522
+ assert.ok(Number.isInteger(entry.minFailing) && entry.minFailing >= 1);
523
+ const target = read(entry.file);
524
+ assert.equal(target.split(entry.mutation.find).length - 1, 1, 'mutation anchor must occur exactly once');
525
+ const mutant = target.replace(entry.mutation.find, entry.mutation.replace);
526
+ assert.ok(swarmContractProblems(mutant).length > 0, 'registered source mutation does not weaken P8');
527
+ });
528
+
529
+ test('P10 - fresh init pack registry manifest and SBOM carry both rules', () => {
530
+ const project = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-honest-dist-')));
531
+ try {
532
+ const init = runCli(project, 'init');
533
+ assert.equal(init.code, 0, init.out);
534
+ for (const slug of RULES) {
535
+ const installed = path.join(project, '.claude', 'rules', `${slug}.md`);
536
+ assert.ok(fs.readFileSync(installed, 'utf8').trim(), `fresh init omitted ${slug}`);
537
+ }
538
+ const verify = runCli(project, 'verify');
539
+ assert.equal(verify.code, 0, verify.out);
540
+ const doctor = runCli(project, 'doctor');
541
+ assert.equal(doctor.code, 0, doctor.out);
542
+ fs.rmSync(path.join(project, '.claude', 'rules', `${RULES[0]}.md`));
543
+ const update = runCli(project, 'update');
544
+ assert.equal(update.code, 0, update.out);
545
+ assert.ok(fs.readFileSync(path.join(project, '.claude', 'rules', `${RULES[0]}.md`), 'utf8').trim(),
546
+ 'update did not restore a registered canonical rule');
547
+ } finally { cleanup(project); }
548
+
549
+ const packageJson = JSON.parse(read('package.json'));
550
+ assert.deepEqual(packageExportProblems(packageJson.files), []);
551
+ assert.deepEqual(packageExportProblems(packageJson.files.filter((entry) => entry !== 'templates/')),
552
+ ['package export omits templates'], 'the deliberately incomplete package allowlist must fire');
553
+ const manifest = JSON.parse(read('.dz-manifest.json'));
554
+ const sbom = JSON.parse(read('sbom.json'));
555
+ for (const slug of RULES) {
556
+ const relative = `templates/.claude/rules/${slug}.md`;
557
+ const digest = hash(path.join(PKG, relative));
558
+ const signed = manifest.manifest.files.find((entry) => entry.path === relative);
559
+ assert.equal(signed?.sha256, digest, `manifest missing or stale for ${relative}`);
560
+ const component = sbom.components.find((entry) => entry.name === relative);
561
+ assert.equal(component?.hashes.find((entry) => entry.alg === 'SHA-256')?.content, digest,
562
+ `SBOM missing or stale for ${relative}`);
563
+ }
564
+ });
565
+
566
+ test('P11 - unmeasured skill surfaces did not absorb the compressed corpus', () => {
567
+ const live = measureUnmeasuredSkillSurfaces(ROOT);
568
+ assert.equal(live.files, UNMEASURED_SKILL_SURFACES_BEFORE.files,
569
+ 'unmeasured skill module/reference inventory changed; re-measure and justify the new surface');
570
+ assert.ok(live.bytes <= UNMEASURED_SKILL_SURFACES_BEFORE.bytes
571
+ + UNMEASURED_SKILL_SURFACES_GROWTH_ALLOWANCE,
572
+ `unmeasured skill modules/references grew from ${UNMEASURED_SKILL_SURFACES_BEFORE.bytes}`
573
+ + ` to ${live.bytes} bytes; always-loaded text may have been relocated outside measureCorpus()`);
574
+ });