@hegemonart/get-design-done 1.40.0 → 1.41.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +78 -0
- package/README.md +8 -0
- package/SKILL.md +1 -0
- package/agents/README.md +1 -0
- package/agents/design-auditor.md +5 -6
- package/bin/gdd-detect +20 -0
- package/package.json +4 -1
- package/reference/anti-patterns.md +26 -0
- package/reference/cli-localization.md +61 -0
- package/reference/config-schema.md +8 -0
- package/reference/registry.json +7 -0
- package/reference/schemas/config.schema.json +5 -0
- package/reference/schemas/generated.d.ts +4 -0
- package/scripts/lib/detect/cli.cjs +111 -0
- package/scripts/lib/detect/engine.cjs +83 -0
- package/scripts/lib/detect/rule-schema.json +31 -0
- package/scripts/lib/detect/rules/ban-01.cjs +33 -0
- package/scripts/lib/detect/rules/ban-02.cjs +33 -0
- package/scripts/lib/detect/rules/ban-03.cjs +33 -0
- package/scripts/lib/detect/rules/ban-05.cjs +33 -0
- package/scripts/lib/detect/rules/ban-06.cjs +33 -0
- package/scripts/lib/detect/rules/ban-07.cjs +33 -0
- package/scripts/lib/detect/rules/ban-08.cjs +33 -0
- package/scripts/lib/detect/rules/ban-09.cjs +33 -0
- package/scripts/lib/detect/rules/ban-11.cjs +33 -0
- package/scripts/lib/detect/rules/ban-12.cjs +33 -0
- package/scripts/lib/detect/rules/ban-13.cjs +33 -0
- package/scripts/lib/detect/rules/index.cjs +21 -0
- package/scripts/lib/i18n/index.cjs +95 -0
- package/scripts/lib/i18n/messages/de.json +7 -0
- package/scripts/lib/i18n/messages/en.json +27 -0
- package/scripts/lib/i18n/messages/fr.json +7 -0
- package/scripts/lib/i18n/messages/ja.json +7 -0
- package/scripts/lib/i18n/messages/ru.json +27 -0
- package/scripts/lib/i18n/messages/uk.json +7 -0
- package/scripts/lib/i18n/messages/zh.json +7 -0
- package/skills/locale/SKILL.md +51 -0
- package/skills/quality-gate/SKILL.md +1 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 41 — gdd-detect engine. Pure, dep-free (regex-fast path). Walks a path, runs each rule's
|
|
3
|
+
// matcher against file content, returns structured findings. The DOM-aware (jsdom) + URL (puppeteer)
|
|
4
|
+
// paths are layered on in cli.cjs via soft try-require; the engine itself never touches the network
|
|
5
|
+
// or any optional dependency — so the SC#10 network-isolation scan stays clean.
|
|
6
|
+
|
|
7
|
+
const fs = require('node:fs');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const { RULES, EXEMPT } = require('./rules/index.cjs');
|
|
10
|
+
|
|
11
|
+
const SCANNABLE_EXT = new Set(['.html', '.htm', '.css', '.scss', '.jsx', '.tsx', '.js', '.ts', '.vue', '.svelte']);
|
|
12
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'coverage', '.design', '.planning']);
|
|
13
|
+
|
|
14
|
+
/** Recursively collect scannable file paths under `root` (a file or dir). */
|
|
15
|
+
function walk(root) {
|
|
16
|
+
const out = [];
|
|
17
|
+
if (!fs.existsSync(root)) return out;
|
|
18
|
+
const st = fs.statSync(root);
|
|
19
|
+
if (st.isFile()) {
|
|
20
|
+
if (SCANNABLE_EXT.has(path.extname(root).toLowerCase())) out.push(root);
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
const stack = [root];
|
|
24
|
+
while (stack.length) {
|
|
25
|
+
const dir = stack.pop();
|
|
26
|
+
let entries;
|
|
27
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
28
|
+
for (const e of entries) {
|
|
29
|
+
const full = path.join(dir, e.name);
|
|
30
|
+
if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) stack.push(full); }
|
|
31
|
+
else if (e.isFile() && SCANNABLE_EXT.has(path.extname(e.name).toLowerCase())) out.push(full);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Select the active rule set. `ruleId` (e.g. 'BAN-08') narrows to one rule. */
|
|
38
|
+
function selectRules(ruleId) {
|
|
39
|
+
if (!ruleId) return RULES;
|
|
40
|
+
const id = String(ruleId).toUpperCase();
|
|
41
|
+
return RULES.filter((r) => r.id === id);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Run `rules` over one file's content. Returns findings with file-relative metadata merged in. */
|
|
45
|
+
function scanContent(content, ctx, rules) {
|
|
46
|
+
const findings = [];
|
|
47
|
+
for (const rule of rules) {
|
|
48
|
+
let hits = [];
|
|
49
|
+
try { hits = rule.matcher({ content, ext: ctx.ext, path: ctx.path }) || []; } catch { hits = []; }
|
|
50
|
+
for (const h of hits) {
|
|
51
|
+
findings.push({
|
|
52
|
+
ruleId: rule.id, category: rule.category, name: rule.name, severity: rule.severity,
|
|
53
|
+
file: ctx.path, line: h.line, column: h.column, match: h.match, references: rule.references,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return findings;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Run the detector over a path.
|
|
62
|
+
* @param {string} root file or directory
|
|
63
|
+
* @param {{ruleId?: string, cwd?: string}} [opts]
|
|
64
|
+
* @returns {{findings: object[], filesScanned: number, errors: number, rules: number}}
|
|
65
|
+
*/
|
|
66
|
+
function run(root, opts) {
|
|
67
|
+
const o = opts || {};
|
|
68
|
+
const rules = selectRules(o.ruleId);
|
|
69
|
+
const cwd = o.cwd || process.cwd();
|
|
70
|
+
const files = walk(root);
|
|
71
|
+
const findings = [];
|
|
72
|
+
let errors = 0;
|
|
73
|
+
for (const abs of files) {
|
|
74
|
+
let content;
|
|
75
|
+
try { content = fs.readFileSync(abs, 'utf8'); } catch { errors++; continue; }
|
|
76
|
+
const rel = path.relative(cwd, abs).split(path.sep).join('/');
|
|
77
|
+
findings.push(...scanContent(content, { path: rel || abs, ext: path.extname(abs).toLowerCase() }, rules));
|
|
78
|
+
}
|
|
79
|
+
findings.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column || a.ruleId.localeCompare(b.ruleId));
|
|
80
|
+
return { findings, filesScanned: files.length, errors, rules: rules.length };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { run, walk, scanContent, selectRules, RULES, EXEMPT, SCANNABLE_EXT, SKIP_DIRS };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://get-design-done.example/schemas/gdd-detect-rule.schema.json",
|
|
4
|
+
"title": "gdd-detect rule",
|
|
5
|
+
"description": "Phase 41 — the shape of a scripts/lib/detect/rules/ban-NN.cjs module export. Each rule ports a statically-detectable BAN-NN anti-pattern from reference/anti-patterns.md into an executable matcher. The markdown catalogue stays the canonical prose; the rule files are the canonical executable; scripts/sync-rule-catalogue.cjs keeps them in parity.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["id", "category", "name", "description", "references", "severity"],
|
|
8
|
+
"additionalProperties": true,
|
|
9
|
+
"properties": {
|
|
10
|
+
"id": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"pattern": "^BAN-[0-9]{2}$",
|
|
13
|
+
"description": "The BAN-NN id — matches a `### BAN-NN:` heading + a `bdId: BAN-NN` marker in reference/anti-patterns.md."
|
|
14
|
+
},
|
|
15
|
+
"category": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"enum": ["decoration", "motion", "color", "accessibility", "performance", "layout", "typography"],
|
|
18
|
+
"description": "Coarse grouping for report sectioning."
|
|
19
|
+
},
|
|
20
|
+
"name": { "type": "string", "minLength": 1, "description": "Human title (the BAN heading text after the id)." },
|
|
21
|
+
"description": { "type": "string", "minLength": 1, "description": "One-line explanation of what the rule flags + why." },
|
|
22
|
+
"references": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"minItems": 1,
|
|
25
|
+
"items": { "type": "string", "pattern": "^reference/.+#" },
|
|
26
|
+
"description": "Bidirectional links to the catalogue paragraph(s) that explain the alternative (e.g. reference/anti-patterns.md#BAN-04)."
|
|
27
|
+
},
|
|
28
|
+
"severity": { "type": "string", "enum": ["error", "warn"], "description": "error fails a strict gate; warn is advisory (open-Q default: error|warn)." },
|
|
29
|
+
"pattern": { "type": "string", "description": "The regex source ported from the catalogue's **Grep** (regex-fast path)." }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 41 — BAN-01: Side-Stripe Borders. Ported from reference/anti-patterns.md (its own **Grep**).
|
|
3
|
+
// Pure, dep-free. No `require`. The matcher scans ctx.content; line/column are 1-based.
|
|
4
|
+
|
|
5
|
+
const PATTERN = "border-left:\\s*[2-9][0-9]*px|border-right:\\s*[2-9][0-9]*px";
|
|
6
|
+
|
|
7
|
+
/** @param {{content: string, ext: string, path: string}} ctx @returns {{line:number,column:number,match:string}[]} */
|
|
8
|
+
function matcher(ctx) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const re = new RegExp(PATTERN, 'gi');
|
|
11
|
+
const text = String((ctx && ctx.content) || '');
|
|
12
|
+
let m;
|
|
13
|
+
while ((m = re.exec(text)) !== null) {
|
|
14
|
+
const upto = text.slice(0, m.index);
|
|
15
|
+
const line = upto.split('\n').length;
|
|
16
|
+
const lastNl = upto.lastIndexOf('\n');
|
|
17
|
+
const column = lastNl < 0 ? m.index + 1 : m.index - lastNl;
|
|
18
|
+
out.push({ line, column, match: m[0] });
|
|
19
|
+
if (m.index === re.lastIndex) re.lastIndex++; // zero-width guard
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
id: "BAN-01",
|
|
26
|
+
category: "decoration",
|
|
27
|
+
name: "Side-Stripe Borders",
|
|
28
|
+
description: "A thick (>=2px) left/right accent border — a dated, decorative side-stripe.",
|
|
29
|
+
references: ["reference/anti-patterns.md#BAN-01"],
|
|
30
|
+
severity: "warn",
|
|
31
|
+
pattern: PATTERN,
|
|
32
|
+
matcher,
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 41 — BAN-02: Gradient Text. Ported from reference/anti-patterns.md (its own **Grep**).
|
|
3
|
+
// Pure, dep-free. No `require`. The matcher scans ctx.content; line/column are 1-based.
|
|
4
|
+
|
|
5
|
+
const PATTERN = "background-clip:\\s*text|text-fill-color:\\s*transparent";
|
|
6
|
+
|
|
7
|
+
/** @param {{content: string, ext: string, path: string}} ctx @returns {{line:number,column:number,match:string}[]} */
|
|
8
|
+
function matcher(ctx) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const re = new RegExp(PATTERN, 'gi');
|
|
11
|
+
const text = String((ctx && ctx.content) || '');
|
|
12
|
+
let m;
|
|
13
|
+
while ((m = re.exec(text)) !== null) {
|
|
14
|
+
const upto = text.slice(0, m.index);
|
|
15
|
+
const line = upto.split('\n').length;
|
|
16
|
+
const lastNl = upto.lastIndexOf('\n');
|
|
17
|
+
const column = lastNl < 0 ? m.index + 1 : m.index - lastNl;
|
|
18
|
+
out.push({ line, column, match: m[0] });
|
|
19
|
+
if (m.index === re.lastIndex) re.lastIndex++; // zero-width guard
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
id: "BAN-02",
|
|
26
|
+
category: "decoration",
|
|
27
|
+
name: "Gradient Text",
|
|
28
|
+
description: "Gradient-filled text via background-clip:text — low legibility, an AI-era cliche.",
|
|
29
|
+
references: ["reference/anti-patterns.md#BAN-02"],
|
|
30
|
+
severity: "warn",
|
|
31
|
+
pattern: PATTERN,
|
|
32
|
+
matcher,
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 41 — BAN-03: Bounce/Elastic Easing. Ported from reference/anti-patterns.md (its own **Grep**).
|
|
3
|
+
// Pure, dep-free. No `require`. The matcher scans ctx.content; line/column are 1-based.
|
|
4
|
+
|
|
5
|
+
const PATTERN = "cubic-bezier\\(.*-[0-9]|bounce|elastic|spring\\(";
|
|
6
|
+
|
|
7
|
+
/** @param {{content: string, ext: string, path: string}} ctx @returns {{line:number,column:number,match:string}[]} */
|
|
8
|
+
function matcher(ctx) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const re = new RegExp(PATTERN, 'gi');
|
|
11
|
+
const text = String((ctx && ctx.content) || '');
|
|
12
|
+
let m;
|
|
13
|
+
while ((m = re.exec(text)) !== null) {
|
|
14
|
+
const upto = text.slice(0, m.index);
|
|
15
|
+
const line = upto.split('\n').length;
|
|
16
|
+
const lastNl = upto.lastIndexOf('\n');
|
|
17
|
+
const column = lastNl < 0 ? m.index + 1 : m.index - lastNl;
|
|
18
|
+
out.push({ line, column, match: m[0] });
|
|
19
|
+
if (m.index === re.lastIndex) re.lastIndex++; // zero-width guard
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
id: "BAN-03",
|
|
26
|
+
category: "motion",
|
|
27
|
+
name: "Bounce/Elastic Easing",
|
|
28
|
+
description: "Bounce/elastic/spring easing — playful overshoot that reads as unserious for product UI.",
|
|
29
|
+
references: ["reference/anti-patterns.md#BAN-03"],
|
|
30
|
+
severity: "warn",
|
|
31
|
+
pattern: PATTERN,
|
|
32
|
+
matcher,
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 41 — BAN-05: Pure Black Dark Mode. Ported from reference/anti-patterns.md (its own **Grep**).
|
|
3
|
+
// Pure, dep-free. No `require`. The matcher scans ctx.content; line/column are 1-based.
|
|
4
|
+
|
|
5
|
+
const PATTERN = "background.*#000000|background.*rgb\\(0,\\s*0,\\s*0\\)";
|
|
6
|
+
|
|
7
|
+
/** @param {{content: string, ext: string, path: string}} ctx @returns {{line:number,column:number,match:string}[]} */
|
|
8
|
+
function matcher(ctx) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const re = new RegExp(PATTERN, 'gi');
|
|
11
|
+
const text = String((ctx && ctx.content) || '');
|
|
12
|
+
let m;
|
|
13
|
+
while ((m = re.exec(text)) !== null) {
|
|
14
|
+
const upto = text.slice(0, m.index);
|
|
15
|
+
const line = upto.split('\n').length;
|
|
16
|
+
const lastNl = upto.lastIndexOf('\n');
|
|
17
|
+
const column = lastNl < 0 ? m.index + 1 : m.index - lastNl;
|
|
18
|
+
out.push({ line, column, match: m[0] });
|
|
19
|
+
if (m.index === re.lastIndex) re.lastIndex++; // zero-width guard
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
id: "BAN-05",
|
|
26
|
+
category: "color",
|
|
27
|
+
name: "Pure Black Dark Mode",
|
|
28
|
+
description: "Pure #000 dark-mode background — harsh contrast + halation; use a near-black surface.",
|
|
29
|
+
references: ["reference/anti-patterns.md#BAN-05"],
|
|
30
|
+
severity: "warn",
|
|
31
|
+
pattern: PATTERN,
|
|
32
|
+
matcher,
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 41 — BAN-06: Disabling Zoom. Ported from reference/anti-patterns.md (its own **Grep**).
|
|
3
|
+
// Pure, dep-free. No `require`. The matcher scans ctx.content; line/column are 1-based.
|
|
4
|
+
|
|
5
|
+
const PATTERN = "user-scalable=no|maximum-scale=1";
|
|
6
|
+
|
|
7
|
+
/** @param {{content: string, ext: string, path: string}} ctx @returns {{line:number,column:number,match:string}[]} */
|
|
8
|
+
function matcher(ctx) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const re = new RegExp(PATTERN, 'gi');
|
|
11
|
+
const text = String((ctx && ctx.content) || '');
|
|
12
|
+
let m;
|
|
13
|
+
while ((m = re.exec(text)) !== null) {
|
|
14
|
+
const upto = text.slice(0, m.index);
|
|
15
|
+
const line = upto.split('\n').length;
|
|
16
|
+
const lastNl = upto.lastIndexOf('\n');
|
|
17
|
+
const column = lastNl < 0 ? m.index + 1 : m.index - lastNl;
|
|
18
|
+
out.push({ line, column, match: m[0] });
|
|
19
|
+
if (m.index === re.lastIndex) re.lastIndex++; // zero-width guard
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
id: "BAN-06",
|
|
26
|
+
category: "accessibility",
|
|
27
|
+
name: "Disabling Zoom",
|
|
28
|
+
description: "Viewport meta that disables pinch-zoom — a WCAG 1.4.4 failure.",
|
|
29
|
+
references: ["reference/anti-patterns.md#BAN-06"],
|
|
30
|
+
severity: "error",
|
|
31
|
+
pattern: PATTERN,
|
|
32
|
+
matcher,
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 41 — BAN-07: Naked outline: none. Ported from reference/anti-patterns.md (its own **Grep**).
|
|
3
|
+
// Pure, dep-free. No `require`. The matcher scans ctx.content; line/column are 1-based.
|
|
4
|
+
|
|
5
|
+
const PATTERN = ":focus\\s*\\{[^}]*outline:\\s*(none|0)";
|
|
6
|
+
|
|
7
|
+
/** @param {{content: string, ext: string, path: string}} ctx @returns {{line:number,column:number,match:string}[]} */
|
|
8
|
+
function matcher(ctx) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const re = new RegExp(PATTERN, 'gi');
|
|
11
|
+
const text = String((ctx && ctx.content) || '');
|
|
12
|
+
let m;
|
|
13
|
+
while ((m = re.exec(text)) !== null) {
|
|
14
|
+
const upto = text.slice(0, m.index);
|
|
15
|
+
const line = upto.split('\n').length;
|
|
16
|
+
const lastNl = upto.lastIndexOf('\n');
|
|
17
|
+
const column = lastNl < 0 ? m.index + 1 : m.index - lastNl;
|
|
18
|
+
out.push({ line, column, match: m[0] });
|
|
19
|
+
if (m.index === re.lastIndex) re.lastIndex++; // zero-width guard
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
id: "BAN-07",
|
|
26
|
+
category: "accessibility",
|
|
27
|
+
name: "Naked outline: none",
|
|
28
|
+
description: "Removing the focus outline without a replacement — a keyboard-a11y failure.",
|
|
29
|
+
references: ["reference/anti-patterns.md#BAN-07"],
|
|
30
|
+
severity: "error",
|
|
31
|
+
pattern: PATTERN,
|
|
32
|
+
matcher,
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 41 — BAN-08: transition: all. Ported from reference/anti-patterns.md (its own **Grep**).
|
|
3
|
+
// Pure, dep-free. No `require`. The matcher scans ctx.content; line/column are 1-based.
|
|
4
|
+
|
|
5
|
+
const PATTERN = "transition:\\s*all\\s";
|
|
6
|
+
|
|
7
|
+
/** @param {{content: string, ext: string, path: string}} ctx @returns {{line:number,column:number,match:string}[]} */
|
|
8
|
+
function matcher(ctx) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const re = new RegExp(PATTERN, 'gi');
|
|
11
|
+
const text = String((ctx && ctx.content) || '');
|
|
12
|
+
let m;
|
|
13
|
+
while ((m = re.exec(text)) !== null) {
|
|
14
|
+
const upto = text.slice(0, m.index);
|
|
15
|
+
const line = upto.split('\n').length;
|
|
16
|
+
const lastNl = upto.lastIndexOf('\n');
|
|
17
|
+
const column = lastNl < 0 ? m.index + 1 : m.index - lastNl;
|
|
18
|
+
out.push({ line, column, match: m[0] });
|
|
19
|
+
if (m.index === re.lastIndex) re.lastIndex++; // zero-width guard
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
id: "BAN-08",
|
|
26
|
+
category: "motion",
|
|
27
|
+
name: "transition: all",
|
|
28
|
+
description: "transition: all animates layout-triggering properties — jank; name the exact properties.",
|
|
29
|
+
references: ["reference/anti-patterns.md#BAN-08"],
|
|
30
|
+
severity: "warn",
|
|
31
|
+
pattern: PATTERN,
|
|
32
|
+
matcher,
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 41 — BAN-09: scale(0) Animation Entry. Ported from reference/anti-patterns.md (its own **Grep**).
|
|
3
|
+
// Pure, dep-free. No `require`. The matcher scans ctx.content; line/column are 1-based.
|
|
4
|
+
|
|
5
|
+
const PATTERN = "transform:\\s*scale\\(\\s*0\\s*\\)|scale\\(\\s*0\\s*\\)";
|
|
6
|
+
|
|
7
|
+
/** @param {{content: string, ext: string, path: string}} ctx @returns {{line:number,column:number,match:string}[]} */
|
|
8
|
+
function matcher(ctx) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const re = new RegExp(PATTERN, 'gi');
|
|
11
|
+
const text = String((ctx && ctx.content) || '');
|
|
12
|
+
let m;
|
|
13
|
+
while ((m = re.exec(text)) !== null) {
|
|
14
|
+
const upto = text.slice(0, m.index);
|
|
15
|
+
const line = upto.split('\n').length;
|
|
16
|
+
const lastNl = upto.lastIndexOf('\n');
|
|
17
|
+
const column = lastNl < 0 ? m.index + 1 : m.index - lastNl;
|
|
18
|
+
out.push({ line, column, match: m[0] });
|
|
19
|
+
if (m.index === re.lastIndex) re.lastIndex++; // zero-width guard
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
id: "BAN-09",
|
|
26
|
+
category: "motion",
|
|
27
|
+
name: "scale(0) Animation Entry",
|
|
28
|
+
description: "Entering from scale(0) — nothing materializes from nothing; start at scale(0.95)+opacity.",
|
|
29
|
+
references: ["reference/anti-patterns.md#BAN-09"],
|
|
30
|
+
severity: "warn",
|
|
31
|
+
pattern: PATTERN,
|
|
32
|
+
matcher,
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 41 — BAN-11: Tinted Image Outline. Ported from reference/anti-patterns.md (its own **Grep**).
|
|
3
|
+
// Pure, dep-free. No `require`. The matcher scans ctx.content; line/column are 1-based.
|
|
4
|
+
|
|
5
|
+
const PATTERN = "outline-(slate|zinc|neutral|gray|stone|blue|red|green|yellow|purple)-\\d+|img\\s*\\{[^}]*outline:\\s*[^}]*#[0-9a-fA-F]{3,8}";
|
|
6
|
+
|
|
7
|
+
/** @param {{content: string, ext: string, path: string}} ctx @returns {{line:number,column:number,match:string}[]} */
|
|
8
|
+
function matcher(ctx) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const re = new RegExp(PATTERN, 'gi');
|
|
11
|
+
const text = String((ctx && ctx.content) || '');
|
|
12
|
+
let m;
|
|
13
|
+
while ((m = re.exec(text)) !== null) {
|
|
14
|
+
const upto = text.slice(0, m.index);
|
|
15
|
+
const line = upto.split('\n').length;
|
|
16
|
+
const lastNl = upto.lastIndexOf('\n');
|
|
17
|
+
const column = lastNl < 0 ? m.index + 1 : m.index - lastNl;
|
|
18
|
+
out.push({ line, column, match: m[0] });
|
|
19
|
+
if (m.index === re.lastIndex) re.lastIndex++; // zero-width guard
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
id: "BAN-11",
|
|
26
|
+
category: "decoration",
|
|
27
|
+
name: "Tinted Image Outline",
|
|
28
|
+
description: "A colored outline on an image — color contamination; use low-opacity black/white.",
|
|
29
|
+
references: ["reference/anti-patterns.md#BAN-11"],
|
|
30
|
+
severity: "warn",
|
|
31
|
+
pattern: PATTERN,
|
|
32
|
+
matcher,
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 41 — BAN-12: transition: all (property). Ported from reference/anti-patterns.md (its own **Grep**).
|
|
3
|
+
// Pure, dep-free. No `require`. The matcher scans ctx.content; line/column are 1-based.
|
|
4
|
+
|
|
5
|
+
const PATTERN = "transition:\\s*all|transition-property:\\s*all";
|
|
6
|
+
|
|
7
|
+
/** @param {{content: string, ext: string, path: string}} ctx @returns {{line:number,column:number,match:string}[]} */
|
|
8
|
+
function matcher(ctx) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const re = new RegExp(PATTERN, 'gi');
|
|
11
|
+
const text = String((ctx && ctx.content) || '');
|
|
12
|
+
let m;
|
|
13
|
+
while ((m = re.exec(text)) !== null) {
|
|
14
|
+
const upto = text.slice(0, m.index);
|
|
15
|
+
const line = upto.split('\n').length;
|
|
16
|
+
const lastNl = upto.lastIndexOf('\n');
|
|
17
|
+
const column = lastNl < 0 ? m.index + 1 : m.index - lastNl;
|
|
18
|
+
out.push({ line, column, match: m[0] });
|
|
19
|
+
if (m.index === re.lastIndex) re.lastIndex++; // zero-width guard
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
id: "BAN-12",
|
|
26
|
+
category: "motion",
|
|
27
|
+
name: "transition: all (property)",
|
|
28
|
+
description: "transition: all / transition-property: all — recalculates layout every transition.",
|
|
29
|
+
references: ["reference/anti-patterns.md#BAN-12"],
|
|
30
|
+
severity: "warn",
|
|
31
|
+
pattern: PATTERN,
|
|
32
|
+
matcher,
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 41 — BAN-13: will-change: all. Ported from reference/anti-patterns.md (its own **Grep**).
|
|
3
|
+
// Pure, dep-free. No `require`. The matcher scans ctx.content; line/column are 1-based.
|
|
4
|
+
|
|
5
|
+
const PATTERN = "will-change:\\s*all";
|
|
6
|
+
|
|
7
|
+
/** @param {{content: string, ext: string, path: string}} ctx @returns {{line:number,column:number,match:string}[]} */
|
|
8
|
+
function matcher(ctx) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const re = new RegExp(PATTERN, 'gi');
|
|
11
|
+
const text = String((ctx && ctx.content) || '');
|
|
12
|
+
let m;
|
|
13
|
+
while ((m = re.exec(text)) !== null) {
|
|
14
|
+
const upto = text.slice(0, m.index);
|
|
15
|
+
const line = upto.split('\n').length;
|
|
16
|
+
const lastNl = upto.lastIndexOf('\n');
|
|
17
|
+
const column = lastNl < 0 ? m.index + 1 : m.index - lastNl;
|
|
18
|
+
out.push({ line, column, match: m[0] });
|
|
19
|
+
if (m.index === re.lastIndex) re.lastIndex++; // zero-width guard
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
id: "BAN-13",
|
|
26
|
+
category: "performance",
|
|
27
|
+
name: "will-change: all",
|
|
28
|
+
description: "will-change: all promotes every property to its own GPU layer — huge texture memory.",
|
|
29
|
+
references: ["reference/anti-patterns.md#BAN-13"],
|
|
30
|
+
severity: "warn",
|
|
31
|
+
pattern: PATTERN,
|
|
32
|
+
matcher,
|
|
33
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 41 — rule registry. Loads every scripts/lib/detect/rules/ban-NN.cjs and exposes the
|
|
3
|
+
// matcher-exempt set (subjective BAN rules with no static matcher: BAN-04 behavior, BAN-10 DOM-depth).
|
|
4
|
+
|
|
5
|
+
const r0 = require('./ban-01.cjs');
|
|
6
|
+
const r1 = require('./ban-02.cjs');
|
|
7
|
+
const r2 = require('./ban-03.cjs');
|
|
8
|
+
const r3 = require('./ban-05.cjs');
|
|
9
|
+
const r4 = require('./ban-06.cjs');
|
|
10
|
+
const r5 = require('./ban-07.cjs');
|
|
11
|
+
const r6 = require('./ban-08.cjs');
|
|
12
|
+
const r7 = require('./ban-09.cjs');
|
|
13
|
+
const r8 = require('./ban-11.cjs');
|
|
14
|
+
const r9 = require('./ban-12.cjs');
|
|
15
|
+
const r10 = require('./ban-13.cjs');
|
|
16
|
+
|
|
17
|
+
const RULES = [r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10];
|
|
18
|
+
// Subjective BAN rules documented in reference/anti-patterns.md but NOT statically detectable.
|
|
19
|
+
const EXEMPT = Object.freeze(['BAN-04', 'BAN-10']);
|
|
20
|
+
|
|
21
|
+
module.exports = { RULES, EXEMPT };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Phase 40.5 — i18n/index.cjs — GDD CLI localization resolver.
|
|
3
|
+
//
|
|
4
|
+
// Resolves a locale (config override > env LANG > English), a fallback chain (locale → base → en),
|
|
5
|
+
// and translates a message key against per-locale flat-JSON tables. The pure functions (baseLocale,
|
|
6
|
+
// fallbackChain, resolveLocale, translate, descriptionFor) take their data as arguments and touch
|
|
7
|
+
// neither fs nor env directly — so they are trivially unit-testable. `loadTable` is the only fs reader.
|
|
8
|
+
//
|
|
9
|
+
// Contract: reference/cli-localization.md. Fallback is always to `en` (the complete source table).
|
|
10
|
+
|
|
11
|
+
const fs = require('node:fs');
|
|
12
|
+
const path = require('node:path');
|
|
13
|
+
|
|
14
|
+
const KNOWN_LOCALES = Object.freeze(['en', 'ru', 'uk', 'de', 'fr', 'zh', 'ja']);
|
|
15
|
+
const DEFAULT_LOCALE = 'en';
|
|
16
|
+
const MESSAGES_DIR = path.join(__dirname, 'messages');
|
|
17
|
+
|
|
18
|
+
/** Normalize a raw locale token: lowercase, `_`→`-`, strip an encoding suffix (`ru_RU.UTF-8` → `ru-ru`). */
|
|
19
|
+
function normalizeLocale(code) {
|
|
20
|
+
if (!code) return '';
|
|
21
|
+
return String(code).split('.')[0].split(':')[0].trim().toLowerCase().replace(/_/g, '-');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The base of a locale (`de-de` → `de`; `en` → `en`). */
|
|
25
|
+
function baseLocale(code) {
|
|
26
|
+
return normalizeLocale(code).split('-')[0];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Resolution chain: [normalized, base, 'en'] with duplicates removed. */
|
|
30
|
+
function fallbackChain(code) {
|
|
31
|
+
const norm = normalizeLocale(code);
|
|
32
|
+
const chain = [];
|
|
33
|
+
for (const c of [norm, baseLocale(norm), DEFAULT_LOCALE]) {
|
|
34
|
+
if (c && !chain.includes(c)) chain.push(c);
|
|
35
|
+
}
|
|
36
|
+
return chain.length ? chain : [DEFAULT_LOCALE];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the active locale. Precedence: explicit config.locale > env.LANG/LC_ALL > 'en'.
|
|
41
|
+
* @param {{env?: object, configLocale?: string}} [opts]
|
|
42
|
+
*/
|
|
43
|
+
function resolveLocale(opts) {
|
|
44
|
+
const o = opts || {};
|
|
45
|
+
const fromConfig = normalizeLocale(o.configLocale);
|
|
46
|
+
if (fromConfig) return fromConfig;
|
|
47
|
+
const env = o.env || {};
|
|
48
|
+
const fromEnv = normalizeLocale(env.LC_ALL || env.LC_MESSAGES || env.LANG || env.LANGUAGE);
|
|
49
|
+
if (fromEnv && fromEnv !== 'c' && fromEnv !== 'posix') return fromEnv;
|
|
50
|
+
return DEFAULT_LOCALE;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Translate `key` for `locale` against the supplied `tables` map ({ <locale>: {<key>: <string>} }).
|
|
55
|
+
* Walks the fallback chain; returns the first hit, else the key itself (so a missing key is visible,
|
|
56
|
+
* never throws).
|
|
57
|
+
*/
|
|
58
|
+
function translate(tables, key, locale) {
|
|
59
|
+
const map = tables || {};
|
|
60
|
+
for (const loc of fallbackChain(locale)) {
|
|
61
|
+
const t = map[loc];
|
|
62
|
+
if (t && Object.prototype.hasOwnProperty.call(t, key) && typeof t[key] === 'string') return t[key];
|
|
63
|
+
}
|
|
64
|
+
return key;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve a skill/agent description for `locale`: frontmatter.description_i18n[<chain>] || .description.
|
|
69
|
+
* Opt-in + backward-compatible — absent description_i18n falls straight back to the English description.
|
|
70
|
+
*/
|
|
71
|
+
function descriptionFor(frontmatter, locale) {
|
|
72
|
+
const fm = frontmatter || {};
|
|
73
|
+
const i18n = fm.description_i18n;
|
|
74
|
+
if (i18n && typeof i18n === 'object') {
|
|
75
|
+
for (const loc of fallbackChain(locale)) {
|
|
76
|
+
if (typeof i18n[loc] === 'string' && i18n[loc].trim()) return i18n[loc];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return fm.description || '';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Read a locale's flat-JSON table from disk. Returns {} on missing/parse error (fail-safe → fallback). */
|
|
83
|
+
function loadTable(locale, dir) {
|
|
84
|
+
const file = path.join(dir || MESSAGES_DIR, `${normalizeLocale(locale) || DEFAULT_LOCALE}.json`);
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
87
|
+
} catch {
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
KNOWN_LOCALES, DEFAULT_LOCALE, MESSAGES_DIR,
|
|
94
|
+
normalizeLocale, baseLocale, fallbackChain, resolveLocale, translate, descriptionFor, loadTable,
|
|
95
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": { "locale": "en", "coverage": "complete", "fallback": null },
|
|
3
|
+
"help.usage": "Usage: gdd <command> [options]",
|
|
4
|
+
"help.tagline": "A design-quality pipeline for AI coding agents.",
|
|
5
|
+
"help.commands_header": "Commands",
|
|
6
|
+
"help.options_header": "Options",
|
|
7
|
+
"help.more": "Run `gdd help <command>` for details on a command.",
|
|
8
|
+
"help.pipeline": "Pipeline: brief -> explore -> plan -> design -> verify",
|
|
9
|
+
"error.no_state": "No .design/STATE.md found. Run /gdd:start to initialize a cycle.",
|
|
10
|
+
"error.no_config": "No .design/config.json found; using defaults.",
|
|
11
|
+
"error.stage_locked": "This stage is locked under the current cycle mode.",
|
|
12
|
+
"error.permission_denied": "Permission denied: your role cannot perform this action on this section.",
|
|
13
|
+
"error.budget_cap": "Budget cap reached. Raise the cap in .design/budget.json or wait for the next task.",
|
|
14
|
+
"error.connection_unavailable": "Connection unavailable. Configure it via /gdd:connections.",
|
|
15
|
+
"error.invalid_locale": "Unknown locale. Known locales: en, ru, uk, de, fr, zh, ja.",
|
|
16
|
+
"error.merge_conflict": "STATE.md has a merge conflict. Run the conflict-resolver to reconcile per section.",
|
|
17
|
+
"error.decision_locked": "This decision is locked. Use /gdd:unlock-decision <id> --approver <who> to reopen it.",
|
|
18
|
+
"error.no_telemetry": "No cost telemetry yet. Run a cycle first.",
|
|
19
|
+
"prompt.brief_header": "Stage 1 of 5 - Brief: capture the problem, audience, constraints, and metrics.",
|
|
20
|
+
"prompt.explore_header": "Stage 2 of 5 - Explore: inventory the design surface and interview for intent.",
|
|
21
|
+
"prompt.plan_header": "Stage 3 of 5 - Plan: decompose the work into tasks.",
|
|
22
|
+
"prompt.design_header": "Stage 4 of 5 - Design: execute the plan.",
|
|
23
|
+
"prompt.verify_header": "Stage 5 of 5 - Verify: score and audit against the design system.",
|
|
24
|
+
"status.complete": "Complete.",
|
|
25
|
+
"status.in_progress": "In progress.",
|
|
26
|
+
"status.blocked": "Blocked."
|
|
27
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": { "locale": "fr", "coverage": "placeholder", "fallback": "en" },
|
|
3
|
+
"help.tagline": "Un pipeline de qualité de conception pour les agents de codage IA.",
|
|
4
|
+
"status.complete": "Terminé.",
|
|
5
|
+
"status.in_progress": "En cours.",
|
|
6
|
+
"status.blocked": "Bloqué."
|
|
7
|
+
}
|