@hegemonart/get-design-done 1.40.5 → 1.41.5

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.
Files changed (34) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +80 -0
  4. package/README.md +4 -0
  5. package/agents/design-auditor.md +5 -6
  6. package/bin/gdd-detect +20 -0
  7. package/package.json +5 -1
  8. package/reference/anti-patterns.md +26 -0
  9. package/scripts/lib/detect/cli.cjs +111 -0
  10. package/scripts/lib/detect/engine.cjs +83 -0
  11. package/scripts/lib/detect/rule-schema.json +31 -0
  12. package/scripts/lib/detect/rules/ban-01.cjs +33 -0
  13. package/scripts/lib/detect/rules/ban-02.cjs +33 -0
  14. package/scripts/lib/detect/rules/ban-03.cjs +33 -0
  15. package/scripts/lib/detect/rules/ban-05.cjs +33 -0
  16. package/scripts/lib/detect/rules/ban-06.cjs +33 -0
  17. package/scripts/lib/detect/rules/ban-07.cjs +33 -0
  18. package/scripts/lib/detect/rules/ban-08.cjs +33 -0
  19. package/scripts/lib/detect/rules/ban-09.cjs +33 -0
  20. package/scripts/lib/detect/rules/ban-11.cjs +33 -0
  21. package/scripts/lib/detect/rules/ban-12.cjs +33 -0
  22. package/scripts/lib/detect/rules/ban-13.cjs +33 -0
  23. package/scripts/lib/detect/rules/index.cjs +21 -0
  24. package/scripts/lib/manifest/README.md +46 -0
  25. package/scripts/lib/manifest/harnesses.cjs +3 -0
  26. package/scripts/lib/manifest/harnesses.json +91 -0
  27. package/scripts/lib/manifest/index.cjs +26 -0
  28. package/scripts/lib/manifest/loader.cjs +51 -0
  29. package/scripts/lib/manifest/prose-denylist.json +126 -0
  30. package/scripts/lib/manifest/schemas/harnesses.schema.json +38 -0
  31. package/scripts/lib/manifest/schemas/prose-denylist.schema.json +41 -0
  32. package/scripts/lib/manifest/schemas/skills.schema.json +33 -0
  33. package/scripts/lib/manifest/skills.json +255 -0
  34. package/skills/quality-gate/SKILL.md +1 -1
@@ -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,46 @@
1
+ # `scripts/lib/manifest/` — Cross-Phase Source-of-Truth Root
2
+
3
+ Phase 41.5. This directory is the **single source-of-truth root** for roadmap-wide cross-phase
4
+ metadata. Before it existed, Phase 42 / 44 / 45 / 47 each scoped its own "single source" in a different
5
+ corner (`scripts/lib/build/`, `scripts/lib/prose/`, `reference/harness-matrix.json`,
6
+ `scripts/skill-metadata.json`) — four formats, four schemas, four CI drift gates. This is the one root,
7
+ one schema directory, one validator.
8
+
9
+ ## Files
10
+
11
+ | File | Owner / writer | Read by |
12
+ |---|---|---|
13
+ | `harnesses.json` (+ `harnesses.cjs` view) | Phase 41.5 seed → **42** (build config) + **45** (capability matrix) extend it | the build pipeline, the harness matrix, the multi-harness compiler |
14
+ | `skills.json` | Phase 41.5 seed (live `skills/` names) → **47** enriches (aliases, pin, description budget) | skill-UX tooling |
15
+ | `prose-denylist.json` | Phase 41.5 seed → **43** + **44** | `scripts/lint-prose.cjs` (the editorial gate) |
16
+ | `schemas/*.schema.json` | one JSON Schema per manifest file | `scripts/validate-manifest.cjs` |
17
+ | `loader.cjs` | Phase 41.5 | every consumer |
18
+ | `index.cjs` | Phase 41.5 | every consumer (`readHarnesses` / `readSkills` / `readProseDenylist`) |
19
+
20
+ ## Contract
21
+
22
+ - **One canonical record, multiple views.** `harnesses.json` is the canonical harness record; Phase 42's
23
+ build config and Phase 45's capability matrix are *views* of it, not separate files. Consumers extend
24
+ the records with new fields (every schema sets `additionalProperties: true`) — they do not fork the file.
25
+ - **Read through `index.cjs`.** Never `require` a manifest JSON directly. `readHarnesses()` /
26
+ `readSkills()` / `readProseDenylist()` return a well-shaped object even when the file is absent.
27
+ - **Graceful out-of-order shipping.** `loader.cjs` returns an empty manifest + a one-line warning when a
28
+ file is missing or unparseable — a phase that ships before its data exists never crashes. (D-03)
29
+ - **mtime cache.** A manifest is re-read only when its file mtime changes. (D-02)
30
+ - **One CI gate.** `scripts/validate-manifest.cjs` (`npm run validate:manifest`) ajv-validates every
31
+ manifest against its schema. This is the only drift gate — 43/44/45/47 do NOT add their own.
32
+
33
+ ## Migration note (for Phase 42 / 43 / 44 / 45 / 47 plan-phases)
34
+
35
+ When you plan your phase, **target this root from day one**. Do not create
36
+ `scripts/lib/build/harness-configs.cjs`, `scripts/lib/prose/denylist.json`,
37
+ `reference/harness-matrix.json`, or `scripts/skill-metadata.json` as new SoTs — extend
38
+ `manifest/harnesses.json` / `manifest/prose-denylist.json` / `manifest/skills.json` instead, read them
39
+ via `index.cjs`, and let `validate-manifest.cjs` be your drift gate. (Phase 46's SoT-consolidation
40
+ paragraph delegates here.)
41
+
42
+ ## Boundaries
43
+
44
+ This root holds **structured cross-phase data** (JSON + typed readers). It does not unify with the
45
+ `reference/*.md` prose registries (a different consumer pattern), and it does not auto-generate
46
+ frontmatter (Phase 46 territory).
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+ // Phase 41.5 — typed re-export of harnesses.json (the .cjs view per D-01).
3
+ module.exports = require('./harnesses.json').harnesses;
@@ -0,0 +1,91 @@
1
+ {
2
+ "schema_version": 1,
3
+ "generated_at": null,
4
+ "note": "Canonical cross-phase harness record (Phase 41.5 SoT root). Phase 42 adds build/compile config; Phase 45 adds the capability matrix — both as views of this one record. Model tiers live in reference/runtime-models.md.",
5
+ "harnesses": [
6
+ {
7
+ "id": "claude",
8
+ "name": "Claude Code",
9
+ "config_dir": ".claude",
10
+ "runtime_models_ref": "reference/runtime-models.md#claude"
11
+ },
12
+ {
13
+ "id": "codex",
14
+ "name": "OpenAI Codex CLI",
15
+ "config_dir": ".codex",
16
+ "runtime_models_ref": "reference/runtime-models.md#codex"
17
+ },
18
+ {
19
+ "id": "gemini",
20
+ "name": "Gemini CLI",
21
+ "config_dir": ".gemini",
22
+ "runtime_models_ref": "reference/runtime-models.md#gemini"
23
+ },
24
+ {
25
+ "id": "qwen",
26
+ "name": "Qwen Code",
27
+ "config_dir": ".qwen",
28
+ "runtime_models_ref": "reference/runtime-models.md#qwen"
29
+ },
30
+ {
31
+ "id": "kilo",
32
+ "name": "Kilo Code",
33
+ "config_dir": ".kilo",
34
+ "runtime_models_ref": "reference/runtime-models.md#kilo"
35
+ },
36
+ {
37
+ "id": "copilot",
38
+ "name": "GitHub Copilot CLI",
39
+ "config_dir": ".copilot",
40
+ "runtime_models_ref": "reference/runtime-models.md#copilot"
41
+ },
42
+ {
43
+ "id": "cursor",
44
+ "name": "Cursor",
45
+ "config_dir": ".cursor",
46
+ "runtime_models_ref": "reference/runtime-models.md#cursor"
47
+ },
48
+ {
49
+ "id": "windsurf",
50
+ "name": "Windsurf (Cascade)",
51
+ "config_dir": ".windsurf",
52
+ "runtime_models_ref": "reference/runtime-models.md#windsurf"
53
+ },
54
+ {
55
+ "id": "antigravity",
56
+ "name": "Antigravity",
57
+ "config_dir": ".antigravity",
58
+ "runtime_models_ref": "reference/runtime-models.md#antigravity"
59
+ },
60
+ {
61
+ "id": "augment",
62
+ "name": "Augment",
63
+ "config_dir": ".augment",
64
+ "runtime_models_ref": "reference/runtime-models.md#augment"
65
+ },
66
+ {
67
+ "id": "trae",
68
+ "name": "Trae",
69
+ "config_dir": ".trae",
70
+ "runtime_models_ref": "reference/runtime-models.md#trae"
71
+ },
72
+ {
73
+ "id": "codebuddy",
74
+ "name": "CodeBuddy",
75
+ "config_dir": ".codebuddy",
76
+ "runtime_models_ref": "reference/runtime-models.md#codebuddy"
77
+ },
78
+ {
79
+ "id": "cline",
80
+ "name": "Cline",
81
+ "config_dir": ".cline",
82
+ "runtime_models_ref": "reference/runtime-models.md#cline"
83
+ },
84
+ {
85
+ "id": "opencode",
86
+ "name": "OpenCode",
87
+ "config_dir": ".opencode",
88
+ "runtime_models_ref": "reference/runtime-models.md#opencode"
89
+ }
90
+ ]
91
+ }
@@ -0,0 +1,26 @@
1
+ 'use strict';
2
+ // Phase 41.5 — manifest/index.cjs — typed readers over the shared loader. Every cross-phase consumer
3
+ // imports from here: `const { readHarnesses } = require('scripts/lib/manifest')`. Each reader returns
4
+ // a well-shaped object even when the underlying file is absent (graceful empty fallback per loader D-03).
5
+
6
+ const loader = require('./loader.cjs');
7
+
8
+ /** @returns {{ schema_version: number, generated_at: string|null, harnesses: object[] }} */
9
+ function readHarnesses(opts) {
10
+ return loader.load('harnesses', { ...opts, fallback: { schema_version: 1, generated_at: null, harnesses: [] } });
11
+ }
12
+
13
+ /** @returns {{ schema_version: number, skills: object[] }} */
14
+ function readSkills(opts) {
15
+ return loader.load('skills', { ...opts, fallback: { schema_version: 1, skills: [] } });
16
+ }
17
+
18
+ /** @returns {{ schema_version: number, tells: object[] }} */
19
+ function readProseDenylist(opts) {
20
+ return loader.load('prose-denylist', { ...opts, fallback: { schema_version: 1, tells: [] } });
21
+ }
22
+
23
+ module.exports = {
24
+ readHarnesses, readSkills, readProseDenylist,
25
+ reset: loader.reset, MANIFEST_DIR: loader.MANIFEST_DIR,
26
+ };
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+ // Phase 41.5 — manifest/loader.cjs — the ONE shared reader for every cross-phase SoT manifest under
3
+ // scripts/lib/manifest/. Phases 42 (harnesses), 43/44 (prose denylist), 45 (capability matrix), and
4
+ // 47 (skill metadata) all read through here instead of hand-rolling their own loader + drift gate.
5
+ //
6
+ // Graceful (D-03): a missing or unparseable manifest returns the caller's `fallback` (an empty
7
+ // manifest) plus a one-line stderr warning — NEVER a throw — so a phase shipping before its data file
8
+ // exists does not crash. File-mtime cache (D-02): a file is re-read only when its mtime changes.
9
+ //
10
+ // Dep-free (no ajv here — validation lives in scripts/validate-manifest.cjs, the CI gate). No require
11
+ // of any third-party module.
12
+
13
+ const fs = require('node:fs');
14
+ const path = require('node:path');
15
+
16
+ const MANIFEST_DIR = __dirname;
17
+ const _cache = new Map(); // absPath -> { mtimeMs, data }
18
+
19
+ /** Clear the in-process cache (tests). */
20
+ function reset() { _cache.clear(); }
21
+
22
+ /**
23
+ * Load a manifest JSON by base name (no extension).
24
+ * @param {string} name e.g. 'harnesses' | 'skills' | 'prose-denylist'
25
+ * @param {{ dir?: string, fallback?: any, quiet?: boolean }} [opts]
26
+ * @returns the parsed manifest, or `fallback` (default {}) on missing/parse-error.
27
+ */
28
+ function load(name, opts) {
29
+ const o = opts || {};
30
+ const dir = o.dir || MANIFEST_DIR;
31
+ const fallback = Object.prototype.hasOwnProperty.call(o, 'fallback') ? o.fallback : {};
32
+ const abs = path.join(dir, `${name}.json`);
33
+
34
+ let stat;
35
+ try { stat = fs.statSync(abs); } catch {
36
+ if (!o.quiet) process.stderr.write(`manifest: ${name}.json not found — using empty fallback (a consumer phase may not have shipped its data yet)\n`);
37
+ return fallback;
38
+ }
39
+ const cached = _cache.get(abs);
40
+ if (cached && cached.mtimeMs === stat.mtimeMs) return cached.data;
41
+ try {
42
+ const data = JSON.parse(fs.readFileSync(abs, 'utf8'));
43
+ _cache.set(abs, { mtimeMs: stat.mtimeMs, data });
44
+ return data;
45
+ } catch (e) {
46
+ if (!o.quiet) process.stderr.write(`manifest: ${name}.json parse error (${e.message}) — using empty fallback\n`);
47
+ return fallback;
48
+ }
49
+ }
50
+
51
+ module.exports = { load, reset, MANIFEST_DIR };
@@ -0,0 +1,126 @@
1
+ {
2
+ "schema_version": 1,
3
+ "note": "AI-tell denylist SoT (Phase 41.5 seed). Phase 43 (lint-prose.cjs) + Phase 44 read this. Case-insensitive phrase match in the project's OWN user-facing prose only.",
4
+ "tells": [
5
+ {
6
+ "pattern": "load-bearing",
7
+ "kind": "phrase",
8
+ "note": "AI-prose tell (training-set monoculture)."
9
+ },
10
+ {
11
+ "pattern": "highest-leverage",
12
+ "kind": "phrase",
13
+ "note": "AI-prose tell (training-set monoculture)."
14
+ },
15
+ {
16
+ "pattern": "delve",
17
+ "kind": "phrase",
18
+ "note": "AI-prose tell (training-set monoculture)."
19
+ },
20
+ {
21
+ "pattern": "delves",
22
+ "kind": "phrase",
23
+ "note": "AI-prose tell (training-set monoculture)."
24
+ },
25
+ {
26
+ "pattern": "seamless",
27
+ "kind": "phrase",
28
+ "note": "AI-prose tell (training-set monoculture)."
29
+ },
30
+ {
31
+ "pattern": "seamlessly",
32
+ "kind": "phrase",
33
+ "note": "AI-prose tell (training-set monoculture)."
34
+ },
35
+ {
36
+ "pattern": "robust",
37
+ "kind": "phrase",
38
+ "note": "AI-prose tell (training-set monoculture)."
39
+ },
40
+ {
41
+ "pattern": "elevate",
42
+ "kind": "phrase",
43
+ "note": "AI-prose tell (training-set monoculture)."
44
+ },
45
+ {
46
+ "pattern": "empower",
47
+ "kind": "phrase",
48
+ "note": "AI-prose tell (training-set monoculture)."
49
+ },
50
+ {
51
+ "pattern": "underscore",
52
+ "kind": "phrase",
53
+ "note": "AI-prose tell (training-set monoculture)."
54
+ },
55
+ {
56
+ "pattern": "underscores",
57
+ "kind": "phrase",
58
+ "note": "AI-prose tell (training-set monoculture)."
59
+ },
60
+ {
61
+ "pattern": "in today's",
62
+ "kind": "phrase",
63
+ "note": "AI-prose tell (training-set monoculture)."
64
+ },
65
+ {
66
+ "pattern": "let's dive in",
67
+ "kind": "phrase",
68
+ "note": "AI-prose tell (training-set monoculture)."
69
+ },
70
+ {
71
+ "pattern": "moreover",
72
+ "kind": "phrase",
73
+ "note": "AI-prose tell (training-set monoculture)."
74
+ },
75
+ {
76
+ "pattern": "furthermore",
77
+ "kind": "phrase",
78
+ "note": "AI-prose tell (training-set monoculture)."
79
+ },
80
+ {
81
+ "pattern": "tapestry",
82
+ "kind": "phrase",
83
+ "note": "AI-prose tell (training-set monoculture)."
84
+ },
85
+ {
86
+ "pattern": "a testament to",
87
+ "kind": "phrase",
88
+ "note": "AI-prose tell (training-set monoculture)."
89
+ },
90
+ {
91
+ "pattern": "navigating the",
92
+ "kind": "phrase",
93
+ "note": "AI-prose tell (training-set monoculture)."
94
+ },
95
+ {
96
+ "pattern": "in the realm of",
97
+ "kind": "phrase",
98
+ "note": "AI-prose tell (training-set monoculture)."
99
+ },
100
+ {
101
+ "pattern": "unlock the power",
102
+ "kind": "phrase",
103
+ "note": "AI-prose tell (training-set monoculture)."
104
+ },
105
+ {
106
+ "pattern": "game-changer",
107
+ "kind": "phrase",
108
+ "note": "AI-prose tell (training-set monoculture)."
109
+ },
110
+ {
111
+ "pattern": "leverage",
112
+ "kind": "phrase",
113
+ "note": "AI-prose tell (training-set monoculture)."
114
+ },
115
+ {
116
+ "pattern": "\\u2014",
117
+ "kind": "token",
118
+ "note": "Em dash — banned in the project's own user-facing prose (Phase 43)."
119
+ },
120
+ {
121
+ "pattern": "--",
122
+ "kind": "token",
123
+ "note": "Double hyphen (often an em-dash surrogate)."
124
+ }
125
+ ]
126
+ }