whale-igniter 1.2.2 → 1.3.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/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="https://unpkg.com/whale-igniter@latest/brand/lockup.svg" alt="Whale Igniter" width="640">
2
+ <img src="https://cdn.jsdelivr.net/npm/whale-igniter@1.3.0/brand/lockup.svg" alt="Whale Igniter" width="640">
3
3
  </p>
4
4
 
5
5
  <p align="center">
@@ -9,332 +9,140 @@
9
9
  <img alt="local first" src="https://img.shields.io/badge/local--first-agent%20memory-%230034D3?labelColor=%23F2ECE1&style=flat-square">
10
10
  </p>
11
11
 
12
- **CLI-first operational intelligence for AI-assisted product workflows.**
12
+ **Local-first project memory for AI agents.**
13
13
 
14
- Whale gives your project a machine-readable memory so AI agents like
15
- Claude Code, Codex, Cursor and Copilot understand your design system,
16
- conventions and decisions from the first commit without you having
17
- to re-explain everything every session.
14
+ Whale Igniter gives your repo a structured memory: design foundations,
15
+ components, decisions, refinements and generated agent context. It is
16
+ CLI-first, deterministic by default, and works with Codex, Claude Code,
17
+ Cursor, Copilot and any MCP client.
18
18
 
19
- > AI-native, not AI-dependent. The core is deterministic and runs
20
- > offline. AI is an enrichment layer — opt-in via API key, or via the
21
- > built-in MCP server.
22
-
23
- ---
24
-
25
- ## Install
19
+ ## Quick Start
26
20
 
27
21
  ```bash
28
- npm install -g whale-igniter
29
- # or use it directly without installing
30
22
  npx whale-igniter ignite my-app
23
+ cd my-app
24
+ npx whale-igniter remember
25
+ npx whale-igniter check
31
26
  ```
32
27
 
33
- Requires Node 20 or newer.
34
-
35
- Whale's executable is `whale`:
28
+ Install globally if you want the shorter `whale` command:
36
29
 
37
30
  ```bash
38
- whale --version
31
+ npm install -g whale-igniter
39
32
  whale ignite my-app
40
33
  ```
41
34
 
42
- ---
35
+ Requires Node 20 or newer.
43
36
 
44
- ## The two-minute version
37
+ ## What It Creates
45
38
 
46
- ```bash
47
- # Brand new project
48
- whale ignite my-app
49
-
50
- # Existing React + Tailwind codebase
51
- cd my-existing-app
52
- whale adopt
53
- whale adopt review # accept/reject scanner proposals
39
+ ```text
40
+ my-app/
41
+ ├── whale.config.json
42
+ ├── CLAUDE.md
43
+ ├── intelligence/
44
+ │ ├── components.json
45
+ │ ├── decisions.json
46
+ │ └── refinements.json
47
+ └── llm-wiki/
54
48
  ```
55
49
 
56
- Either path produces a `CLAUDE.md` at the project root plus a
57
- `llm-wiki/` folder. Open the project in Claude Code (or any AI
58
- assistant) and it picks up the context automatically.
59
-
60
- ---
61
-
62
- ## What Whale does
63
-
64
- ### 1. Bootstraps AI context for new projects
65
-
66
- `whale ignite` creates a workspace with foundations, a config, the
67
- intelligence stores, and a fully-generated `CLAUDE.md`. Three modes:
68
- opinionated default, `--minimal` skeleton, or `--interactive` wizard.
69
-
70
- ### 2. Adopts existing projects
50
+ `intelligence/*.json` is the source of truth. Markdown files are rendered
51
+ from it so agents always read current project state.
71
52
 
72
- `whale adopt` parses your React/Tailwind source with a real AST, infers
73
- foundations (grid, radii, palette), detects components, and stages
74
- proposals you can review and accept. Idempotent — re-runs don't
75
- duplicate or overwrite your decisions.
53
+ ## What's new in v1.3
76
54
 
77
- ### 3. Records operational context as you work
55
+ **UI references library** — curated, copy-paste component references ship with the tool. Run `whale references add forms` (or `--all`) to install them into your project. CLAUDE.md and MCP tools automatically expose the files to agents.
78
56
 
79
- Three structured stores capture what would otherwise live in your head:
57
+ **Cross-framework drift detection** `whale insights drift spacing|color|radii` scans CSS, inline styles, Vue/Svelte style blocks and JSX props for values that don't match your foundations. Run `--review` to accept exceptions as refinements or flag them for refactor.
80
58
 
81
- - **`intelligence/components.json`**component catalog
82
- - **`intelligence/decisions.json`** — architectural and product decisions
83
- - **`intelligence/refinements.json`** — approved exceptions to the system
59
+ **Human-language wizard**`whale ignite --interactive` now asks plain-English questions (what are you building, which framework, team size) and maps answers to a sensible config automatically.
84
60
 
85
- Every write auto-regenerates `CLAUDE.md` so agents always read current state.
61
+ **Template extraction** wiki templates moved to `src/templates/` as composable render functions. CLAUDE.md is now structured as three clear sections: what the project is, what to read, and how to work here.
86
62
 
87
- ### 4. Surfaces accountable insights
63
+ **`whale changes --since <ref>`** — shows what changed in intelligence stores (decisions, components, refinements) since any git ref, ISO date, or `HEAD~1`.
88
64
 
89
- `whale insights` runs deterministic analyzers over your stores and code:
90
- refinement clusters, decision tension, orphan components, token drift,
91
- grid drift, catalog coverage gaps. No AI in the loop — local,
92
- explainable, machine-checkable.
65
+ ## Core Commands
93
66
 
94
- ### 5. Generates code that respects your foundations
95
-
96
- `whale create component Card --variants primary,outline` produces a
97
- typed React component using your actual grid, radii, and accent color.
98
- No invented values. Auto-registers in the catalog.
99
-
100
- ### 6. Bridges to any AI agent (Selene)
101
-
102
- Three composable commands `describe`, `audit`, `suggest` package
103
- your full project context into a structured prompt. They work three ways:
104
-
105
- - **Prompt mode** (default, offline): writes the prompt to clipboard;
106
- paste into Claude/ChatGPT/Cursor; paste response back with
107
- `whale selene apply`.
108
- - **API mode** (opt-in): if `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`
109
- is set, Whale calls the provider directly and auto-applies the result.
110
- Transparent fallback to prompt mode on any failure.
111
- - **MCP mode** (new in v1.0): Whale exposes its capabilities as MCP
112
- tools that Claude Code, Cursor, Zed and any MCP client use natively.
113
-
114
- ### 7. Stays in sync automatically
115
-
116
- `whale watch` regenerates `CLAUDE.md` and the wiki when your foundations
117
- or stores change. Plays well with pre-commit hooks and CI.
118
-
119
- ---
120
-
121
- ## MCP server
122
-
123
- The most powerful integration in v1.0. Wire Whale into your AI tool:
67
+ | Command | Use it when you want to... |
68
+ | --- | --- |
69
+ | `whale ignite my-app` | Create a new Whale workspace |
70
+ | `whale ignite my-app --interactive` | Wizard: project type, stack, team size, packs |
71
+ | `whale adopt` | Scan an existing React/Tailwind project |
72
+ | `whale remember` | Regenerate agent context and wiki |
73
+ | `whale check` | Run validation and project insights |
74
+ | `whale improve` | Ask Selene for improvement suggestions |
75
+ | `whale explain` | Generate docs and AI-readable context |
76
+ | `whale references add --all` | Install UI component reference library |
77
+ | `whale insights drift spacing` | Find off-grid spacing across CSS and JSX |
78
+ | `whale insights drift spacing --review` | Interactively accept or queue each drift issue |
79
+ | `whale changes --since HEAD~5` | Show what changed in intelligence stores |
80
+ | `whale team` | See active operating roles/packs |
81
+ | `whale create component Hero` | Generate a typed React component |
82
+ | `whale mcp config --client cursor` | Configure an MCP client |
83
+
84
+ ## Operating Roles
85
+
86
+ Whale includes five operating roles as packs:
124
87
 
125
88
  ```bash
126
- whale mcp config --client claude-code
127
- # Prints the JSON snippet for ~/Library/Application Support/Claude/claude_desktop_config.json
89
+ whale team add atlas # product strategy
90
+ whale team add forge # implementation architecture
91
+ whale team add scribe # documentation
92
+ whale team add lighthouse # quality checks
93
+ whale team add selene # AI suggestions and audits
128
94
  ```
129
95
 
130
- Once configured, the agent gets tools like:
131
-
132
- - `whale_project_overview` — read foundations, packs, counts in one call
133
- - `whale_list_components` — see what already exists before creating duplicates
134
- - `whale_list_decisions` — understand why the project is structured this way
135
- - `whale_register_component` — record components the agent creates
136
- - `whale_record_decision` — record non-obvious choices as they happen
137
- - `whale_validate`, `whale_insights` — check state mid-session
96
+ They are not autonomous background agents. They are structured project
97
+ roles and instructions that help the agent you already use work with
98
+ better context.
138
99
 
139
- Available client snippets: `--client claude-code | cursor | zed | raw`.
100
+ ## AI Bridge
140
101
 
141
- ---
102
+ Selene packages your project context into structured prompts, or calls an
103
+ API when `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` is configured.
142
104
 
143
- ## Command reference
144
-
145
- | Command | What it does |
146
- | --- | --- |
147
- | `whale ignite [name]` | Bootstrap a workspace. Flags: `--interactive`, `--minimal`. |
148
- | `whale adopt [target]` | Scan an existing project and stage proposals. |
149
- | `whale adopt review` | Walk through pending proposals interactively. |
150
- | `whale adopt status` | List proposals without entering review. |
151
- | `whale sync` | Regenerate CLAUDE.md + wiki from `intelligence/*.json`. |
152
- | `whale remember` | Friendly alias for `sync`: update project memory for AI assistants. |
153
- | `whale check` | Friendly health check: run `validate` and then `insights`. |
154
- | `whale improve` | Friendly Selene entry point: suggest project-wide improvements. |
155
- | `whale explain` | Friendly docs entry point: generate reports and wiki/context. |
156
- | `whale team` | Show active and available AI teammates. |
157
- | `whale team add <name>` | Add atlas, forge, scribe, lighthouse, or selene as an AI teammate. |
158
- | `whale watch` | Auto-regenerate on changes. Flags: `--once`, `--verbose`, `--debounce`. |
159
- | `whale validate` | Run validators. Non-zero exit on errors. |
160
- | `whale insights` | Local analyzers + recommendations. Flags: `--json`, `--category`, `--min-severity`. |
161
- | `whale refine "<note>"` | Record a validator override. |
162
- | `whale decision` | Record an architectural / product decision. |
163
- | `whale component add <name>` | Register a component. |
164
- | `whale create component <name>` | Generate a typed component scaffold. |
165
- | `whale selene describe <component>` | AI-assisted catalog description. |
166
- | `whale selene audit <file>` | AI-assisted audit against foundations. |
167
- | `whale selene suggest` | AI-suggested patterns and decisions. |
168
- | `whale selene apply <kind>` | Apply pasted LLM response. |
169
- | `whale selene status` | Show provider state, mode, cache. |
170
- | `whale mcp serve` | Start the MCP server on stdio. |
171
- | `whale mcp config` | Print client configuration snippets. |
172
- | `whale docs` | Generate human-facing reports. |
173
-
174
- ---
175
-
176
- ## Project structure
177
-
178
- ```
179
- my-app/
180
- ├── whale.config.json foundations, packs, AI targets
181
- ├── CLAUDE.md AI entry point (auto-generated)
182
- ├── AGENTS.md if "codex" in aiTargets
183
- ├── .cursorrules if "cursor" in aiTargets
184
- ├── intelligence/ source of truth (JSON)
185
- │ ├── refinements.json
186
- │ ├── decisions.json
187
- │ └── components.json
188
- ├── llm-wiki/ rendered view (markdown)
189
- │ ├── FOUNDATIONS.md
190
- │ ├── CONVENTIONS.md
191
- │ ├── DECISIONS.md
192
- │ ├── COMPONENTS.md
193
- │ └── WORKFLOWS.md
194
- └── .whale/ runtime caches and Selene stash
195
- └── selene/
196
- ├── cache/
197
- └── *.prompt.md / *.response.md
105
+ ```bash
106
+ whale selene status
107
+ whale selene suggest --focus all
108
+ whale selene audit src/components/Hero.tsx
198
109
  ```
199
110
 
200
- **JSON is the source of truth. Markdown is rendered.** Edit a JSON
201
- store by hand, run `whale sync` (or have `whale watch` running), and
202
- everything else catches up.
203
-
204
- ---
205
-
206
- ## Selene configuration
111
+ Without an API key, Selene runs in prompt mode and prints/copies a prompt
112
+ you can paste into Claude, ChatGPT, Cursor or Codex.
207
113
 
208
- Optional block in `whale.config.json` for API mode:
114
+ ## MCP
209
115
 
210
- ```json
211
- {
212
- "selene": {
213
- "provider": "anthropic",
214
- "model": "claude-sonnet-4-6",
215
- "temperature": 0.2,
216
- "maxTokens": 1500,
217
- "autoCall": true,
218
- "confirmCost": false,
219
- "noCache": false
220
- }
221
- }
116
+ ```bash
117
+ whale mcp config --client claude-code
118
+ whale mcp config --client cursor
119
+ whale mcp config --client zed
222
120
  ```
223
121
 
224
- Everything is optional. With no config and no key, Selene runs in
225
- prompt mode forever. Adding a key (env or here) flips it to API mode.
226
- Set `autoCall: false` to keep prompt mode even with a key available.
227
-
228
- ---
229
-
230
- ## Presentation
231
-
232
- Whale's terminal output is built on a semantic UI layer: commands describe
233
- *intent* (success, warning, section header, key/value pair) and the active
234
- theme decides what each intent looks like. One result is that the same
235
- command produces premium output in a real terminal and clean, grep-friendly
236
- output in CI.
237
-
238
- Three environment variables control presentation:
239
-
240
- | Variable | Effect |
241
- | --- | --- |
242
- | `WHALE_PLAIN=1` | Disable color *and* Unicode glyphs. The output becomes ASCII-only and grep-friendly. Recommended for CI logs and pipelines. |
243
- | `WHALE_UNICODE=0` | Keep color, fall back to ASCII glyphs (`*`, `v`, `x`, `->`). Useful for terminals that render color but mangle Unicode. |
244
- | `NO_COLOR=1` / `FORCE_COLOR=0` | Standard environment overrides — chalk respects them, so does Whale. |
245
-
246
- In normal terminals Whale auto-detects capabilities and uses Unicode + color.
247
- In non-TTY contexts (piping, CI, scripts) it strips both automatically. You
248
- don't need to opt in unless you want to override the default.
249
-
250
- ---
251
-
252
- ## Brand kit
253
-
254
- Whale ships its identity assets in `brand/` so the GitHub README, npm page,
255
- demos and future splash surfaces all speak the same visual language.
122
+ MCP exposes project overview, components, decisions, refinements,
123
+ validation, insights and write tools to compatible AI clients.
256
124
 
257
- ### Palette
258
-
259
- | Token | Hex | Role |
260
- | --- | --- | --- |
261
- | `--wi-royal` | `#0034D3` | Primary ink, mark body, headings |
262
- | `--wi-paper` | `#F2ECE1` | Page background, knockout fill |
263
- | `--wi-sky` | `#99CCFF` | Soft accent and fills |
264
- | `--wi-deep` | `#003087` | Dark surface and on-light text |
265
-
266
- ### Typography
125
+ ## Brand Tokens
267
126
 
268
127
  ```css
128
+ --wi-royal: #0034D3;
129
+ --wi-paper: #F2ECE1;
130
+ --wi-sky: #99CCFF;
131
+ --wi-deep: #003087;
132
+
269
133
  --wi-font-display: "Special Elite", ui-monospace, monospace;
270
134
  --wi-font-mono: "JetBrains Mono", ui-monospace, monospace;
271
135
  ```
272
136
 
273
- - **Display** `Special Elite`, used for the wordmark and expressive headings.
274
- - **Mono** — `JetBrains Mono`, used for CLI surfaces and code samples.
275
-
276
- ### Assets
277
-
278
- | File | Use |
279
- | --- | --- |
280
- | `brand/logo.svg` | Primary mark |
281
- | `brand/wordmark.svg` | Wordmark only |
282
- | `brand/lockup.svg` | Mark + wordmark for README and docs |
283
- | `brand/logo-on-paper.svg` | Mark on paper background |
284
- | `brand/logo-on-royal.svg` | Knockout mark on royal |
285
- | `brand/favicon.svg` | Light favicon |
286
- | `brand/favicon-dark.svg` | Dark favicon |
287
-
288
- ---
289
-
290
- ## CI usage
291
-
292
- ```yaml
293
- - run: npx whale-igniter validate . # exits non-zero on errors
294
- - run: npx whale-igniter sync # ensure CLAUDE.md is current
295
- - run: git diff --exit-code # fail if sync produced uncommitted changes
296
- ```
297
-
298
- This turns "is the AI context current?" into a checkable property of
299
- the repo. Combine with `whale insights --json --min-severity warning`
300
- for automated quality reports.
301
-
302
- ---
303
-
304
- ## Design principles
305
-
306
- 1. **CLI-first.** Whale is a CLI and the MCP server. No dashboard, no SaaS.
307
- 2. **Local-first.** Intelligence lives in your repo, in git. No accounts.
308
- 3. **AI-native, not AI-dependent.** Core works offline. AI is opt-in at
309
- every level.
310
- 4. **Source of truth in JSON; rendered views in Markdown.** One direction.
311
- 5. **Idempotent everywhere.** Re-running adopt, sync, watch, or
312
- register doesn't duplicate or corrupt anything.
313
- 6. **Honest fallbacks.** API down? Falls back to prompt mode. Scan
314
- fails? Insights skip orphan/drift but still run. Watcher recursive
315
- not supported? Falls back to non-recursive. The user is never stuck.
316
-
317
- ---
318
-
319
- ## What v1.2 doesn't do (and why)
320
-
321
- - **Vue / Svelte parsing.** Scanner is React + Tailwind only. Other
322
- frameworks need their own parsers; that's post-v1.0.
323
- - **Hosted SaaS.** Whale is a tool, not a service. By design.
324
- - **Real-time team sync.** Today everything is local + git. Team
325
- workflows happen through PRs against `intelligence/*.json`.
326
- - **Generic linting.** ESLint and Stylelint exist. Whale only checks
327
- what's tied to operational context.
328
-
329
- See [docs/ROADMAP.md](docs/ROADMAP.md) for what's planned next.
330
-
331
- ---
137
+ Brand assets ship in `brand/` and are included in the npm package.
332
138
 
333
- ## License
139
+ ## Links
334
140
 
335
- MIT. See [LICENSE](LICENSE).
141
+ - Roadmap: [`docs/ROADMAP.md`](docs/ROADMAP.md)
142
+ - Package: [npmjs.com/package/whale-igniter](https://www.npmjs.com/package/whale-igniter)
143
+ - License: MIT
336
144
 
337
145
  <p align="center">
338
- <img src="https://unpkg.com/whale-igniter@latest/brand/logo.svg" alt="" width="44"><br>
339
- <sub><b>Whale Igniter</b> · local-first project memory for AI agents</sub>
146
+ <img src="https://cdn.jsdelivr.net/npm/whale-igniter@1.3.0/brand/logo.svg" alt="" width="44"><br>
147
+ <sub><b>Whale Igniter</b> · map the project before the agent moves</sub>
340
148
  </p>
@@ -253,6 +253,145 @@ function analyzeGridDrift(config, observations) {
253
253
  return [];
254
254
  }
255
255
  // ---------------------------------------------------------------------------
256
+ // Cross-framework drift analyzers (Block D)
257
+ // ---------------------------------------------------------------------------
258
+ const SPACING_PROPERTIES = new Set([
259
+ "padding", "padding-top", "padding-right", "padding-bottom", "padding-left",
260
+ "margin", "margin-top", "margin-right", "margin-bottom", "margin-left",
261
+ "gap", "column-gap", "row-gap"
262
+ ]);
263
+ const COLOR_PROPERTIES = new Set(["color", "background-color", "border-color"]);
264
+ const RADIUS_PROPERTIES = new Set([
265
+ "border-radius",
266
+ "border-top-left-radius", "border-top-right-radius",
267
+ "border-bottom-left-radius", "border-bottom-right-radius"
268
+ ]);
269
+ function isRefinedSpacing(refinements) {
270
+ return refinements.some((r) => r.scope?.issueType === "spacing");
271
+ }
272
+ export function analyzeSpacingDrift(observations, config, refinements = []) {
273
+ if (isRefinedSpacing(refinements))
274
+ return [];
275
+ const grid = config.foundations?.grid ?? 8;
276
+ const drifting = observations.filter((o) => SPACING_PROPERTIES.has(o.property) &&
277
+ o.pxValue != null &&
278
+ o.pxValue !== 0 &&
279
+ o.pxValue % grid !== 0);
280
+ if (drifting.length === 0)
281
+ return [];
282
+ // Group by file
283
+ const byFile = new Map();
284
+ for (const obs of drifting) {
285
+ const key = obs.file;
286
+ if (!byFile.has(key))
287
+ byFile.set(key, []);
288
+ byFile.get(key).push(obs);
289
+ }
290
+ const insights = [];
291
+ for (const [file, items] of byFile) {
292
+ const severity = items.length >= 3 ? "warning" : "info";
293
+ const fileShort = file.split("/").slice(-2).join("/");
294
+ insights.push({
295
+ id: `drift.spacing.${Buffer.from(file).toString("base64").slice(0, 12)}`,
296
+ category: "tokens",
297
+ severity,
298
+ title: `${items.length} off-grid spacing value(s) in ${fileShort}`,
299
+ detail: `Grid is ${grid}px. Found values that are not multiples: ${[...new Set(items.map((o) => o.value))].slice(0, 5).join(", ")}.`,
300
+ evidence: items.slice(0, 5).map((o) => `${fileShort}:${o.line} — ${o.property}: ${o.value}`),
301
+ action: `Run \`whale insights drift spacing --review\` to accept as refinement or flag for refactor.`
302
+ });
303
+ }
304
+ return insights;
305
+ }
306
+ export function analyzeColorDrift(observations, _config, refinements = []) {
307
+ const colorObs = observations.filter((o) => COLOR_PROPERTIES.has(o.property) &&
308
+ !o.value.startsWith("var(") &&
309
+ /^#[0-9a-fA-F]{3,8}$/.test(o.value));
310
+ if (colorObs.length === 0)
311
+ return [];
312
+ // Derive reference palette: top-10 most frequent hex values
313
+ const freq = new Map();
314
+ for (const obs of colorObs) {
315
+ const hex = obs.value.toLowerCase();
316
+ freq.set(hex, (freq.get(hex) ?? 0) + 1);
317
+ }
318
+ const palette = new Set([...freq.entries()]
319
+ .sort((a, b) => b[1] - a[1])
320
+ .slice(0, 10)
321
+ .map(([hex]) => hex));
322
+ // Check refinements for color exceptions
323
+ const hasColorRefinement = refinements.some((r) => r.scope?.issueType === "hex");
324
+ if (hasColorRefinement)
325
+ return [];
326
+ // Drifting = hex that is NOT in the top-10 palette
327
+ const drifting = colorObs.filter((o) => !palette.has(o.value.toLowerCase()));
328
+ if (drifting.length === 0)
329
+ return [];
330
+ // Group by hue family (first 2 hex digits after #)
331
+ const byHue = new Map();
332
+ for (const obs of drifting) {
333
+ const hue = obs.value.slice(1, 3).toLowerCase();
334
+ if (!byHue.has(hue))
335
+ byHue.set(hue, []);
336
+ byHue.get(hue).push(obs);
337
+ }
338
+ const insights = [];
339
+ for (const [hue, items] of byHue) {
340
+ const severity = items.length >= 3 ? "warning" : "info";
341
+ const uniqueValues = [...new Set(items.map((o) => o.value))];
342
+ insights.push({
343
+ id: `drift.color.${hue}`,
344
+ category: "tokens",
345
+ severity,
346
+ title: `${items.length} color value(s) outside the project palette (hue ~#${hue})`,
347
+ detail: `Colors not in the top-10 palette: ${uniqueValues.slice(0, 4).join(", ")}.`,
348
+ evidence: items.slice(0, 5).map((o) => {
349
+ const short = o.file.split("/").slice(-2).join("/");
350
+ return `${short}:${o.line} — ${o.property}: ${o.value}`;
351
+ }),
352
+ action: `Run \`whale insights drift color --review\` to accept as refinement or flag for refactor.`
353
+ });
354
+ }
355
+ return insights;
356
+ }
357
+ export function analyzeRadiiDrift(observations, config, refinements = []) {
358
+ const hasRadiusRefinement = refinements.some((r) => r.scope?.issueType === "radius");
359
+ if (hasRadiusRefinement)
360
+ return [];
361
+ const control = config.foundations?.radius?.control ?? 2;
362
+ const container = config.foundations?.radius?.container ?? 4;
363
+ const drifting = observations.filter((o) => RADIUS_PROPERTIES.has(o.property) &&
364
+ o.pxValue != null &&
365
+ o.pxValue !== 0 &&
366
+ o.pxValue !== 9999 &&
367
+ o.pxValue !== control &&
368
+ o.pxValue !== container);
369
+ if (drifting.length === 0)
370
+ return [];
371
+ // Group by px value to show the spread
372
+ const byValue = new Map();
373
+ for (const obs of drifting) {
374
+ const key = obs.pxValue;
375
+ if (!byValue.has(key))
376
+ byValue.set(key, []);
377
+ byValue.get(key).push(obs);
378
+ }
379
+ const spread = [...byValue.keys()].sort((a, b) => a - b);
380
+ const severity = drifting.length >= 5 ? "warning" : "info";
381
+ return [{
382
+ id: "drift.radii",
383
+ category: "tokens",
384
+ severity,
385
+ title: `${drifting.length} border-radius value(s) outside foundations (${control}px / ${container}px)`,
386
+ detail: `Values found: ${spread.map((v) => `${v}px`).join(", ")}. Expected: ${control}px (controls) or ${container}px (containers).`,
387
+ evidence: drifting.slice(0, 5).map((o) => {
388
+ const short = o.file.split("/").slice(-2).join("/");
389
+ return `${short}:${o.line} — ${o.property}: ${o.value}`;
390
+ }),
391
+ action: `Run \`whale insights drift radii --review\` to accept as refinement or flag for refactor.`
392
+ }];
393
+ }
394
+ // ---------------------------------------------------------------------------
256
395
  // Main entry
257
396
  // ---------------------------------------------------------------------------
258
397
  export function analyze(input) {
@@ -264,6 +403,11 @@ export function analyze(input) {
264
403
  all.push(...analyzeTokenDrift(input.observations));
265
404
  all.push(...analyzeDecisionTension(input.decisions));
266
405
  all.push(...analyzeGridDrift(input.config, input.observations));
406
+ if (input.styleObservations) {
407
+ all.push(...analyzeSpacingDrift(input.styleObservations, input.config, input.refinements));
408
+ all.push(...analyzeColorDrift(input.styleObservations, input.config, input.refinements));
409
+ all.push(...analyzeRadiiDrift(input.styleObservations, input.config, input.refinements));
410
+ }
267
411
  // Sort by severity (critical > warning > info), then by category for stable output.
268
412
  const sevRank = { critical: 0, warning: 1, info: 2 };
269
413
  all.sort((a, b) => {
@@ -1,12 +1,14 @@
1
1
  import path from "node:path";
2
2
  import fs from "fs-extra";
3
3
  import ora from "ora";
4
+ import prompts from "prompts";
4
5
  import { resolveTarget } from "../utils/paths.js";
5
6
  import { scanComponents } from "../scanner/componentScanner.js";
6
7
  import { aggregateTailwind, detectTailwindConfig } from "../scanner/tailwindScanner.js";
7
8
  import { inferFoundations } from "../scanner/foundationInferrer.js";
8
9
  import { loadProposals, saveProposals, upsertProposal, fingerprintComponent, fingerprintFoundations, pendingProposals } from "../utils/proposals.js";
9
10
  import { loadConfig, saveConfig, DEFAULT_CONFIG } from "../utils/config.js";
11
+ import { getAiAvailability } from "../utils/aiAvailability.js";
10
12
  import { ui } from "../ui/index.js";
11
13
  const SCANNER_VERSION = "1.1.0";
12
14
  export async function adoptCommand(targetArg, options = {}) {
@@ -140,6 +142,24 @@ export async function adoptCommand(targetArg, options = {}) {
140
142
  if (!(await fs.pathExists(p)))
141
143
  await fs.writeJson(p, [], { spaces: 2 });
142
144
  }
145
+ // ---- Step 7: optional Selene enrichment -----------------------------------
146
+ const ai = await getAiAvailability(target);
147
+ if (ai.available) {
148
+ console.log();
149
+ const { doEnrich } = await prompts({
150
+ type: "confirm",
151
+ name: "doEnrich",
152
+ message: "Use Selene to add prose descriptions to the inferred foundations? (requires API key)",
153
+ initial: false
154
+ });
155
+ if (doEnrich) {
156
+ console.log(ui.note("Selene enrichment: run `whale selene describe` after review to add descriptions."));
157
+ }
158
+ }
159
+ else {
160
+ console.log();
161
+ console.log(ui.muted("AI features available with Claude Code, Codex, or an API key."));
162
+ }
143
163
  // ---- Summary --------------------------------------------------------------
144
164
  console.log();
145
165
  console.log(ui.section("Proposals"));