@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.
- package/.dz-manifest.json +225 -61
- package/CHANGELOG.md +148 -1
- package/LICENSE +21 -0
- package/MULTIPLATFORM_ROADMAP.md +1 -1
- package/README/eng/01_quickstart.md +2 -2
- package/README/eng/02_user_guide.md +1 -1
- package/README/eng/03_admin_guide.md +2 -2
- package/README/eng/05_architecture.md +1 -1
- package/README/eng/README.md +2 -1
- package/README/ru/01_quickstart.md +2 -2
- package/README/ru/02_user_guide.md +1 -1
- package/README/ru/03_admin_guide.md +2 -2
- package/README/ru/05_architecture.md +1 -1
- package/README/ru/README.md +2 -1
- package/README/ru/html/index.html +8 -8
- package/README.md +132 -9
- package/bin/cli.js +0 -0
- package/package.json +10 -11
- package/sbom.json +470 -60
- package/scripts/check-pipeline-gaps.sh +0 -0
- package/src/commands/init.js +1 -1
- package/src/rule-components.json +5 -1
- package/src/utils.js +32 -3
- package/templates/.claude/agents/product-discoverer.md +38 -0
- package/templates/.claude/agents/replicate-coordinator.md +11 -1
- package/templates/.claude/commands/feature.md +29 -5
- package/templates/.claude/commands/go.md +6 -8
- package/templates/.claude/commands/harvest.md +5 -7
- package/templates/.claude/commands/replicate.md +169 -44
- package/templates/.claude/commands/start.md +28 -7
- 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 +27 -6
- 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/statusline.cjs +2 -2
- package/templates/.claude/rules/embeddable-widget.md +73 -0
- package/templates/.claude/rules/feature-lifecycle.md +5 -6
- package/templates/.claude/rules/incoming-webhooks.md +99 -0
- 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 +121 -52
- 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/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 +173 -725
- package/tests/snapshot/baseline.json +60 -38
- 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-ports.test.js +46 -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 +8 -2
- package/tests/unit/detection-ladder-registry.test.js +2 -2
- 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 +91 -9
- 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/utils.test.js +40 -3
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The deterministic half of the embeddable-widget 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. A widget runs on SOMEONE ELSE'S page, and the three ways it dies there
|
|
7
|
+
// all have their conditions on that page: the client's browser refuses the cross-origin call, the
|
|
8
|
+
// host's CSS reaches into the widget, the host's Content-Security-Policy refuses the script. Test on
|
|
9
|
+
// your own page and none of the three CAN occur — same-origin has no preflight, your page ships no
|
|
10
|
+
// hostile CSS and no CSP. So a fully green check at home and an outage at the client are not a
|
|
11
|
+
// contradiction: they are the same measurement of the wrong page.
|
|
12
|
+
//
|
|
13
|
+
// Three sides are covered here, deliberately:
|
|
14
|
+
// 1. BEHAVIOUR — the real utility, real files, real exit codes (P1-P17)
|
|
15
|
+
// 2. FORCE — the rule's text still MANDATES, not suggests (P18-P19); 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
|
+
// 3. WIRING — the seam, the counters and the mutation registry agree (P20-P22)
|
|
19
|
+
|
|
20
|
+
const { test, describe } = require('node:test');
|
|
21
|
+
const assert = require('node:assert/strict');
|
|
22
|
+
const { spawnSync } = require('node:child_process');
|
|
23
|
+
const fs = require('node:fs');
|
|
24
|
+
const os = require('node:os');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
|
|
27
|
+
const PKG = path.resolve(__dirname, '..', '..');
|
|
28
|
+
const TPL = path.join(PKG, 'templates', '.claude');
|
|
29
|
+
const CHECK = path.join(TPL, 'hooks', 'check-embed-contract.cjs');
|
|
30
|
+
const CONTRACT = 'docs/embed-contract.md';
|
|
31
|
+
|
|
32
|
+
const read = (rel) => fs.readFileSync(path.join(PKG, rel), 'utf8');
|
|
33
|
+
|
|
34
|
+
/** Build a throwaway project and run the REAL checker over it. */
|
|
35
|
+
function check(files) {
|
|
36
|
+
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-embed-')));
|
|
37
|
+
try {
|
|
38
|
+
for (const [rel, body] of Object.entries(files || {})) {
|
|
39
|
+
const abs = path.join(dir, rel);
|
|
40
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
41
|
+
fs.writeFileSync(abs, body);
|
|
42
|
+
}
|
|
43
|
+
const r = spawnSync(process.execPath, [CHECK, dir], { encoding: 'utf8' });
|
|
44
|
+
return { code: r.status, out: (r.stdout || '') + (r.stderr || '') };
|
|
45
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const WIDGET = 'https://widget.example.com';
|
|
49
|
+
const HOST = 'http://localhost:8099';
|
|
50
|
+
const CLASSES = ['перекрёстный-запрос', 'протечка-стилей', 'политика-безопасности'];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A contract in whatever state the case needs.
|
|
54
|
+
*
|
|
55
|
+
* Defaults are the HEALTHY ones so that every case stays about the single thing it changes; a case
|
|
56
|
+
* that wants silence passes `null` for that field, and silence is asserted as its own outcome.
|
|
57
|
+
*/
|
|
58
|
+
function contract({
|
|
59
|
+
embeddable = 'да', widget = WIDGET, host = HOST, credentials = 'нет',
|
|
60
|
+
allowed = 'https://client-one.example', run = 'ВЫПОЛНЕНА', reason = null,
|
|
61
|
+
rows = CLASSES.map((name) => ({ name, status: 'ПРОВЕРЕН', evidence: HOST + '/host.html, 2026-09-01' })),
|
|
62
|
+
} = {}) {
|
|
63
|
+
const line = (label, value) => (value === null ? '' : '**' + label + ':** ' + value + '\n');
|
|
64
|
+
return '# Embed contract\n\n'
|
|
65
|
+
+ line('Встраиваемый виджет', embeddable)
|
|
66
|
+
+ line('Origin виджета', widget)
|
|
67
|
+
+ line('Origin хозяйской страницы', host)
|
|
68
|
+
+ line('Учётные данные', credentials)
|
|
69
|
+
+ line('Разрешённые origin', allowed)
|
|
70
|
+
+ line('Проверка на чужой странице', run)
|
|
71
|
+
+ line('Причина', reason)
|
|
72
|
+
+ '\n## Классы отказа\n\n'
|
|
73
|
+
+ '| Класс | Статус | Признак | Лечение | Доказательство |\n'
|
|
74
|
+
+ '|---|---|---|---|---|\n'
|
|
75
|
+
+ rows.map((r) => '| ' + r.name + ' | ' + r.status + ' | консоль клиента | изоляция | '
|
|
76
|
+
+ r.evidence + ' |').join('\n') + (rows.length ? '\n' : '');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe('the embed-contract checker answers three questions and never confuses two of them', () => {
|
|
80
|
+
test('P1 - a widget proven on a foreign origin is exit 0, and both origins are printed', () => {
|
|
81
|
+
const r = check({ [CONTRACT]: contract() });
|
|
82
|
+
assert.equal(r.code, 0, r.out);
|
|
83
|
+
assert.match(r.out, /все 3 классов отказа проверены на чужом origin/, r.out);
|
|
84
|
+
assert.match(r.out, /http:\/\/localhost:8099/, 'the foreign origin must be named: ' + r.out);
|
|
85
|
+
assert.match(r.out, /https:\/\/widget\.example\.com/, r.out);
|
|
86
|
+
// The receipt must state what it does NOT prove, or a reader upgrades it into a rendering proof.
|
|
87
|
+
assert.match(r.out, /Ограничение:/, r.out);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('P2 - no contract at all is exit 2, and the hint refuses the wrong reading', () => {
|
|
91
|
+
const r = check({});
|
|
92
|
+
assert.equal(r.code, 2, r.out);
|
|
93
|
+
assert.match(r.out, /нет файла docs\/embed-contract\.md/, r.out);
|
|
94
|
+
// «no file» means the QUESTION was never asked — never «there is no widget».
|
|
95
|
+
assert.match(r.out, /НЕ ЗАДАВАЛСЯ/, r.out);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('P3 - «виджет: нет» is a legitimate answer and exits 2, never 0', () => {
|
|
99
|
+
const r = check({ [CONTRACT]: contract({ embeddable: 'нет' }) });
|
|
100
|
+
assert.equal(r.code, 2, 'a product that does not embed has nothing to check: ' + r.out);
|
|
101
|
+
assert.match(r.out, /законный ответ/, r.out);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('P4 - a missing or unrecognised declaration is exit 2 with the closed set printed', () => {
|
|
105
|
+
const absent = check({ [CONTRACT]: contract({ embeddable: null }) });
|
|
106
|
+
assert.equal(absent.code, 2, absent.out);
|
|
107
|
+
assert.match(absent.out, /Встраиваемый виджет/, absent.out);
|
|
108
|
+
|
|
109
|
+
for (const bad of ['возможно', '', 'yes']) {
|
|
110
|
+
const r = check({ [CONTRACT]: contract({ embeddable: bad }) });
|
|
111
|
+
assert.equal(r.code, 2, '«' + bad + '» must not be read as a verdict: ' + r.out);
|
|
112
|
+
assert.match(r.out, /допустимы ровно/, r.out);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('P5 - «НЕ ВЫПОЛНЕНА» with a closed-list reason is honest UNKNOWN, exit 2', () => {
|
|
117
|
+
const ok = check({ [CONTRACT]: contract({ run: 'НЕ ВЫПОЛНЕНА', reason: 'no-host-page' }) });
|
|
118
|
+
assert.equal(ok.code, 2, ok.out);
|
|
119
|
+
assert.match(ok.out, /НЕ ВЫПОЛНЕНА, причина: no-host-page/, ok.out);
|
|
120
|
+
assert.match(ok.out, /честное «неизвестно»/, ok.out);
|
|
121
|
+
|
|
122
|
+
// A missing reason and a free-text reason are DIFFERENT mistakes with different repairs.
|
|
123
|
+
const none = check({ [CONTRACT]: contract({ run: 'НЕ ВЫПОЛНЕНА', reason: null }) });
|
|
124
|
+
assert.equal(none.code, 2, none.out);
|
|
125
|
+
assert.match(none.out, /без строки `\*\*Причина:\*\*`/, none.out);
|
|
126
|
+
|
|
127
|
+
for (const bad of ['было некогда', 'no-host-pages', 'no-browser и not-deployed']) {
|
|
128
|
+
const r = check({ [CONTRACT]: contract({ run: 'НЕ ВЫПОЛНЕНА', reason: bad }) });
|
|
129
|
+
assert.equal(r.code, 2, r.out);
|
|
130
|
+
assert.match(r.out, /не из закрытого списка/, '«' + bad + '»: ' + r.out);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('P6 - LOAD-BEARING: a check on your OWN origin is a PROVEN defect, exit 1', () => {
|
|
135
|
+
const r = check({ [CONTRACT]: contract({ host: WIDGET + '/demo.html' }) });
|
|
136
|
+
assert.equal(r.code, 1, 'same-origin must not read as verified: ' + r.out);
|
|
137
|
+
assert.match(r.out, /на СВОЁМ origin/, r.out);
|
|
138
|
+
// The reason must be the MECHANISM, not a scolding: none of the three failures can occur there.
|
|
139
|
+
assert.match(r.out, /НИ ОДИН из трёх классов отказа/, r.out);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('P7 - a different PORT is already a foreign origin, and stays reachable', () => {
|
|
143
|
+
// The exception must remain usable, or the check degenerates into a permanent 1 nobody can
|
|
144
|
+
// clear: an honest host fixture on localhost:8099 against a widget on localhost:3000 does
|
|
145
|
+
// trigger a real preflight, and that is the browser's own definition of another site.
|
|
146
|
+
const r = check({
|
|
147
|
+
[CONTRACT]: contract({
|
|
148
|
+
widget: 'http://localhost:3000', host: 'http://localhost:8099',
|
|
149
|
+
rows: CLASSES.map((name) => ({ name, status: 'ПРОВЕРЕН', evidence: 'http://localhost:8099/host.html' })),
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
152
|
+
assert.equal(r.code, 0, r.out);
|
|
153
|
+
|
|
154
|
+
// ...and the default port is normalised, so https://x:443 is not a second origin.
|
|
155
|
+
const same = check({
|
|
156
|
+
[CONTRACT]: contract({
|
|
157
|
+
widget: 'https://widget.example.com', host: 'https://Widget.example.com:443/host.html',
|
|
158
|
+
}),
|
|
159
|
+
});
|
|
160
|
+
assert.equal(same.code, 1, 'the default port must not manufacture a foreign origin: ' + same.out);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('P8 - a file:// page is the wrong instrument, exit 1', () => {
|
|
164
|
+
const r = check({ [CONTRACT]: contract({ host: 'file:///tmp/host.html' }) });
|
|
165
|
+
assert.equal(r.code, 1, r.out);
|
|
166
|
+
assert.match(r.out, /file:\/\//, r.out);
|
|
167
|
+
assert.match(r.out, /origin равен null/, 'the reason must be the mechanism: ' + r.out);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('P9 - a class left out of the table is a PROVEN omission and is NAMED', () => {
|
|
171
|
+
for (const dropped of CLASSES) {
|
|
172
|
+
const rows = CLASSES.filter((n) => n !== dropped)
|
|
173
|
+
.map((name) => ({ name, status: 'ПРОВЕРЕН', evidence: HOST + '/host.html' }));
|
|
174
|
+
const r = check({ [CONTRACT]: contract({ rows }) });
|
|
175
|
+
assert.equal(r.code, 1, dropped + ' silently missing: ' + r.out);
|
|
176
|
+
assert.match(r.out, new RegExp(dropped), 'the missing class must be named: ' + r.out);
|
|
177
|
+
}
|
|
178
|
+
const empty = check({ [CONTRACT]: contract({ rows: [] }) });
|
|
179
|
+
assert.equal(empty.code, 1, empty.out);
|
|
180
|
+
assert.match(empty.out, /3 из 3/, empty.out);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('P10 - «НЕ ПРОВЕРЕН» under a declared ВЫПОЛНЕНА is a contradiction, exit 1', () => {
|
|
184
|
+
const rows = CLASSES.map((name, i) => ({
|
|
185
|
+
name, status: i === 2 ? 'НЕ ПРОВЕРЕН' : 'ПРОВЕРЕН', evidence: HOST + '/host.html',
|
|
186
|
+
}));
|
|
187
|
+
const r = check({ [CONTRACT]: contract({ rows }) });
|
|
188
|
+
assert.equal(r.code, 1, r.out);
|
|
189
|
+
assert.match(r.out, /политика-безопасности/, r.out);
|
|
190
|
+
assert.match(r.out, /ложная квитанция/, r.out);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('P11 - «ПРОВЕРЕН» whose proof names NO address is exit 1', () => {
|
|
194
|
+
// The kinship with the measured deployment finding: a check must use the address the system
|
|
195
|
+
// ISSUED. Without an address, "checked at the client" and "checked at home" are written the same.
|
|
196
|
+
const rows = CLASSES.map((name) => ({ name, status: 'ПРОВЕРЕН', evidence: 'проверено вручную' }));
|
|
197
|
+
const r = check({ [CONTRACT]: contract({ rows }) });
|
|
198
|
+
assert.equal(r.code, 1, r.out);
|
|
199
|
+
assert.match(r.out, /не называет адрес/, r.out);
|
|
200
|
+
for (const name of CLASSES) assert.match(r.out, new RegExp(name), r.out);
|
|
201
|
+
|
|
202
|
+
// The shipped template's bracketed placeholder counts as NO proof, never as a filled one.
|
|
203
|
+
const tpl = check({
|
|
204
|
+
[CONTRACT]: contract({ rows: CLASSES.map((name) => ({ name, status: 'ПРОВЕРЕН', evidence: '[адрес]' })) }),
|
|
205
|
+
});
|
|
206
|
+
assert.equal(tpl.code, 1, 'an untouched template must not pass: ' + tpl.out);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('P12 - a proof pointing back at the widget\'s own origin is exit 1', () => {
|
|
210
|
+
const rows = CLASSES.map((name, i) => ({
|
|
211
|
+
name, status: 'ПРОВЕРЕН',
|
|
212
|
+
evidence: (i === 1 ? WIDGET + '/demo.html' : HOST + '/host.html'),
|
|
213
|
+
}));
|
|
214
|
+
const r = check({ [CONTRACT]: contract({ rows }) });
|
|
215
|
+
assert.equal(r.code, 1, r.out);
|
|
216
|
+
assert.match(r.out, /указывает на СВОЙ origin/, r.out);
|
|
217
|
+
assert.match(r.out, /протечка-стилей/, 'only the offending class is named: ' + r.out);
|
|
218
|
+
assert.doesNotMatch(r.out, /• перекрёстный-запрос/, r.out);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('P13 - credentials together with `*` is a pair the browser itself refuses, exit 1', () => {
|
|
222
|
+
const r = check({ [CONTRACT]: contract({ credentials: 'да', allowed: '*' }) });
|
|
223
|
+
assert.equal(r.code, 1, r.out);
|
|
224
|
+
assert.match(r.out, /браузер отвергает сам/, r.out);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('P14 - BOUNDED: a wildcard WITHOUT credentials is legal and must stay exit 0', () => {
|
|
228
|
+
// Widening this to "any wildcard is a defect" would refuse a legitimate public read-only widget,
|
|
229
|
+
// and a check that refuses the correct configuration gets switched off.
|
|
230
|
+
const r = check({ [CONTRACT]: contract({ credentials: 'нет', allowed: '*' }) });
|
|
231
|
+
assert.equal(r.code, 0, r.out);
|
|
232
|
+
const named = check({ [CONTRACT]: contract({ credentials: 'да', allowed: 'https://client-one.example' }) });
|
|
233
|
+
assert.equal(named.code, 0, named.out);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('P15 - an absent or unparseable origin is «could not check», exit 2', () => {
|
|
237
|
+
for (const field of ['widget', 'host']) {
|
|
238
|
+
const missing = check({ [CONTRACT]: contract({ [field]: null }) });
|
|
239
|
+
assert.equal(missing.code, 2, field + ' missing: ' + missing.out);
|
|
240
|
+
const junk = check({ [CONTRACT]: contract({ [field]: 'наш стенд' }) });
|
|
241
|
+
assert.equal(junk.code, 2, field + ' unparseable: ' + junk.out);
|
|
242
|
+
assert.match(junk.out, /не разбирается как адрес/, junk.out);
|
|
243
|
+
}
|
|
244
|
+
const noAllowed = check({ [CONTRACT]: contract({ allowed: null }) });
|
|
245
|
+
assert.equal(noAllowed.code, 2, noAllowed.out);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('P16 - a malformed table is exit 2, never a pass and never a proven loss', () => {
|
|
249
|
+
const bogus = check({
|
|
250
|
+
[CONTRACT]: contract({
|
|
251
|
+
rows: CLASSES.map((name) => ({ name, status: 'ок', evidence: HOST + '/h.html' })),
|
|
252
|
+
}),
|
|
253
|
+
});
|
|
254
|
+
assert.equal(bogus.code, 2, bogus.out);
|
|
255
|
+
assert.match(bogus.out, /нераспознанный статус класса/, bogus.out);
|
|
256
|
+
|
|
257
|
+
const dupe = check({
|
|
258
|
+
[CONTRACT]: contract({
|
|
259
|
+
rows: [...CLASSES, 'протечка-стилей']
|
|
260
|
+
.map((name) => ({ name, status: 'ПРОВЕРЕН', evidence: HOST + '/h.html' })),
|
|
261
|
+
}),
|
|
262
|
+
});
|
|
263
|
+
assert.equal(dupe.code, 2, dupe.out);
|
|
264
|
+
assert.match(dupe.out, /повторяются строки/, dupe.out);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('P17 - cannot-check is NEVER clean: a bad path exits 2, not 0', () => {
|
|
268
|
+
const r = spawnSync(process.execPath, [CHECK, path.join(os.tmpdir(), 'p-rep-embed-absent-dir')],
|
|
269
|
+
{ encoding: 'utf8' });
|
|
270
|
+
assert.equal(r.status, 2, r.stdout + r.stderr);
|
|
271
|
+
assert.match(r.stdout, /проверка НЕ выполнена/, r.stdout);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The rule's FORCE, kept apart from its vocabulary.
|
|
277
|
+
*
|
|
278
|
+
* MEASURED on this package 2026-09-01 (commit 37916bd3): a suite of twelve keyword predicates over
|
|
279
|
+
* the swarm receipt contract was fully satisfied by a block retitled "Coordination note" whose
|
|
280
|
+
* imperative had been softened to "Where convenient" — the registered mutation SURVIVED. These
|
|
281
|
+
* predicates are that lesson applied here: the heading must still refuse, the obligation must still
|
|
282
|
+
* read as a duty, and the closed set must still be mandatory.
|
|
283
|
+
*/
|
|
284
|
+
function ruleForceProblems(text) {
|
|
285
|
+
const problems = [];
|
|
286
|
+
if (!/^## Своя страница — не проверка$/m.test(text)) {
|
|
287
|
+
problems.push('the heading no longer refuses the own-page check');
|
|
288
|
+
}
|
|
289
|
+
if (!/ОБЯЗАНА выполняться на странице ЧУЖОГО origin/.test(text)) {
|
|
290
|
+
problems.push('the obligation is advisory, not imperative');
|
|
291
|
+
}
|
|
292
|
+
if (!/«Открыли свою демо-страницу[^»]*»\s*—\s*НЕ проверка/.test(text)) {
|
|
293
|
+
problems.push('the own-page demo is not named insufficient');
|
|
294
|
+
}
|
|
295
|
+
if (!/Набор ЗАКРЫТЫЙ и ОБЯЗАТЕЛЬНЫЙ/.test(text)) {
|
|
296
|
+
problems.push('the three classes are a menu, not a mandatory set');
|
|
297
|
+
}
|
|
298
|
+
// Each class must arrive with the symptom the CLIENT sees — a class named without its признак is
|
|
299
|
+
// a label, and a label cannot be recognised in the field.
|
|
300
|
+
for (const [cls, sign] of [['перекрёстный-запрос', /blocked by CORS policy/],
|
|
301
|
+
['протечка-стилей', /z-index/], ['политика-безопасности', /Refused to load/]]) {
|
|
302
|
+
if (!new RegExp('`' + cls + '`').test(text)) problems.push(cls + ' is not named');
|
|
303
|
+
if (!sign.test(text)) problems.push(cls + ' carries no client-side symptom');
|
|
304
|
+
}
|
|
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('P18 - the shipped rule satisfies every force predicate', () => {
|
|
316
|
+
assert.deepEqual(ruleForceProblems(read('templates/.claude/rules/embeddable-widget.md')), []);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('P19 - MUTATION: a polite rephrasing must turn this suite red', () => {
|
|
320
|
+
const source = read('templates/.claude/rules/embeddable-widget.md');
|
|
321
|
+
|
|
322
|
+
// Retitle only. Every keyword in the file survives; only the refusal in the heading is gone.
|
|
323
|
+
const retitled = source.replace('## Своя страница — не проверка', '## Замечание о демо-странице');
|
|
324
|
+
assert.notEqual(retitled, source, 'mutation fixture did not apply — the heading moved');
|
|
325
|
+
assert.deepEqual(ruleForceProblems(retitled),
|
|
326
|
+
['the heading no longer refuses the own-page check'],
|
|
327
|
+
'a retitled block must fire EXACTLY the heading predicate and nothing else');
|
|
328
|
+
|
|
329
|
+
// Soften «обязана» to «желательно». This is the mutation that survived on the swarm contract.
|
|
330
|
+
const softened = source.replace('ОБЯЗАНА выполняться на странице ЧУЖОГО origin',
|
|
331
|
+
'желательно выполнять на странице чужого origin');
|
|
332
|
+
assert.notEqual(softened, source, 'mutation fixture did not apply — the sentence moved');
|
|
333
|
+
assert.deepEqual(ruleForceProblems(softened),
|
|
334
|
+
['the obligation is advisory, not imperative'],
|
|
335
|
+
'softening the duty must fire EXACTLY the mandate predicate');
|
|
336
|
+
|
|
337
|
+
// Downgrade the closed set to a suggestion.
|
|
338
|
+
const optional = source.replace('Набор ЗАКРЫТЫЙ и ОБЯЗАТЕЛЬНЫЙ',
|
|
339
|
+
'Набор рекомендуемый');
|
|
340
|
+
assert.deepEqual(ruleForceProblems(optional),
|
|
341
|
+
['the three classes are a menu, not a mandatory set'],
|
|
342
|
+
'a downgraded set must fire EXACTLY its own predicate');
|
|
343
|
+
|
|
344
|
+
// And deleting the layer honesty must be observable on its own.
|
|
345
|
+
const dishonest = source.replace('НЕ МОЖЕТ по названной причине', 'пока не сделана');
|
|
346
|
+
assert.deepEqual(ruleForceProblems(dishonest), ['the layer marking hides what stays judgment']);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('P20 - Phase 2 of /replicate calls the check as an acceptance criterion', () => {
|
|
350
|
+
const cmd = read('templates/.claude/commands/replicate.md');
|
|
351
|
+
assert.match(cmd, /node \.claude\/hooks\/check-embed-contract\.cjs \./,
|
|
352
|
+
'the pipeline must actually invoke it, not merely mention the rule');
|
|
353
|
+
assert.match(cmd, /\.claude\/rules\/embeddable-widget\.md/, 'the rule must be reachable from the seam');
|
|
354
|
+
// Exit 2 must be spelled out at the seam too, or the operator reads silence as success.
|
|
355
|
+
assert.match(cmd, /это НЕ «в порядке»/, cmd.slice(cmd.indexOf('check-embed-contract'), -1).slice(0, 600));
|
|
356
|
+
|
|
357
|
+
const rule = read('templates/.claude/rules/replicate-pipeline.md');
|
|
358
|
+
assert.match(rule, /check-embed-contract\.cjs/, 'the hook inventory must list it');
|
|
359
|
+
// DERIVED, never literal. Three hardcoded counts used to stand here, and a hardcoded count is
|
|
360
|
+
// exactly the shape that split in half on 2026-09-01: a number whose only guard is a reader.
|
|
361
|
+
// The shipped directory and the canonical registry cannot be wrong about their own size, so
|
|
362
|
+
// the inventory prose is compared against THEM — which is also what makes this assertion
|
|
363
|
+
// survive the next hook without an edit, instead of failing for being right.
|
|
364
|
+
const shippedRules = Object.keys(JSON.parse(read('src/rule-components.json'))).length;
|
|
365
|
+
const shippedHooks = fs.readdirSync(path.join(TPL, 'hooks')).filter((f) => f.endsWith('.cjs')).length;
|
|
366
|
+
const WIRED_TO_EVENTS = 4; // session-insights + the three autocommit hooks, per settings.json
|
|
367
|
+
assert.match(rule, new RegExp('\\*\\*Rules \\(' + shippedRules + '\\):\\*\\*'),
|
|
368
|
+
'the rule inventory must count what the registry actually carries (' + shippedRules + ')');
|
|
369
|
+
assert.match(rule, new RegExp('\\*\\*Hooks \\(' + shippedHooks + ' files'),
|
|
370
|
+
'the hook inventory must count what the package actually ships (' + shippedHooks + ')');
|
|
371
|
+
assert.match(rule, new RegExp('wired to nothing \\(' + (shippedHooks - WIRED_TO_EVENTS) + '\\):'),
|
|
372
|
+
'the deliberately-invoked count must equal the shipped total minus the four event hooks');
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test('P21 - 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-embed-contract'],
|
|
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-embed-contract'),
|
|
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
|
+
// The rule counter has the same three-way agreement, and this change moved it too.
|
|
394
|
+
const rules = statusline.match(/rulesExpected:\s*(\d+)/);
|
|
395
|
+
assert.ok(rules, 'statusline must declare rulesExpected');
|
|
396
|
+
const registry = JSON.parse(read('src/rule-components.json'));
|
|
397
|
+
assert.ok(registry['embeddable-widget'], 'the rule must be in the canonical registry');
|
|
398
|
+
assert.equal(Number(rules[1]), Object.keys(registry).length,
|
|
399
|
+
'the status line would report a phantom missing rule: ' + rules[1]);
|
|
400
|
+
assert.equal(Number(rules[1]), Object.keys(COMPONENTS.rules.items).length);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('P22 - the mutation registry carries this feature\'s guards', () => {
|
|
404
|
+
const registry = JSON.parse(read('test/mutation-registry.json'));
|
|
405
|
+
const expected = {
|
|
406
|
+
'embed-cannot-check-is-not-clean': 'templates/.claude/hooks/check-embed-contract.cjs',
|
|
407
|
+
'embed-own-origin-is-not-a-foreign-page': 'templates/.claude/hooks/check-embed-contract.cjs',
|
|
408
|
+
'embed-rule-own-page-is-not-a-check': 'templates/.claude/rules/embeddable-widget.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
|
+
}
|
|
421
|
+
});
|
|
422
|
+
});
|