jig-ui 0.24.0 → 0.26.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 +149 -8
- package/README.md +14 -10
- package/dist/index.js +274 -168
- package/examples/J-164.html +7 -0
- package/package.json +2 -2
- package/rules/00-anti-patterns.md +8 -2
- package/rules/01-modes.md +2 -2
- package/rules/02-tokens.md +10 -0
- package/rules/03-patterns.md +22 -4
- package/rules/05-copy.md +1 -1
- package/rules.index.json +7 -0
- package/templates/COMMAND.md.tmpl +72 -7
- package/templates/SKILL.md.tmpl +1 -1
- 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
|
@@ -2098,6 +2098,139 @@ function isReaderText(file) {
|
|
|
2098
2098
|
return hasExtension(file, STYLE_HOST_EXTENSIONS);
|
|
2099
2099
|
}
|
|
2100
2100
|
|
|
2101
|
+
// src/check/length.ts
|
|
2102
|
+
var LENGTH_UNITS = [
|
|
2103
|
+
"px",
|
|
2104
|
+
"rem",
|
|
2105
|
+
"em",
|
|
2106
|
+
"pt",
|
|
2107
|
+
"vh",
|
|
2108
|
+
"vw",
|
|
2109
|
+
"vmin",
|
|
2110
|
+
"vmax",
|
|
2111
|
+
"ch",
|
|
2112
|
+
"ex"
|
|
2113
|
+
];
|
|
2114
|
+
var UNITS = LENGTH_UNITS.join("|");
|
|
2115
|
+
var LENGTH_RE = new RegExp(`^-?\\d*\\.?\\d+(?:${UNITS})$`, "i");
|
|
2116
|
+
var LENGTH_SCAN_RE = new RegExp(`(-?\\d*\\.?\\d+)(${UNITS})`, "gi");
|
|
2117
|
+
function isExcludedLength(value, unit) {
|
|
2118
|
+
if (value === 0) return true;
|
|
2119
|
+
return unit.toLowerCase() === "px" && (Math.abs(value) === 1 || Math.abs(value) === 2);
|
|
2120
|
+
}
|
|
2121
|
+
function hasHardCodedLength(value) {
|
|
2122
|
+
LENGTH_SCAN_RE.lastIndex = 0;
|
|
2123
|
+
let m;
|
|
2124
|
+
while (m = LENGTH_SCAN_RE.exec(value)) {
|
|
2125
|
+
if (!isExcludedLength(parseFloat(m[1]), m[2])) return true;
|
|
2126
|
+
}
|
|
2127
|
+
return false;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// src/check/tailwind.ts
|
|
2131
|
+
var CLASS_ATTR = /\b(?:class|className)\s*=\s*/g;
|
|
2132
|
+
function readBraced(source, open) {
|
|
2133
|
+
let depth = 0;
|
|
2134
|
+
for (let i = open; i < source.length; i++) {
|
|
2135
|
+
const ch = source[i];
|
|
2136
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
2137
|
+
const quote = ch;
|
|
2138
|
+
i++;
|
|
2139
|
+
while (i < source.length && source[i] !== quote) {
|
|
2140
|
+
if (source[i] === "\\") i++;
|
|
2141
|
+
i++;
|
|
2142
|
+
}
|
|
2143
|
+
continue;
|
|
2144
|
+
}
|
|
2145
|
+
if (ch === "{") depth++;
|
|
2146
|
+
else if (ch === "}" && --depth === 0) return source.slice(open + 1, i);
|
|
2147
|
+
}
|
|
2148
|
+
return null;
|
|
2149
|
+
}
|
|
2150
|
+
function classAttributeValues(source) {
|
|
2151
|
+
const out = [];
|
|
2152
|
+
const lineAt = (offset) => source.slice(0, offset).split("\n").length;
|
|
2153
|
+
for (const m of source.matchAll(CLASS_ATTR)) {
|
|
2154
|
+
const at = m.index + m[0].length;
|
|
2155
|
+
const opener = source[at];
|
|
2156
|
+
if (opener === '"' || opener === "'") {
|
|
2157
|
+
const end = source.indexOf(opener, at + 1);
|
|
2158
|
+
if (end !== -1) out.push({ classes: source.slice(at + 1, end), line: lineAt(at) });
|
|
2159
|
+
continue;
|
|
2160
|
+
}
|
|
2161
|
+
if (opener !== "{") continue;
|
|
2162
|
+
const raw = readBraced(source, at);
|
|
2163
|
+
if (raw === null) continue;
|
|
2164
|
+
const literals = [...raw.matchAll(/(["'`])([\s\S]*?)\1/g)].map(
|
|
2165
|
+
(q) => q[2].replace(/\$\{[\s\S]*?\}/g, " ")
|
|
2166
|
+
);
|
|
2167
|
+
if (literals.length > 0) out.push({ classes: literals.join(" ").trim(), line: lineAt(at) });
|
|
2168
|
+
}
|
|
2169
|
+
return out;
|
|
2170
|
+
}
|
|
2171
|
+
var ARBITRARY = /(?:^|\s)(?:[\w-]+:)*([a-z][\w-]*)-\[([^\]\s]+)\]/gi;
|
|
2172
|
+
var COLOUR = /^(?:#[0-9a-f]{3,8}|(?:rgba?|hsla?|oklch|oklab|lab|lch|color)\()/i;
|
|
2173
|
+
var LENGTH_PARTS = /^(-?\d*\.?\d+)([a-z]+)$/i;
|
|
2174
|
+
function arbitraryValues(classes) {
|
|
2175
|
+
const out = [];
|
|
2176
|
+
for (const m of classes.matchAll(ARBITRARY)) {
|
|
2177
|
+
const [, utility, rawValue] = m;
|
|
2178
|
+
const value = rawValue.replace(/_/g, " ");
|
|
2179
|
+
if (/var\(|--/.test(value)) continue;
|
|
2180
|
+
if (COLOUR.test(value)) {
|
|
2181
|
+
out.push({ utility, value, kind: "colour" });
|
|
2182
|
+
continue;
|
|
2183
|
+
}
|
|
2184
|
+
if (!LENGTH_RE.test(value)) continue;
|
|
2185
|
+
const parts = LENGTH_PARTS.exec(value);
|
|
2186
|
+
if (isExcludedLength(parseFloat(parts[1]), parts[2])) continue;
|
|
2187
|
+
out.push({ utility, value, kind: "length" });
|
|
2188
|
+
}
|
|
2189
|
+
return out;
|
|
2190
|
+
}
|
|
2191
|
+
var PALETTE = {
|
|
2192
|
+
white: "#ffffff",
|
|
2193
|
+
black: "#000000",
|
|
2194
|
+
"gray-50": "#f9fafb",
|
|
2195
|
+
"gray-100": "#f3f4f6",
|
|
2196
|
+
"gray-200": "#e5e7eb",
|
|
2197
|
+
"gray-300": "#d1d5db",
|
|
2198
|
+
"gray-400": "#9ca3af",
|
|
2199
|
+
"gray-500": "#6b7280",
|
|
2200
|
+
"gray-600": "#4b5563",
|
|
2201
|
+
"gray-700": "#374151",
|
|
2202
|
+
"gray-800": "#1f2937",
|
|
2203
|
+
"gray-900": "#111827",
|
|
2204
|
+
"gray-950": "#030712",
|
|
2205
|
+
"slate-50": "#f8fafc",
|
|
2206
|
+
"slate-400": "#94a3b8",
|
|
2207
|
+
"slate-500": "#64748b",
|
|
2208
|
+
"slate-800": "#1e293b",
|
|
2209
|
+
"slate-900": "#0f172a",
|
|
2210
|
+
"slate-950": "#020617",
|
|
2211
|
+
"zinc-400": "#a1a1aa",
|
|
2212
|
+
"zinc-500": "#71717a",
|
|
2213
|
+
"zinc-900": "#18181b",
|
|
2214
|
+
"neutral-400": "#a3a3a3",
|
|
2215
|
+
"neutral-500": "#737373",
|
|
2216
|
+
"neutral-900": "#171717",
|
|
2217
|
+
"stone-400": "#a8a29e",
|
|
2218
|
+
"stone-500": "#78716c",
|
|
2219
|
+
"stone-900": "#1c1917"
|
|
2220
|
+
};
|
|
2221
|
+
function palettePairs(classes) {
|
|
2222
|
+
let background;
|
|
2223
|
+
let foreground;
|
|
2224
|
+
for (const token of classes.split(/\s+/)) {
|
|
2225
|
+
const bare2 = token.replace(/^(?:[\w-]+:)*/, "");
|
|
2226
|
+
const bg = /^bg-(.+)$/.exec(bare2);
|
|
2227
|
+
const fg = /^text-(.+)$/.exec(bare2);
|
|
2228
|
+
if (bg && PALETTE[bg[1]]) background = PALETTE[bg[1]];
|
|
2229
|
+
if (fg && PALETTE[fg[1]]) foreground = PALETTE[fg[1]];
|
|
2230
|
+
}
|
|
2231
|
+
return background && foreground ? [{ background, foreground }] : [];
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2101
2234
|
// src/check/run.ts
|
|
2102
2235
|
import { readFileSync as readFileSync15 } from "fs";
|
|
2103
2236
|
import { join as join19 } from "path";
|
|
@@ -2904,139 +3037,6 @@ var pureBlackWhite = {
|
|
|
2904
3037
|
}
|
|
2905
3038
|
};
|
|
2906
3039
|
|
|
2907
|
-
// src/check/length.ts
|
|
2908
|
-
var LENGTH_UNITS = [
|
|
2909
|
-
"px",
|
|
2910
|
-
"rem",
|
|
2911
|
-
"em",
|
|
2912
|
-
"pt",
|
|
2913
|
-
"vh",
|
|
2914
|
-
"vw",
|
|
2915
|
-
"vmin",
|
|
2916
|
-
"vmax",
|
|
2917
|
-
"ch",
|
|
2918
|
-
"ex"
|
|
2919
|
-
];
|
|
2920
|
-
var UNITS = LENGTH_UNITS.join("|");
|
|
2921
|
-
var LENGTH_RE = new RegExp(`^-?\\d*\\.?\\d+(?:${UNITS})$`, "i");
|
|
2922
|
-
var LENGTH_SCAN_RE = new RegExp(`(-?\\d*\\.?\\d+)(${UNITS})`, "gi");
|
|
2923
|
-
function isExcludedLength(value, unit) {
|
|
2924
|
-
if (value === 0) return true;
|
|
2925
|
-
return unit.toLowerCase() === "px" && (Math.abs(value) === 1 || Math.abs(value) === 2);
|
|
2926
|
-
}
|
|
2927
|
-
function hasHardCodedLength(value) {
|
|
2928
|
-
LENGTH_SCAN_RE.lastIndex = 0;
|
|
2929
|
-
let m;
|
|
2930
|
-
while (m = LENGTH_SCAN_RE.exec(value)) {
|
|
2931
|
-
if (!isExcludedLength(parseFloat(m[1]), m[2])) return true;
|
|
2932
|
-
}
|
|
2933
|
-
return false;
|
|
2934
|
-
}
|
|
2935
|
-
|
|
2936
|
-
// src/check/tailwind.ts
|
|
2937
|
-
var CLASS_ATTR = /\b(?:class|className)\s*=\s*/g;
|
|
2938
|
-
function readBraced(source, open) {
|
|
2939
|
-
let depth = 0;
|
|
2940
|
-
for (let i = open; i < source.length; i++) {
|
|
2941
|
-
const ch = source[i];
|
|
2942
|
-
if (ch === '"' || ch === "'" || ch === "`") {
|
|
2943
|
-
const quote = ch;
|
|
2944
|
-
i++;
|
|
2945
|
-
while (i < source.length && source[i] !== quote) {
|
|
2946
|
-
if (source[i] === "\\") i++;
|
|
2947
|
-
i++;
|
|
2948
|
-
}
|
|
2949
|
-
continue;
|
|
2950
|
-
}
|
|
2951
|
-
if (ch === "{") depth++;
|
|
2952
|
-
else if (ch === "}" && --depth === 0) return source.slice(open + 1, i);
|
|
2953
|
-
}
|
|
2954
|
-
return null;
|
|
2955
|
-
}
|
|
2956
|
-
function classAttributeValues(source) {
|
|
2957
|
-
const out = [];
|
|
2958
|
-
const lineAt = (offset) => source.slice(0, offset).split("\n").length;
|
|
2959
|
-
for (const m of source.matchAll(CLASS_ATTR)) {
|
|
2960
|
-
const at = m.index + m[0].length;
|
|
2961
|
-
const opener = source[at];
|
|
2962
|
-
if (opener === '"' || opener === "'") {
|
|
2963
|
-
const end = source.indexOf(opener, at + 1);
|
|
2964
|
-
if (end !== -1) out.push({ classes: source.slice(at + 1, end), line: lineAt(at) });
|
|
2965
|
-
continue;
|
|
2966
|
-
}
|
|
2967
|
-
if (opener !== "{") continue;
|
|
2968
|
-
const raw = readBraced(source, at);
|
|
2969
|
-
if (raw === null) continue;
|
|
2970
|
-
const literals = [...raw.matchAll(/(["'`])([\s\S]*?)\1/g)].map(
|
|
2971
|
-
(q) => q[2].replace(/\$\{[\s\S]*?\}/g, " ")
|
|
2972
|
-
);
|
|
2973
|
-
if (literals.length > 0) out.push({ classes: literals.join(" ").trim(), line: lineAt(at) });
|
|
2974
|
-
}
|
|
2975
|
-
return out;
|
|
2976
|
-
}
|
|
2977
|
-
var ARBITRARY = /(?:^|\s)(?:[\w-]+:)*([a-z][\w-]*)-\[([^\]\s]+)\]/gi;
|
|
2978
|
-
var COLOUR = /^(?:#[0-9a-f]{3,8}|(?:rgba?|hsla?|oklch|oklab|lab|lch|color)\()/i;
|
|
2979
|
-
var LENGTH_PARTS = /^(-?\d*\.?\d+)([a-z]+)$/i;
|
|
2980
|
-
function arbitraryValues(classes) {
|
|
2981
|
-
const out = [];
|
|
2982
|
-
for (const m of classes.matchAll(ARBITRARY)) {
|
|
2983
|
-
const [, utility, rawValue] = m;
|
|
2984
|
-
const value = rawValue.replace(/_/g, " ");
|
|
2985
|
-
if (/var\(|--/.test(value)) continue;
|
|
2986
|
-
if (COLOUR.test(value)) {
|
|
2987
|
-
out.push({ utility, value, kind: "colour" });
|
|
2988
|
-
continue;
|
|
2989
|
-
}
|
|
2990
|
-
if (!LENGTH_RE.test(value)) continue;
|
|
2991
|
-
const parts = LENGTH_PARTS.exec(value);
|
|
2992
|
-
if (isExcludedLength(parseFloat(parts[1]), parts[2])) continue;
|
|
2993
|
-
out.push({ utility, value, kind: "length" });
|
|
2994
|
-
}
|
|
2995
|
-
return out;
|
|
2996
|
-
}
|
|
2997
|
-
var PALETTE = {
|
|
2998
|
-
white: "#ffffff",
|
|
2999
|
-
black: "#000000",
|
|
3000
|
-
"gray-50": "#f9fafb",
|
|
3001
|
-
"gray-100": "#f3f4f6",
|
|
3002
|
-
"gray-200": "#e5e7eb",
|
|
3003
|
-
"gray-300": "#d1d5db",
|
|
3004
|
-
"gray-400": "#9ca3af",
|
|
3005
|
-
"gray-500": "#6b7280",
|
|
3006
|
-
"gray-600": "#4b5563",
|
|
3007
|
-
"gray-700": "#374151",
|
|
3008
|
-
"gray-800": "#1f2937",
|
|
3009
|
-
"gray-900": "#111827",
|
|
3010
|
-
"gray-950": "#030712",
|
|
3011
|
-
"slate-50": "#f8fafc",
|
|
3012
|
-
"slate-400": "#94a3b8",
|
|
3013
|
-
"slate-500": "#64748b",
|
|
3014
|
-
"slate-800": "#1e293b",
|
|
3015
|
-
"slate-900": "#0f172a",
|
|
3016
|
-
"slate-950": "#020617",
|
|
3017
|
-
"zinc-400": "#a1a1aa",
|
|
3018
|
-
"zinc-500": "#71717a",
|
|
3019
|
-
"zinc-900": "#18181b",
|
|
3020
|
-
"neutral-400": "#a3a3a3",
|
|
3021
|
-
"neutral-500": "#737373",
|
|
3022
|
-
"neutral-900": "#171717",
|
|
3023
|
-
"stone-400": "#a8a29e",
|
|
3024
|
-
"stone-500": "#78716c",
|
|
3025
|
-
"stone-900": "#1c1917"
|
|
3026
|
-
};
|
|
3027
|
-
function palettePairs(classes) {
|
|
3028
|
-
let background;
|
|
3029
|
-
let foreground;
|
|
3030
|
-
for (const token of classes.split(/\s+/)) {
|
|
3031
|
-
const bare2 = token.replace(/^(?:[\w-]+:)*/, "");
|
|
3032
|
-
const bg = /^bg-(.+)$/.exec(bare2);
|
|
3033
|
-
const fg = /^text-(.+)$/.exec(bare2);
|
|
3034
|
-
if (bg && PALETTE[bg[1]]) background = PALETTE[bg[1]];
|
|
3035
|
-
if (fg && PALETTE[fg[1]]) foreground = PALETTE[fg[1]];
|
|
3036
|
-
}
|
|
3037
|
-
return background && foreground ? [{ background, foreground }] : [];
|
|
3038
|
-
}
|
|
3039
|
-
|
|
3040
3040
|
// src/check/detectors/contrast-floor.ts
|
|
3041
3041
|
var FLOOR = 4.5;
|
|
3042
3042
|
var LARGE_FLOOR = 3;
|
|
@@ -3631,7 +3631,7 @@ var EM_DASH = /—/;
|
|
|
3631
3631
|
var TAG = /<\/?[A-Za-z][\w.:-]*(?:\s[^<>]*)?\/?>/g;
|
|
3632
3632
|
function maskNonProse(source) {
|
|
3633
3633
|
const blank3 = (m) => " ".repeat(m.length);
|
|
3634
|
-
return source.replace(/^---\n[\s\S]*?\n---/, blank3).replace(/<script\b[\s\S]*?<\/script\s*>/gi, blank3).replace(/<style\b[\s\S]*?<\/style\s*>/gi, blank3).replace(/<!--[\s\S]*?-->/g, blank3).replace(/\/\*[\s\S]*?\*\//g, blank3).replace(/(^|[^:])\/\/[^\n]*/g, (m, p1) => p1 + " ".repeat(m.length - p1.length));
|
|
3634
|
+
return source.replace(/^---\n[\s\S]*?\n---/, blank3).replace(/<script\b[\s\S]*?<\/script\s*>/gi, blank3).replace(/<style\b[\s\S]*?<\/style\s*>/gi, blank3).replace(/<(blockquote|q|pre|code|samp|kbd)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, blank3).replace(/<!--[\s\S]*?-->/g, blank3).replace(/\/\*[\s\S]*?\*\//g, blank3).replace(/(^|[^:])\/\/[^\n]*/g, (m, p1) => p1 + " ".repeat(m.length - p1.length));
|
|
3635
3635
|
}
|
|
3636
3636
|
function markdownProse(source) {
|
|
3637
3637
|
const blank3 = (m) => " ".repeat(m.length);
|
|
@@ -3914,6 +3914,8 @@ function firstGroup(match) {
|
|
|
3914
3914
|
if (!value) return void 0;
|
|
3915
3915
|
return { value, index: match.index ?? 0 };
|
|
3916
3916
|
}
|
|
3917
|
+
var PREVIEW_TITLE = /property\s*=\s*["']og:title["']|(?:^|[\s{,])["']?(?:ogTitle|og:title)["']?\s*:|openGraph\s*:\s*\{[^}]*?\btitle\s*:/;
|
|
3918
|
+
var PREVIEW_DESCRIPTION = /property\s*=\s*["']og:description["']|(?:^|[\s{,])["']?(?:ogDescription|og:description)["']?\s*:|openGraph\s*:\s*\{[^}]*?\bdescription\s*:/;
|
|
3917
3919
|
function readMetadata(source) {
|
|
3918
3920
|
const htmlTitle = firstGroup(HTML_TITLE.exec(source));
|
|
3919
3921
|
const title = htmlTitle ?? firstGroup(KEYED_TITLE.exec(source));
|
|
@@ -3931,6 +3933,9 @@ function readMetadata(source) {
|
|
|
3931
3933
|
description: description && !/^\{|\$\{/.test(description.value) ? description : void 0,
|
|
3932
3934
|
hasTitle: ANY_TITLE.test(source),
|
|
3933
3935
|
hasDescription: ANY_DESCRIPTION.test(source),
|
|
3936
|
+
hasPreviewTitle: PREVIEW_TITLE.test(source),
|
|
3937
|
+
hasPreviewDescription: PREVIEW_DESCRIPTION.test(source),
|
|
3938
|
+
hasPreview: PREVIEW_TITLE.test(source) && PREVIEW_DESCRIPTION.test(source),
|
|
3934
3939
|
declares: DECLARES.test(source),
|
|
3935
3940
|
computedTitle: COMPUTED_TITLE.test(source),
|
|
3936
3941
|
noindex: NOINDEX.some((re) => re.test(source))
|
|
@@ -3943,6 +3948,7 @@ var TITLE_BUDGET = 60;
|
|
|
3943
3948
|
var DESCRIPTION_BUDGET = 155;
|
|
3944
3949
|
|
|
3945
3950
|
// src/check/detectors/metadata.ts
|
|
3951
|
+
var HEAD_FRAGMENT = /<svelte:head[\s>]|<head[\s>]|<Head[\s>]|<Helmet[\s>]|slot\s*=\s*["']head["']/;
|
|
3946
3952
|
var INVENTED_DATE = /(?:lastModified|datePublished|dateModified|publishedTime|modifiedTime)\s*:\s*(?:new Date\(\s*\)|Date\.now\(\s*\))/;
|
|
3947
3953
|
var metadata = {
|
|
3948
3954
|
name: "metadata",
|
|
@@ -3976,9 +3982,19 @@ var metadata = {
|
|
|
3976
3982
|
if (indexable && !found.hasDescription) {
|
|
3977
3983
|
at("J-121", 0, "this page has no meta description \u2014 the search engine writes one from whatever text it finds first, usually the navigation");
|
|
3978
3984
|
}
|
|
3985
|
+
if (indexable && !found.hasPreview) {
|
|
3986
|
+
const missing = [!found.hasPreviewTitle && "og:title", !found.hasPreviewDescription && "og:description"].filter(Boolean).join(" and ");
|
|
3987
|
+
at("J-164", 0, found.hasPreviewTitle || found.hasPreviewDescription ? `this page has no ${missing} \u2014 a link to it, pasted into a message or a post, shows a card with that part guessed` : "this page has no link preview (og:title and og:description) \u2014 a link to it, pasted into a message or a post, shows a card built from whatever the page happens to carry");
|
|
3988
|
+
}
|
|
3979
3989
|
if (!indexable && !found.noindex) {
|
|
3980
3990
|
at("J-123", 0, `this surface is ${ctx.mode}, which is what somebody reaches after signing in, and the page carries no noindex \u2014 robots.txt is public and advisory, and is not this`);
|
|
3981
3991
|
}
|
|
3992
|
+
} else {
|
|
3993
|
+
const own = HEAD_FRAGMENT.test(raw) ? HTML_DESCRIPTION.exec(raw) ?? HTML_DESCRIPTION_REVERSED.exec(raw) : null;
|
|
3994
|
+
const indexable = ctx.mode !== "product" && ctx.mode !== "operator";
|
|
3995
|
+
if (indexable && own && found.description && !found.hasPreviewDescription) {
|
|
3996
|
+
at("J-164", own.index, `this file writes the page's description but no og:description${found.hasPreviewTitle ? "" : " or og:title"} \u2014 a link to it, pasted into a message or a post, shows a card without the description written here`);
|
|
3997
|
+
}
|
|
3982
3998
|
}
|
|
3983
3999
|
return findings.filter((f) => f.ruleId === ctx.ruleId);
|
|
3984
4000
|
}
|
|
@@ -4252,7 +4268,7 @@ var HEADLINE_SELECTOR = /\bh1\b|\bh2\b|hero|display|headline|title/i;
|
|
|
4252
4268
|
var SERIF = /font-family\s*:\s*([^;}]+)/i;
|
|
4253
4269
|
function serifFamily(value) {
|
|
4254
4270
|
const first = value.split(",")[0].trim().toLowerCase();
|
|
4255
|
-
if (/sans|mono|system-ui|ui-sans|inter\b|helvetica|arial/.test(first)) return false;
|
|
4271
|
+
if (/sans|mono|system-ui|ui-sans|-apple-system|blinkmacsystemfont|segoe|roboto|inter\b|helvetica|arial/.test(first)) return false;
|
|
4256
4272
|
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());
|
|
4257
4273
|
}
|
|
4258
4274
|
var italicSerifDisplay = {
|
|
@@ -4837,6 +4853,9 @@ function auditTokenLayer(projectRoot) {
|
|
|
4837
4853
|
}
|
|
4838
4854
|
|
|
4839
4855
|
// src/commands/check.ts
|
|
4856
|
+
function checkExitCode(result) {
|
|
4857
|
+
return result.hasError ? 1 : 0;
|
|
4858
|
+
}
|
|
4840
4859
|
var LEGACY_INDEX_REL = join25(".jig", "rules.index.json");
|
|
4841
4860
|
function resolveIndexPath(projectRoot) {
|
|
4842
4861
|
const legacy = join25(projectRoot, LEGACY_INDEX_REL);
|
|
@@ -4913,7 +4932,9 @@ function check(opts) {
|
|
|
4913
4932
|
const withStyles = files.filter((f) => {
|
|
4914
4933
|
try {
|
|
4915
4934
|
const src = readFileSync21(join25(opts.projectRoot, f), "utf8");
|
|
4916
|
-
|
|
4935
|
+
if (hasExtension(f, CSS_EXTENSIONS)) return src.trim().length > 0;
|
|
4936
|
+
if (maskNonStyleRegions(src, f).trim().length > 0) return true;
|
|
4937
|
+
return isMarkupHost(f) && classAttributeValues(src).some((a) => a.classes.trim().length > 0);
|
|
4917
4938
|
} catch {
|
|
4918
4939
|
return false;
|
|
4919
4940
|
}
|
|
@@ -6119,12 +6140,18 @@ var PROBE_SCRIPT = `(async () => {
|
|
|
6119
6140
|
// the page's own copy around it still counts. So is a tool's output or a
|
|
6120
6141
|
// command shown in <pre>, <code>, <samp> or <kbd>: jig-site's home page
|
|
6121
6142
|
// quotes \`jig check\`'s real output, whose own wording has an em dash, and
|
|
6122
|
-
// the probe blocked a page for words it had rightly not rewritten.
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6143
|
+
// the probe blocked a page for words it had rightly not rewritten. The
|
|
6144
|
+
// quotations are hidden while the text is read, not cut out of it by string:
|
|
6145
|
+
// an inline <code>A-01</code> earlier in the prose removed "A-01" from the
|
|
6146
|
+
// output block below it, so the block no longer matched and its dash counted.
|
|
6147
|
+
const quotedEls = [...document.querySelectorAll('blockquote, q, pre, code, samp, kbd')];
|
|
6148
|
+
const hide = document.createElement('style');
|
|
6149
|
+
hide.textContent = '[data-jig-quoted] { display: none !important; }';
|
|
6150
|
+
for (const q of quotedEls) q.setAttribute('data-jig-quoted', '');
|
|
6151
|
+
document.head.appendChild(hide);
|
|
6152
|
+
const ownText = document.body.innerText || '';
|
|
6153
|
+
hide.remove();
|
|
6154
|
+
for (const q of quotedEls) q.removeAttribute('data-jig-quoted');
|
|
6128
6155
|
const junk = [...new Set(text.match(/\\$\\{|\\{\\{|\\bundefined\\b|\\bNaN\\b|\\[object Object\\]/g) || [])];
|
|
6129
6156
|
const unresolved = new Set();
|
|
6130
6157
|
for (const sheet of document.styleSheets) {
|
|
@@ -6294,6 +6321,30 @@ var PROBE_SCRIPT = `(async () => {
|
|
|
6294
6321
|
fauxSeen.add(key);
|
|
6295
6322
|
fauxFaces.push({ family: first.trim().replace(/^["']|["']$/g, ''), weight, style: italic ? 'italic' : 'normal', text: (el.innerText || '').trim().slice(0, 40) });
|
|
6296
6323
|
}
|
|
6324
|
+
// M-01: \`editorial\` allows no box that scrolls sideways on a phone; a code
|
|
6325
|
+
// block wraps and a wide table stacks (L-01, step 6). In a control run a
|
|
6326
|
+
// spec said the code would wrap and the built page scrolled it, and nothing
|
|
6327
|
+
// measured the box: the page itself was not wider than the screen.
|
|
6328
|
+
const sidewaysBoxes = [];
|
|
6329
|
+
for (const el of [...document.body.querySelectorAll('*')].slice(0, 4000)) {
|
|
6330
|
+
if (sidewaysBoxes.length >= 5) break;
|
|
6331
|
+
const ox = getComputedStyle(el).overflowX;
|
|
6332
|
+
if ((ox !== 'auto' && ox !== 'scroll') || el.scrollWidth <= el.clientWidth + 1 || !vis(el)) continue;
|
|
6333
|
+
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
|
+
}
|
|
6335
|
+
// A table cell given its own display (flex, grid, block) while its table is
|
|
6336
|
+
// still drawn as a table stops being a cell: it no longer takes its row's
|
|
6337
|
+
// height, and its border no longer meets the row's. A team table set its
|
|
6338
|
+
// name cells to flex, to line up a name and a tag, and every name drifted
|
|
6339
|
+
// above its row. A stacked table (the table itself set to block) is left out.
|
|
6340
|
+
const brokenCells = [];
|
|
6341
|
+
for (const cell of [...document.querySelectorAll('td, th')].slice(0, 2000)) {
|
|
6342
|
+
if (brokenCells.length >= 5) break;
|
|
6343
|
+
const table = cell.closest('table');
|
|
6344
|
+
const display = getComputedStyle(cell).display;
|
|
6345
|
+
if (!table || getComputedStyle(table).display !== 'table' || display === 'table-cell' || display === 'none' || !vis(cell)) continue;
|
|
6346
|
+
brokenCells.push({ cell: cell.tagName.toLowerCase() + (cell.classList[0] ? '.' + cell.classList[0] : ''), text: (cell.innerText || '').trim().replace(/\\s+/g, ' ').slice(0, 40), display });
|
|
6347
|
+
}
|
|
6297
6348
|
return JSON.stringify({
|
|
6298
6349
|
jigProbe: ${PROBE_VERSION},
|
|
6299
6350
|
url: location.href,
|
|
@@ -6301,6 +6352,8 @@ var PROBE_SCRIPT = `(async () => {
|
|
|
6301
6352
|
scrollWidth: doc.scrollWidth,
|
|
6302
6353
|
clientWidth: doc.clientWidth,
|
|
6303
6354
|
sidewaysScroll: doc.scrollWidth > doc.clientWidth,
|
|
6355
|
+
sidewaysBoxes,
|
|
6356
|
+
brokenCells,
|
|
6304
6357
|
bodyFont: getComputedStyle(document.body).fontFamily,
|
|
6305
6358
|
defaultFont: getComputedStyle(document.body).fontFamily === defaultFont,
|
|
6306
6359
|
unresolvedTokens: [...unresolved],
|
|
@@ -6317,6 +6370,7 @@ var PROBE_SCRIPT = `(async () => {
|
|
|
6317
6370
|
canonical: (document.querySelector('link[rel=canonical]') || {}).href || '',
|
|
6318
6371
|
robots: (document.querySelector('meta[name=robots]') || {}).content || '',
|
|
6319
6372
|
ogTitle: (document.querySelector('meta[property="og:title"]') || {}).content || '',
|
|
6373
|
+
ogDescription: (document.querySelector('meta[property="og:description"]') || {}).content || '',
|
|
6320
6374
|
ogImage: (document.querySelector('meta[property="og:image"]') || {}).content || '',
|
|
6321
6375
|
},
|
|
6322
6376
|
contentWidth: Math.round(region.getBoundingClientRect().width),
|
|
@@ -6765,29 +6819,42 @@ function readProbes(projectRoot, dir, errors) {
|
|
|
6765
6819
|
}
|
|
6766
6820
|
var TITLE_BUDGET2 = 60;
|
|
6767
6821
|
var DESCRIPTION_BUDGET2 = 155;
|
|
6768
|
-
function
|
|
6822
|
+
function probeCheck(probes, verdictOf, indexable = true, mode) {
|
|
6769
6823
|
const errors = [];
|
|
6824
|
+
const failures = [];
|
|
6770
6825
|
const clean = (id) => ["ok", "n/a"].includes(verdictOf(id) ?? "");
|
|
6771
6826
|
const at = (p) => `probe-${p.width}.json`;
|
|
6772
6827
|
for (const p of probes) {
|
|
6773
|
-
if (p.sidewaysScroll
|
|
6774
|
-
|
|
6828
|
+
if (p.sidewaysScroll) {
|
|
6829
|
+
if (clean("D-115")) {
|
|
6830
|
+
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.`);
|
|
6831
|
+
} else {
|
|
6832
|
+
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).`);
|
|
6833
|
+
}
|
|
6834
|
+
}
|
|
6835
|
+
if (mode === "editorial" && p.width <= 480 && p.sidewaysBoxes?.length) {
|
|
6836
|
+
const shown = p.sidewaysBoxes.map((b) => `${b.box}${b.text ? ` ("${b.text}")` : ""}, ${b.scrollWidth}px in ${b.clientWidth}px`).join("; ");
|
|
6837
|
+
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).`);
|
|
6838
|
+
}
|
|
6839
|
+
if (p.brokenCells?.length) {
|
|
6840
|
+
const shown = p.brokenCells.map((c) => `${c.cell}${c.text ? ` ("${c.text}")` : ""} is display: ${c.display}`).join("; ");
|
|
6841
|
+
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).`);
|
|
6775
6842
|
}
|
|
6776
6843
|
if (p.defaultFont) {
|
|
6777
|
-
|
|
6844
|
+
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.`);
|
|
6778
6845
|
}
|
|
6779
6846
|
if (p.fauxFaces?.length) {
|
|
6780
6847
|
const shown = p.fauxFaces.map((f) => `${f.family} ${f.weight}${f.style === "italic" ? " italic" : ""} ("${f.text}")`).join(", ");
|
|
6781
|
-
|
|
6848
|
+
failures.push(`${at(p)}: the page asks for ${shown}, and no loaded face covers ${p.fauxFaces.length === 1 ? "it" : "them"}, so the browser fakes the weight or slant from the nearest face it has. Load that weight, or use one the page already loads.`);
|
|
6782
6849
|
}
|
|
6783
6850
|
if (p.unresolvedTokens.length) {
|
|
6784
|
-
|
|
6851
|
+
failures.push(`${at(p)}: ${p.unresolvedTokens.length} token(s) have no value in the browser (${p.unresolvedTokens.slice(0, 6).join(", ")}) \u2014 every property using them is dropped (H-117).`);
|
|
6785
6852
|
}
|
|
6786
6853
|
for (const inv of p.orderInversions ?? []) {
|
|
6787
|
-
|
|
6854
|
+
failures.push(`${at(p)}: "${inv.seenFirst}" is read first on screen but comes after "${inv.markupFirst}" in the markup. At this width the markup order is not the reading order (H-119) \u2014 reorder the document, or move it with CSS that leaves the order intact.`);
|
|
6788
6855
|
}
|
|
6789
6856
|
if (p.emDashes?.length) {
|
|
6790
|
-
|
|
6857
|
+
failures.push(`${at(p)}: the rendered page shows an em dash in ${p.emDashes.map((t) => `"${t}"`).join(", ")} (I-118) \u2014 use a full stop, a comma, a colon, or a second element.`);
|
|
6791
6858
|
}
|
|
6792
6859
|
if (clean("B-106") && p.strandedCount) {
|
|
6793
6860
|
const shown = (p.strandedWords ?? []).map((w) => `"${w.word}" (in "${w.text}\u2026")`).join(", ");
|
|
@@ -6801,17 +6868,21 @@ function probeContradictions(probes, verdictOf, indexable = true) {
|
|
|
6801
6868
|
if (head && p.width === probes[0]?.width) {
|
|
6802
6869
|
const noindex = /noindex/i.test(head.robots);
|
|
6803
6870
|
if (indexable && !noindex) {
|
|
6804
|
-
if (!head.title)
|
|
6805
|
-
else if (head.title.length > TITLE_BUDGET2)
|
|
6806
|
-
if (!head.description)
|
|
6807
|
-
else if (head.description.length > DESCRIPTION_BUDGET2)
|
|
6871
|
+
if (!head.title) failures.push(`${at(p)}: the page served no <title> (J-121) \u2014 a search result then shows a truncated URL.`);
|
|
6872
|
+
else if (head.title.length > TITLE_BUDGET2) failures.push(`${at(p)}: the title served is ${head.title.length} characters, past the ${TITLE_BUDGET2} a search result shows (J-122): "${head.title}".`);
|
|
6873
|
+
if (!head.description) failures.push(`${at(p)}: the page served no meta description (J-121) \u2014 the search engine writes one from whatever text it finds first, usually the navigation.`);
|
|
6874
|
+
else if (head.description.length > DESCRIPTION_BUDGET2) failures.push(`${at(p)}: the description served is ${head.description.length} characters, past the ${DESCRIPTION_BUDGET2} (J-122).`);
|
|
6875
|
+
if (typeof head.ogDescription === "string" && (!head.ogTitle || !head.ogDescription)) {
|
|
6876
|
+
const missing = [!head.ogTitle && "og:title", !head.ogDescription && "og:description"].filter(Boolean).join(" and ");
|
|
6877
|
+
failures.push(`${at(p)}: the page served no ${missing} (J-164) \u2014 a link to it, pasted into a message or a post, shows a card built from whatever the page happens to carry.`);
|
|
6878
|
+
}
|
|
6808
6879
|
}
|
|
6809
6880
|
if (!indexable && !noindex) {
|
|
6810
|
-
|
|
6881
|
+
failures.push(`${at(p)}: this page is not indexable, and the page served no noindex (J-123). robots.txt is public and advisory, and is not this.`);
|
|
6811
6882
|
}
|
|
6812
6883
|
}
|
|
6813
6884
|
if (p.junkText.length) {
|
|
6814
|
-
|
|
6885
|
+
failures.push(`${at(p)}: the rendered text contains ${p.junkText.map((j) => `"${j}"`).join(", ")} \u2014 template code or a failed value is showing to readers.`);
|
|
6815
6886
|
}
|
|
6816
6887
|
}
|
|
6817
6888
|
const navClean = clean("P-14") || clean("E-61");
|
|
@@ -6842,7 +6913,7 @@ function probeContradictions(probes, verdictOf, indexable = true) {
|
|
|
6842
6913
|
if (broken.length) errors.push(`P-14 is "${verdictOf("P-14")}", but ${at(phone)} operated the menu: ${broken.join("; ")}.`);
|
|
6843
6914
|
}
|
|
6844
6915
|
}
|
|
6845
|
-
return errors;
|
|
6916
|
+
return { contradictions: errors, failures };
|
|
6846
6917
|
}
|
|
6847
6918
|
|
|
6848
6919
|
// src/check/decisions.ts
|
|
@@ -7336,6 +7407,7 @@ function verifyVerdicts(opts) {
|
|
|
7336
7407
|
const v = screenVerdicts.find((x) => typeof x.id === "string" && x.id.trim().toUpperCase() === id);
|
|
7337
7408
|
return typeof v?.verdict === "string" ? v.verdict : void 0;
|
|
7338
7409
|
};
|
|
7410
|
+
const measured = [];
|
|
7339
7411
|
let specFront = "";
|
|
7340
7412
|
try {
|
|
7341
7413
|
specFront = readText(join38(opts.projectRoot, ".jig", "specs", `${opts.surface}.spec.md`)).split(/^---\s*$/m)[1] ?? "";
|
|
@@ -7344,14 +7416,24 @@ function verifyVerdicts(opts) {
|
|
|
7344
7416
|
if (specIndexableField(specFront) === "unreadable") {
|
|
7345
7417
|
errors.push(`.jig/specs/${opts.surface}.spec.md: \`indexable:\` is neither true nor false, so this review cannot tell whether the page is meant to be found and does not guess. Write \`indexable: true\` or \`indexable: false\`, and put the reason in the spec's body.`);
|
|
7346
7418
|
} else {
|
|
7347
|
-
|
|
7419
|
+
const mode = /^\s*mode\s*:\s*["']?(\w+)/im.exec(specFront)?.[1]?.toLowerCase();
|
|
7420
|
+
const probed = probeCheck(probes, verdictOf, specIndexable(opts.projectRoot, opts.surface), mode);
|
|
7421
|
+
errors.push(...probed.contradictions);
|
|
7422
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7423
|
+
for (const failure of probed.failures) {
|
|
7424
|
+
const what = failure.replace(/^probe-\d+\.json: /, "");
|
|
7425
|
+
if (!seen.has(what)) {
|
|
7426
|
+
seen.add(what);
|
|
7427
|
+
measured.push(failure);
|
|
7428
|
+
}
|
|
7429
|
+
}
|
|
7348
7430
|
}
|
|
7349
7431
|
if (screen.state === "ran" && !rendered) screen.state = "skipped";
|
|
7350
7432
|
const decisions = checkDecisions(opts.projectRoot, dir, errors);
|
|
7351
7433
|
const field = (a) => a.state === "ran" ? `ran:${a.judged}` : `${a.state}:${a.judged}${a.state === "incomplete" ? `/${a.total}` : ""}`;
|
|
7352
|
-
const line = `JIG_VERDICTS: surface=${opts.surface} screen=${field(screen)} code=${field(code)} decisions=${field(decisions)} rendered=${rendered ? "yes" : "no"} findings=${screen.findings + code.findings + decisions.findings}${(screen.ruled ?? 0) + (code.ruled ?? 0) ? ` ruled=${(screen.ruled ?? 0) + (code.ruled ?? 0)}` : ""}`;
|
|
7434
|
+
const line = `JIG_VERDICTS: surface=${opts.surface} screen=${field(screen)} code=${field(code)} decisions=${field(decisions)} rendered=${rendered ? "yes" : "no"} findings=${screen.findings + code.findings + decisions.findings}${(screen.ruled ?? 0) + (code.ruled ?? 0) ? ` ruled=${(screen.ruled ?? 0) + (code.ruled ?? 0)}` : ""}${measured.length ? ` measured=${measured.length}` : ""}`;
|
|
7353
7435
|
const previous = previousFindings(opts.projectRoot, dir);
|
|
7354
|
-
return { ok: errors.length === 0, errors, screen, code, decisions, rendered, line, ...previous ? { previous } : {} };
|
|
7436
|
+
return { ok: errors.length === 0, errors, screen, code, decisions, rendered, measured, line, ...previous ? { previous } : {} };
|
|
7355
7437
|
}
|
|
7356
7438
|
function previousFindings(projectRoot, dir) {
|
|
7357
7439
|
const git = (args) => execFileSync3("git", args, { cwd: projectRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -7816,6 +7898,10 @@ function pageStatus(projectRoot, surface) {
|
|
|
7816
7898
|
if (stale) return { surface, state: "reprobe", detail: `its page is as judged, but the styles or scripts it loads changed: re-probe it (\`jig verdicts ${surface} --reprobe\`)` };
|
|
7817
7899
|
if (!v.ok) return { surface, state: "incomplete", detail: `its critique is not complete: ${v.errors[0]}` };
|
|
7818
7900
|
const findings = v.screen.findings + v.code.findings + v.decisions.findings;
|
|
7901
|
+
if (v.measured.length) {
|
|
7902
|
+
const also = findings > 0 ? `, and ${findings} finding${findings === 1 ? "" : "s"} the owner has not ruled on` : "";
|
|
7903
|
+
return { surface, state: "findings", detail: `the page fails ${v.measured.length} check${v.measured.length === 1 ? "" : "s"} the render probe measured (\`jig verdicts ${surface}\` lists them)${also}` };
|
|
7904
|
+
}
|
|
7819
7905
|
if (findings > 0) return { surface, state: "findings", detail: `${findings} finding${findings === 1 ? "" : "s"} the owner has not ruled on` };
|
|
7820
7906
|
return { surface, state: "judged", detail: "judged as it stands, nothing open" };
|
|
7821
7907
|
}
|
|
@@ -7879,7 +7965,15 @@ function ownerWordProblem(word, quoted, owner, where) {
|
|
|
7879
7965
|
function specChecksum(body) {
|
|
7880
7966
|
return createHash2("sha256").update(body.replace(/\r\n/g, "\n").replace(/^\s*(confirmed|mockup)\s*:.*$/gim, "")).digest("hex");
|
|
7881
7967
|
}
|
|
7882
|
-
|
|
7968
|
+
function mockupField(body) {
|
|
7969
|
+
const raw = /^\s*mockup\s*:\s*(.*)$/im.exec(body.split(/^---\s*$/m)[1] ?? "")?.[1]?.trim() ?? "";
|
|
7970
|
+
const double = /^"((?:[^"\\]|\\.)*)"\s*(?:#.*)?$/.exec(raw);
|
|
7971
|
+
if (double) return double[1].replace(/\\(.)/g, "$1").trim();
|
|
7972
|
+
const single = /^'((?:[^']|'')*)'\s*(?:#.*)?$/.exec(raw);
|
|
7973
|
+
if (single) return single[1].replace(/''/g, "'").trim();
|
|
7974
|
+
return raw;
|
|
7975
|
+
}
|
|
7976
|
+
var mockupLine = mockupField;
|
|
7883
7977
|
function mockupWordProblems(spec, then, owner) {
|
|
7884
7978
|
const now = mockupLine(spec.body);
|
|
7885
7979
|
if (now === mockupLine(then)) return [];
|
|
@@ -8199,7 +8293,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8199
8293
|
}
|
|
8200
8294
|
if (command === "mockup" && spec) {
|
|
8201
8295
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
8202
|
-
const mockup =
|
|
8296
|
+
const mockup = mockupField(spec.body);
|
|
8203
8297
|
if (/^pending/i.test(mockup) || !mockup) problems.push(`${spec.path}: \`mockup:\` is still pending. It records the user's own word \u2014 approved, or skipped with their reason.`);
|
|
8204
8298
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
8205
8299
|
if (/^approved/i.test(mockup)) {
|
|
@@ -8212,7 +8306,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8212
8306
|
if (command === "tweak" && spec) problems.push(...tweakProblems(root, spec, owner, start));
|
|
8213
8307
|
if ((command === "spec" || command === "make") && spec) {
|
|
8214
8308
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
8215
|
-
const mockup =
|
|
8309
|
+
const mockup = mockupField(spec.body);
|
|
8216
8310
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
8217
8311
|
if (/^approved/i.test(mockup) && /\.html?$/i.test(at) && !/^https?:/i.test(at)) {
|
|
8218
8312
|
const drift = approvedDrawingProblems(root, spec.body, at);
|
|
@@ -8233,7 +8327,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8233
8327
|
}
|
|
8234
8328
|
if (command === "mockup" && spec) {
|
|
8235
8329
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
8236
|
-
const mockup =
|
|
8330
|
+
const mockup = mockupField(spec.body);
|
|
8237
8331
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
8238
8332
|
if (/^approved/i.test(mockup) && at && !/^https?:/i.test(at) && existsSync27(join43(root, at))) {
|
|
8239
8333
|
const changedAfter = drawingChangedAfterApproval(root, spec.path, at);
|
|
@@ -8361,8 +8455,8 @@ function tweakProblems(root, spec, owner = "", start) {
|
|
|
8361
8455
|
const problems = [];
|
|
8362
8456
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
8363
8457
|
if (!/^\s*confirmed\s*:\s*true\b/im.test(front)) problems.push(`${spec.path} is not confirmed. A tweak changes a page the owner has confirmed; an unconfirmed spec goes through \`spec\`.`);
|
|
8364
|
-
const mockup =
|
|
8365
|
-
if (!/^(approved|skipped)/i.test(mockup)) problems.push(`${spec.path}: \`mockup:\` is ${mockup || "empty"}. A tweak changes a page whose drawing the owner has approved (or skipped); take a new page through \`mockup\` and \`make\`.`);
|
|
8458
|
+
const mockup = mockupField(spec.body);
|
|
8459
|
+
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\`.`);
|
|
8366
8460
|
else problems.push(...structureSinceApproval(root, spec, front));
|
|
8367
8461
|
const surface = spec.slug;
|
|
8368
8462
|
const record = readTweak(join43(root, ".jig", "critique", surface));
|
|
@@ -8536,6 +8630,13 @@ ${result.report}`);
|
|
|
8536
8630
|
` + shown.join("\n") + (v.errors.length > shown.length ? `
|
|
8537
8631
|
\u2026 and ${v.errors.length - shown.length} more` : "")
|
|
8538
8632
|
);
|
|
8633
|
+
} else if (v.measured.length && command !== "critique") {
|
|
8634
|
+
const shown = v.measured.slice(0, 6).map((e) => ` ${e}`);
|
|
8635
|
+
problems.push(
|
|
8636
|
+
`jig verdicts ${surface}: the page fails what the render probe measured. Fix the page, then re-take the probes (\`jig verdicts ${surface} --reprobe\`).
|
|
8637
|
+
` + shown.join("\n") + (v.measured.length > shown.length ? `
|
|
8638
|
+
\u2026 and ${v.measured.length - shown.length} more` : "")
|
|
8639
|
+
);
|
|
8539
8640
|
}
|
|
8540
8641
|
}
|
|
8541
8642
|
}
|
|
@@ -8874,6 +8975,10 @@ program.command("verdicts").description("Verify a critique's verdict files and c
|
|
|
8874
8975
|
console.error(` The page changed after these probes. \`jig verdicts ${surface} --reprobe\` re-takes them on the page as it is now.`);
|
|
8875
8976
|
}
|
|
8876
8977
|
if (result.ok) console.log(` Every rule in both passes has a verdict.`);
|
|
8978
|
+
if (result.measured.length) {
|
|
8979
|
+
console.log(` The page fails ${result.measured.length} check(s) the render probe measured. They are findings on the page, for make or tweak to fix; a critique reports them and leaves the page as it is:`);
|
|
8980
|
+
for (const failure of result.measured) console.log(` ! ${failure}`);
|
|
8981
|
+
}
|
|
8877
8982
|
const since = result.decisions.since ?? [];
|
|
8878
8983
|
if (since.length) console.log(` ${since.length} decision(s) recorded after this critique, for the next one to judge: ${since.join(", ")}.`);
|
|
8879
8984
|
const p = result.previous;
|
|
@@ -8894,7 +8999,7 @@ program.command("check").description("Check the repo against Jig's mechanical +
|
|
|
8894
8999
|
// this as widening the RULES and attested a clean diff as a clean project.
|
|
8895
9000
|
"scan every file in the repo, not just those changed since HEAD (same rules either way)",
|
|
8896
9001
|
false
|
|
8897
|
-
).option("--ci", "mechanical bucket only
|
|
9002
|
+
).option("--ci", "mechanical bucket only, so the run is deterministic", false).option("--json", "emit findings as JSON", false).action((opts) => {
|
|
8898
9003
|
const projectRoot = findProjectRoot(process.cwd());
|
|
8899
9004
|
try {
|
|
8900
9005
|
const result = check({
|
|
@@ -8905,7 +9010,8 @@ program.command("check").description("Check the repo against Jig's mechanical +
|
|
|
8905
9010
|
ci: opts.ci
|
|
8906
9011
|
});
|
|
8907
9012
|
console.log(opts.json ? JSON.stringify(result.findings, null, 2) : result.report);
|
|
8908
|
-
|
|
9013
|
+
const code = checkExitCode(result);
|
|
9014
|
+
if (code !== 0) process.exit(code);
|
|
8909
9015
|
} catch (err) {
|
|
8910
9016
|
console.error(err.message);
|
|
8911
9017
|
process.exit(1);
|