@dzhechkov/p-replicator 1.10.4 → 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 +12 -10
- 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,315 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test, describe } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { execFileSync, spawnSync } = require('node:child_process');
|
|
6
|
+
const fs = require('node:fs');
|
|
7
|
+
const os = require('node:os');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
|
|
10
|
+
const PKG = path.resolve(__dirname, '..', '..');
|
|
11
|
+
const CLI = path.join(PKG, 'bin', 'cli.js');
|
|
12
|
+
const DOCTOR = path.join(PKG, 'src', 'commands', 'doctor.js');
|
|
13
|
+
const REFERENCE_TIME = new Date(2026, 7, 30, 12, 0, 0);
|
|
14
|
+
const WINDOW_DATES = ['2026-08-28', '2026-08-29', '2026-08-30'];
|
|
15
|
+
|
|
16
|
+
function tempProject(prefix = 'p-rep-doctor-flow-') {
|
|
17
|
+
const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), prefix)));
|
|
18
|
+
execFileSync(process.execPath, [CLI, 'init'], { cwd: root, stdio: 'ignore' });
|
|
19
|
+
return root;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function cleanup(root) {
|
|
23
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function git(root, args, options = {}) {
|
|
27
|
+
const result = spawnSync('git', args, {
|
|
28
|
+
cwd: root,
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
stdio: options.stdio || 'pipe',
|
|
31
|
+
env: { ...process.env, ...(options.env || {}) },
|
|
32
|
+
});
|
|
33
|
+
assert.equal(result.status, 0,
|
|
34
|
+
`git ${args.join(' ')} failed: ${result.stderr || result.error || ''}`);
|
|
35
|
+
return result.stdout || '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function initGit(root) {
|
|
39
|
+
git(root, ['init', '-q']);
|
|
40
|
+
git(root, ['config', 'user.name', 'P-Replicator Test']);
|
|
41
|
+
git(root, ['config', 'user.email', 'p-replicator@example.invalid']);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let commitSequence = 0;
|
|
45
|
+
function commit(root, subject, date) {
|
|
46
|
+
commitSequence += 1;
|
|
47
|
+
fs.writeFileSync(path.join(root, `.doctor-flow-${commitSequence}.txt`), `${subject}\n${date}\n`);
|
|
48
|
+
git(root, ['add', '-A']);
|
|
49
|
+
const env = {
|
|
50
|
+
GIT_AUTHOR_DATE: `${date}T12:00:00`,
|
|
51
|
+
GIT_COMMITTER_DATE: `${date}T12:00:00`,
|
|
52
|
+
};
|
|
53
|
+
git(root, ['commit', '-qm', subject], { env });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function writeInsights(root, records) {
|
|
57
|
+
const index = path.join(root, '.claude', 'insights', 'index.md');
|
|
58
|
+
fs.mkdirSync(path.dirname(index), { recursive: true });
|
|
59
|
+
fs.writeFileSync(index, ['# Project Insights', '', ...records.map(({ date, title }, i) => [
|
|
60
|
+
`## ${date} — ${title}`,
|
|
61
|
+
'',
|
|
62
|
+
'**Problem:**',
|
|
63
|
+
`Problem ${i + 1}.`,
|
|
64
|
+
'',
|
|
65
|
+
'**Solution:**',
|
|
66
|
+
`Solution ${i + 1}.`,
|
|
67
|
+
'',
|
|
68
|
+
'---',
|
|
69
|
+
'',
|
|
70
|
+
].join('\n'))].join('\n'));
|
|
71
|
+
return index;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function runDoctor(root) {
|
|
75
|
+
delete require.cache[require.resolve(DOCTOR)];
|
|
76
|
+
const doctor = require(DOCTOR);
|
|
77
|
+
const lines = [];
|
|
78
|
+
const originalLog = console.log;
|
|
79
|
+
const previousExitCode = process.exitCode;
|
|
80
|
+
console.log = (...args) => lines.push(args.map(String).join(' '));
|
|
81
|
+
process.exitCode = undefined;
|
|
82
|
+
try {
|
|
83
|
+
doctor({ targetDir: root, now: REFERENCE_TIME });
|
|
84
|
+
return { code: process.exitCode ?? 0, out: lines.join('\n') + '\n' };
|
|
85
|
+
} finally {
|
|
86
|
+
console.log = originalLog;
|
|
87
|
+
process.exitCode = previousExitCode;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function flowLines(out) {
|
|
92
|
+
return out.split('\n').filter((line) => /Insight flow/i.test(line));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function assertWindow(line) {
|
|
96
|
+
assert.match(line, /2026-08-28/);
|
|
97
|
+
assert.match(line, /2026-08-30/);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function measuredProject(prefix) {
|
|
101
|
+
const root = tempProject(prefix);
|
|
102
|
+
initGit(root);
|
|
103
|
+
commit(root, 'feat: establish project history', '2026-08-27');
|
|
104
|
+
return root;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
describe('doctor insight-flow observation', () => {
|
|
108
|
+
test('P1 reports one shared three-day window with structural fix and insight counts', () => {
|
|
109
|
+
const root = measuredProject();
|
|
110
|
+
try {
|
|
111
|
+
commit(root, 'fix: first in-window repair', WINDOW_DATES[0]);
|
|
112
|
+
commit(root, 'fix(api): second in-window repair', WINDOW_DATES[1]);
|
|
113
|
+
commit(root, 'fix: third in-window repair', WINDOW_DATES[2]);
|
|
114
|
+
commit(root, 'feat: fix wording in documentation', WINDOW_DATES[2]);
|
|
115
|
+
commit(root, 'docs: describe fix commits', WINDOW_DATES[2]);
|
|
116
|
+
commit(root, 'fix: outside the window', '2026-08-31');
|
|
117
|
+
writeInsights(root, [
|
|
118
|
+
{ date: '2026-08-27', title: 'Outside before' },
|
|
119
|
+
{ date: WINDOW_DATES[0], title: 'First in window' },
|
|
120
|
+
{ date: WINDOW_DATES[2], title: 'Second in window' },
|
|
121
|
+
{ date: '2026-08-31', title: 'Outside after' },
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
const result = runDoctor(root);
|
|
125
|
+
const lines = flowLines(result.out);
|
|
126
|
+
assert.equal(lines.length, 1, result.out);
|
|
127
|
+
assertWindow(lines[0]);
|
|
128
|
+
assert.match(lines[0], /3 fix commits/);
|
|
129
|
+
assert.match(lines[0], /2 insight records/);
|
|
130
|
+
assert.equal(result.code, 0, result.out);
|
|
131
|
+
} finally { cleanup(root); }
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('P2 reports A1 no repository as not performed without changing exit code', () => {
|
|
135
|
+
const root = tempProject();
|
|
136
|
+
try {
|
|
137
|
+
const result = runDoctor(root);
|
|
138
|
+
const lines = flowLines(result.out);
|
|
139
|
+
assert.equal(lines.length, 1, result.out);
|
|
140
|
+
assert.match(lines[0], /check NOT performed/);
|
|
141
|
+
assert.match(lines[0], /not a git repository/);
|
|
142
|
+
assert.doesNotMatch(lines[0], /0 fix commits/);
|
|
143
|
+
assert.equal(result.code, 0, result.out);
|
|
144
|
+
} finally { cleanup(root); }
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('P3 reports A2 no readable history as not performed without changing exit code', () => {
|
|
148
|
+
const root = tempProject();
|
|
149
|
+
try {
|
|
150
|
+
initGit(root);
|
|
151
|
+
const result = runDoctor(root);
|
|
152
|
+
const lines = flowLines(result.out);
|
|
153
|
+
assert.equal(lines.length, 1, result.out);
|
|
154
|
+
assert.match(lines[0], /check NOT performed/);
|
|
155
|
+
assert.match(lines[0], /no readable git history/);
|
|
156
|
+
assert.doesNotMatch(lines[0], /0 fix commits/);
|
|
157
|
+
assert.equal(result.code, 0, result.out);
|
|
158
|
+
} finally { cleanup(root); }
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('P4 treats A3 missing insights carrier as measured zero', () => {
|
|
162
|
+
const root = measuredProject();
|
|
163
|
+
try {
|
|
164
|
+
commit(root, 'fix: measured without a carrier', WINDOW_DATES[1]);
|
|
165
|
+
const result = runDoctor(root);
|
|
166
|
+
const line = flowLines(result.out)[0] || '';
|
|
167
|
+
assertWindow(line);
|
|
168
|
+
assert.match(line, /1 fix commit(?:s)?/);
|
|
169
|
+
assert.match(line, /0 insight records/);
|
|
170
|
+
assert.doesNotMatch(line, /NOT performed/);
|
|
171
|
+
assert.equal(result.code, 0, result.out);
|
|
172
|
+
} finally { cleanup(root); }
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('P5 treats A4 zero fix commits as measured zero', () => {
|
|
176
|
+
const root = measuredProject();
|
|
177
|
+
try {
|
|
178
|
+
commit(root, 'feat: in-window non-fix work', WINDOW_DATES[1]);
|
|
179
|
+
writeInsights(root, [{ date: WINDOW_DATES[1], title: 'One measured insight' }]);
|
|
180
|
+
const result = runDoctor(root);
|
|
181
|
+
const line = flowLines(result.out)[0] || '';
|
|
182
|
+
assertWindow(line);
|
|
183
|
+
assert.match(line, /0 fix commits/);
|
|
184
|
+
assert.match(line, /1 insight record(?:s)?/);
|
|
185
|
+
assert.doesNotMatch(line, /NOT performed/);
|
|
186
|
+
assert.equal(result.code, 0, result.out);
|
|
187
|
+
} finally { cleanup(root); }
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test('P6 keeps claimant-shaped 19/0 evidence neutral and non-gating', () => {
|
|
191
|
+
const root = measuredProject();
|
|
192
|
+
try {
|
|
193
|
+
for (let i = 1; i <= 19; i += 1) {
|
|
194
|
+
commit(root, `fix: claimant-shaped repair ${i}`, WINDOW_DATES[(i - 1) % 3]);
|
|
195
|
+
}
|
|
196
|
+
const result = runDoctor(root);
|
|
197
|
+
const lines = flowLines(result.out);
|
|
198
|
+
assert.equal(lines.length, 1, result.out);
|
|
199
|
+
assertWindow(lines[0]);
|
|
200
|
+
assert.match(lines[0], /19 fix commits/);
|
|
201
|
+
assert.match(lines[0], /0 insight records/);
|
|
202
|
+
assert.doesNotMatch(lines[0], /healthy|clean|few|warning|problem|broken|unused|capture/i);
|
|
203
|
+
assert.doesNotMatch(lines[0], /[✓✗⚠!]/);
|
|
204
|
+
assert.equal(result.code, 0, result.out);
|
|
205
|
+
} finally { cleanup(root); }
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test('P7 re-reads git and insights on each invocation', () => {
|
|
209
|
+
const root = measuredProject();
|
|
210
|
+
try {
|
|
211
|
+
commit(root, 'fix: first live repair', WINDOW_DATES[0]);
|
|
212
|
+
const index = writeInsights(root,
|
|
213
|
+
[{ date: WINDOW_DATES[0], title: 'First live insight' }]);
|
|
214
|
+
const first = flowLines(runDoctor(root).out)[0] || '';
|
|
215
|
+
assert.match(first, /1 fix commit(?:s)?/);
|
|
216
|
+
assert.match(first, /1 insight record(?:s)?/);
|
|
217
|
+
|
|
218
|
+
commit(root, 'fix(core): second live repair', WINDOW_DATES[2]);
|
|
219
|
+
fs.appendFileSync(index, [
|
|
220
|
+
`## ${WINDOW_DATES[2]} — Second live insight`,
|
|
221
|
+
'',
|
|
222
|
+
'**Problem:** Changed after the first doctor run.',
|
|
223
|
+
'',
|
|
224
|
+
'**Solution:** Read the carrier again.',
|
|
225
|
+
'',
|
|
226
|
+
].join('\n'));
|
|
227
|
+
const second = flowLines(runDoctor(root).out)[0] || '';
|
|
228
|
+
assert.match(second, /2 fix commits/);
|
|
229
|
+
assert.match(second, /2 insight records/);
|
|
230
|
+
} finally { cleanup(root); }
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('P8 reports an unreadable insight carrier as not performed', () => {
|
|
234
|
+
const root = measuredProject();
|
|
235
|
+
try {
|
|
236
|
+
commit(root, 'fix: carrier failure remains unknown', WINDOW_DATES[1]);
|
|
237
|
+
const index = path.join(root, '.claude', 'insights', 'index.md');
|
|
238
|
+
fs.mkdirSync(index, { recursive: true });
|
|
239
|
+
const result = runDoctor(root);
|
|
240
|
+
const line = flowLines(result.out)[0] || '';
|
|
241
|
+
assert.match(line, /check NOT performed/);
|
|
242
|
+
assert.match(line, /insight carrier.*unreadable/i);
|
|
243
|
+
assert.doesNotMatch(line, /insight records/);
|
|
244
|
+
assert.equal(result.code, 0, result.out);
|
|
245
|
+
} finally { cleanup(root); }
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('P9 preserves existing zero and nonzero doctor exits across flow states', () => {
|
|
249
|
+
for (const broken of [false, true]) {
|
|
250
|
+
const root = tempProject();
|
|
251
|
+
try {
|
|
252
|
+
if (broken) {
|
|
253
|
+
fs.rmSync(path.join(root, '.claude', 'rules', 'docker-ports.md'));
|
|
254
|
+
}
|
|
255
|
+
const expected = broken ? 1 : 0;
|
|
256
|
+
const noRepository = runDoctor(root);
|
|
257
|
+
assert.equal(noRepository.code, expected, noRepository.out);
|
|
258
|
+
assert.match(flowLines(noRepository.out)[0] || '', /NOT performed/);
|
|
259
|
+
|
|
260
|
+
initGit(root);
|
|
261
|
+
commit(root, 'fix: measured state', WINDOW_DATES[1]);
|
|
262
|
+
const measured = runDoctor(root);
|
|
263
|
+
assert.equal(measured.code, expected, measured.out);
|
|
264
|
+
assert.match(flowLines(measured.out)[0] || '', /1 fix commit(?:s)?/);
|
|
265
|
+
|
|
266
|
+
const index = path.join(root, '.claude', 'insights', 'index.md');
|
|
267
|
+
fs.mkdirSync(index, { recursive: true });
|
|
268
|
+
const unavailable = runDoctor(root);
|
|
269
|
+
assert.equal(unavailable.code, expected, unavailable.out);
|
|
270
|
+
assert.match(flowLines(unavailable.out)[0] || '', /NOT performed/);
|
|
271
|
+
} finally { cleanup(root); }
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('P10 package scripts include the doctor insight-flow suite', () => {
|
|
276
|
+
const pkg = require(path.join(PKG, 'package.json'));
|
|
277
|
+
for (const name of ['test', 'test:unit']) {
|
|
278
|
+
const occurrences = pkg.scripts[name]
|
|
279
|
+
.split('doctor-insight-flow.test.js').length - 1;
|
|
280
|
+
assert.equal(occurrences, 1, `${name} must run the suite exactly once`);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('P11 README describes the doctor insight-flow observation as informational and non-gating', () => {
|
|
285
|
+
const readme = fs.readFileSync(path.join(PKG, 'README.md'), 'utf8');
|
|
286
|
+
const start = readme.indexOf('### Doctor insight-flow observation');
|
|
287
|
+
const end = readme.indexOf('\n### ', start + 1);
|
|
288
|
+
assert.ok(start >= 0 && end > start, 'README must contain a bounded doctor insight-flow section');
|
|
289
|
+
const section = readme.slice(start, end);
|
|
290
|
+
assert.match(section, /three calendar days/i);
|
|
291
|
+
assert.match(section, /git history/i);
|
|
292
|
+
assert.match(section, /\.claude\/insights\/index\.md/);
|
|
293
|
+
assert.match(section, /fix commits[\s\S]*insight records/i);
|
|
294
|
+
assert.match(section, /check NOT performed/);
|
|
295
|
+
assert.match(section, /does not change[^.]*exit status/i);
|
|
296
|
+
assert.match(section, /does not interpret|no threshold/i);
|
|
297
|
+
assert.doesNotMatch(section, /19\s+fix|0\s+insight/i,
|
|
298
|
+
'claimant field figures are not locally reproduced product evidence');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('P12 uses argument-array git execution for metacharacter paths and subjects', () => {
|
|
302
|
+
const root = measuredProject('p rep ; touch P12_SIDE_EFFECT ; ');
|
|
303
|
+
try {
|
|
304
|
+
commit(root, 'fix(shell): $(touch P12_SUBJECT_SIDE_EFFECT)', WINDOW_DATES[1]);
|
|
305
|
+
const result = runDoctor(root);
|
|
306
|
+
const line = flowLines(result.out)[0] || '';
|
|
307
|
+
assert.match(line, /1 fix commit(?:s)?/);
|
|
308
|
+
assert.equal(fs.existsSync(path.join(root, 'P12_SIDE_EFFECT')), false,
|
|
309
|
+
'targetDir metacharacters must never reach a shell');
|
|
310
|
+
assert.equal(fs.existsSync(path.join(root, 'P12_SUBJECT_SIDE_EFFECT')), false,
|
|
311
|
+
'commit subjects are data, never executable text');
|
|
312
|
+
assert.equal(result.code, 0, result.out);
|
|
313
|
+
} finally { cleanup(root); }
|
|
314
|
+
});
|
|
315
|
+
});
|
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
// contact-with-reality defect in this pipeline; everything else it checks, it can check by reading
|
|
8
8
|
// its own output.
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// 2. No external fact may be baked in. What an API can do DRIFTS: a fixture asserting one would
|
|
10
|
+
// One honesty constraint binds this suite from the field report:
|
|
11
|
+
// No external fact may be baked in. What an API can do DRIFTS: a fixture asserting one would
|
|
13
12
|
// fail when the world changes and pass when our own rule breaks. P6 asserts the ABSENCE of
|
|
14
13
|
// vendor names, and no assertion here mentions a capability of any real service.
|
|
15
14
|
|
|
@@ -188,22 +187,23 @@ describe('the pipeline asks whether the outside world can do what a requirement
|
|
|
188
187
|
'doc-validator.md says ' + stated[1] + ' but tables ' + inAgent.size);
|
|
189
188
|
});
|
|
190
189
|
|
|
191
|
-
test('P7 —
|
|
192
|
-
// The report cites it. MEASURED — a repo-wide find returns nothing. Pointing a reader at a
|
|
193
|
-
// script that is not there is a worse failure than the gap it was cited to close.
|
|
194
|
-
for (const f of [SPARC, REPLICATE]) {
|
|
195
|
-
assert.ok(!read(f).includes('check-pipeline-gaps'),
|
|
196
|
-
f + ' references a script that does not exist in this repository');
|
|
197
|
-
}
|
|
198
|
-
// The premise, asserted rather than assumed: if someone ever ADDS the script, this goes red and
|
|
199
|
-
// whoever added it has to revisit the rule instead of leaving a stale prohibition behind.
|
|
190
|
+
test('P7 — check-pipeline-gaps is package-owned and deliberately wired', () => {
|
|
200
191
|
const pkgRoot = path.join(__dirname, '..', '..');
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
192
|
+
const canonical = path.join(pkgRoot, 'scripts', 'check-pipeline-gaps.sh');
|
|
193
|
+
const projection = path.join(TPL, 'hooks', 'check-pipeline-gaps.sh');
|
|
194
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf8'));
|
|
195
|
+
assert.ok(fs.statSync(canonical).isFile(), 'the package canon must exist');
|
|
196
|
+
assert.equal(fs.existsSync(projection), false,
|
|
197
|
+
'the Node-only hooks surface must not duplicate the packaged shell utility');
|
|
198
|
+
assert.ok(pkg.files.includes('scripts/check-pipeline-gaps.sh'),
|
|
199
|
+
'the exact checker path must be in the npm allowlist');
|
|
200
|
+
assert.match(read('commands/feature.md'),
|
|
201
|
+
/require\.resolve\('@dzhechkov\/p-replicator\/scripts\/check-pipeline-gaps\.sh'\)/,
|
|
202
|
+
'/feature must resolve the checker from the installed package');
|
|
203
|
+
assert.doesNotMatch(read('commands/feature.md'), /\.claude\/hooks\/check-pipeline-gaps\.sh/);
|
|
204
|
+
assert.match(read(SPARC), /FR\/NFR\/AC machine-key wire format/,
|
|
205
|
+
'the authoring skill must define the wire format consumed by the checker');
|
|
206
|
+
assert.ok(!read('settings.json').includes('check-pipeline-gaps'),
|
|
207
|
+
'the deliberate blocking gate must not be registered as a non-blocking event hook');
|
|
208
208
|
});
|
|
209
209
|
});
|