@dzhechkov/p-replicator 1.9.0 → 1.10.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 +42 -18
- package/CHANGELOG.md +49 -0
- package/package.json +3 -3
- package/sbom.json +77 -17
- package/src/commands/doctor.js +43 -31
- package/src/commands/verify.js +27 -2
- package/src/utils.js +25 -0
- package/templates/.claude/commands/myinsights.md +22 -5
- package/templates/.claude/hooks/check-docs-complete.cjs +34 -6
- package/templates/.claude/hooks/check-ports.cjs +36 -4
- package/templates/.claude/hooks/statusline.cjs +15 -4
- package/templates/.claude/skills/brutal-honesty-review/scripts/assess-code.sh +40 -7
- package/templates/.claude/skills/brutal-honesty-review/scripts/assess-tests.sh +38 -11
- package/templates/.claude/skills/cc-toolkit-generator-enhanced/references/templates/ddd-hooks-commands.md +40 -4
- package/tests/snapshot/baseline.json +8 -8
- package/tests/unit/absence-is-not-emptiness.test.js +255 -0
- package/tests/unit/assess-scripts.test.js +150 -0
- package/tests/unit/check-docs-complete.test.js +43 -0
- package/tests/unit/check-ports.test.js +99 -0
- package/tests/unit/generated-guard-templates.test.js +134 -0
- package/tests/unit/guard-forms.test.js +302 -0
- package/tests/unit/insights-docs-tell-the-truth.test.js +84 -0
- package/tests/unit/module-copy-identity.test.js +31 -1
- package/tests/unit/shipped-suite-context.test.js +142 -0
- package/tests/unit/sync-templates-guard.test.js +31 -1
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The toolkit generator writes these guards into EVERY project it bootstraps, out of fenced blocks
|
|
4
|
+
// in a markdown reference. They did not inherit the three-code discipline the package's own
|
|
5
|
+
// checkers have, and the result was measured on 2026-08-27 with the threshold substituted to 2:
|
|
6
|
+
//
|
|
7
|
+
// four `class …Entity` on FOUR lines → ❌ VIOLATION, exit 1 correct
|
|
8
|
+
// the SAME four minified onto ONE line → ✅ Aggregate size OK, exit 0
|
|
9
|
+
// the file does not exist → ✅ Aggregate size OK, exit 0
|
|
10
|
+
// the placeholder never substituted → ✅ Aggregate size OK, exit 0 GREEN FOREVER
|
|
11
|
+
//
|
|
12
|
+
// The last is not in the field report and is the worst: `[ 4 -gt "{{MAX_ENTITIES_FROM_FITNESS}}" ]`
|
|
13
|
+
// is an invalid integer comparison, bash errors, the `if` is false, and the script falls through to
|
|
14
|
+
// success. A generator that ever fails to substitute produces a guard that can never refuse.
|
|
15
|
+
//
|
|
16
|
+
// These tests EXTRACT the blocks and RUN them. The defect passed every reading; only execution
|
|
17
|
+
// against real fixtures proves anything.
|
|
18
|
+
|
|
19
|
+
const { test, describe } = require('node:test');
|
|
20
|
+
const assert = require('node:assert/strict');
|
|
21
|
+
const { spawnSync } = require('node:child_process');
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const os = require('node:os');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
|
|
26
|
+
const PKG = path.resolve(__dirname, '..', '..');
|
|
27
|
+
const MD = path.join(PKG, 'templates', '.claude', 'skills', 'cc-toolkit-generator-enhanced',
|
|
28
|
+
'references', 'templates', 'ddd-hooks-commands.md');
|
|
29
|
+
|
|
30
|
+
/** Every fenced shell block, in order — the same text the generator copies out. */
|
|
31
|
+
function blocks() {
|
|
32
|
+
const md = fs.readFileSync(MD, 'utf-8');
|
|
33
|
+
const out = [];
|
|
34
|
+
const re = /^```(?:bash|sh)\s*\n([\s\S]*?)^```/gm;
|
|
35
|
+
for (let m = re.exec(md); m !== null; m = re.exec(md)) out.push(m[1]);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const named = (needle) => {
|
|
40
|
+
const b = blocks().find((x) => x.includes(needle));
|
|
41
|
+
assert.ok(b, 'no fenced block containing ' + needle + ' — the generator has nothing to write');
|
|
42
|
+
return b;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Run a block the way the generator would: substitute the placeholders, write it out, execute it.
|
|
47
|
+
* `substitute: false` leaves the placeholders intact — the un-substituted case.
|
|
48
|
+
*/
|
|
49
|
+
function runBlock(code, args, opts) {
|
|
50
|
+
const o = opts || {};
|
|
51
|
+
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'p-rep-gen-')));
|
|
52
|
+
try {
|
|
53
|
+
let script = code;
|
|
54
|
+
if (o.substitute !== false) {
|
|
55
|
+
script = script
|
|
56
|
+
.replace(/\{\{MAX_ENTITIES_FROM_FITNESS\}\}/g, String(o.maxEntities ?? 2))
|
|
57
|
+
.replace(/\{\{MAX_METHODS_FROM_FITNESS\}\}/g, String(o.maxMethods ?? 10));
|
|
58
|
+
}
|
|
59
|
+
const sh = path.join(dir, 'guard.sh');
|
|
60
|
+
fs.writeFileSync(sh, script);
|
|
61
|
+
for (const [name, body] of Object.entries(o.files || {})) {
|
|
62
|
+
const abs = path.join(dir, name);
|
|
63
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
64
|
+
fs.writeFileSync(abs, body);
|
|
65
|
+
}
|
|
66
|
+
const r = spawnSync('bash', [sh, ...(args || [])], { cwd: dir, encoding: 'utf8' });
|
|
67
|
+
return { code: r.status, out: (r.stdout || '') + (r.stderr || '') };
|
|
68
|
+
} finally { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const FOUR_LINES = 'class AEntity {}\nclass BEntity {}\nclass CEntity {}\nclass DEntity {}\n';
|
|
72
|
+
const FOUR_MINIFIED = 'class AEntity {} class BEntity {} class CEntity {} class DEntity {}\n';
|
|
73
|
+
|
|
74
|
+
describe('a generated guard inherits the discipline the shipped ones have', () => {
|
|
75
|
+
test('P1 - four declarations are four, on four lines or on one', () => {
|
|
76
|
+
// The measured false-green. grep -c counts LINES, so the minified form counted as 1 and the
|
|
77
|
+
// guard blessed four entities against a limit of two.
|
|
78
|
+
const g = named('Aggregate size');
|
|
79
|
+
for (const [label, body] of [['four lines', FOUR_LINES], ['one line', FOUR_MINIFIED]]) {
|
|
80
|
+
const r = runBlock(g, ['src/a.ts'], { files: { 'src/a.ts': body }, maxEntities: 2 });
|
|
81
|
+
assert.equal(r.code, 1, 'four entities over a limit of two must be a VIOLATION (' + label + '): ' + r.out);
|
|
82
|
+
assert.match(r.out, /4/, 'and the count must be reported (' + label + '): ' + r.out);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('P2 - a missing file is exit 2 and is NAMED', () => {
|
|
87
|
+
const g = named('Aggregate size');
|
|
88
|
+
const r = runBlock(g, ['src/nope.ts'], { maxEntities: 2 });
|
|
89
|
+
assert.equal(r.code, 2, 'could-not-read is not "within limits": ' + r.out);
|
|
90
|
+
assert.match(r.out, /nope\.ts/, 'the unreadable path must be named: ' + r.out);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('P3 - an unsubstituted placeholder is exit 2', () => {
|
|
94
|
+
// The mode the field report does not name, and the reason the discipline must hold BEFORE
|
|
95
|
+
// substitution: requiring the generator to always substitute correctly puts the property back
|
|
96
|
+
// on the generator's memory, which is layer 4.
|
|
97
|
+
const g = named('Aggregate size');
|
|
98
|
+
const r = runBlock(g, ['src/a.ts'], { files: { 'src/a.ts': FOUR_LINES }, substitute: false });
|
|
99
|
+
assert.equal(r.code, 2,
|
|
100
|
+
'an unsubstituted threshold made this guard green FOREVER: ' + r.out);
|
|
101
|
+
assert.match(r.out, /MAX_ENTITIES_FROM_FITNESS/,
|
|
102
|
+
'and the placeholder must be named so the generator failure is findable: ' + r.out);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('P4 - the guard can still PASS', () => {
|
|
106
|
+
// A guard that only fails is as useless as one that only passes, and this suite would not tell
|
|
107
|
+
// them apart without this case.
|
|
108
|
+
const g = named('Aggregate size');
|
|
109
|
+
const r = runBlock(g, ['src/a.ts'],
|
|
110
|
+
{ files: { 'src/a.ts': 'class AEntity {}\nclass BEntity {}\n' }, maxEntities: 2 });
|
|
111
|
+
assert.equal(r.code, 0, 'two entities under a limit of two is within limits: ' + r.out);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('P5 - the advisory sibling stays advisory and says so', () => {
|
|
115
|
+
// `exit 0 # Warnings only, don't block` is HONEST, and generated projects may rely on it not
|
|
116
|
+
// blocking. The defect was never the exit code; it was that nothing said so where a user looks.
|
|
117
|
+
const g = named('DDD pattern');
|
|
118
|
+
const r = runBlock(g, ['src/a.ts'], { files: { 'src/a.ts': 'class Thing {}\n' } });
|
|
119
|
+
assert.equal(r.code, 0, 'the advisory reporter must keep exit 0: ' + r.out);
|
|
120
|
+
assert.match(r.out, /advisory|не блокирует|never blocks/i,
|
|
121
|
+
'and must say in its OWN OUTPUT that it does not gate: ' + r.out);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('P6 - all three exit codes come from ONE block in one run', () => {
|
|
125
|
+
// Each case above asserts one direction; a constant-answering script could pass a subset.
|
|
126
|
+
const g = named('Aggregate size');
|
|
127
|
+
const seen = [
|
|
128
|
+
runBlock(g, ['src/a.ts'], { files: { 'src/a.ts': 'class AEntity {}\n' }, maxEntities: 2 }).code,
|
|
129
|
+
runBlock(g, ['src/a.ts'], { files: { 'src/a.ts': FOUR_MINIFIED }, maxEntities: 2 }).code,
|
|
130
|
+
runBlock(g, ['src/nope.ts'], { maxEntities: 2 }).code,
|
|
131
|
+
];
|
|
132
|
+
assert.deepEqual(seen, [0, 1, 2], 'expected within/over/could-not-check: ' + JSON.stringify(seen));
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// A guard is worth exactly what it can refuse. Four shell forms make a guard structurally unable to
|
|
4
|
+
// refuse anything, and all four are recorded from real defects — three of them REPRODUCED in this
|
|
5
|
+
// package on 2026-08-27.
|
|
6
|
+
//
|
|
7
|
+
// The measured one that matters most does NOT live in a .sh file. It is a fenced bash block inside
|
|
8
|
+
// references/templates/ddd-hooks-commands.md, which the toolkit generator writes into EVERY project
|
|
9
|
+
// it bootstraps. Measured there: four `class …Entity` declarations on four lines are caught, the
|
|
10
|
+
// SAME four minified onto one line report "✅ Aggregate size OK", and a missing file reports OK too.
|
|
11
|
+
// A scan limited to *.sh would have missed it entirely — so this test reads markdown as well.
|
|
12
|
+
//
|
|
13
|
+
// THE TRAP THIS TEST IS WRITTEN AROUND: a comment explaining why `grep -c` is wrong necessarily
|
|
14
|
+
// CONTAINS `grep -c`. So does a documentation table of forbidden forms. A mention is not a use —
|
|
15
|
+
// the class that has bitten three separate times in one day, including inside the fix written for
|
|
16
|
+
// it. Comments are stripped before scanning, and P3 is the guard on that.
|
|
17
|
+
|
|
18
|
+
const { test, describe } = require('node:test');
|
|
19
|
+
const assert = require('node:assert/strict');
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
|
|
23
|
+
const PKG = path.resolve(__dirname, '..', '..');
|
|
24
|
+
const TPL = path.join(PKG, 'templates');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The four forbidden forms.
|
|
28
|
+
*
|
|
29
|
+
* Each `re` is applied to COMMENT-STRIPPED shell. `why` is what a reader needs in order to fix it,
|
|
30
|
+
* not a restatement of the pattern.
|
|
31
|
+
*/
|
|
32
|
+
const FORBIDDEN = [
|
|
33
|
+
{
|
|
34
|
+
id: 'grep-c-as-occurrence-count',
|
|
35
|
+
// Fires only on a NON-TRIVIAL pattern. `grep -c ""` counts every line and IS a line count by
|
|
36
|
+
// construction; `grep -c "class.*Entity"` treats matching lines as a count of declarations,
|
|
37
|
+
// which is the measured defect. There is no syntactic difference beyond the pattern itself, and
|
|
38
|
+
// saying so is more honest than pretending the check is complete: `grep -c "^func"` where one
|
|
39
|
+
// per line is guaranteed would false-fire, and needs the opt-out marker below.
|
|
40
|
+
re: /\$\(\s*grep\s+-[a-zA-Z]*c[a-zA-Z]*\s+(?!-)(?!""|''|"\^"|'\^')\S/,
|
|
41
|
+
why: 'grep -c counts matching LINES, not occurrences. Four declarations minified onto one line '
|
|
42
|
+
+ 'count as 1 — MEASURED in ddd-hooks-commands.md, where that exact form reports "OK" for four '
|
|
43
|
+
+ 'entities when the limit is two. Count with `grep -o … | wc -l` when occurrences are meant.',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'uppercase-name-class',
|
|
47
|
+
re: /\[A-Z_\]\+/,
|
|
48
|
+
why: '[A-Z_]+ silently passes any name containing other characters. This package already learned '
|
|
49
|
+
+ 'it once: 06-package-deliver.md records "was {{[A-Z_]+}}, which silently passed {{feature-id}}".',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'bare-grep-substitution-without-not-found-branch',
|
|
53
|
+
// A substitution carrying its own fallback (`|| true`, `|| echo …`) has handled the zero-match
|
|
54
|
+
// exit, so it must not fire. Without this the pattern refused `$(grep … || true)` — the very
|
|
55
|
+
// form it exists to recommend, which is how an eager guard becomes a deleted guard.
|
|
56
|
+
// Two shapes are exempt because grep's exit code cannot reach the variable in either:
|
|
57
|
+
// - an explicit fallback: `$(grep … || true)`
|
|
58
|
+
// - a PIPELINE: `$(grep … | wc -l)` — the exit status belongs to the LAST stage
|
|
59
|
+
// The second was found by this guard firing on the fix written for a different finding in the
|
|
60
|
+
// same session. An eager guard is not a stricter guard; it is a guard people delete.
|
|
61
|
+
re: /^[^\n#]*=\$\(\s*[a-z]*grep(?:(?!\|)[^)])*\)\s*$/m,
|
|
62
|
+
why: 'grep exits 1 when it matches nothing, and a bare $( ) swallows that. Under `set -e` the '
|
|
63
|
+
+ 'script dies; without it the variable is empty and the comparison silently succeeds. Give it '
|
|
64
|
+
+ 'an explicit not-found branch, or `|| true` with the empty case handled.',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'echo-0-appended-to-grep-c',
|
|
68
|
+
re: /grep\s+-c[^\n]*\|\|\s*echo\s+0/,
|
|
69
|
+
why: '`grep -c … || echo 0` prints TWO values when there is no match, because grep -c already '
|
|
70
|
+
+ 'prints 0 and then exits 1. The arithmetic that follows fails and the guard falls through to '
|
|
71
|
+
+ 'success — MEASURED as one of the three false-green inputs in ddd-hooks-commands.md.',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'guard-shaped-script-that-cannot-refuse',
|
|
75
|
+
// Not a grep form — a WHOLE-SCRIPT shape, so it is applied to scripts only (see scan()).
|
|
76
|
+
// A script that prints verdicts and whose only `exit 1` is its own usage check cannot refuse
|
|
77
|
+
// anything it was written to judge. MEASURED 2026-08-27: assess-code.sh returns 0 on code with
|
|
78
|
+
// TODO/FIXME/BUG/HACK, nested infinite loops, an empty catch and eval; assess-tests.sh returns
|
|
79
|
+
// 0 on tests that do not pass — while BOTH return 1 for a nonexistent path. "Could not check"
|
|
80
|
+
// is louder than "found violations", which is the semantics exactly inverted.
|
|
81
|
+
//
|
|
82
|
+
// This is the form the four grep patterns could NOT see. My first pass reported these two
|
|
83
|
+
// scripts for a different reason and the reason was WRONG: their substitutions are pipelines,
|
|
84
|
+
// where grep's exit code never reaches the variable. A guard that finds the right file for the
|
|
85
|
+
// wrong reason will exonerate it the moment the wrong reason is fixed.
|
|
86
|
+
scriptOnly: true,
|
|
87
|
+
test(code) {
|
|
88
|
+
if (!/❌|🔴|VIOLATION|FAIL/.test(code)) return false; // not verdict-shaped
|
|
89
|
+
const exits = [...code.matchAll(/^\s*exit\s+([0-9]+)/gm)].map((m) => m[1]);
|
|
90
|
+
if (!exits.includes('1') && !exits.includes('2')) return true; // cannot refuse at all
|
|
91
|
+
// The principled condition: EVERY non-zero exit happens before the script starts judging.
|
|
92
|
+
// Counting them was wrong — assess-tests.sh has TWO, both setup checks, and slipped through a
|
|
93
|
+
// rule that demanded exactly one. What matters is WHERE the last refusal is: if the script
|
|
94
|
+
// can no longer say no by the time it begins assessing, it cannot refuse its subject.
|
|
95
|
+
const positions = [...code.matchAll(/^\s*exit\s+[1-9]/gm)].map((m) => m.index);
|
|
96
|
+
if (positions.length === 0) return true;
|
|
97
|
+
const lastRefusal = Math.max(...positions);
|
|
98
|
+
// A verdict-shaped line FOLLOWED by a non-zero exit is a REFUSAL, not a judgement — that is
|
|
99
|
+
// the script saying "I cannot check", and it must not count as evidence that the script can
|
|
100
|
+
// refuse its subject. assess-tests.sh prints "🔴 FAILING: Test directory doesn't exist" two
|
|
101
|
+
// lines before its `exit 1`; without this the first-verdict position lands on the setup
|
|
102
|
+
// failure and the whole rule misses.
|
|
103
|
+
const lines = code.split('\n');
|
|
104
|
+
let firstVerdict = -1;
|
|
105
|
+
for (let i = 0; i < lines.length; i++) {
|
|
106
|
+
if (!/❌|🔴|VIOLATION|FAIL/.test(lines[i])) continue;
|
|
107
|
+
const followedByRefusal = lines.slice(i, i + 4).some((l) => /^\s*exit\s+[1-9]/.test(l));
|
|
108
|
+
if (followedByRefusal) continue;
|
|
109
|
+
firstVerdict = lines.slice(0, i).join('\n').length;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
return firstVerdict > 0 && lastRefusal < firstVerdict;
|
|
113
|
+
},
|
|
114
|
+
why: 'this script prints verdicts but its only non-zero exit is the usage check, so it can never '
|
|
115
|
+
+ 'refuse what it judges. Give it the three-code contract: 0 clean, 1 violations found, 2 the '
|
|
116
|
+
+ 'check did not run.',
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
/** Shell with comments removed, so a MENTION of a forbidden form cannot be read as a USE. */
|
|
121
|
+
function stripShellComments(src) {
|
|
122
|
+
return src.split('\n')
|
|
123
|
+
.map((l) => (/^\s*#/.test(l) ? '' : l.replace(/(^|\s)#(?!\{).*$/, '$1')))
|
|
124
|
+
.join('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Fenced blocks whose language tag is a shell, from a markdown file. */
|
|
128
|
+
function shellBlocks(md) {
|
|
129
|
+
const out = [];
|
|
130
|
+
const re = /^```(bash|sh|shell|zsh)\s*\n([\s\S]*?)^```/gm;
|
|
131
|
+
for (let m = re.exec(md); m !== null; m = re.exec(md)) out.push(m[2]);
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function walk(dir, hit) {
|
|
136
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
137
|
+
const p = path.join(dir, e.name);
|
|
138
|
+
if (e.isDirectory()) walk(p, hit);
|
|
139
|
+
else hit(p);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Every shell this package ships: real .sh files, plus fenced shell inside markdown. */
|
|
144
|
+
function shellSources() {
|
|
145
|
+
const out = [];
|
|
146
|
+
walk(TPL, (p) => {
|
|
147
|
+
if (p.endsWith('.sh')) {
|
|
148
|
+
out.push({ file: path.relative(PKG, p), code: fs.readFileSync(p, 'utf-8'), kind: 'script' });
|
|
149
|
+
} else if (p.endsWith('.md')) {
|
|
150
|
+
const md = fs.readFileSync(p, 'utf-8');
|
|
151
|
+
shellBlocks(md).forEach((code, i) => {
|
|
152
|
+
out.push({ file: path.relative(PKG, p) + ` (fenced block #${i + 1})`, code, kind: 'fenced' });
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* An explicit opt-out, on the line itself: `# guard-forms: ok — <reason>`.
|
|
161
|
+
*
|
|
162
|
+
* A guard with no exemption is a guard people delete wholesale the first time it is wrong. The
|
|
163
|
+
* reason is mandatory so the exemption stays reviewable — an unexplained opt-out is the silence this
|
|
164
|
+
* whole file exists to remove, one level down.
|
|
165
|
+
*/
|
|
166
|
+
const OPT_OUT = /#\s*guard-forms:\s*ok\s*[—-]\s*\S/;
|
|
167
|
+
|
|
168
|
+
const scan = (code, kind) => {
|
|
169
|
+
const kept = code.split('\n').filter((l) => !OPT_OUT.test(l)).join('\n');
|
|
170
|
+
const stripped = stripShellComments(kept);
|
|
171
|
+
return FORBIDDEN.filter((f) => {
|
|
172
|
+
// A whole-script shape cannot be judged from a fenced fragment: a block may legitimately show
|
|
173
|
+
// one function of a larger script. Applied to real .sh files only.
|
|
174
|
+
if (f.scriptOnly && kind !== 'script') return false;
|
|
175
|
+
return f.test ? f.test(stripped) : f.re.test(stripped);
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
describe('a guard-shaped script must be able to refuse', () => {
|
|
180
|
+
test('P1 - each forbidden form is DETECTED in a fixture', () => {
|
|
181
|
+
// Without this the whole file could be a set of patterns that match nothing, and every scan
|
|
182
|
+
// below would pass by construction.
|
|
183
|
+
const fixtures = {
|
|
184
|
+
'grep-c-as-occurrence-count': 'N=$(grep -c "class.*Entity" "$FILE")\n',
|
|
185
|
+
'uppercase-name-class': 'grep -oE "\\{\\{[A-Z_]+\\}\\}" "$FILE"\n',
|
|
186
|
+
'bare-grep-substitution-without-not-found-branch': 'HITS=$(grep -n TODO "$FILE")\n',
|
|
187
|
+
'echo-0-appended-to-grep-c': 'N=$(grep -c foo "$F" 2>/dev/null || echo 0)\n',
|
|
188
|
+
};
|
|
189
|
+
for (const [id, code] of Object.entries(fixtures)) {
|
|
190
|
+
const hits = scan(code, 'script').map((f) => f.id);
|
|
191
|
+
assert.ok(hits.includes(id),
|
|
192
|
+
'the pattern for ' + id + ' matched nothing in its own fixture — it guards nothing: '
|
|
193
|
+
+ JSON.stringify(hits));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('P2 - legitimate shell is NOT refused', () => {
|
|
198
|
+
// An eager guard is not a stricter guard: it is a guard people delete.
|
|
199
|
+
const ok = [
|
|
200
|
+
['LINES=$(grep -c "" "$FILE" || true)\n',
|
|
201
|
+
'grep -c with an EMPTY pattern counts every line — a line count by construction — and the '
|
|
202
|
+
+ '|| true handles the zero-count exit. My first draft of this fixture omitted the || true '
|
|
203
|
+
+ 'and was NOT legitimate: grep -c exits 1 when the count is 0, so the bare substitution '
|
|
204
|
+
+ 'really did swallow it. The test caught my own example.'],
|
|
205
|
+
['if grep -q pattern "$FILE"; then echo found; fi\n', 'a quiet membership test'],
|
|
206
|
+
['HITS=$(grep -n TODO "$FILE" || true)\nif [ -z "$HITS" ]; then echo none; fi\n',
|
|
207
|
+
'a substitution WITH an explicit not-found branch'],
|
|
208
|
+
['grep -oE "[A-Za-z_][A-Za-z0-9_]*" "$FILE"\n', 'a name class that is not the narrow one'],
|
|
209
|
+
];
|
|
210
|
+
for (const [code, why] of ok) {
|
|
211
|
+
assert.deepEqual(scan(code, 'script').map((f) => f.id), [],
|
|
212
|
+
'legitimate shell refused (' + why + '): ' + code);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('P3 - the same forms inside comments PASS', () => {
|
|
217
|
+
// A mention is not a use. This class has bitten three separate times in one day, including
|
|
218
|
+
// inside a fix written for it — a whole-file `includes` could not tell the fix's own explanatory
|
|
219
|
+
// comment from the thing it removed.
|
|
220
|
+
const commented = [
|
|
221
|
+
'# never use $(grep -c ...) as an occurrence count\n echo ok\n',
|
|
222
|
+
'# {{[A-Z_]+}} silently passed {{feature-id}} — do not use it\n echo ok\n',
|
|
223
|
+
'echo ok # HITS=$(grep -n TODO "$FILE") would swallow the exit code\n',
|
|
224
|
+
'# grep -c foo "$F" || echo 0 prints TWO values\n echo ok\n',
|
|
225
|
+
];
|
|
226
|
+
for (const code of commented) {
|
|
227
|
+
assert.deepEqual(scan(code, 'script').map((f) => f.id), [],
|
|
228
|
+
'a comment EXPLAINING a forbidden form was read as using it: ' + code);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('P5 - fenced bash inside markdown is scanned', () => {
|
|
233
|
+
// The measured false-green guard is not a .sh file — it is a fenced block the generator writes
|
|
234
|
+
// into every project it bootstraps. A scan limited to *.sh misses it entirely.
|
|
235
|
+
const sources = shellSources();
|
|
236
|
+
const fenced = sources.filter((s) => s.kind === 'fenced');
|
|
237
|
+
assert.ok(fenced.length >= 5,
|
|
238
|
+
'fenced shell blocks must be reachable — found ' + fenced.length);
|
|
239
|
+
const scripts = sources.filter((s) => s.kind === 'script');
|
|
240
|
+
assert.ok(scripts.length >= 1, 'and real .sh files too: ' + scripts.length);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test('P4 - a non-shell fenced block is NOT scanned as shell', () => {
|
|
244
|
+
// A ```js block containing `grep -c` in a string is not a shell guard.
|
|
245
|
+
const md = '```js\nconst cmd = \'grep -c foo bar\';\n```\n';
|
|
246
|
+
assert.deepEqual(shellBlocks(md), [], 'only shell-tagged fences are shell');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('P6 - the package is scanned, and every finding is NAMED with its file and reason', () => {
|
|
250
|
+
// The live inventory. Findings are REPORTED rather than asserted away: the known-bad shipped
|
|
251
|
+
// scripts are filed separately (backlog 5e99d823, 11e62b43) and fixing them inside this change
|
|
252
|
+
// would mix two changes and hide which one did what. What must not happen is that they persist
|
|
253
|
+
// INVISIBLY — so this test fails the moment the count changes in either direction.
|
|
254
|
+
const findings = [];
|
|
255
|
+
for (const src of shellSources()) {
|
|
256
|
+
for (const f of scan(src.code, src.kind)) findings.push({ file: src.file, form: f.id, why: f.why });
|
|
257
|
+
}
|
|
258
|
+
// MEASURED, not guessed: 5 findings across 3 files. The generator block carries THREE forms at
|
|
259
|
+
// once — it is the artifact written into every bootstrapped project, and it is why this test
|
|
260
|
+
// scans markdown.
|
|
261
|
+
// MEASURED. Dropped from 4 when the generator's aggregate guard was rewritten in the same
|
|
262
|
+
// session: the two survivors are assess-code.sh and assess-tests.sh, filed as 11e62b43 and
|
|
263
|
+
// deliberately NOT fixed here. Lower this in the commit that fixes them.
|
|
264
|
+
const KNOWN = 0;
|
|
265
|
+
assert.equal(findings.length, KNOWN,
|
|
266
|
+
'the forbidden-form inventory changed. If you FIXED one, lower KNOWN in the same commit; if a '
|
|
267
|
+
+ 'new one appeared, that is the defect this test exists to catch:\n'
|
|
268
|
+
+ findings.map((f) => ' - ' + f.file + ' :: ' + f.form + '\n ' + f.why).join('\n'));
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('P7 - every entry names its property in words, not in its find string', () => {
|
|
272
|
+
// MONOREPO-ONLY, and measured rather than assumed: files[] ships `tests/`, not `test/`, so the
|
|
273
|
+
// mutation registry does NOT reach a tarball — correctly, it is repo machinery and says nothing
|
|
274
|
+
// about a user's installation. Gated on the same POSITIVE fact the other monorepo-only files
|
|
275
|
+
// use, so a broken detection takes them all down together.
|
|
276
|
+
//
|
|
277
|
+
// Found by running the suite from a freshly packed tarball. My first guess at the cause was
|
|
278
|
+
// wrong — I assumed the shipped .sh scripts were missing; they ship fine. Measuring beat it.
|
|
279
|
+
let siblingPresent = false;
|
|
280
|
+
try {
|
|
281
|
+
siblingPresent = fs.statSync(path.resolve(PKG, '..', 'harness-core', 'package.json')).isFile();
|
|
282
|
+
} catch { siblingPresent = false; }
|
|
283
|
+
if (!siblingPresent) {
|
|
284
|
+
console.log('# SKIP (monorepo-only): the mutation registry is repo machinery and is not '
|
|
285
|
+
+ 'shipped (files[] carries tests/, not test/). This says nothing about your installation.');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// A registry whose entries describe their `find` string stops meaning anything the moment the
|
|
289
|
+
// code is refactored. The gate's contract makes a non-applying mutation a FAILURE, which keeps
|
|
290
|
+
// that honest; the WORDS are what let a human re-derive the entry afterwards.
|
|
291
|
+
const reg = JSON.parse(fs.readFileSync(path.join(PKG, 'test', 'mutation-registry.json'), 'utf-8'));
|
|
292
|
+
assert.ok(reg.entries.length >= 6, 'one entry is not a registry: ' + reg.entries.length);
|
|
293
|
+
for (const e of reg.entries) {
|
|
294
|
+
assert.ok(e.property && e.property.length > 80,
|
|
295
|
+
e.id + ': the property must be stated in words a human can re-derive from: '
|
|
296
|
+
+ JSON.stringify(e.property));
|
|
297
|
+
assert.ok(!e.property.includes(e.mutation.find.trim()),
|
|
298
|
+
e.id + ': the property restates its own find string — it would mean nothing after a refactor');
|
|
299
|
+
assert.ok(fs.existsSync(path.join(PKG, e.file)), e.id + ': names a file that does not exist');
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
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
|
+
const { test, describe } = require('node:test');
|
|
17
|
+
const assert = require('node:assert/strict');
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
|
|
21
|
+
const TPL = path.join(__dirname, '..', '..', 'templates', '.claude');
|
|
22
|
+
const CMD = path.join(TPL, 'commands', 'myinsights.md');
|
|
23
|
+
const HOOK = path.join(TPL, 'hooks', 'session-insights.cjs');
|
|
24
|
+
|
|
25
|
+
const read = (f) => fs.readFileSync(f, 'utf-8');
|
|
26
|
+
/** Prose with code fences and comments removed — a MENTION of a claim is not the claim. */
|
|
27
|
+
const prose = (src) => src.replace(/^```[\s\S]*?^```/gm, '');
|
|
28
|
+
|
|
29
|
+
describe('the insights documents describe the hook that exists', () => {
|
|
30
|
+
test('P1 - no document claims tag matching or relevance at SessionStart', () => {
|
|
31
|
+
const doc = prose(read(CMD));
|
|
32
|
+
for (const lie of [
|
|
33
|
+
/Auto-injected into context on SessionStart for relevant tasks/,
|
|
34
|
+
/when their tags match the current task/,
|
|
35
|
+
]) {
|
|
36
|
+
assert.ok(!lie.test(doc),
|
|
37
|
+
'a removed promise came back: ' + lie + '\n' + doc.slice(0, 200));
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('P2 - the document states what the hook actually selects', () => {
|
|
42
|
+
const doc = read(CMD);
|
|
43
|
+
assert.match(doc, /three most recent/i, 'the real selection must be named');
|
|
44
|
+
assert.match(doc, /by their\s*\n?order in the file/i, 'and how it is ordered');
|
|
45
|
+
assert.match(doc, /There is no tag matching, and it is not an omission/,
|
|
46
|
+
'and WHY there is none, or a future reader files it as a bug');
|
|
47
|
+
assert.match(doc, /BEFORE you have said anything/,
|
|
48
|
+
'the reason must be the timing, which is the load-bearing fact');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('P3 - the code and the doc agree, asserted against the CODE', () => {
|
|
52
|
+
// Pinning only the prose would let the hook change underneath it. This reads the hook.
|
|
53
|
+
const hook = read(HOOK);
|
|
54
|
+
assert.match(hook, /sections\.slice\(-3\)/,
|
|
55
|
+
'if selection changed, myinsights.md must change with it — that is what this test is for');
|
|
56
|
+
assert.ok(!/tag/i.test(hook.replace(/^\s*(\/\/|\*).*$/gm, '')),
|
|
57
|
+
'no tag matching exists in the hook; if it appears, the doc may say so');
|
|
58
|
+
assert.match(hook, /Recent project insights/,
|
|
59
|
+
'the printed heading is the honest one and should stay');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
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
|
+
const doc = read(CMD);
|
|
66
|
+
assert.match(doc, /append-only/, 'the growth behaviour must be named');
|
|
67
|
+
assert.match(doc, /earlier ones stop being injected/,
|
|
68
|
+
'and its consequence, in plain words');
|
|
69
|
+
const rule = read(path.join(TPL, 'rules', 'insights-capture.md'));
|
|
70
|
+
assert.match(rule, /50 entries|> 50/,
|
|
71
|
+
'the rule really does plan for a size the hook cannot show — that is the point');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('P5 - the /harvest link is described as an intention, not a wired path', () => {
|
|
75
|
+
// MEASURED: `grep -ci insight` over harvest.md returns 0. The command promised harvest
|
|
76
|
+
// "extracts reusable patterns from insights", which nothing does.
|
|
77
|
+
const doc = read(CMD);
|
|
78
|
+
assert.match(doc, /does\s*\n?\s*NOT read `\.claude\/insights\/index\.md` today/,
|
|
79
|
+
'the unwired link must be admitted where it is claimed');
|
|
80
|
+
const harvest = read(path.join(TPL, 'commands', 'harvest.md'));
|
|
81
|
+
assert.equal((harvest.match(/insight/gi) || []).length, 0,
|
|
82
|
+
'if harvest ever DOES read insights, this admission must be removed — that is the trigger');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -17,6 +17,35 @@ const crypto = require('node:crypto');
|
|
|
17
17
|
const fs = require('node:fs');
|
|
18
18
|
const path = require('node:path');
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Is this copy sitting inside the monorepo?
|
|
22
|
+
*
|
|
23
|
+
* A POSITIVE fact — the sibling packages EXIST — never the absence of something. An absence-based
|
|
24
|
+
* check would also fire on a broken checkout and quietly disable the guard exactly when something
|
|
25
|
+
* is wrong.
|
|
26
|
+
*
|
|
27
|
+
* MEASURED 2026-08-27: `npm test` from the published 1.9.0 tarball was 288/296. Both failures were
|
|
28
|
+
* monorepo-only BY CONSTRUCTION — one needs `scripts/`, which files[] does not ship; the other
|
|
29
|
+
* compares copies across sibling PACKAGES. Neither says anything about a user's installation, and
|
|
30
|
+
* shipping them red means a user who runs our tests is told their install is broken when it is not.
|
|
31
|
+
*
|
|
32
|
+
* The skip is only acceptable because tests/unit/shipped-suite-context.test.js asserts these files
|
|
33
|
+
* RUN — not skip — inside the monorepo. Without that the skip rots into permanent the day this
|
|
34
|
+
* detection breaks, and nothing would say so.
|
|
35
|
+
*/
|
|
36
|
+
function insideMonorepo() {
|
|
37
|
+
const siblings = path.resolve(__dirname, '..', '..', '..'); // packages/@dzhechkov
|
|
38
|
+
try {
|
|
39
|
+
return fs.statSync(path.join(siblings, 'harness-core', 'package.json')).isFile();
|
|
40
|
+
} catch { return false; }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const MONOREPO_ONLY = !insideMonorepo();
|
|
44
|
+
if (MONOREPO_ONLY) {
|
|
45
|
+
console.log('# SKIP (monorepo-only): sibling package @dzhechkov/harness-core is not present, so '
|
|
46
|
+
+ 'this file cannot compare across packages. This says nothing about your installation.');
|
|
47
|
+
}
|
|
48
|
+
|
|
20
49
|
const PKG = path.resolve(__dirname, '..', '..');
|
|
21
50
|
const REPO = path.resolve(PKG, '..', '..', '..');
|
|
22
51
|
const REL = path.join('.claude', 'skills', 'reverse-engineering-unicorn', 'modules');
|
|
@@ -33,7 +62,8 @@ const COPIES = [
|
|
|
33
62
|
|
|
34
63
|
const sha = (f) => crypto.createHash('sha256').update(fs.readFileSync(f)).digest('hex');
|
|
35
64
|
|
|
36
|
-
describe
|
|
65
|
+
describe.skip = describe.skip || (() => {});
|
|
66
|
+
(MONOREPO_ONLY ? describe.skip : describe)('the four live copies of the growth module agree', () => {
|
|
37
67
|
test('P1 - all four live copies are byte-identical', () => {
|
|
38
68
|
const seen = COPIES.map(([name, dir]) => {
|
|
39
69
|
const f = path.join(dir, '05-growth-engine.md');
|