jig-ui 0.26.0 → 0.26.2

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 existsSync28, readFileSync as readFileSync37 } from "fs";
5
+ import { existsSync as existsSync29, readFileSync as readFileSync38 } from "fs";
6
6
  import { homedir as homedir3 } from "os";
7
- import { join as join44 } from "path";
7
+ import { join as join45 } 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 existsSync16, readFileSync as readFileSync21 } from "fs";
1703
- import { join as join25 } from "path";
1702
+ import { existsSync as existsSync18, readFileSync as readFileSync23 } from "fs";
1703
+ import { join as join27 } from "path";
1704
1704
 
1705
1705
  // src/check/files.ts
1706
1706
  import { execFileSync } from "child_process";
@@ -1994,6 +1994,291 @@ function participatesInTokenLayer(source, tokens) {
1994
1994
  return false;
1995
1995
  }
1996
1996
 
1997
+ // src/check/brand-decided.ts
1998
+ import { existsSync as existsSync12, readdirSync as readdirSync8, readFileSync as readFileSync15 } from "fs";
1999
+ import { basename, join as join19 } from "path";
2000
+
2001
+ // src/check/decisions.ts
2002
+ import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
2003
+ import { join as join18, posix as posix3 } from "path";
2004
+ var HEADING3 = /^(#{2,3})\s+(.+?)\s*$/gm;
2005
+ var NOT_A_DECISION = /^(unresolved|open questions?|undecided|contents?|index)$/i;
2006
+ function decisionsFile(projectRoot) {
2007
+ const candidates = ["jig/DECISIONS.md", "DECISIONS.md", "src/jig/DECISIONS.md", "src/styles/jig/DECISIONS.md", ".jig/DECISIONS.md"];
2008
+ try {
2009
+ const brand = JSON.parse(readFileSync14(join18(projectRoot, "jig.config.json"), "utf8")).brand;
2010
+ if (typeof brand === "string" && brand.includes("/")) candidates.unshift(`${posix3.dirname(brand.replace(/^\.\//, ""))}/DECISIONS.md`);
2011
+ } catch {
2012
+ }
2013
+ for (const candidate of candidates) {
2014
+ if (existsSync11(join18(projectRoot, candidate))) return candidate;
2015
+ }
2016
+ return void 0;
2017
+ }
2018
+ function decisionNames(projectRoot) {
2019
+ return [...decisionHeadings(projectRoot).keys()];
2020
+ }
2021
+ function decisionHeadings(projectRoot) {
2022
+ const headings = /* @__PURE__ */ new Map();
2023
+ const path = decisionsFile(projectRoot);
2024
+ if (!path) return headings;
2025
+ let body;
2026
+ try {
2027
+ body = readText(join18(projectRoot, path));
2028
+ } catch {
2029
+ return headings;
2030
+ }
2031
+ for (const match of body.matchAll(HEADING3)) {
2032
+ const name = match[2].replace(/[`*]/g, "").trim();
2033
+ if (!name || NOT_A_DECISION.test(name)) continue;
2034
+ if (!headings.has(name)) headings.set(name, match[0].trim());
2035
+ }
2036
+ return headings;
2037
+ }
2038
+ function decisionSections(body) {
2039
+ const sections = /* @__PURE__ */ new Map();
2040
+ const lines = body.split("\n");
2041
+ let name;
2042
+ let text = [];
2043
+ const close = () => {
2044
+ if (name && !NOT_A_DECISION.test(name) && !sections.has(name)) sections.set(name, text.join("\n").trim());
2045
+ };
2046
+ for (const line of lines) {
2047
+ const heading = /^(#{1,3})\s+(.+?)\s*$/.exec(line);
2048
+ if (heading) {
2049
+ close();
2050
+ name = heading[1].length >= 2 ? heading[2].replace(/[`*]/g, "").trim() : void 0;
2051
+ text = [];
2052
+ } else text.push(line);
2053
+ }
2054
+ close();
2055
+ return sections;
2056
+ }
2057
+ function changedWhys(current, before, newOnly = false) {
2058
+ const then = decisionSections(before);
2059
+ const paragraphs = (text) => text.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => /^\*\*Why\b[^*]*:\*\*/.test(p));
2060
+ const out = [];
2061
+ for (const [name, text] of decisionSections(current)) {
2062
+ const old = then.get(name);
2063
+ if (old === text) continue;
2064
+ const kept = newOnly && old !== void 0 ? new Set(paragraphs(old)) : /* @__PURE__ */ new Set();
2065
+ const whys = paragraphs(text).filter((p) => !kept.has(p));
2066
+ if (whys.length) out.push([name, whys]);
2067
+ }
2068
+ return out;
2069
+ }
2070
+ function unsourcedReasons(current, before, opts = {}) {
2071
+ const problems = [];
2072
+ for (const [name, whys] of changedWhys(current, before, opts.newWhysOnly)) {
2073
+ for (const why of whys) {
2074
+ const label = /^\s*\*\*(Why\b[^*]*):\*\*/.exec(why)[1];
2075
+ const said = why.replace(/^\s*\*\*Why\b[^*]*:\*\*/, "").trim();
2076
+ if (/inferred/i.test(label)) continue;
2077
+ if (/^not given\b/i.test(said)) continue;
2078
+ if (/["“][^"”]{3,}["”]/.test(said)) continue;
2079
+ problems.push(`"${name}": its \`**${label}:**\` is not the owner's words in quotation marks. Quote what the owner said, write \`not given\`, or put what you added under \`**Why (inferred):**\`.`);
2080
+ break;
2081
+ }
2082
+ }
2083
+ return problems;
2084
+ }
2085
+ function quotesNotFrom(current, before, words, source = "the owner's words in tweak.json (`change`)") {
2086
+ const problems = [];
2087
+ for (const [name, whys] of changedWhys(current, before, true)) {
2088
+ for (const why of whys) {
2089
+ const label = /^\s*\*\*(Why\b[^*]*):\*\*/.exec(why)[1];
2090
+ if (/inferred/i.test(label)) continue;
2091
+ const missing = [...why.matchAll(/["“]([^"”]{3,})["”]/g)].map((m) => m[1]).filter((q) => !quoteHeld(q, words));
2092
+ if (missing.length) {
2093
+ problems.push(`"${name}": its \`**${label}:**\` quotes "${missing[0]}", which ${source} do not say. Quote the owner as they said it, or put your reading under \`**Why (inferred):**\`.`);
2094
+ break;
2095
+ }
2096
+ }
2097
+ }
2098
+ return problems;
2099
+ }
2100
+ var normQuote = (t) => t.toLowerCase().replace(/\\(?=["'\\])/g, "").replace(/[‘’]/g, "'").replace(/[“”]/g, '"').replace(/\s+/g, " ").trim();
2101
+ function quoteHeld(quote, words) {
2102
+ const said = normQuote(words);
2103
+ return quote.split(/…|\.\.\./).map((part) => normQuote(part).replace(/^[\s.,;:]+|[\s.,;:!?]+$/g, "")).filter(Boolean).every((part) => said.includes(part));
2104
+ }
2105
+
2106
+ // src/check/brand-decided.ts
2107
+ function brandFile(projectRoot) {
2108
+ try {
2109
+ const brand = JSON.parse(readFileSync15(join19(projectRoot, "jig.config.json"), "utf8")).brand;
2110
+ if (typeof brand === "string" && existsSync12(join19(projectRoot, brand))) return brand.replace(/^\.\//, "");
2111
+ } catch {
2112
+ }
2113
+ try {
2114
+ const state = JSON.parse(readFileSync15(join19(projectRoot, ".jig", "state.json"), "utf8"));
2115
+ const recorded = Object.keys(state.files ?? {}).find((f) => /^brand\..*\.css$/.test(basename(f)));
2116
+ if (recorded && existsSync12(join19(projectRoot, recorded))) return recorded;
2117
+ } catch {
2118
+ }
2119
+ for (const dir of ["jig", ".jig/tokens"]) {
2120
+ try {
2121
+ const found = readdirSync8(join19(projectRoot, dir)).find((f) => /^brand\..*\.css$/.test(f));
2122
+ if (found) return `${dir}/${found}`;
2123
+ } catch {
2124
+ }
2125
+ }
2126
+ return void 0;
2127
+ }
2128
+ function hexToHsl(hex) {
2129
+ const full = hex.length === 4 ? `#${[...hex.slice(1)].map((c) => c + c).join("")}` : hex;
2130
+ const [r, g, b] = [1, 3, 5].map((i) => parseInt(full.slice(i, i + 2), 16) / 255);
2131
+ const max = Math.max(r, g, b);
2132
+ const min = Math.min(r, g, b);
2133
+ const l = (max + min) / 2;
2134
+ const d = max - min;
2135
+ const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
2136
+ let h = 0;
2137
+ if (d !== 0) h = max === r ? 60 * ((g - b) / d % 6) : max === g ? 60 * ((b - r) / d + 2) : 60 * ((r - g) / d + 4);
2138
+ return { h: (h + 360) % 360, s: s * 100, l: l * 100 };
2139
+ }
2140
+ function declared(source, prop) {
2141
+ const m = new RegExp(`${prop}\\s*:\\s*([^;]+);`).exec(source);
2142
+ if (!m) return void 0;
2143
+ return { value: m[1].trim(), line: source.slice(0, m.index).split("\n").length };
2144
+ }
2145
+ var NOT_A_FACE = /* @__PURE__ */ new Set([
2146
+ "type",
2147
+ "typeface",
2148
+ "typefaces",
2149
+ "font",
2150
+ "fonts",
2151
+ "face",
2152
+ "faces",
2153
+ "one",
2154
+ "the",
2155
+ "a",
2156
+ "an",
2157
+ "and",
2158
+ "for",
2159
+ "both",
2160
+ "no",
2161
+ "not",
2162
+ "why",
2163
+ "inferred",
2164
+ "google",
2165
+ "adobe",
2166
+ "everything",
2167
+ "everywhere",
2168
+ "display",
2169
+ "text",
2170
+ "body",
2171
+ "headings",
2172
+ "heading",
2173
+ "sans",
2174
+ "serif",
2175
+ "mono",
2176
+ "monospace",
2177
+ "system",
2178
+ "default",
2179
+ "plain",
2180
+ "neutral",
2181
+ "loaded",
2182
+ "from",
2183
+ "it",
2184
+ "is",
2185
+ "its",
2186
+ "use",
2187
+ "only",
2188
+ "second",
2189
+ "this",
2190
+ "that",
2191
+ "owner",
2192
+ "agent",
2193
+ "choice",
2194
+ "value",
2195
+ "values",
2196
+ "token",
2197
+ "tokens",
2198
+ "same",
2199
+ "all",
2200
+ "with",
2201
+ "of",
2202
+ "in",
2203
+ "on",
2204
+ "one"
2205
+ ]);
2206
+ function namedFaces(decisions) {
2207
+ const out = [];
2208
+ const add = (face, line) => {
2209
+ const words = face.trim().split(/\s+/).filter((w) => !NOT_A_FACE.has(w.toLowerCase()));
2210
+ if (!words.length) return;
2211
+ const name = words.join(" ");
2212
+ if (!out.some((f) => f.face === name)) out.push({ face: name, line });
2213
+ };
2214
+ let inType = false;
2215
+ decisions.split("\n").forEach((text, i) => {
2216
+ const heading = /^#{2,4}\s+(.*)$/.exec(text);
2217
+ if (heading) inType = /\b(type|typeface|typefaces|font|fonts|typography)\b/i.test(heading[1]);
2218
+ for (const m of text.matchAll(/\b(?:type|typeface|font)\s*:\s*([A-Z][\w-]+(?:\s[A-Z][\w-]+){0,2})/gi)) add(m[1], i + 1);
2219
+ if (!inType) return;
2220
+ const opening = /^(?:#{2,4}\s+[^:]*:\s*)?\*{0,2}([A-Z][\w-]+(?:\s[A-Z][\w-]+)?)\*{0,2}(?:\s+is\b|,|\s+for\b)/.exec(text);
2221
+ if (opening) add(opening[1], i + 1);
2222
+ for (const m of text.matchAll(/--font-(?:text|display)\s*:\s*["']([^"']+)["']/g)) add(m[1], i + 1);
2223
+ });
2224
+ return out;
2225
+ }
2226
+ function decidedHex(decisions) {
2227
+ let aboutColour = false;
2228
+ const lines = decisions.split("\n");
2229
+ for (let i = 0; i < lines.length; i++) {
2230
+ const text = lines[i];
2231
+ const heading = /^#{2,4}\s+(.*)$/.exec(text);
2232
+ if (heading) aboutColour = /\b(brand|colou?r|accent)\b/i.test(heading[1]);
2233
+ const hex = /#[0-9a-f]{6}\b|#[0-9a-f]{3}\b/i.exec(text);
2234
+ if (hex && (aboutColour || /\b(brand|colou?r|accent)\b/i.test(text)) && !/\b(never|not|avoid|forbid)/i.test(text)) return { hex: hex[0], line: i + 1 };
2235
+ }
2236
+ return void 0;
2237
+ }
2238
+ function brandDecidedGaps(projectRoot) {
2239
+ const decisionsRel = decisionsFile(projectRoot);
2240
+ const brandRel = brandFile(projectRoot);
2241
+ if (!decisionsRel || !brandRel) return [];
2242
+ const decisions = readFileSync15(join19(projectRoot, decisionsRel), "utf8");
2243
+ const brand = readFileSync15(join19(projectRoot, brandRel), "utf8");
2244
+ const gaps = [];
2245
+ const decided = decidedHex(decisions);
2246
+ if (decided) {
2247
+ const hex = [decided.hex];
2248
+ const want = hexToHsl(hex[0]);
2249
+ const h = declared(brand, "--brand-h");
2250
+ const s = declared(brand, "--brand-s");
2251
+ const l = declared(brand, "--brand-l");
2252
+ const direct = declared(brand, "--color-brand");
2253
+ const carriesHex = brand.toLowerCase().includes(hex[0].toLowerCase());
2254
+ let matches2 = carriesHex;
2255
+ if (!matches2 && h && s && l) {
2256
+ const [bh, bs, bl] = [parseFloat(h.value), parseFloat(s.value), parseFloat(l.value)];
2257
+ const hueGap = Math.min(Math.abs(bh - want.h), 360 - Math.abs(bh - want.h));
2258
+ matches2 = want.s < 10 && bs < 10 && Math.abs(bl - want.l) <= 12 || hueGap <= 12 && Math.abs(bs - want.s) <= 15 && Math.abs(bl - want.l) <= 12;
2259
+ }
2260
+ if (!matches2) {
2261
+ const line = decided.line;
2262
+ const found = h && s && l ? `hsl(${h.value} ${s.value} ${l.value})` : direct?.value ?? "something else";
2263
+ gaps.push({
2264
+ file: decisionsRel,
2265
+ line,
2266
+ message: `DECISIONS.md decides the brand colour ${hex[0]}, and ${brandRel} still sets ${found}: every page reads the brand file, so none of them carries the decided colour. Change the brand's hue, saturation and lightness in ${brandRel} to match.`
2267
+ });
2268
+ }
2269
+ }
2270
+ const stacks = [declared(brand, "--font-text")?.value ?? "", declared(brand, "--font-display")?.value ?? ""].join(" ").toLowerCase();
2271
+ const missing = namedFaces(decisions).filter((f) => !stacks.includes(f.face.toLowerCase()));
2272
+ if (missing.length) {
2273
+ gaps.push({
2274
+ file: decisionsRel,
2275
+ line: missing[0].line,
2276
+ message: `DECISIONS.md decides the typeface ${missing.map((f) => f.face).join(", ")}, and ${brandRel}'s --font-text does not name it: every page is set in the default stack instead. Put it first in --font-text (and --font-display), and load it wherever the pages load their styles.`
2277
+ });
2278
+ }
2279
+ return gaps;
2280
+ }
2281
+
1997
2282
  // src/check/ext.ts
1998
2283
  var CSS_EXTENSIONS = [".css", ".scss", ".less"];
1999
2284
  function hasExtension(file, exts) {
@@ -2232,8 +2517,8 @@ function palettePairs(classes) {
2232
2517
  }
2233
2518
 
2234
2519
  // src/check/run.ts
2235
- import { readFileSync as readFileSync15 } from "fs";
2236
- import { join as join19 } from "path";
2520
+ import { readFileSync as readFileSync17 } from "fs";
2521
+ import { join as join21 } from "path";
2237
2522
 
2238
2523
  // src/check/css.ts
2239
2524
  function maskComments(source) {
@@ -3519,8 +3804,8 @@ var menuState = {
3519
3804
  };
3520
3805
 
3521
3806
  // src/check/declared-properties.ts
3522
- import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
3523
- import { join as join18 } from "path";
3807
+ import { existsSync as existsSync13, readFileSync as readFileSync16 } from "fs";
3808
+ import { join as join20 } from "path";
3524
3809
  var DECLARATION_RE = /(?<![\w-])['"]?(--[A-Za-z_][\w-]*)['"]?\s*:/g;
3525
3810
  var AT_PROPERTY_RE = /@property\s+(--[\w-]+)/g;
3526
3811
  var SET_PROPERTY_RE = /setProperty\(\s*['"`](--[\w-]+)/g;
@@ -3532,16 +3817,16 @@ function addAll(names, text) {
3532
3817
  }
3533
3818
  }
3534
3819
  function readPackageImport(projectRoot, spec) {
3535
- const base = join18(projectRoot, "node_modules", spec);
3820
+ const base = join20(projectRoot, "node_modules", spec);
3536
3821
  for (const candidate of [base, `${base}.css`]) {
3537
3822
  try {
3538
- if (existsSync11(candidate) && candidate.endsWith(".css")) return readFileSync14(candidate, "utf8");
3823
+ if (existsSync13(candidate) && candidate.endsWith(".css")) return readFileSync16(candidate, "utf8");
3539
3824
  } catch {
3540
3825
  }
3541
3826
  }
3542
3827
  try {
3543
- const pkg = JSON.parse(readFileSync14(join18(base, "package.json"), "utf8"));
3544
- if (pkg.style) return readFileSync14(join18(base, pkg.style), "utf8");
3828
+ const pkg = JSON.parse(readFileSync16(join20(base, "package.json"), "utf8"));
3829
+ if (pkg.style) return readFileSync16(join20(base, pkg.style), "utf8");
3545
3830
  } catch {
3546
3831
  }
3547
3832
  return void 0;
@@ -3553,7 +3838,7 @@ function collectDeclaredProperties(projectRoot, files, tokenNames = []) {
3553
3838
  for (const file of files) {
3554
3839
  let raw;
3555
3840
  try {
3556
- raw = readFileSync14(join18(projectRoot, file), "utf8");
3841
+ raw = readFileSync16(join20(projectRoot, file), "utf8");
3557
3842
  } catch {
3558
3843
  continue;
3559
3844
  }
@@ -3586,9 +3871,9 @@ var undeclaredToken = {
3586
3871
  name: "undeclared-token",
3587
3872
  appliesTo: (file) => isStyleBearing(file),
3588
3873
  run(source, file, ctx) {
3589
- const declared = ctx.declaredProperties;
3590
- if (!declared || !declared.complete) return [];
3591
- const known = (name) => declared.names.has(name) || declared.tailwind && isTailwindDefault(name) || name.startsWith("--tw-");
3874
+ const declared2 = ctx.declaredProperties;
3875
+ if (!declared2 || !declared2.complete) return [];
3876
+ const known = (name) => declared2.names.has(name) || declared2.tailwind && isTailwindDefault(name) || name.startsWith("--tw-");
3592
3877
  const hits = /* @__PURE__ */ new Map();
3593
3878
  const note = (name, offset, text) => {
3594
3879
  if (known(name)) return;
@@ -3741,7 +4026,7 @@ var formulaPhrase = {
3741
4026
  return findings.sort((a, b) => a.line - b.line);
3742
4027
  }
3743
4028
  };
3744
- var HEADING3 = /^ {0,3}#{1,6}[ \t]+(.+)$/gm;
4029
+ var HEADING4 = /^ {0,3}#{1,6}[ \t]+(.+)$/gm;
3745
4030
  var FENCE = /^ {0,3}(`{3,}|~{3,})[\s\S]*?^ {0,3}\1[^\n]*$/gm;
3746
4031
  var headingEmoji = {
3747
4032
  name: "heading-emoji",
@@ -3750,7 +4035,7 @@ var headingEmoji = {
3750
4035
  const masked = ctx.raw.replace(FENCE, (m) => m.replace(/[^\n]/g, " "));
3751
4036
  const starts = buildLineIndex(ctx.raw);
3752
4037
  const findings = [];
3753
- for (const heading of masked.matchAll(HEADING3)) {
4038
+ for (const heading of masked.matchAll(HEADING4)) {
3754
4039
  const glyph = [...heading[1].matchAll(EMOJI_RE)].find((m) => !TEXTUAL.has(m[0]));
3755
4040
  if (!glyph) continue;
3756
4041
  const line = lineForOffset(starts, heading.index);
@@ -4419,7 +4704,7 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
4419
4704
  if (applicable.length === 0) continue;
4420
4705
  let raw;
4421
4706
  try {
4422
- raw = readFileSync15(join19(projectRoot, file), "utf8");
4707
+ raw = readFileSync17(join21(projectRoot, file), "utf8");
4423
4708
  } catch {
4424
4709
  continue;
4425
4710
  }
@@ -4433,8 +4718,8 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
4433
4718
  }
4434
4719
 
4435
4720
  // src/check/tokens.ts
4436
- import { existsSync as existsSync12, readdirSync as readdirSync8, readFileSync as readFileSync16 } from "fs";
4437
- import { join as join20 } from "path";
4721
+ import { existsSync as existsSync14, readdirSync as readdirSync9, readFileSync as readFileSync18 } from "fs";
4722
+ import { join as join22 } from "path";
4438
4723
  function rootDeclarations(source) {
4439
4724
  const out = {};
4440
4725
  for (const block of splitRuleBlocks(source)) {
@@ -4449,20 +4734,20 @@ function rootDeclarations(source) {
4449
4734
  }
4450
4735
  function loadTokenMap(projectRoot, consumerFiles = []) {
4451
4736
  const jig = {};
4452
- const tokensDir = join20(projectRoot, ".jig", "tokens");
4453
- if (existsSync12(tokensDir)) {
4454
- for (const file of readdirSync8(tokensDir).filter((f) => f.endsWith(".css")).sort()) {
4455
- Object.assign(jig, rootDeclarations(readFileSync16(join20(tokensDir, file), "utf8")));
4737
+ const tokensDir = join22(projectRoot, ".jig", "tokens");
4738
+ if (existsSync14(tokensDir)) {
4739
+ for (const file of readdirSync9(tokensDir).filter((f) => f.endsWith(".css")).sort()) {
4740
+ Object.assign(jig, rootDeclarations(readFileSync18(join22(tokensDir, file), "utf8")));
4456
4741
  }
4457
4742
  }
4458
4743
  const consumer = {};
4459
4744
  const ambiguous = /* @__PURE__ */ new Set();
4460
- const vendored = join20(".jig", "tokens");
4745
+ const vendored = join22(".jig", "tokens");
4461
4746
  for (const file of consumerFiles) {
4462
4747
  if (file.includes(vendored)) continue;
4463
4748
  let source;
4464
4749
  try {
4465
- source = readFileSync16(join20(projectRoot, file), "utf8");
4750
+ source = readFileSync18(join22(projectRoot, file), "utf8");
4466
4751
  } catch {
4467
4752
  continue;
4468
4753
  }
@@ -4481,8 +4766,8 @@ function loadTokenMap(projectRoot, consumerFiles = []) {
4481
4766
  }
4482
4767
 
4483
4768
  // src/check/exempt.ts
4484
- import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
4485
- import { join as join21 } from "path";
4769
+ import { existsSync as existsSync15, readFileSync as readFileSync19 } from "fs";
4770
+ import { join as join23 } from "path";
4486
4771
  function globToRegExp(glob) {
4487
4772
  let out = "";
4488
4773
  for (let i = 0; i < glob.length; i++) {
@@ -4502,19 +4787,19 @@ function globToRegExp(glob) {
4502
4787
  }
4503
4788
  function readExemptions(projectRoot) {
4504
4789
  const none = { patterns: [], ignored: [] };
4505
- const configPath = join21(projectRoot, "jig.config.json");
4506
- if (!existsSync13(configPath)) return none;
4507
- let declared;
4790
+ const configPath = join23(projectRoot, "jig.config.json");
4791
+ if (!existsSync15(configPath)) return none;
4792
+ let declared2;
4508
4793
  try {
4509
- declared = JSON.parse(readFileSync17(configPath, "utf8")).exempt;
4794
+ declared2 = JSON.parse(readFileSync19(configPath, "utf8")).exempt;
4510
4795
  } catch {
4511
4796
  return none;
4512
4797
  }
4513
- if (!Array.isArray(declared)) return none;
4798
+ if (!Array.isArray(declared2)) return none;
4514
4799
  const root = projectRoot.replace(/\\/g, "/").replace(/\/+$/, "");
4515
4800
  const patterns = [];
4516
4801
  const ignored = [];
4517
- for (const raw of declared) {
4802
+ for (const raw of declared2) {
4518
4803
  if (typeof raw !== "string" || !raw.trim()) continue;
4519
4804
  let p = raw.trim().replace(/\\/g, "/");
4520
4805
  if (p.startsWith(`${root}/`)) p = p.slice(root.length + 1);
@@ -4556,8 +4841,8 @@ function applyExemptions(files, patterns) {
4556
4841
  }
4557
4842
 
4558
4843
  // src/check/waiver.ts
4559
- import { readFileSync as readFileSync18 } from "fs";
4560
- import { join as join22 } from "path";
4844
+ import { readFileSync as readFileSync20 } from "fs";
4845
+ import { join as join24 } from "path";
4561
4846
  var WAIVER = /jig-allow\s+([A-Z]-\d+(?:\s*,\s*[A-Z]-\d+)*)\s*:(.*)$/;
4562
4847
  function readWaiver(line) {
4563
4848
  const m = WAIVER.exec(line);
@@ -4572,7 +4857,7 @@ function applyWaivers(root, findings) {
4572
4857
  let cached = lines.get(file);
4573
4858
  if (!cached) {
4574
4859
  try {
4575
- cached = readFileSync18(join22(root, file), "utf8").split("\n");
4860
+ cached = readFileSync20(join24(root, file), "utf8").split("\n");
4576
4861
  } catch {
4577
4862
  cached = [];
4578
4863
  }
@@ -4637,8 +4922,8 @@ function hasMenuToggle(raw) {
4637
4922
  }
4638
4923
 
4639
4924
  // src/check/mode-wiring.ts
4640
- import { existsSync as existsSync14, readFileSync as readFileSync19 } from "fs";
4641
- import { dirname as dirname6, join as join23 } from "path";
4925
+ import { existsSync as existsSync16, readFileSync as readFileSync21 } from "fs";
4926
+ import { dirname as dirname6, join as join25 } from "path";
4642
4927
  var MODES = ["editorial", "product", "operator"];
4643
4928
  function configProblems(config) {
4644
4929
  const problems = [];
@@ -4660,10 +4945,10 @@ function configProblems(config) {
4660
4945
  }
4661
4946
  function modeWiringProblems(projectRoot) {
4662
4947
  let config;
4663
- const path = join23(projectRoot, "jig.config.json");
4664
- if (!existsSync14(path)) return [];
4948
+ const path = join25(projectRoot, "jig.config.json");
4949
+ if (!existsSync16(path)) return [];
4665
4950
  try {
4666
- config = JSON.parse(readFileSync19(path, "utf8"));
4951
+ config = JSON.parse(readFileSync21(path, "utf8"));
4667
4952
  } catch {
4668
4953
  return [{ mode: "", barrel: "jig.config.json", message: "jig.config.json is not valid JSON, so nothing in it applies" }];
4669
4954
  }
@@ -4676,10 +4961,10 @@ function modeWiringProblems(projectRoot) {
4676
4961
  const rel = (f) => dir === "." ? f : `${dir}/${f}`;
4677
4962
  const multi = modes.length > 1;
4678
4963
  const barrelOf = (mode) => multi ? `theme.${mode}.css` : "theme.css";
4679
- const anyBarrel = ["theme.css", ...modes.map((m) => `theme.${m}.css`)].some((f) => existsSync14(join23(projectRoot, rel(f))));
4964
+ const anyBarrel = ["theme.css", ...modes.map((m) => `theme.${m}.css`)].some((f) => existsSync16(join25(projectRoot, rel(f))));
4680
4965
  if (!anyBarrel) return [];
4681
4966
  const problems = [];
4682
- if (multi && existsSync14(join23(projectRoot, rel("theme.css")))) {
4967
+ if (multi && existsSync16(join25(projectRoot, rel("theme.css")))) {
4683
4968
  problems.push({
4684
4969
  mode: modes[0],
4685
4970
  barrel: rel("theme.css"),
@@ -4691,7 +4976,7 @@ function modeWiringProblems(projectRoot) {
4691
4976
  const modeFile = `mode.${mode}.css`;
4692
4977
  let body;
4693
4978
  try {
4694
- body = readFileSync19(join23(projectRoot, barrel), "utf8");
4979
+ body = readFileSync21(join25(projectRoot, barrel), "utf8");
4695
4980
  } catch {
4696
4981
  problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${barrel} does not exist` });
4697
4982
  return;
@@ -4703,7 +4988,7 @@ function modeWiringProblems(projectRoot) {
4703
4988
  barrel,
4704
4989
  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`
4705
4990
  });
4706
- } else if (!existsSync14(join23(projectRoot, rel(modeFile)))) {
4991
+ } else if (!existsSync16(join25(projectRoot, rel(modeFile)))) {
4707
4992
  problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${rel(modeFile)} does not exist` });
4708
4993
  }
4709
4994
  });
@@ -4711,8 +4996,8 @@ function modeWiringProblems(projectRoot) {
4711
4996
  }
4712
4997
 
4713
4998
  // src/check/token-audit.ts
4714
- import { existsSync as existsSync15, readdirSync as readdirSync9, readFileSync as readFileSync20 } from "fs";
4715
- import { join as join24 } from "path";
4999
+ import { existsSync as existsSync17, readdirSync as readdirSync10, readFileSync as readFileSync22 } from "fs";
5000
+ import { join as join26 } from "path";
4716
5001
  var TEXT_FLOOR = 4.5;
4717
5002
  var UI_FLOOR = 3;
4718
5003
  var FOREGROUNDS = [
@@ -4768,24 +5053,24 @@ function floorFor(token) {
4768
5053
  function tokenFiles(projectRoot) {
4769
5054
  const read = (rel) => {
4770
5055
  try {
4771
- return readFileSync20(join24(projectRoot, ...rel.split("/")), "utf8");
5056
+ return readFileSync22(join26(projectRoot, ...rel.split("/")), "utf8");
4772
5057
  } catch {
4773
5058
  return null;
4774
5059
  }
4775
5060
  };
4776
- const statePath = join24(projectRoot, ".jig", "state.json");
4777
- if (existsSync15(statePath)) {
5061
+ const statePath = join26(projectRoot, ".jig", "state.json");
5062
+ if (existsSync17(statePath)) {
4778
5063
  try {
4779
- const state = JSON.parse(readFileSync20(statePath, "utf8"));
5064
+ const state = JSON.parse(readFileSync22(statePath, "utf8"));
4780
5065
  const recorded = Object.keys(state.files ?? {}).filter((f) => f.endsWith(".css")).sort();
4781
5066
  const out = recorded.map((path) => ({ path, source: read(path) })).filter((f) => f.source !== null);
4782
5067
  if (out.length > 0) return out;
4783
5068
  } catch {
4784
5069
  }
4785
5070
  }
4786
- const legacy = join24(projectRoot, ".jig", "tokens");
4787
- if (!existsSync15(legacy)) return [];
4788
- return readdirSync9(legacy).filter((f) => f.endsWith(".css")).sort().map((f) => ({ path: `.jig/tokens/${f}`, source: readFileSync20(join24(legacy, f), "utf8") }));
5071
+ const legacy = join26(projectRoot, ".jig", "tokens");
5072
+ if (!existsSync17(legacy)) return [];
5073
+ return readdirSync10(legacy).filter((f) => f.endsWith(".css")).sort().map((f) => ({ path: `.jig/tokens/${f}`, source: readFileSync22(join26(legacy, f), "utf8") }));
4789
5074
  }
4790
5075
  function auditTokenLayer(projectRoot) {
4791
5076
  const files = tokenFiles(projectRoot);
@@ -4856,15 +5141,15 @@ function auditTokenLayer(projectRoot) {
4856
5141
  function checkExitCode(result) {
4857
5142
  return result.hasError ? 1 : 0;
4858
5143
  }
4859
- var LEGACY_INDEX_REL = join25(".jig", "rules.index.json");
5144
+ var LEGACY_INDEX_REL = join27(".jig", "rules.index.json");
4860
5145
  function resolveIndexPath(projectRoot) {
4861
- const legacy = join25(projectRoot, LEGACY_INDEX_REL);
4862
- if (existsSync16(legacy)) return legacy;
4863
- return join25(assetRoot(), "rules.index.json");
5146
+ const legacy = join27(projectRoot, LEGACY_INDEX_REL);
5147
+ if (existsSync18(legacy)) return legacy;
5148
+ return join27(assetRoot(), "rules.index.json");
4864
5149
  }
4865
5150
  function resolveMode(projectRoot) {
4866
5151
  try {
4867
- const config = JSON.parse(readFileSync21(join25(projectRoot, "jig.config.json"), "utf8"));
5152
+ const config = JSON.parse(readFileSync23(join27(projectRoot, "jig.config.json"), "utf8"));
4868
5153
  const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
4869
5154
  if (modes.length === 1) return modes[0];
4870
5155
  return modes.length > 1 ? "mixed" : "unknown";
@@ -4881,14 +5166,14 @@ function summariseUnscanned(files) {
4881
5166
  }
4882
5167
  function countSpecs() {
4883
5168
  try {
4884
- return loadSpecs(join25(assetRoot(), "rules")).length;
5169
+ return loadSpecs(join27(assetRoot(), "rules")).length;
4885
5170
  } catch {
4886
5171
  return 0;
4887
5172
  }
4888
5173
  }
4889
5174
  function check(opts) {
4890
5175
  const indexPath = resolveIndexPath(opts.projectRoot);
4891
- const index = validateIndex(JSON.parse(readFileSync21(indexPath, "utf8")));
5176
+ const index = validateIndex(JSON.parse(readFileSync23(indexPath, "utf8")));
4892
5177
  const selection = selectFiles(opts.projectRoot, opts.all);
4893
5178
  const { patterns: exemptions, ignored: exemptIgnored } = readExemptions(opts.projectRoot);
4894
5179
  const { scanned: files, exempt, byPattern: counted } = applyExemptions(selection.files, exemptions);
@@ -4899,7 +5184,7 @@ function check(opts) {
4899
5184
  const projectParticipates = stylesheets.some((f) => {
4900
5185
  if (!hasExtension(f, CSS_EXTENSIONS)) return false;
4901
5186
  try {
4902
- return participatesInTokenLayer(readFileSync21(join25(opts.projectRoot, f), "utf8"), tokens);
5187
+ return participatesInTokenLayer(readFileSync23(join27(opts.projectRoot, f), "utf8"), tokens);
4903
5188
  } catch {
4904
5189
  return false;
4905
5190
  }
@@ -4907,7 +5192,7 @@ function check(opts) {
4907
5192
  const projectResponsive = stylesheets.some((f) => {
4908
5193
  if (!isStyleBearing(f)) return false;
4909
5194
  try {
4910
- const raw = readFileSync21(join25(opts.projectRoot, f), "utf8");
5195
+ const raw = readFileSync23(join27(opts.projectRoot, f), "utf8");
4911
5196
  return isResponsive(maskComments(maskNonStyleRegions(raw, f)), raw);
4912
5197
  } catch {
4913
5198
  return false;
@@ -4916,14 +5201,14 @@ function check(opts) {
4916
5201
  const viewportFitCover = stylesheets.some((f) => {
4917
5202
  if (!isStyleBearing(f)) return false;
4918
5203
  try {
4919
- return /viewport-fit\s*=\s*cover|viewportFit\s*:\s*['"]cover['"]/i.test(readFileSync21(join25(opts.projectRoot, f), "utf8"));
5204
+ return /viewport-fit\s*=\s*cover|viewportFit\s*:\s*['"]cover['"]/i.test(readFileSync23(join27(opts.projectRoot, f), "utf8"));
4920
5205
  } catch {
4921
5206
  return false;
4922
5207
  }
4923
5208
  });
4924
5209
  const projectMenuToggle = stylesheets.some((f) => {
4925
5210
  try {
4926
- return hasMenuToggle(readFileSync21(join25(opts.projectRoot, f), "utf8"));
5211
+ return hasMenuToggle(readFileSync23(join27(opts.projectRoot, f), "utf8"));
4927
5212
  } catch {
4928
5213
  return false;
4929
5214
  }
@@ -4931,7 +5216,7 @@ function check(opts) {
4931
5216
  const declaredProperties = collectDeclaredProperties(opts.projectRoot, stylesheets, Object.keys(tokens));
4932
5217
  const withStyles = files.filter((f) => {
4933
5218
  try {
4934
- const src = readFileSync21(join25(opts.projectRoot, f), "utf8");
5219
+ const src = readFileSync23(join27(opts.projectRoot, f), "utf8");
4935
5220
  if (hasExtension(f, CSS_EXTENSIONS)) return src.trim().length > 0;
4936
5221
  if (maskNonStyleRegions(src, f).trim().length > 0) return true;
4937
5222
  return isMarkupHost(f) && classAttributeValues(src).some((a) => a.classes.trim().length > 0);
@@ -4958,6 +5243,9 @@ function check(opts) {
4958
5243
  message: problem.message
4959
5244
  });
4960
5245
  }
5246
+ for (const gap of brandDecidedGaps(opts.projectRoot)) {
5247
+ detected.push({ ruleId: "A-01", detector: "brand-decided", bucket: "mechanical", severity: "warning", file: gap.file, line: gap.line, message: gap.message });
5248
+ }
4961
5249
  const { findings, waived } = applyWaivers(opts.projectRoot, detected);
4962
5250
  const hasError = findings.some((f) => f.bucket === "mechanical" && f.severity === "error");
4963
5251
  const cssFiles = files.filter((f) => /\.(css|scss|less)$/i.test(f));
@@ -4966,7 +5254,7 @@ function check(opts) {
4966
5254
  // told to add a token @import it has nowhere to put.
4967
5255
  cssFiles.length > 0 && cssFiles.every((f) => {
4968
5256
  try {
4969
- return !participatesInTokenLayer(readFileSync21(join25(opts.projectRoot, f), "utf8"), tokens);
5257
+ return !participatesInTokenLayer(readFileSync23(join27(opts.projectRoot, f), "utf8"), tokens);
4970
5258
  } catch {
4971
5259
  return true;
4972
5260
  }
@@ -4996,12 +5284,12 @@ function check(opts) {
4996
5284
  }
4997
5285
 
4998
5286
  // src/commands/init.ts
4999
- import { existsSync as existsSync17, mkdirSync as mkdirSync6, readdirSync as readdirSync10, readFileSync as readFileSync25, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
5000
- import { dirname as dirname7, isAbsolute, join as join29, resolve as resolve2, sep as sep3 } from "path";
5287
+ import { existsSync as existsSync19, mkdirSync as mkdirSync6, readdirSync as readdirSync11, readFileSync as readFileSync27, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
5288
+ import { dirname as dirname7, isAbsolute, join as join31, resolve as resolve2, sep as sep3 } from "path";
5001
5289
 
5002
5290
  // src/init/detect.ts
5003
- import { readFileSync as readFileSync22 } from "fs";
5004
- import { join as join26 } from "path";
5291
+ import { readFileSync as readFileSync24 } from "fs";
5292
+ import { join as join28 } from "path";
5005
5293
  var TAILWIND_CONFIG_RE = /^tailwind\.config\.(js|ts|mjs|cjs)$/;
5006
5294
  var TAILWIND_V4_IMPORT_RE = /@import\s+["']tailwindcss["']/;
5007
5295
  var TAILWIND_V4_THEME_RE = /@theme\b/;
@@ -5018,7 +5306,7 @@ var FRAMEWORK_DEPS = [
5018
5306
  function detectFramework(projectRoot) {
5019
5307
  let pkg;
5020
5308
  try {
5021
- pkg = JSON.parse(readFileSync22(join26(projectRoot, "package.json"), "utf8"));
5309
+ pkg = JSON.parse(readFileSync24(join28(projectRoot, "package.json"), "utf8"));
5022
5310
  } catch {
5023
5311
  return void 0;
5024
5312
  }
@@ -5036,7 +5324,7 @@ function detect(projectRoot) {
5036
5324
  for (const f of cssFiles) {
5037
5325
  let source;
5038
5326
  try {
5039
- source = readFileSync22(join26(projectRoot, f), "utf8");
5327
+ source = readFileSync24(join28(projectRoot, f), "utf8");
5040
5328
  } catch {
5041
5329
  continue;
5042
5330
  }
@@ -5071,8 +5359,8 @@ function isIgnored(projectRoot, relPath) {
5071
5359
  }
5072
5360
 
5073
5361
  // src/init/derive.ts
5074
- import { readFileSync as readFileSync23 } from "fs";
5075
- import { join as join27 } from "path";
5362
+ import { readFileSync as readFileSync25 } from "fs";
5363
+ import { join as join29 } from "path";
5076
5364
  var DEFAULT_PROPOSAL = {
5077
5365
  h: 264,
5078
5366
  s: 0,
@@ -5105,7 +5393,7 @@ function fromCssCustomProperties(projectRoot, cssFiles) {
5105
5393
  for (const file of cssFiles) {
5106
5394
  let source;
5107
5395
  try {
5108
- source = readFileSync23(join27(projectRoot, file), "utf8");
5396
+ source = readFileSync25(join29(projectRoot, file), "utf8");
5109
5397
  } catch {
5110
5398
  continue;
5111
5399
  }
@@ -5154,7 +5442,7 @@ function balancedBraceBody(source, openIdx) {
5154
5442
  function fromTailwindConfig(projectRoot, tailwindConfigFile) {
5155
5443
  let source;
5156
5444
  try {
5157
- source = readFileSync23(join27(projectRoot, tailwindConfigFile), "utf8");
5445
+ source = readFileSync25(join29(projectRoot, tailwindConfigFile), "utf8");
5158
5446
  } catch {
5159
5447
  return null;
5160
5448
  }
@@ -5210,7 +5498,7 @@ function fromLiteralFrequency(projectRoot, cssFiles) {
5210
5498
  for (const file of cssFiles) {
5211
5499
  let source;
5212
5500
  try {
5213
- source = readFileSync23(join27(projectRoot, file), "utf8");
5501
+ source = readFileSync25(join29(projectRoot, file), "utf8");
5214
5502
  } catch {
5215
5503
  continue;
5216
5504
  }
@@ -5370,8 +5658,8 @@ function relativeImportPath(fromAbsDir, toAbsFile) {
5370
5658
  }
5371
5659
 
5372
5660
  // src/init/project-name.ts
5373
- import { readFileSync as readFileSync24 } from "fs";
5374
- import { basename, join as join28 } from "path";
5661
+ import { readFileSync as readFileSync26 } from "fs";
5662
+ import { basename as basename2, join as join30 } from "path";
5375
5663
  function slugify(name) {
5376
5664
  const withoutScope = name.replace(/^@[^/]+\//, "");
5377
5665
  const slug = withoutScope.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -5379,11 +5667,11 @@ function slugify(name) {
5379
5667
  }
5380
5668
  function deriveProjectSlug(projectRoot) {
5381
5669
  try {
5382
- const pkg = JSON.parse(readFileSync24(join28(projectRoot, "package.json"), "utf8"));
5670
+ const pkg = JSON.parse(readFileSync26(join30(projectRoot, "package.json"), "utf8"));
5383
5671
  if (typeof pkg.name === "string" && pkg.name.trim()) return slugify(pkg.name);
5384
5672
  } catch {
5385
5673
  }
5386
- return slugify(basename(projectRoot));
5674
+ return slugify(basename2(projectRoot));
5387
5675
  }
5388
5676
 
5389
5677
  // src/init/utilities.ts
@@ -5520,7 +5808,7 @@ function parseSurfaces(answer, fallback) {
5520
5808
  return parsed.length > 0 ? parsed : fallback;
5521
5809
  }
5522
5810
  function fileState(projectRoot, absPath, relPath, initManifest) {
5523
- const existsOnDisk = existsSync17(absPath);
5811
+ const existsOnDisk = existsSync19(absPath);
5524
5812
  const tracked = !!initManifest && relPath in initManifest.files;
5525
5813
  const modified = tracked && isInitFileModified(projectRoot, relPath, initManifest);
5526
5814
  return { existsOnDisk, tracked, modified };
@@ -5531,11 +5819,11 @@ function isValidSurfaceArray(v) {
5531
5819
  );
5532
5820
  }
5533
5821
  function loadEffectiveConfig(projectRoot, configAbsPath, fallbackBrand, fallbackSurfaces) {
5534
- if (!existsSync17(configAbsPath)) return { brand: fallbackBrand, surfaces: fallbackSurfaces };
5822
+ if (!existsSync19(configAbsPath)) return { brand: fallbackBrand, surfaces: fallbackSurfaces };
5535
5823
  try {
5536
- const parsed = JSON.parse(readFileSync25(configAbsPath, "utf8"));
5824
+ const parsed = JSON.parse(readFileSync27(configAbsPath, "utf8"));
5537
5825
  const brandCandidate = typeof parsed.brand === "string" && parsed.brand ? parsed.brand : void 0;
5538
- const brandResolves = brandCandidate ? existsSync17(join29(projectRoot, ...brandCandidate.split("/"))) : false;
5826
+ const brandResolves = brandCandidate ? existsSync19(join31(projectRoot, ...brandCandidate.split("/"))) : false;
5539
5827
  return {
5540
5828
  brand: brandResolves ? brandCandidate : fallbackBrand,
5541
5829
  surfaces: isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : fallbackSurfaces
@@ -5545,15 +5833,15 @@ function loadEffectiveConfig(projectRoot, configAbsPath, fallbackBrand, fallback
5545
5833
  }
5546
5834
  }
5547
5835
  function configuredBrandPath(projectRoot, configAbsPath) {
5548
- if (!existsSync17(configAbsPath)) return { path: null };
5549
- let declared;
5836
+ if (!existsSync19(configAbsPath)) return { path: null };
5837
+ let declared2;
5550
5838
  try {
5551
- declared = JSON.parse(readFileSync25(configAbsPath, "utf8")).brand;
5839
+ declared2 = JSON.parse(readFileSync27(configAbsPath, "utf8")).brand;
5552
5840
  } catch {
5553
5841
  return { path: null };
5554
5842
  }
5555
- if (typeof declared !== "string" || !declared.trim()) return { path: null };
5556
- const rel = declared.trim().replace(/\\/g, "/");
5843
+ if (typeof declared2 !== "string" || !declared2.trim()) return { path: null };
5844
+ const rel = declared2.trim().replace(/\\/g, "/");
5557
5845
  const abs = resolve2(projectRoot, rel);
5558
5846
  const root = resolve2(projectRoot);
5559
5847
  if (isAbsolute(rel) || abs !== root && !abs.startsWith(root + sep3)) {
@@ -5576,9 +5864,9 @@ function defaultTokenDir(detection) {
5576
5864
  return ["jig"];
5577
5865
  }
5578
5866
  function readConfiguredSurfaces(configAbsPath) {
5579
- if (!existsSync17(configAbsPath)) return null;
5867
+ if (!existsSync19(configAbsPath)) return null;
5580
5868
  try {
5581
- const parsed = JSON.parse(readFileSync25(configAbsPath, "utf8"));
5869
+ const parsed = JSON.parse(readFileSync27(configAbsPath, "utf8"));
5582
5870
  return isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : null;
5583
5871
  } catch {
5584
5872
  return null;
@@ -5588,9 +5876,9 @@ function isCssModule(path) {
5588
5876
  return /\.module\.(css|scss)$/i.test(path);
5589
5877
  }
5590
5878
  function legacyBrandFile(projectRoot) {
5591
- const dir = join29(projectRoot, ".jig", "tokens");
5592
- if (!existsSync17(dir)) return void 0;
5593
- const found = readdirSync10(dir).find((f) => /^brand\.[\w.-]+\.css$/.test(f));
5879
+ const dir = join31(projectRoot, ".jig", "tokens");
5880
+ if (!existsSync19(dir)) return void 0;
5881
+ const found = readdirSync11(dir).find((f) => /^brand\.[\w.-]+\.css$/.test(f));
5594
5882
  return found ? relKey(".jig", "tokens", found) : void 0;
5595
5883
  }
5596
5884
  function isTokenLayerFile(f) {
@@ -5609,10 +5897,10 @@ function insertAfterCharset(content, insertion) {
5609
5897
  const charsetLen = m ? m[0].length : 0;
5610
5898
  return content.slice(0, charsetLen) + insertion + content.slice(charsetLen);
5611
5899
  }
5612
- function barrelBody(fileName, brandFile, mode, version2) {
5900
+ function barrelBody(fileName, brandFile2, mode, version2) {
5613
5901
  return vendorHeader(fileName, version2, "css", null) + `/* The token layer for one surface, in one import.
5614
5902
  Brand declares the options; the mode selects from them. */
5615
- @import "./${brandFile}";
5903
+ @import "./${brandFile2}";
5616
5904
  @import "./mode.${mode}.css";
5617
5905
  `;
5618
5906
  }
@@ -5620,7 +5908,7 @@ var LEGACY_PAIR_RE = new RegExp(
5620
5908
  `@import\\s+["'][^"']*brand\\.[\\w.-]+\\.css["'];?\\r?\\n?\\s*@import\\s+["'][^"']*mode\\.(?:${MODES2.join("|")})\\.css["'];?\\r?\\n?`
5621
5909
  );
5622
5910
  function wireBarrel(absPath, barrelImport) {
5623
- const content = existsSync17(absPath) ? readFileSync25(absPath, "utf8") : "";
5911
+ const content = existsSync19(absPath) ? readFileSync27(absPath, "utf8") : "";
5624
5912
  const line = `@import "${barrelImport}";`;
5625
5913
  if (content.includes(line)) return { status: "already-present" };
5626
5914
  const legacy = LEGACY_PAIR_RE.exec(content);
@@ -5715,11 +6003,11 @@ async function init(opts) {
5715
6003
  else if (critiqueAnswer) log(` Could not read '${critiqueAnswer}' \u2014 leaving it page by page; each spec will ask.`);
5716
6004
  }
5717
6005
  if (opts.yes) {
5718
- const declared = readConfiguredSurfaces(join29(opts.projectRoot, "jig.config.json"));
5719
- const inEffect = declared ?? surfaces;
6006
+ const declared2 = readConfiguredSurfaces(join31(opts.projectRoot, "jig.config.json"));
6007
+ const inEffect = declared2 ?? surfaces;
5720
6008
  const map = inEffect.map((x) => `'${x.match}' \u2192 ${x.mode}`).join(", ");
5721
6009
  log(
5722
- declared ? `Surface \u2192 mode: ${map} \u2014 from jig.config.json. Mode sets density, type scale and control sizes, and it outranks an agent's own reading of the project.` : `Surface \u2192 mode: not declared. jig.config.json records no mapping, because --yes has nobody to ask and a guess written there cannot be told from a decision. Mode-gated rules stay silent until you declare one. Token files are written at ${inEffect[0]?.mode ?? "product"} as a baseline. Add "surfaces": [{"match": "/", "mode": "..."}] to jig.config.json and run jig init again \u2014 the config alone changes what agents read, not the token files the page imports \u2014 or run jig init without --yes (modes: ${MODES2.join("/")}).`
6010
+ declared2 ? `Surface \u2192 mode: ${map} \u2014 from jig.config.json. Mode sets density, type scale and control sizes, and it outranks an agent's own reading of the project.` : `Surface \u2192 mode: not declared. jig.config.json records no mapping, because --yes has nobody to ask and a guess written there cannot be told from a decision. Mode-gated rules stay silent until you declare one. Token files are written at ${inEffect[0]?.mode ?? "product"} as a baseline. Add "surfaces": [{"match": "/", "mode": "..."}] to jig.config.json and run jig init again \u2014 the config alone changes what agents read, not the token files the page imports \u2014 or run jig init without --yes (modes: ${MODES2.join("/")}).`
5723
6011
  );
5724
6012
  }
5725
6013
  if (!validation.passesContrast && validation.nearestPassingLightness !== void 0) {
@@ -5734,7 +6022,7 @@ Brand colour adjusted to clear the 4.5:1 contrast floor: l:${originalL}% \u2192
5734
6022
  const finalColor = { h: proposal.h, s: proposal.s, l: proposal.l };
5735
6023
  const projectSlug = deriveProjectSlug(opts.projectRoot);
5736
6024
  const configRelPath = "jig.config.json";
5737
- const configAbsPath = join29(opts.projectRoot, configRelPath);
6025
+ const configAbsPath = join31(opts.projectRoot, configRelPath);
5738
6026
  const configuredBrand = configuredBrandPath(opts.projectRoot, configAbsPath);
5739
6027
  if (configuredBrand.refused) {
5740
6028
  log(
@@ -5768,7 +6056,7 @@ The token layer is at ${currentDir}/, and this project's layout suggests ${would
5768
6056
  }
5769
6057
  }
5770
6058
  const brandRelPath = relocateTo ?? configuredBrand.path ?? priorBrand ?? relKey(...defaultTokenDir(detection), brandFileName(projectSlug));
5771
- const brandAbsPath = join29(opts.projectRoot, ...brandRelPath.split("/"));
6059
+ const brandAbsPath = join31(opts.projectRoot, ...brandRelPath.split("/"));
5772
6060
  const tokensRelDir = brandRelPath.split("/").slice(0, -1);
5773
6061
  log(
5774
6062
  `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.")
@@ -5801,7 +6089,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5801
6089
  if (configAction !== "written") log(` ${configRelPath} exists and is not jig-tracked \u2014 leaving it alone (re-run without --yes to refresh).`);
5802
6090
  }
5803
6091
  if (brandAction === "written") {
5804
- const defaultCssRaw = readFileSync25(join29(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
6092
+ const defaultCssRaw = readFileSync27(join31(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
5805
6093
  const content = renderBrandFile(defaultCssRaw, projectSlug, opts.version, proposal);
5806
6094
  mkdirSync6(dirname7(brandAbsPath), { recursive: true });
5807
6095
  writeFileSync4(brandAbsPath, content, "utf8");
@@ -5809,7 +6097,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5809
6097
  }
5810
6098
  if (relocateTo && configAction !== "written") {
5811
6099
  try {
5812
- const existing = JSON.parse(readFileSync25(configAbsPath, "utf8"));
6100
+ const existing = JSON.parse(readFileSync27(configAbsPath, "utf8"));
5813
6101
  const content = `${JSON.stringify({ ...existing, brand: brandRelPath }, null, 2)}
5814
6102
  `;
5815
6103
  writeFileSync4(configAbsPath, content, "utf8");
@@ -5828,12 +6116,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5828
6116
  } else if (configAction === "merged") {
5829
6117
  let existing = {};
5830
6118
  try {
5831
- existing = JSON.parse(readFileSync25(configAbsPath, "utf8"));
6119
+ existing = JSON.parse(readFileSync27(configAbsPath, "utf8"));
5832
6120
  } catch {
5833
6121
  existing = {};
5834
6122
  }
5835
6123
  const existingBrand = typeof existing.brand === "string" ? existing.brand : void 0;
5836
- const brandStillResolves = existingBrand ? existsSync17(join29(opts.projectRoot, ...existingBrand.split("/"))) : false;
6124
+ const brandStillResolves = existingBrand ? existsSync19(join31(opts.projectRoot, ...existingBrand.split("/"))) : false;
5837
6125
  const existingSurfaces = Array.isArray(existing.surfaces) && existing.surfaces.length > 0 ? existing.surfaces : void 0;
5838
6126
  const merged = {
5839
6127
  ...existing,
@@ -5850,12 +6138,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5850
6138
  }
5851
6139
  const effectiveConfig = loadEffectiveConfig(opts.projectRoot, configAbsPath, brandRelPath, surfaces);
5852
6140
  const primaryMode = effectiveConfig.surfaces[0]?.mode ?? "product";
5853
- const wiringBrandAbsPath = join29(opts.projectRoot, ...effectiveConfig.brand.split("/"));
6141
+ const wiringBrandAbsPath = join31(opts.projectRoot, ...effectiveConfig.brand.split("/"));
5854
6142
  const declaredModes = [...new Set(effectiveConfig.surfaces.map((s) => s.mode))];
5855
6143
  const modeAbsPaths = {};
5856
6144
  for (const mode of declaredModes) {
5857
6145
  const modeFileName = `mode.${mode}.css`;
5858
- const modeAbsPath2 = join29(opts.projectRoot, ...tokensRelDir, modeFileName);
6146
+ const modeAbsPath2 = join31(opts.projectRoot, ...tokensRelDir, modeFileName);
5859
6147
  modeAbsPaths[mode] = modeAbsPath2;
5860
6148
  const modeRelPath = relKey(...tokensRelDir, modeFileName);
5861
6149
  const modeState = fileState(opts.projectRoot, modeAbsPath2, modeRelPath, initManifest);
@@ -5863,7 +6151,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5863
6151
  if (modeState.existsOnDisk && !modeState.tracked) modeAction = "skipped-untracked";
5864
6152
  else if (modeState.modified) modeAction = "skipped-edited";
5865
6153
  if (modeAction === "written") {
5866
- const modeSourceRaw = readFileSync25(join29(opts.packageRoot, "tokens", modeFileName), "utf8");
6154
+ const modeSourceRaw = readFileSync27(join31(opts.packageRoot, "tokens", modeFileName), "utf8");
5867
6155
  const content = vendorHeader(modeFileName, opts.version, "css", null) + modeSourceRaw;
5868
6156
  mkdirSync6(dirname7(modeAbsPath2), { recursive: true });
5869
6157
  writeFileSync4(modeAbsPath2, content, "utf8");
@@ -5878,7 +6166,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5878
6166
  const barrelFor = (mode) => multiMode ? `theme.${mode}.css` : "theme.css";
5879
6167
  for (const mode of declaredModes) {
5880
6168
  const rel = relKey(...tokensRelDir, barrelFor(mode));
5881
- const abs = join29(opts.projectRoot, ...tokensRelDir, barrelFor(mode));
6169
+ const abs = join31(opts.projectRoot, ...tokensRelDir, barrelFor(mode));
5882
6170
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
5883
6171
  if (state.existsOnDisk && (!state.tracked || state.modified)) {
5884
6172
  log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
@@ -5891,8 +6179,8 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5891
6179
  }
5892
6180
  if (multiMode) {
5893
6181
  const rel = relKey(...tokensRelDir, "theme.css");
5894
- const abs = join29(opts.projectRoot, ...tokensRelDir, "theme.css");
5895
- if (existsSync17(abs)) {
6182
+ const abs = join31(opts.projectRoot, ...tokensRelDir, "theme.css");
6183
+ if (existsSync19(abs)) {
5896
6184
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
5897
6185
  if (state.tracked && !state.modified) {
5898
6186
  rmSync2(abs);
@@ -5904,16 +6192,16 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5904
6192
  }
5905
6193
  }
5906
6194
  if (detection.cssSystem === "tailwind-v4") {
5907
- const declared = declaredTokenNames(
6195
+ const declared2 = declaredTokenNames(
5908
6196
  [brandAbsPath, modeAbsPath].map((f) => {
5909
6197
  try {
5910
- return readFileSync25(f, "utf8");
6198
+ return readFileSync27(f, "utf8");
5911
6199
  } catch {
5912
6200
  return "";
5913
6201
  }
5914
6202
  }).join("\n")
5915
6203
  );
5916
- const aliasable = tailwindNamespaced(declared);
6204
+ const aliasable = tailwindNamespaced(declared2);
5917
6205
  const rel = relKey(...tokensRelDir, "utilities.css");
5918
6206
  if (aliasable.length === 0) {
5919
6207
  log(`
@@ -5932,7 +6220,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5932
6220
  log(` This changes how components are written. Both styles are correct.`);
5933
6221
  const answer = (await prompt(" Generate it? [y/N]: ")).toLowerCase();
5934
6222
  if (answer === "y" || answer === "yes") {
5935
- const abs = join29(opts.projectRoot, ...tokensRelDir, "utilities.css");
6223
+ const abs = join31(opts.projectRoot, ...tokensRelDir, "utilities.css");
5936
6224
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
5937
6225
  if (state.existsOnDisk && (!state.tracked || state.modified)) {
5938
6226
  log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
@@ -5943,8 +6231,8 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5943
6231
  files[rel] = checksum(content);
5944
6232
  const target = findWireTarget(detection);
5945
6233
  const importPath = target ? relativeImportPath(
5946
- dirname7(join29(opts.projectRoot, target)),
5947
- join29(opts.projectRoot, ...tokensRelDir, "utilities.css")
6234
+ dirname7(join31(opts.projectRoot, target)),
6235
+ join31(opts.projectRoot, ...tokensRelDir, "utilities.css")
5948
6236
  ) : `./${rel}`;
5949
6237
  log(` Wrote ${rel}. Import it in the stylesheet that imports Tailwind:`);
5950
6238
  log(` @import "tailwindcss";`);
@@ -5963,11 +6251,11 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5963
6251
  writeInitManifest(opts.projectRoot, { version: opts.version, modes: declaredModes, files });
5964
6252
  if (relocateTo && currentBrand) {
5965
6253
  const oldDir = currentBrand.split("/").slice(0, -1);
5966
- const oldAbsDir = join29(opts.projectRoot, ...oldDir);
5967
- if (existsSync17(oldAbsDir)) {
5968
- for (const name of readdirSync10(oldAbsDir).filter((f) => f.endsWith(".css"))) {
6254
+ const oldAbsDir = join31(opts.projectRoot, ...oldDir);
6255
+ if (existsSync19(oldAbsDir)) {
6256
+ for (const name of readdirSync11(oldAbsDir).filter((f) => f.endsWith(".css"))) {
5969
6257
  const rel = relKey(...oldDir, name);
5970
- const abs = join29(oldAbsDir, name);
6258
+ const abs = join31(oldAbsDir, name);
5971
6259
  const state = fileState(opts.projectRoot, abs, rel, initManifest);
5972
6260
  if (!state.tracked || state.modified) {
5973
6261
  log(` ${rel} was edited \u2014 left in place. Delete it yourself once you have moved anything you need.`);
@@ -5981,11 +6269,11 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
5981
6269
  const wireTarget = findWireTarget(detection);
5982
6270
  let wiring;
5983
6271
  if (multiMode) {
5984
- const bare2 = join29(opts.projectRoot, ...tokensRelDir, "theme.css");
6272
+ const bare2 = join31(opts.projectRoot, ...tokensRelDir, "theme.css");
5985
6273
  for (const file of detection.cssFiles.filter((f) => !isTokenLayerFile(f))) {
5986
- const abs = join29(opts.projectRoot, file);
6274
+ const abs = join31(opts.projectRoot, file);
5987
6275
  try {
5988
- const before = readFileSync25(abs, "utf8");
6276
+ const before = readFileSync27(abs, "utf8");
5989
6277
  const after = before.split("\n").filter((l) => {
5990
6278
  const m = /^\s*@import\s+["']([^"']+)["'];?\s*$/.exec(l);
5991
6279
  return !(m && !/^[a-z]+:|^\//i.test(m[1]) && resolve2(dirname7(abs), m[1]) === bare2);
@@ -6007,21 +6295,21 @@ Could not edit ${file}: ${err.message}. Remove its import of ${relKey(...tokensR
6007
6295
  snippet: effectiveConfig.surfaces.map((s) => `${s.match} \u2192 ${relKey(...tokensRelDir, barrelFor(s.mode))}`).join("\n")
6008
6296
  };
6009
6297
  } else if (wireTarget) {
6010
- const targetAbsDir = dirname7(join29(opts.projectRoot, wireTarget));
6298
+ const targetAbsDir = dirname7(join31(opts.projectRoot, wireTarget));
6011
6299
  const barrelImport = relativeImportPath(
6012
6300
  targetAbsDir,
6013
- join29(opts.projectRoot, ...tokensRelDir, "theme.css")
6301
+ join31(opts.projectRoot, ...tokensRelDir, "theme.css")
6014
6302
  );
6015
6303
  const snippet = `@import "${barrelImport}";`;
6016
6304
  try {
6017
6305
  if (relocateTo && currentBrand) {
6018
- const abs = join29(opts.projectRoot, wireTarget);
6306
+ const abs = join31(opts.projectRoot, wireTarget);
6019
6307
  const oldDirName = currentBrand.split("/").slice(0, -1).join("/");
6020
- const before = existsSync17(abs) ? readFileSync25(abs, "utf8") : "";
6308
+ const before = existsSync19(abs) ? readFileSync27(abs, "utf8") : "";
6021
6309
  const after = before.split("\n").filter((l) => !(l.includes("@import") && l.includes(oldDirName))).join("\n");
6022
6310
  if (after !== before) writeFileSync4(abs, after, "utf8");
6023
6311
  }
6024
- const outcome = wireBarrel(join29(opts.projectRoot, wireTarget), barrelImport);
6312
+ const outcome = wireBarrel(join31(opts.projectRoot, wireTarget), barrelImport);
6025
6313
  wiring = { target: wireTarget, status: outcome.status, snippet };
6026
6314
  const verb = outcome.status === "wired" ? "Wired" : outcome.status === "rewired" ? "Rewired" : "Already present in";
6027
6315
  log(`
@@ -6038,7 +6326,7 @@ Could not write to ${wireTarget}: ${err.message}`);
6038
6326
  } else {
6039
6327
  const barrelImport = relativeImportPath(
6040
6328
  opts.projectRoot,
6041
- join29(opts.projectRoot, ...tokensRelDir, "theme.css")
6329
+ join31(opts.projectRoot, ...tokensRelDir, "theme.css")
6042
6330
  );
6043
6331
  const snippet = `@import "${barrelImport}";`;
6044
6332
  wiring = { target: null, status: "print-only", snippet };
@@ -6047,7 +6335,7 @@ Could not write to ${wireTarget}: ${err.message}`);
6047
6335
  if (pasteTargets.length > 0) {
6048
6336
  log("Paste the block for whichever of these is your global stylesheet:");
6049
6337
  for (const target of pasteTargets) {
6050
- const dir = dirname7(join29(opts.projectRoot, target));
6338
+ const dir = dirname7(join31(opts.projectRoot, target));
6051
6339
  const forTarget = `@import "${relativeImportPath(dir, wiringBrandAbsPath)}";
6052
6340
  @import "${relativeImportPath(dir, modeAbsPath)}";`;
6053
6341
  log(`
@@ -6098,28 +6386,28 @@ Could not write to ${wireTarget}: ${err.message}`);
6098
6386
 
6099
6387
  // src/commands/verdicts.ts
6100
6388
  import { execFileSync as execFileSync3 } from "child_process";
6101
- import { existsSync as existsSync24, readFileSync as readFileSync32 } from "fs";
6102
- import { join as join38, relative as relative4, resolve as resolve5, sep as sep6 } from "path";
6389
+ import { existsSync as existsSync25, readFileSync as readFileSync33 } from "fs";
6390
+ import { join as join39, relative as relative4, resolve as resolve5, sep as sep6 } from "path";
6103
6391
 
6104
6392
  // src/rules/citations.ts
6105
- import { join as join30 } from "path";
6393
+ import { join as join32 } from "path";
6106
6394
  function citableIds(packageRoot2) {
6107
- const rulesDir = join30(packageRoot2, "rules");
6395
+ const rulesDir = join32(packageRoot2, "rules");
6108
6396
  const ids = [
6109
- ...loadRules(rulesDir, join30(packageRoot2, "rules.index.json")).map((r) => r.id),
6397
+ ...loadRules(rulesDir, join32(packageRoot2, "rules.index.json")).map((r) => r.id),
6110
6398
  ...loadSpecs(rulesDir).map((s) => s.id)
6111
6399
  ];
6112
6400
  return [...new Set(ids)].sort();
6113
6401
  }
6114
6402
 
6115
6403
  // src/probe/check.ts
6116
- import { existsSync as existsSync21, readdirSync as readdirSync13, readFileSync as readFileSync29 } from "fs";
6117
- import { join as join35 } from "path";
6404
+ import { existsSync as existsSync23, readdirSync as readdirSync14, readFileSync as readFileSync31 } from "fs";
6405
+ import { join as join37 } from "path";
6118
6406
 
6119
6407
  // src/probe/save.ts
6120
- import { existsSync as existsSync20, mkdirSync as mkdirSync7, readdirSync as readdirSync12, readFileSync as readFileSync28, writeFileSync as writeFileSync5 } from "fs";
6408
+ import { existsSync as existsSync22, mkdirSync as mkdirSync7, readdirSync as readdirSync13, readFileSync as readFileSync30, writeFileSync as writeFileSync5 } from "fs";
6121
6409
  import { fileURLToPath as fileURLToPath2 } from "url";
6122
- import { join as join34, relative as relative3, resolve as resolve4, sep as sep5 } from "path";
6410
+ import { join as join36, relative as relative3, resolve as resolve4, sep as sep5 } from "path";
6123
6411
 
6124
6412
  // src/probe/script.ts
6125
6413
  var PROBE_VERSION = 9;
@@ -6345,6 +6633,25 @@ var PROBE_SCRIPT = `(async () => {
6345
6633
  if (!table || getComputedStyle(table).display !== 'table' || display === 'table-cell' || display === 'none' || !vis(cell)) continue;
6346
6634
  brokenCells.push({ cell: cell.tagName.toLowerCase() + (cell.classList[0] ? '.' + cell.classList[0] : ''), text: (cell.innerText || '').trim().replace(/\\s+/g, ' ').slice(0, 40), display });
6347
6635
  }
6636
+ // Rows of one table at different heights, where the taller rows are the
6637
+ // ones holding a control taller than the short rows: a button given the
6638
+ // touch-target floor as its own minimum height. An invoices table had its
6639
+ // "Mark as paid" rows at 65px among 41px ones. Rows that differ because a
6640
+ // cell's text wraps are left alone: only a control can be the cause here.
6641
+ const unevenRows = [];
6642
+ for (const table of [...document.querySelectorAll('table')].slice(0, 20)) {
6643
+ if (unevenRows.length >= 3 || getComputedStyle(table).display !== 'table' || !vis(table)) continue;
6644
+ const rows = [...table.querySelectorAll('tbody tr')].filter((r) => vis(r));
6645
+ if (rows.length < 2) continue;
6646
+ const heights = rows.map((r) => Math.round(r.getBoundingClientRect().height));
6647
+ const short = Math.min(...heights);
6648
+ const control = rows
6649
+ .filter((r, i) => heights[i] > short + 2)
6650
+ .map((r) => [...r.querySelectorAll('button, a[href], input, select')].find((c) => vis(c) && c.getBoundingClientRect().height > short))
6651
+ .find(Boolean);
6652
+ if (!control) continue;
6653
+ unevenRows.push({ table: 'table' + (table.classList[0] ? '.' + table.classList[0] : ''), short, tall: Math.max(...heights), control: control.tagName.toLowerCase() + (control.classList[0] ? '.' + control.classList[0] : ''), controlHeight: Math.round(control.getBoundingClientRect().height) });
6654
+ }
6348
6655
  return JSON.stringify({
6349
6656
  jigProbe: ${PROBE_VERSION},
6350
6657
  url: location.href,
@@ -6354,6 +6661,7 @@ var PROBE_SCRIPT = `(async () => {
6354
6661
  sidewaysScroll: doc.scrollWidth > doc.clientWidth,
6355
6662
  sidewaysBoxes,
6356
6663
  brokenCells,
6664
+ unevenRows,
6357
6665
  bodyFont: getComputedStyle(document.body).fontFamily,
6358
6666
  defaultFont: getComputedStyle(document.body).fontFamily === defaultFont,
6359
6667
  unresolvedTokens: [...unresolved],
@@ -6384,9 +6692,9 @@ var PROBE_SCRIPT = `(async () => {
6384
6692
 
6385
6693
  // src/probe/browser.ts
6386
6694
  import { spawn } from "child_process";
6387
- import { existsSync as existsSync18, mkdtempSync, readdirSync as readdirSync11, rmSync as rmSync3 } from "fs";
6695
+ import { existsSync as existsSync20, mkdtempSync, readdirSync as readdirSync12, rmSync as rmSync3 } from "fs";
6388
6696
  import { homedir as homedir2, tmpdir } from "os";
6389
- import { join as join31 } from "path";
6697
+ import { join as join33 } from "path";
6390
6698
  var CHROME_ENV = ["JIG_CHROME", "CHROME_PATH", "PUPPETEER_EXECUTABLE_PATH", "CHROMIUM_PATH"];
6391
6699
  var CHROME_COMMANDS = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge", "brave-browser", "chrome", "msedge"];
6392
6700
  var CHROME_APPS = [
@@ -6398,20 +6706,20 @@ var CHROME_APPS = [
6398
6706
  // Every Windows machine has Edge; Chrome is often installed per user.
6399
6707
  "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
6400
6708
  "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
6401
- ...process.env.LOCALAPPDATA ? [join31(process.env.LOCALAPPDATA, "Google", "Chrome", "Application", "chrome.exe")] : []
6709
+ ...process.env.LOCALAPPDATA ? [join33(process.env.LOCALAPPDATA, "Google", "Chrome", "Application", "chrome.exe")] : []
6402
6710
  ];
6403
6711
  function cachedBrowsers() {
6404
6712
  const roots = [
6405
- join31(homedir2(), ".cache", "ms-playwright"),
6406
- join31(homedir2(), "Library", "Caches", "ms-playwright"),
6407
- join31(homedir2(), "AppData", "Local", "ms-playwright"),
6408
- join31(homedir2(), ".cache", "puppeteer", "chrome")
6713
+ join33(homedir2(), ".cache", "ms-playwright"),
6714
+ join33(homedir2(), "Library", "Caches", "ms-playwright"),
6715
+ join33(homedir2(), "AppData", "Local", "ms-playwright"),
6716
+ join33(homedir2(), ".cache", "puppeteer", "chrome")
6409
6717
  ];
6410
6718
  const found = [];
6411
6719
  for (const root of roots) {
6412
6720
  let entries;
6413
6721
  try {
6414
- entries = readdirSync11(root);
6722
+ entries = readdirSync12(root);
6415
6723
  } catch {
6416
6724
  continue;
6417
6725
  }
@@ -6425,8 +6733,8 @@ function cachedBrowsers() {
6425
6733
  ["chrome-win", "chrome.exe"],
6426
6734
  ["chrome-win64", "chrome.exe"]
6427
6735
  ]) {
6428
- const candidate = join31(root, entry, ...rel);
6429
- if (existsSync18(candidate)) found.push(candidate);
6736
+ const candidate = join33(root, entry, ...rel);
6737
+ if (existsSync20(candidate)) found.push(candidate);
6430
6738
  }
6431
6739
  }
6432
6740
  }
@@ -6435,13 +6743,13 @@ function cachedBrowsers() {
6435
6743
  function findChrome() {
6436
6744
  for (const key of CHROME_ENV) {
6437
6745
  const value = process.env[key];
6438
- if (value && existsSync18(value)) return value;
6746
+ if (value && existsSync20(value)) return value;
6439
6747
  }
6440
6748
  for (const command of CHROME_COMMANDS) {
6441
6749
  const resolved = which(command);
6442
6750
  if (resolved) return resolved;
6443
6751
  }
6444
- for (const app of CHROME_APPS) if (existsSync18(app)) return app;
6752
+ for (const app of CHROME_APPS) if (existsSync20(app)) return app;
6445
6753
  return cachedBrowsers()[0];
6446
6754
  }
6447
6755
  function which(command) {
@@ -6451,8 +6759,8 @@ function which(command) {
6451
6759
  for (const dir of dirs) {
6452
6760
  if (!dir) continue;
6453
6761
  for (const ext of exts) {
6454
- const candidate = join31(dir, command + ext);
6455
- if (existsSync18(candidate)) return candidate;
6762
+ const candidate = join33(dir, command + ext);
6763
+ if (existsSync20(candidate)) return candidate;
6456
6764
  }
6457
6765
  }
6458
6766
  return void 0;
@@ -6464,7 +6772,7 @@ async function runProbe(opts) {
6464
6772
  "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."
6465
6773
  );
6466
6774
  }
6467
- const profile = mkdtempSync(join31(tmpdir(), "jig-probe-"));
6775
+ const profile = mkdtempSync(join33(tmpdir(), "jig-probe-"));
6468
6776
  const child = spawn(chrome, [
6469
6777
  "--headless=new",
6470
6778
  "--remote-debugging-port=0",
@@ -6568,8 +6876,8 @@ async function evaluate(endpoint, url, width, height, timeoutMs, expression) {
6568
6876
 
6569
6877
  // src/probe/serve.ts
6570
6878
  import { createServer } from "http";
6571
- import { existsSync as existsSync19, readFileSync as readFileSync26, statSync as statSync3 } from "fs";
6572
- import { extname, join as join32, resolve as resolve3, sep as sep4 } from "path";
6879
+ import { existsSync as existsSync21, readFileSync as readFileSync28, statSync as statSync3 } from "fs";
6880
+ import { extname, join as join34, resolve as resolve3, sep as sep4 } from "path";
6573
6881
  var TYPES = {
6574
6882
  ".html": "text/html; charset=utf-8",
6575
6883
  ".htm": "text/html; charset=utf-8",
@@ -6596,19 +6904,19 @@ async function serveDirectory(dir) {
6596
6904
  const root = resolve3(dir);
6597
6905
  const server = createServer((req, res) => {
6598
6906
  let path = decodeURIComponent((req.url ?? "/").split("?")[0].split("#")[0]);
6599
- let file = resolve3(join32(root, path));
6907
+ let file = resolve3(join34(root, path));
6600
6908
  if (file !== root && !file.startsWith(root + sep4)) {
6601
6909
  res.writeHead(403).end();
6602
6910
  return;
6603
6911
  }
6604
- if (existsSync19(file) && statSync3(file).isDirectory()) file = join32(file, "index.html");
6605
- else if (!existsSync19(file) && existsSync19(`${file}.html`)) file = `${file}.html`;
6606
- if (!existsSync19(file)) {
6912
+ if (existsSync21(file) && statSync3(file).isDirectory()) file = join34(file, "index.html");
6913
+ else if (!existsSync21(file) && existsSync21(`${file}.html`)) file = `${file}.html`;
6914
+ if (!existsSync21(file)) {
6607
6915
  res.writeHead(404).end();
6608
6916
  return;
6609
6917
  }
6610
6918
  res.writeHead(200, { "content-type": TYPES[extname(file).toLowerCase()] ?? "application/octet-stream" });
6611
- res.end(readFileSync26(file));
6919
+ res.end(readFileSync28(file));
6612
6920
  });
6613
6921
  await new Promise((done) => server.listen(0, "127.0.0.1", done));
6614
6922
  const address = server.address();
@@ -6620,8 +6928,8 @@ async function serveDirectory(dir) {
6620
6928
  }
6621
6929
 
6622
6930
  // src/probe/switches.ts
6623
- import { readFileSync as readFileSync27 } from "fs";
6624
- import { join as join33 } from "path";
6931
+ import { readFileSync as readFileSync29 } from "fs";
6932
+ import { join as join35 } from "path";
6625
6933
  var DECLARATION = /--breakpoint-([a-z0-9-]+)\s*:\s*(\d+(?:\.\d+)?)(px|rem)\b/g;
6626
6934
  function declaredSwitches(projectRoot) {
6627
6935
  const found = /* @__PURE__ */ new Map();
@@ -6629,7 +6937,7 @@ function declaredSwitches(projectRoot) {
6629
6937
  if (!/\.(css|scss|astro|vue|svelte|html)$/i.test(file)) continue;
6630
6938
  let text;
6631
6939
  try {
6632
- text = readFileSync27(join33(projectRoot, file), "utf8");
6940
+ text = readFileSync29(join35(projectRoot, file), "utf8");
6633
6941
  } catch {
6634
6942
  continue;
6635
6943
  }
@@ -6664,18 +6972,18 @@ function saveProbe(opts) {
6664
6972
  }
6665
6973
  probe.pageFile = relative3(opts.projectRoot, page).split("\\").join("/");
6666
6974
  if (opts.serveRoot) probe.serveRoot = relative3(opts.projectRoot, resolve4(opts.projectRoot, opts.serveRoot)).split("\\").join("/") || ".";
6667
- probe.pageChecksum = checksum(readFileSync28(page, "utf8"));
6975
+ probe.pageChecksum = checksum(readFileSync30(page, "utf8"));
6668
6976
  probe.recordedAt = (/* @__PURE__ */ new Date()).toISOString();
6669
- const dir = join34(opts.projectRoot, ".jig", "critique", opts.surface);
6977
+ const dir = join36(opts.projectRoot, ".jig", "critique", opts.surface);
6670
6978
  mkdirSync7(dir, { recursive: true });
6671
- const path = join34(dir, `probe-${width}.json`);
6979
+ const path = join36(dir, `probe-${width}.json`);
6672
6980
  writeFileSync5(path, JSON.stringify(probe), "utf8");
6673
6981
  return { path: `.jig/critique/${opts.surface}/probe-${width}.json`, width, page: probe.pageFile };
6674
6982
  }
6675
6983
  function projectFile(projectRoot, path) {
6676
6984
  const abs = resolve4(projectRoot, path);
6677
6985
  if (abs !== resolve4(projectRoot) && !abs.startsWith(resolve4(projectRoot) + sep5)) return void 0;
6678
- return existsSync20(abs) ? abs : void 0;
6986
+ return existsSync22(abs) ? abs : void 0;
6679
6987
  }
6680
6988
  function pageFile(projectRoot, url) {
6681
6989
  if (!url.startsWith("file://")) return void 0;
@@ -6687,12 +6995,12 @@ function pageFile(projectRoot, url) {
6687
6995
  }
6688
6996
  const abs = resolve4(path);
6689
6997
  if (!abs.startsWith(resolve4(projectRoot))) return void 0;
6690
- return existsSync20(abs) ? abs : void 0;
6998
+ return existsSync22(abs) ? abs : void 0;
6691
6999
  }
6692
7000
  var PROBE_WIDTHS = [360, 768, 1280, 1600];
6693
7001
  async function runAndSaveProbes(opts) {
6694
7002
  const abs = resolve4(opts.projectRoot, opts.page);
6695
- if (!existsSync20(abs)) throw new Error(`${opts.page} does not exist, so there is nothing to render.`);
7003
+ if (!existsSync22(abs)) throw new Error(`${opts.page} does not exist, so there is nothing to render.`);
6696
7004
  const saved = [];
6697
7005
  const widths = opts.widths ?? probeWidths(PROBE_WIDTHS, declaredSwitches(opts.projectRoot));
6698
7006
  await withPageUrl(opts.projectRoot, abs, opts.serve, async (url) => {
@@ -6706,7 +7014,7 @@ async function runAndSaveProbes(opts) {
6706
7014
  async function withPageUrl(projectRoot, abs, serve, use) {
6707
7015
  if (!serve) return use(`file://${abs}`);
6708
7016
  const root = resolve4(projectRoot, serve);
6709
- if (!existsSync20(root)) throw new Error(`${serve} does not exist, so there is nothing to serve.`);
7017
+ if (!existsSync22(root)) throw new Error(`${serve} does not exist, so there is nothing to serve.`);
6710
7018
  if (!abs.startsWith(root + sep5)) throw new Error(`The page is not inside ${serve}, so serving ${serve} cannot show it.`);
6711
7019
  const server = await serveDirectory(root);
6712
7020
  try {
@@ -6717,31 +7025,31 @@ async function withPageUrl(projectRoot, abs, serve, use) {
6717
7025
  }
6718
7026
  }
6719
7027
  function recordedPage(projectRoot, surface) {
6720
- const dir = join34(projectRoot, ".jig", "critique", surface);
6721
- if (!existsSync20(dir)) return void 0;
6722
- for (const f of readdirSync12(dir).filter((name) => /^probe-\d+\.json$/.test(name)).sort()) {
7028
+ const dir = join36(projectRoot, ".jig", "critique", surface);
7029
+ if (!existsSync22(dir)) return void 0;
7030
+ for (const f of readdirSync13(dir).filter((name) => /^probe-\d+\.json$/.test(name)).sort()) {
6723
7031
  try {
6724
- const probe = JSON.parse(readFileSync28(join34(dir, f), "utf8"));
6725
- if (probe.pageFile && existsSync20(resolve4(projectRoot, probe.pageFile))) return probe.pageFile;
7032
+ const probe = JSON.parse(readFileSync30(join36(dir, f), "utf8"));
7033
+ if (probe.pageFile && existsSync22(resolve4(projectRoot, probe.pageFile))) return probe.pageFile;
6726
7034
  } catch {
6727
7035
  }
6728
7036
  }
6729
7037
  return void 0;
6730
7038
  }
6731
7039
  async function ensureProbes(opts) {
6732
- const dir = join34(opts.projectRoot, ".jig", "critique", opts.surface);
7040
+ const dir = join36(opts.projectRoot, ".jig", "critique", opts.surface);
6733
7041
  const abs = resolve4(opts.projectRoot, opts.page);
6734
7042
  let current;
6735
7043
  try {
6736
- current = checksum(readFileSync28(abs, "utf8"));
7044
+ current = checksum(readFileSync30(abs, "utf8"));
6737
7045
  } catch {
6738
7046
  return { recorded: [], reason: `${opts.page} could not be read` };
6739
7047
  }
6740
- const recorded = existsSync20(dir) ? readdirSync12(dir).map((f) => /^probe-(\d+)\.json$/.exec(f)?.[1]).filter((w) => !!w).map(Number) : [];
7048
+ const recorded = existsSync22(dir) ? readdirSync13(dir).map((f) => /^probe-(\d+)\.json$/.exec(f)?.[1]).filter((w) => !!w).map(Number) : [];
6741
7049
  const widths = [.../* @__PURE__ */ new Set([...probeWidths(PROBE_WIDTHS, declaredSwitches(opts.projectRoot)), ...recorded])].sort((a, b) => a - b);
6742
7050
  const missing = widths.filter((width) => {
6743
7051
  try {
6744
- const probe = JSON.parse(readFileSync28(join34(dir, `probe-${width}.json`), "utf8"));
7052
+ const probe = JSON.parse(readFileSync30(join36(dir, `probe-${width}.json`), "utf8"));
6745
7053
  return probe.jigProbe !== PROBE_VERSION || probe.pageChecksum !== current;
6746
7054
  } catch {
6747
7055
  return true;
@@ -6752,7 +7060,7 @@ async function ensureProbes(opts) {
6752
7060
  let serve;
6753
7061
  for (const width of widths) {
6754
7062
  try {
6755
- const probe = JSON.parse(readFileSync28(join34(dir, `probe-${width}.json`), "utf8"));
7063
+ const probe = JSON.parse(readFileSync30(join36(dir, `probe-${width}.json`), "utf8"));
6756
7064
  if (probe.serveRoot) {
6757
7065
  serve = probe.serveRoot;
6758
7066
  break;
@@ -6768,9 +7076,9 @@ async function ensureProbes(opts) {
6768
7076
  return { recorded: missing };
6769
7077
  }
6770
7078
  function critiquedSurfaces(projectRoot) {
6771
- const root = join34(projectRoot, ".jig", "critique");
6772
- if (!existsSync20(root)) return [];
6773
- return readdirSync12(root).filter((surface) => !surface.startsWith("_") && (existsSync20(join34(root, surface, "screen.json")) || existsSync20(join34(root, surface, "code.json"))));
7079
+ const root = join36(projectRoot, ".jig", "critique");
7080
+ if (!existsSync22(root)) return [];
7081
+ return readdirSync13(root).filter((surface) => !surface.startsWith("_") && (existsSync22(join36(root, surface, "screen.json")) || existsSync22(join36(root, surface, "code.json"))));
6774
7082
  }
6775
7083
 
6776
7084
  // src/probe/check.ts
@@ -6782,7 +7090,7 @@ function stampProblem(projectRoot, file, p) {
6782
7090
  if (!page) return `${file} names a page outside this project (${p.serveRoot ? p.pageFile : p.url ?? "no url"}).`;
6783
7091
  let current;
6784
7092
  try {
6785
- current = readFileSync29(page, "utf8");
7093
+ current = readFileSync31(page, "utf8");
6786
7094
  } catch {
6787
7095
  return `${file} was taken on ${p.pageFile}, which no longer exists.`;
6788
7096
  }
@@ -6792,11 +7100,11 @@ function stampProblem(projectRoot, file, p) {
6792
7100
  return void 0;
6793
7101
  }
6794
7102
  function readProbes(projectRoot, dir, errors) {
6795
- if (!existsSync21(dir)) return [];
7103
+ if (!existsSync23(dir)) return [];
6796
7104
  const probes = [];
6797
- for (const f of readdirSync13(dir).filter((n) => /^probe-\d+\.json$/.test(n)).sort()) {
7105
+ for (const f of readdirSync14(dir).filter((n) => /^probe-\d+\.json$/.test(n)).sort()) {
6798
7106
  try {
6799
- const p = JSON.parse(readFileSync29(join35(dir, f), "utf8"));
7107
+ const p = JSON.parse(readFileSync31(join37(dir, f), "utf8"));
6800
7108
  if (typeof p.jigProbe === "number" && p.jigProbe < PROBE_VERSION && typeof p.width === "number") {
6801
7109
  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\`.`);
6802
7110
  continue;
@@ -6840,6 +7148,9 @@ function probeCheck(probes, verdictOf, indexable = true, mode) {
6840
7148
  const shown = p.brokenCells.map((c) => `${c.cell}${c.text ? ` ("${c.text}")` : ""} is display: ${c.display}`).join("; ");
6841
7149
  failures.push(`${at(p)}: ${p.brokenCells.length === 1 ? "a table cell has" : `${p.brokenCells.length} table cells have`} left the table: ${shown}. A cell given its own display stops taking its row's height, so its border and its text drift from the row's. Keep the cell a cell and put the flex on a wrapper inside it (P-06).`);
6842
7150
  }
7151
+ for (const u of p.unevenRows ?? []) {
7152
+ failures.push(`${at(p)}: ${u.table}'s rows are not one height: those holding ${u.control} (${u.controlHeight}px tall) stand ${u.tall}px, the rest ${u.short}px. A button in a row takes the row's height and has no minimum height of its own; on a touch screen the row rises to the touch target instead (P-02, P-06).`);
7153
+ }
6843
7154
  if (p.defaultFont) {
6844
7155
  failures.push(`${at(p)}: the page renders in the browser's default font (${p.bodyFont ?? "unknown"}) \u2014 its styles are not applying. No review of this page can pass until they do.`);
6845
7156
  }
@@ -6916,126 +7227,21 @@ function probeCheck(probes, verdictOf, indexable = true, mode) {
6916
7227
  return { contradictions: errors, failures };
6917
7228
  }
6918
7229
 
6919
- // src/check/decisions.ts
6920
- import { existsSync as existsSync22, readFileSync as readFileSync30 } from "fs";
6921
- import { join as join36, posix as posix3 } from "path";
6922
- var HEADING4 = /^(#{2,3})\s+(.+?)\s*$/gm;
6923
- var NOT_A_DECISION = /^(unresolved|open questions?|undecided|contents?|index)$/i;
6924
- function decisionsFile(projectRoot) {
6925
- const candidates = ["jig/DECISIONS.md", "DECISIONS.md", "src/jig/DECISIONS.md", "src/styles/jig/DECISIONS.md", ".jig/DECISIONS.md"];
6926
- try {
6927
- const brand = JSON.parse(readFileSync30(join36(projectRoot, "jig.config.json"), "utf8")).brand;
6928
- if (typeof brand === "string" && brand.includes("/")) candidates.unshift(`${posix3.dirname(brand.replace(/^\.\//, ""))}/DECISIONS.md`);
6929
- } catch {
6930
- }
6931
- for (const candidate of candidates) {
6932
- if (existsSync22(join36(projectRoot, candidate))) return candidate;
6933
- }
6934
- return void 0;
6935
- }
6936
- function decisionNames(projectRoot) {
6937
- return [...decisionHeadings(projectRoot).keys()];
6938
- }
6939
- function decisionHeadings(projectRoot) {
6940
- const headings = /* @__PURE__ */ new Map();
6941
- const path = decisionsFile(projectRoot);
6942
- if (!path) return headings;
6943
- let body;
6944
- try {
6945
- body = readText(join36(projectRoot, path));
6946
- } catch {
6947
- return headings;
6948
- }
6949
- for (const match of body.matchAll(HEADING4)) {
6950
- const name = match[2].replace(/[`*]/g, "").trim();
6951
- if (!name || NOT_A_DECISION.test(name)) continue;
6952
- if (!headings.has(name)) headings.set(name, match[0].trim());
6953
- }
6954
- return headings;
6955
- }
6956
- function decisionSections(body) {
6957
- const sections = /* @__PURE__ */ new Map();
6958
- const lines = body.split("\n");
6959
- let name;
6960
- let text = [];
6961
- const close = () => {
6962
- if (name && !NOT_A_DECISION.test(name) && !sections.has(name)) sections.set(name, text.join("\n").trim());
6963
- };
6964
- for (const line of lines) {
6965
- const heading = /^(#{1,3})\s+(.+?)\s*$/.exec(line);
6966
- if (heading) {
6967
- close();
6968
- name = heading[1].length >= 2 ? heading[2].replace(/[`*]/g, "").trim() : void 0;
6969
- text = [];
6970
- } else text.push(line);
6971
- }
6972
- close();
6973
- return sections;
6974
- }
6975
- function changedWhys(current, before, newOnly = false) {
6976
- const then = decisionSections(before);
6977
- const paragraphs = (text) => text.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => /^\*\*Why\b[^*]*:\*\*/.test(p));
6978
- const out = [];
6979
- for (const [name, text] of decisionSections(current)) {
6980
- const old = then.get(name);
6981
- if (old === text) continue;
6982
- const kept = newOnly && old !== void 0 ? new Set(paragraphs(old)) : /* @__PURE__ */ new Set();
6983
- const whys = paragraphs(text).filter((p) => !kept.has(p));
6984
- if (whys.length) out.push([name, whys]);
6985
- }
6986
- return out;
6987
- }
6988
- function unsourcedReasons(current, before, opts = {}) {
6989
- const problems = [];
6990
- for (const [name, whys] of changedWhys(current, before, opts.newWhysOnly)) {
6991
- for (const why of whys) {
6992
- const label = /^\s*\*\*(Why\b[^*]*):\*\*/.exec(why)[1];
6993
- const said = why.replace(/^\s*\*\*Why\b[^*]*:\*\*/, "").trim();
6994
- if (/inferred/i.test(label)) continue;
6995
- if (/^not given\b/i.test(said)) continue;
6996
- if (/["“][^"”]{3,}["”]/.test(said)) continue;
6997
- problems.push(`"${name}": its \`**${label}:**\` is not the owner's words in quotation marks. Quote what the owner said, write \`not given\`, or put what you added under \`**Why (inferred):**\`.`);
6998
- break;
6999
- }
7000
- }
7001
- return problems;
7002
- }
7003
- function quotesNotFrom(current, before, words, source = "the owner's words in tweak.json (`change`)") {
7004
- const problems = [];
7005
- for (const [name, whys] of changedWhys(current, before, true)) {
7006
- for (const why of whys) {
7007
- const label = /^\s*\*\*(Why\b[^*]*):\*\*/.exec(why)[1];
7008
- if (/inferred/i.test(label)) continue;
7009
- const missing = [...why.matchAll(/["“]([^"”]{3,})["”]/g)].map((m) => m[1]).filter((q) => !quoteHeld(q, words));
7010
- if (missing.length) {
7011
- problems.push(`"${name}": its \`**${label}:**\` quotes "${missing[0]}", which ${source} do not say. Quote the owner as they said it, or put your reading under \`**Why (inferred):**\`.`);
7012
- break;
7013
- }
7014
- }
7015
- }
7016
- return problems;
7017
- }
7018
- var normQuote = (t) => t.toLowerCase().replace(/\\(?=["'\\])/g, "").replace(/[‘’]/g, "'").replace(/[“”]/g, '"').replace(/\s+/g, " ").trim();
7019
- function quoteHeld(quote, words) {
7020
- const said = normQuote(words);
7021
- return quote.split(/…|\.\.\./).map((part) => normQuote(part).replace(/^[\s.,;:]+|[\s.,;:!?]+$/g, "")).filter(Boolean).every((part) => said.includes(part));
7022
- }
7023
-
7024
7230
  // src/check/spec-shape.ts
7025
- import { existsSync as existsSync23, readdirSync as readdirSync14, statSync as statSync4 } from "fs";
7026
- import { join as join37 } from "path";
7231
+ import { existsSync as existsSync24, readdirSync as readdirSync15, statSync as statSync4 } from "fs";
7232
+ import { join as join38 } from "path";
7027
7233
  function newestSpec(projectRoot) {
7028
- const dir = join37(projectRoot, ".jig", "specs");
7029
- if (!existsSync23(dir)) return void 0;
7030
- const files = readdirSync14(dir).filter((f) => f.endsWith(".md"));
7234
+ const dir = join38(projectRoot, ".jig", "specs");
7235
+ if (!existsSync24(dir)) return void 0;
7236
+ const files = readdirSync15(dir).filter((f) => f.endsWith(".md"));
7031
7237
  if (files.length === 0) return void 0;
7032
- const newest = files.map((f) => ({ f, at: statSync4(join37(dir, f)).mtimeMs })).sort((a, b) => b.at - a.at)[0].f;
7033
- return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body: readText(join37(dir, newest)) };
7238
+ const newest = files.map((f) => ({ f, at: statSync4(join38(dir, f)).mtimeMs })).sort((a, b) => b.at - a.at)[0].f;
7239
+ return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body: readText(join38(dir, newest)) };
7034
7240
  }
7035
7241
  function specFor(projectRoot, surface) {
7036
7242
  if (surface) {
7037
- const path = join37(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7038
- if (existsSync23(path)) return { path: `.jig/specs/${surface}.spec.md`, slug: surface, body: readText(path) };
7243
+ const path = join38(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7244
+ if (existsSync24(path)) return { path: `.jig/specs/${surface}.spec.md`, slug: surface, body: readText(path) };
7039
7245
  }
7040
7246
  return newestSpec(projectRoot);
7041
7247
  }
@@ -7190,29 +7396,29 @@ function reasonProblems(file, judged, errors) {
7190
7396
  }
7191
7397
  }
7192
7398
  function readJson(path, errors) {
7193
- if (!existsSync24(path)) return null;
7399
+ if (!existsSync25(path)) return null;
7194
7400
  try {
7195
- return JSON.parse(readFileSync32(path, "utf8"));
7401
+ return JSON.parse(readFileSync33(path, "utf8"));
7196
7402
  } catch (e) {
7197
7403
  errors.push(`${path}: not valid JSON (${e.message})`);
7198
7404
  return {};
7199
7405
  }
7200
7406
  }
7201
7407
  function specIndexable(projectRoot, surface) {
7202
- const path = join38(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7408
+ const path = join39(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7203
7409
  let front = "";
7204
7410
  try {
7205
7411
  front = readText(path).split(/^---\s*$/m)[1] ?? "";
7206
7412
  } catch {
7207
7413
  }
7208
- const declared = specIndexableField(front);
7209
- if (declared === true || declared === false) return declared;
7414
+ const declared2 = specIndexableField(front);
7415
+ if (declared2 === true || declared2 === false) return declared2;
7210
7416
  const mode = /^\s*mode\s*:\s*(\w+)/im.exec(front)?.[1]?.toLowerCase();
7211
7417
  return mode !== "product" && mode !== "operator";
7212
7418
  }
7213
7419
  function specNeedsNav(projectRoot, surface) {
7214
- const path = join38(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7215
- if (!existsSync24(path)) return false;
7420
+ const path = join39(projectRoot, ".jig", "specs", `${surface}.spec.md`);
7421
+ if (!existsSync25(path)) return false;
7216
7422
  const front = readText(path).split(/^---\s*$/m)[1] ?? "";
7217
7423
  const navField = [...front.matchAll(/^\s*nav:\s*(.+)$/gim)].some((m) => !/^\s*(none|n\/a|-)\b/i.test(m[1]));
7218
7424
  const navRegion = /^\s*-\s*(nav|navigation)\s*:/im.test(front);
@@ -7290,7 +7496,7 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
7290
7496
  function checkDecisions(projectRoot, dir, errors) {
7291
7497
  const required = decisionNames(projectRoot);
7292
7498
  if (required.length === 0) return { state: "ran", judged: 0, total: 0, findings: 0 };
7293
- const file = readJson(join38(dir, "decisions.json"), errors);
7499
+ const file = readJson(join39(dir, "decisions.json"), errors);
7294
7500
  if (!file) {
7295
7501
  errors.push(
7296
7502
  `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.`
@@ -7345,7 +7551,7 @@ function decisionsSince(projectRoot, dir, unjudged) {
7345
7551
  if (!path) return [];
7346
7552
  const git = (args) => execFileSync3("git", args, { cwd: projectRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
7347
7553
  try {
7348
- const verdictFile = relative4(projectRoot, join38(dir, "decisions.json")).split(sep6).join("/");
7554
+ const verdictFile = relative4(projectRoot, join39(dir, "decisions.json")).split(sep6).join("/");
7349
7555
  if (git(["status", "--porcelain", "--", verdictFile])) return [];
7350
7556
  const judgedIn = git(["log", "-1", "--format=%H", "--", verdictFile]);
7351
7557
  if (!judgedIn) return [];
@@ -7369,7 +7575,7 @@ function decisionsSince(projectRoot, dir, unjudged) {
7369
7575
  function verifyVerdicts(opts) {
7370
7576
  const root = opts.packageRoot ?? assetRoot();
7371
7577
  const errors = [];
7372
- const index = JSON.parse(readFileSync32(join38(root, "rules.index.json"), "utf8"));
7578
+ const index = JSON.parse(readFileSync33(join39(root, "rules.index.json"), "utf8"));
7373
7579
  const judgment = index.filter((r) => r.bucket === "judgment" || r.bucket === "hybrid" && r.pass !== void 0);
7374
7580
  const screenIds = judgment.filter((r) => r.pass === "screen").map((r) => r.id);
7375
7581
  const codeIds = judgment.filter((r) => r.pass === "code").map((r) => r.id);
@@ -7378,9 +7584,9 @@ function verifyVerdicts(opts) {
7378
7584
  ...judgment.map((r) => [r.id, r.pass ?? ""])
7379
7585
  ]);
7380
7586
  const specIds = new Set(citableIds(root).filter((id) => /^[PMLRT]-\d+$/.test(id)));
7381
- const dir = join38(opts.projectRoot, ".jig", "critique", opts.surface);
7382
- const screenFile = readJson(join38(dir, "screen.json"), errors);
7383
- const codeFile = readJson(join38(dir, "code.json"), errors);
7587
+ const dir = join39(opts.projectRoot, ".jig", "critique", opts.surface);
7588
+ const screenFile = readJson(join39(dir, "screen.json"), errors);
7589
+ const codeFile = readJson(join39(dir, "code.json"), errors);
7384
7590
  const screenExtraRequired = specNeedsNav(opts.projectRoot, opts.surface) ? ["P-14"] : [];
7385
7591
  const decisionList = decisionNames(opts.projectRoot);
7386
7592
  const screen = checkArm("screen", screenFile, screenIds, passOf, specIds, screenExtraRequired, errors, decisionList);
@@ -7388,7 +7594,7 @@ function verifyVerdicts(opts) {
7388
7594
  let rendered = false;
7389
7595
  if (screenFile && screenFile.rendered === true) {
7390
7596
  const artefacts = Array.isArray(screenFile.artefacts) ? screenFile.artefacts.filter((a) => typeof a === "string") : [];
7391
- const absent = artefacts.filter((a) => !existsSync24(resolve5(opts.projectRoot, a)));
7597
+ const absent = artefacts.filter((a) => !existsSync25(resolve5(opts.projectRoot, a)));
7392
7598
  if (artefacts.length === 0) errors.push("screen.json says rendered: true but lists no artefacts. A render leaves a screenshot.");
7393
7599
  else if (absent.length) errors.push(`screen.json says rendered: true, but these artefacts do not exist: ${absent.join(", ")}.`);
7394
7600
  else rendered = true;
@@ -7410,7 +7616,7 @@ function verifyVerdicts(opts) {
7410
7616
  const measured = [];
7411
7617
  let specFront = "";
7412
7618
  try {
7413
- specFront = readText(join38(opts.projectRoot, ".jig", "specs", `${opts.surface}.spec.md`)).split(/^---\s*$/m)[1] ?? "";
7619
+ specFront = readText(join39(opts.projectRoot, ".jig", "specs", `${opts.surface}.spec.md`)).split(/^---\s*$/m)[1] ?? "";
7414
7620
  } catch {
7415
7621
  }
7416
7622
  if (specIndexableField(specFront) === "unreadable") {
@@ -7455,7 +7661,7 @@ function previousFindings(projectRoot, dir) {
7455
7661
  const out = /* @__PURE__ */ new Map();
7456
7662
  let text;
7457
7663
  try {
7458
- text = at ? lf(git(["show", `${at}:./${rel}/${f}`])) : readText(join38(dir, f));
7664
+ text = at ? lf(git(["show", `${at}:./${rel}/${f}`])) : readText(join39(dir, f));
7459
7665
  } catch {
7460
7666
  return out;
7461
7667
  }
@@ -7537,17 +7743,17 @@ function sourceChanges(git, from, to) {
7537
7743
  }
7538
7744
 
7539
7745
  // src/commands/gate.ts
7540
- import { existsSync as existsSync27, mkdirSync as mkdirSync8, readdirSync as readdirSync16, readFileSync as readFileSync36, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
7746
+ import { existsSync as existsSync28, mkdirSync as mkdirSync8, readdirSync as readdirSync17, readFileSync as readFileSync37, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
7541
7747
  import { createHash as createHash3 } from "crypto";
7542
- import { join as join43 } from "path";
7748
+ import { join as join44 } from "path";
7543
7749
 
7544
7750
  // src/check/mockup-drawing.ts
7545
- import { join as join39 } from "path";
7751
+ import { join as join40 } from "path";
7546
7752
  var FRAMES = [["phone", 360], ["tablet", 768], ["desktop", 1280], ["wide", 1600]];
7547
7753
  function mockupDrawingProblems(root, specBody, at) {
7548
7754
  let html;
7549
7755
  try {
7550
- html = readText(join39(root, at));
7756
+ html = readText(join40(root, at));
7551
7757
  } catch {
7552
7758
  return [];
7553
7759
  }
@@ -7608,7 +7814,7 @@ function frameRegionProblems(frames, specBody, at) {
7608
7814
  function approvedDrawingProblems(root, specBody, at) {
7609
7815
  let html;
7610
7816
  try {
7611
- html = readText(join39(root, at));
7817
+ html = readText(join40(root, at));
7612
7818
  } catch {
7613
7819
  return [];
7614
7820
  }
@@ -7714,12 +7920,12 @@ function sizeBlock2(sizes, size) {
7714
7920
  import { execFileSync as execFileSync4 } from "child_process";
7715
7921
 
7716
7922
  // src/commands/ship.ts
7717
- import { existsSync as existsSync25, readdirSync as readdirSync15, readFileSync as readFileSync34 } from "fs";
7718
- import { join as join41 } from "path";
7923
+ import { existsSync as existsSync26, readdirSync as readdirSync16, readFileSync as readFileSync35 } from "fs";
7924
+ import { join as join42 } from "path";
7719
7925
 
7720
7926
  // src/commands/seo.ts
7721
- import { readFileSync as readFileSync33 } from "fs";
7722
- import { join as join40 } from "path";
7927
+ import { readFileSync as readFileSync34 } from "fs";
7928
+ import { join as join41 } from "path";
7723
7929
  var SITEMAP_FILE = /(^|\/)sitemap(-index)?\.(xml|ts|js|mjs|tsx|jsx|rb|php|py)$/i;
7724
7930
  var ROBOTS_FILE = /(^|\/)robots\.(txt|ts|js|mjs|tsx|jsx)$/i;
7725
7931
  var ROUTE_IN_SITEMAP = /(?:<loc>\s*([^<\s]+)\s*<\/loc>)|(?:url\s*:\s*["'`]([^"'`]+)["'`])/gi;
@@ -7760,7 +7966,7 @@ function seo(opts) {
7760
7966
  for (const file of files) {
7761
7967
  let raw;
7762
7968
  try {
7763
- raw = readFileSync33(join40(root, file), "utf8");
7969
+ raw = readFileSync34(join41(root, file), "utf8");
7764
7970
  } catch {
7765
7971
  continue;
7766
7972
  }
@@ -7825,7 +8031,7 @@ function critiqueAtShip(projectRoot, specBody) {
7825
8031
  const own = specCritique(specBody);
7826
8032
  if (own) return own === "at-ship";
7827
8033
  try {
7828
- return JSON.parse(readFileSync34(join41(projectRoot, "jig.config.json"), "utf8")).critique === "at-ship";
8034
+ return JSON.parse(readFileSync35(join42(projectRoot, "jig.config.json"), "utf8")).critique === "at-ship";
7829
8035
  } catch {
7830
8036
  return false;
7831
8037
  }
@@ -7833,14 +8039,14 @@ function critiqueAtShip(projectRoot, specBody) {
7833
8039
  function tweakDeferred(dir) {
7834
8040
  let t;
7835
8041
  try {
7836
- t = JSON.parse(readFileSync34(join41(dir, "tweak.json"), "utf8"));
8042
+ t = JSON.parse(readFileSync35(join42(dir, "tweak.json"), "utf8"));
7837
8043
  } catch {
7838
8044
  return false;
7839
8045
  }
7840
8046
  if (t.deferred === void 0 || t.deferred === false) return false;
7841
8047
  let judged;
7842
8048
  try {
7843
- judged = JSON.parse(readFileSync34(join41(dir, "verdicts.lock"), "utf8")).tweak;
8049
+ judged = JSON.parse(readFileSync35(join42(dir, "verdicts.lock"), "utf8")).tweak;
7844
8050
  } catch {
7845
8051
  }
7846
8052
  return judged !== t.at;
@@ -7851,14 +8057,14 @@ function pageChecksum(html) {
7851
8057
  function changedSinceLock(projectRoot, dir) {
7852
8058
  let lock;
7853
8059
  try {
7854
- lock = JSON.parse(readFileSync34(join41(dir, "verdicts.lock"), "utf8"));
8060
+ lock = JSON.parse(readFileSync35(join42(dir, "verdicts.lock"), "utf8"));
7855
8061
  } catch {
7856
8062
  return void 0;
7857
8063
  }
7858
8064
  const file = lock.page?.file;
7859
8065
  if (!file || !lock.page?.checksum) return void 0;
7860
8066
  try {
7861
- const html = readFileSync34(join41(projectRoot, file), "utf8");
8067
+ const html = readFileSync35(join42(projectRoot, file), "utf8");
7862
8068
  const seen = [pageChecksum(html), checksum(html)];
7863
8069
  return seen.includes(lock.page.checksum) ? void 0 : file;
7864
8070
  } catch {
@@ -7870,7 +8076,7 @@ function supersededBy(front) {
7870
8076
  return named ? named : void 0;
7871
8077
  }
7872
8078
  function pageStatus(projectRoot, surface) {
7873
- const spec = readText(join41(projectRoot, ".jig", "specs", `${surface}.spec.md`));
8079
+ const spec = readText(join42(projectRoot, ".jig", "specs", `${surface}.spec.md`));
7874
8080
  const front = spec.split(/^---\s*$/m)[1] ?? "";
7875
8081
  if (!/^\s*confirmed\s*:\s*true\b/im.test(front)) {
7876
8082
  return { surface, state: "in-progress", detail: "its spec is not confirmed, so nothing of it is built to ship" };
@@ -7878,10 +8084,10 @@ function pageStatus(projectRoot, surface) {
7878
8084
  const successor = supersededBy(front);
7879
8085
  if (successor !== void 0) {
7880
8086
  const file = successor.replace(/(\.spec\.md)?$/, ".spec.md");
7881
- return existsSync25(join41(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` };
8087
+ return existsSync26(join42(projectRoot, ".jig", "specs", file)) ? { surface, state: "superseded", detail: `superseded by ${file}, whose critique judges its page` } : { surface, state: "incomplete", detail: `\`superseded_by: ${successor}\` names a spec that does not exist in .jig/specs` };
7882
8088
  }
7883
- const dir = join41(projectRoot, ".jig", "critique", surface);
7884
- if (!existsSync25(join41(dir, "screen.json")) && !existsSync25(join41(dir, "code.json"))) {
8089
+ const dir = join42(projectRoot, ".jig", "critique", surface);
8090
+ if (!existsSync26(join42(dir, "screen.json")) && !existsSync26(join42(dir, "code.json"))) {
7885
8091
  return { surface, state: "never", detail: "never critiqued" };
7886
8092
  }
7887
8093
  if (tweakDeferred(dir)) return { surface, state: "deferred", detail: "a tweak deferred its re-judge" };
@@ -7891,7 +8097,7 @@ function pageStatus(projectRoot, surface) {
7891
8097
  const stale = v.errors.find((e) => /was taken (on|by) an older/.test(e));
7892
8098
  let judgedPage = false;
7893
8099
  try {
7894
- judgedPage = !!JSON.parse(readFileSync34(join41(dir, "verdicts.lock"), "utf8")).page?.checksum;
8100
+ judgedPage = !!JSON.parse(readFileSync35(join42(dir, "verdicts.lock"), "utf8")).page?.checksum;
7895
8101
  } catch {
7896
8102
  }
7897
8103
  if (stale && !judgedPage) return { surface, state: "changed", detail: "the page changed after its probes were taken" };
@@ -7911,8 +8117,8 @@ function ship(opts) {
7911
8117
  const mechanicalErrors = mechanical.findings.filter((f) => f.bucket === "mechanical" && f.severity === "error").length;
7912
8118
  const seoResult = seo({ projectRoot: root });
7913
8119
  const seoErrors = seoResult.findings.filter((f) => f.severity === "error").length;
7914
- const specsDir = join41(root, ".jig", "specs");
7915
- const surfaces = existsSync25(specsDir) ? readdirSync15(specsDir).filter((f) => f.endsWith(".spec.md") && !f.startsWith("_")).map((f) => f.replace(/\.spec\.md$/, "")).sort() : [];
8120
+ const specsDir = join42(root, ".jig", "specs");
8121
+ const surfaces = existsSync26(specsDir) ? readdirSync16(specsDir).filter((f) => f.endsWith(".spec.md") && !f.startsWith("_")).map((f) => f.replace(/\.spec\.md$/, "")).sort() : [];
7916
8122
  const pages = surfaces.map((s) => pageStatus(root, s));
7917
8123
  const owed = pages.filter((p) => p.state !== "judged" && p.state !== "in-progress" && p.state !== "superseded");
7918
8124
  const agentFiles = agentFileProblems(root);
@@ -7933,9 +8139,9 @@ function ship(opts) {
7933
8139
  }
7934
8140
 
7935
8141
  // src/check/spec-checked.ts
7936
- import { existsSync as existsSync26, readFileSync as readFileSync35 } from "fs";
8142
+ import { existsSync as existsSync27, readFileSync as readFileSync36 } from "fs";
7937
8143
  import { createHash as createHash2 } from "crypto";
7938
- import { join as join42 } from "path";
8144
+ import { join as join43 } from "path";
7939
8145
 
7940
8146
  // src/check/owner-word.ts
7941
8147
  var SAYS = {
@@ -8020,7 +8226,7 @@ function specCheckProblems(root, spec, then, owner, decisions) {
8020
8226
  const recordPath = `.jig/specs/${spec.slug}.checked.json`;
8021
8227
  let record;
8022
8228
  try {
8023
- record = JSON.parse(readFileSync35(join42(root, recordPath), "utf8"));
8229
+ record = JSON.parse(readFileSync36(join43(root, recordPath), "utf8"));
8024
8230
  } catch {
8025
8231
  problems.push(
8026
8232
  `${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.`
@@ -8049,10 +8255,10 @@ function specCheckProblems(root, spec, then, owner, decisions) {
8049
8255
  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.`);
8050
8256
  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) => {
8051
8257
  const [, file, line] = /^(.+?)(?::(\d+)(?:-\d+)?)?$/.exec(s) ?? [];
8052
- if (!file || !existsSync26(join42(root, file))) return true;
8258
+ if (!file || !existsSync27(join43(root, file))) return true;
8053
8259
  if (!line) return false;
8054
8260
  try {
8055
- return Number(line) > readFileSync35(join42(root, file), "utf8").split("\n").length;
8261
+ return Number(line) > readFileSync36(join43(root, file), "utf8").split("\n").length;
8056
8262
  } catch {
8057
8263
  return true;
8058
8264
  }
@@ -8066,10 +8272,10 @@ function specCheckProblems(root, spec, then, owner, decisions) {
8066
8272
  // src/commands/gate.ts
8067
8273
  var MAX_BLOCKS = 3;
8068
8274
  function lastJigInvocation(transcriptPath) {
8069
- if (!transcriptPath || !existsSync27(transcriptPath)) return void 0;
8275
+ if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
8070
8276
  let text;
8071
8277
  try {
8072
- text = readFileSync36(transcriptPath, "utf8");
8278
+ text = readFileSync37(transcriptPath, "utf8");
8073
8279
  } catch {
8074
8280
  return void 0;
8075
8281
  }
@@ -8094,10 +8300,10 @@ function lastJigInvocation(transcriptPath) {
8094
8300
  return found;
8095
8301
  }
8096
8302
  function ownerWords(transcriptPath) {
8097
- if (!transcriptPath || !existsSync27(transcriptPath)) return "";
8303
+ if (!transcriptPath || !existsSync28(transcriptPath)) return "";
8098
8304
  const said = [];
8099
8305
  try {
8100
- for (const line of readFileSync36(transcriptPath, "utf8").split("\n")) {
8306
+ for (const line of readFileSync37(transcriptPath, "utf8").split("\n")) {
8101
8307
  if (!line.includes('"user"')) continue;
8102
8308
  let entry;
8103
8309
  try {
@@ -8119,16 +8325,16 @@ function ownerWords(transcriptPath) {
8119
8325
  }
8120
8326
  function namedSpec(root, surface) {
8121
8327
  if (!surface) return specFor(root, void 0);
8122
- return existsSync27(join43(root, ".jig", "specs", `${surface}.spec.md`)) ? specFor(root, surface) : void 0;
8328
+ return existsSync28(join44(root, ".jig", "specs", `${surface}.spec.md`)) ? specFor(root, surface) : void 0;
8123
8329
  }
8124
8330
  function isConfirmed(body) {
8125
8331
  return /^\s*confirmed\s*:\s*true\b/im.test(body.split(/^---\s*$/m)[1] ?? "");
8126
8332
  }
8127
8333
  function ownerReplyAfter(transcriptPath, asked) {
8128
- if (!transcriptPath || !existsSync27(transcriptPath)) return void 0;
8334
+ if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
8129
8335
  const turns = [];
8130
8336
  try {
8131
- for (const line of readFileSync36(transcriptPath, "utf8").split("\n")) {
8337
+ for (const line of readFileSync37(transcriptPath, "utf8").split("\n")) {
8132
8338
  let entry;
8133
8339
  try {
8134
8340
  entry = JSON.parse(line);
@@ -8153,10 +8359,10 @@ function ownerReplyAfter(transcriptPath, asked) {
8153
8359
  return answer;
8154
8360
  }
8155
8361
  function lastAssistantText(transcriptPath) {
8156
- if (!transcriptPath || !existsSync27(transcriptPath)) return void 0;
8362
+ if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
8157
8363
  let found;
8158
8364
  try {
8159
- for (const line of readFileSync36(transcriptPath, "utf8").split("\n")) {
8365
+ for (const line of readFileSync37(transcriptPath, "utf8").split("\n")) {
8160
8366
  if (!line.includes('"assistant"')) continue;
8161
8367
  let entry;
8162
8368
  try {
@@ -8180,9 +8386,9 @@ function asksOwner(text) {
8180
8386
  }
8181
8387
  var ASKS_THE_OWNER = /* @__PURE__ */ new Set(["decide", "spec", "mockup", "tweak"]);
8182
8388
  function sessionStart(transcriptPath) {
8183
- if (!transcriptPath || !existsSync27(transcriptPath)) return void 0;
8389
+ if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
8184
8390
  try {
8185
- for (const line of readFileSync36(transcriptPath, "utf8").split("\n")) {
8391
+ for (const line of readFileSync37(transcriptPath, "utf8").split("\n")) {
8186
8392
  const m = /"timestamp"\s*:\s*"([^"]+)"/.exec(line);
8187
8393
  if (m) {
8188
8394
  const t = Date.parse(m[1]);
@@ -8197,20 +8403,20 @@ function verdictsMtime(dir) {
8197
8403
  let newest = 0;
8198
8404
  for (const name of VERDICT_FILES) {
8199
8405
  try {
8200
- newest = Math.max(newest, statSync5(join43(dir, name)).mtimeMs);
8406
+ newest = Math.max(newest, statSync5(join44(dir, name)).mtimeMs);
8201
8407
  } catch {
8202
8408
  }
8203
8409
  }
8204
8410
  return newest;
8205
8411
  }
8206
8412
  function surfacesInPlay(root, command, transcriptPath, surface) {
8207
- const critiqueDir = join43(root, ".jig", "critique");
8208
- if (!existsSync27(critiqueDir)) return [];
8209
- const all = readdirSync16(critiqueDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("_") && !d.name.startsWith(".")).map((d) => d.name);
8413
+ const critiqueDir = join44(root, ".jig", "critique");
8414
+ if (!existsSync28(critiqueDir)) return [];
8415
+ const all = readdirSync17(critiqueDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("_") && !d.name.startsWith(".")).map((d) => d.name);
8210
8416
  const start = sessionStart(transcriptPath);
8211
8417
  if (start === void 0) return all;
8212
8418
  const current = command === "critique" || command === "tweak" ? specFor(root, surface)?.slug : void 0;
8213
- return all.filter((s) => s === current || verdictsMtime(join43(critiqueDir, s)) >= start - 1e3);
8419
+ return all.filter((s) => s === current || verdictsMtime(join44(critiqueDir, s)) >= start - 1e3);
8214
8420
  }
8215
8421
  function surfacesToProbe(root, input) {
8216
8422
  const invocation = lastJigInvocation(input.transcript_path);
@@ -8219,7 +8425,7 @@ function surfacesToProbe(root, input) {
8219
8425
  function fileAtSessionStart(root, path, start) {
8220
8426
  const git = (args) => execFileSync4("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
8221
8427
  try {
8222
- if (start !== void 0 && statSync5(join43(root, path)).mtimeMs < start - 1e3) return readText(join43(root, path));
8428
+ if (start !== void 0 && statSync5(join44(root, path)).mtimeMs < start - 1e3) return readText(join44(root, path));
8223
8429
  } catch {
8224
8430
  }
8225
8431
  try {
@@ -8237,7 +8443,7 @@ function changedOutsideRecords(root, start) {
8237
8443
  const changed = git(["diff", "--name-only", base, "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean);
8238
8444
  const created = git(["ls-files", "--others", "--exclude-standard", "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean).filter((f) => {
8239
8445
  try {
8240
- return start === void 0 || statSync5(join43(root, f)).mtimeMs >= start - 1e3;
8446
+ return start === void 0 || statSync5(join44(root, f)).mtimeMs >= start - 1e3;
8241
8447
  } catch {
8242
8448
  return false;
8243
8449
  }
@@ -8253,7 +8459,7 @@ function drawingChangedAfterApproval(root, specPath, at) {
8253
8459
  const approvedIn = git(["log", "-1", "--format=%H", "-G", "^mockup[[:space:]]*:[[:space:]]*approved", "--", specPath]).trim();
8254
8460
  if (!approvedIn) return void 0;
8255
8461
  const then = lf(git(["show", `${approvedIn}:./${at}`]));
8256
- return then !== readText(join43(root, at)) ? approvedIn.slice(0, 7) : void 0;
8462
+ return then !== readText(join44(root, at)) ? approvedIn.slice(0, 7) : void 0;
8257
8463
  } catch {
8258
8464
  return void 0;
8259
8465
  }
@@ -8265,7 +8471,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8265
8471
  const found = decisionsFile(root);
8266
8472
  if (!found) problems.push("decide wrote no DECISIONS.md beside the token layer.");
8267
8473
  else {
8268
- const body = readText(join43(root, found));
8474
+ const body = readText(join44(root, found));
8269
8475
  if (!/^##\s+Unresolved\s*$/im.test(body)) {
8270
8476
  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.`");
8271
8477
  }
@@ -8275,7 +8481,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8275
8481
  if (owner) problems.push(...quotesNotFrom(body, then, owner, "the owner's words in this session").map((p) => `DECISIONS.md: ${p}`));
8276
8482
  }
8277
8483
  }
8278
- if (command === "spec" && surface && !existsSync27(join43(root, ".jig", "specs", `${surface}.spec.md`))) {
8484
+ if (command === "spec" && surface && !existsSync28(join44(root, ".jig", "specs", `${surface}.spec.md`))) {
8279
8485
  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.`);
8280
8486
  return problems;
8281
8487
  }
@@ -8298,10 +8504,10 @@ function commandProblems(root, command, surface, start, owner = "") {
8298
8504
  const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
8299
8505
  if (/^approved/i.test(mockup)) {
8300
8506
  if (!at) problems.push(`${spec.path}: \`mockup_at:\` is empty. Record where the approved drawing is.`);
8301
- else if (!/^https?:/i.test(at) && !existsSync27(join43(root, at))) problems.push(`${spec.path}: \`mockup_at: ${at}\` does not exist.`);
8507
+ else if (!/^https?:/i.test(at) && !existsSync28(join44(root, at))) problems.push(`${spec.path}: \`mockup_at: ${at}\` does not exist.`);
8302
8508
  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.`);
8303
8509
  }
8304
- if (at && /\.html?$/i.test(at) && existsSync27(join43(root, at))) problems.push(...mockupDrawingProblems(root, spec.body, at));
8510
+ if (at && /\.html?$/i.test(at) && existsSync28(join44(root, at))) problems.push(...mockupDrawingProblems(root, spec.body, at));
8305
8511
  }
8306
8512
  if (command === "tweak" && spec) problems.push(...tweakProblems(root, spec, owner, start));
8307
8513
  if ((command === "spec" || command === "make") && spec) {
@@ -8329,7 +8535,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8329
8535
  const front = spec.body.split(/^---\s*$/m)[1] ?? "";
8330
8536
  const mockup = mockupField(spec.body);
8331
8537
  const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
8332
- if (/^approved/i.test(mockup) && at && !/^https?:/i.test(at) && existsSync27(join43(root, at))) {
8538
+ if (/^approved/i.test(mockup) && at && !/^https?:/i.test(at) && existsSync28(join44(root, at))) {
8333
8539
  const changedAfter = drawingChangedAfterApproval(root, spec.path, at);
8334
8540
  if (changedAfter) {
8335
8541
  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.`);
@@ -8341,7 +8547,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8341
8547
  if (found) {
8342
8548
  let now = "";
8343
8549
  try {
8344
- now = readText(join43(root, found));
8550
+ now = readText(join44(root, found));
8345
8551
  } catch {
8346
8552
  }
8347
8553
  const then = fileAtSessionStart(root, found, start);
@@ -8355,13 +8561,13 @@ function commandProblems(root, command, surface, start, owner = "") {
8355
8561
  if (found) {
8356
8562
  let now = "";
8357
8563
  try {
8358
- now = readText(join43(root, found));
8564
+ now = readText(join44(root, found));
8359
8565
  } catch {
8360
8566
  }
8361
8567
  const then = fileAtSessionStart(root, found, start);
8362
8568
  if (now && now !== then) {
8363
8569
  problems.push(...unsourcedReasons(now, then, { newWhysOnly: true }).map((p) => `${found}: ${p}`));
8364
- const change = readTweak(join43(root, ".jig", "critique", spec.slug))?.change;
8570
+ const change = readTweak(join44(root, ".jig", "critique", spec.slug))?.change;
8365
8571
  if (change) problems.push(...quotesNotFrom(now, then, change).map((p) => `${found}: ${p}`));
8366
8572
  }
8367
8573
  }
@@ -8370,7 +8576,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8370
8576
  for (const file of modeFiles(root)) {
8371
8577
  let now = "";
8372
8578
  try {
8373
- now = readText(join43(root, file));
8579
+ now = readText(join44(root, file));
8374
8580
  } catch {
8375
8581
  continue;
8376
8582
  }
@@ -8381,8 +8587,8 @@ function commandProblems(root, command, surface, start, owner = "") {
8381
8587
  }
8382
8588
  }
8383
8589
  if (command === "critique") {
8384
- const dir = join43(root, ".jig", "critique");
8385
- const surfaces = existsSync27(dir) ? readdirSync16(dir).filter((s) => existsSync27(join43(dir, s, "screen.json")) || existsSync27(join43(dir, s, "code.json"))) : [];
8590
+ const dir = join44(root, ".jig", "critique");
8591
+ const surfaces = existsSync28(dir) ? readdirSync17(dir).filter((s) => existsSync28(join44(dir, s, "screen.json")) || existsSync28(join44(dir, s, "code.json"))) : [];
8386
8592
  for (const surface2 of surfaces) {
8387
8593
  const v = verifyVerdicts({ projectRoot: root, surface: surface2 });
8388
8594
  if (v.ok && v.decisions.state !== "ran" && v.decisions.total > 0) {
@@ -8393,7 +8599,7 @@ function commandProblems(root, command, surface, start, owner = "") {
8393
8599
  }
8394
8600
  }
8395
8601
  const own = specFor(root, surface)?.slug;
8396
- if (own && surfaces.includes(own)) problems.push(...reportOmissions(join43(dir, own), own));
8602
+ if (own && surfaces.includes(own)) problems.push(...reportOmissions(join44(dir, own), own));
8397
8603
  if (surfaces.length === 0) {
8398
8604
  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.");
8399
8605
  }
@@ -8406,7 +8612,7 @@ function drawingBeforeAsking(root, surface) {
8406
8612
  const front = spec.body.split(/^---\s*$/m)[1] ?? "";
8407
8613
  const named = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
8408
8614
  const at = named || `.jig/mockups/${spec.slug}.html`;
8409
- if (/^https?:/i.test(at) || !/\.html?$/i.test(at) || !existsSync27(join43(root, at))) return [];
8615
+ if (/^https?:/i.test(at) || !/\.html?$/i.test(at) || !existsSync28(join44(root, at))) return [];
8410
8616
  const found = mockupDrawingProblems(root, spec.body, at);
8411
8617
  return found.length ? [`Fix the drawing before you put it to the owner; they approve what the spec lists, drawn. ${found.join(" ")}`] : [];
8412
8618
  }
@@ -8414,14 +8620,14 @@ function specChecked(root, spec, start, owner) {
8414
8620
  const found = decisionsFile(root);
8415
8621
  let decisions = "";
8416
8622
  try {
8417
- if (found) decisions = readText(join43(root, found));
8623
+ if (found) decisions = readText(join44(root, found));
8418
8624
  } catch {
8419
8625
  }
8420
8626
  return specCheckProblems(root, spec, fileAtSessionStart(root, spec.path, start), owner, decisions);
8421
8627
  }
8422
8628
  function modeFiles(root) {
8423
8629
  try {
8424
- const state = JSON.parse(readFileSync36(join43(root, ".jig", "state.json"), "utf8"));
8630
+ const state = JSON.parse(readFileSync37(join44(root, ".jig", "state.json"), "utf8"));
8425
8631
  return Object.keys(state.files ?? {}).filter((f) => /(^|\/)mode\.[\w-]+\.css$/.test(f));
8426
8632
  } catch {
8427
8633
  return [];
@@ -8430,7 +8636,7 @@ function modeFiles(root) {
8430
8636
  function reportOmissions(dir, surface) {
8431
8637
  let report2;
8432
8638
  try {
8433
- report2 = readText(join43(dir, "REPORT.md")).toLowerCase();
8639
+ report2 = readText(join44(dir, "REPORT.md")).toLowerCase();
8434
8640
  } catch {
8435
8641
  return [];
8436
8642
  }
@@ -8438,7 +8644,7 @@ function reportOmissions(dir, surface) {
8438
8644
  for (const f of VERDICT_FILES) {
8439
8645
  let file;
8440
8646
  try {
8441
- file = JSON.parse(readFileSync36(join43(dir, f), "utf8"));
8647
+ file = JSON.parse(readFileSync37(join44(dir, f), "utf8"));
8442
8648
  } catch {
8443
8649
  continue;
8444
8650
  }
@@ -8459,16 +8665,16 @@ function tweakProblems(root, spec, owner = "", start) {
8459
8665
  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\`.`);
8460
8666
  else problems.push(...structureSinceApproval(root, spec, front));
8461
8667
  const surface = spec.slug;
8462
- const record = readTweak(join43(root, ".jig", "critique", surface));
8668
+ const record = readTweak(join44(root, ".jig", "critique", surface));
8463
8669
  if (!record) {
8464
8670
  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\`).`);
8465
8671
  } else {
8466
8672
  if (!record.change) problems.push(`.jig/critique/${surface}/tweak.json has no \`change\`: the owner's words for what changed.`);
8467
- else if (owner && fileAtSessionStart(root, `.jig/critique/${surface}/tweak.json`, start) !== readText(join43(root, ".jig", "critique", surface, "tweak.json")) && !quoteHeld(record.change.replace(/^["“]|["”]$/g, ""), owner)) {
8673
+ else if (owner && fileAtSessionStart(root, `.jig/critique/${surface}/tweak.json`, start) !== readText(join44(root, ".jig", "critique", surface, "tweak.json")) && !quoteHeld(record.change.replace(/^["“]|["”]$/g, ""), owner)) {
8468
8674
  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.`);
8469
8675
  }
8470
8676
  try {
8471
- const raw = JSON.parse(readFileSync36(join43(root, ".jig", "critique", surface, "tweak.json"), "utf8"));
8677
+ const raw = JSON.parse(readFileSync37(join44(root, ".jig", "critique", surface, "tweak.json"), "utf8"));
8472
8678
  const extra = Object.keys(raw).filter((k) => !["at", "change", "ids", "deferred"].includes(k));
8473
8679
  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.`);
8474
8680
  } catch {
@@ -8483,7 +8689,7 @@ function tweakProblems(root, spec, owner = "", start) {
8483
8689
  }
8484
8690
  return problems;
8485
8691
  }
8486
- const unjudged = unjudgedTweakIds(join43(root, ".jig", "critique", surface), record);
8692
+ const unjudged = unjudgedTweakIds(join44(root, ".jig", "critique", surface), record);
8487
8693
  if (unjudged.length) {
8488
8694
  problems.push(
8489
8695
  `.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.`
@@ -8519,9 +8725,9 @@ function structureSinceApproval(root, spec, front) {
8519
8725
  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\`.`);
8520
8726
  }
8521
8727
  const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
8522
- if (at && !/^https?:/i.test(at) && existsSync27(join43(root, at))) {
8728
+ if (at && !/^https?:/i.test(at) && existsSync28(join44(root, at))) {
8523
8729
  try {
8524
- if (lf(git(["show", `${baseline}:./${at}`])) !== readText(join43(root, at))) {
8730
+ if (lf(git(["show", `${baseline}:./${at}`])) !== readText(join44(root, at))) {
8525
8731
  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\`.`);
8526
8732
  }
8527
8733
  } catch {
@@ -8531,7 +8737,7 @@ function structureSinceApproval(root, spec, front) {
8531
8737
  }
8532
8738
  function readTweak(dir) {
8533
8739
  try {
8534
- const raw = JSON.parse(readFileSync36(join43(dir, "tweak.json"), "utf8"));
8740
+ const raw = JSON.parse(readFileSync37(join44(dir, "tweak.json"), "utf8"));
8535
8741
  const deferred = typeof raw.deferred === "string" || raw.deferred === true ? raw.deferred : void 0;
8536
8742
  return { at: String(raw.at ?? ""), change: String(raw.change ?? "").trim(), ids: Array.isArray(raw.ids) ? raw.ids.map(String) : [], ...deferred !== void 0 ? { deferred } : {} };
8537
8743
  } catch {
@@ -8541,16 +8747,16 @@ function readTweak(dir) {
8541
8747
  function surfacePage(projectRoot, surface) {
8542
8748
  const spec = specFor(projectRoot, surface);
8543
8749
  const front = spec?.body.split(/^---\s*$/m)[1] ?? "";
8544
- const declared = /^\s*surface\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
8545
- const candidates = [declared, `${surface}.html`, declared ? `${declared.replace(/^\//, "")}.html` : void 0].filter((c) => !!c && /\.\w+$/.test(c) === (c === declared ? /\.\w+$/.test(c) : true));
8750
+ const declared2 = /^\s*surface\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
8751
+ const candidates = [declared2, `${surface}.html`, declared2 ? `${declared2.replace(/^\//, "")}.html` : void 0].filter((c) => !!c && /\.\w+$/.test(c) === (c === declared2 ? /\.\w+$/.test(c) : true));
8546
8752
  for (const candidate of candidates) {
8547
- if (candidate && existsSync27(join43(projectRoot, candidate))) return candidate;
8753
+ if (candidate && existsSync28(join44(projectRoot, candidate))) return candidate;
8548
8754
  }
8549
8755
  return void 0;
8550
8756
  }
8551
8757
  function gate(opts) {
8552
8758
  const root = opts.projectRoot;
8553
- if (!existsSync27(join43(root, "jig.config.json")) && !existsSync27(join43(root, ".jig"))) {
8759
+ if (!existsSync28(join44(root, "jig.config.json")) && !existsSync28(join44(root, ".jig"))) {
8554
8760
  return { block: false, reason: "" };
8555
8761
  }
8556
8762
  const invocation = lastJigInvocation(opts.input.transcript_path);
@@ -8586,7 +8792,7 @@ ${result.report}`);
8586
8792
  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.`);
8587
8793
  }
8588
8794
  if (spec) problems.push(...specChecked(root, spec, start, owner).map((p) => `/jig spec: ${p}`));
8589
- if (spec && existsSync27(join43(root, ".jig", "specs", `${spec.slug}.checked.json`))) {
8795
+ if (spec && existsSync28(join44(root, ".jig", "specs", `${spec.slug}.checked.json`))) {
8590
8796
  problems.push(...[...specProblems(spec), ...newFieldProblems(spec, fileAtSessionStart(root, spec.path, start))].map((p) => `/jig spec: ${p}`));
8591
8797
  }
8592
8798
  }
@@ -8613,15 +8819,15 @@ ${result.report}`);
8613
8819
  problems.push(...verdictGuard(root, command, surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)));
8614
8820
  problems.push(...probesLeftBehind(root, surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)));
8615
8821
  const touched = ["AGENTS.md", "CLAUDE.md"].some((f) => {
8616
- const now = existsSync27(join43(root, f)) ? readText(join43(root, f)) : "";
8822
+ const now = existsSync28(join44(root, f)) ? readText(join44(root, f)) : "";
8617
8823
  return fileAtSessionStart(root, f, start) !== now;
8618
8824
  });
8619
8825
  if (touched) problems.push(...agentFileProblems(root).map((p) => `Jig's agent instructions: ${p}`));
8620
- const critiqueDir = join43(root, ".jig", "critique");
8621
- if (existsSync27(critiqueDir)) {
8826
+ const critiqueDir = join44(root, ".jig", "critique");
8827
+ if (existsSync28(critiqueDir)) {
8622
8828
  for (const surface of surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)) {
8623
- const dir = join43(critiqueDir, surface);
8624
- if (!existsSync27(join43(dir, "screen.json")) && !existsSync27(join43(dir, "code.json"))) continue;
8829
+ const dir = join44(critiqueDir, surface);
8830
+ if (!existsSync28(join44(dir, "screen.json")) && !existsSync28(join44(dir, "code.json"))) continue;
8625
8831
  const v = verifyVerdicts({ projectRoot: root, surface });
8626
8832
  if (!v.ok) {
8627
8833
  const shown = v.errors.slice(0, 6).map((e) => ` ${e}`);
@@ -8651,10 +8857,10 @@ ${problems.join("\n\n")}`
8651
8857
  }
8652
8858
  const session = opts.input.session_id ?? "unknown";
8653
8859
  const key = `${session}:${createHash3("sha256").update(problems.join("\n")).digest("hex").slice(0, 12)}`;
8654
- const stateFile = join43(root, ".jig", "gate.json");
8860
+ const stateFile = join44(root, ".jig", "gate.json");
8655
8861
  let state = {};
8656
8862
  try {
8657
- state = JSON.parse(readFileSync36(stateFile, "utf8"));
8863
+ state = JSON.parse(readFileSync37(stateFile, "utf8"));
8658
8864
  } catch {
8659
8865
  }
8660
8866
  if (problems.length === 0) {
@@ -8689,7 +8895,7 @@ function verdictDigests(dir) {
8689
8895
  for (const f of VERDICT_FILES) {
8690
8896
  let file;
8691
8897
  try {
8692
- file = JSON.parse(readFileSync36(join43(dir, f), "utf8"));
8898
+ file = JSON.parse(readFileSync37(join44(dir, f), "utf8"));
8693
8899
  } catch {
8694
8900
  continue;
8695
8901
  }
@@ -8704,7 +8910,7 @@ function writeLock(root, surface, lockPath, dir, now) {
8704
8910
  const pageFile2 = recordedPage(root, surface);
8705
8911
  let page;
8706
8912
  try {
8707
- if (pageFile2) page = { file: pageFile2, checksum: pageChecksum(readFileSync36(join43(root, pageFile2), "utf8")) };
8913
+ if (pageFile2) page = { file: pageFile2, checksum: pageChecksum(readFileSync37(join44(root, pageFile2), "utf8")) };
8708
8914
  } catch {
8709
8915
  }
8710
8916
  const tweakAt = readTweak(dir)?.at;
@@ -8717,7 +8923,7 @@ function verdictChecksum(dir) {
8717
8923
  const parts = VERDICT_FILES.map((f) => {
8718
8924
  try {
8719
8925
  return `${f}
8720
- ${readFileSync36(join43(dir, f), "utf8")}`;
8926
+ ${readFileSync37(join44(dir, f), "utf8")}`;
8721
8927
  } catch {
8722
8928
  return "";
8723
8929
  }
@@ -8725,15 +8931,15 @@ ${readFileSync36(join43(dir, f), "utf8")}`;
8725
8931
  return parts.some(Boolean) ? checksum(parts.join("\n")) : void 0;
8726
8932
  }
8727
8933
  function verdictGuard(root, command, inPlay) {
8728
- const critiqueDir = join43(root, ".jig", "critique");
8729
- if (!existsSync27(critiqueDir)) return [];
8934
+ const critiqueDir = join44(root, ".jig", "critique");
8935
+ if (!existsSync28(critiqueDir)) return [];
8730
8936
  const problems = [];
8731
- for (const surface of readdirSync16(critiqueDir)) {
8937
+ for (const surface of readdirSync17(critiqueDir)) {
8732
8938
  if (surface.startsWith("_")) continue;
8733
- const dir = join43(critiqueDir, surface);
8939
+ const dir = join44(critiqueDir, surface);
8734
8940
  const now = verdictChecksum(dir);
8735
8941
  if (!now) continue;
8736
- const lockPath = join43(dir, LOCK);
8942
+ const lockPath = join44(dir, LOCK);
8737
8943
  if ((command === "critique" || command === "ship") && (!inPlay || inPlay.includes(surface))) {
8738
8944
  writeLock(root, surface, lockPath, dir, now);
8739
8945
  problems.push(...lockLeftBehind(root, surface));
@@ -8741,7 +8947,7 @@ function verdictGuard(root, command, inPlay) {
8741
8947
  }
8742
8948
  let lock = {};
8743
8949
  try {
8744
- lock = JSON.parse(readFileSync36(lockPath, "utf8"));
8950
+ lock = JSON.parse(readFileSync37(lockPath, "utf8"));
8745
8951
  } catch {
8746
8952
  continue;
8747
8953
  }
@@ -8817,7 +9023,7 @@ function unjudgedTweakIds(dir, record) {
8817
9023
  for (const f of VERDICT_FILES) {
8818
9024
  let file;
8819
9025
  try {
8820
- file = JSON.parse(readFileSync36(join43(dir, f), "utf8"));
9026
+ file = JSON.parse(readFileSync37(join44(dir, f), "utf8"));
8821
9027
  } catch {
8822
9028
  continue;
8823
9029
  }
@@ -8829,7 +9035,7 @@ function unjudgedTweakIds(dir, record) {
8829
9035
  }
8830
9036
  function save(file, state) {
8831
9037
  try {
8832
- mkdirSync8(join43(file, ".."), { recursive: true });
9038
+ mkdirSync8(join44(file, ".."), { recursive: true });
8833
9039
  writeFileSync6(file, JSON.stringify(state), "utf8");
8834
9040
  } catch {
8835
9041
  }
@@ -8837,7 +9043,7 @@ function save(file, state) {
8837
9043
 
8838
9044
  // src/index.ts
8839
9045
  var packageRoot = getPackageRoot();
8840
- var { version } = JSON.parse(readFileSync37(join44(packageRoot, "package.json"), "utf8"));
9046
+ var { version } = JSON.parse(readFileSync38(join45(packageRoot, "package.json"), "utf8"));
8841
9047
  function warnIfUnpublishedPin() {
8842
9048
  if (isDevVersion(version)) {
8843
9049
  console.warn(
@@ -8949,12 +9155,12 @@ program.command("update").description("Update vendored Jig rules, skipping files
8949
9155
  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) => {
8950
9156
  const projectRoot = findProjectRoot(process.cwd());
8951
9157
  const slug = name.replace(/^.*\//, "").replace(/\.spec\.md$/, "");
8952
- const path = join44(projectRoot, ".jig", "specs", `${slug}.spec.md`);
8953
- if (!existsSync28(path)) {
9158
+ const path = join45(projectRoot, ".jig", "specs", `${slug}.spec.md`);
9159
+ if (!existsSync29(path)) {
8954
9160
  console.error(` \u2717 no spec at .jig/specs/${slug}.spec.md`);
8955
9161
  process.exit(1);
8956
9162
  }
8957
- console.log(specChecksum(readFileSync37(path, "utf8")));
9163
+ console.log(specChecksum(readFileSync38(path, "utf8")));
8958
9164
  });
8959
9165
  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) => {
8960
9166
  const projectRoot = findProjectRoot(process.cwd());
@@ -9088,7 +9294,7 @@ program.command("probe").description("Print the render probe. With --save, read
9088
9294
  program.command("gate").description("Run by the Claude Code Stop hook: block stopping while check or a critique fails.").action(async () => {
9089
9295
  let input = {};
9090
9296
  try {
9091
- if (!process.stdin.isTTY) input = JSON.parse(readFileSync37(0, "utf8") || "{}");
9297
+ if (!process.stdin.isTTY) input = JSON.parse(readFileSync38(0, "utf8") || "{}");
9092
9298
  } catch {
9093
9299
  }
9094
9300
  const cwd = input.cwd ?? process.cwd();
@@ -9129,7 +9335,7 @@ program.command("init").description("Set the project up to use Jig: a brand file
9129
9335
  });
9130
9336
  function resolveSurfaceMode(projectRoot) {
9131
9337
  try {
9132
- const config = JSON.parse(readFileSync37(join44(projectRoot, "jig.config.json"), "utf8"));
9338
+ const config = JSON.parse(readFileSync38(join45(projectRoot, "jig.config.json"), "utf8"));
9133
9339
  const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
9134
9340
  return modes.length === 1 ? modes[0] : void 0;
9135
9341
  } catch {