@dzhechkov/p-replicator 1.10.4 → 1.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dz-manifest.json +312 -76
- package/CHANGELOG.md +232 -0
- package/MULTIPLATFORM_ROADMAP.md +1 -1
- package/README/eng/01_quickstart.md +3 -3
- package/README/eng/02_user_guide.md +1 -1
- package/README/eng/03_admin_guide.md +2 -2
- package/README/eng/04_api_reference.md +11 -5
- package/README/eng/05_architecture.md +1 -1
- package/README/eng/README.md +2 -1
- package/README/ru/01_quickstart.md +3 -3
- package/README/ru/02_user_guide.md +1 -1
- package/README/ru/03_admin_guide.md +2 -2
- package/README/ru/04_api_reference.md +11 -5
- package/README/ru/05_architecture.md +1 -1
- package/README/ru/README.md +2 -1
- package/README/ru/html/index.html +9 -9
- package/README.md +278 -39
- package/package.json +5 -4
- package/sbom.json +665 -75
- package/scripts/check-pipeline-gaps.sh +413 -0
- package/src/commands/doctor.js +94 -4
- package/src/commands/init.js +1 -1
- package/src/rule-components.json +15 -0
- package/src/utils.js +35 -11
- package/templates/.claude/agents/harvest-coordinator.md +10 -1
- package/templates/.claude/agents/product-discoverer.md +38 -0
- package/templates/.claude/agents/replicate-coordinator.md +11 -1
- package/templates/.claude/commands/feature.md +81 -9
- package/templates/.claude/commands/go.md +9 -0
- package/templates/.claude/commands/harvest.md +39 -3
- package/templates/.claude/commands/myinsights.md +21 -26
- package/templates/.claude/commands/replicate.md +171 -37
- package/templates/.claude/commands/start.md +29 -0
- package/templates/.claude/hooks/capture-source-path.cjs +795 -0
- package/templates/.claude/hooks/check-canon.cjs +493 -0
- package/templates/.claude/hooks/check-embed-contract.cjs +374 -0
- package/templates/.claude/hooks/check-external-deps.cjs +288 -0
- package/templates/.claude/hooks/check-file-ownership.cjs +424 -0
- package/templates/.claude/hooks/check-handoff-manifest.cjs +367 -0
- package/templates/.claude/hooks/check-job-contract.cjs +501 -0
- package/templates/.claude/hooks/check-look-origin.cjs +240 -0
- package/templates/.claude/hooks/check-look-trace.cjs +385 -0
- package/templates/.claude/hooks/check-metric-source.cjs +296 -0
- package/templates/.claude/hooks/check-model-cost.cjs +470 -0
- package/templates/.claude/hooks/check-ports.cjs +434 -24
- package/templates/.claude/hooks/check-source-version.cjs +312 -0
- package/templates/.claude/hooks/check-swarm-receipts.cjs +197 -0
- package/templates/.claude/hooks/check-webhook-contract.cjs +535 -0
- package/templates/.claude/hooks/session-insights.cjs +158 -25
- package/templates/.claude/hooks/statusline.cjs +2 -2
- package/templates/.claude/hooks/write-insight.cjs +253 -0
- package/templates/.claude/rules/cost-of-detection-ladder.md +96 -0
- package/templates/.claude/rules/docker-ports.md +41 -19
- package/templates/.claude/rules/embeddable-widget.md +73 -0
- package/templates/.claude/rules/feature-lifecycle.md +13 -3
- package/templates/.claude/rules/honest-configuration.md +54 -0
- package/templates/.claude/rules/incoming-webhooks.md +99 -0
- package/templates/.claude/rules/insights-capture.md +10 -5
- package/templates/.claude/rules/long-running-job.md +73 -0
- package/templates/.claude/rules/model-call-cost.md +85 -0
- package/templates/.claude/rules/replicate-pipeline.md +123 -52
- package/templates/.claude/rules/skill-interface-protocol.md +1 -0
- package/templates/.claude/rules/swarm-file-evidence.md +46 -0
- package/templates/.claude/settings.json +13 -1
- package/templates/.claude/skills/brutal-honesty-review/SKILL.md +9 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/SKILL.md +4 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/03-generate-p0.md +46 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/04-generate-p1.md +7 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/modules/06-package-deliver.md +20 -2
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/claude-md-strategy.md +7 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/automation-commands.md +17 -0
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle-ent.md +43 -5
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/feature-lifecycle.md +43 -7
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/start-command.md +19 -1
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/swarm-file-evidence.md +151 -0
- package/templates/.claude/skills/goap-research-ed25519/SKILL.md +37 -22
- package/templates/.claude/skills/goap-research-ed25519/references/negative-results.md +94 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/check_report_evidence.py +368 -4
- package/templates/.claude/skills/goap-research-ed25519/scripts/ed25519_verifier.py +122 -5
- package/templates/.claude/skills/goap-research-ed25519/scripts/evidence_fetch.py +33 -16
- package/templates/.claude/skills/goap-research-ed25519/scripts/quote_provenance.py +342 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_ed25519_verifier.py +60 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_evidence_provenance.py +139 -6
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_quote_provenance.py +274 -0
- package/templates/.claude/skills/goap-research-ed25519/scripts/test_suite_completeness.py +2 -1
- package/templates/.claude/skills/knowledge-extractor/SKILL.md +4 -0
- package/templates/.claude/skills/knowledge-extractor/modules/01-agent-review.md +16 -5
- package/templates/.claude/skills/pipeline-forge/SKILL.md +18 -23
- package/templates/.claude/skills/pipeline-forge/examples/replicate-analysis.md +7 -2
- package/templates/.claude/skills/pipeline-forge/references/patterns-catalog.md +19 -1
- package/templates/.claude/skills/pipeline-forge/references/self-extracted-patterns.md +17 -6
- package/templates/.claude/skills/pipeline-forge/references/skill-anatomy.md +0 -1
- package/templates/.claude/skills/reverse-engineering-unicorn/modules/025-cjm-prototype.md +21 -1
- package/templates/.claude/skills/sparc-prd-mini/SKILL.md +234 -716
- package/tests/e2e/lifecycle.test.js +55 -9
- package/tests/e2e/packed-insights-writer.test.js +308 -0
- package/tests/fixtures/prep-traceability-fixture/docs/features/order-refund/01_specification.md +29 -0
- package/tests/fixtures/prep-traceability-fixture/docs/features/order-refund/02_pseudocode.md +57 -0
- package/tests/snapshot/baseline.json +72 -46
- package/tests/snapshot/templates.test.js +47 -0
- package/tests/unit/absence-is-not-emptiness.test.js +15 -1
- package/tests/unit/capture-source-path.test.js +492 -0
- package/tests/unit/check-canon.test.js +403 -0
- package/tests/unit/check-embed-contract.test.js +422 -0
- package/tests/unit/check-external-deps.test.js +363 -0
- package/tests/unit/check-file-ownership.test.js +388 -0
- package/tests/unit/check-handoff-manifest.test.js +410 -0
- package/tests/unit/check-job-contract.test.js +514 -0
- package/tests/unit/check-look-origin.test.js +180 -0
- package/tests/unit/check-look-trace.test.js +420 -0
- package/tests/unit/check-metric-source.test.js +325 -0
- package/tests/unit/check-model-cost.test.js +425 -0
- package/tests/unit/check-pipeline-gaps.test.js +94 -0
- package/tests/unit/check-ports.test.js +773 -2
- package/tests/unit/check-source-version.test.js +344 -0
- package/tests/unit/check-swarm-receipts.test.js +231 -0
- package/tests/unit/check-webhook-contract.test.js +536 -0
- package/tests/unit/db-port-rule.test.js +43 -6
- package/tests/unit/detection-ladder-contract.test.js +302 -0
- package/tests/unit/detection-ladder-registry.test.js +52 -0
- package/tests/unit/doctor-insight-flow.test.js +315 -0
- package/tests/unit/external-dependency-check.test.js +19 -19
- package/tests/unit/generator-swarm-contract.test.js +287 -0
- package/tests/unit/guard-honest-input-meta.test.js +64 -0
- package/tests/unit/honest-failure-rules.test.js +574 -0
- package/tests/unit/hooks-project-anchored.test.js +67 -3
- package/tests/unit/insights-docs-tell-the-truth.test.js +52 -31
- package/tests/unit/insights-dz-delegation.test.js +197 -0
- package/tests/unit/insights-writer.test.js +285 -0
- package/tests/unit/look-phase-contract.test.js +231 -0
- package/tests/unit/negative-conclusion-gate.test.js +300 -0
- package/tests/unit/quote-provenance.test.js +122 -0
- package/tests/unit/shipped-suite-context.test.js +3 -1
- package/tests/unit/traceability-machine-ids.test.js +413 -0
- package/tests/unit/traceability-negative-fixture.test.js +322 -0
- package/tests/unit/utils.test.js +40 -2
|
@@ -65,14 +65,15 @@ function project() {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
/** Run one hook FROM THE SUBDIRECTORY — the only place this defect is visible. */
|
|
68
|
-
function runFromSub(p, hook, { withEnv = true, args = [] } = {}) {
|
|
68
|
+
function runFromSub(p, hook, { withEnv = true, args = [], input, envOverrides = {} } = {}) {
|
|
69
69
|
const env = { ...process.env };
|
|
70
70
|
if (withEnv) env.CLAUDE_PROJECT_DIR = p.root;
|
|
71
71
|
else delete env.CLAUDE_PROJECT_DIR;
|
|
72
|
+
Object.assign(env, envOverrides);
|
|
72
73
|
try {
|
|
73
74
|
const stdout = execFileSync(process.execPath,
|
|
74
75
|
[path.join(p.root, '.claude', 'hooks', hook + '.cjs'), ...args],
|
|
75
|
-
{ cwd: p.sub, env, encoding: 'utf8', stdio: 'pipe' });
|
|
76
|
+
{ cwd: p.sub, env, encoding: 'utf8', stdio: 'pipe', input });
|
|
76
77
|
return { code: 0, stdout: stdout || '' };
|
|
77
78
|
} catch (err) {
|
|
78
79
|
return { code: err.status ?? 1, stdout: err.stdout?.toString() ?? '' };
|
|
@@ -138,13 +139,76 @@ describe('hooks resolve against the project, not the drifting cwd (PR-013)', ()
|
|
|
138
139
|
fs.mkdirSync(path.join(p.root, '.claude', 'insights'), { recursive: true });
|
|
139
140
|
fs.writeFileSync(path.join(p.root, '.claude', 'insights', 'index.md'),
|
|
140
141
|
'## Insight one\n\nbody one\n');
|
|
141
|
-
const r = runFromSub(p, 'session-insights', {
|
|
142
|
+
const r = runFromSub(p, 'session-insights', {
|
|
143
|
+
withEnv: false,
|
|
144
|
+
envOverrides: { PATH: '' },
|
|
145
|
+
input: JSON.stringify({ hook_event_name: 'UserPromptSubmit', prompt: 'anchor' }),
|
|
146
|
+
});
|
|
142
147
|
assert.equal(r.code, 0, 'the hook must stay non-blocking');
|
|
143
148
|
assert.match(r.stdout, /Insight one/,
|
|
144
149
|
'with no env var and cwd in a subdirectory, the hook must still read the ROOT index');
|
|
145
150
|
} finally { fs.rmSync(p.root, { recursive: true, force: true }); }
|
|
146
151
|
});
|
|
147
152
|
|
|
153
|
+
test('P8 - missing store triggers exactly one no-insights hint without creating the carrier', () => {
|
|
154
|
+
const p = project();
|
|
155
|
+
const hint = 'инсайтов пока нет; /myinsights создаст первую запись\n';
|
|
156
|
+
const index = path.join(p.root, '.claude', 'insights', 'index.md');
|
|
157
|
+
try {
|
|
158
|
+
const missing = runFromSub(p, 'session-insights');
|
|
159
|
+
assert.equal(missing.code, 0, 'the missing state stays advisory');
|
|
160
|
+
assert.equal(missing.stdout, hint, 'the genuine missing branch prints exactly one line');
|
|
161
|
+
assert.equal(fs.existsSync(path.dirname(index)), false,
|
|
162
|
+
'the reader must not blur missing into empty by creating the carrier');
|
|
163
|
+
|
|
164
|
+
fs.mkdirSync(path.dirname(index), { recursive: true });
|
|
165
|
+
fs.writeFileSync(index, '');
|
|
166
|
+
const empty = runFromSub(p, 'session-insights');
|
|
167
|
+
assert.equal(empty.code, 0);
|
|
168
|
+
assert.equal(empty.stdout, '', 'an existing empty carrier is not the missing branch');
|
|
169
|
+
|
|
170
|
+
fs.writeFileSync(index, '## 2026-08-30 — existing\n\nBody.\n');
|
|
171
|
+
const populated = runFromSub(p, 'session-insights');
|
|
172
|
+
assert.equal(populated.code, 0);
|
|
173
|
+
assert.equal(populated.stdout, '',
|
|
174
|
+
'SessionStart must not inject populated insights before a prompt exists');
|
|
175
|
+
assert.doesNotMatch(populated.stdout, /инсайтов пока нет/);
|
|
176
|
+
|
|
177
|
+
fs.rmSync(index);
|
|
178
|
+
fs.mkdirSync(index);
|
|
179
|
+
const unreadable = runFromSub(p, 'session-insights');
|
|
180
|
+
assert.equal(unreadable.code, 0, 'read errors remain never-blocking');
|
|
181
|
+
assert.equal(unreadable.stdout, '',
|
|
182
|
+
'an unrelated read failure must not be laundered into the ordinary missing state');
|
|
183
|
+
} finally { fs.rmSync(p.root, { recursive: true, force: true }); }
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test('P9 - SessionStart preserves only missing-carrier visibility', () => {
|
|
187
|
+
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-start-state-')));
|
|
188
|
+
const hook = path.join(TPL, 'hooks', 'session-insights.cjs');
|
|
189
|
+
delete require.cache[require.resolve(hook)];
|
|
190
|
+
const { emitInsights } = require(hook);
|
|
191
|
+
const run = () => {
|
|
192
|
+
const output = [];
|
|
193
|
+
emitInsights(root, { write: (chunk) => output.push(chunk) }, {
|
|
194
|
+
rawEvent: JSON.stringify({ hook_event_name: 'SessionStart' }),
|
|
195
|
+
});
|
|
196
|
+
return output;
|
|
197
|
+
};
|
|
198
|
+
try {
|
|
199
|
+
assert.deepEqual(run(), ['инсайтов пока нет; /myinsights создаст первую запись\n']);
|
|
200
|
+
assert.equal(fs.existsSync(path.join(root, '.claude', 'insights')), false,
|
|
201
|
+
'the callable production branch must remain read-only on an absent root');
|
|
202
|
+
|
|
203
|
+
const index = path.join(root, '.claude', 'insights', 'index.md');
|
|
204
|
+
fs.mkdirSync(path.dirname(index), { recursive: true });
|
|
205
|
+
fs.writeFileSync(index, '');
|
|
206
|
+
assert.deepEqual(run(), [], 'an empty carrier is not a missing-carrier warning');
|
|
207
|
+
fs.writeFileSync(index, '## 2026-08-30 — populated\n\nBody.\n');
|
|
208
|
+
assert.deepEqual(run(), [], 'populated delivery waits for UserPromptSubmit');
|
|
209
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
210
|
+
});
|
|
211
|
+
|
|
148
212
|
test('P5 — EFFECT from a subdirectory: the roadmap is actually committed', () => {
|
|
149
213
|
// The property the whole feature exists for. An exit-code check passes on the broken hook;
|
|
150
214
|
// this does not. Fails if EITHER half of the fix is missing.
|
|
@@ -1,26 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Two shipped documents promised something the hook does not do — and, as written, CANNOT do.
|
|
4
|
-
//
|
|
5
|
-
// `myinsights.md` said insights are injected "when their tags match the current task". The hook
|
|
6
|
-
// runs on SessionStart, BEFORE the user has said anything, so there is no current task to match
|
|
7
|
-
// tags against. MEASURED: `session-insights.cjs:33` is `sections.slice(-3)` — the last three by
|
|
8
|
-
// file order, and no tag matching exists anywhere in the package.
|
|
9
|
-
//
|
|
10
|
-
// The hook's own printed heading, "Recent project insights", was already honest. Only the documents
|
|
11
|
-
// around it were not.
|
|
12
|
-
//
|
|
13
|
-
// This test exists because a promise removed from prose comes back. It pins the CODE and the DOC to
|
|
14
|
-
// each other: if selection ever becomes relevance-based, this test is where the doc must change too.
|
|
15
|
-
|
|
16
3
|
const { test, describe } = require('node:test');
|
|
17
4
|
const assert = require('node:assert/strict');
|
|
18
5
|
const fs = require('node:fs');
|
|
19
6
|
const path = require('node:path');
|
|
20
7
|
|
|
21
8
|
const TPL = path.join(__dirname, '..', '..', 'templates', '.claude');
|
|
9
|
+
const PKG = path.join(__dirname, '..', '..');
|
|
22
10
|
const CMD = path.join(TPL, 'commands', 'myinsights.md');
|
|
23
11
|
const HOOK = path.join(TPL, 'hooks', 'session-insights.cjs');
|
|
12
|
+
const RULE = path.join(TPL, 'rules', 'insights-capture.md');
|
|
13
|
+
const HARVEST = path.join(TPL, 'commands', 'harvest.md');
|
|
14
|
+
const README = path.join(PKG, 'README.md');
|
|
15
|
+
const API_EN = path.join(PKG, 'README', 'eng', '04_api_reference.md');
|
|
16
|
+
const API_RU = path.join(PKG, 'README', 'ru', '04_api_reference.md');
|
|
24
17
|
|
|
25
18
|
const read = (f) => fs.readFileSync(f, 'utf-8');
|
|
26
19
|
/** Prose with code fences and comments removed — a MENTION of a claim is not the claim. */
|
|
@@ -40,12 +33,13 @@ describe('the insights documents describe the hook that exists', () => {
|
|
|
40
33
|
|
|
41
34
|
test('P2 - the document states what the hook actually selects', () => {
|
|
42
35
|
const doc = read(CMD);
|
|
43
|
-
assert.match(doc, /
|
|
44
|
-
assert.match(doc, /
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
assert.match(doc, /UserPromptSubmit/, 'selection must occur after the real prompt exists');
|
|
37
|
+
assert.match(doc, /successful,? non-empty.*dz recall/is,
|
|
38
|
+
'the value-armed recall condition must be named');
|
|
39
|
+
assert.match(doc, /falls back.*three most recent/is,
|
|
40
|
+
'the local last-three fallback must remain visible');
|
|
41
|
+
assert.match(doc, /only.*suppresses.*local/is,
|
|
42
|
+
'binary presence alone must not read as readiness');
|
|
49
43
|
});
|
|
50
44
|
|
|
51
45
|
test('P3 - the code and the doc agree, asserted against the CODE', () => {
|
|
@@ -60,25 +54,52 @@ describe('the insights documents describe the hook that exists', () => {
|
|
|
60
54
|
});
|
|
61
55
|
|
|
62
56
|
test('P4 - the consequence of last-three is stated, not left to be discovered', () => {
|
|
63
|
-
// The file is append-only and the hook takes the last three, so a long-lived project stops
|
|
64
|
-
// seeing its earlier entries. insights-capture.md plans for 50+.
|
|
65
57
|
const doc = read(CMD);
|
|
66
58
|
assert.match(doc, /append-only/, 'the growth behaviour must be named');
|
|
67
|
-
assert.match(doc, /earlier ones stop being injected
|
|
59
|
+
assert.match(doc, /fallback.*earlier ones stop being injected/is,
|
|
68
60
|
'and its consequence, in plain words');
|
|
69
|
-
const rule = read(
|
|
61
|
+
const rule = read(RULE);
|
|
70
62
|
assert.match(rule, /50 entries|> 50/,
|
|
71
63
|
'the rule really does plan for a size the hook cannot show — that is the point');
|
|
72
64
|
});
|
|
73
65
|
|
|
74
|
-
test('P5 -
|
|
75
|
-
// MEASURED: `grep -ci insight` over harvest.md returns 0. The command promised harvest
|
|
76
|
-
// "extracts reusable patterns from insights", which nothing does.
|
|
66
|
+
test('P5 - /harvest and /myinsights name their executable shared-carrier contract', () => {
|
|
77
67
|
const doc = read(CMD);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
assert.
|
|
82
|
-
'
|
|
68
|
+
const harvest = read(HARVEST);
|
|
69
|
+
assert.doesNotMatch(doc, /does\s*\n?\s*NOT read `\.claude\/insights\/index\.md` today/,
|
|
70
|
+
'the old unwired admission became false once harvest acquired a writer');
|
|
71
|
+
assert.match(doc, /share the same Markdown carrier/i,
|
|
72
|
+
'manual and harvest capture must be described as one carrier, not two stores');
|
|
73
|
+
assert.match(harvest, /REQUIRED INSIGHT PERSISTENCE GATE/,
|
|
74
|
+
'the relationship must be executable wiring, not a prose aspiration');
|
|
75
|
+
assert.match(harvest, /\.claude\/hooks\/write-insight\.cjs/,
|
|
76
|
+
'the shipped writer boundary must be named exactly');
|
|
77
|
+
assert.match(harvest, /\.claude\/insights\/index\.md/,
|
|
78
|
+
'the command and /myinsights must converge on the same Markdown source of truth');
|
|
79
|
+
assert.match(harvest, /Only `created`, `appended`, or `duplicate`/,
|
|
80
|
+
'normal completion must be bound to the three canonical writer receipts');
|
|
81
|
+
assert.match(harvest, /writer exits non-zero.*MUST NOT\s+report.*completed/is,
|
|
82
|
+
'an unestablished write must remain a blocking harvest outcome');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('P6 - docs describe prompt-time armed recall and Markdown authority', () => {
|
|
86
|
+
const hook = read(HOOK);
|
|
87
|
+
assert.match(hook, /recall\.kind === 'ok'/,
|
|
88
|
+
'the docs cannot claim armed recall without the production predicate');
|
|
89
|
+
assert.doesNotMatch(hook, /command\s+-v\s+dz/,
|
|
90
|
+
'binary discovery must not become the suppression condition');
|
|
91
|
+
|
|
92
|
+
const docs = [read(CMD), read(RULE), read(HARVEST), read(README), read(API_EN), read(API_RU)];
|
|
93
|
+
for (const [index, doc] of docs.entries()) {
|
|
94
|
+
assert.match(doc, /Markdown/i, `behavior document ${index + 1} omits Markdown authority`);
|
|
95
|
+
assert.match(doc, /best-effort/i, `behavior document ${index + 1} omits optional teach`);
|
|
96
|
+
}
|
|
97
|
+
for (const doc of [docs[0], docs[1], docs[3], docs[4], docs[5]]) {
|
|
98
|
+
assert.match(doc, /UserPromptSubmit/, 'prompt-time delivery must be named');
|
|
99
|
+
assert.match(doc, /(?:non-empty|непуст(?:ой|ого)).*(?:recall|вывод)/is,
|
|
100
|
+
'successful non-empty output must be the documented arming condition');
|
|
101
|
+
assert.match(doc, /(?:local|локальн).*(?:fallback|фолб[эе]к)/is,
|
|
102
|
+
'absent/failing/empty recall must retain local delivery');
|
|
103
|
+
}
|
|
83
104
|
});
|
|
84
105
|
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test, describe } = 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 HOOK = path.join(PKG, 'templates', '.claude', 'hooks', 'session-insights.cjs');
|
|
11
|
+
const LOCAL = 'LOCAL_ONLY';
|
|
12
|
+
const DZ = 'DZ_ONLY';
|
|
13
|
+
|
|
14
|
+
function loadHook() {
|
|
15
|
+
delete require.cache[require.resolve(HOOK)];
|
|
16
|
+
return require(HOOK);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function project(prefix = 'p-rep-dz-contract-') {
|
|
20
|
+
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), prefix)));
|
|
21
|
+
const index = path.join(root, '.claude', 'insights', 'index.md');
|
|
22
|
+
fs.mkdirSync(path.dirname(index), { recursive: true });
|
|
23
|
+
fs.writeFileSync(index, `## 2026-08-30 — ${LOCAL}\n\nlocal body\n`);
|
|
24
|
+
return root;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function userPrompt(prompt = 'choose an optional dependency fallback') {
|
|
28
|
+
return JSON.stringify({ hook_event_name: 'UserPromptSubmit', prompt });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function runHook(root, result, rawEvent = userPrompt()) {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
const runner = (_file, _args, _options) => result;
|
|
34
|
+
loadHook().emitInsights(root, { write: (chunk) => chunks.push(chunk) }, { rawEvent, runner });
|
|
35
|
+
return chunks.join('');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function envelope(stdout) {
|
|
39
|
+
const lines = stdout.trimEnd().split('\n');
|
|
40
|
+
assert.equal(lines.length, 1, `expected one hook envelope, got ${lines.length}: ${stdout}`);
|
|
41
|
+
const parsed = JSON.parse(lines[0]);
|
|
42
|
+
assert.equal(parsed.hookSpecificOutput.hookEventName, 'UserPromptSubmit');
|
|
43
|
+
return parsed.hookSpecificOutput.additionalContext;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function recalled(pattern = DZ) {
|
|
47
|
+
return {
|
|
48
|
+
status: 0,
|
|
49
|
+
stdout: JSON.stringify([{ pattern, domain: 'p-replicator-insights' }]),
|
|
50
|
+
stderr: '',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function absent() {
|
|
55
|
+
return { status: null, stdout: '', stderr: '', error: { code: 'ENOENT' } };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('PR-022 optional dz delivery contract', () => {
|
|
59
|
+
test('P10 - absent dz emits local only without dz error text', () => {
|
|
60
|
+
const root = project();
|
|
61
|
+
try {
|
|
62
|
+
const context = envelope(runHook(root, absent()));
|
|
63
|
+
assert.match(context, new RegExp(LOCAL));
|
|
64
|
+
assert.doesNotMatch(context, new RegExp(DZ));
|
|
65
|
+
assert.doesNotMatch(context, /dz recall (unavailable|failed)/i);
|
|
66
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('P11 - armed non-empty recall emits dz only', () => {
|
|
70
|
+
const root = project();
|
|
71
|
+
try {
|
|
72
|
+
const context = envelope(runHook(root, recalled()));
|
|
73
|
+
assert.match(context, new RegExp(DZ));
|
|
74
|
+
assert.doesNotMatch(context, new RegExp(LOCAL));
|
|
75
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('P12 - failing or timed-out recall names degradation and emits local only', () => {
|
|
79
|
+
const root = project();
|
|
80
|
+
const failures = [
|
|
81
|
+
{ status: 23, stdout: '', stderr: 'SECRET child stderr' },
|
|
82
|
+
{ status: null, stdout: '', stderr: '', error: { code: 'ETIMEDOUT' }, signal: 'SIGTERM' },
|
|
83
|
+
];
|
|
84
|
+
try {
|
|
85
|
+
for (const result of failures) {
|
|
86
|
+
const context = envelope(runHook(root, result));
|
|
87
|
+
assert.match(context, /dz recall unavailable: (exit 23|timeout); using local recent insights/i);
|
|
88
|
+
assert.match(context, new RegExp(LOCAL));
|
|
89
|
+
assert.doesNotMatch(context, new RegExp(DZ));
|
|
90
|
+
assert.doesNotMatch(context, /SECRET child stderr/);
|
|
91
|
+
}
|
|
92
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('P13 - empty successful recall emits local only', () => {
|
|
96
|
+
const root = project();
|
|
97
|
+
const emptyResults = [
|
|
98
|
+
{ status: 0, stdout: '[]', stderr: '' },
|
|
99
|
+
{ status: 0, stdout: JSON.stringify([
|
|
100
|
+
{ pattern: DZ, domain: 'another-domain' },
|
|
101
|
+
{ pattern: ' ', domain: 'p-replicator-insights' },
|
|
102
|
+
]), stderr: '' },
|
|
103
|
+
];
|
|
104
|
+
try {
|
|
105
|
+
for (const result of emptyResults) {
|
|
106
|
+
const context = envelope(runHook(root, result));
|
|
107
|
+
assert.match(context, new RegExp(LOCAL));
|
|
108
|
+
assert.doesNotMatch(context, new RegExp(DZ));
|
|
109
|
+
assert.doesNotMatch(context, /dz recall unavailable/i);
|
|
110
|
+
}
|
|
111
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('P14 - local suppression requires armed recall and never double-injects across absent/ok/failing/empty', () => {
|
|
115
|
+
const root = project();
|
|
116
|
+
const states = {
|
|
117
|
+
absent: absent(),
|
|
118
|
+
ok: recalled(),
|
|
119
|
+
failing: { status: 2, stdout: '', stderr: '' },
|
|
120
|
+
empty: { status: 0, stdout: '[]', stderr: '' },
|
|
121
|
+
};
|
|
122
|
+
try {
|
|
123
|
+
for (const [state, result] of Object.entries(states)) {
|
|
124
|
+
const context = envelope(runHook(root, result));
|
|
125
|
+
const hasLocal = context.includes(LOCAL);
|
|
126
|
+
const hasDz = context.includes(DZ);
|
|
127
|
+
assert.notEqual(hasLocal, hasDz, `${state} must select exactly one insight source`);
|
|
128
|
+
assert.equal(hasDz, state === 'ok', `${state} armedness selected the wrong source`);
|
|
129
|
+
}
|
|
130
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('P15 - malformed successful stdout cannot arm recall', () => {
|
|
134
|
+
const root = project();
|
|
135
|
+
try {
|
|
136
|
+
for (const stdout of ['not-json', '{}']) {
|
|
137
|
+
const context = envelope(runHook(root, { status: 0, stdout, stderr: '' }));
|
|
138
|
+
assert.match(context, /dz recall unavailable: invalid (JSON|result)/i);
|
|
139
|
+
assert.match(context, new RegExp(LOCAL));
|
|
140
|
+
assert.doesNotMatch(context, new RegExp(DZ));
|
|
141
|
+
}
|
|
142
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('P15a - prompt parsing root pin argv and output bounds survive hostile input', () => {
|
|
146
|
+
const api = loadHook();
|
|
147
|
+
for (const key of ['prompt', 'user_prompt', 'userPrompt']) {
|
|
148
|
+
assert.deepEqual(
|
|
149
|
+
api.parseHookEvent(JSON.stringify({ hook_event_name: 'UserPromptSubmit', [key]: ' task ' })),
|
|
150
|
+
{ kind: 'user-prompt', prompt: 'task' },
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const root = project('p rep dz contract ');
|
|
155
|
+
const calls = [];
|
|
156
|
+
try {
|
|
157
|
+
const result = api.recallFromDz('literal $(touch never) `echo never`', root, {
|
|
158
|
+
runner(file, args, options) {
|
|
159
|
+
calls.push({ file, args, options });
|
|
160
|
+
return recalled('X'.repeat(20 * 1024));
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
assert.equal(result.kind, 'ok');
|
|
164
|
+
assert.ok(Buffer.byteLength(result.context, 'utf8') <= 16 * 1024,
|
|
165
|
+
`rendered context exceeded 16 KiB: ${Buffer.byteLength(result.context, 'utf8')}`);
|
|
166
|
+
assert.equal(calls.length, 1);
|
|
167
|
+
assert.equal(calls[0].file, 'dz');
|
|
168
|
+
// The ADR's Decision pins this argv exactly, --domain included: it is a rank BOOST, and
|
|
169
|
+
// without it a shared multi-domain store buries insight records below the top-12 cut, so
|
|
170
|
+
// the armed state never fires. This assertion previously omitted the flag and therefore
|
|
171
|
+
// cemented the very gap cross-family QE found.
|
|
172
|
+
assert.deepEqual(calls[0].args, [
|
|
173
|
+
'recall', 'literal $(touch never) `echo never`', '--limit', '12',
|
|
174
|
+
'--domain', 'p-replicator-insights',
|
|
175
|
+
'--project', root, '--json',
|
|
176
|
+
]);
|
|
177
|
+
assert.equal(calls[0].options.cwd, root);
|
|
178
|
+
assert.equal(calls[0].options.shell, false);
|
|
179
|
+
assert.equal(calls[0].options.timeout, 1500);
|
|
180
|
+
assert.equal(calls[0].options.maxBuffer, 1024 * 1024);
|
|
181
|
+
|
|
182
|
+
const oversized = api.recallFromDz('task', root, {
|
|
183
|
+
runner: () => ({ status: null, stdout: '', stderr: '', error: { code: 'ENOBUFS' } }),
|
|
184
|
+
});
|
|
185
|
+
assert.deepEqual(oversized, { kind: 'failing', reason: 'spawn ENOBUFS' });
|
|
186
|
+
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test('P19 - production comments contain no decision-history narration', () => {
|
|
190
|
+
for (const file of ['session-insights.cjs', 'write-insight.cjs']) {
|
|
191
|
+
const source = fs.readFileSync(path.join(PKG, 'templates', '.claude', 'hooks', file), 'utf8');
|
|
192
|
+
const comments = source.split('\n').filter((line) => /^\s*(\/\/|\/\*|\*)/.test(line)).join('\n');
|
|
193
|
+
assert.doesNotMatch(comments, /supersed|a0ea8b|cross-model|FIX-\d|AM-\d|third answer/i,
|
|
194
|
+
`${file} contains decision history that belongs in the ADR`);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
});
|