whale-igniter 1.1.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/LICENSE +21 -0
- package/README.md +275 -0
- package/dist/analyzer/imports.js +88 -0
- package/dist/analyzer/insights.js +276 -0
- package/dist/commands/add.js +36 -0
- package/dist/commands/adopt.js +180 -0
- package/dist/commands/adoptReview.js +267 -0
- package/dist/commands/component.js +93 -0
- package/dist/commands/createComponent.js +207 -0
- package/dist/commands/decision.js +98 -0
- package/dist/commands/docs.js +34 -0
- package/dist/commands/ignite.js +212 -0
- package/dist/commands/init.js +66 -0
- package/dist/commands/insights.js +123 -0
- package/dist/commands/mcp.js +106 -0
- package/dist/commands/refine.js +36 -0
- package/dist/commands/selene.js +516 -0
- package/dist/commands/sync.js +43 -0
- package/dist/commands/validate.js +48 -0
- package/dist/commands/watch.js +150 -0
- package/dist/commands/wiki.js +21 -0
- package/dist/generators/markdownGenerator.js +112 -0
- package/dist/generators/reportGenerator.js +50 -0
- package/dist/generators/wikiGenerator.js +365 -0
- package/dist/index.js +213 -0
- package/dist/mcp/server.js +404 -0
- package/dist/scanner/componentScanner.js +522 -0
- package/dist/scanner/foundationInferrer.js +174 -0
- package/dist/scanner/tailwindMapper.js +58 -0
- package/dist/scanner/tailwindScanner.js +186 -0
- package/dist/selene/apiClient.js +168 -0
- package/dist/selene/cache.js +68 -0
- package/dist/selene/clipboard.js +56 -0
- package/dist/selene/promptBuilder.js +229 -0
- package/dist/selene/providers.js +67 -0
- package/dist/selene/responseParser.js +149 -0
- package/dist/ui/atoms.js +30 -0
- package/dist/ui/blocks.js +208 -0
- package/dist/ui/capabilities.js +64 -0
- package/dist/ui/index.js +13 -0
- package/dist/ui/symbols.js +41 -0
- package/dist/ui/theme.js +78 -0
- package/dist/utils/components.js +40 -0
- package/dist/utils/config.js +31 -0
- package/dist/utils/decisions.js +32 -0
- package/dist/utils/paths.js +4 -0
- package/dist/utils/proposals.js +61 -0
- package/dist/utils/refinements.js +81 -0
- package/dist/utils/registry.js +45 -0
- package/dist/utils/writeJson.js +6 -0
- package/dist/validators/cssValidator.js +204 -0
- package/dist/version.js +1 -0
- package/docs/ROADMAP.md +206 -0
- package/package.json +76 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal capability detection.
|
|
3
|
+
*
|
|
4
|
+
* The UI layer asks two questions of every output:
|
|
5
|
+
* 1. Can the terminal render ANSI color?
|
|
6
|
+
* 2. Can the terminal render Unicode symbols?
|
|
7
|
+
*
|
|
8
|
+
* Answering "no" to (1) is common in CI logs and when piping. Answering
|
|
9
|
+
* "no" to (2) is rare on modern terminals but happens in containers
|
|
10
|
+
* with C/POSIX locales and in dumb terminals. Chalk handles (1) on its
|
|
11
|
+
* own, so this module mostly exists for (2) plus a global "plain"
|
|
12
|
+
* override.
|
|
13
|
+
*
|
|
14
|
+
* Overrides:
|
|
15
|
+
* - WHALE_PLAIN=1 — disable color AND unicode, useful for CI logs and grep
|
|
16
|
+
* - WHALE_UNICODE=0 — disable unicode but keep color
|
|
17
|
+
* - NO_COLOR / FORCE_COLOR — standard chalk-respected vars (informational only here)
|
|
18
|
+
*/
|
|
19
|
+
const ASCII_LOCALE = /^(C|POSIX)$/i;
|
|
20
|
+
function detectColor() {
|
|
21
|
+
if (process.env.WHALE_PLAIN === "1")
|
|
22
|
+
return false;
|
|
23
|
+
if (process.env.NO_COLOR)
|
|
24
|
+
return false;
|
|
25
|
+
if (process.env.FORCE_COLOR === "0")
|
|
26
|
+
return false;
|
|
27
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0")
|
|
28
|
+
return true;
|
|
29
|
+
return process.stdout.isTTY === true;
|
|
30
|
+
}
|
|
31
|
+
function detectUnicode() {
|
|
32
|
+
if (process.env.WHALE_PLAIN === "1")
|
|
33
|
+
return false;
|
|
34
|
+
if (process.env.WHALE_UNICODE === "0")
|
|
35
|
+
return false;
|
|
36
|
+
if (process.env.LANG && /UTF-?8/i.test(process.env.LANG))
|
|
37
|
+
return true;
|
|
38
|
+
if (process.env.LC_ALL && /UTF-?8/i.test(process.env.LC_ALL))
|
|
39
|
+
return true;
|
|
40
|
+
if (process.env.LC_CTYPE && /UTF-?8/i.test(process.env.LC_CTYPE))
|
|
41
|
+
return true;
|
|
42
|
+
// Common modern-terminal signals.
|
|
43
|
+
if (process.env.WT_SESSION)
|
|
44
|
+
return true;
|
|
45
|
+
if (process.env.TERM_PROGRAM === "vscode")
|
|
46
|
+
return true;
|
|
47
|
+
if (process.env.TERM_PROGRAM === "iTerm.app")
|
|
48
|
+
return true;
|
|
49
|
+
if (process.env.LANG && ASCII_LOCALE.test(process.env.LANG))
|
|
50
|
+
return false;
|
|
51
|
+
// Default: assume Unicode if attached to a TTY.
|
|
52
|
+
return process.stdout.isTTY === true;
|
|
53
|
+
}
|
|
54
|
+
let cached = null;
|
|
55
|
+
export function capabilities() {
|
|
56
|
+
if (cached)
|
|
57
|
+
return cached;
|
|
58
|
+
cached = { color: detectColor(), unicode: detectUnicode() };
|
|
59
|
+
return cached;
|
|
60
|
+
}
|
|
61
|
+
/** Test-only — clear the memo so tests can flip env vars between cases. */
|
|
62
|
+
export function resetCapabilities() {
|
|
63
|
+
cached = null;
|
|
64
|
+
}
|
package/dist/ui/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The UI layer's public surface.
|
|
3
|
+
*
|
|
4
|
+
* import { ui } from "../ui/index.js";
|
|
5
|
+
* console.log(ui.header("Whale Igniter", "ignite"));
|
|
6
|
+
*
|
|
7
|
+
* Everything a command needs lives on `ui`. Individual atoms are
|
|
8
|
+
* importable if needed but `ui.<name>` is the canonical entry point.
|
|
9
|
+
*/
|
|
10
|
+
export { ui } from "./blocks.js";
|
|
11
|
+
export * from "./atoms.js";
|
|
12
|
+
export { glyph } from "./symbols.js";
|
|
13
|
+
export { theme, setTheme } from "./theme.js";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-styled glyphs. Use these when you want the icon + the correct
|
|
3
|
+
* color in one shot, instead of composing them every time.
|
|
4
|
+
*
|
|
5
|
+
* Naming mirrors the symbol roles in theme.ts so the autocomplete is
|
|
6
|
+
* predictable: `glyph.check`, `glyph.cross`, etc.
|
|
7
|
+
*/
|
|
8
|
+
import { theme } from "./theme.js";
|
|
9
|
+
import { accent, success, danger, warning, info as infoColor, muted } from "./atoms.js";
|
|
10
|
+
/**
|
|
11
|
+
* Standalone glyphs. Use when a renderer needs the glyph alone.
|
|
12
|
+
*/
|
|
13
|
+
export const glyph = {
|
|
14
|
+
get diamond() {
|
|
15
|
+
return accent(theme().symbols.diamond);
|
|
16
|
+
},
|
|
17
|
+
get check() {
|
|
18
|
+
return success(theme().symbols.check);
|
|
19
|
+
},
|
|
20
|
+
get cross() {
|
|
21
|
+
return danger(theme().symbols.cross);
|
|
22
|
+
},
|
|
23
|
+
get warn() {
|
|
24
|
+
return warning(theme().symbols.warn);
|
|
25
|
+
},
|
|
26
|
+
get info() {
|
|
27
|
+
return infoColor(theme().symbols.info);
|
|
28
|
+
},
|
|
29
|
+
get bullet() {
|
|
30
|
+
return muted(theme().symbols.bullet);
|
|
31
|
+
},
|
|
32
|
+
get dot() {
|
|
33
|
+
return muted(theme().symbols.dot);
|
|
34
|
+
},
|
|
35
|
+
get arrow() {
|
|
36
|
+
return accent(theme().symbols.arrow);
|
|
37
|
+
},
|
|
38
|
+
get tee() {
|
|
39
|
+
return muted(theme().symbols.tee);
|
|
40
|
+
}
|
|
41
|
+
};
|
package/dist/ui/theme.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme — semantic tokens for the Whale Igniter CLI.
|
|
3
|
+
*
|
|
4
|
+
* The rule: commands describe *intent* (this is a section header,
|
|
5
|
+
* this is a success state, this is dim metadata), not appearance.
|
|
6
|
+
* The theme decides what intent looks like.
|
|
7
|
+
*
|
|
8
|
+
* v1.1 ships one theme. The structure deliberately matches the shape
|
|
9
|
+
* a multi-theme system in v1.2 will need, so swapping themes will be
|
|
10
|
+
* a one-line change in the resolver, not a refactor of every renderer.
|
|
11
|
+
*/
|
|
12
|
+
import chalkLib, { Chalk } from "chalk";
|
|
13
|
+
import { capabilities } from "./capabilities.js";
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the chalk instance once per process. If WHALE_PLAIN is set
|
|
16
|
+
* we force level 0, otherwise we let chalk's auto-detection decide.
|
|
17
|
+
*/
|
|
18
|
+
function resolveChalk() {
|
|
19
|
+
if (process.env.WHALE_PLAIN === "1") {
|
|
20
|
+
return new Chalk({ level: 0 });
|
|
21
|
+
}
|
|
22
|
+
return chalkLib;
|
|
23
|
+
}
|
|
24
|
+
const chalk = resolveChalk();
|
|
25
|
+
/**
|
|
26
|
+
* Pick Unicode or ASCII symbol based on terminal capabilities.
|
|
27
|
+
* Used inline below so the active theme always has the right glyphs.
|
|
28
|
+
*/
|
|
29
|
+
function pick(unicodeGlyph, asciiFallback) {
|
|
30
|
+
return capabilities().unicode ? unicodeGlyph : asciiFallback;
|
|
31
|
+
}
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Whale theme — the only theme that ships in v1.1.
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
const whale = {
|
|
36
|
+
name: "whale",
|
|
37
|
+
identity: {
|
|
38
|
+
accent: chalk.cyan,
|
|
39
|
+
// bold without color — combined with accent it produces "cyan bold"
|
|
40
|
+
// when wrapped through accent(emphasis(...))
|
|
41
|
+
emphasis: chalk.bold
|
|
42
|
+
},
|
|
43
|
+
semantic: {
|
|
44
|
+
success: chalk.green,
|
|
45
|
+
warning: chalk.yellow,
|
|
46
|
+
danger: chalk.red,
|
|
47
|
+
info: chalk.cyan,
|
|
48
|
+
muted: chalk.gray,
|
|
49
|
+
code: chalk.cyan,
|
|
50
|
+
path: chalk.cyan,
|
|
51
|
+
heading: chalk.bold,
|
|
52
|
+
subheading: chalk.bold.gray
|
|
53
|
+
},
|
|
54
|
+
symbols: {
|
|
55
|
+
diamond: pick("◆", "*"),
|
|
56
|
+
check: pick("✓", "v"),
|
|
57
|
+
cross: pick("✗", "x"),
|
|
58
|
+
warn: pick("!", "!"),
|
|
59
|
+
info: pick("·", "-"),
|
|
60
|
+
bullet: pick("•", "-"),
|
|
61
|
+
dot: pick("·", "."),
|
|
62
|
+
arrow: pick("→", "->"),
|
|
63
|
+
rule: pick("─", "-"),
|
|
64
|
+
tee: pick("└", "`-")
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
let active = whale;
|
|
68
|
+
/** Get the active theme. Single global — fine for a CLI process. */
|
|
69
|
+
export function theme() {
|
|
70
|
+
return active;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Replace the active theme. Exists so v1.2 can introduce a `whale theme set`
|
|
74
|
+
* command without touching anything in src/ui/. v1.1 doesn't expose this.
|
|
75
|
+
*/
|
|
76
|
+
export function setTheme(next) {
|
|
77
|
+
active = next;
|
|
78
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
const STORE = "intelligence/components.json";
|
|
5
|
+
export async function loadComponents(target) {
|
|
6
|
+
const file = path.join(target, STORE);
|
|
7
|
+
if (!(await fs.pathExists(file)))
|
|
8
|
+
return [];
|
|
9
|
+
try {
|
|
10
|
+
return await fs.readJson(file);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function saveComponents(target, components) {
|
|
17
|
+
const file = path.join(target, STORE);
|
|
18
|
+
await fs.ensureDir(path.dirname(file));
|
|
19
|
+
await fs.writeJson(file, components, { spaces: 2 });
|
|
20
|
+
}
|
|
21
|
+
export async function upsertComponent(target, input) {
|
|
22
|
+
const components = await loadComponents(target);
|
|
23
|
+
const now = new Date().toISOString();
|
|
24
|
+
const existing = components.find((c) => c.name === input.name);
|
|
25
|
+
if (existing) {
|
|
26
|
+
Object.assign(existing, input, { updatedAt: now });
|
|
27
|
+
await saveComponents(target, components);
|
|
28
|
+
return existing;
|
|
29
|
+
}
|
|
30
|
+
const created = {
|
|
31
|
+
id: randomUUID(),
|
|
32
|
+
addedAt: now,
|
|
33
|
+
updatedAt: now,
|
|
34
|
+
...input,
|
|
35
|
+
name: input.name
|
|
36
|
+
};
|
|
37
|
+
components.push(created);
|
|
38
|
+
await saveComponents(target, components);
|
|
39
|
+
return created;
|
|
40
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
export const DEFAULT_CONFIG = {
|
|
4
|
+
projectType: "landing-page",
|
|
5
|
+
stack: "css",
|
|
6
|
+
aiTargets: ["claude"],
|
|
7
|
+
packs: ["lighthouse", "scribe"],
|
|
8
|
+
foundations: {
|
|
9
|
+
grid: 8,
|
|
10
|
+
radius: { control: 2, container: 4 }
|
|
11
|
+
},
|
|
12
|
+
branding: { tone: "enterprise-minimal", accent: "cyan" },
|
|
13
|
+
intelligence: { trackPreferences: true, trackRefinements: true, decisionLogging: true }
|
|
14
|
+
};
|
|
15
|
+
export async function loadConfig(target) {
|
|
16
|
+
const configPath = path.join(target, "whale.config.json");
|
|
17
|
+
if (!(await fs.pathExists(configPath))) {
|
|
18
|
+
return DEFAULT_CONFIG;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const raw = await fs.readJson(configPath);
|
|
22
|
+
return { ...DEFAULT_CONFIG, ...raw };
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return DEFAULT_CONFIG;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export async function saveConfig(target, config) {
|
|
29
|
+
const configPath = path.join(target, "whale.config.json");
|
|
30
|
+
await fs.writeJson(configPath, config, { spaces: 2 });
|
|
31
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
const STORE = "intelligence/decisions.json";
|
|
5
|
+
export async function loadDecisions(target) {
|
|
6
|
+
const file = path.join(target, STORE);
|
|
7
|
+
if (!(await fs.pathExists(file)))
|
|
8
|
+
return [];
|
|
9
|
+
try {
|
|
10
|
+
return await fs.readJson(file);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function saveDecisions(target, decisions) {
|
|
17
|
+
const file = path.join(target, STORE);
|
|
18
|
+
await fs.ensureDir(path.dirname(file));
|
|
19
|
+
await fs.writeJson(file, decisions, { spaces: 2 });
|
|
20
|
+
}
|
|
21
|
+
export async function appendDecision(target, input) {
|
|
22
|
+
const decisions = await loadDecisions(target);
|
|
23
|
+
const decision = {
|
|
24
|
+
id: randomUUID(),
|
|
25
|
+
timestamp: new Date().toISOString(),
|
|
26
|
+
status: "active",
|
|
27
|
+
...input
|
|
28
|
+
};
|
|
29
|
+
decisions.push(decision);
|
|
30
|
+
await saveDecisions(target, decisions);
|
|
31
|
+
return decision;
|
|
32
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
const STORE_PATH = "intelligence/proposed.json";
|
|
5
|
+
const EMPTY = { scannedAt: null, scannerVersion: "0.8.0", proposals: [] };
|
|
6
|
+
export async function loadProposals(target) {
|
|
7
|
+
const file = path.join(target, STORE_PATH);
|
|
8
|
+
if (!(await fs.pathExists(file)))
|
|
9
|
+
return { ...EMPTY };
|
|
10
|
+
try {
|
|
11
|
+
return await fs.readJson(file);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return { ...EMPTY };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export async function saveProposals(target, store) {
|
|
18
|
+
const file = path.join(target, STORE_PATH);
|
|
19
|
+
await fs.ensureDir(path.dirname(file));
|
|
20
|
+
await fs.writeJson(file, store, { spaces: 2 });
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Deterministic fingerprint so re-running `adopt` doesn't duplicate.
|
|
24
|
+
* We hash kind + identifying fields, not the whole payload — that way
|
|
25
|
+
* a component whose className list changes still has the same
|
|
26
|
+
* fingerprint and we can update it in place.
|
|
27
|
+
*/
|
|
28
|
+
export function fingerprintComponent(name, file) {
|
|
29
|
+
return createHash("sha256").update(`component::${name}::${file}`).digest("hex").slice(0, 16);
|
|
30
|
+
}
|
|
31
|
+
export function fingerprintDecision(title, category) {
|
|
32
|
+
return createHash("sha256").update(`decision::${title}::${category}`).digest("hex").slice(0, 16);
|
|
33
|
+
}
|
|
34
|
+
export function fingerprintFoundations() {
|
|
35
|
+
// There's only ever one foundations proposal at a time.
|
|
36
|
+
return "foundations::singleton";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Upsert a proposal by fingerprint. Returns true if it was newly added,
|
|
40
|
+
* false if it updated an existing entry. Status of an existing reviewed
|
|
41
|
+
* proposal is preserved — we don't re-open accepted/rejected items
|
|
42
|
+
* automatically. The user can re-open via `--reset` (future).
|
|
43
|
+
*/
|
|
44
|
+
export function upsertProposal(store, proposal) {
|
|
45
|
+
const idx = store.proposals.findIndex((p) => p.fingerprint === proposal.fingerprint);
|
|
46
|
+
if (idx === -1) {
|
|
47
|
+
store.proposals.push(proposal);
|
|
48
|
+
return { added: true, preserved: false };
|
|
49
|
+
}
|
|
50
|
+
const existing = store.proposals[idx];
|
|
51
|
+
if (existing.status !== "pending") {
|
|
52
|
+
// User already decided on this. Don't overwrite their decision.
|
|
53
|
+
return { added: false, preserved: true };
|
|
54
|
+
}
|
|
55
|
+
// Update payload/evidence but keep timestamps.
|
|
56
|
+
store.proposals[idx] = { ...proposal, proposedAt: existing.proposedAt };
|
|
57
|
+
return { added: false, preserved: false };
|
|
58
|
+
}
|
|
59
|
+
export function pendingProposals(store) {
|
|
60
|
+
return store.proposals.filter((p) => p.status === "pending");
|
|
61
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
const STORE_FILENAME = "refinements.json";
|
|
4
|
+
const STORE_DIR = "intelligence";
|
|
5
|
+
export async function loadRefinements(target) {
|
|
6
|
+
const file = path.join(target, STORE_DIR, STORE_FILENAME);
|
|
7
|
+
if (!(await fs.pathExists(file)))
|
|
8
|
+
return [];
|
|
9
|
+
try {
|
|
10
|
+
return await fs.readJson(file);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function saveRefinements(target, refinements) {
|
|
17
|
+
const dir = path.join(target, STORE_DIR);
|
|
18
|
+
await fs.ensureDir(dir);
|
|
19
|
+
const file = path.join(dir, STORE_FILENAME);
|
|
20
|
+
await fs.writeJson(file, refinements, { spaces: 2 });
|
|
21
|
+
}
|
|
22
|
+
export async function appendRefinement(target, refinement) {
|
|
23
|
+
const current = await loadRefinements(target);
|
|
24
|
+
current.push(refinement);
|
|
25
|
+
await saveRefinements(target, current);
|
|
26
|
+
}
|
|
27
|
+
// Lightweight natural-language extractor. Recognises hints like:
|
|
28
|
+
// "Button radius should remain 2px for controls" -> issueType: radius-consistency
|
|
29
|
+
// "Spacing of 6px is intentional in legacy/" -> issueType: spacing-scale, file: legacy
|
|
30
|
+
// This is intentionally conservative: it only suppresses when the note
|
|
31
|
+
// clearly references a known issue type. Anything ambiguous becomes a
|
|
32
|
+
// note-only refinement (still logged, but not active).
|
|
33
|
+
export function inferScope(note) {
|
|
34
|
+
const lower = note.toLowerCase();
|
|
35
|
+
const scope = {};
|
|
36
|
+
// Issue type detection
|
|
37
|
+
if (/\bradius\b|\bborder[\s-]?radius\b/.test(lower)) {
|
|
38
|
+
scope.issueType = "radius-consistency";
|
|
39
|
+
}
|
|
40
|
+
else if (/\bspacing\b|\bpadding\b|\bmargin\b|\bgap\b|\bgrid\b/.test(lower)) {
|
|
41
|
+
scope.issueType = "spacing-scale";
|
|
42
|
+
}
|
|
43
|
+
else if (/\bhex\b|\bcolou?r\b/.test(lower)) {
|
|
44
|
+
scope.issueType = "raw-hex-color";
|
|
45
|
+
}
|
|
46
|
+
else if (/\bfocus\b/.test(lower)) {
|
|
47
|
+
scope.issueType = "focus-visible";
|
|
48
|
+
}
|
|
49
|
+
// Selector detection: words like "button", "input", "card"
|
|
50
|
+
const selectorMatch = lower.match(/\b(button|input|select|card|hero|nav|footer|header|modal)s?\b/);
|
|
51
|
+
if (selectorMatch) {
|
|
52
|
+
scope.selector = selectorMatch[1];
|
|
53
|
+
}
|
|
54
|
+
// File scope: "in <path>" or "for <path>"
|
|
55
|
+
const fileMatch = note.match(/\b(?:in|for|inside)\s+([./\w-]+\/[\w./-]+)/);
|
|
56
|
+
if (fileMatch) {
|
|
57
|
+
scope.file = fileMatch[1];
|
|
58
|
+
}
|
|
59
|
+
return Object.keys(scope).length > 0 ? scope : undefined;
|
|
60
|
+
}
|
|
61
|
+
// Check whether a given issue should be suppressed by any active refinement.
|
|
62
|
+
// A refinement suppresses an issue when ALL its specified scope fields match.
|
|
63
|
+
// If a refinement has no scope at all, it does NOT suppress anything — it's
|
|
64
|
+
// just a logged note.
|
|
65
|
+
export function isSuppressed(issue, refinements) {
|
|
66
|
+
for (const r of refinements) {
|
|
67
|
+
if (!r.scope)
|
|
68
|
+
continue;
|
|
69
|
+
if (r.scope.issueType && r.scope.issueType !== issue.type)
|
|
70
|
+
continue;
|
|
71
|
+
if (r.scope.file && !issue.file.includes(r.scope.file))
|
|
72
|
+
continue;
|
|
73
|
+
if (r.scope.selector) {
|
|
74
|
+
if (!issue.selector || !issue.selector.toLowerCase().includes(r.scope.selector))
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
// All present scope fields matched.
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = path.dirname(__filename);
|
|
5
|
+
// The registry lives in the package itself, not in user projects.
|
|
6
|
+
// User projects only reference packs by name in whale.config.json.
|
|
7
|
+
const REGISTRY = {
|
|
8
|
+
lighthouse: {
|
|
9
|
+
name: "lighthouse",
|
|
10
|
+
kind: "executable",
|
|
11
|
+
description: "Accessibility audits, contrast and semantic validation."
|
|
12
|
+
},
|
|
13
|
+
selene: {
|
|
14
|
+
name: "selene",
|
|
15
|
+
kind: "executable",
|
|
16
|
+
description: "AI-assisted UX intelligence and refinement memory."
|
|
17
|
+
},
|
|
18
|
+
forge: {
|
|
19
|
+
name: "forge",
|
|
20
|
+
kind: "generator",
|
|
21
|
+
description: "Implementation specs, dev contracts and handoff.",
|
|
22
|
+
audience: "engineering",
|
|
23
|
+
template: "handoff"
|
|
24
|
+
},
|
|
25
|
+
scribe: {
|
|
26
|
+
name: "scribe",
|
|
27
|
+
kind: "generator",
|
|
28
|
+
description: "Generated docs, recipes and READMEs.",
|
|
29
|
+
audience: "team",
|
|
30
|
+
template: "docs"
|
|
31
|
+
},
|
|
32
|
+
atlas: {
|
|
33
|
+
name: "atlas",
|
|
34
|
+
kind: "generator",
|
|
35
|
+
description: "Product strategy, decision logs and roadmaps.",
|
|
36
|
+
audience: "product",
|
|
37
|
+
template: "strategy"
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
export function getPack(name) {
|
|
41
|
+
return REGISTRY[name];
|
|
42
|
+
}
|
|
43
|
+
export function listPacks() {
|
|
44
|
+
return Object.values(REGISTRY);
|
|
45
|
+
}
|