jig-ui 0.26.2 → 0.27.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/dist/index.js CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import { existsSync as existsSync29, readFileSync as readFileSync38 } from "fs";
5
+ import { existsSync as existsSync29, readFileSync as readFileSync39 } from "fs";
6
6
  import { homedir as homedir3 } from "os";
7
- import { join as join45 } from "path";
7
+ import { join as join47 } from "path";
8
8
 
9
9
  // src/paths.ts
10
10
  import { existsSync } from "fs";
@@ -1699,8 +1699,8 @@ function sectionsOf(entries) {
1699
1699
  }
1700
1700
 
1701
1701
  // src/commands/check.ts
1702
- import { existsSync as existsSync18, readFileSync as readFileSync23 } from "fs";
1703
- import { join as join27 } from "path";
1702
+ import { existsSync as existsSync18, readdirSync as readdirSync11, readFileSync as readFileSync24 } from "fs";
1703
+ import { join as join29 } from "path";
1704
1704
 
1705
1705
  // src/check/files.ts
1706
1706
  import { execFileSync } from "child_process";
@@ -1952,6 +1952,7 @@ function formatReport(findings, meta) {
1952
1952
  ` ${meta.unscanned.count} file(s) were not scanned (${exts}) \u2014 no style extraction exists for them, so any CSS they carry is invisible to this check and a clean result above does not cover it.`
1953
1953
  );
1954
1954
  }
1955
+ if (meta.tailwindSkipped) lines.push(` Tailwind classes not checked: ${meta.tailwindSkipped}.`);
1955
1956
  if (meta.modeUnwired && meta.modeUnwired.length > 0) {
1956
1957
  lines.push("");
1957
1958
  for (const p of meta.modeUnwired) lines.push(` \u26A0 ${p.message}.`);
@@ -2279,6 +2280,52 @@ function brandDecidedGaps(projectRoot) {
2279
2280
  return gaps;
2280
2281
  }
2281
2282
 
2283
+ // src/check/tailwind-dead.ts
2284
+ import { readFileSync as readFileSync16 } from "fs";
2285
+ import { dirname as dirname6, join as join21 } from "path";
2286
+
2287
+ // src/tailwind/ask.ts
2288
+ import { execFileSync as execFileSync2 } from "child_process";
2289
+ import { createRequire } from "module";
2290
+ import { join as join20 } from "path";
2291
+ import { pathToFileURL } from "url";
2292
+ function tailwindNode(projectRoot) {
2293
+ const from = createRequire(join20(projectRoot, "package.json"));
2294
+ try {
2295
+ return from.resolve("@tailwindcss/node");
2296
+ } catch {
2297
+ }
2298
+ for (const plugin of ["@tailwindcss/vite", "@tailwindcss/postcss", "@tailwindcss/cli"]) {
2299
+ try {
2300
+ return createRequire(from.resolve(`${plugin}/package.json`)).resolve("@tailwindcss/node");
2301
+ } catch {
2302
+ }
2303
+ }
2304
+ return void 0;
2305
+ }
2306
+ function askTailwind(projectRoot, module, body, input) {
2307
+ const code = [
2308
+ "import { readFileSync } from 'node:fs';",
2309
+ `const { __unstable__loadDesignSystem: loadDesignSystem } = await import(${JSON.stringify(pathToFileURL(module).href)});`,
2310
+ "const input = JSON.parse(readFileSync(0, 'utf8'));",
2311
+ "const answer = (value) => process.stdout.write(JSON.stringify(value));",
2312
+ body
2313
+ ].join("\n");
2314
+ const out = execFileSync2(process.execPath, ["--input-type=module", "-e", code], {
2315
+ cwd: projectRoot,
2316
+ input: JSON.stringify(input),
2317
+ encoding: "utf8",
2318
+ timeout: 6e4,
2319
+ maxBuffer: 64 * 1024 * 1024,
2320
+ stdio: ["pipe", "pipe", "pipe"]
2321
+ });
2322
+ return JSON.parse(out);
2323
+ }
2324
+ function whyNot(err) {
2325
+ const text = String(err.stderr || err.message).trim();
2326
+ return (text.split("\n").find((l) => /error/i.test(l)) ?? text.split("\n")[0] ?? "it did not answer").slice(0, 160);
2327
+ }
2328
+
2282
2329
  // src/check/ext.ts
2283
2330
  var CSS_EXTENSIONS = [".css", ".scss", ".less"];
2284
2331
  function hasExtension(file, exts) {
@@ -2383,40 +2430,138 @@ function isReaderText(file) {
2383
2430
  return hasExtension(file, STYLE_HOST_EXTENSIONS);
2384
2431
  }
2385
2432
 
2386
- // src/check/length.ts
2387
- var LENGTH_UNITS = [
2388
- "px",
2389
- "rem",
2390
- "em",
2391
- "pt",
2392
- "vh",
2393
- "vw",
2394
- "vmin",
2395
- "vmax",
2396
- "ch",
2397
- "ex"
2433
+ // src/check/styles.ts
2434
+ function isStyleHost(file) {
2435
+ return hasExtension(file, STYLE_HOST_EXTENSIONS);
2436
+ }
2437
+ function isMarkupHost(file) {
2438
+ return hasExtension(file, MARKUP_HOSTS);
2439
+ }
2440
+ var MARKUP_HOSTS = [
2441
+ ".html",
2442
+ ".htm",
2443
+ ".vue",
2444
+ ".svelte",
2445
+ ".astro",
2446
+ ".php",
2447
+ ".erb",
2448
+ ".twig",
2449
+ ".hbs",
2450
+ ".mdx",
2451
+ ".jsx",
2452
+ ".tsx",
2453
+ ".asp",
2454
+ ".aspx",
2455
+ ".ascx",
2456
+ ".master",
2457
+ ".cshtml",
2458
+ ".vbhtml",
2459
+ ".razor",
2460
+ ".jsp",
2461
+ ".jspx",
2462
+ ".eex",
2463
+ ".heex",
2464
+ ".leex",
2465
+ ".ejs",
2466
+ ".njk",
2467
+ ".liquid",
2468
+ ".mustache",
2469
+ ".vm",
2470
+ ".ftl",
2471
+ ".jinja",
2472
+ ".jinja2",
2473
+ ".j2"
2398
2474
  ];
2399
- var UNITS = LENGTH_UNITS.join("|");
2400
- var LENGTH_RE = new RegExp(`^-?\\d*\\.?\\d+(?:${UNITS})$`, "i");
2401
- var LENGTH_SCAN_RE = new RegExp(`(-?\\d*\\.?\\d+)(${UNITS})`, "gi");
2402
- function isExcludedLength(value, unit) {
2403
- if (value === 0) return true;
2404
- return unit.toLowerCase() === "px" && (Math.abs(value) === 1 || Math.abs(value) === 2);
2475
+ var INDENTED_SYNTAXES = {
2476
+ ".pug": { blockMarker: /^(\s*)style\.?\s*$/, attr: /\bstyle\s*=\s*(["'])([\s\S]*?)\1/g },
2477
+ ".jade": { blockMarker: /^(\s*)style\.?\s*$/, attr: /\bstyle\s*=\s*(["'])([\s\S]*?)\1/g },
2478
+ ".haml": {
2479
+ blockMarker: /^(\s*)(?::css|%style)\s*$/,
2480
+ attr: /(?::style\s*=>|\bstyle\s*:)\s*(["'])([\s\S]*?)\1/g
2481
+ },
2482
+ ".slim": {
2483
+ blockMarker: /^(\s*)(?:css:|style:)\s*$/,
2484
+ attr: /\bstyle\s*=\s*(["'])([\s\S]*?)\1/g
2485
+ }
2486
+ };
2487
+ function indentedSyntaxFor(file) {
2488
+ const lower = file.toLowerCase();
2489
+ const ext = Object.keys(INDENTED_SYNTAXES).find((e) => lower.endsWith(e));
2490
+ return ext ? INDENTED_SYNTAXES[ext] : void 0;
2405
2491
  }
2406
- function hasHardCodedLength(value) {
2407
- LENGTH_SCAN_RE.lastIndex = 0;
2408
- let m;
2409
- while (m = LENGTH_SCAN_RE.exec(value)) {
2410
- if (!isExcludedLength(parseFloat(m[1]), m[2])) return true;
2492
+ function indentedSpans(source, syntax) {
2493
+ const spans = [];
2494
+ const lines = source.split("\n");
2495
+ const offsets = [];
2496
+ let at = 0;
2497
+ for (const line of lines) {
2498
+ offsets.push(at);
2499
+ at += line.length + 1;
2411
2500
  }
2412
- return false;
2501
+ for (let i = 0; i < lines.length; i++) {
2502
+ const marker = syntax.blockMarker.exec(lines[i]);
2503
+ if (!marker) continue;
2504
+ const markerIndent = marker[1].length;
2505
+ let end = i;
2506
+ for (let j = i + 1; j < lines.length; j++) {
2507
+ if (lines[j].trim() === "") {
2508
+ end = j;
2509
+ continue;
2510
+ }
2511
+ const indent = lines[j].length - lines[j].trimStart().length;
2512
+ if (indent <= markerIndent) break;
2513
+ end = j;
2514
+ }
2515
+ if (end === i) continue;
2516
+ const start = offsets[i + 1];
2517
+ const last = offsets[end] + lines[end].length;
2518
+ spans.push({ start, end: last });
2519
+ i = end;
2520
+ }
2521
+ return spans;
2413
2522
  }
2414
-
2415
- // src/check/tailwind.ts
2416
- var CLASS_ATTR = /\b(?:class|className)\s*=\s*/g;
2417
- function readBraced(source, open) {
2523
+ var STYLE_BLOCK = /<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi;
2524
+ var STYLE_ATTR = /\bstyle\s*=\s*(["'])([\s\S]*?)\1/gi;
2525
+ var CSS_TAG = /\b(?:styled(?:\.\w+|\([^)]*\))(?:\s*\.\w+\([^)]*\))*|css|createGlobalStyle|keyframes|injectGlobal)\s*`/g;
2526
+ function readTemplate(source, openBacktick) {
2527
+ let i = openBacktick + 1;
2528
+ const start = i;
2529
+ while (i < source.length) {
2530
+ const ch = source[i];
2531
+ if (ch === "\\") {
2532
+ i += 2;
2533
+ continue;
2534
+ }
2535
+ if (ch === "`") return { start, end: i };
2536
+ if (ch === "$" && source[i + 1] === "{") {
2537
+ let depth = 1;
2538
+ i += 2;
2539
+ while (i < source.length && depth > 0) {
2540
+ if (source[i] === "\\") {
2541
+ i += 2;
2542
+ continue;
2543
+ }
2544
+ if (source[i] === "{") depth++;
2545
+ else if (source[i] === "}") depth--;
2546
+ else if (source[i] === "`") {
2547
+ const inner = readTemplate(source, i);
2548
+ if (!inner) return null;
2549
+ i = inner.end;
2550
+ }
2551
+ i++;
2552
+ }
2553
+ continue;
2554
+ }
2555
+ i++;
2556
+ }
2557
+ return null;
2558
+ }
2559
+ var STYLE_OBJECT = /\bstyle\s*=\s*\{\{/g;
2560
+ function readObject(source, open) {
2418
2561
  let depth = 0;
2419
- for (let i = open; i < source.length; i++) {
2562
+ let i = open + 1;
2563
+ const start = open + 2;
2564
+ while (i < source.length) {
2420
2565
  const ch = source[i];
2421
2566
  if (ch === '"' || ch === "'" || ch === "`") {
2422
2567
  const quote = ch;
@@ -2425,100 +2570,82 @@ function readBraced(source, open) {
2425
2570
  if (source[i] === "\\") i++;
2426
2571
  i++;
2427
2572
  }
2573
+ i++;
2428
2574
  continue;
2429
2575
  }
2430
2576
  if (ch === "{") depth++;
2431
- else if (ch === "}" && --depth === 0) return source.slice(open + 1, i);
2577
+ else if (ch === "}") {
2578
+ depth--;
2579
+ if (depth === 0) return { start, end: i };
2580
+ }
2581
+ i++;
2432
2582
  }
2433
2583
  return null;
2434
2584
  }
2435
- function classAttributeValues(source) {
2436
- const out = [];
2437
- const lineAt = (offset) => source.slice(0, offset).split("\n").length;
2438
- for (const m of source.matchAll(CLASS_ATTR)) {
2439
- const at = m.index + m[0].length;
2440
- const opener = source[at];
2441
- if (opener === '"' || opener === "'") {
2442
- const end = source.indexOf(opener, at + 1);
2443
- if (end !== -1) out.push({ classes: source.slice(at + 1, end), line: lineAt(at) });
2444
- continue;
2445
- }
2446
- if (opener !== "{") continue;
2447
- const raw = readBraced(source, at);
2448
- if (raw === null) continue;
2449
- const literals = [...raw.matchAll(/(["'`])([\s\S]*?)\1/g)].map(
2450
- (q) => q[2].replace(/\$\{[\s\S]*?\}/g, " ")
2451
- );
2452
- if (literals.length > 0) out.push({ classes: literals.join(" ").trim(), line: lineAt(at) });
2585
+ function styleObjectToCss(body) {
2586
+ let out = "";
2587
+ for (const ch of body) {
2588
+ if (ch === '"' || ch === "'" || ch === "`") out += " ";
2589
+ else if (ch === ",") out += ";";
2590
+ else out += ch;
2453
2591
  }
2454
2592
  return out;
2455
2593
  }
2456
- var ARBITRARY = /(?:^|\s)(?:[\w-]+:)*([a-z][\w-]*)-\[([^\]\s]+)\]/gi;
2457
- var COLOUR = /^(?:#[0-9a-f]{3,8}|(?:rgba?|hsla?|oklch|oklab|lab|lch|color)\()/i;
2458
- var LENGTH_PARTS = /^(-?\d*\.?\d+)([a-z]+)$/i;
2459
- function arbitraryValues(classes) {
2460
- const out = [];
2461
- for (const m of classes.matchAll(ARBITRARY)) {
2462
- const [, utility, rawValue] = m;
2463
- const value = rawValue.replace(/_/g, " ");
2464
- if (/var\(|--/.test(value)) continue;
2465
- if (COLOUR.test(value)) {
2466
- out.push({ utility, value, kind: "colour" });
2467
- continue;
2594
+ function maskNonStyleRegions(source, file) {
2595
+ if (hasExtension(file, CSS_EXTENSIONS)) return source;
2596
+ if (!isStyleHost(file)) return blank(source);
2597
+ const spans = [];
2598
+ const indented = indentedSyntaxFor(file);
2599
+ if (indented) {
2600
+ spans.push(...indentedSpans(source, indented));
2601
+ for (const m of source.matchAll(indented.attr)) {
2602
+ const valueStart = m.index + m[0].indexOf(m[1]) + 1;
2603
+ spans.push({ start: valueStart, end: valueStart + m[2].length, braced: true });
2468
2604
  }
2469
- if (!LENGTH_RE.test(value)) continue;
2470
- const parts = LENGTH_PARTS.exec(value);
2471
- if (isExcludedLength(parseFloat(parts[1]), parts[2])) continue;
2472
- out.push({ utility, value, kind: "length" });
2605
+ return applySpans(source, spans);
2473
2606
  }
2474
- return out;
2607
+ if (hasExtension(file, MARKUP_HOSTS)) {
2608
+ for (const m of source.matchAll(STYLE_BLOCK)) {
2609
+ const bodyStart = m.index + m[0].indexOf(">") + 1;
2610
+ spans.push({ start: bodyStart, end: bodyStart + m[1].length });
2611
+ }
2612
+ for (const m of source.matchAll(STYLE_ATTR)) {
2613
+ const valueStart = m.index + m[0].indexOf(m[1]) + 1;
2614
+ spans.push({ start: valueStart, end: valueStart + m[2].length, braced: true });
2615
+ }
2616
+ }
2617
+ for (const m of source.matchAll(CSS_TAG)) {
2618
+ const backtick = m.index + m[0].length - 1;
2619
+ const span = readTemplate(source, backtick);
2620
+ if (span) spans.push({ ...span, braced: true });
2621
+ }
2622
+ for (const m of source.matchAll(STYLE_OBJECT)) {
2623
+ const span = readObject(source, m.index + m[0].length - 2);
2624
+ if (span) spans.push({ ...span, transform: styleObjectToCss, braced: true });
2625
+ }
2626
+ return applySpans(source, spans);
2475
2627
  }
2476
- var PALETTE = {
2477
- white: "#ffffff",
2478
- black: "#000000",
2479
- "gray-50": "#f9fafb",
2480
- "gray-100": "#f3f4f6",
2481
- "gray-200": "#e5e7eb",
2482
- "gray-300": "#d1d5db",
2483
- "gray-400": "#9ca3af",
2484
- "gray-500": "#6b7280",
2485
- "gray-600": "#4b5563",
2486
- "gray-700": "#374151",
2487
- "gray-800": "#1f2937",
2488
- "gray-900": "#111827",
2489
- "gray-950": "#030712",
2490
- "slate-50": "#f8fafc",
2491
- "slate-400": "#94a3b8",
2492
- "slate-500": "#64748b",
2493
- "slate-800": "#1e293b",
2494
- "slate-900": "#0f172a",
2495
- "slate-950": "#020617",
2496
- "zinc-400": "#a1a1aa",
2497
- "zinc-500": "#71717a",
2498
- "zinc-900": "#18181b",
2499
- "neutral-400": "#a3a3a3",
2500
- "neutral-500": "#737373",
2501
- "neutral-900": "#171717",
2502
- "stone-400": "#a8a29e",
2503
- "stone-500": "#78716c",
2504
- "stone-900": "#1c1917"
2505
- };
2506
- function palettePairs(classes) {
2507
- let background;
2508
- let foreground;
2509
- for (const token of classes.split(/\s+/)) {
2510
- const bare2 = token.replace(/^(?:[\w-]+:)*/, "");
2511
- const bg = /^bg-(.+)$/.exec(bare2);
2512
- const fg = /^text-(.+)$/.exec(bare2);
2513
- if (bg && PALETTE[bg[1]]) background = PALETTE[bg[1]];
2514
- if (fg && PALETTE[fg[1]]) foreground = PALETTE[fg[1]];
2628
+ function applySpans(source, spans) {
2629
+ if (spans.length === 0) return blank(source);
2630
+ const out = blank(source).split("");
2631
+ for (const span of spans) {
2632
+ const text = source.slice(span.start, span.end);
2633
+ const rendered = span.transform ? span.transform(text) : text;
2634
+ for (let i = 0; i < rendered.length; i++) {
2635
+ out[span.start + i] = rendered[i];
2636
+ }
2637
+ if (span.braced && span.start > 0 && span.end < out.length) {
2638
+ out[span.start - 1] = "{";
2639
+ out[span.end] = "}";
2640
+ }
2515
2641
  }
2516
- return background && foreground ? [{ background, foreground }] : [];
2642
+ return out.join("");
2643
+ }
2644
+ function blank(source) {
2645
+ let out = "";
2646
+ for (const ch of source) out += ch === "\n" ? "\n" : " ";
2647
+ return out;
2517
2648
  }
2518
-
2519
- // src/check/run.ts
2520
- import { readFileSync as readFileSync17 } from "fs";
2521
- import { join as join21 } from "path";
2522
2649
 
2523
2650
  // src/check/css.ts
2524
2651
  function maskComments(source) {
@@ -2650,138 +2777,40 @@ function sourceLine(source, line) {
2650
2777
  return (lines[line - 1] ?? "").trim();
2651
2778
  }
2652
2779
 
2653
- // src/check/styles.ts
2654
- function isStyleHost(file) {
2655
- return hasExtension(file, STYLE_HOST_EXTENSIONS);
2656
- }
2657
- function isMarkupHost(file) {
2658
- return hasExtension(file, MARKUP_HOSTS);
2659
- }
2660
- var MARKUP_HOSTS = [
2661
- ".html",
2662
- ".htm",
2663
- ".vue",
2664
- ".svelte",
2665
- ".astro",
2666
- ".php",
2667
- ".erb",
2668
- ".twig",
2669
- ".hbs",
2670
- ".mdx",
2671
- ".jsx",
2672
- ".tsx",
2673
- ".asp",
2674
- ".aspx",
2675
- ".ascx",
2676
- ".master",
2677
- ".cshtml",
2678
- ".vbhtml",
2679
- ".razor",
2680
- ".jsp",
2681
- ".jspx",
2682
- ".eex",
2683
- ".heex",
2684
- ".leex",
2685
- ".ejs",
2686
- ".njk",
2687
- ".liquid",
2688
- ".mustache",
2689
- ".vm",
2690
- ".ftl",
2691
- ".jinja",
2692
- ".jinja2",
2693
- ".j2"
2694
- ];
2695
- var INDENTED_SYNTAXES = {
2696
- ".pug": { blockMarker: /^(\s*)style\.?\s*$/, attr: /\bstyle\s*=\s*(["'])([\s\S]*?)\1/g },
2697
- ".jade": { blockMarker: /^(\s*)style\.?\s*$/, attr: /\bstyle\s*=\s*(["'])([\s\S]*?)\1/g },
2698
- ".haml": {
2699
- blockMarker: /^(\s*)(?::css|%style)\s*$/,
2700
- attr: /(?::style\s*=>|\bstyle\s*:)\s*(["'])([\s\S]*?)\1/g
2701
- },
2702
- ".slim": {
2703
- blockMarker: /^(\s*)(?:css:|style:)\s*$/,
2704
- attr: /\bstyle\s*=\s*(["'])([\s\S]*?)\1/g
2705
- }
2706
- };
2707
- function indentedSyntaxFor(file) {
2708
- const lower = file.toLowerCase();
2709
- const ext = Object.keys(INDENTED_SYNTAXES).find((e) => lower.endsWith(e));
2710
- return ext ? INDENTED_SYNTAXES[ext] : void 0;
2711
- }
2712
- function indentedSpans(source, syntax) {
2713
- const spans = [];
2714
- const lines = source.split("\n");
2715
- const offsets = [];
2716
- let at = 0;
2717
- for (const line of lines) {
2718
- offsets.push(at);
2719
- at += line.length + 1;
2720
- }
2721
- for (let i = 0; i < lines.length; i++) {
2722
- const marker = syntax.blockMarker.exec(lines[i]);
2723
- if (!marker) continue;
2724
- const markerIndent = marker[1].length;
2725
- let end = i;
2726
- for (let j = i + 1; j < lines.length; j++) {
2727
- if (lines[j].trim() === "") {
2728
- end = j;
2729
- continue;
2730
- }
2731
- const indent = lines[j].length - lines[j].trimStart().length;
2732
- if (indent <= markerIndent) break;
2733
- end = j;
2734
- }
2735
- if (end === i) continue;
2736
- const start = offsets[i + 1];
2737
- const last = offsets[end] + lines[end].length;
2738
- spans.push({ start, end: last });
2739
- i = end;
2740
- }
2741
- return spans;
2742
- }
2743
- var STYLE_BLOCK = /<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi;
2744
- var STYLE_ATTR = /\bstyle\s*=\s*(["'])([\s\S]*?)\1/gi;
2745
- var CSS_TAG = /\b(?:styled(?:\.\w+|\([^)]*\))(?:\s*\.\w+\([^)]*\))*|css|createGlobalStyle|keyframes|injectGlobal)\s*`/g;
2746
- function readTemplate(source, openBacktick) {
2747
- let i = openBacktick + 1;
2748
- const start = i;
2749
- while (i < source.length) {
2750
- const ch = source[i];
2751
- if (ch === "\\") {
2752
- i += 2;
2753
- continue;
2754
- }
2755
- if (ch === "`") return { start, end: i };
2756
- if (ch === "$" && source[i + 1] === "{") {
2757
- let depth = 1;
2758
- i += 2;
2759
- while (i < source.length && depth > 0) {
2760
- if (source[i] === "\\") {
2761
- i += 2;
2762
- continue;
2763
- }
2764
- if (source[i] === "{") depth++;
2765
- else if (source[i] === "}") depth--;
2766
- else if (source[i] === "`") {
2767
- const inner = readTemplate(source, i);
2768
- if (!inner) return null;
2769
- i = inner.end;
2770
- }
2771
- i++;
2772
- }
2773
- continue;
2774
- }
2775
- i++;
2780
+ // src/check/length.ts
2781
+ var LENGTH_UNITS = [
2782
+ "px",
2783
+ "rem",
2784
+ "em",
2785
+ "pt",
2786
+ "vh",
2787
+ "vw",
2788
+ "vmin",
2789
+ "vmax",
2790
+ "ch",
2791
+ "ex"
2792
+ ];
2793
+ var UNITS = LENGTH_UNITS.join("|");
2794
+ var LENGTH_RE = new RegExp(`^-?\\d*\\.?\\d+(?:${UNITS})$`, "i");
2795
+ var LENGTH_SCAN_RE = new RegExp(`(-?\\d*\\.?\\d+)(${UNITS})`, "gi");
2796
+ function isExcludedLength(value, unit) {
2797
+ if (value === 0) return true;
2798
+ return unit.toLowerCase() === "px" && (Math.abs(value) === 1 || Math.abs(value) === 2);
2799
+ }
2800
+ function hasHardCodedLength(value) {
2801
+ LENGTH_SCAN_RE.lastIndex = 0;
2802
+ let m;
2803
+ while (m = LENGTH_SCAN_RE.exec(value)) {
2804
+ if (!isExcludedLength(parseFloat(m[1]), m[2])) return true;
2776
2805
  }
2777
- return null;
2806
+ return false;
2778
2807
  }
2779
- var STYLE_OBJECT = /\bstyle\s*=\s*\{\{/g;
2780
- function readObject(source, open) {
2808
+
2809
+ // src/check/tailwind.ts
2810
+ var CLASS_ATTR = /\b(?:class|className)\s*=\s*/g;
2811
+ function readBraced(source, open) {
2781
2812
  let depth = 0;
2782
- let i = open + 1;
2783
- const start = open + 2;
2784
- while (i < source.length) {
2813
+ for (let i = open; i < source.length; i++) {
2785
2814
  const ch = source[i];
2786
2815
  if (ch === '"' || ch === "'" || ch === "`") {
2787
2816
  const quote = ch;
@@ -2790,83 +2819,151 @@ function readObject(source, open) {
2790
2819
  if (source[i] === "\\") i++;
2791
2820
  i++;
2792
2821
  }
2793
- i++;
2794
2822
  continue;
2795
2823
  }
2796
2824
  if (ch === "{") depth++;
2797
- else if (ch === "}") {
2798
- depth--;
2799
- if (depth === 0) return { start, end: i };
2800
- }
2801
- i++;
2825
+ else if (ch === "}" && --depth === 0) return source.slice(open + 1, i);
2802
2826
  }
2803
2827
  return null;
2804
2828
  }
2805
- function styleObjectToCss(body) {
2806
- let out = "";
2807
- for (const ch of body) {
2808
- if (ch === '"' || ch === "'" || ch === "`") out += " ";
2809
- else if (ch === ",") out += ";";
2810
- else out += ch;
2829
+ function classAttributeValues(source) {
2830
+ const out = [];
2831
+ const lineAt = (offset) => source.slice(0, offset).split("\n").length;
2832
+ for (const m of source.matchAll(CLASS_ATTR)) {
2833
+ const at = m.index + m[0].length;
2834
+ const opener = source[at];
2835
+ if (opener === '"' || opener === "'") {
2836
+ const end = source.indexOf(opener, at + 1);
2837
+ if (end !== -1) out.push({ classes: source.slice(at + 1, end), line: lineAt(at) });
2838
+ continue;
2839
+ }
2840
+ if (opener !== "{") continue;
2841
+ const raw = readBraced(source, at);
2842
+ if (raw === null) continue;
2843
+ const literals = [...raw.matchAll(/(["'`])([\s\S]*?)\1/g)].map(
2844
+ (q) => q[2].replace(/\$\{[\s\S]*?\}/g, " ")
2845
+ );
2846
+ if (literals.length > 0) out.push({ classes: literals.join(" ").trim(), line: lineAt(at) });
2811
2847
  }
2812
2848
  return out;
2813
2849
  }
2814
- function maskNonStyleRegions(source, file) {
2815
- if (hasExtension(file, CSS_EXTENSIONS)) return source;
2816
- if (!isStyleHost(file)) return blank(source);
2817
- const spans = [];
2818
- const indented = indentedSyntaxFor(file);
2819
- if (indented) {
2820
- spans.push(...indentedSpans(source, indented));
2821
- for (const m of source.matchAll(indented.attr)) {
2822
- const valueStart = m.index + m[0].indexOf(m[1]) + 1;
2823
- spans.push({ start: valueStart, end: valueStart + m[2].length, braced: true });
2824
- }
2825
- return applySpans(source, spans);
2826
- }
2827
- if (hasExtension(file, MARKUP_HOSTS)) {
2828
- for (const m of source.matchAll(STYLE_BLOCK)) {
2829
- const bodyStart = m.index + m[0].indexOf(">") + 1;
2830
- spans.push({ start: bodyStart, end: bodyStart + m[1].length });
2831
- }
2832
- for (const m of source.matchAll(STYLE_ATTR)) {
2833
- const valueStart = m.index + m[0].indexOf(m[1]) + 1;
2834
- spans.push({ start: valueStart, end: valueStart + m[2].length, braced: true });
2850
+ var ARBITRARY = /(?:^|\s)(?:[\w-]+:)*([a-z][\w-]*)-\[([^\]\s]+)\]/gi;
2851
+ var COLOUR = /^(?:#[0-9a-f]{3,8}|(?:rgba?|hsla?|oklch|oklab|lab|lch|color)\()/i;
2852
+ var LENGTH_PARTS = /^(-?\d*\.?\d+)([a-z]+)$/i;
2853
+ function arbitraryValues(classes) {
2854
+ const out = [];
2855
+ for (const m of classes.matchAll(ARBITRARY)) {
2856
+ const [, utility, rawValue] = m;
2857
+ const value = rawValue.replace(/_/g, " ");
2858
+ if (/var\(|--/.test(value)) continue;
2859
+ if (COLOUR.test(value)) {
2860
+ out.push({ utility, value, kind: "colour" });
2861
+ continue;
2835
2862
  }
2863
+ if (!LENGTH_RE.test(value)) continue;
2864
+ const parts = LENGTH_PARTS.exec(value);
2865
+ if (isExcludedLength(parseFloat(parts[1]), parts[2])) continue;
2866
+ out.push({ utility, value, kind: "length" });
2836
2867
  }
2837
- for (const m of source.matchAll(CSS_TAG)) {
2838
- const backtick = m.index + m[0].length - 1;
2839
- const span = readTemplate(source, backtick);
2840
- if (span) spans.push({ ...span, braced: true });
2841
- }
2842
- for (const m of source.matchAll(STYLE_OBJECT)) {
2843
- const span = readObject(source, m.index + m[0].length - 2);
2844
- if (span) spans.push({ ...span, transform: styleObjectToCss, braced: true });
2868
+ return out;
2869
+ }
2870
+ var PALETTE = {
2871
+ white: "#ffffff",
2872
+ black: "#000000",
2873
+ "gray-50": "#f9fafb",
2874
+ "gray-100": "#f3f4f6",
2875
+ "gray-200": "#e5e7eb",
2876
+ "gray-300": "#d1d5db",
2877
+ "gray-400": "#9ca3af",
2878
+ "gray-500": "#6b7280",
2879
+ "gray-600": "#4b5563",
2880
+ "gray-700": "#374151",
2881
+ "gray-800": "#1f2937",
2882
+ "gray-900": "#111827",
2883
+ "gray-950": "#030712",
2884
+ "slate-50": "#f8fafc",
2885
+ "slate-400": "#94a3b8",
2886
+ "slate-500": "#64748b",
2887
+ "slate-800": "#1e293b",
2888
+ "slate-900": "#0f172a",
2889
+ "slate-950": "#020617",
2890
+ "zinc-400": "#a1a1aa",
2891
+ "zinc-500": "#71717a",
2892
+ "zinc-900": "#18181b",
2893
+ "neutral-400": "#a3a3a3",
2894
+ "neutral-500": "#737373",
2895
+ "neutral-900": "#171717",
2896
+ "stone-400": "#a8a29e",
2897
+ "stone-500": "#78716c",
2898
+ "stone-900": "#1c1917"
2899
+ };
2900
+ function palettePairs(classes) {
2901
+ let background;
2902
+ let foreground;
2903
+ for (const token of classes.split(/\s+/)) {
2904
+ const bare2 = token.replace(/^(?:[\w-]+:)*/, "");
2905
+ const bg = /^bg-(.+)$/.exec(bare2);
2906
+ const fg = /^text-(.+)$/.exec(bare2);
2907
+ if (bg && PALETTE[bg[1]]) background = PALETTE[bg[1]];
2908
+ if (fg && PALETTE[fg[1]]) foreground = PALETTE[fg[1]];
2845
2909
  }
2846
- return applySpans(source, spans);
2910
+ return background && foreground ? [{ background, foreground }] : [];
2847
2911
  }
2848
- function applySpans(source, spans) {
2849
- if (spans.length === 0) return blank(source);
2850
- const out = blank(source).split("");
2851
- for (const span of spans) {
2852
- const text = source.slice(span.start, span.end);
2853
- const rendered = span.transform ? span.transform(text) : text;
2854
- for (let i = 0; i < rendered.length; i++) {
2855
- out[span.start + i] = rendered[i];
2912
+
2913
+ // src/check/tailwind-dead.ts
2914
+ var TAILWIND_IMPORT = /@import\s+(?:url\(\s*)?["']tailwindcss["']/;
2915
+ var QUERY = `
2916
+ const systems = [];
2917
+ for (const entry of input.entries) systems.push(await loadDesignSystem(readFileSync(entry.file, 'utf8'), { base: entry.base }));
2918
+ const css = systems.map((ds) => ds.candidatesToCss(input.classes));
2919
+ answer(input.classes.filter((c, i) => systems.some((ds) => ds.parseCandidate(c).length > 0) && css.every((list) => !list[i])));
2920
+ `;
2921
+ function definedClasses(css) {
2922
+ const out = /* @__PURE__ */ new Set();
2923
+ for (const m of css.matchAll(/\.(-?[_a-zA-Z][\w-]*)/g)) out.add(m[1]);
2924
+ return out;
2925
+ }
2926
+ function deadTailwindClasses(projectRoot, files, stylesheets) {
2927
+ const read = (f) => {
2928
+ try {
2929
+ return readFileSync16(join21(projectRoot, f), "utf8");
2930
+ } catch {
2931
+ return "";
2856
2932
  }
2857
- if (span.braced && span.start > 0 && span.end < out.length) {
2858
- out[span.start - 1] = "{";
2859
- out[span.end] = "}";
2933
+ };
2934
+ const entries = stylesheets.filter((f) => hasExtension(f, CSS_EXTENSIONS) && TAILWIND_IMPORT.test(maskComments(read(f))));
2935
+ if (entries.length === 0) return { dead: [] };
2936
+ const module = tailwindNode(projectRoot);
2937
+ if (!module) return { dead: [], skipped: "a stylesheet imports Tailwind v4, but @tailwindcss/node is not installed here, so no class was asked about" };
2938
+ const defined = /* @__PURE__ */ new Set();
2939
+ for (const f of stylesheets) for (const c of definedClasses(maskComments(maskNonStyleRegions(read(f), f)))) defined.add(c);
2940
+ const uses = [];
2941
+ for (const file of files) {
2942
+ if (!isMarkupHost(file)) continue;
2943
+ for (const { classes, line } of classAttributeValues(read(file))) {
2944
+ for (const className of classes.split(/\s+/)) {
2945
+ if (!className || /[{}$`'"]/.test(className)) continue;
2946
+ uses.push({ file, line, className });
2947
+ }
2860
2948
  }
2861
2949
  }
2862
- return out.join("");
2863
- }
2864
- function blank(source) {
2865
- let out = "";
2866
- for (const ch of source) out += ch === "\n" ? "\n" : " ";
2867
- return out;
2950
+ const candidates = [...new Set(uses.map((u) => u.className))].filter((c) => !defined.has(c.split(":").pop()));
2951
+ if (candidates.length === 0) return { dead: [] };
2952
+ let dead;
2953
+ try {
2954
+ const input = { entries: entries.map((e) => ({ file: join21(projectRoot, e), base: dirname6(join21(projectRoot, e)) })), classes: candidates };
2955
+ dead = askTailwind(projectRoot, module, QUERY, input);
2956
+ } catch (err) {
2957
+ return { dead: [], skipped: `Tailwind could not be asked which classes generate CSS (${whyNot(err)})` };
2958
+ }
2959
+ const deadSet = new Set(dead);
2960
+ return { dead: uses.filter((u) => deadSet.has(u.className)) };
2868
2961
  }
2869
2962
 
2963
+ // src/check/run.ts
2964
+ import { readFileSync as readFileSync18 } from "fs";
2965
+ import { join as join23 } from "path";
2966
+
2870
2967
  // src/check/finding.ts
2871
2968
  function mkFinding(ctx, detector, file, line, message, excerpt) {
2872
2969
  return {
@@ -3329,6 +3426,7 @@ var REM_TO_PX = 16;
3329
3426
  var FONT_SIZE_DECL_RE = /(?<![-\w])font-size\s*:\s*([^;]+?)(?:;|$)/i;
3330
3427
  var FONT_SIZE_PX_RE = /^(-?\d*\.?\d+)px$/i;
3331
3428
  var FONT_SIZE_REM_RE = /^(-?\d*\.?\d+)rem$/i;
3429
+ var NOT_LARGER_RE = /^(?:0*\.\d+em|1(?:\.0+)?em|(?:\d{1,2}(?:\.\d+)?|100)%|smaller|small|x-small|xx-small)$/i;
3332
3430
  var BOLD_RE = /(?<![-\w])font-weight\s*:\s*(bold|[6-9]\d\d)/i;
3333
3431
  function resolveFloor(body) {
3334
3432
  const m = FONT_SIZE_DECL_RE.exec(body);
@@ -3339,6 +3437,7 @@ function resolveFloor(body) {
3339
3437
  let px2;
3340
3438
  if (pxMatch) px2 = parseFloat(pxMatch[1]);
3341
3439
  else if (remMatch) px2 = parseFloat(remMatch[1]) * REM_TO_PX;
3440
+ else if (NOT_LARGER_RE.test(raw)) return FLOOR;
3342
3441
  else return LARGE_FLOOR;
3343
3442
  const large = BOLD_RE.test(body) ? px2 >= 18.66 : px2 >= 24;
3344
3443
  return large ? LARGE_FLOOR : FLOOR;
@@ -3669,9 +3768,22 @@ var viewportHeight = {
3669
3768
 
3670
3769
  // src/check/detectors/input-zoom.ts
3671
3770
  var SELECTOR_RE = /(?:^|[\s,>+~(])(?:input|select|textarea)\b(?![-\w])|\[contenteditable/i;
3672
- var SIZE_RE = /(?<![-\w])font-size\s*:\s*([^;}]+)/gi;
3673
- var TOUCH_QUERY_RE = /@media[^{]*\b(?:pointer\s*:\s*coarse|hover\s*:\s*none)\b/i;
3771
+ var SIZE_RE = /(?<![-\w])font(?:-size)?\s*:\s*([^;}]+)/gi;
3772
+ var TOUCH_QUERY_RE = /^@media[^{]*\b(?:pointer\s*:\s*coarse|hover\s*:\s*none)\b/i;
3773
+ var BODY_SELECTOR_RE = /(?:^|,)\s*(?:html\s*,\s*)?body\s*(?:,|$)/i;
3674
3774
  var ROOT_PX = 16;
3775
+ function touchFormText(source) {
3776
+ const out = { body: false, control: false };
3777
+ for (const media of splitRuleBlocks(source)) {
3778
+ if (!TOUCH_QUERY_RE.test(media.selector.trim())) continue;
3779
+ for (const block of leafBlocks(media.body)) {
3780
+ const body = /--text-body\s*:\s*([^;}]+)/i.exec(block.body);
3781
+ if (body && (pixels(body[1], void 0) ?? 0) >= ROOT_PX) out.body = true;
3782
+ if (SELECTOR_RE.test(block.selector) && /(?<![-\w])font(?:-size)?\s*:/i.test(block.body)) out.control = true;
3783
+ }
3784
+ }
3785
+ return out;
3786
+ }
3675
3787
  function pixels(value, mode) {
3676
3788
  const v = value.trim().replace(/\s*!important$/i, "");
3677
3789
  const unit = /^(\d*\.?\d+)(px|rem|em)$/i.exec(v);
@@ -3680,18 +3792,36 @@ function pixels(value, mode) {
3680
3792
  if (/^var\(\s*--text-body\s*\)$/i.test(v) && mode === "operator") return 14;
3681
3793
  return void 0;
3682
3794
  }
3795
+ function sizeOf(property, value) {
3796
+ if (!/^font\s*:/i.test(property)) return value;
3797
+ const m = /(?:^|\s)((?:\d*\.?\d+(?:px|rem|em))|var\(\s*--text-[\w-]+\s*\))\s*(?:\/|\s|$)/i.exec(value.trim());
3798
+ return m ? m[1] : value.trim();
3799
+ }
3800
+ function bodySize(source) {
3801
+ for (const block of leafBlocks(source)) {
3802
+ if (block.atRuleDepth !== 0 || !BODY_SELECTOR_RE.test(block.selector.trim())) continue;
3803
+ const m = /(?<![-\w])font-size\s*:\s*([^;}]+)/i.exec(block.body);
3804
+ if (m) return m[1];
3805
+ }
3806
+ return void 0;
3807
+ }
3683
3808
  var inputZoom = {
3684
3809
  name: "input-zoom",
3685
3810
  appliesTo: (file) => isStyleBearing(file),
3686
3811
  run(source, file, ctx) {
3687
- if (TOUCH_QUERY_RE.test(source)) return [];
3812
+ const own = touchFormText(source);
3813
+ if (own.control || ctx.touchFormText?.control) return [];
3814
+ const mode = own.body || ctx.touchFormText?.body ? void 0 : ctx.mode;
3688
3815
  const findings = [];
3689
3816
  for (const block of leafBlocks(source)) {
3690
3817
  if (!SELECTOR_RE.test(block.selector)) continue;
3691
3818
  SIZE_RE.lastIndex = 0;
3692
3819
  let m;
3693
3820
  while (m = SIZE_RE.exec(block.body)) {
3694
- const px2 = pixels(m[1], ctx.mode);
3821
+ let value = sizeOf(m[0], m[1]);
3822
+ const inherited = /^inherit\b/i.test(value.trim());
3823
+ if (inherited) value = bodySize(source) ?? "";
3824
+ const px2 = pixels(value, mode);
3695
3825
  if (px2 === void 0 || px2 >= ROOT_PX) continue;
3696
3826
  const line = lineOfOffset(block, m.index);
3697
3827
  findings.push(
@@ -3700,7 +3830,7 @@ var inputZoom = {
3700
3830
  "input-zoom",
3701
3831
  file,
3702
3832
  line,
3703
- `form control text resolves to ${px2}px; iOS zooms the page on focus below 16px`,
3833
+ inherited ? `form control inherits the body's ${px2}px; iOS zooms the page on focus below 16px` : `form control text resolves to ${px2}px; iOS zooms the page on focus below 16px`,
3704
3834
  sourceLine(source, line)
3705
3835
  )
3706
3836
  );
@@ -3804,8 +3934,8 @@ var menuState = {
3804
3934
  };
3805
3935
 
3806
3936
  // src/check/declared-properties.ts
3807
- import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
3808
- import { join as join20 } from "path";
3937
+ import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
3938
+ import { join as join22 } from "path";
3809
3939
  var DECLARATION_RE = /(?<![\w-])['"]?(--[A-Za-z_][\w-]*)['"]?\s*:/g;
3810
3940
  var AT_PROPERTY_RE = /@property\s+(--[\w-]+)/g;
3811
3941
  var SET_PROPERTY_RE = /setProperty\(\s*['"`](--[\w-]+)/g;
@@ -3817,16 +3947,16 @@ function addAll(names, text) {
3817
3947
  }
3818
3948
  }
3819
3949
  function readPackageImport(projectRoot, spec) {
3820
- const base = join20(projectRoot, "node_modules", spec);
3950
+ const base = join22(projectRoot, "node_modules", spec);
3821
3951
  for (const candidate of [base, `${base}.css`]) {
3822
3952
  try {
3823
- if (existsSync13(candidate) && candidate.endsWith(".css")) return readFileSync16(candidate, "utf8");
3953
+ if (existsSync13(candidate) && candidate.endsWith(".css")) return readFileSync17(candidate, "utf8");
3824
3954
  } catch {
3825
3955
  }
3826
3956
  }
3827
3957
  try {
3828
- const pkg = JSON.parse(readFileSync16(join20(base, "package.json"), "utf8"));
3829
- if (pkg.style) return readFileSync16(join20(base, pkg.style), "utf8");
3958
+ const pkg = JSON.parse(readFileSync17(join22(base, "package.json"), "utf8"));
3959
+ if (pkg.style) return readFileSync17(join22(base, pkg.style), "utf8");
3830
3960
  } catch {
3831
3961
  }
3832
3962
  return void 0;
@@ -3838,7 +3968,7 @@ function collectDeclaredProperties(projectRoot, files, tokenNames = []) {
3838
3968
  for (const file of files) {
3839
3969
  let raw;
3840
3970
  try {
3841
- raw = readFileSync16(join20(projectRoot, file), "utf8");
3971
+ raw = readFileSync17(join22(projectRoot, file), "utf8");
3842
3972
  } catch {
3843
3973
  continue;
3844
3974
  }
@@ -4331,6 +4461,40 @@ var interfaceSafety = {
4331
4461
  }
4332
4462
  };
4333
4463
 
4464
+ // src/check/detectors/new-tab.ts
4465
+ var BLANK_LINK2 = /<a\b([^>]*\btarget\s*=\s*\{?\s*["'`]?_blank["'`]?\s*\}?[^>]*)>([\s\S]*?)<\/a\s*>/gi;
4466
+ var SAYS_SO = /new\s+(?:tab|window)|opens?\s+in\s+a\s+new/i;
4467
+ var ATTR = /\b(?:aria-label|title)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi;
4468
+ var newTabLink = {
4469
+ name: "new-tab-link",
4470
+ appliesTo: (file) => isReaderText(file),
4471
+ run(_source, file, ctx) {
4472
+ const raw = ctx.raw;
4473
+ const starts = buildLineIndex(raw);
4474
+ const findings = [];
4475
+ for (const m of raw.matchAll(BLANK_LINK2)) {
4476
+ const attrs = m[1];
4477
+ const inner = m[2];
4478
+ const named = [...attrs.matchAll(ATTR)].map((a) => a[1] ?? a[2] ?? "").join(" ");
4479
+ if (SAYS_SO.test(named) || SAYS_SO.test(inner)) continue;
4480
+ if (/\{[^}]*\}/.test(inner.replace(/<[^>]*>/g, "")) || /\{\s*\.\.\./.test(attrs)) continue;
4481
+ const text = inner.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim().slice(0, 40);
4482
+ const line = lineForOffset(starts, m.index);
4483
+ findings.push(
4484
+ mkFinding(
4485
+ ctx,
4486
+ "new-tab-link",
4487
+ file,
4488
+ line,
4489
+ `"${text || "a link"}" opens a new tab and does not say so: keep it in the same tab, or, where leaving would lose the reader's work, add "(opens in a new tab)" to its accessible name`,
4490
+ sourceLine(raw, line)
4491
+ )
4492
+ );
4493
+ }
4494
+ return findings;
4495
+ }
4496
+ };
4497
+
4334
4498
  // src/check/detectors/generated-look.ts
4335
4499
  function report(ctx, name, file, raw, hits) {
4336
4500
  const seen = /* @__PURE__ */ new Set();
@@ -4495,12 +4659,13 @@ var glowAccent = {
4495
4659
  }
4496
4660
  };
4497
4661
  var PAGE_SELECTOR = /(?:^|[\s,>])(?:html|body|:root|main)\b|--[\w-]*(?:bg|background|surface|page|canvas)[\w-]*/i;
4662
+ var NOT_PAGE_TOKEN = /-(?:success|error|danger|warn|warning|info|notice|overdue|due|paid|draft|pending|status|badge|tag|chip|pill|alert|toast|hover|active|selected|highlight|mark|code|kbd|tooltip|input|button|btn)(?=-|$)/i;
4498
4663
  var TW_CREAM = /^bg-(?:amber|orange|yellow)-(?:50|100)$|^bg-stone-(?:100|200)$/;
4499
4664
  function cream(value, tokens) {
4500
4665
  const c = extractColorComponents(value.trim(), tokens);
4501
4666
  if (!c || c.alpha < 0.9) return false;
4502
4667
  const { r, g, b } = c.rgb;
4503
- return Math.min(r, g, b) >= 215 && r >= g && g >= b && r - b >= 6;
4668
+ return Math.min(r, g, b) >= 215 && r >= g && g >= b && r - b >= 6 && g - b >= (r - b) * 0.35;
4504
4669
  }
4505
4670
  var creamPalette = {
4506
4671
  name: "cream-palette",
@@ -4510,7 +4675,7 @@ var creamPalette = {
4510
4675
  for (const block of leafBlocks(source)) {
4511
4676
  for (const m of block.body.matchAll(/(?:^|[;{\s])((?:--[\w-]+|background(?:-color)?))\s*:\s*([^;}]+)/g)) {
4512
4677
  const prop = m[1];
4513
- const onPage = prop.startsWith("--") ? PAGE_SELECTOR.test(prop) : PAGE_SELECTOR.test(block.selector);
4678
+ const onPage = prop.startsWith("--") ? PAGE_SELECTOR.test(prop) && !NOT_PAGE_TOKEN.test(prop) : PAGE_SELECTOR.test(block.selector);
4514
4679
  if (!onPage || !cream(m[2], ctx.tokens)) continue;
4515
4680
  hits.push({ line: lineOfOffset(block, m.index), message: `a cream page surface (${m[2].trim()}): the palette a model reaches for when asked to look tasteful` });
4516
4681
  }
@@ -4685,6 +4850,7 @@ var DETECTORS = [
4685
4850
  semanticElement,
4686
4851
  metadata,
4687
4852
  interfaceSafety,
4853
+ newTabLink,
4688
4854
  ...generatedLookDetectors
4689
4855
  ];
4690
4856
  function getDetector(name) {
@@ -4692,7 +4858,7 @@ function getDetector(name) {
4692
4858
  }
4693
4859
 
4694
4860
  // src/check/run.ts
4695
- function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParticipates = false, mode, projectResponsive, viewportFitCover, projectMenuToggle, declaredProperties) {
4861
+ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParticipates = false, mode, projectResponsive, viewportFitCover, projectMenuToggle, declaredProperties, touchFormText2) {
4696
4862
  const findings = [];
4697
4863
  const active = index.filter(
4698
4864
  (e) => !!e.detector && !!getDetector(e.detector)
@@ -4704,13 +4870,13 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
4704
4870
  if (applicable.length === 0) continue;
4705
4871
  let raw;
4706
4872
  try {
4707
- raw = readFileSync17(join21(projectRoot, file), "utf8");
4873
+ raw = readFileSync18(join23(projectRoot, file), "utf8");
4708
4874
  } catch {
4709
4875
  continue;
4710
4876
  }
4711
4877
  const source = maskComments(maskNonStyleRegions(raw, file));
4712
4878
  for (const { entry, detector } of applicable) {
4713
- const ctx = { ruleId: entry.id, bucket: entry.bucket, severity: entry.severity, tokens, projectParticipates, raw, mode, projectResponsive, viewportFitCover, projectMenuToggle, declaredProperties };
4879
+ const ctx = { ruleId: entry.id, bucket: entry.bucket, severity: entry.severity, tokens, projectParticipates, raw, mode, projectResponsive, viewportFitCover, projectMenuToggle, declaredProperties, touchFormText: touchFormText2 };
4714
4880
  findings.push(...detector.run(source, file, ctx));
4715
4881
  }
4716
4882
  }
@@ -4718,8 +4884,8 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
4718
4884
  }
4719
4885
 
4720
4886
  // src/check/tokens.ts
4721
- import { existsSync as existsSync14, readdirSync as readdirSync9, readFileSync as readFileSync18 } from "fs";
4722
- import { join as join22 } from "path";
4887
+ import { existsSync as existsSync14, readdirSync as readdirSync9, readFileSync as readFileSync19 } from "fs";
4888
+ import { join as join24 } from "path";
4723
4889
  function rootDeclarations(source) {
4724
4890
  const out = {};
4725
4891
  for (const block of splitRuleBlocks(source)) {
@@ -4734,20 +4900,20 @@ function rootDeclarations(source) {
4734
4900
  }
4735
4901
  function loadTokenMap(projectRoot, consumerFiles = []) {
4736
4902
  const jig = {};
4737
- const tokensDir = join22(projectRoot, ".jig", "tokens");
4903
+ const tokensDir = join24(projectRoot, ".jig", "tokens");
4738
4904
  if (existsSync14(tokensDir)) {
4739
4905
  for (const file of readdirSync9(tokensDir).filter((f) => f.endsWith(".css")).sort()) {
4740
- Object.assign(jig, rootDeclarations(readFileSync18(join22(tokensDir, file), "utf8")));
4906
+ Object.assign(jig, rootDeclarations(readFileSync19(join24(tokensDir, file), "utf8")));
4741
4907
  }
4742
4908
  }
4743
4909
  const consumer = {};
4744
4910
  const ambiguous = /* @__PURE__ */ new Set();
4745
- const vendored = join22(".jig", "tokens");
4911
+ const vendored = join24(".jig", "tokens");
4746
4912
  for (const file of consumerFiles) {
4747
4913
  if (file.includes(vendored)) continue;
4748
4914
  let source;
4749
4915
  try {
4750
- source = readFileSync18(join22(projectRoot, file), "utf8");
4916
+ source = readFileSync19(join24(projectRoot, file), "utf8");
4751
4917
  } catch {
4752
4918
  continue;
4753
4919
  }
@@ -4766,8 +4932,8 @@ function loadTokenMap(projectRoot, consumerFiles = []) {
4766
4932
  }
4767
4933
 
4768
4934
  // src/check/exempt.ts
4769
- import { existsSync as existsSync15, readFileSync as readFileSync19 } from "fs";
4770
- import { join as join23 } from "path";
4935
+ import { existsSync as existsSync15, readFileSync as readFileSync20 } from "fs";
4936
+ import { join as join25 } from "path";
4771
4937
  function globToRegExp(glob) {
4772
4938
  let out = "";
4773
4939
  for (let i = 0; i < glob.length; i++) {
@@ -4787,11 +4953,11 @@ function globToRegExp(glob) {
4787
4953
  }
4788
4954
  function readExemptions(projectRoot) {
4789
4955
  const none = { patterns: [], ignored: [] };
4790
- const configPath = join23(projectRoot, "jig.config.json");
4956
+ const configPath = join25(projectRoot, "jig.config.json");
4791
4957
  if (!existsSync15(configPath)) return none;
4792
4958
  let declared2;
4793
4959
  try {
4794
- declared2 = JSON.parse(readFileSync19(configPath, "utf8")).exempt;
4960
+ declared2 = JSON.parse(readFileSync20(configPath, "utf8")).exempt;
4795
4961
  } catch {
4796
4962
  return none;
4797
4963
  }
@@ -4841,8 +5007,8 @@ function applyExemptions(files, patterns) {
4841
5007
  }
4842
5008
 
4843
5009
  // src/check/waiver.ts
4844
- import { readFileSync as readFileSync20 } from "fs";
4845
- import { join as join24 } from "path";
5010
+ import { readFileSync as readFileSync21 } from "fs";
5011
+ import { join as join26 } from "path";
4846
5012
  var WAIVER = /jig-allow\s+([A-Z]-\d+(?:\s*,\s*[A-Z]-\d+)*)\s*:(.*)$/;
4847
5013
  function readWaiver(line) {
4848
5014
  const m = WAIVER.exec(line);
@@ -4857,7 +5023,7 @@ function applyWaivers(root, findings) {
4857
5023
  let cached = lines.get(file);
4858
5024
  if (!cached) {
4859
5025
  try {
4860
- cached = readFileSync20(join24(root, file), "utf8").split("\n");
5026
+ cached = readFileSync21(join26(root, file), "utf8").split("\n");
4861
5027
  } catch {
4862
5028
  cached = [];
4863
5029
  }
@@ -4922,8 +5088,8 @@ function hasMenuToggle(raw) {
4922
5088
  }
4923
5089
 
4924
5090
  // src/check/mode-wiring.ts
4925
- import { existsSync as existsSync16, readFileSync as readFileSync21 } from "fs";
4926
- import { dirname as dirname6, join as join25 } from "path";
5091
+ import { existsSync as existsSync16, readFileSync as readFileSync22 } from "fs";
5092
+ import { dirname as dirname7, join as join27 } from "path";
4927
5093
  var MODES = ["editorial", "product", "operator"];
4928
5094
  function configProblems(config) {
4929
5095
  const problems = [];
@@ -4945,10 +5111,10 @@ function configProblems(config) {
4945
5111
  }
4946
5112
  function modeWiringProblems(projectRoot) {
4947
5113
  let config;
4948
- const path = join25(projectRoot, "jig.config.json");
5114
+ const path = join27(projectRoot, "jig.config.json");
4949
5115
  if (!existsSync16(path)) return [];
4950
5116
  try {
4951
- config = JSON.parse(readFileSync21(path, "utf8"));
5117
+ config = JSON.parse(readFileSync22(path, "utf8"));
4952
5118
  } catch {
4953
5119
  return [{ mode: "", barrel: "jig.config.json", message: "jig.config.json is not valid JSON, so nothing in it applies" }];
4954
5120
  }
@@ -4957,14 +5123,14 @@ function modeWiringProblems(projectRoot) {
4957
5123
  if (typeof config.brand !== "string" || !Array.isArray(config.surfaces)) return [];
4958
5124
  const modes = [...new Set(config.surfaces.map((s) => s?.mode).filter((m) => typeof m === "string"))];
4959
5125
  if (modes.length === 0) return [];
4960
- const dir = dirname6(config.brand);
5126
+ const dir = dirname7(config.brand);
4961
5127
  const rel = (f) => dir === "." ? f : `${dir}/${f}`;
4962
5128
  const multi = modes.length > 1;
4963
5129
  const barrelOf = (mode) => multi ? `theme.${mode}.css` : "theme.css";
4964
- const anyBarrel = ["theme.css", ...modes.map((m) => `theme.${m}.css`)].some((f) => existsSync16(join25(projectRoot, rel(f))));
5130
+ const anyBarrel = ["theme.css", ...modes.map((m) => `theme.${m}.css`)].some((f) => existsSync16(join27(projectRoot, rel(f))));
4965
5131
  if (!anyBarrel) return [];
4966
5132
  const problems = [];
4967
- if (multi && existsSync16(join25(projectRoot, rel("theme.css")))) {
5133
+ if (multi && existsSync16(join27(projectRoot, rel("theme.css")))) {
4968
5134
  problems.push({
4969
5135
  mode: modes[0],
4970
5136
  barrel: rel("theme.css"),
@@ -4976,7 +5142,7 @@ function modeWiringProblems(projectRoot) {
4976
5142
  const modeFile = `mode.${mode}.css`;
4977
5143
  let body;
4978
5144
  try {
4979
- body = readFileSync21(join25(projectRoot, barrel), "utf8");
5145
+ body = readFileSync22(join27(projectRoot, barrel), "utf8");
4980
5146
  } catch {
4981
5147
  problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${barrel} does not exist` });
4982
5148
  return;
@@ -4988,7 +5154,7 @@ function modeWiringProblems(projectRoot) {
4988
5154
  barrel,
4989
5155
  message: `jig.config.json declares ${mode}, but ${barrel} imports ${imported ? `mode.${imported}.css` : "no mode file"} \u2014 the page is styled at ${imported ?? "no"} density`
4990
5156
  });
4991
- } else if (!existsSync16(join25(projectRoot, rel(modeFile)))) {
5157
+ } else if (!existsSync16(join27(projectRoot, rel(modeFile)))) {
4992
5158
  problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${rel(modeFile)} does not exist` });
4993
5159
  }
4994
5160
  });
@@ -4996,8 +5162,8 @@ function modeWiringProblems(projectRoot) {
4996
5162
  }
4997
5163
 
4998
5164
  // src/check/token-audit.ts
4999
- import { existsSync as existsSync17, readdirSync as readdirSync10, readFileSync as readFileSync22 } from "fs";
5000
- import { join as join26 } from "path";
5165
+ import { existsSync as existsSync17, readdirSync as readdirSync10, readFileSync as readFileSync23 } from "fs";
5166
+ import { join as join28 } from "path";
5001
5167
  var TEXT_FLOOR = 4.5;
5002
5168
  var UI_FLOOR = 3;
5003
5169
  var FOREGROUNDS = [
@@ -5053,24 +5219,24 @@ function floorFor(token) {
5053
5219
  function tokenFiles(projectRoot) {
5054
5220
  const read = (rel) => {
5055
5221
  try {
5056
- return readFileSync22(join26(projectRoot, ...rel.split("/")), "utf8");
5222
+ return readFileSync23(join28(projectRoot, ...rel.split("/")), "utf8");
5057
5223
  } catch {
5058
5224
  return null;
5059
5225
  }
5060
5226
  };
5061
- const statePath = join26(projectRoot, ".jig", "state.json");
5227
+ const statePath = join28(projectRoot, ".jig", "state.json");
5062
5228
  if (existsSync17(statePath)) {
5063
5229
  try {
5064
- const state = JSON.parse(readFileSync22(statePath, "utf8"));
5230
+ const state = JSON.parse(readFileSync23(statePath, "utf8"));
5065
5231
  const recorded = Object.keys(state.files ?? {}).filter((f) => f.endsWith(".css")).sort();
5066
5232
  const out = recorded.map((path) => ({ path, source: read(path) })).filter((f) => f.source !== null);
5067
5233
  if (out.length > 0) return out;
5068
5234
  } catch {
5069
5235
  }
5070
5236
  }
5071
- const legacy = join26(projectRoot, ".jig", "tokens");
5237
+ const legacy = join28(projectRoot, ".jig", "tokens");
5072
5238
  if (!existsSync17(legacy)) return [];
5073
- return readdirSync10(legacy).filter((f) => f.endsWith(".css")).sort().map((f) => ({ path: `.jig/tokens/${f}`, source: readFileSync22(join26(legacy, f), "utf8") }));
5239
+ return readdirSync10(legacy).filter((f) => f.endsWith(".css")).sort().map((f) => ({ path: `.jig/tokens/${f}`, source: readFileSync23(join28(legacy, f), "utf8") }));
5074
5240
  }
5075
5241
  function auditTokenLayer(projectRoot) {
5076
5242
  const files = tokenFiles(projectRoot);
@@ -5141,15 +5307,15 @@ function auditTokenLayer(projectRoot) {
5141
5307
  function checkExitCode(result) {
5142
5308
  return result.hasError ? 1 : 0;
5143
5309
  }
5144
- var LEGACY_INDEX_REL = join27(".jig", "rules.index.json");
5310
+ var LEGACY_INDEX_REL = join29(".jig", "rules.index.json");
5145
5311
  function resolveIndexPath(projectRoot) {
5146
- const legacy = join27(projectRoot, LEGACY_INDEX_REL);
5312
+ const legacy = join29(projectRoot, LEGACY_INDEX_REL);
5147
5313
  if (existsSync18(legacy)) return legacy;
5148
- return join27(assetRoot(), "rules.index.json");
5314
+ return join29(assetRoot(), "rules.index.json");
5149
5315
  }
5150
5316
  function resolveMode(projectRoot) {
5151
5317
  try {
5152
- const config = JSON.parse(readFileSync23(join27(projectRoot, "jig.config.json"), "utf8"));
5318
+ const config = JSON.parse(readFileSync24(join29(projectRoot, "jig.config.json"), "utf8"));
5153
5319
  const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
5154
5320
  if (modes.length === 1) return modes[0];
5155
5321
  return modes.length > 1 ? "mixed" : "unknown";
@@ -5166,14 +5332,14 @@ function summariseUnscanned(files) {
5166
5332
  }
5167
5333
  function countSpecs() {
5168
5334
  try {
5169
- return loadSpecs(join27(assetRoot(), "rules")).length;
5335
+ return loadSpecs(join29(assetRoot(), "rules")).length;
5170
5336
  } catch {
5171
5337
  return 0;
5172
5338
  }
5173
5339
  }
5174
5340
  function check(opts) {
5175
5341
  const indexPath = resolveIndexPath(opts.projectRoot);
5176
- const index = validateIndex(JSON.parse(readFileSync23(indexPath, "utf8")));
5342
+ const index = validateIndex(JSON.parse(readFileSync24(indexPath, "utf8")));
5177
5343
  const selection = selectFiles(opts.projectRoot, opts.all);
5178
5344
  const { patterns: exemptions, ignored: exemptIgnored } = readExemptions(opts.projectRoot);
5179
5345
  const { scanned: files, exempt, byPattern: counted } = applyExemptions(selection.files, exemptions);
@@ -5184,7 +5350,7 @@ function check(opts) {
5184
5350
  const projectParticipates = stylesheets.some((f) => {
5185
5351
  if (!hasExtension(f, CSS_EXTENSIONS)) return false;
5186
5352
  try {
5187
- return participatesInTokenLayer(readFileSync23(join27(opts.projectRoot, f), "utf8"), tokens);
5353
+ return participatesInTokenLayer(readFileSync24(join29(opts.projectRoot, f), "utf8"), tokens);
5188
5354
  } catch {
5189
5355
  return false;
5190
5356
  }
@@ -5192,7 +5358,7 @@ function check(opts) {
5192
5358
  const projectResponsive = stylesheets.some((f) => {
5193
5359
  if (!isStyleBearing(f)) return false;
5194
5360
  try {
5195
- const raw = readFileSync23(join27(opts.projectRoot, f), "utf8");
5361
+ const raw = readFileSync24(join29(opts.projectRoot, f), "utf8");
5196
5362
  return isResponsive(maskComments(maskNonStyleRegions(raw, f)), raw);
5197
5363
  } catch {
5198
5364
  return false;
@@ -5201,22 +5367,33 @@ function check(opts) {
5201
5367
  const viewportFitCover = stylesheets.some((f) => {
5202
5368
  if (!isStyleBearing(f)) return false;
5203
5369
  try {
5204
- return /viewport-fit\s*=\s*cover|viewportFit\s*:\s*['"]cover['"]/i.test(readFileSync23(join27(opts.projectRoot, f), "utf8"));
5370
+ return /viewport-fit\s*=\s*cover|viewportFit\s*:\s*['"]cover['"]/i.test(readFileSync24(join29(opts.projectRoot, f), "utf8"));
5205
5371
  } catch {
5206
5372
  return false;
5207
5373
  }
5208
5374
  });
5209
5375
  const projectMenuToggle = stylesheets.some((f) => {
5210
5376
  try {
5211
- return hasMenuToggle(readFileSync23(join27(opts.projectRoot, f), "utf8"));
5377
+ return hasMenuToggle(readFileSync24(join29(opts.projectRoot, f), "utf8"));
5212
5378
  } catch {
5213
5379
  return false;
5214
5380
  }
5215
5381
  });
5216
5382
  const declaredProperties = collectDeclaredProperties(opts.projectRoot, stylesheets, Object.keys(tokens));
5383
+ const touchFormText2 = { body: false, control: false };
5384
+ const vendoredTokens = existsSync18(join29(opts.projectRoot, ".jig", "tokens")) ? readdirSync11(join29(opts.projectRoot, ".jig", "tokens")).filter((f) => f.endsWith(".css")).map((f) => join29(".jig", "tokens", f)) : [];
5385
+ for (const f of [...stylesheets, ...vendoredTokens]) {
5386
+ if (!isStyleBearing(f)) continue;
5387
+ try {
5388
+ const found = touchFormText(maskComments(maskNonStyleRegions(readFileSync24(join29(opts.projectRoot, f), "utf8"), f)));
5389
+ touchFormText2.body ||= found.body;
5390
+ touchFormText2.control ||= found.control;
5391
+ } catch {
5392
+ }
5393
+ }
5217
5394
  const withStyles = files.filter((f) => {
5218
5395
  try {
5219
- const src = readFileSync23(join27(opts.projectRoot, f), "utf8");
5396
+ const src = readFileSync24(join29(opts.projectRoot, f), "utf8");
5220
5397
  if (hasExtension(f, CSS_EXTENSIONS)) return src.trim().length > 0;
5221
5398
  if (maskNonStyleRegions(src, f).trim().length > 0) return true;
5222
5399
  return isMarkupHost(f) && classAttributeValues(src).some((a) => a.classes.trim().length > 0);
@@ -5225,7 +5402,7 @@ function check(opts) {
5225
5402
  }
5226
5403
  }).length;
5227
5404
  const resolvedMode = resolveMode(opts.projectRoot);
5228
- const detected = runChecks(opts.projectRoot, files, index, tokens, bucketFilter, projectParticipates, resolvedMode, projectResponsive, viewportFitCover, projectMenuToggle, declaredProperties);
5405
+ const detected = runChecks(opts.projectRoot, files, index, tokens, bucketFilter, projectParticipates, resolvedMode, projectResponsive, viewportFitCover, projectMenuToggle, declaredProperties, touchFormText2);
5229
5406
  const TOKEN_RULE = {
5230
5407
  "--text-prose": "B-75",
5231
5408
  "--size-touch-target": "E-51"
@@ -5246,6 +5423,19 @@ function check(opts) {
5246
5423
  for (const gap of brandDecidedGaps(opts.projectRoot)) {
5247
5424
  detected.push({ ruleId: "A-01", detector: "brand-decided", bucket: "mechanical", severity: "warning", file: gap.file, line: gap.line, message: gap.message });
5248
5425
  }
5426
+ const tailwind = deadTailwindClasses(opts.projectRoot, files, stylesheets);
5427
+ const h117 = index.find((e) => e.id === "H-117");
5428
+ for (const d of tailwind.dead) {
5429
+ detected.push({
5430
+ ruleId: "H-117",
5431
+ detector: "tailwind-dead-class",
5432
+ bucket: h117?.bucket ?? "mechanical",
5433
+ severity: h117?.severity ?? "error",
5434
+ file: d.file,
5435
+ line: d.line,
5436
+ message: `\`${d.className}\` generates no CSS: Tailwind reads it as a utility, and nothing in this project's theme gives it a value, so the element gets nothing. Declare the token it needs in @theme, or use a class that exists`
5437
+ });
5438
+ }
5249
5439
  const { findings, waived } = applyWaivers(opts.projectRoot, detected);
5250
5440
  const hasError = findings.some((f) => f.bucket === "mechanical" && f.severity === "error");
5251
5441
  const cssFiles = files.filter((f) => /\.(css|scss|less)$/i.test(f));
@@ -5254,7 +5444,7 @@ function check(opts) {
5254
5444
  // told to add a token @import it has nowhere to put.
5255
5445
  cssFiles.length > 0 && cssFiles.every((f) => {
5256
5446
  try {
5257
- return !participatesInTokenLayer(readFileSync23(join27(opts.projectRoot, f), "utf8"), tokens);
5447
+ return !participatesInTokenLayer(readFileSync24(join29(opts.projectRoot, f), "utf8"), tokens);
5258
5448
  } catch {
5259
5449
  return true;
5260
5450
  }
@@ -5277,6 +5467,7 @@ function check(opts) {
5277
5467
  exemptIgnored,
5278
5468
  scope: selection.mode,
5279
5469
  modeUnwired: modeWiringProblems(opts.projectRoot),
5470
+ tailwindSkipped: tailwind.skipped,
5280
5471
  agentFiles: agentFileProblems(opts.projectRoot, opts.homeDir || void 0),
5281
5472
  waived
5282
5473
  });
@@ -5284,12 +5475,12 @@ function check(opts) {
5284
5475
  }
5285
5476
 
5286
5477
  // src/commands/init.ts
5287
- import { existsSync as existsSync19, mkdirSync as mkdirSync6, readdirSync as readdirSync11, readFileSync as readFileSync27, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
5288
- import { dirname as dirname7, isAbsolute, join as join31, resolve as resolve2, sep as sep3 } from "path";
5478
+ import { existsSync as existsSync19, mkdirSync as mkdirSync6, readdirSync as readdirSync12, readFileSync as readFileSync28, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
5479
+ import { dirname as dirname8, isAbsolute, join as join33, resolve as resolve2, sep as sep3 } from "path";
5289
5480
 
5290
5481
  // src/init/detect.ts
5291
- import { readFileSync as readFileSync24 } from "fs";
5292
- import { join as join28 } from "path";
5482
+ import { readFileSync as readFileSync25 } from "fs";
5483
+ import { join as join30 } from "path";
5293
5484
  var TAILWIND_CONFIG_RE = /^tailwind\.config\.(js|ts|mjs|cjs)$/;
5294
5485
  var TAILWIND_V4_IMPORT_RE = /@import\s+["']tailwindcss["']/;
5295
5486
  var TAILWIND_V4_THEME_RE = /@theme\b/;
@@ -5306,7 +5497,7 @@ var FRAMEWORK_DEPS = [
5306
5497
  function detectFramework(projectRoot) {
5307
5498
  let pkg;
5308
5499
  try {
5309
- pkg = JSON.parse(readFileSync24(join28(projectRoot, "package.json"), "utf8"));
5500
+ pkg = JSON.parse(readFileSync25(join30(projectRoot, "package.json"), "utf8"));
5310
5501
  } catch {
5311
5502
  return void 0;
5312
5503
  }
@@ -5324,7 +5515,7 @@ function detect(projectRoot) {
5324
5515
  for (const f of cssFiles) {
5325
5516
  let source;
5326
5517
  try {
5327
- source = readFileSync24(join28(projectRoot, f), "utf8");
5518
+ source = readFileSync25(join30(projectRoot, f), "utf8");
5328
5519
  } catch {
5329
5520
  continue;
5330
5521
  }
@@ -5342,12 +5533,12 @@ function detect(projectRoot) {
5342
5533
  }
5343
5534
 
5344
5535
  // src/init/gitignore.ts
5345
- import { execFileSync as execFileSync2 } from "child_process";
5536
+ import { execFileSync as execFileSync3 } from "child_process";
5346
5537
  function isIgnored(projectRoot, relPath) {
5347
5538
  const candidates = relPath.endsWith("/") ? [relPath] : [relPath, `${relPath}/`];
5348
5539
  return candidates.some((path) => {
5349
5540
  try {
5350
- execFileSync2("git", ["check-ignore", "-q", "--", path], {
5541
+ execFileSync3("git", ["check-ignore", "-q", "--", path], {
5351
5542
  cwd: projectRoot,
5352
5543
  stdio: ["ignore", "ignore", "ignore"]
5353
5544
  });
@@ -5359,8 +5550,8 @@ function isIgnored(projectRoot, relPath) {
5359
5550
  }
5360
5551
 
5361
5552
  // src/init/derive.ts
5362
- import { readFileSync as readFileSync25 } from "fs";
5363
- import { join as join29 } from "path";
5553
+ import { readFileSync as readFileSync26 } from "fs";
5554
+ import { join as join31 } from "path";
5364
5555
  var DEFAULT_PROPOSAL = {
5365
5556
  h: 264,
5366
5557
  s: 0,
@@ -5393,7 +5584,7 @@ function fromCssCustomProperties(projectRoot, cssFiles) {
5393
5584
  for (const file of cssFiles) {
5394
5585
  let source;
5395
5586
  try {
5396
- source = readFileSync25(join29(projectRoot, file), "utf8");
5587
+ source = readFileSync26(join31(projectRoot, file), "utf8");
5397
5588
  } catch {
5398
5589
  continue;
5399
5590
  }
@@ -5442,7 +5633,7 @@ function balancedBraceBody(source, openIdx) {
5442
5633
  function fromTailwindConfig(projectRoot, tailwindConfigFile) {
5443
5634
  let source;
5444
5635
  try {
5445
- source = readFileSync25(join29(projectRoot, tailwindConfigFile), "utf8");
5636
+ source = readFileSync26(join31(projectRoot, tailwindConfigFile), "utf8");
5446
5637
  } catch {
5447
5638
  return null;
5448
5639
  }
@@ -5498,7 +5689,7 @@ function fromLiteralFrequency(projectRoot, cssFiles) {
5498
5689
  for (const file of cssFiles) {
5499
5690
  let source;
5500
5691
  try {
5501
- source = readFileSync25(join29(projectRoot, file), "utf8");
5692
+ source = readFileSync26(join31(projectRoot, file), "utf8");
5502
5693
  } catch {
5503
5694
  continue;
5504
5695
  }
@@ -5658,8 +5849,8 @@ function relativeImportPath(fromAbsDir, toAbsFile) {
5658
5849
  }
5659
5850
 
5660
5851
  // src/init/project-name.ts
5661
- import { readFileSync as readFileSync26 } from "fs";
5662
- import { basename as basename2, join as join30 } from "path";
5852
+ import { readFileSync as readFileSync27 } from "fs";
5853
+ import { basename as basename2, join as join32 } from "path";
5663
5854
  function slugify(name) {
5664
5855
  const withoutScope = name.replace(/^@[^/]+\//, "");
5665
5856
  const slug = withoutScope.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -5667,7 +5858,7 @@ function slugify(name) {
5667
5858
  }
5668
5859
  function deriveProjectSlug(projectRoot) {
5669
5860
  try {
5670
- const pkg = JSON.parse(readFileSync26(join30(projectRoot, "package.json"), "utf8"));
5861
+ const pkg = JSON.parse(readFileSync27(join32(projectRoot, "package.json"), "utf8"));
5671
5862
  if (typeof pkg.name === "string" && pkg.name.trim()) return slugify(pkg.name);
5672
5863
  } catch {
5673
5864
  }
@@ -5676,6 +5867,8 @@ function deriveProjectSlug(projectRoot) {
5676
5867
 
5677
5868
  // src/init/utilities.ts
5678
5869
  var TAILWIND_NAMESPACES = [
5870
+ "--opacity-",
5871
+ "--transition-duration-",
5679
5872
  "--color-",
5680
5873
  "--font-",
5681
5874
  "--text-",
@@ -5697,21 +5890,60 @@ var TAILWIND_NAMESPACES = [
5697
5890
  "--size-",
5698
5891
  "--border-width-"
5699
5892
  ];
5700
- function tailwindNamespaced(names) {
5701
- const kept = names.filter((n) => TAILWIND_NAMESPACES.some((ns) => n.startsWith(ns)));
5702
- return [...new Set(kept)].sort();
5893
+ var NOT_UTILITIES = /* @__PURE__ */ new Set(["--shadow-none"]);
5894
+ var RENAMES = [["--duration-", "--transition-duration-"]];
5895
+ function aliasCandidates(names) {
5896
+ const out = /* @__PURE__ */ new Map();
5897
+ for (const token of new Set(names)) {
5898
+ out.set(token, { alias: token, token });
5899
+ for (const [from, to] of RENAMES) if (token.startsWith(from)) {
5900
+ const alias = to + token.slice(from.length);
5901
+ out.set(alias, { alias, token });
5902
+ }
5903
+ }
5904
+ return [...out.values()].sort((a, b) => a.alias.localeCompare(b.alias));
5905
+ }
5906
+ function listedAliases(names) {
5907
+ return aliasCandidates(names).filter((a) => TAILWIND_NAMESPACES.some((ns) => a.alias.startsWith(ns)) && !NOT_UTILITIES.has(a.alias));
5908
+ }
5909
+ var ALIASES_QUERY = `
5910
+ const ds = await loadDesignSystem('@import "tailwindcss";\\n@theme inline {\\n' + input.aliases.map((a, i) => ' ' + a + ': var(--jig-alias-' + i + ');').join('\\n') + '\\n}', { base: input.base });
5911
+ const used = new Set();
5912
+ const note = (list) => { for (const css of list) if (css) for (const m of css.matchAll(/var\\(--jig-alias-(\\d+)/g)) used.add(Number(m[1])); };
5913
+ note(ds.candidatesToCss(ds.getClassList().map(([c]) => c)));
5914
+ const roots = ds.utilities.keys('functional');
5915
+ const tries = [];
5916
+ input.aliases.forEach((a, i) => {
5917
+ if (used.has(i)) return;
5918
+ const parts = a.slice(2).split('-');
5919
+ for (let k = 1; k < parts.length; k++) for (const r of roots) tries.push(r + '-' + parts.slice(k).join('-'));
5920
+ });
5921
+ note(ds.candidatesToCss(tries));
5922
+ answer(input.aliases.filter((_, i) => used.has(i)));
5923
+ `;
5924
+ function compiledAliases(projectRoot, names) {
5925
+ const module = tailwindNode(projectRoot);
5926
+ if (!module) return void 0;
5927
+ const candidates = aliasCandidates(names);
5928
+ try {
5929
+ const kept = new Set(askTailwind(projectRoot, module, ALIASES_QUERY, { base: projectRoot, aliases: candidates.map((a) => a.alias) }));
5930
+ return candidates.filter((a) => kept.has(a.alias));
5931
+ } catch {
5932
+ return void 0;
5933
+ }
5703
5934
  }
5704
5935
  function declaredTokenNames(css) {
5705
5936
  return [...css.matchAll(/(^|[;{\s])(--[\w-]+)\s*:/g)].map((m) => m[2]);
5706
5937
  }
5707
- function utilitiesBody(names, version2) {
5708
- if (names.length === 0) {
5938
+ function utilitiesBody(entries, version2) {
5939
+ const aliases = entries.map((e) => typeof e === "string" ? { alias: e, token: e } : e);
5940
+ if (aliases.length === 0) {
5709
5941
  throw new Error(
5710
5942
  "Refusing to write an empty @theme block: it generates no utilities while looking like it should. No token in this layer matches a Tailwind namespace."
5711
5943
  );
5712
5944
  }
5713
- const width = Math.max(...names.map((n) => n.length));
5714
- const lines = names.map((n) => ` ${n}:${" ".repeat(width - n.length)} var(${n});`);
5945
+ const width = Math.max(...aliases.map((a) => a.alias.length));
5946
+ const lines = aliases.map((a) => ` ${a.alias}:${" ".repeat(width - a.alias.length)} var(${a.token});`);
5715
5947
  return `/* utilities.css \u2014 vendored from Jig v${version2}.
5716
5948
  Licensed Apache-2.0. LICENSE and NOTICE ship beside the jig skill.
5717
5949
 
@@ -5735,10 +5967,11 @@ function utilitiesBody(names, version2) {
5735
5967
  order, so Jig's value always wins. Do not "fix" it \u2014 deleting the alias
5736
5968
  removes the utility, deleting Jig's removes the value.
5737
5969
 
5738
- Only tokens in a Tailwind namespace are listed. \`--measure-*\`, \`--focus-ring-*\`
5739
- and \`--duration-*\` have none, so they stay \`var()\`-only \u2014 read them from a class
5740
- with Tailwind's custom-property form instead: \`max-w-(--measure-prose)\`,
5741
- \`duration-(--duration-fast)\`.
5970
+ Only tokens a Tailwind utility reads are listed. Durations are listed under the
5971
+ name Tailwind reads them by, \`--transition-duration-fast: var(--duration-fast)\`,
5972
+ so \`duration-fast\` works. \`--measure-*\` and \`--focus-ring-*\` have no utility,
5973
+ so they stay \`var()\`-only \u2014 read them from a class with Tailwind's
5974
+ custom-property form instead: \`max-w-(--measure-prose)\`.
5742
5975
 
5743
5976
  Regenerate with \`jig init\` after the token layer changes; a token missing here
5744
5977
  is a class that renders and matches nothing. */
@@ -5821,9 +6054,9 @@ function isValidSurfaceArray(v) {
5821
6054
  function loadEffectiveConfig(projectRoot, configAbsPath, fallbackBrand, fallbackSurfaces) {
5822
6055
  if (!existsSync19(configAbsPath)) return { brand: fallbackBrand, surfaces: fallbackSurfaces };
5823
6056
  try {
5824
- const parsed = JSON.parse(readFileSync27(configAbsPath, "utf8"));
6057
+ const parsed = JSON.parse(readFileSync28(configAbsPath, "utf8"));
5825
6058
  const brandCandidate = typeof parsed.brand === "string" && parsed.brand ? parsed.brand : void 0;
5826
- const brandResolves = brandCandidate ? existsSync19(join31(projectRoot, ...brandCandidate.split("/"))) : false;
6059
+ const brandResolves = brandCandidate ? existsSync19(join33(projectRoot, ...brandCandidate.split("/"))) : false;
5827
6060
  return {
5828
6061
  brand: brandResolves ? brandCandidate : fallbackBrand,
5829
6062
  surfaces: isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : fallbackSurfaces
@@ -5836,7 +6069,7 @@ function configuredBrandPath(projectRoot, configAbsPath) {
5836
6069
  if (!existsSync19(configAbsPath)) return { path: null };
5837
6070
  let declared2;
5838
6071
  try {
5839
- declared2 = JSON.parse(readFileSync27(configAbsPath, "utf8")).brand;
6072
+ declared2 = JSON.parse(readFileSync28(configAbsPath, "utf8")).brand;
5840
6073
  } catch {
5841
6074
  return { path: null };
5842
6075
  }
@@ -5866,7 +6099,7 @@ function defaultTokenDir(detection) {
5866
6099
  function readConfiguredSurfaces(configAbsPath) {
5867
6100
  if (!existsSync19(configAbsPath)) return null;
5868
6101
  try {
5869
- const parsed = JSON.parse(readFileSync27(configAbsPath, "utf8"));
6102
+ const parsed = JSON.parse(readFileSync28(configAbsPath, "utf8"));
5870
6103
  return isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : null;
5871
6104
  } catch {
5872
6105
  return null;
@@ -5876,9 +6109,9 @@ function isCssModule(path) {
5876
6109
  return /\.module\.(css|scss)$/i.test(path);
5877
6110
  }
5878
6111
  function legacyBrandFile(projectRoot) {
5879
- const dir = join31(projectRoot, ".jig", "tokens");
6112
+ const dir = join33(projectRoot, ".jig", "tokens");
5880
6113
  if (!existsSync19(dir)) return void 0;
5881
- const found = readdirSync11(dir).find((f) => /^brand\.[\w.-]+\.css$/.test(f));
6114
+ const found = readdirSync12(dir).find((f) => /^brand\.[\w.-]+\.css$/.test(f));
5882
6115
  return found ? relKey(".jig", "tokens", found) : void 0;
5883
6116
  }
5884
6117
  function isTokenLayerFile(f) {
@@ -5908,7 +6141,7 @@ var LEGACY_PAIR_RE = new RegExp(
5908
6141
  `@import\\s+["'][^"']*brand\\.[\\w.-]+\\.css["'];?\\r?\\n?\\s*@import\\s+["'][^"']*mode\\.(?:${MODES2.join("|")})\\.css["'];?\\r?\\n?`
5909
6142
  );
5910
6143
  function wireBarrel(absPath, barrelImport) {
5911
- const content = existsSync19(absPath) ? readFileSync27(absPath, "utf8") : "";
6144
+ const content = existsSync19(absPath) ? readFileSync28(absPath, "utf8") : "";
5912
6145
  const line = `@import "${barrelImport}";`;
5913
6146
  if (content.includes(line)) return { status: "already-present" };
5914
6147
  const legacy = LEGACY_PAIR_RE.exec(content);
@@ -5924,7 +6157,7 @@ function wireBarrel(absPath, barrelImport) {
5924
6157
 
5925
6158
  ` : `${line}
5926
6159
  `);
5927
- mkdirSync6(dirname7(absPath), { recursive: true });
6160
+ mkdirSync6(dirname8(absPath), { recursive: true });
5928
6161
  writeFileSync4(next === content ? absPath : absPath, next, "utf8");
5929
6162
  return { status: "wired" };
5930
6163
  }
@@ -6003,7 +6236,7 @@ async function init(opts) {
6003
6236
  else if (critiqueAnswer) log(` Could not read '${critiqueAnswer}' \u2014 leaving it page by page; each spec will ask.`);
6004
6237
  }
6005
6238
  if (opts.yes) {
6006
- const declared2 = readConfiguredSurfaces(join31(opts.projectRoot, "jig.config.json"));
6239
+ const declared2 = readConfiguredSurfaces(join33(opts.projectRoot, "jig.config.json"));
6007
6240
  const inEffect = declared2 ?? surfaces;
6008
6241
  const map = inEffect.map((x) => `'${x.match}' \u2192 ${x.mode}`).join(", ");
6009
6242
  log(
@@ -6022,7 +6255,7 @@ Brand colour adjusted to clear the 4.5:1 contrast floor: l:${originalL}% \u2192
6022
6255
  const finalColor = { h: proposal.h, s: proposal.s, l: proposal.l };
6023
6256
  const projectSlug = deriveProjectSlug(opts.projectRoot);
6024
6257
  const configRelPath = "jig.config.json";
6025
- const configAbsPath = join31(opts.projectRoot, configRelPath);
6258
+ const configAbsPath = join33(opts.projectRoot, configRelPath);
6026
6259
  const configuredBrand = configuredBrandPath(opts.projectRoot, configAbsPath);
6027
6260
  if (configuredBrand.refused) {
6028
6261
  log(
@@ -6056,7 +6289,7 @@ The token layer is at ${currentDir}/, and this project's layout suggests ${would
6056
6289
  }
6057
6290
  }
6058
6291
  const brandRelPath = relocateTo ?? configuredBrand.path ?? priorBrand ?? relKey(...defaultTokenDir(detection), brandFileName(projectSlug));
6059
- const brandAbsPath = join31(opts.projectRoot, ...brandRelPath.split("/"));
6292
+ const brandAbsPath = join33(opts.projectRoot, ...brandRelPath.split("/"));
6060
6293
  const tokensRelDir = brandRelPath.split("/").slice(0, -1);
6061
6294
  log(
6062
6295
  `Token layer: ${tokensRelDir.join("/") || "."}/ \u2014 ` + (configuredBrand.path ? "from jig.config.json." : priorBrand ? "the layout this project already had. Set `brand` in jig.config.json to move it." : findWireTarget(detection) ? "beside the stylesheet being wired. Set `brand` in jig.config.json to put it elsewhere." : "no stylesheet found to follow, so the project root. Set `brand` in jig.config.json to move it.")
@@ -6089,15 +6322,15 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
6089
6322
  if (configAction !== "written") log(` ${configRelPath} exists and is not jig-tracked \u2014 leaving it alone (re-run without --yes to refresh).`);
6090
6323
  }
6091
6324
  if (brandAction === "written") {
6092
- const defaultCssRaw = readFileSync27(join31(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
6325
+ const defaultCssRaw = readFileSync28(join33(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
6093
6326
  const content = renderBrandFile(defaultCssRaw, projectSlug, opts.version, proposal);
6094
- mkdirSync6(dirname7(brandAbsPath), { recursive: true });
6327
+ mkdirSync6(dirname8(brandAbsPath), { recursive: true });
6095
6328
  writeFileSync4(brandAbsPath, content, "utf8");
6096
6329
  files[brandRelPath] = checksum(content);
6097
6330
  }
6098
6331
  if (relocateTo && configAction !== "written") {
6099
6332
  try {
6100
- const existing = JSON.parse(readFileSync27(configAbsPath, "utf8"));
6333
+ const existing = JSON.parse(readFileSync28(configAbsPath, "utf8"));
6101
6334
  const content = `${JSON.stringify({ ...existing, brand: brandRelPath }, null, 2)}
6102
6335
  `;
6103
6336
  writeFileSync4(configAbsPath, content, "utf8");
@@ -6116,12 +6349,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
6116
6349
  } else if (configAction === "merged") {
6117
6350
  let existing = {};
6118
6351
  try {
6119
- existing = JSON.parse(readFileSync27(configAbsPath, "utf8"));
6352
+ existing = JSON.parse(readFileSync28(configAbsPath, "utf8"));
6120
6353
  } catch {
6121
6354
  existing = {};
6122
6355
  }
6123
6356
  const existingBrand = typeof existing.brand === "string" ? existing.brand : void 0;
6124
- const brandStillResolves = existingBrand ? existsSync19(join31(opts.projectRoot, ...existingBrand.split("/"))) : false;
6357
+ const brandStillResolves = existingBrand ? existsSync19(join33(opts.projectRoot, ...existingBrand.split("/"))) : false;
6125
6358
  const existingSurfaces = Array.isArray(existing.surfaces) && existing.surfaces.length > 0 ? existing.surfaces : void 0;
6126
6359
  const merged = {
6127
6360
  ...existing,
@@ -6138,12 +6371,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
6138
6371
  }
6139
6372
  const effectiveConfig = loadEffectiveConfig(opts.projectRoot, configAbsPath, brandRelPath, surfaces);
6140
6373
  const primaryMode = effectiveConfig.surfaces[0]?.mode ?? "product";
6141
- const wiringBrandAbsPath = join31(opts.projectRoot, ...effectiveConfig.brand.split("/"));
6374
+ const wiringBrandAbsPath = join33(opts.projectRoot, ...effectiveConfig.brand.split("/"));
6142
6375
  const declaredModes = [...new Set(effectiveConfig.surfaces.map((s) => s.mode))];
6143
6376
  const modeAbsPaths = {};
6144
6377
  for (const mode of declaredModes) {
6145
6378
  const modeFileName = `mode.${mode}.css`;
6146
- const modeAbsPath2 = join31(opts.projectRoot, ...tokensRelDir, modeFileName);
6379
+ const modeAbsPath2 = join33(opts.projectRoot, ...tokensRelDir, modeFileName);
6147
6380
  modeAbsPaths[mode] = modeAbsPath2;
6148
6381
  const modeRelPath = relKey(...tokensRelDir, modeFileName);
6149
6382
  const modeState = fileState(opts.projectRoot, modeAbsPath2, modeRelPath, initManifest);
@@ -6151,9 +6384,9 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
6151
6384
  if (modeState.existsOnDisk && !modeState.tracked) modeAction = "skipped-untracked";
6152
6385
  else if (modeState.modified) modeAction = "skipped-edited";
6153
6386
  if (modeAction === "written") {
6154
- const modeSourceRaw = readFileSync27(join31(opts.packageRoot, "tokens", modeFileName), "utf8");
6387
+ const modeSourceRaw = readFileSync28(join33(opts.packageRoot, "tokens", modeFileName), "utf8");
6155
6388
  const content = vendorHeader(modeFileName, opts.version, "css", null) + modeSourceRaw;
6156
- mkdirSync6(dirname7(modeAbsPath2), { recursive: true });
6389
+ mkdirSync6(dirname8(modeAbsPath2), { recursive: true });
6157
6390
  writeFileSync4(modeAbsPath2, content, "utf8");
6158
6391
  files[modeRelPath] = checksum(content);
6159
6392
  } else {
@@ -6166,20 +6399,20 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
6166
6399
  const barrelFor = (mode) => multiMode ? `theme.${mode}.css` : "theme.css";
6167
6400
  for (const mode of declaredModes) {
6168
6401
  const rel = relKey(...tokensRelDir, barrelFor(mode));
6169
- const abs = join31(opts.projectRoot, ...tokensRelDir, barrelFor(mode));
6402
+ const abs = join33(opts.projectRoot, ...tokensRelDir, barrelFor(mode));
6170
6403
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
6171
6404
  if (state.existsOnDisk && (!state.tracked || state.modified)) {
6172
6405
  log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
6173
6406
  continue;
6174
6407
  }
6175
6408
  const content = barrelBody(barrelFor(mode), brandFileOnly, mode, opts.version);
6176
- mkdirSync6(dirname7(abs), { recursive: true });
6409
+ mkdirSync6(dirname8(abs), { recursive: true });
6177
6410
  writeFileSync4(abs, content, "utf8");
6178
6411
  files[rel] = checksum(content);
6179
6412
  }
6180
6413
  if (multiMode) {
6181
6414
  const rel = relKey(...tokensRelDir, "theme.css");
6182
- const abs = join31(opts.projectRoot, ...tokensRelDir, "theme.css");
6415
+ const abs = join33(opts.projectRoot, ...tokensRelDir, "theme.css");
6183
6416
  if (existsSync19(abs)) {
6184
6417
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
6185
6418
  if (state.tracked && !state.modified) {
@@ -6195,13 +6428,13 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
6195
6428
  const declared2 = declaredTokenNames(
6196
6429
  [brandAbsPath, modeAbsPath].map((f) => {
6197
6430
  try {
6198
- return readFileSync27(f, "utf8");
6431
+ return readFileSync28(f, "utf8");
6199
6432
  } catch {
6200
6433
  return "";
6201
6434
  }
6202
6435
  }).join("\n")
6203
6436
  );
6204
- const aliasable = tailwindNamespaced(declared2);
6437
+ const aliasable = compiledAliases(opts.projectRoot, declared2) ?? listedAliases(declared2);
6205
6438
  const rel = relKey(...tokensRelDir, "utilities.css");
6206
6439
  if (aliasable.length === 0) {
6207
6440
  log(`
@@ -6220,19 +6453,19 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
6220
6453
  log(` This changes how components are written. Both styles are correct.`);
6221
6454
  const answer = (await prompt(" Generate it? [y/N]: ")).toLowerCase();
6222
6455
  if (answer === "y" || answer === "yes") {
6223
- const abs = join31(opts.projectRoot, ...tokensRelDir, "utilities.css");
6456
+ const abs = join33(opts.projectRoot, ...tokensRelDir, "utilities.css");
6224
6457
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
6225
6458
  if (state.existsOnDisk && (!state.tracked || state.modified)) {
6226
6459
  log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
6227
6460
  } else {
6228
6461
  const content = utilitiesBody(aliasable, opts.version);
6229
- mkdirSync6(dirname7(abs), { recursive: true });
6462
+ mkdirSync6(dirname8(abs), { recursive: true });
6230
6463
  writeFileSync4(abs, content, "utf8");
6231
6464
  files[rel] = checksum(content);
6232
6465
  const target = findWireTarget(detection);
6233
6466
  const importPath = target ? relativeImportPath(
6234
- dirname7(join31(opts.projectRoot, target)),
6235
- join31(opts.projectRoot, ...tokensRelDir, "utilities.css")
6467
+ dirname8(join33(opts.projectRoot, target)),
6468
+ join33(opts.projectRoot, ...tokensRelDir, "utilities.css")
6236
6469
  ) : `./${rel}`;
6237
6470
  log(` Wrote ${rel}. Import it in the stylesheet that imports Tailwind:`);
6238
6471
  log(` @import "tailwindcss";`);
@@ -6251,11 +6484,11 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
6251
6484
  writeInitManifest(opts.projectRoot, { version: opts.version, modes: declaredModes, files });
6252
6485
  if (relocateTo && currentBrand) {
6253
6486
  const oldDir = currentBrand.split("/").slice(0, -1);
6254
- const oldAbsDir = join31(opts.projectRoot, ...oldDir);
6487
+ const oldAbsDir = join33(opts.projectRoot, ...oldDir);
6255
6488
  if (existsSync19(oldAbsDir)) {
6256
- for (const name of readdirSync11(oldAbsDir).filter((f) => f.endsWith(".css"))) {
6489
+ for (const name of readdirSync12(oldAbsDir).filter((f) => f.endsWith(".css"))) {
6257
6490
  const rel = relKey(...oldDir, name);
6258
- const abs = join31(oldAbsDir, name);
6491
+ const abs = join33(oldAbsDir, name);
6259
6492
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
6260
6493
  if (!state.tracked || state.modified) {
6261
6494
  log(` ${rel} was edited \u2014 left in place. Delete it yourself once you have moved anything you need.`);
@@ -6269,14 +6502,14 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
6269
6502
  const wireTarget = findWireTarget(detection);
6270
6503
  let wiring;
6271
6504
  if (multiMode) {
6272
- const bare2 = join31(opts.projectRoot, ...tokensRelDir, "theme.css");
6505
+ const bare2 = join33(opts.projectRoot, ...tokensRelDir, "theme.css");
6273
6506
  for (const file of detection.cssFiles.filter((f) => !isTokenLayerFile(f))) {
6274
- const abs = join31(opts.projectRoot, file);
6507
+ const abs = join33(opts.projectRoot, file);
6275
6508
  try {
6276
- const before = readFileSync27(abs, "utf8");
6509
+ const before = readFileSync28(abs, "utf8");
6277
6510
  const after = before.split("\n").filter((l) => {
6278
6511
  const m = /^\s*@import\s+["']([^"']+)["'];?\s*$/.exec(l);
6279
- return !(m && !/^[a-z]+:|^\//i.test(m[1]) && resolve2(dirname7(abs), m[1]) === bare2);
6512
+ return !(m && !/^[a-z]+:|^\//i.test(m[1]) && resolve2(dirname8(abs), m[1]) === bare2);
6280
6513
  }).join("\n");
6281
6514
  if (after !== before) {
6282
6515
  writeFileSync4(abs, after, "utf8");
@@ -6295,21 +6528,21 @@ Could not edit ${file}: ${err.message}. Remove its import of ${relKey(...tokensR
6295
6528
  snippet: effectiveConfig.surfaces.map((s) => `${s.match} \u2192 ${relKey(...tokensRelDir, barrelFor(s.mode))}`).join("\n")
6296
6529
  };
6297
6530
  } else if (wireTarget) {
6298
- const targetAbsDir = dirname7(join31(opts.projectRoot, wireTarget));
6531
+ const targetAbsDir = dirname8(join33(opts.projectRoot, wireTarget));
6299
6532
  const barrelImport = relativeImportPath(
6300
6533
  targetAbsDir,
6301
- join31(opts.projectRoot, ...tokensRelDir, "theme.css")
6534
+ join33(opts.projectRoot, ...tokensRelDir, "theme.css")
6302
6535
  );
6303
6536
  const snippet = `@import "${barrelImport}";`;
6304
6537
  try {
6305
6538
  if (relocateTo && currentBrand) {
6306
- const abs = join31(opts.projectRoot, wireTarget);
6539
+ const abs = join33(opts.projectRoot, wireTarget);
6307
6540
  const oldDirName = currentBrand.split("/").slice(0, -1).join("/");
6308
- const before = existsSync19(abs) ? readFileSync27(abs, "utf8") : "";
6541
+ const before = existsSync19(abs) ? readFileSync28(abs, "utf8") : "";
6309
6542
  const after = before.split("\n").filter((l) => !(l.includes("@import") && l.includes(oldDirName))).join("\n");
6310
6543
  if (after !== before) writeFileSync4(abs, after, "utf8");
6311
6544
  }
6312
- const outcome = wireBarrel(join31(opts.projectRoot, wireTarget), barrelImport);
6545
+ const outcome = wireBarrel(join33(opts.projectRoot, wireTarget), barrelImport);
6313
6546
  wiring = { target: wireTarget, status: outcome.status, snippet };
6314
6547
  const verb = outcome.status === "wired" ? "Wired" : outcome.status === "rewired" ? "Rewired" : "Already present in";
6315
6548
  log(`
@@ -6326,7 +6559,7 @@ Could not write to ${wireTarget}: ${err.message}`);
6326
6559
  } else {
6327
6560
  const barrelImport = relativeImportPath(
6328
6561
  opts.projectRoot,
6329
- join31(opts.projectRoot, ...tokensRelDir, "theme.css")
6562
+ join33(opts.projectRoot, ...tokensRelDir, "theme.css")
6330
6563
  );
6331
6564
  const snippet = `@import "${barrelImport}";`;
6332
6565
  wiring = { target: null, status: "print-only", snippet };
@@ -6335,7 +6568,7 @@ Could not write to ${wireTarget}: ${err.message}`);
6335
6568
  if (pasteTargets.length > 0) {
6336
6569
  log("Paste the block for whichever of these is your global stylesheet:");
6337
6570
  for (const target of pasteTargets) {
6338
- const dir = dirname7(join31(opts.projectRoot, target));
6571
+ const dir = dirname8(join33(opts.projectRoot, target));
6339
6572
  const forTarget = `@import "${relativeImportPath(dir, wiringBrandAbsPath)}";
6340
6573
  @import "${relativeImportPath(dir, modeAbsPath)}";`;
6341
6574
  log(`
@@ -6385,29 +6618,29 @@ Could not write to ${wireTarget}: ${err.message}`);
6385
6618
  }
6386
6619
 
6387
6620
  // src/commands/verdicts.ts
6388
- import { execFileSync as execFileSync3 } from "child_process";
6389
- import { existsSync as existsSync25, readFileSync as readFileSync33 } from "fs";
6390
- import { join as join39, relative as relative4, resolve as resolve5, sep as sep6 } from "path";
6621
+ import { execFileSync as execFileSync4 } from "child_process";
6622
+ import { existsSync as existsSync25, readFileSync as readFileSync34 } from "fs";
6623
+ import { join as join41, relative as relative4, resolve as resolve5, sep as sep6 } from "path";
6391
6624
 
6392
6625
  // src/rules/citations.ts
6393
- import { join as join32 } from "path";
6626
+ import { join as join34 } from "path";
6394
6627
  function citableIds(packageRoot2) {
6395
- const rulesDir = join32(packageRoot2, "rules");
6628
+ const rulesDir = join34(packageRoot2, "rules");
6396
6629
  const ids = [
6397
- ...loadRules(rulesDir, join32(packageRoot2, "rules.index.json")).map((r) => r.id),
6630
+ ...loadRules(rulesDir, join34(packageRoot2, "rules.index.json")).map((r) => r.id),
6398
6631
  ...loadSpecs(rulesDir).map((s) => s.id)
6399
6632
  ];
6400
6633
  return [...new Set(ids)].sort();
6401
6634
  }
6402
6635
 
6403
6636
  // src/probe/check.ts
6404
- import { existsSync as existsSync23, readdirSync as readdirSync14, readFileSync as readFileSync31 } from "fs";
6405
- import { join as join37 } from "path";
6637
+ import { existsSync as existsSync23, readdirSync as readdirSync15, readFileSync as readFileSync32 } from "fs";
6638
+ import { join as join39 } from "path";
6406
6639
 
6407
6640
  // src/probe/save.ts
6408
- import { existsSync as existsSync22, mkdirSync as mkdirSync7, readdirSync as readdirSync13, readFileSync as readFileSync30, writeFileSync as writeFileSync5 } from "fs";
6641
+ import { existsSync as existsSync22, mkdirSync as mkdirSync7, readdirSync as readdirSync14, readFileSync as readFileSync31, writeFileSync as writeFileSync5 } from "fs";
6409
6642
  import { fileURLToPath as fileURLToPath2 } from "url";
6410
- import { join as join36, relative as relative3, resolve as resolve4, sep as sep5 } from "path";
6643
+ import { join as join38, relative as relative3, resolve as resolve4, sep as sep5 } from "path";
6411
6644
 
6412
6645
  // src/probe/script.ts
6413
6646
  var PROBE_VERSION = 9;
@@ -6692,9 +6925,9 @@ var PROBE_SCRIPT = `(async () => {
6692
6925
 
6693
6926
  // src/probe/browser.ts
6694
6927
  import { spawn } from "child_process";
6695
- import { existsSync as existsSync20, mkdtempSync, readdirSync as readdirSync12, rmSync as rmSync3 } from "fs";
6928
+ import { existsSync as existsSync20, mkdtempSync, readdirSync as readdirSync13, rmSync as rmSync3 } from "fs";
6696
6929
  import { homedir as homedir2, tmpdir } from "os";
6697
- import { join as join33 } from "path";
6930
+ import { join as join35 } from "path";
6698
6931
  var CHROME_ENV = ["JIG_CHROME", "CHROME_PATH", "PUPPETEER_EXECUTABLE_PATH", "CHROMIUM_PATH"];
6699
6932
  var CHROME_COMMANDS = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge", "brave-browser", "chrome", "msedge"];
6700
6933
  var CHROME_APPS = [
@@ -6706,20 +6939,20 @@ var CHROME_APPS = [
6706
6939
  // Every Windows machine has Edge; Chrome is often installed per user.
6707
6940
  "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
6708
6941
  "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
6709
- ...process.env.LOCALAPPDATA ? [join33(process.env.LOCALAPPDATA, "Google", "Chrome", "Application", "chrome.exe")] : []
6942
+ ...process.env.LOCALAPPDATA ? [join35(process.env.LOCALAPPDATA, "Google", "Chrome", "Application", "chrome.exe")] : []
6710
6943
  ];
6711
6944
  function cachedBrowsers() {
6712
6945
  const roots = [
6713
- join33(homedir2(), ".cache", "ms-playwright"),
6714
- join33(homedir2(), "Library", "Caches", "ms-playwright"),
6715
- join33(homedir2(), "AppData", "Local", "ms-playwright"),
6716
- join33(homedir2(), ".cache", "puppeteer", "chrome")
6946
+ join35(homedir2(), ".cache", "ms-playwright"),
6947
+ join35(homedir2(), "Library", "Caches", "ms-playwright"),
6948
+ join35(homedir2(), "AppData", "Local", "ms-playwright"),
6949
+ join35(homedir2(), ".cache", "puppeteer", "chrome")
6717
6950
  ];
6718
6951
  const found = [];
6719
6952
  for (const root of roots) {
6720
6953
  let entries;
6721
6954
  try {
6722
- entries = readdirSync12(root);
6955
+ entries = readdirSync13(root);
6723
6956
  } catch {
6724
6957
  continue;
6725
6958
  }
@@ -6733,7 +6966,7 @@ function cachedBrowsers() {
6733
6966
  ["chrome-win", "chrome.exe"],
6734
6967
  ["chrome-win64", "chrome.exe"]
6735
6968
  ]) {
6736
- const candidate = join33(root, entry, ...rel);
6969
+ const candidate = join35(root, entry, ...rel);
6737
6970
  if (existsSync20(candidate)) found.push(candidate);
6738
6971
  }
6739
6972
  }
@@ -6759,7 +6992,7 @@ function which(command) {
6759
6992
  for (const dir of dirs) {
6760
6993
  if (!dir) continue;
6761
6994
  for (const ext of exts) {
6762
- const candidate = join33(dir, command + ext);
6995
+ const candidate = join35(dir, command + ext);
6763
6996
  if (existsSync20(candidate)) return candidate;
6764
6997
  }
6765
6998
  }
@@ -6772,7 +7005,7 @@ async function runProbe(opts) {
6772
7005
  "No browser found to run the probe. Install one, or set JIG_CHROME to a Chrome, Chromium or Edge binary. A project with Playwright or Puppeteer already has one, and Jig will find it."
6773
7006
  );
6774
7007
  }
6775
- const profile = mkdtempSync(join33(tmpdir(), "jig-probe-"));
7008
+ const profile = mkdtempSync(join35(tmpdir(), "jig-probe-"));
6776
7009
  const child = spawn(chrome, [
6777
7010
  "--headless=new",
6778
7011
  "--remote-debugging-port=0",
@@ -6876,8 +7109,8 @@ async function evaluate(endpoint, url, width, height, timeoutMs, expression) {
6876
7109
 
6877
7110
  // src/probe/serve.ts
6878
7111
  import { createServer } from "http";
6879
- import { existsSync as existsSync21, readFileSync as readFileSync28, statSync as statSync3 } from "fs";
6880
- import { extname, join as join34, resolve as resolve3, sep as sep4 } from "path";
7112
+ import { existsSync as existsSync21, readFileSync as readFileSync29, statSync as statSync3 } from "fs";
7113
+ import { extname, join as join36, resolve as resolve3, sep as sep4 } from "path";
6881
7114
  var TYPES = {
6882
7115
  ".html": "text/html; charset=utf-8",
6883
7116
  ".htm": "text/html; charset=utf-8",
@@ -6904,19 +7137,19 @@ async function serveDirectory(dir) {
6904
7137
  const root = resolve3(dir);
6905
7138
  const server = createServer((req, res) => {
6906
7139
  let path = decodeURIComponent((req.url ?? "/").split("?")[0].split("#")[0]);
6907
- let file = resolve3(join34(root, path));
7140
+ let file = resolve3(join36(root, path));
6908
7141
  if (file !== root && !file.startsWith(root + sep4)) {
6909
7142
  res.writeHead(403).end();
6910
7143
  return;
6911
7144
  }
6912
- if (existsSync21(file) && statSync3(file).isDirectory()) file = join34(file, "index.html");
7145
+ if (existsSync21(file) && statSync3(file).isDirectory()) file = join36(file, "index.html");
6913
7146
  else if (!existsSync21(file) && existsSync21(`${file}.html`)) file = `${file}.html`;
6914
7147
  if (!existsSync21(file)) {
6915
7148
  res.writeHead(404).end();
6916
7149
  return;
6917
7150
  }
6918
7151
  res.writeHead(200, { "content-type": TYPES[extname(file).toLowerCase()] ?? "application/octet-stream" });
6919
- res.end(readFileSync28(file));
7152
+ res.end(readFileSync29(file));
6920
7153
  });
6921
7154
  await new Promise((done) => server.listen(0, "127.0.0.1", done));
6922
7155
  const address = server.address();
@@ -6928,8 +7161,8 @@ async function serveDirectory(dir) {
6928
7161
  }
6929
7162
 
6930
7163
  // src/probe/switches.ts
6931
- import { readFileSync as readFileSync29 } from "fs";
6932
- import { join as join35 } from "path";
7164
+ import { readFileSync as readFileSync30 } from "fs";
7165
+ import { join as join37 } from "path";
6933
7166
  var DECLARATION = /--breakpoint-([a-z0-9-]+)\s*:\s*(\d+(?:\.\d+)?)(px|rem)\b/g;
6934
7167
  function declaredSwitches(projectRoot) {
6935
7168
  const found = /* @__PURE__ */ new Map();
@@ -6937,7 +7170,7 @@ function declaredSwitches(projectRoot) {
6937
7170
  if (!/\.(css|scss|astro|vue|svelte|html)$/i.test(file)) continue;
6938
7171
  let text;
6939
7172
  try {
6940
- text = readFileSync29(join35(projectRoot, file), "utf8");
7173
+ text = readFileSync30(join37(projectRoot, file), "utf8");
6941
7174
  } catch {
6942
7175
  continue;
6943
7176
  }
@@ -6972,11 +7205,11 @@ function saveProbe(opts) {
6972
7205
  }
6973
7206
  probe.pageFile = relative3(opts.projectRoot, page).split("\\").join("/");
6974
7207
  if (opts.serveRoot) probe.serveRoot = relative3(opts.projectRoot, resolve4(opts.projectRoot, opts.serveRoot)).split("\\").join("/") || ".";
6975
- probe.pageChecksum = checksum(readFileSync30(page, "utf8"));
7208
+ probe.pageChecksum = checksum(readFileSync31(page, "utf8"));
6976
7209
  probe.recordedAt = (/* @__PURE__ */ new Date()).toISOString();
6977
- const dir = join36(opts.projectRoot, ".jig", "critique", opts.surface);
7210
+ const dir = join38(opts.projectRoot, ".jig", "critique", opts.surface);
6978
7211
  mkdirSync7(dir, { recursive: true });
6979
- const path = join36(dir, `probe-${width}.json`);
7212
+ const path = join38(dir, `probe-${width}.json`);
6980
7213
  writeFileSync5(path, JSON.stringify(probe), "utf8");
6981
7214
  return { path: `.jig/critique/${opts.surface}/probe-${width}.json`, width, page: probe.pageFile };
6982
7215
  }
@@ -7025,11 +7258,11 @@ async function withPageUrl(projectRoot, abs, serve, use) {
7025
7258
  }
7026
7259
  }
7027
7260
  function recordedPage(projectRoot, surface) {
7028
- const dir = join36(projectRoot, ".jig", "critique", surface);
7261
+ const dir = join38(projectRoot, ".jig", "critique", surface);
7029
7262
  if (!existsSync22(dir)) return void 0;
7030
- for (const f of readdirSync13(dir).filter((name) => /^probe-\d+\.json$/.test(name)).sort()) {
7263
+ for (const f of readdirSync14(dir).filter((name) => /^probe-\d+\.json$/.test(name)).sort()) {
7031
7264
  try {
7032
- const probe = JSON.parse(readFileSync30(join36(dir, f), "utf8"));
7265
+ const probe = JSON.parse(readFileSync31(join38(dir, f), "utf8"));
7033
7266
  if (probe.pageFile && existsSync22(resolve4(projectRoot, probe.pageFile))) return probe.pageFile;
7034
7267
  } catch {
7035
7268
  }
@@ -7037,19 +7270,19 @@ function recordedPage(projectRoot, surface) {
7037
7270
  return void 0;
7038
7271
  }
7039
7272
  async function ensureProbes(opts) {
7040
- const dir = join36(opts.projectRoot, ".jig", "critique", opts.surface);
7273
+ const dir = join38(opts.projectRoot, ".jig", "critique", opts.surface);
7041
7274
  const abs = resolve4(opts.projectRoot, opts.page);
7042
7275
  let current;
7043
7276
  try {
7044
- current = checksum(readFileSync30(abs, "utf8"));
7277
+ current = checksum(readFileSync31(abs, "utf8"));
7045
7278
  } catch {
7046
7279
  return { recorded: [], reason: `${opts.page} could not be read` };
7047
7280
  }
7048
- const recorded = existsSync22(dir) ? readdirSync13(dir).map((f) => /^probe-(\d+)\.json$/.exec(f)?.[1]).filter((w) => !!w).map(Number) : [];
7281
+ const recorded = existsSync22(dir) ? readdirSync14(dir).map((f) => /^probe-(\d+)\.json$/.exec(f)?.[1]).filter((w) => !!w).map(Number) : [];
7049
7282
  const widths = [.../* @__PURE__ */ new Set([...probeWidths(PROBE_WIDTHS, declaredSwitches(opts.projectRoot)), ...recorded])].sort((a, b) => a - b);
7050
7283
  const missing = widths.filter((width) => {
7051
7284
  try {
7052
- const probe = JSON.parse(readFileSync30(join36(dir, `probe-${width}.json`), "utf8"));
7285
+ const probe = JSON.parse(readFileSync31(join38(dir, `probe-${width}.json`), "utf8"));
7053
7286
  return probe.jigProbe !== PROBE_VERSION || probe.pageChecksum !== current;
7054
7287
  } catch {
7055
7288
  return true;
@@ -7060,7 +7293,7 @@ async function ensureProbes(opts) {
7060
7293
  let serve;
7061
7294
  for (const width of widths) {
7062
7295
  try {
7063
- const probe = JSON.parse(readFileSync30(join36(dir, `probe-${width}.json`), "utf8"));
7296
+ const probe = JSON.parse(readFileSync31(join38(dir, `probe-${width}.json`), "utf8"));
7064
7297
  if (probe.serveRoot) {
7065
7298
  serve = probe.serveRoot;
7066
7299
  break;
@@ -7076,9 +7309,9 @@ async function ensureProbes(opts) {
7076
7309
  return { recorded: missing };
7077
7310
  }
7078
7311
  function critiquedSurfaces(projectRoot) {
7079
- const root = join36(projectRoot, ".jig", "critique");
7312
+ const root = join38(projectRoot, ".jig", "critique");
7080
7313
  if (!existsSync22(root)) return [];
7081
- return readdirSync13(root).filter((surface) => !surface.startsWith("_") && (existsSync22(join36(root, surface, "screen.json")) || existsSync22(join36(root, surface, "code.json"))));
7314
+ return readdirSync14(root).filter((surface) => !surface.startsWith("_") && (existsSync22(join38(root, surface, "screen.json")) || existsSync22(join38(root, surface, "code.json"))));
7082
7315
  }
7083
7316
 
7084
7317
  // src/probe/check.ts
@@ -7090,7 +7323,7 @@ function stampProblem(projectRoot, file, p) {
7090
7323
  if (!page) return `${file} names a page outside this project (${p.serveRoot ? p.pageFile : p.url ?? "no url"}).`;
7091
7324
  let current;
7092
7325
  try {
7093
- current = readFileSync31(page, "utf8");
7326
+ current = readFileSync32(page, "utf8");
7094
7327
  } catch {
7095
7328
  return `${file} was taken on ${p.pageFile}, which no longer exists.`;
7096
7329
  }
@@ -7102,9 +7335,9 @@ function stampProblem(projectRoot, file, p) {
7102
7335
  function readProbes(projectRoot, dir, errors) {
7103
7336
  if (!existsSync23(dir)) return [];
7104
7337
  const probes = [];
7105
- for (const f of readdirSync14(dir).filter((n) => /^probe-\d+\.json$/.test(n)).sort()) {
7338
+ for (const f of readdirSync15(dir).filter((n) => /^probe-\d+\.json$/.test(n)).sort()) {
7106
7339
  try {
7107
- const p = JSON.parse(readFileSync31(join37(dir, f), "utf8"));
7340
+ const p = JSON.parse(readFileSync32(join39(dir, f), "utf8"));
7108
7341
  if (typeof p.jigProbe === "number" && p.jigProbe < PROBE_VERSION && typeof p.width === "number") {
7109
7342
  errors.push(`${f} was taken by an older \`jig probe\` (version ${p.jigProbe}; this Jig reads version ${PROBE_VERSION}). Record it again with \`jig probe --run\`.`);
7110
7343
  continue;
@@ -7228,19 +7461,19 @@ function probeCheck(probes, verdictOf, indexable = true, mode) {
7228
7461
  }
7229
7462
 
7230
7463
  // src/check/spec-shape.ts
7231
- import { existsSync as existsSync24, readdirSync as readdirSync15, statSync as statSync4 } from "fs";
7232
- import { join as join38 } from "path";
7464
+ import { existsSync as existsSync24, readdirSync as readdirSync16, statSync as statSync4 } from "fs";
7465
+ import { join as join40 } from "path";
7233
7466
  function newestSpec(projectRoot) {
7234
- const dir = join38(projectRoot, ".jig", "specs");
7467
+ const dir = join40(projectRoot, ".jig", "specs");
7235
7468
  if (!existsSync24(dir)) return void 0;
7236
- const files = readdirSync15(dir).filter((f) => f.endsWith(".md"));
7469
+ const files = readdirSync16(dir).filter((f) => f.endsWith(".md"));
7237
7470
  if (files.length === 0) return void 0;
7238
- const newest = files.map((f) => ({ f, at: statSync4(join38(dir, f)).mtimeMs })).sort((a, b) => b.at - a.at)[0].f;
7239
- return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body: readText(join38(dir, newest)) };
7471
+ const newest = files.map((f) => ({ f, at: statSync4(join40(dir, f)).mtimeMs })).sort((a, b) => b.at - a.at)[0].f;
7472
+ return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body: readText(join40(dir, newest)) };
7240
7473
  }
7241
7474
  function specFor(projectRoot, surface) {
7242
7475
  if (surface) {
7243
- const path = join38(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7476
+ const path = join40(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7244
7477
  if (existsSync24(path)) return { path: `.jig/specs/${surface}.spec.md`, slug: surface, body: readText(path) };
7245
7478
  }
7246
7479
  return newestSpec(projectRoot);
@@ -7398,14 +7631,14 @@ function reasonProblems(file, judged, errors) {
7398
7631
  function readJson(path, errors) {
7399
7632
  if (!existsSync25(path)) return null;
7400
7633
  try {
7401
- return JSON.parse(readFileSync33(path, "utf8"));
7634
+ return JSON.parse(readFileSync34(path, "utf8"));
7402
7635
  } catch (e) {
7403
7636
  errors.push(`${path}: not valid JSON (${e.message})`);
7404
7637
  return {};
7405
7638
  }
7406
7639
  }
7407
7640
  function specIndexable(projectRoot, surface) {
7408
- const path = join39(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7641
+ const path = join41(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7409
7642
  let front = "";
7410
7643
  try {
7411
7644
  front = readText(path).split(/^---\s*$/m)[1] ?? "";
@@ -7413,11 +7646,20 @@ function specIndexable(projectRoot, surface) {
7413
7646
  }
7414
7647
  const declared2 = specIndexableField(front);
7415
7648
  if (declared2 === true || declared2 === false) return declared2;
7416
- const mode = /^\s*mode\s*:\s*(\w+)/im.exec(front)?.[1]?.toLowerCase();
7649
+ const mode = /^\s*mode\s*:\s*(\w+)/im.exec(front)?.[1]?.toLowerCase() ?? projectMode(projectRoot);
7417
7650
  return mode !== "product" && mode !== "operator";
7418
7651
  }
7652
+ function projectMode(projectRoot) {
7653
+ try {
7654
+ const config = JSON.parse(readText(join41(projectRoot, "jig.config.json")));
7655
+ const modes = [...new Set((config.surfaces ?? []).map((s) => s?.mode).filter((m) => typeof m === "string"))];
7656
+ return modes.length === 1 ? modes[0].toLowerCase() : void 0;
7657
+ } catch {
7658
+ return void 0;
7659
+ }
7660
+ }
7419
7661
  function specNeedsNav(projectRoot, surface) {
7420
- const path = join39(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7662
+ const path = join41(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7421
7663
  if (!existsSync25(path)) return false;
7422
7664
  const front = readText(path).split(/^---\s*$/m)[1] ?? "";
7423
7665
  const navField = [...front.matchAll(/^\s*nav:\s*(.+)$/gim)].some((m) => !/^\s*(none|n\/a|-)\b/i.test(m[1]));
@@ -7496,7 +7738,7 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
7496
7738
  function checkDecisions(projectRoot, dir, errors) {
7497
7739
  const required = decisionNames(projectRoot);
7498
7740
  if (required.length === 0) return { state: "ran", judged: 0, total: 0, findings: 0 };
7499
- const file = readJson(join39(dir, "decisions.json"), errors);
7741
+ const file = readJson(join41(dir, "decisions.json"), errors);
7500
7742
  if (!file) {
7501
7743
  errors.push(
7502
7744
  `decisions.json is missing. Every decision in DECISIONS.md is judged against the built page, one verdict each: ${required.slice(0, 4).join(", ")}${required.length > 4 ? `, and ${required.length - 4} more` : ""}. A page can satisfy every rule and still break what this project decided.`
@@ -7549,9 +7791,9 @@ function decisionsSince(projectRoot, dir, unjudged) {
7549
7791
  if (unjudged.length === 0) return [];
7550
7792
  const path = decisionsFile(projectRoot);
7551
7793
  if (!path) return [];
7552
- const git = (args) => execFileSync3("git", args, { cwd: projectRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
7794
+ const git = (args) => execFileSync4("git", args, { cwd: projectRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
7553
7795
  try {
7554
- const verdictFile = relative4(projectRoot, join39(dir, "decisions.json")).split(sep6).join("/");
7796
+ const verdictFile = relative4(projectRoot, join41(dir, "decisions.json")).split(sep6).join("/");
7555
7797
  if (git(["status", "--porcelain", "--", verdictFile])) return [];
7556
7798
  const judgedIn = git(["log", "-1", "--format=%H", "--", verdictFile]);
7557
7799
  if (!judgedIn) return [];
@@ -7575,7 +7817,7 @@ function decisionsSince(projectRoot, dir, unjudged) {
7575
7817
  function verifyVerdicts(opts) {
7576
7818
  const root = opts.packageRoot ?? assetRoot();
7577
7819
  const errors = [];
7578
- const index = JSON.parse(readFileSync33(join39(root, "rules.index.json"), "utf8"));
7820
+ const index = JSON.parse(readFileSync34(join41(root, "rules.index.json"), "utf8"));
7579
7821
  const judgment = index.filter((r) => r.bucket === "judgment" || r.bucket === "hybrid" && r.pass !== void 0);
7580
7822
  const screenIds = judgment.filter((r) => r.pass === "screen").map((r) => r.id);
7581
7823
  const codeIds = judgment.filter((r) => r.pass === "code").map((r) => r.id);
@@ -7584,9 +7826,9 @@ function verifyVerdicts(opts) {
7584
7826
  ...judgment.map((r) => [r.id, r.pass ?? ""])
7585
7827
  ]);
7586
7828
  const specIds = new Set(citableIds(root).filter((id) => /^[PMLRT]-\d+$/.test(id)));
7587
- const dir = join39(opts.projectRoot, ".jig", "critique", opts.surface);
7588
- const screenFile = readJson(join39(dir, "screen.json"), errors);
7589
- const codeFile = readJson(join39(dir, "code.json"), errors);
7829
+ const dir = join41(opts.projectRoot, ".jig", "critique", opts.surface);
7830
+ const screenFile = readJson(join41(dir, "screen.json"), errors);
7831
+ const codeFile = readJson(join41(dir, "code.json"), errors);
7590
7832
  const screenExtraRequired = specNeedsNav(opts.projectRoot, opts.surface) ? ["P-14"] : [];
7591
7833
  const decisionList = decisionNames(opts.projectRoot);
7592
7834
  const screen = checkArm("screen", screenFile, screenIds, passOf, specIds, screenExtraRequired, errors, decisionList);
@@ -7616,13 +7858,13 @@ function verifyVerdicts(opts) {
7616
7858
  const measured = [];
7617
7859
  let specFront = "";
7618
7860
  try {
7619
- specFront = readText(join39(opts.projectRoot, ".jig", "specs", `${opts.surface}.spec.md`)).split(/^---\s*$/m)[1] ?? "";
7861
+ specFront = readText(join41(opts.projectRoot, ".jig", "specs", `${opts.surface}.spec.md`)).split(/^---\s*$/m)[1] ?? "";
7620
7862
  } catch {
7621
7863
  }
7622
7864
  if (specIndexableField(specFront) === "unreadable") {
7623
7865
  errors.push(`.jig/specs/${opts.surface}.spec.md: \`indexable:\` is neither true nor false, so this review cannot tell whether the page is meant to be found and does not guess. Write \`indexable: true\` or \`indexable: false\`, and put the reason in the spec's body.`);
7624
7866
  } else {
7625
- const mode = /^\s*mode\s*:\s*["']?(\w+)/im.exec(specFront)?.[1]?.toLowerCase();
7867
+ const mode = /^\s*mode\s*:\s*["']?(\w+)/im.exec(specFront)?.[1]?.toLowerCase() ?? projectMode(opts.projectRoot);
7626
7868
  const probed = probeCheck(probes, verdictOf, specIndexable(opts.projectRoot, opts.surface), mode);
7627
7869
  errors.push(...probed.contradictions);
7628
7870
  const seen = /* @__PURE__ */ new Set();
@@ -7642,7 +7884,7 @@ function verifyVerdicts(opts) {
7642
7884
  return { ok: errors.length === 0, errors, screen, code, decisions, rendered, measured, line, ...previous ? { previous } : {} };
7643
7885
  }
7644
7886
  function previousFindings(projectRoot, dir) {
7645
- const git = (args) => execFileSync3("git", args, { cwd: projectRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
7887
+ const git = (args) => execFileSync4("git", args, { cwd: projectRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
7646
7888
  const rel = relative4(projectRoot, dir).split("\\").join("/");
7647
7889
  const files = ["screen.json", "code.json", "decisions.json"];
7648
7890
  const paths = files.map((f) => `${rel}/${f}`);
@@ -7661,7 +7903,7 @@ function previousFindings(projectRoot, dir) {
7661
7903
  const out = /* @__PURE__ */ new Map();
7662
7904
  let text;
7663
7905
  try {
7664
- text = at ? lf(git(["show", `${at}:./${rel}/${f}`])) : readText(join39(dir, f));
7906
+ text = at ? lf(git(["show", `${at}:./${rel}/${f}`])) : readText(join41(dir, f));
7665
7907
  } catch {
7666
7908
  return out;
7667
7909
  }
@@ -7743,17 +7985,17 @@ function sourceChanges(git, from, to) {
7743
7985
  }
7744
7986
 
7745
7987
  // src/commands/gate.ts
7746
- import { existsSync as existsSync28, mkdirSync as mkdirSync8, readdirSync as readdirSync17, readFileSync as readFileSync37, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
7988
+ import { existsSync as existsSync28, mkdirSync as mkdirSync8, readdirSync as readdirSync18, readFileSync as readFileSync38, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
7747
7989
  import { createHash as createHash3 } from "crypto";
7748
- import { join as join44 } from "path";
7990
+ import { join as join46 } from "path";
7749
7991
 
7750
7992
  // src/check/mockup-drawing.ts
7751
- import { join as join40 } from "path";
7993
+ import { join as join42 } from "path";
7752
7994
  var FRAMES = [["phone", 360], ["tablet", 768], ["desktop", 1280], ["wide", 1600]];
7753
7995
  function mockupDrawingProblems(root, specBody, at) {
7754
7996
  let html;
7755
7997
  try {
7756
- html = readText(join40(root, at));
7998
+ html = readText(join42(root, at));
7757
7999
  } catch {
7758
8000
  return [];
7759
8001
  }
@@ -7814,7 +8056,7 @@ function frameRegionProblems(frames, specBody, at) {
7814
8056
  function approvedDrawingProblems(root, specBody, at) {
7815
8057
  let html;
7816
8058
  try {
7817
- html = readText(join40(root, at));
8059
+ html = readText(join42(root, at));
7818
8060
  } catch {
7819
8061
  return [];
7820
8062
  }
@@ -7917,15 +8159,15 @@ function sizeBlock2(sizes, size) {
7917
8159
  }
7918
8160
 
7919
8161
  // src/commands/gate.ts
7920
- import { execFileSync as execFileSync4 } from "child_process";
8162
+ import { execFileSync as execFileSync5 } from "child_process";
7921
8163
 
7922
8164
  // src/commands/ship.ts
7923
- import { existsSync as existsSync26, readdirSync as readdirSync16, readFileSync as readFileSync35 } from "fs";
7924
- import { join as join42 } from "path";
8165
+ import { existsSync as existsSync26, readdirSync as readdirSync17, readFileSync as readFileSync36 } from "fs";
8166
+ import { join as join44 } from "path";
7925
8167
 
7926
8168
  // src/commands/seo.ts
7927
- import { readFileSync as readFileSync34 } from "fs";
7928
- import { join as join41 } from "path";
8169
+ import { readFileSync as readFileSync35 } from "fs";
8170
+ import { join as join43 } from "path";
7929
8171
  var SITEMAP_FILE = /(^|\/)sitemap(-index)?\.(xml|ts|js|mjs|tsx|jsx|rb|php|py)$/i;
7930
8172
  var ROBOTS_FILE = /(^|\/)robots\.(txt|ts|js|mjs|tsx|jsx)$/i;
7931
8173
  var ROUTE_IN_SITEMAP = /(?:<loc>\s*([^<\s]+)\s*<\/loc>)|(?:url\s*:\s*["'`]([^"'`]+)["'`])/gi;
@@ -7966,7 +8208,7 @@ function seo(opts) {
7966
8208
  for (const file of files) {
7967
8209
  let raw;
7968
8210
  try {
7969
- raw = readFileSync34(join41(root, file), "utf8");
8211
+ raw = readFileSync35(join43(root, file), "utf8");
7970
8212
  } catch {
7971
8213
  continue;
7972
8214
  }
@@ -8031,7 +8273,7 @@ function critiqueAtShip(projectRoot, specBody) {
8031
8273
  const own = specCritique(specBody);
8032
8274
  if (own) return own === "at-ship";
8033
8275
  try {
8034
- return JSON.parse(readFileSync35(join42(projectRoot, "jig.config.json"), "utf8")).critique === "at-ship";
8276
+ return JSON.parse(readFileSync36(join44(projectRoot, "jig.config.json"), "utf8")).critique === "at-ship";
8035
8277
  } catch {
8036
8278
  return false;
8037
8279
  }
@@ -8039,14 +8281,14 @@ function critiqueAtShip(projectRoot, specBody) {
8039
8281
  function tweakDeferred(dir) {
8040
8282
  let t;
8041
8283
  try {
8042
- t = JSON.parse(readFileSync35(join42(dir, "tweak.json"), "utf8"));
8284
+ t = JSON.parse(readFileSync36(join44(dir, "tweak.json"), "utf8"));
8043
8285
  } catch {
8044
8286
  return false;
8045
8287
  }
8046
8288
  if (t.deferred === void 0 || t.deferred === false) return false;
8047
8289
  let judged;
8048
8290
  try {
8049
- judged = JSON.parse(readFileSync35(join42(dir, "verdicts.lock"), "utf8")).tweak;
8291
+ judged = JSON.parse(readFileSync36(join44(dir, "verdicts.lock"), "utf8")).tweak;
8050
8292
  } catch {
8051
8293
  }
8052
8294
  return judged !== t.at;
@@ -8057,14 +8299,14 @@ function pageChecksum(html) {
8057
8299
  function changedSinceLock(projectRoot, dir) {
8058
8300
  let lock;
8059
8301
  try {
8060
- lock = JSON.parse(readFileSync35(join42(dir, "verdicts.lock"), "utf8"));
8302
+ lock = JSON.parse(readFileSync36(join44(dir, "verdicts.lock"), "utf8"));
8061
8303
  } catch {
8062
8304
  return void 0;
8063
8305
  }
8064
8306
  const file = lock.page?.file;
8065
8307
  if (!file || !lock.page?.checksum) return void 0;
8066
8308
  try {
8067
- const html = readFileSync35(join42(projectRoot, file), "utf8");
8309
+ const html = readFileSync36(join44(projectRoot, file), "utf8");
8068
8310
  const seen = [pageChecksum(html), checksum(html)];
8069
8311
  return seen.includes(lock.page.checksum) ? void 0 : file;
8070
8312
  } catch {
@@ -8076,7 +8318,7 @@ function supersededBy(front) {
8076
8318
  return named ? named : void 0;
8077
8319
  }
8078
8320
  function pageStatus(projectRoot, surface) {
8079
- const spec = readText(join42(projectRoot, ".jig", "specs", `${surface}.spec.md`));
8321
+ const spec = readText(join44(projectRoot, ".jig", "specs", `${surface}.spec.md`));
8080
8322
  const front = spec.split(/^---\s*$/m)[1] ?? "";
8081
8323
  if (!/^\s*confirmed\s*:\s*true\b/im.test(front)) {
8082
8324
  return { surface, state: "in-progress", detail: "its spec is not confirmed, so nothing of it is built to ship" };
@@ -8084,10 +8326,10 @@ function pageStatus(projectRoot, surface) {
8084
8326
  const successor = supersededBy(front);
8085
8327
  if (successor !== void 0) {
8086
8328
  const file = successor.replace(/(\.spec\.md)?$/, ".spec.md");
8087
- return existsSync26(join42(projectRoot, ".jig", "specs", file)) ? { surface, state: "superseded", detail: `superseded by ${file}, whose critique judges its page` } : { surface, state: "incomplete", detail: `\`superseded_by: ${successor}\` names a spec that does not exist in .jig/specs` };
8329
+ return existsSync26(join44(projectRoot, ".jig", "specs", file)) ? { surface, state: "superseded", detail: `superseded by ${file}, whose critique judges its page` } : { surface, state: "incomplete", detail: `\`superseded_by: ${successor}\` names a spec that does not exist in .jig/specs` };
8088
8330
  }
8089
- const dir = join42(projectRoot, ".jig", "critique", surface);
8090
- if (!existsSync26(join42(dir, "screen.json")) && !existsSync26(join42(dir, "code.json"))) {
8331
+ const dir = join44(projectRoot, ".jig", "critique", surface);
8332
+ if (!existsSync26(join44(dir, "screen.json")) && !existsSync26(join44(dir, "code.json"))) {
8091
8333
  return { surface, state: "never", detail: "never critiqued" };
8092
8334
  }
8093
8335
  if (tweakDeferred(dir)) return { surface, state: "deferred", detail: "a tweak deferred its re-judge" };
@@ -8097,7 +8339,7 @@ function pageStatus(projectRoot, surface) {
8097
8339
  const stale = v.errors.find((e) => /was taken (on|by) an older/.test(e));
8098
8340
  let judgedPage = false;
8099
8341
  try {
8100
- judgedPage = !!JSON.parse(readFileSync35(join42(dir, "verdicts.lock"), "utf8")).page?.checksum;
8342
+ judgedPage = !!JSON.parse(readFileSync36(join44(dir, "verdicts.lock"), "utf8")).page?.checksum;
8101
8343
  } catch {
8102
8344
  }
8103
8345
  if (stale && !judgedPage) return { surface, state: "changed", detail: "the page changed after its probes were taken" };
@@ -8117,8 +8359,8 @@ function ship(opts) {
8117
8359
  const mechanicalErrors = mechanical.findings.filter((f) => f.bucket === "mechanical" && f.severity === "error").length;
8118
8360
  const seoResult = seo({ projectRoot: root });
8119
8361
  const seoErrors = seoResult.findings.filter((f) => f.severity === "error").length;
8120
- const specsDir = join42(root, ".jig", "specs");
8121
- const surfaces = existsSync26(specsDir) ? readdirSync16(specsDir).filter((f) => f.endsWith(".spec.md") && !f.startsWith("_")).map((f) => f.replace(/\.spec\.md$/, "")).sort() : [];
8362
+ const specsDir = join44(root, ".jig", "specs");
8363
+ const surfaces = existsSync26(specsDir) ? readdirSync17(specsDir).filter((f) => f.endsWith(".spec.md") && !f.startsWith("_")).map((f) => f.replace(/\.spec\.md$/, "")).sort() : [];
8122
8364
  const pages = surfaces.map((s) => pageStatus(root, s));
8123
8365
  const owed = pages.filter((p) => p.state !== "judged" && p.state !== "in-progress" && p.state !== "superseded");
8124
8366
  const agentFiles = agentFileProblems(root);
@@ -8139,9 +8381,9 @@ function ship(opts) {
8139
8381
  }
8140
8382
 
8141
8383
  // src/check/spec-checked.ts
8142
- import { existsSync as existsSync27, readFileSync as readFileSync36 } from "fs";
8384
+ import { existsSync as existsSync27, readFileSync as readFileSync37 } from "fs";
8143
8385
  import { createHash as createHash2 } from "crypto";
8144
- import { join as join43 } from "path";
8386
+ import { join as join45 } from "path";
8145
8387
 
8146
8388
  // src/check/owner-word.ts
8147
8389
  var SAYS = {
@@ -8226,7 +8468,7 @@ function specCheckProblems(root, spec, then, owner, decisions) {
8226
8468
  const recordPath = `.jig/specs/${spec.slug}.checked.json`;
8227
8469
  let record;
8228
8470
  try {
8229
- record = JSON.parse(readFileSync36(join43(root, recordPath), "utf8"));
8471
+ record = JSON.parse(readFileSync37(join45(root, recordPath), "utf8"));
8230
8472
  } catch {
8231
8473
  problems.push(
8232
8474
  `${recordPath} is missing. Before the owner is asked to confirm, a reader who did not write the spec checks its quotations against the owner's words and its facts against their sources, and writes that record (step 3c). The owner checks the spec by the sheet it gives them.`
@@ -8255,10 +8497,10 @@ function specCheckProblems(root, spec, then, owner, decisions) {
8255
8497
  if (unsaid.length) problems.push(`${recordPath}: ${unsaid.length} unchecked ${unsaid.length === 1 ? "fact says" : "facts say"} nothing of why. Say in \`note\` why the source could not be opened; the owner opens it instead.`);
8256
8498
  const missing = facts.map((f) => typeof f?.source === "string" ? f.source.trim() : "").filter((s) => s && !/^[a-z][a-z0-9+.-]*:\/\//i.test(s)).filter((s) => {
8257
8499
  const [, file, line] = /^(.+?)(?::(\d+)(?:-\d+)?)?$/.exec(s) ?? [];
8258
- if (!file || !existsSync27(join43(root, file))) return true;
8500
+ if (!file || !existsSync27(join45(root, file))) return true;
8259
8501
  if (!line) return false;
8260
8502
  try {
8261
- return Number(line) > readFileSync36(join43(root, file), "utf8").split("\n").length;
8503
+ return Number(line) > readFileSync37(join45(root, file), "utf8").split("\n").length;
8262
8504
  } catch {
8263
8505
  return true;
8264
8506
  }
@@ -8275,7 +8517,7 @@ function lastJigInvocation(transcriptPath) {
8275
8517
  if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
8276
8518
  let text;
8277
8519
  try {
8278
- text = readFileSync37(transcriptPath, "utf8");
8520
+ text = readFileSync38(transcriptPath, "utf8");
8279
8521
  } catch {
8280
8522
  return void 0;
8281
8523
  }
@@ -8303,7 +8545,7 @@ function ownerWords(transcriptPath) {
8303
8545
  if (!transcriptPath || !existsSync28(transcriptPath)) return "";
8304
8546
  const said = [];
8305
8547
  try {
8306
- for (const line of readFileSync37(transcriptPath, "utf8").split("\n")) {
8548
+ for (const line of readFileSync38(transcriptPath, "utf8").split("\n")) {
8307
8549
  if (!line.includes('"user"')) continue;
8308
8550
  let entry;
8309
8551
  try {
@@ -8325,7 +8567,7 @@ function ownerWords(transcriptPath) {
8325
8567
  }
8326
8568
  function namedSpec(root, surface) {
8327
8569
  if (!surface) return specFor(root, void 0);
8328
- return existsSync28(join44(root, ".jig", "specs", `${surface}.spec.md`)) ? specFor(root, surface) : void 0;
8570
+ return existsSync28(join46(root, ".jig", "specs", `${surface}.spec.md`)) ? specFor(root, surface) : void 0;
8329
8571
  }
8330
8572
  function isConfirmed(body) {
8331
8573
  return /^\s*confirmed\s*:\s*true\b/im.test(body.split(/^---\s*$/m)[1] ?? "");
@@ -8334,7 +8576,7 @@ function ownerReplyAfter(transcriptPath, asked) {
8334
8576
  if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
8335
8577
  const turns = [];
8336
8578
  try {
8337
- for (const line of readFileSync37(transcriptPath, "utf8").split("\n")) {
8579
+ for (const line of readFileSync38(transcriptPath, "utf8").split("\n")) {
8338
8580
  let entry;
8339
8581
  try {
8340
8582
  entry = JSON.parse(line);
@@ -8362,7 +8604,7 @@ function lastAssistantText(transcriptPath) {
8362
8604
  if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
8363
8605
  let found;
8364
8606
  try {
8365
- for (const line of readFileSync37(transcriptPath, "utf8").split("\n")) {
8607
+ for (const line of readFileSync38(transcriptPath, "utf8").split("\n")) {
8366
8608
  if (!line.includes('"assistant"')) continue;
8367
8609
  let entry;
8368
8610
  try {
@@ -8388,7 +8630,7 @@ var ASKS_THE_OWNER = /* @__PURE__ */ new Set(["decide", "spec", "mockup", "tweak
8388
8630
  function sessionStart(transcriptPath) {
8389
8631
  if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
8390
8632
  try {
8391
- for (const line of readFileSync37(transcriptPath, "utf8").split("\n")) {
8633
+ for (const line of readFileSync38(transcriptPath, "utf8").split("\n")) {
8392
8634
  const m = /"timestamp"\s*:\s*"([^"]+)"/.exec(line);
8393
8635
  if (m) {
8394
8636
  const t = Date.parse(m[1]);
@@ -8403,29 +8645,29 @@ function verdictsMtime(dir) {
8403
8645
  let newest = 0;
8404
8646
  for (const name of VERDICT_FILES) {
8405
8647
  try {
8406
- newest = Math.max(newest, statSync5(join44(dir, name)).mtimeMs);
8648
+ newest = Math.max(newest, statSync5(join46(dir, name)).mtimeMs);
8407
8649
  } catch {
8408
8650
  }
8409
8651
  }
8410
8652
  return newest;
8411
8653
  }
8412
8654
  function surfacesInPlay(root, command, transcriptPath, surface) {
8413
- const critiqueDir = join44(root, ".jig", "critique");
8655
+ const critiqueDir = join46(root, ".jig", "critique");
8414
8656
  if (!existsSync28(critiqueDir)) return [];
8415
- const all = readdirSync17(critiqueDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("_") && !d.name.startsWith(".")).map((d) => d.name);
8657
+ const all = readdirSync18(critiqueDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("_") && !d.name.startsWith(".")).map((d) => d.name);
8416
8658
  const start = sessionStart(transcriptPath);
8417
8659
  if (start === void 0) return all;
8418
8660
  const current = command === "critique" || command === "tweak" ? specFor(root, surface)?.slug : void 0;
8419
- return all.filter((s) => s === current || verdictsMtime(join44(critiqueDir, s)) >= start - 1e3);
8661
+ return all.filter((s) => s === current || verdictsMtime(join46(critiqueDir, s)) >= start - 1e3);
8420
8662
  }
8421
8663
  function surfacesToProbe(root, input) {
8422
8664
  const invocation = lastJigInvocation(input.transcript_path);
8423
8665
  return surfacesInPlay(root, invocation?.command, input.transcript_path, invocation?.surface);
8424
8666
  }
8425
8667
  function fileAtSessionStart(root, path, start) {
8426
- const git = (args) => execFileSync4("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
8668
+ const git = (args) => execFileSync5("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
8427
8669
  try {
8428
- if (start !== void 0 && statSync5(join44(root, path)).mtimeMs < start - 1e3) return readText(join44(root, path));
8670
+ if (start !== void 0 && statSync5(join46(root, path)).mtimeMs < start - 1e3) return readText(join46(root, path));
8429
8671
  } catch {
8430
8672
  }
8431
8673
  try {
@@ -8436,14 +8678,14 @@ function fileAtSessionStart(root, path, start) {
8436
8678
  }
8437
8679
  }
8438
8680
  function changedOutsideRecords(root, start) {
8439
- const git = (args) => execFileSync4("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
8681
+ const git = (args) => execFileSync5("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
8440
8682
  try {
8441
8683
  const base = start === void 0 ? "HEAD" : git(["rev-list", "-1", `--before=@${Math.floor(start / 1e3)}`, "HEAD"]).trim();
8442
8684
  if (!base) return [];
8443
8685
  const changed = git(["diff", "--name-only", base, "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean);
8444
8686
  const created = git(["ls-files", "--others", "--exclude-standard", "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean).filter((f) => {
8445
8687
  try {
8446
- return start === void 0 || statSync5(join44(root, f)).mtimeMs >= start - 1e3;
8688
+ return start === void 0 || statSync5(join46(root, f)).mtimeMs >= start - 1e3;
8447
8689
  } catch {
8448
8690
  return false;
8449
8691
  }
@@ -8454,12 +8696,12 @@ function changedOutsideRecords(root, start) {
8454
8696
  }
8455
8697
  }
8456
8698
  function drawingChangedAfterApproval(root, specPath, at) {
8457
- const git = (args) => execFileSync4("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
8699
+ const git = (args) => execFileSync5("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
8458
8700
  try {
8459
8701
  const approvedIn = git(["log", "-1", "--format=%H", "-G", "^mockup[[:space:]]*:[[:space:]]*approved", "--", specPath]).trim();
8460
8702
  if (!approvedIn) return void 0;
8461
8703
  const then = lf(git(["show", `${approvedIn}:./${at}`]));
8462
- return then !== readText(join44(root, at)) ? approvedIn.slice(0, 7) : void 0;
8704
+ return then !== readText(join46(root, at)) ? approvedIn.slice(0, 7) : void 0;
8463
8705
  } catch {
8464
8706
  return void 0;
8465
8707
  }
@@ -8471,7 +8713,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8471
8713
  const found = decisionsFile(root);
8472
8714
  if (!found) problems.push("decide wrote no DECISIONS.md beside the token layer.");
8473
8715
  else {
8474
- const body = readText(join44(root, found));
8716
+ const body = readText(join46(root, found));
8475
8717
  if (!/^##\s+Unresolved\s*$/im.test(body)) {
8476
8718
  problems.push("DECISIONS.md has no `## Unresolved` section. Round 3 asks by name what is still undecided; write what the owner named, or `None named by the owner.`");
8477
8719
  }
@@ -8481,7 +8723,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8481
8723
  if (owner) problems.push(...quotesNotFrom(body, then, owner, "the owner's words in this session").map((p) => `DECISIONS.md: ${p}`));
8482
8724
  }
8483
8725
  }
8484
- if (command === "spec" && surface && !existsSync28(join44(root, ".jig", "specs", `${surface}.spec.md`))) {
8726
+ if (command === "spec" && surface && !existsSync28(join46(root, ".jig", "specs", `${surface}.spec.md`))) {
8485
8727
  problems.push(`spec wrote no .jig/specs/${surface}.spec.md. The spec is that file: a draft anywhere else is read by no later step and checked by nothing.`);
8486
8728
  return problems;
8487
8729
  }
@@ -8504,10 +8746,10 @@ function commandProblems(root, command, surface, start, owner = "") {
8504
8746
  const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
8505
8747
  if (/^approved/i.test(mockup)) {
8506
8748
  if (!at) problems.push(`${spec.path}: \`mockup_at:\` is empty. Record where the approved drawing is.`);
8507
- else if (!/^https?:/i.test(at) && !existsSync28(join44(root, at))) problems.push(`${spec.path}: \`mockup_at: ${at}\` does not exist.`);
8749
+ else if (!/^https?:/i.test(at) && !existsSync28(join46(root, at))) problems.push(`${spec.path}: \`mockup_at: ${at}\` does not exist.`);
8508
8750
  else if (!/^https?:/i.test(at) && !at.startsWith(".jig/mockups/")) problems.push(`The drawing is at ${at}. A mockup lives in .jig/mockups/, outside what check scans and outside what ships.`);
8509
8751
  }
8510
- if (at && /\.html?$/i.test(at) && existsSync28(join44(root, at))) problems.push(...mockupDrawingProblems(root, spec.body, at));
8752
+ if (at && /\.html?$/i.test(at) && existsSync28(join46(root, at))) problems.push(...mockupDrawingProblems(root, spec.body, at));
8511
8753
  }
8512
8754
  if (command === "tweak" && spec) problems.push(...tweakProblems(root, spec, owner, start));
8513
8755
  if ((command === "spec" || command === "make") && spec) {
@@ -8535,7 +8777,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8535
8777
  const front = spec.body.split(/^---\s*$/m)[1] ?? "";
8536
8778
  const mockup = mockupField(spec.body);
8537
8779
  const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
8538
- if (/^approved/i.test(mockup) && at && !/^https?:/i.test(at) && existsSync28(join44(root, at))) {
8780
+ if (/^approved/i.test(mockup) && at && !/^https?:/i.test(at) && existsSync28(join46(root, at))) {
8539
8781
  const changedAfter = drawingChangedAfterApproval(root, spec.path, at);
8540
8782
  if (changedAfter) {
8541
8783
  problems.push(`${at} has changed since the owner approved it (${changedAfter}). An approval is of the drawing as the owner saw it: set \`mockup: pending\` in ${spec.path} and ask again.`);
@@ -8547,7 +8789,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8547
8789
  if (found) {
8548
8790
  let now = "";
8549
8791
  try {
8550
- now = readText(join44(root, found));
8792
+ now = readText(join46(root, found));
8551
8793
  } catch {
8552
8794
  }
8553
8795
  const then = fileAtSessionStart(root, found, start);
@@ -8561,13 +8803,13 @@ function commandProblems(root, command, surface, start, owner = "") {
8561
8803
  if (found) {
8562
8804
  let now = "";
8563
8805
  try {
8564
- now = readText(join44(root, found));
8806
+ now = readText(join46(root, found));
8565
8807
  } catch {
8566
8808
  }
8567
8809
  const then = fileAtSessionStart(root, found, start);
8568
8810
  if (now && now !== then) {
8569
8811
  problems.push(...unsourcedReasons(now, then, { newWhysOnly: true }).map((p) => `${found}: ${p}`));
8570
- const change = readTweak(join44(root, ".jig", "critique", spec.slug))?.change;
8812
+ const change = readTweak(join46(root, ".jig", "critique", spec.slug))?.change;
8571
8813
  if (change) problems.push(...quotesNotFrom(now, then, change).map((p) => `${found}: ${p}`));
8572
8814
  }
8573
8815
  }
@@ -8576,7 +8818,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8576
8818
  for (const file of modeFiles(root)) {
8577
8819
  let now = "";
8578
8820
  try {
8579
- now = readText(join44(root, file));
8821
+ now = readText(join46(root, file));
8580
8822
  } catch {
8581
8823
  continue;
8582
8824
  }
@@ -8587,8 +8829,8 @@ function commandProblems(root, command, surface, start, owner = "") {
8587
8829
  }
8588
8830
  }
8589
8831
  if (command === "critique") {
8590
- const dir = join44(root, ".jig", "critique");
8591
- const surfaces = existsSync28(dir) ? readdirSync17(dir).filter((s) => existsSync28(join44(dir, s, "screen.json")) || existsSync28(join44(dir, s, "code.json"))) : [];
8832
+ const dir = join46(root, ".jig", "critique");
8833
+ const surfaces = existsSync28(dir) ? readdirSync18(dir).filter((s) => existsSync28(join46(dir, s, "screen.json")) || existsSync28(join46(dir, s, "code.json"))) : [];
8592
8834
  for (const surface2 of surfaces) {
8593
8835
  const v = verifyVerdicts({ projectRoot: root, surface: surface2 });
8594
8836
  if (v.ok && v.decisions.state !== "ran" && v.decisions.total > 0) {
@@ -8599,7 +8841,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8599
8841
  }
8600
8842
  }
8601
8843
  const own = specFor(root, surface)?.slug;
8602
- if (own && surfaces.includes(own)) problems.push(...reportOmissions(join44(dir, own), own));
8844
+ if (own && surfaces.includes(own)) problems.push(...reportOmissions(join46(dir, own), own));
8603
8845
  if (surfaces.length === 0) {
8604
8846
  problems.push("critique wrote no verdict files. Each reader arm writes .jig/critique/<surface>/screen.json or code.json, and `jig verdicts <surface>` \u2014 not your own count \u2014 decides whether the review is complete. A report without them is not a review.");
8605
8847
  }
@@ -8612,7 +8854,7 @@ function drawingBeforeAsking(root, surface) {
8612
8854
  const front = spec.body.split(/^---\s*$/m)[1] ?? "";
8613
8855
  const named = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
8614
8856
  const at = named || `.jig/mockups/${spec.slug}.html`;
8615
- if (/^https?:/i.test(at) || !/\.html?$/i.test(at) || !existsSync28(join44(root, at))) return [];
8857
+ if (/^https?:/i.test(at) || !/\.html?$/i.test(at) || !existsSync28(join46(root, at))) return [];
8616
8858
  const found = mockupDrawingProblems(root, spec.body, at);
8617
8859
  return found.length ? [`Fix the drawing before you put it to the owner; they approve what the spec lists, drawn. ${found.join(" ")}`] : [];
8618
8860
  }
@@ -8620,14 +8862,14 @@ function specChecked(root, spec, start, owner) {
8620
8862
  const found = decisionsFile(root);
8621
8863
  let decisions = "";
8622
8864
  try {
8623
- if (found) decisions = readText(join44(root, found));
8865
+ if (found) decisions = readText(join46(root, found));
8624
8866
  } catch {
8625
8867
  }
8626
8868
  return specCheckProblems(root, spec, fileAtSessionStart(root, spec.path, start), owner, decisions);
8627
8869
  }
8628
8870
  function modeFiles(root) {
8629
8871
  try {
8630
- const state = JSON.parse(readFileSync37(join44(root, ".jig", "state.json"), "utf8"));
8872
+ const state = JSON.parse(readFileSync38(join46(root, ".jig", "state.json"), "utf8"));
8631
8873
  return Object.keys(state.files ?? {}).filter((f) => /(^|\/)mode\.[\w-]+\.css$/.test(f));
8632
8874
  } catch {
8633
8875
  return [];
@@ -8636,7 +8878,7 @@ function modeFiles(root) {
8636
8878
  function reportOmissions(dir, surface) {
8637
8879
  let report2;
8638
8880
  try {
8639
- report2 = readText(join44(dir, "REPORT.md")).toLowerCase();
8881
+ report2 = readText(join46(dir, "REPORT.md")).toLowerCase();
8640
8882
  } catch {
8641
8883
  return [];
8642
8884
  }
@@ -8644,7 +8886,7 @@ function reportOmissions(dir, surface) {
8644
8886
  for (const f of VERDICT_FILES) {
8645
8887
  let file;
8646
8888
  try {
8647
- file = JSON.parse(readFileSync37(join44(dir, f), "utf8"));
8889
+ file = JSON.parse(readFileSync38(join46(dir, f), "utf8"));
8648
8890
  } catch {
8649
8891
  continue;
8650
8892
  }
@@ -8665,16 +8907,16 @@ function tweakProblems(root, spec, owner = "", start) {
8665
8907
  if (!/^(approved|skipped)/i.test(mockup)) problems.push(`${spec.path}: \`mockup:\` is ${mockup.split(/\s/)[0] || "empty"}. A tweak changes a page whose drawing the owner has approved (or skipped); take a new page through \`mockup\` and \`make\`.`);
8666
8908
  else problems.push(...structureSinceApproval(root, spec, front));
8667
8909
  const surface = spec.slug;
8668
- const record = readTweak(join44(root, ".jig", "critique", surface));
8910
+ const record = readTweak(join46(root, ".jig", "critique", surface));
8669
8911
  if (!record) {
8670
8912
  problems.push(`.jig/critique/${surface}/tweak.json is missing. It names the change in the owner's words (\`change\`), when it was made (\`at\`) and the rule ids and decisions re-judged for it (\`ids\`).`);
8671
8913
  } else {
8672
8914
  if (!record.change) problems.push(`.jig/critique/${surface}/tweak.json has no \`change\`: the owner's words for what changed.`);
8673
- else if (owner && fileAtSessionStart(root, `.jig/critique/${surface}/tweak.json`, start) !== readText(join44(root, ".jig", "critique", surface, "tweak.json")) && !quoteHeld(record.change.replace(/^["“]|["”]$/g, ""), owner)) {
8915
+ else if (owner && fileAtSessionStart(root, `.jig/critique/${surface}/tweak.json`, start) !== readText(join46(root, ".jig", "critique", surface, "tweak.json")) && !quoteHeld(record.change.replace(/^["“]|["”]$/g, ""), owner)) {
8674
8916
  problems.push(`.jig/critique/${surface}/tweak.json: \`change\` is "${record.change.length > 90 ? `${record.change.slice(0, 90)}\u2026` : record.change}", which the owner did not say in this session. It is their words for the change, as they gave them; what you made of them goes in the spec's Tweak entry.`);
8675
8917
  }
8676
8918
  try {
8677
- const raw = JSON.parse(readFileSync37(join44(root, ".jig", "critique", surface, "tweak.json"), "utf8"));
8919
+ const raw = JSON.parse(readFileSync38(join46(root, ".jig", "critique", surface, "tweak.json"), "utf8"));
8678
8920
  const extra = Object.keys(raw).filter((k) => !["at", "change", "ids", "deferred"].includes(k));
8679
8921
  if (extra.length) problems.push(`.jig/critique/${surface}/tweak.json: ${extra.map((k) => `\`${k}\``).join(", ")} ${extra.length === 1 ? "is not a field" : "are not fields"} Jig reads. tweak.json holds \`at\`, \`change\`, \`ids\` and \`deferred\`; what else you have to say goes in the spec's Tweak entry.`);
8680
8922
  } catch {
@@ -8689,7 +8931,7 @@ function tweakProblems(root, spec, owner = "", start) {
8689
8931
  }
8690
8932
  return problems;
8691
8933
  }
8692
- const unjudged = unjudgedTweakIds(join44(root, ".jig", "critique", surface), record);
8934
+ const unjudged = unjudgedTweakIds(join46(root, ".jig", "critique", surface), record);
8693
8935
  if (unjudged.length) {
8694
8936
  problems.push(
8695
8937
  `.jig/critique/${surface}: tweak.json names ${unjudged.slice(0, 6).join(", ")}${unjudged.length > 6 ? " and more" : ""} but no verdict for ${unjudged.length === 1 ? "it" : "them"} carries \`"tweak": "${record.at}"\`. Each named verdict is re-judged on the changed page by a reader that did not make the change.`
@@ -8699,7 +8941,7 @@ function tweakProblems(root, spec, owner = "", start) {
8699
8941
  return problems;
8700
8942
  }
8701
8943
  function structureSinceApproval(root, spec, front) {
8702
- const git = (args) => execFileSync4("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
8944
+ const git = (args) => execFileSync5("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
8703
8945
  let baseline;
8704
8946
  let judged = true;
8705
8947
  try {
@@ -8725,9 +8967,9 @@ function structureSinceApproval(root, spec, front) {
8725
8967
  problems.push(`${spec.path}: the regions under \`sizes:\` differ from the ones the page had ${when}. A change to what the page holds is not a tweak: take it through \`spec\` and \`mockup\`.`);
8726
8968
  }
8727
8969
  const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
8728
- if (at && !/^https?:/i.test(at) && existsSync28(join44(root, at))) {
8970
+ if (at && !/^https?:/i.test(at) && existsSync28(join46(root, at))) {
8729
8971
  try {
8730
- if (lf(git(["show", `${baseline}:./${at}`])) !== readText(join44(root, at))) {
8972
+ if (lf(git(["show", `${baseline}:./${at}`])) !== readText(join46(root, at))) {
8731
8973
  problems.push(`${at} has changed since ${when.replace(/^when /, "")}. A tweak leaves the drawing as approved; a change the drawing must show goes through \`mockup\`.`);
8732
8974
  }
8733
8975
  } catch {
@@ -8737,7 +8979,7 @@ function structureSinceApproval(root, spec, front) {
8737
8979
  }
8738
8980
  function readTweak(dir) {
8739
8981
  try {
8740
- const raw = JSON.parse(readFileSync37(join44(dir, "tweak.json"), "utf8"));
8982
+ const raw = JSON.parse(readFileSync38(join46(dir, "tweak.json"), "utf8"));
8741
8983
  const deferred = typeof raw.deferred === "string" || raw.deferred === true ? raw.deferred : void 0;
8742
8984
  return { at: String(raw.at ?? ""), change: String(raw.change ?? "").trim(), ids: Array.isArray(raw.ids) ? raw.ids.map(String) : [], ...deferred !== void 0 ? { deferred } : {} };
8743
8985
  } catch {
@@ -8750,13 +8992,13 @@ function surfacePage(projectRoot, surface) {
8750
8992
  const declared2 = /^\s*surface\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
8751
8993
  const candidates = [declared2, `${surface}.html`, declared2 ? `${declared2.replace(/^\//, "")}.html` : void 0].filter((c) => !!c && /\.\w+$/.test(c) === (c === declared2 ? /\.\w+$/.test(c) : true));
8752
8994
  for (const candidate of candidates) {
8753
- if (candidate && existsSync28(join44(projectRoot, candidate))) return candidate;
8995
+ if (candidate && existsSync28(join46(projectRoot, candidate))) return candidate;
8754
8996
  }
8755
8997
  return void 0;
8756
8998
  }
8757
8999
  function gate(opts) {
8758
9000
  const root = opts.projectRoot;
8759
- if (!existsSync28(join44(root, "jig.config.json")) && !existsSync28(join44(root, ".jig"))) {
9001
+ if (!existsSync28(join46(root, "jig.config.json")) && !existsSync28(join46(root, ".jig"))) {
8760
9002
  return { block: false, reason: "" };
8761
9003
  }
8762
9004
  const invocation = lastJigInvocation(opts.input.transcript_path);
@@ -8792,7 +9034,7 @@ ${result.report}`);
8792
9034
  problems.push(`/jig spec: you are asking the owner to confirm a spec that is not at .jig/specs/${invocation.surface}.spec.md. Write it there, with \`confirmed: false\`, have it checked (step 3c), and ask again: the checks the owner relies on read that file, and a draft kept anywhere else is checked by nothing.`);
8793
9035
  }
8794
9036
  if (spec) problems.push(...specChecked(root, spec, start, owner).map((p) => `/jig spec: ${p}`));
8795
- if (spec && existsSync28(join44(root, ".jig", "specs", `${spec.slug}.checked.json`))) {
9037
+ if (spec && existsSync28(join46(root, ".jig", "specs", `${spec.slug}.checked.json`))) {
8796
9038
  problems.push(...[...specProblems(spec), ...newFieldProblems(spec, fileAtSessionStart(root, spec.path, start))].map((p) => `/jig spec: ${p}`));
8797
9039
  }
8798
9040
  }
@@ -8819,15 +9061,15 @@ ${result.report}`);
8819
9061
  problems.push(...verdictGuard(root, command, surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)));
8820
9062
  problems.push(...probesLeftBehind(root, surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)));
8821
9063
  const touched = ["AGENTS.md", "CLAUDE.md"].some((f) => {
8822
- const now = existsSync28(join44(root, f)) ? readText(join44(root, f)) : "";
9064
+ const now = existsSync28(join46(root, f)) ? readText(join46(root, f)) : "";
8823
9065
  return fileAtSessionStart(root, f, start) !== now;
8824
9066
  });
8825
9067
  if (touched) problems.push(...agentFileProblems(root).map((p) => `Jig's agent instructions: ${p}`));
8826
- const critiqueDir = join44(root, ".jig", "critique");
9068
+ const critiqueDir = join46(root, ".jig", "critique");
8827
9069
  if (existsSync28(critiqueDir)) {
8828
9070
  for (const surface of surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)) {
8829
- const dir = join44(critiqueDir, surface);
8830
- if (!existsSync28(join44(dir, "screen.json")) && !existsSync28(join44(dir, "code.json"))) continue;
9071
+ const dir = join46(critiqueDir, surface);
9072
+ if (!existsSync28(join46(dir, "screen.json")) && !existsSync28(join46(dir, "code.json"))) continue;
8831
9073
  const v = verifyVerdicts({ projectRoot: root, surface });
8832
9074
  if (!v.ok) {
8833
9075
  const shown = v.errors.slice(0, 6).map((e) => ` ${e}`);
@@ -8857,10 +9099,10 @@ ${problems.join("\n\n")}`
8857
9099
  }
8858
9100
  const session = opts.input.session_id ?? "unknown";
8859
9101
  const key = `${session}:${createHash3("sha256").update(problems.join("\n")).digest("hex").slice(0, 12)}`;
8860
- const stateFile = join44(root, ".jig", "gate.json");
9102
+ const stateFile = join46(root, ".jig", "gate.json");
8861
9103
  let state = {};
8862
9104
  try {
8863
- state = JSON.parse(readFileSync37(stateFile, "utf8"));
9105
+ state = JSON.parse(readFileSync38(stateFile, "utf8"));
8864
9106
  } catch {
8865
9107
  }
8866
9108
  if (problems.length === 0) {
@@ -8895,7 +9137,7 @@ function verdictDigests(dir) {
8895
9137
  for (const f of VERDICT_FILES) {
8896
9138
  let file;
8897
9139
  try {
8898
- file = JSON.parse(readFileSync37(join44(dir, f), "utf8"));
9140
+ file = JSON.parse(readFileSync38(join46(dir, f), "utf8"));
8899
9141
  } catch {
8900
9142
  continue;
8901
9143
  }
@@ -8910,7 +9152,7 @@ function writeLock(root, surface, lockPath, dir, now) {
8910
9152
  const pageFile2 = recordedPage(root, surface);
8911
9153
  let page;
8912
9154
  try {
8913
- if (pageFile2) page = { file: pageFile2, checksum: pageChecksum(readFileSync37(join44(root, pageFile2), "utf8")) };
9155
+ if (pageFile2) page = { file: pageFile2, checksum: pageChecksum(readFileSync38(join46(root, pageFile2), "utf8")) };
8914
9156
  } catch {
8915
9157
  }
8916
9158
  const tweakAt = readTweak(dir)?.at;
@@ -8923,7 +9165,7 @@ function verdictChecksum(dir) {
8923
9165
  const parts = VERDICT_FILES.map((f) => {
8924
9166
  try {
8925
9167
  return `${f}
8926
- ${readFileSync37(join44(dir, f), "utf8")}`;
9168
+ ${readFileSync38(join46(dir, f), "utf8")}`;
8927
9169
  } catch {
8928
9170
  return "";
8929
9171
  }
@@ -8931,15 +9173,15 @@ ${readFileSync37(join44(dir, f), "utf8")}`;
8931
9173
  return parts.some(Boolean) ? checksum(parts.join("\n")) : void 0;
8932
9174
  }
8933
9175
  function verdictGuard(root, command, inPlay) {
8934
- const critiqueDir = join44(root, ".jig", "critique");
9176
+ const critiqueDir = join46(root, ".jig", "critique");
8935
9177
  if (!existsSync28(critiqueDir)) return [];
8936
9178
  const problems = [];
8937
- for (const surface of readdirSync17(critiqueDir)) {
9179
+ for (const surface of readdirSync18(critiqueDir)) {
8938
9180
  if (surface.startsWith("_")) continue;
8939
- const dir = join44(critiqueDir, surface);
9181
+ const dir = join46(critiqueDir, surface);
8940
9182
  const now = verdictChecksum(dir);
8941
9183
  if (!now) continue;
8942
- const lockPath = join44(dir, LOCK);
9184
+ const lockPath = join46(dir, LOCK);
8943
9185
  if ((command === "critique" || command === "ship") && (!inPlay || inPlay.includes(surface))) {
8944
9186
  writeLock(root, surface, lockPath, dir, now);
8945
9187
  problems.push(...lockLeftBehind(root, surface));
@@ -8947,7 +9189,7 @@ function verdictGuard(root, command, inPlay) {
8947
9189
  }
8948
9190
  let lock = {};
8949
9191
  try {
8950
- lock = JSON.parse(readFileSync37(lockPath, "utf8"));
9192
+ lock = JSON.parse(readFileSync38(lockPath, "utf8"));
8951
9193
  } catch {
8952
9194
  continue;
8953
9195
  }
@@ -8972,7 +9214,7 @@ function verdictGuard(root, command, inPlay) {
8972
9214
  function lockLeftBehind(root, surface) {
8973
9215
  const rel = `.jig/critique/${surface}`;
8974
9216
  try {
8975
- const status = (paths) => execFileSync4("git", ["status", "--porcelain", "--", ...paths], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
9217
+ const status = (paths) => execFileSync5("git", ["status", "--porcelain", "--", ...paths], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
8976
9218
  if (!status([`${rel}/${LOCK}`])) return [];
8977
9219
  if (status(VERDICT_FILES.map((f) => `${rel}/${f}`))) return [];
8978
9220
  } catch {
@@ -8984,7 +9226,7 @@ function probesLeftBehind(root, surfaces) {
8984
9226
  if (!surfaces.length) return [];
8985
9227
  let lines;
8986
9228
  try {
8987
- lines = execFileSync4("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\n").filter(Boolean);
9229
+ lines = execFileSync5("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\n").filter(Boolean);
8988
9230
  } catch {
8989
9231
  return [];
8990
9232
  }
@@ -9023,7 +9265,7 @@ function unjudgedTweakIds(dir, record) {
9023
9265
  for (const f of VERDICT_FILES) {
9024
9266
  let file;
9025
9267
  try {
9026
- file = JSON.parse(readFileSync37(join44(dir, f), "utf8"));
9268
+ file = JSON.parse(readFileSync38(join46(dir, f), "utf8"));
9027
9269
  } catch {
9028
9270
  continue;
9029
9271
  }
@@ -9035,7 +9277,7 @@ function unjudgedTweakIds(dir, record) {
9035
9277
  }
9036
9278
  function save(file, state) {
9037
9279
  try {
9038
- mkdirSync8(join44(file, ".."), { recursive: true });
9280
+ mkdirSync8(join46(file, ".."), { recursive: true });
9039
9281
  writeFileSync6(file, JSON.stringify(state), "utf8");
9040
9282
  } catch {
9041
9283
  }
@@ -9043,7 +9285,7 @@ function save(file, state) {
9043
9285
 
9044
9286
  // src/index.ts
9045
9287
  var packageRoot = getPackageRoot();
9046
- var { version } = JSON.parse(readFileSync38(join45(packageRoot, "package.json"), "utf8"));
9288
+ var { version } = JSON.parse(readFileSync39(join47(packageRoot, "package.json"), "utf8"));
9047
9289
  function warnIfUnpublishedPin() {
9048
9290
  if (isDevVersion(version)) {
9049
9291
  console.warn(
@@ -9155,12 +9397,12 @@ program.command("update").description("Update vendored Jig rules, skipping files
9155
9397
  program.command("checksum").description("Print a spec's checksum, the value its .checked.json records as `spec`.").argument("<spec>", "the spec by name (tokens-and-modes) or path (.jig/specs/tokens-and-modes.spec.md)").action((name) => {
9156
9398
  const projectRoot = findProjectRoot(process.cwd());
9157
9399
  const slug = name.replace(/^.*\//, "").replace(/\.spec\.md$/, "");
9158
- const path = join45(projectRoot, ".jig", "specs", `${slug}.spec.md`);
9400
+ const path = join47(projectRoot, ".jig", "specs", `${slug}.spec.md`);
9159
9401
  if (!existsSync29(path)) {
9160
9402
  console.error(` \u2717 no spec at .jig/specs/${slug}.spec.md`);
9161
9403
  process.exit(1);
9162
9404
  }
9163
- console.log(specChecksum(readFileSync38(path, "utf8")));
9405
+ console.log(specChecksum(readFileSync39(path, "utf8")));
9164
9406
  });
9165
9407
  program.command("verdicts").description("Verify a critique's verdict files and compute its counts.").argument("<surface>", "the surface slug the critique wrote under .jig/critique/").option("--reprobe", "re-take every probe that is missing or was taken on an older version of the page, first (needs a browser)").action(async (surface, opts) => {
9166
9408
  const projectRoot = findProjectRoot(process.cwd());
@@ -9294,7 +9536,7 @@ program.command("probe").description("Print the render probe. With --save, read
9294
9536
  program.command("gate").description("Run by the Claude Code Stop hook: block stopping while check or a critique fails.").action(async () => {
9295
9537
  let input = {};
9296
9538
  try {
9297
- if (!process.stdin.isTTY) input = JSON.parse(readFileSync38(0, "utf8") || "{}");
9539
+ if (!process.stdin.isTTY) input = JSON.parse(readFileSync39(0, "utf8") || "{}");
9298
9540
  } catch {
9299
9541
  }
9300
9542
  const cwd = input.cwd ?? process.cwd();
@@ -9335,7 +9577,7 @@ program.command("init").description("Set the project up to use Jig: a brand file
9335
9577
  });
9336
9578
  function resolveSurfaceMode(projectRoot) {
9337
9579
  try {
9338
- const config = JSON.parse(readFileSync38(join45(projectRoot, "jig.config.json"), "utf8"));
9580
+ const config = JSON.parse(readFileSync39(join47(projectRoot, "jig.config.json"), "utf8"));
9339
9581
  const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
9340
9582
  return modes.length === 1 ? modes[0] : void 0;
9341
9583
  } catch {