jig-ui 0.26.3 → 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, readdirSync as readdirSync11, 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) {
@@ -2643,145 +2770,47 @@ function lineForOffset(starts, offset) {
2643
2770
  if (starts[mid] <= offset) lo = mid;
2644
2771
  else hi = mid - 1;
2645
2772
  }
2646
- return lo + 1;
2647
- }
2648
- function sourceLine(source, line) {
2649
- const lines = source.split("\n");
2650
- return (lines[line - 1] ?? "").trim();
2651
- }
2652
-
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++;
2773
+ return lo + 1;
2774
+ }
2775
+ function sourceLine(source, line) {
2776
+ const lines = source.split("\n");
2777
+ return (lines[line - 1] ?? "").trim();
2778
+ }
2779
+
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 {
@@ -3837,8 +3934,8 @@ var menuState = {
3837
3934
  };
3838
3935
 
3839
3936
  // src/check/declared-properties.ts
3840
- import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
3841
- import { join as join20 } from "path";
3937
+ import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
3938
+ import { join as join22 } from "path";
3842
3939
  var DECLARATION_RE = /(?<![\w-])['"]?(--[A-Za-z_][\w-]*)['"]?\s*:/g;
3843
3940
  var AT_PROPERTY_RE = /@property\s+(--[\w-]+)/g;
3844
3941
  var SET_PROPERTY_RE = /setProperty\(\s*['"`](--[\w-]+)/g;
@@ -3850,16 +3947,16 @@ function addAll(names, text) {
3850
3947
  }
3851
3948
  }
3852
3949
  function readPackageImport(projectRoot, spec) {
3853
- const base = join20(projectRoot, "node_modules", spec);
3950
+ const base = join22(projectRoot, "node_modules", spec);
3854
3951
  for (const candidate of [base, `${base}.css`]) {
3855
3952
  try {
3856
- if (existsSync13(candidate) && candidate.endsWith(".css")) return readFileSync16(candidate, "utf8");
3953
+ if (existsSync13(candidate) && candidate.endsWith(".css")) return readFileSync17(candidate, "utf8");
3857
3954
  } catch {
3858
3955
  }
3859
3956
  }
3860
3957
  try {
3861
- const pkg = JSON.parse(readFileSync16(join20(base, "package.json"), "utf8"));
3862
- 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");
3863
3960
  } catch {
3864
3961
  }
3865
3962
  return void 0;
@@ -3871,7 +3968,7 @@ function collectDeclaredProperties(projectRoot, files, tokenNames = []) {
3871
3968
  for (const file of files) {
3872
3969
  let raw;
3873
3970
  try {
3874
- raw = readFileSync16(join20(projectRoot, file), "utf8");
3971
+ raw = readFileSync17(join22(projectRoot, file), "utf8");
3875
3972
  } catch {
3876
3973
  continue;
3877
3974
  }
@@ -4364,6 +4461,40 @@ var interfaceSafety = {
4364
4461
  }
4365
4462
  };
4366
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
+
4367
4498
  // src/check/detectors/generated-look.ts
4368
4499
  function report(ctx, name, file, raw, hits) {
4369
4500
  const seen = /* @__PURE__ */ new Set();
@@ -4719,6 +4850,7 @@ var DETECTORS = [
4719
4850
  semanticElement,
4720
4851
  metadata,
4721
4852
  interfaceSafety,
4853
+ newTabLink,
4722
4854
  ...generatedLookDetectors
4723
4855
  ];
4724
4856
  function getDetector(name) {
@@ -4738,7 +4870,7 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
4738
4870
  if (applicable.length === 0) continue;
4739
4871
  let raw;
4740
4872
  try {
4741
- raw = readFileSync17(join21(projectRoot, file), "utf8");
4873
+ raw = readFileSync18(join23(projectRoot, file), "utf8");
4742
4874
  } catch {
4743
4875
  continue;
4744
4876
  }
@@ -4752,8 +4884,8 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
4752
4884
  }
4753
4885
 
4754
4886
  // src/check/tokens.ts
4755
- import { existsSync as existsSync14, readdirSync as readdirSync9, readFileSync as readFileSync18 } from "fs";
4756
- 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";
4757
4889
  function rootDeclarations(source) {
4758
4890
  const out = {};
4759
4891
  for (const block of splitRuleBlocks(source)) {
@@ -4768,20 +4900,20 @@ function rootDeclarations(source) {
4768
4900
  }
4769
4901
  function loadTokenMap(projectRoot, consumerFiles = []) {
4770
4902
  const jig = {};
4771
- const tokensDir = join22(projectRoot, ".jig", "tokens");
4903
+ const tokensDir = join24(projectRoot, ".jig", "tokens");
4772
4904
  if (existsSync14(tokensDir)) {
4773
4905
  for (const file of readdirSync9(tokensDir).filter((f) => f.endsWith(".css")).sort()) {
4774
- Object.assign(jig, rootDeclarations(readFileSync18(join22(tokensDir, file), "utf8")));
4906
+ Object.assign(jig, rootDeclarations(readFileSync19(join24(tokensDir, file), "utf8")));
4775
4907
  }
4776
4908
  }
4777
4909
  const consumer = {};
4778
4910
  const ambiguous = /* @__PURE__ */ new Set();
4779
- const vendored = join22(".jig", "tokens");
4911
+ const vendored = join24(".jig", "tokens");
4780
4912
  for (const file of consumerFiles) {
4781
4913
  if (file.includes(vendored)) continue;
4782
4914
  let source;
4783
4915
  try {
4784
- source = readFileSync18(join22(projectRoot, file), "utf8");
4916
+ source = readFileSync19(join24(projectRoot, file), "utf8");
4785
4917
  } catch {
4786
4918
  continue;
4787
4919
  }
@@ -4800,8 +4932,8 @@ function loadTokenMap(projectRoot, consumerFiles = []) {
4800
4932
  }
4801
4933
 
4802
4934
  // src/check/exempt.ts
4803
- import { existsSync as existsSync15, readFileSync as readFileSync19 } from "fs";
4804
- import { join as join23 } from "path";
4935
+ import { existsSync as existsSync15, readFileSync as readFileSync20 } from "fs";
4936
+ import { join as join25 } from "path";
4805
4937
  function globToRegExp(glob) {
4806
4938
  let out = "";
4807
4939
  for (let i = 0; i < glob.length; i++) {
@@ -4821,11 +4953,11 @@ function globToRegExp(glob) {
4821
4953
  }
4822
4954
  function readExemptions(projectRoot) {
4823
4955
  const none = { patterns: [], ignored: [] };
4824
- const configPath = join23(projectRoot, "jig.config.json");
4956
+ const configPath = join25(projectRoot, "jig.config.json");
4825
4957
  if (!existsSync15(configPath)) return none;
4826
4958
  let declared2;
4827
4959
  try {
4828
- declared2 = JSON.parse(readFileSync19(configPath, "utf8")).exempt;
4960
+ declared2 = JSON.parse(readFileSync20(configPath, "utf8")).exempt;
4829
4961
  } catch {
4830
4962
  return none;
4831
4963
  }
@@ -4875,8 +5007,8 @@ function applyExemptions(files, patterns) {
4875
5007
  }
4876
5008
 
4877
5009
  // src/check/waiver.ts
4878
- import { readFileSync as readFileSync20 } from "fs";
4879
- import { join as join24 } from "path";
5010
+ import { readFileSync as readFileSync21 } from "fs";
5011
+ import { join as join26 } from "path";
4880
5012
  var WAIVER = /jig-allow\s+([A-Z]-\d+(?:\s*,\s*[A-Z]-\d+)*)\s*:(.*)$/;
4881
5013
  function readWaiver(line) {
4882
5014
  const m = WAIVER.exec(line);
@@ -4891,7 +5023,7 @@ function applyWaivers(root, findings) {
4891
5023
  let cached = lines.get(file);
4892
5024
  if (!cached) {
4893
5025
  try {
4894
- cached = readFileSync20(join24(root, file), "utf8").split("\n");
5026
+ cached = readFileSync21(join26(root, file), "utf8").split("\n");
4895
5027
  } catch {
4896
5028
  cached = [];
4897
5029
  }
@@ -4956,8 +5088,8 @@ function hasMenuToggle(raw) {
4956
5088
  }
4957
5089
 
4958
5090
  // src/check/mode-wiring.ts
4959
- import { existsSync as existsSync16, readFileSync as readFileSync21 } from "fs";
4960
- 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";
4961
5093
  var MODES = ["editorial", "product", "operator"];
4962
5094
  function configProblems(config) {
4963
5095
  const problems = [];
@@ -4979,10 +5111,10 @@ function configProblems(config) {
4979
5111
  }
4980
5112
  function modeWiringProblems(projectRoot) {
4981
5113
  let config;
4982
- const path = join25(projectRoot, "jig.config.json");
5114
+ const path = join27(projectRoot, "jig.config.json");
4983
5115
  if (!existsSync16(path)) return [];
4984
5116
  try {
4985
- config = JSON.parse(readFileSync21(path, "utf8"));
5117
+ config = JSON.parse(readFileSync22(path, "utf8"));
4986
5118
  } catch {
4987
5119
  return [{ mode: "", barrel: "jig.config.json", message: "jig.config.json is not valid JSON, so nothing in it applies" }];
4988
5120
  }
@@ -4991,14 +5123,14 @@ function modeWiringProblems(projectRoot) {
4991
5123
  if (typeof config.brand !== "string" || !Array.isArray(config.surfaces)) return [];
4992
5124
  const modes = [...new Set(config.surfaces.map((s) => s?.mode).filter((m) => typeof m === "string"))];
4993
5125
  if (modes.length === 0) return [];
4994
- const dir = dirname6(config.brand);
5126
+ const dir = dirname7(config.brand);
4995
5127
  const rel = (f) => dir === "." ? f : `${dir}/${f}`;
4996
5128
  const multi = modes.length > 1;
4997
5129
  const barrelOf = (mode) => multi ? `theme.${mode}.css` : "theme.css";
4998
- 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))));
4999
5131
  if (!anyBarrel) return [];
5000
5132
  const problems = [];
5001
- if (multi && existsSync16(join25(projectRoot, rel("theme.css")))) {
5133
+ if (multi && existsSync16(join27(projectRoot, rel("theme.css")))) {
5002
5134
  problems.push({
5003
5135
  mode: modes[0],
5004
5136
  barrel: rel("theme.css"),
@@ -5010,7 +5142,7 @@ function modeWiringProblems(projectRoot) {
5010
5142
  const modeFile = `mode.${mode}.css`;
5011
5143
  let body;
5012
5144
  try {
5013
- body = readFileSync21(join25(projectRoot, barrel), "utf8");
5145
+ body = readFileSync22(join27(projectRoot, barrel), "utf8");
5014
5146
  } catch {
5015
5147
  problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${barrel} does not exist` });
5016
5148
  return;
@@ -5022,7 +5154,7 @@ function modeWiringProblems(projectRoot) {
5022
5154
  barrel,
5023
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`
5024
5156
  });
5025
- } else if (!existsSync16(join25(projectRoot, rel(modeFile)))) {
5157
+ } else if (!existsSync16(join27(projectRoot, rel(modeFile)))) {
5026
5158
  problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${rel(modeFile)} does not exist` });
5027
5159
  }
5028
5160
  });
@@ -5030,8 +5162,8 @@ function modeWiringProblems(projectRoot) {
5030
5162
  }
5031
5163
 
5032
5164
  // src/check/token-audit.ts
5033
- import { existsSync as existsSync17, readdirSync as readdirSync10, readFileSync as readFileSync22 } from "fs";
5034
- 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";
5035
5167
  var TEXT_FLOOR = 4.5;
5036
5168
  var UI_FLOOR = 3;
5037
5169
  var FOREGROUNDS = [
@@ -5087,24 +5219,24 @@ function floorFor(token) {
5087
5219
  function tokenFiles(projectRoot) {
5088
5220
  const read = (rel) => {
5089
5221
  try {
5090
- return readFileSync22(join26(projectRoot, ...rel.split("/")), "utf8");
5222
+ return readFileSync23(join28(projectRoot, ...rel.split("/")), "utf8");
5091
5223
  } catch {
5092
5224
  return null;
5093
5225
  }
5094
5226
  };
5095
- const statePath = join26(projectRoot, ".jig", "state.json");
5227
+ const statePath = join28(projectRoot, ".jig", "state.json");
5096
5228
  if (existsSync17(statePath)) {
5097
5229
  try {
5098
- const state = JSON.parse(readFileSync22(statePath, "utf8"));
5230
+ const state = JSON.parse(readFileSync23(statePath, "utf8"));
5099
5231
  const recorded = Object.keys(state.files ?? {}).filter((f) => f.endsWith(".css")).sort();
5100
5232
  const out = recorded.map((path) => ({ path, source: read(path) })).filter((f) => f.source !== null);
5101
5233
  if (out.length > 0) return out;
5102
5234
  } catch {
5103
5235
  }
5104
5236
  }
5105
- const legacy = join26(projectRoot, ".jig", "tokens");
5237
+ const legacy = join28(projectRoot, ".jig", "tokens");
5106
5238
  if (!existsSync17(legacy)) return [];
5107
- 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") }));
5108
5240
  }
5109
5241
  function auditTokenLayer(projectRoot) {
5110
5242
  const files = tokenFiles(projectRoot);
@@ -5175,15 +5307,15 @@ function auditTokenLayer(projectRoot) {
5175
5307
  function checkExitCode(result) {
5176
5308
  return result.hasError ? 1 : 0;
5177
5309
  }
5178
- var LEGACY_INDEX_REL = join27(".jig", "rules.index.json");
5310
+ var LEGACY_INDEX_REL = join29(".jig", "rules.index.json");
5179
5311
  function resolveIndexPath(projectRoot) {
5180
- const legacy = join27(projectRoot, LEGACY_INDEX_REL);
5312
+ const legacy = join29(projectRoot, LEGACY_INDEX_REL);
5181
5313
  if (existsSync18(legacy)) return legacy;
5182
- return join27(assetRoot(), "rules.index.json");
5314
+ return join29(assetRoot(), "rules.index.json");
5183
5315
  }
5184
5316
  function resolveMode(projectRoot) {
5185
5317
  try {
5186
- const config = JSON.parse(readFileSync23(join27(projectRoot, "jig.config.json"), "utf8"));
5318
+ const config = JSON.parse(readFileSync24(join29(projectRoot, "jig.config.json"), "utf8"));
5187
5319
  const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
5188
5320
  if (modes.length === 1) return modes[0];
5189
5321
  return modes.length > 1 ? "mixed" : "unknown";
@@ -5200,14 +5332,14 @@ function summariseUnscanned(files) {
5200
5332
  }
5201
5333
  function countSpecs() {
5202
5334
  try {
5203
- return loadSpecs(join27(assetRoot(), "rules")).length;
5335
+ return loadSpecs(join29(assetRoot(), "rules")).length;
5204
5336
  } catch {
5205
5337
  return 0;
5206
5338
  }
5207
5339
  }
5208
5340
  function check(opts) {
5209
5341
  const indexPath = resolveIndexPath(opts.projectRoot);
5210
- const index = validateIndex(JSON.parse(readFileSync23(indexPath, "utf8")));
5342
+ const index = validateIndex(JSON.parse(readFileSync24(indexPath, "utf8")));
5211
5343
  const selection = selectFiles(opts.projectRoot, opts.all);
5212
5344
  const { patterns: exemptions, ignored: exemptIgnored } = readExemptions(opts.projectRoot);
5213
5345
  const { scanned: files, exempt, byPattern: counted } = applyExemptions(selection.files, exemptions);
@@ -5218,7 +5350,7 @@ function check(opts) {
5218
5350
  const projectParticipates = stylesheets.some((f) => {
5219
5351
  if (!hasExtension(f, CSS_EXTENSIONS)) return false;
5220
5352
  try {
5221
- return participatesInTokenLayer(readFileSync23(join27(opts.projectRoot, f), "utf8"), tokens);
5353
+ return participatesInTokenLayer(readFileSync24(join29(opts.projectRoot, f), "utf8"), tokens);
5222
5354
  } catch {
5223
5355
  return false;
5224
5356
  }
@@ -5226,7 +5358,7 @@ function check(opts) {
5226
5358
  const projectResponsive = stylesheets.some((f) => {
5227
5359
  if (!isStyleBearing(f)) return false;
5228
5360
  try {
5229
- const raw = readFileSync23(join27(opts.projectRoot, f), "utf8");
5361
+ const raw = readFileSync24(join29(opts.projectRoot, f), "utf8");
5230
5362
  return isResponsive(maskComments(maskNonStyleRegions(raw, f)), raw);
5231
5363
  } catch {
5232
5364
  return false;
@@ -5235,25 +5367,25 @@ function check(opts) {
5235
5367
  const viewportFitCover = stylesheets.some((f) => {
5236
5368
  if (!isStyleBearing(f)) return false;
5237
5369
  try {
5238
- 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"));
5239
5371
  } catch {
5240
5372
  return false;
5241
5373
  }
5242
5374
  });
5243
5375
  const projectMenuToggle = stylesheets.some((f) => {
5244
5376
  try {
5245
- return hasMenuToggle(readFileSync23(join27(opts.projectRoot, f), "utf8"));
5377
+ return hasMenuToggle(readFileSync24(join29(opts.projectRoot, f), "utf8"));
5246
5378
  } catch {
5247
5379
  return false;
5248
5380
  }
5249
5381
  });
5250
5382
  const declaredProperties = collectDeclaredProperties(opts.projectRoot, stylesheets, Object.keys(tokens));
5251
5383
  const touchFormText2 = { body: false, control: false };
5252
- const vendoredTokens = existsSync18(join27(opts.projectRoot, ".jig", "tokens")) ? readdirSync11(join27(opts.projectRoot, ".jig", "tokens")).filter((f) => f.endsWith(".css")).map((f) => join27(".jig", "tokens", f)) : [];
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)) : [];
5253
5385
  for (const f of [...stylesheets, ...vendoredTokens]) {
5254
5386
  if (!isStyleBearing(f)) continue;
5255
5387
  try {
5256
- const found = touchFormText(maskComments(maskNonStyleRegions(readFileSync23(join27(opts.projectRoot, f), "utf8"), f)));
5388
+ const found = touchFormText(maskComments(maskNonStyleRegions(readFileSync24(join29(opts.projectRoot, f), "utf8"), f)));
5257
5389
  touchFormText2.body ||= found.body;
5258
5390
  touchFormText2.control ||= found.control;
5259
5391
  } catch {
@@ -5261,7 +5393,7 @@ function check(opts) {
5261
5393
  }
5262
5394
  const withStyles = files.filter((f) => {
5263
5395
  try {
5264
- const src = readFileSync23(join27(opts.projectRoot, f), "utf8");
5396
+ const src = readFileSync24(join29(opts.projectRoot, f), "utf8");
5265
5397
  if (hasExtension(f, CSS_EXTENSIONS)) return src.trim().length > 0;
5266
5398
  if (maskNonStyleRegions(src, f).trim().length > 0) return true;
5267
5399
  return isMarkupHost(f) && classAttributeValues(src).some((a) => a.classes.trim().length > 0);
@@ -5291,6 +5423,19 @@ function check(opts) {
5291
5423
  for (const gap of brandDecidedGaps(opts.projectRoot)) {
5292
5424
  detected.push({ ruleId: "A-01", detector: "brand-decided", bucket: "mechanical", severity: "warning", file: gap.file, line: gap.line, message: gap.message });
5293
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
+ }
5294
5439
  const { findings, waived } = applyWaivers(opts.projectRoot, detected);
5295
5440
  const hasError = findings.some((f) => f.bucket === "mechanical" && f.severity === "error");
5296
5441
  const cssFiles = files.filter((f) => /\.(css|scss|less)$/i.test(f));
@@ -5299,7 +5444,7 @@ function check(opts) {
5299
5444
  // told to add a token @import it has nowhere to put.
5300
5445
  cssFiles.length > 0 && cssFiles.every((f) => {
5301
5446
  try {
5302
- return !participatesInTokenLayer(readFileSync23(join27(opts.projectRoot, f), "utf8"), tokens);
5447
+ return !participatesInTokenLayer(readFileSync24(join29(opts.projectRoot, f), "utf8"), tokens);
5303
5448
  } catch {
5304
5449
  return true;
5305
5450
  }
@@ -5322,6 +5467,7 @@ function check(opts) {
5322
5467
  exemptIgnored,
5323
5468
  scope: selection.mode,
5324
5469
  modeUnwired: modeWiringProblems(opts.projectRoot),
5470
+ tailwindSkipped: tailwind.skipped,
5325
5471
  agentFiles: agentFileProblems(opts.projectRoot, opts.homeDir || void 0),
5326
5472
  waived
5327
5473
  });
@@ -5329,12 +5475,12 @@ function check(opts) {
5329
5475
  }
5330
5476
 
5331
5477
  // src/commands/init.ts
5332
- import { existsSync as existsSync19, mkdirSync as mkdirSync6, readdirSync as readdirSync12, readFileSync as readFileSync27, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
5333
- 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";
5334
5480
 
5335
5481
  // src/init/detect.ts
5336
- import { readFileSync as readFileSync24 } from "fs";
5337
- import { join as join28 } from "path";
5482
+ import { readFileSync as readFileSync25 } from "fs";
5483
+ import { join as join30 } from "path";
5338
5484
  var TAILWIND_CONFIG_RE = /^tailwind\.config\.(js|ts|mjs|cjs)$/;
5339
5485
  var TAILWIND_V4_IMPORT_RE = /@import\s+["']tailwindcss["']/;
5340
5486
  var TAILWIND_V4_THEME_RE = /@theme\b/;
@@ -5351,7 +5497,7 @@ var FRAMEWORK_DEPS = [
5351
5497
  function detectFramework(projectRoot) {
5352
5498
  let pkg;
5353
5499
  try {
5354
- pkg = JSON.parse(readFileSync24(join28(projectRoot, "package.json"), "utf8"));
5500
+ pkg = JSON.parse(readFileSync25(join30(projectRoot, "package.json"), "utf8"));
5355
5501
  } catch {
5356
5502
  return void 0;
5357
5503
  }
@@ -5369,7 +5515,7 @@ function detect(projectRoot) {
5369
5515
  for (const f of cssFiles) {
5370
5516
  let source;
5371
5517
  try {
5372
- source = readFileSync24(join28(projectRoot, f), "utf8");
5518
+ source = readFileSync25(join30(projectRoot, f), "utf8");
5373
5519
  } catch {
5374
5520
  continue;
5375
5521
  }
@@ -5387,12 +5533,12 @@ function detect(projectRoot) {
5387
5533
  }
5388
5534
 
5389
5535
  // src/init/gitignore.ts
5390
- import { execFileSync as execFileSync2 } from "child_process";
5536
+ import { execFileSync as execFileSync3 } from "child_process";
5391
5537
  function isIgnored(projectRoot, relPath) {
5392
5538
  const candidates = relPath.endsWith("/") ? [relPath] : [relPath, `${relPath}/`];
5393
5539
  return candidates.some((path) => {
5394
5540
  try {
5395
- execFileSync2("git", ["check-ignore", "-q", "--", path], {
5541
+ execFileSync3("git", ["check-ignore", "-q", "--", path], {
5396
5542
  cwd: projectRoot,
5397
5543
  stdio: ["ignore", "ignore", "ignore"]
5398
5544
  });
@@ -5404,8 +5550,8 @@ function isIgnored(projectRoot, relPath) {
5404
5550
  }
5405
5551
 
5406
5552
  // src/init/derive.ts
5407
- import { readFileSync as readFileSync25 } from "fs";
5408
- import { join as join29 } from "path";
5553
+ import { readFileSync as readFileSync26 } from "fs";
5554
+ import { join as join31 } from "path";
5409
5555
  var DEFAULT_PROPOSAL = {
5410
5556
  h: 264,
5411
5557
  s: 0,
@@ -5438,7 +5584,7 @@ function fromCssCustomProperties(projectRoot, cssFiles) {
5438
5584
  for (const file of cssFiles) {
5439
5585
  let source;
5440
5586
  try {
5441
- source = readFileSync25(join29(projectRoot, file), "utf8");
5587
+ source = readFileSync26(join31(projectRoot, file), "utf8");
5442
5588
  } catch {
5443
5589
  continue;
5444
5590
  }
@@ -5487,7 +5633,7 @@ function balancedBraceBody(source, openIdx) {
5487
5633
  function fromTailwindConfig(projectRoot, tailwindConfigFile) {
5488
5634
  let source;
5489
5635
  try {
5490
- source = readFileSync25(join29(projectRoot, tailwindConfigFile), "utf8");
5636
+ source = readFileSync26(join31(projectRoot, tailwindConfigFile), "utf8");
5491
5637
  } catch {
5492
5638
  return null;
5493
5639
  }
@@ -5543,7 +5689,7 @@ function fromLiteralFrequency(projectRoot, cssFiles) {
5543
5689
  for (const file of cssFiles) {
5544
5690
  let source;
5545
5691
  try {
5546
- source = readFileSync25(join29(projectRoot, file), "utf8");
5692
+ source = readFileSync26(join31(projectRoot, file), "utf8");
5547
5693
  } catch {
5548
5694
  continue;
5549
5695
  }
@@ -5703,8 +5849,8 @@ function relativeImportPath(fromAbsDir, toAbsFile) {
5703
5849
  }
5704
5850
 
5705
5851
  // src/init/project-name.ts
5706
- import { readFileSync as readFileSync26 } from "fs";
5707
- 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";
5708
5854
  function slugify(name) {
5709
5855
  const withoutScope = name.replace(/^@[^/]+\//, "");
5710
5856
  const slug = withoutScope.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -5712,7 +5858,7 @@ function slugify(name) {
5712
5858
  }
5713
5859
  function deriveProjectSlug(projectRoot) {
5714
5860
  try {
5715
- const pkg = JSON.parse(readFileSync26(join30(projectRoot, "package.json"), "utf8"));
5861
+ const pkg = JSON.parse(readFileSync27(join32(projectRoot, "package.json"), "utf8"));
5716
5862
  if (typeof pkg.name === "string" && pkg.name.trim()) return slugify(pkg.name);
5717
5863
  } catch {
5718
5864
  }
@@ -5721,6 +5867,8 @@ function deriveProjectSlug(projectRoot) {
5721
5867
 
5722
5868
  // src/init/utilities.ts
5723
5869
  var TAILWIND_NAMESPACES = [
5870
+ "--opacity-",
5871
+ "--transition-duration-",
5724
5872
  "--color-",
5725
5873
  "--font-",
5726
5874
  "--text-",
@@ -5742,21 +5890,60 @@ var TAILWIND_NAMESPACES = [
5742
5890
  "--size-",
5743
5891
  "--border-width-"
5744
5892
  ];
5745
- function tailwindNamespaced(names) {
5746
- const kept = names.filter((n) => TAILWIND_NAMESPACES.some((ns) => n.startsWith(ns)));
5747
- 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
+ }
5748
5934
  }
5749
5935
  function declaredTokenNames(css) {
5750
5936
  return [...css.matchAll(/(^|[;{\s])(--[\w-]+)\s*:/g)].map((m) => m[2]);
5751
5937
  }
5752
- function utilitiesBody(names, version2) {
5753
- 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) {
5754
5941
  throw new Error(
5755
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."
5756
5943
  );
5757
5944
  }
5758
- const width = Math.max(...names.map((n) => n.length));
5759
- 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});`);
5760
5947
  return `/* utilities.css \u2014 vendored from Jig v${version2}.
5761
5948
  Licensed Apache-2.0. LICENSE and NOTICE ship beside the jig skill.
5762
5949
 
@@ -5780,10 +5967,11 @@ function utilitiesBody(names, version2) {
5780
5967
  order, so Jig's value always wins. Do not "fix" it \u2014 deleting the alias
5781
5968
  removes the utility, deleting Jig's removes the value.
5782
5969
 
5783
- Only tokens in a Tailwind namespace are listed. \`--measure-*\`, \`--focus-ring-*\`
5784
- and \`--duration-*\` have none, so they stay \`var()\`-only \u2014 read them from a class
5785
- with Tailwind's custom-property form instead: \`max-w-(--measure-prose)\`,
5786
- \`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)\`.
5787
5975
 
5788
5976
  Regenerate with \`jig init\` after the token layer changes; a token missing here
5789
5977
  is a class that renders and matches nothing. */
@@ -5866,9 +6054,9 @@ function isValidSurfaceArray(v) {
5866
6054
  function loadEffectiveConfig(projectRoot, configAbsPath, fallbackBrand, fallbackSurfaces) {
5867
6055
  if (!existsSync19(configAbsPath)) return { brand: fallbackBrand, surfaces: fallbackSurfaces };
5868
6056
  try {
5869
- const parsed = JSON.parse(readFileSync27(configAbsPath, "utf8"));
6057
+ const parsed = JSON.parse(readFileSync28(configAbsPath, "utf8"));
5870
6058
  const brandCandidate = typeof parsed.brand === "string" && parsed.brand ? parsed.brand : void 0;
5871
- const brandResolves = brandCandidate ? existsSync19(join31(projectRoot, ...brandCandidate.split("/"))) : false;
6059
+ const brandResolves = brandCandidate ? existsSync19(join33(projectRoot, ...brandCandidate.split("/"))) : false;
5872
6060
  return {
5873
6061
  brand: brandResolves ? brandCandidate : fallbackBrand,
5874
6062
  surfaces: isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : fallbackSurfaces
@@ -5881,7 +6069,7 @@ function configuredBrandPath(projectRoot, configAbsPath) {
5881
6069
  if (!existsSync19(configAbsPath)) return { path: null };
5882
6070
  let declared2;
5883
6071
  try {
5884
- declared2 = JSON.parse(readFileSync27(configAbsPath, "utf8")).brand;
6072
+ declared2 = JSON.parse(readFileSync28(configAbsPath, "utf8")).brand;
5885
6073
  } catch {
5886
6074
  return { path: null };
5887
6075
  }
@@ -5911,7 +6099,7 @@ function defaultTokenDir(detection) {
5911
6099
  function readConfiguredSurfaces(configAbsPath) {
5912
6100
  if (!existsSync19(configAbsPath)) return null;
5913
6101
  try {
5914
- const parsed = JSON.parse(readFileSync27(configAbsPath, "utf8"));
6102
+ const parsed = JSON.parse(readFileSync28(configAbsPath, "utf8"));
5915
6103
  return isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : null;
5916
6104
  } catch {
5917
6105
  return null;
@@ -5921,7 +6109,7 @@ function isCssModule(path) {
5921
6109
  return /\.module\.(css|scss)$/i.test(path);
5922
6110
  }
5923
6111
  function legacyBrandFile(projectRoot) {
5924
- const dir = join31(projectRoot, ".jig", "tokens");
6112
+ const dir = join33(projectRoot, ".jig", "tokens");
5925
6113
  if (!existsSync19(dir)) return void 0;
5926
6114
  const found = readdirSync12(dir).find((f) => /^brand\.[\w.-]+\.css$/.test(f));
5927
6115
  return found ? relKey(".jig", "tokens", found) : void 0;
@@ -5953,7 +6141,7 @@ var LEGACY_PAIR_RE = new RegExp(
5953
6141
  `@import\\s+["'][^"']*brand\\.[\\w.-]+\\.css["'];?\\r?\\n?\\s*@import\\s+["'][^"']*mode\\.(?:${MODES2.join("|")})\\.css["'];?\\r?\\n?`
5954
6142
  );
5955
6143
  function wireBarrel(absPath, barrelImport) {
5956
- const content = existsSync19(absPath) ? readFileSync27(absPath, "utf8") : "";
6144
+ const content = existsSync19(absPath) ? readFileSync28(absPath, "utf8") : "";
5957
6145
  const line = `@import "${barrelImport}";`;
5958
6146
  if (content.includes(line)) return { status: "already-present" };
5959
6147
  const legacy = LEGACY_PAIR_RE.exec(content);
@@ -5969,7 +6157,7 @@ function wireBarrel(absPath, barrelImport) {
5969
6157
 
5970
6158
  ` : `${line}
5971
6159
  `);
5972
- mkdirSync6(dirname7(absPath), { recursive: true });
6160
+ mkdirSync6(dirname8(absPath), { recursive: true });
5973
6161
  writeFileSync4(next === content ? absPath : absPath, next, "utf8");
5974
6162
  return { status: "wired" };
5975
6163
  }
@@ -6048,7 +6236,7 @@ async function init(opts) {
6048
6236
  else if (critiqueAnswer) log(` Could not read '${critiqueAnswer}' \u2014 leaving it page by page; each spec will ask.`);
6049
6237
  }
6050
6238
  if (opts.yes) {
6051
- const declared2 = readConfiguredSurfaces(join31(opts.projectRoot, "jig.config.json"));
6239
+ const declared2 = readConfiguredSurfaces(join33(opts.projectRoot, "jig.config.json"));
6052
6240
  const inEffect = declared2 ?? surfaces;
6053
6241
  const map = inEffect.map((x) => `'${x.match}' \u2192 ${x.mode}`).join(", ");
6054
6242
  log(
@@ -6067,7 +6255,7 @@ Brand colour adjusted to clear the 4.5:1 contrast floor: l:${originalL}% \u2192
6067
6255
  const finalColor = { h: proposal.h, s: proposal.s, l: proposal.l };
6068
6256
  const projectSlug = deriveProjectSlug(opts.projectRoot);
6069
6257
  const configRelPath = "jig.config.json";
6070
- const configAbsPath = join31(opts.projectRoot, configRelPath);
6258
+ const configAbsPath = join33(opts.projectRoot, configRelPath);
6071
6259
  const configuredBrand = configuredBrandPath(opts.projectRoot, configAbsPath);
6072
6260
  if (configuredBrand.refused) {
6073
6261
  log(
@@ -6101,7 +6289,7 @@ The token layer is at ${currentDir}/, and this project's layout suggests ${would
6101
6289
  }
6102
6290
  }
6103
6291
  const brandRelPath = relocateTo ?? configuredBrand.path ?? priorBrand ?? relKey(...defaultTokenDir(detection), brandFileName(projectSlug));
6104
- const brandAbsPath = join31(opts.projectRoot, ...brandRelPath.split("/"));
6292
+ const brandAbsPath = join33(opts.projectRoot, ...brandRelPath.split("/"));
6105
6293
  const tokensRelDir = brandRelPath.split("/").slice(0, -1);
6106
6294
  log(
6107
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.")
@@ -6134,15 +6322,15 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
6134
6322
  if (configAction !== "written") log(` ${configRelPath} exists and is not jig-tracked \u2014 leaving it alone (re-run without --yes to refresh).`);
6135
6323
  }
6136
6324
  if (brandAction === "written") {
6137
- const defaultCssRaw = readFileSync27(join31(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
6325
+ const defaultCssRaw = readFileSync28(join33(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
6138
6326
  const content = renderBrandFile(defaultCssRaw, projectSlug, opts.version, proposal);
6139
- mkdirSync6(dirname7(brandAbsPath), { recursive: true });
6327
+ mkdirSync6(dirname8(brandAbsPath), { recursive: true });
6140
6328
  writeFileSync4(brandAbsPath, content, "utf8");
6141
6329
  files[brandRelPath] = checksum(content);
6142
6330
  }
6143
6331
  if (relocateTo && configAction !== "written") {
6144
6332
  try {
6145
- const existing = JSON.parse(readFileSync27(configAbsPath, "utf8"));
6333
+ const existing = JSON.parse(readFileSync28(configAbsPath, "utf8"));
6146
6334
  const content = `${JSON.stringify({ ...existing, brand: brandRelPath }, null, 2)}
6147
6335
  `;
6148
6336
  writeFileSync4(configAbsPath, content, "utf8");
@@ -6161,12 +6349,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
6161
6349
  } else if (configAction === "merged") {
6162
6350
  let existing = {};
6163
6351
  try {
6164
- existing = JSON.parse(readFileSync27(configAbsPath, "utf8"));
6352
+ existing = JSON.parse(readFileSync28(configAbsPath, "utf8"));
6165
6353
  } catch {
6166
6354
  existing = {};
6167
6355
  }
6168
6356
  const existingBrand = typeof existing.brand === "string" ? existing.brand : void 0;
6169
- const brandStillResolves = existingBrand ? existsSync19(join31(opts.projectRoot, ...existingBrand.split("/"))) : false;
6357
+ const brandStillResolves = existingBrand ? existsSync19(join33(opts.projectRoot, ...existingBrand.split("/"))) : false;
6170
6358
  const existingSurfaces = Array.isArray(existing.surfaces) && existing.surfaces.length > 0 ? existing.surfaces : void 0;
6171
6359
  const merged = {
6172
6360
  ...existing,
@@ -6183,12 +6371,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
6183
6371
  }
6184
6372
  const effectiveConfig = loadEffectiveConfig(opts.projectRoot, configAbsPath, brandRelPath, surfaces);
6185
6373
  const primaryMode = effectiveConfig.surfaces[0]?.mode ?? "product";
6186
- const wiringBrandAbsPath = join31(opts.projectRoot, ...effectiveConfig.brand.split("/"));
6374
+ const wiringBrandAbsPath = join33(opts.projectRoot, ...effectiveConfig.brand.split("/"));
6187
6375
  const declaredModes = [...new Set(effectiveConfig.surfaces.map((s) => s.mode))];
6188
6376
  const modeAbsPaths = {};
6189
6377
  for (const mode of declaredModes) {
6190
6378
  const modeFileName = `mode.${mode}.css`;
6191
- const modeAbsPath2 = join31(opts.projectRoot, ...tokensRelDir, modeFileName);
6379
+ const modeAbsPath2 = join33(opts.projectRoot, ...tokensRelDir, modeFileName);
6192
6380
  modeAbsPaths[mode] = modeAbsPath2;
6193
6381
  const modeRelPath = relKey(...tokensRelDir, modeFileName);
6194
6382
  const modeState = fileState(opts.projectRoot, modeAbsPath2, modeRelPath, initManifest);
@@ -6196,9 +6384,9 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
6196
6384
  if (modeState.existsOnDisk && !modeState.tracked) modeAction = "skipped-untracked";
6197
6385
  else if (modeState.modified) modeAction = "skipped-edited";
6198
6386
  if (modeAction === "written") {
6199
- const modeSourceRaw = readFileSync27(join31(opts.packageRoot, "tokens", modeFileName), "utf8");
6387
+ const modeSourceRaw = readFileSync28(join33(opts.packageRoot, "tokens", modeFileName), "utf8");
6200
6388
  const content = vendorHeader(modeFileName, opts.version, "css", null) + modeSourceRaw;
6201
- mkdirSync6(dirname7(modeAbsPath2), { recursive: true });
6389
+ mkdirSync6(dirname8(modeAbsPath2), { recursive: true });
6202
6390
  writeFileSync4(modeAbsPath2, content, "utf8");
6203
6391
  files[modeRelPath] = checksum(content);
6204
6392
  } else {
@@ -6211,20 +6399,20 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
6211
6399
  const barrelFor = (mode) => multiMode ? `theme.${mode}.css` : "theme.css";
6212
6400
  for (const mode of declaredModes) {
6213
6401
  const rel = relKey(...tokensRelDir, barrelFor(mode));
6214
- const abs = join31(opts.projectRoot, ...tokensRelDir, barrelFor(mode));
6402
+ const abs = join33(opts.projectRoot, ...tokensRelDir, barrelFor(mode));
6215
6403
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
6216
6404
  if (state.existsOnDisk && (!state.tracked || state.modified)) {
6217
6405
  log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
6218
6406
  continue;
6219
6407
  }
6220
6408
  const content = barrelBody(barrelFor(mode), brandFileOnly, mode, opts.version);
6221
- mkdirSync6(dirname7(abs), { recursive: true });
6409
+ mkdirSync6(dirname8(abs), { recursive: true });
6222
6410
  writeFileSync4(abs, content, "utf8");
6223
6411
  files[rel] = checksum(content);
6224
6412
  }
6225
6413
  if (multiMode) {
6226
6414
  const rel = relKey(...tokensRelDir, "theme.css");
6227
- const abs = join31(opts.projectRoot, ...tokensRelDir, "theme.css");
6415
+ const abs = join33(opts.projectRoot, ...tokensRelDir, "theme.css");
6228
6416
  if (existsSync19(abs)) {
6229
6417
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
6230
6418
  if (state.tracked && !state.modified) {
@@ -6240,13 +6428,13 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
6240
6428
  const declared2 = declaredTokenNames(
6241
6429
  [brandAbsPath, modeAbsPath].map((f) => {
6242
6430
  try {
6243
- return readFileSync27(f, "utf8");
6431
+ return readFileSync28(f, "utf8");
6244
6432
  } catch {
6245
6433
  return "";
6246
6434
  }
6247
6435
  }).join("\n")
6248
6436
  );
6249
- const aliasable = tailwindNamespaced(declared2);
6437
+ const aliasable = compiledAliases(opts.projectRoot, declared2) ?? listedAliases(declared2);
6250
6438
  const rel = relKey(...tokensRelDir, "utilities.css");
6251
6439
  if (aliasable.length === 0) {
6252
6440
  log(`
@@ -6265,19 +6453,19 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
6265
6453
  log(` This changes how components are written. Both styles are correct.`);
6266
6454
  const answer = (await prompt(" Generate it? [y/N]: ")).toLowerCase();
6267
6455
  if (answer === "y" || answer === "yes") {
6268
- const abs = join31(opts.projectRoot, ...tokensRelDir, "utilities.css");
6456
+ const abs = join33(opts.projectRoot, ...tokensRelDir, "utilities.css");
6269
6457
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
6270
6458
  if (state.existsOnDisk && (!state.tracked || state.modified)) {
6271
6459
  log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
6272
6460
  } else {
6273
6461
  const content = utilitiesBody(aliasable, opts.version);
6274
- mkdirSync6(dirname7(abs), { recursive: true });
6462
+ mkdirSync6(dirname8(abs), { recursive: true });
6275
6463
  writeFileSync4(abs, content, "utf8");
6276
6464
  files[rel] = checksum(content);
6277
6465
  const target = findWireTarget(detection);
6278
6466
  const importPath = target ? relativeImportPath(
6279
- dirname7(join31(opts.projectRoot, target)),
6280
- join31(opts.projectRoot, ...tokensRelDir, "utilities.css")
6467
+ dirname8(join33(opts.projectRoot, target)),
6468
+ join33(opts.projectRoot, ...tokensRelDir, "utilities.css")
6281
6469
  ) : `./${rel}`;
6282
6470
  log(` Wrote ${rel}. Import it in the stylesheet that imports Tailwind:`);
6283
6471
  log(` @import "tailwindcss";`);
@@ -6296,11 +6484,11 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
6296
6484
  writeInitManifest(opts.projectRoot, { version: opts.version, modes: declaredModes, files });
6297
6485
  if (relocateTo && currentBrand) {
6298
6486
  const oldDir = currentBrand.split("/").slice(0, -1);
6299
- const oldAbsDir = join31(opts.projectRoot, ...oldDir);
6487
+ const oldAbsDir = join33(opts.projectRoot, ...oldDir);
6300
6488
  if (existsSync19(oldAbsDir)) {
6301
6489
  for (const name of readdirSync12(oldAbsDir).filter((f) => f.endsWith(".css"))) {
6302
6490
  const rel = relKey(...oldDir, name);
6303
- const abs = join31(oldAbsDir, name);
6491
+ const abs = join33(oldAbsDir, name);
6304
6492
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
6305
6493
  if (!state.tracked || state.modified) {
6306
6494
  log(` ${rel} was edited \u2014 left in place. Delete it yourself once you have moved anything you need.`);
@@ -6314,14 +6502,14 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
6314
6502
  const wireTarget = findWireTarget(detection);
6315
6503
  let wiring;
6316
6504
  if (multiMode) {
6317
- const bare2 = join31(opts.projectRoot, ...tokensRelDir, "theme.css");
6505
+ const bare2 = join33(opts.projectRoot, ...tokensRelDir, "theme.css");
6318
6506
  for (const file of detection.cssFiles.filter((f) => !isTokenLayerFile(f))) {
6319
- const abs = join31(opts.projectRoot, file);
6507
+ const abs = join33(opts.projectRoot, file);
6320
6508
  try {
6321
- const before = readFileSync27(abs, "utf8");
6509
+ const before = readFileSync28(abs, "utf8");
6322
6510
  const after = before.split("\n").filter((l) => {
6323
6511
  const m = /^\s*@import\s+["']([^"']+)["'];?\s*$/.exec(l);
6324
- 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);
6325
6513
  }).join("\n");
6326
6514
  if (after !== before) {
6327
6515
  writeFileSync4(abs, after, "utf8");
@@ -6340,21 +6528,21 @@ Could not edit ${file}: ${err.message}. Remove its import of ${relKey(...tokensR
6340
6528
  snippet: effectiveConfig.surfaces.map((s) => `${s.match} \u2192 ${relKey(...tokensRelDir, barrelFor(s.mode))}`).join("\n")
6341
6529
  };
6342
6530
  } else if (wireTarget) {
6343
- const targetAbsDir = dirname7(join31(opts.projectRoot, wireTarget));
6531
+ const targetAbsDir = dirname8(join33(opts.projectRoot, wireTarget));
6344
6532
  const barrelImport = relativeImportPath(
6345
6533
  targetAbsDir,
6346
- join31(opts.projectRoot, ...tokensRelDir, "theme.css")
6534
+ join33(opts.projectRoot, ...tokensRelDir, "theme.css")
6347
6535
  );
6348
6536
  const snippet = `@import "${barrelImport}";`;
6349
6537
  try {
6350
6538
  if (relocateTo && currentBrand) {
6351
- const abs = join31(opts.projectRoot, wireTarget);
6539
+ const abs = join33(opts.projectRoot, wireTarget);
6352
6540
  const oldDirName = currentBrand.split("/").slice(0, -1).join("/");
6353
- const before = existsSync19(abs) ? readFileSync27(abs, "utf8") : "";
6541
+ const before = existsSync19(abs) ? readFileSync28(abs, "utf8") : "";
6354
6542
  const after = before.split("\n").filter((l) => !(l.includes("@import") && l.includes(oldDirName))).join("\n");
6355
6543
  if (after !== before) writeFileSync4(abs, after, "utf8");
6356
6544
  }
6357
- const outcome = wireBarrel(join31(opts.projectRoot, wireTarget), barrelImport);
6545
+ const outcome = wireBarrel(join33(opts.projectRoot, wireTarget), barrelImport);
6358
6546
  wiring = { target: wireTarget, status: outcome.status, snippet };
6359
6547
  const verb = outcome.status === "wired" ? "Wired" : outcome.status === "rewired" ? "Rewired" : "Already present in";
6360
6548
  log(`
@@ -6371,7 +6559,7 @@ Could not write to ${wireTarget}: ${err.message}`);
6371
6559
  } else {
6372
6560
  const barrelImport = relativeImportPath(
6373
6561
  opts.projectRoot,
6374
- join31(opts.projectRoot, ...tokensRelDir, "theme.css")
6562
+ join33(opts.projectRoot, ...tokensRelDir, "theme.css")
6375
6563
  );
6376
6564
  const snippet = `@import "${barrelImport}";`;
6377
6565
  wiring = { target: null, status: "print-only", snippet };
@@ -6380,7 +6568,7 @@ Could not write to ${wireTarget}: ${err.message}`);
6380
6568
  if (pasteTargets.length > 0) {
6381
6569
  log("Paste the block for whichever of these is your global stylesheet:");
6382
6570
  for (const target of pasteTargets) {
6383
- const dir = dirname7(join31(opts.projectRoot, target));
6571
+ const dir = dirname8(join33(opts.projectRoot, target));
6384
6572
  const forTarget = `@import "${relativeImportPath(dir, wiringBrandAbsPath)}";
6385
6573
  @import "${relativeImportPath(dir, modeAbsPath)}";`;
6386
6574
  log(`
@@ -6430,29 +6618,29 @@ Could not write to ${wireTarget}: ${err.message}`);
6430
6618
  }
6431
6619
 
6432
6620
  // src/commands/verdicts.ts
6433
- import { execFileSync as execFileSync3 } from "child_process";
6434
- import { existsSync as existsSync25, readFileSync as readFileSync33 } from "fs";
6435
- 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";
6436
6624
 
6437
6625
  // src/rules/citations.ts
6438
- import { join as join32 } from "path";
6626
+ import { join as join34 } from "path";
6439
6627
  function citableIds(packageRoot2) {
6440
- const rulesDir = join32(packageRoot2, "rules");
6628
+ const rulesDir = join34(packageRoot2, "rules");
6441
6629
  const ids = [
6442
- ...loadRules(rulesDir, join32(packageRoot2, "rules.index.json")).map((r) => r.id),
6630
+ ...loadRules(rulesDir, join34(packageRoot2, "rules.index.json")).map((r) => r.id),
6443
6631
  ...loadSpecs(rulesDir).map((s) => s.id)
6444
6632
  ];
6445
6633
  return [...new Set(ids)].sort();
6446
6634
  }
6447
6635
 
6448
6636
  // src/probe/check.ts
6449
- import { existsSync as existsSync23, readdirSync as readdirSync15, readFileSync as readFileSync31 } from "fs";
6450
- 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";
6451
6639
 
6452
6640
  // src/probe/save.ts
6453
- import { existsSync as existsSync22, mkdirSync as mkdirSync7, readdirSync as readdirSync14, 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";
6454
6642
  import { fileURLToPath as fileURLToPath2 } from "url";
6455
- 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";
6456
6644
 
6457
6645
  // src/probe/script.ts
6458
6646
  var PROBE_VERSION = 9;
@@ -6739,7 +6927,7 @@ var PROBE_SCRIPT = `(async () => {
6739
6927
  import { spawn } from "child_process";
6740
6928
  import { existsSync as existsSync20, mkdtempSync, readdirSync as readdirSync13, rmSync as rmSync3 } from "fs";
6741
6929
  import { homedir as homedir2, tmpdir } from "os";
6742
- import { join as join33 } from "path";
6930
+ import { join as join35 } from "path";
6743
6931
  var CHROME_ENV = ["JIG_CHROME", "CHROME_PATH", "PUPPETEER_EXECUTABLE_PATH", "CHROMIUM_PATH"];
6744
6932
  var CHROME_COMMANDS = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge", "brave-browser", "chrome", "msedge"];
6745
6933
  var CHROME_APPS = [
@@ -6751,14 +6939,14 @@ var CHROME_APPS = [
6751
6939
  // Every Windows machine has Edge; Chrome is often installed per user.
6752
6940
  "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
6753
6941
  "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
6754
- ...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")] : []
6755
6943
  ];
6756
6944
  function cachedBrowsers() {
6757
6945
  const roots = [
6758
- join33(homedir2(), ".cache", "ms-playwright"),
6759
- join33(homedir2(), "Library", "Caches", "ms-playwright"),
6760
- join33(homedir2(), "AppData", "Local", "ms-playwright"),
6761
- 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")
6762
6950
  ];
6763
6951
  const found = [];
6764
6952
  for (const root of roots) {
@@ -6778,7 +6966,7 @@ function cachedBrowsers() {
6778
6966
  ["chrome-win", "chrome.exe"],
6779
6967
  ["chrome-win64", "chrome.exe"]
6780
6968
  ]) {
6781
- const candidate = join33(root, entry, ...rel);
6969
+ const candidate = join35(root, entry, ...rel);
6782
6970
  if (existsSync20(candidate)) found.push(candidate);
6783
6971
  }
6784
6972
  }
@@ -6804,7 +6992,7 @@ function which(command) {
6804
6992
  for (const dir of dirs) {
6805
6993
  if (!dir) continue;
6806
6994
  for (const ext of exts) {
6807
- const candidate = join33(dir, command + ext);
6995
+ const candidate = join35(dir, command + ext);
6808
6996
  if (existsSync20(candidate)) return candidate;
6809
6997
  }
6810
6998
  }
@@ -6817,7 +7005,7 @@ async function runProbe(opts) {
6817
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."
6818
7006
  );
6819
7007
  }
6820
- const profile = mkdtempSync(join33(tmpdir(), "jig-probe-"));
7008
+ const profile = mkdtempSync(join35(tmpdir(), "jig-probe-"));
6821
7009
  const child = spawn(chrome, [
6822
7010
  "--headless=new",
6823
7011
  "--remote-debugging-port=0",
@@ -6921,8 +7109,8 @@ async function evaluate(endpoint, url, width, height, timeoutMs, expression) {
6921
7109
 
6922
7110
  // src/probe/serve.ts
6923
7111
  import { createServer } from "http";
6924
- import { existsSync as existsSync21, readFileSync as readFileSync28, statSync as statSync3 } from "fs";
6925
- 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";
6926
7114
  var TYPES = {
6927
7115
  ".html": "text/html; charset=utf-8",
6928
7116
  ".htm": "text/html; charset=utf-8",
@@ -6949,19 +7137,19 @@ async function serveDirectory(dir) {
6949
7137
  const root = resolve3(dir);
6950
7138
  const server = createServer((req, res) => {
6951
7139
  let path = decodeURIComponent((req.url ?? "/").split("?")[0].split("#")[0]);
6952
- let file = resolve3(join34(root, path));
7140
+ let file = resolve3(join36(root, path));
6953
7141
  if (file !== root && !file.startsWith(root + sep4)) {
6954
7142
  res.writeHead(403).end();
6955
7143
  return;
6956
7144
  }
6957
- if (existsSync21(file) && statSync3(file).isDirectory()) file = join34(file, "index.html");
7145
+ if (existsSync21(file) && statSync3(file).isDirectory()) file = join36(file, "index.html");
6958
7146
  else if (!existsSync21(file) && existsSync21(`${file}.html`)) file = `${file}.html`;
6959
7147
  if (!existsSync21(file)) {
6960
7148
  res.writeHead(404).end();
6961
7149
  return;
6962
7150
  }
6963
7151
  res.writeHead(200, { "content-type": TYPES[extname(file).toLowerCase()] ?? "application/octet-stream" });
6964
- res.end(readFileSync28(file));
7152
+ res.end(readFileSync29(file));
6965
7153
  });
6966
7154
  await new Promise((done) => server.listen(0, "127.0.0.1", done));
6967
7155
  const address = server.address();
@@ -6973,8 +7161,8 @@ async function serveDirectory(dir) {
6973
7161
  }
6974
7162
 
6975
7163
  // src/probe/switches.ts
6976
- import { readFileSync as readFileSync29 } from "fs";
6977
- import { join as join35 } from "path";
7164
+ import { readFileSync as readFileSync30 } from "fs";
7165
+ import { join as join37 } from "path";
6978
7166
  var DECLARATION = /--breakpoint-([a-z0-9-]+)\s*:\s*(\d+(?:\.\d+)?)(px|rem)\b/g;
6979
7167
  function declaredSwitches(projectRoot) {
6980
7168
  const found = /* @__PURE__ */ new Map();
@@ -6982,7 +7170,7 @@ function declaredSwitches(projectRoot) {
6982
7170
  if (!/\.(css|scss|astro|vue|svelte|html)$/i.test(file)) continue;
6983
7171
  let text;
6984
7172
  try {
6985
- text = readFileSync29(join35(projectRoot, file), "utf8");
7173
+ text = readFileSync30(join37(projectRoot, file), "utf8");
6986
7174
  } catch {
6987
7175
  continue;
6988
7176
  }
@@ -7017,11 +7205,11 @@ function saveProbe(opts) {
7017
7205
  }
7018
7206
  probe.pageFile = relative3(opts.projectRoot, page).split("\\").join("/");
7019
7207
  if (opts.serveRoot) probe.serveRoot = relative3(opts.projectRoot, resolve4(opts.projectRoot, opts.serveRoot)).split("\\").join("/") || ".";
7020
- probe.pageChecksum = checksum(readFileSync30(page, "utf8"));
7208
+ probe.pageChecksum = checksum(readFileSync31(page, "utf8"));
7021
7209
  probe.recordedAt = (/* @__PURE__ */ new Date()).toISOString();
7022
- const dir = join36(opts.projectRoot, ".jig", "critique", opts.surface);
7210
+ const dir = join38(opts.projectRoot, ".jig", "critique", opts.surface);
7023
7211
  mkdirSync7(dir, { recursive: true });
7024
- const path = join36(dir, `probe-${width}.json`);
7212
+ const path = join38(dir, `probe-${width}.json`);
7025
7213
  writeFileSync5(path, JSON.stringify(probe), "utf8");
7026
7214
  return { path: `.jig/critique/${opts.surface}/probe-${width}.json`, width, page: probe.pageFile };
7027
7215
  }
@@ -7070,11 +7258,11 @@ async function withPageUrl(projectRoot, abs, serve, use) {
7070
7258
  }
7071
7259
  }
7072
7260
  function recordedPage(projectRoot, surface) {
7073
- const dir = join36(projectRoot, ".jig", "critique", surface);
7261
+ const dir = join38(projectRoot, ".jig", "critique", surface);
7074
7262
  if (!existsSync22(dir)) return void 0;
7075
7263
  for (const f of readdirSync14(dir).filter((name) => /^probe-\d+\.json$/.test(name)).sort()) {
7076
7264
  try {
7077
- const probe = JSON.parse(readFileSync30(join36(dir, f), "utf8"));
7265
+ const probe = JSON.parse(readFileSync31(join38(dir, f), "utf8"));
7078
7266
  if (probe.pageFile && existsSync22(resolve4(projectRoot, probe.pageFile))) return probe.pageFile;
7079
7267
  } catch {
7080
7268
  }
@@ -7082,11 +7270,11 @@ function recordedPage(projectRoot, surface) {
7082
7270
  return void 0;
7083
7271
  }
7084
7272
  async function ensureProbes(opts) {
7085
- const dir = join36(opts.projectRoot, ".jig", "critique", opts.surface);
7273
+ const dir = join38(opts.projectRoot, ".jig", "critique", opts.surface);
7086
7274
  const abs = resolve4(opts.projectRoot, opts.page);
7087
7275
  let current;
7088
7276
  try {
7089
- current = checksum(readFileSync30(abs, "utf8"));
7277
+ current = checksum(readFileSync31(abs, "utf8"));
7090
7278
  } catch {
7091
7279
  return { recorded: [], reason: `${opts.page} could not be read` };
7092
7280
  }
@@ -7094,7 +7282,7 @@ async function ensureProbes(opts) {
7094
7282
  const widths = [.../* @__PURE__ */ new Set([...probeWidths(PROBE_WIDTHS, declaredSwitches(opts.projectRoot)), ...recorded])].sort((a, b) => a - b);
7095
7283
  const missing = widths.filter((width) => {
7096
7284
  try {
7097
- const probe = JSON.parse(readFileSync30(join36(dir, `probe-${width}.json`), "utf8"));
7285
+ const probe = JSON.parse(readFileSync31(join38(dir, `probe-${width}.json`), "utf8"));
7098
7286
  return probe.jigProbe !== PROBE_VERSION || probe.pageChecksum !== current;
7099
7287
  } catch {
7100
7288
  return true;
@@ -7105,7 +7293,7 @@ async function ensureProbes(opts) {
7105
7293
  let serve;
7106
7294
  for (const width of widths) {
7107
7295
  try {
7108
- const probe = JSON.parse(readFileSync30(join36(dir, `probe-${width}.json`), "utf8"));
7296
+ const probe = JSON.parse(readFileSync31(join38(dir, `probe-${width}.json`), "utf8"));
7109
7297
  if (probe.serveRoot) {
7110
7298
  serve = probe.serveRoot;
7111
7299
  break;
@@ -7121,9 +7309,9 @@ async function ensureProbes(opts) {
7121
7309
  return { recorded: missing };
7122
7310
  }
7123
7311
  function critiquedSurfaces(projectRoot) {
7124
- const root = join36(projectRoot, ".jig", "critique");
7312
+ const root = join38(projectRoot, ".jig", "critique");
7125
7313
  if (!existsSync22(root)) return [];
7126
- return readdirSync14(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"))));
7127
7315
  }
7128
7316
 
7129
7317
  // src/probe/check.ts
@@ -7135,7 +7323,7 @@ function stampProblem(projectRoot, file, p) {
7135
7323
  if (!page) return `${file} names a page outside this project (${p.serveRoot ? p.pageFile : p.url ?? "no url"}).`;
7136
7324
  let current;
7137
7325
  try {
7138
- current = readFileSync31(page, "utf8");
7326
+ current = readFileSync32(page, "utf8");
7139
7327
  } catch {
7140
7328
  return `${file} was taken on ${p.pageFile}, which no longer exists.`;
7141
7329
  }
@@ -7149,7 +7337,7 @@ function readProbes(projectRoot, dir, errors) {
7149
7337
  const probes = [];
7150
7338
  for (const f of readdirSync15(dir).filter((n) => /^probe-\d+\.json$/.test(n)).sort()) {
7151
7339
  try {
7152
- const p = JSON.parse(readFileSync31(join37(dir, f), "utf8"));
7340
+ const p = JSON.parse(readFileSync32(join39(dir, f), "utf8"));
7153
7341
  if (typeof p.jigProbe === "number" && p.jigProbe < PROBE_VERSION && typeof p.width === "number") {
7154
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\`.`);
7155
7343
  continue;
@@ -7274,18 +7462,18 @@ function probeCheck(probes, verdictOf, indexable = true, mode) {
7274
7462
 
7275
7463
  // src/check/spec-shape.ts
7276
7464
  import { existsSync as existsSync24, readdirSync as readdirSync16, statSync as statSync4 } from "fs";
7277
- import { join as join38 } from "path";
7465
+ import { join as join40 } from "path";
7278
7466
  function newestSpec(projectRoot) {
7279
- const dir = join38(projectRoot, ".jig", "specs");
7467
+ const dir = join40(projectRoot, ".jig", "specs");
7280
7468
  if (!existsSync24(dir)) return void 0;
7281
7469
  const files = readdirSync16(dir).filter((f) => f.endsWith(".md"));
7282
7470
  if (files.length === 0) return void 0;
7283
- const newest = files.map((f) => ({ f, at: statSync4(join38(dir, f)).mtimeMs })).sort((a, b) => b.at - a.at)[0].f;
7284
- 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)) };
7285
7473
  }
7286
7474
  function specFor(projectRoot, surface) {
7287
7475
  if (surface) {
7288
- const path = join38(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7476
+ const path = join40(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7289
7477
  if (existsSync24(path)) return { path: `.jig/specs/${surface}.spec.md`, slug: surface, body: readText(path) };
7290
7478
  }
7291
7479
  return newestSpec(projectRoot);
@@ -7443,14 +7631,14 @@ function reasonProblems(file, judged, errors) {
7443
7631
  function readJson(path, errors) {
7444
7632
  if (!existsSync25(path)) return null;
7445
7633
  try {
7446
- return JSON.parse(readFileSync33(path, "utf8"));
7634
+ return JSON.parse(readFileSync34(path, "utf8"));
7447
7635
  } catch (e) {
7448
7636
  errors.push(`${path}: not valid JSON (${e.message})`);
7449
7637
  return {};
7450
7638
  }
7451
7639
  }
7452
7640
  function specIndexable(projectRoot, surface) {
7453
- const path = join39(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7641
+ const path = join41(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7454
7642
  let front = "";
7455
7643
  try {
7456
7644
  front = readText(path).split(/^---\s*$/m)[1] ?? "";
@@ -7463,7 +7651,7 @@ function specIndexable(projectRoot, surface) {
7463
7651
  }
7464
7652
  function projectMode(projectRoot) {
7465
7653
  try {
7466
- const config = JSON.parse(readText(join39(projectRoot, "jig.config.json")));
7654
+ const config = JSON.parse(readText(join41(projectRoot, "jig.config.json")));
7467
7655
  const modes = [...new Set((config.surfaces ?? []).map((s) => s?.mode).filter((m) => typeof m === "string"))];
7468
7656
  return modes.length === 1 ? modes[0].toLowerCase() : void 0;
7469
7657
  } catch {
@@ -7471,7 +7659,7 @@ function projectMode(projectRoot) {
7471
7659
  }
7472
7660
  }
7473
7661
  function specNeedsNav(projectRoot, surface) {
7474
- const path = join39(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7662
+ const path = join41(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7475
7663
  if (!existsSync25(path)) return false;
7476
7664
  const front = readText(path).split(/^---\s*$/m)[1] ?? "";
7477
7665
  const navField = [...front.matchAll(/^\s*nav:\s*(.+)$/gim)].some((m) => !/^\s*(none|n\/a|-)\b/i.test(m[1]));
@@ -7550,7 +7738,7 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
7550
7738
  function checkDecisions(projectRoot, dir, errors) {
7551
7739
  const required = decisionNames(projectRoot);
7552
7740
  if (required.length === 0) return { state: "ran", judged: 0, total: 0, findings: 0 };
7553
- const file = readJson(join39(dir, "decisions.json"), errors);
7741
+ const file = readJson(join41(dir, "decisions.json"), errors);
7554
7742
  if (!file) {
7555
7743
  errors.push(
7556
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.`
@@ -7603,9 +7791,9 @@ function decisionsSince(projectRoot, dir, unjudged) {
7603
7791
  if (unjudged.length === 0) return [];
7604
7792
  const path = decisionsFile(projectRoot);
7605
7793
  if (!path) return [];
7606
- 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();
7607
7795
  try {
7608
- const verdictFile = relative4(projectRoot, join39(dir, "decisions.json")).split(sep6).join("/");
7796
+ const verdictFile = relative4(projectRoot, join41(dir, "decisions.json")).split(sep6).join("/");
7609
7797
  if (git(["status", "--porcelain", "--", verdictFile])) return [];
7610
7798
  const judgedIn = git(["log", "-1", "--format=%H", "--", verdictFile]);
7611
7799
  if (!judgedIn) return [];
@@ -7629,7 +7817,7 @@ function decisionsSince(projectRoot, dir, unjudged) {
7629
7817
  function verifyVerdicts(opts) {
7630
7818
  const root = opts.packageRoot ?? assetRoot();
7631
7819
  const errors = [];
7632
- const index = JSON.parse(readFileSync33(join39(root, "rules.index.json"), "utf8"));
7820
+ const index = JSON.parse(readFileSync34(join41(root, "rules.index.json"), "utf8"));
7633
7821
  const judgment = index.filter((r) => r.bucket === "judgment" || r.bucket === "hybrid" && r.pass !== void 0);
7634
7822
  const screenIds = judgment.filter((r) => r.pass === "screen").map((r) => r.id);
7635
7823
  const codeIds = judgment.filter((r) => r.pass === "code").map((r) => r.id);
@@ -7638,9 +7826,9 @@ function verifyVerdicts(opts) {
7638
7826
  ...judgment.map((r) => [r.id, r.pass ?? ""])
7639
7827
  ]);
7640
7828
  const specIds = new Set(citableIds(root).filter((id) => /^[PMLRT]-\d+$/.test(id)));
7641
- const dir = join39(opts.projectRoot, ".jig", "critique", opts.surface);
7642
- const screenFile = readJson(join39(dir, "screen.json"), errors);
7643
- 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);
7644
7832
  const screenExtraRequired = specNeedsNav(opts.projectRoot, opts.surface) ? ["P-14"] : [];
7645
7833
  const decisionList = decisionNames(opts.projectRoot);
7646
7834
  const screen = checkArm("screen", screenFile, screenIds, passOf, specIds, screenExtraRequired, errors, decisionList);
@@ -7670,7 +7858,7 @@ function verifyVerdicts(opts) {
7670
7858
  const measured = [];
7671
7859
  let specFront = "";
7672
7860
  try {
7673
- 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] ?? "";
7674
7862
  } catch {
7675
7863
  }
7676
7864
  if (specIndexableField(specFront) === "unreadable") {
@@ -7696,7 +7884,7 @@ function verifyVerdicts(opts) {
7696
7884
  return { ok: errors.length === 0, errors, screen, code, decisions, rendered, measured, line, ...previous ? { previous } : {} };
7697
7885
  }
7698
7886
  function previousFindings(projectRoot, dir) {
7699
- 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"] });
7700
7888
  const rel = relative4(projectRoot, dir).split("\\").join("/");
7701
7889
  const files = ["screen.json", "code.json", "decisions.json"];
7702
7890
  const paths = files.map((f) => `${rel}/${f}`);
@@ -7715,7 +7903,7 @@ function previousFindings(projectRoot, dir) {
7715
7903
  const out = /* @__PURE__ */ new Map();
7716
7904
  let text;
7717
7905
  try {
7718
- 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));
7719
7907
  } catch {
7720
7908
  return out;
7721
7909
  }
@@ -7797,17 +7985,17 @@ function sourceChanges(git, from, to) {
7797
7985
  }
7798
7986
 
7799
7987
  // src/commands/gate.ts
7800
- import { existsSync as existsSync28, mkdirSync as mkdirSync8, readdirSync as readdirSync18, 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";
7801
7989
  import { createHash as createHash3 } from "crypto";
7802
- import { join as join44 } from "path";
7990
+ import { join as join46 } from "path";
7803
7991
 
7804
7992
  // src/check/mockup-drawing.ts
7805
- import { join as join40 } from "path";
7993
+ import { join as join42 } from "path";
7806
7994
  var FRAMES = [["phone", 360], ["tablet", 768], ["desktop", 1280], ["wide", 1600]];
7807
7995
  function mockupDrawingProblems(root, specBody, at) {
7808
7996
  let html;
7809
7997
  try {
7810
- html = readText(join40(root, at));
7998
+ html = readText(join42(root, at));
7811
7999
  } catch {
7812
8000
  return [];
7813
8001
  }
@@ -7868,7 +8056,7 @@ function frameRegionProblems(frames, specBody, at) {
7868
8056
  function approvedDrawingProblems(root, specBody, at) {
7869
8057
  let html;
7870
8058
  try {
7871
- html = readText(join40(root, at));
8059
+ html = readText(join42(root, at));
7872
8060
  } catch {
7873
8061
  return [];
7874
8062
  }
@@ -7971,15 +8159,15 @@ function sizeBlock2(sizes, size) {
7971
8159
  }
7972
8160
 
7973
8161
  // src/commands/gate.ts
7974
- import { execFileSync as execFileSync4 } from "child_process";
8162
+ import { execFileSync as execFileSync5 } from "child_process";
7975
8163
 
7976
8164
  // src/commands/ship.ts
7977
- import { existsSync as existsSync26, readdirSync as readdirSync17, readFileSync as readFileSync35 } from "fs";
7978
- 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";
7979
8167
 
7980
8168
  // src/commands/seo.ts
7981
- import { readFileSync as readFileSync34 } from "fs";
7982
- import { join as join41 } from "path";
8169
+ import { readFileSync as readFileSync35 } from "fs";
8170
+ import { join as join43 } from "path";
7983
8171
  var SITEMAP_FILE = /(^|\/)sitemap(-index)?\.(xml|ts|js|mjs|tsx|jsx|rb|php|py)$/i;
7984
8172
  var ROBOTS_FILE = /(^|\/)robots\.(txt|ts|js|mjs|tsx|jsx)$/i;
7985
8173
  var ROUTE_IN_SITEMAP = /(?:<loc>\s*([^<\s]+)\s*<\/loc>)|(?:url\s*:\s*["'`]([^"'`]+)["'`])/gi;
@@ -8020,7 +8208,7 @@ function seo(opts) {
8020
8208
  for (const file of files) {
8021
8209
  let raw;
8022
8210
  try {
8023
- raw = readFileSync34(join41(root, file), "utf8");
8211
+ raw = readFileSync35(join43(root, file), "utf8");
8024
8212
  } catch {
8025
8213
  continue;
8026
8214
  }
@@ -8085,7 +8273,7 @@ function critiqueAtShip(projectRoot, specBody) {
8085
8273
  const own = specCritique(specBody);
8086
8274
  if (own) return own === "at-ship";
8087
8275
  try {
8088
- 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";
8089
8277
  } catch {
8090
8278
  return false;
8091
8279
  }
@@ -8093,14 +8281,14 @@ function critiqueAtShip(projectRoot, specBody) {
8093
8281
  function tweakDeferred(dir) {
8094
8282
  let t;
8095
8283
  try {
8096
- t = JSON.parse(readFileSync35(join42(dir, "tweak.json"), "utf8"));
8284
+ t = JSON.parse(readFileSync36(join44(dir, "tweak.json"), "utf8"));
8097
8285
  } catch {
8098
8286
  return false;
8099
8287
  }
8100
8288
  if (t.deferred === void 0 || t.deferred === false) return false;
8101
8289
  let judged;
8102
8290
  try {
8103
- judged = JSON.parse(readFileSync35(join42(dir, "verdicts.lock"), "utf8")).tweak;
8291
+ judged = JSON.parse(readFileSync36(join44(dir, "verdicts.lock"), "utf8")).tweak;
8104
8292
  } catch {
8105
8293
  }
8106
8294
  return judged !== t.at;
@@ -8111,14 +8299,14 @@ function pageChecksum(html) {
8111
8299
  function changedSinceLock(projectRoot, dir) {
8112
8300
  let lock;
8113
8301
  try {
8114
- lock = JSON.parse(readFileSync35(join42(dir, "verdicts.lock"), "utf8"));
8302
+ lock = JSON.parse(readFileSync36(join44(dir, "verdicts.lock"), "utf8"));
8115
8303
  } catch {
8116
8304
  return void 0;
8117
8305
  }
8118
8306
  const file = lock.page?.file;
8119
8307
  if (!file || !lock.page?.checksum) return void 0;
8120
8308
  try {
8121
- const html = readFileSync35(join42(projectRoot, file), "utf8");
8309
+ const html = readFileSync36(join44(projectRoot, file), "utf8");
8122
8310
  const seen = [pageChecksum(html), checksum(html)];
8123
8311
  return seen.includes(lock.page.checksum) ? void 0 : file;
8124
8312
  } catch {
@@ -8130,7 +8318,7 @@ function supersededBy(front) {
8130
8318
  return named ? named : void 0;
8131
8319
  }
8132
8320
  function pageStatus(projectRoot, surface) {
8133
- const spec = readText(join42(projectRoot, ".jig", "specs", `${surface}.spec.md`));
8321
+ const spec = readText(join44(projectRoot, ".jig", "specs", `${surface}.spec.md`));
8134
8322
  const front = spec.split(/^---\s*$/m)[1] ?? "";
8135
8323
  if (!/^\s*confirmed\s*:\s*true\b/im.test(front)) {
8136
8324
  return { surface, state: "in-progress", detail: "its spec is not confirmed, so nothing of it is built to ship" };
@@ -8138,10 +8326,10 @@ function pageStatus(projectRoot, surface) {
8138
8326
  const successor = supersededBy(front);
8139
8327
  if (successor !== void 0) {
8140
8328
  const file = successor.replace(/(\.spec\.md)?$/, ".spec.md");
8141
- 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` };
8142
8330
  }
8143
- const dir = join42(projectRoot, ".jig", "critique", surface);
8144
- 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"))) {
8145
8333
  return { surface, state: "never", detail: "never critiqued" };
8146
8334
  }
8147
8335
  if (tweakDeferred(dir)) return { surface, state: "deferred", detail: "a tweak deferred its re-judge" };
@@ -8151,7 +8339,7 @@ function pageStatus(projectRoot, surface) {
8151
8339
  const stale = v.errors.find((e) => /was taken (on|by) an older/.test(e));
8152
8340
  let judgedPage = false;
8153
8341
  try {
8154
- judgedPage = !!JSON.parse(readFileSync35(join42(dir, "verdicts.lock"), "utf8")).page?.checksum;
8342
+ judgedPage = !!JSON.parse(readFileSync36(join44(dir, "verdicts.lock"), "utf8")).page?.checksum;
8155
8343
  } catch {
8156
8344
  }
8157
8345
  if (stale && !judgedPage) return { surface, state: "changed", detail: "the page changed after its probes were taken" };
@@ -8171,7 +8359,7 @@ function ship(opts) {
8171
8359
  const mechanicalErrors = mechanical.findings.filter((f) => f.bucket === "mechanical" && f.severity === "error").length;
8172
8360
  const seoResult = seo({ projectRoot: root });
8173
8361
  const seoErrors = seoResult.findings.filter((f) => f.severity === "error").length;
8174
- const specsDir = join42(root, ".jig", "specs");
8362
+ const specsDir = join44(root, ".jig", "specs");
8175
8363
  const surfaces = existsSync26(specsDir) ? readdirSync17(specsDir).filter((f) => f.endsWith(".spec.md") && !f.startsWith("_")).map((f) => f.replace(/\.spec\.md$/, "")).sort() : [];
8176
8364
  const pages = surfaces.map((s) => pageStatus(root, s));
8177
8365
  const owed = pages.filter((p) => p.state !== "judged" && p.state !== "in-progress" && p.state !== "superseded");
@@ -8193,9 +8381,9 @@ function ship(opts) {
8193
8381
  }
8194
8382
 
8195
8383
  // src/check/spec-checked.ts
8196
- import { existsSync as existsSync27, readFileSync as readFileSync36 } from "fs";
8384
+ import { existsSync as existsSync27, readFileSync as readFileSync37 } from "fs";
8197
8385
  import { createHash as createHash2 } from "crypto";
8198
- import { join as join43 } from "path";
8386
+ import { join as join45 } from "path";
8199
8387
 
8200
8388
  // src/check/owner-word.ts
8201
8389
  var SAYS = {
@@ -8280,7 +8468,7 @@ function specCheckProblems(root, spec, then, owner, decisions) {
8280
8468
  const recordPath = `.jig/specs/${spec.slug}.checked.json`;
8281
8469
  let record;
8282
8470
  try {
8283
- record = JSON.parse(readFileSync36(join43(root, recordPath), "utf8"));
8471
+ record = JSON.parse(readFileSync37(join45(root, recordPath), "utf8"));
8284
8472
  } catch {
8285
8473
  problems.push(
8286
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.`
@@ -8309,10 +8497,10 @@ function specCheckProblems(root, spec, then, owner, decisions) {
8309
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.`);
8310
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) => {
8311
8499
  const [, file, line] = /^(.+?)(?::(\d+)(?:-\d+)?)?$/.exec(s) ?? [];
8312
- if (!file || !existsSync27(join43(root, file))) return true;
8500
+ if (!file || !existsSync27(join45(root, file))) return true;
8313
8501
  if (!line) return false;
8314
8502
  try {
8315
- return Number(line) > readFileSync36(join43(root, file), "utf8").split("\n").length;
8503
+ return Number(line) > readFileSync37(join45(root, file), "utf8").split("\n").length;
8316
8504
  } catch {
8317
8505
  return true;
8318
8506
  }
@@ -8329,7 +8517,7 @@ function lastJigInvocation(transcriptPath) {
8329
8517
  if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
8330
8518
  let text;
8331
8519
  try {
8332
- text = readFileSync37(transcriptPath, "utf8");
8520
+ text = readFileSync38(transcriptPath, "utf8");
8333
8521
  } catch {
8334
8522
  return void 0;
8335
8523
  }
@@ -8357,7 +8545,7 @@ function ownerWords(transcriptPath) {
8357
8545
  if (!transcriptPath || !existsSync28(transcriptPath)) return "";
8358
8546
  const said = [];
8359
8547
  try {
8360
- for (const line of readFileSync37(transcriptPath, "utf8").split("\n")) {
8548
+ for (const line of readFileSync38(transcriptPath, "utf8").split("\n")) {
8361
8549
  if (!line.includes('"user"')) continue;
8362
8550
  let entry;
8363
8551
  try {
@@ -8379,7 +8567,7 @@ function ownerWords(transcriptPath) {
8379
8567
  }
8380
8568
  function namedSpec(root, surface) {
8381
8569
  if (!surface) return specFor(root, void 0);
8382
- 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;
8383
8571
  }
8384
8572
  function isConfirmed(body) {
8385
8573
  return /^\s*confirmed\s*:\s*true\b/im.test(body.split(/^---\s*$/m)[1] ?? "");
@@ -8388,7 +8576,7 @@ function ownerReplyAfter(transcriptPath, asked) {
8388
8576
  if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
8389
8577
  const turns = [];
8390
8578
  try {
8391
- for (const line of readFileSync37(transcriptPath, "utf8").split("\n")) {
8579
+ for (const line of readFileSync38(transcriptPath, "utf8").split("\n")) {
8392
8580
  let entry;
8393
8581
  try {
8394
8582
  entry = JSON.parse(line);
@@ -8416,7 +8604,7 @@ function lastAssistantText(transcriptPath) {
8416
8604
  if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
8417
8605
  let found;
8418
8606
  try {
8419
- for (const line of readFileSync37(transcriptPath, "utf8").split("\n")) {
8607
+ for (const line of readFileSync38(transcriptPath, "utf8").split("\n")) {
8420
8608
  if (!line.includes('"assistant"')) continue;
8421
8609
  let entry;
8422
8610
  try {
@@ -8442,7 +8630,7 @@ var ASKS_THE_OWNER = /* @__PURE__ */ new Set(["decide", "spec", "mockup", "tweak
8442
8630
  function sessionStart(transcriptPath) {
8443
8631
  if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
8444
8632
  try {
8445
- for (const line of readFileSync37(transcriptPath, "utf8").split("\n")) {
8633
+ for (const line of readFileSync38(transcriptPath, "utf8").split("\n")) {
8446
8634
  const m = /"timestamp"\s*:\s*"([^"]+)"/.exec(line);
8447
8635
  if (m) {
8448
8636
  const t = Date.parse(m[1]);
@@ -8457,29 +8645,29 @@ function verdictsMtime(dir) {
8457
8645
  let newest = 0;
8458
8646
  for (const name of VERDICT_FILES) {
8459
8647
  try {
8460
- newest = Math.max(newest, statSync5(join44(dir, name)).mtimeMs);
8648
+ newest = Math.max(newest, statSync5(join46(dir, name)).mtimeMs);
8461
8649
  } catch {
8462
8650
  }
8463
8651
  }
8464
8652
  return newest;
8465
8653
  }
8466
8654
  function surfacesInPlay(root, command, transcriptPath, surface) {
8467
- const critiqueDir = join44(root, ".jig", "critique");
8655
+ const critiqueDir = join46(root, ".jig", "critique");
8468
8656
  if (!existsSync28(critiqueDir)) return [];
8469
8657
  const all = readdirSync18(critiqueDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("_") && !d.name.startsWith(".")).map((d) => d.name);
8470
8658
  const start = sessionStart(transcriptPath);
8471
8659
  if (start === void 0) return all;
8472
8660
  const current = command === "critique" || command === "tweak" ? specFor(root, surface)?.slug : void 0;
8473
- 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);
8474
8662
  }
8475
8663
  function surfacesToProbe(root, input) {
8476
8664
  const invocation = lastJigInvocation(input.transcript_path);
8477
8665
  return surfacesInPlay(root, invocation?.command, input.transcript_path, invocation?.surface);
8478
8666
  }
8479
8667
  function fileAtSessionStart(root, path, start) {
8480
- 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"] });
8481
8669
  try {
8482
- 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));
8483
8671
  } catch {
8484
8672
  }
8485
8673
  try {
@@ -8490,14 +8678,14 @@ function fileAtSessionStart(root, path, start) {
8490
8678
  }
8491
8679
  }
8492
8680
  function changedOutsideRecords(root, start) {
8493
- 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"] });
8494
8682
  try {
8495
8683
  const base = start === void 0 ? "HEAD" : git(["rev-list", "-1", `--before=@${Math.floor(start / 1e3)}`, "HEAD"]).trim();
8496
8684
  if (!base) return [];
8497
8685
  const changed = git(["diff", "--name-only", base, "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean);
8498
8686
  const created = git(["ls-files", "--others", "--exclude-standard", "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean).filter((f) => {
8499
8687
  try {
8500
- return start === void 0 || statSync5(join44(root, f)).mtimeMs >= start - 1e3;
8688
+ return start === void 0 || statSync5(join46(root, f)).mtimeMs >= start - 1e3;
8501
8689
  } catch {
8502
8690
  return false;
8503
8691
  }
@@ -8508,12 +8696,12 @@ function changedOutsideRecords(root, start) {
8508
8696
  }
8509
8697
  }
8510
8698
  function drawingChangedAfterApproval(root, specPath, at) {
8511
- 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"] });
8512
8700
  try {
8513
8701
  const approvedIn = git(["log", "-1", "--format=%H", "-G", "^mockup[[:space:]]*:[[:space:]]*approved", "--", specPath]).trim();
8514
8702
  if (!approvedIn) return void 0;
8515
8703
  const then = lf(git(["show", `${approvedIn}:./${at}`]));
8516
- 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;
8517
8705
  } catch {
8518
8706
  return void 0;
8519
8707
  }
@@ -8525,7 +8713,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8525
8713
  const found = decisionsFile(root);
8526
8714
  if (!found) problems.push("decide wrote no DECISIONS.md beside the token layer.");
8527
8715
  else {
8528
- const body = readText(join44(root, found));
8716
+ const body = readText(join46(root, found));
8529
8717
  if (!/^##\s+Unresolved\s*$/im.test(body)) {
8530
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.`");
8531
8719
  }
@@ -8535,7 +8723,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8535
8723
  if (owner) problems.push(...quotesNotFrom(body, then, owner, "the owner's words in this session").map((p) => `DECISIONS.md: ${p}`));
8536
8724
  }
8537
8725
  }
8538
- 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`))) {
8539
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.`);
8540
8728
  return problems;
8541
8729
  }
@@ -8558,10 +8746,10 @@ function commandProblems(root, command, surface, start, owner = "") {
8558
8746
  const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
8559
8747
  if (/^approved/i.test(mockup)) {
8560
8748
  if (!at) problems.push(`${spec.path}: \`mockup_at:\` is empty. Record where the approved drawing is.`);
8561
- 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.`);
8562
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.`);
8563
8751
  }
8564
- 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));
8565
8753
  }
8566
8754
  if (command === "tweak" && spec) problems.push(...tweakProblems(root, spec, owner, start));
8567
8755
  if ((command === "spec" || command === "make") && spec) {
@@ -8589,7 +8777,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8589
8777
  const front = spec.body.split(/^---\s*$/m)[1] ?? "";
8590
8778
  const mockup = mockupField(spec.body);
8591
8779
  const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
8592
- 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))) {
8593
8781
  const changedAfter = drawingChangedAfterApproval(root, spec.path, at);
8594
8782
  if (changedAfter) {
8595
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.`);
@@ -8601,7 +8789,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8601
8789
  if (found) {
8602
8790
  let now = "";
8603
8791
  try {
8604
- now = readText(join44(root, found));
8792
+ now = readText(join46(root, found));
8605
8793
  } catch {
8606
8794
  }
8607
8795
  const then = fileAtSessionStart(root, found, start);
@@ -8615,13 +8803,13 @@ function commandProblems(root, command, surface, start, owner = "") {
8615
8803
  if (found) {
8616
8804
  let now = "";
8617
8805
  try {
8618
- now = readText(join44(root, found));
8806
+ now = readText(join46(root, found));
8619
8807
  } catch {
8620
8808
  }
8621
8809
  const then = fileAtSessionStart(root, found, start);
8622
8810
  if (now && now !== then) {
8623
8811
  problems.push(...unsourcedReasons(now, then, { newWhysOnly: true }).map((p) => `${found}: ${p}`));
8624
- const change = readTweak(join44(root, ".jig", "critique", spec.slug))?.change;
8812
+ const change = readTweak(join46(root, ".jig", "critique", spec.slug))?.change;
8625
8813
  if (change) problems.push(...quotesNotFrom(now, then, change).map((p) => `${found}: ${p}`));
8626
8814
  }
8627
8815
  }
@@ -8630,7 +8818,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8630
8818
  for (const file of modeFiles(root)) {
8631
8819
  let now = "";
8632
8820
  try {
8633
- now = readText(join44(root, file));
8821
+ now = readText(join46(root, file));
8634
8822
  } catch {
8635
8823
  continue;
8636
8824
  }
@@ -8641,8 +8829,8 @@ function commandProblems(root, command, surface, start, owner = "") {
8641
8829
  }
8642
8830
  }
8643
8831
  if (command === "critique") {
8644
- const dir = join44(root, ".jig", "critique");
8645
- const surfaces = existsSync28(dir) ? readdirSync18(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"))) : [];
8646
8834
  for (const surface2 of surfaces) {
8647
8835
  const v = verifyVerdicts({ projectRoot: root, surface: surface2 });
8648
8836
  if (v.ok && v.decisions.state !== "ran" && v.decisions.total > 0) {
@@ -8653,7 +8841,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8653
8841
  }
8654
8842
  }
8655
8843
  const own = specFor(root, surface)?.slug;
8656
- 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));
8657
8845
  if (surfaces.length === 0) {
8658
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.");
8659
8847
  }
@@ -8666,7 +8854,7 @@ function drawingBeforeAsking(root, surface) {
8666
8854
  const front = spec.body.split(/^---\s*$/m)[1] ?? "";
8667
8855
  const named = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
8668
8856
  const at = named || `.jig/mockups/${spec.slug}.html`;
8669
- 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 [];
8670
8858
  const found = mockupDrawingProblems(root, spec.body, at);
8671
8859
  return found.length ? [`Fix the drawing before you put it to the owner; they approve what the spec lists, drawn. ${found.join(" ")}`] : [];
8672
8860
  }
@@ -8674,14 +8862,14 @@ function specChecked(root, spec, start, owner) {
8674
8862
  const found = decisionsFile(root);
8675
8863
  let decisions = "";
8676
8864
  try {
8677
- if (found) decisions = readText(join44(root, found));
8865
+ if (found) decisions = readText(join46(root, found));
8678
8866
  } catch {
8679
8867
  }
8680
8868
  return specCheckProblems(root, spec, fileAtSessionStart(root, spec.path, start), owner, decisions);
8681
8869
  }
8682
8870
  function modeFiles(root) {
8683
8871
  try {
8684
- const state = JSON.parse(readFileSync37(join44(root, ".jig", "state.json"), "utf8"));
8872
+ const state = JSON.parse(readFileSync38(join46(root, ".jig", "state.json"), "utf8"));
8685
8873
  return Object.keys(state.files ?? {}).filter((f) => /(^|\/)mode\.[\w-]+\.css$/.test(f));
8686
8874
  } catch {
8687
8875
  return [];
@@ -8690,7 +8878,7 @@ function modeFiles(root) {
8690
8878
  function reportOmissions(dir, surface) {
8691
8879
  let report2;
8692
8880
  try {
8693
- report2 = readText(join44(dir, "REPORT.md")).toLowerCase();
8881
+ report2 = readText(join46(dir, "REPORT.md")).toLowerCase();
8694
8882
  } catch {
8695
8883
  return [];
8696
8884
  }
@@ -8698,7 +8886,7 @@ function reportOmissions(dir, surface) {
8698
8886
  for (const f of VERDICT_FILES) {
8699
8887
  let file;
8700
8888
  try {
8701
- file = JSON.parse(readFileSync37(join44(dir, f), "utf8"));
8889
+ file = JSON.parse(readFileSync38(join46(dir, f), "utf8"));
8702
8890
  } catch {
8703
8891
  continue;
8704
8892
  }
@@ -8719,16 +8907,16 @@ function tweakProblems(root, spec, owner = "", start) {
8719
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\`.`);
8720
8908
  else problems.push(...structureSinceApproval(root, spec, front));
8721
8909
  const surface = spec.slug;
8722
- const record = readTweak(join44(root, ".jig", "critique", surface));
8910
+ const record = readTweak(join46(root, ".jig", "critique", surface));
8723
8911
  if (!record) {
8724
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\`).`);
8725
8913
  } else {
8726
8914
  if (!record.change) problems.push(`.jig/critique/${surface}/tweak.json has no \`change\`: the owner's words for what changed.`);
8727
- 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)) {
8728
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.`);
8729
8917
  }
8730
8918
  try {
8731
- 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"));
8732
8920
  const extra = Object.keys(raw).filter((k) => !["at", "change", "ids", "deferred"].includes(k));
8733
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.`);
8734
8922
  } catch {
@@ -8743,7 +8931,7 @@ function tweakProblems(root, spec, owner = "", start) {
8743
8931
  }
8744
8932
  return problems;
8745
8933
  }
8746
- const unjudged = unjudgedTweakIds(join44(root, ".jig", "critique", surface), record);
8934
+ const unjudged = unjudgedTweakIds(join46(root, ".jig", "critique", surface), record);
8747
8935
  if (unjudged.length) {
8748
8936
  problems.push(
8749
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.`
@@ -8753,7 +8941,7 @@ function tweakProblems(root, spec, owner = "", start) {
8753
8941
  return problems;
8754
8942
  }
8755
8943
  function structureSinceApproval(root, spec, front) {
8756
- 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"] });
8757
8945
  let baseline;
8758
8946
  let judged = true;
8759
8947
  try {
@@ -8779,9 +8967,9 @@ function structureSinceApproval(root, spec, front) {
8779
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\`.`);
8780
8968
  }
8781
8969
  const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
8782
- if (at && !/^https?:/i.test(at) && existsSync28(join44(root, at))) {
8970
+ if (at && !/^https?:/i.test(at) && existsSync28(join46(root, at))) {
8783
8971
  try {
8784
- if (lf(git(["show", `${baseline}:./${at}`])) !== readText(join44(root, at))) {
8972
+ if (lf(git(["show", `${baseline}:./${at}`])) !== readText(join46(root, at))) {
8785
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\`.`);
8786
8974
  }
8787
8975
  } catch {
@@ -8791,7 +8979,7 @@ function structureSinceApproval(root, spec, front) {
8791
8979
  }
8792
8980
  function readTweak(dir) {
8793
8981
  try {
8794
- const raw = JSON.parse(readFileSync37(join44(dir, "tweak.json"), "utf8"));
8982
+ const raw = JSON.parse(readFileSync38(join46(dir, "tweak.json"), "utf8"));
8795
8983
  const deferred = typeof raw.deferred === "string" || raw.deferred === true ? raw.deferred : void 0;
8796
8984
  return { at: String(raw.at ?? ""), change: String(raw.change ?? "").trim(), ids: Array.isArray(raw.ids) ? raw.ids.map(String) : [], ...deferred !== void 0 ? { deferred } : {} };
8797
8985
  } catch {
@@ -8804,13 +8992,13 @@ function surfacePage(projectRoot, surface) {
8804
8992
  const declared2 = /^\s*surface\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
8805
8993
  const candidates = [declared2, `${surface}.html`, declared2 ? `${declared2.replace(/^\//, "")}.html` : void 0].filter((c) => !!c && /\.\w+$/.test(c) === (c === declared2 ? /\.\w+$/.test(c) : true));
8806
8994
  for (const candidate of candidates) {
8807
- if (candidate && existsSync28(join44(projectRoot, candidate))) return candidate;
8995
+ if (candidate && existsSync28(join46(projectRoot, candidate))) return candidate;
8808
8996
  }
8809
8997
  return void 0;
8810
8998
  }
8811
8999
  function gate(opts) {
8812
9000
  const root = opts.projectRoot;
8813
- if (!existsSync28(join44(root, "jig.config.json")) && !existsSync28(join44(root, ".jig"))) {
9001
+ if (!existsSync28(join46(root, "jig.config.json")) && !existsSync28(join46(root, ".jig"))) {
8814
9002
  return { block: false, reason: "" };
8815
9003
  }
8816
9004
  const invocation = lastJigInvocation(opts.input.transcript_path);
@@ -8846,7 +9034,7 @@ ${result.report}`);
8846
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.`);
8847
9035
  }
8848
9036
  if (spec) problems.push(...specChecked(root, spec, start, owner).map((p) => `/jig spec: ${p}`));
8849
- if (spec && existsSync28(join44(root, ".jig", "specs", `${spec.slug}.checked.json`))) {
9037
+ if (spec && existsSync28(join46(root, ".jig", "specs", `${spec.slug}.checked.json`))) {
8850
9038
  problems.push(...[...specProblems(spec), ...newFieldProblems(spec, fileAtSessionStart(root, spec.path, start))].map((p) => `/jig spec: ${p}`));
8851
9039
  }
8852
9040
  }
@@ -8873,15 +9061,15 @@ ${result.report}`);
8873
9061
  problems.push(...verdictGuard(root, command, surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)));
8874
9062
  problems.push(...probesLeftBehind(root, surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)));
8875
9063
  const touched = ["AGENTS.md", "CLAUDE.md"].some((f) => {
8876
- const now = existsSync28(join44(root, f)) ? readText(join44(root, f)) : "";
9064
+ const now = existsSync28(join46(root, f)) ? readText(join46(root, f)) : "";
8877
9065
  return fileAtSessionStart(root, f, start) !== now;
8878
9066
  });
8879
9067
  if (touched) problems.push(...agentFileProblems(root).map((p) => `Jig's agent instructions: ${p}`));
8880
- const critiqueDir = join44(root, ".jig", "critique");
9068
+ const critiqueDir = join46(root, ".jig", "critique");
8881
9069
  if (existsSync28(critiqueDir)) {
8882
9070
  for (const surface of surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)) {
8883
- const dir = join44(critiqueDir, surface);
8884
- 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;
8885
9073
  const v = verifyVerdicts({ projectRoot: root, surface });
8886
9074
  if (!v.ok) {
8887
9075
  const shown = v.errors.slice(0, 6).map((e) => ` ${e}`);
@@ -8911,10 +9099,10 @@ ${problems.join("\n\n")}`
8911
9099
  }
8912
9100
  const session = opts.input.session_id ?? "unknown";
8913
9101
  const key = `${session}:${createHash3("sha256").update(problems.join("\n")).digest("hex").slice(0, 12)}`;
8914
- const stateFile = join44(root, ".jig", "gate.json");
9102
+ const stateFile = join46(root, ".jig", "gate.json");
8915
9103
  let state = {};
8916
9104
  try {
8917
- state = JSON.parse(readFileSync37(stateFile, "utf8"));
9105
+ state = JSON.parse(readFileSync38(stateFile, "utf8"));
8918
9106
  } catch {
8919
9107
  }
8920
9108
  if (problems.length === 0) {
@@ -8949,7 +9137,7 @@ function verdictDigests(dir) {
8949
9137
  for (const f of VERDICT_FILES) {
8950
9138
  let file;
8951
9139
  try {
8952
- file = JSON.parse(readFileSync37(join44(dir, f), "utf8"));
9140
+ file = JSON.parse(readFileSync38(join46(dir, f), "utf8"));
8953
9141
  } catch {
8954
9142
  continue;
8955
9143
  }
@@ -8964,7 +9152,7 @@ function writeLock(root, surface, lockPath, dir, now) {
8964
9152
  const pageFile2 = recordedPage(root, surface);
8965
9153
  let page;
8966
9154
  try {
8967
- 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")) };
8968
9156
  } catch {
8969
9157
  }
8970
9158
  const tweakAt = readTweak(dir)?.at;
@@ -8977,7 +9165,7 @@ function verdictChecksum(dir) {
8977
9165
  const parts = VERDICT_FILES.map((f) => {
8978
9166
  try {
8979
9167
  return `${f}
8980
- ${readFileSync37(join44(dir, f), "utf8")}`;
9168
+ ${readFileSync38(join46(dir, f), "utf8")}`;
8981
9169
  } catch {
8982
9170
  return "";
8983
9171
  }
@@ -8985,15 +9173,15 @@ ${readFileSync37(join44(dir, f), "utf8")}`;
8985
9173
  return parts.some(Boolean) ? checksum(parts.join("\n")) : void 0;
8986
9174
  }
8987
9175
  function verdictGuard(root, command, inPlay) {
8988
- const critiqueDir = join44(root, ".jig", "critique");
9176
+ const critiqueDir = join46(root, ".jig", "critique");
8989
9177
  if (!existsSync28(critiqueDir)) return [];
8990
9178
  const problems = [];
8991
9179
  for (const surface of readdirSync18(critiqueDir)) {
8992
9180
  if (surface.startsWith("_")) continue;
8993
- const dir = join44(critiqueDir, surface);
9181
+ const dir = join46(critiqueDir, surface);
8994
9182
  const now = verdictChecksum(dir);
8995
9183
  if (!now) continue;
8996
- const lockPath = join44(dir, LOCK);
9184
+ const lockPath = join46(dir, LOCK);
8997
9185
  if ((command === "critique" || command === "ship") && (!inPlay || inPlay.includes(surface))) {
8998
9186
  writeLock(root, surface, lockPath, dir, now);
8999
9187
  problems.push(...lockLeftBehind(root, surface));
@@ -9001,7 +9189,7 @@ function verdictGuard(root, command, inPlay) {
9001
9189
  }
9002
9190
  let lock = {};
9003
9191
  try {
9004
- lock = JSON.parse(readFileSync37(lockPath, "utf8"));
9192
+ lock = JSON.parse(readFileSync38(lockPath, "utf8"));
9005
9193
  } catch {
9006
9194
  continue;
9007
9195
  }
@@ -9026,7 +9214,7 @@ function verdictGuard(root, command, inPlay) {
9026
9214
  function lockLeftBehind(root, surface) {
9027
9215
  const rel = `.jig/critique/${surface}`;
9028
9216
  try {
9029
- 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();
9030
9218
  if (!status([`${rel}/${LOCK}`])) return [];
9031
9219
  if (status(VERDICT_FILES.map((f) => `${rel}/${f}`))) return [];
9032
9220
  } catch {
@@ -9038,7 +9226,7 @@ function probesLeftBehind(root, surfaces) {
9038
9226
  if (!surfaces.length) return [];
9039
9227
  let lines;
9040
9228
  try {
9041
- 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);
9042
9230
  } catch {
9043
9231
  return [];
9044
9232
  }
@@ -9077,7 +9265,7 @@ function unjudgedTweakIds(dir, record) {
9077
9265
  for (const f of VERDICT_FILES) {
9078
9266
  let file;
9079
9267
  try {
9080
- file = JSON.parse(readFileSync37(join44(dir, f), "utf8"));
9268
+ file = JSON.parse(readFileSync38(join46(dir, f), "utf8"));
9081
9269
  } catch {
9082
9270
  continue;
9083
9271
  }
@@ -9089,7 +9277,7 @@ function unjudgedTweakIds(dir, record) {
9089
9277
  }
9090
9278
  function save(file, state) {
9091
9279
  try {
9092
- mkdirSync8(join44(file, ".."), { recursive: true });
9280
+ mkdirSync8(join46(file, ".."), { recursive: true });
9093
9281
  writeFileSync6(file, JSON.stringify(state), "utf8");
9094
9282
  } catch {
9095
9283
  }
@@ -9097,7 +9285,7 @@ function save(file, state) {
9097
9285
 
9098
9286
  // src/index.ts
9099
9287
  var packageRoot = getPackageRoot();
9100
- var { version } = JSON.parse(readFileSync38(join45(packageRoot, "package.json"), "utf8"));
9288
+ var { version } = JSON.parse(readFileSync39(join47(packageRoot, "package.json"), "utf8"));
9101
9289
  function warnIfUnpublishedPin() {
9102
9290
  if (isDevVersion(version)) {
9103
9291
  console.warn(
@@ -9209,12 +9397,12 @@ program.command("update").description("Update vendored Jig rules, skipping files
9209
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) => {
9210
9398
  const projectRoot = findProjectRoot(process.cwd());
9211
9399
  const slug = name.replace(/^.*\//, "").replace(/\.spec\.md$/, "");
9212
- const path = join45(projectRoot, ".jig", "specs", `${slug}.spec.md`);
9400
+ const path = join47(projectRoot, ".jig", "specs", `${slug}.spec.md`);
9213
9401
  if (!existsSync29(path)) {
9214
9402
  console.error(` \u2717 no spec at .jig/specs/${slug}.spec.md`);
9215
9403
  process.exit(1);
9216
9404
  }
9217
- console.log(specChecksum(readFileSync38(path, "utf8")));
9405
+ console.log(specChecksum(readFileSync39(path, "utf8")));
9218
9406
  });
9219
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) => {
9220
9408
  const projectRoot = findProjectRoot(process.cwd());
@@ -9348,7 +9536,7 @@ program.command("probe").description("Print the render probe. With --save, read
9348
9536
  program.command("gate").description("Run by the Claude Code Stop hook: block stopping while check or a critique fails.").action(async () => {
9349
9537
  let input = {};
9350
9538
  try {
9351
- if (!process.stdin.isTTY) input = JSON.parse(readFileSync38(0, "utf8") || "{}");
9539
+ if (!process.stdin.isTTY) input = JSON.parse(readFileSync39(0, "utf8") || "{}");
9352
9540
  } catch {
9353
9541
  }
9354
9542
  const cwd = input.cwd ?? process.cwd();
@@ -9389,7 +9577,7 @@ program.command("init").description("Set the project up to use Jig: a brand file
9389
9577
  });
9390
9578
  function resolveSurfaceMode(projectRoot) {
9391
9579
  try {
9392
- const config = JSON.parse(readFileSync38(join45(projectRoot, "jig.config.json"), "utf8"));
9580
+ const config = JSON.parse(readFileSync39(join47(projectRoot, "jig.config.json"), "utf8"));
9393
9581
  const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
9394
9582
  return modes.length === 1 ? modes[0] : void 0;
9395
9583
  } catch {