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 +89 -281
- package/dist/analyzer/insights.js +144 -0
- package/dist/commands/adopt.js +20 -0
- package/dist/commands/changes.js +127 -0
- package/dist/commands/ignite.js +142 -76
- package/dist/commands/insights.js +146 -9
- package/dist/commands/references.js +193 -0
- package/dist/commands/sync.js +8 -0
- package/dist/generators/wikiGenerator.js +8 -304
- package/dist/index.js +33 -2
- package/dist/mcp/server.js +29 -0
- package/dist/scanner/extractors/css.js +95 -0
- package/dist/scanner/extractors/inline.js +131 -0
- package/dist/scanner/extractors/styleBlocks.js +37 -0
- package/dist/scanner/normalizer.js +59 -0
- package/dist/scanner/tailwindScanner.js +39 -0
- package/dist/templates/claude.js +93 -0
- package/dist/templates/components.js +45 -0
- package/dist/templates/conventions.js +45 -0
- package/dist/templates/decisions.js +34 -0
- package/dist/templates/foundations.js +34 -0
- package/dist/templates/project.js +82 -0
- package/dist/utils/aiAvailability.js +25 -0
- package/dist/utils/wizardMapping.js +54 -0
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="https://
|
|
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
|
-
**
|
|
12
|
+
**Local-first project memory for AI agents.**
|
|
13
13
|
|
|
14
|
-
Whale gives your
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
Whale's executable is `whale`:
|
|
28
|
+
Install globally if you want the shorter `whale` command:
|
|
36
29
|
|
|
37
30
|
```bash
|
|
38
|
-
whale
|
|
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
|
-
##
|
|
37
|
+
## What It Creates
|
|
45
38
|
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
whale
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
`whale
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
127
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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
|
-
|
|
114
|
+
## MCP
|
|
209
115
|
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
139
|
+
## Links
|
|
334
140
|
|
|
335
|
-
|
|
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://
|
|
339
|
-
<sub><b>Whale Igniter</b> ·
|
|
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) => {
|
package/dist/commands/adopt.js
CHANGED
|
@@ -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"));
|