jig-ui 0.25.0 → 0.26.1
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/CHANGELOG.md +70 -8
- package/dist/index.js +631 -426
- package/package.json +1 -1
- package/rules/02-tokens.md +10 -0
- package/rules/03-patterns.md +12 -4
- package/templates/COMMAND.md.tmpl +17 -4
- package/tokens/brand.default.css +6 -2
- package/tokens/mode.operator.css +9 -0
- package/tokens/mode.product.css +7 -0
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
|
|
5
|
+
import { existsSync as existsSync29, readFileSync as readFileSync38 } from "fs";
|
|
6
6
|
import { homedir as homedir3 } from "os";
|
|
7
|
-
import { join as
|
|
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
|
|
1703
|
-
import { join as
|
|
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
|
|
2236
|
-
import { join as
|
|
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
|
|
3523
|
-
import { join as
|
|
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 =
|
|
3820
|
+
const base = join20(projectRoot, "node_modules", spec);
|
|
3536
3821
|
for (const candidate of [base, `${base}.css`]) {
|
|
3537
3822
|
try {
|
|
3538
|
-
if (
|
|
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(
|
|
3544
|
-
if (pkg.style) return
|
|
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 =
|
|
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
|
|
3590
|
-
if (!
|
|
3591
|
-
const known = (name) =>
|
|
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
|
|
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(
|
|
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);
|
|
@@ -4268,7 +4553,7 @@ var HEADLINE_SELECTOR = /\bh1\b|\bh2\b|hero|display|headline|title/i;
|
|
|
4268
4553
|
var SERIF = /font-family\s*:\s*([^;}]+)/i;
|
|
4269
4554
|
function serifFamily(value) {
|
|
4270
4555
|
const first = value.split(",")[0].trim().toLowerCase();
|
|
4271
|
-
if (/sans|mono|system-ui|ui-sans|inter\b|helvetica|arial/.test(first)) return false;
|
|
4556
|
+
if (/sans|mono|system-ui|ui-sans|-apple-system|blinkmacsystemfont|segoe|roboto|inter\b|helvetica|arial/.test(first)) return false;
|
|
4272
4557
|
return /serif|georgia|garamond|playfair|fraunces|lora|merriweather|instrument serif|dm serif|cormorant|newsreader|times|baskerville|bodoni|caslon|libre caslon|source serif|pt serif|crimson/.test(value.toLowerCase());
|
|
4273
4558
|
}
|
|
4274
4559
|
var italicSerifDisplay = {
|
|
@@ -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 =
|
|
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
|
|
4437
|
-
import { join as
|
|
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 =
|
|
4453
|
-
if (
|
|
4454
|
-
for (const file of
|
|
4455
|
-
Object.assign(jig, rootDeclarations(
|
|
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 =
|
|
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 =
|
|
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
|
|
4485
|
-
import { join as
|
|
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 =
|
|
4506
|
-
if (!
|
|
4507
|
-
let
|
|
4790
|
+
const configPath = join23(projectRoot, "jig.config.json");
|
|
4791
|
+
if (!existsSync15(configPath)) return none;
|
|
4792
|
+
let declared2;
|
|
4508
4793
|
try {
|
|
4509
|
-
|
|
4794
|
+
declared2 = JSON.parse(readFileSync19(configPath, "utf8")).exempt;
|
|
4510
4795
|
} catch {
|
|
4511
4796
|
return none;
|
|
4512
4797
|
}
|
|
4513
|
-
if (!Array.isArray(
|
|
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
|
|
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
|
|
4560
|
-
import { join as
|
|
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 =
|
|
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
|
|
4641
|
-
import { dirname as dirname6, join as
|
|
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 =
|
|
4664
|
-
if (!
|
|
4948
|
+
const path = join25(projectRoot, "jig.config.json");
|
|
4949
|
+
if (!existsSync16(path)) return [];
|
|
4665
4950
|
try {
|
|
4666
|
-
config = JSON.parse(
|
|
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) =>
|
|
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 &&
|
|
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 =
|
|
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 (!
|
|
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
|
|
4715
|
-
import { join as
|
|
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
|
|
5056
|
+
return readFileSync22(join26(projectRoot, ...rel.split("/")), "utf8");
|
|
4772
5057
|
} catch {
|
|
4773
5058
|
return null;
|
|
4774
5059
|
}
|
|
4775
5060
|
};
|
|
4776
|
-
const statePath =
|
|
4777
|
-
if (
|
|
5061
|
+
const statePath = join26(projectRoot, ".jig", "state.json");
|
|
5062
|
+
if (existsSync17(statePath)) {
|
|
4778
5063
|
try {
|
|
4779
|
-
const state = JSON.parse(
|
|
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 =
|
|
4787
|
-
if (!
|
|
4788
|
-
return
|
|
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 =
|
|
5144
|
+
var LEGACY_INDEX_REL = join27(".jig", "rules.index.json");
|
|
4860
5145
|
function resolveIndexPath(projectRoot) {
|
|
4861
|
-
const legacy =
|
|
4862
|
-
if (
|
|
4863
|
-
return
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
5000
|
-
import { dirname as dirname7, isAbsolute, join as
|
|
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
|
|
5004
|
-
import { join as
|
|
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(
|
|
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 =
|
|
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
|
|
5075
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
5374
|
-
import { basename, join as
|
|
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(
|
|
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(
|
|
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 =
|
|
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 (!
|
|
5822
|
+
if (!existsSync19(configAbsPath)) return { brand: fallbackBrand, surfaces: fallbackSurfaces };
|
|
5535
5823
|
try {
|
|
5536
|
-
const parsed = JSON.parse(
|
|
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 ?
|
|
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 (!
|
|
5549
|
-
let
|
|
5836
|
+
if (!existsSync19(configAbsPath)) return { path: null };
|
|
5837
|
+
let declared2;
|
|
5550
5838
|
try {
|
|
5551
|
-
|
|
5839
|
+
declared2 = JSON.parse(readFileSync27(configAbsPath, "utf8")).brand;
|
|
5552
5840
|
} catch {
|
|
5553
5841
|
return { path: null };
|
|
5554
5842
|
}
|
|
5555
|
-
if (typeof
|
|
5556
|
-
const rel =
|
|
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 (!
|
|
5867
|
+
if (!existsSync19(configAbsPath)) return null;
|
|
5580
5868
|
try {
|
|
5581
|
-
const parsed = JSON.parse(
|
|
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 =
|
|
5592
|
-
if (!
|
|
5593
|
-
const found =
|
|
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,
|
|
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 "./${
|
|
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 =
|
|
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
|
|
5719
|
-
const inEffect =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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 ?
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
5895
|
-
if (
|
|
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
|
|
6195
|
+
const declared2 = declaredTokenNames(
|
|
5908
6196
|
[brandAbsPath, modeAbsPath].map((f) => {
|
|
5909
6197
|
try {
|
|
5910
|
-
return
|
|
6198
|
+
return readFileSync27(f, "utf8");
|
|
5911
6199
|
} catch {
|
|
5912
6200
|
return "";
|
|
5913
6201
|
}
|
|
5914
6202
|
}).join("\n")
|
|
5915
6203
|
);
|
|
5916
|
-
const aliasable = tailwindNamespaced(
|
|
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 =
|
|
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(
|
|
5947
|
-
|
|
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 =
|
|
5967
|
-
if (
|
|
5968
|
-
for (const name of
|
|
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 =
|
|
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 =
|
|
6272
|
+
const bare2 = join31(opts.projectRoot, ...tokensRelDir, "theme.css");
|
|
5985
6273
|
for (const file of detection.cssFiles.filter((f) => !isTokenLayerFile(f))) {
|
|
5986
|
-
const abs =
|
|
6274
|
+
const abs = join31(opts.projectRoot, file);
|
|
5987
6275
|
try {
|
|
5988
|
-
const before =
|
|
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(
|
|
6298
|
+
const targetAbsDir = dirname7(join31(opts.projectRoot, wireTarget));
|
|
6011
6299
|
const barrelImport = relativeImportPath(
|
|
6012
6300
|
targetAbsDir,
|
|
6013
|
-
|
|
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 =
|
|
6306
|
+
const abs = join31(opts.projectRoot, wireTarget);
|
|
6019
6307
|
const oldDirName = currentBrand.split("/").slice(0, -1).join("/");
|
|
6020
|
-
const before =
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
|
6102
|
-
import { join as
|
|
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
|
|
6393
|
+
import { join as join32 } from "path";
|
|
6106
6394
|
function citableIds(packageRoot2) {
|
|
6107
|
-
const rulesDir =
|
|
6395
|
+
const rulesDir = join32(packageRoot2, "rules");
|
|
6108
6396
|
const ids = [
|
|
6109
|
-
...loadRules(rulesDir,
|
|
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
|
|
6117
|
-
import { join as
|
|
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
|
|
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
|
|
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;
|
|
@@ -6332,6 +6620,19 @@ var PROBE_SCRIPT = `(async () => {
|
|
|
6332
6620
|
if ((ox !== 'auto' && ox !== 'scroll') || el.scrollWidth <= el.clientWidth + 1 || !vis(el)) continue;
|
|
6333
6621
|
sidewaysBoxes.push({ box: el.tagName.toLowerCase() + (el.classList[0] ? '.' + el.classList[0] : ''), text: (el.innerText || '').trim().replace(/\\s+/g, ' ').slice(0, 40), scrollWidth: el.scrollWidth, clientWidth: el.clientWidth });
|
|
6334
6622
|
}
|
|
6623
|
+
// A table cell given its own display (flex, grid, block) while its table is
|
|
6624
|
+
// still drawn as a table stops being a cell: it no longer takes its row's
|
|
6625
|
+
// height, and its border no longer meets the row's. A team table set its
|
|
6626
|
+
// name cells to flex, to line up a name and a tag, and every name drifted
|
|
6627
|
+
// above its row. A stacked table (the table itself set to block) is left out.
|
|
6628
|
+
const brokenCells = [];
|
|
6629
|
+
for (const cell of [...document.querySelectorAll('td, th')].slice(0, 2000)) {
|
|
6630
|
+
if (brokenCells.length >= 5) break;
|
|
6631
|
+
const table = cell.closest('table');
|
|
6632
|
+
const display = getComputedStyle(cell).display;
|
|
6633
|
+
if (!table || getComputedStyle(table).display !== 'table' || display === 'table-cell' || display === 'none' || !vis(cell)) continue;
|
|
6634
|
+
brokenCells.push({ cell: cell.tagName.toLowerCase() + (cell.classList[0] ? '.' + cell.classList[0] : ''), text: (cell.innerText || '').trim().replace(/\\s+/g, ' ').slice(0, 40), display });
|
|
6635
|
+
}
|
|
6335
6636
|
return JSON.stringify({
|
|
6336
6637
|
jigProbe: ${PROBE_VERSION},
|
|
6337
6638
|
url: location.href,
|
|
@@ -6340,6 +6641,7 @@ var PROBE_SCRIPT = `(async () => {
|
|
|
6340
6641
|
clientWidth: doc.clientWidth,
|
|
6341
6642
|
sidewaysScroll: doc.scrollWidth > doc.clientWidth,
|
|
6342
6643
|
sidewaysBoxes,
|
|
6644
|
+
brokenCells,
|
|
6343
6645
|
bodyFont: getComputedStyle(document.body).fontFamily,
|
|
6344
6646
|
defaultFont: getComputedStyle(document.body).fontFamily === defaultFont,
|
|
6345
6647
|
unresolvedTokens: [...unresolved],
|
|
@@ -6370,9 +6672,9 @@ var PROBE_SCRIPT = `(async () => {
|
|
|
6370
6672
|
|
|
6371
6673
|
// src/probe/browser.ts
|
|
6372
6674
|
import { spawn } from "child_process";
|
|
6373
|
-
import { existsSync as
|
|
6675
|
+
import { existsSync as existsSync20, mkdtempSync, readdirSync as readdirSync12, rmSync as rmSync3 } from "fs";
|
|
6374
6676
|
import { homedir as homedir2, tmpdir } from "os";
|
|
6375
|
-
import { join as
|
|
6677
|
+
import { join as join33 } from "path";
|
|
6376
6678
|
var CHROME_ENV = ["JIG_CHROME", "CHROME_PATH", "PUPPETEER_EXECUTABLE_PATH", "CHROMIUM_PATH"];
|
|
6377
6679
|
var CHROME_COMMANDS = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge", "brave-browser", "chrome", "msedge"];
|
|
6378
6680
|
var CHROME_APPS = [
|
|
@@ -6384,20 +6686,20 @@ var CHROME_APPS = [
|
|
|
6384
6686
|
// Every Windows machine has Edge; Chrome is often installed per user.
|
|
6385
6687
|
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
6386
6688
|
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
6387
|
-
...process.env.LOCALAPPDATA ? [
|
|
6689
|
+
...process.env.LOCALAPPDATA ? [join33(process.env.LOCALAPPDATA, "Google", "Chrome", "Application", "chrome.exe")] : []
|
|
6388
6690
|
];
|
|
6389
6691
|
function cachedBrowsers() {
|
|
6390
6692
|
const roots = [
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6693
|
+
join33(homedir2(), ".cache", "ms-playwright"),
|
|
6694
|
+
join33(homedir2(), "Library", "Caches", "ms-playwright"),
|
|
6695
|
+
join33(homedir2(), "AppData", "Local", "ms-playwright"),
|
|
6696
|
+
join33(homedir2(), ".cache", "puppeteer", "chrome")
|
|
6395
6697
|
];
|
|
6396
6698
|
const found = [];
|
|
6397
6699
|
for (const root of roots) {
|
|
6398
6700
|
let entries;
|
|
6399
6701
|
try {
|
|
6400
|
-
entries =
|
|
6702
|
+
entries = readdirSync12(root);
|
|
6401
6703
|
} catch {
|
|
6402
6704
|
continue;
|
|
6403
6705
|
}
|
|
@@ -6411,8 +6713,8 @@ function cachedBrowsers() {
|
|
|
6411
6713
|
["chrome-win", "chrome.exe"],
|
|
6412
6714
|
["chrome-win64", "chrome.exe"]
|
|
6413
6715
|
]) {
|
|
6414
|
-
const candidate =
|
|
6415
|
-
if (
|
|
6716
|
+
const candidate = join33(root, entry, ...rel);
|
|
6717
|
+
if (existsSync20(candidate)) found.push(candidate);
|
|
6416
6718
|
}
|
|
6417
6719
|
}
|
|
6418
6720
|
}
|
|
@@ -6421,13 +6723,13 @@ function cachedBrowsers() {
|
|
|
6421
6723
|
function findChrome() {
|
|
6422
6724
|
for (const key of CHROME_ENV) {
|
|
6423
6725
|
const value = process.env[key];
|
|
6424
|
-
if (value &&
|
|
6726
|
+
if (value && existsSync20(value)) return value;
|
|
6425
6727
|
}
|
|
6426
6728
|
for (const command of CHROME_COMMANDS) {
|
|
6427
6729
|
const resolved = which(command);
|
|
6428
6730
|
if (resolved) return resolved;
|
|
6429
6731
|
}
|
|
6430
|
-
for (const app of CHROME_APPS) if (
|
|
6732
|
+
for (const app of CHROME_APPS) if (existsSync20(app)) return app;
|
|
6431
6733
|
return cachedBrowsers()[0];
|
|
6432
6734
|
}
|
|
6433
6735
|
function which(command) {
|
|
@@ -6437,8 +6739,8 @@ function which(command) {
|
|
|
6437
6739
|
for (const dir of dirs) {
|
|
6438
6740
|
if (!dir) continue;
|
|
6439
6741
|
for (const ext of exts) {
|
|
6440
|
-
const candidate =
|
|
6441
|
-
if (
|
|
6742
|
+
const candidate = join33(dir, command + ext);
|
|
6743
|
+
if (existsSync20(candidate)) return candidate;
|
|
6442
6744
|
}
|
|
6443
6745
|
}
|
|
6444
6746
|
return void 0;
|
|
@@ -6450,7 +6752,7 @@ async function runProbe(opts) {
|
|
|
6450
6752
|
"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."
|
|
6451
6753
|
);
|
|
6452
6754
|
}
|
|
6453
|
-
const profile = mkdtempSync(
|
|
6755
|
+
const profile = mkdtempSync(join33(tmpdir(), "jig-probe-"));
|
|
6454
6756
|
const child = spawn(chrome, [
|
|
6455
6757
|
"--headless=new",
|
|
6456
6758
|
"--remote-debugging-port=0",
|
|
@@ -6554,8 +6856,8 @@ async function evaluate(endpoint, url, width, height, timeoutMs, expression) {
|
|
|
6554
6856
|
|
|
6555
6857
|
// src/probe/serve.ts
|
|
6556
6858
|
import { createServer } from "http";
|
|
6557
|
-
import { existsSync as
|
|
6558
|
-
import { extname, join as
|
|
6859
|
+
import { existsSync as existsSync21, readFileSync as readFileSync28, statSync as statSync3 } from "fs";
|
|
6860
|
+
import { extname, join as join34, resolve as resolve3, sep as sep4 } from "path";
|
|
6559
6861
|
var TYPES = {
|
|
6560
6862
|
".html": "text/html; charset=utf-8",
|
|
6561
6863
|
".htm": "text/html; charset=utf-8",
|
|
@@ -6582,19 +6884,19 @@ async function serveDirectory(dir) {
|
|
|
6582
6884
|
const root = resolve3(dir);
|
|
6583
6885
|
const server = createServer((req, res) => {
|
|
6584
6886
|
let path = decodeURIComponent((req.url ?? "/").split("?")[0].split("#")[0]);
|
|
6585
|
-
let file = resolve3(
|
|
6887
|
+
let file = resolve3(join34(root, path));
|
|
6586
6888
|
if (file !== root && !file.startsWith(root + sep4)) {
|
|
6587
6889
|
res.writeHead(403).end();
|
|
6588
6890
|
return;
|
|
6589
6891
|
}
|
|
6590
|
-
if (
|
|
6591
|
-
else if (!
|
|
6592
|
-
if (!
|
|
6892
|
+
if (existsSync21(file) && statSync3(file).isDirectory()) file = join34(file, "index.html");
|
|
6893
|
+
else if (!existsSync21(file) && existsSync21(`${file}.html`)) file = `${file}.html`;
|
|
6894
|
+
if (!existsSync21(file)) {
|
|
6593
6895
|
res.writeHead(404).end();
|
|
6594
6896
|
return;
|
|
6595
6897
|
}
|
|
6596
6898
|
res.writeHead(200, { "content-type": TYPES[extname(file).toLowerCase()] ?? "application/octet-stream" });
|
|
6597
|
-
res.end(
|
|
6899
|
+
res.end(readFileSync28(file));
|
|
6598
6900
|
});
|
|
6599
6901
|
await new Promise((done) => server.listen(0, "127.0.0.1", done));
|
|
6600
6902
|
const address = server.address();
|
|
@@ -6606,8 +6908,8 @@ async function serveDirectory(dir) {
|
|
|
6606
6908
|
}
|
|
6607
6909
|
|
|
6608
6910
|
// src/probe/switches.ts
|
|
6609
|
-
import { readFileSync as
|
|
6610
|
-
import { join as
|
|
6911
|
+
import { readFileSync as readFileSync29 } from "fs";
|
|
6912
|
+
import { join as join35 } from "path";
|
|
6611
6913
|
var DECLARATION = /--breakpoint-([a-z0-9-]+)\s*:\s*(\d+(?:\.\d+)?)(px|rem)\b/g;
|
|
6612
6914
|
function declaredSwitches(projectRoot) {
|
|
6613
6915
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -6615,7 +6917,7 @@ function declaredSwitches(projectRoot) {
|
|
|
6615
6917
|
if (!/\.(css|scss|astro|vue|svelte|html)$/i.test(file)) continue;
|
|
6616
6918
|
let text;
|
|
6617
6919
|
try {
|
|
6618
|
-
text =
|
|
6920
|
+
text = readFileSync29(join35(projectRoot, file), "utf8");
|
|
6619
6921
|
} catch {
|
|
6620
6922
|
continue;
|
|
6621
6923
|
}
|
|
@@ -6650,18 +6952,18 @@ function saveProbe(opts) {
|
|
|
6650
6952
|
}
|
|
6651
6953
|
probe.pageFile = relative3(opts.projectRoot, page).split("\\").join("/");
|
|
6652
6954
|
if (opts.serveRoot) probe.serveRoot = relative3(opts.projectRoot, resolve4(opts.projectRoot, opts.serveRoot)).split("\\").join("/") || ".";
|
|
6653
|
-
probe.pageChecksum = checksum(
|
|
6955
|
+
probe.pageChecksum = checksum(readFileSync30(page, "utf8"));
|
|
6654
6956
|
probe.recordedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6655
|
-
const dir =
|
|
6957
|
+
const dir = join36(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
6656
6958
|
mkdirSync7(dir, { recursive: true });
|
|
6657
|
-
const path =
|
|
6959
|
+
const path = join36(dir, `probe-${width}.json`);
|
|
6658
6960
|
writeFileSync5(path, JSON.stringify(probe), "utf8");
|
|
6659
6961
|
return { path: `.jig/critique/${opts.surface}/probe-${width}.json`, width, page: probe.pageFile };
|
|
6660
6962
|
}
|
|
6661
6963
|
function projectFile(projectRoot, path) {
|
|
6662
6964
|
const abs = resolve4(projectRoot, path);
|
|
6663
6965
|
if (abs !== resolve4(projectRoot) && !abs.startsWith(resolve4(projectRoot) + sep5)) return void 0;
|
|
6664
|
-
return
|
|
6966
|
+
return existsSync22(abs) ? abs : void 0;
|
|
6665
6967
|
}
|
|
6666
6968
|
function pageFile(projectRoot, url) {
|
|
6667
6969
|
if (!url.startsWith("file://")) return void 0;
|
|
@@ -6673,12 +6975,12 @@ function pageFile(projectRoot, url) {
|
|
|
6673
6975
|
}
|
|
6674
6976
|
const abs = resolve4(path);
|
|
6675
6977
|
if (!abs.startsWith(resolve4(projectRoot))) return void 0;
|
|
6676
|
-
return
|
|
6978
|
+
return existsSync22(abs) ? abs : void 0;
|
|
6677
6979
|
}
|
|
6678
6980
|
var PROBE_WIDTHS = [360, 768, 1280, 1600];
|
|
6679
6981
|
async function runAndSaveProbes(opts) {
|
|
6680
6982
|
const abs = resolve4(opts.projectRoot, opts.page);
|
|
6681
|
-
if (!
|
|
6983
|
+
if (!existsSync22(abs)) throw new Error(`${opts.page} does not exist, so there is nothing to render.`);
|
|
6682
6984
|
const saved = [];
|
|
6683
6985
|
const widths = opts.widths ?? probeWidths(PROBE_WIDTHS, declaredSwitches(opts.projectRoot));
|
|
6684
6986
|
await withPageUrl(opts.projectRoot, abs, opts.serve, async (url) => {
|
|
@@ -6692,7 +6994,7 @@ async function runAndSaveProbes(opts) {
|
|
|
6692
6994
|
async function withPageUrl(projectRoot, abs, serve, use) {
|
|
6693
6995
|
if (!serve) return use(`file://${abs}`);
|
|
6694
6996
|
const root = resolve4(projectRoot, serve);
|
|
6695
|
-
if (!
|
|
6997
|
+
if (!existsSync22(root)) throw new Error(`${serve} does not exist, so there is nothing to serve.`);
|
|
6696
6998
|
if (!abs.startsWith(root + sep5)) throw new Error(`The page is not inside ${serve}, so serving ${serve} cannot show it.`);
|
|
6697
6999
|
const server = await serveDirectory(root);
|
|
6698
7000
|
try {
|
|
@@ -6703,31 +7005,31 @@ async function withPageUrl(projectRoot, abs, serve, use) {
|
|
|
6703
7005
|
}
|
|
6704
7006
|
}
|
|
6705
7007
|
function recordedPage(projectRoot, surface) {
|
|
6706
|
-
const dir =
|
|
6707
|
-
if (!
|
|
6708
|
-
for (const f of
|
|
7008
|
+
const dir = join36(projectRoot, ".jig", "critique", surface);
|
|
7009
|
+
if (!existsSync22(dir)) return void 0;
|
|
7010
|
+
for (const f of readdirSync13(dir).filter((name) => /^probe-\d+\.json$/.test(name)).sort()) {
|
|
6709
7011
|
try {
|
|
6710
|
-
const probe = JSON.parse(
|
|
6711
|
-
if (probe.pageFile &&
|
|
7012
|
+
const probe = JSON.parse(readFileSync30(join36(dir, f), "utf8"));
|
|
7013
|
+
if (probe.pageFile && existsSync22(resolve4(projectRoot, probe.pageFile))) return probe.pageFile;
|
|
6712
7014
|
} catch {
|
|
6713
7015
|
}
|
|
6714
7016
|
}
|
|
6715
7017
|
return void 0;
|
|
6716
7018
|
}
|
|
6717
7019
|
async function ensureProbes(opts) {
|
|
6718
|
-
const dir =
|
|
7020
|
+
const dir = join36(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
6719
7021
|
const abs = resolve4(opts.projectRoot, opts.page);
|
|
6720
7022
|
let current;
|
|
6721
7023
|
try {
|
|
6722
|
-
current = checksum(
|
|
7024
|
+
current = checksum(readFileSync30(abs, "utf8"));
|
|
6723
7025
|
} catch {
|
|
6724
7026
|
return { recorded: [], reason: `${opts.page} could not be read` };
|
|
6725
7027
|
}
|
|
6726
|
-
const recorded =
|
|
7028
|
+
const recorded = existsSync22(dir) ? readdirSync13(dir).map((f) => /^probe-(\d+)\.json$/.exec(f)?.[1]).filter((w) => !!w).map(Number) : [];
|
|
6727
7029
|
const widths = [.../* @__PURE__ */ new Set([...probeWidths(PROBE_WIDTHS, declaredSwitches(opts.projectRoot)), ...recorded])].sort((a, b) => a - b);
|
|
6728
7030
|
const missing = widths.filter((width) => {
|
|
6729
7031
|
try {
|
|
6730
|
-
const probe = JSON.parse(
|
|
7032
|
+
const probe = JSON.parse(readFileSync30(join36(dir, `probe-${width}.json`), "utf8"));
|
|
6731
7033
|
return probe.jigProbe !== PROBE_VERSION || probe.pageChecksum !== current;
|
|
6732
7034
|
} catch {
|
|
6733
7035
|
return true;
|
|
@@ -6738,7 +7040,7 @@ async function ensureProbes(opts) {
|
|
|
6738
7040
|
let serve;
|
|
6739
7041
|
for (const width of widths) {
|
|
6740
7042
|
try {
|
|
6741
|
-
const probe = JSON.parse(
|
|
7043
|
+
const probe = JSON.parse(readFileSync30(join36(dir, `probe-${width}.json`), "utf8"));
|
|
6742
7044
|
if (probe.serveRoot) {
|
|
6743
7045
|
serve = probe.serveRoot;
|
|
6744
7046
|
break;
|
|
@@ -6754,9 +7056,9 @@ async function ensureProbes(opts) {
|
|
|
6754
7056
|
return { recorded: missing };
|
|
6755
7057
|
}
|
|
6756
7058
|
function critiquedSurfaces(projectRoot) {
|
|
6757
|
-
const root =
|
|
6758
|
-
if (!
|
|
6759
|
-
return
|
|
7059
|
+
const root = join36(projectRoot, ".jig", "critique");
|
|
7060
|
+
if (!existsSync22(root)) return [];
|
|
7061
|
+
return readdirSync13(root).filter((surface) => !surface.startsWith("_") && (existsSync22(join36(root, surface, "screen.json")) || existsSync22(join36(root, surface, "code.json"))));
|
|
6760
7062
|
}
|
|
6761
7063
|
|
|
6762
7064
|
// src/probe/check.ts
|
|
@@ -6768,7 +7070,7 @@ function stampProblem(projectRoot, file, p) {
|
|
|
6768
7070
|
if (!page) return `${file} names a page outside this project (${p.serveRoot ? p.pageFile : p.url ?? "no url"}).`;
|
|
6769
7071
|
let current;
|
|
6770
7072
|
try {
|
|
6771
|
-
current =
|
|
7073
|
+
current = readFileSync31(page, "utf8");
|
|
6772
7074
|
} catch {
|
|
6773
7075
|
return `${file} was taken on ${p.pageFile}, which no longer exists.`;
|
|
6774
7076
|
}
|
|
@@ -6778,11 +7080,11 @@ function stampProblem(projectRoot, file, p) {
|
|
|
6778
7080
|
return void 0;
|
|
6779
7081
|
}
|
|
6780
7082
|
function readProbes(projectRoot, dir, errors) {
|
|
6781
|
-
if (!
|
|
7083
|
+
if (!existsSync23(dir)) return [];
|
|
6782
7084
|
const probes = [];
|
|
6783
|
-
for (const f of
|
|
7085
|
+
for (const f of readdirSync14(dir).filter((n) => /^probe-\d+\.json$/.test(n)).sort()) {
|
|
6784
7086
|
try {
|
|
6785
|
-
const p = JSON.parse(
|
|
7087
|
+
const p = JSON.parse(readFileSync31(join37(dir, f), "utf8"));
|
|
6786
7088
|
if (typeof p.jigProbe === "number" && p.jigProbe < PROBE_VERSION && typeof p.width === "number") {
|
|
6787
7089
|
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\`.`);
|
|
6788
7090
|
continue;
|
|
@@ -6811,13 +7113,21 @@ function probeCheck(probes, verdictOf, indexable = true, mode) {
|
|
|
6811
7113
|
const clean = (id) => ["ok", "n/a"].includes(verdictOf(id) ?? "");
|
|
6812
7114
|
const at = (p) => `probe-${p.width}.json`;
|
|
6813
7115
|
for (const p of probes) {
|
|
6814
|
-
if (p.sidewaysScroll
|
|
6815
|
-
|
|
7116
|
+
if (p.sidewaysScroll) {
|
|
7117
|
+
if (clean("D-115")) {
|
|
7118
|
+
errors.push(`D-115 is "${verdictOf("D-115")}", but ${at(p)} measured the page ${p.scrollWidth}px wide in a ${p.clientWidth}px viewport \u2014 it scrolls sideways.`);
|
|
7119
|
+
} else {
|
|
7120
|
+
failures.push(`${at(p)}: the page is ${p.scrollWidth}px wide in a ${p.clientWidth}px viewport, so the whole page scrolls sideways (D-115). Find what is wider than the screen and let it wrap, stack, or scroll inside its own box (L-01, step 6).`);
|
|
7121
|
+
}
|
|
6816
7122
|
}
|
|
6817
7123
|
if (mode === "editorial" && p.width <= 480 && p.sidewaysBoxes?.length) {
|
|
6818
7124
|
const shown = p.sidewaysBoxes.map((b) => `${b.box}${b.text ? ` ("${b.text}")` : ""}, ${b.scrollWidth}px in ${b.clientWidth}px`).join("; ");
|
|
6819
7125
|
failures.push(`${at(p)}: ${p.sidewaysBoxes.length === 1 ? "a box scrolls" : `${p.sidewaysBoxes.length} boxes scroll`} sideways on a phone: ${shown}. \`editorial\` allows no scrolling region on mobile (M-01): a code block wraps its long lines, keeping its line breaks and indents, and a wide table stacks its rows (L-01, step 6).`);
|
|
6820
7126
|
}
|
|
7127
|
+
if (p.brokenCells?.length) {
|
|
7128
|
+
const shown = p.brokenCells.map((c) => `${c.cell}${c.text ? ` ("${c.text}")` : ""} is display: ${c.display}`).join("; ");
|
|
7129
|
+
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).`);
|
|
7130
|
+
}
|
|
6821
7131
|
if (p.defaultFont) {
|
|
6822
7132
|
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.`);
|
|
6823
7133
|
}
|
|
@@ -6894,126 +7204,21 @@ function probeCheck(probes, verdictOf, indexable = true, mode) {
|
|
|
6894
7204
|
return { contradictions: errors, failures };
|
|
6895
7205
|
}
|
|
6896
7206
|
|
|
6897
|
-
// src/check/decisions.ts
|
|
6898
|
-
import { existsSync as existsSync22, readFileSync as readFileSync30 } from "fs";
|
|
6899
|
-
import { join as join36, posix as posix3 } from "path";
|
|
6900
|
-
var HEADING4 = /^(#{2,3})\s+(.+?)\s*$/gm;
|
|
6901
|
-
var NOT_A_DECISION = /^(unresolved|open questions?|undecided|contents?|index)$/i;
|
|
6902
|
-
function decisionsFile(projectRoot) {
|
|
6903
|
-
const candidates = ["jig/DECISIONS.md", "DECISIONS.md", "src/jig/DECISIONS.md", "src/styles/jig/DECISIONS.md", ".jig/DECISIONS.md"];
|
|
6904
|
-
try {
|
|
6905
|
-
const brand = JSON.parse(readFileSync30(join36(projectRoot, "jig.config.json"), "utf8")).brand;
|
|
6906
|
-
if (typeof brand === "string" && brand.includes("/")) candidates.unshift(`${posix3.dirname(brand.replace(/^\.\//, ""))}/DECISIONS.md`);
|
|
6907
|
-
} catch {
|
|
6908
|
-
}
|
|
6909
|
-
for (const candidate of candidates) {
|
|
6910
|
-
if (existsSync22(join36(projectRoot, candidate))) return candidate;
|
|
6911
|
-
}
|
|
6912
|
-
return void 0;
|
|
6913
|
-
}
|
|
6914
|
-
function decisionNames(projectRoot) {
|
|
6915
|
-
return [...decisionHeadings(projectRoot).keys()];
|
|
6916
|
-
}
|
|
6917
|
-
function decisionHeadings(projectRoot) {
|
|
6918
|
-
const headings = /* @__PURE__ */ new Map();
|
|
6919
|
-
const path = decisionsFile(projectRoot);
|
|
6920
|
-
if (!path) return headings;
|
|
6921
|
-
let body;
|
|
6922
|
-
try {
|
|
6923
|
-
body = readText(join36(projectRoot, path));
|
|
6924
|
-
} catch {
|
|
6925
|
-
return headings;
|
|
6926
|
-
}
|
|
6927
|
-
for (const match of body.matchAll(HEADING4)) {
|
|
6928
|
-
const name = match[2].replace(/[`*]/g, "").trim();
|
|
6929
|
-
if (!name || NOT_A_DECISION.test(name)) continue;
|
|
6930
|
-
if (!headings.has(name)) headings.set(name, match[0].trim());
|
|
6931
|
-
}
|
|
6932
|
-
return headings;
|
|
6933
|
-
}
|
|
6934
|
-
function decisionSections(body) {
|
|
6935
|
-
const sections = /* @__PURE__ */ new Map();
|
|
6936
|
-
const lines = body.split("\n");
|
|
6937
|
-
let name;
|
|
6938
|
-
let text = [];
|
|
6939
|
-
const close = () => {
|
|
6940
|
-
if (name && !NOT_A_DECISION.test(name) && !sections.has(name)) sections.set(name, text.join("\n").trim());
|
|
6941
|
-
};
|
|
6942
|
-
for (const line of lines) {
|
|
6943
|
-
const heading = /^(#{1,3})\s+(.+?)\s*$/.exec(line);
|
|
6944
|
-
if (heading) {
|
|
6945
|
-
close();
|
|
6946
|
-
name = heading[1].length >= 2 ? heading[2].replace(/[`*]/g, "").trim() : void 0;
|
|
6947
|
-
text = [];
|
|
6948
|
-
} else text.push(line);
|
|
6949
|
-
}
|
|
6950
|
-
close();
|
|
6951
|
-
return sections;
|
|
6952
|
-
}
|
|
6953
|
-
function changedWhys(current, before, newOnly = false) {
|
|
6954
|
-
const then = decisionSections(before);
|
|
6955
|
-
const paragraphs = (text) => text.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => /^\*\*Why\b[^*]*:\*\*/.test(p));
|
|
6956
|
-
const out = [];
|
|
6957
|
-
for (const [name, text] of decisionSections(current)) {
|
|
6958
|
-
const old = then.get(name);
|
|
6959
|
-
if (old === text) continue;
|
|
6960
|
-
const kept = newOnly && old !== void 0 ? new Set(paragraphs(old)) : /* @__PURE__ */ new Set();
|
|
6961
|
-
const whys = paragraphs(text).filter((p) => !kept.has(p));
|
|
6962
|
-
if (whys.length) out.push([name, whys]);
|
|
6963
|
-
}
|
|
6964
|
-
return out;
|
|
6965
|
-
}
|
|
6966
|
-
function unsourcedReasons(current, before, opts = {}) {
|
|
6967
|
-
const problems = [];
|
|
6968
|
-
for (const [name, whys] of changedWhys(current, before, opts.newWhysOnly)) {
|
|
6969
|
-
for (const why of whys) {
|
|
6970
|
-
const label = /^\s*\*\*(Why\b[^*]*):\*\*/.exec(why)[1];
|
|
6971
|
-
const said = why.replace(/^\s*\*\*Why\b[^*]*:\*\*/, "").trim();
|
|
6972
|
-
if (/inferred/i.test(label)) continue;
|
|
6973
|
-
if (/^not given\b/i.test(said)) continue;
|
|
6974
|
-
if (/["“][^"”]{3,}["”]/.test(said)) continue;
|
|
6975
|
-
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):**\`.`);
|
|
6976
|
-
break;
|
|
6977
|
-
}
|
|
6978
|
-
}
|
|
6979
|
-
return problems;
|
|
6980
|
-
}
|
|
6981
|
-
function quotesNotFrom(current, before, words, source = "the owner's words in tweak.json (`change`)") {
|
|
6982
|
-
const problems = [];
|
|
6983
|
-
for (const [name, whys] of changedWhys(current, before, true)) {
|
|
6984
|
-
for (const why of whys) {
|
|
6985
|
-
const label = /^\s*\*\*(Why\b[^*]*):\*\*/.exec(why)[1];
|
|
6986
|
-
if (/inferred/i.test(label)) continue;
|
|
6987
|
-
const missing = [...why.matchAll(/["“]([^"”]{3,})["”]/g)].map((m) => m[1]).filter((q) => !quoteHeld(q, words));
|
|
6988
|
-
if (missing.length) {
|
|
6989
|
-
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):**\`.`);
|
|
6990
|
-
break;
|
|
6991
|
-
}
|
|
6992
|
-
}
|
|
6993
|
-
}
|
|
6994
|
-
return problems;
|
|
6995
|
-
}
|
|
6996
|
-
var normQuote = (t) => t.toLowerCase().replace(/\\(?=["'\\])/g, "").replace(/[‘’]/g, "'").replace(/[“”]/g, '"').replace(/\s+/g, " ").trim();
|
|
6997
|
-
function quoteHeld(quote, words) {
|
|
6998
|
-
const said = normQuote(words);
|
|
6999
|
-
return quote.split(/…|\.\.\./).map((part) => normQuote(part).replace(/^[\s.,;:]+|[\s.,;:!?]+$/g, "")).filter(Boolean).every((part) => said.includes(part));
|
|
7000
|
-
}
|
|
7001
|
-
|
|
7002
7207
|
// src/check/spec-shape.ts
|
|
7003
|
-
import { existsSync as
|
|
7004
|
-
import { join as
|
|
7208
|
+
import { existsSync as existsSync24, readdirSync as readdirSync15, statSync as statSync4 } from "fs";
|
|
7209
|
+
import { join as join38 } from "path";
|
|
7005
7210
|
function newestSpec(projectRoot) {
|
|
7006
|
-
const dir =
|
|
7007
|
-
if (!
|
|
7008
|
-
const files =
|
|
7211
|
+
const dir = join38(projectRoot, ".jig", "specs");
|
|
7212
|
+
if (!existsSync24(dir)) return void 0;
|
|
7213
|
+
const files = readdirSync15(dir).filter((f) => f.endsWith(".md"));
|
|
7009
7214
|
if (files.length === 0) return void 0;
|
|
7010
|
-
const newest = files.map((f) => ({ f, at: statSync4(
|
|
7011
|
-
return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body: readText(
|
|
7215
|
+
const newest = files.map((f) => ({ f, at: statSync4(join38(dir, f)).mtimeMs })).sort((a, b) => b.at - a.at)[0].f;
|
|
7216
|
+
return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body: readText(join38(dir, newest)) };
|
|
7012
7217
|
}
|
|
7013
7218
|
function specFor(projectRoot, surface) {
|
|
7014
7219
|
if (surface) {
|
|
7015
|
-
const path =
|
|
7016
|
-
if (
|
|
7220
|
+
const path = join38(projectRoot, ".jig", "specs", `${surface}.spec.md`);
|
|
7221
|
+
if (existsSync24(path)) return { path: `.jig/specs/${surface}.spec.md`, slug: surface, body: readText(path) };
|
|
7017
7222
|
}
|
|
7018
7223
|
return newestSpec(projectRoot);
|
|
7019
7224
|
}
|
|
@@ -7168,29 +7373,29 @@ function reasonProblems(file, judged, errors) {
|
|
|
7168
7373
|
}
|
|
7169
7374
|
}
|
|
7170
7375
|
function readJson(path, errors) {
|
|
7171
|
-
if (!
|
|
7376
|
+
if (!existsSync25(path)) return null;
|
|
7172
7377
|
try {
|
|
7173
|
-
return JSON.parse(
|
|
7378
|
+
return JSON.parse(readFileSync33(path, "utf8"));
|
|
7174
7379
|
} catch (e) {
|
|
7175
7380
|
errors.push(`${path}: not valid JSON (${e.message})`);
|
|
7176
7381
|
return {};
|
|
7177
7382
|
}
|
|
7178
7383
|
}
|
|
7179
7384
|
function specIndexable(projectRoot, surface) {
|
|
7180
|
-
const path =
|
|
7385
|
+
const path = join39(projectRoot, ".jig", "specs", `${surface}.spec.md`);
|
|
7181
7386
|
let front = "";
|
|
7182
7387
|
try {
|
|
7183
7388
|
front = readText(path).split(/^---\s*$/m)[1] ?? "";
|
|
7184
7389
|
} catch {
|
|
7185
7390
|
}
|
|
7186
|
-
const
|
|
7187
|
-
if (
|
|
7391
|
+
const declared2 = specIndexableField(front);
|
|
7392
|
+
if (declared2 === true || declared2 === false) return declared2;
|
|
7188
7393
|
const mode = /^\s*mode\s*:\s*(\w+)/im.exec(front)?.[1]?.toLowerCase();
|
|
7189
7394
|
return mode !== "product" && mode !== "operator";
|
|
7190
7395
|
}
|
|
7191
7396
|
function specNeedsNav(projectRoot, surface) {
|
|
7192
|
-
const path =
|
|
7193
|
-
if (!
|
|
7397
|
+
const path = join39(projectRoot, ".jig", "specs", `${surface}.spec.md`);
|
|
7398
|
+
if (!existsSync25(path)) return false;
|
|
7194
7399
|
const front = readText(path).split(/^---\s*$/m)[1] ?? "";
|
|
7195
7400
|
const navField = [...front.matchAll(/^\s*nav:\s*(.+)$/gim)].some((m) => !/^\s*(none|n\/a|-)\b/i.test(m[1]));
|
|
7196
7401
|
const navRegion = /^\s*-\s*(nav|navigation)\s*:/im.test(front);
|
|
@@ -7268,7 +7473,7 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
|
|
|
7268
7473
|
function checkDecisions(projectRoot, dir, errors) {
|
|
7269
7474
|
const required = decisionNames(projectRoot);
|
|
7270
7475
|
if (required.length === 0) return { state: "ran", judged: 0, total: 0, findings: 0 };
|
|
7271
|
-
const file = readJson(
|
|
7476
|
+
const file = readJson(join39(dir, "decisions.json"), errors);
|
|
7272
7477
|
if (!file) {
|
|
7273
7478
|
errors.push(
|
|
7274
7479
|
`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.`
|
|
@@ -7323,7 +7528,7 @@ function decisionsSince(projectRoot, dir, unjudged) {
|
|
|
7323
7528
|
if (!path) return [];
|
|
7324
7529
|
const git = (args) => execFileSync3("git", args, { cwd: projectRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
7325
7530
|
try {
|
|
7326
|
-
const verdictFile = relative4(projectRoot,
|
|
7531
|
+
const verdictFile = relative4(projectRoot, join39(dir, "decisions.json")).split(sep6).join("/");
|
|
7327
7532
|
if (git(["status", "--porcelain", "--", verdictFile])) return [];
|
|
7328
7533
|
const judgedIn = git(["log", "-1", "--format=%H", "--", verdictFile]);
|
|
7329
7534
|
if (!judgedIn) return [];
|
|
@@ -7347,7 +7552,7 @@ function decisionsSince(projectRoot, dir, unjudged) {
|
|
|
7347
7552
|
function verifyVerdicts(opts) {
|
|
7348
7553
|
const root = opts.packageRoot ?? assetRoot();
|
|
7349
7554
|
const errors = [];
|
|
7350
|
-
const index = JSON.parse(
|
|
7555
|
+
const index = JSON.parse(readFileSync33(join39(root, "rules.index.json"), "utf8"));
|
|
7351
7556
|
const judgment = index.filter((r) => r.bucket === "judgment" || r.bucket === "hybrid" && r.pass !== void 0);
|
|
7352
7557
|
const screenIds = judgment.filter((r) => r.pass === "screen").map((r) => r.id);
|
|
7353
7558
|
const codeIds = judgment.filter((r) => r.pass === "code").map((r) => r.id);
|
|
@@ -7356,9 +7561,9 @@ function verifyVerdicts(opts) {
|
|
|
7356
7561
|
...judgment.map((r) => [r.id, r.pass ?? ""])
|
|
7357
7562
|
]);
|
|
7358
7563
|
const specIds = new Set(citableIds(root).filter((id) => /^[PMLRT]-\d+$/.test(id)));
|
|
7359
|
-
const dir =
|
|
7360
|
-
const screenFile = readJson(
|
|
7361
|
-
const codeFile = readJson(
|
|
7564
|
+
const dir = join39(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
7565
|
+
const screenFile = readJson(join39(dir, "screen.json"), errors);
|
|
7566
|
+
const codeFile = readJson(join39(dir, "code.json"), errors);
|
|
7362
7567
|
const screenExtraRequired = specNeedsNav(opts.projectRoot, opts.surface) ? ["P-14"] : [];
|
|
7363
7568
|
const decisionList = decisionNames(opts.projectRoot);
|
|
7364
7569
|
const screen = checkArm("screen", screenFile, screenIds, passOf, specIds, screenExtraRequired, errors, decisionList);
|
|
@@ -7366,7 +7571,7 @@ function verifyVerdicts(opts) {
|
|
|
7366
7571
|
let rendered = false;
|
|
7367
7572
|
if (screenFile && screenFile.rendered === true) {
|
|
7368
7573
|
const artefacts = Array.isArray(screenFile.artefacts) ? screenFile.artefacts.filter((a) => typeof a === "string") : [];
|
|
7369
|
-
const absent = artefacts.filter((a) => !
|
|
7574
|
+
const absent = artefacts.filter((a) => !existsSync25(resolve5(opts.projectRoot, a)));
|
|
7370
7575
|
if (artefacts.length === 0) errors.push("screen.json says rendered: true but lists no artefacts. A render leaves a screenshot.");
|
|
7371
7576
|
else if (absent.length) errors.push(`screen.json says rendered: true, but these artefacts do not exist: ${absent.join(", ")}.`);
|
|
7372
7577
|
else rendered = true;
|
|
@@ -7388,7 +7593,7 @@ function verifyVerdicts(opts) {
|
|
|
7388
7593
|
const measured = [];
|
|
7389
7594
|
let specFront = "";
|
|
7390
7595
|
try {
|
|
7391
|
-
specFront = readText(
|
|
7596
|
+
specFront = readText(join39(opts.projectRoot, ".jig", "specs", `${opts.surface}.spec.md`)).split(/^---\s*$/m)[1] ?? "";
|
|
7392
7597
|
} catch {
|
|
7393
7598
|
}
|
|
7394
7599
|
if (specIndexableField(specFront) === "unreadable") {
|
|
@@ -7433,7 +7638,7 @@ function previousFindings(projectRoot, dir) {
|
|
|
7433
7638
|
const out = /* @__PURE__ */ new Map();
|
|
7434
7639
|
let text;
|
|
7435
7640
|
try {
|
|
7436
|
-
text = at ? lf(git(["show", `${at}:./${rel}/${f}`])) : readText(
|
|
7641
|
+
text = at ? lf(git(["show", `${at}:./${rel}/${f}`])) : readText(join39(dir, f));
|
|
7437
7642
|
} catch {
|
|
7438
7643
|
return out;
|
|
7439
7644
|
}
|
|
@@ -7515,17 +7720,17 @@ function sourceChanges(git, from, to) {
|
|
|
7515
7720
|
}
|
|
7516
7721
|
|
|
7517
7722
|
// src/commands/gate.ts
|
|
7518
|
-
import { existsSync as
|
|
7723
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync8, readdirSync as readdirSync17, readFileSync as readFileSync37, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
7519
7724
|
import { createHash as createHash3 } from "crypto";
|
|
7520
|
-
import { join as
|
|
7725
|
+
import { join as join44 } from "path";
|
|
7521
7726
|
|
|
7522
7727
|
// src/check/mockup-drawing.ts
|
|
7523
|
-
import { join as
|
|
7728
|
+
import { join as join40 } from "path";
|
|
7524
7729
|
var FRAMES = [["phone", 360], ["tablet", 768], ["desktop", 1280], ["wide", 1600]];
|
|
7525
7730
|
function mockupDrawingProblems(root, specBody, at) {
|
|
7526
7731
|
let html;
|
|
7527
7732
|
try {
|
|
7528
|
-
html = readText(
|
|
7733
|
+
html = readText(join40(root, at));
|
|
7529
7734
|
} catch {
|
|
7530
7735
|
return [];
|
|
7531
7736
|
}
|
|
@@ -7586,7 +7791,7 @@ function frameRegionProblems(frames, specBody, at) {
|
|
|
7586
7791
|
function approvedDrawingProblems(root, specBody, at) {
|
|
7587
7792
|
let html;
|
|
7588
7793
|
try {
|
|
7589
|
-
html = readText(
|
|
7794
|
+
html = readText(join40(root, at));
|
|
7590
7795
|
} catch {
|
|
7591
7796
|
return [];
|
|
7592
7797
|
}
|
|
@@ -7692,12 +7897,12 @@ function sizeBlock2(sizes, size) {
|
|
|
7692
7897
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
7693
7898
|
|
|
7694
7899
|
// src/commands/ship.ts
|
|
7695
|
-
import { existsSync as
|
|
7696
|
-
import { join as
|
|
7900
|
+
import { existsSync as existsSync26, readdirSync as readdirSync16, readFileSync as readFileSync35 } from "fs";
|
|
7901
|
+
import { join as join42 } from "path";
|
|
7697
7902
|
|
|
7698
7903
|
// src/commands/seo.ts
|
|
7699
|
-
import { readFileSync as
|
|
7700
|
-
import { join as
|
|
7904
|
+
import { readFileSync as readFileSync34 } from "fs";
|
|
7905
|
+
import { join as join41 } from "path";
|
|
7701
7906
|
var SITEMAP_FILE = /(^|\/)sitemap(-index)?\.(xml|ts|js|mjs|tsx|jsx|rb|php|py)$/i;
|
|
7702
7907
|
var ROBOTS_FILE = /(^|\/)robots\.(txt|ts|js|mjs|tsx|jsx)$/i;
|
|
7703
7908
|
var ROUTE_IN_SITEMAP = /(?:<loc>\s*([^<\s]+)\s*<\/loc>)|(?:url\s*:\s*["'`]([^"'`]+)["'`])/gi;
|
|
@@ -7738,7 +7943,7 @@ function seo(opts) {
|
|
|
7738
7943
|
for (const file of files) {
|
|
7739
7944
|
let raw;
|
|
7740
7945
|
try {
|
|
7741
|
-
raw =
|
|
7946
|
+
raw = readFileSync34(join41(root, file), "utf8");
|
|
7742
7947
|
} catch {
|
|
7743
7948
|
continue;
|
|
7744
7949
|
}
|
|
@@ -7803,7 +8008,7 @@ function critiqueAtShip(projectRoot, specBody) {
|
|
|
7803
8008
|
const own = specCritique(specBody);
|
|
7804
8009
|
if (own) return own === "at-ship";
|
|
7805
8010
|
try {
|
|
7806
|
-
return JSON.parse(
|
|
8011
|
+
return JSON.parse(readFileSync35(join42(projectRoot, "jig.config.json"), "utf8")).critique === "at-ship";
|
|
7807
8012
|
} catch {
|
|
7808
8013
|
return false;
|
|
7809
8014
|
}
|
|
@@ -7811,14 +8016,14 @@ function critiqueAtShip(projectRoot, specBody) {
|
|
|
7811
8016
|
function tweakDeferred(dir) {
|
|
7812
8017
|
let t;
|
|
7813
8018
|
try {
|
|
7814
|
-
t = JSON.parse(
|
|
8019
|
+
t = JSON.parse(readFileSync35(join42(dir, "tweak.json"), "utf8"));
|
|
7815
8020
|
} catch {
|
|
7816
8021
|
return false;
|
|
7817
8022
|
}
|
|
7818
8023
|
if (t.deferred === void 0 || t.deferred === false) return false;
|
|
7819
8024
|
let judged;
|
|
7820
8025
|
try {
|
|
7821
|
-
judged = JSON.parse(
|
|
8026
|
+
judged = JSON.parse(readFileSync35(join42(dir, "verdicts.lock"), "utf8")).tweak;
|
|
7822
8027
|
} catch {
|
|
7823
8028
|
}
|
|
7824
8029
|
return judged !== t.at;
|
|
@@ -7829,14 +8034,14 @@ function pageChecksum(html) {
|
|
|
7829
8034
|
function changedSinceLock(projectRoot, dir) {
|
|
7830
8035
|
let lock;
|
|
7831
8036
|
try {
|
|
7832
|
-
lock = JSON.parse(
|
|
8037
|
+
lock = JSON.parse(readFileSync35(join42(dir, "verdicts.lock"), "utf8"));
|
|
7833
8038
|
} catch {
|
|
7834
8039
|
return void 0;
|
|
7835
8040
|
}
|
|
7836
8041
|
const file = lock.page?.file;
|
|
7837
8042
|
if (!file || !lock.page?.checksum) return void 0;
|
|
7838
8043
|
try {
|
|
7839
|
-
const html =
|
|
8044
|
+
const html = readFileSync35(join42(projectRoot, file), "utf8");
|
|
7840
8045
|
const seen = [pageChecksum(html), checksum(html)];
|
|
7841
8046
|
return seen.includes(lock.page.checksum) ? void 0 : file;
|
|
7842
8047
|
} catch {
|
|
@@ -7848,7 +8053,7 @@ function supersededBy(front) {
|
|
|
7848
8053
|
return named ? named : void 0;
|
|
7849
8054
|
}
|
|
7850
8055
|
function pageStatus(projectRoot, surface) {
|
|
7851
|
-
const spec = readText(
|
|
8056
|
+
const spec = readText(join42(projectRoot, ".jig", "specs", `${surface}.spec.md`));
|
|
7852
8057
|
const front = spec.split(/^---\s*$/m)[1] ?? "";
|
|
7853
8058
|
if (!/^\s*confirmed\s*:\s*true\b/im.test(front)) {
|
|
7854
8059
|
return { surface, state: "in-progress", detail: "its spec is not confirmed, so nothing of it is built to ship" };
|
|
@@ -7856,10 +8061,10 @@ function pageStatus(projectRoot, surface) {
|
|
|
7856
8061
|
const successor = supersededBy(front);
|
|
7857
8062
|
if (successor !== void 0) {
|
|
7858
8063
|
const file = successor.replace(/(\.spec\.md)?$/, ".spec.md");
|
|
7859
|
-
return
|
|
8064
|
+
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` };
|
|
7860
8065
|
}
|
|
7861
|
-
const dir =
|
|
7862
|
-
if (!
|
|
8066
|
+
const dir = join42(projectRoot, ".jig", "critique", surface);
|
|
8067
|
+
if (!existsSync26(join42(dir, "screen.json")) && !existsSync26(join42(dir, "code.json"))) {
|
|
7863
8068
|
return { surface, state: "never", detail: "never critiqued" };
|
|
7864
8069
|
}
|
|
7865
8070
|
if (tweakDeferred(dir)) return { surface, state: "deferred", detail: "a tweak deferred its re-judge" };
|
|
@@ -7869,7 +8074,7 @@ function pageStatus(projectRoot, surface) {
|
|
|
7869
8074
|
const stale = v.errors.find((e) => /was taken (on|by) an older/.test(e));
|
|
7870
8075
|
let judgedPage = false;
|
|
7871
8076
|
try {
|
|
7872
|
-
judgedPage = !!JSON.parse(
|
|
8077
|
+
judgedPage = !!JSON.parse(readFileSync35(join42(dir, "verdicts.lock"), "utf8")).page?.checksum;
|
|
7873
8078
|
} catch {
|
|
7874
8079
|
}
|
|
7875
8080
|
if (stale && !judgedPage) return { surface, state: "changed", detail: "the page changed after its probes were taken" };
|
|
@@ -7889,8 +8094,8 @@ function ship(opts) {
|
|
|
7889
8094
|
const mechanicalErrors = mechanical.findings.filter((f) => f.bucket === "mechanical" && f.severity === "error").length;
|
|
7890
8095
|
const seoResult = seo({ projectRoot: root });
|
|
7891
8096
|
const seoErrors = seoResult.findings.filter((f) => f.severity === "error").length;
|
|
7892
|
-
const specsDir =
|
|
7893
|
-
const surfaces =
|
|
8097
|
+
const specsDir = join42(root, ".jig", "specs");
|
|
8098
|
+
const surfaces = existsSync26(specsDir) ? readdirSync16(specsDir).filter((f) => f.endsWith(".spec.md") && !f.startsWith("_")).map((f) => f.replace(/\.spec\.md$/, "")).sort() : [];
|
|
7894
8099
|
const pages = surfaces.map((s) => pageStatus(root, s));
|
|
7895
8100
|
const owed = pages.filter((p) => p.state !== "judged" && p.state !== "in-progress" && p.state !== "superseded");
|
|
7896
8101
|
const agentFiles = agentFileProblems(root);
|
|
@@ -7911,9 +8116,9 @@ function ship(opts) {
|
|
|
7911
8116
|
}
|
|
7912
8117
|
|
|
7913
8118
|
// src/check/spec-checked.ts
|
|
7914
|
-
import { existsSync as
|
|
8119
|
+
import { existsSync as existsSync27, readFileSync as readFileSync36 } from "fs";
|
|
7915
8120
|
import { createHash as createHash2 } from "crypto";
|
|
7916
|
-
import { join as
|
|
8121
|
+
import { join as join43 } from "path";
|
|
7917
8122
|
|
|
7918
8123
|
// src/check/owner-word.ts
|
|
7919
8124
|
var SAYS = {
|
|
@@ -7998,7 +8203,7 @@ function specCheckProblems(root, spec, then, owner, decisions) {
|
|
|
7998
8203
|
const recordPath = `.jig/specs/${spec.slug}.checked.json`;
|
|
7999
8204
|
let record;
|
|
8000
8205
|
try {
|
|
8001
|
-
record = JSON.parse(
|
|
8206
|
+
record = JSON.parse(readFileSync36(join43(root, recordPath), "utf8"));
|
|
8002
8207
|
} catch {
|
|
8003
8208
|
problems.push(
|
|
8004
8209
|
`${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.`
|
|
@@ -8027,10 +8232,10 @@ function specCheckProblems(root, spec, then, owner, decisions) {
|
|
|
8027
8232
|
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.`);
|
|
8028
8233
|
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) => {
|
|
8029
8234
|
const [, file, line] = /^(.+?)(?::(\d+)(?:-\d+)?)?$/.exec(s) ?? [];
|
|
8030
|
-
if (!file || !
|
|
8235
|
+
if (!file || !existsSync27(join43(root, file))) return true;
|
|
8031
8236
|
if (!line) return false;
|
|
8032
8237
|
try {
|
|
8033
|
-
return Number(line) >
|
|
8238
|
+
return Number(line) > readFileSync36(join43(root, file), "utf8").split("\n").length;
|
|
8034
8239
|
} catch {
|
|
8035
8240
|
return true;
|
|
8036
8241
|
}
|
|
@@ -8044,10 +8249,10 @@ function specCheckProblems(root, spec, then, owner, decisions) {
|
|
|
8044
8249
|
// src/commands/gate.ts
|
|
8045
8250
|
var MAX_BLOCKS = 3;
|
|
8046
8251
|
function lastJigInvocation(transcriptPath) {
|
|
8047
|
-
if (!transcriptPath || !
|
|
8252
|
+
if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
|
|
8048
8253
|
let text;
|
|
8049
8254
|
try {
|
|
8050
|
-
text =
|
|
8255
|
+
text = readFileSync37(transcriptPath, "utf8");
|
|
8051
8256
|
} catch {
|
|
8052
8257
|
return void 0;
|
|
8053
8258
|
}
|
|
@@ -8072,10 +8277,10 @@ function lastJigInvocation(transcriptPath) {
|
|
|
8072
8277
|
return found;
|
|
8073
8278
|
}
|
|
8074
8279
|
function ownerWords(transcriptPath) {
|
|
8075
|
-
if (!transcriptPath || !
|
|
8280
|
+
if (!transcriptPath || !existsSync28(transcriptPath)) return "";
|
|
8076
8281
|
const said = [];
|
|
8077
8282
|
try {
|
|
8078
|
-
for (const line of
|
|
8283
|
+
for (const line of readFileSync37(transcriptPath, "utf8").split("\n")) {
|
|
8079
8284
|
if (!line.includes('"user"')) continue;
|
|
8080
8285
|
let entry;
|
|
8081
8286
|
try {
|
|
@@ -8097,16 +8302,16 @@ function ownerWords(transcriptPath) {
|
|
|
8097
8302
|
}
|
|
8098
8303
|
function namedSpec(root, surface) {
|
|
8099
8304
|
if (!surface) return specFor(root, void 0);
|
|
8100
|
-
return
|
|
8305
|
+
return existsSync28(join44(root, ".jig", "specs", `${surface}.spec.md`)) ? specFor(root, surface) : void 0;
|
|
8101
8306
|
}
|
|
8102
8307
|
function isConfirmed(body) {
|
|
8103
8308
|
return /^\s*confirmed\s*:\s*true\b/im.test(body.split(/^---\s*$/m)[1] ?? "");
|
|
8104
8309
|
}
|
|
8105
8310
|
function ownerReplyAfter(transcriptPath, asked) {
|
|
8106
|
-
if (!transcriptPath || !
|
|
8311
|
+
if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
|
|
8107
8312
|
const turns = [];
|
|
8108
8313
|
try {
|
|
8109
|
-
for (const line of
|
|
8314
|
+
for (const line of readFileSync37(transcriptPath, "utf8").split("\n")) {
|
|
8110
8315
|
let entry;
|
|
8111
8316
|
try {
|
|
8112
8317
|
entry = JSON.parse(line);
|
|
@@ -8131,10 +8336,10 @@ function ownerReplyAfter(transcriptPath, asked) {
|
|
|
8131
8336
|
return answer;
|
|
8132
8337
|
}
|
|
8133
8338
|
function lastAssistantText(transcriptPath) {
|
|
8134
|
-
if (!transcriptPath || !
|
|
8339
|
+
if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
|
|
8135
8340
|
let found;
|
|
8136
8341
|
try {
|
|
8137
|
-
for (const line of
|
|
8342
|
+
for (const line of readFileSync37(transcriptPath, "utf8").split("\n")) {
|
|
8138
8343
|
if (!line.includes('"assistant"')) continue;
|
|
8139
8344
|
let entry;
|
|
8140
8345
|
try {
|
|
@@ -8158,9 +8363,9 @@ function asksOwner(text) {
|
|
|
8158
8363
|
}
|
|
8159
8364
|
var ASKS_THE_OWNER = /* @__PURE__ */ new Set(["decide", "spec", "mockup", "tweak"]);
|
|
8160
8365
|
function sessionStart(transcriptPath) {
|
|
8161
|
-
if (!transcriptPath || !
|
|
8366
|
+
if (!transcriptPath || !existsSync28(transcriptPath)) return void 0;
|
|
8162
8367
|
try {
|
|
8163
|
-
for (const line of
|
|
8368
|
+
for (const line of readFileSync37(transcriptPath, "utf8").split("\n")) {
|
|
8164
8369
|
const m = /"timestamp"\s*:\s*"([^"]+)"/.exec(line);
|
|
8165
8370
|
if (m) {
|
|
8166
8371
|
const t = Date.parse(m[1]);
|
|
@@ -8175,20 +8380,20 @@ function verdictsMtime(dir) {
|
|
|
8175
8380
|
let newest = 0;
|
|
8176
8381
|
for (const name of VERDICT_FILES) {
|
|
8177
8382
|
try {
|
|
8178
|
-
newest = Math.max(newest, statSync5(
|
|
8383
|
+
newest = Math.max(newest, statSync5(join44(dir, name)).mtimeMs);
|
|
8179
8384
|
} catch {
|
|
8180
8385
|
}
|
|
8181
8386
|
}
|
|
8182
8387
|
return newest;
|
|
8183
8388
|
}
|
|
8184
8389
|
function surfacesInPlay(root, command, transcriptPath, surface) {
|
|
8185
|
-
const critiqueDir =
|
|
8186
|
-
if (!
|
|
8187
|
-
const all =
|
|
8390
|
+
const critiqueDir = join44(root, ".jig", "critique");
|
|
8391
|
+
if (!existsSync28(critiqueDir)) return [];
|
|
8392
|
+
const all = readdirSync17(critiqueDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("_") && !d.name.startsWith(".")).map((d) => d.name);
|
|
8188
8393
|
const start = sessionStart(transcriptPath);
|
|
8189
8394
|
if (start === void 0) return all;
|
|
8190
8395
|
const current = command === "critique" || command === "tweak" ? specFor(root, surface)?.slug : void 0;
|
|
8191
|
-
return all.filter((s) => s === current || verdictsMtime(
|
|
8396
|
+
return all.filter((s) => s === current || verdictsMtime(join44(critiqueDir, s)) >= start - 1e3);
|
|
8192
8397
|
}
|
|
8193
8398
|
function surfacesToProbe(root, input) {
|
|
8194
8399
|
const invocation = lastJigInvocation(input.transcript_path);
|
|
@@ -8197,7 +8402,7 @@ function surfacesToProbe(root, input) {
|
|
|
8197
8402
|
function fileAtSessionStart(root, path, start) {
|
|
8198
8403
|
const git = (args) => execFileSync4("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
8199
8404
|
try {
|
|
8200
|
-
if (start !== void 0 && statSync5(
|
|
8405
|
+
if (start !== void 0 && statSync5(join44(root, path)).mtimeMs < start - 1e3) return readText(join44(root, path));
|
|
8201
8406
|
} catch {
|
|
8202
8407
|
}
|
|
8203
8408
|
try {
|
|
@@ -8215,7 +8420,7 @@ function changedOutsideRecords(root, start) {
|
|
|
8215
8420
|
const changed = git(["diff", "--name-only", base, "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean);
|
|
8216
8421
|
const created = git(["ls-files", "--others", "--exclude-standard", "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean).filter((f) => {
|
|
8217
8422
|
try {
|
|
8218
|
-
return start === void 0 || statSync5(
|
|
8423
|
+
return start === void 0 || statSync5(join44(root, f)).mtimeMs >= start - 1e3;
|
|
8219
8424
|
} catch {
|
|
8220
8425
|
return false;
|
|
8221
8426
|
}
|
|
@@ -8231,7 +8436,7 @@ function drawingChangedAfterApproval(root, specPath, at) {
|
|
|
8231
8436
|
const approvedIn = git(["log", "-1", "--format=%H", "-G", "^mockup[[:space:]]*:[[:space:]]*approved", "--", specPath]).trim();
|
|
8232
8437
|
if (!approvedIn) return void 0;
|
|
8233
8438
|
const then = lf(git(["show", `${approvedIn}:./${at}`]));
|
|
8234
|
-
return then !== readText(
|
|
8439
|
+
return then !== readText(join44(root, at)) ? approvedIn.slice(0, 7) : void 0;
|
|
8235
8440
|
} catch {
|
|
8236
8441
|
return void 0;
|
|
8237
8442
|
}
|
|
@@ -8243,7 +8448,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8243
8448
|
const found = decisionsFile(root);
|
|
8244
8449
|
if (!found) problems.push("decide wrote no DECISIONS.md beside the token layer.");
|
|
8245
8450
|
else {
|
|
8246
|
-
const body = readText(
|
|
8451
|
+
const body = readText(join44(root, found));
|
|
8247
8452
|
if (!/^##\s+Unresolved\s*$/im.test(body)) {
|
|
8248
8453
|
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.`");
|
|
8249
8454
|
}
|
|
@@ -8253,7 +8458,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8253
8458
|
if (owner) problems.push(...quotesNotFrom(body, then, owner, "the owner's words in this session").map((p) => `DECISIONS.md: ${p}`));
|
|
8254
8459
|
}
|
|
8255
8460
|
}
|
|
8256
|
-
if (command === "spec" && surface && !
|
|
8461
|
+
if (command === "spec" && surface && !existsSync28(join44(root, ".jig", "specs", `${surface}.spec.md`))) {
|
|
8257
8462
|
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.`);
|
|
8258
8463
|
return problems;
|
|
8259
8464
|
}
|
|
@@ -8276,10 +8481,10 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8276
8481
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
8277
8482
|
if (/^approved/i.test(mockup)) {
|
|
8278
8483
|
if (!at) problems.push(`${spec.path}: \`mockup_at:\` is empty. Record where the approved drawing is.`);
|
|
8279
|
-
else if (!/^https?:/i.test(at) && !
|
|
8484
|
+
else if (!/^https?:/i.test(at) && !existsSync28(join44(root, at))) problems.push(`${spec.path}: \`mockup_at: ${at}\` does not exist.`);
|
|
8280
8485
|
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.`);
|
|
8281
8486
|
}
|
|
8282
|
-
if (at && /\.html?$/i.test(at) &&
|
|
8487
|
+
if (at && /\.html?$/i.test(at) && existsSync28(join44(root, at))) problems.push(...mockupDrawingProblems(root, spec.body, at));
|
|
8283
8488
|
}
|
|
8284
8489
|
if (command === "tweak" && spec) problems.push(...tweakProblems(root, spec, owner, start));
|
|
8285
8490
|
if ((command === "spec" || command === "make") && spec) {
|
|
@@ -8307,7 +8512,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8307
8512
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
8308
8513
|
const mockup = mockupField(spec.body);
|
|
8309
8514
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
8310
|
-
if (/^approved/i.test(mockup) && at && !/^https?:/i.test(at) &&
|
|
8515
|
+
if (/^approved/i.test(mockup) && at && !/^https?:/i.test(at) && existsSync28(join44(root, at))) {
|
|
8311
8516
|
const changedAfter = drawingChangedAfterApproval(root, spec.path, at);
|
|
8312
8517
|
if (changedAfter) {
|
|
8313
8518
|
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.`);
|
|
@@ -8319,7 +8524,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8319
8524
|
if (found) {
|
|
8320
8525
|
let now = "";
|
|
8321
8526
|
try {
|
|
8322
|
-
now = readText(
|
|
8527
|
+
now = readText(join44(root, found));
|
|
8323
8528
|
} catch {
|
|
8324
8529
|
}
|
|
8325
8530
|
const then = fileAtSessionStart(root, found, start);
|
|
@@ -8333,13 +8538,13 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8333
8538
|
if (found) {
|
|
8334
8539
|
let now = "";
|
|
8335
8540
|
try {
|
|
8336
|
-
now = readText(
|
|
8541
|
+
now = readText(join44(root, found));
|
|
8337
8542
|
} catch {
|
|
8338
8543
|
}
|
|
8339
8544
|
const then = fileAtSessionStart(root, found, start);
|
|
8340
8545
|
if (now && now !== then) {
|
|
8341
8546
|
problems.push(...unsourcedReasons(now, then, { newWhysOnly: true }).map((p) => `${found}: ${p}`));
|
|
8342
|
-
const change = readTweak(
|
|
8547
|
+
const change = readTweak(join44(root, ".jig", "critique", spec.slug))?.change;
|
|
8343
8548
|
if (change) problems.push(...quotesNotFrom(now, then, change).map((p) => `${found}: ${p}`));
|
|
8344
8549
|
}
|
|
8345
8550
|
}
|
|
@@ -8348,7 +8553,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8348
8553
|
for (const file of modeFiles(root)) {
|
|
8349
8554
|
let now = "";
|
|
8350
8555
|
try {
|
|
8351
|
-
now = readText(
|
|
8556
|
+
now = readText(join44(root, file));
|
|
8352
8557
|
} catch {
|
|
8353
8558
|
continue;
|
|
8354
8559
|
}
|
|
@@ -8359,8 +8564,8 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8359
8564
|
}
|
|
8360
8565
|
}
|
|
8361
8566
|
if (command === "critique") {
|
|
8362
|
-
const dir =
|
|
8363
|
-
const surfaces =
|
|
8567
|
+
const dir = join44(root, ".jig", "critique");
|
|
8568
|
+
const surfaces = existsSync28(dir) ? readdirSync17(dir).filter((s) => existsSync28(join44(dir, s, "screen.json")) || existsSync28(join44(dir, s, "code.json"))) : [];
|
|
8364
8569
|
for (const surface2 of surfaces) {
|
|
8365
8570
|
const v = verifyVerdicts({ projectRoot: root, surface: surface2 });
|
|
8366
8571
|
if (v.ok && v.decisions.state !== "ran" && v.decisions.total > 0) {
|
|
@@ -8371,7 +8576,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8371
8576
|
}
|
|
8372
8577
|
}
|
|
8373
8578
|
const own = specFor(root, surface)?.slug;
|
|
8374
|
-
if (own && surfaces.includes(own)) problems.push(...reportOmissions(
|
|
8579
|
+
if (own && surfaces.includes(own)) problems.push(...reportOmissions(join44(dir, own), own));
|
|
8375
8580
|
if (surfaces.length === 0) {
|
|
8376
8581
|
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.");
|
|
8377
8582
|
}
|
|
@@ -8384,7 +8589,7 @@ function drawingBeforeAsking(root, surface) {
|
|
|
8384
8589
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
8385
8590
|
const named = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
8386
8591
|
const at = named || `.jig/mockups/${spec.slug}.html`;
|
|
8387
|
-
if (/^https?:/i.test(at) || !/\.html?$/i.test(at) || !
|
|
8592
|
+
if (/^https?:/i.test(at) || !/\.html?$/i.test(at) || !existsSync28(join44(root, at))) return [];
|
|
8388
8593
|
const found = mockupDrawingProblems(root, spec.body, at);
|
|
8389
8594
|
return found.length ? [`Fix the drawing before you put it to the owner; they approve what the spec lists, drawn. ${found.join(" ")}`] : [];
|
|
8390
8595
|
}
|
|
@@ -8392,14 +8597,14 @@ function specChecked(root, spec, start, owner) {
|
|
|
8392
8597
|
const found = decisionsFile(root);
|
|
8393
8598
|
let decisions = "";
|
|
8394
8599
|
try {
|
|
8395
|
-
if (found) decisions = readText(
|
|
8600
|
+
if (found) decisions = readText(join44(root, found));
|
|
8396
8601
|
} catch {
|
|
8397
8602
|
}
|
|
8398
8603
|
return specCheckProblems(root, spec, fileAtSessionStart(root, spec.path, start), owner, decisions);
|
|
8399
8604
|
}
|
|
8400
8605
|
function modeFiles(root) {
|
|
8401
8606
|
try {
|
|
8402
|
-
const state = JSON.parse(
|
|
8607
|
+
const state = JSON.parse(readFileSync37(join44(root, ".jig", "state.json"), "utf8"));
|
|
8403
8608
|
return Object.keys(state.files ?? {}).filter((f) => /(^|\/)mode\.[\w-]+\.css$/.test(f));
|
|
8404
8609
|
} catch {
|
|
8405
8610
|
return [];
|
|
@@ -8408,7 +8613,7 @@ function modeFiles(root) {
|
|
|
8408
8613
|
function reportOmissions(dir, surface) {
|
|
8409
8614
|
let report2;
|
|
8410
8615
|
try {
|
|
8411
|
-
report2 = readText(
|
|
8616
|
+
report2 = readText(join44(dir, "REPORT.md")).toLowerCase();
|
|
8412
8617
|
} catch {
|
|
8413
8618
|
return [];
|
|
8414
8619
|
}
|
|
@@ -8416,7 +8621,7 @@ function reportOmissions(dir, surface) {
|
|
|
8416
8621
|
for (const f of VERDICT_FILES) {
|
|
8417
8622
|
let file;
|
|
8418
8623
|
try {
|
|
8419
|
-
file = JSON.parse(
|
|
8624
|
+
file = JSON.parse(readFileSync37(join44(dir, f), "utf8"));
|
|
8420
8625
|
} catch {
|
|
8421
8626
|
continue;
|
|
8422
8627
|
}
|
|
@@ -8437,16 +8642,16 @@ function tweakProblems(root, spec, owner = "", start) {
|
|
|
8437
8642
|
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\`.`);
|
|
8438
8643
|
else problems.push(...structureSinceApproval(root, spec, front));
|
|
8439
8644
|
const surface = spec.slug;
|
|
8440
|
-
const record = readTweak(
|
|
8645
|
+
const record = readTweak(join44(root, ".jig", "critique", surface));
|
|
8441
8646
|
if (!record) {
|
|
8442
8647
|
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\`).`);
|
|
8443
8648
|
} else {
|
|
8444
8649
|
if (!record.change) problems.push(`.jig/critique/${surface}/tweak.json has no \`change\`: the owner's words for what changed.`);
|
|
8445
|
-
else if (owner && fileAtSessionStart(root, `.jig/critique/${surface}/tweak.json`, start) !== readText(
|
|
8650
|
+
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)) {
|
|
8446
8651
|
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.`);
|
|
8447
8652
|
}
|
|
8448
8653
|
try {
|
|
8449
|
-
const raw = JSON.parse(
|
|
8654
|
+
const raw = JSON.parse(readFileSync37(join44(root, ".jig", "critique", surface, "tweak.json"), "utf8"));
|
|
8450
8655
|
const extra = Object.keys(raw).filter((k) => !["at", "change", "ids", "deferred"].includes(k));
|
|
8451
8656
|
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.`);
|
|
8452
8657
|
} catch {
|
|
@@ -8461,7 +8666,7 @@ function tweakProblems(root, spec, owner = "", start) {
|
|
|
8461
8666
|
}
|
|
8462
8667
|
return problems;
|
|
8463
8668
|
}
|
|
8464
|
-
const unjudged = unjudgedTweakIds(
|
|
8669
|
+
const unjudged = unjudgedTweakIds(join44(root, ".jig", "critique", surface), record);
|
|
8465
8670
|
if (unjudged.length) {
|
|
8466
8671
|
problems.push(
|
|
8467
8672
|
`.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.`
|
|
@@ -8497,9 +8702,9 @@ function structureSinceApproval(root, spec, front) {
|
|
|
8497
8702
|
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\`.`);
|
|
8498
8703
|
}
|
|
8499
8704
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
|
|
8500
|
-
if (at && !/^https?:/i.test(at) &&
|
|
8705
|
+
if (at && !/^https?:/i.test(at) && existsSync28(join44(root, at))) {
|
|
8501
8706
|
try {
|
|
8502
|
-
if (lf(git(["show", `${baseline}:./${at}`])) !== readText(
|
|
8707
|
+
if (lf(git(["show", `${baseline}:./${at}`])) !== readText(join44(root, at))) {
|
|
8503
8708
|
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\`.`);
|
|
8504
8709
|
}
|
|
8505
8710
|
} catch {
|
|
@@ -8509,7 +8714,7 @@ function structureSinceApproval(root, spec, front) {
|
|
|
8509
8714
|
}
|
|
8510
8715
|
function readTweak(dir) {
|
|
8511
8716
|
try {
|
|
8512
|
-
const raw = JSON.parse(
|
|
8717
|
+
const raw = JSON.parse(readFileSync37(join44(dir, "tweak.json"), "utf8"));
|
|
8513
8718
|
const deferred = typeof raw.deferred === "string" || raw.deferred === true ? raw.deferred : void 0;
|
|
8514
8719
|
return { at: String(raw.at ?? ""), change: String(raw.change ?? "").trim(), ids: Array.isArray(raw.ids) ? raw.ids.map(String) : [], ...deferred !== void 0 ? { deferred } : {} };
|
|
8515
8720
|
} catch {
|
|
@@ -8519,16 +8724,16 @@ function readTweak(dir) {
|
|
|
8519
8724
|
function surfacePage(projectRoot, surface) {
|
|
8520
8725
|
const spec = specFor(projectRoot, surface);
|
|
8521
8726
|
const front = spec?.body.split(/^---\s*$/m)[1] ?? "";
|
|
8522
|
-
const
|
|
8523
|
-
const candidates = [
|
|
8727
|
+
const declared2 = /^\s*surface\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
|
|
8728
|
+
const candidates = [declared2, `${surface}.html`, declared2 ? `${declared2.replace(/^\//, "")}.html` : void 0].filter((c) => !!c && /\.\w+$/.test(c) === (c === declared2 ? /\.\w+$/.test(c) : true));
|
|
8524
8729
|
for (const candidate of candidates) {
|
|
8525
|
-
if (candidate &&
|
|
8730
|
+
if (candidate && existsSync28(join44(projectRoot, candidate))) return candidate;
|
|
8526
8731
|
}
|
|
8527
8732
|
return void 0;
|
|
8528
8733
|
}
|
|
8529
8734
|
function gate(opts) {
|
|
8530
8735
|
const root = opts.projectRoot;
|
|
8531
|
-
if (!
|
|
8736
|
+
if (!existsSync28(join44(root, "jig.config.json")) && !existsSync28(join44(root, ".jig"))) {
|
|
8532
8737
|
return { block: false, reason: "" };
|
|
8533
8738
|
}
|
|
8534
8739
|
const invocation = lastJigInvocation(opts.input.transcript_path);
|
|
@@ -8564,7 +8769,7 @@ ${result.report}`);
|
|
|
8564
8769
|
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.`);
|
|
8565
8770
|
}
|
|
8566
8771
|
if (spec) problems.push(...specChecked(root, spec, start, owner).map((p) => `/jig spec: ${p}`));
|
|
8567
|
-
if (spec &&
|
|
8772
|
+
if (spec && existsSync28(join44(root, ".jig", "specs", `${spec.slug}.checked.json`))) {
|
|
8568
8773
|
problems.push(...[...specProblems(spec), ...newFieldProblems(spec, fileAtSessionStart(root, spec.path, start))].map((p) => `/jig spec: ${p}`));
|
|
8569
8774
|
}
|
|
8570
8775
|
}
|
|
@@ -8591,15 +8796,15 @@ ${result.report}`);
|
|
|
8591
8796
|
problems.push(...verdictGuard(root, command, surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)));
|
|
8592
8797
|
problems.push(...probesLeftBehind(root, surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)));
|
|
8593
8798
|
const touched = ["AGENTS.md", "CLAUDE.md"].some((f) => {
|
|
8594
|
-
const now =
|
|
8799
|
+
const now = existsSync28(join44(root, f)) ? readText(join44(root, f)) : "";
|
|
8595
8800
|
return fileAtSessionStart(root, f, start) !== now;
|
|
8596
8801
|
});
|
|
8597
8802
|
if (touched) problems.push(...agentFileProblems(root).map((p) => `Jig's agent instructions: ${p}`));
|
|
8598
|
-
const critiqueDir =
|
|
8599
|
-
if (
|
|
8803
|
+
const critiqueDir = join44(root, ".jig", "critique");
|
|
8804
|
+
if (existsSync28(critiqueDir)) {
|
|
8600
8805
|
for (const surface of surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)) {
|
|
8601
|
-
const dir =
|
|
8602
|
-
if (!
|
|
8806
|
+
const dir = join44(critiqueDir, surface);
|
|
8807
|
+
if (!existsSync28(join44(dir, "screen.json")) && !existsSync28(join44(dir, "code.json"))) continue;
|
|
8603
8808
|
const v = verifyVerdicts({ projectRoot: root, surface });
|
|
8604
8809
|
if (!v.ok) {
|
|
8605
8810
|
const shown = v.errors.slice(0, 6).map((e) => ` ${e}`);
|
|
@@ -8629,10 +8834,10 @@ ${problems.join("\n\n")}`
|
|
|
8629
8834
|
}
|
|
8630
8835
|
const session = opts.input.session_id ?? "unknown";
|
|
8631
8836
|
const key = `${session}:${createHash3("sha256").update(problems.join("\n")).digest("hex").slice(0, 12)}`;
|
|
8632
|
-
const stateFile =
|
|
8837
|
+
const stateFile = join44(root, ".jig", "gate.json");
|
|
8633
8838
|
let state = {};
|
|
8634
8839
|
try {
|
|
8635
|
-
state = JSON.parse(
|
|
8840
|
+
state = JSON.parse(readFileSync37(stateFile, "utf8"));
|
|
8636
8841
|
} catch {
|
|
8637
8842
|
}
|
|
8638
8843
|
if (problems.length === 0) {
|
|
@@ -8667,7 +8872,7 @@ function verdictDigests(dir) {
|
|
|
8667
8872
|
for (const f of VERDICT_FILES) {
|
|
8668
8873
|
let file;
|
|
8669
8874
|
try {
|
|
8670
|
-
file = JSON.parse(
|
|
8875
|
+
file = JSON.parse(readFileSync37(join44(dir, f), "utf8"));
|
|
8671
8876
|
} catch {
|
|
8672
8877
|
continue;
|
|
8673
8878
|
}
|
|
@@ -8682,7 +8887,7 @@ function writeLock(root, surface, lockPath, dir, now) {
|
|
|
8682
8887
|
const pageFile2 = recordedPage(root, surface);
|
|
8683
8888
|
let page;
|
|
8684
8889
|
try {
|
|
8685
|
-
if (pageFile2) page = { file: pageFile2, checksum: pageChecksum(
|
|
8890
|
+
if (pageFile2) page = { file: pageFile2, checksum: pageChecksum(readFileSync37(join44(root, pageFile2), "utf8")) };
|
|
8686
8891
|
} catch {
|
|
8687
8892
|
}
|
|
8688
8893
|
const tweakAt = readTweak(dir)?.at;
|
|
@@ -8695,7 +8900,7 @@ function verdictChecksum(dir) {
|
|
|
8695
8900
|
const parts = VERDICT_FILES.map((f) => {
|
|
8696
8901
|
try {
|
|
8697
8902
|
return `${f}
|
|
8698
|
-
${
|
|
8903
|
+
${readFileSync37(join44(dir, f), "utf8")}`;
|
|
8699
8904
|
} catch {
|
|
8700
8905
|
return "";
|
|
8701
8906
|
}
|
|
@@ -8703,15 +8908,15 @@ ${readFileSync36(join43(dir, f), "utf8")}`;
|
|
|
8703
8908
|
return parts.some(Boolean) ? checksum(parts.join("\n")) : void 0;
|
|
8704
8909
|
}
|
|
8705
8910
|
function verdictGuard(root, command, inPlay) {
|
|
8706
|
-
const critiqueDir =
|
|
8707
|
-
if (!
|
|
8911
|
+
const critiqueDir = join44(root, ".jig", "critique");
|
|
8912
|
+
if (!existsSync28(critiqueDir)) return [];
|
|
8708
8913
|
const problems = [];
|
|
8709
|
-
for (const surface of
|
|
8914
|
+
for (const surface of readdirSync17(critiqueDir)) {
|
|
8710
8915
|
if (surface.startsWith("_")) continue;
|
|
8711
|
-
const dir =
|
|
8916
|
+
const dir = join44(critiqueDir, surface);
|
|
8712
8917
|
const now = verdictChecksum(dir);
|
|
8713
8918
|
if (!now) continue;
|
|
8714
|
-
const lockPath =
|
|
8919
|
+
const lockPath = join44(dir, LOCK);
|
|
8715
8920
|
if ((command === "critique" || command === "ship") && (!inPlay || inPlay.includes(surface))) {
|
|
8716
8921
|
writeLock(root, surface, lockPath, dir, now);
|
|
8717
8922
|
problems.push(...lockLeftBehind(root, surface));
|
|
@@ -8719,7 +8924,7 @@ function verdictGuard(root, command, inPlay) {
|
|
|
8719
8924
|
}
|
|
8720
8925
|
let lock = {};
|
|
8721
8926
|
try {
|
|
8722
|
-
lock = JSON.parse(
|
|
8927
|
+
lock = JSON.parse(readFileSync37(lockPath, "utf8"));
|
|
8723
8928
|
} catch {
|
|
8724
8929
|
continue;
|
|
8725
8930
|
}
|
|
@@ -8795,7 +9000,7 @@ function unjudgedTweakIds(dir, record) {
|
|
|
8795
9000
|
for (const f of VERDICT_FILES) {
|
|
8796
9001
|
let file;
|
|
8797
9002
|
try {
|
|
8798
|
-
file = JSON.parse(
|
|
9003
|
+
file = JSON.parse(readFileSync37(join44(dir, f), "utf8"));
|
|
8799
9004
|
} catch {
|
|
8800
9005
|
continue;
|
|
8801
9006
|
}
|
|
@@ -8807,7 +9012,7 @@ function unjudgedTweakIds(dir, record) {
|
|
|
8807
9012
|
}
|
|
8808
9013
|
function save(file, state) {
|
|
8809
9014
|
try {
|
|
8810
|
-
mkdirSync8(
|
|
9015
|
+
mkdirSync8(join44(file, ".."), { recursive: true });
|
|
8811
9016
|
writeFileSync6(file, JSON.stringify(state), "utf8");
|
|
8812
9017
|
} catch {
|
|
8813
9018
|
}
|
|
@@ -8815,7 +9020,7 @@ function save(file, state) {
|
|
|
8815
9020
|
|
|
8816
9021
|
// src/index.ts
|
|
8817
9022
|
var packageRoot = getPackageRoot();
|
|
8818
|
-
var { version } = JSON.parse(
|
|
9023
|
+
var { version } = JSON.parse(readFileSync38(join45(packageRoot, "package.json"), "utf8"));
|
|
8819
9024
|
function warnIfUnpublishedPin() {
|
|
8820
9025
|
if (isDevVersion(version)) {
|
|
8821
9026
|
console.warn(
|
|
@@ -8927,12 +9132,12 @@ program.command("update").description("Update vendored Jig rules, skipping files
|
|
|
8927
9132
|
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) => {
|
|
8928
9133
|
const projectRoot = findProjectRoot(process.cwd());
|
|
8929
9134
|
const slug = name.replace(/^.*\//, "").replace(/\.spec\.md$/, "");
|
|
8930
|
-
const path =
|
|
8931
|
-
if (!
|
|
9135
|
+
const path = join45(projectRoot, ".jig", "specs", `${slug}.spec.md`);
|
|
9136
|
+
if (!existsSync29(path)) {
|
|
8932
9137
|
console.error(` \u2717 no spec at .jig/specs/${slug}.spec.md`);
|
|
8933
9138
|
process.exit(1);
|
|
8934
9139
|
}
|
|
8935
|
-
console.log(specChecksum(
|
|
9140
|
+
console.log(specChecksum(readFileSync38(path, "utf8")));
|
|
8936
9141
|
});
|
|
8937
9142
|
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) => {
|
|
8938
9143
|
const projectRoot = findProjectRoot(process.cwd());
|
|
@@ -9066,7 +9271,7 @@ program.command("probe").description("Print the render probe. With --save, read
|
|
|
9066
9271
|
program.command("gate").description("Run by the Claude Code Stop hook: block stopping while check or a critique fails.").action(async () => {
|
|
9067
9272
|
let input = {};
|
|
9068
9273
|
try {
|
|
9069
|
-
if (!process.stdin.isTTY) input = JSON.parse(
|
|
9274
|
+
if (!process.stdin.isTTY) input = JSON.parse(readFileSync38(0, "utf8") || "{}");
|
|
9070
9275
|
} catch {
|
|
9071
9276
|
}
|
|
9072
9277
|
const cwd = input.cwd ?? process.cwd();
|
|
@@ -9107,7 +9312,7 @@ program.command("init").description("Set the project up to use Jig: a brand file
|
|
|
9107
9312
|
});
|
|
9108
9313
|
function resolveSurfaceMode(projectRoot) {
|
|
9109
9314
|
try {
|
|
9110
|
-
const config = JSON.parse(
|
|
9315
|
+
const config = JSON.parse(readFileSync38(join45(projectRoot, "jig.config.json"), "utf8"));
|
|
9111
9316
|
const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
|
|
9112
9317
|
return modes.length === 1 ? modes[0] : void 0;
|
|
9113
9318
|
} catch {
|