@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,287 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// THE DEFECT THIS SUITE CLOSES.
|
|
4
|
+
//
|
|
5
|
+
// A worker that died looks exactly like a worker that is still running: both are silent. Silence
|
|
6
|
+
// therefore reads as "in progress", and a coordinator can report in good faith that a review is
|
|
7
|
+
// running when no review exists — the absence of a receipt is indistinguishable from unfinished
|
|
8
|
+
// work, so it reads as progress. The cure is to declare the RESULT of every parallel unit to be a
|
|
9
|
+
// FILE at a named path: no file, no work, and a machine can say so.
|
|
10
|
+
//
|
|
11
|
+
// MEASURED 2026-09-01, before this change:
|
|
12
|
+
// templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle.md
|
|
13
|
+
// — 22 matches for parallel-agent dispatch, 0 matches for a file-result requirement.
|
|
14
|
+
// Reproducer: `grep -cE 'parallel|swarm|subagent|Task\(|Task tool' <file>` -> 22
|
|
15
|
+
// `grep -c 'TRACE_PATH' <file>` -> 0
|
|
16
|
+
//
|
|
17
|
+
// The package had ALREADY cured itself: templates/.claude/rules/swarm-file-evidence.md, cited by
|
|
18
|
+
// nine of its own files. What it had not done was put the cure in the GENERATORS — the skills whose
|
|
19
|
+
// output is written into somebody else's project. So every toolkit this package generates inherited
|
|
20
|
+
// a defect its author had already fixed at home. tests/unit/honest-failure-rules.test.js P6 named
|
|
21
|
+
// that gap honestly as deferred; this suite is what makes the deferral over.
|
|
22
|
+
//
|
|
23
|
+
// Three sides, deliberately:
|
|
24
|
+
// 1. IDENTITY — the rule the generator emits is byte-identical to the pre-shipped one.
|
|
25
|
+
// 2. CONTRACT — every generator artifact that dispatches parallel agents carries the contract,
|
|
26
|
+
// proved by the same twelve-predicate checker the first-party seams use.
|
|
27
|
+
// 3. DISCOVERY — no generator file may dispatch parallel agents and be absent from the inventory.
|
|
28
|
+
// Without this, the next uncured generator file ships in silence.
|
|
29
|
+
|
|
30
|
+
const { test, describe } = require('node:test');
|
|
31
|
+
const assert = require('node:assert/strict');
|
|
32
|
+
const fs = require('node:fs');
|
|
33
|
+
const path = require('node:path');
|
|
34
|
+
|
|
35
|
+
const PKG = path.resolve(__dirname, '..', '..');
|
|
36
|
+
const TPL = path.join(PKG, 'templates', '.claude');
|
|
37
|
+
const SKILLS = path.join(TPL, 'skills');
|
|
38
|
+
|
|
39
|
+
/** The skills whose OUTPUT is instructions written into a project this package did not create. */
|
|
40
|
+
const GENERATOR_SKILLS = ['cc-toolkit-generator-enhanced', 'pipeline-forge'];
|
|
41
|
+
|
|
42
|
+
/** The emitted-rule template and the pre-shipped rule it must reproduce verbatim. */
|
|
43
|
+
const RULE = path.join(TPL, 'rules', 'swarm-file-evidence.md');
|
|
44
|
+
const RULE_TEMPLATE = path.join(SKILLS, 'cc-toolkit-generator-enhanced',
|
|
45
|
+
'references', 'templates', 'swarm-file-evidence.md');
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A generator file that DISPATCHES parallel agents. Deliberately broader than the seam list: this
|
|
49
|
+
* is the discovery net, and a net that only matches what we already fixed would catch nothing.
|
|
50
|
+
*/
|
|
51
|
+
const DISPATCH = /Task tool|Task\(|[Ss]pawn (?:parallel|concurrent|one|specialized|multiple)|parallel (?:agents?|Task|validators?|tasks)|parallel Tasks/;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The inventory, split by how much of the contract each file must carry.
|
|
55
|
+
*
|
|
56
|
+
* `full` — the file IS a dispatch instruction (or the template of one), so it must stand alone:
|
|
57
|
+
* somebody reads only this file and runs a swarm from it.
|
|
58
|
+
* `pointer` — the file SUMMARISES or ORCHESTRATES; it must name the rule and the two silences, so a
|
|
59
|
+
* reader knows a contract exists and where it lives, without paying for it twice.
|
|
60
|
+
*
|
|
61
|
+
* The distinction is not cosmetic. Requiring the full block in an index would spend the
|
|
62
|
+
* always-loaded budget on repetition; accepting a pointer in a command template would ship a
|
|
63
|
+
* dispatch instruction whose contract lives somewhere the executing agent never opens.
|
|
64
|
+
*/
|
|
65
|
+
const GENERATOR_SEAMS = [
|
|
66
|
+
['cc-toolkit-generator-enhanced/references/templates/feature-lifecycle.md', 'full'],
|
|
67
|
+
['cc-toolkit-generator-enhanced/references/templates/feature-lifecycle-ent.md', 'full'],
|
|
68
|
+
['cc-toolkit-generator-enhanced/references/templates/start-command.md', 'full'],
|
|
69
|
+
['cc-toolkit-generator-enhanced/references/templates/automation-commands.md', 'full'],
|
|
70
|
+
['cc-toolkit-generator-enhanced/references/templates/swarm-file-evidence.md', 'full'],
|
|
71
|
+
['pipeline-forge/references/patterns-catalog.md', 'full'],
|
|
72
|
+
['cc-toolkit-generator-enhanced/SKILL.md', 'pointer'],
|
|
73
|
+
['cc-toolkit-generator-enhanced/references/claude-md-strategy.md', 'pointer'],
|
|
74
|
+
['cc-toolkit-generator-enhanced/modules/03-generate-p0.md', 'pointer'],
|
|
75
|
+
['cc-toolkit-generator-enhanced/modules/04-generate-p1.md', 'pointer'],
|
|
76
|
+
['cc-toolkit-generator-enhanced/modules/06-package-deliver.md', 'pointer'],
|
|
77
|
+
['pipeline-forge/SKILL.md', 'pointer'],
|
|
78
|
+
['pipeline-forge/references/self-extracted-patterns.md', 'pointer'],
|
|
79
|
+
['pipeline-forge/examples/replicate-analysis.md', 'pointer'],
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const read = (rel) => fs.readFileSync(path.join(SKILLS, rel), 'utf8');
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The twelve predicates of the file-receipt contract, in the same shape
|
|
86
|
+
* tests/unit/honest-failure-rules.test.js applies to the eight first-party seams. Kept as a local
|
|
87
|
+
* copy on purpose: importing a `.test.js` would execute its suite, and a shared helper module for
|
|
88
|
+
* two callers is a worse trade than a duplicated function that both sides can mutate against.
|
|
89
|
+
*/
|
|
90
|
+
function swarmContractProblems(text) {
|
|
91
|
+
const problems = [];
|
|
92
|
+
// MEASURED 2026-09-01: the twelve keyword predicates below are all satisfied by a block retitled
|
|
93
|
+
// "Coordination note" whose opening imperative was softened to "Where convenient". A registered
|
|
94
|
+
// mutation doing exactly that SURVIVED the first version of this suite — so the vocabulary of the
|
|
95
|
+
// contract was tested and its FORCE was not. These two predicates are that gap closed: the
|
|
96
|
+
// heading must say the block is required, and the worker obligation must read `must`, never a
|
|
97
|
+
// suggestion. An optional receipt is not a receipt.
|
|
98
|
+
if (!/Positive file receipt \(required\)/.test(text)) {
|
|
99
|
+
problems.push('the receipt block is not titled as required');
|
|
100
|
+
}
|
|
101
|
+
if (!/must write[\s\S]{0,160}TRACE_PATH/i.test(text)) {
|
|
102
|
+
problems.push('the worker obligation is advisory, not imperative');
|
|
103
|
+
}
|
|
104
|
+
if (!/WORK_UNIT_ID/.test(text)) problems.push('dispatch does not name WORK_UNIT_ID');
|
|
105
|
+
if (!/TRACE_PATH/.test(text)) problems.push('dispatch does not name TRACE_PATH');
|
|
106
|
+
if (!/absolute/i.test(text)) problems.push('TRACE_PATH is not resolved as absolute');
|
|
107
|
+
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)) {
|
|
108
|
+
problems.push('trace path is not unique per work unit');
|
|
109
|
+
}
|
|
110
|
+
const write = text.search(/write[\s\S]{0,100}TRACE_PATH/i);
|
|
111
|
+
const reportAfterWrite = write < 0 ? -1 : text.slice(write)
|
|
112
|
+
.search(/one-line[\s\S]{0,40}(?:pointer|report)|(?:pointer|report)[\s\S]{0,40}one-line/i);
|
|
113
|
+
if (write < 0 || reportAfterWrite < 0) problems.push('worker does not write before report');
|
|
114
|
+
const required = [
|
|
115
|
+
[/regular/i, 'regular-file check missing'],
|
|
116
|
+
[/non-symlink|not a symlink/i, 'symlink refusal missing'],
|
|
117
|
+
[/non-(?:empty|whitespace)|substantive/i, 'substantive-body check missing'],
|
|
118
|
+
[/post-launch|after[\s\S]{0,40}launch|pre-dispatch/i, 'freshness boundary missing'],
|
|
119
|
+
[/Status: completed/, 'completed terminal marker missing'],
|
|
120
|
+
[/Status: failed/, 'failed terminal marker missing'],
|
|
121
|
+
[/(?:narrative|chat)[\s\S]{0,80}(?:not|never)[\s\S]{0,40}(?:receipt|result)|silence[\s\S]{0,80}(?:not|never)/i,
|
|
122
|
+
'narrative or silence can still act as a receipt'],
|
|
123
|
+
[/(?:refuse|block|must not)[\s\S]{0,100}(?:merge|synthesis|aggregation|completion)/i,
|
|
124
|
+
'invalid receipt does not block aggregation'],
|
|
125
|
+
];
|
|
126
|
+
for (const [pattern, message] of required) if (!pattern.test(text)) problems.push(message);
|
|
127
|
+
return problems;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* What a POINTER owes its reader: where the contract lives, that a file is the result, and that
|
|
132
|
+
* silence proves nothing. Weaker than the full block by design — and still falsifiable, which is
|
|
133
|
+
* the only property that matters.
|
|
134
|
+
*/
|
|
135
|
+
function pointerProblems(text) {
|
|
136
|
+
const problems = [];
|
|
137
|
+
if (!/swarm-file-evidence\.md/.test(text)) problems.push('does not name the rule file');
|
|
138
|
+
if (!/TRACE_PATH/.test(text)) problems.push('does not name TRACE_PATH');
|
|
139
|
+
if (!/silent|silence/i.test(text)) problems.push('does not say that silence proves nothing');
|
|
140
|
+
return problems;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Every file under the generator skills, as skill-relative POSIX paths. */
|
|
144
|
+
function walk(dir, base, out = []) {
|
|
145
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
146
|
+
const full = path.join(dir, entry.name);
|
|
147
|
+
if (entry.isDirectory()) walk(full, base, out);
|
|
148
|
+
else out.push(path.relative(base, full).split(path.sep).join('/'));
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function generatorFiles() {
|
|
154
|
+
const out = [];
|
|
155
|
+
for (const skill of GENERATOR_SKILLS) out.push(...walk(path.join(SKILLS, skill), SKILLS));
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The discovery verdict: which dispatching generator files are NOT in the inventory.
|
|
161
|
+
*
|
|
162
|
+
* This is the half that survives the next change. A contract test proves the files we already
|
|
163
|
+
* fixed stayed fixed; only this one fires when somebody adds a sixteenth generator file that
|
|
164
|
+
* launches agents and requires nothing.
|
|
165
|
+
*/
|
|
166
|
+
function uncoveredDispatchers(files, inventory) {
|
|
167
|
+
const declared = new Set(inventory.map(([file]) => file));
|
|
168
|
+
return files.filter((rel) => {
|
|
169
|
+
if (!/\.(md|cjs|js)$/.test(rel)) return false;
|
|
170
|
+
return DISPATCH.test(fs.readFileSync(path.join(SKILLS, rel), 'utf8')) && !declared.has(rel);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
describe('the swarm file contract reached the GENERATORS, not only the package itself', () => {
|
|
175
|
+
test('G1 - the emitted rule is byte-identical to the pre-shipped rule', () => {
|
|
176
|
+
const canonical = fs.readFileSync(RULE, 'utf8').trimEnd();
|
|
177
|
+
const template = fs.readFileSync(RULE_TEMPLATE, 'utf8');
|
|
178
|
+
const fenced = /```markdown\n([\s\S]*?)\n```/.exec(template);
|
|
179
|
+
assert.ok(fenced, 'the template must carry the rule body in a markdown fence');
|
|
180
|
+
assert.equal(fenced[1], canonical,
|
|
181
|
+
'the generated rule drifted from the pre-shipped one — two copies of a contract that '
|
|
182
|
+
+ 'disagree are worse than one, because each side believes it is authoritative');
|
|
183
|
+
|
|
184
|
+
// Mutation: a single weakened word in the copy must be observable.
|
|
185
|
+
const mutant = template.replace('silence is\nneither progress nor completion',
|
|
186
|
+
'silence may be read as progress');
|
|
187
|
+
assert.notEqual(mutant, template, 'mutation fixture did not apply — the anchor text moved');
|
|
188
|
+
const mutated = /```markdown\n([\s\S]*?)\n```/.exec(mutant)[1];
|
|
189
|
+
assert.notEqual(mutated, canonical, 'a weakened copy must not compare equal');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('G2 - every full-contract generator seam carries all twelve predicates', () => {
|
|
193
|
+
for (const [file, profile] of GENERATOR_SEAMS) {
|
|
194
|
+
if (profile !== 'full') continue;
|
|
195
|
+
assert.deepEqual(swarmContractProblems(read(file)), [], file);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('G2m - MUTATION: removing the receipt block from a seam turns it red', () => {
|
|
200
|
+
const file = 'cc-toolkit-generator-enhanced/references/templates/feature-lifecycle.md';
|
|
201
|
+
const source = read(file);
|
|
202
|
+
assert.deepEqual(swarmContractProblems(source), [], 'precondition: the real file is green');
|
|
203
|
+
|
|
204
|
+
// Strip exactly what the change added: every "Positive file receipt (required)" block.
|
|
205
|
+
const stripped = source.replace(/#### Positive file receipt \(required\)[\s\S]*?non-atomic-write exception\.\n/g, '');
|
|
206
|
+
assert.notEqual(stripped, source, 'mutation fixture did not apply — the block heading moved');
|
|
207
|
+
const problems = swarmContractProblems(stripped);
|
|
208
|
+
assert.ok(problems.length >= 6,
|
|
209
|
+
'a seam without the block must fail on most predicates, got: ' + problems.join(', '));
|
|
210
|
+
// The two terminal markers are the receipt's whole point: without them a "trace" is a note.
|
|
211
|
+
assert.ok(problems.includes('completed terminal marker missing'), problems.join(', '));
|
|
212
|
+
assert.ok(problems.includes('failed terminal marker missing'), problems.join(', '));
|
|
213
|
+
assert.ok(problems.includes('invalid receipt does not block aggregation'), problems.join(', '));
|
|
214
|
+
|
|
215
|
+
// The FORCE of the block, separately from its vocabulary. Downgrading the heading alone leaves
|
|
216
|
+
// every keyword in place — that is how the registered start-command mutation first survived.
|
|
217
|
+
const softened = source.replaceAll('#### Positive file receipt (required)', '#### Coordination note');
|
|
218
|
+
assert.notEqual(softened, source, 'mutation fixture did not apply — the heading moved');
|
|
219
|
+
assert.deepEqual(swarmContractProblems(softened),
|
|
220
|
+
['the receipt block is not titled as required'],
|
|
221
|
+
'a retitled block must fire exactly the mandate predicate and nothing else');
|
|
222
|
+
|
|
223
|
+
// And each predicate must fire INDEPENDENTLY, or one weakened word could hide behind another.
|
|
224
|
+
const weakened = source.replaceAll('Narrative output or\nsilence is never a receipt.', '');
|
|
225
|
+
assert.ok(swarmContractProblems(weakened)
|
|
226
|
+
.includes('narrative or silence can still act as a receipt'),
|
|
227
|
+
'deleting only the silence clause must fire only that predicate');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('G3 - every pointer names the rule, the trace path, and the silence', () => {
|
|
231
|
+
for (const [file, profile] of GENERATOR_SEAMS) {
|
|
232
|
+
if (profile !== 'pointer') continue;
|
|
233
|
+
assert.deepEqual(pointerProblems(read(file)), [], file);
|
|
234
|
+
}
|
|
235
|
+
assert.deepEqual(pointerProblems('Spawn parallel agents via Task tool.'),
|
|
236
|
+
['does not name the rule file', 'does not name TRACE_PATH',
|
|
237
|
+
'does not say that silence proves nothing'],
|
|
238
|
+
'a bare dispatch instruction must fail all three');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('G4 - DISCOVERY: no generator file dispatches agents outside the inventory', () => {
|
|
242
|
+
const files = generatorFiles();
|
|
243
|
+
assert.ok(files.length > 20, 'the walk found suspiciously few generator files: ' + files.length);
|
|
244
|
+
assert.deepEqual(uncoveredDispatchers(files, GENERATOR_SEAMS), [],
|
|
245
|
+
'these generator files launch parallel agents and are not covered by the contract');
|
|
246
|
+
|
|
247
|
+
// Mutation: shrink the inventory and the net must catch what it no longer declares.
|
|
248
|
+
const short = GENERATOR_SEAMS.filter(([f]) => f !== 'pipeline-forge/SKILL.md');
|
|
249
|
+
assert.deepEqual(uncoveredDispatchers(files, short), ['pipeline-forge/SKILL.md'],
|
|
250
|
+
'dropping a declared seam must reappear as an uncovered dispatcher');
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('G5 - the deterministic half is registered and wired to no event', () => {
|
|
254
|
+
const { COMPONENTS } = require(path.join(PKG, 'src', 'utils.js'));
|
|
255
|
+
assert.ok(COMPONENTS.hooks.items['check-swarm-receipts'],
|
|
256
|
+
'unregistered, init/doctor/verify would never require the checker');
|
|
257
|
+
const settings = fs.readFileSync(path.join(TPL, 'settings.json'), 'utf-8');
|
|
258
|
+
assert.ok(!settings.includes('check-swarm-receipts'),
|
|
259
|
+
'it must not be wired to an event: this package\'s hooks are non-blocking by contract, so a '
|
|
260
|
+
+ 'hook could only print — it could never refuse anything');
|
|
261
|
+
const statusline = fs.readFileSync(path.join(TPL, 'hooks', 'statusline.cjs'), 'utf-8');
|
|
262
|
+
const m = statusline.match(/hooksExpected:\s*(\d+)/);
|
|
263
|
+
assert.ok(m, 'statusline must declare hooksExpected');
|
|
264
|
+
assert.equal(Number(m[1]), Object.keys(COMPONENTS.hooks.items).length,
|
|
265
|
+
'the status line would report a phantom missing hook: ' + m[1]);
|
|
266
|
+
const rule = fs.readFileSync(path.join(TPL, 'rules', 'replicate-pipeline.md'), 'utf-8');
|
|
267
|
+
assert.match(rule, /check-swarm-receipts\.cjs/,
|
|
268
|
+
'the pre-shipped inventory must list it, or the third counter drifts from the other two');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('G6 - the generator EMITS the rule and the checker, not just prose about them', () => {
|
|
272
|
+
const p0 = read('cc-toolkit-generator-enhanced/modules/03-generate-p0.md');
|
|
273
|
+
assert.match(p0, /Item 9b: swarm-file-evidence\.md rule/,
|
|
274
|
+
'the P0 item list must carry the rule as a generated artifact');
|
|
275
|
+
assert.match(p0, /\.claude\/rules\/swarm-file-evidence\.md/, 'the output path must be named');
|
|
276
|
+
assert.match(p0, /check-swarm-receipts\.cjs/,
|
|
277
|
+
'the deterministic half must travel with the text half — text alone is layer 2');
|
|
278
|
+
assert.match(p0, /Do NOT overwrite/,
|
|
279
|
+
'a project initialised by p-replicator already has the rule; regenerating it would fight the '
|
|
280
|
+
+ 'do-not-overwrite gate');
|
|
281
|
+
|
|
282
|
+
const deliver = read('cc-toolkit-generator-enhanced/modules/06-package-deliver.md');
|
|
283
|
+
assert.match(deliver, /CHECK 7b/, 'delivery must gate on the contract having reached the toolkit');
|
|
284
|
+
assert.match(deliver, /FAIL-CLOSED/,
|
|
285
|
+
'a delivery check that passes on absence is the same substitution this whole feature refuses');
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
const PKG = path.resolve(__dirname, '..', '..');
|
|
10
|
+
const HOOKS = path.join(PKG, 'templates', '.claude', 'hooks');
|
|
11
|
+
const TESTS = path.join(PKG, 'tests', 'unit');
|
|
12
|
+
const HONEST_INPUT_TITLE = /clean|чист|honest|exits? 0/i;
|
|
13
|
+
|
|
14
|
+
function testTitles(source) {
|
|
15
|
+
return [...source.matchAll(/\b(?:test|it)\s*\(\s*(['"`])([^\r\n]*?)\1/g)]
|
|
16
|
+
.map((match) => match[2]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function auditHonestInputs(hooksDir, testsDir) {
|
|
20
|
+
const hooks = fs.readdirSync(hooksDir)
|
|
21
|
+
.filter((name) => /^check-.*\.cjs$/.test(name))
|
|
22
|
+
.sort();
|
|
23
|
+
const issues = [];
|
|
24
|
+
for (const hook of hooks) {
|
|
25
|
+
const testFile = hook.replace(/\.cjs$/, '.test.js');
|
|
26
|
+
const testPath = path.join(testsDir, testFile);
|
|
27
|
+
if (!fs.existsSync(testPath)) {
|
|
28
|
+
issues.push({ hook, testFile, reason: 'missing-test' });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const titles = testTitles(fs.readFileSync(testPath, 'utf8'));
|
|
32
|
+
if (!titles.some((title) => HONEST_INPUT_TITLE.test(title))) {
|
|
33
|
+
issues.push({ hook, testFile, reason: 'missing-honest-input-title' });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { hooks, issues };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
test('catalog requires an honest-input title for every shipped check-*.cjs', () => {
|
|
40
|
+
const result = auditHonestInputs(HOOKS, TESTS);
|
|
41
|
+
assert.ok(result.hooks.length > 0, 'an empty hook catalog cannot prove the invariant');
|
|
42
|
+
assert.deepEqual(result.issues, [], JSON.stringify(result.issues, null, 2));
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('honest-input catalog probe reports a fake check with no same-named test', () => {
|
|
46
|
+
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-honest-meta-')));
|
|
47
|
+
const hooksCopy = path.join(root, 'hooks');
|
|
48
|
+
const testsCopy = path.join(root, 'unit');
|
|
49
|
+
try {
|
|
50
|
+
fs.cpSync(HOOKS, hooksCopy, { recursive: true });
|
|
51
|
+
fs.cpSync(TESTS, testsCopy, { recursive: true });
|
|
52
|
+
fs.writeFileSync(path.join(hooksCopy, 'check-zzz.cjs'), "'use strict';\n");
|
|
53
|
+
|
|
54
|
+
const result = auditHonestInputs(hooksCopy, testsCopy);
|
|
55
|
+
assert.ok(result.hooks.includes('check-zzz.cjs'),
|
|
56
|
+
'the derived enumeration must see a newly added catalog entry');
|
|
57
|
+
assert.deepEqual(result.issues.filter((issue) => issue.hook === 'check-zzz.cjs'), [{
|
|
58
|
+
hook: 'check-zzz.cjs', testFile: 'check-zzz.test.js', reason: 'missing-test',
|
|
59
|
+
}]);
|
|
60
|
+
} finally {
|
|
61
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
@@ -12,8 +12,43 @@ const PKG = path.resolve(__dirname, '..', '..');
|
|
|
12
12
|
const ROOT = path.join(PKG, 'templates', '.claude');
|
|
13
13
|
const CLI = path.join(PKG, 'bin', 'cli.js');
|
|
14
14
|
const RULES = ['honest-configuration', 'swarm-file-evidence'];
|
|
15
|
-
|
|
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 };
|
|
16
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;
|
|
17
52
|
const SEAMS = [
|
|
18
53
|
['commands/feature.md', 'both'],
|
|
19
54
|
['rules/feature-lifecycle.md', 'both'],
|
|
@@ -161,6 +196,29 @@ function walkSkills(dir, out) {
|
|
|
161
196
|
}
|
|
162
197
|
}
|
|
163
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
|
+
|
|
164
222
|
function measureCorpus(root) {
|
|
165
223
|
const files = [];
|
|
166
224
|
for (const dir of ['rules', 'commands']) {
|
|
@@ -248,11 +306,11 @@ test('P0 - ADR boundary is one feature with exactly two non-duplicated rules', (
|
|
|
248
306
|
}
|
|
249
307
|
});
|
|
250
308
|
|
|
251
|
-
test('P1 - registry and rule filesystem agree on the
|
|
309
|
+
test('P1 - registry and rule filesystem agree on the thirteen-rule contract', () => {
|
|
252
310
|
const registry = JSON.parse(read('src/rule-components.json'));
|
|
253
311
|
const files = fs.readdirSync(path.join(ROOT, 'rules'))
|
|
254
312
|
.filter((name) => name.endsWith('.md')).map((name) => name.slice(0, -3)).sort();
|
|
255
|
-
assert.equal(Object.keys(registry).length,
|
|
313
|
+
assert.equal(Object.keys(registry).length, 13, 'the canonical registry must carry thirteen rules');
|
|
256
314
|
assert.deepEqual(Object.keys(registry).sort(), files, 'registry and canonical rule files diverged');
|
|
257
315
|
for (const slug of RULES) assert.ok(registry[slug], `registry missing ${slug}`);
|
|
258
316
|
});
|
|
@@ -379,18 +437,32 @@ test('P5 - context delta uses fresh measurement and honest load-surface labels',
|
|
|
379
437
|
'unreadable or mislabeled measurement must not become a zero delta');
|
|
380
438
|
});
|
|
381
439
|
|
|
382
|
-
test('P6 - the owned inventory is eight first-party seams and
|
|
440
|
+
test('P6 - the owned inventory is eight first-party seams, and the generators have their own', () => {
|
|
383
441
|
const files = SEAMS.map(([file]) => file);
|
|
384
442
|
assert.deepEqual(seamInventoryProblems(files), []);
|
|
385
443
|
assert.equal(seamInventoryProblems(files.slice(0, -1)).length, 1,
|
|
386
444
|
'omitting an owned seam must be observable');
|
|
387
445
|
assert.equal(seamInventoryProblems([...files, 'skills/generator/example.md']).length, 1,
|
|
388
|
-
'a
|
|
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');
|
|
389
461
|
});
|
|
390
462
|
|
|
391
|
-
test('P7 - current package inventories derive the
|
|
392
|
-
assert.deepEqual(countClaimProblems('Pre-shipped:
|
|
393
|
-
assert.equal(countClaimProblems('Pre-shipped: 7 rules.',
|
|
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,
|
|
394
466
|
'the deliberately stale count fixture must fire');
|
|
395
467
|
const docs = [
|
|
396
468
|
'README.md',
|
|
@@ -407,7 +479,7 @@ test('P7 - current package inventories derive the nine-rule count', () => {
|
|
|
407
479
|
'templates/.claude/commands/replicate.md',
|
|
408
480
|
'templates/.claude/rules/replicate-pipeline.md',
|
|
409
481
|
];
|
|
410
|
-
const wrong = docs.flatMap((file) => countClaimProblems(read(file),
|
|
482
|
+
const wrong = docs.flatMap((file) => countClaimProblems(read(file), 11).map((problem) => `${file}: ${problem}`));
|
|
411
483
|
assert.deepEqual(wrong, []);
|
|
412
484
|
});
|
|
413
485
|
|
|
@@ -490,3 +562,13 @@ test('P10 - fresh init pack registry manifest and SBOM carry both rules', () =>
|
|
|
490
562
|
`SBOM missing or stale for ${relative}`);
|
|
491
563
|
}
|
|
492
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
|
+
});
|