jig-ui 0.16.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +7 -6
- package/dist/index.js +630 -160
- package/examples/A-01.html +7 -0
- package/examples/A-02.html +7 -0
- package/examples/A-03.html +7 -0
- package/examples/A-04.html +7 -0
- package/examples/A-05.html +7 -0
- package/examples/A-06.html +7 -0
- package/examples/A-07.html +7 -0
- package/examples/A-08.html +7 -0
- package/examples/A-09.html +7 -0
- package/examples/A-10.html +7 -0
- package/examples/A-135.html +7 -0
- package/examples/A-136.html +7 -0
- package/examples/A-137.html +7 -0
- package/examples/A-138.html +7 -0
- package/examples/A-139.html +7 -0
- package/examples/A-140.html +7 -0
- package/examples/A-141.html +7 -0
- package/examples/A-142.html +7 -0
- package/examples/A-143.html +7 -0
- package/examples/A-144.html +7 -0
- package/examples/A-146.html +7 -0
- package/examples/A-58.html +7 -0
- package/examples/A-59.html +7 -0
- package/examples/A-60.html +7 -0
- package/examples/A-67.html +7 -0
- package/examples/B-105.html +7 -0
- package/examples/B-106.html +7 -0
- package/examples/B-11.html +7 -0
- package/examples/B-12.html +7 -0
- package/examples/B-13.html +7 -0
- package/examples/B-14.html +7 -0
- package/examples/B-15.html +7 -0
- package/examples/B-16.html +7 -0
- package/examples/B-17.html +7 -0
- package/examples/B-75.html +7 -0
- package/examples/B-76.html +7 -0
- package/examples/B-77.html +7 -0
- package/examples/B-78.html +7 -0
- package/examples/C-18.html +7 -0
- package/examples/C-19.html +7 -0
- package/examples/C-20.html +7 -0
- package/examples/C-21.html +7 -0
- package/examples/C-22.html +11 -0
- package/examples/C-49.html +7 -0
- package/examples/C-50.html +7 -0
- package/examples/C-66.html +7 -0
- package/examples/C-68.html +7 -0
- package/examples/D-111.html +7 -0
- package/examples/D-112.html +7 -0
- package/examples/D-114.html +7 -0
- package/examples/D-115.html +7 -0
- package/examples/D-23.html +11 -0
- package/examples/D-24.html +7 -0
- package/examples/D-25.html +7 -0
- package/examples/D-26.html +7 -0
- package/examples/D-27.html +7 -0
- package/examples/D-69.html +7 -0
- package/examples/D-70.html +7 -0
- package/examples/D-71.html +7 -0
- package/examples/D-72.html +7 -0
- package/examples/D-96.html +7 -0
- package/examples/E-116.html +7 -0
- package/examples/E-28.html +7 -0
- package/examples/E-29.html +7 -0
- package/examples/E-30.html +7 -0
- package/examples/E-31.html +7 -0
- package/examples/E-32.html +7 -0
- package/examples/E-33.html +11 -0
- package/examples/E-34.html +7 -0
- package/examples/E-35.html +7 -0
- package/examples/E-51.html +7 -0
- package/examples/E-52.html +7 -0
- package/examples/E-61.html +7 -0
- package/examples/E-62.html +7 -0
- package/examples/E-63.html +7 -0
- package/examples/E-64.html +7 -0
- package/examples/E-65.html +7 -0
- package/examples/E-73.html +7 -0
- package/examples/E-74.html +7 -0
- package/examples/E-91.html +7 -0
- package/examples/E-92.html +7 -0
- package/examples/E-93.html +7 -0
- package/examples/E-94.html +7 -0
- package/examples/E-95.html +7 -0
- package/examples/F-100.html +7 -0
- package/examples/F-101.html +7 -0
- package/examples/F-102.html +7 -0
- package/examples/F-103.html +7 -0
- package/examples/F-104.html +7 -0
- package/examples/F-113.html +7 -0
- package/examples/F-147.html +7 -0
- package/examples/F-36.html +7 -0
- package/examples/F-37.html +7 -0
- package/examples/F-38.html +7 -0
- package/examples/F-39.html +7 -0
- package/examples/F-40.html +7 -0
- package/examples/F-41.html +11 -0
- package/examples/F-97.html +7 -0
- package/examples/F-98.html +7 -0
- package/examples/F-99.html +7 -0
- package/examples/G-145.html +7 -0
- package/examples/G-42.html +7 -0
- package/examples/G-43.html +10 -0
- package/examples/G-44.html +7 -0
- package/examples/H-117.html +11 -0
- package/examples/H-119.html +13 -0
- package/examples/H-45.html +13 -0
- package/examples/H-46.html +10 -0
- package/examples/H-47.html +15 -0
- package/examples/H-48.html +8 -0
- package/examples/I-118.html +7 -0
- package/examples/I-53.html +7 -0
- package/examples/I-54.html +7 -0
- package/examples/I-55.html +7 -0
- package/examples/I-56.html +7 -0
- package/examples/I-57.html +7 -0
- package/examples/I-79.html +7 -0
- package/examples/I-80.html +7 -0
- package/examples/I-81.html +7 -0
- package/examples/I-82.html +7 -0
- package/examples/I-83.html +7 -0
- package/examples/I-84.html +7 -0
- package/examples/I-85.html +7 -0
- package/examples/I-86.html +7 -0
- package/examples/I-87.html +7 -0
- package/examples/I-88.html +7 -0
- package/examples/I-89.html +7 -0
- package/examples/I-90.html +7 -0
- package/examples/J-120.html +7 -0
- package/examples/J-121.html +7 -0
- package/examples/J-122.html +7 -0
- package/examples/J-123.html +7 -0
- package/examples/J-124.html +8 -0
- package/examples/J-125.html +10 -0
- package/examples/J-126.html +9 -0
- package/examples/J-127.html +10 -0
- package/examples/K-128.html +9 -0
- package/examples/K-129.html +7 -0
- package/examples/K-130.html +7 -0
- package/examples/K-131.html +11 -0
- package/examples/K-132.html +7 -0
- package/examples/K-133.html +8 -0
- package/examples/K-134.html +9 -0
- package/examples/README.md +44 -0
- package/package.json +3 -2
- package/rules/00-anti-patterns.md +67 -0
- package/rules/01-modes.md +1 -1
- package/rules/05-copy.md +2 -0
- package/rules.index.json +101 -0
- package/templates/COMMAND.md.tmpl +10 -3
package/dist/index.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import { readFileSync as
|
|
5
|
+
import { readFileSync as readFileSync32 } from "fs";
|
|
6
6
|
import { homedir as homedir2 } from "os";
|
|
7
|
-
import { join as
|
|
7
|
+
import { join as join39 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/paths.ts
|
|
10
10
|
import { existsSync } from "fs";
|
|
@@ -844,15 +844,15 @@ function detectLegacyCursorRules(projectRoot) {
|
|
|
844
844
|
}
|
|
845
845
|
return { present: files.length > 0, files };
|
|
846
846
|
}
|
|
847
|
-
function describeLegacyFiles(
|
|
848
|
-
if (!
|
|
847
|
+
function describeLegacyFiles(report2, headerLine, subheaderLine) {
|
|
848
|
+
if (!report2.present) return [];
|
|
849
849
|
const lines = [];
|
|
850
850
|
lines.push(`
|
|
851
851
|
${headerLine}`);
|
|
852
852
|
lines.push(subheaderLine);
|
|
853
|
-
for (const f of
|
|
854
|
-
const edited =
|
|
855
|
-
const unknown =
|
|
853
|
+
for (const f of report2.files) lines.push(` ${f.relPath}`);
|
|
854
|
+
const edited = report2.files.filter((f) => f.modified === true).map((f) => f.relPath);
|
|
855
|
+
const unknown = report2.files.filter((f) => f.modified === "unknown").map((f) => f.relPath);
|
|
856
856
|
if (edited.length) {
|
|
857
857
|
lines.push(" Edited since install \u2014 kept, never removed automatically:");
|
|
858
858
|
for (const relPath of edited) lines.push(` ${relPath}`);
|
|
@@ -863,22 +863,22 @@ ${headerLine}`);
|
|
|
863
863
|
}
|
|
864
864
|
return lines;
|
|
865
865
|
}
|
|
866
|
-
function describeLegacyReport(
|
|
866
|
+
function describeLegacyReport(report2) {
|
|
867
867
|
return describeLegacyFiles(
|
|
868
|
-
|
|
869
|
-
`Found a pre-0.4.0 Jig install in .jig/ (${
|
|
868
|
+
report2,
|
|
869
|
+
`Found a pre-0.4.0 Jig install in .jig/ (${report2.files.length} file(s)). These are no longer used \u2014`,
|
|
870
870
|
" since 0.4.0 the skill and its rules live in your agent's skill directory, not the project:"
|
|
871
871
|
);
|
|
872
872
|
}
|
|
873
|
-
function describeLegacyCursorReport(
|
|
873
|
+
function describeLegacyCursorReport(report2) {
|
|
874
874
|
return describeLegacyFiles(
|
|
875
|
-
|
|
876
|
-
`Found a legacy Cursor install (${
|
|
875
|
+
report2,
|
|
876
|
+
`Found a legacy Cursor install (${report2.files.length} file(s)). Cursor's skill has moved \u2014`,
|
|
877
877
|
" from .cursor/rules/jig.mdc to .cursor/skills/jig/SKILL.md; these are no longer read:"
|
|
878
878
|
);
|
|
879
879
|
}
|
|
880
|
-
function removableLegacyFiles(
|
|
881
|
-
return
|
|
880
|
+
function removableLegacyFiles(report2) {
|
|
881
|
+
return report2.files.filter((f) => f.modified === false).map((f) => f.relPath);
|
|
882
882
|
}
|
|
883
883
|
function removeLegacyFiles(projectRoot, relPaths) {
|
|
884
884
|
for (const relPath of relPaths) {
|
|
@@ -1031,6 +1031,7 @@ function updateInitFiles(opts) {
|
|
|
1031
1031
|
}
|
|
1032
1032
|
|
|
1033
1033
|
// src/commands/explain.ts
|
|
1034
|
+
import { existsSync as existsSync9 } from "fs";
|
|
1034
1035
|
import { join as join15 } from "path";
|
|
1035
1036
|
|
|
1036
1037
|
// src/rules/load.ts
|
|
@@ -1410,7 +1411,12 @@ function explain(opts) {
|
|
|
1410
1411
|
if (shape) {
|
|
1411
1412
|
const id = `${shape[1]}-${shape[2]}`;
|
|
1412
1413
|
const rule = rules.find((r) => r.id === id);
|
|
1413
|
-
if (rule)
|
|
1414
|
+
if (rule) {
|
|
1415
|
+
const example = join15(root, "examples", `${id}.html`);
|
|
1416
|
+
return existsSync9(example) ? `${renderRule(rule)}
|
|
1417
|
+
|
|
1418
|
+
Example: ${example} (a dont and a do, each a self-contained HTML fragment)` : renderRule(rule);
|
|
1419
|
+
}
|
|
1414
1420
|
const spec = specs.find((s) => s.id === id);
|
|
1415
1421
|
if (spec) return renderSpec(spec);
|
|
1416
1422
|
const siblings = entries.filter((e) => e.id.startsWith(`${shape[1]}-`)).map((e) => e.id).sort(byNumber);
|
|
@@ -1463,7 +1469,7 @@ function sectionsOf(entries) {
|
|
|
1463
1469
|
}
|
|
1464
1470
|
|
|
1465
1471
|
// src/commands/check.ts
|
|
1466
|
-
import { existsSync as
|
|
1472
|
+
import { existsSync as existsSync15, readFileSync as readFileSync19 } from "fs";
|
|
1467
1473
|
import { join as join24 } from "path";
|
|
1468
1474
|
|
|
1469
1475
|
// src/check/files.ts
|
|
@@ -2764,9 +2770,9 @@ function palettePairs(classes) {
|
|
|
2764
2770
|
let background;
|
|
2765
2771
|
let foreground;
|
|
2766
2772
|
for (const token of classes.split(/\s+/)) {
|
|
2767
|
-
const
|
|
2768
|
-
const bg = /^bg-(.+)$/.exec(
|
|
2769
|
-
const fg = /^text-(.+)$/.exec(
|
|
2773
|
+
const bare2 = token.replace(/^(?:[\w-]+:)*/, "");
|
|
2774
|
+
const bg = /^bg-(.+)$/.exec(bare2);
|
|
2775
|
+
const fg = /^text-(.+)$/.exec(bare2);
|
|
2770
2776
|
if (bg && PALETTE[bg[1]]) background = PALETTE[bg[1]];
|
|
2771
2777
|
if (fg && PALETTE[fg[1]]) foreground = PALETTE[fg[1]];
|
|
2772
2778
|
}
|
|
@@ -2787,11 +2793,11 @@ function resolveFloor(body) {
|
|
|
2787
2793
|
const raw = m[1].trim();
|
|
2788
2794
|
const pxMatch = FONT_SIZE_PX_RE.exec(raw);
|
|
2789
2795
|
const remMatch = FONT_SIZE_REM_RE.exec(raw);
|
|
2790
|
-
let
|
|
2791
|
-
if (pxMatch)
|
|
2792
|
-
else if (remMatch)
|
|
2796
|
+
let px2;
|
|
2797
|
+
if (pxMatch) px2 = parseFloat(pxMatch[1]);
|
|
2798
|
+
else if (remMatch) px2 = parseFloat(remMatch[1]) * REM_TO_PX;
|
|
2793
2799
|
else return LARGE_FLOOR;
|
|
2794
|
-
const large = BOLD_RE.test(body) ?
|
|
2800
|
+
const large = BOLD_RE.test(body) ? px2 >= 18.66 : px2 >= 24;
|
|
2795
2801
|
return large ? LARGE_FLOOR : FLOOR;
|
|
2796
2802
|
}
|
|
2797
2803
|
var DECL_RE = /(?<![-\w])(color|background(?:-color)?)\s*:\s*([^;]+)(?:;|$)/gi;
|
|
@@ -3142,8 +3148,8 @@ var inputZoom = {
|
|
|
3142
3148
|
SIZE_RE.lastIndex = 0;
|
|
3143
3149
|
let m;
|
|
3144
3150
|
while (m = SIZE_RE.exec(block.body)) {
|
|
3145
|
-
const
|
|
3146
|
-
if (
|
|
3151
|
+
const px2 = pixels(m[1], ctx.mode);
|
|
3152
|
+
if (px2 === void 0 || px2 >= ROOT_PX) continue;
|
|
3147
3153
|
const line = lineOfOffset(block, m.index);
|
|
3148
3154
|
findings.push(
|
|
3149
3155
|
mkFinding(
|
|
@@ -3151,7 +3157,7 @@ var inputZoom = {
|
|
|
3151
3157
|
"input-zoom",
|
|
3152
3158
|
file,
|
|
3153
3159
|
line,
|
|
3154
|
-
`form control text resolves to ${
|
|
3160
|
+
`form control text resolves to ${px2}px; iOS zooms the page on focus below 16px`,
|
|
3155
3161
|
sourceLine(source, line)
|
|
3156
3162
|
)
|
|
3157
3163
|
);
|
|
@@ -3255,7 +3261,7 @@ var menuState = {
|
|
|
3255
3261
|
};
|
|
3256
3262
|
|
|
3257
3263
|
// src/check/declared-properties.ts
|
|
3258
|
-
import { existsSync as
|
|
3264
|
+
import { existsSync as existsSync10, readFileSync as readFileSync12 } from "fs";
|
|
3259
3265
|
import { join as join17 } from "path";
|
|
3260
3266
|
var DECLARATION_RE = /(?<![\w-])['"]?(--[A-Za-z_][\w-]*)['"]?\s*:/g;
|
|
3261
3267
|
var AT_PROPERTY_RE = /@property\s+(--[\w-]+)/g;
|
|
@@ -3271,7 +3277,7 @@ function readPackageImport(projectRoot, spec) {
|
|
|
3271
3277
|
const base = join17(projectRoot, "node_modules", spec);
|
|
3272
3278
|
for (const candidate of [base, `${base}.css`]) {
|
|
3273
3279
|
try {
|
|
3274
|
-
if (
|
|
3280
|
+
if (existsSync10(candidate) && candidate.endsWith(".css")) return readFileSync12(candidate, "utf8");
|
|
3275
3281
|
} catch {
|
|
3276
3282
|
}
|
|
3277
3283
|
}
|
|
@@ -3635,6 +3641,335 @@ var interfaceSafety = {
|
|
|
3635
3641
|
}
|
|
3636
3642
|
};
|
|
3637
3643
|
|
|
3644
|
+
// src/check/detectors/generated-look.ts
|
|
3645
|
+
function report(ctx, name, file, raw, hits) {
|
|
3646
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3647
|
+
return hits.filter((h) => seen.has(h.line) ? false : (seen.add(h.line), true)).map((h) => mkFinding(ctx, name, file, h.line, h.message, sourceLine(raw, h.line))).sort((a, b) => a.line - b.line);
|
|
3648
|
+
}
|
|
3649
|
+
function classLists(file, raw) {
|
|
3650
|
+
if (!isMarkupHost(file)) return [];
|
|
3651
|
+
return classAttributeValues(raw).map((a) => ({ classes: a.classes.split(/\s+/).filter(Boolean), line: a.line }));
|
|
3652
|
+
}
|
|
3653
|
+
var bare = (c) => c.replace(/^(?:[\w-]+:)+/, "").replace(/^!/, "");
|
|
3654
|
+
var px = (v) => {
|
|
3655
|
+
const m = /^(-?\d*\.?\d+)(px|rem|em)?$/.exec(v.trim());
|
|
3656
|
+
if (!m) return void 0;
|
|
3657
|
+
const n = parseFloat(m[1]);
|
|
3658
|
+
return m[2] === "rem" || m[2] === "em" ? n * 16 : n;
|
|
3659
|
+
};
|
|
3660
|
+
function twBorderWidth(suffix) {
|
|
3661
|
+
if (!suffix) return 1;
|
|
3662
|
+
const arb = /^\[(.+)\]$/.exec(suffix);
|
|
3663
|
+
if (arb) return px(arb[1]) ?? 0;
|
|
3664
|
+
return Number(suffix) || 0;
|
|
3665
|
+
}
|
|
3666
|
+
var TW_COLOUR = /^(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d{2,3}/;
|
|
3667
|
+
function saturated(value, tokens, minS = 25, minAlpha = 0.2) {
|
|
3668
|
+
const c = extractColorComponents(value.trim(), tokens);
|
|
3669
|
+
return !!c && c.alpha >= minAlpha && c.hsl.s >= minS && c.hsl.l > 8 && c.hsl.l < 92;
|
|
3670
|
+
}
|
|
3671
|
+
function colourIn(value) {
|
|
3672
|
+
return value.replace(/\b\d*\.?\d+(px|rem|em)\b/g, " ").replace(/\b(solid|dashed|dotted|double|groove|ridge|inset|outset|none|hidden)\b/gi, " ").trim();
|
|
3673
|
+
}
|
|
3674
|
+
var SIDE_DECL = /(?<![-\w])border-(left|inline-start|right|inline-end)(?:-width)?\s*:\s*([^;}]+)/gi;
|
|
3675
|
+
var OTHER_SIDE_WIDE = /(?<![-\w])border(?:-(?:top|bottom))?(?:-width)?\s*:\s*(\d*\.?\d+)px/gi;
|
|
3676
|
+
var sideTabBorder = {
|
|
3677
|
+
name: "side-tab-border",
|
|
3678
|
+
appliesTo: (file) => isStyleBearing(file),
|
|
3679
|
+
run(source, file, ctx) {
|
|
3680
|
+
const hits = [];
|
|
3681
|
+
for (const block of leafBlocks(source)) {
|
|
3682
|
+
if (/\bblockquote\b|\bq\b/.test(block.selector)) continue;
|
|
3683
|
+
SIDE_DECL.lastIndex = 0;
|
|
3684
|
+
let m;
|
|
3685
|
+
while (m = SIDE_DECL.exec(block.body)) {
|
|
3686
|
+
const width = Math.max(0, ...[...m[2].matchAll(/(\d*\.?\d+)(px|rem)/g)].map((w) => px(w[0]) ?? 0));
|
|
3687
|
+
if (width < 3) continue;
|
|
3688
|
+
const colour = colourIn(m[2]);
|
|
3689
|
+
const colourDecl = new RegExp(`border-${m[1]}-color\\s*:\\s*([^;}]+)`, "i").exec(block.body)?.[1] ?? colour;
|
|
3690
|
+
if (colourDecl && !saturated(colourDecl, ctx.tokens) && !/var\(--color-(?:brand|text-brand|stroke-brand|accent)/.test(colourDecl)) continue;
|
|
3691
|
+
OTHER_SIDE_WIDE.lastIndex = 0;
|
|
3692
|
+
if ([...block.body.matchAll(OTHER_SIDE_WIDE)].some((o) => parseFloat(o[1]) >= width)) continue;
|
|
3693
|
+
hits.push({ line: lineOfOffset(block, m.index), message: `a ${width}px coloured stripe on one side of ${block.selector.trim()}: the shape of an alert on something that is not one` });
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
for (const { classes, line } of classLists(file, ctx.raw)) {
|
|
3697
|
+
const plain = classes.map(bare);
|
|
3698
|
+
const side = plain.map((c) => /^border-(l|s|r|e)(?:-(\d+|\[[^\]]+\]))?$/.exec(c)).find((x) => x && twBorderWidth(x[2]) >= 3);
|
|
3699
|
+
if (!side) continue;
|
|
3700
|
+
const colour = plain.find((c) => new RegExp(`^border-(?:${side[1]}-)?(.+)$`).test(c) && TW_COLOUR.test(c.replace(/^border-(?:[lsre]-)?/, "")));
|
|
3701
|
+
if (!colour) continue;
|
|
3702
|
+
if (plain.some((c) => /^border-(?:\d+|\[[^\]]+\])$/.test(c))) continue;
|
|
3703
|
+
hits.push({ line, message: `\`${side[0]}\` with \`${colour}\`: a coloured stripe on one side, the shape of an alert on something that is not one` });
|
|
3704
|
+
}
|
|
3705
|
+
return report(ctx, "side-tab-border", file, ctx.raw, hits);
|
|
3706
|
+
}
|
|
3707
|
+
};
|
|
3708
|
+
var BORDER_ALL = /(?<![-\w])border(?:-width)?\s*:\s*([^;}]+)/i;
|
|
3709
|
+
var RADIUS = /(?<![-\w])border-radius\s*:\s*([^;}]+)/i;
|
|
3710
|
+
var roundedAccentBorder = {
|
|
3711
|
+
name: "rounded-accent-border",
|
|
3712
|
+
appliesTo: (file) => isStyleBearing(file),
|
|
3713
|
+
run(source, file, ctx) {
|
|
3714
|
+
const hits = [];
|
|
3715
|
+
for (const block of leafBlocks(source)) {
|
|
3716
|
+
const border = BORDER_ALL.exec(block.body);
|
|
3717
|
+
const radius = RADIUS.exec(block.body);
|
|
3718
|
+
if (!border || !radius) continue;
|
|
3719
|
+
const width = Math.max(0, ...[...border[1].matchAll(/(\d*\.?\d+)px/g)].map((w) => parseFloat(w[1])));
|
|
3720
|
+
const r = /%|9999|999px/.test(radius[1]) ? 999 : Math.max(0, ...[...radius[1].matchAll(/(\d*\.?\d+)(px|rem)/g)].map((w) => px(w[0]) ?? 0));
|
|
3721
|
+
if (width < 2 || r < 12) continue;
|
|
3722
|
+
const colour = /border-color\s*:\s*([^;}]+)/i.exec(block.body)?.[1] ?? colourIn(border[1]);
|
|
3723
|
+
if (!saturated(colour, ctx.tokens)) continue;
|
|
3724
|
+
hits.push({ line: lineOfOffset(block, border.index), message: `a ${width}px coloured border on a ${r >= 999 ? "fully" : `${r}px`} rounded element: the outline becomes the most visible shape in it` });
|
|
3725
|
+
}
|
|
3726
|
+
for (const { classes, line } of classLists(file, ctx.raw)) {
|
|
3727
|
+
const plain = classes.map(bare);
|
|
3728
|
+
const width = plain.map((c) => /^border-(\d+|\[[^\]]+\])$/.exec(c)).find((x) => x && twBorderWidth(x[1]) >= 2);
|
|
3729
|
+
const colour = plain.find((c) => /^border-/.test(c) && TW_COLOUR.test(c.slice("border-".length)));
|
|
3730
|
+
const round3 = plain.find((c) => /^rounded-(?:xl|2xl|3xl|full|\[(?:1[2-9]|[2-9]\d|\d{3,})px\])$/.test(c));
|
|
3731
|
+
if (width && colour && round3) hits.push({ line, message: `\`${width[0]} ${colour} ${round3}\`: a heavy coloured outline on a large curve` });
|
|
3732
|
+
}
|
|
3733
|
+
return report(ctx, "rounded-accent-border", file, ctx.raw, hits);
|
|
3734
|
+
}
|
|
3735
|
+
};
|
|
3736
|
+
var BG_IMAGE = /(?<![-\w])background(?:-image)?\s*:\s*([^;}]+)/i;
|
|
3737
|
+
function gridLike(value, block, sizeHint) {
|
|
3738
|
+
if (/repeating-(?:linear|radial)-gradient\(/i.test(value)) return true;
|
|
3739
|
+
const linear = (value.match(/linear-gradient\(/gi) ?? []).length;
|
|
3740
|
+
const radial = (value.match(/radial-gradient\(/gi) ?? []).length;
|
|
3741
|
+
const lines = /\b1px\b|transparent\s+1px|\s1px\s*,\s*transparent/i.test(value);
|
|
3742
|
+
const sized = sizeHint || (block ? /background-size\s*:/i.test(block.body) : false);
|
|
3743
|
+
return sized && (linear >= 2 && lines || radial >= 1 && /\b1px\b|\b2px\b/.test(value));
|
|
3744
|
+
}
|
|
3745
|
+
var decorativeGridBackground = {
|
|
3746
|
+
name: "decorative-grid-background",
|
|
3747
|
+
appliesTo: (file) => isStyleBearing(file),
|
|
3748
|
+
run(source, file, ctx) {
|
|
3749
|
+
const hits = [];
|
|
3750
|
+
for (const block of leafBlocks(source)) {
|
|
3751
|
+
const bg = BG_IMAGE.exec(block.body);
|
|
3752
|
+
if (bg && gridLike(bg[1], block, false)) {
|
|
3753
|
+
hits.push({ line: lineOfOffset(block, bg.index), message: "a background of grid lines, dots or stripes behind content: noise at the contrast that makes text harder to read" });
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
for (const { classes, line } of classLists(file, ctx.raw)) {
|
|
3757
|
+
const arb = classes.map(bare).filter((c) => /^bg-\[/.test(c)).map((c) => c.replace(/_/g, " "));
|
|
3758
|
+
const image = arb.find((c) => /gradient\(/.test(c));
|
|
3759
|
+
if (image && gridLike(image, void 0, arb.some((c) => /^bg-\[size:/.test(c)) || /repeating-/.test(image))) {
|
|
3760
|
+
hits.push({ line, message: "a grid, dot or stripe background drawn with utility classes: decoration behind content" });
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
return report(ctx, "decorative-grid-background", file, ctx.raw, hits);
|
|
3764
|
+
}
|
|
3765
|
+
};
|
|
3766
|
+
var SHADOW = /(?<![-\w])(box-shadow|text-shadow)\s*:\s*([^;}]+)/gi;
|
|
3767
|
+
var DROP = /drop-shadow\(\s*0(?:px)?\s+0(?:px)?\s+(\d*\.?\d+)px\s+([^)]+\)?)\s*\)/gi;
|
|
3768
|
+
var COLOUR_IN = /(?:rgba?|hsla?|oklch|oklab|lab|lch|color)\([^)]*\)|#[0-9a-f]{3,8}\b|var\([^)]*\)|\b[a-z]{3,}\b/i;
|
|
3769
|
+
function glowLayers(value, tokens) {
|
|
3770
|
+
return value.split(/,(?![^(]*\))/).some((layer) => {
|
|
3771
|
+
if (/\binset\b/.test(layer)) return false;
|
|
3772
|
+
const colour = COLOUR_IN.exec(layer.replace(/\binset\b/, ""))?.[0];
|
|
3773
|
+
if (!colour) return false;
|
|
3774
|
+
const nums = [...layer.replace(colour, " ").matchAll(/-?\d*\.?\d+/g)].map((n) => parseFloat(n[0]));
|
|
3775
|
+
if (nums.length < 3) return false;
|
|
3776
|
+
const [x, y, blur] = nums;
|
|
3777
|
+
if (x !== 0 || y !== 0 || blur < 8) return false;
|
|
3778
|
+
return saturated(colour, tokens, 40, 0.15);
|
|
3779
|
+
});
|
|
3780
|
+
}
|
|
3781
|
+
var glowAccent = {
|
|
3782
|
+
name: "glow-accent",
|
|
3783
|
+
appliesTo: (file) => isStyleBearing(file),
|
|
3784
|
+
run(source, file, ctx) {
|
|
3785
|
+
const hits = [];
|
|
3786
|
+
for (const block of leafBlocks(source)) {
|
|
3787
|
+
SHADOW.lastIndex = 0;
|
|
3788
|
+
let m;
|
|
3789
|
+
while (m = SHADOW.exec(block.body)) {
|
|
3790
|
+
if (glowLayers(m[2], ctx.tokens)) hits.push({ line: lineOfOffset(block, m.index), message: `a coloured ${m[1]} glow: contrast added without meaning` });
|
|
3791
|
+
}
|
|
3792
|
+
DROP.lastIndex = 0;
|
|
3793
|
+
while (m = DROP.exec(block.body)) {
|
|
3794
|
+
if (parseFloat(m[1]) >= 8 && saturated(m[2], ctx.tokens, 40)) hits.push({ line: lineOfOffset(block, m.index), message: "a coloured drop-shadow glow: contrast added without meaning" });
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
for (const { classes, line } of classLists(file, ctx.raw)) {
|
|
3798
|
+
const plain = classes.map(bare);
|
|
3799
|
+
const arb = plain.find((c) => /^(?:shadow|drop-shadow)-\[/.test(c) && glowLayers(c.replace(/^[\w-]+-\[|\]$/g, "").replace(/_/g, " "), ctx.tokens));
|
|
3800
|
+
const tinted = plain.find((c) => /^shadow-/.test(c) && TW_COLOUR.test(c.slice("shadow-".length)));
|
|
3801
|
+
if (arb) hits.push({ line, message: `\`${arb}\`: a coloured glow` });
|
|
3802
|
+
else if (tinted && plain.some((c) => /^shadow-(?:lg|xl|2xl)$/.test(c))) hits.push({ line, message: `\`${tinted}\`: a large shadow in a saturated colour, which reads as a glow` });
|
|
3803
|
+
}
|
|
3804
|
+
return report(ctx, "glow-accent", file, ctx.raw, hits);
|
|
3805
|
+
}
|
|
3806
|
+
};
|
|
3807
|
+
var PAGE_SELECTOR = /(?:^|[\s,>])(?:html|body|:root|main)\b|--[\w-]*(?:bg|background|surface|page|canvas)[\w-]*/i;
|
|
3808
|
+
var TW_CREAM = /^bg-(?:amber|orange|yellow)-(?:50|100)$|^bg-stone-(?:100|200)$/;
|
|
3809
|
+
function cream(value, tokens) {
|
|
3810
|
+
const c = extractColorComponents(value.trim(), tokens);
|
|
3811
|
+
if (!c || c.alpha < 0.9) return false;
|
|
3812
|
+
const { r, g, b } = c.rgb;
|
|
3813
|
+
return Math.min(r, g, b) >= 215 && r >= g && g >= b && r - b >= 6;
|
|
3814
|
+
}
|
|
3815
|
+
var creamPalette = {
|
|
3816
|
+
name: "cream-palette",
|
|
3817
|
+
appliesTo: (file) => isStyleBearing(file),
|
|
3818
|
+
run(source, file, ctx) {
|
|
3819
|
+
const hits = [];
|
|
3820
|
+
for (const block of leafBlocks(source)) {
|
|
3821
|
+
for (const m of block.body.matchAll(/(?:^|[;{\s])((?:--[\w-]+|background(?:-color)?))\s*:\s*([^;}]+)/g)) {
|
|
3822
|
+
const prop = m[1];
|
|
3823
|
+
const onPage = prop.startsWith("--") ? PAGE_SELECTOR.test(prop) : PAGE_SELECTOR.test(block.selector);
|
|
3824
|
+
if (!onPage || !cream(m[2], ctx.tokens)) continue;
|
|
3825
|
+
hits.push({ line: lineOfOffset(block, m.index), message: `a cream page surface (${m[2].trim()}): the palette a model reaches for when asked to look tasteful` });
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
if (isMarkupHost(file)) {
|
|
3829
|
+
for (const m of ctx.raw.matchAll(/<(html|body|main)\b[^>]*\b(?:class|className)\s*=\s*["']([^"']*)["']/gi)) {
|
|
3830
|
+
const hit = m[2].split(/\s+/).map(bare).find((c) => TW_CREAM.test(c) || /^bg-\[#?[\w(),.%\s]+\]$/.test(c) && cream(c.slice(4, -1), ctx.tokens));
|
|
3831
|
+
if (hit) hits.push({ line: lineForOffset(buildLineIndex(ctx.raw), m.index), message: `\`${hit}\` on <${m[1]}>: a cream page surface by default` });
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
return report(ctx, "cream-palette", file, ctx.raw, hits);
|
|
3835
|
+
}
|
|
3836
|
+
};
|
|
3837
|
+
var pulsingDot = {
|
|
3838
|
+
name: "pulsing-dot",
|
|
3839
|
+
appliesTo: (file) => isStyleBearing(file),
|
|
3840
|
+
run(source, file, ctx) {
|
|
3841
|
+
const hits = [];
|
|
3842
|
+
const pulsingClasses = /* @__PURE__ */ new Set();
|
|
3843
|
+
for (const block of leafBlocks(source)) {
|
|
3844
|
+
const anim = /(?<![-\w])animation(?:-name)?\s*:\s*([^;}]+)/i.exec(block.body);
|
|
3845
|
+
if (!anim || !/\b(?:pulse|ping|blink|breathe|beacon|glow)[\w-]*/i.test(anim[1])) continue;
|
|
3846
|
+
if (!/infinite/i.test(anim[1]) && !/animation-iteration-count\s*:\s*infinite/i.test(block.body)) continue;
|
|
3847
|
+
const single = /^\s*\.([\w-]+)\s*$/.exec(block.selector);
|
|
3848
|
+
if (single) pulsingClasses.add(single[1]);
|
|
3849
|
+
if (!/border-radius\s*:\s*(?:50%|9999px|999px|100%)/i.test(block.body)) continue;
|
|
3850
|
+
hits.push({ line: lineOfOffset(block, anim.index), message: "a round element pulsing forever: an alarm on a state that is not changing" });
|
|
3851
|
+
}
|
|
3852
|
+
for (const { classes, line } of classLists(file, ctx.raw)) {
|
|
3853
|
+
const plain = classes.map(bare);
|
|
3854
|
+
const ping = plain.includes("animate-ping");
|
|
3855
|
+
const dot = plain.includes("rounded-full") && plain.some((c) => /^(?:size|w|h)-(?:1|1\.5|2|2\.5|3|3\.5)$/.test(c));
|
|
3856
|
+
const pulsing = plain.includes("animate-pulse") || plain.some((c) => pulsingClasses.has(c));
|
|
3857
|
+
if (ping || pulsing && dot) hits.push({ line, message: `a status dot that ${ping ? "pings" : "pulses"} forever: motion on a state that is not changing` });
|
|
3858
|
+
}
|
|
3859
|
+
return report(ctx, "pulsing-dot", file, ctx.raw, hits);
|
|
3860
|
+
}
|
|
3861
|
+
};
|
|
3862
|
+
var HEADLINE_SELECTOR = /\bh1\b|\bh2\b|hero|display|headline|title/i;
|
|
3863
|
+
var SERIF = /font-family\s*:\s*([^;}]+)/i;
|
|
3864
|
+
function serifFamily(value) {
|
|
3865
|
+
const first = value.split(",")[0].trim().toLowerCase();
|
|
3866
|
+
if (/sans|mono|system-ui|ui-sans|inter\b|helvetica|arial/.test(first)) return false;
|
|
3867
|
+
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());
|
|
3868
|
+
}
|
|
3869
|
+
var italicSerifDisplay = {
|
|
3870
|
+
name: "italic-serif-display",
|
|
3871
|
+
appliesTo: (file) => isStyleBearing(file),
|
|
3872
|
+
run(source, file, ctx) {
|
|
3873
|
+
const hits = [];
|
|
3874
|
+
const serifClasses = /* @__PURE__ */ new Set();
|
|
3875
|
+
for (const block of leafBlocks(source)) {
|
|
3876
|
+
const family = SERIF.exec(block.body);
|
|
3877
|
+
const single = /^\s*\.([\w-]+)\s*$/.exec(block.selector);
|
|
3878
|
+
if (family && single && serifFamily(family[1])) serifClasses.add(single[1]);
|
|
3879
|
+
}
|
|
3880
|
+
for (const block of leafBlocks(source)) {
|
|
3881
|
+
if (!HEADLINE_SELECTOR.test(block.selector) && !/\bh1\b.*\b(?:em|i)\b/.test(block.selector)) continue;
|
|
3882
|
+
const italic = /font-style\s*:\s*italic/i.exec(block.body);
|
|
3883
|
+
const family = SERIF.exec(block.body);
|
|
3884
|
+
if (italic && family && serifFamily(family[1])) {
|
|
3885
|
+
hits.push({ line: lineOfOffset(block, italic.index), message: `${block.selector.trim()} is an italic serif headline: the shortcut to an editorial look` });
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
if (isMarkupHost(file)) {
|
|
3889
|
+
const starts = buildLineIndex(ctx.raw);
|
|
3890
|
+
for (const m of ctx.raw.matchAll(/<(h[12])\b([^>]*)>([\s\S]{0,400}?)<\/\1\s*>/gi)) {
|
|
3891
|
+
const own = (/\b(?:class|className)\s*=\s*["']([^"']*)["']/.exec(m[2])?.[1] ?? "").split(/\s+/).map(bare);
|
|
3892
|
+
const serif = own.includes("font-serif") || own.some((c) => serifClasses.has(c) || /^font-\[.*serif/i.test(c) && !/sans/i.test(c));
|
|
3893
|
+
const italicSelf = own.includes("italic");
|
|
3894
|
+
const italicChild = /<(em|i)\b(?![^>]*\bnot-italic\b)/i.test(m[3]) || /\bclass(?:Name)?\s*=\s*["'][^"']*\bitalic\b/.test(m[3]);
|
|
3895
|
+
const childSerif = /\bclass(?:Name)?\s*=\s*["'][^"']*\bfont-serif\b/.test(m[3]);
|
|
3896
|
+
if (serif && (italicSelf || italicChild) || italicChild && childSerif) {
|
|
3897
|
+
hits.push({ line: lineForOffset(starts, m.index), message: `an italic serif <${m[1].toLowerCase()}>: the shortcut to an editorial look` });
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
}
|
|
3901
|
+
return report(ctx, "italic-serif-display", file, ctx.raw, hits);
|
|
3902
|
+
}
|
|
3903
|
+
};
|
|
3904
|
+
var BEFORE_HEADING = /<(p|span|div|small|a)\b([^>]*)>((?:[^<]|<\/?(?:span|svg|path|circle|i|b|strong|em|img)\b[^>]*>){1,400}?)<\/\1\s*>\s*(?:<[^>]*>\s*){0,2}<(h[1-3])\b/gi;
|
|
3905
|
+
var LABEL_CLASS = /\b(?:eyebrow|kicker|overline|pretitle|pre-title|tagline|section-label|superscript-label)\b/i;
|
|
3906
|
+
function labelKind(attrs) {
|
|
3907
|
+
const cls = /\b(?:class|className)\s*=\s*["']([^"']*)["']/.exec(attrs)?.[1] ?? "";
|
|
3908
|
+
const plain = cls.split(/\s+/).map(bare);
|
|
3909
|
+
const pill = plain.includes("rounded-full") && plain.some((c) => /^(?:border|bg-|ring)/.test(c)) && plain.some((c) => /^(?:px|py|p)-/.test(c));
|
|
3910
|
+
if (pill) return "chip";
|
|
3911
|
+
const caps = plain.includes("uppercase") && plain.some((c) => /^tracking-/.test(c) && !/tracking-(?:tight|tighter|normal)/.test(c));
|
|
3912
|
+
if (caps || LABEL_CLASS.test(cls)) return "kicker";
|
|
3913
|
+
return void 0;
|
|
3914
|
+
}
|
|
3915
|
+
function labelHits(raw) {
|
|
3916
|
+
const out = [];
|
|
3917
|
+
for (const m of raw.matchAll(BEFORE_HEADING)) {
|
|
3918
|
+
const kind = labelKind(m[2]);
|
|
3919
|
+
const text = m[3].replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
|
|
3920
|
+
if (kind && text && text.length <= 60) out.push({ kind, heading: m[4].toLowerCase(), offset: m.index, text });
|
|
3921
|
+
}
|
|
3922
|
+
return out;
|
|
3923
|
+
}
|
|
3924
|
+
var headingKicker = {
|
|
3925
|
+
name: "heading-kicker",
|
|
3926
|
+
appliesTo: (file) => isReaderText(file) && isMarkupHost(file),
|
|
3927
|
+
run(_source, file, ctx) {
|
|
3928
|
+
const kickers = labelHits(ctx.raw).filter((h) => h.kind === "kicker");
|
|
3929
|
+
if (kickers.length < 2) return [];
|
|
3930
|
+
const starts = buildLineIndex(ctx.raw);
|
|
3931
|
+
return report(ctx, "heading-kicker", file, ctx.raw, kickers.map((k) => ({
|
|
3932
|
+
line: lineForOffset(starts, k.offset),
|
|
3933
|
+
message: `"${k.text}" as a small caps line over an <${k.heading}>, one of ${kickers.length} in this file: a kicker over every heading`
|
|
3934
|
+
})));
|
|
3935
|
+
}
|
|
3936
|
+
};
|
|
3937
|
+
var eyebrowChip = {
|
|
3938
|
+
name: "eyebrow-chip",
|
|
3939
|
+
appliesTo: (file) => isReaderText(file) && isMarkupHost(file),
|
|
3940
|
+
run(_source, file, ctx) {
|
|
3941
|
+
const starts = buildLineIndex(ctx.raw);
|
|
3942
|
+
return report(ctx, "eyebrow-chip", file, ctx.raw, labelHits(ctx.raw).filter((h) => h.kind === "chip" && h.heading === "h1").map((h) => ({ line: lineForOffset(starts, h.offset), message: `"${h.text}" in a pill over the headline: the shape of a control, pressed and doing nothing` })));
|
|
3943
|
+
}
|
|
3944
|
+
};
|
|
3945
|
+
var NUMBER_LABEL = /<(span|p|div|small)\b[^>]*>\s*(?:\(|№\s*)?0[1-9](?:\s*[./)]|\s*\/\s*\d{2})?\s*<\/\1\s*>\s*(?:<[^>]*>\s*){0,2}<h[2-4]\b|<h[2-4]\b[^>]*>\s*(?:<[^>]+>\s*)?0[1-9]\s*(?:<\/[^>]+>)?\s*[.·—–/|:]?\s*(?:<[^>]+>)?\s*[A-Za-z]/gi;
|
|
3946
|
+
var numberedSectionLabels = {
|
|
3947
|
+
name: "numbered-section-labels",
|
|
3948
|
+
appliesTo: (file) => isReaderText(file) && isMarkupHost(file),
|
|
3949
|
+
run(_source, file, ctx) {
|
|
3950
|
+
const raw = ctx.raw.replace(/<ol\b[\s\S]*?<\/ol\s*>/gi, (m) => m.replace(/[^\n]/g, " "));
|
|
3951
|
+
const found = [...raw.matchAll(NUMBER_LABEL)];
|
|
3952
|
+
if (found.length < 2) return [];
|
|
3953
|
+
const starts = buildLineIndex(ctx.raw);
|
|
3954
|
+
return report(ctx, "numbered-section-labels", file, ctx.raw, found.map((m) => ({
|
|
3955
|
+
line: lineForOffset(starts, m.index),
|
|
3956
|
+
message: `a "0N" label on a section heading, one of ${found.length} here: a number promises an order`
|
|
3957
|
+
})));
|
|
3958
|
+
}
|
|
3959
|
+
};
|
|
3960
|
+
var generatedLookDetectors = [
|
|
3961
|
+
sideTabBorder,
|
|
3962
|
+
roundedAccentBorder,
|
|
3963
|
+
decorativeGridBackground,
|
|
3964
|
+
glowAccent,
|
|
3965
|
+
creamPalette,
|
|
3966
|
+
pulsingDot,
|
|
3967
|
+
italicSerifDisplay,
|
|
3968
|
+
headingKicker,
|
|
3969
|
+
eyebrowChip,
|
|
3970
|
+
numberedSectionLabels
|
|
3971
|
+
];
|
|
3972
|
+
|
|
3638
3973
|
// src/check/registry.ts
|
|
3639
3974
|
var DETECTORS = [
|
|
3640
3975
|
gradientText,
|
|
@@ -3656,7 +3991,8 @@ var DETECTORS = [
|
|
|
3656
3991
|
emDash,
|
|
3657
3992
|
semanticElement,
|
|
3658
3993
|
metadata,
|
|
3659
|
-
interfaceSafety
|
|
3994
|
+
interfaceSafety,
|
|
3995
|
+
...generatedLookDetectors
|
|
3660
3996
|
];
|
|
3661
3997
|
function getDetector(name) {
|
|
3662
3998
|
return DETECTORS.find((d) => d.name === name);
|
|
@@ -3689,7 +4025,7 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
|
|
|
3689
4025
|
}
|
|
3690
4026
|
|
|
3691
4027
|
// src/check/tokens.ts
|
|
3692
|
-
import { existsSync as
|
|
4028
|
+
import { existsSync as existsSync11, readdirSync as readdirSync8, readFileSync as readFileSync14 } from "fs";
|
|
3693
4029
|
import { join as join19 } from "path";
|
|
3694
4030
|
function rootDeclarations(source) {
|
|
3695
4031
|
const out = {};
|
|
@@ -3706,7 +4042,7 @@ function rootDeclarations(source) {
|
|
|
3706
4042
|
function loadTokenMap(projectRoot, consumerFiles = []) {
|
|
3707
4043
|
const jig = {};
|
|
3708
4044
|
const tokensDir = join19(projectRoot, ".jig", "tokens");
|
|
3709
|
-
if (
|
|
4045
|
+
if (existsSync11(tokensDir)) {
|
|
3710
4046
|
for (const file of readdirSync8(tokensDir).filter((f) => f.endsWith(".css")).sort()) {
|
|
3711
4047
|
Object.assign(jig, rootDeclarations(readFileSync14(join19(tokensDir, file), "utf8")));
|
|
3712
4048
|
}
|
|
@@ -3737,7 +4073,7 @@ function loadTokenMap(projectRoot, consumerFiles = []) {
|
|
|
3737
4073
|
}
|
|
3738
4074
|
|
|
3739
4075
|
// src/check/exempt.ts
|
|
3740
|
-
import { existsSync as
|
|
4076
|
+
import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
|
|
3741
4077
|
import { join as join20 } from "path";
|
|
3742
4078
|
function globToRegExp(glob) {
|
|
3743
4079
|
let out = "";
|
|
@@ -3758,7 +4094,7 @@ function globToRegExp(glob) {
|
|
|
3758
4094
|
}
|
|
3759
4095
|
function readExemptions(projectRoot) {
|
|
3760
4096
|
const configPath = join20(projectRoot, "jig.config.json");
|
|
3761
|
-
if (!
|
|
4097
|
+
if (!existsSync12(configPath)) return [];
|
|
3762
4098
|
let declared;
|
|
3763
4099
|
try {
|
|
3764
4100
|
declared = JSON.parse(readFileSync15(configPath, "utf8")).exempt;
|
|
@@ -3874,7 +4210,7 @@ function hasMenuToggle(raw) {
|
|
|
3874
4210
|
}
|
|
3875
4211
|
|
|
3876
4212
|
// src/check/mode-wiring.ts
|
|
3877
|
-
import { existsSync as
|
|
4213
|
+
import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
|
|
3878
4214
|
import { dirname as dirname6, join as join22 } from "path";
|
|
3879
4215
|
var MODES = ["editorial", "product", "operator"];
|
|
3880
4216
|
function configProblems(config) {
|
|
@@ -3898,7 +4234,7 @@ function configProblems(config) {
|
|
|
3898
4234
|
function modeWiringProblems(projectRoot) {
|
|
3899
4235
|
let config;
|
|
3900
4236
|
const path = join22(projectRoot, "jig.config.json");
|
|
3901
|
-
if (!
|
|
4237
|
+
if (!existsSync13(path)) return [];
|
|
3902
4238
|
try {
|
|
3903
4239
|
config = JSON.parse(readFileSync17(path, "utf8"));
|
|
3904
4240
|
} catch {
|
|
@@ -3913,10 +4249,10 @@ function modeWiringProblems(projectRoot) {
|
|
|
3913
4249
|
const rel = (f) => dir === "." ? f : `${dir}/${f}`;
|
|
3914
4250
|
const multi = modes.length > 1;
|
|
3915
4251
|
const barrelOf = (mode) => multi ? `theme.${mode}.css` : "theme.css";
|
|
3916
|
-
const anyBarrel = ["theme.css", ...modes.map((m) => `theme.${m}.css`)].some((f) =>
|
|
4252
|
+
const anyBarrel = ["theme.css", ...modes.map((m) => `theme.${m}.css`)].some((f) => existsSync13(join22(projectRoot, rel(f))));
|
|
3917
4253
|
if (!anyBarrel) return [];
|
|
3918
4254
|
const problems = [];
|
|
3919
|
-
if (multi &&
|
|
4255
|
+
if (multi && existsSync13(join22(projectRoot, rel("theme.css")))) {
|
|
3920
4256
|
problems.push({
|
|
3921
4257
|
mode: modes[0],
|
|
3922
4258
|
barrel: rel("theme.css"),
|
|
@@ -3940,7 +4276,7 @@ function modeWiringProblems(projectRoot) {
|
|
|
3940
4276
|
barrel,
|
|
3941
4277
|
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`
|
|
3942
4278
|
});
|
|
3943
|
-
} else if (!
|
|
4279
|
+
} else if (!existsSync13(join22(projectRoot, rel(modeFile)))) {
|
|
3944
4280
|
problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${rel(modeFile)} does not exist` });
|
|
3945
4281
|
}
|
|
3946
4282
|
});
|
|
@@ -3948,7 +4284,7 @@ function modeWiringProblems(projectRoot) {
|
|
|
3948
4284
|
}
|
|
3949
4285
|
|
|
3950
4286
|
// src/check/token-audit.ts
|
|
3951
|
-
import { existsSync as
|
|
4287
|
+
import { existsSync as existsSync14, readdirSync as readdirSync9, readFileSync as readFileSync18 } from "fs";
|
|
3952
4288
|
import { join as join23 } from "path";
|
|
3953
4289
|
var TEXT_FLOOR = 4.5;
|
|
3954
4290
|
var UI_FLOOR = 3;
|
|
@@ -4011,7 +4347,7 @@ function tokenFiles(projectRoot) {
|
|
|
4011
4347
|
}
|
|
4012
4348
|
};
|
|
4013
4349
|
const statePath = join23(projectRoot, ".jig", "state.json");
|
|
4014
|
-
if (
|
|
4350
|
+
if (existsSync14(statePath)) {
|
|
4015
4351
|
try {
|
|
4016
4352
|
const state = JSON.parse(readFileSync18(statePath, "utf8"));
|
|
4017
4353
|
const recorded = Object.keys(state.files ?? {}).filter((f) => f.endsWith(".css")).sort();
|
|
@@ -4021,7 +4357,7 @@ function tokenFiles(projectRoot) {
|
|
|
4021
4357
|
}
|
|
4022
4358
|
}
|
|
4023
4359
|
const legacy = join23(projectRoot, ".jig", "tokens");
|
|
4024
|
-
if (!
|
|
4360
|
+
if (!existsSync14(legacy)) return [];
|
|
4025
4361
|
return readdirSync9(legacy).filter((f) => f.endsWith(".css")).sort().map((f) => ({ path: `.jig/tokens/${f}`, source: readFileSync18(join23(legacy, f), "utf8") }));
|
|
4026
4362
|
}
|
|
4027
4363
|
function auditTokenLayer(projectRoot) {
|
|
@@ -4071,9 +4407,9 @@ function auditTokenLayer(projectRoot) {
|
|
|
4071
4407
|
for (const { token, min, why } of NUMERIC_FLOORS) {
|
|
4072
4408
|
const decl = scope[token];
|
|
4073
4409
|
if (!decl) continue;
|
|
4074
|
-
const
|
|
4075
|
-
if (!
|
|
4076
|
-
const value = Number(
|
|
4410
|
+
const px2 = /^([\d.]+)px$/.exec(decl.value.trim());
|
|
4411
|
+
if (!px2) continue;
|
|
4412
|
+
const value = Number(px2[1]);
|
|
4077
4413
|
if (value >= min) continue;
|
|
4078
4414
|
problems.push({
|
|
4079
4415
|
token,
|
|
@@ -4093,7 +4429,7 @@ function auditTokenLayer(projectRoot) {
|
|
|
4093
4429
|
var LEGACY_INDEX_REL = join24(".jig", "rules.index.json");
|
|
4094
4430
|
function resolveIndexPath(projectRoot) {
|
|
4095
4431
|
const legacy = join24(projectRoot, LEGACY_INDEX_REL);
|
|
4096
|
-
if (
|
|
4432
|
+
if (existsSync15(legacy)) return legacy;
|
|
4097
4433
|
return join24(assetRoot(), "rules.index.json");
|
|
4098
4434
|
}
|
|
4099
4435
|
function resolveMode(projectRoot) {
|
|
@@ -4203,7 +4539,7 @@ function check(opts) {
|
|
|
4203
4539
|
}
|
|
4204
4540
|
})
|
|
4205
4541
|
);
|
|
4206
|
-
const
|
|
4542
|
+
const report2 = formatReport(findings, {
|
|
4207
4543
|
totalRules: index.length,
|
|
4208
4544
|
// From the CLI's own bundle, like `explain` — the spec count describes the
|
|
4209
4545
|
// system, not whatever a project happens to have vendored. A legacy project
|
|
@@ -4221,11 +4557,11 @@ function check(opts) {
|
|
|
4221
4557
|
modeUnwired: modeWiringProblems(opts.projectRoot),
|
|
4222
4558
|
waived
|
|
4223
4559
|
});
|
|
4224
|
-
return { findings, report, hasError, waived };
|
|
4560
|
+
return { findings, report: report2, hasError, waived };
|
|
4225
4561
|
}
|
|
4226
4562
|
|
|
4227
4563
|
// src/commands/init.ts
|
|
4228
|
-
import { existsSync as
|
|
4564
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync6, readdirSync as readdirSync10, readFileSync as readFileSync23, rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
|
|
4229
4565
|
import { dirname as dirname7, isAbsolute, join as join28, resolve as resolve2, sep as sep3 } from "path";
|
|
4230
4566
|
|
|
4231
4567
|
// src/init/detect.ts
|
|
@@ -4749,7 +5085,7 @@ function parseSurfaces(answer, fallback) {
|
|
|
4749
5085
|
return parsed.length > 0 ? parsed : fallback;
|
|
4750
5086
|
}
|
|
4751
5087
|
function fileState(projectRoot, absPath, relPath, initManifest) {
|
|
4752
|
-
const existsOnDisk =
|
|
5088
|
+
const existsOnDisk = existsSync16(absPath);
|
|
4753
5089
|
const tracked = !!initManifest && relPath in initManifest.files;
|
|
4754
5090
|
const modified = tracked && isInitFileModified(projectRoot, relPath, initManifest);
|
|
4755
5091
|
return { existsOnDisk, tracked, modified };
|
|
@@ -4760,11 +5096,11 @@ function isValidSurfaceArray(v) {
|
|
|
4760
5096
|
);
|
|
4761
5097
|
}
|
|
4762
5098
|
function loadEffectiveConfig(projectRoot, configAbsPath, fallbackBrand, fallbackSurfaces) {
|
|
4763
|
-
if (!
|
|
5099
|
+
if (!existsSync16(configAbsPath)) return { brand: fallbackBrand, surfaces: fallbackSurfaces };
|
|
4764
5100
|
try {
|
|
4765
5101
|
const parsed = JSON.parse(readFileSync23(configAbsPath, "utf8"));
|
|
4766
5102
|
const brandCandidate = typeof parsed.brand === "string" && parsed.brand ? parsed.brand : void 0;
|
|
4767
|
-
const brandResolves = brandCandidate ?
|
|
5103
|
+
const brandResolves = brandCandidate ? existsSync16(join28(projectRoot, ...brandCandidate.split("/"))) : false;
|
|
4768
5104
|
return {
|
|
4769
5105
|
brand: brandResolves ? brandCandidate : fallbackBrand,
|
|
4770
5106
|
surfaces: isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : fallbackSurfaces
|
|
@@ -4774,7 +5110,7 @@ function loadEffectiveConfig(projectRoot, configAbsPath, fallbackBrand, fallback
|
|
|
4774
5110
|
}
|
|
4775
5111
|
}
|
|
4776
5112
|
function configuredBrandPath(projectRoot, configAbsPath) {
|
|
4777
|
-
if (!
|
|
5113
|
+
if (!existsSync16(configAbsPath)) return { path: null };
|
|
4778
5114
|
let declared;
|
|
4779
5115
|
try {
|
|
4780
5116
|
declared = JSON.parse(readFileSync23(configAbsPath, "utf8")).brand;
|
|
@@ -4805,7 +5141,7 @@ function defaultTokenDir(detection) {
|
|
|
4805
5141
|
return ["jig"];
|
|
4806
5142
|
}
|
|
4807
5143
|
function readConfiguredSurfaces(configAbsPath) {
|
|
4808
|
-
if (!
|
|
5144
|
+
if (!existsSync16(configAbsPath)) return null;
|
|
4809
5145
|
try {
|
|
4810
5146
|
const parsed = JSON.parse(readFileSync23(configAbsPath, "utf8"));
|
|
4811
5147
|
return isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : null;
|
|
@@ -4818,7 +5154,7 @@ function isCssModule(path) {
|
|
|
4818
5154
|
}
|
|
4819
5155
|
function legacyBrandFile(projectRoot) {
|
|
4820
5156
|
const dir = join28(projectRoot, ".jig", "tokens");
|
|
4821
|
-
if (!
|
|
5157
|
+
if (!existsSync16(dir)) return void 0;
|
|
4822
5158
|
const found = readdirSync10(dir).find((f) => /^brand\.[\w.-]+\.css$/.test(f));
|
|
4823
5159
|
return found ? relKey(".jig", "tokens", found) : void 0;
|
|
4824
5160
|
}
|
|
@@ -4849,7 +5185,7 @@ var LEGACY_PAIR_RE = new RegExp(
|
|
|
4849
5185
|
`@import\\s+["'][^"']*brand\\.[\\w.-]+\\.css["'];?\\r?\\n?\\s*@import\\s+["'][^"']*mode\\.(?:${MODES2.join("|")})\\.css["'];?\\r?\\n?`
|
|
4850
5186
|
);
|
|
4851
5187
|
function wireBarrel(absPath, barrelImport) {
|
|
4852
|
-
const content =
|
|
5188
|
+
const content = existsSync16(absPath) ? readFileSync23(absPath, "utf8") : "";
|
|
4853
5189
|
const line = `@import "${barrelImport}";`;
|
|
4854
5190
|
if (content.includes(line)) return { status: "already-present" };
|
|
4855
5191
|
const legacy = LEGACY_PAIR_RE.exec(content);
|
|
@@ -4877,10 +5213,10 @@ async function init(opts) {
|
|
|
4877
5213
|
"'jig init' asks questions and stdin is not a terminal, so it cannot. Re-run with --yes to accept the derived defaults, or run it in a terminal. A jig.config.json does not replace --yes, because this check runs before it is read. With both, init takes the mode from the config instead of deriving it."
|
|
4878
5214
|
);
|
|
4879
5215
|
}
|
|
4880
|
-
const migrateLegacy = async (
|
|
4881
|
-
if (!
|
|
4882
|
-
for (const line of describe(
|
|
4883
|
-
const removable = removableLegacyFiles(
|
|
5216
|
+
const migrateLegacy = async (report2, describe) => {
|
|
5217
|
+
if (!report2.present) return;
|
|
5218
|
+
for (const line of describe(report2)) log(line);
|
|
5219
|
+
const removable = removableLegacyFiles(report2);
|
|
4884
5220
|
if (removable.length === 0) return;
|
|
4885
5221
|
if (opts.yes) {
|
|
4886
5222
|
log(" Re-run without --yes to be prompted for removal, or delete them yourself.");
|
|
@@ -5055,7 +5391,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5055
5391
|
existing = {};
|
|
5056
5392
|
}
|
|
5057
5393
|
const existingBrand = typeof existing.brand === "string" ? existing.brand : void 0;
|
|
5058
|
-
const brandStillResolves = existingBrand ?
|
|
5394
|
+
const brandStillResolves = existingBrand ? existsSync16(join28(opts.projectRoot, ...existingBrand.split("/"))) : false;
|
|
5059
5395
|
const existingSurfaces = Array.isArray(existing.surfaces) && existing.surfaces.length > 0 ? existing.surfaces : void 0;
|
|
5060
5396
|
const merged = {
|
|
5061
5397
|
brand: brandStillResolves ? existingBrand : brandRelPath,
|
|
@@ -5112,7 +5448,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5112
5448
|
if (multiMode) {
|
|
5113
5449
|
const rel = relKey(...tokensRelDir, "theme.css");
|
|
5114
5450
|
const abs = join28(opts.projectRoot, ...tokensRelDir, "theme.css");
|
|
5115
|
-
if (
|
|
5451
|
+
if (existsSync16(abs)) {
|
|
5116
5452
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
5117
5453
|
if (state.tracked && !state.modified) {
|
|
5118
5454
|
rmSync2(abs);
|
|
@@ -5184,7 +5520,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5184
5520
|
if (relocateTo && currentBrand) {
|
|
5185
5521
|
const oldDir = currentBrand.split("/").slice(0, -1);
|
|
5186
5522
|
const oldAbsDir = join28(opts.projectRoot, ...oldDir);
|
|
5187
|
-
if (
|
|
5523
|
+
if (existsSync16(oldAbsDir)) {
|
|
5188
5524
|
for (const name of readdirSync10(oldAbsDir).filter((f) => f.endsWith(".css"))) {
|
|
5189
5525
|
const rel = relKey(...oldDir, name);
|
|
5190
5526
|
const abs = join28(oldAbsDir, name);
|
|
@@ -5201,14 +5537,14 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5201
5537
|
const wireTarget = findWireTarget(detection);
|
|
5202
5538
|
let wiring;
|
|
5203
5539
|
if (multiMode) {
|
|
5204
|
-
const
|
|
5540
|
+
const bare2 = join28(opts.projectRoot, ...tokensRelDir, "theme.css");
|
|
5205
5541
|
for (const file of detection.cssFiles.filter((f) => !isTokenLayerFile(f))) {
|
|
5206
5542
|
const abs = join28(opts.projectRoot, file);
|
|
5207
5543
|
try {
|
|
5208
5544
|
const before = readFileSync23(abs, "utf8");
|
|
5209
5545
|
const after = before.split("\n").filter((l) => {
|
|
5210
5546
|
const m = /^\s*@import\s+["']([^"']+)["'];?\s*$/.exec(l);
|
|
5211
|
-
return !(m && !/^[a-z]+:|^\//i.test(m[1]) && resolve2(dirname7(abs), m[1]) ===
|
|
5547
|
+
return !(m && !/^[a-z]+:|^\//i.test(m[1]) && resolve2(dirname7(abs), m[1]) === bare2);
|
|
5212
5548
|
}).join("\n");
|
|
5213
5549
|
if (after !== before) {
|
|
5214
5550
|
writeFileSync5(abs, after, "utf8");
|
|
@@ -5237,7 +5573,7 @@ Could not edit ${file}: ${err.message}. Remove its import of ${relKey(...tokensR
|
|
|
5237
5573
|
if (relocateTo && currentBrand) {
|
|
5238
5574
|
const abs = join28(opts.projectRoot, wireTarget);
|
|
5239
5575
|
const oldDirName = currentBrand.split("/").slice(0, -1).join("/");
|
|
5240
|
-
const before =
|
|
5576
|
+
const before = existsSync16(abs) ? readFileSync23(abs, "utf8") : "";
|
|
5241
5577
|
const after = before.split("\n").filter((l) => !(l.includes("@import") && l.includes(oldDirName))).join("\n");
|
|
5242
5578
|
if (after !== before) writeFileSync5(abs, after, "utf8");
|
|
5243
5579
|
}
|
|
@@ -5314,8 +5650,8 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
5314
5650
|
}
|
|
5315
5651
|
|
|
5316
5652
|
// src/commands/verdicts.ts
|
|
5317
|
-
import { existsSync as
|
|
5318
|
-
import { join as
|
|
5653
|
+
import { existsSync as existsSync23, readFileSync as readFileSync29 } from "fs";
|
|
5654
|
+
import { join as join36, resolve as resolve5 } from "path";
|
|
5319
5655
|
|
|
5320
5656
|
// src/rules/citations.ts
|
|
5321
5657
|
import { join as join29 } from "path";
|
|
@@ -5329,13 +5665,13 @@ function citableIds(packageRoot2) {
|
|
|
5329
5665
|
}
|
|
5330
5666
|
|
|
5331
5667
|
// src/probe/check.ts
|
|
5332
|
-
import { existsSync as
|
|
5333
|
-
import { join as
|
|
5668
|
+
import { existsSync as existsSync20, readdirSync as readdirSync13, readFileSync as readFileSync26 } from "fs";
|
|
5669
|
+
import { join as join33 } from "path";
|
|
5334
5670
|
|
|
5335
5671
|
// src/probe/save.ts
|
|
5336
|
-
import { existsSync as
|
|
5672
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync7, readdirSync as readdirSync12, readFileSync as readFileSync25, writeFileSync as writeFileSync6 } from "fs";
|
|
5337
5673
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5338
|
-
import { join as
|
|
5674
|
+
import { join as join32, relative as relative3, resolve as resolve4, sep as sep5 } from "path";
|
|
5339
5675
|
|
|
5340
5676
|
// src/probe/script.ts
|
|
5341
5677
|
var PROBE_VERSION = 5;
|
|
@@ -5488,7 +5824,7 @@ var PROBE_SCRIPT = `(async () => {
|
|
|
5488
5824
|
|
|
5489
5825
|
// src/probe/browser.ts
|
|
5490
5826
|
import { spawn } from "child_process";
|
|
5491
|
-
import { existsSync as
|
|
5827
|
+
import { existsSync as existsSync17, mkdtempSync, readdirSync as readdirSync11, rmSync as rmSync3 } from "fs";
|
|
5492
5828
|
import { homedir, tmpdir } from "os";
|
|
5493
5829
|
import { join as join30 } from "path";
|
|
5494
5830
|
var CHROME_ENV = ["JIG_CHROME", "CHROME_PATH", "PUPPETEER_EXECUTABLE_PATH", "CHROMIUM_PATH"];
|
|
@@ -5526,7 +5862,7 @@ function cachedBrowsers() {
|
|
|
5526
5862
|
["chrome-win64", "chrome.exe"]
|
|
5527
5863
|
]) {
|
|
5528
5864
|
const candidate = join30(root, entry, ...rel);
|
|
5529
|
-
if (
|
|
5865
|
+
if (existsSync17(candidate)) found.push(candidate);
|
|
5530
5866
|
}
|
|
5531
5867
|
}
|
|
5532
5868
|
}
|
|
@@ -5535,13 +5871,13 @@ function cachedBrowsers() {
|
|
|
5535
5871
|
function findChrome() {
|
|
5536
5872
|
for (const key of CHROME_ENV) {
|
|
5537
5873
|
const value = process.env[key];
|
|
5538
|
-
if (value &&
|
|
5874
|
+
if (value && existsSync17(value)) return value;
|
|
5539
5875
|
}
|
|
5540
5876
|
for (const command of CHROME_COMMANDS) {
|
|
5541
5877
|
const resolved = which(command);
|
|
5542
5878
|
if (resolved) return resolved;
|
|
5543
5879
|
}
|
|
5544
|
-
for (const app of CHROME_APPS) if (
|
|
5880
|
+
for (const app of CHROME_APPS) if (existsSync17(app)) return app;
|
|
5545
5881
|
return cachedBrowsers()[0];
|
|
5546
5882
|
}
|
|
5547
5883
|
function which(command) {
|
|
@@ -5549,7 +5885,7 @@ function which(command) {
|
|
|
5549
5885
|
for (const dir of dirs) {
|
|
5550
5886
|
if (!dir) continue;
|
|
5551
5887
|
const candidate = join30(dir, command);
|
|
5552
|
-
if (
|
|
5888
|
+
if (existsSync17(candidate)) return candidate;
|
|
5553
5889
|
}
|
|
5554
5890
|
return void 0;
|
|
5555
5891
|
}
|
|
@@ -5594,8 +5930,13 @@ async function runProbe(opts) {
|
|
|
5594
5930
|
});
|
|
5595
5931
|
return await evaluate(endpoint, opts.url, opts.width, opts.height ?? 900, timeoutMs);
|
|
5596
5932
|
} finally {
|
|
5933
|
+
const exited = child.exitCode !== null ? Promise.resolve() : new Promise((done) => child.once("exit", () => done()));
|
|
5597
5934
|
child.kill();
|
|
5598
|
-
|
|
5935
|
+
await Promise.race([exited, new Promise((done) => setTimeout(done, 5e3))]);
|
|
5936
|
+
try {
|
|
5937
|
+
rmSync3(profile, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
|
5938
|
+
} catch {
|
|
5939
|
+
}
|
|
5599
5940
|
}
|
|
5600
5941
|
}
|
|
5601
5942
|
async function evaluate(endpoint, url, width, height, timeoutMs) {
|
|
@@ -5654,6 +5995,59 @@ async function evaluate(endpoint, url, width, height, timeoutMs) {
|
|
|
5654
5995
|
}
|
|
5655
5996
|
}
|
|
5656
5997
|
|
|
5998
|
+
// src/probe/serve.ts
|
|
5999
|
+
import { createServer } from "http";
|
|
6000
|
+
import { existsSync as existsSync18, readFileSync as readFileSync24, statSync as statSync2 } from "fs";
|
|
6001
|
+
import { extname, join as join31, resolve as resolve3, sep as sep4 } from "path";
|
|
6002
|
+
var TYPES = {
|
|
6003
|
+
".html": "text/html; charset=utf-8",
|
|
6004
|
+
".htm": "text/html; charset=utf-8",
|
|
6005
|
+
".css": "text/css; charset=utf-8",
|
|
6006
|
+
".js": "text/javascript; charset=utf-8",
|
|
6007
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
6008
|
+
".json": "application/json",
|
|
6009
|
+
".txt": "text/plain; charset=utf-8",
|
|
6010
|
+
".xml": "application/xml",
|
|
6011
|
+
".svg": "image/svg+xml",
|
|
6012
|
+
".png": "image/png",
|
|
6013
|
+
".jpg": "image/jpeg",
|
|
6014
|
+
".jpeg": "image/jpeg",
|
|
6015
|
+
".gif": "image/gif",
|
|
6016
|
+
".webp": "image/webp",
|
|
6017
|
+
".avif": "image/avif",
|
|
6018
|
+
".ico": "image/x-icon",
|
|
6019
|
+
".woff": "font/woff",
|
|
6020
|
+
".woff2": "font/woff2",
|
|
6021
|
+
".ttf": "font/ttf",
|
|
6022
|
+
".otf": "font/otf"
|
|
6023
|
+
};
|
|
6024
|
+
async function serveDirectory(dir) {
|
|
6025
|
+
const root = resolve3(dir);
|
|
6026
|
+
const server = createServer((req, res) => {
|
|
6027
|
+
let path = decodeURIComponent((req.url ?? "/").split("?")[0].split("#")[0]);
|
|
6028
|
+
let file = resolve3(join31(root, path));
|
|
6029
|
+
if (file !== root && !file.startsWith(root + sep4)) {
|
|
6030
|
+
res.writeHead(403).end();
|
|
6031
|
+
return;
|
|
6032
|
+
}
|
|
6033
|
+
if (existsSync18(file) && statSync2(file).isDirectory()) file = join31(file, "index.html");
|
|
6034
|
+
else if (!existsSync18(file) && existsSync18(`${file}.html`)) file = `${file}.html`;
|
|
6035
|
+
if (!existsSync18(file)) {
|
|
6036
|
+
res.writeHead(404).end();
|
|
6037
|
+
return;
|
|
6038
|
+
}
|
|
6039
|
+
res.writeHead(200, { "content-type": TYPES[extname(file).toLowerCase()] ?? "application/octet-stream" });
|
|
6040
|
+
res.end(readFileSync24(file));
|
|
6041
|
+
});
|
|
6042
|
+
await new Promise((done) => server.listen(0, "127.0.0.1", done));
|
|
6043
|
+
const address = server.address();
|
|
6044
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
6045
|
+
return {
|
|
6046
|
+
origin: `http://127.0.0.1:${port}`,
|
|
6047
|
+
close: () => new Promise((done) => server.close(() => done()))
|
|
6048
|
+
};
|
|
6049
|
+
}
|
|
6050
|
+
|
|
5657
6051
|
// src/probe/save.ts
|
|
5658
6052
|
function saveProbe(opts) {
|
|
5659
6053
|
let probe;
|
|
@@ -5668,19 +6062,25 @@ function saveProbe(opts) {
|
|
|
5668
6062
|
const width = typeof probe.width === "number" ? probe.width : NaN;
|
|
5669
6063
|
if (!Number.isFinite(width)) throw new Error("The probe output has no width. Evaluate it in a browser, at the width you are judging.");
|
|
5670
6064
|
const url = typeof probe.url === "string" ? probe.url : "";
|
|
5671
|
-
const page = pageFile(opts.projectRoot, url);
|
|
6065
|
+
const page = opts.page ? projectFile(opts.projectRoot, opts.page) : pageFile(opts.projectRoot, url);
|
|
5672
6066
|
if (!page) {
|
|
5673
|
-
throw new Error(`The probe ran at ${url || "no url"}, which is not a file in this project. Open the page you built
|
|
6067
|
+
throw new Error(`The probe ran at ${url || "no url"}, which is not a file in this project. Open the page you built, as a file:// URL under the project, or let \`jig probe --run <page> --serve <dir>\` serve it.`);
|
|
5674
6068
|
}
|
|
5675
6069
|
probe.pageFile = relative3(opts.projectRoot, page).split("\\").join("/");
|
|
5676
|
-
probe.
|
|
6070
|
+
if (opts.serveRoot) probe.serveRoot = relative3(opts.projectRoot, resolve4(opts.projectRoot, opts.serveRoot)).split("\\").join("/") || ".";
|
|
6071
|
+
probe.pageChecksum = checksum(readFileSync25(page, "utf8"));
|
|
5677
6072
|
probe.recordedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5678
|
-
const dir =
|
|
6073
|
+
const dir = join32(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
5679
6074
|
mkdirSync7(dir, { recursive: true });
|
|
5680
|
-
const path =
|
|
6075
|
+
const path = join32(dir, `probe-${width}.json`);
|
|
5681
6076
|
writeFileSync6(path, JSON.stringify(probe), "utf8");
|
|
5682
6077
|
return { path: `.jig/critique/${opts.surface}/probe-${width}.json`, width, page: probe.pageFile };
|
|
5683
6078
|
}
|
|
6079
|
+
function projectFile(projectRoot, path) {
|
|
6080
|
+
const abs = resolve4(projectRoot, path);
|
|
6081
|
+
if (abs !== resolve4(projectRoot) && !abs.startsWith(resolve4(projectRoot) + sep5)) return void 0;
|
|
6082
|
+
return existsSync19(abs) ? abs : void 0;
|
|
6083
|
+
}
|
|
5684
6084
|
function pageFile(projectRoot, url) {
|
|
5685
6085
|
if (!url.startsWith("file://")) return void 0;
|
|
5686
6086
|
let path;
|
|
@@ -5689,34 +6089,48 @@ function pageFile(projectRoot, url) {
|
|
|
5689
6089
|
} catch {
|
|
5690
6090
|
return void 0;
|
|
5691
6091
|
}
|
|
5692
|
-
const abs =
|
|
5693
|
-
if (!abs.startsWith(
|
|
5694
|
-
return
|
|
6092
|
+
const abs = resolve4(path);
|
|
6093
|
+
if (!abs.startsWith(resolve4(projectRoot))) return void 0;
|
|
6094
|
+
return existsSync19(abs) ? abs : void 0;
|
|
5695
6095
|
}
|
|
5696
6096
|
var PROBE_WIDTHS = [360, 768, 1280, 1600];
|
|
5697
6097
|
async function runAndSaveProbes(opts) {
|
|
5698
|
-
const abs =
|
|
5699
|
-
if (!
|
|
5700
|
-
const url = /^https?:/i.test(opts.page) ? opts.page : `file://${abs}`;
|
|
6098
|
+
const abs = resolve4(opts.projectRoot, opts.page);
|
|
6099
|
+
if (!existsSync19(abs)) throw new Error(`${opts.page} does not exist, so there is nothing to render.`);
|
|
5701
6100
|
const saved = [];
|
|
5702
|
-
|
|
5703
|
-
const
|
|
5704
|
-
|
|
5705
|
-
|
|
6101
|
+
await withPageUrl(opts.projectRoot, abs, opts.serve, async (url) => {
|
|
6102
|
+
for (const width of opts.widths ?? PROBE_WIDTHS) {
|
|
6103
|
+
const json = await runProbe({ url, width });
|
|
6104
|
+
saved.push(saveProbe({ projectRoot: opts.projectRoot, surface: opts.surface, json, page: abs, serveRoot: opts.serve }));
|
|
6105
|
+
}
|
|
6106
|
+
});
|
|
5706
6107
|
return saved;
|
|
5707
6108
|
}
|
|
6109
|
+
async function withPageUrl(projectRoot, abs, serve, use) {
|
|
6110
|
+
if (!serve) return use(`file://${abs}`);
|
|
6111
|
+
const root = resolve4(projectRoot, serve);
|
|
6112
|
+
if (!existsSync19(root)) throw new Error(`${serve} does not exist, so there is nothing to serve.`);
|
|
6113
|
+
if (!abs.startsWith(root + sep5)) throw new Error(`The page is not inside ${serve}, so serving ${serve} cannot show it.`);
|
|
6114
|
+
const server = await serveDirectory(root);
|
|
6115
|
+
try {
|
|
6116
|
+
const path = relative3(root, abs).split(sep5).join("/");
|
|
6117
|
+
await use(`${server.origin}/${path}`);
|
|
6118
|
+
} finally {
|
|
6119
|
+
await server.close();
|
|
6120
|
+
}
|
|
6121
|
+
}
|
|
5708
6122
|
async function ensureProbes(opts) {
|
|
5709
|
-
const dir =
|
|
5710
|
-
const abs =
|
|
6123
|
+
const dir = join32(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
6124
|
+
const abs = resolve4(opts.projectRoot, opts.page);
|
|
5711
6125
|
let current;
|
|
5712
6126
|
try {
|
|
5713
|
-
current = checksum(
|
|
6127
|
+
current = checksum(readFileSync25(abs, "utf8"));
|
|
5714
6128
|
} catch {
|
|
5715
6129
|
return { recorded: [], reason: `${opts.page} could not be read` };
|
|
5716
6130
|
}
|
|
5717
6131
|
const missing = PROBE_WIDTHS.filter((width) => {
|
|
5718
6132
|
try {
|
|
5719
|
-
const probe = JSON.parse(
|
|
6133
|
+
const probe = JSON.parse(readFileSync25(join32(dir, `probe-${width}.json`), "utf8"));
|
|
5720
6134
|
return probe.jigProbe !== PROBE_VERSION || probe.pageChecksum !== current;
|
|
5721
6135
|
} catch {
|
|
5722
6136
|
return true;
|
|
@@ -5724,16 +6138,28 @@ async function ensureProbes(opts) {
|
|
|
5724
6138
|
});
|
|
5725
6139
|
if (missing.length === 0) return { recorded: [] };
|
|
5726
6140
|
if (!findChrome()) return { recorded: [], reason: "no browser on this machine" };
|
|
5727
|
-
|
|
5728
|
-
for (const width of
|
|
5729
|
-
|
|
6141
|
+
let serve;
|
|
6142
|
+
for (const width of PROBE_WIDTHS) {
|
|
6143
|
+
try {
|
|
6144
|
+
const probe = JSON.parse(readFileSync25(join32(dir, `probe-${width}.json`), "utf8"));
|
|
6145
|
+
if (probe.serveRoot) {
|
|
6146
|
+
serve = probe.serveRoot;
|
|
6147
|
+
break;
|
|
6148
|
+
}
|
|
6149
|
+
} catch {
|
|
6150
|
+
}
|
|
5730
6151
|
}
|
|
6152
|
+
await withPageUrl(opts.projectRoot, abs, serve, async (url) => {
|
|
6153
|
+
for (const width of missing) {
|
|
6154
|
+
saveProbe({ projectRoot: opts.projectRoot, surface: opts.surface, json: await runProbe({ url, width }), page: abs, serveRoot: serve });
|
|
6155
|
+
}
|
|
6156
|
+
});
|
|
5731
6157
|
return { recorded: missing };
|
|
5732
6158
|
}
|
|
5733
6159
|
function critiquedSurfaces(projectRoot) {
|
|
5734
|
-
const root =
|
|
5735
|
-
if (!
|
|
5736
|
-
return readdirSync12(root).filter((surface) =>
|
|
6160
|
+
const root = join32(projectRoot, ".jig", "critique");
|
|
6161
|
+
if (!existsSync19(root)) return [];
|
|
6162
|
+
return readdirSync12(root).filter((surface) => existsSync19(join32(root, surface, "screen.json")) || existsSync19(join32(root, surface, "code.json")));
|
|
5737
6163
|
}
|
|
5738
6164
|
|
|
5739
6165
|
// src/probe/check.ts
|
|
@@ -5741,11 +6167,11 @@ function stampProblem(projectRoot, file, p) {
|
|
|
5741
6167
|
if (!p.pageChecksum || !p.pageFile) {
|
|
5742
6168
|
return `${file} was not written by \`jig probe --save\`, so nothing measured it. Evaluate \`jig probe\` in the browser and pipe its output into \`jig probe --save <surface>\`.`;
|
|
5743
6169
|
}
|
|
5744
|
-
const page = pageFile(projectRoot, p.url ?? "");
|
|
5745
|
-
if (!page) return `${file} names a page outside this project (${p.url ?? "no url"}).`;
|
|
6170
|
+
const page = p.serveRoot ? projectFile(projectRoot, p.pageFile) : pageFile(projectRoot, p.url ?? "");
|
|
6171
|
+
if (!page) return `${file} names a page outside this project (${p.serveRoot ? p.pageFile : p.url ?? "no url"}).`;
|
|
5746
6172
|
let current;
|
|
5747
6173
|
try {
|
|
5748
|
-
current =
|
|
6174
|
+
current = readFileSync26(page, "utf8");
|
|
5749
6175
|
} catch {
|
|
5750
6176
|
return `${file} was taken on ${p.pageFile}, which no longer exists.`;
|
|
5751
6177
|
}
|
|
@@ -5755,11 +6181,11 @@ function stampProblem(projectRoot, file, p) {
|
|
|
5755
6181
|
return void 0;
|
|
5756
6182
|
}
|
|
5757
6183
|
function readProbes(projectRoot, dir, errors) {
|
|
5758
|
-
if (!
|
|
6184
|
+
if (!existsSync20(dir)) return [];
|
|
5759
6185
|
const probes = [];
|
|
5760
6186
|
for (const f of readdirSync13(dir).filter((n) => /^probe-\d+\.json$/.test(n)).sort()) {
|
|
5761
6187
|
try {
|
|
5762
|
-
const p = JSON.parse(
|
|
6188
|
+
const p = JSON.parse(readFileSync26(join33(dir, f), "utf8"));
|
|
5763
6189
|
if (p.jigProbe !== PROBE_VERSION || typeof p.width !== "number") {
|
|
5764
6190
|
errors.push(`${f} is not output of \`jig probe\` (version ${PROBE_VERSION}). Re-run the probe; do not write it by hand.`);
|
|
5765
6191
|
continue;
|
|
@@ -5851,19 +6277,19 @@ function probeContradictions(probes, verdictOf, indexable = true) {
|
|
|
5851
6277
|
}
|
|
5852
6278
|
|
|
5853
6279
|
// src/check/decisions.ts
|
|
5854
|
-
import { existsSync as
|
|
5855
|
-
import { join as
|
|
6280
|
+
import { existsSync as existsSync21, readFileSync as readFileSync27 } from "fs";
|
|
6281
|
+
import { join as join34, posix as posix3 } from "path";
|
|
5856
6282
|
var HEADING3 = /^(#{2,3})\s+(.+?)\s*$/gm;
|
|
5857
6283
|
var NOT_A_DECISION = /^(unresolved|open questions?|undecided|contents?|index)$/i;
|
|
5858
6284
|
function decisionsFile(projectRoot) {
|
|
5859
6285
|
const candidates = ["jig/DECISIONS.md", "DECISIONS.md", "src/jig/DECISIONS.md", "src/styles/jig/DECISIONS.md", ".jig/DECISIONS.md"];
|
|
5860
6286
|
try {
|
|
5861
|
-
const brand = JSON.parse(
|
|
6287
|
+
const brand = JSON.parse(readFileSync27(join34(projectRoot, "jig.config.json"), "utf8")).brand;
|
|
5862
6288
|
if (typeof brand === "string" && brand.includes("/")) candidates.unshift(`${posix3.dirname(brand.replace(/^\.\//, ""))}/DECISIONS.md`);
|
|
5863
6289
|
} catch {
|
|
5864
6290
|
}
|
|
5865
6291
|
for (const candidate of candidates) {
|
|
5866
|
-
if (
|
|
6292
|
+
if (existsSync21(join34(projectRoot, candidate))) return candidate;
|
|
5867
6293
|
}
|
|
5868
6294
|
return void 0;
|
|
5869
6295
|
}
|
|
@@ -5872,7 +6298,7 @@ function decisionNames(projectRoot) {
|
|
|
5872
6298
|
if (!path) return [];
|
|
5873
6299
|
let body;
|
|
5874
6300
|
try {
|
|
5875
|
-
body =
|
|
6301
|
+
body = readFileSync27(join34(projectRoot, path), "utf8");
|
|
5876
6302
|
} catch {
|
|
5877
6303
|
return [];
|
|
5878
6304
|
}
|
|
@@ -5886,15 +6312,15 @@ function decisionNames(projectRoot) {
|
|
|
5886
6312
|
}
|
|
5887
6313
|
|
|
5888
6314
|
// src/check/spec-shape.ts
|
|
5889
|
-
import { existsSync as
|
|
5890
|
-
import { join as
|
|
6315
|
+
import { existsSync as existsSync22, readFileSync as readFileSync28, readdirSync as readdirSync14, statSync as statSync3 } from "fs";
|
|
6316
|
+
import { join as join35 } from "path";
|
|
5891
6317
|
function newestSpec(projectRoot) {
|
|
5892
|
-
const dir =
|
|
5893
|
-
if (!
|
|
6318
|
+
const dir = join35(projectRoot, ".jig", "specs");
|
|
6319
|
+
if (!existsSync22(dir)) return void 0;
|
|
5894
6320
|
const files = readdirSync14(dir).filter((f) => f.endsWith(".md"));
|
|
5895
6321
|
if (files.length === 0) return void 0;
|
|
5896
|
-
const newest = files.map((f) => ({ f, at:
|
|
5897
|
-
return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body:
|
|
6322
|
+
const newest = files.map((f) => ({ f, at: statSync3(join35(dir, f)).mtimeMs })).sort((a, b) => b.at - a.at)[0].f;
|
|
6323
|
+
return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body: readFileSync28(join35(dir, newest), "utf8") };
|
|
5898
6324
|
}
|
|
5899
6325
|
function specProblems(spec) {
|
|
5900
6326
|
const parts = spec.body.split(/^---\s*$/m);
|
|
@@ -5975,19 +6401,19 @@ function specIndexableField(front) {
|
|
|
5975
6401
|
var VERDICTS = ["ok", "finding", "n/a"];
|
|
5976
6402
|
var ABSENCE = /\b(rule (not found|does not exist)|context unavailable|cannot (find|read|access) (the )?rule|not in (the )?(accessible )?corpus)\b/i;
|
|
5977
6403
|
function readJson(path, errors) {
|
|
5978
|
-
if (!
|
|
6404
|
+
if (!existsSync23(path)) return null;
|
|
5979
6405
|
try {
|
|
5980
|
-
return JSON.parse(
|
|
6406
|
+
return JSON.parse(readFileSync29(path, "utf8"));
|
|
5981
6407
|
} catch (e) {
|
|
5982
6408
|
errors.push(`${path}: not valid JSON (${e.message})`);
|
|
5983
6409
|
return {};
|
|
5984
6410
|
}
|
|
5985
6411
|
}
|
|
5986
6412
|
function specIndexable(projectRoot, surface) {
|
|
5987
|
-
const path =
|
|
6413
|
+
const path = join36(projectRoot, ".jig", "specs", `${surface}.spec.md`);
|
|
5988
6414
|
let front = "";
|
|
5989
6415
|
try {
|
|
5990
|
-
front =
|
|
6416
|
+
front = readFileSync29(path, "utf8").split(/^---\s*$/m)[1] ?? "";
|
|
5991
6417
|
} catch {
|
|
5992
6418
|
}
|
|
5993
6419
|
const declared = specIndexableField(front);
|
|
@@ -5996,9 +6422,9 @@ function specIndexable(projectRoot, surface) {
|
|
|
5996
6422
|
return mode !== "product" && mode !== "operator";
|
|
5997
6423
|
}
|
|
5998
6424
|
function specNeedsNav(projectRoot, surface) {
|
|
5999
|
-
const path =
|
|
6000
|
-
if (!
|
|
6001
|
-
const front =
|
|
6425
|
+
const path = join36(projectRoot, ".jig", "specs", `${surface}.spec.md`);
|
|
6426
|
+
if (!existsSync23(path)) return false;
|
|
6427
|
+
const front = readFileSync29(path, "utf8").split(/^---\s*$/m)[1] ?? "";
|
|
6002
6428
|
const navField = [...front.matchAll(/^\s*nav:\s*(.+)$/gim)].some((m) => !/^\s*(none|n\/a|-)\b/i.test(m[1]));
|
|
6003
6429
|
const navRegion = /^\s*-\s*(nav|navigation)\s*:/im.test(front);
|
|
6004
6430
|
return navField || navRegion;
|
|
@@ -6049,7 +6475,7 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
|
|
|
6049
6475
|
function checkDecisions(projectRoot, dir, errors) {
|
|
6050
6476
|
const required = decisionNames(projectRoot);
|
|
6051
6477
|
if (required.length === 0) return { state: "ran", judged: 0, total: 0, findings: 0 };
|
|
6052
|
-
const file = readJson(
|
|
6478
|
+
const file = readJson(join36(dir, "decisions.json"), errors);
|
|
6053
6479
|
if (!file) {
|
|
6054
6480
|
errors.push(
|
|
6055
6481
|
`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.`
|
|
@@ -6094,7 +6520,7 @@ function checkDecisions(projectRoot, dir, errors) {
|
|
|
6094
6520
|
function verifyVerdicts(opts) {
|
|
6095
6521
|
const root = opts.packageRoot ?? assetRoot();
|
|
6096
6522
|
const errors = [];
|
|
6097
|
-
const index = JSON.parse(
|
|
6523
|
+
const index = JSON.parse(readFileSync29(join36(root, "rules.index.json"), "utf8"));
|
|
6098
6524
|
const judgment = index.filter((r) => r.bucket === "judgment");
|
|
6099
6525
|
const screenIds = judgment.filter((r) => r.pass === "screen").map((r) => r.id);
|
|
6100
6526
|
const codeIds = judgment.filter((r) => r.pass === "code").map((r) => r.id);
|
|
@@ -6103,16 +6529,16 @@ function verifyVerdicts(opts) {
|
|
|
6103
6529
|
...judgment.map((r) => [r.id, r.pass ?? ""])
|
|
6104
6530
|
]);
|
|
6105
6531
|
const specIds = new Set(citableIds(root).filter((id) => /^[PMLRT]-\d+$/.test(id)));
|
|
6106
|
-
const dir =
|
|
6107
|
-
const screenFile = readJson(
|
|
6108
|
-
const codeFile = readJson(
|
|
6532
|
+
const dir = join36(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
6533
|
+
const screenFile = readJson(join36(dir, "screen.json"), errors);
|
|
6534
|
+
const codeFile = readJson(join36(dir, "code.json"), errors);
|
|
6109
6535
|
const screenExtraRequired = specNeedsNav(opts.projectRoot, opts.surface) ? ["P-14"] : [];
|
|
6110
6536
|
const screen = checkArm("screen", screenFile, screenIds, passOf, specIds, screenExtraRequired, errors);
|
|
6111
6537
|
const code = checkArm("code", codeFile, codeIds, passOf, specIds, [], errors);
|
|
6112
6538
|
let rendered = false;
|
|
6113
6539
|
if (screenFile && screenFile.rendered === true) {
|
|
6114
6540
|
const artefacts = Array.isArray(screenFile.artefacts) ? screenFile.artefacts.filter((a) => typeof a === "string") : [];
|
|
6115
|
-
const absent = artefacts.filter((a) => !
|
|
6541
|
+
const absent = artefacts.filter((a) => !existsSync23(resolve5(opts.projectRoot, a)));
|
|
6116
6542
|
if (artefacts.length === 0) errors.push("screen.json says rendered: true but lists no artefacts. A render leaves a screenshot.");
|
|
6117
6543
|
else if (absent.length) errors.push(`screen.json says rendered: true, but these artefacts do not exist: ${absent.join(", ")}.`);
|
|
6118
6544
|
else rendered = true;
|
|
@@ -6133,7 +6559,7 @@ function verifyVerdicts(opts) {
|
|
|
6133
6559
|
};
|
|
6134
6560
|
let specFront = "";
|
|
6135
6561
|
try {
|
|
6136
|
-
specFront =
|
|
6562
|
+
specFront = readFileSync29(join36(opts.projectRoot, ".jig", "specs", `${opts.surface}.spec.md`), "utf8").split(/^---\s*$/m)[1] ?? "";
|
|
6137
6563
|
} catch {
|
|
6138
6564
|
}
|
|
6139
6565
|
if (specIndexableField(specFront) === "unreadable") {
|
|
@@ -6149,15 +6575,15 @@ function verifyVerdicts(opts) {
|
|
|
6149
6575
|
}
|
|
6150
6576
|
|
|
6151
6577
|
// src/commands/gate.ts
|
|
6152
|
-
import { existsSync as
|
|
6578
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync8, readdirSync as readdirSync15, readFileSync as readFileSync30, writeFileSync as writeFileSync7 } from "fs";
|
|
6153
6579
|
import { createHash as createHash2 } from "crypto";
|
|
6154
|
-
import { join as
|
|
6580
|
+
import { join as join37 } from "path";
|
|
6155
6581
|
var MAX_BLOCKS = 3;
|
|
6156
6582
|
function lastJigCommand(transcriptPath) {
|
|
6157
|
-
if (!transcriptPath || !
|
|
6583
|
+
if (!transcriptPath || !existsSync24(transcriptPath)) return void 0;
|
|
6158
6584
|
let text;
|
|
6159
6585
|
try {
|
|
6160
|
-
text =
|
|
6586
|
+
text = readFileSync30(transcriptPath, "utf8");
|
|
6161
6587
|
} catch {
|
|
6162
6588
|
return void 0;
|
|
6163
6589
|
}
|
|
@@ -6172,10 +6598,10 @@ function lastJigCommand(transcriptPath) {
|
|
|
6172
6598
|
return found;
|
|
6173
6599
|
}
|
|
6174
6600
|
function lastAssistantText(transcriptPath) {
|
|
6175
|
-
if (!transcriptPath || !
|
|
6601
|
+
if (!transcriptPath || !existsSync24(transcriptPath)) return void 0;
|
|
6176
6602
|
let found;
|
|
6177
6603
|
try {
|
|
6178
|
-
for (const line of
|
|
6604
|
+
for (const line of readFileSync30(transcriptPath, "utf8").split("\n")) {
|
|
6179
6605
|
if (!line.includes('"assistant"')) continue;
|
|
6180
6606
|
let entry;
|
|
6181
6607
|
try {
|
|
@@ -6205,7 +6631,7 @@ function commandProblems(root, command) {
|
|
|
6205
6631
|
const found = decisionsFile(root);
|
|
6206
6632
|
if (!found) problems.push("decide wrote no DECISIONS.md beside the token layer.");
|
|
6207
6633
|
else {
|
|
6208
|
-
const body =
|
|
6634
|
+
const body = readFileSync30(join37(root, found), "utf8");
|
|
6209
6635
|
if (!/^##\s+Unresolved\s*$/im.test(body)) {
|
|
6210
6636
|
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.`");
|
|
6211
6637
|
}
|
|
@@ -6226,13 +6652,13 @@ function commandProblems(root, command) {
|
|
|
6226
6652
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
6227
6653
|
if (/^approved/i.test(mockup)) {
|
|
6228
6654
|
if (!at) problems.push(`${spec.path}: \`mockup_at:\` is empty. Record where the approved drawing is.`);
|
|
6229
|
-
else if (!/^https?:/i.test(at) && !
|
|
6655
|
+
else if (!/^https?:/i.test(at) && !existsSync24(join37(root, at))) problems.push(`${spec.path}: \`mockup_at: ${at}\` does not exist.`);
|
|
6230
6656
|
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.`);
|
|
6231
6657
|
}
|
|
6232
6658
|
}
|
|
6233
6659
|
if (command === "critique") {
|
|
6234
|
-
const dir =
|
|
6235
|
-
const surfaces =
|
|
6660
|
+
const dir = join37(root, ".jig", "critique");
|
|
6661
|
+
const surfaces = existsSync24(dir) ? readdirSync15(dir).filter((s) => existsSync24(join37(dir, s, "screen.json")) || existsSync24(join37(dir, s, "code.json"))) : [];
|
|
6236
6662
|
for (const surface of surfaces) {
|
|
6237
6663
|
const v = verifyVerdicts({ projectRoot: root, surface });
|
|
6238
6664
|
if (v.ok && v.decisions.state !== "ran" && v.decisions.total > 0) {
|
|
@@ -6254,13 +6680,13 @@ function surfacePage(projectRoot, surface) {
|
|
|
6254
6680
|
const declared = /^\s*surface\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
|
|
6255
6681
|
const candidates = [declared, `${surface}.html`, declared ? `${declared.replace(/^\//, "")}.html` : void 0].filter((c) => !!c && /\.\w+$/.test(c) === (c === declared ? /\.\w+$/.test(c) : true));
|
|
6256
6682
|
for (const candidate of candidates) {
|
|
6257
|
-
if (candidate &&
|
|
6683
|
+
if (candidate && existsSync24(join37(projectRoot, candidate))) return candidate;
|
|
6258
6684
|
}
|
|
6259
6685
|
return void 0;
|
|
6260
6686
|
}
|
|
6261
6687
|
function gate(opts) {
|
|
6262
6688
|
const root = opts.projectRoot;
|
|
6263
|
-
if (!
|
|
6689
|
+
if (!existsSync24(join37(root, "jig.config.json")) && !existsSync24(join37(root, ".jig"))) {
|
|
6264
6690
|
return { block: false, reason: "" };
|
|
6265
6691
|
}
|
|
6266
6692
|
const command = lastJigCommand(opts.input.transcript_path);
|
|
@@ -6286,11 +6712,12 @@ function gate(opts) {
|
|
|
6286
6712
|
);
|
|
6287
6713
|
}
|
|
6288
6714
|
}
|
|
6289
|
-
|
|
6290
|
-
|
|
6715
|
+
problems.push(...verdictGuard(root, command));
|
|
6716
|
+
const critiqueDir = join37(root, ".jig", "critique");
|
|
6717
|
+
if (existsSync24(critiqueDir)) {
|
|
6291
6718
|
for (const surface of readdirSync15(critiqueDir)) {
|
|
6292
|
-
const dir =
|
|
6293
|
-
if (!
|
|
6719
|
+
const dir = join37(critiqueDir, surface);
|
|
6720
|
+
if (!existsSync24(join37(dir, "screen.json")) && !existsSync24(join37(dir, "code.json"))) continue;
|
|
6294
6721
|
const v = verifyVerdicts({ projectRoot: root, surface });
|
|
6295
6722
|
if (!v.ok) {
|
|
6296
6723
|
const shown = v.errors.slice(0, 6).map((e) => ` ${e}`);
|
|
@@ -6304,10 +6731,10 @@ function gate(opts) {
|
|
|
6304
6731
|
}
|
|
6305
6732
|
const session = opts.input.session_id ?? "unknown";
|
|
6306
6733
|
const key = `${session}:${createHash2("sha256").update(problems.join("\n")).digest("hex").slice(0, 12)}`;
|
|
6307
|
-
const stateFile =
|
|
6734
|
+
const stateFile = join37(root, ".jig", "gate.json");
|
|
6308
6735
|
let state = {};
|
|
6309
6736
|
try {
|
|
6310
|
-
state = JSON.parse(
|
|
6737
|
+
state = JSON.parse(readFileSync30(stateFile, "utf8"));
|
|
6311
6738
|
} catch {
|
|
6312
6739
|
}
|
|
6313
6740
|
if (problems.length === 0) {
|
|
@@ -6335,17 +6762,60 @@ ${problems.join("\n\n")}`
|
|
|
6335
6762
|
${problems.join("\n\n")}`
|
|
6336
6763
|
};
|
|
6337
6764
|
}
|
|
6765
|
+
var VERDICT_FILES = ["screen.json", "code.json", "decisions.json"];
|
|
6766
|
+
var LOCK = "verdicts.lock";
|
|
6767
|
+
function verdictChecksum(dir) {
|
|
6768
|
+
const parts = VERDICT_FILES.map((f) => {
|
|
6769
|
+
try {
|
|
6770
|
+
return `${f}
|
|
6771
|
+
${readFileSync30(join37(dir, f), "utf8")}`;
|
|
6772
|
+
} catch {
|
|
6773
|
+
return "";
|
|
6774
|
+
}
|
|
6775
|
+
});
|
|
6776
|
+
return parts.some(Boolean) ? checksum(parts.join("\n")) : void 0;
|
|
6777
|
+
}
|
|
6778
|
+
function verdictGuard(root, command) {
|
|
6779
|
+
const critiqueDir = join37(root, ".jig", "critique");
|
|
6780
|
+
if (!existsSync24(critiqueDir)) return [];
|
|
6781
|
+
const problems = [];
|
|
6782
|
+
for (const surface of readdirSync15(critiqueDir)) {
|
|
6783
|
+
const dir = join37(critiqueDir, surface);
|
|
6784
|
+
const now = verdictChecksum(dir);
|
|
6785
|
+
if (!now) continue;
|
|
6786
|
+
const lockPath = join37(dir, LOCK);
|
|
6787
|
+
if (command === "critique") {
|
|
6788
|
+
try {
|
|
6789
|
+
writeFileSync7(lockPath, JSON.stringify({ checksum: now }) + "\n", "utf8");
|
|
6790
|
+
} catch {
|
|
6791
|
+
}
|
|
6792
|
+
continue;
|
|
6793
|
+
}
|
|
6794
|
+
let locked;
|
|
6795
|
+
try {
|
|
6796
|
+
locked = JSON.parse(readFileSync30(lockPath, "utf8")).checksum;
|
|
6797
|
+
} catch {
|
|
6798
|
+
continue;
|
|
6799
|
+
}
|
|
6800
|
+
if (locked && locked !== now) {
|
|
6801
|
+
problems.push(
|
|
6802
|
+
`.jig/critique/${surface}: the verdict files changed after \`/jig critique\` wrote them${command ? `, in a session that ran \`/jig ${command}\`` : ""}. Verdicts are the review's, not the builder's: restore them (\`git checkout -- .jig/critique/${surface}\`) and run \`/jig critique\` to judge the fix.`
|
|
6803
|
+
);
|
|
6804
|
+
}
|
|
6805
|
+
}
|
|
6806
|
+
return problems;
|
|
6807
|
+
}
|
|
6338
6808
|
function save(file, state) {
|
|
6339
6809
|
try {
|
|
6340
|
-
mkdirSync8(
|
|
6810
|
+
mkdirSync8(join37(file, ".."), { recursive: true });
|
|
6341
6811
|
writeFileSync7(file, JSON.stringify(state), "utf8");
|
|
6342
6812
|
} catch {
|
|
6343
6813
|
}
|
|
6344
6814
|
}
|
|
6345
6815
|
|
|
6346
6816
|
// src/commands/seo.ts
|
|
6347
|
-
import { readFileSync as
|
|
6348
|
-
import { join as
|
|
6817
|
+
import { readFileSync as readFileSync31 } from "fs";
|
|
6818
|
+
import { join as join38 } from "path";
|
|
6349
6819
|
var SITEMAP_FILE = /(^|\/)sitemap(-index)?\.(xml|ts|js|mjs|tsx|jsx|rb|php|py)$/i;
|
|
6350
6820
|
var ROBOTS_FILE = /(^|\/)robots\.(txt|ts|js|mjs|tsx|jsx)$/i;
|
|
6351
6821
|
var ROUTE_IN_SITEMAP = /(?:<loc>\s*([^<\s]+)\s*<\/loc>)|(?:url\s*:\s*["'`]([^"'`]+)["'`])/gi;
|
|
@@ -6386,7 +6856,7 @@ function seo(opts) {
|
|
|
6386
6856
|
for (const file of files) {
|
|
6387
6857
|
let raw;
|
|
6388
6858
|
try {
|
|
6389
|
-
raw =
|
|
6859
|
+
raw = readFileSync31(join38(root, file), "utf8");
|
|
6390
6860
|
} catch {
|
|
6391
6861
|
continue;
|
|
6392
6862
|
}
|
|
@@ -6442,7 +6912,7 @@ function seo(opts) {
|
|
|
6442
6912
|
|
|
6443
6913
|
// src/index.ts
|
|
6444
6914
|
var packageRoot = getPackageRoot();
|
|
6445
|
-
var { version } = JSON.parse(
|
|
6915
|
+
var { version } = JSON.parse(readFileSync32(join39(packageRoot, "package.json"), "utf8"));
|
|
6446
6916
|
function warnIfUnpublishedPin() {
|
|
6447
6917
|
if (isPublishedBuild(packageRoot)) return;
|
|
6448
6918
|
console.warn(
|
|
@@ -6592,7 +7062,7 @@ program.command("seo").description("Audit what a search engine and a link previe
|
|
|
6592
7062
|
process.exit(1);
|
|
6593
7063
|
}
|
|
6594
7064
|
});
|
|
6595
|
-
program.command("probe").description("Print the render probe. With --save, read what it returned on stdin and record it for `jig verdicts`.").option("--save <surface>", "record the probe's output (piped in) under .jig/critique/<surface>/").option("--run <page>", "render this page here, at 360, 768 and
|
|
7065
|
+
program.command("probe").description("Print the render probe. With --save, read what it returned on stdin and record it for `jig verdicts`.").option("--save <surface>", "record the probe's output (piped in) under .jig/critique/<surface>/").option("--run <page>", "render this page here, at 360, 768, 1280 and 1600, and record each (needs --save)").option("--serve <dir>", "serve this directory over local http and load --run from it, so root-relative links resolve (a built static site)").action(async (opts) => {
|
|
6596
7066
|
if (opts.run) {
|
|
6597
7067
|
if (!opts.save) {
|
|
6598
7068
|
console.error(" \u2717 --run records what it measures, so it needs --save <surface>.");
|
|
@@ -6600,7 +7070,7 @@ program.command("probe").description("Print the render probe. With --save, read
|
|
|
6600
7070
|
}
|
|
6601
7071
|
const projectRoot2 = findProjectRoot(process.cwd());
|
|
6602
7072
|
try {
|
|
6603
|
-
for (const saved of await runAndSaveProbes({ projectRoot: projectRoot2, surface: opts.save, page: opts.run })) {
|
|
7073
|
+
for (const saved of await runAndSaveProbes({ projectRoot: projectRoot2, surface: opts.save, page: opts.run, serve: opts.serve })) {
|
|
6604
7074
|
console.log(` Recorded ${saved.path} \u2014 ${saved.page} at ${saved.width}px.`);
|
|
6605
7075
|
}
|
|
6606
7076
|
} catch (err) {
|
|
@@ -6634,7 +7104,7 @@ program.command("probe").description("Print the render probe. With --save, read
|
|
|
6634
7104
|
program.command("gate").description("Run by the Claude Code Stop hook: block stopping while check or a critique fails.").action(async () => {
|
|
6635
7105
|
let input = {};
|
|
6636
7106
|
try {
|
|
6637
|
-
if (!process.stdin.isTTY) input = JSON.parse(
|
|
7107
|
+
if (!process.stdin.isTTY) input = JSON.parse(readFileSync32(0, "utf8") || "{}");
|
|
6638
7108
|
} catch {
|
|
6639
7109
|
}
|
|
6640
7110
|
const cwd = input.cwd ?? process.cwd();
|
|
@@ -6673,7 +7143,7 @@ program.command("init").description("Set the project up to use Jig: a brand file
|
|
|
6673
7143
|
});
|
|
6674
7144
|
function resolveSurfaceMode(projectRoot) {
|
|
6675
7145
|
try {
|
|
6676
|
-
const config = JSON.parse(
|
|
7146
|
+
const config = JSON.parse(readFileSync32(join39(projectRoot, "jig.config.json"), "utf8"));
|
|
6677
7147
|
const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
|
|
6678
7148
|
return modes.length === 1 ? modes[0] : void 0;
|
|
6679
7149
|
} catch {
|