@dzhechkov/p-replicator 1.10.3 → 1.12.0
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 +119 -47
- package/CHANGELOG.md +85 -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/README.md +1 -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/README.md +1 -1
- package/README/ru/html/index.html +7 -7
- package/README.md +154 -38
- package/bin/cli.js +0 -0
- package/package.json +15 -13
- package/sbom.json +226 -46
- package/scripts/check-pipeline-gaps.sh +413 -0
- package/src/commands/doctor.js +94 -4
- package/src/rule-components.json +11 -0
- package/src/utils.js +3 -8
- package/templates/.claude/agents/harvest-coordinator.md +10 -1
- package/templates/.claude/commands/feature.md +57 -9
- package/templates/.claude/commands/go.md +11 -0
- package/templates/.claude/commands/harvest.md +41 -3
- package/templates/.claude/commands/myinsights.md +21 -26
- package/templates/.claude/commands/replicate.md +10 -1
- package/templates/.claude/commands/start.md +8 -0
- package/templates/.claude/hooks/check-ports.cjs +409 -20
- 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/feature-lifecycle.md +14 -3
- package/templates/.claude/rules/honest-configuration.md +54 -0
- package/templates/.claude/rules/insights-capture.md +10 -5
- package/templates/.claude/rules/replicate-pipeline.md +4 -2
- 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/knowledge-extractor/modules/01-agent-review.md +16 -5
- package/templates/.claude/skills/sparc-prd-mini/SKILL.md +86 -16
- 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 +24 -20
- package/tests/snapshot/templates.test.js +47 -0
- package/tests/unit/absence-is-not-emptiness.test.js +15 -1
- package/tests/unit/check-pipeline-gaps.test.js +94 -0
- package/tests/unit/check-ports.test.js +729 -2
- package/tests/unit/db-port-rule.test.js +36 -5
- 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/honest-failure-rules.test.js +492 -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/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 +3 -2
- package/LICENSE +0 -21
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test, describe, before, after } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { spawnSync } = require('node:child_process');
|
|
6
|
+
const crypto = require('node:crypto');
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const os = require('node:os');
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
|
|
11
|
+
const PKG = path.resolve(__dirname, '..', '..');
|
|
12
|
+
const NPM = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
13
|
+
const MISSING_HINT = 'инсайтов пока нет; /myinsights создаст первую запись\n';
|
|
14
|
+
const LOCAL_PACKED = 'PACKED_LOCAL_ONLY';
|
|
15
|
+
const DZ_PACKED = 'PACKED_DZ_ONLY';
|
|
16
|
+
let packDir;
|
|
17
|
+
let tarball;
|
|
18
|
+
let tarballSha256;
|
|
19
|
+
|
|
20
|
+
function run(file, args, options = {}) {
|
|
21
|
+
return spawnSync(file, args, { encoding: 'utf8', ...options });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function assertOk(result, label) {
|
|
25
|
+
assert.equal(result.status, 0,
|
|
26
|
+
`${label} failed\nstdout:\n${result.stdout || ''}\nstderr:\n${result.stderr || ''}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function assertConsumerPath(candidate, consumer) {
|
|
30
|
+
const realCandidate = fs.realpathSync(candidate);
|
|
31
|
+
const realConsumer = fs.realpathSync(consumer);
|
|
32
|
+
const relative = path.relative(realConsumer, realCandidate);
|
|
33
|
+
assert.ok(relative && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative),
|
|
34
|
+
`source-only evidence rejected: ${realCandidate} is outside packed consumer ${realConsumer}`);
|
|
35
|
+
return realCandidate;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function installConsumer() {
|
|
39
|
+
const consumer = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-packed-consumer-')));
|
|
40
|
+
fs.writeFileSync(path.join(consumer, 'package.json'),
|
|
41
|
+
JSON.stringify({ name: 'packed-insights-consumer', private: true }) + '\n');
|
|
42
|
+
const install = run(NPM, [
|
|
43
|
+
'install', '--ignore-scripts', '--omit=optional', '--no-audit', '--no-fund',
|
|
44
|
+
'--no-package-lock', tarball,
|
|
45
|
+
], { cwd: consumer });
|
|
46
|
+
assertOk(install, 'fresh tarball install');
|
|
47
|
+
|
|
48
|
+
const packageRoot = path.join(consumer, 'node_modules', '@dzhechkov', 'p-replicator');
|
|
49
|
+
const cli = assertConsumerPath(path.join(packageRoot, 'bin', 'cli.js'), consumer);
|
|
50
|
+
const init = run(process.execPath, [cli, 'init'], { cwd: consumer });
|
|
51
|
+
assertOk(init, 'packed init');
|
|
52
|
+
return { consumer, packageRoot, cli };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function insight(overrides = {}) {
|
|
56
|
+
return {
|
|
57
|
+
date: '2026-08-30',
|
|
58
|
+
title: 'Packed writer creates the first carrier',
|
|
59
|
+
tags: ['packed', 'insights'],
|
|
60
|
+
problem: 'Source-only checks cannot prove what npm consumers receive.',
|
|
61
|
+
solution: 'Install and execute the exact npm pack tarball.',
|
|
62
|
+
references: ['tests/e2e/packed-insights-writer.test.js'],
|
|
63
|
+
...overrides,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function runPackedWriter(consumer, input, options = {}) {
|
|
68
|
+
const writer = assertConsumerPath(
|
|
69
|
+
path.join(consumer, '.claude', 'hooks', 'write-insight.cjs'), consumer);
|
|
70
|
+
return run(process.execPath, [writer], {
|
|
71
|
+
cwd: path.join(consumer, '.claude'),
|
|
72
|
+
env: { ...process.env, PATH: options.path === undefined ? '' : options.path,
|
|
73
|
+
CLAUDE_PROJECT_DIR: consumer },
|
|
74
|
+
input: JSON.stringify(input),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function parseReceipt(result) {
|
|
79
|
+
assertOk(result, 'packed writer');
|
|
80
|
+
return JSON.parse(result.stdout);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function fakeDz(mode) {
|
|
84
|
+
const bin = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-fake-dz-')));
|
|
85
|
+
const script = path.join(bin, 'dz.js');
|
|
86
|
+
const outcome = mode === 'ok'
|
|
87
|
+
? `process.stdout.write(${JSON.stringify(JSON.stringify([
|
|
88
|
+
{ pattern: DZ_PACKED, domain: 'p-replicator-insights' },
|
|
89
|
+
]))});`
|
|
90
|
+
: mode === 'empty'
|
|
91
|
+
? "process.stdout.write('[]');"
|
|
92
|
+
: 'process.exitCode = 19;';
|
|
93
|
+
fs.writeFileSync(script, `'use strict';\n${outcome}\n`);
|
|
94
|
+
if (process.platform === 'win32') {
|
|
95
|
+
fs.writeFileSync(path.join(bin, 'dz.cmd'),
|
|
96
|
+
`@\"${process.execPath}\" \"%~dp0\\dz.js\" %*\r\n`);
|
|
97
|
+
} else {
|
|
98
|
+
const executable = path.join(bin, 'dz');
|
|
99
|
+
fs.writeFileSync(executable, `#!${process.execPath}\nrequire('./dz.js');\n`, { mode: 0o700 });
|
|
100
|
+
}
|
|
101
|
+
return bin;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function runPackedPrompt(consumer, pathValue) {
|
|
105
|
+
const hook = assertConsumerPath(
|
|
106
|
+
path.join(consumer, '.claude', 'hooks', 'session-insights.cjs'), consumer);
|
|
107
|
+
return run(process.execPath, [hook], {
|
|
108
|
+
cwd: path.join(consumer, '.claude'),
|
|
109
|
+
env: { ...process.env, PATH: pathValue, CLAUDE_PROJECT_DIR: consumer },
|
|
110
|
+
input: JSON.stringify({ hook_event_name: 'UserPromptSubmit', prompt: 'packed fallback' }),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function promptContext(result, label) {
|
|
115
|
+
assertOk(result, label);
|
|
116
|
+
const lines = result.stdout.trimEnd().split('\n');
|
|
117
|
+
assert.equal(lines.length, 1, `${label} emitted ${lines.length} envelopes`);
|
|
118
|
+
return JSON.parse(lines[0]).hookSpecificOutput.additionalContext;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
before(() => {
|
|
122
|
+
packDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-pack-')));
|
|
123
|
+
const packed = run(NPM, ['pack', '--json', '--pack-destination', packDir], { cwd: PKG });
|
|
124
|
+
assertOk(packed, 'npm pack --json');
|
|
125
|
+
const receipt = JSON.parse(packed.stdout);
|
|
126
|
+
assert.equal(receipt.length, 1, 'npm pack must identify exactly one consumer artifact');
|
|
127
|
+
tarball = path.join(packDir, receipt[0].filename);
|
|
128
|
+
assert.ok(fs.existsSync(tarball), 'npm pack receipt names a missing tarball');
|
|
129
|
+
tarballSha256 = crypto.createHash('sha256').update(fs.readFileSync(tarball)).digest('hex');
|
|
130
|
+
process.stdout.write(`# packed artifact ${path.basename(tarball)} sha256=${tarballSha256}\n`);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
after(() => {
|
|
134
|
+
if (packDir) fs.rmSync(packDir, { recursive: true, force: true });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('PR-022 exact packed artifact', () => {
|
|
138
|
+
test('P10-packed - absent dz emits local only without dz error text', () => {
|
|
139
|
+
const { consumer } = installConsumer();
|
|
140
|
+
try {
|
|
141
|
+
assert.equal(parseReceipt(runPackedWriter(consumer,
|
|
142
|
+
insight({ title: LOCAL_PACKED }))).status, 'created');
|
|
143
|
+
const context = promptContext(runPackedPrompt(consumer, ''), 'packed absent recall');
|
|
144
|
+
assert.match(context, new RegExp(LOCAL_PACKED));
|
|
145
|
+
assert.doesNotMatch(context, new RegExp(DZ_PACKED));
|
|
146
|
+
assert.doesNotMatch(context, /dz recall unavailable/i);
|
|
147
|
+
} finally { fs.rmSync(consumer, { recursive: true, force: true }); }
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('P11-packed - armed recall emits dz only', () => {
|
|
151
|
+
const { consumer } = installConsumer();
|
|
152
|
+
const bin = fakeDz('ok');
|
|
153
|
+
try {
|
|
154
|
+
assert.equal(parseReceipt(runPackedWriter(consumer,
|
|
155
|
+
insight({ title: LOCAL_PACKED }))).status, 'created');
|
|
156
|
+
const context = promptContext(runPackedPrompt(consumer, bin), 'packed armed recall');
|
|
157
|
+
assert.match(context, new RegExp(DZ_PACKED));
|
|
158
|
+
assert.doesNotMatch(context, new RegExp(LOCAL_PACKED));
|
|
159
|
+
} finally {
|
|
160
|
+
fs.rmSync(bin, { recursive: true, force: true });
|
|
161
|
+
fs.rmSync(consumer, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('P12-packed - failing recall names degradation and emits local only', () => {
|
|
166
|
+
const { consumer } = installConsumer();
|
|
167
|
+
const bin = fakeDz('failing');
|
|
168
|
+
try {
|
|
169
|
+
assert.equal(parseReceipt(runPackedWriter(consumer,
|
|
170
|
+
insight({ title: LOCAL_PACKED }))).status, 'created');
|
|
171
|
+
const context = promptContext(runPackedPrompt(consumer, bin), 'packed failing recall');
|
|
172
|
+
assert.match(context, /dz recall unavailable: exit 19; using local recent insights/i);
|
|
173
|
+
assert.match(context, new RegExp(LOCAL_PACKED));
|
|
174
|
+
assert.doesNotMatch(context, new RegExp(DZ_PACKED));
|
|
175
|
+
} finally {
|
|
176
|
+
fs.rmSync(bin, { recursive: true, force: true });
|
|
177
|
+
fs.rmSync(consumer, { recursive: true, force: true });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('P13-packed - empty recall emits local only', () => {
|
|
182
|
+
const { consumer } = installConsumer();
|
|
183
|
+
const bin = fakeDz('empty');
|
|
184
|
+
try {
|
|
185
|
+
assert.equal(parseReceipt(runPackedWriter(consumer,
|
|
186
|
+
insight({ title: LOCAL_PACKED }))).status, 'created');
|
|
187
|
+
const context = promptContext(runPackedPrompt(consumer, bin), 'packed empty recall');
|
|
188
|
+
assert.match(context, new RegExp(LOCAL_PACKED));
|
|
189
|
+
assert.doesNotMatch(context, new RegExp(DZ_PACKED));
|
|
190
|
+
assert.doesNotMatch(context, /dz recall unavailable/i);
|
|
191
|
+
} finally {
|
|
192
|
+
fs.rmSync(bin, { recursive: true, force: true });
|
|
193
|
+
fs.rmSync(consumer, { recursive: true, force: true });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('P1 — packed harvest writer creates, appends, and deduplicates from a missing carrier without dz', () => {
|
|
198
|
+
const { consumer, packageRoot } = installConsumer();
|
|
199
|
+
try {
|
|
200
|
+
assert.match(tarballSha256, /^[a-f0-9]{64}$/);
|
|
201
|
+
assert.equal(fs.existsSync(path.join(consumer, '.claude', 'insights')), false,
|
|
202
|
+
'fresh init must leave the legitimate missing state intact');
|
|
203
|
+
const harvest = assertConsumerPath(
|
|
204
|
+
path.join(packageRoot, 'templates', '.claude', 'commands', 'harvest.md'), consumer);
|
|
205
|
+
assert.ok((fs.readFileSync(harvest, 'utf8').match(/insight/gi) || []).length > 0,
|
|
206
|
+
'the packed command has no insight-writer binding');
|
|
207
|
+
|
|
208
|
+
assert.equal(parseReceipt(runPackedWriter(consumer, insight())).status, 'created');
|
|
209
|
+
const index = path.join(consumer, '.claude', 'insights', 'index.md');
|
|
210
|
+
const first = fs.readFileSync(index);
|
|
211
|
+
const second = insight({ title: 'Packed append keeps prior bytes', tags: ['append'] });
|
|
212
|
+
assert.equal(parseReceipt(runPackedWriter(consumer, second)).status, 'appended');
|
|
213
|
+
assert.ok(fs.readFileSync(index).subarray(0, first.length).equals(first));
|
|
214
|
+
const beforeReplay = fs.readFileSync(index);
|
|
215
|
+
assert.equal(parseReceipt(runPackedWriter(consumer,
|
|
216
|
+
{ ...second, date: '2026-08-31' })).status, 'duplicate');
|
|
217
|
+
assert.deepEqual(fs.readFileSync(index), beforeReplay);
|
|
218
|
+
assert.equal(fs.existsSync(path.join(consumer, '.dz')), false,
|
|
219
|
+
'the markdown lifecycle must not create dz state');
|
|
220
|
+
} finally { fs.rmSync(consumer, { recursive: true, force: true }); }
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('P17 - deleting .dz retains the packed Markdown record and local delivery', () => {
|
|
224
|
+
const { consumer } = installConsumer();
|
|
225
|
+
try {
|
|
226
|
+
const record = insight({ title: LOCAL_PACKED });
|
|
227
|
+
const written = parseReceipt(runPackedWriter(consumer, record, { path: process.env.PATH }));
|
|
228
|
+
assert.equal(written.status, 'created');
|
|
229
|
+
assert.equal(written.teach.state, 'ok');
|
|
230
|
+
|
|
231
|
+
const query = run('dz', ['recall', '--all', '--json', '--project', consumer], {
|
|
232
|
+
cwd: consumer,
|
|
233
|
+
env: process.env,
|
|
234
|
+
timeout: 5000,
|
|
235
|
+
});
|
|
236
|
+
assertOk(query, 'packed real dz query');
|
|
237
|
+
assert.ok(JSON.parse(query.stdout).some((row) =>
|
|
238
|
+
row.domain === 'p-replicator-insights' && row.pattern.includes(LOCAL_PACKED)),
|
|
239
|
+
'the exact packed writer did not establish the temporary learned projection');
|
|
240
|
+
|
|
241
|
+
const index = path.join(consumer, '.claude', 'insights', 'index.md');
|
|
242
|
+
const before = fs.readFileSync(index);
|
|
243
|
+
const dzState = path.join(consumer, '.dz');
|
|
244
|
+
assert.ok(fs.existsSync(dzState), 'real dz did not establish the deletion fixture');
|
|
245
|
+
fs.rmSync(dzState, { recursive: true, force: true });
|
|
246
|
+
assert.deepEqual(fs.readFileSync(index), before,
|
|
247
|
+
'removing only .dz must not change one Markdown byte');
|
|
248
|
+
|
|
249
|
+
const context = promptContext(runPackedPrompt(consumer, ''), 'packed fallback after .dz removal');
|
|
250
|
+
assert.match(context, new RegExp(LOCAL_PACKED));
|
|
251
|
+
assert.doesNotMatch(context, new RegExp(DZ_PACKED));
|
|
252
|
+
} finally { fs.rmSync(consumer, { recursive: true, force: true }); }
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('P1 - packed harvest writer completes the absent-create-append-delete lifecycle without dz', () => {
|
|
256
|
+
const { consumer, cli } = installConsumer();
|
|
257
|
+
try {
|
|
258
|
+
const index = path.join(consumer, '.claude', 'insights', 'index.md');
|
|
259
|
+
assert.equal(parseReceipt(runPackedWriter(consumer, insight())).status, 'created');
|
|
260
|
+
assert.equal(parseReceipt(runPackedWriter(consumer,
|
|
261
|
+
insight({ title: 'A distinct second record', solution: 'Append it.' }))).status, 'appended');
|
|
262
|
+
|
|
263
|
+
fs.rmSync(path.join(consumer, '.claude', 'insights'), { recursive: true, force: true });
|
|
264
|
+
const hook = assertConsumerPath(
|
|
265
|
+
path.join(consumer, '.claude', 'hooks', 'session-insights.cjs'), consumer);
|
|
266
|
+
const missing = run(process.execPath, [hook], {
|
|
267
|
+
cwd: path.join(consumer, '.claude'),
|
|
268
|
+
env: { ...process.env, PATH: '', CLAUDE_PROJECT_DIR: consumer },
|
|
269
|
+
});
|
|
270
|
+
assertOk(missing, 'packed missing-carrier hook');
|
|
271
|
+
assert.equal(missing.stdout, MISSING_HINT);
|
|
272
|
+
assert.equal(fs.existsSync(path.dirname(index)), false,
|
|
273
|
+
'the SessionStart reader must leave the carrier absent');
|
|
274
|
+
|
|
275
|
+
const doctor = run(process.execPath, [cli, 'doctor'], { cwd: consumer });
|
|
276
|
+
assertOk(doctor, 'packed doctor after carrier removal');
|
|
277
|
+
assert.match(doctor.stdout, /insights carrier:\s*NOT STARTED/i);
|
|
278
|
+
assert.match(doctor.stdout, /\.claude\/insights\/index\.md/);
|
|
279
|
+
|
|
280
|
+
assert.equal(parseReceipt(runPackedWriter(consumer,
|
|
281
|
+
insight({ date: '2026-08-31' }))).status, 'created');
|
|
282
|
+
assert.ok(fs.existsSync(index), 'the next real write must recreate the removed carrier');
|
|
283
|
+
assert.equal(fs.existsSync(path.join(consumer, '.dz')), false);
|
|
284
|
+
} finally { fs.rmSync(consumer, { recursive: true, force: true }); }
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test('P4 - packed doctor names a removed insights carrier without failing', () => {
|
|
288
|
+
const { consumer, cli } = installConsumer();
|
|
289
|
+
try {
|
|
290
|
+
assert.equal(parseReceipt(runPackedWriter(consumer, insight())).status, 'created');
|
|
291
|
+
fs.rmSync(path.join(consumer, '.claude', 'insights'), { recursive: true, force: true });
|
|
292
|
+
const doctor = run(process.execPath, [cli, 'doctor'], { cwd: consumer });
|
|
293
|
+
assertOk(doctor, 'packed doctor missing-state preservation');
|
|
294
|
+
assert.match(doctor.stdout, /insights carrier:\s*NOT STARTED/i);
|
|
295
|
+
assert.match(doctor.stdout, /no \.claude\/insights\/index\.md/i);
|
|
296
|
+
} finally { fs.rmSync(consumer, { recursive: true, force: true }); }
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('P5 - source-only evidence cannot satisfy packed acceptance', () => {
|
|
300
|
+
const consumer = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-provenance-')));
|
|
301
|
+
try {
|
|
302
|
+
const checkoutHook = path.join(PKG, 'templates', '.claude', 'hooks', 'session-insights.cjs');
|
|
303
|
+
assert.throws(() => assertConsumerPath(checkoutHook, consumer),
|
|
304
|
+
/source-only evidence rejected/,
|
|
305
|
+
'the provenance safeguard must fire on a real checkout path');
|
|
306
|
+
} finally { fs.rmSync(consumer, { recursive: true, force: true }); }
|
|
307
|
+
});
|
|
308
|
+
});
|
package/tests/fixtures/prep-traceability-fixture/docs/features/order-refund/01_specification.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Specification — Order refund
|
|
2
|
+
|
|
3
|
+
<!-- FIXTURE: prep-traceability-fixture / feature slug order-refund. Not a live project artifact. -->
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
### FR-order-refund-1 — Validate refund eligibility
|
|
8
|
+
|
|
9
|
+
The service must accept a refund request only while its order is inside the refund window.
|
|
10
|
+
|
|
11
|
+
Acceptance scenarios:
|
|
12
|
+
|
|
13
|
+
- `SC-FR-order-refund-1-1`: Given an eligible paid order, when the buyer requests a refund, then validation succeeds.
|
|
14
|
+
|
|
15
|
+
### FR-order-refund-2 — Notify the buyer
|
|
16
|
+
|
|
17
|
+
The service must notify the buyer after the refund request is accepted.
|
|
18
|
+
|
|
19
|
+
Acceptance scenarios:
|
|
20
|
+
|
|
21
|
+
- `SC-FR-order-refund-2-1`: Given an accepted refund request, when processing completes, then the buyer receives a confirmation.
|
|
22
|
+
|
|
23
|
+
### FR-order-refund-3 — Record the audit event
|
|
24
|
+
|
|
25
|
+
The service must append an immutable audit event for every accepted refund request.
|
|
26
|
+
|
|
27
|
+
Acceptance scenarios:
|
|
28
|
+
|
|
29
|
+
- `SC-FR-order-refund-3-1`: Given an accepted refund request, when it is persisted, then an audit event records the order and request identifiers.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Pseudocode — Order refund
|
|
2
|
+
|
|
3
|
+
<!-- FIXTURE: prep-traceability-fixture / feature slug order-refund. Not a live project artifact. -->
|
|
4
|
+
|
|
5
|
+
## Core Algorithms
|
|
6
|
+
|
|
7
|
+
### Algorithm: Validate refund eligibility
|
|
8
|
+
|
|
9
|
+
REQUIREMENT: `FR-order-refund-1`
|
|
10
|
+
|
|
11
|
+
REALISES: `SC-FR-order-refund-1-1`
|
|
12
|
+
|
|
13
|
+
INPUT: order, current time
|
|
14
|
+
|
|
15
|
+
OUTPUT: eligibility decision
|
|
16
|
+
|
|
17
|
+
STEPS:
|
|
18
|
+
|
|
19
|
+
1. Reject an order that is not paid.
|
|
20
|
+
2. Compare the current time with the refund-window deadline.
|
|
21
|
+
3. Return the eligibility decision.
|
|
22
|
+
|
|
23
|
+
COMPLEXITY: O(1)
|
|
24
|
+
|
|
25
|
+
### Algorithm: Record the refund audit event
|
|
26
|
+
|
|
27
|
+
REQUIREMENT: `FR-order-refund-3`
|
|
28
|
+
|
|
29
|
+
REALISES: `SC-FR-order-refund-3-1`
|
|
30
|
+
|
|
31
|
+
INPUT: order identifier, refund request identifier
|
|
32
|
+
|
|
33
|
+
OUTPUT: persisted audit event
|
|
34
|
+
|
|
35
|
+
STEPS:
|
|
36
|
+
|
|
37
|
+
1. Build an immutable audit event from both identifiers.
|
|
38
|
+
2. Append the event to the order audit stream.
|
|
39
|
+
3. Return the persisted event.
|
|
40
|
+
|
|
41
|
+
COMPLEXITY: O(1)
|
|
42
|
+
|
|
43
|
+
## Scenario Coverage
|
|
44
|
+
|
|
45
|
+
Scenarios in Specification.md: 3 · claimed by an algorithm: 2
|
|
46
|
+
|
|
47
|
+
Not claimed by any algorithm:
|
|
48
|
+
|
|
49
|
+
| Scenario | Reason |
|
|
50
|
+
|---|---|
|
|
51
|
+
| FR-order-refund-2 | out-of-mvp-scope |
|
|
52
|
+
|
|
53
|
+
Claimed by an algorithm but absent from Specification.md:
|
|
54
|
+
|
|
55
|
+
| Algorithm | Claimed ID |
|
|
56
|
+
|---|---|
|
|
57
|
+
| none | none |
|
|
@@ -1,38 +1,42 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-08-
|
|
3
|
-
"totalFiles":
|
|
2
|
+
"generatedAt": "2026-08-31T05:41:46.211Z",
|
|
3
|
+
"totalFiles": 142,
|
|
4
4
|
"files": {
|
|
5
5
|
".claude/agents/doc-validator.md": "e57950758cd4764b2cb54b3f1905567e9dbc1ed20ac6ecd30998614894689f4f",
|
|
6
|
-
".claude/agents/harvest-coordinator.md": "
|
|
6
|
+
".claude/agents/harvest-coordinator.md": "71e524e8379df46bbf457dbc1d4480634e05c99b7d3a4f59a356b8d71a0a4166",
|
|
7
7
|
".claude/agents/product-discoverer.md": "f74683fe91b4357d6fb4024ed47b4d5a129ea6375ca6b223dee97176d74acf1c",
|
|
8
8
|
".claude/agents/replicate-coordinator.md": "e86060110ab188109cf908878206543f414993e14dad01f8dc19f041ee6497a7",
|
|
9
9
|
".claude/commands/deploy.md": "27f5424ee217163c55e5e6b9322a1f0858159407bd24ebd6d6588d6c22a5f890",
|
|
10
10
|
".claude/commands/docs.md": "be03592cb3f1d61b60dc96d3e26d71281ad4eb2d4626012c080ce609a32c96e4",
|
|
11
|
-
".claude/commands/feature.md": "
|
|
12
|
-
".claude/commands/go.md": "
|
|
13
|
-
".claude/commands/harvest.md": "
|
|
14
|
-
".claude/commands/myinsights.md": "
|
|
11
|
+
".claude/commands/feature.md": "2df6ab3e6c015dc294955234192843c24a00e07784ac30994c7fe7572f8de775",
|
|
12
|
+
".claude/commands/go.md": "df92c2ec060707638cc7d26d9c9e6a5fecc381e89915bdb0671c5489fb530574",
|
|
13
|
+
".claude/commands/harvest.md": "5d78e189947b8c91602de89742099f25df4c85f5625299bd77bc073af667e155",
|
|
14
|
+
".claude/commands/myinsights.md": "f31c5e133b5027197359e4179cbba1ab535f990634a69ea7c5c73bd2bc329d68",
|
|
15
15
|
".claude/commands/next.md": "1e4984306c3aa80dfc8734dc1c975dfe5fdf2cf6396ddd759e4c7635646107b8",
|
|
16
16
|
".claude/commands/plan.md": "6fef889f51168981bf4a98b8392a0d595e3d199e1cfaa8e08c0a181ec6c440e4",
|
|
17
|
-
".claude/commands/replicate.md": "
|
|
17
|
+
".claude/commands/replicate.md": "7dbf352a122f415bb987d3c968b905d5a9bda932e619382ce24f9ef2c099723a",
|
|
18
18
|
".claude/commands/run.md": "87144dfe8c56c15ba7c117daa544d8a08b84d7205f21dafcc48d27c6c3995059",
|
|
19
|
-
".claude/commands/start.md": "
|
|
19
|
+
".claude/commands/start.md": "2c9d2bd450d6a8cd6f8f0e958b77ae4430efc745645a11300cf29e85dcb468a6",
|
|
20
20
|
".claude/hooks/autocommit-insights.cjs": "5e7cef87b24d457f25315d79fe15f0dfaf94d9269495d638817f0faee2593c6f",
|
|
21
21
|
".claude/hooks/autocommit-plans.cjs": "9bb404b7f4fda5de03e6c2f16bf8939a79ec1944538767b04bcccc6cf50cc03d",
|
|
22
22
|
".claude/hooks/autocommit-roadmap.cjs": "f4dcc9ffd9a3aed91dd56e011e699fcf8af903cc81594e4ab1ef61e5432a36ad",
|
|
23
23
|
".claude/hooks/check-docs-complete.cjs": "27c05c2adcedc806e395b30b455243193a4fbd0968ce96e415679e1f78db951a",
|
|
24
24
|
".claude/hooks/check-growth-trace.cjs": "91994fc109c8440306c9dd3d5eea176c533eb0818fcdb09bac2b309397d9908a",
|
|
25
|
-
".claude/hooks/check-ports.cjs": "
|
|
26
|
-
".claude/hooks/session-insights.cjs": "
|
|
25
|
+
".claude/hooks/check-ports.cjs": "2385687994c882ebb430cdf8ae2bef9309fe5ba284354671413dc134f8e17694",
|
|
26
|
+
".claude/hooks/session-insights.cjs": "31fbe90d81b01dfb56557ed5d68dbde2211a14d5469c5d3761f45fb63c07c3a7",
|
|
27
27
|
".claude/hooks/state-update.cjs": "47146a76b768ac2273abe8f2f017856a0e28b45afaf90048148da417dd19604e",
|
|
28
|
-
".claude/hooks/statusline.cjs": "
|
|
29
|
-
".claude/
|
|
30
|
-
".claude/rules/
|
|
28
|
+
".claude/hooks/statusline.cjs": "5a842162e710b6cb07f59ee0711f2b190d5e505e4546ea01931ba4ba25be8228",
|
|
29
|
+
".claude/hooks/write-insight.cjs": "743f2d85b037cf522dfaf4775947b0cd331e52f2f304e88360c160e0e8e9a589",
|
|
30
|
+
".claude/rules/cost-of-detection-ladder.md": "f15309ee8f1494478ff7678380dbe8fc8eecd154be36d57c9090cd17126a07da",
|
|
31
|
+
".claude/rules/docker-ports.md": "59eb430f29dd762e8b76ad6d1177fe8b5b250de007856c743387cb88ee157942",
|
|
32
|
+
".claude/rules/feature-lifecycle.md": "4a7bb631bc3600c8aeb5a5a484258298688d87604ac5d2d3cacdff9139d05edc",
|
|
31
33
|
".claude/rules/git-workflow.md": "7d8d2ded47fcc0fb95da3dc226efa6cb3c80d6adf7296977588c181dfce20192",
|
|
32
|
-
".claude/rules/
|
|
33
|
-
".claude/rules/
|
|
34
|
-
".claude/rules/
|
|
35
|
-
".claude/
|
|
34
|
+
".claude/rules/honest-configuration.md": "851bc28e9e2baf1043bc09b982f3df3431bc7af1e076f8ff9df3d6729835bad5",
|
|
35
|
+
".claude/rules/insights-capture.md": "d529e6be79178636ca362d83366b1dd7a843371e8ba4f2b09d65c009fab52dc4",
|
|
36
|
+
".claude/rules/replicate-pipeline.md": "fce6480a56bd4ab2d6908faac5985d7557527b1ca36ad597bb00feabca3688bd",
|
|
37
|
+
".claude/rules/skill-interface-protocol.md": "c3f624d0a8a4c7e0f5744e529eb4486cc0589fdb732b9799bccc75091a6d63ca",
|
|
38
|
+
".claude/rules/swarm-file-evidence.md": "c71a216dbc3fd633c30949823ceac354f8439b96279a1a8dd606afb1b3ef5c45",
|
|
39
|
+
".claude/settings.json": "18db09827dce410667b93e2b1a41ef8e46d4431114733e4304a5fc2192b2fc66",
|
|
36
40
|
".claude/skills/brutal-honesty-review/evals/brutal-honesty-review.yaml": "855c155d55575e12aab23fb3a5fb2fada74d93974ff755dfedac6f2d5bd41f13",
|
|
37
41
|
".claude/skills/brutal-honesty-review/README.md": "2a026b4af50b21456e50424fbfa089cb983fbbaa0c68024c8c17c695bea25a57",
|
|
38
42
|
".claude/skills/brutal-honesty-review/resources/assessment-rubrics.md": "dac09c91c765e2211799cc230738fb72a966f47511edb872306a906e49640cbe",
|
|
@@ -95,7 +99,7 @@
|
|
|
95
99
|
".claude/skills/goap-research-ed25519/scripts/test_suite_completeness.py": "727c5d5b0dad215957012276ebc146397178255077e762ed7b88a60d8178d2ab",
|
|
96
100
|
".claude/skills/goap-research-ed25519/SKILL.md": "404cb696d537d29585ec35f97a86173a582cd6fcf8d5f172a4f5bbd75ec971a8",
|
|
97
101
|
".claude/skills/knowledge-extractor/examples/rate-limiting-extraction.md": "fab68772135532aaa7675dfca4ec068cfa68a847f964f872f201f08fb9ee4034",
|
|
98
|
-
".claude/skills/knowledge-extractor/modules/01-agent-review.md": "
|
|
102
|
+
".claude/skills/knowledge-extractor/modules/01-agent-review.md": "33395c8b87c4d7d1f63dff0891aaebcf50e302cbe2026005b3c6d51817f35a70",
|
|
99
103
|
".claude/skills/knowledge-extractor/modules/02-classify.md": "3b6a59cde451fc89e2e683b7cd35f5535d12cc6072cd40ca35cf7d5f61eb78aa",
|
|
100
104
|
".claude/skills/knowledge-extractor/modules/03-decontextualize.md": "ecb082116577cb86dfa4f43066d3c6d61b5d4a70964cb9ed4f15c6311d526347",
|
|
101
105
|
".claude/skills/knowledge-extractor/modules/04-integrate.md": "15a31634dd52e87bcad78a2b612c821dcd11b91ed21f2ddb2cde741411aa287e",
|
|
@@ -137,7 +141,7 @@
|
|
|
137
141
|
".claude/skills/reverse-engineering-unicorn/references/jtbd-canvas.md": "7938e657af631109ca7b4752002c21ac84a7dad3a35cc522be088f814d102773",
|
|
138
142
|
".claude/skills/reverse-engineering-unicorn/SKILL.md": "20d6b667971f9749d091c1d47e2f75df957f259647cbb861ac54174280096c71",
|
|
139
143
|
".claude/skills/sparc-prd-mini/references/sparc-methodology.md": "9c93dafe8149590809ff6b27508d7df52c8efa7f31f8e8f8052b7124a6561fee",
|
|
140
|
-
".claude/skills/sparc-prd-mini/SKILL.md": "
|
|
144
|
+
".claude/skills/sparc-prd-mini/SKILL.md": "29c74d628f86f79fe2ba7b4d2a69b34de332519b8a11c945568e59830535742a",
|
|
141
145
|
".claude/skills/sparc-prd-mini/templates/CLAUDE.md": "9eeb66752cc4dec5c3c3fa5a8f7cfc3dd282f75d245bd36835f690a945b534ed",
|
|
142
146
|
".claude/skills/sparc-prd-mini/templates/prd.md": "7b4497bc76bb421fae7a07ecb46af01e021412180a394a5487a3c3c42dc004b6"
|
|
143
147
|
}
|
|
@@ -9,6 +9,10 @@ const crypto = require('node:crypto');
|
|
|
9
9
|
const PKG_DIR = path.resolve(__dirname, '..', '..');
|
|
10
10
|
const TEMPLATES = path.join(PKG_DIR, 'templates');
|
|
11
11
|
const BASELINE = path.join(__dirname, 'baseline.json');
|
|
12
|
+
const DETECTION_LADDER = '.claude/rules/cost-of-detection-ladder.md';
|
|
13
|
+
const PACK_LADDER = `templates/${DETECTION_LADDER}`;
|
|
14
|
+
const MANIFEST = path.join(PKG_DIR, '.dz-manifest.json');
|
|
15
|
+
const SBOM = path.join(PKG_DIR, 'sbom.json');
|
|
12
16
|
|
|
13
17
|
function walk(dir, base = dir, out = []) {
|
|
14
18
|
if (!fs.existsSync(dir)) return out;
|
|
@@ -29,6 +33,20 @@ function sha256(p) {
|
|
|
29
33
|
return crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex');
|
|
30
34
|
}
|
|
31
35
|
|
|
36
|
+
function integrityProblems(signed, sbom, file, digest) {
|
|
37
|
+
const problems = [];
|
|
38
|
+
const manifestEntry = signed.manifest.files.find((entry) => entry.path === file);
|
|
39
|
+
if (!manifestEntry) problems.push(`manifest missing ${file}`);
|
|
40
|
+
else if (manifestEntry.sha256 !== digest) problems.push(`manifest digest mismatch for ${file}`);
|
|
41
|
+
|
|
42
|
+
const component = sbom.components.find((entry) => entry.name === file);
|
|
43
|
+
if (!component) problems.push(`SBOM missing ${file}`);
|
|
44
|
+
else if (component.hashes.find((entry) => entry.alg === 'SHA-256')?.content !== digest) {
|
|
45
|
+
problems.push(`SBOM digest mismatch for ${file}`);
|
|
46
|
+
}
|
|
47
|
+
return problems;
|
|
48
|
+
}
|
|
49
|
+
|
|
32
50
|
describe('snapshot: templates/', () => {
|
|
33
51
|
test('baseline.json exists', () => {
|
|
34
52
|
assert.ok(
|
|
@@ -37,6 +55,35 @@ describe('snapshot: templates/', () => {
|
|
|
37
55
|
);
|
|
38
56
|
});
|
|
39
57
|
|
|
58
|
+
test('baseline explicitly registers the consumer detection ladder', () => {
|
|
59
|
+
const baseline = JSON.parse(fs.readFileSync(BASELINE, 'utf8'));
|
|
60
|
+
assert.match(
|
|
61
|
+
baseline.files[DETECTION_LADDER] || '',
|
|
62
|
+
/^[a-f0-9]{64}$/,
|
|
63
|
+
`${DETECTION_LADDER} must have a SHA-256 entry in baseline.json`
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('signed manifest and SBOM register the settled detection-ladder digest', () => {
|
|
68
|
+
const digest = sha256(path.join(TEMPLATES, DETECTION_LADDER));
|
|
69
|
+
const signed = JSON.parse(fs.readFileSync(MANIFEST, 'utf8'));
|
|
70
|
+
const sbom = JSON.parse(fs.readFileSync(SBOM, 'utf8'));
|
|
71
|
+
assert.deepEqual(integrityProblems(signed, sbom, PACK_LADDER, digest), []);
|
|
72
|
+
|
|
73
|
+
const omitted = JSON.parse(JSON.stringify(signed));
|
|
74
|
+
omitted.manifest.files = omitted.manifest.files.filter((entry) => entry.path !== PACK_LADDER);
|
|
75
|
+
assert.deepEqual(integrityProblems(omitted, sbom, PACK_LADDER, digest),
|
|
76
|
+
[`manifest missing ${PACK_LADDER}`],
|
|
77
|
+
'removing the real manifest entry must fire the integrity safeguard');
|
|
78
|
+
|
|
79
|
+
const changed = JSON.parse(JSON.stringify(sbom));
|
|
80
|
+
const component = changed.components.find((entry) => entry.name === PACK_LADDER);
|
|
81
|
+
component.hashes.find((entry) => entry.alg === 'SHA-256').content = '0'.repeat(64);
|
|
82
|
+
assert.deepEqual(integrityProblems(signed, changed, PACK_LADDER, digest),
|
|
83
|
+
[`SBOM digest mismatch for ${PACK_LADDER}`],
|
|
84
|
+
'changing the real SBOM digest must fire the integrity safeguard');
|
|
85
|
+
});
|
|
86
|
+
|
|
40
87
|
test('current templates match recorded baseline (no drift)', () => {
|
|
41
88
|
const baseline = JSON.parse(fs.readFileSync(BASELINE, 'utf8'));
|
|
42
89
|
const current = walk(TEMPLATES);
|
|
@@ -140,7 +140,21 @@ describe('an artifact that is there but says nothing is not verified', () => {
|
|
|
140
140
|
const zero = run(dir, 'doctor');
|
|
141
141
|
assert.equal(zero.code, 0, 'zero entries must not fail either: ' + zero.out);
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
const writer = path.join(dir, '.claude', 'hooks', 'write-insight.cjs');
|
|
144
|
+
const written = spawnSync(process.execPath, [writer], {
|
|
145
|
+
cwd: dir,
|
|
146
|
+
env: { ...process.env, CLAUDE_PROJECT_DIR: dir },
|
|
147
|
+
input: JSON.stringify({
|
|
148
|
+
date: '2026-08-30',
|
|
149
|
+
title: 'Doctor reads writer output',
|
|
150
|
+
tags: ['doctor'],
|
|
151
|
+
problem: 'A synthetic fixture could drift from the real writer format.',
|
|
152
|
+
solution: 'Create the populated state through the installed writer.',
|
|
153
|
+
references: ['tests/unit/absence-is-not-emptiness.test.js'],
|
|
154
|
+
}),
|
|
155
|
+
encoding: 'utf8',
|
|
156
|
+
});
|
|
157
|
+
assert.equal(written.status, 0, written.stderr || written.stdout);
|
|
144
158
|
const some = run(dir, 'doctor');
|
|
145
159
|
assert.equal(some.code, 0);
|
|
146
160
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { describe, 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
|
+
const { spawnSync } = require('node:child_process');
|
|
9
|
+
|
|
10
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..', '..');
|
|
11
|
+
const CHECKER = path.join(PACKAGE_ROOT, 'scripts', 'check-pipeline-gaps.sh');
|
|
12
|
+
|
|
13
|
+
function write(root, relative, body) {
|
|
14
|
+
const target = path.join(root, relative);
|
|
15
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
16
|
+
fs.writeFileSync(target, body);
|
|
17
|
+
return target;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function mapSource(root, relative, heading, values) {
|
|
21
|
+
const lines = Object.entries(values).map(([role, value]) => ` ${role}: ${value}`);
|
|
22
|
+
return write(root, relative, `${heading}\n\n\`\`\`yaml\nDOCUMENT_ROLE_MAP:\n${lines.join('\n')}\n\`\`\`\n`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function run(root, featureMap, projectMap) {
|
|
26
|
+
const result = spawnSync('bash', [
|
|
27
|
+
CHECKER,
|
|
28
|
+
root,
|
|
29
|
+
'--traceability',
|
|
30
|
+
'--role-map-source', featureMap,
|
|
31
|
+
'--project-role-map-source', projectMap,
|
|
32
|
+
], { encoding: 'utf8' });
|
|
33
|
+
return { status: result.status, output: `${result.stdout || ''}${result.stderr || ''}` };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('check-pipeline-gaps executable contract', () => {
|
|
37
|
+
test('ADR Confirmation — the real process compares structural FR NFR and AC keys', (t) => {
|
|
38
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'p-replicator-checker-'));
|
|
39
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
40
|
+
const roles = {
|
|
41
|
+
specification: 'requirements.md',
|
|
42
|
+
pseudocode: 'algorithms.md',
|
|
43
|
+
architecture: 'architecture.md',
|
|
44
|
+
refinement: 'refinement.md',
|
|
45
|
+
completion: 'completion.md',
|
|
46
|
+
};
|
|
47
|
+
const featureMap = mapSource(root, 'feature-map.md', '### Phase 1 document role map', roles);
|
|
48
|
+
const projectMap = mapSource(root, 'project-map.md', '### Project-level default', roles);
|
|
49
|
+
write(root, 'docs/features/payments/requirements.md', [
|
|
50
|
+
'### FR-payments-1 — charge',
|
|
51
|
+
'### NFR-payments-2 — latency',
|
|
52
|
+
'### AC-payments-3 — accepted',
|
|
53
|
+
'',
|
|
54
|
+
].join('\n'));
|
|
55
|
+
write(root, 'docs/features/payments/algorithms.md', [
|
|
56
|
+
'### Algorithm: charge',
|
|
57
|
+
'REQUIREMENT: `FR-payments-1`',
|
|
58
|
+
'REQUIREMENT: `NFR-payments-2`',
|
|
59
|
+
'REQUIREMENT: `AC-payments-3`',
|
|
60
|
+
'',
|
|
61
|
+
].join('\n'));
|
|
62
|
+
|
|
63
|
+
const result = run(root, featureMap, projectMap);
|
|
64
|
+
assert.equal(result.status, 0, result.output);
|
|
65
|
+
assert.match(result.output, /COUNT requirements=3 algorithms=3 missing-algorithm=0 orphan-algorithm=0/);
|
|
66
|
+
assert.match(result.output, /VERDICT traceability=PASS features=1 gaps=0 inconclusive=0/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('the default mode is the same role-mapped traceability contract', (t) => {
|
|
70
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'p-replicator-default-mode-'));
|
|
71
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
72
|
+
const roles = {
|
|
73
|
+
specification: 'requirements custom.md',
|
|
74
|
+
pseudocode: 'algorithms custom.md',
|
|
75
|
+
architecture: 'architecture custom.md',
|
|
76
|
+
refinement: 'refinement custom.md',
|
|
77
|
+
completion: 'completion custom.md',
|
|
78
|
+
};
|
|
79
|
+
const featureMap = mapSource(root, 'feature-map.md', '### Phase 1 document role map', roles);
|
|
80
|
+
const projectMap = mapSource(root, 'project-map.md', '### Project-level default', roles);
|
|
81
|
+
write(root, 'docs/requirements custom.md', '### FR-project-1 — mapped\n');
|
|
82
|
+
write(root, 'docs/algorithms custom.md',
|
|
83
|
+
'### Algorithm: mapped\nREQUIREMENT: `FR-project-1`\n');
|
|
84
|
+
|
|
85
|
+
const result = spawnSync('bash', [
|
|
86
|
+
CHECKER, root, '--role-map-source', featureMap,
|
|
87
|
+
'--project-role-map-source', projectMap,
|
|
88
|
+
], { encoding: 'utf8' });
|
|
89
|
+
const output = `${result.stdout || ''}${result.stderr || ''}`;
|
|
90
|
+
assert.equal(result.status, 0, output);
|
|
91
|
+
assert.match(output, /TRACE contour=project/);
|
|
92
|
+
assert.match(output, /VERDICT traceability=PASS features=0 gaps=0 inconclusive=0/);
|
|
93
|
+
});
|
|
94
|
+
});
|