monomind 2.5.2 → 2.5.3
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/README.md +1 -1
- package/package.json +1 -1
- package/packages/@monomind/cli/.claude/agents/github/repo-architect.md +3 -3
- package/packages/@monomind/cli/.claude/agents/github/sync-coordinator.md +2 -2
- package/packages/@monomind/cli/.claude/agents/specialists/integration-architect.md +4 -4
- package/packages/@monomind/cli/.claude/commands/github/repo-architect.md +4 -4
- package/packages/@monomind/cli/.claude/commands/github/sync-coordinator.md +2 -2
- package/packages/@monomind/cli/.claude/helpers/event-logger.cjs +2 -2
- package/packages/@monomind/cli/.claude/helpers/extras-registry.json +1 -1
- package/packages/@monomind/cli/.claude/helpers/handlers/agent-start-handler.cjs +2 -2
- package/packages/@monomind/cli/.claude/helpers/handlers/edit-handler.cjs +2 -2
- package/packages/@monomind/cli/.claude/helpers/handlers/gates-handler.cjs +1 -1
- package/packages/@monomind/cli/.claude/helpers/handlers/route-handler.cjs +1 -1
- package/packages/@monomind/cli/.claude/helpers/handlers/session-handler.cjs +1 -1
- package/packages/@monomind/cli/.claude/helpers/handlers/session-restore-handler.cjs +11 -11
- package/packages/@monomind/cli/.claude/helpers/handlers/task-handler.cjs +2 -2
- package/packages/@monomind/cli/.claude/helpers/hook-handler.cjs +8 -8
- package/packages/@monomind/cli/.claude/skills/mastermind-techport/SKILL.md +1 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/SKILL.md +2 -2
- package/packages/@monomind/cli/.claude/skills/monodesign/reference/antipatterns-catalog.md +47 -2
- package/packages/@monomind/cli/.claude/skills/monodesign/reference/critique.md +1 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/reference/live.md +0 -8
- package/packages/@monomind/cli/.claude/skills/monodesign/reference/polish.md +2 -2
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/context-signals.mjs +39 -6
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/context.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/critique-storage.mjs +175 -2
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detect-csp.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detect.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/browser/injected/index.mjs +25 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/cli/main.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/design-system.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/detect-antipatterns-browser.js +356 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/detect-antipatterns.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/engines/browser/detect-url.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/engines/browser/drivers.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/engines/regex/detect-text.mjs +63 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/engines/static-html/css-cascade.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/engines/static-html/detect-html.mjs +19 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/engines/visual/screenshot-contrast.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/findings.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/fix/css-regions.mjs +181 -0
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/fix/fixers.mjs +373 -0
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/fix/index.mjs +426 -0
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/node/file-system.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/profile/profiler.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/registry/antipatterns.mjs +49 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/rules/checks.mjs +292 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/shared/color.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/shared/constants.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/shared/fonts.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/shared/inline-ignores.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/shared/page.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/hook-admin.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/hook-before-edit.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/hook-lib.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/hook.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/lib/design-parser.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/lib/is-generated.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/lib/monodesign-config.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/lib/monodesign-paths.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/lib/provider.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/lib/target-args.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/browser-script-parts.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/completion.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/event-validation.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/insert-ui.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/manual-apply.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/manual-edit-routes.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/manual-edits-buffer.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/session-store.mjs +5 -2
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/svelte-component.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/sveltekit-adapter.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/ui-core.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live/vocabulary.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-accept.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-browser-dom.js +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-browser-session.js +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-browser.js +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-commit-manual-edits.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-complete.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-copy-edit-agent.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-discard-manual-edits.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-inject.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-insert.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-manual-edit-evidence.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-poll.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-resume.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-server.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-status.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-target.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live-wrap.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/live.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/modern-screenshot.umd.js +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/palette.mjs +0 -1
- package/packages/@monomind/cli/.claude/skills/monodesign/scripts/pin.mjs +0 -1
- package/packages/@monomind/cli/README.md +1 -1
- package/packages/@monomind/cli/dist/src/commands/design-detect.js +62 -1
- package/packages/@monomind/cli/dist/src/commands/design-palette.d.ts +0 -1
- package/packages/@monomind/cli/dist/src/commands/design-palette.js +0 -1
- package/packages/@monomind/cli/dist/src/commands/doctor-project-checks.d.ts +1 -1
- package/packages/@monomind/cli/dist/src/commands/doctor-project-checks.js +1 -1
- package/packages/@monomind/cli/dist/src/commands/hooks-workers.d.ts +1 -1
- package/packages/@monomind/cli/dist/src/commands/hooks-workers.js +8 -8
- package/packages/@monomind/cli/dist/src/commands/init.js +2 -2
- package/packages/@monomind/cli/dist/src/index.d.ts +1 -1
- package/packages/@monomind/cli/dist/src/index.js +3 -3
- package/packages/@monomind/cli/dist/src/init/claudemd-generator.js +5 -5
- package/packages/@monomind/cli/dist/src/init/executor.js +3 -3
- package/packages/@monomind/cli/dist/src/knowledge/document-pipeline.js +3 -1
- package/packages/@monomind/cli/dist/src/mcp-server.js +2 -2
- package/packages/@monomind/cli/dist/src/mcp-tools/hooks-intelligence.d.ts +1 -1
- package/packages/@monomind/cli/dist/src/mcp-tools/hooks-intelligence.js +1 -1
- package/packages/@monomind/cli/dist/src/mcp-tools/hooks-routing.js +2 -2
- package/packages/@monomind/cli/dist/src/mcp-tools/hooks-tools.js +1 -1
- package/packages/@monomind/cli/dist/src/memory/memory-kg.d.ts +5 -4
- package/packages/@monomind/cli/dist/src/memory/memory-kg.js +5 -4
- package/packages/@monomind/cli/dist/src/routing/embed-worker.js +1 -1
- package/packages/@monomind/cli/dist/src/routing/llm-caller.js +1 -1
- package/packages/@monomind/cli/dist/src/routing/route-layer-factory.js +1 -1
- package/packages/@monomind/cli/dist/src/ui/dashboard.html +22 -20
- package/packages/@monomind/cli/dist/src/update/checker.js +3 -3
- package/packages/@monomind/cli/package.json +5 -5
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-fix engine: detect -> apply safe codemods -> re-detect -> report.
|
|
3
|
+
*
|
|
4
|
+
* Single-pass by design: every fixer is idempotent and lands values on the
|
|
5
|
+
* safe side of its detector threshold, so the re-detect step only verifies —
|
|
6
|
+
* it never triggers another round of edits (no oscillation).
|
|
7
|
+
*
|
|
8
|
+
* Honors the same filter pipeline as `detect`: project config ignores
|
|
9
|
+
* (.monodesign/config.json), inline monodesign-disable* directives, and file
|
|
10
|
+
* ignores — an ignored finding is never "fixed".
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
import { loadDesignSystemForCwd } from '../design-system.mjs';
|
|
17
|
+
import { detectHtml } from '../engines/static-html/detect-html.mjs';
|
|
18
|
+
import { detectText } from '../engines/regex/detect-text.mjs';
|
|
19
|
+
import {
|
|
20
|
+
filterDetectionFindings,
|
|
21
|
+
readDetectionConfig,
|
|
22
|
+
shouldIgnoreDetectionFile,
|
|
23
|
+
} from '../../lib/monodesign-config.mjs';
|
|
24
|
+
import { HTML_EXTENSIONS, walkDir } from '../node/file-system.mjs';
|
|
25
|
+
import { UNFIXABLE_REASONS, getFixer, fixableRuleIds, FIXERS } from './fixers.mjs';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Edit application
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/** Apply edits to content. Edits are sorted; overlapping edits are dropped
|
|
32
|
+
* (first wins). Returns { content, applied } — applied lists the edits used. */
|
|
33
|
+
function applyEdits(content, edits) {
|
|
34
|
+
const sorted = [...edits].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
35
|
+
const kept = [];
|
|
36
|
+
let lastEnd = -1;
|
|
37
|
+
for (const edit of sorted) {
|
|
38
|
+
if (edit.start < lastEnd) continue; // overlap — first edit wins
|
|
39
|
+
kept.push(edit);
|
|
40
|
+
lastEnd = Math.max(edit.end, edit.start);
|
|
41
|
+
}
|
|
42
|
+
let result = content;
|
|
43
|
+
for (let i = kept.length - 1; i >= 0; i--) {
|
|
44
|
+
const { start, end, replacement } = kept[i];
|
|
45
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
46
|
+
}
|
|
47
|
+
return { content: result, applied: kept };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Atomic write: temp file in the same directory, then rename over target. */
|
|
51
|
+
function writeFileAtomic(filePath, content) {
|
|
52
|
+
const tmp = path.join(
|
|
53
|
+
path.dirname(filePath),
|
|
54
|
+
`.${path.basename(filePath)}.${process.pid}.monodesign-tmp`,
|
|
55
|
+
);
|
|
56
|
+
fs.writeFileSync(tmp, content, 'utf-8');
|
|
57
|
+
fs.renameSync(tmp, filePath);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Unified diff (minimal single-hunk implementation for --dry-run)
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
function unifiedDiff(oldText, newText, label) {
|
|
65
|
+
if (oldText === newText) return '';
|
|
66
|
+
const a = oldText.split('\n');
|
|
67
|
+
const b = newText.split('\n');
|
|
68
|
+
let p = 0;
|
|
69
|
+
while (p < a.length && p < b.length && a[p] === b[p]) p++;
|
|
70
|
+
let s = 0;
|
|
71
|
+
while (s < a.length - p && s < b.length - p && a[a.length - 1 - s] === b[b.length - 1 - s]) s++;
|
|
72
|
+
const aEnd = a.length - s;
|
|
73
|
+
const bEnd = b.length - s;
|
|
74
|
+
const ctxStart = Math.max(0, p - 3);
|
|
75
|
+
const ctxEnd = Math.min(a.length, aEnd + 3);
|
|
76
|
+
const oldLen = ctxEnd - ctxStart;
|
|
77
|
+
const newLen = (p - ctxStart) + (bEnd - p) + (ctxEnd - aEnd);
|
|
78
|
+
const lines = [`--- a/${label}`, `+++ b/${label}`, `@@ -${ctxStart + 1},${oldLen} +${ctxStart + 1},${newLen} @@`];
|
|
79
|
+
for (let i = ctxStart; i < p; i++) lines.push(` ${a[i]}`);
|
|
80
|
+
for (let i = p; i < aEnd; i++) lines.push(`-${a[i]}`);
|
|
81
|
+
for (let i = p; i < bEnd; i++) lines.push(`+${b[i]}`);
|
|
82
|
+
for (let i = aEnd; i < ctxEnd; i++) lines.push(` ${a[i]}`);
|
|
83
|
+
return lines.join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// Linked stylesheet resolution (HTML findings whose declaration lives in CSS)
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
function extractLinkedStylesheets(html, htmlPath) {
|
|
91
|
+
const out = [];
|
|
92
|
+
const linkRe = /<link\b[^>]*>/gi;
|
|
93
|
+
let m;
|
|
94
|
+
while ((m = linkRe.exec(html)) !== null) {
|
|
95
|
+
const tag = m[0];
|
|
96
|
+
if (!/rel\s*=\s*["']?stylesheet/i.test(tag)) continue;
|
|
97
|
+
const href = tag.match(/href\s*=\s*["']([^"']+)["']/i)?.[1];
|
|
98
|
+
if (!href || /^(?:https?:)?\/\//i.test(href) || href.startsWith('data:')) continue;
|
|
99
|
+
const resolved = path.resolve(path.dirname(htmlPath), href.split(/[?#]/)[0]);
|
|
100
|
+
if (fs.existsSync(resolved)) out.push(resolved);
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Fix run
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
function groupByRule(findings) {
|
|
110
|
+
const byRule = new Map();
|
|
111
|
+
for (const f of findings) {
|
|
112
|
+
if (!byRule.has(f.antipattern)) byRule.set(f.antipattern, []);
|
|
113
|
+
byRule.get(f.antipattern).push(f);
|
|
114
|
+
}
|
|
115
|
+
return byRule;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Run the fix loop over file/directory targets.
|
|
120
|
+
*
|
|
121
|
+
* options:
|
|
122
|
+
* cwd — project root for config + design-system loading
|
|
123
|
+
* dryRun — compute edits and diffs; write nothing
|
|
124
|
+
* rules — restrict fixing to these rule ids
|
|
125
|
+
* noConfig — skip project config + inline-ignore filtering
|
|
126
|
+
*
|
|
127
|
+
* Returns a report:
|
|
128
|
+
* { dryRun, fixed, remaining, skipped, filesChanged, diffs, warnings }
|
|
129
|
+
*/
|
|
130
|
+
async function runFix(targets, options = {}) {
|
|
131
|
+
const cwd = options.cwd || process.cwd();
|
|
132
|
+
const dryRun = Boolean(options.dryRun);
|
|
133
|
+
const ruleFilter = options.rules && options.rules.length ? new Set(options.rules) : null;
|
|
134
|
+
const config = options.noConfig
|
|
135
|
+
? { ignoreRules: [], ignoreFiles: [], ignoreValues: [] }
|
|
136
|
+
: readDetectionConfig(cwd);
|
|
137
|
+
const designSystemEnabled = !options.noConfig && config.designSystem?.enabled !== false;
|
|
138
|
+
const designSystem = designSystemEnabled ? loadDesignSystemForCwd(cwd) : null;
|
|
139
|
+
const scanOptions = { providers: [], inlineIgnores: !options.noConfig };
|
|
140
|
+
if (designSystem) scanOptions.designSystem = designSystem;
|
|
141
|
+
|
|
142
|
+
const warnings = [];
|
|
143
|
+
const files = [];
|
|
144
|
+
for (const target of targets) {
|
|
145
|
+
if (/^https?:\/\//i.test(target)) {
|
|
146
|
+
warnings.push(`Skipping URL target ${target}: fix operates on local files only.`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const resolved = path.resolve(cwd, target);
|
|
150
|
+
let stat;
|
|
151
|
+
try { stat = fs.statSync(resolved); }
|
|
152
|
+
catch { warnings.push(`Warning: cannot access ${target}`); continue; }
|
|
153
|
+
if (stat.isDirectory()) {
|
|
154
|
+
files.push(...walkDir(resolved).filter(f => !shouldIgnoreDetectionFile(f, cwd, config)));
|
|
155
|
+
} else if (!shouldIgnoreDetectionFile(resolved, cwd, config)) {
|
|
156
|
+
files.push(resolved);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const uniqueFiles = [...new Set(files)];
|
|
160
|
+
|
|
161
|
+
const detectFile = async (file) => {
|
|
162
|
+
const ext = path.extname(file).toLowerCase();
|
|
163
|
+
const found = HTML_EXTENSIONS.has(ext)
|
|
164
|
+
? await detectHtml(file, scanOptions)
|
|
165
|
+
: detectText(fs.readFileSync(file, 'utf-8'), file, scanOptions);
|
|
166
|
+
return filterDetectionFindings(found, config);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// 1. Detect
|
|
170
|
+
const findingsByFile = new Map();
|
|
171
|
+
for (const file of uniqueFiles) {
|
|
172
|
+
const found = await detectFile(file);
|
|
173
|
+
if (found.length > 0) findingsByFile.set(file, found);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 2. Plan + apply fixes in memory. Buffers exist only for files that carry
|
|
177
|
+
// findings (or are stylesheets linked from such a file) — files with no
|
|
178
|
+
// findings are never touched.
|
|
179
|
+
const buffers = new Map(); // path -> { original, content, changed, parents:Set }
|
|
180
|
+
const loadBuffer = (file) => {
|
|
181
|
+
let buf = buffers.get(file);
|
|
182
|
+
if (!buf) {
|
|
183
|
+
const original = fs.readFileSync(file, 'utf-8');
|
|
184
|
+
buf = { original, content: original, changed: false, parents: new Set() };
|
|
185
|
+
buffers.set(file, buf);
|
|
186
|
+
}
|
|
187
|
+
return buf;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const skipped = []; // { file, antipattern, count, reason }
|
|
191
|
+
const attempts = []; // { file, antipattern, before, notes, targetFile }
|
|
192
|
+
|
|
193
|
+
for (const [file, findings] of findingsByFile) {
|
|
194
|
+
const ext = path.extname(file).toLowerCase();
|
|
195
|
+
for (const [rule, ruleFindings] of groupByRule(findings)) {
|
|
196
|
+
const count = ruleFindings.length;
|
|
197
|
+
if (ruleFilter && !ruleFilter.has(rule)) {
|
|
198
|
+
skipped.push({ file, antipattern: rule, count, reason: 'excluded by --rule' });
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const fixer = getFixer(rule);
|
|
202
|
+
if (!fixer) {
|
|
203
|
+
skipped.push({
|
|
204
|
+
file,
|
|
205
|
+
antipattern: rule,
|
|
206
|
+
count,
|
|
207
|
+
reason: UNFIXABLE_REASONS[rule] || 'no deterministic safe fix exists for this rule',
|
|
208
|
+
});
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const buf = loadBuffer(file);
|
|
212
|
+
let edits = fixer.fix(buf.content, ext);
|
|
213
|
+
let targetFile = file;
|
|
214
|
+
if (edits.length === 0 && HTML_EXTENSIONS.has(ext)) {
|
|
215
|
+
// The offending declaration may live in a linked stylesheet — the
|
|
216
|
+
// detector cascades linked CSS into HTML findings.
|
|
217
|
+
for (const cssPath of extractLinkedStylesheets(buf.original, file)) {
|
|
218
|
+
const cssBuf = loadBuffer(cssPath);
|
|
219
|
+
const cssEdits = fixer.fix(cssBuf.content, path.extname(cssPath).toLowerCase());
|
|
220
|
+
if (cssEdits.length > 0) {
|
|
221
|
+
edits = cssEdits;
|
|
222
|
+
targetFile = cssPath;
|
|
223
|
+
cssBuf.parents.add(file);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (edits.length === 0) {
|
|
229
|
+
skipped.push({
|
|
230
|
+
file,
|
|
231
|
+
antipattern: rule,
|
|
232
|
+
count,
|
|
233
|
+
reason: 'no safely rewritable declaration found in source',
|
|
234
|
+
});
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const targetBuf = loadBuffer(targetFile);
|
|
238
|
+
const { content, applied } = applyEdits(targetBuf.content, edits);
|
|
239
|
+
if (content !== targetBuf.content) {
|
|
240
|
+
targetBuf.content = content;
|
|
241
|
+
targetBuf.changed = true;
|
|
242
|
+
}
|
|
243
|
+
attempts.push({
|
|
244
|
+
file,
|
|
245
|
+
antipattern: rule,
|
|
246
|
+
before: count,
|
|
247
|
+
targetFile,
|
|
248
|
+
notes: applied.map(e => e.note).filter(Boolean),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const changedFiles = [...buffers.entries()].filter(([, b]) => b.changed);
|
|
254
|
+
const filesChanged = changedFiles.map(([p]) => p);
|
|
255
|
+
|
|
256
|
+
// 3. Write (atomic) or diff (dry-run)
|
|
257
|
+
const diffs = [];
|
|
258
|
+
if (dryRun) {
|
|
259
|
+
for (const [p, b] of changedFiles) {
|
|
260
|
+
diffs.push({ file: p, diff: unifiedDiff(b.original, b.content, path.relative(cwd, p) || p) });
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
for (const [p, b] of changedFiles) writeFileAtomic(p, b.content);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 4. Re-detect touched files to verify (skip in dry-run — nothing changed
|
|
267
|
+
// on disk). Linked-CSS edits are verified through their parent HTML.
|
|
268
|
+
const remaining = [];
|
|
269
|
+
if (!dryRun && changedFiles.length > 0) {
|
|
270
|
+
const recheck = new Set();
|
|
271
|
+
for (const [p, b] of changedFiles) {
|
|
272
|
+
if (findingsByFile.has(p)) recheck.add(p);
|
|
273
|
+
for (const parent of b.parents) recheck.add(parent);
|
|
274
|
+
}
|
|
275
|
+
const after = new Map();
|
|
276
|
+
for (const p of recheck) after.set(p, await detectFile(p));
|
|
277
|
+
for (const attempt of attempts) {
|
|
278
|
+
const afterFindings = after.get(attempt.file) || [];
|
|
279
|
+
const stillThere = afterFindings.filter(f => f.antipattern === attempt.antipattern).length;
|
|
280
|
+
attempt.after = stillThere;
|
|
281
|
+
attempt.fixed = Math.max(0, attempt.before - stillThere);
|
|
282
|
+
if (stillThere > 0) {
|
|
283
|
+
remaining.push({ file: attempt.file, antipattern: attempt.antipattern, count: stillThere });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
} else {
|
|
287
|
+
for (const attempt of attempts) {
|
|
288
|
+
attempt.after = dryRun ? null : attempt.before;
|
|
289
|
+
attempt.fixed = dryRun ? attempt.before : 0;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
dryRun,
|
|
295
|
+
fixed: attempts.map(a => ({
|
|
296
|
+
file: a.file,
|
|
297
|
+
antipattern: a.antipattern,
|
|
298
|
+
before: a.before,
|
|
299
|
+
after: a.after,
|
|
300
|
+
fixed: a.fixed,
|
|
301
|
+
targetFile: a.targetFile,
|
|
302
|
+
notes: a.notes,
|
|
303
|
+
})),
|
|
304
|
+
remaining,
|
|
305
|
+
skipped,
|
|
306
|
+
filesChanged,
|
|
307
|
+
diffs,
|
|
308
|
+
warnings,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
// CLI
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
|
|
316
|
+
function printFixUsage() {
|
|
317
|
+
console.log(`Usage: monodesign fix [options] <file-or-dir...>
|
|
318
|
+
|
|
319
|
+
Auto-fix detector findings where a deterministic, safe codemod exists.
|
|
320
|
+
Runs detect, applies fixes, re-detects to verify, and reports
|
|
321
|
+
fixed / remaining / skipped findings. Files without findings are never touched.
|
|
322
|
+
|
|
323
|
+
Options:
|
|
324
|
+
--dry-run Print unified diffs of would-be changes; write nothing
|
|
325
|
+
--json Output the fix report as JSON
|
|
326
|
+
--rule <id,...> Only fix the given rule ids
|
|
327
|
+
--no-config Skip project config and inline-ignore filtering
|
|
328
|
+
--help Show this help message
|
|
329
|
+
|
|
330
|
+
Fixable rules:
|
|
331
|
+
${Object.entries(FIXERS).map(([id, f]) => ` ${id.padEnd(26)} ${f.description}`).join('\n')}
|
|
332
|
+
|
|
333
|
+
Everything else is reported as skipped with a reason (e.g. broken-image:
|
|
334
|
+
choosing a real image is a content decision, not a codemod).
|
|
335
|
+
|
|
336
|
+
Examples:
|
|
337
|
+
monodesign fix src/
|
|
338
|
+
monodesign fix --dry-run index.html
|
|
339
|
+
monodesign fix --rule tight-leading,tiny-text styles.css`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function formatFixReport(report, jsonMode) {
|
|
343
|
+
if (jsonMode) return JSON.stringify(report, null, 2);
|
|
344
|
+
const out = [];
|
|
345
|
+
if (report.dryRun) {
|
|
346
|
+
for (const { diff } of report.diffs) {
|
|
347
|
+
if (diff) out.push(diff, '');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const verb = report.dryRun ? 'would fix' : 'fixed';
|
|
351
|
+
for (const item of report.fixed) {
|
|
352
|
+
const where = item.targetFile && item.targetFile !== item.file
|
|
353
|
+
? `${item.file} (edited ${item.targetFile})`
|
|
354
|
+
: item.file;
|
|
355
|
+
out.push(`${verb} [${item.antipattern}] ${item.fixed}/${item.before} in ${where}`);
|
|
356
|
+
}
|
|
357
|
+
for (const item of report.remaining) {
|
|
358
|
+
out.push(`remaining [${item.antipattern}] ${item.count} in ${item.file}`);
|
|
359
|
+
}
|
|
360
|
+
for (const item of report.skipped) {
|
|
361
|
+
out.push(`skipped [${item.antipattern}] ${item.count} in ${item.file} — ${item.reason}`);
|
|
362
|
+
}
|
|
363
|
+
const fixedTotal = report.fixed.reduce((n, f) => n + (f.fixed || 0), 0);
|
|
364
|
+
const remainingTotal = report.remaining.reduce((n, f) => n + f.count, 0);
|
|
365
|
+
const skippedTotal = report.skipped.reduce((n, f) => n + f.count, 0);
|
|
366
|
+
out.push(`\n${report.dryRun ? 'Would fix' : 'Fixed'} ${fixedTotal}, remaining ${remainingTotal}, skipped ${skippedTotal}. ${report.filesChanged.length} file${report.filesChanged.length === 1 ? '' : 's'} ${report.dryRun ? 'would change' : 'changed'}.`);
|
|
367
|
+
return out.join('\n');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function runFixCli(args) {
|
|
371
|
+
const targets = [];
|
|
372
|
+
const options = { dryRun: false, rules: null, noConfig: false };
|
|
373
|
+
let jsonMode = false;
|
|
374
|
+
for (let i = 0; i < args.length; i++) {
|
|
375
|
+
const arg = args[i];
|
|
376
|
+
if (arg === '--help' || arg === '-h') { printFixUsage(); process.exit(0); }
|
|
377
|
+
else if (arg === '--dry-run') options.dryRun = true;
|
|
378
|
+
else if (arg === '--json') jsonMode = true;
|
|
379
|
+
else if (arg === '--no-config') options.noConfig = true;
|
|
380
|
+
else if (arg === '--rule' || arg.startsWith('--rule=')) {
|
|
381
|
+
const value = arg.startsWith('--rule=') ? arg.slice('--rule='.length) : args[++i];
|
|
382
|
+
if (!value || value.startsWith('--')) {
|
|
383
|
+
process.stderr.write('Error: --rule requires a comma-separated list of rule ids.\n');
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
options.rules = [...(options.rules || []), ...value.split(',').map(s => s.trim()).filter(Boolean)];
|
|
387
|
+
} else if (arg.startsWith('--')) {
|
|
388
|
+
process.stderr.write(`Error: unknown option ${arg}\n\nRun: monodesign fix --help\n`);
|
|
389
|
+
process.exit(1);
|
|
390
|
+
} else {
|
|
391
|
+
targets.push(arg);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (options.rules) {
|
|
396
|
+
const fixable = new Set(fixableRuleIds());
|
|
397
|
+
const unknown = options.rules.filter(r => !fixable.has(r));
|
|
398
|
+
if (unknown.length > 0) {
|
|
399
|
+
process.stderr.write(`Error: no fixer for rule(s): ${unknown.join(', ')}.\nFixable rules: ${fixableRuleIds().join(', ')}\n`);
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (targets.length === 0) {
|
|
405
|
+
process.stderr.write('Error: fix requires at least one file or directory target.\n\nRun: monodesign fix --help\n');
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const report = await runFix(targets, options);
|
|
410
|
+
for (const warning of report.warnings) process.stderr.write(`${warning}\n`);
|
|
411
|
+
const formatted = formatFixReport(report, jsonMode);
|
|
412
|
+
if (jsonMode || report.dryRun) process.stdout.write(formatted + '\n');
|
|
413
|
+
else process.stderr.write(formatted + '\n');
|
|
414
|
+
process.exit(!report.dryRun && report.remaining.length > 0 ? 2 : 0);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export {
|
|
418
|
+
runFix,
|
|
419
|
+
runFixCli,
|
|
420
|
+
applyEdits,
|
|
421
|
+
unifiedDiff,
|
|
422
|
+
writeFileAtomic,
|
|
423
|
+
extractLinkedStylesheets,
|
|
424
|
+
printFixUsage,
|
|
425
|
+
formatFixReport,
|
|
426
|
+
};
|
package/packages/@monomind/cli/.claude/skills/monodesign/scripts/detector/registry/antipatterns.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// Pattern adapted from impeccable (Apache-2.0) — rebranded for monomind
|
|
2
1
|
const ANTIPATTERNS = [
|
|
3
2
|
// ── AI slop: tells that something was AI-generated ──
|
|
4
3
|
{
|
|
@@ -389,6 +388,55 @@ const ANTIPATTERNS = [
|
|
|
389
388
|
skillGuideline: 'font size outside the project design system',
|
|
390
389
|
},
|
|
391
390
|
|
|
391
|
+
{
|
|
392
|
+
id: 'missing-focus-visible',
|
|
393
|
+
category: 'quality',
|
|
394
|
+
name: 'Suppressed focus outline with no replacement',
|
|
395
|
+
description:
|
|
396
|
+
'An interactive element (link, button, input) removes its focus outline (outline: none / 0) but the stylesheet never provides a :focus-visible or :focus replacement. Keyboard users lose all sense of where they are. Remove the suppression, or pair it with a visible :focus-visible ring (outline, box-shadow, or border).',
|
|
397
|
+
skillSection: 'Interaction',
|
|
398
|
+
skillGuideline: 'outline removed without a focus-visible replacement',
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
id: 'small-touch-target',
|
|
402
|
+
category: 'quality',
|
|
403
|
+
scopes: ['layout'],
|
|
404
|
+
name: 'Touch target below 44px',
|
|
405
|
+
description:
|
|
406
|
+
'A clickable control (button, link, input, [role=button]) renders smaller than 44×44px on one or both axes. Fingers miss small targets and mis-tap neighbors. Give standalone controls at least 44×44px of hit area via padding or min-width/height. Inline text links inside prose are exempt.',
|
|
407
|
+
skillSection: 'Layout & Space',
|
|
408
|
+
skillGuideline: 'touch target smaller than 44px',
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
id: 'hover-only-affordance',
|
|
412
|
+
category: 'quality',
|
|
413
|
+
name: 'Functionality gated behind hover only',
|
|
414
|
+
description:
|
|
415
|
+
'An element is hidden by default and revealed only on :hover, with no :focus, :focus-within, or :active equivalent. Keyboard and touch users can never reach it. Mirror every hover reveal with a focus-within (or active) rule so the affordance is reachable without a pointer.',
|
|
416
|
+
skillSection: 'Interaction',
|
|
417
|
+
skillGuideline: 'affordance revealed on hover with no focus equivalent',
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
id: 'image-missing-dimensions',
|
|
421
|
+
category: 'quality',
|
|
422
|
+
scopes: ['layout'],
|
|
423
|
+
name: 'Image without reserved dimensions',
|
|
424
|
+
description:
|
|
425
|
+
'An <img> ships without width and height attributes and without a CSS aspect-ratio or explicit height. The browser cannot reserve space before the image loads, so surrounding content jumps (cumulative layout shift). Set width and height attributes, or give the image a CSS aspect-ratio.',
|
|
426
|
+
skillSection: 'Imagery',
|
|
427
|
+
skillGuideline: 'image with no reserved dimensions',
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
id: 'dark-scheme-contrast-blindspot',
|
|
431
|
+
category: 'quality',
|
|
432
|
+
severity: 'advisory',
|
|
433
|
+
name: 'Dark-scheme contrast blindspot',
|
|
434
|
+
description:
|
|
435
|
+
'The page ships dark styling (a prefers-color-scheme: dark block or a .dark / [data-theme=dark] scope), but for some selector the dark override changes the background without changing the paired text color (or the reverse). The half-updated pair often collapses to unreadable contrast in dark mode. Override text and background together, or verify the inherited half still contrasts.',
|
|
436
|
+
skillSection: 'Color & Contrast',
|
|
437
|
+
skillGuideline: 'dark scheme overrides one of a color pair but not the other',
|
|
438
|
+
},
|
|
439
|
+
|
|
392
440
|
// ── Provider tells: opt-in via --gpt / --gemini (gated off by default) ──
|
|
393
441
|
{
|
|
394
442
|
id: 'gpt-thin-border-wide-shadow',
|