@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
package/src/commands/verify.js
CHANGED
|
@@ -4,7 +4,7 @@ const path = require('path');
|
|
|
4
4
|
const {
|
|
5
5
|
green, red, yellow, cyan, bold, dim,
|
|
6
6
|
info, success, error: logError,
|
|
7
|
-
readManifest, fileExists,
|
|
7
|
+
readManifest, fileExists, artifactState,
|
|
8
8
|
COMPONENTS, getItemRelativePath,
|
|
9
9
|
} = require('../utils');
|
|
10
10
|
|
|
@@ -44,8 +44,14 @@ function run(options) {
|
|
|
44
44
|
const label = groupKey === 'commands'
|
|
45
45
|
? `/${itemKey}`
|
|
46
46
|
: `${groupKey}: ${itemKey}`;
|
|
47
|
-
|
|
47
|
+
const state = artifactState(full);
|
|
48
|
+
if (state === 'present') {
|
|
48
49
|
pass(`${label} ${dim('— ' + desc)}`);
|
|
50
|
+
} else if (state === 'empty') {
|
|
51
|
+
// NOT "missing". The cures differ: missing -> run `update`; empty -> something truncated
|
|
52
|
+
// your file, and `update` would silently repair it without you ever learning that. Naming
|
|
53
|
+
// both "missing" moves the silence up one level instead of removing it.
|
|
54
|
+
fail(`${label} — EMPTY, cannot load (${rel})`);
|
|
49
55
|
} else {
|
|
50
56
|
fail(`${label} — missing (${rel})`);
|
|
51
57
|
}
|
|
@@ -91,6 +97,25 @@ function run(options) {
|
|
|
91
97
|
console.log('');
|
|
92
98
|
|
|
93
99
|
// ── Summary ─────────────────────────────────────────────────────────────
|
|
100
|
+
// The insights carrier: three states, none of them failing.
|
|
101
|
+
//
|
|
102
|
+
// FR-5 asks for three SURFACES, and the first pass shipped two — doctor and the statusline —
|
|
103
|
+
// leaving verify producing identical output for absent, empty and populated. Cross-family review
|
|
104
|
+
// caught it. A carrier that never existed must not read like one being used and found empty:
|
|
105
|
+
// that indistinguishability is what let 27 recorded insights become 0 across four real projects.
|
|
106
|
+
const insightsIndex = path.join(targetDir, '.claude', 'insights', 'index.md');
|
|
107
|
+
const insightsState = artifactState(insightsIndex);
|
|
108
|
+
if (insightsState === 'missing') {
|
|
109
|
+
hint('insights carrier: NOT STARTED — no .claude/insights/index.md. Record one with /myinsights');
|
|
110
|
+
} else if (insightsState === 'empty') {
|
|
111
|
+
hint('insights carrier: EXISTS but holds ZERO entries — nothing is injected at SessionStart');
|
|
112
|
+
} else {
|
|
113
|
+
const entries = (require('fs').readFileSync(insightsIndex, 'utf-8')
|
|
114
|
+
.match(/^##\s+\d{4}-\d{2}-\d{2}/gm) || []).length;
|
|
115
|
+
pass(`insights carrier: ${entries} entr${entries === 1 ? 'y' : 'ies'} recorded`);
|
|
116
|
+
}
|
|
117
|
+
console.log('');
|
|
118
|
+
|
|
94
119
|
console.log(bold('─'.repeat(60)));
|
|
95
120
|
if (issues === 0 && warnings === 0) {
|
|
96
121
|
success(bold('All artifacts verified.'));
|
package/src/utils.js
CHANGED
|
@@ -94,6 +94,30 @@ function copyDirFiltered(src, dest, filterFn) {
|
|
|
94
94
|
/**
|
|
95
95
|
* Returns true if the path exists.
|
|
96
96
|
*/
|
|
97
|
+
/**
|
|
98
|
+
* Three states, because two were not enough.
|
|
99
|
+
*
|
|
100
|
+
* `fileExists` answers about PRESENCE and is deliberately left alone — it has 31 call sites, several
|
|
101
|
+
* of which ask a genuine presence question about files that may legitimately hold nothing. This is
|
|
102
|
+
* the predicate for the different question: is this artifact USABLE?
|
|
103
|
+
*
|
|
104
|
+
* MEASURED 2026-08-27: 31 artifacts truncated to zero bytes — every SKILL.md, command, rule and
|
|
105
|
+
* agent — and both `verify` and `doctor` reported clean with exit 0. Deleting one was caught. The
|
|
106
|
+
* gap was exactly this: accessSync asks whether the path resolves, never what is in it.
|
|
107
|
+
*
|
|
108
|
+
* Whitespace counts as empty. A file holding a newline is exactly as dead as one holding nothing,
|
|
109
|
+
* and a size check alone would pass it.
|
|
110
|
+
*/
|
|
111
|
+
function artifactState(filePath) {
|
|
112
|
+
let body;
|
|
113
|
+
try {
|
|
114
|
+
body = fs.readFileSync(filePath, 'utf-8');
|
|
115
|
+
} catch {
|
|
116
|
+
return 'missing';
|
|
117
|
+
}
|
|
118
|
+
return body.trim().length === 0 ? 'empty' : 'present';
|
|
119
|
+
}
|
|
120
|
+
|
|
97
121
|
function fileExists(filePath) {
|
|
98
122
|
try {
|
|
99
123
|
fs.accessSync(filePath);
|
|
@@ -524,6 +548,7 @@ function getItemRelativePath(comp, itemKey) {
|
|
|
524
548
|
// ===========================================================================
|
|
525
549
|
|
|
526
550
|
module.exports = {
|
|
551
|
+
artifactState,
|
|
527
552
|
// Colors
|
|
528
553
|
green, red, yellow, blue, cyan, bold, dim, gray,
|
|
529
554
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Capture and recall development insights. Append a new insight to `.claude/insights/index.md` with structured fields (problem, solution, tags).
|
|
2
|
+
description: Capture and recall development insights. Append a new insight to `.claude/insights/index.md` with structured fields (problem, solution, tags). The three most recent are injected into context at SessionStart.
|
|
3
3
|
argument-hint: '[recall <query> | <free-form insight>]'
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -8,9 +8,24 @@ argument-hint: '[recall <query> | <free-form insight>]'
|
|
|
8
8
|
## Purpose
|
|
9
9
|
|
|
10
10
|
Build a project-local knowledge base of "грабли" (rakes) — errors, workarounds,
|
|
11
|
-
discoveries — so they don't have to be re-learned.
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
discoveries — so they don't have to be re-learned.
|
|
12
|
+
|
|
13
|
+
**What actually happens, stated exactly.** The `SessionStart` hook
|
|
14
|
+
(`.claude/hooks/session-insights.cjs`, wired in `.claude/settings.json`) reads
|
|
15
|
+
`.claude/insights/index.md` and injects the **three most recent entries**, by their
|
|
16
|
+
order in the file. It prints them under the heading *"Recent project insights"* —
|
|
17
|
+
which is what they are.
|
|
18
|
+
|
|
19
|
+
**There is no tag matching, and it is not an omission.** The hook fires at
|
|
20
|
+
`SessionStart`, BEFORE you have said anything, so there is no current task to match
|
|
21
|
+
tags against. Tags remain useful for a human reading or grepping the file, and for
|
|
22
|
+
`/myinsights recall <query>`, which searches on demand — when a query exists.
|
|
23
|
+
|
|
24
|
+
**The consequence, so nobody is surprised by it.** The file is append-only and the
|
|
25
|
+
hook takes the LAST three. As a project accumulates entries — `insights-capture.md`
|
|
26
|
+
plans for 50+ — earlier ones stop being injected. Selection by relevance would need
|
|
27
|
+
to happen at a moment when a task is known; that is a separate design question, and
|
|
28
|
+
it is filed rather than quietly implied here.
|
|
14
29
|
|
|
15
30
|
## Modes
|
|
16
31
|
|
|
@@ -69,4 +84,6 @@ mistakes without manual recall.
|
|
|
69
84
|
|
|
70
85
|
- `.claude/rules/insights-capture.md` — when/how to capture
|
|
71
86
|
- `.claude/hooks/session-insights.cjs` — session injection
|
|
72
|
-
- `/harvest` — extracts reusable
|
|
87
|
+
- `/harvest` — extracts reusable knowledge at project end. **Honest limit:** it does
|
|
88
|
+
NOT read `.claude/insights/index.md` today (`grep -ci insight` over `harvest.md`
|
|
89
|
+
returns 0). The capture→harvest link is a stated intention, not a wired path.
|
|
@@ -36,7 +36,13 @@ const DOCS = [
|
|
|
36
36
|
{ file: 'Refinement.md' },
|
|
37
37
|
{ file: 'Completion.md' },
|
|
38
38
|
{ file: 'Research_Findings.md' },
|
|
39
|
-
|
|
39
|
+
// REPORTED, not required. MEASURED 2026-08-27 against a real completed /replicate project:
|
|
40
|
+
// 8 of 9 promised documents were produced and this one was NOT, though replicate.md and
|
|
41
|
+
// sparc-prd-mini both promise it (three places, including a whole SYNTHESIS phase). One project
|
|
42
|
+
// is not enough evidence to decide whether the pipeline is broken or the document is optional in
|
|
43
|
+
// practice — and blocking on it would have refused every project that ran like that one.
|
|
44
|
+
// The discrepancy is filed; until it is settled this reports rather than refuses.
|
|
45
|
+
{ file: 'Final_Summary.md', optional: true, expected: true },
|
|
40
46
|
{ file: 'C4_Diagrams.md', optional: true },
|
|
41
47
|
{ file: 'ADR.md', optional: true },
|
|
42
48
|
];
|
|
@@ -88,6 +94,17 @@ const GAP = /\[GAP:[^\]\n]*\]/g;
|
|
|
88
94
|
|
|
89
95
|
const SUSPECT = /\[[^\]\n]{1,80}\](?![(\[])/g;
|
|
90
96
|
|
|
97
|
+
/**
|
|
98
|
+
* A markdown TASK-LIST CHECKBOX is not a placeholder.
|
|
99
|
+
*
|
|
100
|
+
* MEASURED 2026-08-27 against a real project: `- [ ] AC покрыты автотестами` and 16 siblings were
|
|
101
|
+
* reported as "possibly unfilled". Worse, that project's own Completion.md TEACHES the convention —
|
|
102
|
+
* "флажки `[ ]` при каждом FR-GROWTH-00N" — so this warning fired on the notation the pipeline
|
|
103
|
+
* itself prescribes. Noise on a legitimate convention trains people to ignore warnings, which is
|
|
104
|
+
* the failure this whole checker exists to prevent, one level down.
|
|
105
|
+
*/
|
|
106
|
+
const CHECKBOX = /^\s*(?:[-*+]\s+)?\[[ xX]?\]/;
|
|
107
|
+
|
|
91
108
|
function scan(body) {
|
|
92
109
|
const clean = stripFences(body).replace(GAP, '');
|
|
93
110
|
const blocking = [];
|
|
@@ -98,11 +115,21 @@ function scan(body) {
|
|
|
98
115
|
}
|
|
99
116
|
}
|
|
100
117
|
const warn = [];
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (
|
|
105
|
-
|
|
118
|
+
// Line-wise, so a checkbox can be recognised by its POSITION in the line — `[ ]` anywhere else
|
|
119
|
+
// is not a task item. A table cell holding `| [ ] |` counts too: the same convention, in a table.
|
|
120
|
+
for (const line of clean.split('\n')) {
|
|
121
|
+
if (warn.length >= 3) break;
|
|
122
|
+
// STRIP the checkbox, do not skip the LINE. Skipping it would be an escape hatch: a genuine
|
|
123
|
+
// placeholder could hide behind a checkbox prefix, which is exactly what the test found when
|
|
124
|
+
// the first version skipped whole lines.
|
|
125
|
+
const rest = line.replace(CHECKBOX, '').replace(/\|\s*\[[ xX]?\]\s*\|/g, '| |');
|
|
126
|
+
SUSPECT.lastIndex = 0;
|
|
127
|
+
for (let m = SUSPECT.exec(rest); m !== null && warn.length < 3; m = SUSPECT.exec(rest)) {
|
|
128
|
+
const t = m[0];
|
|
129
|
+
if (/^\[\^?\d+\]$/.test(t)) continue; // a citation or footnote
|
|
130
|
+
if (/^\[[ xX]?\]$/.test(t)) continue; // a bare checkbox mid-line
|
|
131
|
+
warn.push(t.slice(0, 40));
|
|
132
|
+
}
|
|
106
133
|
}
|
|
107
134
|
return { blocking, warn };
|
|
108
135
|
}
|
|
@@ -127,6 +154,7 @@ function main() {
|
|
|
127
154
|
try { body = fs.readFileSync(abs, 'utf-8'); } catch (e) {
|
|
128
155
|
if (e && e.code === 'ENOENT') {
|
|
129
156
|
if (!d.optional) problems.push(d.file + ': отсутствует');
|
|
157
|
+
else if (d.expected) warnings.push(d.file + ': отсутствует, хотя конвейер его обещает');
|
|
130
158
|
continue; // an optional absence is a legitimate answer
|
|
131
159
|
}
|
|
132
160
|
cannotCheck('не читается ' + d.file + ': ' + ((e && e.message) || e));
|
|
@@ -59,8 +59,19 @@ function cannotCheck(reason, hint) {
|
|
|
59
59
|
process.exit(2);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Absolutise ONCE, at the boundary.
|
|
64
|
+
*
|
|
65
|
+
* The invariant this restores: one frame of reference per path. A relative `-f` handed to a
|
|
66
|
+
* subprocess whose cwd we also override is resolved TWICE against two different origins, and the
|
|
67
|
+
* directory component appears twice. Keeping the argument relative past this point is what made
|
|
68
|
+
* `check-ports.cjs projects/01` report `.../projects/01/projects/01/docker-compose.yml`.
|
|
69
|
+
*
|
|
70
|
+
* Absolute from here on means the existence checks, the `-f` argument and the printed cure all name
|
|
71
|
+
* the same object — so the cure REPRODUCES the failure instead of refuting it.
|
|
72
|
+
*/
|
|
62
73
|
function resolveCompose(arg) {
|
|
63
|
-
const target = arg || '.';
|
|
74
|
+
const target = path.resolve(process.cwd(), arg || '.');
|
|
64
75
|
let file = target;
|
|
65
76
|
try {
|
|
66
77
|
if (fs.statSync(target).isDirectory()) file = path.join(target, 'docker-compose.yml');
|
|
@@ -74,16 +85,37 @@ function resolveCompose(arg) {
|
|
|
74
85
|
/** The normalised config. Parsing the raw YAML would re-implement `extends`, interpolation and the
|
|
75
86
|
* short `"5432:5432"` form — and the short form is exactly where a hand parser gets host_ip wrong. */
|
|
76
87
|
function normalisedConfig(file) {
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
// NO cwd override — deliberately, and the deletion is the fix rather than a tidy-up.
|
|
89
|
+
//
|
|
90
|
+
// It used to be `cwd: path.dirname(path.resolve(file))`, which is how the doubling happened: `-f`
|
|
91
|
+
// was relative and got re-resolved against the cwd this very option installed. Absolutising `file`
|
|
92
|
+
// alone would have made the line harmless while leaving the false premise that compose needs its
|
|
93
|
+
// cwd set — and the next relative path added here would reopen the class.
|
|
94
|
+
//
|
|
95
|
+
// MEASURED (Compose v5.1.1), same absolute -f from two different cwds, byte-identical output:
|
|
96
|
+
// project name -> from the file's directory, not cwd
|
|
97
|
+
// build: ./app -> context resolved under the file's directory
|
|
98
|
+
// .env discovery -> the project-dir .env won; the cwd's .env was NOT even a fallback
|
|
99
|
+
// env_file: ./x.env -> compose still demanded the project-dir copy
|
|
100
|
+
// All three candidate justifications are project-directory-derived, and the project directory
|
|
101
|
+
// comes from the -f path. Scoped honestly: this is Compose v2+ semantics; v1 differed.
|
|
102
|
+
const r = spawnSync('docker', ['compose', '-f', file, 'config'], { encoding: 'utf8' });
|
|
79
103
|
if (r.error && r.error.code === 'ENOENT') {
|
|
80
104
|
cannotCheck('docker недоступен на этой машине',
|
|
81
105
|
'без него нормализованный конфиг получить нечем, а разбирать YAML руками — значит ошибиться на короткой форме портов');
|
|
82
106
|
}
|
|
83
107
|
if (r.status !== 0) {
|
|
108
|
+
// Report, do not guess. The old hint said "обычно это незаданная переменная" — a cause that
|
|
109
|
+
// CANNOT produce this exit: a plain unset ${VAR} makes `docker compose config` exit 0 with a
|
|
110
|
+
// warning; only the required form ${VAR:?msg} exits 1. It named a subset of an already-narrow
|
|
111
|
+
// class while the actual cause was this checker's own invocation.
|
|
112
|
+
//
|
|
113
|
+
// And the cure now carries the ABSOLUTE path actually passed. It used to print the relative form
|
|
114
|
+
// without the cwd override — i.e. the invocation that SUCCEEDS — so the tool handed the user a
|
|
115
|
+
// reproducer that refuted it.
|
|
84
116
|
const why = String(r.stderr || '').trim().split('\n')[0] || 'причина неизвестна';
|
|
85
117
|
cannotCheck('docker compose config вернул ошибку: ' + why,
|
|
86
|
-
'
|
|
118
|
+
'повторить ровно то, что делали мы: docker compose -f ' + file + ' config');
|
|
87
119
|
}
|
|
88
120
|
return String(r.stdout || '');
|
|
89
121
|
}
|
|
@@ -198,10 +198,19 @@ function parsePlans() {
|
|
|
198
198
|
return safeListDir(dir).filter((f) => f.endsWith('.md')).length;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
/**
|
|
202
|
+
* THREE states, because two were not enough.
|
|
203
|
+
*
|
|
204
|
+
* `{count:0}` was returned both for a carrier that does not exist and for one that exists and holds
|
|
205
|
+
* nothing — so a project that had never recorded an insight rendered identically to one being used
|
|
206
|
+
* and found empty. That indistinguishability is what let 27 recorded insights become 0 across four
|
|
207
|
+
* real projects without any surface saying so (MEASURED 2026-08-27).
|
|
208
|
+
*/
|
|
201
209
|
function parseInsights() {
|
|
202
210
|
const p = path.join(CWD, '.claude', 'insights', 'index.md');
|
|
203
211
|
const text = safeReadText(p);
|
|
204
|
-
if (
|
|
212
|
+
if (text === null || text === undefined) return { count: 0, lastDate: null, started: false };
|
|
213
|
+
if (!text.trim()) return { count: 0, lastDate: null, started: true };
|
|
205
214
|
const headings = text.match(/^##\s+\d{4}-\d{2}-\d{2}/gm) || [];
|
|
206
215
|
// Last date: extract from last heading
|
|
207
216
|
let lastDate = null;
|
|
@@ -210,7 +219,7 @@ function parseInsights() {
|
|
|
210
219
|
const m = last.match(/\d{4}-\d{2}-\d{2}/);
|
|
211
220
|
if (m) lastDate = m[0];
|
|
212
221
|
}
|
|
213
|
-
return { count: headings.length, lastDate };
|
|
222
|
+
return { count: headings.length, lastDate, started: true };
|
|
214
223
|
}
|
|
215
224
|
|
|
216
225
|
function parseToolkit() {
|
|
@@ -467,7 +476,9 @@ function buildToolkit(toolkit, expected) {
|
|
|
467
476
|
|
|
468
477
|
function buildStatus(insights, lastTest, mcpServers, settingsStatus, keysarium) {
|
|
469
478
|
const parts = [];
|
|
470
|
-
|
|
479
|
+
// '0' meant two different things: no carrier at all, and a carrier holding nothing. A dash
|
|
480
|
+
// says the first; a zero says the second. The reader can now tell which one they are looking at.
|
|
481
|
+
parts.push(`💡 ${bold('Insights')} ${insights.count > 0 ? green('●' + insights.count) : insights.started ? '0' : dim('—')}` +
|
|
471
482
|
(insights.lastDate ? ` ${dim('(' + insights.lastDate + ')')}` : ''));
|
|
472
483
|
|
|
473
484
|
if (lastTest && typeof lastTest.passed === 'number') {
|
|
@@ -498,7 +509,7 @@ function main() {
|
|
|
498
509
|
const validation = safeRun(() => parseValidationScore(), null);
|
|
499
510
|
const adrs = safeRun(() => parseAdrs(), 0);
|
|
500
511
|
const plans = safeRun(() => parsePlans(), 0);
|
|
501
|
-
const insights = safeRun(() => parseInsights(), { count: 0, lastDate: null });
|
|
512
|
+
const insights = safeRun(() => parseInsights(), { count: 0, lastDate: null, started: false });
|
|
502
513
|
const toolkit = safeRun(() => parseToolkit(), { skills: 0, commands: 0, agents: 0, rules: 0, hooks: 0 });
|
|
503
514
|
const expected = parseExpectedToolkit();
|
|
504
515
|
const settingsStatus = safeRun(() => parseSettingsStatus(manifest), null);
|
|
@@ -15,12 +15,34 @@ echo ""
|
|
|
15
15
|
|
|
16
16
|
# Check if file argument provided
|
|
17
17
|
if [ -z "$1" ]; then
|
|
18
|
+
echo "⚠️ check did NOT run: no argument given"
|
|
18
19
|
echo "Usage: $0 <file-or-directory>"
|
|
19
|
-
exit
|
|
20
|
+
exit 2
|
|
20
21
|
fi
|
|
21
22
|
|
|
22
23
|
TARGET="$1"
|
|
23
24
|
|
|
25
|
+
# ── Findings counter ─────────────────────────────────────────────────────────
|
|
26
|
+
#
|
|
27
|
+
# This script DETECTED correctly and could not FAIL. MEASURED 2026-08-27: deliberately awful input
|
|
28
|
+
# produced red verdicts on screen and exit 0, while a nonexistent path exited 1 — "I could not
|
|
29
|
+
# check" was louder than "I found violations", so any gate reading 1 could not tell them apart.
|
|
30
|
+
#
|
|
31
|
+
# Nothing about the detection changed. What was missing is that nobody counted.
|
|
32
|
+
#
|
|
33
|
+
# 0 ran, found nothing
|
|
34
|
+
# 1 ran, found violations — the count is printed
|
|
35
|
+
# 2 COULD NOT CHECK: no argument, or a target that does not exist
|
|
36
|
+
FINDINGS=0
|
|
37
|
+
finding() { FINDINGS=$((FINDINGS + 1)); }
|
|
38
|
+
|
|
39
|
+
if [ ! -e "$TARGET" ]; then
|
|
40
|
+
echo "⚠️ check did NOT run: '$TARGET' does not exist"
|
|
41
|
+
echo " → This is NOT a clean bill: nothing was examined."
|
|
42
|
+
exit 2
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
|
|
24
46
|
# Function to assess correctness
|
|
25
47
|
assess_correctness() {
|
|
26
48
|
echo "📊 CORRECTNESS CHECK"
|
|
@@ -28,7 +50,7 @@ assess_correctness() {
|
|
|
28
50
|
|
|
29
51
|
# Check for common bug patterns
|
|
30
52
|
if grep -r "TODO\|FIXME\|BUG\|HACK" "$TARGET" 2>/dev/null; then
|
|
31
|
-
echo -e "${RED}🔴 FAILING: Found TODO/FIXME/BUG/HACK comments${NC}"
|
|
53
|
+
echo -e "${RED}🔴 FAILING: Found TODO/FIXME/BUG/HACK comments${NC}"; finding
|
|
32
54
|
echo " → This code admits it's broken. Fix it before review."
|
|
33
55
|
return 0
|
|
34
56
|
fi
|
|
@@ -51,14 +73,14 @@ assess_performance() {
|
|
|
51
73
|
# Check for nested loops (potential O(n²))
|
|
52
74
|
nested_loops=$(grep -r "for.*{" "$TARGET" | wc -l)
|
|
53
75
|
if [ "$nested_loops" -gt 5 ]; then
|
|
54
|
-
echo -e "${RED}🔴 FAILING: Found $nested_loops loops${NC}"
|
|
76
|
+
echo -e "${RED}🔴 FAILING: Found $nested_loops loops${NC}"; finding
|
|
55
77
|
echo " → Are you creating O(n²) complexity where O(n) exists?"
|
|
56
78
|
echo " → Use hash maps, sets, or better algorithms."
|
|
57
79
|
fi
|
|
58
80
|
|
|
59
81
|
# Check for synchronous I/O in hot paths
|
|
60
82
|
if grep -r "readFileSync\|writeFileSync" "$TARGET" 2>/dev/null; then
|
|
61
|
-
echo -e "${RED}🔴 FAILING: Synchronous file I/O detected${NC}"
|
|
83
|
+
echo -e "${RED}🔴 FAILING: Synchronous file I/O detected${NC}"; finding
|
|
62
84
|
echo " → You're blocking the event loop. Use async operations."
|
|
63
85
|
fi
|
|
64
86
|
|
|
@@ -74,7 +96,7 @@ assess_error_handling() {
|
|
|
74
96
|
# Check for try/catch usage
|
|
75
97
|
try_count=$(grep -r "try\|catch" "$TARGET" 2>/dev/null | wc -l)
|
|
76
98
|
if [ "$try_count" -eq 0 ]; then
|
|
77
|
-
echo -e "${RED}🔴 FAILING: No error handling found${NC}"
|
|
99
|
+
echo -e "${RED}🔴 FAILING: No error handling found${NC}"; finding
|
|
78
100
|
echo " → What happens when this code fails? It crashes."
|
|
79
101
|
else
|
|
80
102
|
echo -e "${GREEN}✓ Found error handling (verify it's sufficient)${NC}"
|
|
@@ -82,7 +104,7 @@ assess_error_handling() {
|
|
|
82
104
|
|
|
83
105
|
# Check for empty catch blocks
|
|
84
106
|
if grep -A 1 "catch" "$TARGET" 2>/dev/null | grep -q "^\s*}"; then
|
|
85
|
-
echo -e "${RED}🔴 FAILING: Empty catch blocks detected${NC}"
|
|
107
|
+
echo -e "${RED}🔴 FAILING: Empty catch blocks detected${NC}"; finding
|
|
86
108
|
echo " → Swallowing errors silently is worse than crashing."
|
|
87
109
|
fi
|
|
88
110
|
}
|
|
@@ -118,7 +140,7 @@ assess_testability() {
|
|
|
118
140
|
if [ -d "tests" ] || [ -d "test" ] || [ -d "__tests__" ]; then
|
|
119
141
|
echo -e "${GREEN}✓ Test directory exists${NC}"
|
|
120
142
|
else
|
|
121
|
-
echo -e "${RED}🔴 FAILING: No test directory found${NC}"
|
|
143
|
+
echo -e "${RED}🔴 FAILING: No test directory found${NC}"; finding
|
|
122
144
|
echo " → Where are the tests? Did you even test this?"
|
|
123
145
|
fi
|
|
124
146
|
|
|
@@ -177,3 +199,14 @@ echo " - Tests exist and pass"
|
|
|
177
199
|
echo " - Code is clear and maintainable"
|
|
178
200
|
echo ""
|
|
179
201
|
echo "If you wouldn't deploy this to production, don't submit it for review."
|
|
202
|
+
|
|
203
|
+
# ── Verdict ──────────────────────────────────────────────────────────────────
|
|
204
|
+
# ADDED, never substituted: the closing prose above is this skill's character and a reader wants it.
|
|
205
|
+
# What follows is the same answer in a form a gate can act on.
|
|
206
|
+
echo ""
|
|
207
|
+
if [ "$FINDINGS" -gt 0 ]; then
|
|
208
|
+
echo "VERDICT: $FINDINGS finding(s). Not ready."
|
|
209
|
+
exit 1
|
|
210
|
+
fi
|
|
211
|
+
echo "VERDICT: 0 findings."
|
|
212
|
+
exit 0
|
|
@@ -15,17 +15,33 @@ echo ""
|
|
|
15
15
|
|
|
16
16
|
# Check if test directory argument provided
|
|
17
17
|
if [ -z "$1" ]; then
|
|
18
|
+
echo "⚠️ check did NOT run: no argument given"
|
|
18
19
|
echo "Usage: $0 <test-directory>"
|
|
19
|
-
exit
|
|
20
|
+
exit 2
|
|
20
21
|
fi
|
|
21
22
|
|
|
22
23
|
TEST_DIR="$1"
|
|
23
24
|
|
|
25
|
+
# ── Findings counter ─────────────────────────────────────────────────────────
|
|
26
|
+
#
|
|
27
|
+
# This script DETECTED correctly and could not FAIL. MEASURED 2026-08-27: deliberately awful input
|
|
28
|
+
# produced red verdicts on screen and exit 0, while a nonexistent path exited 1 — "I could not
|
|
29
|
+
# check" was louder than "I found violations", so any gate reading 1 could not tell them apart.
|
|
30
|
+
#
|
|
31
|
+
# Nothing about the detection changed. What was missing is that nobody counted.
|
|
32
|
+
#
|
|
33
|
+
# 0 ran, found nothing
|
|
34
|
+
# 1 ran, found violations — the count is printed
|
|
35
|
+
# 2 COULD NOT CHECK: no argument, or a target that does not exist
|
|
36
|
+
FINDINGS=0
|
|
37
|
+
finding() { FINDINGS=$((FINDINGS + 1)); }
|
|
38
|
+
|
|
39
|
+
|
|
24
40
|
# Check if test directory exists
|
|
25
41
|
if [ ! -d "$TEST_DIR" ]; then
|
|
26
|
-
echo
|
|
27
|
-
echo " →
|
|
28
|
-
exit
|
|
42
|
+
echo "⚠️ check did NOT run: test directory '$TEST_DIR' does not exist"
|
|
43
|
+
echo " → This is NOT a clean bill: nothing was examined."
|
|
44
|
+
exit 2
|
|
29
45
|
fi
|
|
30
46
|
|
|
31
47
|
# Function to assess coverage
|
|
@@ -42,7 +58,7 @@ assess_coverage() {
|
|
|
42
58
|
coverage=$(npm run test:coverage 2>&1 | grep -oP '\d+\.\d+(?=%)' | head -1 || echo "0")
|
|
43
59
|
|
|
44
60
|
if (( $(echo "$coverage < 50" | bc -l) )); then
|
|
45
|
-
echo -e "${RED}🔴 RAW: ${coverage}% coverage${NC}"
|
|
61
|
+
echo -e "${RED}🔴 RAW: ${coverage}% coverage${NC}"; finding
|
|
46
62
|
echo " → This is embarrassing. You're barely testing anything."
|
|
47
63
|
elif (( $(echo "$coverage < 80" | bc -l) )); then
|
|
48
64
|
echo -e "${YELLOW}🟡 ACCEPTABLE: ${coverage}% coverage${NC}"
|
|
@@ -83,7 +99,7 @@ assess_edge_cases() {
|
|
|
83
99
|
done
|
|
84
100
|
|
|
85
101
|
if [ "$found_count" -eq 0 ]; then
|
|
86
|
-
echo -e "${RED}🔴 RAW: No edge cases tested${NC}"
|
|
102
|
+
echo -e "${RED}🔴 RAW: No edge cases tested${NC}"; finding
|
|
87
103
|
echo " → You're only testing the happy path. That's not testing."
|
|
88
104
|
elif [ "$found_count" -lt 3 ]; then
|
|
89
105
|
echo -e "${YELLOW}🟡 ACCEPTABLE: Found $found_count edge case patterns${NC}"
|
|
@@ -102,7 +118,7 @@ assess_clarity() {
|
|
|
102
118
|
# Check for descriptive test names
|
|
103
119
|
unclear_tests=$(grep -r "test('test" "$TEST_DIR" 2>/dev/null | wc -l)
|
|
104
120
|
if [ "$unclear_tests" -gt 0 ]; then
|
|
105
|
-
echo -e "${RED}🔴 RAW: Found $unclear_tests unclear test names${NC}"
|
|
121
|
+
echo -e "${RED}🔴 RAW: Found $unclear_tests unclear test names${NC}"; finding
|
|
106
122
|
echo " → 'test1', 'test2' - What are you testing? Use descriptive names."
|
|
107
123
|
fi
|
|
108
124
|
|
|
@@ -129,7 +145,7 @@ assess_speed() {
|
|
|
129
145
|
duration=$((end_time - start_time))
|
|
130
146
|
|
|
131
147
|
if [ "$duration" -gt 60 ]; then
|
|
132
|
-
echo -e "${RED}🔴 RAW: Tests took ${duration}s${NC}"
|
|
148
|
+
echo -e "${RED}🔴 RAW: Tests took ${duration}s${NC}"; finding
|
|
133
149
|
echo " → Unit tests should run in seconds, not minutes."
|
|
134
150
|
echo " → Are you calling real databases/networks?"
|
|
135
151
|
elif [ "$duration" -gt 10 ]; then
|
|
@@ -139,7 +155,7 @@ assess_speed() {
|
|
|
139
155
|
echo -e "${GREEN}🟢 MICHELIN STAR: Tests took ${duration}s${NC}"
|
|
140
156
|
fi
|
|
141
157
|
else
|
|
142
|
-
echo -e "${RED}🔴 FAILING: Tests don't even pass${NC}"
|
|
158
|
+
echo -e "${RED}🔴 FAILING: Tests don't even pass${NC}"; finding
|
|
143
159
|
echo " → Fix your broken tests before worrying about speed."
|
|
144
160
|
fi
|
|
145
161
|
}
|
|
@@ -152,7 +168,7 @@ assess_stability() {
|
|
|
152
168
|
|
|
153
169
|
# Check for flaky patterns
|
|
154
170
|
if grep -ri "setTimeout\|sleep\|wait" "$TEST_DIR" > /dev/null 2>&1; then
|
|
155
|
-
echo -e "${RED}🔴 RAW: Timing-based tests detected${NC}"
|
|
171
|
+
echo -e "${RED}🔴 RAW: Timing-based tests detected${NC}"; finding
|
|
156
172
|
echo " → You're creating flaky tests. Use proper async/await."
|
|
157
173
|
fi
|
|
158
174
|
|
|
@@ -166,7 +182,7 @@ assess_stability() {
|
|
|
166
182
|
done
|
|
167
183
|
|
|
168
184
|
if [ "$failures" -gt 0 ]; then
|
|
169
|
-
echo -e "${RED}🔴 RAW: Tests failed $failures/3 times${NC}"
|
|
185
|
+
echo -e "${RED}🔴 RAW: Tests failed $failures/3 times${NC}"; finding
|
|
170
186
|
echo " → FLAKY TESTS. These are worse than no tests."
|
|
171
187
|
echo " → Fix the non-determinism before merging."
|
|
172
188
|
else
|
|
@@ -221,3 +237,14 @@ echo " - 0% flaky"
|
|
|
221
237
|
echo " - Independent tests"
|
|
222
238
|
echo ""
|
|
223
239
|
echo "You know what good tests look like. Why aren't you writing them?"
|
|
240
|
+
|
|
241
|
+
# ── Verdict ──────────────────────────────────────────────────────────────────
|
|
242
|
+
# ADDED, never substituted: the closing prose above is this skill's character and a reader wants it.
|
|
243
|
+
# What follows is the same answer in a form a gate can act on.
|
|
244
|
+
echo ""
|
|
245
|
+
if [ "$FINDINGS" -gt 0 ]; then
|
|
246
|
+
echo "VERDICT: $FINDINGS finding(s). Not ready."
|
|
247
|
+
exit 1
|
|
248
|
+
fi
|
|
249
|
+
echo "VERDICT: 0 findings."
|
|
250
|
+
exit 0
|
|
@@ -70,13 +70,43 @@ Enhanced hooks and commands for DDD-aware Claude Code instruments.
|
|
|
70
70
|
#!/bin/bash
|
|
71
71
|
# Validates aggregate doesn't exceed size limits
|
|
72
72
|
# Source: Fitness Function FF-02
|
|
73
|
+
#
|
|
74
|
+
# THREE exit codes, and the third is the point:
|
|
75
|
+
# 0 within limits
|
|
76
|
+
# 1 over the limit — the count and the limit are both reported
|
|
77
|
+
# 2 THE CHECK DID NOT RUN — unreadable file, or an unsubstituted threshold
|
|
78
|
+
#
|
|
79
|
+
# A guard that answers "OK" when it could not look turns an unknown into a reassurance. MEASURED
|
|
80
|
+
# before this contract existed: four declarations minified onto ONE line reported OK against a limit
|
|
81
|
+
# of two, a missing file reported OK, and an unsubstituted {{...}} placeholder reported OK FOREVER —
|
|
82
|
+
# `[ 4 -gt "{{MAX_ENTITIES_FROM_FITNESS}}" ]` is an invalid comparison, so the `if` is simply false.
|
|
73
83
|
|
|
74
84
|
FILE="$1"
|
|
75
85
|
MAX_ENTITIES={{MAX_ENTITIES_FROM_FITNESS}}
|
|
76
86
|
MAX_METHODS={{MAX_METHODS_FROM_FITNESS}}
|
|
77
87
|
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
if [ -z "$FILE" ]; then
|
|
89
|
+
echo "⚠️ check did NOT run: no file given (usage: $0 <file>)"
|
|
90
|
+
exit 2
|
|
91
|
+
fi
|
|
92
|
+
if [ ! -r "$FILE" ]; then
|
|
93
|
+
echo "⚠️ check did NOT run: cannot read $FILE"
|
|
94
|
+
exit 2
|
|
95
|
+
fi
|
|
96
|
+
# An unsubstituted placeholder must REFUSE, not pass. Otherwise a generator that failed to
|
|
97
|
+
# substitute ships a guard that can never say no, and says nothing about it.
|
|
98
|
+
case "$MAX_ENTITIES" in
|
|
99
|
+
''|*[!0-9]*)
|
|
100
|
+
echo "⚠️ check did NOT run: MAX_ENTITIES is not a number ('$MAX_ENTITIES')."
|
|
101
|
+
echo " The generator did not substitute {{MAX_ENTITIES_FROM_FITNESS}}."
|
|
102
|
+
exit 2
|
|
103
|
+
;;
|
|
104
|
+
esac
|
|
105
|
+
|
|
106
|
+
# OCCURRENCES, not lines: `grep -c` counts matching LINES, so four declarations on one line count
|
|
107
|
+
# as one. `|| true` because grep exits 1 when nothing matches, which is a legitimate count of zero.
|
|
108
|
+
ENTITY_COUNT=$(grep -oE "class[A-Za-z0-9_ ]*Entity" "$FILE" | wc -l | tr -d ' ')
|
|
109
|
+
[ -z "$ENTITY_COUNT" ] && ENTITY_COUNT=0
|
|
80
110
|
|
|
81
111
|
if [ "$ENTITY_COUNT" -gt "$MAX_ENTITIES" ]; then
|
|
82
112
|
echo "❌ VIOLATION: Aggregate has $ENTITY_COUNT entities (max: $MAX_ENTITIES)"
|
|
@@ -84,7 +114,7 @@ if [ "$ENTITY_COUNT" -gt "$MAX_ENTITIES" ]; then
|
|
|
84
114
|
exit 1
|
|
85
115
|
fi
|
|
86
116
|
|
|
87
|
-
echo "✅ Aggregate size OK"
|
|
117
|
+
echo "✅ Aggregate size OK ($ENTITY_COUNT entities, max $MAX_ENTITIES)"
|
|
88
118
|
exit 0
|
|
89
119
|
```
|
|
90
120
|
|
|
@@ -114,7 +144,13 @@ if [ "$VIOLATIONS" -gt 0 ]; then
|
|
|
114
144
|
echo "Found $VIOLATIONS potential DDD violations"
|
|
115
145
|
fi
|
|
116
146
|
|
|
117
|
-
|
|
147
|
+
# ADVISORY, by design and stated out loud. This reporter never blocks: it prints what it noticed
|
|
148
|
+
# and exits 0 whatever it found. That is a legitimate shape — but a reader must not mistake a
|
|
149
|
+
# reporter for a gate, so it says so in its OWN OUTPUT rather than only in a comment nobody reads.
|
|
150
|
+
echo ""
|
|
151
|
+
echo "ℹ️ advisory only — this reporter never blocks (exit 0 regardless of findings)."
|
|
152
|
+
echo " For a check that can refuse, see validate-aggregate-size.sh (exit 0/1/2)."
|
|
153
|
+
exit 0
|
|
118
154
|
```
|
|
119
155
|
|
|
120
156
|
---
|