start-vibing 4.3.1 → 4.3.2
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/package.json +2 -2
- package/template/.claude/skills/super-design/SKILL.md +88 -2
- package/template/.claude/skills/super-design/audit-state.schema.json +226 -0
- package/template/.claude/skills/super-design/scripts/build-import-graph.sh +208 -0
- package/template/.claude/skills/super-design/scripts/detect-apps.sh +180 -0
- package/template/.claude/skills/super-design/scripts/detect-changes.sh +73 -12
- package/template/.claude/skills/super-design/scripts/discover-routes.sh +120 -13
- package/template/.claude/skills/super-design/scripts/extract-tokens.mjs +153 -9
- package/template/.claude/skills/super-design/scripts/hash-pages.sh +208 -28
- package/template/.claude/skills/super-design/scripts/validate-state.sh +46 -15
- package/template/.claude/skills/super-design/scripts/verify-audit.sh +62 -9
- package/template/.claude/skills/super-design/scripts/visual-regression.sh +275 -0
- package/template/.claude/skills/super-design/scripts/write-state.sh +29 -2
- package/template/.claude/skills/super-design/templates/audit-state.schema.json +0 -57
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "start-vibing",
|
|
3
|
-
"version": "4.3.
|
|
4
|
-
"description": "Setup Claude Code with 9 plugins, 6 community skills, and 8 MCP servers. Parallel install, auto-accept, superpowers + ralph-loop. super-design 0.6.
|
|
3
|
+
"version": "4.3.2",
|
|
4
|
+
"description": "Setup Claude Code with 9 plugins, 6 community skills, and 8 MCP servers. Parallel install, auto-accept, superpowers + ralph-loop. super-design 0.6.2: canonical audit-state.schema.json + verify --strict, per-viewport {sha256,phash,png_path} hashes with sharp/fpr fallback + MASK_SELECTORS, visual-regression.sh (pixelmatch→odiff→sha256-fallback), DTCG *.tokens.json + Tokens Studio aliases, @fixture-<id> dynamic routes + madge import-graph N=3, per-app monorepo state (pnpm/npm/yarn/Bun/Nx/Turbo).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"start-vibing": "./dist/cli.js"
|
|
@@ -8,7 +8,7 @@ description: >
|
|
|
8
8
|
UX audit (WCAG 2.2 AA, Nielsen heuristics, Baymard, CWV), and synthesized
|
|
9
9
|
overview. Re-audits only what changed since last run. On explicit user request,
|
|
10
10
|
applies surgical fixes with full rollback.
|
|
11
|
-
version: 0.6.
|
|
11
|
+
version: 0.6.2
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# super-design
|
|
@@ -105,10 +105,96 @@ Do NOT paste overview into chat.
|
|
|
105
105
|
- `--refresh-research` — rerun sd-research
|
|
106
106
|
- `--only <cat>` — a11y | design | ux | perf | research
|
|
107
107
|
- `--scope <url>` — specific route
|
|
108
|
+
- `--app <name>` — scope the entire run to one monorepo app (matches a
|
|
109
|
+
`name` entry from `scripts/detect-apps.sh`). Required when `--scope <url>`
|
|
110
|
+
is ambiguous between multiple apps.
|
|
108
111
|
- `--fix` — run sd-fix after audit
|
|
109
112
|
- `--dry-run` — artifacts without committing state
|
|
110
113
|
- `--ci` — non-interactive, create PR, exit non-zero on blockers
|
|
111
|
-
- `--update-baselines` — Re-hash pages and tokens without re-auditing (use after accepted cosmetic drift)
|
|
114
|
+
- `--update-baselines` — Re-hash pages and tokens without re-auditing (use after accepted cosmetic drift). Also accepted by `scripts/visual-regression.sh` to overwrite `.super-design/baselines/*.png` with the current capture.
|
|
115
|
+
- `--visual-regression` — Run `scripts/visual-regression.sh` after hashing. Reads the `visual_regression` block from `.audit-state.json` (engine: pixelmatch | odiff | sha256-fallback; threshold 0.1; max_diff_pixel_ratio 0.01). See artifact §16.
|
|
116
|
+
- `MASK_SELECTORS=<sel,sel,...>` (env) — Extra CSS selectors masked in every screenshot captured by `scripts/hash-pages.sh`. Artifact §3.4 defaults (`[data-timestamp], .relative-time, [data-react-hydration], video, canvas`) are always applied.
|
|
117
|
+
|
|
118
|
+
## Monorepo support
|
|
119
|
+
|
|
120
|
+
Audit state is per-app (artifact §11 line 902) so independent deploys
|
|
121
|
+
carry independent freshness, `git_sha_at_audit`, and tool results. Layout
|
|
122
|
+
is auto-detected; nothing else to configure.
|
|
123
|
+
|
|
124
|
+
### Detection
|
|
125
|
+
|
|
126
|
+
`scripts/detect-apps.sh` reads the first workspace manifest it finds:
|
|
127
|
+
|
|
128
|
+
| Manifest | Source of globs |
|
|
129
|
+
|----------|-----------------|
|
|
130
|
+
| `pnpm-workspace.yaml` | `packages:` list |
|
|
131
|
+
| `package.json` | `workspaces: [...]` or `workspaces.packages: [...]` (npm, yarn, Bun) |
|
|
132
|
+
| `turbo.json` | Presence → uses pnpm/npm/yarn workspaces; falls back to `apps/*` + `packages/*` if none |
|
|
133
|
+
| `nx.json` | `workspaceLayout.appsDir` / `libsDir` (default `apps/*`, `libs/*`) |
|
|
134
|
+
| `bunfig.toml` | Presence → falls back to `apps/*` + `packages/*` if package.json has no workspaces |
|
|
135
|
+
|
|
136
|
+
Each matched directory that also has a `package.json` becomes an app
|
|
137
|
+
with `name` taken from `package.json#name` (scope stripped), `path` the
|
|
138
|
+
directory, and `state_path` = `<path>/docs/super-design/.audit-state.json`.
|
|
139
|
+
If nothing matches, `detect-apps.sh` emits a `single` layout with
|
|
140
|
+
`path: "."` and the repo-root state path — preserving existing single-app
|
|
141
|
+
behavior.
|
|
142
|
+
|
|
143
|
+
### Per-app pipeline
|
|
144
|
+
|
|
145
|
+
- **Preflight**: per app, read `<app>/docs/super-design/.audit-state.json`
|
|
146
|
+
via `validate-state.sh <app_path>`.
|
|
147
|
+
- **Change detection**: `scripts/detect-changes.sh --all-apps` loops over
|
|
148
|
+
every app and narrows `git diff` to `-- <app_path>/` so each app's
|
|
149
|
+
scope decision sees only its own files. Single-app shape is preserved
|
|
150
|
+
with `detect-changes.sh <last_sha>`.
|
|
151
|
+
- **Write state**: `scripts/write-state.sh <app_path>` derives the target
|
|
152
|
+
path; for single-app repos pass `.` or omit.
|
|
153
|
+
|
|
154
|
+
### URL → app disambiguation
|
|
155
|
+
|
|
156
|
+
`--scope <url>` still targets one URL. When the URL maps cleanly to a
|
|
157
|
+
single app (e.g. `apps/admin` serves `https://admin.example.com`), the
|
|
158
|
+
pipeline picks that app automatically. When mapping is ambiguous
|
|
159
|
+
(multiple apps serve overlapping hostnames, or URL patterns cross apps),
|
|
160
|
+
the user MUST pass `--app <name>` — otherwise the skill aborts with a
|
|
161
|
+
`{"error":"ambiguous-app","candidates":[...]}` verdict instead of
|
|
162
|
+
guessing.
|
|
163
|
+
|
|
164
|
+
## Scripts
|
|
165
|
+
|
|
166
|
+
Reusable shell helpers under `scripts/`. All POSIX/bash, tested on
|
|
167
|
+
Windows git-bash + Linux.
|
|
168
|
+
|
|
169
|
+
- `discover-routes.sh` — emits `route_map` as a JSON array. Dynamic
|
|
170
|
+
segments (`[slug]`, `[[...all]]`, `$id`, `:uid`) are suffixed with
|
|
171
|
+
`@fixture-<id>` (artifact §2.7). Fixtures resolved from sibling
|
|
172
|
+
`*.fixture.json`, `fixtures/<name>.json`, or `$SUPER_DESIGN_FIXTURES`
|
|
173
|
+
env JSON; falls back to `@fixture-default` with a warning. Consumers
|
|
174
|
+
(hash-pages, sd-audit) MUST strip the suffix before navigating.
|
|
175
|
+
- `build-import-graph.sh` — builds `.super-design/import-graph.json`
|
|
176
|
+
(`{nodes, edges, hash, backend}`) and persists `import_graph_sha` to
|
|
177
|
+
state. Prefers `npx madge --json <roots>`; falls back to a regex
|
|
178
|
+
scanner (JS/TS only, no alias resolution) if madge is missing.
|
|
179
|
+
- Query: `bash .../build-import-graph.sh importers <file> --hops 3`
|
|
180
|
+
→ BFS over reversed edges; `detect-changes.sh` uses this to close
|
|
181
|
+
the component→pages gap when only components changed (Step 2 scope
|
|
182
|
+
decision: "Only components changed → re-audit pages importing them
|
|
183
|
+
(N=3 hops via madge)").
|
|
184
|
+
- `hash-pages.sh` — captures 3 viewports per URL (mobile_375, tablet_768,
|
|
185
|
+
desktop_1280), emits `{html_hash, dom_structure_hash, viewport_hashes:
|
|
186
|
+
{<vp>: {sha256, phash, png_path}}}` per page to
|
|
187
|
+
`docs/super-design/.cache/hashes/hashes.json` and persists each PNG to
|
|
188
|
+
`<cache>/screenshots/<url-enc>/<vp>.png`. Applies artifact §3.4 mask
|
|
189
|
+
defaults plus `MASK_SELECTORS`; `phash` uses `sharp` when available
|
|
190
|
+
(tagged `phash:`) or a deterministic PNG fingerprint otherwise
|
|
191
|
+
(tagged `fpr:`, only useful for exact-match comparison).
|
|
192
|
+
- `visual-regression.sh [--update-baselines] [<state>]` — reads the
|
|
193
|
+
`visual_regression` block from `.audit-state.json` and diffs current
|
|
194
|
+
screenshots against `.super-design/baselines/`. Engine chain:
|
|
195
|
+
`pixelmatch` → `odiff` → `sha256-fallback`. Emits
|
|
196
|
+
`{page, viewport, diff_ratio, threshold, pass, diff_image_path}` to
|
|
197
|
+
`<diff_dir>/results.json`. Exits non-zero if any page fails.
|
|
112
198
|
|
|
113
199
|
## References (Read on demand)
|
|
114
200
|
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://hakutaku.ai/schemas/super-design/audit-state.schema.json",
|
|
4
|
+
"title": "super-design audit state",
|
|
5
|
+
"description": "Canonical schema for docs/super-design/.audit-state.json — the state file that super-design reads on every run to decide what to re-audit. Derived from docs/compass_artifact_wf-f52f98c2-73c9-4483-b49c-6b080ef7dc92_text_markdown.md sections 5, 10, and 16.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": true,
|
|
8
|
+
"required": [
|
|
9
|
+
"schema_version",
|
|
10
|
+
"skill_version",
|
|
11
|
+
"last_audit_at",
|
|
12
|
+
"git_sha_at_audit"
|
|
13
|
+
],
|
|
14
|
+
"properties": {
|
|
15
|
+
"schema_version": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "Semver string for this state file's schema. Major bumps force a full re-audit (see §12 line 934, §10).",
|
|
18
|
+
"pattern": "^\\d+\\.\\d+\\.\\d+(?:[-+].+)?$"
|
|
19
|
+
},
|
|
20
|
+
"skill_version": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Semver of the super-design skill that wrote this state.",
|
|
23
|
+
"pattern": "^\\d+\\.\\d+\\.\\d+(?:[-+].+)?$"
|
|
24
|
+
},
|
|
25
|
+
"last_audit_at": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "ISO-8601 timestamp of the last completed audit (used for freshness cascade: 90d soft, 180d hard).",
|
|
28
|
+
"format": "date-time"
|
|
29
|
+
},
|
|
30
|
+
"git_sha_at_audit": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Git SHA (7–64 hex chars) that HEAD pointed to at audit time. Used as the range-start for next audit's git log (§2.1).",
|
|
33
|
+
"pattern": "^[0-9a-f]{7,64}$"
|
|
34
|
+
},
|
|
35
|
+
"git_branch": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "Branch name at audit time (informational)."
|
|
38
|
+
},
|
|
39
|
+
"is_shallow_clone": {
|
|
40
|
+
"type": "boolean",
|
|
41
|
+
"description": "True if the repo was a shallow clone at audit time. Signals that __MISSING__ fallback (§6) may need git fetch --unshallow."
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
"theory_doc_sha": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"description": "sha256: of references/design-theory.md. Mismatch forces full re-audit (§9.1).",
|
|
47
|
+
"pattern": "^sha256:[0-9a-f]{4,64}$"
|
|
48
|
+
},
|
|
49
|
+
"market_analysis_sha": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "sha256: of the current market-analysis.md output.",
|
|
52
|
+
"pattern": "^sha256:[0-9a-f]{4,64}$"
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
"tools": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"description": "Versions of the audit toolchain at audit time. Major bumps can invalidate prior findings.",
|
|
58
|
+
"additionalProperties": true,
|
|
59
|
+
"properties": {
|
|
60
|
+
"axe-core": { "type": "string" },
|
|
61
|
+
"lighthouse": { "type": "string" },
|
|
62
|
+
"playwright": { "type": "string" },
|
|
63
|
+
"playwright-mcp": { "type": "string" },
|
|
64
|
+
"pa11y": { "type": "string" }
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
"framework": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"description": "Detected framework info. Router/version migration triggers full re-audit (§7).",
|
|
71
|
+
"additionalProperties": true,
|
|
72
|
+
"properties": {
|
|
73
|
+
"name": { "type": "string" },
|
|
74
|
+
"router": { "type": "string" },
|
|
75
|
+
"version": { "type": "string" }
|
|
76
|
+
},
|
|
77
|
+
"required": ["name"]
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
"route_map": {
|
|
81
|
+
"type": "array",
|
|
82
|
+
"description": "Canonical route list at audit time. Diff against prior route_map produces new/deleted routes (§7).",
|
|
83
|
+
"items": { "type": "string" }
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
"pages_audited": {
|
|
87
|
+
"type": "array",
|
|
88
|
+
"description": "Per-page hashes + findings (§3, §5). Incremental audits compare these to decide which pages to re-visit.",
|
|
89
|
+
"items": {
|
|
90
|
+
"type": "object",
|
|
91
|
+
"additionalProperties": true,
|
|
92
|
+
"required": ["url"],
|
|
93
|
+
"properties": {
|
|
94
|
+
"url": { "type": "string" },
|
|
95
|
+
"route_file": { "type": "string" },
|
|
96
|
+
"html_hash": { "type": "string", "pattern": "^sha256:[0-9a-f]{4,64}$" },
|
|
97
|
+
"dom_structure_hash": { "type": "string", "pattern": "^sha256:[0-9a-f]{4,64}$" },
|
|
98
|
+
"screenshot_hash": { "type": "string", "pattern": "^(sha256|phash):[0-9a-f]{4,64}$" },
|
|
99
|
+
"viewport_hashes": {
|
|
100
|
+
"type": "object",
|
|
101
|
+
"description": "Per-viewport hashes (mobile_375/tablet_768/desktop_1280). Each entry may be a bare phash string (back-compat with artifact §10 line 492) or the extended {sha256,phash,png_path} object emitted by hash-pages.sh. Any viewport drift -> re-audit that page.",
|
|
102
|
+
"additionalProperties": {
|
|
103
|
+
"oneOf": [
|
|
104
|
+
{ "type": "string" },
|
|
105
|
+
{
|
|
106
|
+
"type": "object",
|
|
107
|
+
"properties": {
|
|
108
|
+
"sha256": { "type": "string" },
|
|
109
|
+
"phash": { "type": "string" },
|
|
110
|
+
"png_path": { "type": "string" }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"mask_selectors": {
|
|
117
|
+
"type": "array",
|
|
118
|
+
"description": "CSS selectors masked in this page's screenshots (artifact §3.4). Merged from hash-pages.sh defaults + MASK_SELECTORS env.",
|
|
119
|
+
"items": { "type": "string" }
|
|
120
|
+
},
|
|
121
|
+
"phash_engine": {
|
|
122
|
+
"type": "string",
|
|
123
|
+
"description": "Which engine produced the phash values. `phash:` = sharp-based aHash. `fpr:` = zero-dep PNG fingerprint fallback (exact-match only)."
|
|
124
|
+
},
|
|
125
|
+
"last_audited": { "type": "string", "format": "date-time" },
|
|
126
|
+
"findings_ids": {
|
|
127
|
+
"type": "array",
|
|
128
|
+
"items": { "type": "string" }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
"components": {
|
|
135
|
+
"type": "object",
|
|
136
|
+
"description": "Map of component path -> xxh3 hash (§8). Source of truth for component-level change detection.",
|
|
137
|
+
"additionalProperties": { "type": "string" }
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
"token_hash": {
|
|
141
|
+
"type": "string",
|
|
142
|
+
"description": "sha256: hash of the canonicalized design-token map. Mismatch invalidates every page (§9).",
|
|
143
|
+
"pattern": "^sha256:[0-9a-f]{4,64}$"
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
"import_graph_sha": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"description": "sha256: of the serialized import graph (madge). Used to compute blast radius of component changes.",
|
|
149
|
+
"pattern": "^sha256:[0-9a-f]{4,64}$"
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
"findings_counts": {
|
|
153
|
+
"type": "object",
|
|
154
|
+
"description": "Aggregate tally after last audit (§5).",
|
|
155
|
+
"additionalProperties": false,
|
|
156
|
+
"properties": {
|
|
157
|
+
"blockers": { "type": "integer", "minimum": 0 },
|
|
158
|
+
"high": { "type": "integer", "minimum": 0 },
|
|
159
|
+
"medium": { "type": "integer", "minimum": 0 },
|
|
160
|
+
"nitpicks": { "type": "integer", "minimum": 0 }
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
"research_at": {
|
|
165
|
+
"type": "string",
|
|
166
|
+
"description": "ISO-8601 timestamp of the last sd-research run. Used to decide whether to rerun research (>90d -> refresh).",
|
|
167
|
+
"format": "date-time"
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
"ignored_paths": {
|
|
171
|
+
"type": "array",
|
|
172
|
+
"description": "Globs excluded from design-relevance classification (§2.3).",
|
|
173
|
+
"items": { "type": "string" }
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
"visual_regression": {
|
|
177
|
+
"type": "object",
|
|
178
|
+
"description": "Optional visual-regression config (§16). Absent when user hasn't opted in. Consumed by scripts/visual-regression.sh.",
|
|
179
|
+
"additionalProperties": true,
|
|
180
|
+
"properties": {
|
|
181
|
+
"enabled": { "type": "boolean", "default": false },
|
|
182
|
+
"engine": {
|
|
183
|
+
"type": "string",
|
|
184
|
+
"description": "Engine chain: scripts/visual-regression.sh tries in order and falls back. `sha256-fallback` = exact-match only, always available.",
|
|
185
|
+
"enum": ["pixelmatch", "odiff", "resemble", "looks-same", "playwright", "sha256-fallback"],
|
|
186
|
+
"default": "pixelmatch"
|
|
187
|
+
},
|
|
188
|
+
"threshold": { "type": "number", "minimum": 0, "default": 0.1 },
|
|
189
|
+
"max_diff_pixel_ratio": { "type": "number", "minimum": 0, "maximum": 1, "default": 0.01 },
|
|
190
|
+
"antialiasing": { "type": "boolean", "default": true },
|
|
191
|
+
"viewports": {
|
|
192
|
+
"type": "array",
|
|
193
|
+
"items": {
|
|
194
|
+
"type": "object",
|
|
195
|
+
"required": ["label", "width", "height"],
|
|
196
|
+
"properties": {
|
|
197
|
+
"label": { "type": "string" },
|
|
198
|
+
"width": { "type": "integer", "minimum": 1 },
|
|
199
|
+
"height": { "type": "integer", "minimum": 1 }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
"mask_selectors": {
|
|
204
|
+
"type": "array",
|
|
205
|
+
"items": { "type": "string" }
|
|
206
|
+
},
|
|
207
|
+
"baseline_dir": {
|
|
208
|
+
"type": "string",
|
|
209
|
+
"description": "Where accepted baseline PNGs live. Committed to git (small repos) or stored via LFS/artifacts (large).",
|
|
210
|
+
"default": ".super-design/baselines"
|
|
211
|
+
},
|
|
212
|
+
"current_dir": {
|
|
213
|
+
"type": "string",
|
|
214
|
+
"description": "Where hash-pages.sh writes the current run's PNGs. Usually gitignored.",
|
|
215
|
+
"default": "docs/super-design/.cache/hashes/screenshots"
|
|
216
|
+
},
|
|
217
|
+
"diff_dir": {
|
|
218
|
+
"type": "string",
|
|
219
|
+
"description": "Where diff images + results.json are emitted by visual-regression.sh.",
|
|
220
|
+
"default": "docs/super-design/.cache/hashes/diffs"
|
|
221
|
+
},
|
|
222
|
+
"docker_image": { "type": "string" }
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# build-import-graph.sh — build a JS/TS import graph for the repo so
|
|
3
|
+
# super-design can propagate component-level changes to pages within N
|
|
4
|
+
# hops (artifact §8, line 666: default N=3).
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# build-import-graph.sh [--state <path>] # full build
|
|
8
|
+
# build-import-graph.sh importers <file> [--hops N] # BFS query
|
|
9
|
+
#
|
|
10
|
+
# Output (full build): .super-design/import-graph.json with shape
|
|
11
|
+
# {
|
|
12
|
+
# "nodes": ["src/app/page.tsx", ...],
|
|
13
|
+
# "edges": [{"from":"src/app/page.tsx","to":"src/components/Nav.tsx"}, ...],
|
|
14
|
+
# "hash": "sha256:<hex>",
|
|
15
|
+
# "backend": "madge" | "regex-fallback",
|
|
16
|
+
# "built_at":"<ISO-8601>"
|
|
17
|
+
# }
|
|
18
|
+
#
|
|
19
|
+
# State: writes `import_graph_sha` back into .audit-state.json via
|
|
20
|
+
# scripts/write-state.sh. The sha lives in audit-state.schema.json so
|
|
21
|
+
# detect-changes.sh can short-circuit re-propagation if the graph is
|
|
22
|
+
# unchanged.
|
|
23
|
+
#
|
|
24
|
+
# Backend choice:
|
|
25
|
+
# 1. If `npx madge --version` works, use `npx madge --json <roots>`
|
|
26
|
+
# (artifact §8: madge reads .madgerc / package.json#madge).
|
|
27
|
+
# 2. Else fall back to a zero-dep regex scanner (JS/TS/JSX/TSX only —
|
|
28
|
+
# no CSS/Vue/Svelte detectives, no alias resolution). Logs a
|
|
29
|
+
# warning so the caller knows propagation is best-effort.
|
|
30
|
+
set -euo pipefail
|
|
31
|
+
|
|
32
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
33
|
+
STATE_PATH="docs/super-design/.audit-state.json"
|
|
34
|
+
OUT_DIR=".super-design"
|
|
35
|
+
OUT_FILE="$OUT_DIR/import-graph.json"
|
|
36
|
+
|
|
37
|
+
log() { printf '[build-import-graph] %s\n' "$*" >&2; }
|
|
38
|
+
|
|
39
|
+
# Source roots — read from state, else default.
|
|
40
|
+
resolve_roots() {
|
|
41
|
+
local roots=""
|
|
42
|
+
if [[ -f "$STATE_PATH" ]]; then
|
|
43
|
+
roots="$(jq -r '.source_roots // empty | .[]?' "$STATE_PATH" 2>/dev/null || true)"
|
|
44
|
+
fi
|
|
45
|
+
if [[ -z "$roots" ]]; then
|
|
46
|
+
for d in src app pages; do [[ -d "$d" ]] && echo "$d"; done
|
|
47
|
+
else
|
|
48
|
+
printf '%s\n' "$roots"
|
|
49
|
+
fi
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# --- Full build -----------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
build_madge() {
|
|
55
|
+
# madge prints { "file": [deps], ... }. We flatten to edges.
|
|
56
|
+
local roots_json
|
|
57
|
+
roots_json="$(resolve_roots | jq -Rn '[inputs]')"
|
|
58
|
+
local roots
|
|
59
|
+
roots="$(printf '%s' "$roots_json" | jq -r '.[]' | tr '\n' ' ')"
|
|
60
|
+
[[ -z "$roots" ]] && { log "no source roots found"; echo '{}'; return; }
|
|
61
|
+
# shellcheck disable=SC2086
|
|
62
|
+
npx --yes madge --json $roots 2>/dev/null
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
build_regex_fallback() {
|
|
66
|
+
log "madge unavailable; using regex fallback (JS/TS only, no alias resolution)"
|
|
67
|
+
local roots
|
|
68
|
+
roots="$(resolve_roots | tr '\n' ' ')"
|
|
69
|
+
[[ -z "$roots" ]] && { echo '{}'; return; }
|
|
70
|
+
|
|
71
|
+
# Emit NDJSON: {"from":"<path>","to":"<dep>"} for each import line in
|
|
72
|
+
# each JS/TS file, then fold into {file: [deps]} via jq.
|
|
73
|
+
# shellcheck disable=SC2086
|
|
74
|
+
find $roots -type f \( \
|
|
75
|
+
-name '*.ts' -o -name '*.tsx' \
|
|
76
|
+
-o -name '*.js' -o -name '*.jsx' \
|
|
77
|
+
-o -name '*.mjs' -o -name '*.cjs' \) 2>/dev/null \
|
|
78
|
+
| while IFS= read -r f; do
|
|
79
|
+
# Grep import/require specifiers. Handles:
|
|
80
|
+
# import ... from 'x'
|
|
81
|
+
# import 'x'
|
|
82
|
+
# require('x')
|
|
83
|
+
# import('x') (dynamic)
|
|
84
|
+
# export ... from 'x'
|
|
85
|
+
grep -hoE "(from|require|import)[[:space:]]*\(?[[:space:]]*['\"][^'\"]+['\"]" "$f" 2>/dev/null \
|
|
86
|
+
| sed -E "s|.*['\"]([^'\"]+)['\"].*|\1|" \
|
|
87
|
+
| while IFS= read -r spec; do
|
|
88
|
+
[[ -z "$spec" ]] && continue
|
|
89
|
+
# Skip bare package specifiers for graph purposes (we only
|
|
90
|
+
# want local file edges to compute importers).
|
|
91
|
+
case "$spec" in
|
|
92
|
+
.*|/*) : ;;
|
|
93
|
+
*) continue ;;
|
|
94
|
+
esac
|
|
95
|
+
# Normalize: resolve relative to $f's directory, best-effort.
|
|
96
|
+
local dir resolved
|
|
97
|
+
dir="$(dirname "$f")"
|
|
98
|
+
resolved="$dir/$spec"
|
|
99
|
+
# Strip ./ and trailing /
|
|
100
|
+
resolved="$(printf '%s' "$resolved" \
|
|
101
|
+
| sed -E 's|/\./|/|g; s|/$||')"
|
|
102
|
+
jq -cn --arg from "$f" --arg to "$resolved" '{from:$from,to:$to}'
|
|
103
|
+
done
|
|
104
|
+
done \
|
|
105
|
+
| jq -sc 'group_by(.from) | map({key:.[0].from, value:(map(.to)|unique)}) | from_entries'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
build_graph_json() {
|
|
109
|
+
local raw
|
|
110
|
+
if npx --yes madge --version >/dev/null 2>&1; then
|
|
111
|
+
raw="$(build_madge || echo '{}')"
|
|
112
|
+
BACKEND="madge"
|
|
113
|
+
else
|
|
114
|
+
raw="$(build_regex_fallback || echo '{}')"
|
|
115
|
+
BACKEND="regex-fallback"
|
|
116
|
+
fi
|
|
117
|
+
[[ -z "$raw" ]] && raw='{}'
|
|
118
|
+
printf '%s' "$raw"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
emit_final() {
|
|
122
|
+
local raw="$1"
|
|
123
|
+
mkdir -p "$OUT_DIR"
|
|
124
|
+
# Build nodes + edges from the {file: [deps]} shape. Keep original
|
|
125
|
+
# paths verbatim (do not attempt alias resolution here).
|
|
126
|
+
local body
|
|
127
|
+
body="$(printf '%s' "$raw" | jq --arg backend "$BACKEND" --arg now "$(date -u +%FT%TZ)" '
|
|
128
|
+
. as $g
|
|
129
|
+
| ([keys[], (.[] | .[])] | unique) as $nodes
|
|
130
|
+
| [to_entries[] | .key as $from | .value[] | {from:$from, to:.}] as $edges
|
|
131
|
+
| {nodes:$nodes, edges:$edges, backend:$backend, built_at:$now}
|
|
132
|
+
')"
|
|
133
|
+
# Hash over nodes+edges (stable serialization via jq --sort-keys -c).
|
|
134
|
+
local hash
|
|
135
|
+
hash="$(printf '%s' "$body" | jq -S -c '{nodes, edges}' | sha256sum | awk '{print "sha256:"$1}')"
|
|
136
|
+
printf '%s' "$body" | jq --arg h "$hash" '. + {hash:$h}' > "$OUT_FILE"
|
|
137
|
+
|
|
138
|
+
# Update state.import_graph_sha if state file exists.
|
|
139
|
+
if [[ -f "$STATE_PATH" ]]; then
|
|
140
|
+
jq --arg h "$hash" '. + {import_graph_sha:$h}' "$STATE_PATH" \
|
|
141
|
+
| bash "$SCRIPT_DIR/write-state.sh" "$STATE_PATH" >/dev/null \
|
|
142
|
+
|| log "failed to persist import_graph_sha to state"
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
jq -n --arg path "$OUT_FILE" --arg hash "$hash" --arg backend "$BACKEND" \
|
|
146
|
+
--argjson nodes "$(jq '.nodes|length' "$OUT_FILE")" \
|
|
147
|
+
--argjson edges "$(jq '.edges|length' "$OUT_FILE")" \
|
|
148
|
+
'{status:"ok", path:$path, hash:$hash, backend:$backend, nodes:$nodes, edges:$edges}'
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# --- Query: importers_of --------------------------------------------------
|
|
152
|
+
|
|
153
|
+
# importers_of <file> [--hops N]
|
|
154
|
+
# BFS over the edge list using jq. Emits one path per line.
|
|
155
|
+
importers_of() {
|
|
156
|
+
local file="$1"; shift || true
|
|
157
|
+
local hops=3
|
|
158
|
+
while [[ $# -gt 0 ]]; do
|
|
159
|
+
case "$1" in
|
|
160
|
+
--hops) hops="${2:-3}"; shift 2 ;;
|
|
161
|
+
*) shift ;;
|
|
162
|
+
esac
|
|
163
|
+
done
|
|
164
|
+
[[ -f "$OUT_FILE" ]] || { log "no import graph; run build first"; return 1; }
|
|
165
|
+
jq -r --arg start "$file" --argjson hops "$hops" '
|
|
166
|
+
. as $g
|
|
167
|
+
# Reverse adjacency: to → [from...]
|
|
168
|
+
| ([.edges[] | {key:.to, value:.from}] | group_by(.key)
|
|
169
|
+
| map({key:.[0].key, value:(map(.value)|unique)}) | from_entries) as $rev
|
|
170
|
+
| def bfs(frontier; seen; depth):
|
|
171
|
+
if depth >= $hops or (frontier|length)==0 then seen
|
|
172
|
+
else
|
|
173
|
+
(frontier | map($rev[.] // []) | add // [] | unique) as $next
|
|
174
|
+
| ($next - (seen|keys | map(.))) as $fresh
|
|
175
|
+
| bfs($fresh; (seen + ($fresh | map({(.):true}) | add // {})); depth + 1)
|
|
176
|
+
end;
|
|
177
|
+
bfs([$start]; {}; 0) | keys[]
|
|
178
|
+
' "$OUT_FILE"
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
# --- Dispatch -------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
main() {
|
|
184
|
+
case "${1:-build}" in
|
|
185
|
+
build|"")
|
|
186
|
+
shift || true
|
|
187
|
+
while [[ $# -gt 0 ]]; do
|
|
188
|
+
case "$1" in
|
|
189
|
+
--state) STATE_PATH="${2:?}"; shift 2 ;;
|
|
190
|
+
*) shift ;;
|
|
191
|
+
esac
|
|
192
|
+
done
|
|
193
|
+
raw="$(build_graph_json)"
|
|
194
|
+
emit_final "$raw"
|
|
195
|
+
;;
|
|
196
|
+
importers)
|
|
197
|
+
shift
|
|
198
|
+
[[ -z "${1:-}" ]] && { log "usage: importers <file> [--hops N]"; exit 2; }
|
|
199
|
+
importers_of "$@"
|
|
200
|
+
;;
|
|
201
|
+
*)
|
|
202
|
+
log "unknown subcommand: $1"
|
|
203
|
+
exit 2
|
|
204
|
+
;;
|
|
205
|
+
esac
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
main "$@"
|