baldart 5.8.0 → 5.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/CHANGELOG.md +25 -0
- package/README.md +30 -29
- package/VERSION +1 -1
- package/framework/.claude/agents/CHANGELOG.md +42 -0
- package/framework/.claude/agents/REGISTRY.md +4 -0
- package/framework/.claude/agents/code-simplifier.md +120 -0
- package/framework/.claude/agents/coder.md +3 -1
- package/framework/.claude/agents/security-reviewer.md +53 -1
- package/framework/.claude/skills/new/references/review-cycle.md +5 -6
- package/framework/.claude/skills/new/references/team-mode.md +1 -1
- package/framework/.claude/skills/simplify/CHANGELOG.md +7 -0
- package/framework/.claude/skills/simplify/SKILL.md +28 -8
- package/framework/.claude/skills/simplify/scripts/simplify-scan.mjs +234 -0
- package/framework/.claude/skills/simplify/scripts/simplify-scan.test.mjs +87 -0
- package/framework/.claude/workflows/new-card-review.js +5 -3
- package/framework/agents/index.md +4 -1
- package/framework/agents/security-review-protocol.md +233 -0
- package/framework/agents/security.md +50 -46
- package/framework/agents/simplify-protocol.md +280 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: simplify
|
|
3
3
|
effort: medium
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
description: Review changed code for reuse, quality, and efficiency, then fix any issues found. Use when completing a coding task, after implementation, or when the user says /simplify. Covers deduplication (exact/near/inline), code quality patterns, and performance.
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -39,15 +39,31 @@ If the diff is empty (everything is committed), run `git diff HEAD~5..HEAD` to c
|
|
|
39
39
|
|
|
40
40
|
If there are no git changes at all, review the most recently modified files that the user mentioned or that you edited earlier in this conversation.
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
> **Methodology SSOT**: the taxonomy (A1–A9), the canonical refactoring vocabulary (Fowler smells / Four Rules of Simple Design / AHA·YAGNI·Rule of Three), the clone-type vocabulary, the deterministic-tier contract, the internal-documentation awareness rules, and the judge-bias guards this skill applies all live in `framework/agents/simplify-protocol.md`. This body owns the OPERATIONAL workflow (Steps 1–5); it CITES the module for those rules rather than restating them (`SECTION=<taxonomy|canonical-vocab|clone-vocab|rubric|deterministic-tier|doc-awareness|bias-guards>`).
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
## Step 1.5: Run the Deterministic Clone Floor (before eyeballing)
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
An LLM eyeballing a diff misses clones exactly as it misses its own smells — so ground the Reuse lens with an EXTERNAL signal first (`agents/simplify-protocol.md SECTION=deterministic-tier`):
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
node framework/.claude/skills/simplify/scripts/simplify-scan.mjs --diff HEAD --json
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
(Zero-dep, identical on Codex; `--diff HEAD~5..HEAD` when the work is committed, or pass explicit file paths.) The scan PROPOSES anchored clone candidates (`path:line` × matching loci + type); it never decides. Pass its candidate list to Agent 1 below — Agent 1 verifies each (discards false positives like import blocks / boilerplate / fixtures, promotes real ones). A clean floor is a legitimate result, not a reason to invent duplicates.
|
|
53
|
+
|
|
54
|
+
## Step 2: Launch the Code Simplifier
|
|
55
|
+
|
|
56
|
+
Use the Agent tool to launch **one `code-simplifier` agent** (`subagent_type: code-simplifier`), passing it the diff path + the Step 1.5 clone-floor candidates. It covers ALL the lenses below as one persona (Reuse / Quality / Design-altitude / Efficiency) — it is the independent, evidence-grounded reviewer; you (the skill) apply its fixes in Step 4. The lens descriptions below are its operational rubric.
|
|
57
|
+
|
|
58
|
+
> **Methodology SSOT**: the taxonomy, canonical vocabulary, clone-vocab, bias guards and doc-awareness the agent applies live in `framework/agents/simplify-protocol.md` — the lens descriptions here are the operational surface, not the SSOT. If you change a lens, keep the module + the `coder` author-time mirror (`framework/.claude/agents/coder.md § Author-Time Simplicity Discipline`) in sync.
|
|
59
|
+
|
|
60
|
+
> **Author-time prevention mirror:** `coder` already applies an author-time subset of the Quality + Efficiency + Design-altitude taxonomy WHILE writing, so the diff this pass reviews should already be cleaner there. This pass remains the **independent net** — its load-bearing value is the cross-codebase **Reuse** detection (exact/near duplicate, inline reimplementation, dependency-reimplements-native) the author cannot see from inside the task.
|
|
47
61
|
|
|
48
62
|
### Agent 1: Duplication & Reuse Review
|
|
49
63
|
|
|
50
|
-
|
|
64
|
+
**Consume the Step 1.5 clone-floor candidates FIRST** (pass them into this agent's briefing): for each anchored candidate, verify the match is a real duplicate/reuse-miss (not an import block, boilerplate, or fixture the scan over-matched) and confirm the "existing" side is LIVE code, not dead — then promote it to a finding with an `existing` pointer. The floor catches Type-1 and gapped Type-3; YOU still catch Type-2 (renamed identifiers, which the floor deliberately does not normalize) and Type-4 (semantic) via retrieval below. Classify every duplicate per `agents/simplify-protocol.md SECTION=clone-vocab`.
|
|
65
|
+
|
|
66
|
+
Then, for each new piece of code in the diff:
|
|
51
67
|
|
|
52
68
|
1. **Start with `${paths.references_dir}/component-registry.md`** — the authoritative inventory of all UI primitives, shared components, hooks, and utility modules. Scan the relevant tables before grepping. Then search for existing utilities and helpers that could replace newly written code in the project's utility roots (e.g. `${paths.components_root}/shared/`, `${paths.components_primitives}/`, and `src/lib/`, `src/hooks/` if they exist), and files adjacent to the changed ones.
|
|
53
69
|
2. **For each new component**, Grep the codebase for components with similar names, similar prop signatures, or similar JSX structure. Check for copy-pasted layout components (`layout.tsx` files with near-identical structure).
|
|
@@ -82,11 +98,15 @@ Review the same changes for hacky patterns:
|
|
|
82
98
|
5. **Stringly-typed code**: using raw strings where constants, enums (string unions), or branded types already exist in the codebase
|
|
83
99
|
6. **Unnecessary JSX nesting**: wrapper Boxes/elements that add no layout value — check if inner component props (flexShrink, alignItems, etc.) already provide the needed behavior
|
|
84
100
|
7. **Unnecessary comments**: comments explaining WHAT the code does (well-named identifiers already do that), narrating the change, or referencing the task/caller — delete; keep only non-obvious WHY (hidden constraints, subtle invariants, workarounds)
|
|
101
|
+
8. **Wrong-altitude abstraction (A4 — advisory)**: an interface/factory/generic/config-flag/parameter with exactly ONE caller or ONE implementation, or a speculative extension point with no present consumer — the Rule of Three is violated (do not abstract before the third occurrence). Name the smell (`Speculative Generality` / `Rule-of-Three`). Advisory, not a blocker — a *simplify* pass that fabricates abstraction complaints causes the very churn it exists to prevent (`agents/simplify-protocol.md SECTION=canonical-vocab`). Corollary caution (AHA): prefer a little duplication over unifying two blocks whose shared shape is only coincidental.
|
|
102
|
+
9. **Pattern inconsistency vs siblings (A6 — advisory)**: the new code uses a different idiom / error-handling / naming / state pattern than the 2–3 nearest existing files it lives beside — align it to the established local convention.
|
|
103
|
+
|
|
104
|
+
> **Bias guard (a simplify judge is inverted)**: NEVER prefer a change because it adds structure — longer code is a NEGATIVE signal for this pass, never positive. Every finding cites executed/retrieved evidence (a clone match, a zero-caller proof, an existing-utility `path:line`), never unaided opinion. Zero findings on a clean diff is the correct outcome. Full rules: `agents/simplify-protocol.md SECTION=bias-guards`.
|
|
85
105
|
|
|
86
106
|
#### Agent 2 — Design System SSOT Compliance sub-checklist (MANDATORY for UI refactors)
|
|
87
107
|
|
|
88
|
-
This is part of
|
|
89
|
-
|
|
108
|
+
This is part of the Quality lens (Agent 2), which the single `code-simplifier` agent covers alongside
|
|
109
|
+
the other lenses — NOT a separate agent. When the diff includes files under `${paths.components_root}/`,
|
|
90
110
|
`${paths.components_root}/**/*.tsx`, or any `.css`/`.module.css`, the simplify pass MUST NOT introduce
|
|
91
111
|
hardcoded design values while refactoring. Agent 2 runs this as part of its quality review:
|
|
92
112
|
|
|
@@ -116,7 +136,7 @@ Review the same changes for efficiency:
|
|
|
116
136
|
|
|
117
137
|
## Step 3: Aggregate Findings
|
|
118
138
|
|
|
119
|
-
Collect
|
|
139
|
+
Collect the `code-simplifier` findings (across all lenses). For each finding, categorize as:
|
|
120
140
|
|
|
121
141
|
1. **Exact duplicate** — same logic exists elsewhere. Use the existing one. Delete the new one.
|
|
122
142
|
2. **Near duplicate** — similar logic with minor differences. Extract a shared version with parameters.
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* simplify-scan.mjs — deterministic clone floor for the /simplify pass.
|
|
4
|
+
*
|
|
5
|
+
* The always-on, zero-dependency, stack-agnostic near-duplicate detector that
|
|
6
|
+
* grounds the code-simplifier reviewer with an EXTERNAL signal it lacks by
|
|
7
|
+
* eyeballing (research: an LLM misses clones exactly as it misses its own
|
|
8
|
+
* smells). Twin of ui-design/scripts/craft-check.mjs — runs identically on
|
|
9
|
+
* Claude Code and Codex (Node >= 18, no deps).
|
|
10
|
+
*
|
|
11
|
+
* It PROPOSES anchored clone candidates (path:line x matching loci + type); the
|
|
12
|
+
* reviewer DISPOSES (verifies each, discards false positives, promotes real
|
|
13
|
+
* ones). It NEVER edits and NEVER decides. Contract + rationale:
|
|
14
|
+
* framework/agents/simplify-protocol.md SECTION=deterministic-tier.
|
|
15
|
+
*
|
|
16
|
+
* Scope: the CHANGED files' code windows matched against the whole repo
|
|
17
|
+
* (diff x repo) AND against each other. Catches Type-1 (exact modulo
|
|
18
|
+
* whitespace/comments) and gapped Type-3; Type-2 (renamed identifiers) is
|
|
19
|
+
* deliberately NOT normalized away (it explodes boilerplate false positives —
|
|
20
|
+
* that is the reviewer's job), and Type-4 is structurally invisible here.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* node simplify-scan.mjs [--diff <ref-or-range>] [file ...] [--json]
|
|
24
|
+
* [--min-lines N] [--repo DIR]
|
|
25
|
+
*
|
|
26
|
+
* --diff <range> git range whose changed files are the "new" side
|
|
27
|
+
* (default: HEAD -> `git diff --name-only HEAD`; if that is
|
|
28
|
+
* empty, `HEAD~5..HEAD`). Positional files override detection.
|
|
29
|
+
* --min-lines N minimum window of non-trivial normalized lines (default 6).
|
|
30
|
+
* --json machine-readable output for the agent.
|
|
31
|
+
* --repo DIR repo root (default: `git rev-parse --show-toplevel` or cwd).
|
|
32
|
+
*
|
|
33
|
+
* Exit codes: 0 = ran (candidates, if any, are in the output — this is an
|
|
34
|
+
* advisory FEEDER, not a CI gate, so it never fails on findings), 2 = usage/env
|
|
35
|
+
* error. It is safe to run on any stack; a non-git or empty scope is a clean
|
|
36
|
+
* no-op, never an abort.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
40
|
+
import { execFileSync } from 'node:child_process';
|
|
41
|
+
import { join, extname, relative, isAbsolute } from 'node:path';
|
|
42
|
+
|
|
43
|
+
const argv = process.argv.slice(2);
|
|
44
|
+
const asJson = argv.includes('--json');
|
|
45
|
+
const getOpt = (name, def) => {
|
|
46
|
+
const i = argv.indexOf(name);
|
|
47
|
+
return i >= 0 && argv[i + 1] ? argv[i + 1] : def;
|
|
48
|
+
};
|
|
49
|
+
const MIN_LINES = Math.max(3, parseInt(getOpt('--min-lines', '6'), 10) || 6);
|
|
50
|
+
const DIFF_REF = getOpt('--diff', null);
|
|
51
|
+
const positional = argv.filter(
|
|
52
|
+
(a, i) => !a.startsWith('--') && argv[i - 1] !== '--diff' && argv[i - 1] !== '--min-lines' && argv[i - 1] !== '--repo'
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// ---- repo root ---------------------------------------------------------------
|
|
56
|
+
let REPO;
|
|
57
|
+
function git(args, opts = {}) {
|
|
58
|
+
return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], ...opts });
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
REPO = getOpt('--repo', git(['rev-parse', '--show-toplevel']).trim());
|
|
62
|
+
} catch {
|
|
63
|
+
REPO = getOpt('--repo', process.cwd());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---- what counts as a source file we index ----------------------------------
|
|
67
|
+
const SOURCE_EXT = new Set([
|
|
68
|
+
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.vue', '.svelte',
|
|
69
|
+
'.py', '.rb', '.go', '.rs', '.java', '.kt', '.swift', '.scala', '.php',
|
|
70
|
+
'.c', '.h', '.cc', '.cpp', '.hpp', '.cs', '.m', '.lua', '.dart', '.sh',
|
|
71
|
+
]);
|
|
72
|
+
const IGNORE_DIR = /(^|\/)(node_modules|\.git|dist|build|out|\.next|coverage|vendor|__snapshots__|migrations|\.venv|venv|target|__pycache__)(\/|$)/;
|
|
73
|
+
const IGNORE_FILE = /(\.min\.[a-z]+$|\.map$|\.snap$|-lock\.[a-z]+$|package-lock\.json$|yarn\.lock$|pnpm-lock\.yaml$|\.d\.ts$)/i;
|
|
74
|
+
const MAX_BYTES = 512 * 1024; // skip huge/generated blobs
|
|
75
|
+
|
|
76
|
+
function isSource(rel) {
|
|
77
|
+
if (IGNORE_DIR.test(rel) || IGNORE_FILE.test(rel)) return false;
|
|
78
|
+
return SOURCE_EXT.has(extname(rel));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---- normalization: strip comments + whitespace, drop trivial lines ----------
|
|
82
|
+
// NO identifier normalization (deliberate — see header). A "trivial" line is
|
|
83
|
+
// pure punctuation, an import, a bare export brace, or a single keyword: the
|
|
84
|
+
// biggest false-positive sources for a naive hash.
|
|
85
|
+
const TRIVIAL_RE = /^(?:[{}()\[\];,]*|import\b.*|export\s*\{?\s*|export\s+default\s*|module\.exports.*|use\s+strict;?|package\s+\w.*|from\b.*|['"]use client['"];?|else\s*\{?|try\s*\{?|finally\s*\{?|return;?|break;?|continue;?|pass|end)$/;
|
|
86
|
+
|
|
87
|
+
function normalizeLines(text) {
|
|
88
|
+
// strip block comments spanning the file, then per-line line-comments.
|
|
89
|
+
const noBlock = text.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
90
|
+
const out = [];
|
|
91
|
+
const rawLines = noBlock.split(/\r?\n/);
|
|
92
|
+
for (let i = 0; i < rawLines.length; i++) {
|
|
93
|
+
let s = rawLines[i];
|
|
94
|
+
s = s.replace(/(^|\s)(\/\/|#|--)\s.*$/, '$1'); // trailing line comment (space-guarded so it does not eat urls/operators mid-token)
|
|
95
|
+
s = s.replace(/\s+/g, ' ').trim();
|
|
96
|
+
if (!s) continue;
|
|
97
|
+
if (TRIVIAL_RE.test(s)) continue;
|
|
98
|
+
out.push({ norm: s, line: i + 1 }); // 1-based original line
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---- window index ------------------------------------------------------------
|
|
104
|
+
// key = MIN_LINES consecutive normalized lines joined. Value = list of loci.
|
|
105
|
+
function fnv1a(str) {
|
|
106
|
+
let h = 0x811c9dc5;
|
|
107
|
+
for (let i = 0; i < str.length; i++) {
|
|
108
|
+
h ^= str.charCodeAt(i);
|
|
109
|
+
h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
|
|
110
|
+
}
|
|
111
|
+
return h.toString(36);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function windowsOf(nlines) {
|
|
115
|
+
const w = [];
|
|
116
|
+
for (let i = 0; i + MIN_LINES <= nlines.length; i++) {
|
|
117
|
+
const slice = nlines.slice(i, i + MIN_LINES);
|
|
118
|
+
// contiguity guard: the window must not span an enormous original gap
|
|
119
|
+
if (slice[slice.length - 1].line - slice[0].line > MIN_LINES * 4) continue;
|
|
120
|
+
const key = fnv1a(slice.map((x) => x.norm).join('\n'));
|
|
121
|
+
w.push({ key, start: slice[0].line, end: slice[slice.length - 1].line });
|
|
122
|
+
}
|
|
123
|
+
return w;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function listTrackedSource() {
|
|
127
|
+
try {
|
|
128
|
+
return git(['ls-files'], { cwd: REPO }).split('\n').filter((f) => f && isSource(f));
|
|
129
|
+
} catch {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function changedFiles() {
|
|
135
|
+
if (positional.length) {
|
|
136
|
+
return positional.map((p) => (isAbsolute(p) ? relative(REPO, p) : p)).filter(isSource);
|
|
137
|
+
}
|
|
138
|
+
const tryRange = (r) => {
|
|
139
|
+
try { return git(['diff', '--name-only', r], { cwd: REPO }).split('\n').filter(Boolean); }
|
|
140
|
+
catch { return []; }
|
|
141
|
+
};
|
|
142
|
+
let files = tryRange(DIFF_REF || 'HEAD');
|
|
143
|
+
if (!files.length && !DIFF_REF) files = tryRange('HEAD~5..HEAD');
|
|
144
|
+
return files.filter(isSource);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function readSafe(rel) {
|
|
148
|
+
try {
|
|
149
|
+
const abs = join(REPO, rel);
|
|
150
|
+
if (statSync(abs).size > MAX_BYTES) return null;
|
|
151
|
+
const buf = readFileSync(abs);
|
|
152
|
+
if (buf.includes(0)) return null; // binary
|
|
153
|
+
return buf.toString('utf8');
|
|
154
|
+
} catch { return null; }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---- run --------------------------------------------------------------------
|
|
158
|
+
const changed = changedFiles();
|
|
159
|
+
if (!changed.length) {
|
|
160
|
+
const msg = { candidates: [], scanned: 0, note: 'no changed source files in scope — clean no-op' };
|
|
161
|
+
console.log(asJson ? JSON.stringify(msg, null, 2) : 'simplify-scan: no changed source files in scope (clean no-op).');
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// index the whole repo (so a new block matching OLD code is caught)
|
|
166
|
+
const index = new Map(); // key -> [{file,start,end}]
|
|
167
|
+
const repoFiles = new Set([...listTrackedSource(), ...changed]);
|
|
168
|
+
for (const f of repoFiles) {
|
|
169
|
+
const txt = readSafe(f);
|
|
170
|
+
if (!txt) continue;
|
|
171
|
+
for (const w of windowsOf(normalizeLines(txt))) {
|
|
172
|
+
if (!index.has(w.key)) index.set(w.key, []);
|
|
173
|
+
index.get(w.key).push({ file: f, start: w.start, end: w.end });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// for each changed file window, report matches at OTHER loci
|
|
178
|
+
const changedSet = new Set(changed);
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
const candidates = [];
|
|
181
|
+
for (const f of changed) {
|
|
182
|
+
const txt = readSafe(f);
|
|
183
|
+
if (!txt) continue;
|
|
184
|
+
for (const w of windowsOf(normalizeLines(txt))) {
|
|
185
|
+
const loci = index.get(w.key) || [];
|
|
186
|
+
const others = loci.filter((l) => !(l.file === f && l.start === w.start));
|
|
187
|
+
if (!others.length) continue;
|
|
188
|
+
const dedup = `${w.key}:${f}:${w.start}`;
|
|
189
|
+
if (seen.has(dedup)) continue;
|
|
190
|
+
seen.add(dedup);
|
|
191
|
+
const sameFile = others.every((l) => l.file === f);
|
|
192
|
+
candidates.push({
|
|
193
|
+
new: { file: f, start: w.start, end: w.end },
|
|
194
|
+
matches: others.map((l) => ({ file: l.file, start: l.start, end: l.end })),
|
|
195
|
+
clone_type: sameFile ? 'type-1 (intra-file)' : 'type-1/3 (cross-file)',
|
|
196
|
+
note: changedSet.has(others[0].file)
|
|
197
|
+
? 'both sides are in the diff — likely copy-paste in this change'
|
|
198
|
+
: 'matches existing repo code — likely a reuse-miss (verify the existing side is live, not dead)',
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// merge adjacent/overlapping candidate windows in the same new file into one
|
|
204
|
+
// region candidate (a sliding window over one duplicated block emits N
|
|
205
|
+
// overlapping hits — collapse them so the reviewer sees one finding per block).
|
|
206
|
+
candidates.sort((a, b) => (a.new.file < b.new.file ? -1 : a.new.file > b.new.file ? 1 : a.new.start - b.new.start));
|
|
207
|
+
const merged = [];
|
|
208
|
+
for (const c of candidates) {
|
|
209
|
+
const prev = merged[merged.length - 1];
|
|
210
|
+
if (prev && prev.new.file === c.new.file && c.new.start <= prev.new.end + 2) {
|
|
211
|
+
prev.new.end = Math.max(prev.new.end, c.new.end);
|
|
212
|
+
const key = (m) => `${m.file}:${m.start}`;
|
|
213
|
+
const have = new Set(prev.matches.map(key));
|
|
214
|
+
for (const m of c.matches) if (!have.has(key(m))) { prev.matches.push(m); have.add(key(m)); }
|
|
215
|
+
} else {
|
|
216
|
+
merged.push({ new: { ...c.new }, matches: [...c.matches], clone_type: c.clone_type, note: c.note });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (asJson) {
|
|
221
|
+
console.log(JSON.stringify({ candidates: merged, scanned: repoFiles.size, changed: changed.length, min_lines: MIN_LINES }, null, 2));
|
|
222
|
+
} else {
|
|
223
|
+
if (!merged.length) {
|
|
224
|
+
console.log(`simplify-scan: 0 clone candidates across ${changed.length} changed file(s). Clean floor.`);
|
|
225
|
+
} else {
|
|
226
|
+
console.log(`simplify-scan: ${merged.length} clone candidate(s) (min ${MIN_LINES} lines) — VERIFY each (advisory, feeds the reviewer):\n`);
|
|
227
|
+
for (const c of merged) {
|
|
228
|
+
console.log(` ${c.new.file}:${c.new.start}-${c.new.end} [${c.clone_type}]`);
|
|
229
|
+
for (const m of c.matches) console.log(` ↔ ${m.file}:${m.start}-${m.end}`);
|
|
230
|
+
console.log(` ${c.note}\n`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
process.exit(0);
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* simplify-scan.test.mjs — fixture test for the clone floor.
|
|
4
|
+
*
|
|
5
|
+
* Proves the two properties that make the floor a signal and not noise:
|
|
6
|
+
* (1) SILENT on a boilerplate-heavy file (imports, component shell, braces).
|
|
7
|
+
* (2) CATCHES a real cross-file duplicated block.
|
|
8
|
+
*
|
|
9
|
+
* Zero-dep, runnable directly (`node simplify-scan.test.mjs`) or via
|
|
10
|
+
* `node --test`. Uses positional-file mode so no git repo is needed.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { test } from 'node:test';
|
|
14
|
+
import assert from 'node:assert/strict';
|
|
15
|
+
import { execFileSync } from 'node:child_process';
|
|
16
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
import { join, dirname } from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
|
|
21
|
+
const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), 'simplify-scan.mjs');
|
|
22
|
+
|
|
23
|
+
function runScan(dir, files) {
|
|
24
|
+
const out = execFileSync('node', [SCRIPT, '--repo', dir, '--json', ...files], { encoding: 'utf8' });
|
|
25
|
+
return JSON.parse(out);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const BOILERPLATE = `import React from 'react';
|
|
29
|
+
import { useState, useEffect } from 'react';
|
|
30
|
+
import { Button } from '@/ui/button';
|
|
31
|
+
import { Card } from '@/ui/card';
|
|
32
|
+
|
|
33
|
+
export default function Widget() {
|
|
34
|
+
const [open, setOpen] = useState(false);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
setOpen(true);
|
|
37
|
+
}, []);
|
|
38
|
+
return (
|
|
39
|
+
<Card>
|
|
40
|
+
<Button onClick={() => setOpen(!open)}>Toggle</Button>
|
|
41
|
+
</Card>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
// a genuine, non-trivial ~8-line block duplicated verbatim across two files
|
|
47
|
+
const DUP_BLOCK = `function computeInvoiceTotal(items, taxRate, discount) {
|
|
48
|
+
let subtotal = 0;
|
|
49
|
+
for (const it of items) {
|
|
50
|
+
subtotal += it.unitPrice * it.quantity;
|
|
51
|
+
}
|
|
52
|
+
const taxed = subtotal * (1 + taxRate);
|
|
53
|
+
const afterDiscount = taxed - taxed * discount;
|
|
54
|
+
const rounded = Math.round(afterDiscount * 100) / 100;
|
|
55
|
+
return rounded;
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
test('silent on boilerplate-heavy file (no false positives)', () => {
|
|
60
|
+
const dir = mkdtempSync(join(tmpdir(), 'simpscan-'));
|
|
61
|
+
try {
|
|
62
|
+
const f = join(dir, 'widget.tsx');
|
|
63
|
+
writeFileSync(f, BOILERPLATE);
|
|
64
|
+
const res = runScan(dir, [f]);
|
|
65
|
+
assert.equal(res.candidates.length, 0, `expected 0 candidates, got ${res.candidates.length}`);
|
|
66
|
+
} finally {
|
|
67
|
+
rmSync(dir, { recursive: true, force: true });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('catches a real cross-file duplicated block', () => {
|
|
72
|
+
const dir = mkdtempSync(join(tmpdir(), 'simpscan-'));
|
|
73
|
+
try {
|
|
74
|
+
const a = join(dir, 'billing.ts');
|
|
75
|
+
const b = join(dir, 'checkout.ts');
|
|
76
|
+
writeFileSync(a, `export const CFG = 1;\n${DUP_BLOCK}`);
|
|
77
|
+
writeFileSync(b, `export const OTHER = 2;\n${DUP_BLOCK}`);
|
|
78
|
+
const res = runScan(dir, [a, b]);
|
|
79
|
+
assert.ok(res.candidates.length >= 1, 'expected >=1 clone candidate for the duplicated block');
|
|
80
|
+
const hit = res.candidates.some(
|
|
81
|
+
(c) => c.matches.some((m) => m.file.endsWith('billing.ts') || m.file.endsWith('checkout.ts'))
|
|
82
|
+
);
|
|
83
|
+
assert.ok(hit, 'candidate should link billing.ts <-> checkout.ts');
|
|
84
|
+
} finally {
|
|
85
|
+
rmSync(dir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
@@ -385,9 +385,11 @@ const qaPrompt =
|
|
|
385
385
|
: '')
|
|
386
386
|
|
|
387
387
|
function simplifyPrompt(c) {
|
|
388
|
-
return `Simplify analysis (read-only — you do NOT edit, the workflow applies fixes afterward) over ONE card's committed diff, per ${protocolRef} (Phase 2.55).
|
|
389
|
-
` •
|
|
388
|
+
return `Simplify analysis (read-only — you do NOT edit, the workflow applies fixes afterward) over ONE card's committed diff, per ${protocolRef} (Phase 2.55) and your code-simplifier system prompt (agents/simplify-protocol.md). FIRST run the deterministic clone floor and verify its candidates, then cover ALL lenses and return findings:\n` +
|
|
389
|
+
` • Clone floor — run \`node "$(ls .claude/skills/simplify/scripts/simplify-scan.mjs .framework/framework/.claude/skills/simplify/scripts/simplify-scan.mjs 2>/dev/null | head -1)" --diff /tmp/diff-${c.cardId}.txt --json\` (missing → skip, note 'scan-unavailable'); verify each anchored candidate (discard boilerplate/import/fixture false positives, confirm the existing side is LIVE) before promoting it.\n` +
|
|
390
|
+
` • Reuse — newly written code that duplicates an existing util/helper (ground reuse-misses in the registry/code-graph/LSP per SECTION=doc-awareness, retrieve-then-verify); inline logic that could use existing code; a NEW third-party dependency that reimplements a stdlib/platform capability (prefer the native primitive).\n` +
|
|
390
391
|
` • Quality — redundant state, parameter sprawl, copy-paste with slight variation, leaky abstractions, stringly-typed code where enums exist, unnecessary JSX nesting, WHAT-comments / narration.\n` +
|
|
392
|
+
` • Design-altitude (advisory) — single-caller/one-implementation abstraction, Rule-of-Three violation, speculative generality with no consumer, pattern inconsistency vs the 2-3 nearest sibling files. Do NOT fabricate abstraction complaints (churn is the anti-goal).\n` +
|
|
391
393
|
` • Efficiency — redundant computation, duplicate API calls, N+1, missed concurrency, hot-path bloat, missing change-detection guards, unbounded structures.\n\n` +
|
|
392
394
|
`${cardScopeBrief(c)}\n\n${baselineBrief}\n\n` +
|
|
393
395
|
`Run a false-positive check on every finding and SUPPRESS the unconvincing ones (your surviving findings are treated as validated). ` +
|
|
@@ -405,7 +407,7 @@ function securityPrompt(c) {
|
|
|
405
407
|
const findThunks = []
|
|
406
408
|
for (const c of cards) {
|
|
407
409
|
if (c.runSimplify !== false) {
|
|
408
|
-
findThunks.push(() => reviewSafe('simplify', c, simplifyPrompt(c), { label: `simplify:${c.cardId}`, phase: 'Discovery', schema: FINDINGS_SCHEMA }))
|
|
410
|
+
findThunks.push(() => reviewSafe('simplify', c, simplifyPrompt(c), { label: `simplify:${c.cardId}`, phase: 'Discovery', agentType: 'code-simplifier', schema: FINDINGS_SCHEMA }))
|
|
409
411
|
}
|
|
410
412
|
if (c.hasSecurityFiles === true) {
|
|
411
413
|
findThunks.push(() => reviewSafe('security', c, securityPrompt(c), { label: `security:${c.cardId}`, phase: 'Discovery', agentType: 'security-reviewer', schema: FINDINGS_SCHEMA }))
|
|
@@ -37,7 +37,8 @@ Route agents to the right module with minimal reading.
|
|
|
37
37
|
- If touching terminology or naming -> read `agents/coding-standards.md`.
|
|
38
38
|
- If touching env vars or scripts -> read `agents/runbook.md` and `agents/env-reference.md`.
|
|
39
39
|
- If touching operational procedures (backup, cleanup, secrets) -> read `docs/operations/` if it exists in your project.
|
|
40
|
-
- If touching security or auth risk -> read `agents/security.md
|
|
40
|
+
- If touching security or auth risk -> read `agents/security.md` (OWASP 2021 posture + secure-defaults). If performing a dedicated AppSec review (the `security-reviewer` agent, `/codexreview` security pass) -> also read `agents/security-review-protocol.md` (the matching `SECTION=` only): boundary-gate → repo-scoped threat-model → high-miss discovery-lens → class proof-tuples → structured attack-path → adversarial-refute (attacker≠victim) → coverage-ledger. It EXTENDS `review-protocol.md` (generic passes), never duplicates it.
|
|
41
|
+
- If reviewing changed code for reuse/duplication/simplicity (the `code-simplifier` agent, the `/simplify` skill, `/new` Phase 2.55) -> read `agents/simplify-protocol.md` (the matching `SECTION=` only): deterministic clone floor → doc-awareness retrieval → taxonomy (A1–A9) + canonical-vocab (Fowler / Four Rules / AHA·YAGNI·Rule-of-Three) + clone-vocab → rubric lenses → bias-guards (a simplify judge never rewards verbosity). It EXTENDS `review-protocol.md` (generic passes), never duplicates it; partitioned from `code-reviewer` (correctness/security stay there).
|
|
41
42
|
- If touching performance limits or scaling -> read `agents/performance.md`.
|
|
42
43
|
- If touching monitoring/logging -> read `agents/observability.md`.
|
|
43
44
|
- If tuning reasoning depth — setting a skill's `effort:` baseline or honoring an inline `effort=<level>` override -> read `agents/effort-protocol.md`.
|
|
@@ -86,6 +87,8 @@ When adding or updating agents, update REGISTRY.md — not this file.
|
|
|
86
87
|
- `agents/runtime-portability-protocol.md` — Runtime-mechanics binding: the abstract-operation ↔ Claude/Codex map (spawn / permissions / workflow-accel / state-spine / decision-gate / read-write path / adversarial-vs-cross-model), detect-once capability contract; cited by `/new` + `/prd`. The runtime-mechanics twin of `effort-protocol.md` + `return-contract-protocol.md` (since the Codex-parity S4 wave)
|
|
87
88
|
- `agents/agent-operating-protocol.md` — Shared operating procedures for the core agents (injection guard, doc-retrieval consumption, persistent-memory hygiene, tool-budget discipline), `SECTION=` dispatch, read only the matching section; agents keep 1-line binding versions inline (since v5.0.0)
|
|
88
89
|
- `agents/review-protocol.md` — The shared verification engine for reviewers: Challenge + Actionability, Simulation (diff-walk / plan-walk), Chain-of-Verification, quantified risk scoring + absolute severity calibration, specialist-spawn discipline incl. orchestrated-mode `specialist_dispatch` suppression; `SECTION=` dispatch (since v5.0.0)
|
|
90
|
+
- `agents/security-review-protocol.md` — The AppSec verification engine for `security-reviewer`: boundary-gate, repository-scoped threat-model, modern high-miss discovery-lens (fail-open / allowlist-escape / control-regression / gate-action mismatch / parser differential / CI-CD trust / IaC omitted arg / over-broad grant), class-specific proof-tuples (method-anchored confidence, instance-preserving), structured attack-path → mechanical severity, adversarial-refute (attacker≠victim, precondition vs counterevidence), coverage-ledger; `SECTION=` dispatch. EXTENDS `review-protocol.md`, never duplicates it. Portable Claude+Codex (since v5.9.0)
|
|
91
|
+
- `agents/simplify-protocol.md` — The reuse & simplicity verification engine for `code-simplifier` (+ the `/simplify` skill + the `coder` author-time mirror): agentic-defect taxonomy (A1–A9 with detectable signals), canonical refactoring vocabulary (Fowler smells / Four Rules of Simple Design / AHA·YAGNI·Rule-of-Three / Beck tidyings), clone-type vocabulary (Type-1/2/3/4), the deterministic clone-floor contract (`simplify-scan.mjs`), internal-documentation awareness (retrieve-then-verify reuse retrieval), and the judge-bias guards a simplify reviewer needs (no verbosity reward, evidence-over-opinion, independence); `SECTION=` dispatch. EXTENDS `review-protocol.md`, never duplicates it. Portable Claude+Codex (since v5.10.0)
|
|
89
92
|
- `agents/doc-audit-protocol.md` — doc-reviewer's audit-mode procedures (drift validator suite, topological generation, epistemic metadata, SCIP anchors, schema/registry drift, coverage gauges); read ONLY when invoked without card context; `SECTION=` dispatch (since v5.0.0)
|
|
90
93
|
- `agents/research-protocol.md` — Research discipline: `PROFILE=<decision|deep|compare|regulatory>` output contracts (+ `DEPTH=` iterative loop), the research library (`paths.research_dir` — layout, report frontmatter, INDEX, archive-not-delete), the reuse pre-flight (`FULL_REUSE`/`DELTA`/`NEW` + per-category TTLs), and the versioned source matrix with its growth loop (`SOURCE_MATRIX_CANDIDATE`); `SECTION=` dispatch. The research-side sibling of `analysis-profiles.md` (since v5.1.0)
|
|
91
94
|
|