@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,425 @@
1
+ 'use strict';
2
+
3
+ // The deterministic half of the model-call-cost rule. The rule itself is prose a model executes —
4
+ // layer 2. This is layer 1, and only for whoever runs it.
5
+ //
6
+ // THE DEFECT IT EXISTS FOR. An external model call is billed PER CALL, and in these products the
7
+ // call is triggered by a STRANGER: a photo upload, a submitted episode, a pasted knowledge base. So
8
+ // the spend curve is drawn by someone whose behaviour you do not control — and unlike every other
9
+ // defect this package guards, the failure CANNOT BE ROLLED BACK. A wrong port is closed and the
10
+ // story ends. Here the invoice arrives and the money is gone. That is why the checks below are all
11
+ // about what refuses BEFORE the call, never about noticing afterwards.
12
+ //
13
+ // Three sides are covered here, deliberately:
14
+ // 1. BEHAVIOUR — the real utility, real files, real exit codes (P1-P16)
15
+ // 2. FORCE — the rule's text still MANDATES, not suggests (P17-P18); a polite rephrasing
16
+ // must turn this suite red, because a suite that tests a contract's VOCABULARY
17
+ // and not its FORCE has already been observed to pass an optional obligation
18
+ // (MEASURED on this package, commit 37916bd3)
19
+ // 3. WIRING — the seam, the counters and the mutation registry agree (P19-P21)
20
+
21
+ const { test, describe } = require('node:test');
22
+ const assert = require('node:assert/strict');
23
+ const { spawnSync } = require('node:child_process');
24
+ const fs = require('node:fs');
25
+ const os = require('node:os');
26
+ const path = require('node:path');
27
+
28
+ const PKG = path.resolve(__dirname, '..', '..');
29
+ const TPL = path.join(PKG, 'templates', '.claude');
30
+ const CHECK = path.join(TPL, 'hooks', 'check-model-cost.cjs');
31
+ const CONTRACT = 'docs/model-cost-contract.md';
32
+
33
+ const read = (rel) => fs.readFileSync(path.join(PKG, rel), 'utf8');
34
+
35
+ /** Build a throwaway project and run the REAL checker over it. */
36
+ function check(files) {
37
+ const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-cost-')));
38
+ try {
39
+ for (const [rel, body] of Object.entries(files || {})) {
40
+ const abs = path.join(dir, rel);
41
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
42
+ fs.writeFileSync(abs, body);
43
+ }
44
+ const r = spawnSync(process.execPath, [CHECK, dir], { encoding: 'utf8' });
45
+ return { code: r.status, out: (r.stdout || '') + (r.stderr || '') };
46
+ } finally { fs.rmSync(dir, { recursive: true, force: true }); }
47
+ }
48
+
49
+ /** The Cal-AI-clone row: an outsider uploads a photo, nobody logs in, sessions are the unit. */
50
+ const OUTSIDER = {
51
+ name: 'распознавание-фото', trigger: 'посторонний', auth: 'без-входа', unit: 'сессия',
52
+ perUser: '20', perDay: '2000', onLimit: 'ОТКАЗ',
53
+ };
54
+ /** The developer's own nightly job: you can only ruin yourself with it. */
55
+ const OWN = {
56
+ name: 'ночной-пересчёт', trigger: 'свой-код', auth: '—', unit: '—',
57
+ perUser: '500', perDay: '500', onLimit: 'ОТКАЗ',
58
+ };
59
+
60
+ /**
61
+ * A contract in whatever state the case needs.
62
+ *
63
+ * Defaults are the HEALTHY ones so that every case stays about the single thing it changes; a case
64
+ * that wants silence passes `null` for that field, and silence is asserted as its own outcome.
65
+ */
66
+ function contract({
67
+ calls = 'да', where = 'https://console.example.com/usage', billed = 'попыткам',
68
+ unset = 'ОТКАЗ ПРИ СТАРТЕ', run = 'ВЫПОЛНЕНА', reason = null, rows = [OUTSIDER, OWN],
69
+ } = {}) {
70
+ const line = (label, value) => (value === null ? '' : '**' + label + ':** ' + value + '\n');
71
+ return '# Model cost contract\n\n'
72
+ + line('Внешние вызовы модели', calls)
73
+ + line('Где виден расход', where)
74
+ + line('Счёт ведётся по', billed)
75
+ + line('Потолок не сконфигурирован', unset)
76
+ + line('Проверка пределов', run)
77
+ + line('Причина', reason)
78
+ + '\n## Внешние вызовы\n\n'
79
+ + '| Вызов | Кто запускает | Вход | Единица счёта | Предел на пользователя | Предел в сутки | При достижении |\n'
80
+ + '|---|---|---|---|---|---|---|\n'
81
+ + rows.map((r) => '| ' + [r.name, r.trigger, r.auth, r.unit, r.perUser, r.perDay, r.onLimit]
82
+ .join(' | ') + ' |').join('\n') + (rows.length ? '\n' : '');
83
+ }
84
+
85
+ const withRow = (patch, base = OUTSIDER) => [{ ...base, ...patch }, OWN];
86
+
87
+ describe('the model-cost checker answers three questions and never confuses two of them', () => {
88
+ test('P1 - bounded ceilings on every call is exit 0, and the outsider count is printed', () => {
89
+ const r = check({ [CONTRACT]: contract() });
90
+ assert.equal(r.code, 0, r.out);
91
+ assert.match(r.out, /2 вызов\(ов\) названы/, r.out);
92
+ // The number of calls a STRANGER can trigger is the number that decides the risk, so it is
93
+ // printed rather than folded into a total.
94
+ assert.match(r.out, /запускаются посторонними: 1 \(из них без входа: 1\)/, r.out);
95
+ // The receipt must state what it does NOT prove, or a reader upgrades it into an enforcement proof.
96
+ assert.match(r.out, /Ограничение:/, r.out);
97
+ });
98
+
99
+ test('P2 - no contract at all is exit 2, and the hint refuses the wrong reading', () => {
100
+ const r = check({});
101
+ assert.equal(r.code, 2, r.out);
102
+ assert.match(r.out, /нет файла docs\/model-cost-contract\.md/, r.out);
103
+ // «no file» means the QUESTION was never asked — never «there are no paid calls».
104
+ assert.match(r.out, /НЕ ЗАДАВАЛСЯ/, r.out);
105
+ });
106
+
107
+ test('P3 - «внешних вызовов нет» is a legitimate answer and exits 2, never 0', () => {
108
+ const r = check({ [CONTRACT]: contract({ calls: 'нет' }) });
109
+ assert.equal(r.code, 2, 'a product with no paid call has nothing to bound: ' + r.out);
110
+ assert.match(r.out, /законный ответ/, r.out);
111
+ });
112
+
113
+ test('P4 - «НЕ ВЫПОЛНЕНА» with a closed-list reason is honest UNKNOWN, exit 2', () => {
114
+ const ok = check({ [CONTRACT]: contract({ run: 'НЕ ВЫПОЛНЕНА', reason: 'не-подключено' }) });
115
+ assert.equal(ok.code, 2, ok.out);
116
+ assert.match(ok.out, /НЕ ВЫПОЛНЕНА, причина: не-подключено/, ok.out);
117
+ assert.match(ok.out, /потраченного не вернуть/, 'the irreversibility must be said here: ' + ok.out);
118
+
119
+ const none = check({ [CONTRACT]: contract({ run: 'НЕ ВЫПОЛНЕНА', reason: null }) });
120
+ assert.equal(none.code, 2, none.out);
121
+ assert.match(none.out, /без строки `\*\*Причина:\*\*`/, none.out);
122
+
123
+ for (const bad of ['потом разберёмся', 'не-подключено и вне-объёма']) {
124
+ const r = check({ [CONTRACT]: contract({ run: 'НЕ ВЫПОЛНЕНА', reason: bad }) });
125
+ assert.equal(r.code, 2, r.out);
126
+ assert.match(r.out, /не из закрытого списка/, '«' + bad + '»: ' + r.out);
127
+ }
128
+ });
129
+
130
+ test('P5 - ПРАВИЛО №0: an unconfigured ceiling declared as «без ограничений» is exit 1', () => {
131
+ const r = check({ [CONTRACT]: contract({ unset: 'БЕЗ ОГРАНИЧЕНИЙ' }) });
132
+ assert.equal(r.code, 1, 'an absent limit must not mean infinity: ' + r.out);
133
+ assert.match(r.out, /CFG-S1/, 'the honest-configuration lineage must be named: ' + r.out);
134
+ assert.match(r.out, /Запуск обязан ПАДАТЬ/, r.out);
135
+ });
136
+
137
+ test('P6 - LOAD-BEARING: a ceiling named as an INTENTION is a proven defect, exit 1', () => {
138
+ for (const bad of ['разумный', 'по ситуации', '[20]', '', '—']) {
139
+ const r = check({ [CONTRACT]: contract({ rows: withRow({ perUser: bad }) }) });
140
+ assert.equal(r.code, 1, '«' + bad + '» must not pass as a ceiling: ' + r.out);
141
+ assert.match(r.out, /намерением, а не числом/, r.out);
142
+ assert.match(r.out, /распознавание-фото \/ на пользователя/, 'the cell must be named: ' + r.out);
143
+ }
144
+ // A number a HUMAN writes with a separator is still a number; refusing it would push people to
145
+ // omit the ceiling rather than to write it.
146
+ const spaced = check({ [CONTRACT]: contract({ rows: withRow({ perDay: '2 000' }) }) });
147
+ assert.equal(spaced.code, 0, spaced.out);
148
+ });
149
+
150
+ test('P7 - an INFINITE ceiling is named apart from a typo, exit 1', () => {
151
+ for (const bad of ['∞', 'unlimited', 'без ограничений', '-1', 'нет']) {
152
+ const r = check({ [CONTRACT]: contract({ rows: withRow({ perDay: bad }) }) });
153
+ assert.equal(r.code, 1, r.out);
154
+ assert.match(r.out, /предел объявлен бесконечным/, '«' + bad + '»: ' + r.out);
155
+ }
156
+ const zero = check({ [CONTRACT]: contract({ rows: withRow({ perUser: '0' }) }) });
157
+ assert.equal(zero.code, 1, zero.out);
158
+ assert.match(zero.out, /предел не положителен/, zero.out);
159
+ });
160
+
161
+ test('P8 - a per-user ceiling above the daily one can never bind, exit 1', () => {
162
+ const r = check({ [CONTRACT]: contract({ rows: withRow({ perUser: '5000', perDay: '2000' }) }) });
163
+ assert.equal(r.code, 1, r.out);
164
+ assert.match(r.out, /не сработает никогда/, r.out);
165
+ assert.match(r.out, /на пользователя 5000 > в сутки 2000/, r.out);
166
+ // BOUNDED: equal is legitimate — a single-user-per-day product is not a defect.
167
+ const equal = check({ [CONTRACT]: contract({ rows: withRow({ perUser: '2000', perDay: '2000' }) }) });
168
+ assert.equal(equal.code, 0, equal.out);
169
+ });
170
+
171
+ test('P9 - LOAD-BEARING: a counting unit that cannot exist for the caller is exit 1', () => {
172
+ // The whole feature in one comparison. `без-входа` + `аккаунт` is a limit written like a
173
+ // defence that can never bind a single call — the same shape as a widget checked on its own
174
+ // origin: a measurement that cannot observe the failure it names.
175
+ const r = check({ [CONTRACT]: contract({ rows: withRow({ auth: 'без-входа', unit: 'аккаунт' }) }) });
176
+ assert.equal(r.code, 1, r.out);
177
+ assert.match(r.out, /не существует для того, кто вызов запускает/, r.out);
178
+ assert.match(r.out, /у анонимного посетителя нет аккаунта/, 'the mechanism, not a scolding: ' + r.out);
179
+
180
+ // BOUNDED: the SAME unit is legitimate once the caller must log in, or the check would refuse
181
+ // the correct configuration and get switched off.
182
+ const authed = check({ [CONTRACT]: contract({ rows: withRow({ auth: 'после-входа', unit: 'аккаунт' }) }) });
183
+ assert.equal(authed.code, 0, authed.out);
184
+ });
185
+
186
+ test('P10 - an outsider-triggered call with no unit, or no answer about login, is exit 1', () => {
187
+ const noUnit = check({ [CONTRACT]: contract({ rows: withRow({ unit: '—' }) }) });
188
+ assert.equal(noUnit.code, 1, noUnit.out);
189
+ assert.match(noUnit.out, /не к чему привязать/, noUnit.out);
190
+
191
+ const noAuth = check({ [CONTRACT]: contract({ rows: withRow({ auth: '—' }) }) });
192
+ assert.equal(noAuth.code, 1, noAuth.out);
193
+ assert.match(noAuth.out, /не сказано, нужен ли ему вход/, noAuth.out);
194
+
195
+ // BOUNDED: `—` in both columns is exactly right for свой-код, and must stay exit 0 (the OWN row
196
+ // carries it in every healthy fixture above, and here alone).
197
+ const ownOnly = check({ [CONTRACT]: contract({ rows: [OWN] }) });
198
+ assert.equal(ownOnly.code, 0, 'own-code calls need no identity: ' + ownOnly.out);
199
+ assert.match(ownOnly.out, /посторонними: 0/, ownOnly.out);
200
+ });
201
+
202
+ test('P11 - reaching the ceiling must REFUSE: degradation and a queue are exit 1, named apart', () => {
203
+ const degraded = check({ [CONTRACT]: contract({ rows: withRow({ onLimit: 'ДЕГРАДАЦИЯ' }) }) });
204
+ assert.equal(degraded.code, 1, 'silent degradation must not read as a ceiling: ' + degraded.out);
205
+ assert.match(degraded.out, /ДЕГРАДАЦИЯ, а не отказ/, degraded.out);
206
+ assert.match(degraded.out, /вы узнаете из счёта/, degraded.out);
207
+
208
+ const queued = check({ [CONTRACT]: contract({ rows: withRow({ onLimit: 'ОЧЕРЕДЬ' }) }) });
209
+ assert.equal(queued.code, 1, queued.out);
210
+ assert.match(queued.out, /ОЧЕРЕДЬ, а не отказ/, queued.out);
211
+ assert.match(queued.out, /не отменяет расход/, 'a queue postpones the spend, it does not cancel it');
212
+ });
213
+
214
+ test('P12 - billing counted by successes leaves the retry path unmetered, exit 1', () => {
215
+ const r = check({ [CONTRACT]: contract({ billed: 'успехам' }) });
216
+ assert.equal(r.code, 1, r.out);
217
+ assert.match(r.out, /по УСПЕХАМ/, r.out);
218
+ assert.match(r.out, /счёт за ПОПЫТКУ/, r.out);
219
+ // The adjacency is NAMED, and only named: the retry policy itself belongs to the neighbouring rule.
220
+ assert.match(r.out, /долгих фоновых задач/, r.out);
221
+ });
222
+
223
+ test('P13 - a spend place without an ADDRESS is exit 1; three address forms pass', () => {
224
+ for (const vague of ['в логах', 'в консоли провайдера', 'спросить у меня']) {
225
+ const r = check({ [CONTRACT]: contract({ where: vague }) });
226
+ assert.equal(r.code, 1, '«' + vague + '»: ' + r.out);
227
+ assert.match(r.out, /жанром, а не адресом/, r.out);
228
+ }
229
+ for (const good of ['https://console.example.com/usage', '/var/log/model-spend.jsonl',
230
+ 'команда `make spend-report`']) {
231
+ const r = check({ [CONTRACT]: contract({ where: good }) });
232
+ assert.equal(r.code, 0, '«' + good + '» must be accepted: ' + r.out);
233
+ }
234
+ const empty = check({ [CONTRACT]: contract({ where: null }) });
235
+ assert.equal(empty.code, 2, 'an absent header is «could not check», not a proven defect: ' + empty.out);
236
+ });
237
+
238
+ test('P14 - an empty call table under «да» is a PROVEN omission, exit 1', () => {
239
+ const r = check({ [CONTRACT]: contract({ rows: [] }) });
240
+ assert.equal(r.code, 1, r.out);
241
+ assert.match(r.out, /ни один не назван/, r.out);
242
+ assert.match(r.out, /доказанный пропуск, а не неизвестность/, r.out);
243
+ });
244
+
245
+ test('P15 - an unrecognised value or a duplicate row is exit 2, never a pass', () => {
246
+ for (const [field, bad] of [['trigger', 'кто-то'], ['auth', 'иногда'], ['unit', 'человек'],
247
+ ['onLimit', 'подумаем']]) {
248
+ const r = check({ [CONTRACT]: contract({ rows: withRow({ [field]: bad }) }) });
249
+ assert.equal(r.code, 2, field + '=«' + bad + '» must not be read as a verdict: ' + r.out);
250
+ assert.match(r.out, /допустимы ровно/, r.out);
251
+ }
252
+ for (const bad of ['возможно', '', 'yes']) {
253
+ const r = check({ [CONTRACT]: contract({ calls: bad }) });
254
+ assert.equal(r.code, 2, r.out);
255
+ }
256
+ const dupe = check({ [CONTRACT]: contract({ rows: [OUTSIDER, { ...OUTSIDER, perDay: '9' }] }) });
257
+ assert.equal(dupe.code, 2, dupe.out);
258
+ assert.match(dupe.out, /повторяются строки/, dupe.out);
259
+ });
260
+
261
+ test('P16 - cannot-check is NEVER clean: a bad path exits 2, not 0', () => {
262
+ const r = spawnSync(process.execPath, [CHECK, path.join(os.tmpdir(), 'p-rep-cost-absent-dir')],
263
+ { encoding: 'utf8' });
264
+ assert.equal(r.status, 2, r.stdout + r.stderr);
265
+ assert.match(r.stdout, /проверка НЕ выполнена/, r.stdout);
266
+ });
267
+ });
268
+
269
+ /**
270
+ * The rule's FORCE, kept apart from its vocabulary.
271
+ *
272
+ * MEASURED on this package 2026-09-01 (commit 37916bd3): a suite of twelve keyword predicates over
273
+ * the swarm receipt contract was fully satisfied by a block retitled "Coordination note" whose
274
+ * imperative had been softened to "Where convenient" — the registered mutation SURVIVED. These
275
+ * predicates are that lesson applied here: the heading must still refuse, each obligation must still
276
+ * read as a duty, the closed set must still be closed, and the irreversibility must still be stated.
277
+ */
278
+ function ruleForceProblems(text) {
279
+ const problems = [];
280
+ if (!/^## Правило №0 — несконфигурированный потолок ОТКАЗЫВАЕТ$/m.test(text)) {
281
+ problems.push('the Правило №0 heading no longer refuses an unconfigured ceiling');
282
+ }
283
+ if (!/ОБЯЗАН валить запуск/.test(text)) {
284
+ problems.push('the unconfigured-ceiling duty is advisory, not imperative');
285
+ }
286
+ if (!/Предел ОБЯЗАН быть назван числом, а не намерением/.test(text)) {
287
+ problems.push('the ceiling may be an intention rather than a number');
288
+ }
289
+ if (!/Достижение предела есть ОТКАЗ, а не тихая деградация/.test(text)) {
290
+ problems.push('reaching the ceiling may degrade instead of refusing');
291
+ }
292
+ if (!/Отказ, который НЕЛЬЗЯ ОТКАТИТЬ/.test(text)) {
293
+ problems.push('the irreversibility that defines this class is gone');
294
+ }
295
+ if (!/набор ЗАКРЫТЫЙ/.test(text)) {
296
+ problems.push('the two spend sources are a menu, not a closed set');
297
+ }
298
+ // Each source must arrive with WHO turns the meter — a source named without its actor is a label,
299
+ // and a label cannot be recognised in the field.
300
+ for (const [src, actor] of [['свой-код', /ваш цикл/], ['посторонний', /посетитель решает/]]) {
301
+ if (!new RegExp('`' + src + '`').test(text)) problems.push(src + ' is not named');
302
+ if (!actor.test(text)) problems.push(src + ' carries no actor');
303
+ }
304
+ if (!/место, где расход виден/.test(text)) problems.push('the spend has no named place');
305
+ if (!/Слой 3–4/.test(text) || !/НЕ МОЖЕТ по названной причине/.test(text)) {
306
+ problems.push('the layer marking hides what stays judgment');
307
+ }
308
+ if (!/Код `2` никогда не значит «всё в порядке»/.test(text)) {
309
+ problems.push('exit 2 may be read as clean');
310
+ }
311
+ return problems;
312
+ }
313
+
314
+ describe('the rule still mandates, and the pipeline still calls the check', () => {
315
+ test('P17 - the shipped rule satisfies every force predicate', () => {
316
+ assert.deepEqual(ruleForceProblems(read('templates/.claude/rules/model-call-cost.md')), []);
317
+ });
318
+
319
+ test('P18 - MUTATION: a polite rephrasing must turn this suite red', () => {
320
+ const source = read('templates/.claude/rules/model-call-cost.md');
321
+
322
+ // Retitle only. Every keyword in the file survives; only the refusal in the heading is gone.
323
+ const retitled = source.replace('## Правило №0 — несконфигурированный потолок ОТКАЗЫВАЕТ',
324
+ '## Замечание о настройке потолка');
325
+ assert.notEqual(retitled, source, 'mutation fixture did not apply — the heading moved');
326
+ assert.deepEqual(ruleForceProblems(retitled),
327
+ ['the Правило №0 heading no longer refuses an unconfigured ceiling'],
328
+ 'a retitled block must fire EXACTLY the heading predicate and nothing else');
329
+
330
+ // Soften «обязан» to «желательно». This is the mutation that survived on the swarm contract.
331
+ const softened = source.replace('ОБЯЗАН валить запуск', 'желательно останавливать запуск');
332
+ assert.notEqual(softened, source, 'mutation fixture did not apply — the sentence moved');
333
+ assert.deepEqual(ruleForceProblems(softened),
334
+ ['the unconfigured-ceiling duty is advisory, not imperative'],
335
+ 'softening the duty must fire EXACTLY the mandate predicate');
336
+
337
+ // Downgrade the number requirement to a preference.
338
+ const preference = source.replace('Предел ОБЯЗАН быть назван числом, а не намерением',
339
+ 'Предел желательно называть числом');
340
+ assert.deepEqual(ruleForceProblems(preference),
341
+ ['the ceiling may be an intention rather than a number'],
342
+ 'a downgraded ceiling requirement must fire EXACTLY its own predicate');
343
+
344
+ // Turn the REFUSAL into a warning — the exact softening the brief names.
345
+ const warning = source.replace('Достижение предела есть ОТКАЗ, а не тихая деградация',
346
+ 'Достижение предела стоит сопровождать предупреждением');
347
+ assert.deepEqual(ruleForceProblems(warning),
348
+ ['reaching the ceiling may degrade instead of refusing'],
349
+ 'downgrading ОТКАЗ to a warning must fire EXACTLY its own predicate');
350
+
351
+ // Delete what makes this class different — the money that does not come back.
352
+ const reversible = source.replace('Отказ, который НЕЛЬЗЯ ОТКАТИТЬ',
353
+ 'Неприятность, которую стоит учитывать');
354
+ assert.deepEqual(ruleForceProblems(reversible),
355
+ ['the irreversibility that defines this class is gone']);
356
+
357
+ // And deleting the layer honesty must be observable on its own.
358
+ const dishonest = source.replace('НЕ МОЖЕТ по названной причине', 'пока не сделана');
359
+ assert.deepEqual(ruleForceProblems(dishonest), ['the layer marking hides what stays judgment']);
360
+ });
361
+
362
+ test('P19 - Phase 2 of /replicate calls the check as an acceptance criterion', () => {
363
+ const cmd = read('templates/.claude/commands/replicate.md');
364
+ assert.match(cmd, /node \.claude\/hooks\/check-model-cost\.cjs \./,
365
+ 'the pipeline must actually invoke it, not merely mention the rule');
366
+ assert.match(cmd, /\.claude\/rules\/model-call-cost\.md/, 'the rule must be reachable from the seam');
367
+ // Exit 2 must be spelled out at the seam too, or the operator reads silence as success.
368
+ const seam = cmd.slice(cmd.indexOf('check-model-cost'));
369
+ assert.match(seam.slice(0, 800), /это НЕ «в порядке»/, seam.slice(0, 800));
370
+
371
+ const rule = read('templates/.claude/rules/replicate-pipeline.md');
372
+ assert.match(rule, /check-model-cost\.cjs/, 'the hook inventory must list it');
373
+ });
374
+
375
+ test('P20 - it is a hooks component wired to NO event, and every counter agrees', () => {
376
+ const { COMPONENTS } = require(path.join(PKG, 'src', 'utils.js'));
377
+ assert.ok(COMPONENTS.hooks.items['check-model-cost'],
378
+ 'it must be registered, or init/doctor/verify will not know it');
379
+ const settings = read('templates/.claude/settings.json');
380
+ assert.ok(!settings.includes('check-model-cost'),
381
+ 'it must not be wired to an event: this package\'s hooks are non-blocking by contract, so a '
382
+ + 'hook could only print — it could never refuse anything');
383
+
384
+ const statusline = read('templates/.claude/hooks/statusline.cjs');
385
+ const hooks = statusline.match(/hooksExpected:\s*(\d+)/);
386
+ assert.ok(hooks, 'statusline must declare hooksExpected');
387
+ assert.equal(Number(hooks[1]), Object.keys(COMPONENTS.hooks.items).length,
388
+ 'the status line would report a phantom missing hook: ' + hooks[1]);
389
+ const onDisk = fs.readdirSync(path.join(TPL, 'hooks')).filter((f) => f.endsWith('.cjs'));
390
+ assert.equal(onDisk.length, Object.keys(COMPONENTS.hooks.items).length,
391
+ 'the shipped directory and the declared contract disagree: ' + onDisk.join(', '));
392
+
393
+ const rules = statusline.match(/rulesExpected:\s*(\d+)/);
394
+ assert.ok(rules, 'statusline must declare rulesExpected');
395
+ const registry = JSON.parse(read('src/rule-components.json'));
396
+ assert.ok(registry['model-call-cost'], 'the rule must be in the canonical registry');
397
+ assert.equal(Number(rules[1]), Object.keys(registry).length,
398
+ 'the status line would report a phantom missing rule: ' + rules[1]);
399
+ assert.equal(Number(rules[1]), Object.keys(COMPONENTS.rules.items).length);
400
+ });
401
+
402
+ test('P21 - the mutation registry carries this feature\'s guards', () => {
403
+ const registry = JSON.parse(read('test/mutation-registry.json'));
404
+ const expected = {
405
+ 'cost-cannot-check-is-not-clean': 'templates/.claude/hooks/check-model-cost.cjs',
406
+ 'cost-unit-must-exist-for-the-caller': 'templates/.claude/hooks/check-model-cost.cjs',
407
+ 'cost-unset-ceiling-is-not-infinity': 'templates/.claude/hooks/check-model-cost.cjs',
408
+ 'cost-rule-limit-is-a-number-and-a-refusal': 'templates/.claude/rules/model-call-cost.md',
409
+ };
410
+ for (const [id, file] of Object.entries(expected)) {
411
+ const entry = registry.entries.find((e) => e.id === id);
412
+ assert.ok(entry, 'missing targeted mutation id ' + id);
413
+ assert.equal(entry.file, file, id + ' mutates the wrong surface');
414
+ assert.ok(entry.property && entry.property.length > 40,
415
+ id + ' needs a behavioral property, not a cosmetic label');
416
+ assert.ok(entry.mutation && entry.mutation.find && entry.mutation.replace,
417
+ id + ' needs a real source mutation');
418
+ assert.ok(Number.isInteger(entry.minFailing) && entry.minFailing >= 1,
419
+ id + ' must require at least one failing test');
420
+ // The anchor must be UNIQUE, or the registered mutation silently rewrites something else.
421
+ assert.equal(read(entry.file).split(entry.mutation.find).length - 1, 1,
422
+ id + ': mutation anchor must occur exactly once');
423
+ }
424
+ });
425
+ });
@@ -374,11 +374,16 @@ describe('the storage-port rule finally has a check that can fail', () => {
374
374
  'postgres', 'postgresql', 'pgvector', 'mysql', 'mariadb', 'percona', 'mongo', 'mongodb',
375
375
  'redis', 'valkey', 'keydb', 'elasticsearch', 'opensearch', 'minio', 'rabbitmq',
376
376
  'memcached', 'clickhouse', 'cassandra', 'scylla', 'neo4j', 'influxdb', 'timescaledb',
377
- 'mssql', 'sqlserver', 'couchdb', 'etcd',
377
+ 'mssql', 'sqlserver', 'couchdb', 'etcd', 'qdrant',
378
378
  ]);
379
379
  assert.deepEqual([...portsMatch[1].matchAll(/\d+/g)].map((match) => Number(match[0])), [
380
380
  5432, 3306, 27017, 6379, 9200, 9300, 5672, 11211, 9000, 8123, 1433, 9042, 7687,
381
- 8086, 2379, 5984,
381
+ 8086, 2379, 5984, 6333,
382
+ ]);
383
+ const proxiesMatch = source.match(/const PROXY_NAMES = \/\^\(([^)]+)\)\$\/i;/);
384
+ assert.ok(proxiesMatch, 'the existing PROXY_NAMES literal must remain readable');
385
+ assert.deepEqual(proxiesMatch[1].split('|'), [
386
+ 'caddy', 'nginx', 'traefik', 'haproxy', 'envoy', 'openresty',
382
387
  ]);
383
388
 
384
389
  const seen = [
@@ -576,6 +581,45 @@ describe('the storage-port rule finally has a check that can fail', () => {
576
581
  'the former scope-free receipt is not a machine-wide claim');
577
582
  });
578
583
 
584
+ test('P29 — qdrant exposure extends the recognized storage class', () => {
585
+ const qdrant = fakeDockerCheck({ compose: 'services:\n qdrant:\n'
586
+ + ' image: qdrant/qdrant:latest\n'
587
+ + ' ports:\n - target: 6333\n published: 6333\n' }, ['$PROJECT']);
588
+ assert.equal(qdrant.result.status, 1,
589
+ 'qdrant/qdrant:latest on 6333 must not retain the old green receipt: '
590
+ + qdrant.result.stdout + qdrant.result.stderr);
591
+ assert.match(qdrant.result.stdout, /qdrant/);
592
+ assert.match(qdrant.result.stdout, /6333/);
593
+ assert.doesNotMatch(qdrant.result.stdout, /✅/);
594
+ });
595
+
596
+ test('P30 — openresty makes a directly published app a proxy bypass', () => {
597
+ const bypass = fakeDockerCheck({ compose: 'services:\n gateway:\n'
598
+ + ' image: openresty/openresty:alpine\n'
599
+ + ' ports:\n - target: 80\n published: 80\n'
600
+ + ' app:\n image: node:22\n'
601
+ + ' ports:\n - target: 3000\n published: 3000\n' }, ['$PROJECT']);
602
+ assert.equal(bypass.result.status, 1,
603
+ 'OpenResty must establish the reverse-proxy class before bypass analysis: '
604
+ + bypass.result.stdout + bypass.result.stderr);
605
+ assert.match(bypass.result.stdout, /app/);
606
+ assert.match(bypass.result.stdout, /reverse-proxy.*gateway/i);
607
+ assert.doesNotMatch(bypass.result.stdout, /✅/);
608
+ });
609
+
610
+ test('P31 — honest exit 0 receipt counts scope and names an unchecked published app', () => {
611
+ const catalog = fakeDockerCheck({ compose: 'services:\n db:\n image: postgres:16\n'
612
+ + ' web:\n image: node:22\n'
613
+ + ' ports:\n - target: 3000\n published: 3000\n' }, ['$PROJECT']);
614
+ assert.equal(catalog.result.status, 0, catalog.result.stdout + catalog.result.stderr);
615
+ assert.match(catalog.result.stdout,
616
+ /из 2 сервисов распознано хранилищ 1, reverse-proxy 0/i);
617
+ assert.match(catalog.result.stdout,
618
+ /НЕ распознаны и не проверялись: web \(порт 3000 наружу\)/i);
619
+ assert.doesNotMatch(catalog.result.stdout, /ни одно хранилище не публикует/i,
620
+ 'a roster-scoped check must not certify an absolute class claim');
621
+ });
622
+
579
623
  test('P19 — catalog mode preserves P12 and never invokes machine or runtime Docker commands',
580
624
  { skip: !COMPOSE }, () => {
581
625
  const absent = check('services:\n cache:\n image: redis:7\n');