whale-igniter 1.2.3 → 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.
@@ -0,0 +1,37 @@
1
+ import fs from "fs-extra";
2
+ import { glob } from "glob";
3
+ import { extractFromContent } from "./css.js";
4
+ const IGNORE = ["node_modules/**", "dist/**", ".next/**", "build/**", "coverage/**"];
5
+ const STYLE_BLOCK_REGEX = /<style[^>]*>([\s\S]*?)<\/style>/gi;
6
+ function linesBefore(content, index) {
7
+ let count = 0;
8
+ for (let i = 0; i < index; i++) {
9
+ if (content[i] === "\n")
10
+ count++;
11
+ }
12
+ return count;
13
+ }
14
+ async function extractFromFile(filePath) {
15
+ const content = await fs.readFile(filePath, "utf-8");
16
+ const observations = [];
17
+ let match;
18
+ STYLE_BLOCK_REGEX.lastIndex = 0;
19
+ while ((match = STYLE_BLOCK_REGEX.exec(content)) !== null) {
20
+ const blockStart = match.index;
21
+ const cssContent = match[1];
22
+ // +1 because the opening <style> tag is on that line, CSS starts on next
23
+ const lineOffset = linesBefore(content, blockStart) + 1;
24
+ const blockObs = await extractFromContent(cssContent, filePath, lineOffset, "style-block");
25
+ observations.push(...blockObs);
26
+ }
27
+ return observations;
28
+ }
29
+ export async function extractFromStyleBlocks(root) {
30
+ const files = await glob("**/*.{html,vue,svelte,astro}", {
31
+ cwd: root,
32
+ ignore: IGNORE,
33
+ absolute: true
34
+ });
35
+ const results = await Promise.all(files.map(extractFromFile));
36
+ return results.flat();
37
+ }
@@ -0,0 +1,59 @@
1
+ export const TRACKED_PROPERTIES = new Set([
2
+ "padding", "padding-top", "padding-right", "padding-bottom", "padding-left",
3
+ "margin", "margin-top", "margin-right", "margin-bottom", "margin-left",
4
+ "gap", "column-gap", "row-gap",
5
+ "color", "background-color", "border-color",
6
+ "border-radius",
7
+ "border-top-left-radius", "border-top-right-radius",
8
+ "border-bottom-left-radius", "border-bottom-right-radius",
9
+ "font-size", "font-weight", "line-height"
10
+ ]);
11
+ const REM_BASE = 16;
12
+ export function parsePxValue(value) {
13
+ if (!value)
14
+ return undefined;
15
+ const trimmed = value.trim();
16
+ const pxMatch = trimmed.match(/^(-?\d+(?:\.\d+)?)px$/);
17
+ if (pxMatch)
18
+ return parseFloat(pxMatch[1]);
19
+ const remMatch = trimmed.match(/^(-?\d+(?:\.\d+)?)rem$/);
20
+ if (remMatch)
21
+ return parseFloat(remMatch[1]) * REM_BASE;
22
+ const emMatch = trimmed.match(/^(-?\d+(?:\.\d+)?)em$/);
23
+ if (emMatch)
24
+ return parseFloat(emMatch[1]) * REM_BASE;
25
+ if (trimmed === "0")
26
+ return 0;
27
+ return undefined;
28
+ }
29
+ // Camel-case JSX property names → CSS property names
30
+ const CAMEL_TO_CSS = {
31
+ padding: "padding",
32
+ paddingTop: "padding-top",
33
+ paddingRight: "padding-right",
34
+ paddingBottom: "padding-bottom",
35
+ paddingLeft: "padding-left",
36
+ margin: "margin",
37
+ marginTop: "margin-top",
38
+ marginRight: "margin-right",
39
+ marginBottom: "margin-bottom",
40
+ marginLeft: "margin-left",
41
+ gap: "gap",
42
+ columnGap: "column-gap",
43
+ rowGap: "row-gap",
44
+ color: "color",
45
+ backgroundColor: "background-color",
46
+ borderColor: "border-color",
47
+ borderRadius: "border-radius",
48
+ borderTopLeftRadius: "border-top-left-radius",
49
+ borderTopRightRadius: "border-top-right-radius",
50
+ borderBottomLeftRadius: "border-bottom-left-radius",
51
+ borderBottomRightRadius: "border-bottom-right-radius",
52
+ fontSize: "font-size",
53
+ fontWeight: "font-weight",
54
+ lineHeight: "line-height"
55
+ };
56
+ export function camelToCssProperty(camel) {
57
+ const css = CAMEL_TO_CSS[camel];
58
+ return (css && TRACKED_PROPERTIES.has(css)) ? css : null;
59
+ }
@@ -166,6 +166,45 @@ export function aggregateTailwind(classNameStrings) {
166
166
  }
167
167
  return Array.from(map.values()).sort((a, b) => b.count - a.count);
168
168
  }
169
+ /**
170
+ * Converts aggregated TailwindObservations to the unified StyleObservation type.
171
+ * Tailwind observations don't carry file/line info — they are class-level aggregates.
172
+ * We emit source: "tailwind" with line: 0 to signal this.
173
+ */
174
+ export function tailwindToStyleObservations(observations, sourceFile = "(tailwind)") {
175
+ return observations.flatMap((obs) => {
176
+ if (obs.kind === "spacing") {
177
+ return [{
178
+ property: "padding",
179
+ value: obs.pxValue != null ? `${obs.pxValue}px` : obs.raw,
180
+ pxValue: obs.pxValue ?? undefined,
181
+ file: sourceFile,
182
+ line: 0,
183
+ source: "tailwind"
184
+ }];
185
+ }
186
+ if (obs.kind === "radius") {
187
+ return [{
188
+ property: "border-radius",
189
+ value: obs.pxValue != null ? `${obs.pxValue}px` : obs.raw,
190
+ pxValue: obs.pxValue ?? undefined,
191
+ file: sourceFile,
192
+ line: 0,
193
+ source: "tailwind"
194
+ }];
195
+ }
196
+ if (obs.kind === "color" && obs.colorToken) {
197
+ return [{
198
+ property: "color",
199
+ value: obs.colorToken,
200
+ file: sourceFile,
201
+ line: 0,
202
+ source: "tailwind"
203
+ }];
204
+ }
205
+ return [];
206
+ });
207
+ }
169
208
  /**
170
209
  * Try to read tailwind.config.{js,ts,mjs,cjs} to override default scales.
171
210
  * We use a conservative approach: don't execute the file (no eval, no
@@ -0,0 +1,93 @@
1
+ export function renderReferencesSection(uiReferences) {
2
+ if (uiReferences.length === 0)
3
+ return "";
4
+ const categoryDescriptions = {
5
+ forms: "buttons, inputs, selects, checkboxes, radios, toggles, textareas",
6
+ navigation: "nav bars, tabs, sidebar, breadcrumbs, dropdown menus, pagination, command palette",
7
+ feedback: "toasts, modals, alerts, progress, loading states, skeletons, empty states",
8
+ surface: "cards, popovers, tooltips, sheets, avatars, separators, badges",
9
+ layout: "container, stack, grid, responsive patterns, sidebar layout",
10
+ "data-display": "tables, lists, code blocks, stats, timelines"
11
+ };
12
+ const lines = [
13
+ "## UI references available",
14
+ "",
15
+ "This project ships with curated component references in `references/`.",
16
+ "Consult these when building UI — they define the quality bar and best",
17
+ "implementations to model.",
18
+ "",
19
+ "- `references/INDEX.md` — start here for category navigation"
20
+ ];
21
+ for (const cat of uiReferences) {
22
+ const desc = categoryDescriptions[cat] ?? cat;
23
+ lines.push(`- \`references/${cat}.md\` — ${desc}`);
24
+ }
25
+ lines.push("", "When asked to build a component, consult the relevant references file first.", "Look for the **Minimum quality bar** checklist — every item must be satisfied", "before the component is considered complete.", "", "");
26
+ return lines.join("\n");
27
+ }
28
+ export function renderRootIndex(config, stats) {
29
+ const name = config.projectName ?? "this project";
30
+ const grid = config.foundations?.grid ?? 8;
31
+ const ctrl = config.foundations?.radius?.control ?? 2;
32
+ const cont = config.foundations?.radius?.container ?? 4;
33
+ const stack = config.stack ?? "css";
34
+ const packs = (config.packs ?? []).join(", ") || "(none)";
35
+ const refsSection = renderReferencesSection(config.uiReferences ?? []);
36
+ return `# AI Agent Context — ${name}
37
+
38
+ > This file is auto-generated by [Whale Igniter](https://github.com/whale-igniter).
39
+ > Do not edit by hand. Run \`whale sync\` after changes to regenerate.
40
+
41
+ ## What this project is
42
+
43
+ You are working in **${name}** — a ${config.projectType ?? "web"} project managed by Whale Igniter.
44
+ Whale maintains machine-readable operational context so AI agents understand
45
+ the design system, conventions and decisions without re-explaining them every session.
46
+
47
+ - **Stack:** ${stack}
48
+ - **Grid:** ${grid}px — all spacing must be a multiple
49
+ - **Border radius:** ${ctrl}px (controls) · ${cont}px (containers)
50
+ - **Active packs:** ${packs}
51
+
52
+ ## What to read
53
+
54
+ Whale stores structured context in two locations:
55
+
56
+ 1. **\`intelligence/\`** — source of truth (JSON). Prefer these for precise data:
57
+ - \`intelligence/refinements.json\` — approved validator overrides
58
+ - \`intelligence/decisions.json\` — architectural and product decisions
59
+ - \`intelligence/components.json\` — component catalog
60
+
61
+ 2. **\`llm-wiki/\`** — human + AI readable markdown rendered from the JSON:
62
+ - \`llm-wiki/FOUNDATIONS.md\` — design tokens and grid rules
63
+ - \`llm-wiki/CONVENTIONS.md\` — coding and design conventions
64
+ - \`llm-wiki/DECISIONS.md\` — decision log in narrative form
65
+ - \`llm-wiki/COMPONENTS.md\` — component catalog
66
+ - \`llm-wiki/WORKFLOWS.md\` — how the team uses Whale day to day
67
+
68
+ **Current status:** ${stats.errors} error(s) · ${stats.warnings} warning(s) · ${stats.decisionCount} decision(s) · ${stats.refinementCount} refinement(s) · ${stats.componentCount} component(s)
69
+
70
+ ${refsSection}## How to work here
71
+
72
+ 1. **Spacing.** Every padding, margin and gap must be a multiple of \`${grid}px\`. No exceptions.
73
+ 2. **Radius.** Buttons, inputs, selects → \`${ctrl}px\`. Cards, modals, sheets → \`${cont}px\`.
74
+ 3. **Color.** Prefer semantic tokens over raw hex. Adding a hex requires a refinement note.
75
+ 4. **Focus.** Every interactive element needs a visible \`:focus-visible\` state.
76
+ 5. **Decisions.** Non-obvious choices → propose \`whale decision\` so future sessions inherit the reasoning.
77
+ 6. **Refinements.** Honor \`intelligence/refinements.json\` — those are intentional team-approved exceptions.
78
+ 7. **Components.** Before building, check \`intelligence/components.json\`. After building, run \`whale component add <name>\`.
79
+
80
+ ## Commands available
81
+
82
+ - \`whale validate\` — run validators, exits non-zero on errors
83
+ - \`whale refine "<note>"\` — record a validator override
84
+ - \`whale decision\` — record an architectural decision (interactive)
85
+ - \`whale component add <name>\` — register a component
86
+ - \`whale sync\` — regenerate this file and the wiki
87
+ - \`whale insights drift <category>\` — review style drift interactively
88
+ - \`whale docs\` — generate human-facing reports
89
+
90
+ ---
91
+ _Last sync: ${new Date().toISOString()}_
92
+ `;
93
+ }
@@ -0,0 +1,45 @@
1
+ export function renderComponents(components) {
2
+ const lines = ["# Component Catalog", ""];
3
+ if (components.length === 0) {
4
+ lines.push("_No components catalogued yet. Use `whale component add <name>` to register one._");
5
+ lines.push("", "_Generated by Whale Igniter._");
6
+ return lines.join("\n");
7
+ }
8
+ const sorted = [...components].sort((a, b) => a.name.localeCompare(b.name));
9
+ // Group by category
10
+ const grouped = new Map();
11
+ for (const c of sorted) {
12
+ const key = c.category ?? "uncategorized";
13
+ if (!grouped.has(key))
14
+ grouped.set(key, []);
15
+ grouped.get(key).push(c);
16
+ }
17
+ for (const [category, entries] of grouped) {
18
+ lines.push(`## ${capitalize(category)}`, "");
19
+ for (const c of entries) {
20
+ lines.push(`### ${c.name}`, "");
21
+ if (c.description)
22
+ lines.push(c.description, "");
23
+ const meta = [];
24
+ if (c.variants?.length)
25
+ meta.push(`Variants: ${c.variants.join(", ")}`);
26
+ if (c.states?.length)
27
+ meta.push(`States: ${c.states.join(", ")}`);
28
+ if (c.tokens?.length)
29
+ meta.push(`Tokens: ${c.tokens.join(", ")}`);
30
+ if (meta.length)
31
+ lines.push(meta.map((m) => `- ${m}`).join("\n"), "");
32
+ if (c.files?.length) {
33
+ lines.push("**Files:**");
34
+ for (const f of c.files)
35
+ lines.push(`- \`${f}\``);
36
+ lines.push("");
37
+ }
38
+ }
39
+ }
40
+ lines.push("_Generated by Whale Igniter._");
41
+ return lines.join("\n");
42
+ }
43
+ function capitalize(s) {
44
+ return s.charAt(0).toUpperCase() + s.slice(1);
45
+ }
@@ -0,0 +1,45 @@
1
+ export function renderConventions(config, refinements) {
2
+ const grid = config.foundations?.grid ?? 8;
3
+ const ctrl = config.foundations?.radius?.control ?? 2;
4
+ const cont = config.foundations?.radius?.container ?? 4;
5
+ const scoped = refinements.filter((r) => r.scope);
6
+ const scopeless = refinements.filter((r) => !r.scope);
7
+ const lines = [
8
+ "# Conventions",
9
+ "",
10
+ "## Confirmed conventions",
11
+ "",
12
+ `- Spacing is a multiple of ${grid}px. No exceptions without a refinement.`,
13
+ `- Controls (buttons, inputs, selects) use border-radius ${ctrl}px.`,
14
+ `- Containers (cards, modals, sheets) use border-radius ${cont}px.`,
15
+ "- Every interactive element has a visible `:focus-visible` state.",
16
+ "- Prefer semantic tokens over raw color values.",
17
+ ""
18
+ ];
19
+ if (scoped.length > 0) {
20
+ lines.push("### Active refinements (approved exceptions)", "");
21
+ lines.push("These have been reviewed and approved. The validator suppresses matching issues.", "");
22
+ for (const r of scoped) {
23
+ const scopeParts = [];
24
+ if (r.scope?.issueType)
25
+ scopeParts.push(`type=\`${r.scope.issueType}\``);
26
+ if (r.scope?.selector)
27
+ scopeParts.push(`selector=\`${r.scope.selector}\``);
28
+ if (r.scope?.file)
29
+ scopeParts.push(`file=\`${r.scope.file}\``);
30
+ const scope = scopeParts.length ? ` _(${scopeParts.join(", ")})_` : "";
31
+ lines.push(`- **${r.timestamp.slice(0, 10)}** — ${r.note}${scope}`);
32
+ }
33
+ lines.push("");
34
+ }
35
+ if (scopeless.length > 0) {
36
+ lines.push("## Open questions", "");
37
+ lines.push("These notes do not yet have a scope — the validator cannot act on them.", "Review and convert to scoped refinements or decisions.", "");
38
+ for (const r of scopeless) {
39
+ lines.push(`> ⚠️ Unresolved — **${r.timestamp.slice(0, 10)}**: ${r.note}`);
40
+ }
41
+ lines.push("");
42
+ }
43
+ lines.push("_Generated by Whale Igniter._");
44
+ return lines.join("\n");
45
+ }
@@ -0,0 +1,34 @@
1
+ const MAX_DETAIL = 500;
2
+ function truncate(text) {
3
+ return text.length > MAX_DETAIL ? text.slice(0, MAX_DETAIL) + "…" : text;
4
+ }
5
+ export function renderDecisions(decisions) {
6
+ const lines = [
7
+ "# Decision Log",
8
+ "",
9
+ "Architectural, product and tooling decisions, most recent first.",
10
+ ""
11
+ ];
12
+ if (decisions.length === 0) {
13
+ lines.push("_No decisions logged yet. Use `whale decision` to record one._");
14
+ }
15
+ else {
16
+ const sorted = [...decisions].sort((a, b) => b.timestamp.localeCompare(a.timestamp));
17
+ for (const d of sorted) {
18
+ lines.push(`## ${d.title}`);
19
+ lines.push("");
20
+ lines.push(`_${d.timestamp.slice(0, 10)} · ${d.category} · ${d.status}_`);
21
+ lines.push("");
22
+ if (d.context) {
23
+ lines.push("**Context**", "", truncate(d.context), "");
24
+ }
25
+ lines.push("**Decision**", "", truncate(d.decision));
26
+ if (d.consequences) {
27
+ lines.push("", "**Consequences**", "", truncate(d.consequences));
28
+ }
29
+ lines.push("", "---", "");
30
+ }
31
+ }
32
+ lines.push("_Generated by Whale Igniter._");
33
+ return lines.join("\n");
34
+ }
@@ -0,0 +1,34 @@
1
+ export function renderFoundations(config, decisions = []) {
2
+ const grid = config.foundations?.grid ?? 8;
3
+ const ctrl = config.foundations?.radius?.control ?? 2;
4
+ const cont = config.foundations?.radius?.container ?? 4;
5
+ const foundationDecisions = decisions.filter((d) => d.category === "design-system" && d.status === "active");
6
+ const lines = [
7
+ "# Foundations",
8
+ "",
9
+ "The invariants of this project. All UI must conform unless explicitly refined.",
10
+ "",
11
+ "## Core values",
12
+ "",
13
+ `| Property | Value | Rule |`,
14
+ `| --- | --- | --- |`,
15
+ `| Grid unit | **${grid}px** | All spacing must be a multiple |`,
16
+ `| Control radius | **${ctrl}px** | Buttons, inputs, selects, toggles |`,
17
+ `| Container radius | **${cont}px** | Cards, modals, sheets, popovers |`,
18
+ ];
19
+ if (config.branding?.accent) {
20
+ lines.push(`| Accent | **${config.branding.accent}** | Brand color |`);
21
+ }
22
+ lines.push("", "## Spacing scale", "");
23
+ lines.push(`Grid: **${grid}px**. Allowed values:`);
24
+ lines.push("");
25
+ lines.push("`" + [1, 2, 3, 4, 5, 6, 8, 10, 12].map((n) => `${n * grid}px`).join(" ") + "`");
26
+ if (foundationDecisions.length > 0) {
27
+ lines.push("", "## Why these foundations", "");
28
+ for (const d of foundationDecisions.slice(0, 5)) {
29
+ lines.push(`- **${d.title}** _(${d.timestamp.slice(0, 10)})_ — ${d.decision}`);
30
+ }
31
+ }
32
+ lines.push("", "_Generated by Whale Igniter._");
33
+ return lines.join("\n");
34
+ }
@@ -0,0 +1,82 @@
1
+ export function renderProject(config, errors, warnings, refinementCount, decisionCount) {
2
+ return `# Project Context
3
+
4
+ - **Project name:** ${config.projectName ?? "(unset)"}
5
+ - **Project type:** ${config.projectType ?? "unspecified"}
6
+ - **Stack:** ${config.stack ?? "css"}
7
+ - **AI targets:** ${(config.aiTargets ?? []).join(", ") || "(none)"}
8
+ - **Active packs:** ${(config.packs ?? []).join(", ") || "(none)"}
9
+ - **Ignited:** ${config.ignited?.at ?? "(not yet)"} (${config.ignited?.mode ?? "—"})
10
+
11
+ ## Current status
12
+
13
+ - Validation errors: ${errors}
14
+ - Validation warnings: ${warnings}
15
+ - Decisions logged: ${decisionCount}
16
+ - Active refinements: ${refinementCount}
17
+
18
+ _Generated by Whale Igniter._
19
+ `;
20
+ }
21
+ export function renderWikiReadme(_config) {
22
+ return `# LLM Wiki
23
+
24
+ This directory is generated by Whale Igniter. It contains a human-and-AI
25
+ readable view of the project's operational context.
26
+
27
+ **Do not edit these files directly** — they are regenerated from
28
+ \`intelligence/*.json\` and \`whale.config.json\` whenever you run
29
+ \`whale sync\`.
30
+
31
+ ## Files
32
+
33
+ - \`PROJECT.md\` — high-level project facts
34
+ - \`FOUNDATIONS.md\` — grid, radius, tokens
35
+ - \`CONVENTIONS.md\` — active conventions and refinements
36
+ - \`DECISIONS.md\` — decision log
37
+ - \`COMPONENTS.md\` — component catalog
38
+ - \`WORKFLOWS.md\` — recommended usage flow
39
+
40
+ ## For AI agents
41
+
42
+ The canonical entry point for AI agents is \`/CLAUDE.md\` at the project root.
43
+ This wiki is the detailed view — start with the root file, drill in here.
44
+
45
+ _Generated by Whale Igniter._
46
+ `;
47
+ }
48
+ export function renderWorkflows(_config) {
49
+ return `# Workflows
50
+
51
+ ## Daily flow
52
+
53
+ \`\`\`
54
+ whale validate # see what's drifting
55
+ whale refine "<note>" # accept an intentional exception
56
+ whale decision # record a non-obvious choice
57
+ whale component add <name> # register a new component
58
+ whale sync # regenerate AI context
59
+ \`\`\`
60
+
61
+ ## When to use what
62
+
63
+ | You want to... | Command |
64
+ | --------------------------------------------- | ----------------------------- |
65
+ | See what's broken | \`whale validate\` |
66
+ | Accept an exception ("we use radius 0 here") | \`whale refine "..."\` |
67
+ | Log an architecture/product choice | \`whale decision\` |
68
+ | Track a new component | \`whale component add\` |
69
+ | Refresh AI context after manual edits | \`whale sync\` |
70
+ | Produce human reports | \`whale docs\` |
71
+ | Review style drift interactively | \`whale insights drift <cat>\`|
72
+
73
+ ## Recommended cadence
74
+
75
+ - **Per change:** run \`validate\` before commit.
76
+ - **Per significant choice:** record a \`decision\` or \`refinement\`.
77
+ - **Per session:** run \`sync\` before handing off to an AI agent.
78
+ - **Per sprint:** review \`whale insights drift spacing\` for accumulated drift.
79
+
80
+ _Generated by Whale Igniter._
81
+ `;
82
+ }
@@ -0,0 +1,25 @@
1
+ import { loadConfig } from "./config.js";
2
+ import { resolveTarget } from "./paths.js";
3
+ export async function getAiAvailability(target) {
4
+ if (process.env.ANTHROPIC_API_KEY) {
5
+ return { available: true, source: "env-anthropic" };
6
+ }
7
+ if (process.env.OPENAI_API_KEY) {
8
+ return { available: true, source: "env-openai" };
9
+ }
10
+ try {
11
+ const ws = resolveTarget(target);
12
+ const config = await loadConfig(ws);
13
+ const provider = config.selene?.provider;
14
+ if (provider === "anthropic" && process.env.ANTHROPIC_API_KEY) {
15
+ return { available: true, source: "config" };
16
+ }
17
+ if (provider === "openai" && process.env.OPENAI_API_KEY) {
18
+ return { available: true, source: "config" };
19
+ }
20
+ }
21
+ catch {
22
+ // config unreadable — no AI
23
+ }
24
+ return { available: false, source: "none" };
25
+ }
@@ -0,0 +1,54 @@
1
+ const PROJECT_TYPE_MAP = {
2
+ "web-app": "web-app",
3
+ "marketing-site": "landing-page",
4
+ "portfolio": "landing-page",
5
+ "internal-tool": "web-app",
6
+ "prototype": "web-app",
7
+ "other": "other"
8
+ };
9
+ const STACK_MAP = {
10
+ "react-ts": "tailwind",
11
+ "vue-ts": "css",
12
+ "svelte-ts": "css",
13
+ "html-css-js": "css",
14
+ "other": "css"
15
+ };
16
+ const AI_TARGETS_MAP = {
17
+ solo: ["claude"],
18
+ small: ["claude", "codex"],
19
+ large: ["claude", "codex", "cursor"]
20
+ };
21
+ export function suggestUiCategories(projectType) {
22
+ const map = {
23
+ "web-app": ["forms", "navigation", "feedback"],
24
+ "marketing-site": ["surface", "layout"],
25
+ "portfolio": ["surface", "layout"],
26
+ "internal-tool": ["forms", "navigation", "feedback", "data-display"],
27
+ "prototype": ["forms", "feedback"],
28
+ "other": ["forms"]
29
+ };
30
+ return map[projectType] ?? ["forms"];
31
+ }
32
+ export function mapWizardAnswers(answers) {
33
+ const projectType = PROJECT_TYPE_MAP[answers.projectType];
34
+ const stack = STACK_MAP[answers.stack];
35
+ const aiTargets = AI_TARGETS_MAP[answers.teamSize];
36
+ const packs = answers.agentPacks && answers.agentPacks.length > 0
37
+ ? answers.agentPacks
38
+ : ["selene"];
39
+ const uiReferences = answers.uiCategories ?? [];
40
+ const config = {
41
+ projectType,
42
+ stack,
43
+ aiTargets: aiTargets,
44
+ packs,
45
+ uiReferences,
46
+ foundations: { grid: 8, radius: { control: 2, container: 4 } },
47
+ branding: { tone: "enterprise-minimal", accent: "cyan" },
48
+ intelligence: { trackPreferences: true, trackRefinements: true, decisionLogging: true }
49
+ };
50
+ const projectIntentNote = answers.projectType === "other" && answers.projectTypeOther
51
+ ? answers.projectTypeOther
52
+ : undefined;
53
+ return { config, projectIntentNote };
54
+ }
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const PACKAGE_VERSION = "1.2.3";
1
+ export const PACKAGE_VERSION = "1.3.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whale-igniter",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "CLI-first operational intelligence. Bootstraps and adopts AI-readable project context so agents like Claude Code, Codex and Cursor understand your design system from the first commit. Includes an MCP server, a file watcher, deterministic insights, and an opt-in AI bridge (Selene).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,7 +18,7 @@
18
18
  "dev": "tsx src/index.ts",
19
19
  "build": "tsc",
20
20
  "start": "node dist/index.js",
21
- "test": "tsx tests/run.ts && tsx tests/mcp-smoke.ts",
21
+ "test": "tsx tests/run.ts && tsx tests/mcp-smoke.ts && tsx tests/extractors.ts && tsx tests/driftAnalyzers.ts",
22
22
  "prepublishOnly": "npm run build && npm test"
23
23
  },
24
24
  "engines": {