@aayambansal/squint 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -167,7 +167,12 @@ squint doctor --probe # run every engine end to end, verify auth act
167
167
  pixel-compared with the last (drift as a number), load performance is tracked with
168
168
  deltas (`perf: LCP 812ms (+420ms)`), hardcoded colors get pointed at the nearest
169
169
  design token, and the mechanical anti-slop sweep flags generic-AI tells as
170
- distinctiveness debt in `/review`.
170
+ distinctiveness debt in `/review`. The phantom-class check diffs every DOM class
171
+ against the compiled CSS — hallucinated utilities surface as named problems instead
172
+ of silently unstyled elements.
173
+ - **The design ledger**: `/decide` (plus chosen variants, rollbacks, accepted
174
+ sandboxes) appends to a committed `.squint/design-log.jsonl`; recent decisions ride
175
+ into every ask so they stop getting silently undone between sessions.
171
176
  - **`/btw <question>`** asks about the codebase read-only without touching the main
172
177
  thread's context. `.squint/locks` lists paths the engine must never touch; `/save`
173
178
  exports the transcript as markdown.
@@ -4,7 +4,7 @@ import {
4
4
  hasWebSocket,
5
5
  pixelDiffPct,
6
6
  runFlow
7
- } from "./chunk-7YKO5MMN.js";
7
+ } from "./chunk-Y47X4ENN.js";
8
8
  export {
9
9
  cdpCapture,
10
10
  hasWebSocket,
@@ -1,60 +1,7 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/prompt/brief.ts
4
- import fs from "fs";
5
- import path from "path";
6
- var DEFAULT_BRIEF = `You are acting as a senior product designer and frontend engineer. Treat every change as production work, not a demo.
7
-
8
- Direction before code:
9
- - Commit to one specific visual direction derived from what this product is for, and state it in one sentence before implementing. If the direction could be guessed from the product category alone, sharpen it until it couldn't.
10
- - Choose a color strategy deliberately: restrained (neutrals + one accent), committed (one dominant color owning 30\u201360% of the surface), or a small palette of 3\u20135 named roles. Never purple/violet gradients unless asked. Body text contrast stays at least 4.5:1 \u2014 no light gray body copy "for elegance".
11
- - Typography: at most two families, paired on contrast (display serif + geometric sans, or sans + mono). Never Inter/Roboto/Open Sans/system defaults for display type. Pair weight extremes (300 against 700\u2013900); display sizes jump 3x over body, not 1.5x.
12
-
13
- Tokens are the system:
14
- - Define or extend design tokens (CSS variables / theme config) first, then compose the UI from them. Never scatter literal colors or one-off spacing values through components.
15
- - One spacing rhythm on a 4/8px grid; one type scale with a ratio of at least 1.25 between steps.
16
-
17
- Banned tells \u2014 these read instantly as machine-generated:
18
- - Purple gradient on white; glassmorphism cards; cream/beige page background as a reflex "warmth" move
19
- - Emoji as icons; identical icon-topped card grids; stat banner rows; numbered 01/02/03 section markers
20
- - Tiny all-caps tracked eyebrow labels over every section; gradient text; colored left-border strips on cards
21
- - Centered hero + badge + three feature cards; unmodified component-library defaults
22
- If someone could look at the result and say "AI made that" without doubt, it has failed.
23
-
24
- Craft details:
25
- - Every interactive element gets hover, focus-visible, and active treatment; keyboard focus stays visible.
26
- - Anything that loads data gets loading, empty, and error states.
27
- - Motion: 150\u2013250ms ease-out; one orchestrated entrance with staggered reveals beats scattered micro-interactions; honor prefers-reduced-motion; never leave content invisible until a scroll observer fires.
28
- - Body line length 65\u201375ch; text-wrap: balance on headings.
29
-
30
- Engineering:
31
- - Follow the repo's existing conventions and extend its patterns; new components in new files, small and focused; semantic HTML.
32
- - Responsive from 360px up with no horizontal overflow \u2014 check intermediate widths, not just phone and desktop.
33
- - Let errors surface instead of swallowing them in try/catch; log clearly so failures can be traced.
34
- - Not done until the app builds cleanly, typechecks, and renders without console errors.`;
35
- var FIRST_TURN_ADDENDUM = `This is the opening move on this task: establish the design foundation before building. State the visual direction, set up the tokens/theme first, then build components from them. The first render should feel like a designed product, not a scaffold \u2014 impressive on sight.`;
36
- function loadBrief(cwd) {
37
- const custom = path.join(cwd, ".squint", "brief.md");
38
- try {
39
- const text = fs.readFileSync(custom, "utf8").trim();
40
- if (text.length > 0) return text;
41
- } catch {
42
- }
43
- return DEFAULT_BRIEF;
44
- }
45
- function composePrompt(ask, opts) {
46
- if (opts.noBrief) return ask;
47
- const brief = loadBrief(opts.cwd);
48
- const firstTurn = opts.firstTurn ?? true;
49
- const addendum = firstTurn ? `
50
-
51
- ${FIRST_TURN_ADDENDUM}` : "";
52
- return `${brief}${addendum}
53
-
54
- ## Task
55
-
56
- ${ask}`;
57
- }
2
+ import {
3
+ DEFAULT_BRIEF
4
+ } from "./chunk-WAJXATCO.js";
58
5
 
59
6
  // src/prompt/families.ts
60
7
  var FAMILIES = [
@@ -174,8 +121,6 @@ ${coreStandards()}`;
174
121
  }
175
122
 
176
123
  export {
177
- FIRST_TURN_ADDENDUM,
178
- composePrompt,
179
124
  FAMILIES,
180
125
  getFamily,
181
126
  renderFamilyBrief
@@ -26,6 +26,8 @@ var COMMANDS = [
26
26
  { name: "copy", group: "session", description: "copy the last reply to the clipboard" },
27
27
  { name: "save", group: "session", description: "export the transcript to .squint/transcripts/" },
28
28
  { name: "find", args: "<term>", group: "session", description: "search this session and saved transcripts" },
29
+ { name: "decide", args: "<text>", group: "session", description: "record a design decision; injected into every future ask" },
30
+ { name: "context", group: "session", description: "what squint injects per ask, token-costed, with staleness warnings" },
29
31
  { name: "resume", group: "session", description: "pick up the previous session for this repo" },
30
32
  { name: "clear", group: "session", description: "new session (transcript, totals, persisted state)" },
31
33
  { name: "help", group: "session", description: "list commands" },
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  variantsRoot
4
- } from "./chunk-A64VVTPU.js";
4
+ } from "./chunk-YGSF2TSO.js";
5
5
  import {
6
6
  findChrome,
7
7
  screenshot
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/prompt/brief.ts
4
+ import fs from "fs";
5
+ import path from "path";
6
+ var DEFAULT_BRIEF = `You are acting as a senior product designer and frontend engineer. Treat every change as production work, not a demo.
7
+
8
+ Direction before code:
9
+ - Commit to one specific visual direction derived from what this product is for, and state it in one sentence before implementing. If the direction could be guessed from the product category alone, sharpen it until it couldn't.
10
+ - Choose a color strategy deliberately: restrained (neutrals + one accent), committed (one dominant color owning 30\u201360% of the surface), or a small palette of 3\u20135 named roles. Never purple/violet gradients unless asked. Body text contrast stays at least 4.5:1 \u2014 no light gray body copy "for elegance".
11
+ - Typography: at most two families, paired on contrast (display serif + geometric sans, or sans + mono). Never Inter/Roboto/Open Sans/system defaults for display type. Pair weight extremes (300 against 700\u2013900); display sizes jump 3x over body, not 1.5x.
12
+
13
+ Tokens are the system:
14
+ - Define or extend design tokens (CSS variables / theme config) first, then compose the UI from them. Never scatter literal colors or one-off spacing values through components.
15
+ - One spacing rhythm on a 4/8px grid; one type scale with a ratio of at least 1.25 between steps.
16
+
17
+ Banned tells \u2014 these read instantly as machine-generated:
18
+ - Purple gradient on white; glassmorphism cards; cream/beige page background as a reflex "warmth" move
19
+ - Emoji as icons; identical icon-topped card grids; stat banner rows; numbered 01/02/03 section markers
20
+ - Tiny all-caps tracked eyebrow labels over every section; gradient text; colored left-border strips on cards
21
+ - Centered hero + badge + three feature cards; unmodified component-library defaults
22
+ If someone could look at the result and say "AI made that" without doubt, it has failed.
23
+
24
+ Craft details:
25
+ - Every interactive element gets hover, focus-visible, and active treatment; keyboard focus stays visible.
26
+ - Anything that loads data gets loading, empty, and error states.
27
+ - Motion: 150\u2013250ms ease-out; one orchestrated entrance with staggered reveals beats scattered micro-interactions; honor prefers-reduced-motion; never leave content invisible until a scroll observer fires.
28
+ - Body line length 65\u201375ch; text-wrap: balance on headings.
29
+
30
+ Engineering:
31
+ - Follow the repo's existing conventions and extend its patterns; new components in new files, small and focused; semantic HTML.
32
+ - Responsive from 360px up with no horizontal overflow \u2014 check intermediate widths, not just phone and desktop.
33
+ - Let errors surface instead of swallowing them in try/catch; log clearly so failures can be traced.
34
+ - Not done until the app builds cleanly, typechecks, and renders without console errors.`;
35
+ var FIRST_TURN_ADDENDUM = `This is the opening move on this task: establish the design foundation before building. State the visual direction, set up the tokens/theme first, then build components from them. The first render should feel like a designed product, not a scaffold \u2014 impressive on sight.`;
36
+ function loadBrief(cwd) {
37
+ const custom = path.join(cwd, ".squint", "brief.md");
38
+ try {
39
+ const text = fs.readFileSync(custom, "utf8").trim();
40
+ if (text.length > 0) return text;
41
+ } catch {
42
+ }
43
+ return DEFAULT_BRIEF;
44
+ }
45
+ function composePrompt(ask, opts) {
46
+ if (opts.noBrief) return ask;
47
+ const brief = loadBrief(opts.cwd);
48
+ const firstTurn = opts.firstTurn ?? true;
49
+ const addendum = firstTurn ? `
50
+
51
+ ${FIRST_TURN_ADDENDUM}` : "";
52
+ return `${brief}${addendum}
53
+
54
+ ## Task
55
+
56
+ ${ask}`;
57
+ }
58
+
59
+ export {
60
+ DEFAULT_BRIEF,
61
+ FIRST_TURN_ADDENDUM,
62
+ loadBrief,
63
+ composePrompt
64
+ };
@@ -9,7 +9,7 @@ import {
9
9
  import {
10
10
  cdpCapture,
11
11
  hasWebSocket
12
- } from "./chunk-7YKO5MMN.js";
12
+ } from "./chunk-Y47X4ENN.js";
13
13
 
14
14
  // src/preview/preview.ts
15
15
  import fs from "fs";
@@ -47,7 +47,7 @@ async function captureViewports(cwd, url) {
47
47
  const base = url.replace(/\/+$/, "");
48
48
  if (hasWebSocket()) {
49
49
  try {
50
- const { report, shots: shots2, a11y, slop, narration } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
50
+ const { report, shots: shots2, a11y, slop, narration, phantoms } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
51
51
  const errors2 = [];
52
52
  for (const route of routes.slice(1)) {
53
53
  try {
@@ -62,7 +62,7 @@ async function captureViewports(cwd, url) {
62
62
  errors2.push(`${route}: capture failed`);
63
63
  }
64
64
  }
65
- return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration };
65
+ return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms };
66
66
  } catch {
67
67
  }
68
68
  }
@@ -133,7 +133,7 @@ async function probeRuntime(url, cwd) {
133
133
  async function comparePulse(previous, current) {
134
134
  const chrome = findChrome();
135
135
  if (!chrome || !hasWebSocket()) return null;
136
- const { pixelDiffPct } = await import("./cdp-UGLMW5R5.js");
136
+ const { pixelDiffPct } = await import("./cdp-TPP2RGSR.js");
137
137
  return pixelDiffPct(chrome, previous, current);
138
138
  }
139
139
  function buildRuntimeFixPrompt(report) {
@@ -161,6 +161,16 @@ ${narration.join("\n")}
161
161
 
162
162
  Judge this narration as an experience: does the reading order make sense? do names actually describe their targets? is anything announced as "(no accessible name)"? Fix real incoherence \u2014 this is how non-visual users meet the page.`;
163
163
  }
164
+ function phantomSection(phantoms) {
165
+ if (!phantoms || phantoms.length === 0) return "";
166
+ return `
167
+
168
+ ## Phantom classes (in the DOM, absent from the CSS)
169
+
170
+ ${phantoms.join("\n")}
171
+
172
+ Each of these is an element silently unstyled \u2014 usually a misspelled or version-mismatched utility (Tailwind v3 spellings in a v4 project) or a concatenated class the scanner never compiled. Fix the class names or define the styles.`;
173
+ }
164
174
  function slopSection(findings) {
165
175
  if (!findings || findings.length === 0) return "";
166
176
  return `
@@ -171,13 +181,13 @@ ${findings.join("\n")}
171
181
 
172
182
  These patterns make the page read as template output. Rework them within the committed design direction \u2014 this is style debt, not a defect list.`;
173
183
  }
174
- function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration) {
184
+ function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms) {
175
185
  const list = shots.map((s) => `- ${s.name}: ${s.path}`).join("\n");
176
186
  return `Screenshots of the running app were just captured:
177
187
 
178
188
  ${list}
179
189
 
180
- Read each screenshot and review the rendered UI against the design standards you were given. Check: visual hierarchy and spacing rhythm, typography, color and contrast, alignment, empty-looking or broken regions, and whether the mobile capture shows horizontal overflow or cramped layout. List the concrete issues you can SEE (not hypothetical ones), ranked by visual impact${extra ? `, with special attention to: ${extra}` : ""}. Then fix them and verify the app still builds.${runtimeSection(runtime)}${a11ySection(a11y)}${slopSection(slop)}${narrationSection(narration)}`;
190
+ Read each screenshot and review the rendered UI against the design standards you were given. Check: visual hierarchy and spacing rhythm, typography, color and contrast, alignment, empty-looking or broken regions, and whether the mobile capture shows horizontal overflow or cramped layout. List the concrete issues you can SEE (not hypothetical ones), ranked by visual impact${extra ? `, with special attention to: ${extra}` : ""}. Then fix them and verify the app still builds.${runtimeSection(runtime)}${a11ySection(a11y)}${slopSection(slop)}${narrationSection(narration)}${phantomSection(phantoms)}`;
181
191
  }
182
192
 
183
193
  export {
@@ -329,6 +329,37 @@ var SLOP_AUDIT = `(() => {
329
329
  }
330
330
  return out.slice(0, 8);
331
331
  })()`;
332
+ var PHANTOM_AUDIT = `(() => {
333
+ const defined = new Set();
334
+ for (const sheet of document.styleSheets) {
335
+ let rules; try { rules = sheet.cssRules; } catch { continue; }
336
+ const walk = (list) => {
337
+ for (const rule of list) {
338
+ if (rule.selectorText) {
339
+ for (const m of rule.selectorText.matchAll(/\\.((?:[\\w-]|\\\\.)+)/g)) {
340
+ defined.add(m[1].replace(/\\\\(.)/g, '$1'));
341
+ }
342
+ }
343
+ if (rule.cssRules) walk(rule.cssRules);
344
+ }
345
+ };
346
+ walk(rules);
347
+ }
348
+ if (defined.size < 10) return []; // no CSSOM visibility \u2014 stay silent
349
+ const seen = new Map();
350
+ for (const el of document.querySelectorAll('[class]')) {
351
+ for (const cls of el.classList) {
352
+ if (cls.length < 3) continue;
353
+ if (!defined.has(cls) && !seen.has(cls)) seen.set(cls, el.tagName.toLowerCase());
354
+ }
355
+ }
356
+ const out = [];
357
+ for (const [cls, tag] of seen) {
358
+ out.push(cls + ' (on <' + tag + '>)');
359
+ if (out.length >= 12) break;
360
+ }
361
+ return out;
362
+ })()`;
332
363
  async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, audit = false) {
333
364
  const { child, wsUrl, profileDir } = await launchChrome(chromePath);
334
365
  const report = { consoleErrors: [], pageErrors: [], failedRequests: [] };
@@ -337,6 +368,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
337
368
  let slop = [];
338
369
  let perf = {};
339
370
  let narration = [];
371
+ let phantoms = [];
340
372
  const requests = /* @__PURE__ */ new Map();
341
373
  let connection = null;
342
374
  try {
@@ -409,6 +441,15 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
409
441
  if (Array.isArray(result?.value)) slop = result.value.map(String);
410
442
  } catch {
411
443
  }
444
+ try {
445
+ const { result } = await connection.send(
446
+ "Runtime.evaluate",
447
+ { expression: PHANTOM_AUDIT, returnByValue: true },
448
+ sessionId
449
+ );
450
+ if (Array.isArray(result?.value)) phantoms = result.value.map(String);
451
+ } catch {
452
+ }
412
453
  try {
413
454
  await connection.send("Accessibility.enable", {}, sessionId);
414
455
  const { nodes } = await connection.send("Accessibility.getFullAXTree", {}, sessionId);
@@ -473,7 +514,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
473
514
  child.kill("SIGKILL");
474
515
  setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
475
516
  }
476
- return { report, shots, a11y, slop, perf, narration };
517
+ return { report, shots, a11y, slop, perf, narration, phantoms };
477
518
  }
478
519
 
479
520
  export {
@@ -4,9 +4,11 @@ import {
4
4
  } from "./chunk-VH7OOFQP.js";
5
5
  import {
6
6
  FAMILIES,
7
- FIRST_TURN_ADDENDUM,
8
7
  renderFamilyBrief
9
- } from "./chunk-P3H4N2EN.js";
8
+ } from "./chunk-K5QJMSJH.js";
9
+ import {
10
+ FIRST_TURN_ADDENDUM
11
+ } from "./chunk-WAJXATCO.js";
10
12
 
11
13
  // src/variants/variants.ts
12
14
  import { execFileSync } from "child_process";
@@ -1,25 +1,70 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/prompt/skills.ts
4
- import fs2 from "fs";
5
- import path2 from "path";
4
+ import fs3 from "fs";
5
+ import path3 from "path";
6
6
 
7
- // src/prompt/registry.ts
7
+ // src/session/designLog.ts
8
8
  import fs from "fs";
9
9
  import path from "path";
10
+ function logFile(cwd) {
11
+ return path.join(cwd, ".squint", "design-log.jsonl");
12
+ }
13
+ function appendDecision(cwd, entry) {
14
+ try {
15
+ fs.mkdirSync(path.join(cwd, ".squint"), { recursive: true });
16
+ const record = { ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry };
17
+ fs.appendFileSync(logFile(cwd), JSON.stringify(record) + "\n");
18
+ } catch {
19
+ }
20
+ }
21
+ function loadDecisions(cwd, limit = 8) {
22
+ try {
23
+ const lines = fs.readFileSync(logFile(cwd), "utf8").trim().split("\n");
24
+ const decisions = [];
25
+ for (const line of lines.slice(-limit * 2)) {
26
+ try {
27
+ const parsed = JSON.parse(line);
28
+ if (typeof parsed?.decision === "string" && typeof parsed?.ts === "string") decisions.push(parsed);
29
+ } catch {
30
+ }
31
+ }
32
+ return decisions.slice(-limit);
33
+ } catch {
34
+ return [];
35
+ }
36
+ }
37
+ function decisionsSection(cwd) {
38
+ const decisions = loadDecisions(cwd);
39
+ if (decisions.length === 0) return "";
40
+ const lines = decisions.map((d) => {
41
+ const days = Math.floor((Date.now() - Date.parse(d.ts)) / 864e5);
42
+ const age = days <= 0 ? "today" : `${days}d ago`;
43
+ return `- ${d.decision} (${d.source}, ${age}${d.screenshot ? `, evidence: ${d.screenshot}` : ""})`;
44
+ });
45
+ return `## Design decisions on record
46
+
47
+ ${lines.join("\n")}
48
+
49
+ These were decided deliberately. Do not silently undo them; if a task genuinely requires reversing one, say so explicitly first.`;
50
+ }
51
+
52
+ // src/prompt/registry.ts
53
+ import fs2 from "fs";
54
+ import path2 from "path";
10
55
  function loadComponentInventory(cwd) {
11
56
  let config;
12
57
  try {
13
- config = JSON.parse(fs.readFileSync(path.join(cwd, "components.json"), "utf8"));
58
+ config = JSON.parse(fs2.readFileSync(path2.join(cwd, "components.json"), "utf8"));
14
59
  } catch {
15
60
  return null;
16
61
  }
17
62
  const alias = config.aliases?.ui ?? (config.aliases?.components ? `${config.aliases.components}/ui` : "@/components/ui");
18
63
  const relative = alias.replace(/^@\//, "src/").replace(/^~\//, "");
19
- const uiDir = path.join(cwd, relative);
64
+ const uiDir = path2.join(cwd, relative);
20
65
  let entries;
21
66
  try {
22
- entries = fs.readdirSync(uiDir);
67
+ entries = fs2.readdirSync(uiDir);
23
68
  } catch {
24
69
  return null;
25
70
  }
@@ -55,17 +100,17 @@ function parseSkill(name, raw) {
55
100
  return { name, triggers, body };
56
101
  }
57
102
  function loadSkills(cwd) {
58
- const dir = path2.join(cwd, ".squint", "skills");
103
+ const dir = path3.join(cwd, ".squint", "skills");
59
104
  let entries;
60
105
  try {
61
- entries = fs2.readdirSync(dir).filter((f) => f.endsWith(".md"));
106
+ entries = fs3.readdirSync(dir).filter((f) => f.endsWith(".md"));
62
107
  } catch {
63
108
  return [];
64
109
  }
65
110
  const skills = [];
66
111
  for (const entry of entries.sort()) {
67
112
  try {
68
- const skill = parseSkill(entry.replace(/\.md$/, ""), fs2.readFileSync(path2.join(dir, entry), "utf8"));
113
+ const skill = parseSkill(entry.replace(/\.md$/, ""), fs3.readFileSync(path3.join(dir, entry), "utf8"));
69
114
  if (skill) skills.push(skill);
70
115
  } catch {
71
116
  }
@@ -74,7 +119,7 @@ function loadSkills(cwd) {
74
119
  }
75
120
  function loadRules(cwd) {
76
121
  try {
77
- const text = fs2.readFileSync(path2.join(cwd, ".squint", "rules.md"), "utf8").trim();
122
+ const text = fs3.readFileSync(path3.join(cwd, ".squint", "rules.md"), "utf8").trim();
78
123
  return text.length > 0 ? text : null;
79
124
  } catch {
80
125
  return null;
@@ -86,7 +131,7 @@ function matchSkills(skills, ask) {
86
131
  }
87
132
  function loadLocks(cwd) {
88
133
  try {
89
- return fs2.readFileSync(path2.join(cwd, ".squint", "locks"), "utf8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
134
+ return fs3.readFileSync(path3.join(cwd, ".squint", "locks"), "utf8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
90
135
  } catch {
91
136
  return [];
92
137
  }
@@ -97,6 +142,8 @@ function enrich(cwd, ask) {
97
142
  if (rules) parts.push(`## Project rules (always apply)
98
143
 
99
144
  ${rules}`);
145
+ const decisions = decisionsSection(cwd);
146
+ if (decisions) parts.push(decisions);
100
147
  const inventory = loadComponentInventory(cwd);
101
148
  if (inventory) parts.push(inventorySection(inventory));
102
149
  const locks = loadLocks(cwd);
@@ -124,6 +171,11 @@ ${parts.join("\n\n")}` : "",
124
171
  }
125
172
 
126
173
  export {
174
+ appendDecision,
175
+ loadDecisions,
176
+ decisionsSection,
177
+ loadComponentInventory,
178
+ inventorySection,
127
179
  parseSkill,
128
180
  loadSkills,
129
181
  loadRules,
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  completeCommand
4
- } from "./chunk-SVKV77TO.js";
4
+ } from "./chunk-O7NWGLS3.js";
5
5
  import {
6
6
  applySandbox,
7
7
  discardSandbox,
@@ -22,13 +22,13 @@ import {
22
22
  buildFixPrompt,
23
23
  detectDevCommand,
24
24
  screenshotVariants
25
- } from "./chunk-F6JZSXEO.js";
25
+ } from "./chunk-QCHBDP46.js";
26
26
  import {
27
27
  applyVariant,
28
28
  cleanVariants,
29
29
  listVariants,
30
30
  runVariants
31
- } from "./chunk-A64VVTPU.js";
31
+ } from "./chunk-YGSF2TSO.js";
32
32
  import {
33
33
  buildGatePrompt,
34
34
  detectFastGates,
@@ -42,7 +42,7 @@ import {
42
42
  comparePulse,
43
43
  probeRuntime,
44
44
  runtimeSummary
45
- } from "./chunk-UTBTLCUO.js";
45
+ } from "./chunk-WCLQZFOR.js";
46
46
  import {
47
47
  clearState,
48
48
  loadState,
@@ -58,13 +58,15 @@ import {
58
58
  detectEngines,
59
59
  getEngine
60
60
  } from "./chunk-KVYGPLWW.js";
61
- import "./chunk-7YKO5MMN.js";
61
+ import "./chunk-Y47X4ENN.js";
62
62
  import {
63
+ appendDecision,
63
64
  enrich
64
- } from "./chunk-H5K55LXY.js";
65
+ } from "./chunk-YT6K2YIS.js";
66
+ import "./chunk-K5QJMSJH.js";
65
67
  import {
66
68
  composePrompt
67
- } from "./chunk-P3H4N2EN.js";
69
+ } from "./chunk-WAJXATCO.js";
68
70
 
69
71
  // src/cli.tsx
70
72
  import { Command } from "commander";
@@ -238,7 +240,7 @@ function registerEnv(program2) {
238
240
  }
239
241
  }
240
242
  const { findChrome: findChrome2 } = await import("./chrome-SBV3H77F.js");
241
- const { hasWebSocket } = await import("./cdp-UGLMW5R5.js");
243
+ const { hasWebSocket } = await import("./cdp-TPP2RGSR.js");
242
244
  const chrome = findChrome2();
243
245
  console.log(
244
246
  chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
@@ -262,7 +264,7 @@ import pc3 from "picocolors";
262
264
  function registerProject(program2) {
263
265
  const skillsCommand = program2.command("skills").description("Project knowledge injected into asks (.squint/rules.md + .squint/skills/)");
264
266
  skillsCommand.command("list").description("Show always-on rules and trigger-matched skills").action(async () => {
265
- const { loadRules, loadSkills } = await import("./skills-ODOVNE6E.js");
267
+ const { loadRules, loadSkills } = await import("./skills-GTTF4PNU.js");
266
268
  const cwd = process.cwd();
267
269
  const rules = loadRules(cwd);
268
270
  console.log(
@@ -304,7 +306,7 @@ function registerProject(program2) {
304
306
  program2.command("brief").description("Set a committed design direction for this project (.squint/brief.md)").argument("[family]", "aesthetic family id (omit to list)").option("--force", "overwrite an existing project brief").action(async (familyId, options) => {
305
307
  const fs3 = await import("fs");
306
308
  const nodePath = await import("path");
307
- const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-2G5SLLY7.js");
309
+ const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-3ARYRBMH.js");
308
310
  if (!familyId) {
309
311
  console.log(pc3.bold("Aesthetic families") + pc3.dim(" \u2014 squint brief <id>\n"));
310
312
  for (const family2 of FAMILIES) {
@@ -379,7 +381,7 @@ function registerProject(program2) {
379
381
  process.exitCode = 1;
380
382
  return;
381
383
  }
382
- const { runVariants: runVariants2, cleanVariants: cleanVariants2 } = await import("./variants-3ZSINJGX.js");
384
+ const { runVariants: runVariants2, cleanVariants: cleanVariants2 } = await import("./variants-3IEP7DFY.js");
383
385
  const config = loadConfig(defaultPaths(cwd));
384
386
  const engineId = resolveEngineId(config, options.engine);
385
387
  const engine = getEngine(engineId);
@@ -397,7 +399,7 @@ function registerProject(program2) {
397
399
  );
398
400
  const succeeded = runs.filter((r) => r.result.ok);
399
401
  if (options.shots && succeeded.length > 0) {
400
- const { screenshotVariants: screenshotVariants2 } = await import("./shots-E2AWBDCN.js");
402
+ const { screenshotVariants: screenshotVariants2 } = await import("./shots-OKWOYF7F.js");
401
403
  console.log(pc3.dim("capturing screenshots\u2026"));
402
404
  const shots = await screenshotVariants2(cwd, succeeded.map((r) => r.variant));
403
405
  for (const shot of shots) {
@@ -412,7 +414,7 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
412
414
  }
413
415
  );
414
416
  variantsCommand.command("list").description("List generated variants").action(async () => {
415
- const { listVariants: listVariants2 } = await import("./variants-3ZSINJGX.js");
417
+ const { listVariants: listVariants2 } = await import("./variants-3IEP7DFY.js");
416
418
  const ids = listVariants2(process.cwd());
417
419
  if (ids.length === 0) {
418
420
  console.log(pc3.dim('no variants \u2014 squint variants gen <n> "<ask>"'));
@@ -421,7 +423,7 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
421
423
  for (const id of ids) console.log(id);
422
424
  });
423
425
  variantsCommand.command("apply").description("Apply one variant\u2019s changes to the main tree and discard the rest").argument("<id>", "family id of the winning variant").action(async (id) => {
424
- const { applyVariant: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-3ZSINJGX.js");
426
+ const { applyVariant: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-3IEP7DFY.js");
425
427
  const cwd = process.cwd();
426
428
  const result = applyVariant2(cwd, id);
427
429
  if (!result.ok) {
@@ -433,7 +435,7 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
433
435
  console.log(pc3.green(`\u2713 applied ${id} to the working tree`) + pc3.dim(" \u2014 review with git diff"));
434
436
  });
435
437
  variantsCommand.command("clean").description("Discard all variants").action(async () => {
436
- const { cleanVariants: cleanVariants2 } = await import("./variants-3ZSINJGX.js");
438
+ const { cleanVariants: cleanVariants2 } = await import("./variants-3IEP7DFY.js");
437
439
  const count = cleanVariants2(process.cwd());
438
440
  console.log(pc3.dim(`removed ${count} variant(s)`));
439
441
  });
@@ -461,7 +463,7 @@ function registerQuality(program2) {
461
463
  if (results.some((r) => !r.ok)) process.exitCode = 1;
462
464
  });
463
465
  program2.command("shot").description("Screenshot a running app at mobile/tablet/desktop viewports (+ .squint/routes)").argument("<url>", "URL of the running app (e.g. http://localhost:5173)").action(async (url) => {
464
- const { captureViewports: captureViewports2 } = await import("./preview-6MML2NEG.js");
466
+ const { captureViewports: captureViewports2 } = await import("./preview-SKHIXVCE.js");
465
467
  const result = await captureViewports2(process.cwd(), url);
466
468
  if (!result) {
467
469
  console.error(pc4.red("\u2717 no Chrome/Chromium found"));
@@ -823,7 +825,7 @@ ${question}`,
823
825
  return;
824
826
  }
825
827
  await this.runTurn(
826
- buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration),
828
+ buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms),
827
829
  `\u{1F441} polish round ${round}/${rounds}`
828
830
  );
829
831
  }
@@ -1050,6 +1052,26 @@ ${driftSummary(drift)}`);
1050
1052
  }
1051
1053
  } catch {
1052
1054
  }
1055
+ try {
1056
+ const { rulePackSummary, scanRulePacks } = await import("./rulepacks-JOA675OJ.js");
1057
+ const findings = scanRulePacks(this.execCwd(), checkpoint.snapshot.stashHash ?? "HEAD");
1058
+ const hard = findings.filter((f) => f.hard);
1059
+ if (hard.length > 0) {
1060
+ this.addProblem(
1061
+ "gates",
1062
+ `${hard.length} class(es)/idiom(s) from an older toolchain major
1063
+ ${rulePackSummary(hard)}`,
1064
+ `This project's toolchain is newer than the patterns just written. Apply the exact renames below \u2014 do not downgrade dependencies or add compatibility configs:
1065
+ ${rulePackSummary(hard)}`
1066
+ );
1067
+ }
1068
+ const soft = findings.filter((f) => !f.hard);
1069
+ if (soft.length > 0) {
1070
+ this.push("status", `renamed-scale traps (verify intent):
1071
+ ${rulePackSummary(soft)}`);
1072
+ }
1073
+ } catch {
1074
+ }
1053
1075
  }
1054
1076
  runHook(this.opts.cwd, "on-turn-end", {
1055
1077
  cost: String(result.costUsd ?? 0),
@@ -1141,7 +1163,7 @@ ${errors.slice(-5).join("\n")}`);
1141
1163
  const captureResult = await this.capture();
1142
1164
  if (captureResult) {
1143
1165
  await this.runTurn(
1144
- buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration),
1166
+ buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms),
1145
1167
  "\u{1F441} auto-review rendered UI"
1146
1168
  );
1147
1169
  }
@@ -1189,6 +1211,7 @@ ${errors.slice(-5).join("\n")}`);
1189
1211
  const result = restoreSnapshot(this.opts.cwd, checkpoint.snapshot);
1190
1212
  if (result.restored) {
1191
1213
  const dropped = this.checkpoints.length - index;
1214
+ appendDecision(this.opts.cwd, { decision: `rejected and rolled back: "${checkpoint.label}"`, source: "restore" });
1192
1215
  this.checkpoints = this.checkpoints.slice(0, index);
1193
1216
  this.push(
1194
1217
  "status",
@@ -1280,6 +1303,20 @@ ${errors.slice(-5).join("\n")}`);
1280
1303
  this.push("status", "runtime clean \u2014 no console errors, exceptions, or failed requests");
1281
1304
  }
1282
1305
  }
1306
+ if (result.phantoms && result.phantoms.length > 0) {
1307
+ this.push("error", `phantom classes: ${result.phantoms.length} (in the DOM, absent from CSS)
1308
+ ${result.phantoms.slice(0, 5).join("\n")}`);
1309
+ this.addProblem(
1310
+ "runtime",
1311
+ `${result.phantoms.length} phantom class(es) \u2014 elements silently unstyled`,
1312
+ `These class tokens appear in the DOM but match no CSS rule, so the elements are silently unstyled. Usually a misspelled or version-mismatched utility (e.g. Tailwind v3 spellings in a v4 project) or a concatenated class the scanner never compiled:
1313
+
1314
+ ${result.phantoms.join("\n")}
1315
+
1316
+ Fix the class names or define the styles, then confirm visually.`
1317
+ );
1318
+ this.push("status", "/fix sends open problems to the engine");
1319
+ }
1283
1320
  if (result.slop && result.slop.length > 0) {
1284
1321
  this.push("status", `distinctiveness: ${result.slop.length} tell(s)
1285
1322
  ${result.slop.join("\n")}`);
@@ -1395,6 +1432,27 @@ They are objective defects, not style preferences.`
1395
1432
  }
1396
1433
  break;
1397
1434
  }
1435
+ case "context": {
1436
+ import("./contextDoctor-M3R3PICP.js").then(({ contextReport, formatContextReport }) => {
1437
+ this.push("status", formatContextReport(contextReport(this.execCwd())));
1438
+ }).catch((error) => {
1439
+ this.push("status", `context report failed: ${error instanceof Error ? error.message : String(error)}`);
1440
+ });
1441
+ break;
1442
+ }
1443
+ case "decide": {
1444
+ if (!arg) {
1445
+ this.push("status", "usage: /decide <the decision> \u2014 recorded in .squint/design-log.jsonl and injected into every future ask");
1446
+ break;
1447
+ }
1448
+ appendDecision(this.opts.cwd, {
1449
+ decision: arg,
1450
+ source: "decide",
1451
+ screenshot: this.lastPulse ? ".squint/preview/pulse.png" : void 0
1452
+ });
1453
+ this.push("status", `decision recorded: ${arg}`);
1454
+ break;
1455
+ }
1398
1456
  case "find": {
1399
1457
  if (!arg) {
1400
1458
  this.push("status", "usage: /find <term> \u2014 searches this session and saved transcripts");
@@ -1491,8 +1549,8 @@ They are objective defects, not style preferences.`
1491
1549
  this.push("error", "no Chrome/Chromium found for flows");
1492
1550
  return;
1493
1551
  }
1494
- const { runFlow } = await import("./cdp-UGLMW5R5.js");
1495
- const { previewDir } = await import("./preview-6MML2NEG.js");
1552
+ const { runFlow } = await import("./cdp-TPP2RGSR.js");
1553
+ const { previewDir } = await import("./preview-SKHIXVCE.js");
1496
1554
  this.push("status", `replaying ${flows.length} flow(s)\u2026`);
1497
1555
  this.notify({ running: true, runStartedAt: Date.now() });
1498
1556
  const failures = [];
@@ -1630,7 +1688,7 @@ They are objective defects, not style preferences.`
1630
1688
  const result = await this.capture();
1631
1689
  if (result) {
1632
1690
  await this.runTurn(
1633
- buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration),
1691
+ buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms),
1634
1692
  `\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
1635
1693
  );
1636
1694
  }
@@ -1709,6 +1767,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
1709
1767
  discardSandbox(this.opts.cwd);
1710
1768
  this.notify({ sandbox: false });
1711
1769
  this.resetDevServer();
1770
+ appendDecision(this.opts.cwd, { decision: "accepted the sandboxed session onto the real tree", source: "sandbox" });
1712
1771
  this.push("status", "sandbox applied to the real tree \u2014 review with git diff");
1713
1772
  } else {
1714
1773
  this.push("error", applied.detail ?? "apply failed");
@@ -1750,6 +1809,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
1750
1809
  const applied = applyVariant(this.opts.cwd, id);
1751
1810
  if (applied.ok) {
1752
1811
  cleanVariants(this.opts.cwd);
1812
+ appendDecision(this.opts.cwd, { decision: `chose the ${id} direction over the other variants`, source: "variant" });
1753
1813
  this.push("status", `applied ${id} to the working tree \u2014 review with git diff`);
1754
1814
  } else {
1755
1815
  this.push("error", applied.detail ?? "apply failed");
@@ -1814,7 +1874,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
1814
1874
  this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
1815
1875
  break;
1816
1876
  case "help": {
1817
- void import("./commands-DE5Q7VWY.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1877
+ void import("./commands-VDICJJT2.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1818
1878
  break;
1819
1879
  }
1820
1880
  case "quit":
@@ -2433,7 +2493,7 @@ function registerTui(program2) {
2433
2493
  }
2434
2494
 
2435
2495
  // src/cli.tsx
2436
- var VERSION = true ? "0.4.1" : "0.0.0-dev";
2496
+ var VERSION = true ? "0.4.3" : "0.0.0-dev";
2437
2497
  var program = new Command();
2438
2498
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
2439
2499
  registerRun(program);
@@ -3,7 +3,7 @@ import {
3
3
  COMMANDS,
4
4
  commandHelp,
5
5
  completeCommand
6
- } from "./chunk-SVKV77TO.js";
6
+ } from "./chunk-O7NWGLS3.js";
7
7
  export {
8
8
  COMMANDS,
9
9
  commandHelp,
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ decisionsSection,
4
+ inventorySection,
5
+ loadComponentInventory,
6
+ loadDecisions,
7
+ loadLocks,
8
+ loadRules,
9
+ loadSkills
10
+ } from "./chunk-YT6K2YIS.js";
11
+ import {
12
+ loadBrief
13
+ } from "./chunk-WAJXATCO.js";
14
+
15
+ // src/quality/contextDoctor.ts
16
+ import fs from "fs";
17
+ import path from "path";
18
+ var tokens = (text) => Math.ceil(text.length / 4);
19
+ function contextReport(cwd) {
20
+ const lines = [];
21
+ const warnings = [];
22
+ const brief = loadBrief(cwd);
23
+ const custom = fs.existsSync(path.join(cwd, ".squint", "brief.md"));
24
+ lines.push({ source: custom ? "brief (.squint/brief.md)" : "brief (built-in)", tokens: tokens(brief), when: "first turn" });
25
+ if (tokens(brief) > 1500) warnings.push(`the brief is ${tokens(brief)} tokens \u2014 past ~1500 the engine starts skimming; distill it`);
26
+ const rules = loadRules(cwd);
27
+ if (rules) {
28
+ lines.push({ source: "rules (.squint/rules.md)", tokens: tokens(rules), when: "every ask" });
29
+ if (tokens(rules) > 800) warnings.push(`rules.md is ${tokens(rules)} tokens of always-on context \u2014 move situational parts into triggered skills`);
30
+ }
31
+ const decisions = decisionsSection(cwd);
32
+ if (decisions) {
33
+ lines.push({ source: `design ledger (${loadDecisions(cwd).length} recent decisions)`, tokens: tokens(decisions), when: "every ask" });
34
+ }
35
+ const inventory = loadComponentInventory(cwd);
36
+ if (inventory) {
37
+ lines.push({ source: "component inventory", tokens: tokens(inventorySection(inventory)), when: "every ask" });
38
+ }
39
+ const locks = loadLocks(cwd);
40
+ if (locks.length > 0) {
41
+ lines.push({ source: `locks (${locks.length} paths)`, tokens: tokens(locks.join("\n")) + 40, when: "every ask" });
42
+ for (const lock of locks) {
43
+ if (!fs.existsSync(path.join(cwd, lock))) warnings.push(`stale lock: ${lock} no longer exists \u2014 remove it from .squint/locks`);
44
+ }
45
+ }
46
+ for (const skill of loadSkills(cwd)) {
47
+ lines.push({
48
+ source: `skill: ${skill.name}`,
49
+ tokens: tokens(skill.body),
50
+ when: `when the ask mentions ${skill.triggers.map((t) => `"${t}"`).join(", ")}`
51
+ });
52
+ const generic = skill.triggers.filter((t) => t.length <= 3);
53
+ if (generic.length > 0) {
54
+ warnings.push(`skill "${skill.name}" has trigger(s) ${generic.map((t) => `"${t}"`).join(", ")} short enough to match almost any ask \u2014 make them more specific`);
55
+ }
56
+ }
57
+ const totalAlways = lines.filter((l) => l.when === "every ask").reduce((sum, l) => sum + l.tokens, 0);
58
+ if (totalAlways > 3e3) warnings.push(`~${totalAlways} tokens ride on every single ask \u2014 that is real money and real attention; trim the always-on set`);
59
+ return { lines, warnings, totalAlways };
60
+ }
61
+ function formatContextReport(report) {
62
+ const width = Math.max(...report.lines.map((l) => l.source.length), 10);
63
+ const rows = report.lines.map((l) => ` ${l.source.padEnd(width)} ~${String(l.tokens).padStart(5)} tok ${l.when}`);
64
+ const out = [`what squint injects (estimates):`, ...rows, ` ${"always-on total".padEnd(width)} ~${String(report.totalAlways).padStart(5)} tok every ask`];
65
+ if (report.warnings.length > 0) {
66
+ out.push("", "warnings:", ...report.warnings.map((w) => ` \u26A0 ${w}`));
67
+ }
68
+ return out.join("\n");
69
+ }
70
+ export {
71
+ contextReport,
72
+ formatContextReport
73
+ };
@@ -3,7 +3,8 @@ import {
3
3
  FAMILIES,
4
4
  getFamily,
5
5
  renderFamilyBrief
6
- } from "./chunk-P3H4N2EN.js";
6
+ } from "./chunk-K5QJMSJH.js";
7
+ import "./chunk-WAJXATCO.js";
7
8
  export {
8
9
  FAMILIES,
9
10
  getFamily,
@@ -10,11 +10,11 @@ import {
10
10
  probeRuntime,
11
11
  routeShotName,
12
12
  runtimeSummary
13
- } from "./chunk-UTBTLCUO.js";
13
+ } from "./chunk-WCLQZFOR.js";
14
14
  import "./chunk-O2S6PAJE.js";
15
15
  import "./chunk-IMDRXXFU.js";
16
16
  import "./chunk-KVYGPLWW.js";
17
- import "./chunk-7YKO5MMN.js";
17
+ import "./chunk-Y47X4ENN.js";
18
18
  export {
19
19
  VIEWPORTS,
20
20
  buildReviewPrompt,
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/quality/rulepacks.ts
4
+ import { execFileSync } from "child_process";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ var TAILWIND_V4_RULES = [
8
+ {
9
+ re: /\bbg-gradient-to-(t|tr|r|br|b|bl|l|tl)\b/g,
10
+ hint: (m) => `${m} is v3 \u2014 v4 renamed it bg-linear-to-${m.split("-").pop()}`,
11
+ hard: true
12
+ },
13
+ {
14
+ re: /\bflex-(shrink|grow)(-\d+)?\b/g,
15
+ hint: (m) => `${m} is v3 \u2014 v4 uses ${m.replace("flex-", "")}`,
16
+ hard: true
17
+ },
18
+ {
19
+ re: /\boverflow-ellipsis\b/g,
20
+ hint: () => "overflow-ellipsis is v3 \u2014 v4 uses text-ellipsis",
21
+ hard: true
22
+ },
23
+ {
24
+ re: /\bdecoration-(slice|clone)\b/g,
25
+ hint: (m) => `${m} is v3 \u2014 v4 uses box-${m}`,
26
+ hard: true
27
+ },
28
+ {
29
+ re: /\b(shadow|rounded|blur|drop-shadow)-sm\b/g,
30
+ hint: (m) => `${m} means one step larger in v4 (the scale shifted; v3 ${m} is now ${m.replace(/-sm$/, "-xs")}) \u2014 verify the intent`,
31
+ hard: false
32
+ },
33
+ {
34
+ re: /\boutline-none\b/g,
35
+ hint: () => "outline-none changed meaning in v4 (now outline-style: none; the old invisible-but-accessible reset is outline-hidden) \u2014 verify the intent",
36
+ hard: false
37
+ }
38
+ ];
39
+ var VITE_RULES = [
40
+ {
41
+ re: /\bsplitVendorChunkPlugin\b/g,
42
+ hint: () => "splitVendorChunkPlugin was removed \u2014 use build.rollupOptions.output.manualChunks",
43
+ hard: true
44
+ },
45
+ {
46
+ re: /\boptimizeDeps:\s*\{[^}]*esbuildOptions\b|\besbuildOptions\b/g,
47
+ hint: () => "optimizeDeps.esbuildOptions is esbuild-era \u2014 Rolldown Vite uses optimizeDeps.rollupOptions",
48
+ hard: false
49
+ }
50
+ ];
51
+ function readPackageJson(cwd) {
52
+ try {
53
+ return JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+ function depMajor(pkg, name) {
59
+ if (!pkg) return null;
60
+ for (const key of ["dependencies", "devDependencies"]) {
61
+ const deps = pkg[key];
62
+ const range = deps?.[name];
63
+ const major = range?.match(/(\d+)/)?.[1];
64
+ if (major) return Number.parseInt(major, 10);
65
+ }
66
+ return null;
67
+ }
68
+ function detectTailwindMajor(cwd) {
69
+ const pkg = readPackageJson(cwd);
70
+ const major = depMajor(pkg, "tailwindcss");
71
+ if (major !== null) return major;
72
+ if (depMajor(pkg, "@tailwindcss/vite") !== null) return 4;
73
+ return null;
74
+ }
75
+ function detectViteMajor(cwd) {
76
+ return depMajor(readPackageJson(cwd), "vite");
77
+ }
78
+ function scanRulePacks(cwd, source) {
79
+ const packs = [];
80
+ const tailwind = detectTailwindMajor(cwd);
81
+ if (tailwind !== null && tailwind >= 4) packs.push({ rules: TAILWIND_V4_RULES });
82
+ const vite = detectViteMajor(cwd);
83
+ if (vite !== null && vite >= 6) packs.push({ rules: VITE_RULES, files: /vite\.config\.[cm]?[jt]s$/ });
84
+ if (packs.length === 0) return [];
85
+ let diff;
86
+ try {
87
+ diff = execFileSync("git", ["diff", "-U0", source, "--", "*.tsx", "*.jsx", "*.ts", "*.js", "*.html", "*.css", "*.vue", "*.svelte"], {
88
+ cwd,
89
+ encoding: "utf8",
90
+ stdio: ["ignore", "pipe", "pipe"]
91
+ });
92
+ } catch {
93
+ return [];
94
+ }
95
+ const findings = [];
96
+ const seen = /* @__PURE__ */ new Set();
97
+ let currentFile = "";
98
+ for (const line of diff.split("\n")) {
99
+ if (line.startsWith("+++ b/")) {
100
+ currentFile = line.slice(6);
101
+ if (tailwind !== null && tailwind >= 4 && /(^|\/)tailwind\.config\.[cm]?[jt]s$/.test(currentFile)) {
102
+ findings.push({
103
+ file: currentFile,
104
+ match: "tailwind.config",
105
+ hint: "Tailwind v4 is CSS-first \u2014 configure with @theme in your CSS, not a tailwind.config file",
106
+ hard: true
107
+ });
108
+ }
109
+ continue;
110
+ }
111
+ if (!line.startsWith("+") || line.startsWith("+++")) continue;
112
+ for (const pack of packs) {
113
+ if (pack.files && !pack.files.test(currentFile)) continue;
114
+ for (const rule of pack.rules) {
115
+ for (const match of line.matchAll(rule.re)) {
116
+ const key = `${currentFile}:${match[0]}`;
117
+ if (seen.has(key)) continue;
118
+ seen.add(key);
119
+ findings.push({ file: currentFile, match: match[0], hint: rule.hint(match[0]), hard: rule.hard });
120
+ if (findings.length >= 12) return findings;
121
+ }
122
+ }
123
+ }
124
+ }
125
+ return findings;
126
+ }
127
+ function rulePackSummary(findings) {
128
+ return findings.map((f) => ` ${f.file}: ${f.hint}`).join("\n");
129
+ }
130
+ export {
131
+ detectTailwindMajor,
132
+ detectViteMajor,
133
+ rulePackSummary,
134
+ scanRulePacks
135
+ };
@@ -1,12 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  screenshotVariants
4
- } from "./chunk-F6JZSXEO.js";
5
- import "./chunk-A64VVTPU.js";
4
+ } from "./chunk-QCHBDP46.js";
5
+ import "./chunk-YGSF2TSO.js";
6
6
  import "./chunk-VH7OOFQP.js";
7
7
  import "./chunk-IMDRXXFU.js";
8
8
  import "./chunk-KVYGPLWW.js";
9
- import "./chunk-P3H4N2EN.js";
9
+ import "./chunk-K5QJMSJH.js";
10
+ import "./chunk-WAJXATCO.js";
10
11
  export {
11
12
  screenshotVariants
12
13
  };
@@ -6,7 +6,7 @@ import {
6
6
  loadSkills,
7
7
  matchSkills,
8
8
  parseSkill
9
- } from "./chunk-H5K55LXY.js";
9
+ } from "./chunk-YT6K2YIS.js";
10
10
  export {
11
11
  enrich,
12
12
  loadLocks,
@@ -9,10 +9,11 @@ import {
9
9
  runVariants,
10
10
  variantPrompt,
11
11
  variantsRoot
12
- } from "./chunk-A64VVTPU.js";
12
+ } from "./chunk-YGSF2TSO.js";
13
13
  import "./chunk-VH7OOFQP.js";
14
14
  import "./chunk-KVYGPLWW.js";
15
- import "./chunk-P3H4N2EN.js";
15
+ import "./chunk-K5QJMSJH.js";
16
+ import "./chunk-WAJXATCO.js";
16
17
  export {
17
18
  MAX_VARIANTS,
18
19
  applyVariant,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Lovable for your terminal — a frontend harness on top of Claude Code, Codex, and other coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",