start-vibing 4.3.1 → 4.3.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "start-vibing",
3
- "version": "4.3.1",
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.1: compass-artifact alignmentWCAG 2.2 SCs, CrUX field data, tool triangulation, Baymard sub-rules, Doherty/Tesler/Postel rationale, atomic state write, unshallow ladder.",
3
+ "version": "4.3.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: DSC-choice active selection score-typeui.mjs ranks 12 typeui-* skills by 7-axis fingerprint (density/contrast/geometry/color/typography/motion/audience) from findings+DIS, top-3 written to typeui-proposal.json with copy-paste CLI; harvest-typeui.sh pulls typeui.sh catalog + vercel-labs/anthropic fallbacks; sd-fix --typeui <name> rebrands V1-V8 fixes to snap spacing/color/radius to chosen direction's tokens.",
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.1
11
+ version: 0.6.3
12
12
  ---
13
13
 
14
14
  # super-design
@@ -33,17 +33,25 @@ Four-phase pipeline with 6 specialist agents:
33
33
  low density, weak CTA hierarchy, vibecode smell).
34
34
  - **Step 3h mobile-native audit** (21-item Duolingo/Linear/Arc/Cash-App
35
35
  checklist) — replaces "responsive-web-on-a-phone" thinking.
36
- - C16 ≤ 4 → design-skill advisory finding citing typeui-* selection matrix.
36
+ - C16 ≤ 4 → **DSC-choice** proposal: sd-synthesis runs
37
+ `scripts/score-typeui.mjs --from-audit <dir>` to derive a 7-axis site
38
+ fingerprint (density/contrast/geometry/color/typography/motion/audience)
39
+ and rank all installed typeui-* skills by fit 0-1. Top-3 are written to
40
+ `typeui-proposal.json` + shown in overview.md with a copy-paste CLI.
37
41
  Produces findings.json + design-intelligence.json with SHOT+QUOTE+SEL+VAL.
38
42
  3. **Synthesis** (sd-synthesis) — unifies research + audit + design-intelligence
39
- into overview.md (per-page DIS table + executive summary).
43
+ into overview.md (per-page DIS table + executive summary + typeui proposal
44
+ when craft ≤ 4).
40
45
  4. **Fix** (sd-fix + two-stage verify) — optional. Applies safe fixes with
41
46
  technical gates (types/lint/tests) AND semantic verification ("does this
42
47
  fix actually resolve the finding, or just mask it?"). Template families:
43
48
  A1-A15 a11y · V1-V8 design · U1-U10 ux · P1-P10 perf · **M1-M15 mobile**
44
49
  (cards-in-flex-col → compact list, table-on-mobile → card-per-row,
45
50
  centered-modal → bottom-sheet, etc.) · **DSC-1 design-skill advisory**
46
- (proposes typeui-* direction, never auto-applies HIGH risk). After each
51
+ (proposes typeui-* direction). With `--typeui <name>` flag, sd-fix loads
52
+ the chosen typeui skill (tokens: primary, radius, spacing scale,
53
+ typography) and rebrands V1-V8 fixes so every spacing/color/radius target
54
+ snaps to that direction's scale — turning advisory into applied. After each
47
55
  successful fix, re-drives Playwright to capture an after-screenshot (full
48
56
  page + element crop) and emits `docs/super-design/sessions/<id>/fix-report.md`:
49
57
  a self-contained visual diff with before/after images, file diffs,
@@ -105,10 +113,105 @@ Do NOT paste overview into chat.
105
113
  - `--refresh-research` — rerun sd-research
106
114
  - `--only <cat>` — a11y | design | ux | perf | research
107
115
  - `--scope <url>` — specific route
116
+ - `--app <name>` — scope the entire run to one monorepo app (matches a
117
+ `name` entry from `scripts/detect-apps.sh`). Required when `--scope <url>`
118
+ is ambiguous between multiple apps.
108
119
  - `--fix` — run sd-fix after audit
120
+ - `--typeui <name>` — combine with `--fix` to apply fixes aligned to a
121
+ chosen typeui direction (e.g. `--fix --typeui application`). Loads tokens
122
+ from `~/.claude/skills/typeui-<name>/SKILL.md` and rebrands V1-V8 targets
123
+ to that scale. Picked from the top-3 proposed in overview.md typeui
124
+ block. Without this flag, DSC-1 stays advisory. Run
125
+ `bash scripts/score-typeui.mjs --list` to see all installed directions.
126
+ - `--harvest-typeui` — run `scripts/harvest-typeui.sh` first to pull
127
+ missing typeui-* and related design skills (typeui.sh registry with
128
+ built-in catalog fallback). Idempotent; add `--refresh` to re-download.
109
129
  - `--dry-run` — artifacts without committing state
110
130
  - `--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)
131
+ - `--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.
132
+ - `--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.
133
+ - `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.
134
+
135
+ ## Monorepo support
136
+
137
+ Audit state is per-app (artifact §11 line 902) so independent deploys
138
+ carry independent freshness, `git_sha_at_audit`, and tool results. Layout
139
+ is auto-detected; nothing else to configure.
140
+
141
+ ### Detection
142
+
143
+ `scripts/detect-apps.sh` reads the first workspace manifest it finds:
144
+
145
+ | Manifest | Source of globs |
146
+ |----------|-----------------|
147
+ | `pnpm-workspace.yaml` | `packages:` list |
148
+ | `package.json` | `workspaces: [...]` or `workspaces.packages: [...]` (npm, yarn, Bun) |
149
+ | `turbo.json` | Presence → uses pnpm/npm/yarn workspaces; falls back to `apps/*` + `packages/*` if none |
150
+ | `nx.json` | `workspaceLayout.appsDir` / `libsDir` (default `apps/*`, `libs/*`) |
151
+ | `bunfig.toml` | Presence → falls back to `apps/*` + `packages/*` if package.json has no workspaces |
152
+
153
+ Each matched directory that also has a `package.json` becomes an app
154
+ with `name` taken from `package.json#name` (scope stripped), `path` the
155
+ directory, and `state_path` = `<path>/docs/super-design/.audit-state.json`.
156
+ If nothing matches, `detect-apps.sh` emits a `single` layout with
157
+ `path: "."` and the repo-root state path — preserving existing single-app
158
+ behavior.
159
+
160
+ ### Per-app pipeline
161
+
162
+ - **Preflight**: per app, read `<app>/docs/super-design/.audit-state.json`
163
+ via `validate-state.sh <app_path>`.
164
+ - **Change detection**: `scripts/detect-changes.sh --all-apps` loops over
165
+ every app and narrows `git diff` to `-- <app_path>/` so each app's
166
+ scope decision sees only its own files. Single-app shape is preserved
167
+ with `detect-changes.sh <last_sha>`.
168
+ - **Write state**: `scripts/write-state.sh <app_path>` derives the target
169
+ path; for single-app repos pass `.` or omit.
170
+
171
+ ### URL → app disambiguation
172
+
173
+ `--scope <url>` still targets one URL. When the URL maps cleanly to a
174
+ single app (e.g. `apps/admin` serves `https://admin.example.com`), the
175
+ pipeline picks that app automatically. When mapping is ambiguous
176
+ (multiple apps serve overlapping hostnames, or URL patterns cross apps),
177
+ the user MUST pass `--app <name>` — otherwise the skill aborts with a
178
+ `{"error":"ambiguous-app","candidates":[...]}` verdict instead of
179
+ guessing.
180
+
181
+ ## Scripts
182
+
183
+ Reusable shell helpers under `scripts/`. All POSIX/bash, tested on
184
+ Windows git-bash + Linux.
185
+
186
+ - `discover-routes.sh` — emits `route_map` as a JSON array. Dynamic
187
+ segments (`[slug]`, `[[...all]]`, `$id`, `:uid`) are suffixed with
188
+ `@fixture-<id>` (artifact §2.7). Fixtures resolved from sibling
189
+ `*.fixture.json`, `fixtures/<name>.json`, or `$SUPER_DESIGN_FIXTURES`
190
+ env JSON; falls back to `@fixture-default` with a warning. Consumers
191
+ (hash-pages, sd-audit) MUST strip the suffix before navigating.
192
+ - `build-import-graph.sh` — builds `.super-design/import-graph.json`
193
+ (`{nodes, edges, hash, backend}`) and persists `import_graph_sha` to
194
+ state. Prefers `npx madge --json <roots>`; falls back to a regex
195
+ scanner (JS/TS only, no alias resolution) if madge is missing.
196
+ - Query: `bash .../build-import-graph.sh importers <file> --hops 3`
197
+ → BFS over reversed edges; `detect-changes.sh` uses this to close
198
+ the component→pages gap when only components changed (Step 2 scope
199
+ decision: "Only components changed → re-audit pages importing them
200
+ (N=3 hops via madge)").
201
+ - `hash-pages.sh` — captures 3 viewports per URL (mobile_375, tablet_768,
202
+ desktop_1280), emits `{html_hash, dom_structure_hash, viewport_hashes:
203
+ {<vp>: {sha256, phash, png_path}}}` per page to
204
+ `docs/super-design/.cache/hashes/hashes.json` and persists each PNG to
205
+ `<cache>/screenshots/<url-enc>/<vp>.png`. Applies artifact §3.4 mask
206
+ defaults plus `MASK_SELECTORS`; `phash` uses `sharp` when available
207
+ (tagged `phash:`) or a deterministic PNG fingerprint otherwise
208
+ (tagged `fpr:`, only useful for exact-match comparison).
209
+ - `visual-regression.sh [--update-baselines] [<state>]` — reads the
210
+ `visual_regression` block from `.audit-state.json` and diffs current
211
+ screenshots against `.super-design/baselines/`. Engine chain:
212
+ `pixelmatch` → `odiff` → `sha256-fallback`. Emits
213
+ `{page, viewport, diff_ratio, threshold, pass, diff_image_path}` to
214
+ `<diff_dir>/results.json`. Exits non-zero if any page fails.
112
215
 
113
216
  ## References (Read on demand)
114
217
 
@@ -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 "$@"