jig-ui 0.22.0 → 0.24.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 +185 -0
- package/README.md +107 -58
- package/dist/index.js +1066 -536
- package/examples/G-161.html +7 -0
- package/examples/G-162.html +18 -0
- package/examples/G-163.html +7 -0
- package/examples/I-148.html +7 -0
- package/examples/I-149.html +7 -0
- package/examples/I-150.html +7 -0
- package/examples/I-151.html +7 -0
- package/examples/I-152.html +7 -0
- package/examples/I-153.html +7 -0
- package/examples/I-154.html +7 -0
- package/examples/I-155.html +7 -0
- package/examples/I-156.html +7 -0
- package/examples/I-157.html +7 -0
- package/examples/I-158.html +7 -0
- package/examples/I-159.html +7 -0
- package/examples/I-160.html +7 -0
- package/package.json +4 -3
- package/rules/00-anti-patterns.md +19 -0
- package/rules/01-modes.md +1 -1
- package/rules/02-tokens.md +3 -0
- package/rules/03-patterns.md +2 -1
- package/rules/05-copy.md +88 -2
- package/rules.index.json +114 -0
- package/templates/COMMAND.md.tmpl +133 -38
- package/templates/SKILL.md.tmpl +13 -1
- package/templates/command-metadata.json +5 -0
package/dist/index.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import { readFileSync as
|
|
6
|
-
import { homedir as
|
|
7
|
-
import { join as
|
|
5
|
+
import { existsSync as existsSync28, readFileSync as readFileSync37 } from "fs";
|
|
6
|
+
import { homedir as homedir3 } from "os";
|
|
7
|
+
import { join as join44 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/paths.ts
|
|
10
10
|
import { existsSync } from "fs";
|
|
@@ -28,7 +28,7 @@ function assetRoot(startDir = getPackageRoot()) {
|
|
|
28
28
|
let current = startDir;
|
|
29
29
|
const { root } = parse(startDir);
|
|
30
30
|
while (true) {
|
|
31
|
-
if (existsSync(join(current, ASSET_MARKER))) return current;
|
|
31
|
+
if (existsSync(join(current, ASSET_MARKER))) return sourceOf(current);
|
|
32
32
|
if (current === root) {
|
|
33
33
|
throw new Error(
|
|
34
34
|
`assetRoot(): could not find an ancestor of "${startDir}" containing "${ASSET_MARKER}"`
|
|
@@ -37,6 +37,11 @@ function assetRoot(startDir = getPackageRoot()) {
|
|
|
37
37
|
current = dirname(current);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
function sourceOf(found) {
|
|
41
|
+
if (isPublishedBuild(found)) return found;
|
|
42
|
+
const repo = join(found, "..", "..");
|
|
43
|
+
return existsSync(join(repo, ASSET_MARKER)) && existsSync(join(repo, "packages", "cli", "package.json")) ? repo : found;
|
|
44
|
+
}
|
|
40
45
|
function isPublishedBuild(packageRoot2) {
|
|
41
46
|
return packageRoot2.split(/[\\/]/).includes("node_modules");
|
|
42
47
|
}
|
|
@@ -50,8 +55,8 @@ function cliInvocation(version2) {
|
|
|
50
55
|
var JIG_GATE_HOOK = /(?:\bjig-ui@[^\s"]+|^jig) gate\b/;
|
|
51
56
|
|
|
52
57
|
// src/commands/install.ts
|
|
53
|
-
import { existsSync as
|
|
54
|
-
import { join as
|
|
58
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "fs";
|
|
59
|
+
import { join as join8 } from "path";
|
|
55
60
|
|
|
56
61
|
// src/adapters/types.ts
|
|
57
62
|
var COMMAND_DESCRIPTION = "Run a Jig design-system command: set the project up, plan and build one feature at a time (decide, spec, mockup, make, critique), make a small change to a built page (tweak), check the UI against the rules, or refresh the install.";
|
|
@@ -83,6 +88,236 @@ function agentsPointer(skillPath) {
|
|
|
83
88
|
].join("\n");
|
|
84
89
|
}
|
|
85
90
|
|
|
91
|
+
// src/install/agent-files.ts
|
|
92
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
93
|
+
import { homedir } from "os";
|
|
94
|
+
import { join as join4 } from "path";
|
|
95
|
+
|
|
96
|
+
// src/text.ts
|
|
97
|
+
import { readFileSync } from "fs";
|
|
98
|
+
function lf(text) {
|
|
99
|
+
return text.replace(/\r\n/g, "\n");
|
|
100
|
+
}
|
|
101
|
+
function readText(path) {
|
|
102
|
+
return lf(readFileSync(path, "utf8"));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/install/atomic.ts
|
|
106
|
+
import { mkdirSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
107
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
108
|
+
function writeFileAtomic(path, content) {
|
|
109
|
+
const dir = dirname2(path);
|
|
110
|
+
mkdirSync(dir, { recursive: true });
|
|
111
|
+
const tmp = join2(dir, `.${process.pid}-${Date.now()}.tmp`);
|
|
112
|
+
try {
|
|
113
|
+
writeFileSync(tmp, content, "utf8");
|
|
114
|
+
renameWithRetry(tmp, path);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
try {
|
|
117
|
+
unlinkSync(tmp);
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function renameWithRetry(from, to) {
|
|
124
|
+
const retryable = /* @__PURE__ */ new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
125
|
+
for (let waited = 0, delay = 10; ; waited += delay, delay = Math.min(delay * 2, 200)) {
|
|
126
|
+
try {
|
|
127
|
+
renameSync(from, to);
|
|
128
|
+
return;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
const code = err.code ?? "";
|
|
131
|
+
if (process.platform !== "win32" || !retryable.has(code) || waited > 2e3) throw err;
|
|
132
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, delay);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/install/vendor.ts
|
|
138
|
+
import { posix } from "path";
|
|
139
|
+
var { join: join3 } = posix;
|
|
140
|
+
var DELIMITERS = {
|
|
141
|
+
html: ["<!--", "-->"],
|
|
142
|
+
css: ["/*", "*/"]
|
|
143
|
+
};
|
|
144
|
+
function licencePathFor(relPath) {
|
|
145
|
+
const depth = relPath.split("/").length - 1;
|
|
146
|
+
return depth === 0 ? "." : Array(depth).fill("..").join("/");
|
|
147
|
+
}
|
|
148
|
+
function vendorHeader(file, version2, style = "html", licencePath = ".") {
|
|
149
|
+
const [open, close] = DELIMITERS[style];
|
|
150
|
+
const attribution = licencePath === null ? " Licensed Apache-2.0. LICENSE and NOTICE ship beside the jig skill." : ` Licensed Apache-2.0. See ${join3(licencePath, "LICENSE")} and ${join3(licencePath, "NOTICE")}.`;
|
|
151
|
+
return [
|
|
152
|
+
`${open} ${file} \u2014 vendored from Jig v${version2}.`,
|
|
153
|
+
attribution,
|
|
154
|
+
` Edit freely: \`jig update\` will not overwrite a file you have changed. ${close}`,
|
|
155
|
+
"",
|
|
156
|
+
""
|
|
157
|
+
].join("\n");
|
|
158
|
+
}
|
|
159
|
+
function findMarkerRegions(existing) {
|
|
160
|
+
const regions = [];
|
|
161
|
+
let searchFrom = 0;
|
|
162
|
+
let openStart = null;
|
|
163
|
+
while (true) {
|
|
164
|
+
const nextStart = existing.indexOf(BLOCK_START, searchFrom);
|
|
165
|
+
const nextEnd = existing.indexOf(BLOCK_END, searchFrom);
|
|
166
|
+
if (nextStart === -1 && nextEnd === -1) break;
|
|
167
|
+
const endIsNext = nextEnd !== -1 && (nextStart === -1 || nextEnd < nextStart);
|
|
168
|
+
if (endIsNext) {
|
|
169
|
+
if (openStart !== null) {
|
|
170
|
+
regions.push({ start: openStart, end: nextEnd + BLOCK_END.length });
|
|
171
|
+
openStart = null;
|
|
172
|
+
} else {
|
|
173
|
+
regions.push({ start: nextEnd, end: nextEnd + BLOCK_END.length });
|
|
174
|
+
}
|
|
175
|
+
searchFrom = nextEnd + BLOCK_END.length;
|
|
176
|
+
} else {
|
|
177
|
+
if (openStart !== null) {
|
|
178
|
+
regions.push({ start: openStart, end: openStart + BLOCK_START.length });
|
|
179
|
+
}
|
|
180
|
+
openStart = nextStart;
|
|
181
|
+
searchFrom = nextStart + BLOCK_START.length;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (openStart !== null) {
|
|
185
|
+
regions.push({ start: openStart, end: existing.length });
|
|
186
|
+
}
|
|
187
|
+
regions.sort((a, b) => a.start - b.start);
|
|
188
|
+
return regions;
|
|
189
|
+
}
|
|
190
|
+
function stripMarkerRegions(existing) {
|
|
191
|
+
const regions = findMarkerRegions(existing);
|
|
192
|
+
if (regions.length === 0) {
|
|
193
|
+
return { text: existing, insertAt: existing.length, removed: false };
|
|
194
|
+
}
|
|
195
|
+
let text = "";
|
|
196
|
+
let cursor = 0;
|
|
197
|
+
let insertAt = -1;
|
|
198
|
+
regions.forEach((region, i) => {
|
|
199
|
+
let chunk = existing.slice(cursor, region.start);
|
|
200
|
+
if (i > 0) chunk = chunk.replace(/^\n/, "");
|
|
201
|
+
text += chunk;
|
|
202
|
+
if (insertAt === -1) insertAt = text.length;
|
|
203
|
+
cursor = region.end;
|
|
204
|
+
});
|
|
205
|
+
const tail = existing.slice(cursor).replace(/^\n/, "");
|
|
206
|
+
text += tail;
|
|
207
|
+
return { text, insertAt, removed: true };
|
|
208
|
+
}
|
|
209
|
+
function upsertBlock(existing, block) {
|
|
210
|
+
const { text, insertAt, removed } = stripMarkerRegions(existing);
|
|
211
|
+
if (!removed) {
|
|
212
|
+
const separator = text.length && !text.endsWith("\n") ? "\n\n" : text.length ? "\n" : "";
|
|
213
|
+
return `${text}${separator}${block}`;
|
|
214
|
+
}
|
|
215
|
+
return `${text.slice(0, insertAt)}${block}${text.slice(insertAt)}`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/install/agent-files.ts
|
|
219
|
+
var AGENTS_FILE = "AGENTS.md";
|
|
220
|
+
var CLAUDE_FILE = "CLAUDE.md";
|
|
221
|
+
function agentsGuide() {
|
|
222
|
+
return [
|
|
223
|
+
BLOCK_START,
|
|
224
|
+
"",
|
|
225
|
+
"# Jig: the design system for this project's UI",
|
|
226
|
+
"",
|
|
227
|
+
"Before building or reviewing any interface, load the Jig skill and follow it:",
|
|
228
|
+
"`.agents/skills/jig/SKILL.md`, or `skills/jig/SKILL.md` in your agent's own",
|
|
229
|
+
"folder (`.claude/skills/jig/SKILL.md` for Claude Code). It says which rule",
|
|
230
|
+
"files to load for the task.",
|
|
231
|
+
"",
|
|
232
|
+
"## How work goes",
|
|
233
|
+
"",
|
|
234
|
+
"- `/jig decide`, once: the project's design decisions, from the owner.",
|
|
235
|
+
"- For each page or feature: `/jig spec`, then `/jig mockup` (or skip it), then",
|
|
236
|
+
" `/jig make`, then `/jig critique` (now, or left for `/jig ship`).",
|
|
237
|
+
"- A small change to a built page: `/jig tweak`.",
|
|
238
|
+
"- Before a release: `/jig ship`.",
|
|
239
|
+
"- After any change to UI files: `jig check`.",
|
|
240
|
+
"",
|
|
241
|
+
'With no slash commands, ask in plain words: "run jig spec for the pricing page".',
|
|
242
|
+
"",
|
|
243
|
+
"## The owner's word",
|
|
244
|
+
"",
|
|
245
|
+
"A spec confirmed, a drawing approved or skipped, a critique left for later, a",
|
|
246
|
+
"decision's reason: each is recorded in the owner's own words. Never infer one",
|
|
247
|
+
"from something else they said; ask.",
|
|
248
|
+
"",
|
|
249
|
+
"## Jig's files",
|
|
250
|
+
"",
|
|
251
|
+
"- `jig.config.json`: the mode each route uses, files `check` skips (`exempt`),",
|
|
252
|
+
" and when pages are critiqued.",
|
|
253
|
+
"- The token folder: `brand.*.css` is the project's identity, edit it freely;",
|
|
254
|
+
" `mode.*.css` is Jig's, refreshed by `jig update`; `theme.css` imports both.",
|
|
255
|
+
"- `DECISIONS.md`, beside the tokens: the project's decisions, written by",
|
|
256
|
+
" `/jig decide` and `/jig tweak` only.",
|
|
257
|
+
"- `.jig/specs/`: what each page must be; `<name>.checked.json` is its check.",
|
|
258
|
+
"- `.jig/mockups/`: approved drawings.",
|
|
259
|
+
"- `.jig/critique/<page>/`: a critique's verdicts, its report and its lock.",
|
|
260
|
+
"- `.jig/state.json`: what Jig wrote, so `jig update` leaves your edits alone.",
|
|
261
|
+
"",
|
|
262
|
+
"Jig keeps this block current. Write your own instructions above or below it.",
|
|
263
|
+
"",
|
|
264
|
+
BLOCK_END,
|
|
265
|
+
""
|
|
266
|
+
].join("\n");
|
|
267
|
+
}
|
|
268
|
+
function claudeImport() {
|
|
269
|
+
return [
|
|
270
|
+
BLOCK_START,
|
|
271
|
+
"",
|
|
272
|
+
"Jig's instructions for this project are in AGENTS.md, imported here:",
|
|
273
|
+
"",
|
|
274
|
+
`@${AGENTS_FILE}`,
|
|
275
|
+
"",
|
|
276
|
+
BLOCK_END,
|
|
277
|
+
""
|
|
278
|
+
].join("\n");
|
|
279
|
+
}
|
|
280
|
+
function claudeInstalled(projectRoot, homeDir = homedir()) {
|
|
281
|
+
return existsSync2(join4(projectRoot, ".claude", "skills", "jig", "SKILL.md")) || existsSync2(join4(homeDir, ".claude", "skills", "jig", "SKILL.md"));
|
|
282
|
+
}
|
|
283
|
+
function blockIn(text) {
|
|
284
|
+
const t = lf(text);
|
|
285
|
+
const start = t.indexOf(BLOCK_START);
|
|
286
|
+
const end = t.indexOf(BLOCK_END, start);
|
|
287
|
+
return start >= 0 && end > start ? t.slice(start, end + BLOCK_END.length) : void 0;
|
|
288
|
+
}
|
|
289
|
+
var expected = (block) => block.trim();
|
|
290
|
+
function ensureAgentFiles(projectRoot, opts) {
|
|
291
|
+
const wanted = [[AGENTS_FILE, agentsGuide()]];
|
|
292
|
+
if (opts.claude) wanted.push([CLAUDE_FILE, claudeImport()]);
|
|
293
|
+
const written = [];
|
|
294
|
+
for (const [file, block] of wanted) {
|
|
295
|
+
const path = join4(projectRoot, file);
|
|
296
|
+
const before = existsSync2(path) ? readFileSync2(path, "utf8") : "";
|
|
297
|
+
if (blockIn(before) === expected(block)) continue;
|
|
298
|
+
writeFileAtomic(path, upsertBlock(before, block));
|
|
299
|
+
written.push(file);
|
|
300
|
+
}
|
|
301
|
+
return written;
|
|
302
|
+
}
|
|
303
|
+
function agentFileProblems(projectRoot, homeDir = homedir()) {
|
|
304
|
+
if (!existsSync2(join4(projectRoot, ".jig", "state.json"))) return [];
|
|
305
|
+
const wanted = [[AGENTS_FILE, agentsGuide()]];
|
|
306
|
+
if (claudeInstalled(projectRoot, homeDir)) wanted.push([CLAUDE_FILE, claudeImport()]);
|
|
307
|
+
const problems = [];
|
|
308
|
+
for (const [file, block] of wanted) {
|
|
309
|
+
const path = join4(projectRoot, file);
|
|
310
|
+
if (!existsSync2(path)) {
|
|
311
|
+
problems.push(`${file} is missing, so an agent starts without Jig's instructions.`);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const found = blockIn(readFileSync2(path, "utf8"));
|
|
315
|
+
if (!found) problems.push(`${file} has no Jig block, so an agent starts without Jig's instructions.`);
|
|
316
|
+
else if (found !== expected(block)) problems.push(`${file}: Jig's block has been edited or is out of date.`);
|
|
317
|
+
}
|
|
318
|
+
return problems.map((p) => `${p} Run \`jig update\` to put it back; your own text in the file stays as it is.`);
|
|
319
|
+
}
|
|
320
|
+
|
|
86
321
|
// src/adapters/codex.ts
|
|
87
322
|
var codex = {
|
|
88
323
|
name: "codex",
|
|
@@ -91,7 +326,7 @@ var codex = {
|
|
|
91
326
|
const skillPath = ".agents/skills/jig/SKILL.md";
|
|
92
327
|
const files = [{ relPath: skillPath, content: ctx.skillBody }];
|
|
93
328
|
const agentsPath = ctx.scope === "global" ? ".codex/AGENTS.md" : "AGENTS.md";
|
|
94
|
-
files.push({ relPath: agentsPath, content: agentsPointer(skillPath) });
|
|
329
|
+
files.push({ relPath: agentsPath, content: ctx.scope === "global" ? agentsPointer(skillPath) : agentsGuide() });
|
|
95
330
|
if (ctx.commandBody && ctx.scope === "global") {
|
|
96
331
|
files.push({ relPath: ".codex/prompts/jig.md", content: `${ctx.commandBody}
|
|
97
332
|
` });
|
|
@@ -253,22 +488,22 @@ function skillFilesFor(adapter, ctx) {
|
|
|
253
488
|
}
|
|
254
489
|
|
|
255
490
|
// src/install/references.ts
|
|
256
|
-
import { existsSync as
|
|
257
|
-
import { join as
|
|
491
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
492
|
+
import { join as join5, posix as posix2 } from "path";
|
|
258
493
|
function referenceFiles(packageRoot2) {
|
|
259
|
-
const root =
|
|
260
|
-
if (!
|
|
494
|
+
const root = join5(packageRoot2, "references");
|
|
495
|
+
if (!existsSync3(root)) return [];
|
|
261
496
|
const found = [];
|
|
262
497
|
const walk2 = (dir, prefix) => {
|
|
263
498
|
for (const entry of readdirSync(dir, { withFileTypes: true }).sort(
|
|
264
499
|
(a, b) => a.name.localeCompare(b.name)
|
|
265
500
|
)) {
|
|
266
|
-
const child =
|
|
267
|
-
const rel = prefix ?
|
|
501
|
+
const child = join5(dir, entry.name);
|
|
502
|
+
const rel = prefix ? posix2.join(prefix, entry.name) : entry.name;
|
|
268
503
|
if (entry.isDirectory()) {
|
|
269
504
|
walk2(child, rel);
|
|
270
505
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
271
|
-
found.push({ relPath: rel, content:
|
|
506
|
+
found.push({ relPath: rel, content: readFileSync3(child, "utf8") });
|
|
272
507
|
}
|
|
273
508
|
}
|
|
274
509
|
};
|
|
@@ -278,32 +513,11 @@ function referenceFiles(packageRoot2) {
|
|
|
278
513
|
|
|
279
514
|
// src/install/manifest.ts
|
|
280
515
|
import { createHash } from "crypto";
|
|
281
|
-
import { closeSync, existsSync as
|
|
282
|
-
import { dirname as dirname3, join as
|
|
283
|
-
|
|
284
|
-
// src/install/atomic.ts
|
|
285
|
-
import { mkdirSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
286
|
-
import { dirname as dirname2, join as join3 } from "path";
|
|
287
|
-
function writeFileAtomic(path, content) {
|
|
288
|
-
const dir = dirname2(path);
|
|
289
|
-
mkdirSync(dir, { recursive: true });
|
|
290
|
-
const tmp = join3(dir, `.${process.pid}-${Date.now()}.tmp`);
|
|
291
|
-
try {
|
|
292
|
-
writeFileSync(tmp, content, "utf8");
|
|
293
|
-
renameSync(tmp, path);
|
|
294
|
-
} catch (err) {
|
|
295
|
-
try {
|
|
296
|
-
unlinkSync(tmp);
|
|
297
|
-
} catch {
|
|
298
|
-
}
|
|
299
|
-
throw err;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// src/install/manifest.ts
|
|
516
|
+
import { closeSync, existsSync as existsSync4, mkdirSync as mkdirSync2, openSync, readFileSync as readFileSync4, statSync, unlinkSync as unlinkSync2, writeSync } from "fs";
|
|
517
|
+
import { dirname as dirname3, join as join6 } from "path";
|
|
304
518
|
var REGENERATE_HINT = "Re-run 'jig install --agent <name>' to fix it.";
|
|
305
519
|
function manifestPath(root, manifestDir) {
|
|
306
|
-
return
|
|
520
|
+
return join6(root, ...manifestDir.split("/"), "manifest.json");
|
|
307
521
|
}
|
|
308
522
|
function checksum(content) {
|
|
309
523
|
const normalized = content.replace(/\r\n/g, "\n");
|
|
@@ -341,10 +555,10 @@ function validateManifest(raw, path) {
|
|
|
341
555
|
}
|
|
342
556
|
function readManifest(root, manifestDir = ".jig") {
|
|
343
557
|
const path = manifestPath(root, manifestDir);
|
|
344
|
-
if (!
|
|
558
|
+
if (!existsSync4(path)) return null;
|
|
345
559
|
let raw;
|
|
346
560
|
try {
|
|
347
|
-
raw = JSON.parse(
|
|
561
|
+
raw = JSON.parse(readFileSync4(path, "utf8"));
|
|
348
562
|
} catch {
|
|
349
563
|
throw new Error(`Jig's manifest at ${path} is not valid JSON. ${REGENERATE_HINT}`);
|
|
350
564
|
}
|
|
@@ -374,12 +588,9 @@ function withLock(lock, fn) {
|
|
|
374
588
|
}
|
|
375
589
|
break;
|
|
376
590
|
} catch (err) {
|
|
377
|
-
if (err.code
|
|
591
|
+
if (!BUSY.has(err.code ?? "")) throw err;
|
|
378
592
|
if (lockIsStale(lock) || Date.now() > deadline) {
|
|
379
|
-
|
|
380
|
-
unlinkSync2(lock);
|
|
381
|
-
} catch {
|
|
382
|
-
}
|
|
593
|
+
unlinkRetrying(lock);
|
|
383
594
|
continue;
|
|
384
595
|
}
|
|
385
596
|
sleep(10 + Math.random() * 20);
|
|
@@ -388,15 +599,25 @@ function withLock(lock, fn) {
|
|
|
388
599
|
try {
|
|
389
600
|
fn();
|
|
390
601
|
} finally {
|
|
602
|
+
unlinkRetrying(lock);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
var BUSY = /* @__PURE__ */ new Set(["EEXIST", "EPERM", "EACCES", "EBUSY"]);
|
|
606
|
+
function unlinkRetrying(lock) {
|
|
607
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
391
608
|
try {
|
|
392
609
|
unlinkSync2(lock);
|
|
393
|
-
|
|
610
|
+
return;
|
|
611
|
+
} catch (err) {
|
|
612
|
+
const code = err.code ?? "";
|
|
613
|
+
if (code === "ENOENT" || !BUSY.has(code)) return;
|
|
614
|
+
sleep(5 + attempt * 5);
|
|
394
615
|
}
|
|
395
616
|
}
|
|
396
617
|
}
|
|
397
618
|
function lockIsStale(lock) {
|
|
398
619
|
try {
|
|
399
|
-
const [pid, at] =
|
|
620
|
+
const [pid, at] = readFileSync4(lock, "utf8").trim().split(/\s+/).map(Number);
|
|
400
621
|
if (at && Date.now() - at > LOCK_STALE_MS) return true;
|
|
401
622
|
if (pid && pid !== process.pid) {
|
|
402
623
|
try {
|
|
@@ -416,7 +637,7 @@ function sleep(ms) {
|
|
|
416
637
|
}
|
|
417
638
|
function readManifestQuietly(path) {
|
|
418
639
|
try {
|
|
419
|
-
const parsed = JSON.parse(
|
|
640
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
420
641
|
if (!isPlainObject(parsed) || !isPlainObject(parsed.files)) return null;
|
|
421
642
|
return parsed;
|
|
422
643
|
} catch {
|
|
@@ -426,9 +647,9 @@ function readManifestQuietly(path) {
|
|
|
426
647
|
function isModified(projectRoot, relPath, m) {
|
|
427
648
|
const recorded = m.files[relPath];
|
|
428
649
|
if (!recorded) return false;
|
|
429
|
-
const abs =
|
|
430
|
-
if (!
|
|
431
|
-
return checksum(
|
|
650
|
+
const abs = join6(projectRoot, relPath);
|
|
651
|
+
if (!existsSync4(abs)) return false;
|
|
652
|
+
return checksum(readFileSync4(abs, "utf8")) !== recorded;
|
|
432
653
|
}
|
|
433
654
|
|
|
434
655
|
// src/template/render.ts
|
|
@@ -455,97 +676,16 @@ function renderCommandTable(metadata2) {
|
|
|
455
676
|
return ["| Command | Description | Status |", "| --- | --- | --- |", ...rows].join("\n");
|
|
456
677
|
}
|
|
457
678
|
|
|
458
|
-
// src/install/vendor.ts
|
|
459
|
-
import { posix as posix2 } from "path";
|
|
460
|
-
var { join: join5 } = posix2;
|
|
461
|
-
var DELIMITERS = {
|
|
462
|
-
html: ["<!--", "-->"],
|
|
463
|
-
css: ["/*", "*/"]
|
|
464
|
-
};
|
|
465
|
-
function licencePathFor(relPath) {
|
|
466
|
-
const depth = relPath.split("/").length - 1;
|
|
467
|
-
return depth === 0 ? "." : Array(depth).fill("..").join("/");
|
|
468
|
-
}
|
|
469
|
-
function vendorHeader(file, version2, style = "html", licencePath = ".") {
|
|
470
|
-
const [open, close] = DELIMITERS[style];
|
|
471
|
-
const attribution = licencePath === null ? " Licensed Apache-2.0. LICENSE and NOTICE ship beside the jig skill." : ` Licensed Apache-2.0. See ${join5(licencePath, "LICENSE")} and ${join5(licencePath, "NOTICE")}.`;
|
|
472
|
-
return [
|
|
473
|
-
`${open} ${file} \u2014 vendored from Jig v${version2}.`,
|
|
474
|
-
attribution,
|
|
475
|
-
` Edit freely: \`jig update\` will not overwrite a file you have changed. ${close}`,
|
|
476
|
-
"",
|
|
477
|
-
""
|
|
478
|
-
].join("\n");
|
|
479
|
-
}
|
|
480
|
-
function findMarkerRegions(existing) {
|
|
481
|
-
const regions = [];
|
|
482
|
-
let searchFrom = 0;
|
|
483
|
-
let openStart = null;
|
|
484
|
-
while (true) {
|
|
485
|
-
const nextStart = existing.indexOf(BLOCK_START, searchFrom);
|
|
486
|
-
const nextEnd = existing.indexOf(BLOCK_END, searchFrom);
|
|
487
|
-
if (nextStart === -1 && nextEnd === -1) break;
|
|
488
|
-
const endIsNext = nextEnd !== -1 && (nextStart === -1 || nextEnd < nextStart);
|
|
489
|
-
if (endIsNext) {
|
|
490
|
-
if (openStart !== null) {
|
|
491
|
-
regions.push({ start: openStart, end: nextEnd + BLOCK_END.length });
|
|
492
|
-
openStart = null;
|
|
493
|
-
} else {
|
|
494
|
-
regions.push({ start: nextEnd, end: nextEnd + BLOCK_END.length });
|
|
495
|
-
}
|
|
496
|
-
searchFrom = nextEnd + BLOCK_END.length;
|
|
497
|
-
} else {
|
|
498
|
-
if (openStart !== null) {
|
|
499
|
-
regions.push({ start: openStart, end: openStart + BLOCK_START.length });
|
|
500
|
-
}
|
|
501
|
-
openStart = nextStart;
|
|
502
|
-
searchFrom = nextStart + BLOCK_START.length;
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
if (openStart !== null) {
|
|
506
|
-
regions.push({ start: openStart, end: existing.length });
|
|
507
|
-
}
|
|
508
|
-
regions.sort((a, b) => a.start - b.start);
|
|
509
|
-
return regions;
|
|
510
|
-
}
|
|
511
|
-
function stripMarkerRegions(existing) {
|
|
512
|
-
const regions = findMarkerRegions(existing);
|
|
513
|
-
if (regions.length === 0) {
|
|
514
|
-
return { text: existing, insertAt: existing.length, removed: false };
|
|
515
|
-
}
|
|
516
|
-
let text = "";
|
|
517
|
-
let cursor = 0;
|
|
518
|
-
let insertAt = -1;
|
|
519
|
-
regions.forEach((region, i) => {
|
|
520
|
-
let chunk = existing.slice(cursor, region.start);
|
|
521
|
-
if (i > 0) chunk = chunk.replace(/^\n/, "");
|
|
522
|
-
text += chunk;
|
|
523
|
-
if (insertAt === -1) insertAt = text.length;
|
|
524
|
-
cursor = region.end;
|
|
525
|
-
});
|
|
526
|
-
const tail = existing.slice(cursor).replace(/^\n/, "");
|
|
527
|
-
text += tail;
|
|
528
|
-
return { text, insertAt, removed: true };
|
|
529
|
-
}
|
|
530
|
-
function upsertBlock(existing, block) {
|
|
531
|
-
const { text, insertAt, removed } = stripMarkerRegions(existing);
|
|
532
|
-
if (!removed) {
|
|
533
|
-
const separator = text.length && !text.endsWith("\n") ? "\n\n" : text.length ? "\n" : "";
|
|
534
|
-
return `${text}${separator}${block}`;
|
|
535
|
-
}
|
|
536
|
-
return `${text.slice(0, insertAt)}${block}${text.slice(insertAt)}`;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
679
|
// src/install/writer.ts
|
|
540
|
-
import { existsSync as
|
|
541
|
-
import { dirname as dirname4, join as
|
|
680
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
|
|
681
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
542
682
|
|
|
543
683
|
// src/install/line-endings.ts
|
|
544
684
|
function dominantEnding(existing) {
|
|
545
685
|
const crlf = (existing.match(/\r\n/g) ?? []).length;
|
|
546
|
-
const
|
|
547
|
-
if (crlf === 0 &&
|
|
548
|
-
return crlf >=
|
|
686
|
+
const lf5 = (existing.match(/(?<!\r)\n/g) ?? []).length;
|
|
687
|
+
if (crlf === 0 && lf5 === 0) return "\n";
|
|
688
|
+
return crlf >= lf5 ? "\r\n" : "\n";
|
|
549
689
|
}
|
|
550
690
|
function matchLineEndings(content, existing) {
|
|
551
691
|
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
@@ -562,9 +702,9 @@ function createWriter(root, seed = {}) {
|
|
|
562
702
|
return {
|
|
563
703
|
files,
|
|
564
704
|
write(key, content) {
|
|
565
|
-
const abs =
|
|
705
|
+
const abs = join7(root, ...key.split("/"));
|
|
566
706
|
mkdirSync3(dirname4(abs), { recursive: true });
|
|
567
|
-
const existing =
|
|
707
|
+
const existing = existsSync5(abs) ? readFileSync5(abs, "utf8") : void 0;
|
|
568
708
|
const toWrite = matchLineEndings(content, existing);
|
|
569
709
|
writeFileAtomic(abs, toWrite);
|
|
570
710
|
files[key] = checksum(toWrite);
|
|
@@ -574,15 +714,15 @@ function createWriter(root, seed = {}) {
|
|
|
574
714
|
}
|
|
575
715
|
function bundleFiles(packageRoot2, dir) {
|
|
576
716
|
const ext = dir === "rules" ? ".md" : ".css";
|
|
577
|
-
return readdirSync2(
|
|
717
|
+
return readdirSync2(join7(packageRoot2, dir)).filter((f) => f.endsWith(ext)).sort();
|
|
578
718
|
}
|
|
579
719
|
|
|
580
720
|
// src/commands/install.ts
|
|
581
721
|
var ASK_INSTRUCTION = "Ask the user directly in chat, one question at a time, and wait for an answer before continuing.";
|
|
582
722
|
function buildSkillBody(packageRoot2, rulesPath, version2) {
|
|
583
|
-
const template =
|
|
723
|
+
const template = readFileSync6(join8(packageRoot2, "templates", "SKILL.md.tmpl"), "utf8");
|
|
584
724
|
const metadata2 = JSON.parse(
|
|
585
|
-
|
|
725
|
+
readFileSync6(join8(packageRoot2, "templates", "command-metadata.json"), "utf8")
|
|
586
726
|
);
|
|
587
727
|
return render(template, {
|
|
588
728
|
command_prefix: "/jig ",
|
|
@@ -599,12 +739,12 @@ function buildSkillBody(packageRoot2, rulesPath, version2) {
|
|
|
599
739
|
}
|
|
600
740
|
function buildCommandBody(packageRoot2, rulesPath, version2, argsPlaceholder) {
|
|
601
741
|
const metadata2 = JSON.parse(
|
|
602
|
-
|
|
742
|
+
readFileSync6(join8(packageRoot2, "templates", "command-metadata.json"), "utf8")
|
|
603
743
|
);
|
|
604
744
|
const subcommands = Object.entries(metadata2).filter(([, meta]) => meta.status === "available" || meta.status === "agent").map(([name]) => name).sort();
|
|
605
|
-
const templatePath =
|
|
606
|
-
if (!
|
|
607
|
-
const template =
|
|
745
|
+
const templatePath = join8(packageRoot2, "templates", "COMMAND.md.tmpl");
|
|
746
|
+
if (!existsSync6(templatePath)) return { body: "", subcommands };
|
|
747
|
+
const template = readFileSync6(templatePath, "utf8");
|
|
608
748
|
const body = template.replace(/\{\{command_prefix\}\}/g, "/jig ").replace(/\{\{args_placeholder\}\}/g, argsPlaceholder).replace(/\{\{subcommand_list\}\}/g, subcommands.join(", ")).replace(/\{\{scripts_path\}\}/g, cliInvocation(version2)).replace(/\{\{update_path\}\}/g, "npx jig-ui@latest").replace(/\{\{rules_path\}\}/g, rulesPath).replace(/\{\{config_file\}\}/g, "jig.config.json");
|
|
609
749
|
const unresolved = [...new Set(body.match(/\{\{[a-z_]+\}\}/g) ?? [])];
|
|
610
750
|
if (unresolved.length > 0) {
|
|
@@ -632,6 +772,7 @@ function install(opts) {
|
|
|
632
772
|
written: [],
|
|
633
773
|
skipped: [],
|
|
634
774
|
stopHook: installStopHook(opts.projectRoot, opts.version),
|
|
775
|
+
agentFiles: ensureAgentFiles(opts.projectRoot, { claude: true }),
|
|
635
776
|
hookOnly: `Jig's skill is installed globally for 'claude' (${globalReferenceDir} under your home directory), so it stays there. Added only this project's Stop hook.`
|
|
636
777
|
};
|
|
637
778
|
}
|
|
@@ -652,9 +793,9 @@ function install(opts) {
|
|
|
652
793
|
existingManifest = null;
|
|
653
794
|
}
|
|
654
795
|
const planned = [];
|
|
655
|
-
const rulesDir =
|
|
796
|
+
const rulesDir = join8(opts.packageRoot, "rules");
|
|
656
797
|
for (const file of bundleFiles(opts.packageRoot, "rules")) {
|
|
657
|
-
const body =
|
|
798
|
+
const body = readFileSync6(join8(rulesDir, file), "utf8");
|
|
658
799
|
planned.push({
|
|
659
800
|
key: relKey(referenceDir, "rules", file),
|
|
660
801
|
content: vendorHeader(file, opts.version, "html", licencePathFor(`rules/${file}`)) + body,
|
|
@@ -670,13 +811,13 @@ function install(opts) {
|
|
|
670
811
|
}
|
|
671
812
|
planned.push({
|
|
672
813
|
key: relKey(referenceDir, "rules.index.json"),
|
|
673
|
-
content:
|
|
814
|
+
content: readFileSync6(join8(opts.packageRoot, "rules.index.json"), "utf8"),
|
|
674
815
|
checkable: true
|
|
675
816
|
});
|
|
676
817
|
for (const src of ["LICENSE", "NOTICE"]) {
|
|
677
818
|
planned.push({
|
|
678
819
|
key: relKey(referenceDir, src),
|
|
679
|
-
content:
|
|
820
|
+
content: readFileSync6(join8(opts.packageRoot, src), "utf8"),
|
|
680
821
|
checkable: false
|
|
681
822
|
});
|
|
682
823
|
}
|
|
@@ -698,9 +839,9 @@ function install(opts) {
|
|
|
698
839
|
});
|
|
699
840
|
for (const file of skillFiles) {
|
|
700
841
|
const key = file.relPath;
|
|
701
|
-
const abs =
|
|
842
|
+
const abs = join8(installRoot, ...key.split("/"));
|
|
702
843
|
const isBlockFile = file.content.includes(BLOCK_START);
|
|
703
|
-
const content = isBlockFile &&
|
|
844
|
+
const content = isBlockFile && existsSync6(abs) ? upsertBlock(readFileSync6(abs, "utf8"), file.content) : file.content;
|
|
704
845
|
planned.push({ key, content, checkable: !isBlockFile });
|
|
705
846
|
}
|
|
706
847
|
const writer = createWriter(installRoot);
|
|
@@ -727,11 +868,12 @@ function install(opts) {
|
|
|
727
868
|
writeManifest(installRoot, manifest, referenceDir);
|
|
728
869
|
written.push(relKey(referenceDir, "manifest.json"));
|
|
729
870
|
const stopHook = opts.hook && opts.agent === "claude" && opts.scope === "project" ? installStopHook(installRoot, opts.version) : void 0;
|
|
730
|
-
|
|
871
|
+
const agentFiles = opts.scope === "project" ? ensureAgentFiles(opts.projectRoot, { claude: opts.agent === "claude" || claudeInstalled(opts.projectRoot, opts.homeDir) }) : [];
|
|
872
|
+
return { written, skipped, stopHook, agentFiles };
|
|
731
873
|
}
|
|
732
874
|
function hasStopHook(projectRoot) {
|
|
733
875
|
try {
|
|
734
|
-
const settings =
|
|
876
|
+
const settings = readFileSync6(join8(projectRoot, ".claude", "settings.json"), "utf8");
|
|
735
877
|
const hooks = JSON.parse(settings).hooks?.Stop ?? [];
|
|
736
878
|
return hooks.some((group) => (group.hooks ?? []).some((h) => JIG_GATE_HOOK.test(h.command ?? "")));
|
|
737
879
|
} catch {
|
|
@@ -739,11 +881,11 @@ function hasStopHook(projectRoot) {
|
|
|
739
881
|
}
|
|
740
882
|
}
|
|
741
883
|
function installStopHook(projectRoot, version2) {
|
|
742
|
-
const path =
|
|
884
|
+
const path = join8(projectRoot, ".claude", "settings.json");
|
|
743
885
|
let settings = {};
|
|
744
|
-
if (
|
|
886
|
+
if (existsSync6(path)) {
|
|
745
887
|
try {
|
|
746
|
-
settings = JSON.parse(
|
|
888
|
+
settings = JSON.parse(readFileSync6(path, "utf8"));
|
|
747
889
|
} catch {
|
|
748
890
|
return false;
|
|
749
891
|
}
|
|
@@ -754,14 +896,14 @@ function installStopHook(projectRoot, version2) {
|
|
|
754
896
|
const stop = (hooks.Stop ?? []).map((group) => ({ ...group, hooks: group.hooks.filter((h) => !isJig(h)) })).filter((group) => group.hooks.length > 0);
|
|
755
897
|
stop.push({ hooks: [{ type: "command", command }] });
|
|
756
898
|
settings.hooks = { ...hooks, Stop: stop };
|
|
757
|
-
mkdirSync4(
|
|
899
|
+
mkdirSync4(join8(projectRoot, ".claude"), { recursive: true });
|
|
758
900
|
writeFileSync2(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
759
901
|
return true;
|
|
760
902
|
}
|
|
761
903
|
|
|
762
904
|
// src/commands/update.ts
|
|
763
|
-
import { join as
|
|
764
|
-
import { existsSync as
|
|
905
|
+
import { join as join11 } from "path";
|
|
906
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
765
907
|
|
|
766
908
|
// src/install/target.ts
|
|
767
909
|
import { resolve } from "path";
|
|
@@ -804,14 +946,14 @@ function resolveAllInstalled(projectRoot, homeDir) {
|
|
|
804
946
|
}
|
|
805
947
|
|
|
806
948
|
// src/init/state.ts
|
|
807
|
-
import { existsSync as
|
|
808
|
-
import { dirname as dirname5, join as
|
|
809
|
-
var INIT_MANIFEST_REL =
|
|
949
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync7 } from "fs";
|
|
950
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
951
|
+
var INIT_MANIFEST_REL = join9(".jig", "state.json");
|
|
810
952
|
function readInitManifest(projectRoot) {
|
|
811
|
-
const path =
|
|
812
|
-
if (!
|
|
953
|
+
const path = join9(projectRoot, INIT_MANIFEST_REL);
|
|
954
|
+
if (!existsSync7(path)) return null;
|
|
813
955
|
try {
|
|
814
|
-
const raw = JSON.parse(
|
|
956
|
+
const raw = JSON.parse(readFileSync7(path, "utf8"));
|
|
815
957
|
if (typeof raw !== "object" || raw === null) return null;
|
|
816
958
|
const { files, version: version2, modes } = raw;
|
|
817
959
|
if (typeof files !== "object" || files === null) return null;
|
|
@@ -826,7 +968,7 @@ function readInitManifest(projectRoot) {
|
|
|
826
968
|
}
|
|
827
969
|
var MERGE_ATTEMPTS = 5;
|
|
828
970
|
function writeInitManifest(projectRoot, m) {
|
|
829
|
-
const path =
|
|
971
|
+
const path = join9(projectRoot, INIT_MANIFEST_REL);
|
|
830
972
|
mkdirSync5(dirname5(path), { recursive: true });
|
|
831
973
|
for (let attempt = 0; attempt < MERGE_ATTEMPTS; attempt++) {
|
|
832
974
|
const onDisk = readInitManifest(projectRoot);
|
|
@@ -847,15 +989,15 @@ function isInitFileModified(projectRoot, relPath, m) {
|
|
|
847
989
|
}
|
|
848
990
|
|
|
849
991
|
// src/init/migrate.ts
|
|
850
|
-
import { existsSync as
|
|
851
|
-
import { join as
|
|
992
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8, readdirSync as readdirSync3, rmSync } from "fs";
|
|
993
|
+
import { join as join10 } from "path";
|
|
852
994
|
var LEGACY_RULE_RE = /^\d{2}-.*\.md$/;
|
|
853
995
|
function isLegacyInstallArtifact(name) {
|
|
854
996
|
return LEGACY_RULE_RE.test(name) || name === "rules.index.json" || name === "LICENSE" || name === "NOTICE" || name === "manifest.json";
|
|
855
997
|
}
|
|
856
998
|
function readLegacyManifest(dirAbs) {
|
|
857
999
|
try {
|
|
858
|
-
const raw = JSON.parse(
|
|
1000
|
+
const raw = JSON.parse(readFileSync8(join10(dirAbs, "manifest.json"), "utf8"));
|
|
859
1001
|
if (raw && typeof raw === "object" && typeof raw.files === "object") {
|
|
860
1002
|
return raw;
|
|
861
1003
|
}
|
|
@@ -867,8 +1009,8 @@ function classify(projectRoot, relPath, legacyManifest) {
|
|
|
867
1009
|
return legacyManifest && relPath in legacyManifest.files ? isModified(projectRoot, relPath, legacyManifest) : "unknown";
|
|
868
1010
|
}
|
|
869
1011
|
function detectLegacyRules(projectRoot) {
|
|
870
|
-
const jigDir =
|
|
871
|
-
if (!
|
|
1012
|
+
const jigDir = join10(projectRoot, ".jig");
|
|
1013
|
+
if (!existsSync8(jigDir)) return { present: false, files: [] };
|
|
872
1014
|
const legacyManifest = readLegacyManifest(jigDir);
|
|
873
1015
|
let entries;
|
|
874
1016
|
try {
|
|
@@ -884,12 +1026,12 @@ function detectLegacyRules(projectRoot) {
|
|
|
884
1026
|
}
|
|
885
1027
|
function detectLegacyCursorRules(projectRoot) {
|
|
886
1028
|
const bundleDirRel = ".cursor/rules/jig";
|
|
887
|
-
const bundleDir =
|
|
1029
|
+
const bundleDir = join10(projectRoot, ...bundleDirRel.split("/"));
|
|
888
1030
|
const mdcRel = ".cursor/rules/jig.mdc";
|
|
889
|
-
const mdcAbs =
|
|
1031
|
+
const mdcAbs = join10(projectRoot, ...mdcRel.split("/"));
|
|
890
1032
|
const legacyManifest = readLegacyManifest(bundleDir);
|
|
891
1033
|
const files = [];
|
|
892
|
-
if (
|
|
1034
|
+
if (existsSync8(mdcAbs)) {
|
|
893
1035
|
files.push({ relPath: mdcRel, modified: classify(projectRoot, mdcRel, legacyManifest) });
|
|
894
1036
|
}
|
|
895
1037
|
let entries;
|
|
@@ -943,7 +1085,7 @@ function removableLegacyFiles(report2) {
|
|
|
943
1085
|
function removeLegacyFiles(projectRoot, relPaths) {
|
|
944
1086
|
for (const relPath of relPaths) {
|
|
945
1087
|
try {
|
|
946
|
-
rmSync(
|
|
1088
|
+
rmSync(join10(projectRoot, ...relPath.split("/")), { force: true });
|
|
947
1089
|
} catch {
|
|
948
1090
|
}
|
|
949
1091
|
}
|
|
@@ -986,6 +1128,9 @@ function update(opts) {
|
|
|
986
1128
|
const initResult = updateInitFiles(opts);
|
|
987
1129
|
updated.push(...initResult.updated);
|
|
988
1130
|
skipped.push(...initResult.skipped);
|
|
1131
|
+
if (existsSync9(join11(opts.projectRoot, ".jig", "state.json"))) {
|
|
1132
|
+
updated.push(...ensureAgentFiles(opts.projectRoot, { claude: claudeInstalled(opts.projectRoot, opts.homeDir) }));
|
|
1133
|
+
}
|
|
989
1134
|
return {
|
|
990
1135
|
updated,
|
|
991
1136
|
skipped,
|
|
@@ -1008,14 +1153,14 @@ function updateTarget(opts, resolved) {
|
|
|
1008
1153
|
const write = (key, content) => {
|
|
1009
1154
|
updated.push(writer.write(key, content));
|
|
1010
1155
|
};
|
|
1011
|
-
const rulesDir =
|
|
1156
|
+
const rulesDir = join11(opts.packageRoot, "rules");
|
|
1012
1157
|
for (const file of bundleFiles(opts.packageRoot, "rules")) {
|
|
1013
1158
|
const key = relKey(referenceDir, "rules", file);
|
|
1014
1159
|
if (isModified(installRoot, key, existing)) {
|
|
1015
1160
|
skipped.push(key);
|
|
1016
1161
|
continue;
|
|
1017
1162
|
}
|
|
1018
|
-
write(key, vendorHeader(file, opts.version, "html", licencePathFor(`rules/${file}`)) +
|
|
1163
|
+
write(key, vendorHeader(file, opts.version, "html", licencePathFor(`rules/${file}`)) + readFileSync9(join11(rulesDir, file), "utf8"));
|
|
1019
1164
|
}
|
|
1020
1165
|
for (const ref of referenceFiles(opts.packageRoot)) {
|
|
1021
1166
|
const key = relKey(referenceDir, ...ref.relPath.split("/"));
|
|
@@ -1029,7 +1174,7 @@ function updateTarget(opts, resolved) {
|
|
|
1029
1174
|
if (isModified(installRoot, indexKey, existing)) {
|
|
1030
1175
|
skipped.push(indexKey);
|
|
1031
1176
|
} else {
|
|
1032
|
-
write(indexKey,
|
|
1177
|
+
write(indexKey, readFileSync9(join11(opts.packageRoot, "rules.index.json"), "utf8"));
|
|
1033
1178
|
}
|
|
1034
1179
|
const rulesPath = rulesPathFor(referenceDir, discoveredScope);
|
|
1035
1180
|
const skillBody = buildSkillBody(opts.packageRoot, rulesPath, opts.version);
|
|
@@ -1051,8 +1196,8 @@ function updateTarget(opts, resolved) {
|
|
|
1051
1196
|
const key = file.relPath;
|
|
1052
1197
|
const isBlockFile = file.content.includes(BLOCK_START);
|
|
1053
1198
|
if (isBlockFile) {
|
|
1054
|
-
const abs =
|
|
1055
|
-
const current =
|
|
1199
|
+
const abs = join11(installRoot, ...key.split("/"));
|
|
1200
|
+
const current = existsSync9(abs) ? readFileSync9(abs, "utf8") : "";
|
|
1056
1201
|
write(key, upsertBlock(current, file.content));
|
|
1057
1202
|
continue;
|
|
1058
1203
|
}
|
|
@@ -1063,7 +1208,7 @@ function updateTarget(opts, resolved) {
|
|
|
1063
1208
|
write(key, file.content);
|
|
1064
1209
|
}
|
|
1065
1210
|
for (const src of ["LICENSE", "NOTICE"]) {
|
|
1066
|
-
write(relKey(referenceDir, src),
|
|
1211
|
+
write(relKey(referenceDir, src), readFileSync9(join11(opts.packageRoot, src), "utf8"));
|
|
1067
1212
|
}
|
|
1068
1213
|
const manifest = {
|
|
1069
1214
|
...existing,
|
|
@@ -1079,7 +1224,7 @@ function updateInitFiles(opts) {
|
|
|
1079
1224
|
const updated = [];
|
|
1080
1225
|
const skipped = [];
|
|
1081
1226
|
const tokens = [];
|
|
1082
|
-
const tokensDir =
|
|
1227
|
+
const tokensDir = join11(opts.packageRoot, "tokens");
|
|
1083
1228
|
const initManifest = readInitManifest(opts.projectRoot);
|
|
1084
1229
|
if (initManifest) {
|
|
1085
1230
|
const initFiles = { ...initManifest.files };
|
|
@@ -1091,10 +1236,10 @@ function updateInitFiles(opts) {
|
|
|
1091
1236
|
for (const key of keys) {
|
|
1092
1237
|
let before = "";
|
|
1093
1238
|
try {
|
|
1094
|
-
before =
|
|
1239
|
+
before = readFileSync9(join11(opts.projectRoot, ...key.split("/")), "utf8");
|
|
1095
1240
|
} catch {
|
|
1096
1241
|
}
|
|
1097
|
-
const fresh =
|
|
1242
|
+
const fresh = readFileSync9(join11(tokensDir, file), "utf8");
|
|
1098
1243
|
if (isInitFileModified(opts.projectRoot, key, initManifest)) {
|
|
1099
1244
|
skipped.push(key);
|
|
1100
1245
|
const change2 = tokenChange(key, before, fresh, true);
|
|
@@ -1115,12 +1260,12 @@ function updateInitFiles(opts) {
|
|
|
1115
1260
|
}
|
|
1116
1261
|
|
|
1117
1262
|
// src/commands/explain.ts
|
|
1118
|
-
import { existsSync as
|
|
1119
|
-
import { join as
|
|
1263
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1264
|
+
import { join as join16 } from "path";
|
|
1120
1265
|
|
|
1121
1266
|
// src/rules/load.ts
|
|
1122
|
-
import { readFileSync as
|
|
1123
|
-
import { join as
|
|
1267
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync4 } from "fs";
|
|
1268
|
+
import { join as join12 } from "path";
|
|
1124
1269
|
|
|
1125
1270
|
// src/rules/parse.ts
|
|
1126
1271
|
var HEADING = /^###\s+([A-Z])-(\d+)\s+(.+?)\s*$/;
|
|
@@ -1216,9 +1361,9 @@ function loadRules(rulesDir, indexPath) {
|
|
|
1216
1361
|
const files = readdirSync4(rulesDir).filter((f) => f.endsWith(".md")).sort();
|
|
1217
1362
|
const parsed = [];
|
|
1218
1363
|
for (const file of files) {
|
|
1219
|
-
parsed.push(...parseRules(
|
|
1364
|
+
parsed.push(...parseRules(readFileSync10(join12(rulesDir, file), "utf8"), file));
|
|
1220
1365
|
}
|
|
1221
|
-
const entries = validateIndex(JSON.parse(
|
|
1366
|
+
const entries = validateIndex(JSON.parse(readFileSync10(indexPath, "utf8")));
|
|
1222
1367
|
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
1223
1368
|
const missing = parsed.filter((r) => !byId.has(r.id)).map((r) => r.id);
|
|
1224
1369
|
if (missing.length) {
|
|
@@ -1240,8 +1385,8 @@ function loadRules(rulesDir, indexPath) {
|
|
|
1240
1385
|
}
|
|
1241
1386
|
|
|
1242
1387
|
// src/rules/specs.ts
|
|
1243
|
-
import { readFileSync as
|
|
1244
|
-
import { join as
|
|
1388
|
+
import { readFileSync as readFileSync11, readdirSync as readdirSync5 } from "fs";
|
|
1389
|
+
import { join as join13 } from "path";
|
|
1245
1390
|
var KINDS = {
|
|
1246
1391
|
P: "pattern",
|
|
1247
1392
|
M: "mode",
|
|
@@ -1300,14 +1445,14 @@ function parseSpecs(markdown, sourceFile) {
|
|
|
1300
1445
|
function loadSpecs(rulesDir) {
|
|
1301
1446
|
const specs = [];
|
|
1302
1447
|
for (const file of readdirSync5(rulesDir).filter((f) => f.endsWith(".md")).sort()) {
|
|
1303
|
-
specs.push(...parseSpecs(
|
|
1448
|
+
specs.push(...parseSpecs(readFileSync11(join13(rulesDir, file), "utf8"), file));
|
|
1304
1449
|
}
|
|
1305
1450
|
return specs;
|
|
1306
1451
|
}
|
|
1307
1452
|
|
|
1308
1453
|
// src/rules/sections.ts
|
|
1309
|
-
import { readFileSync as
|
|
1310
|
-
import { join as
|
|
1454
|
+
import { readFileSync as readFileSync12, readdirSync as readdirSync6 } from "fs";
|
|
1455
|
+
import { join as join14 } from "path";
|
|
1311
1456
|
var RULE_HEADING = /^###\s+[A-Z]-\d+\b/;
|
|
1312
1457
|
var SPEC_HEADING = /^##\s+[PMLRT]-\d+\b/;
|
|
1313
1458
|
var ANY_HEADING = /^(#{1,6})\s+(.*?)\s*$/;
|
|
@@ -1348,16 +1493,16 @@ function parseSections(markdown, file) {
|
|
|
1348
1493
|
function loadSections(rulesDir) {
|
|
1349
1494
|
const out = [];
|
|
1350
1495
|
for (const file of readdirSync6(rulesDir).filter((f) => f.endsWith(".md")).sort()) {
|
|
1351
|
-
out.push(...parseSections(
|
|
1496
|
+
out.push(...parseSections(readFileSync12(join14(rulesDir, file), "utf8"), file));
|
|
1352
1497
|
}
|
|
1353
1498
|
return out;
|
|
1354
1499
|
}
|
|
1355
1500
|
|
|
1356
1501
|
// src/rules/layers.ts
|
|
1357
|
-
import { readFileSync as
|
|
1358
|
-
import { join as
|
|
1502
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
1503
|
+
import { join as join15 } from "path";
|
|
1359
1504
|
function loadLayers(packageRoot2) {
|
|
1360
|
-
return JSON.parse(
|
|
1505
|
+
return JSON.parse(readFileSync13(join15(packageRoot2, "layers.json"), "utf8"));
|
|
1361
1506
|
}
|
|
1362
1507
|
|
|
1363
1508
|
// src/commands/explain.ts
|
|
@@ -1439,8 +1584,8 @@ function renderSection(s) {
|
|
|
1439
1584
|
function explain(opts) {
|
|
1440
1585
|
const raw = opts.ruleId.trim().toUpperCase();
|
|
1441
1586
|
const root = opts.packageRoot ?? assetRoot();
|
|
1442
|
-
const rulesDir =
|
|
1443
|
-
const rules = loadRules(rulesDir,
|
|
1587
|
+
const rulesDir = join16(root, "rules");
|
|
1588
|
+
const rules = loadRules(rulesDir, join16(root, "rules.index.json"));
|
|
1444
1589
|
const specs = loadSpecs(rulesDir);
|
|
1445
1590
|
const sections = loadSections(rulesDir);
|
|
1446
1591
|
const entries = searchEntries(rules, specs, sections);
|
|
@@ -1496,8 +1641,8 @@ function explain(opts) {
|
|
|
1496
1641
|
const id = `${shape[1]}-${shape[2]}`;
|
|
1497
1642
|
const rule = rules.find((r) => r.id === id);
|
|
1498
1643
|
if (rule) {
|
|
1499
|
-
const example =
|
|
1500
|
-
return
|
|
1644
|
+
const example = join16(root, "examples", `${id}.html`);
|
|
1645
|
+
return existsSync10(example) ? `${renderRule(rule)}
|
|
1501
1646
|
|
|
1502
1647
|
Example: ${example} (a dont and a do, each a self-contained HTML fragment)` : renderRule(rule);
|
|
1503
1648
|
}
|
|
@@ -1522,8 +1667,9 @@ Example: ${example} (a dont and a do, each a self-contained HTML fragment)` : re
|
|
|
1522
1667
|
const idBody = ids.filter((e) => !idTitle.includes(e) && matches(e.text, needle));
|
|
1523
1668
|
const secTitle = secs.filter((e) => matches(e.title, needle));
|
|
1524
1669
|
const secBody = secs.filter((e) => !secTitle.includes(e) && matches(e.text, needle));
|
|
1670
|
+
const opens = (e) => e.title.toLowerCase().startsWith(needle.toLowerCase()) ? 0 : 1;
|
|
1525
1671
|
const hits = [
|
|
1526
|
-
...idTitle.sort((a, b) => byNumber(a.id, b.id)),
|
|
1672
|
+
...idTitle.sort((a, b) => opens(a) - opens(b) || byNumber(a.id, b.id)),
|
|
1527
1673
|
...idBody.sort((a, b) => byNumber(a.id, b.id)),
|
|
1528
1674
|
...secTitle,
|
|
1529
1675
|
...secBody
|
|
@@ -1553,13 +1699,13 @@ function sectionsOf(entries) {
|
|
|
1553
1699
|
}
|
|
1554
1700
|
|
|
1555
1701
|
// src/commands/check.ts
|
|
1556
|
-
import { existsSync as
|
|
1557
|
-
import { join as
|
|
1702
|
+
import { existsSync as existsSync16, readFileSync as readFileSync21 } from "fs";
|
|
1703
|
+
import { join as join25 } from "path";
|
|
1558
1704
|
|
|
1559
1705
|
// src/check/files.ts
|
|
1560
1706
|
import { execFileSync } from "child_process";
|
|
1561
1707
|
import { readdirSync as readdirSync7, realpathSync, statSync as statSync2 } from "fs";
|
|
1562
|
-
import { join as
|
|
1708
|
+
import { join as join17, relative, sep } from "path";
|
|
1563
1709
|
var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
|
|
1564
1710
|
"node_modules",
|
|
1565
1711
|
"dist",
|
|
@@ -1634,7 +1780,7 @@ function walk(root, dir, out, seen) {
|
|
|
1634
1780
|
}
|
|
1635
1781
|
for (const entry of entries) {
|
|
1636
1782
|
if (EXCLUDE_DIRS.has(entry.name)) continue;
|
|
1637
|
-
const abs =
|
|
1783
|
+
const abs = join17(dir, entry.name);
|
|
1638
1784
|
let isDir = entry.isDirectory();
|
|
1639
1785
|
let isFile = entry.isFile();
|
|
1640
1786
|
if (entry.isSymbolicLink()) {
|
|
@@ -1776,6 +1922,10 @@ function formatReport(findings, meta) {
|
|
|
1776
1922
|
` Scope: files changed since HEAD, not the whole project \u2014 this is the inner-loop default. A clean result here means nothing in your diff fired, not that the project is clean. Run 'jig check --all' for that.`
|
|
1777
1923
|
);
|
|
1778
1924
|
}
|
|
1925
|
+
for (const problem of meta.agentFiles ?? []) lines.push(` ${problem}`);
|
|
1926
|
+
for (const { entry, why } of meta.exemptIgnored ?? []) {
|
|
1927
|
+
lines.push(` jig.config.json exempt: "${entry}" is ignored: ${why}`);
|
|
1928
|
+
}
|
|
1779
1929
|
if (meta.exemptPatterns && meta.exemptPatterns.length > 0) {
|
|
1780
1930
|
const n = meta.exempt?.length ?? 0;
|
|
1781
1931
|
lines.push(` ${n} file(s) exempt via jig.config.json and not scanned:`);
|
|
@@ -1783,6 +1933,9 @@ function formatReport(findings, meta) {
|
|
|
1783
1933
|
const note = count === 0 ? meta.scope === "changed" ? "matches nothing among the changed files \u2014 not necessarily wrong" : "matches nothing \u2014 check the path" : tooBroad ? `${count} files \u2014 likely too broad, review it` : `${count} file${count > 1 ? "s" : ""}`;
|
|
1784
1934
|
lines.push(` ${pattern} (${note})`);
|
|
1785
1935
|
}
|
|
1936
|
+
for (const { pattern, suggest } of meta.exemptPatterns) {
|
|
1937
|
+
if (suggest?.length) lines.push(` ${pattern}: did you mean ${suggest.map((f) => `"${f}"`).join(" or ")}? Paths start at the project folder.`);
|
|
1938
|
+
}
|
|
1786
1939
|
if (n > 0) {
|
|
1787
1940
|
lines.push(` ${meta.exempt.slice(0, 8).join(", ")}${n > 8 ? `, and ${n - 8} more` : ""}`);
|
|
1788
1941
|
}
|
|
@@ -1946,8 +2099,8 @@ function isReaderText(file) {
|
|
|
1946
2099
|
}
|
|
1947
2100
|
|
|
1948
2101
|
// src/check/run.ts
|
|
1949
|
-
import { readFileSync as
|
|
1950
|
-
import { join as
|
|
2102
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
2103
|
+
import { join as join19 } from "path";
|
|
1951
2104
|
|
|
1952
2105
|
// src/check/css.ts
|
|
1953
2106
|
function maskComments(source) {
|
|
@@ -3366,8 +3519,8 @@ var menuState = {
|
|
|
3366
3519
|
};
|
|
3367
3520
|
|
|
3368
3521
|
// src/check/declared-properties.ts
|
|
3369
|
-
import { existsSync as
|
|
3370
|
-
import { join as
|
|
3522
|
+
import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
|
|
3523
|
+
import { join as join18 } from "path";
|
|
3371
3524
|
var DECLARATION_RE = /(?<![\w-])['"]?(--[A-Za-z_][\w-]*)['"]?\s*:/g;
|
|
3372
3525
|
var AT_PROPERTY_RE = /@property\s+(--[\w-]+)/g;
|
|
3373
3526
|
var SET_PROPERTY_RE = /setProperty\(\s*['"`](--[\w-]+)/g;
|
|
@@ -3379,16 +3532,16 @@ function addAll(names, text) {
|
|
|
3379
3532
|
}
|
|
3380
3533
|
}
|
|
3381
3534
|
function readPackageImport(projectRoot, spec) {
|
|
3382
|
-
const base =
|
|
3535
|
+
const base = join18(projectRoot, "node_modules", spec);
|
|
3383
3536
|
for (const candidate of [base, `${base}.css`]) {
|
|
3384
3537
|
try {
|
|
3385
|
-
if (
|
|
3538
|
+
if (existsSync11(candidate) && candidate.endsWith(".css")) return readFileSync14(candidate, "utf8");
|
|
3386
3539
|
} catch {
|
|
3387
3540
|
}
|
|
3388
3541
|
}
|
|
3389
3542
|
try {
|
|
3390
|
-
const pkg = JSON.parse(
|
|
3391
|
-
if (pkg.style) return
|
|
3543
|
+
const pkg = JSON.parse(readFileSync14(join18(base, "package.json"), "utf8"));
|
|
3544
|
+
if (pkg.style) return readFileSync14(join18(base, pkg.style), "utf8");
|
|
3392
3545
|
} catch {
|
|
3393
3546
|
}
|
|
3394
3547
|
return void 0;
|
|
@@ -3400,7 +3553,7 @@ function collectDeclaredProperties(projectRoot, files, tokenNames = []) {
|
|
|
3400
3553
|
for (const file of files) {
|
|
3401
3554
|
let raw;
|
|
3402
3555
|
try {
|
|
3403
|
-
raw =
|
|
3556
|
+
raw = readFileSync14(join18(projectRoot, file), "utf8");
|
|
3404
3557
|
} catch {
|
|
3405
3558
|
continue;
|
|
3406
3559
|
}
|
|
@@ -3505,19 +3658,21 @@ function indentedProse(source) {
|
|
|
3505
3658
|
const blank3 = (m) => " ".repeat(m.length);
|
|
3506
3659
|
return source.split("\n").map((line) => /^\s*(-|\/\/|#(?!\w)|=)/.test(line) ? blank3(line) : line).join("\n");
|
|
3507
3660
|
}
|
|
3661
|
+
function readerSpans(file, raw) {
|
|
3662
|
+
const markdown = MARKDOWN.test(file);
|
|
3663
|
+
const indented = INDENTED.test(file);
|
|
3664
|
+
const masked = markdown ? markdownProse(raw) : indented ? indentedProse(maskNonProse(raw)) : maskNonProse(raw);
|
|
3665
|
+
const script = SCRIPT.test(file);
|
|
3666
|
+
return markdown || indented ? [{ index: 0, text: masked }] : proseSpans(masked, !script).filter((span) => !script || !CODEY.test(span.text));
|
|
3667
|
+
}
|
|
3508
3668
|
var emDash = {
|
|
3509
3669
|
name: "em-dash",
|
|
3510
3670
|
appliesTo: (file) => isReaderText(file),
|
|
3511
3671
|
run(_source, file, ctx) {
|
|
3512
|
-
const markdown = MARKDOWN.test(file);
|
|
3513
|
-
const indented = INDENTED.test(file);
|
|
3514
|
-
const masked = markdown ? markdownProse(ctx.raw) : indented ? indentedProse(maskNonProse(ctx.raw)) : maskNonProse(ctx.raw);
|
|
3515
3672
|
const starts = buildLineIndex(ctx.raw);
|
|
3516
3673
|
const findings = [];
|
|
3517
3674
|
const seen = /* @__PURE__ */ new Set();
|
|
3518
|
-
const
|
|
3519
|
-
const spans = markdown || indented ? [{ index: 0, text: masked }] : proseSpans(masked, !script).filter((span) => !script || !CODEY.test(span.text));
|
|
3520
|
-
for (const span of spans) {
|
|
3675
|
+
for (const span of readerSpans(file, ctx.raw)) {
|
|
3521
3676
|
if (!EM_DASH.test(span.text)) continue;
|
|
3522
3677
|
for (const hit of span.text.matchAll(/\u2014/g)) {
|
|
3523
3678
|
const line = lineForOffset(starts, span.index + hit.index);
|
|
@@ -3539,6 +3694,135 @@ var emDash = {
|
|
|
3539
3694
|
}
|
|
3540
3695
|
};
|
|
3541
3696
|
|
|
3697
|
+
// src/check/detectors/prose.ts
|
|
3698
|
+
var FORMULA = new RegExp(
|
|
3699
|
+
[
|
|
3700
|
+
String.raw`in today['’]s (?:fast[- ]paced|rapidly|ever[- ]|increasingly|digital|modern|busy)`,
|
|
3701
|
+
String.raw`in an (?:increasingly|ever[- ](?:changing|evolving)) \w+ (?:world|landscape|age|marketplace|era)`,
|
|
3702
|
+
String.raw`as technology continues to evolve`,
|
|
3703
|
+
String.raw`it(?: is|['’]s) (?:important|worth) (?:to note|noting)`,
|
|
3704
|
+
String.raw`let['’]s (?:dive|delve)\b`,
|
|
3705
|
+
String.raw`here['’]s the thing\b`,
|
|
3706
|
+
String.raw`without further ado`,
|
|
3707
|
+
String.raw`in conclusion\b`,
|
|
3708
|
+
String.raw`only time will tell`,
|
|
3709
|
+
String.raw`the future (?:looks|is) (?:bright|incredibly promising)`
|
|
3710
|
+
].map((p) => `\\b${p}`).join("|"),
|
|
3711
|
+
"gi"
|
|
3712
|
+
);
|
|
3713
|
+
function maskQuotations(raw) {
|
|
3714
|
+
const blank3 = (m) => m.replace(/[^\n]/g, " ");
|
|
3715
|
+
return raw.replace(/<blockquote\b[\s\S]*?<\/blockquote\s*>/gi, blank3).replace(/<q\b[\s\S]*?<\/q\s*>/gi, blank3).replace(/^ {0,3}>[^\n]*/gm, blank3);
|
|
3716
|
+
}
|
|
3717
|
+
var formulaPhrase = {
|
|
3718
|
+
name: "formula-phrase",
|
|
3719
|
+
appliesTo: (file) => isReaderText(file),
|
|
3720
|
+
run(_source, file, ctx) {
|
|
3721
|
+
const starts = buildLineIndex(ctx.raw);
|
|
3722
|
+
const findings = [];
|
|
3723
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3724
|
+
for (const span of readerSpans(file, maskQuotations(ctx.raw))) {
|
|
3725
|
+
for (const hit of span.text.matchAll(FORMULA)) {
|
|
3726
|
+
const line = lineForOffset(starts, span.index + hit.index);
|
|
3727
|
+
if (seen.has(line)) continue;
|
|
3728
|
+
seen.add(line);
|
|
3729
|
+
findings.push(
|
|
3730
|
+
mkFinding(
|
|
3731
|
+
ctx,
|
|
3732
|
+
"formula-phrase",
|
|
3733
|
+
file,
|
|
3734
|
+
line,
|
|
3735
|
+
`a formula phrase ("${hit[0]}"): start with the point, and end when it has been made`,
|
|
3736
|
+
sourceLine(ctx.raw, line)
|
|
3737
|
+
)
|
|
3738
|
+
);
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
return findings.sort((a, b) => a.line - b.line);
|
|
3742
|
+
}
|
|
3743
|
+
};
|
|
3744
|
+
var HEADING3 = /^ {0,3}#{1,6}[ \t]+(.+)$/gm;
|
|
3745
|
+
var FENCE = /^ {0,3}(`{3,}|~{3,})[\s\S]*?^ {0,3}\1[^\n]*$/gm;
|
|
3746
|
+
var headingEmoji = {
|
|
3747
|
+
name: "heading-emoji",
|
|
3748
|
+
appliesTo: (file) => isReaderText(file) && hasExtension(file, [".md", ".markdown"]) && !isStyleBearing(file),
|
|
3749
|
+
run(_source, file, ctx) {
|
|
3750
|
+
const masked = ctx.raw.replace(FENCE, (m) => m.replace(/[^\n]/g, " "));
|
|
3751
|
+
const starts = buildLineIndex(ctx.raw);
|
|
3752
|
+
const findings = [];
|
|
3753
|
+
for (const heading of masked.matchAll(HEADING3)) {
|
|
3754
|
+
const glyph = [...heading[1].matchAll(EMOJI_RE)].find((m) => !TEXTUAL.has(m[0]));
|
|
3755
|
+
if (!glyph) continue;
|
|
3756
|
+
const line = lineForOffset(starts, heading.index);
|
|
3757
|
+
findings.push(
|
|
3758
|
+
mkFinding(
|
|
3759
|
+
ctx,
|
|
3760
|
+
"heading-emoji",
|
|
3761
|
+
file,
|
|
3762
|
+
line,
|
|
3763
|
+
`an emoji in a heading ("${glyph[0]}"): let the words carry it; a screen reader reads the emoji's name first`,
|
|
3764
|
+
sourceLine(ctx.raw, line)
|
|
3765
|
+
)
|
|
3766
|
+
);
|
|
3767
|
+
}
|
|
3768
|
+
return findings;
|
|
3769
|
+
}
|
|
3770
|
+
};
|
|
3771
|
+
|
|
3772
|
+
// src/check/detectors/layout-motion.ts
|
|
3773
|
+
var LAYOUT = String.raw`(?:(?:min-|max-)?(?:width|height)|(?:inline|block)-size|top|left|right|bottom|inset(?:-[a-z-]+)?|margin(?:-[a-z-]+)?|padding(?:-[a-z-]+)?)`;
|
|
3774
|
+
var LAYOUT_NAME = new RegExp(`^${LAYOUT}$`, "i");
|
|
3775
|
+
var TRANSITION = /(?<![-\w])transition(-property)?\s*:\s*([^;}]+)/gi;
|
|
3776
|
+
var KEYFRAMES = /@(?:-webkit-)?keyframes\b[^{]*\{/gi;
|
|
3777
|
+
var KEYFRAME_DECL = new RegExp(`(?<![-\\w])(${LAYOUT})\\s*:`, "gi");
|
|
3778
|
+
var TAILWIND = new RegExp(`(?<![-\\w\\[])transition-(all|\\[[^\\]\\s]*?\\b${LAYOUT}\\b[^\\]\\s]*\\])(?![-\\w])`, "gi");
|
|
3779
|
+
function namedProperties(value, longhand) {
|
|
3780
|
+
return value.split(",").map((item) => {
|
|
3781
|
+
const words = item.trim().split(/\s+/);
|
|
3782
|
+
return longhand ? words[0] : words.find((w) => /^[a-z-]+$/i.test(w) && !/^(ease|ease-in|ease-out|ease-in-out|linear|step-start|step-end|allow-discrete|normal|none)$/i.test(w)) ?? "";
|
|
3783
|
+
}).filter(Boolean);
|
|
3784
|
+
}
|
|
3785
|
+
function blockEnd(source, open) {
|
|
3786
|
+
let depth = 0;
|
|
3787
|
+
for (let i = open; i < source.length; i++) {
|
|
3788
|
+
if (source[i] === "{") depth++;
|
|
3789
|
+
else if (source[i] === "}" && --depth === 0) return i;
|
|
3790
|
+
}
|
|
3791
|
+
return source.length;
|
|
3792
|
+
}
|
|
3793
|
+
var layoutMotion = {
|
|
3794
|
+
name: "layout-motion",
|
|
3795
|
+
appliesTo: (file) => isStyleBearing(file),
|
|
3796
|
+
run(source, file, ctx) {
|
|
3797
|
+
const findings = [];
|
|
3798
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3799
|
+
const report2 = (text, offset, message) => {
|
|
3800
|
+
const line = lineForOffset(buildLineIndex(text), offset);
|
|
3801
|
+
if (seen.has(line)) return;
|
|
3802
|
+
seen.add(line);
|
|
3803
|
+
findings.push(mkFinding(ctx, "layout-motion", file, line, message, sourceLine(text, line)));
|
|
3804
|
+
};
|
|
3805
|
+
for (const m of source.matchAll(TRANSITION)) {
|
|
3806
|
+
const names = namedProperties(m[2], Boolean(m[1]));
|
|
3807
|
+
const bad = names.find((n) => n.toLowerCase() === "all" || LAYOUT_NAME.test(n));
|
|
3808
|
+
if (!bad) continue;
|
|
3809
|
+
report2(source, m.index, bad.toLowerCase() === "all" ? "a transition on `all`: name the properties it covers, and animate transform and opacity" : `a transition on \`${bad}\`, which lays the page out again every frame: animate transform or opacity instead`);
|
|
3810
|
+
}
|
|
3811
|
+
for (const k of source.matchAll(KEYFRAMES)) {
|
|
3812
|
+
const open = k.index + k[0].length - 1;
|
|
3813
|
+
const body = source.slice(open, blockEnd(source, open));
|
|
3814
|
+
for (const d of body.matchAll(KEYFRAME_DECL)) {
|
|
3815
|
+
report2(source, open + d.index, `a keyframe animates \`${d[1]}\`, which lays the page out again every frame: animate transform or opacity instead`);
|
|
3816
|
+
}
|
|
3817
|
+
}
|
|
3818
|
+
const raw = maskProseComments(ctx.raw);
|
|
3819
|
+
for (const t of raw.matchAll(TAILWIND)) {
|
|
3820
|
+
report2(ctx.raw, t.index, `\`${t[0]}\` animates ${t[1] === "all" ? "every property that changes" : "layout"}: use \`transition\`, \`transition-opacity\` or \`transition-transform\``);
|
|
3821
|
+
}
|
|
3822
|
+
return findings.sort((a, b) => a.line - b.line);
|
|
3823
|
+
}
|
|
3824
|
+
};
|
|
3825
|
+
|
|
3542
3826
|
// src/check/detectors/semantic-element.ts
|
|
3543
3827
|
var BODY = /<body\b/i;
|
|
3544
3828
|
var MAIN = /<main\b|role\s*=\s*["']main["']/i;
|
|
@@ -4094,6 +4378,9 @@ var DETECTORS = [
|
|
|
4094
4378
|
menuState,
|
|
4095
4379
|
undeclaredToken,
|
|
4096
4380
|
emDash,
|
|
4381
|
+
formulaPhrase,
|
|
4382
|
+
headingEmoji,
|
|
4383
|
+
layoutMotion,
|
|
4097
4384
|
semanticElement,
|
|
4098
4385
|
metadata,
|
|
4099
4386
|
interfaceSafety,
|
|
@@ -4116,7 +4403,7 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
|
|
|
4116
4403
|
if (applicable.length === 0) continue;
|
|
4117
4404
|
let raw;
|
|
4118
4405
|
try {
|
|
4119
|
-
raw =
|
|
4406
|
+
raw = readFileSync15(join19(projectRoot, file), "utf8");
|
|
4120
4407
|
} catch {
|
|
4121
4408
|
continue;
|
|
4122
4409
|
}
|
|
@@ -4130,8 +4417,8 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
|
|
|
4130
4417
|
}
|
|
4131
4418
|
|
|
4132
4419
|
// src/check/tokens.ts
|
|
4133
|
-
import { existsSync as
|
|
4134
|
-
import { join as
|
|
4420
|
+
import { existsSync as existsSync12, readdirSync as readdirSync8, readFileSync as readFileSync16 } from "fs";
|
|
4421
|
+
import { join as join20 } from "path";
|
|
4135
4422
|
function rootDeclarations(source) {
|
|
4136
4423
|
const out = {};
|
|
4137
4424
|
for (const block of splitRuleBlocks(source)) {
|
|
@@ -4146,20 +4433,20 @@ function rootDeclarations(source) {
|
|
|
4146
4433
|
}
|
|
4147
4434
|
function loadTokenMap(projectRoot, consumerFiles = []) {
|
|
4148
4435
|
const jig = {};
|
|
4149
|
-
const tokensDir =
|
|
4150
|
-
if (
|
|
4436
|
+
const tokensDir = join20(projectRoot, ".jig", "tokens");
|
|
4437
|
+
if (existsSync12(tokensDir)) {
|
|
4151
4438
|
for (const file of readdirSync8(tokensDir).filter((f) => f.endsWith(".css")).sort()) {
|
|
4152
|
-
Object.assign(jig, rootDeclarations(
|
|
4439
|
+
Object.assign(jig, rootDeclarations(readFileSync16(join20(tokensDir, file), "utf8")));
|
|
4153
4440
|
}
|
|
4154
4441
|
}
|
|
4155
4442
|
const consumer = {};
|
|
4156
4443
|
const ambiguous = /* @__PURE__ */ new Set();
|
|
4157
|
-
const vendored =
|
|
4444
|
+
const vendored = join20(".jig", "tokens");
|
|
4158
4445
|
for (const file of consumerFiles) {
|
|
4159
4446
|
if (file.includes(vendored)) continue;
|
|
4160
4447
|
let source;
|
|
4161
4448
|
try {
|
|
4162
|
-
source =
|
|
4449
|
+
source = readFileSync16(join20(projectRoot, file), "utf8");
|
|
4163
4450
|
} catch {
|
|
4164
4451
|
continue;
|
|
4165
4452
|
}
|
|
@@ -4178,8 +4465,8 @@ function loadTokenMap(projectRoot, consumerFiles = []) {
|
|
|
4178
4465
|
}
|
|
4179
4466
|
|
|
4180
4467
|
// src/check/exempt.ts
|
|
4181
|
-
import { existsSync as
|
|
4182
|
-
import { join as
|
|
4468
|
+
import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
|
|
4469
|
+
import { join as join21 } from "path";
|
|
4183
4470
|
function globToRegExp(glob) {
|
|
4184
4471
|
let out = "";
|
|
4185
4472
|
for (let i = 0; i < glob.length; i++) {
|
|
@@ -4198,18 +4485,37 @@ function globToRegExp(glob) {
|
|
|
4198
4485
|
return new RegExp(`^${out}$`);
|
|
4199
4486
|
}
|
|
4200
4487
|
function readExemptions(projectRoot) {
|
|
4201
|
-
const
|
|
4202
|
-
|
|
4488
|
+
const none = { patterns: [], ignored: [] };
|
|
4489
|
+
const configPath = join21(projectRoot, "jig.config.json");
|
|
4490
|
+
if (!existsSync13(configPath)) return none;
|
|
4203
4491
|
let declared;
|
|
4204
4492
|
try {
|
|
4205
|
-
declared = JSON.parse(
|
|
4493
|
+
declared = JSON.parse(readFileSync17(configPath, "utf8")).exempt;
|
|
4206
4494
|
} catch {
|
|
4207
|
-
return
|
|
4495
|
+
return none;
|
|
4496
|
+
}
|
|
4497
|
+
if (!Array.isArray(declared)) return none;
|
|
4498
|
+
const root = projectRoot.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
4499
|
+
const patterns = [];
|
|
4500
|
+
const ignored = [];
|
|
4501
|
+
for (const raw of declared) {
|
|
4502
|
+
if (typeof raw !== "string" || !raw.trim()) continue;
|
|
4503
|
+
let p = raw.trim().replace(/\\/g, "/");
|
|
4504
|
+
if (p.startsWith(`${root}/`)) p = p.slice(root.length + 1);
|
|
4505
|
+
p = p.replace(/^(\.\/)+/, "");
|
|
4506
|
+
if (p.startsWith("/") || /^[A-Za-z]:\//.test(p)) {
|
|
4507
|
+
ignored.push({ entry: raw, why: "an absolute path outside this project. Write it from the project folder, where jig.config.json is: `src/\u2026`" });
|
|
4508
|
+
} else if (p.split("/").includes("..")) {
|
|
4509
|
+
ignored.push({ entry: raw, why: "it leaves the project folder (`..`). Write it from the project folder, where jig.config.json is: `src/\u2026`" });
|
|
4510
|
+
} else {
|
|
4511
|
+
patterns.push(p);
|
|
4512
|
+
}
|
|
4208
4513
|
}
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
);
|
|
4514
|
+
return { patterns, ignored };
|
|
4515
|
+
}
|
|
4516
|
+
function suggestPaths(pattern, files) {
|
|
4517
|
+
if (pattern.includes("/") || /[*?]/.test(pattern)) return [];
|
|
4518
|
+
return files.filter((f) => f === pattern || f.endsWith(`/${pattern}`)).slice(0, 3);
|
|
4213
4519
|
}
|
|
4214
4520
|
var LIKELY_TOO_BROAD = 3;
|
|
4215
4521
|
function applyExemptions(files, patterns) {
|
|
@@ -4234,8 +4540,8 @@ function applyExemptions(files, patterns) {
|
|
|
4234
4540
|
}
|
|
4235
4541
|
|
|
4236
4542
|
// src/check/waiver.ts
|
|
4237
|
-
import { readFileSync as
|
|
4238
|
-
import { join as
|
|
4543
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
4544
|
+
import { join as join22 } from "path";
|
|
4239
4545
|
var WAIVER = /jig-allow\s+([A-Z]-\d+(?:\s*,\s*[A-Z]-\d+)*)\s*:(.*)$/;
|
|
4240
4546
|
function readWaiver(line) {
|
|
4241
4547
|
const m = WAIVER.exec(line);
|
|
@@ -4250,7 +4556,7 @@ function applyWaivers(root, findings) {
|
|
|
4250
4556
|
let cached = lines.get(file);
|
|
4251
4557
|
if (!cached) {
|
|
4252
4558
|
try {
|
|
4253
|
-
cached =
|
|
4559
|
+
cached = readFileSync18(join22(root, file), "utf8").split("\n");
|
|
4254
4560
|
} catch {
|
|
4255
4561
|
cached = [];
|
|
4256
4562
|
}
|
|
@@ -4315,8 +4621,8 @@ function hasMenuToggle(raw) {
|
|
|
4315
4621
|
}
|
|
4316
4622
|
|
|
4317
4623
|
// src/check/mode-wiring.ts
|
|
4318
|
-
import { existsSync as
|
|
4319
|
-
import { dirname as dirname6, join as
|
|
4624
|
+
import { existsSync as existsSync14, readFileSync as readFileSync19 } from "fs";
|
|
4625
|
+
import { dirname as dirname6, join as join23 } from "path";
|
|
4320
4626
|
var MODES = ["editorial", "product", "operator"];
|
|
4321
4627
|
function configProblems(config) {
|
|
4322
4628
|
const problems = [];
|
|
@@ -4338,10 +4644,10 @@ function configProblems(config) {
|
|
|
4338
4644
|
}
|
|
4339
4645
|
function modeWiringProblems(projectRoot) {
|
|
4340
4646
|
let config;
|
|
4341
|
-
const path =
|
|
4342
|
-
if (!
|
|
4647
|
+
const path = join23(projectRoot, "jig.config.json");
|
|
4648
|
+
if (!existsSync14(path)) return [];
|
|
4343
4649
|
try {
|
|
4344
|
-
config = JSON.parse(
|
|
4650
|
+
config = JSON.parse(readFileSync19(path, "utf8"));
|
|
4345
4651
|
} catch {
|
|
4346
4652
|
return [{ mode: "", barrel: "jig.config.json", message: "jig.config.json is not valid JSON, so nothing in it applies" }];
|
|
4347
4653
|
}
|
|
@@ -4354,10 +4660,10 @@ function modeWiringProblems(projectRoot) {
|
|
|
4354
4660
|
const rel = (f) => dir === "." ? f : `${dir}/${f}`;
|
|
4355
4661
|
const multi = modes.length > 1;
|
|
4356
4662
|
const barrelOf = (mode) => multi ? `theme.${mode}.css` : "theme.css";
|
|
4357
|
-
const anyBarrel = ["theme.css", ...modes.map((m) => `theme.${m}.css`)].some((f) =>
|
|
4663
|
+
const anyBarrel = ["theme.css", ...modes.map((m) => `theme.${m}.css`)].some((f) => existsSync14(join23(projectRoot, rel(f))));
|
|
4358
4664
|
if (!anyBarrel) return [];
|
|
4359
4665
|
const problems = [];
|
|
4360
|
-
if (multi &&
|
|
4666
|
+
if (multi && existsSync14(join23(projectRoot, rel("theme.css")))) {
|
|
4361
4667
|
problems.push({
|
|
4362
4668
|
mode: modes[0],
|
|
4363
4669
|
barrel: rel("theme.css"),
|
|
@@ -4369,7 +4675,7 @@ function modeWiringProblems(projectRoot) {
|
|
|
4369
4675
|
const modeFile = `mode.${mode}.css`;
|
|
4370
4676
|
let body;
|
|
4371
4677
|
try {
|
|
4372
|
-
body =
|
|
4678
|
+
body = readFileSync19(join23(projectRoot, barrel), "utf8");
|
|
4373
4679
|
} catch {
|
|
4374
4680
|
problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${barrel} does not exist` });
|
|
4375
4681
|
return;
|
|
@@ -4381,7 +4687,7 @@ function modeWiringProblems(projectRoot) {
|
|
|
4381
4687
|
barrel,
|
|
4382
4688
|
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`
|
|
4383
4689
|
});
|
|
4384
|
-
} else if (!
|
|
4690
|
+
} else if (!existsSync14(join23(projectRoot, rel(modeFile)))) {
|
|
4385
4691
|
problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${rel(modeFile)} does not exist` });
|
|
4386
4692
|
}
|
|
4387
4693
|
});
|
|
@@ -4389,8 +4695,8 @@ function modeWiringProblems(projectRoot) {
|
|
|
4389
4695
|
}
|
|
4390
4696
|
|
|
4391
4697
|
// src/check/token-audit.ts
|
|
4392
|
-
import { existsSync as
|
|
4393
|
-
import { join as
|
|
4698
|
+
import { existsSync as existsSync15, readdirSync as readdirSync9, readFileSync as readFileSync20 } from "fs";
|
|
4699
|
+
import { join as join24 } from "path";
|
|
4394
4700
|
var TEXT_FLOOR = 4.5;
|
|
4395
4701
|
var UI_FLOOR = 3;
|
|
4396
4702
|
var FOREGROUNDS = [
|
|
@@ -4446,24 +4752,24 @@ function floorFor(token) {
|
|
|
4446
4752
|
function tokenFiles(projectRoot) {
|
|
4447
4753
|
const read = (rel) => {
|
|
4448
4754
|
try {
|
|
4449
|
-
return
|
|
4755
|
+
return readFileSync20(join24(projectRoot, ...rel.split("/")), "utf8");
|
|
4450
4756
|
} catch {
|
|
4451
4757
|
return null;
|
|
4452
4758
|
}
|
|
4453
4759
|
};
|
|
4454
|
-
const statePath =
|
|
4455
|
-
if (
|
|
4760
|
+
const statePath = join24(projectRoot, ".jig", "state.json");
|
|
4761
|
+
if (existsSync15(statePath)) {
|
|
4456
4762
|
try {
|
|
4457
|
-
const state = JSON.parse(
|
|
4763
|
+
const state = JSON.parse(readFileSync20(statePath, "utf8"));
|
|
4458
4764
|
const recorded = Object.keys(state.files ?? {}).filter((f) => f.endsWith(".css")).sort();
|
|
4459
4765
|
const out = recorded.map((path) => ({ path, source: read(path) })).filter((f) => f.source !== null);
|
|
4460
4766
|
if (out.length > 0) return out;
|
|
4461
4767
|
} catch {
|
|
4462
4768
|
}
|
|
4463
4769
|
}
|
|
4464
|
-
const legacy =
|
|
4465
|
-
if (!
|
|
4466
|
-
return readdirSync9(legacy).filter((f) => f.endsWith(".css")).sort().map((f) => ({ path: `.jig/tokens/${f}`, source:
|
|
4770
|
+
const legacy = join24(projectRoot, ".jig", "tokens");
|
|
4771
|
+
if (!existsSync15(legacy)) return [];
|
|
4772
|
+
return readdirSync9(legacy).filter((f) => f.endsWith(".css")).sort().map((f) => ({ path: `.jig/tokens/${f}`, source: readFileSync20(join24(legacy, f), "utf8") }));
|
|
4467
4773
|
}
|
|
4468
4774
|
function auditTokenLayer(projectRoot) {
|
|
4469
4775
|
const files = tokenFiles(projectRoot);
|
|
@@ -4531,15 +4837,15 @@ function auditTokenLayer(projectRoot) {
|
|
|
4531
4837
|
}
|
|
4532
4838
|
|
|
4533
4839
|
// src/commands/check.ts
|
|
4534
|
-
var LEGACY_INDEX_REL =
|
|
4840
|
+
var LEGACY_INDEX_REL = join25(".jig", "rules.index.json");
|
|
4535
4841
|
function resolveIndexPath(projectRoot) {
|
|
4536
|
-
const legacy =
|
|
4537
|
-
if (
|
|
4538
|
-
return
|
|
4842
|
+
const legacy = join25(projectRoot, LEGACY_INDEX_REL);
|
|
4843
|
+
if (existsSync16(legacy)) return legacy;
|
|
4844
|
+
return join25(assetRoot(), "rules.index.json");
|
|
4539
4845
|
}
|
|
4540
4846
|
function resolveMode(projectRoot) {
|
|
4541
4847
|
try {
|
|
4542
|
-
const config = JSON.parse(
|
|
4848
|
+
const config = JSON.parse(readFileSync21(join25(projectRoot, "jig.config.json"), "utf8"));
|
|
4543
4849
|
const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
|
|
4544
4850
|
if (modes.length === 1) return modes[0];
|
|
4545
4851
|
return modes.length > 1 ? "mixed" : "unknown";
|
|
@@ -4556,24 +4862,25 @@ function summariseUnscanned(files) {
|
|
|
4556
4862
|
}
|
|
4557
4863
|
function countSpecs() {
|
|
4558
4864
|
try {
|
|
4559
|
-
return loadSpecs(
|
|
4865
|
+
return loadSpecs(join25(assetRoot(), "rules")).length;
|
|
4560
4866
|
} catch {
|
|
4561
4867
|
return 0;
|
|
4562
4868
|
}
|
|
4563
4869
|
}
|
|
4564
4870
|
function check(opts) {
|
|
4565
4871
|
const indexPath = resolveIndexPath(opts.projectRoot);
|
|
4566
|
-
const index = validateIndex(JSON.parse(
|
|
4872
|
+
const index = validateIndex(JSON.parse(readFileSync21(indexPath, "utf8")));
|
|
4567
4873
|
const selection = selectFiles(opts.projectRoot, opts.all);
|
|
4568
|
-
const exemptions = readExemptions(opts.projectRoot);
|
|
4569
|
-
const { scanned: files, exempt, byPattern } = applyExemptions(selection.files, exemptions);
|
|
4874
|
+
const { patterns: exemptions, ignored: exemptIgnored } = readExemptions(opts.projectRoot);
|
|
4875
|
+
const { scanned: files, exempt, byPattern: counted } = applyExemptions(selection.files, exemptions);
|
|
4876
|
+
const byPattern = counted.map((p) => ({ ...p, suggest: p.count === 0 && selection.mode !== "changed" ? suggestPaths(p.pattern, selection.files) : [] }));
|
|
4570
4877
|
const bucketFilter = opts.ci ? (b) => b === "mechanical" : void 0;
|
|
4571
4878
|
const stylesheets = selection.mode === "all" ? files : selectFiles(opts.projectRoot, true).files;
|
|
4572
4879
|
const tokens = loadTokenMap(opts.projectRoot, stylesheets);
|
|
4573
4880
|
const projectParticipates = stylesheets.some((f) => {
|
|
4574
4881
|
if (!hasExtension(f, CSS_EXTENSIONS)) return false;
|
|
4575
4882
|
try {
|
|
4576
|
-
return participatesInTokenLayer(
|
|
4883
|
+
return participatesInTokenLayer(readFileSync21(join25(opts.projectRoot, f), "utf8"), tokens);
|
|
4577
4884
|
} catch {
|
|
4578
4885
|
return false;
|
|
4579
4886
|
}
|
|
@@ -4581,7 +4888,7 @@ function check(opts) {
|
|
|
4581
4888
|
const projectResponsive = stylesheets.some((f) => {
|
|
4582
4889
|
if (!isStyleBearing(f)) return false;
|
|
4583
4890
|
try {
|
|
4584
|
-
const raw =
|
|
4891
|
+
const raw = readFileSync21(join25(opts.projectRoot, f), "utf8");
|
|
4585
4892
|
return isResponsive(maskComments(maskNonStyleRegions(raw, f)), raw);
|
|
4586
4893
|
} catch {
|
|
4587
4894
|
return false;
|
|
@@ -4590,14 +4897,14 @@ function check(opts) {
|
|
|
4590
4897
|
const viewportFitCover = stylesheets.some((f) => {
|
|
4591
4898
|
if (!isStyleBearing(f)) return false;
|
|
4592
4899
|
try {
|
|
4593
|
-
return /viewport-fit\s*=\s*cover|viewportFit\s*:\s*['"]cover['"]/i.test(
|
|
4900
|
+
return /viewport-fit\s*=\s*cover|viewportFit\s*:\s*['"]cover['"]/i.test(readFileSync21(join25(opts.projectRoot, f), "utf8"));
|
|
4594
4901
|
} catch {
|
|
4595
4902
|
return false;
|
|
4596
4903
|
}
|
|
4597
4904
|
});
|
|
4598
4905
|
const projectMenuToggle = stylesheets.some((f) => {
|
|
4599
4906
|
try {
|
|
4600
|
-
return hasMenuToggle(
|
|
4907
|
+
return hasMenuToggle(readFileSync21(join25(opts.projectRoot, f), "utf8"));
|
|
4601
4908
|
} catch {
|
|
4602
4909
|
return false;
|
|
4603
4910
|
}
|
|
@@ -4605,7 +4912,7 @@ function check(opts) {
|
|
|
4605
4912
|
const declaredProperties = collectDeclaredProperties(opts.projectRoot, stylesheets, Object.keys(tokens));
|
|
4606
4913
|
const withStyles = files.filter((f) => {
|
|
4607
4914
|
try {
|
|
4608
|
-
const src =
|
|
4915
|
+
const src = readFileSync21(join25(opts.projectRoot, f), "utf8");
|
|
4609
4916
|
return hasExtension(f, CSS_EXTENSIONS) ? src.trim().length > 0 : maskNonStyleRegions(src, f).trim().length > 0;
|
|
4610
4917
|
} catch {
|
|
4611
4918
|
return false;
|
|
@@ -4638,7 +4945,7 @@ function check(opts) {
|
|
|
4638
4945
|
// told to add a token @import it has nowhere to put.
|
|
4639
4946
|
cssFiles.length > 0 && cssFiles.every((f) => {
|
|
4640
4947
|
try {
|
|
4641
|
-
return !participatesInTokenLayer(
|
|
4948
|
+
return !participatesInTokenLayer(readFileSync21(join25(opts.projectRoot, f), "utf8"), tokens);
|
|
4642
4949
|
} catch {
|
|
4643
4950
|
return true;
|
|
4644
4951
|
}
|
|
@@ -4658,20 +4965,22 @@ function check(opts) {
|
|
|
4658
4965
|
withStyles,
|
|
4659
4966
|
exempt,
|
|
4660
4967
|
exemptPatterns: byPattern,
|
|
4968
|
+
exemptIgnored,
|
|
4661
4969
|
scope: selection.mode,
|
|
4662
4970
|
modeUnwired: modeWiringProblems(opts.projectRoot),
|
|
4971
|
+
agentFiles: agentFileProblems(opts.projectRoot, opts.homeDir || void 0),
|
|
4663
4972
|
waived
|
|
4664
4973
|
});
|
|
4665
4974
|
return { findings, report: report2, hasError, waived };
|
|
4666
4975
|
}
|
|
4667
4976
|
|
|
4668
4977
|
// src/commands/init.ts
|
|
4669
|
-
import { existsSync as
|
|
4670
|
-
import { dirname as dirname7, isAbsolute, join as
|
|
4978
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync6, readdirSync as readdirSync10, readFileSync as readFileSync25, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
4979
|
+
import { dirname as dirname7, isAbsolute, join as join29, resolve as resolve2, sep as sep3 } from "path";
|
|
4671
4980
|
|
|
4672
4981
|
// src/init/detect.ts
|
|
4673
|
-
import { readFileSync as
|
|
4674
|
-
import { join as
|
|
4982
|
+
import { readFileSync as readFileSync22 } from "fs";
|
|
4983
|
+
import { join as join26 } from "path";
|
|
4675
4984
|
var TAILWIND_CONFIG_RE = /^tailwind\.config\.(js|ts|mjs|cjs)$/;
|
|
4676
4985
|
var TAILWIND_V4_IMPORT_RE = /@import\s+["']tailwindcss["']/;
|
|
4677
4986
|
var TAILWIND_V4_THEME_RE = /@theme\b/;
|
|
@@ -4688,7 +4997,7 @@ var FRAMEWORK_DEPS = [
|
|
|
4688
4997
|
function detectFramework(projectRoot) {
|
|
4689
4998
|
let pkg;
|
|
4690
4999
|
try {
|
|
4691
|
-
pkg = JSON.parse(
|
|
5000
|
+
pkg = JSON.parse(readFileSync22(join26(projectRoot, "package.json"), "utf8"));
|
|
4692
5001
|
} catch {
|
|
4693
5002
|
return void 0;
|
|
4694
5003
|
}
|
|
@@ -4706,7 +5015,7 @@ function detect(projectRoot) {
|
|
|
4706
5015
|
for (const f of cssFiles) {
|
|
4707
5016
|
let source;
|
|
4708
5017
|
try {
|
|
4709
|
-
source =
|
|
5018
|
+
source = readFileSync22(join26(projectRoot, f), "utf8");
|
|
4710
5019
|
} catch {
|
|
4711
5020
|
continue;
|
|
4712
5021
|
}
|
|
@@ -4741,8 +5050,8 @@ function isIgnored(projectRoot, relPath) {
|
|
|
4741
5050
|
}
|
|
4742
5051
|
|
|
4743
5052
|
// src/init/derive.ts
|
|
4744
|
-
import { readFileSync as
|
|
4745
|
-
import { join as
|
|
5053
|
+
import { readFileSync as readFileSync23 } from "fs";
|
|
5054
|
+
import { join as join27 } from "path";
|
|
4746
5055
|
var DEFAULT_PROPOSAL = {
|
|
4747
5056
|
h: 264,
|
|
4748
5057
|
s: 0,
|
|
@@ -4775,7 +5084,7 @@ function fromCssCustomProperties(projectRoot, cssFiles) {
|
|
|
4775
5084
|
for (const file of cssFiles) {
|
|
4776
5085
|
let source;
|
|
4777
5086
|
try {
|
|
4778
|
-
source =
|
|
5087
|
+
source = readFileSync23(join27(projectRoot, file), "utf8");
|
|
4779
5088
|
} catch {
|
|
4780
5089
|
continue;
|
|
4781
5090
|
}
|
|
@@ -4824,7 +5133,7 @@ function balancedBraceBody(source, openIdx) {
|
|
|
4824
5133
|
function fromTailwindConfig(projectRoot, tailwindConfigFile) {
|
|
4825
5134
|
let source;
|
|
4826
5135
|
try {
|
|
4827
|
-
source =
|
|
5136
|
+
source = readFileSync23(join27(projectRoot, tailwindConfigFile), "utf8");
|
|
4828
5137
|
} catch {
|
|
4829
5138
|
return null;
|
|
4830
5139
|
}
|
|
@@ -4880,7 +5189,7 @@ function fromLiteralFrequency(projectRoot, cssFiles) {
|
|
|
4880
5189
|
for (const file of cssFiles) {
|
|
4881
5190
|
let source;
|
|
4882
5191
|
try {
|
|
4883
|
-
source =
|
|
5192
|
+
source = readFileSync23(join27(projectRoot, file), "utf8");
|
|
4884
5193
|
} catch {
|
|
4885
5194
|
continue;
|
|
4886
5195
|
}
|
|
@@ -5040,8 +5349,8 @@ function relativeImportPath(fromAbsDir, toAbsFile) {
|
|
|
5040
5349
|
}
|
|
5041
5350
|
|
|
5042
5351
|
// src/init/project-name.ts
|
|
5043
|
-
import { readFileSync as
|
|
5044
|
-
import { basename, join as
|
|
5352
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
5353
|
+
import { basename, join as join28 } from "path";
|
|
5045
5354
|
function slugify(name) {
|
|
5046
5355
|
const withoutScope = name.replace(/^@[^/]+\//, "");
|
|
5047
5356
|
const slug = withoutScope.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -5049,7 +5358,7 @@ function slugify(name) {
|
|
|
5049
5358
|
}
|
|
5050
5359
|
function deriveProjectSlug(projectRoot) {
|
|
5051
5360
|
try {
|
|
5052
|
-
const pkg = JSON.parse(
|
|
5361
|
+
const pkg = JSON.parse(readFileSync24(join28(projectRoot, "package.json"), "utf8"));
|
|
5053
5362
|
if (typeof pkg.name === "string" && pkg.name.trim()) return slugify(pkg.name);
|
|
5054
5363
|
} catch {
|
|
5055
5364
|
}
|
|
@@ -5190,7 +5499,7 @@ function parseSurfaces(answer, fallback) {
|
|
|
5190
5499
|
return parsed.length > 0 ? parsed : fallback;
|
|
5191
5500
|
}
|
|
5192
5501
|
function fileState(projectRoot, absPath, relPath, initManifest) {
|
|
5193
|
-
const existsOnDisk =
|
|
5502
|
+
const existsOnDisk = existsSync17(absPath);
|
|
5194
5503
|
const tracked = !!initManifest && relPath in initManifest.files;
|
|
5195
5504
|
const modified = tracked && isInitFileModified(projectRoot, relPath, initManifest);
|
|
5196
5505
|
return { existsOnDisk, tracked, modified };
|
|
@@ -5201,11 +5510,11 @@ function isValidSurfaceArray(v) {
|
|
|
5201
5510
|
);
|
|
5202
5511
|
}
|
|
5203
5512
|
function loadEffectiveConfig(projectRoot, configAbsPath, fallbackBrand, fallbackSurfaces) {
|
|
5204
|
-
if (!
|
|
5513
|
+
if (!existsSync17(configAbsPath)) return { brand: fallbackBrand, surfaces: fallbackSurfaces };
|
|
5205
5514
|
try {
|
|
5206
|
-
const parsed = JSON.parse(
|
|
5515
|
+
const parsed = JSON.parse(readFileSync25(configAbsPath, "utf8"));
|
|
5207
5516
|
const brandCandidate = typeof parsed.brand === "string" && parsed.brand ? parsed.brand : void 0;
|
|
5208
|
-
const brandResolves = brandCandidate ?
|
|
5517
|
+
const brandResolves = brandCandidate ? existsSync17(join29(projectRoot, ...brandCandidate.split("/"))) : false;
|
|
5209
5518
|
return {
|
|
5210
5519
|
brand: brandResolves ? brandCandidate : fallbackBrand,
|
|
5211
5520
|
surfaces: isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : fallbackSurfaces
|
|
@@ -5215,10 +5524,10 @@ function loadEffectiveConfig(projectRoot, configAbsPath, fallbackBrand, fallback
|
|
|
5215
5524
|
}
|
|
5216
5525
|
}
|
|
5217
5526
|
function configuredBrandPath(projectRoot, configAbsPath) {
|
|
5218
|
-
if (!
|
|
5527
|
+
if (!existsSync17(configAbsPath)) return { path: null };
|
|
5219
5528
|
let declared;
|
|
5220
5529
|
try {
|
|
5221
|
-
declared = JSON.parse(
|
|
5530
|
+
declared = JSON.parse(readFileSync25(configAbsPath, "utf8")).brand;
|
|
5222
5531
|
} catch {
|
|
5223
5532
|
return { path: null };
|
|
5224
5533
|
}
|
|
@@ -5246,9 +5555,9 @@ function defaultTokenDir(detection) {
|
|
|
5246
5555
|
return ["jig"];
|
|
5247
5556
|
}
|
|
5248
5557
|
function readConfiguredSurfaces(configAbsPath) {
|
|
5249
|
-
if (!
|
|
5558
|
+
if (!existsSync17(configAbsPath)) return null;
|
|
5250
5559
|
try {
|
|
5251
|
-
const parsed = JSON.parse(
|
|
5560
|
+
const parsed = JSON.parse(readFileSync25(configAbsPath, "utf8"));
|
|
5252
5561
|
return isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : null;
|
|
5253
5562
|
} catch {
|
|
5254
5563
|
return null;
|
|
@@ -5258,8 +5567,8 @@ function isCssModule(path) {
|
|
|
5258
5567
|
return /\.module\.(css|scss)$/i.test(path);
|
|
5259
5568
|
}
|
|
5260
5569
|
function legacyBrandFile(projectRoot) {
|
|
5261
|
-
const dir =
|
|
5262
|
-
if (!
|
|
5570
|
+
const dir = join29(projectRoot, ".jig", "tokens");
|
|
5571
|
+
if (!existsSync17(dir)) return void 0;
|
|
5263
5572
|
const found = readdirSync10(dir).find((f) => /^brand\.[\w.-]+\.css$/.test(f));
|
|
5264
5573
|
return found ? relKey(".jig", "tokens", found) : void 0;
|
|
5265
5574
|
}
|
|
@@ -5290,7 +5599,7 @@ var LEGACY_PAIR_RE = new RegExp(
|
|
|
5290
5599
|
`@import\\s+["'][^"']*brand\\.[\\w.-]+\\.css["'];?\\r?\\n?\\s*@import\\s+["'][^"']*mode\\.(?:${MODES2.join("|")})\\.css["'];?\\r?\\n?`
|
|
5291
5600
|
);
|
|
5292
5601
|
function wireBarrel(absPath, barrelImport) {
|
|
5293
|
-
const content =
|
|
5602
|
+
const content = existsSync17(absPath) ? readFileSync25(absPath, "utf8") : "";
|
|
5294
5603
|
const line = `@import "${barrelImport}";`;
|
|
5295
5604
|
if (content.includes(line)) return { status: "already-present" };
|
|
5296
5605
|
const legacy = LEGACY_PAIR_RE.exec(content);
|
|
@@ -5385,7 +5694,7 @@ async function init(opts) {
|
|
|
5385
5694
|
else if (critiqueAnswer) log(` Could not read '${critiqueAnswer}' \u2014 leaving it page by page; each spec will ask.`);
|
|
5386
5695
|
}
|
|
5387
5696
|
if (opts.yes) {
|
|
5388
|
-
const declared = readConfiguredSurfaces(
|
|
5697
|
+
const declared = readConfiguredSurfaces(join29(opts.projectRoot, "jig.config.json"));
|
|
5389
5698
|
const inEffect = declared ?? surfaces;
|
|
5390
5699
|
const map = inEffect.map((x) => `'${x.match}' \u2192 ${x.mode}`).join(", ");
|
|
5391
5700
|
log(
|
|
@@ -5404,7 +5713,7 @@ Brand colour adjusted to clear the 4.5:1 contrast floor: l:${originalL}% \u2192
|
|
|
5404
5713
|
const finalColor = { h: proposal.h, s: proposal.s, l: proposal.l };
|
|
5405
5714
|
const projectSlug = deriveProjectSlug(opts.projectRoot);
|
|
5406
5715
|
const configRelPath = "jig.config.json";
|
|
5407
|
-
const configAbsPath =
|
|
5716
|
+
const configAbsPath = join29(opts.projectRoot, configRelPath);
|
|
5408
5717
|
const configuredBrand = configuredBrandPath(opts.projectRoot, configAbsPath);
|
|
5409
5718
|
if (configuredBrand.refused) {
|
|
5410
5719
|
log(
|
|
@@ -5438,7 +5747,7 @@ The token layer is at ${currentDir}/, and this project's layout suggests ${would
|
|
|
5438
5747
|
}
|
|
5439
5748
|
}
|
|
5440
5749
|
const brandRelPath = relocateTo ?? configuredBrand.path ?? priorBrand ?? relKey(...defaultTokenDir(detection), brandFileName(projectSlug));
|
|
5441
|
-
const brandAbsPath =
|
|
5750
|
+
const brandAbsPath = join29(opts.projectRoot, ...brandRelPath.split("/"));
|
|
5442
5751
|
const tokensRelDir = brandRelPath.split("/").slice(0, -1);
|
|
5443
5752
|
log(
|
|
5444
5753
|
`Token layer: ${tokensRelDir.join("/") || "."}/ \u2014 ` + (configuredBrand.path ? "from jig.config.json." : priorBrand ? "the layout this project already had. Set `brand` in jig.config.json to move it." : findWireTarget(detection) ? "beside the stylesheet being wired. Set `brand` in jig.config.json to put it elsewhere." : "no stylesheet found to follow, so the project root. Set `brand` in jig.config.json to move it.")
|
|
@@ -5471,7 +5780,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5471
5780
|
if (configAction !== "written") log(` ${configRelPath} exists and is not jig-tracked \u2014 leaving it alone (re-run without --yes to refresh).`);
|
|
5472
5781
|
}
|
|
5473
5782
|
if (brandAction === "written") {
|
|
5474
|
-
const defaultCssRaw =
|
|
5783
|
+
const defaultCssRaw = readFileSync25(join29(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
|
|
5475
5784
|
const content = renderBrandFile(defaultCssRaw, projectSlug, opts.version, proposal);
|
|
5476
5785
|
mkdirSync6(dirname7(brandAbsPath), { recursive: true });
|
|
5477
5786
|
writeFileSync4(brandAbsPath, content, "utf8");
|
|
@@ -5479,7 +5788,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5479
5788
|
}
|
|
5480
5789
|
if (relocateTo && configAction !== "written") {
|
|
5481
5790
|
try {
|
|
5482
|
-
const existing = JSON.parse(
|
|
5791
|
+
const existing = JSON.parse(readFileSync25(configAbsPath, "utf8"));
|
|
5483
5792
|
const content = `${JSON.stringify({ ...existing, brand: brandRelPath }, null, 2)}
|
|
5484
5793
|
`;
|
|
5485
5794
|
writeFileSync4(configAbsPath, content, "utf8");
|
|
@@ -5498,12 +5807,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5498
5807
|
} else if (configAction === "merged") {
|
|
5499
5808
|
let existing = {};
|
|
5500
5809
|
try {
|
|
5501
|
-
existing = JSON.parse(
|
|
5810
|
+
existing = JSON.parse(readFileSync25(configAbsPath, "utf8"));
|
|
5502
5811
|
} catch {
|
|
5503
5812
|
existing = {};
|
|
5504
5813
|
}
|
|
5505
5814
|
const existingBrand = typeof existing.brand === "string" ? existing.brand : void 0;
|
|
5506
|
-
const brandStillResolves = existingBrand ?
|
|
5815
|
+
const brandStillResolves = existingBrand ? existsSync17(join29(opts.projectRoot, ...existingBrand.split("/"))) : false;
|
|
5507
5816
|
const existingSurfaces = Array.isArray(existing.surfaces) && existing.surfaces.length > 0 ? existing.surfaces : void 0;
|
|
5508
5817
|
const merged = {
|
|
5509
5818
|
...existing,
|
|
@@ -5520,12 +5829,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5520
5829
|
}
|
|
5521
5830
|
const effectiveConfig = loadEffectiveConfig(opts.projectRoot, configAbsPath, brandRelPath, surfaces);
|
|
5522
5831
|
const primaryMode = effectiveConfig.surfaces[0]?.mode ?? "product";
|
|
5523
|
-
const wiringBrandAbsPath =
|
|
5832
|
+
const wiringBrandAbsPath = join29(opts.projectRoot, ...effectiveConfig.brand.split("/"));
|
|
5524
5833
|
const declaredModes = [...new Set(effectiveConfig.surfaces.map((s) => s.mode))];
|
|
5525
5834
|
const modeAbsPaths = {};
|
|
5526
5835
|
for (const mode of declaredModes) {
|
|
5527
5836
|
const modeFileName = `mode.${mode}.css`;
|
|
5528
|
-
const modeAbsPath2 =
|
|
5837
|
+
const modeAbsPath2 = join29(opts.projectRoot, ...tokensRelDir, modeFileName);
|
|
5529
5838
|
modeAbsPaths[mode] = modeAbsPath2;
|
|
5530
5839
|
const modeRelPath = relKey(...tokensRelDir, modeFileName);
|
|
5531
5840
|
const modeState = fileState(opts.projectRoot, modeAbsPath2, modeRelPath, initManifest);
|
|
@@ -5533,7 +5842,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5533
5842
|
if (modeState.existsOnDisk && !modeState.tracked) modeAction = "skipped-untracked";
|
|
5534
5843
|
else if (modeState.modified) modeAction = "skipped-edited";
|
|
5535
5844
|
if (modeAction === "written") {
|
|
5536
|
-
const modeSourceRaw =
|
|
5845
|
+
const modeSourceRaw = readFileSync25(join29(opts.packageRoot, "tokens", modeFileName), "utf8");
|
|
5537
5846
|
const content = vendorHeader(modeFileName, opts.version, "css", null) + modeSourceRaw;
|
|
5538
5847
|
mkdirSync6(dirname7(modeAbsPath2), { recursive: true });
|
|
5539
5848
|
writeFileSync4(modeAbsPath2, content, "utf8");
|
|
@@ -5548,7 +5857,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5548
5857
|
const barrelFor = (mode) => multiMode ? `theme.${mode}.css` : "theme.css";
|
|
5549
5858
|
for (const mode of declaredModes) {
|
|
5550
5859
|
const rel = relKey(...tokensRelDir, barrelFor(mode));
|
|
5551
|
-
const abs =
|
|
5860
|
+
const abs = join29(opts.projectRoot, ...tokensRelDir, barrelFor(mode));
|
|
5552
5861
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
5553
5862
|
if (state.existsOnDisk && (!state.tracked || state.modified)) {
|
|
5554
5863
|
log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
|
|
@@ -5561,8 +5870,8 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5561
5870
|
}
|
|
5562
5871
|
if (multiMode) {
|
|
5563
5872
|
const rel = relKey(...tokensRelDir, "theme.css");
|
|
5564
|
-
const abs =
|
|
5565
|
-
if (
|
|
5873
|
+
const abs = join29(opts.projectRoot, ...tokensRelDir, "theme.css");
|
|
5874
|
+
if (existsSync17(abs)) {
|
|
5566
5875
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
5567
5876
|
if (state.tracked && !state.modified) {
|
|
5568
5877
|
rmSync2(abs);
|
|
@@ -5577,7 +5886,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5577
5886
|
const declared = declaredTokenNames(
|
|
5578
5887
|
[brandAbsPath, modeAbsPath].map((f) => {
|
|
5579
5888
|
try {
|
|
5580
|
-
return
|
|
5889
|
+
return readFileSync25(f, "utf8");
|
|
5581
5890
|
} catch {
|
|
5582
5891
|
return "";
|
|
5583
5892
|
}
|
|
@@ -5602,7 +5911,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5602
5911
|
log(` This changes how components are written. Both styles are correct.`);
|
|
5603
5912
|
const answer = (await prompt(" Generate it? [y/N]: ")).toLowerCase();
|
|
5604
5913
|
if (answer === "y" || answer === "yes") {
|
|
5605
|
-
const abs =
|
|
5914
|
+
const abs = join29(opts.projectRoot, ...tokensRelDir, "utilities.css");
|
|
5606
5915
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
5607
5916
|
if (state.existsOnDisk && (!state.tracked || state.modified)) {
|
|
5608
5917
|
log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
|
|
@@ -5613,8 +5922,8 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5613
5922
|
files[rel] = checksum(content);
|
|
5614
5923
|
const target = findWireTarget(detection);
|
|
5615
5924
|
const importPath = target ? relativeImportPath(
|
|
5616
|
-
dirname7(
|
|
5617
|
-
|
|
5925
|
+
dirname7(join29(opts.projectRoot, target)),
|
|
5926
|
+
join29(opts.projectRoot, ...tokensRelDir, "utilities.css")
|
|
5618
5927
|
) : `./${rel}`;
|
|
5619
5928
|
log(` Wrote ${rel}. Import it in the stylesheet that imports Tailwind:`);
|
|
5620
5929
|
log(` @import "tailwindcss";`);
|
|
@@ -5633,11 +5942,11 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5633
5942
|
writeInitManifest(opts.projectRoot, { version: opts.version, modes: declaredModes, files });
|
|
5634
5943
|
if (relocateTo && currentBrand) {
|
|
5635
5944
|
const oldDir = currentBrand.split("/").slice(0, -1);
|
|
5636
|
-
const oldAbsDir =
|
|
5637
|
-
if (
|
|
5945
|
+
const oldAbsDir = join29(opts.projectRoot, ...oldDir);
|
|
5946
|
+
if (existsSync17(oldAbsDir)) {
|
|
5638
5947
|
for (const name of readdirSync10(oldAbsDir).filter((f) => f.endsWith(".css"))) {
|
|
5639
5948
|
const rel = relKey(...oldDir, name);
|
|
5640
|
-
const abs =
|
|
5949
|
+
const abs = join29(oldAbsDir, name);
|
|
5641
5950
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
5642
5951
|
if (!state.tracked || state.modified) {
|
|
5643
5952
|
log(` ${rel} was edited \u2014 left in place. Delete it yourself once you have moved anything you need.`);
|
|
@@ -5651,11 +5960,11 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5651
5960
|
const wireTarget = findWireTarget(detection);
|
|
5652
5961
|
let wiring;
|
|
5653
5962
|
if (multiMode) {
|
|
5654
|
-
const bare2 =
|
|
5963
|
+
const bare2 = join29(opts.projectRoot, ...tokensRelDir, "theme.css");
|
|
5655
5964
|
for (const file of detection.cssFiles.filter((f) => !isTokenLayerFile(f))) {
|
|
5656
|
-
const abs =
|
|
5965
|
+
const abs = join29(opts.projectRoot, file);
|
|
5657
5966
|
try {
|
|
5658
|
-
const before =
|
|
5967
|
+
const before = readFileSync25(abs, "utf8");
|
|
5659
5968
|
const after = before.split("\n").filter((l) => {
|
|
5660
5969
|
const m = /^\s*@import\s+["']([^"']+)["'];?\s*$/.exec(l);
|
|
5661
5970
|
return !(m && !/^[a-z]+:|^\//i.test(m[1]) && resolve2(dirname7(abs), m[1]) === bare2);
|
|
@@ -5677,21 +5986,21 @@ Could not edit ${file}: ${err.message}. Remove its import of ${relKey(...tokensR
|
|
|
5677
5986
|
snippet: effectiveConfig.surfaces.map((s) => `${s.match} \u2192 ${relKey(...tokensRelDir, barrelFor(s.mode))}`).join("\n")
|
|
5678
5987
|
};
|
|
5679
5988
|
} else if (wireTarget) {
|
|
5680
|
-
const targetAbsDir = dirname7(
|
|
5989
|
+
const targetAbsDir = dirname7(join29(opts.projectRoot, wireTarget));
|
|
5681
5990
|
const barrelImport = relativeImportPath(
|
|
5682
5991
|
targetAbsDir,
|
|
5683
|
-
|
|
5992
|
+
join29(opts.projectRoot, ...tokensRelDir, "theme.css")
|
|
5684
5993
|
);
|
|
5685
5994
|
const snippet = `@import "${barrelImport}";`;
|
|
5686
5995
|
try {
|
|
5687
5996
|
if (relocateTo && currentBrand) {
|
|
5688
|
-
const abs =
|
|
5997
|
+
const abs = join29(opts.projectRoot, wireTarget);
|
|
5689
5998
|
const oldDirName = currentBrand.split("/").slice(0, -1).join("/");
|
|
5690
|
-
const before =
|
|
5999
|
+
const before = existsSync17(abs) ? readFileSync25(abs, "utf8") : "";
|
|
5691
6000
|
const after = before.split("\n").filter((l) => !(l.includes("@import") && l.includes(oldDirName))).join("\n");
|
|
5692
6001
|
if (after !== before) writeFileSync4(abs, after, "utf8");
|
|
5693
6002
|
}
|
|
5694
|
-
const outcome = wireBarrel(
|
|
6003
|
+
const outcome = wireBarrel(join29(opts.projectRoot, wireTarget), barrelImport);
|
|
5695
6004
|
wiring = { target: wireTarget, status: outcome.status, snippet };
|
|
5696
6005
|
const verb = outcome.status === "wired" ? "Wired" : outcome.status === "rewired" ? "Rewired" : "Already present in";
|
|
5697
6006
|
log(`
|
|
@@ -5708,7 +6017,7 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
5708
6017
|
} else {
|
|
5709
6018
|
const barrelImport = relativeImportPath(
|
|
5710
6019
|
opts.projectRoot,
|
|
5711
|
-
|
|
6020
|
+
join29(opts.projectRoot, ...tokensRelDir, "theme.css")
|
|
5712
6021
|
);
|
|
5713
6022
|
const snippet = `@import "${barrelImport}";`;
|
|
5714
6023
|
wiring = { target: null, status: "print-only", snippet };
|
|
@@ -5717,7 +6026,7 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
5717
6026
|
if (pasteTargets.length > 0) {
|
|
5718
6027
|
log("Paste the block for whichever of these is your global stylesheet:");
|
|
5719
6028
|
for (const target of pasteTargets) {
|
|
5720
|
-
const dir = dirname7(
|
|
6029
|
+
const dir = dirname7(join29(opts.projectRoot, target));
|
|
5721
6030
|
const forTarget = `@import "${relativeImportPath(dir, wiringBrandAbsPath)}";
|
|
5722
6031
|
@import "${relativeImportPath(dir, modeAbsPath)}";`;
|
|
5723
6032
|
log(`
|
|
@@ -5743,6 +6052,9 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
5743
6052
|
"\n WARNING: .jig/ is gitignored. It holds state.json \u2014 which `update` reads to leave your edited files alone \u2014 and the specs, mockups and critique verdicts this project agreed. Ignored, update cannot tell your edits from its own writes, and every spec is invisible to the next agent. Commit it."
|
|
5744
6053
|
);
|
|
5745
6054
|
}
|
|
6055
|
+
const agentFiles = ensureAgentFiles(opts.projectRoot, { claude: claudeInstalled(opts.projectRoot, opts.homeDir) });
|
|
6056
|
+
if (agentFiles.length) log(`
|
|
6057
|
+
Wrote Jig's instructions for your agent into ${agentFiles.join(" and ")}; your own text in them is untouched.`);
|
|
5746
6058
|
const baselineResult = check({ projectRoot: opts.projectRoot, homeDir: opts.homeDir, version: opts.version, all: true, ci: false });
|
|
5747
6059
|
log("\nBaseline:");
|
|
5748
6060
|
log(baselineResult.report);
|
|
@@ -5765,28 +6077,28 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
5765
6077
|
|
|
5766
6078
|
// src/commands/verdicts.ts
|
|
5767
6079
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
5768
|
-
import { existsSync as
|
|
5769
|
-
import { join as
|
|
6080
|
+
import { existsSync as existsSync24, readFileSync as readFileSync32 } from "fs";
|
|
6081
|
+
import { join as join38, relative as relative4, resolve as resolve5, sep as sep6 } from "path";
|
|
5770
6082
|
|
|
5771
6083
|
// src/rules/citations.ts
|
|
5772
|
-
import { join as
|
|
6084
|
+
import { join as join30 } from "path";
|
|
5773
6085
|
function citableIds(packageRoot2) {
|
|
5774
|
-
const rulesDir =
|
|
6086
|
+
const rulesDir = join30(packageRoot2, "rules");
|
|
5775
6087
|
const ids = [
|
|
5776
|
-
...loadRules(rulesDir,
|
|
6088
|
+
...loadRules(rulesDir, join30(packageRoot2, "rules.index.json")).map((r) => r.id),
|
|
5777
6089
|
...loadSpecs(rulesDir).map((s) => s.id)
|
|
5778
6090
|
];
|
|
5779
6091
|
return [...new Set(ids)].sort();
|
|
5780
6092
|
}
|
|
5781
6093
|
|
|
5782
6094
|
// src/probe/check.ts
|
|
5783
|
-
import { existsSync as
|
|
5784
|
-
import { join as
|
|
6095
|
+
import { existsSync as existsSync21, readdirSync as readdirSync13, readFileSync as readFileSync29 } from "fs";
|
|
6096
|
+
import { join as join35 } from "path";
|
|
5785
6097
|
|
|
5786
6098
|
// src/probe/save.ts
|
|
5787
|
-
import { existsSync as
|
|
6099
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync7, readdirSync as readdirSync12, readFileSync as readFileSync28, writeFileSync as writeFileSync5 } from "fs";
|
|
5788
6100
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5789
|
-
import { join as
|
|
6101
|
+
import { join as join34, relative as relative3, resolve as resolve4, sep as sep5 } from "path";
|
|
5790
6102
|
|
|
5791
6103
|
// src/probe/script.ts
|
|
5792
6104
|
var PROBE_VERSION = 9;
|
|
@@ -6018,24 +6330,28 @@ var PROBE_SCRIPT = `(async () => {
|
|
|
6018
6330
|
|
|
6019
6331
|
// src/probe/browser.ts
|
|
6020
6332
|
import { spawn } from "child_process";
|
|
6021
|
-
import { existsSync as
|
|
6022
|
-
import { homedir, tmpdir } from "os";
|
|
6023
|
-
import { join as
|
|
6333
|
+
import { existsSync as existsSync18, mkdtempSync, readdirSync as readdirSync11, rmSync as rmSync3 } from "fs";
|
|
6334
|
+
import { homedir as homedir2, tmpdir } from "os";
|
|
6335
|
+
import { join as join31 } from "path";
|
|
6024
6336
|
var CHROME_ENV = ["JIG_CHROME", "CHROME_PATH", "PUPPETEER_EXECUTABLE_PATH", "CHROMIUM_PATH"];
|
|
6025
|
-
var CHROME_COMMANDS = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge", "brave-browser"];
|
|
6337
|
+
var CHROME_COMMANDS = ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "microsoft-edge", "brave-browser", "chrome", "msedge"];
|
|
6026
6338
|
var CHROME_APPS = [
|
|
6027
6339
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
6028
6340
|
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
6029
6341
|
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
6030
6342
|
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
6031
|
-
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe"
|
|
6343
|
+
"C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
|
|
6344
|
+
// Every Windows machine has Edge; Chrome is often installed per user.
|
|
6345
|
+
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
6346
|
+
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
6347
|
+
...process.env.LOCALAPPDATA ? [join31(process.env.LOCALAPPDATA, "Google", "Chrome", "Application", "chrome.exe")] : []
|
|
6032
6348
|
];
|
|
6033
6349
|
function cachedBrowsers() {
|
|
6034
6350
|
const roots = [
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6351
|
+
join31(homedir2(), ".cache", "ms-playwright"),
|
|
6352
|
+
join31(homedir2(), "Library", "Caches", "ms-playwright"),
|
|
6353
|
+
join31(homedir2(), "AppData", "Local", "ms-playwright"),
|
|
6354
|
+
join31(homedir2(), ".cache", "puppeteer", "chrome")
|
|
6039
6355
|
];
|
|
6040
6356
|
const found = [];
|
|
6041
6357
|
for (const root of roots) {
|
|
@@ -6055,8 +6371,8 @@ function cachedBrowsers() {
|
|
|
6055
6371
|
["chrome-win", "chrome.exe"],
|
|
6056
6372
|
["chrome-win64", "chrome.exe"]
|
|
6057
6373
|
]) {
|
|
6058
|
-
const candidate =
|
|
6059
|
-
if (
|
|
6374
|
+
const candidate = join31(root, entry, ...rel);
|
|
6375
|
+
if (existsSync18(candidate)) found.push(candidate);
|
|
6060
6376
|
}
|
|
6061
6377
|
}
|
|
6062
6378
|
}
|
|
@@ -6065,21 +6381,25 @@ function cachedBrowsers() {
|
|
|
6065
6381
|
function findChrome() {
|
|
6066
6382
|
for (const key of CHROME_ENV) {
|
|
6067
6383
|
const value = process.env[key];
|
|
6068
|
-
if (value &&
|
|
6384
|
+
if (value && existsSync18(value)) return value;
|
|
6069
6385
|
}
|
|
6070
6386
|
for (const command of CHROME_COMMANDS) {
|
|
6071
6387
|
const resolved = which(command);
|
|
6072
6388
|
if (resolved) return resolved;
|
|
6073
6389
|
}
|
|
6074
|
-
for (const app of CHROME_APPS) if (
|
|
6390
|
+
for (const app of CHROME_APPS) if (existsSync18(app)) return app;
|
|
6075
6391
|
return cachedBrowsers()[0];
|
|
6076
6392
|
}
|
|
6077
6393
|
function which(command) {
|
|
6078
|
-
const
|
|
6394
|
+
const windows = process.platform === "win32";
|
|
6395
|
+
const dirs = (process.env.PATH ?? "").split(windows ? ";" : ":");
|
|
6396
|
+
const exts = windows ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean) : [""];
|
|
6079
6397
|
for (const dir of dirs) {
|
|
6080
6398
|
if (!dir) continue;
|
|
6081
|
-
const
|
|
6082
|
-
|
|
6399
|
+
for (const ext of exts) {
|
|
6400
|
+
const candidate = join31(dir, command + ext);
|
|
6401
|
+
if (existsSync18(candidate)) return candidate;
|
|
6402
|
+
}
|
|
6083
6403
|
}
|
|
6084
6404
|
return void 0;
|
|
6085
6405
|
}
|
|
@@ -6090,7 +6410,7 @@ async function runProbe(opts) {
|
|
|
6090
6410
|
"No browser found to run the probe. Install one, or set JIG_CHROME to a Chrome, Chromium or Edge binary. A project with Playwright or Puppeteer already has one, and Jig will find it."
|
|
6091
6411
|
);
|
|
6092
6412
|
}
|
|
6093
|
-
const profile = mkdtempSync(
|
|
6413
|
+
const profile = mkdtempSync(join31(tmpdir(), "jig-probe-"));
|
|
6094
6414
|
const child = spawn(chrome, [
|
|
6095
6415
|
"--headless=new",
|
|
6096
6416
|
"--remote-debugging-port=0",
|
|
@@ -6134,6 +6454,9 @@ async function runProbe(opts) {
|
|
|
6134
6454
|
}
|
|
6135
6455
|
}
|
|
6136
6456
|
async function evaluate(endpoint, url, width, height, timeoutMs, expression) {
|
|
6457
|
+
if (typeof WebSocket === "undefined") {
|
|
6458
|
+
throw new Error(`rendering a page needs Node 22 or later (this is Node ${process.versions.node}). Upgrade Node, then run it again.`);
|
|
6459
|
+
}
|
|
6137
6460
|
const socket = new WebSocket(endpoint);
|
|
6138
6461
|
let nextId = 1;
|
|
6139
6462
|
const pending = /* @__PURE__ */ new Map();
|
|
@@ -6191,8 +6514,8 @@ async function evaluate(endpoint, url, width, height, timeoutMs, expression) {
|
|
|
6191
6514
|
|
|
6192
6515
|
// src/probe/serve.ts
|
|
6193
6516
|
import { createServer } from "http";
|
|
6194
|
-
import { existsSync as
|
|
6195
|
-
import { extname, join as
|
|
6517
|
+
import { existsSync as existsSync19, readFileSync as readFileSync26, statSync as statSync3 } from "fs";
|
|
6518
|
+
import { extname, join as join32, resolve as resolve3, sep as sep4 } from "path";
|
|
6196
6519
|
var TYPES = {
|
|
6197
6520
|
".html": "text/html; charset=utf-8",
|
|
6198
6521
|
".htm": "text/html; charset=utf-8",
|
|
@@ -6219,19 +6542,19 @@ async function serveDirectory(dir) {
|
|
|
6219
6542
|
const root = resolve3(dir);
|
|
6220
6543
|
const server = createServer((req, res) => {
|
|
6221
6544
|
let path = decodeURIComponent((req.url ?? "/").split("?")[0].split("#")[0]);
|
|
6222
|
-
let file = resolve3(
|
|
6545
|
+
let file = resolve3(join32(root, path));
|
|
6223
6546
|
if (file !== root && !file.startsWith(root + sep4)) {
|
|
6224
6547
|
res.writeHead(403).end();
|
|
6225
6548
|
return;
|
|
6226
6549
|
}
|
|
6227
|
-
if (
|
|
6228
|
-
else if (!
|
|
6229
|
-
if (!
|
|
6550
|
+
if (existsSync19(file) && statSync3(file).isDirectory()) file = join32(file, "index.html");
|
|
6551
|
+
else if (!existsSync19(file) && existsSync19(`${file}.html`)) file = `${file}.html`;
|
|
6552
|
+
if (!existsSync19(file)) {
|
|
6230
6553
|
res.writeHead(404).end();
|
|
6231
6554
|
return;
|
|
6232
6555
|
}
|
|
6233
6556
|
res.writeHead(200, { "content-type": TYPES[extname(file).toLowerCase()] ?? "application/octet-stream" });
|
|
6234
|
-
res.end(
|
|
6557
|
+
res.end(readFileSync26(file));
|
|
6235
6558
|
});
|
|
6236
6559
|
await new Promise((done) => server.listen(0, "127.0.0.1", done));
|
|
6237
6560
|
const address = server.address();
|
|
@@ -6243,8 +6566,8 @@ async function serveDirectory(dir) {
|
|
|
6243
6566
|
}
|
|
6244
6567
|
|
|
6245
6568
|
// src/probe/switches.ts
|
|
6246
|
-
import { readFileSync as
|
|
6247
|
-
import { join as
|
|
6569
|
+
import { readFileSync as readFileSync27 } from "fs";
|
|
6570
|
+
import { join as join33 } from "path";
|
|
6248
6571
|
var DECLARATION = /--breakpoint-([a-z0-9-]+)\s*:\s*(\d+(?:\.\d+)?)(px|rem)\b/g;
|
|
6249
6572
|
function declaredSwitches(projectRoot) {
|
|
6250
6573
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -6252,7 +6575,7 @@ function declaredSwitches(projectRoot) {
|
|
|
6252
6575
|
if (!/\.(css|scss|astro|vue|svelte|html)$/i.test(file)) continue;
|
|
6253
6576
|
let text;
|
|
6254
6577
|
try {
|
|
6255
|
-
text =
|
|
6578
|
+
text = readFileSync27(join33(projectRoot, file), "utf8");
|
|
6256
6579
|
} catch {
|
|
6257
6580
|
continue;
|
|
6258
6581
|
}
|
|
@@ -6287,18 +6610,18 @@ function saveProbe(opts) {
|
|
|
6287
6610
|
}
|
|
6288
6611
|
probe.pageFile = relative3(opts.projectRoot, page).split("\\").join("/");
|
|
6289
6612
|
if (opts.serveRoot) probe.serveRoot = relative3(opts.projectRoot, resolve4(opts.projectRoot, opts.serveRoot)).split("\\").join("/") || ".";
|
|
6290
|
-
probe.pageChecksum = checksum(
|
|
6613
|
+
probe.pageChecksum = checksum(readFileSync28(page, "utf8"));
|
|
6291
6614
|
probe.recordedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6292
|
-
const dir =
|
|
6615
|
+
const dir = join34(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
6293
6616
|
mkdirSync7(dir, { recursive: true });
|
|
6294
|
-
const path =
|
|
6617
|
+
const path = join34(dir, `probe-${width}.json`);
|
|
6295
6618
|
writeFileSync5(path, JSON.stringify(probe), "utf8");
|
|
6296
6619
|
return { path: `.jig/critique/${opts.surface}/probe-${width}.json`, width, page: probe.pageFile };
|
|
6297
6620
|
}
|
|
6298
6621
|
function projectFile(projectRoot, path) {
|
|
6299
6622
|
const abs = resolve4(projectRoot, path);
|
|
6300
6623
|
if (abs !== resolve4(projectRoot) && !abs.startsWith(resolve4(projectRoot) + sep5)) return void 0;
|
|
6301
|
-
return
|
|
6624
|
+
return existsSync20(abs) ? abs : void 0;
|
|
6302
6625
|
}
|
|
6303
6626
|
function pageFile(projectRoot, url) {
|
|
6304
6627
|
if (!url.startsWith("file://")) return void 0;
|
|
@@ -6310,12 +6633,12 @@ function pageFile(projectRoot, url) {
|
|
|
6310
6633
|
}
|
|
6311
6634
|
const abs = resolve4(path);
|
|
6312
6635
|
if (!abs.startsWith(resolve4(projectRoot))) return void 0;
|
|
6313
|
-
return
|
|
6636
|
+
return existsSync20(abs) ? abs : void 0;
|
|
6314
6637
|
}
|
|
6315
6638
|
var PROBE_WIDTHS = [360, 768, 1280, 1600];
|
|
6316
6639
|
async function runAndSaveProbes(opts) {
|
|
6317
6640
|
const abs = resolve4(opts.projectRoot, opts.page);
|
|
6318
|
-
if (!
|
|
6641
|
+
if (!existsSync20(abs)) throw new Error(`${opts.page} does not exist, so there is nothing to render.`);
|
|
6319
6642
|
const saved = [];
|
|
6320
6643
|
const widths = opts.widths ?? probeWidths(PROBE_WIDTHS, declaredSwitches(opts.projectRoot));
|
|
6321
6644
|
await withPageUrl(opts.projectRoot, abs, opts.serve, async (url) => {
|
|
@@ -6329,7 +6652,7 @@ async function runAndSaveProbes(opts) {
|
|
|
6329
6652
|
async function withPageUrl(projectRoot, abs, serve, use) {
|
|
6330
6653
|
if (!serve) return use(`file://${abs}`);
|
|
6331
6654
|
const root = resolve4(projectRoot, serve);
|
|
6332
|
-
if (!
|
|
6655
|
+
if (!existsSync20(root)) throw new Error(`${serve} does not exist, so there is nothing to serve.`);
|
|
6333
6656
|
if (!abs.startsWith(root + sep5)) throw new Error(`The page is not inside ${serve}, so serving ${serve} cannot show it.`);
|
|
6334
6657
|
const server = await serveDirectory(root);
|
|
6335
6658
|
try {
|
|
@@ -6340,31 +6663,31 @@ async function withPageUrl(projectRoot, abs, serve, use) {
|
|
|
6340
6663
|
}
|
|
6341
6664
|
}
|
|
6342
6665
|
function recordedPage(projectRoot, surface) {
|
|
6343
|
-
const dir =
|
|
6344
|
-
if (!
|
|
6666
|
+
const dir = join34(projectRoot, ".jig", "critique", surface);
|
|
6667
|
+
if (!existsSync20(dir)) return void 0;
|
|
6345
6668
|
for (const f of readdirSync12(dir).filter((name) => /^probe-\d+\.json$/.test(name)).sort()) {
|
|
6346
6669
|
try {
|
|
6347
|
-
const probe = JSON.parse(
|
|
6348
|
-
if (probe.pageFile &&
|
|
6670
|
+
const probe = JSON.parse(readFileSync28(join34(dir, f), "utf8"));
|
|
6671
|
+
if (probe.pageFile && existsSync20(resolve4(projectRoot, probe.pageFile))) return probe.pageFile;
|
|
6349
6672
|
} catch {
|
|
6350
6673
|
}
|
|
6351
6674
|
}
|
|
6352
6675
|
return void 0;
|
|
6353
6676
|
}
|
|
6354
6677
|
async function ensureProbes(opts) {
|
|
6355
|
-
const dir =
|
|
6678
|
+
const dir = join34(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
6356
6679
|
const abs = resolve4(opts.projectRoot, opts.page);
|
|
6357
6680
|
let current;
|
|
6358
6681
|
try {
|
|
6359
|
-
current = checksum(
|
|
6682
|
+
current = checksum(readFileSync28(abs, "utf8"));
|
|
6360
6683
|
} catch {
|
|
6361
6684
|
return { recorded: [], reason: `${opts.page} could not be read` };
|
|
6362
6685
|
}
|
|
6363
|
-
const recorded =
|
|
6686
|
+
const recorded = existsSync20(dir) ? readdirSync12(dir).map((f) => /^probe-(\d+)\.json$/.exec(f)?.[1]).filter((w) => !!w).map(Number) : [];
|
|
6364
6687
|
const widths = [.../* @__PURE__ */ new Set([...probeWidths(PROBE_WIDTHS, declaredSwitches(opts.projectRoot)), ...recorded])].sort((a, b) => a - b);
|
|
6365
6688
|
const missing = widths.filter((width) => {
|
|
6366
6689
|
try {
|
|
6367
|
-
const probe = JSON.parse(
|
|
6690
|
+
const probe = JSON.parse(readFileSync28(join34(dir, `probe-${width}.json`), "utf8"));
|
|
6368
6691
|
return probe.jigProbe !== PROBE_VERSION || probe.pageChecksum !== current;
|
|
6369
6692
|
} catch {
|
|
6370
6693
|
return true;
|
|
@@ -6375,7 +6698,7 @@ async function ensureProbes(opts) {
|
|
|
6375
6698
|
let serve;
|
|
6376
6699
|
for (const width of widths) {
|
|
6377
6700
|
try {
|
|
6378
|
-
const probe = JSON.parse(
|
|
6701
|
+
const probe = JSON.parse(readFileSync28(join34(dir, `probe-${width}.json`), "utf8"));
|
|
6379
6702
|
if (probe.serveRoot) {
|
|
6380
6703
|
serve = probe.serveRoot;
|
|
6381
6704
|
break;
|
|
@@ -6391,9 +6714,9 @@ async function ensureProbes(opts) {
|
|
|
6391
6714
|
return { recorded: missing };
|
|
6392
6715
|
}
|
|
6393
6716
|
function critiquedSurfaces(projectRoot) {
|
|
6394
|
-
const root =
|
|
6395
|
-
if (!
|
|
6396
|
-
return readdirSync12(root).filter((surface) => !surface.startsWith("_") && (
|
|
6717
|
+
const root = join34(projectRoot, ".jig", "critique");
|
|
6718
|
+
if (!existsSync20(root)) return [];
|
|
6719
|
+
return readdirSync12(root).filter((surface) => !surface.startsWith("_") && (existsSync20(join34(root, surface, "screen.json")) || existsSync20(join34(root, surface, "code.json"))));
|
|
6397
6720
|
}
|
|
6398
6721
|
|
|
6399
6722
|
// src/probe/check.ts
|
|
@@ -6405,7 +6728,7 @@ function stampProblem(projectRoot, file, p) {
|
|
|
6405
6728
|
if (!page) return `${file} names a page outside this project (${p.serveRoot ? p.pageFile : p.url ?? "no url"}).`;
|
|
6406
6729
|
let current;
|
|
6407
6730
|
try {
|
|
6408
|
-
current =
|
|
6731
|
+
current = readFileSync29(page, "utf8");
|
|
6409
6732
|
} catch {
|
|
6410
6733
|
return `${file} was taken on ${p.pageFile}, which no longer exists.`;
|
|
6411
6734
|
}
|
|
@@ -6415,11 +6738,11 @@ function stampProblem(projectRoot, file, p) {
|
|
|
6415
6738
|
return void 0;
|
|
6416
6739
|
}
|
|
6417
6740
|
function readProbes(projectRoot, dir, errors) {
|
|
6418
|
-
if (!
|
|
6741
|
+
if (!existsSync21(dir)) return [];
|
|
6419
6742
|
const probes = [];
|
|
6420
6743
|
for (const f of readdirSync13(dir).filter((n) => /^probe-\d+\.json$/.test(n)).sort()) {
|
|
6421
6744
|
try {
|
|
6422
|
-
const p = JSON.parse(
|
|
6745
|
+
const p = JSON.parse(readFileSync29(join35(dir, f), "utf8"));
|
|
6423
6746
|
if (typeof p.jigProbe === "number" && p.jigProbe < PROBE_VERSION && typeof p.width === "number") {
|
|
6424
6747
|
errors.push(`${f} was taken by an older \`jig probe\` (version ${p.jigProbe}; this Jig reads version ${PROBE_VERSION}). Record it again with \`jig probe --run\`.`);
|
|
6425
6748
|
continue;
|
|
@@ -6523,19 +6846,19 @@ function probeContradictions(probes, verdictOf, indexable = true) {
|
|
|
6523
6846
|
}
|
|
6524
6847
|
|
|
6525
6848
|
// src/check/decisions.ts
|
|
6526
|
-
import { existsSync as
|
|
6527
|
-
import { join as
|
|
6528
|
-
var
|
|
6849
|
+
import { existsSync as existsSync22, readFileSync as readFileSync30 } from "fs";
|
|
6850
|
+
import { join as join36, posix as posix3 } from "path";
|
|
6851
|
+
var HEADING4 = /^(#{2,3})\s+(.+?)\s*$/gm;
|
|
6529
6852
|
var NOT_A_DECISION = /^(unresolved|open questions?|undecided|contents?|index)$/i;
|
|
6530
6853
|
function decisionsFile(projectRoot) {
|
|
6531
6854
|
const candidates = ["jig/DECISIONS.md", "DECISIONS.md", "src/jig/DECISIONS.md", "src/styles/jig/DECISIONS.md", ".jig/DECISIONS.md"];
|
|
6532
6855
|
try {
|
|
6533
|
-
const brand = JSON.parse(
|
|
6856
|
+
const brand = JSON.parse(readFileSync30(join36(projectRoot, "jig.config.json"), "utf8")).brand;
|
|
6534
6857
|
if (typeof brand === "string" && brand.includes("/")) candidates.unshift(`${posix3.dirname(brand.replace(/^\.\//, ""))}/DECISIONS.md`);
|
|
6535
6858
|
} catch {
|
|
6536
6859
|
}
|
|
6537
6860
|
for (const candidate of candidates) {
|
|
6538
|
-
if (
|
|
6861
|
+
if (existsSync22(join36(projectRoot, candidate))) return candidate;
|
|
6539
6862
|
}
|
|
6540
6863
|
return void 0;
|
|
6541
6864
|
}
|
|
@@ -6548,11 +6871,11 @@ function decisionHeadings(projectRoot) {
|
|
|
6548
6871
|
if (!path) return headings;
|
|
6549
6872
|
let body;
|
|
6550
6873
|
try {
|
|
6551
|
-
body =
|
|
6874
|
+
body = readText(join36(projectRoot, path));
|
|
6552
6875
|
} catch {
|
|
6553
6876
|
return headings;
|
|
6554
6877
|
}
|
|
6555
|
-
for (const match of body.matchAll(
|
|
6878
|
+
for (const match of body.matchAll(HEADING4)) {
|
|
6556
6879
|
const name = match[2].replace(/[`*]/g, "").trim();
|
|
6557
6880
|
if (!name || NOT_A_DECISION.test(name)) continue;
|
|
6558
6881
|
if (!headings.has(name)) headings.set(name, match[0].trim());
|
|
@@ -6606,7 +6929,7 @@ function unsourcedReasons(current, before, opts = {}) {
|
|
|
6606
6929
|
}
|
|
6607
6930
|
return problems;
|
|
6608
6931
|
}
|
|
6609
|
-
function quotesNotFrom(current, before, words) {
|
|
6932
|
+
function quotesNotFrom(current, before, words, source = "the owner's words in tweak.json (`change`)") {
|
|
6610
6933
|
const problems = [];
|
|
6611
6934
|
for (const [name, whys] of changedWhys(current, before, true)) {
|
|
6612
6935
|
for (const why of whys) {
|
|
@@ -6614,7 +6937,7 @@ function quotesNotFrom(current, before, words) {
|
|
|
6614
6937
|
if (/inferred/i.test(label)) continue;
|
|
6615
6938
|
const missing = [...why.matchAll(/["“]([^"”]{3,})["”]/g)].map((m) => m[1]).filter((q) => !quoteHeld(q, words));
|
|
6616
6939
|
if (missing.length) {
|
|
6617
|
-
problems.push(`"${name}": its \`**${label}:**\` quotes "${missing[0]}", which
|
|
6940
|
+
problems.push(`"${name}": its \`**${label}:**\` quotes "${missing[0]}", which ${source} do not say. Quote the owner as they said it, or put your reading under \`**Why (inferred):**\`.`);
|
|
6618
6941
|
break;
|
|
6619
6942
|
}
|
|
6620
6943
|
}
|
|
@@ -6628,25 +6951,25 @@ function quoteHeld(quote, words) {
|
|
|
6628
6951
|
}
|
|
6629
6952
|
|
|
6630
6953
|
// src/check/spec-shape.ts
|
|
6631
|
-
import { existsSync as
|
|
6632
|
-
import { join as
|
|
6954
|
+
import { existsSync as existsSync23, readdirSync as readdirSync14, statSync as statSync4 } from "fs";
|
|
6955
|
+
import { join as join37 } from "path";
|
|
6633
6956
|
function newestSpec(projectRoot) {
|
|
6634
|
-
const dir =
|
|
6635
|
-
if (!
|
|
6957
|
+
const dir = join37(projectRoot, ".jig", "specs");
|
|
6958
|
+
if (!existsSync23(dir)) return void 0;
|
|
6636
6959
|
const files = readdirSync14(dir).filter((f) => f.endsWith(".md"));
|
|
6637
6960
|
if (files.length === 0) return void 0;
|
|
6638
|
-
const newest = files.map((f) => ({ f, at: statSync4(
|
|
6639
|
-
return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body:
|
|
6961
|
+
const newest = files.map((f) => ({ f, at: statSync4(join37(dir, f)).mtimeMs })).sort((a, b) => b.at - a.at)[0].f;
|
|
6962
|
+
return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body: readText(join37(dir, newest)) };
|
|
6640
6963
|
}
|
|
6641
6964
|
function specFor(projectRoot, surface) {
|
|
6642
6965
|
if (surface) {
|
|
6643
|
-
const path =
|
|
6644
|
-
if (
|
|
6966
|
+
const path = join37(projectRoot, ".jig", "specs", `${surface}.spec.md`);
|
|
6967
|
+
if (existsSync23(path)) return { path: `.jig/specs/${surface}.spec.md`, slug: surface, body: readText(path) };
|
|
6645
6968
|
}
|
|
6646
6969
|
return newestSpec(projectRoot);
|
|
6647
6970
|
}
|
|
6648
6971
|
function specProblems(spec) {
|
|
6649
|
-
const parts = spec.body.split(/^---\s*$/m);
|
|
6972
|
+
const parts = lf(spec.body).split(/^---\s*$/m);
|
|
6650
6973
|
const front = parts.length >= 3 ? parts[1] : "";
|
|
6651
6974
|
if (!front.trim()) {
|
|
6652
6975
|
return [`${spec.path} has no frontmatter. A spec is the frontmatter \u2014 feature, surface, mode, sizes (phone, tablet, desktop), states, decisions, later, mockup, confirmed \u2014 with the reasoning below it. Rewrite it in that shape.`];
|
|
@@ -6660,6 +6983,18 @@ function specProblems(spec) {
|
|
|
6660
6983
|
if (critique !== void 0 && !/^(each|at-ship)$/i.test(critique)) {
|
|
6661
6984
|
problems.push(`${spec.path}: \`critique: ${critique}\` is neither \`each\` nor \`at-ship\`. Leave it out to follow the project's default.`);
|
|
6662
6985
|
}
|
|
6986
|
+
const motion = /^motion\s*:[ \t]*(.*)\n?((?:[ \t]+-.*\n?)*)/im.exec(front);
|
|
6987
|
+
if (motion) {
|
|
6988
|
+
const inline = motion[1].replace(/\s+#.*$/, "").trim();
|
|
6989
|
+
const items = motion[2].split("\n").map((l) => l.replace(/^[ \t]+-\s*/, "").trim()).filter(Boolean);
|
|
6990
|
+
if (!items.length && !/^none\b(\s*$|\s*[—–:,;(-])/i.test(inline)) {
|
|
6991
|
+
problems.push(`${spec.path}: \`motion:\` is neither \`none\` nor a list. List each movement as \`- <what moves>: <how>, on <trigger>; tells <what the reader learns>\`, or write \`motion: none\`.`);
|
|
6992
|
+
}
|
|
6993
|
+
const unsaid = items.filter((i) => !/\bon\b/i.test(i) || !/\btells?\b/i.test(i));
|
|
6994
|
+
if (unsaid.length) {
|
|
6995
|
+
problems.push(`${spec.path}: ${unsaid.length === 1 ? "a movement in `motion:` does" : `${unsaid.length} movements in \`motion:\` do`} not say what triggers it and what it tells the reader ("${unsaid[0]}"). Write \`on <trigger>; tells <what>\`, or cut the movement.`);
|
|
6996
|
+
}
|
|
6997
|
+
}
|
|
6663
6998
|
for (const field of ["feature", "surface", "mode", "sizes", "confirmed", "mockup"]) {
|
|
6664
6999
|
if (!has(field)) problems.push(`${spec.path} frontmatter has no \`${field}:\`.`);
|
|
6665
7000
|
}
|
|
@@ -6723,6 +7058,41 @@ function specIndexableField(front) {
|
|
|
6723
7058
|
if (value === "false" || value === "no") return false;
|
|
6724
7059
|
return "unreadable";
|
|
6725
7060
|
}
|
|
7061
|
+
var SPEC_FIELDS = [
|
|
7062
|
+
"feature",
|
|
7063
|
+
"surface",
|
|
7064
|
+
"route",
|
|
7065
|
+
"mode",
|
|
7066
|
+
"indexable",
|
|
7067
|
+
"title",
|
|
7068
|
+
"description",
|
|
7069
|
+
"sizes",
|
|
7070
|
+
"switches",
|
|
7071
|
+
"states",
|
|
7072
|
+
"motion",
|
|
7073
|
+
"decisions",
|
|
7074
|
+
"later",
|
|
7075
|
+
"mockup",
|
|
7076
|
+
"mockup_at",
|
|
7077
|
+
"deviations",
|
|
7078
|
+
"critique",
|
|
7079
|
+
"superseded_by",
|
|
7080
|
+
"confirmed"
|
|
7081
|
+
];
|
|
7082
|
+
var topLevelKeys = (body) => [...(body.split(/^---\s*$/m)[1] ?? "").matchAll(/^([A-Za-z_][\w-]*)\s*:/gm)].map((m) => m[1]);
|
|
7083
|
+
function newFieldProblems(spec, then) {
|
|
7084
|
+
spec = { ...spec, body: lf(spec.body) };
|
|
7085
|
+
then = lf(then);
|
|
7086
|
+
const had = new Set(topLevelKeys(then));
|
|
7087
|
+
const known = new Set(SPEC_FIELDS);
|
|
7088
|
+
const added = [...new Set(topLevelKeys(spec.body))].filter((k) => !known.has(k) && !had.has(k));
|
|
7089
|
+
if (!added.length) return [];
|
|
7090
|
+
const near = (k) => SPEC_FIELDS.find((f) => k.includes(f) || f.startsWith(k.replace(/[-_].*$/, "")));
|
|
7091
|
+
return added.map((k) => {
|
|
7092
|
+
const hint = near(k);
|
|
7093
|
+
return `${spec.path}: \`${k}:\` is not a field Jig reads, so nothing checks what it says. ` + (hint ? `If it is \`${hint}:\`, write it there. ` : "") + `Otherwise put it in the prose below the front matter. Jig's fields: ${SPEC_FIELDS.join(", ")}.`;
|
|
7094
|
+
});
|
|
7095
|
+
}
|
|
6726
7096
|
|
|
6727
7097
|
// src/commands/verdicts.ts
|
|
6728
7098
|
var VERDICTS = ["ok", "finding", "n/a"];
|
|
@@ -6749,19 +7119,19 @@ function reasonProblems(file, judged, errors) {
|
|
|
6749
7119
|
}
|
|
6750
7120
|
}
|
|
6751
7121
|
function readJson(path, errors) {
|
|
6752
|
-
if (!
|
|
7122
|
+
if (!existsSync24(path)) return null;
|
|
6753
7123
|
try {
|
|
6754
|
-
return JSON.parse(
|
|
7124
|
+
return JSON.parse(readFileSync32(path, "utf8"));
|
|
6755
7125
|
} catch (e) {
|
|
6756
7126
|
errors.push(`${path}: not valid JSON (${e.message})`);
|
|
6757
7127
|
return {};
|
|
6758
7128
|
}
|
|
6759
7129
|
}
|
|
6760
7130
|
function specIndexable(projectRoot, surface) {
|
|
6761
|
-
const path =
|
|
7131
|
+
const path = join38(projectRoot, ".jig", "specs", `${surface}.spec.md`);
|
|
6762
7132
|
let front = "";
|
|
6763
7133
|
try {
|
|
6764
|
-
front =
|
|
7134
|
+
front = readText(path).split(/^---\s*$/m)[1] ?? "";
|
|
6765
7135
|
} catch {
|
|
6766
7136
|
}
|
|
6767
7137
|
const declared = specIndexableField(front);
|
|
@@ -6770,13 +7140,17 @@ function specIndexable(projectRoot, surface) {
|
|
|
6770
7140
|
return mode !== "product" && mode !== "operator";
|
|
6771
7141
|
}
|
|
6772
7142
|
function specNeedsNav(projectRoot, surface) {
|
|
6773
|
-
const path =
|
|
6774
|
-
if (!
|
|
6775
|
-
const front =
|
|
7143
|
+
const path = join38(projectRoot, ".jig", "specs", `${surface}.spec.md`);
|
|
7144
|
+
if (!existsSync24(path)) return false;
|
|
7145
|
+
const front = readText(path).split(/^---\s*$/m)[1] ?? "";
|
|
6776
7146
|
const navField = [...front.matchAll(/^\s*nav:\s*(.+)$/gim)].some((m) => !/^\s*(none|n\/a|-)\b/i.test(m[1]));
|
|
6777
7147
|
const navRegion = /^\s*-\s*(nav|navigation)\s*:/im.test(front);
|
|
6778
7148
|
return navField || navRegion;
|
|
6779
7149
|
}
|
|
7150
|
+
function unreadFields(label, file, known) {
|
|
7151
|
+
if (!file || typeof file !== "object") return [];
|
|
7152
|
+
return Object.entries(file).filter(([k, v]) => !known.includes(k) && v !== null && typeof v === "object" && Object.keys(v).length > 0).map(([k]) => `${label}: \`${k}\` is not a field Jig reads, so nothing counts what it holds. A rule's verdict goes in \`verdicts\`; what no rule names, in \`differences\`; a note, in the report.`);
|
|
7153
|
+
}
|
|
6780
7154
|
function checkArm(name, file, required, otherPass, extraAllowed, extraRequired, errors, decisionList = []) {
|
|
6781
7155
|
const total = required.length + extraRequired.length;
|
|
6782
7156
|
if (file === null) {
|
|
@@ -6803,7 +7177,7 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
|
|
|
6803
7177
|
seen.add(id);
|
|
6804
7178
|
const pass = otherPass.get(id);
|
|
6805
7179
|
if (!required.includes(id) && !extraAllowed.has(id)) {
|
|
6806
|
-
errors.push(pass === "mechanical" ? `${name}.json: ${id} is a mechanical rule \u2014 \`jig check\` decides it, so it has no verdict here. Remove it.` : pass ? `${name}.json: ${id} is a pass: ${pass} rule \u2014 it belongs to the other arm.` : `${name}.json: ${written} is not a rule or spec in this corpus. Run \`jig explain ${written}\`; an id that does not resolve is not a verdict.`);
|
|
7180
|
+
errors.push(pass === "mechanical" ? `${name}.json: ${id} is a mechanical rule \u2014 \`jig check\` decides it, so it has no verdict here. Remove it.` : pass ? `${name}.json: ${id} is a pass: ${pass} rule \u2014 it belongs to the other arm.` : `${name}.json: ${written} is not a rule or spec in this corpus. Run \`jig explain ${written}\`; an id that does not resolve is not a verdict. A difference from the spec, or from a source the page quotes, is not a rule: list it under \`differences\` in the same file.`);
|
|
6807
7181
|
continue;
|
|
6808
7182
|
}
|
|
6809
7183
|
if (typeof v.verdict !== "string" || !RULE_VERDICTS.includes(v.verdict)) {
|
|
@@ -6824,6 +7198,17 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
|
|
|
6824
7198
|
if (v.verdict === "finding") findings++;
|
|
6825
7199
|
}
|
|
6826
7200
|
reasonProblems(`${name}.json`, reasons, errors);
|
|
7201
|
+
const differences = file.differences;
|
|
7202
|
+
if (differences !== void 0) {
|
|
7203
|
+
if (!Array.isArray(differences)) errors.push(`${name}.json: \`differences\` is not a list.`);
|
|
7204
|
+
else for (const d of differences) {
|
|
7205
|
+
const said = (x) => typeof x === "string" && x.trim() !== "";
|
|
7206
|
+
if (!said(d?.what) || !said(d?.where) || !said(d?.against)) {
|
|
7207
|
+
errors.push(`${name}.json: a difference needs \`what\` (what differs), \`where\` (the page's file and line) and \`against\` (the spec line or source it differs from).`);
|
|
7208
|
+
} else findings++;
|
|
7209
|
+
}
|
|
7210
|
+
}
|
|
7211
|
+
errors.push(...unreadFields(`${name}.json`, file, ["verdicts", "differences", "rendered", "artefacts"]));
|
|
6827
7212
|
const missing = [...required, ...extraRequired].filter((id) => !seen.has(id));
|
|
6828
7213
|
if (missing.length) {
|
|
6829
7214
|
errors.push(`${name}.json: ${missing.length} of ${total} ids have no verdict: ${missing.join(", ")}. Re-run the arm; never report a short pass.`);
|
|
@@ -6834,7 +7219,7 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
|
|
|
6834
7219
|
function checkDecisions(projectRoot, dir, errors) {
|
|
6835
7220
|
const required = decisionNames(projectRoot);
|
|
6836
7221
|
if (required.length === 0) return { state: "ran", judged: 0, total: 0, findings: 0 };
|
|
6837
|
-
const file = readJson(
|
|
7222
|
+
const file = readJson(join38(dir, "decisions.json"), errors);
|
|
6838
7223
|
if (!file) {
|
|
6839
7224
|
errors.push(
|
|
6840
7225
|
`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.`
|
|
@@ -6843,6 +7228,7 @@ function checkDecisions(projectRoot, dir, errors) {
|
|
|
6843
7228
|
}
|
|
6844
7229
|
const list = Array.isArray(file.verdicts) ? file.verdicts : [];
|
|
6845
7230
|
if (!Array.isArray(file.verdicts)) errors.push('decisions.json has no "verdicts" array.');
|
|
7231
|
+
errors.push(...unreadFields("decisions.json", file, ["verdicts"]));
|
|
6846
7232
|
const seen = /* @__PURE__ */ new Set();
|
|
6847
7233
|
const reasons = [];
|
|
6848
7234
|
let findings = 0;
|
|
@@ -6863,7 +7249,7 @@ function checkDecisions(projectRoot, dir, errors) {
|
|
|
6863
7249
|
}
|
|
6864
7250
|
seen.add(match);
|
|
6865
7251
|
if (typeof v.verdict !== "string" || !VERDICTS.includes(v.verdict)) {
|
|
6866
|
-
errors.push(`decisions.json: "${match}" has verdict ${JSON.stringify(v.verdict)} \u2014 it must be ok, finding or n/a.`);
|
|
7252
|
+
errors.push(v.verdict === "ruled" ? `decisions.json: "${match}" is marked ruled. A decision is not excused by a decision: judge whether the page follows it, ok, finding or n/a. \`ruled\` is for a rule in screen.json or code.json that a decision overrides.` : `decisions.json: "${match}" has verdict ${JSON.stringify(v.verdict)} \u2014 it must be ok, finding or n/a.`);
|
|
6867
7253
|
continue;
|
|
6868
7254
|
}
|
|
6869
7255
|
const reason = typeof v.reason === "string" ? v.reason.trim() : "";
|
|
@@ -6888,7 +7274,7 @@ function decisionsSince(projectRoot, dir, unjudged) {
|
|
|
6888
7274
|
if (!path) return [];
|
|
6889
7275
|
const git = (args) => execFileSync3("git", args, { cwd: projectRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
6890
7276
|
try {
|
|
6891
|
-
const verdictFile = relative4(projectRoot,
|
|
7277
|
+
const verdictFile = relative4(projectRoot, join38(dir, "decisions.json")).split(sep6).join("/");
|
|
6892
7278
|
if (git(["status", "--porcelain", "--", verdictFile])) return [];
|
|
6893
7279
|
const judgedIn = git(["log", "-1", "--format=%H", "--", verdictFile]);
|
|
6894
7280
|
if (!judgedIn) return [];
|
|
@@ -6912,7 +7298,7 @@ function decisionsSince(projectRoot, dir, unjudged) {
|
|
|
6912
7298
|
function verifyVerdicts(opts) {
|
|
6913
7299
|
const root = opts.packageRoot ?? assetRoot();
|
|
6914
7300
|
const errors = [];
|
|
6915
|
-
const index = JSON.parse(
|
|
7301
|
+
const index = JSON.parse(readFileSync32(join38(root, "rules.index.json"), "utf8"));
|
|
6916
7302
|
const judgment = index.filter((r) => r.bucket === "judgment" || r.bucket === "hybrid" && r.pass !== void 0);
|
|
6917
7303
|
const screenIds = judgment.filter((r) => r.pass === "screen").map((r) => r.id);
|
|
6918
7304
|
const codeIds = judgment.filter((r) => r.pass === "code").map((r) => r.id);
|
|
@@ -6921,9 +7307,9 @@ function verifyVerdicts(opts) {
|
|
|
6921
7307
|
...judgment.map((r) => [r.id, r.pass ?? ""])
|
|
6922
7308
|
]);
|
|
6923
7309
|
const specIds = new Set(citableIds(root).filter((id) => /^[PMLRT]-\d+$/.test(id)));
|
|
6924
|
-
const dir =
|
|
6925
|
-
const screenFile = readJson(
|
|
6926
|
-
const codeFile = readJson(
|
|
7310
|
+
const dir = join38(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
7311
|
+
const screenFile = readJson(join38(dir, "screen.json"), errors);
|
|
7312
|
+
const codeFile = readJson(join38(dir, "code.json"), errors);
|
|
6927
7313
|
const screenExtraRequired = specNeedsNav(opts.projectRoot, opts.surface) ? ["P-14"] : [];
|
|
6928
7314
|
const decisionList = decisionNames(opts.projectRoot);
|
|
6929
7315
|
const screen = checkArm("screen", screenFile, screenIds, passOf, specIds, screenExtraRequired, errors, decisionList);
|
|
@@ -6931,7 +7317,7 @@ function verifyVerdicts(opts) {
|
|
|
6931
7317
|
let rendered = false;
|
|
6932
7318
|
if (screenFile && screenFile.rendered === true) {
|
|
6933
7319
|
const artefacts = Array.isArray(screenFile.artefacts) ? screenFile.artefacts.filter((a) => typeof a === "string") : [];
|
|
6934
|
-
const absent = artefacts.filter((a) => !
|
|
7320
|
+
const absent = artefacts.filter((a) => !existsSync24(resolve5(opts.projectRoot, a)));
|
|
6935
7321
|
if (artefacts.length === 0) errors.push("screen.json says rendered: true but lists no artefacts. A render leaves a screenshot.");
|
|
6936
7322
|
else if (absent.length) errors.push(`screen.json says rendered: true, but these artefacts do not exist: ${absent.join(", ")}.`);
|
|
6937
7323
|
else rendered = true;
|
|
@@ -6952,7 +7338,7 @@ function verifyVerdicts(opts) {
|
|
|
6952
7338
|
};
|
|
6953
7339
|
let specFront = "";
|
|
6954
7340
|
try {
|
|
6955
|
-
specFront =
|
|
7341
|
+
specFront = readText(join38(opts.projectRoot, ".jig", "specs", `${opts.surface}.spec.md`)).split(/^---\s*$/m)[1] ?? "";
|
|
6956
7342
|
} catch {
|
|
6957
7343
|
}
|
|
6958
7344
|
if (specIndexableField(specFront) === "unreadable") {
|
|
@@ -6987,7 +7373,7 @@ function previousFindings(projectRoot, dir) {
|
|
|
6987
7373
|
const out = /* @__PURE__ */ new Map();
|
|
6988
7374
|
let text;
|
|
6989
7375
|
try {
|
|
6990
|
-
text = at ? git(["show", `${at}:./${rel}/${f}`]) :
|
|
7376
|
+
text = at ? lf(git(["show", `${at}:./${rel}/${f}`])) : readText(join38(dir, f));
|
|
6991
7377
|
} catch {
|
|
6992
7378
|
return out;
|
|
6993
7379
|
}
|
|
@@ -7069,18 +7455,17 @@ function sourceChanges(git, from, to) {
|
|
|
7069
7455
|
}
|
|
7070
7456
|
|
|
7071
7457
|
// src/commands/gate.ts
|
|
7072
|
-
import { existsSync as
|
|
7458
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync8, readdirSync as readdirSync16, readFileSync as readFileSync36, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
7073
7459
|
import { createHash as createHash3 } from "crypto";
|
|
7074
|
-
import { join as
|
|
7460
|
+
import { join as join43 } from "path";
|
|
7075
7461
|
|
|
7076
7462
|
// src/check/mockup-drawing.ts
|
|
7077
|
-
import {
|
|
7078
|
-
import { join as join38 } from "path";
|
|
7463
|
+
import { join as join39 } from "path";
|
|
7079
7464
|
var FRAMES = [["phone", 360], ["tablet", 768], ["desktop", 1280], ["wide", 1600]];
|
|
7080
7465
|
function mockupDrawingProblems(root, specBody, at) {
|
|
7081
7466
|
let html;
|
|
7082
7467
|
try {
|
|
7083
|
-
html =
|
|
7468
|
+
html = readText(join39(root, at));
|
|
7084
7469
|
} catch {
|
|
7085
7470
|
return [];
|
|
7086
7471
|
}
|
|
@@ -7141,7 +7526,7 @@ function frameRegionProblems(frames, specBody, at) {
|
|
|
7141
7526
|
function approvedDrawingProblems(root, specBody, at) {
|
|
7142
7527
|
let html;
|
|
7143
7528
|
try {
|
|
7144
|
-
html =
|
|
7529
|
+
html = readText(join39(root, at));
|
|
7145
7530
|
} catch {
|
|
7146
7531
|
return [];
|
|
7147
7532
|
}
|
|
@@ -7247,12 +7632,12 @@ function sizeBlock2(sizes, size) {
|
|
|
7247
7632
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
7248
7633
|
|
|
7249
7634
|
// src/commands/ship.ts
|
|
7250
|
-
import { existsSync as
|
|
7251
|
-
import { join as
|
|
7635
|
+
import { existsSync as existsSync25, readdirSync as readdirSync15, readFileSync as readFileSync34 } from "fs";
|
|
7636
|
+
import { join as join41 } from "path";
|
|
7252
7637
|
|
|
7253
7638
|
// src/commands/seo.ts
|
|
7254
|
-
import { readFileSync as
|
|
7255
|
-
import { join as
|
|
7639
|
+
import { readFileSync as readFileSync33 } from "fs";
|
|
7640
|
+
import { join as join40 } from "path";
|
|
7256
7641
|
var SITEMAP_FILE = /(^|\/)sitemap(-index)?\.(xml|ts|js|mjs|tsx|jsx|rb|php|py)$/i;
|
|
7257
7642
|
var ROBOTS_FILE = /(^|\/)robots\.(txt|ts|js|mjs|tsx|jsx)$/i;
|
|
7258
7643
|
var ROUTE_IN_SITEMAP = /(?:<loc>\s*([^<\s]+)\s*<\/loc>)|(?:url\s*:\s*["'`]([^"'`]+)["'`])/gi;
|
|
@@ -7293,7 +7678,7 @@ function seo(opts) {
|
|
|
7293
7678
|
for (const file of files) {
|
|
7294
7679
|
let raw;
|
|
7295
7680
|
try {
|
|
7296
|
-
raw =
|
|
7681
|
+
raw = readFileSync33(join40(root, file), "utf8");
|
|
7297
7682
|
} catch {
|
|
7298
7683
|
continue;
|
|
7299
7684
|
}
|
|
@@ -7358,7 +7743,7 @@ function critiqueAtShip(projectRoot, specBody) {
|
|
|
7358
7743
|
const own = specCritique(specBody);
|
|
7359
7744
|
if (own) return own === "at-ship";
|
|
7360
7745
|
try {
|
|
7361
|
-
return JSON.parse(
|
|
7746
|
+
return JSON.parse(readFileSync34(join41(projectRoot, "jig.config.json"), "utf8")).critique === "at-ship";
|
|
7362
7747
|
} catch {
|
|
7363
7748
|
return false;
|
|
7364
7749
|
}
|
|
@@ -7366,14 +7751,14 @@ function critiqueAtShip(projectRoot, specBody) {
|
|
|
7366
7751
|
function tweakDeferred(dir) {
|
|
7367
7752
|
let t;
|
|
7368
7753
|
try {
|
|
7369
|
-
t = JSON.parse(
|
|
7754
|
+
t = JSON.parse(readFileSync34(join41(dir, "tweak.json"), "utf8"));
|
|
7370
7755
|
} catch {
|
|
7371
7756
|
return false;
|
|
7372
7757
|
}
|
|
7373
7758
|
if (t.deferred === void 0 || t.deferred === false) return false;
|
|
7374
7759
|
let judged;
|
|
7375
7760
|
try {
|
|
7376
|
-
judged = JSON.parse(
|
|
7761
|
+
judged = JSON.parse(readFileSync34(join41(dir, "verdicts.lock"), "utf8")).tweak;
|
|
7377
7762
|
} catch {
|
|
7378
7763
|
}
|
|
7379
7764
|
return judged !== t.at;
|
|
@@ -7384,27 +7769,37 @@ function pageChecksum(html) {
|
|
|
7384
7769
|
function changedSinceLock(projectRoot, dir) {
|
|
7385
7770
|
let lock;
|
|
7386
7771
|
try {
|
|
7387
|
-
lock = JSON.parse(
|
|
7772
|
+
lock = JSON.parse(readFileSync34(join41(dir, "verdicts.lock"), "utf8"));
|
|
7388
7773
|
} catch {
|
|
7389
7774
|
return void 0;
|
|
7390
7775
|
}
|
|
7391
7776
|
const file = lock.page?.file;
|
|
7392
7777
|
if (!file || !lock.page?.checksum) return void 0;
|
|
7393
7778
|
try {
|
|
7394
|
-
const html =
|
|
7395
|
-
|
|
7779
|
+
const html = readFileSync34(join41(projectRoot, file), "utf8");
|
|
7780
|
+
const seen = [pageChecksum(html), checksum(html)];
|
|
7781
|
+
return seen.includes(lock.page.checksum) ? void 0 : file;
|
|
7396
7782
|
} catch {
|
|
7397
7783
|
return void 0;
|
|
7398
7784
|
}
|
|
7399
7785
|
}
|
|
7786
|
+
function supersededBy(front) {
|
|
7787
|
+
const named = /^\s*superseded_by\s*:\s*(.*)$/im.exec(front)?.[1]?.replace(/\s+#.*$/, "").trim();
|
|
7788
|
+
return named ? named : void 0;
|
|
7789
|
+
}
|
|
7400
7790
|
function pageStatus(projectRoot, surface) {
|
|
7401
|
-
const spec =
|
|
7791
|
+
const spec = readText(join41(projectRoot, ".jig", "specs", `${surface}.spec.md`));
|
|
7402
7792
|
const front = spec.split(/^---\s*$/m)[1] ?? "";
|
|
7403
7793
|
if (!/^\s*confirmed\s*:\s*true\b/im.test(front)) {
|
|
7404
7794
|
return { surface, state: "in-progress", detail: "its spec is not confirmed, so nothing of it is built to ship" };
|
|
7405
7795
|
}
|
|
7406
|
-
const
|
|
7407
|
-
if (
|
|
7796
|
+
const successor = supersededBy(front);
|
|
7797
|
+
if (successor !== void 0) {
|
|
7798
|
+
const file = successor.replace(/(\.spec\.md)?$/, ".spec.md");
|
|
7799
|
+
return existsSync25(join41(projectRoot, ".jig", "specs", file)) ? { surface, state: "superseded", detail: `superseded by ${file}, whose critique judges its page` } : { surface, state: "incomplete", detail: `\`superseded_by: ${successor}\` names a spec that does not exist in .jig/specs` };
|
|
7800
|
+
}
|
|
7801
|
+
const dir = join41(projectRoot, ".jig", "critique", surface);
|
|
7802
|
+
if (!existsSync25(join41(dir, "screen.json")) && !existsSync25(join41(dir, "code.json"))) {
|
|
7408
7803
|
return { surface, state: "never", detail: "never critiqued" };
|
|
7409
7804
|
}
|
|
7410
7805
|
if (tweakDeferred(dir)) return { surface, state: "deferred", detail: "a tweak deferred its re-judge" };
|
|
@@ -7414,7 +7809,7 @@ function pageStatus(projectRoot, surface) {
|
|
|
7414
7809
|
const stale = v.errors.find((e) => /was taken (on|by) an older/.test(e));
|
|
7415
7810
|
let judgedPage = false;
|
|
7416
7811
|
try {
|
|
7417
|
-
judgedPage = !!JSON.parse(
|
|
7812
|
+
judgedPage = !!JSON.parse(readFileSync34(join41(dir, "verdicts.lock"), "utf8")).page?.checksum;
|
|
7418
7813
|
} catch {
|
|
7419
7814
|
}
|
|
7420
7815
|
if (stale && !judgedPage) return { surface, state: "changed", detail: "the page changed after its probes were taken" };
|
|
@@ -7430,17 +7825,19 @@ function ship(opts) {
|
|
|
7430
7825
|
const mechanicalErrors = mechanical.findings.filter((f) => f.bucket === "mechanical" && f.severity === "error").length;
|
|
7431
7826
|
const seoResult = seo({ projectRoot: root });
|
|
7432
7827
|
const seoErrors = seoResult.findings.filter((f) => f.severity === "error").length;
|
|
7433
|
-
const specsDir =
|
|
7434
|
-
const surfaces =
|
|
7828
|
+
const specsDir = join41(root, ".jig", "specs");
|
|
7829
|
+
const surfaces = existsSync25(specsDir) ? readdirSync15(specsDir).filter((f) => f.endsWith(".spec.md") && !f.startsWith("_")).map((f) => f.replace(/\.spec\.md$/, "")).sort() : [];
|
|
7435
7830
|
const pages = surfaces.map((s) => pageStatus(root, s));
|
|
7436
|
-
const owed = pages.filter((p) => p.state !== "judged" && p.state !== "in-progress");
|
|
7437
|
-
const
|
|
7831
|
+
const owed = pages.filter((p) => p.state !== "judged" && p.state !== "in-progress" && p.state !== "superseded");
|
|
7832
|
+
const agentFiles = agentFileProblems(root);
|
|
7833
|
+
const ready = mechanicalErrors === 0 && seoErrors === 0 && owed.length === 0 && agentFiles.length === 0;
|
|
7438
7834
|
const count = (state) => pages.filter((p) => p.state === state).length;
|
|
7439
|
-
const line = `JIG_SHIP: ready=${ready ? "yes" : "no"} mechanical=${mechanicalErrors} seo=${seoErrors} pages=${pages.length} judged=${count("judged")} owed=${owed.length} in-progress=${count("in-progress")}`;
|
|
7440
|
-
const mark = { judged: "\u2713", never: "\u2717", changed: "\u2717", reprobe: "\u2717", deferred: "\u2717", incomplete: "\u2717", findings: "\u2717", "in-progress": "\xB7" };
|
|
7835
|
+
const line = `JIG_SHIP: ready=${ready ? "yes" : "no"} mechanical=${mechanicalErrors} seo=${seoErrors} pages=${pages.length} judged=${count("judged")} owed=${owed.length} in-progress=${count("in-progress")} superseded=${count("superseded")}`;
|
|
7836
|
+
const mark = { judged: "\u2713", never: "\u2717", changed: "\u2717", reprobe: "\u2717", deferred: "\u2717", incomplete: "\u2717", findings: "\u2717", "in-progress": "\xB7", superseded: "\xB7" };
|
|
7441
7837
|
const report2 = [
|
|
7442
7838
|
` ${mechanicalErrors === 0 ? "\u2713" : "\u2717"} check --all --ci: ${mechanicalErrors} mechanical error${mechanicalErrors === 1 ? "" : "s"}`,
|
|
7443
7839
|
` ${seoErrors === 0 ? "\u2713" : "\u2717"} seo: ${seoErrors} error${seoErrors === 1 ? "" : "s"}`,
|
|
7840
|
+
...agentFiles.map((p) => ` \u2717 ${p}`),
|
|
7444
7841
|
...pages.map((p) => ` ${mark[p.state]} ${p.surface}: ${p.detail}`),
|
|
7445
7842
|
owed.length ? ` Critique each page marked \u2717 (\`/jig critique <page>\`); fix what it finds, or record the owner's ruling, and run \`jig ship\` again.` : "",
|
|
7446
7843
|
` Not checked by Jig, here or anywhere: ${NOT_COVERED}.`,
|
|
@@ -7450,11 +7847,37 @@ function ship(opts) {
|
|
|
7450
7847
|
}
|
|
7451
7848
|
|
|
7452
7849
|
// src/check/spec-checked.ts
|
|
7453
|
-
import { existsSync as
|
|
7850
|
+
import { existsSync as existsSync26, readFileSync as readFileSync35 } from "fs";
|
|
7454
7851
|
import { createHash as createHash2 } from "crypto";
|
|
7455
|
-
import { join as
|
|
7852
|
+
import { join as join42 } from "path";
|
|
7853
|
+
|
|
7854
|
+
// src/check/owner-word.ts
|
|
7855
|
+
var SAYS = {
|
|
7856
|
+
confirm: /\b(confirm(s|ed)?|yes|yep|approve[ds]?|looks (good|right|fine)|lgtm|go ahead|agreed|that['’]?s right|correct|ok(ay)?)\b|^\s*go\b/im,
|
|
7857
|
+
approve: /\b(approve[ds]?|looks (good|right|fine)|lgtm|yes|go ahead|good to go|confirm(s|ed)?|agreed|build it|that['’]?s (it|right)|ok(ay)?)\b/i,
|
|
7858
|
+
skip: /\bskip|\bno (mockup|drawing)|without (a )?(mockup|drawing)|(don['’]?t|do not|no need to) (draw|mock)|(mockup|drawing) (isn['’]?t|is not) needed/i,
|
|
7859
|
+
defer: /\b(later|at ship|defer(red)?|wait|leave (it|the critique|the re-?judge)|(don['’]?t|do not) (re-?judge|critique)|no (re-?judge|critique)|skip the (re-?judge|critique))\b/i
|
|
7860
|
+
};
|
|
7861
|
+
function saysSo(word, text) {
|
|
7862
|
+
return SAYS[word].test(text);
|
|
7863
|
+
}
|
|
7864
|
+
var WHAT = {
|
|
7865
|
+
confirm: "confirm the spec",
|
|
7866
|
+
approve: "approve the drawing",
|
|
7867
|
+
skip: "skip the mockup",
|
|
7868
|
+
defer: "leave the re-judge for later"
|
|
7869
|
+
};
|
|
7870
|
+
function ownerWordProblem(word, quoted, owner, where) {
|
|
7871
|
+
const q = quoted.replace(/^["“]|["”]$/g, "").trim();
|
|
7872
|
+
if (!q) return `${where} records the owner's word to ${WHAT[word]} without their words. Quote what they said.`;
|
|
7873
|
+
if (!quoteHeld(q, owner)) return `${where} quotes "${q}", which the owner did not say in this session. Quote their reply as they wrote it; a word nobody gave is not one.`;
|
|
7874
|
+
if (!saysSo(word, q)) return `${where} quotes "${q}", which does not ${WHAT[word]}. Words the owner said about something else are not their word on this: ask them, and quote the answer.`;
|
|
7875
|
+
return void 0;
|
|
7876
|
+
}
|
|
7877
|
+
|
|
7878
|
+
// src/check/spec-checked.ts
|
|
7456
7879
|
function specChecksum(body) {
|
|
7457
|
-
return createHash2("sha256").update(body.replace(/^\s*(confirmed|mockup)\s*:.*$/gim, "")).digest("hex");
|
|
7880
|
+
return createHash2("sha256").update(body.replace(/\r\n/g, "\n").replace(/^\s*(confirmed|mockup)\s*:.*$/gim, "")).digest("hex");
|
|
7458
7881
|
}
|
|
7459
7882
|
var mockupLine = (body) => /^\s*mockup\s*:\s*(.*)$/im.exec(body.split(/^---\s*$/m)[1] ?? "")?.[1]?.trim() ?? "";
|
|
7460
7883
|
function mockupWordProblems(spec, then, owner) {
|
|
@@ -7467,10 +7890,8 @@ function mockupWordProblems(spec, then, owner) {
|
|
|
7467
7890
|
if (!quoted) {
|
|
7468
7891
|
return [`${spec.path}: \`mockup: ${word}\` is recorded without the owner's words. Write \`mockup: ${word} \u2014 "<what they said>"\`, quoting the reply that ${word === "approved" ? "approved it" : "said to skip it"}.`];
|
|
7469
7892
|
}
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
}
|
|
7473
|
-
return [];
|
|
7893
|
+
const problem = ownerWordProblem(word === "approved" ? "approve" : "skip", quoted, owner, `${spec.path}: \`mockup: ${word}\``);
|
|
7894
|
+
return problem ? [problem] : [];
|
|
7474
7895
|
}
|
|
7475
7896
|
function mockupPending(body) {
|
|
7476
7897
|
const now = mockupLine(body);
|
|
@@ -7505,7 +7926,7 @@ function specCheckProblems(root, spec, then, owner, decisions) {
|
|
|
7505
7926
|
const recordPath = `.jig/specs/${spec.slug}.checked.json`;
|
|
7506
7927
|
let record;
|
|
7507
7928
|
try {
|
|
7508
|
-
record = JSON.parse(
|
|
7929
|
+
record = JSON.parse(readFileSync35(join42(root, recordPath), "utf8"));
|
|
7509
7930
|
} catch {
|
|
7510
7931
|
problems.push(
|
|
7511
7932
|
`${recordPath} is missing. Before the owner is asked to confirm, a reader who did not write the spec checks its quotations against the owner's words and its facts against their sources, and writes that record (step 3c). The owner checks the spec by the sheet it gives them.`
|
|
@@ -7534,16 +7955,16 @@ function specCheckProblems(root, spec, then, owner, decisions) {
|
|
|
7534
7955
|
if (unsaid.length) problems.push(`${recordPath}: ${unsaid.length} unchecked ${unsaid.length === 1 ? "fact says" : "facts say"} nothing of why. Say in \`note\` why the source could not be opened; the owner opens it instead.`);
|
|
7535
7956
|
const missing = facts.map((f) => typeof f?.source === "string" ? f.source.trim() : "").filter((s) => s && !/^[a-z][a-z0-9+.-]*:\/\//i.test(s)).filter((s) => {
|
|
7536
7957
|
const [, file, line] = /^(.+?)(?::(\d+)(?:-\d+)?)?$/.exec(s) ?? [];
|
|
7537
|
-
if (!file || !
|
|
7958
|
+
if (!file || !existsSync26(join42(root, file))) return true;
|
|
7538
7959
|
if (!line) return false;
|
|
7539
7960
|
try {
|
|
7540
|
-
return Number(line) >
|
|
7961
|
+
return Number(line) > readFileSync35(join42(root, file), "utf8").split("\n").length;
|
|
7541
7962
|
} catch {
|
|
7542
7963
|
return true;
|
|
7543
7964
|
}
|
|
7544
7965
|
});
|
|
7545
7966
|
if (missing.length) {
|
|
7546
|
-
problems.push(`${recordPath} cites ${missing.length === 1 ? "a source" : "sources"} the project does not have: ${missing.slice(0, 4).join(", ")}. A fact is checked at a source that exists
|
|
7967
|
+
problems.push(`${recordPath} cites ${missing.length === 1 ? "a source" : "sources"} the project does not have: ${missing.slice(0, 4).join(", ")}. A fact is checked at a source that exists: \`source\` is a path from the project root, \`path:line\`, or a URL, and nothing else; what you found there goes in \`note\`.`);
|
|
7547
7968
|
}
|
|
7548
7969
|
return problems;
|
|
7549
7970
|
}
|
|
@@ -7551,10 +7972,10 @@ function specCheckProblems(root, spec, then, owner, decisions) {
|
|
|
7551
7972
|
// src/commands/gate.ts
|
|
7552
7973
|
var MAX_BLOCKS = 3;
|
|
7553
7974
|
function lastJigInvocation(transcriptPath) {
|
|
7554
|
-
if (!transcriptPath || !
|
|
7975
|
+
if (!transcriptPath || !existsSync27(transcriptPath)) return void 0;
|
|
7555
7976
|
let text;
|
|
7556
7977
|
try {
|
|
7557
|
-
text =
|
|
7978
|
+
text = readFileSync36(transcriptPath, "utf8");
|
|
7558
7979
|
} catch {
|
|
7559
7980
|
return void 0;
|
|
7560
7981
|
}
|
|
@@ -7579,10 +8000,10 @@ function lastJigInvocation(transcriptPath) {
|
|
|
7579
8000
|
return found;
|
|
7580
8001
|
}
|
|
7581
8002
|
function ownerWords(transcriptPath) {
|
|
7582
|
-
if (!transcriptPath || !
|
|
8003
|
+
if (!transcriptPath || !existsSync27(transcriptPath)) return "";
|
|
7583
8004
|
const said = [];
|
|
7584
8005
|
try {
|
|
7585
|
-
for (const line of
|
|
8006
|
+
for (const line of readFileSync36(transcriptPath, "utf8").split("\n")) {
|
|
7586
8007
|
if (!line.includes('"user"')) continue;
|
|
7587
8008
|
let entry;
|
|
7588
8009
|
try {
|
|
@@ -7602,11 +8023,46 @@ function ownerWords(transcriptPath) {
|
|
|
7602
8023
|
}
|
|
7603
8024
|
return said.join("\n");
|
|
7604
8025
|
}
|
|
8026
|
+
function namedSpec(root, surface) {
|
|
8027
|
+
if (!surface) return specFor(root, void 0);
|
|
8028
|
+
return existsSync27(join43(root, ".jig", "specs", `${surface}.spec.md`)) ? specFor(root, surface) : void 0;
|
|
8029
|
+
}
|
|
8030
|
+
function isConfirmed(body) {
|
|
8031
|
+
return /^\s*confirmed\s*:\s*true\b/im.test(body.split(/^---\s*$/m)[1] ?? "");
|
|
8032
|
+
}
|
|
8033
|
+
function ownerReplyAfter(transcriptPath, asked) {
|
|
8034
|
+
if (!transcriptPath || !existsSync27(transcriptPath)) return void 0;
|
|
8035
|
+
const turns = [];
|
|
8036
|
+
try {
|
|
8037
|
+
for (const line of readFileSync36(transcriptPath, "utf8").split("\n")) {
|
|
8038
|
+
let entry;
|
|
8039
|
+
try {
|
|
8040
|
+
entry = JSON.parse(line);
|
|
8041
|
+
} catch {
|
|
8042
|
+
continue;
|
|
8043
|
+
}
|
|
8044
|
+
if (entry.type !== "user" && entry.type !== "assistant" || entry.isMeta) continue;
|
|
8045
|
+
const content = entry.message?.content;
|
|
8046
|
+
const texts = typeof content === "string" ? [content] : Array.isArray(content) ? content.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text) : [];
|
|
8047
|
+
for (const text of texts) turns.push({ who: entry.type === "user" ? "owner" : "agent", text });
|
|
8048
|
+
}
|
|
8049
|
+
} catch {
|
|
8050
|
+
return void 0;
|
|
8051
|
+
}
|
|
8052
|
+
let answer;
|
|
8053
|
+
turns.forEach((t, i) => {
|
|
8054
|
+
if (t.who !== "agent" || !asked.test(t.text)) return;
|
|
8055
|
+
const reply = [];
|
|
8056
|
+
for (let j = i + 1; j < turns.length && turns[j].who === "owner"; j++) reply.push(turns[j].text);
|
|
8057
|
+
if (reply.length) answer = reply.join("\n");
|
|
8058
|
+
});
|
|
8059
|
+
return answer;
|
|
8060
|
+
}
|
|
7605
8061
|
function lastAssistantText(transcriptPath) {
|
|
7606
|
-
if (!transcriptPath || !
|
|
8062
|
+
if (!transcriptPath || !existsSync27(transcriptPath)) return void 0;
|
|
7607
8063
|
let found;
|
|
7608
8064
|
try {
|
|
7609
|
-
for (const line of
|
|
8065
|
+
for (const line of readFileSync36(transcriptPath, "utf8").split("\n")) {
|
|
7610
8066
|
if (!line.includes('"assistant"')) continue;
|
|
7611
8067
|
let entry;
|
|
7612
8068
|
try {
|
|
@@ -7630,9 +8086,9 @@ function asksOwner(text) {
|
|
|
7630
8086
|
}
|
|
7631
8087
|
var ASKS_THE_OWNER = /* @__PURE__ */ new Set(["decide", "spec", "mockup", "tweak"]);
|
|
7632
8088
|
function sessionStart(transcriptPath) {
|
|
7633
|
-
if (!transcriptPath || !
|
|
8089
|
+
if (!transcriptPath || !existsSync27(transcriptPath)) return void 0;
|
|
7634
8090
|
try {
|
|
7635
|
-
for (const line of
|
|
8091
|
+
for (const line of readFileSync36(transcriptPath, "utf8").split("\n")) {
|
|
7636
8092
|
const m = /"timestamp"\s*:\s*"([^"]+)"/.exec(line);
|
|
7637
8093
|
if (m) {
|
|
7638
8094
|
const t = Date.parse(m[1]);
|
|
@@ -7647,20 +8103,20 @@ function verdictsMtime(dir) {
|
|
|
7647
8103
|
let newest = 0;
|
|
7648
8104
|
for (const name of VERDICT_FILES) {
|
|
7649
8105
|
try {
|
|
7650
|
-
newest = Math.max(newest, statSync5(
|
|
8106
|
+
newest = Math.max(newest, statSync5(join43(dir, name)).mtimeMs);
|
|
7651
8107
|
} catch {
|
|
7652
8108
|
}
|
|
7653
8109
|
}
|
|
7654
8110
|
return newest;
|
|
7655
8111
|
}
|
|
7656
8112
|
function surfacesInPlay(root, command, transcriptPath, surface) {
|
|
7657
|
-
const critiqueDir =
|
|
7658
|
-
if (!
|
|
8113
|
+
const critiqueDir = join43(root, ".jig", "critique");
|
|
8114
|
+
if (!existsSync27(critiqueDir)) return [];
|
|
7659
8115
|
const all = readdirSync16(critiqueDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("_") && !d.name.startsWith(".")).map((d) => d.name);
|
|
7660
8116
|
const start = sessionStart(transcriptPath);
|
|
7661
8117
|
if (start === void 0) return all;
|
|
7662
8118
|
const current = command === "critique" || command === "tweak" ? specFor(root, surface)?.slug : void 0;
|
|
7663
|
-
return all.filter((s) => s === current || verdictsMtime(
|
|
8119
|
+
return all.filter((s) => s === current || verdictsMtime(join43(critiqueDir, s)) >= start - 1e3);
|
|
7664
8120
|
}
|
|
7665
8121
|
function surfacesToProbe(root, input) {
|
|
7666
8122
|
const invocation = lastJigInvocation(input.transcript_path);
|
|
@@ -7668,9 +8124,13 @@ function surfacesToProbe(root, input) {
|
|
|
7668
8124
|
}
|
|
7669
8125
|
function fileAtSessionStart(root, path, start) {
|
|
7670
8126
|
const git = (args) => execFileSync4("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
8127
|
+
try {
|
|
8128
|
+
if (start !== void 0 && statSync5(join43(root, path)).mtimeMs < start - 1e3) return readText(join43(root, path));
|
|
8129
|
+
} catch {
|
|
8130
|
+
}
|
|
7671
8131
|
try {
|
|
7672
8132
|
const base = start === void 0 ? "HEAD" : git(["rev-list", "-1", `--before=@${Math.floor(start / 1e3)}`, "HEAD"]).trim();
|
|
7673
|
-
return base ? git(["show", `${base}:./${path}`]) : "";
|
|
8133
|
+
return base ? lf(git(["show", `${base}:./${path}`])) : "";
|
|
7674
8134
|
} catch {
|
|
7675
8135
|
return "";
|
|
7676
8136
|
}
|
|
@@ -7683,7 +8143,7 @@ function changedOutsideRecords(root, start) {
|
|
|
7683
8143
|
const changed = git(["diff", "--name-only", base, "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean);
|
|
7684
8144
|
const created = git(["ls-files", "--others", "--exclude-standard", "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean).filter((f) => {
|
|
7685
8145
|
try {
|
|
7686
|
-
return start === void 0 || statSync5(
|
|
8146
|
+
return start === void 0 || statSync5(join43(root, f)).mtimeMs >= start - 1e3;
|
|
7687
8147
|
} catch {
|
|
7688
8148
|
return false;
|
|
7689
8149
|
}
|
|
@@ -7698,8 +8158,8 @@ function drawingChangedAfterApproval(root, specPath, at) {
|
|
|
7698
8158
|
try {
|
|
7699
8159
|
const approvedIn = git(["log", "-1", "--format=%H", "-G", "^mockup[[:space:]]*:[[:space:]]*approved", "--", specPath]).trim();
|
|
7700
8160
|
if (!approvedIn) return void 0;
|
|
7701
|
-
const then = git(["show", `${approvedIn}:./${at}`]);
|
|
7702
|
-
return then !==
|
|
8161
|
+
const then = lf(git(["show", `${approvedIn}:./${at}`]));
|
|
8162
|
+
return then !== readText(join43(root, at)) ? approvedIn.slice(0, 7) : void 0;
|
|
7703
8163
|
} catch {
|
|
7704
8164
|
return void 0;
|
|
7705
8165
|
}
|
|
@@ -7711,18 +8171,25 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
7711
8171
|
const found = decisionsFile(root);
|
|
7712
8172
|
if (!found) problems.push("decide wrote no DECISIONS.md beside the token layer.");
|
|
7713
8173
|
else {
|
|
7714
|
-
const body =
|
|
8174
|
+
const body = readText(join43(root, found));
|
|
7715
8175
|
if (!/^##\s+Unresolved\s*$/im.test(body)) {
|
|
7716
8176
|
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.`");
|
|
7717
8177
|
}
|
|
7718
8178
|
if (/\[TODO\]/.test(body)) problems.push("DECISIONS.md still contains [TODO] markers.");
|
|
7719
|
-
|
|
8179
|
+
const then = fileAtSessionStart(root, found, start);
|
|
8180
|
+
problems.push(...unsourcedReasons(body, then, { newWhysOnly: true }).map((p) => `DECISIONS.md: ${p}`));
|
|
8181
|
+
if (owner) problems.push(...quotesNotFrom(body, then, owner, "the owner's words in this session").map((p) => `DECISIONS.md: ${p}`));
|
|
7720
8182
|
}
|
|
7721
8183
|
}
|
|
8184
|
+
if (command === "spec" && surface && !existsSync27(join43(root, ".jig", "specs", `${surface}.spec.md`))) {
|
|
8185
|
+
problems.push(`spec wrote no .jig/specs/${surface}.spec.md. The spec is that file: a draft anywhere else is read by no later step and checked by nothing.`);
|
|
8186
|
+
return problems;
|
|
8187
|
+
}
|
|
7722
8188
|
if (command === "spec" || command === "mockup" || command === "make" || command === "critique" || command === "tweak") {
|
|
7723
8189
|
if (!spec) problems.push(`${command} needs a spec: there is no file in .jig/specs/.`);
|
|
7724
8190
|
else {
|
|
7725
8191
|
problems.push(...specProblems(spec));
|
|
8192
|
+
problems.push(...newFieldProblems(spec, fileAtSessionStart(root, spec.path, start)));
|
|
7726
8193
|
if (problems.length === 0) problems.push(...navProblems(spec));
|
|
7727
8194
|
}
|
|
7728
8195
|
}
|
|
@@ -7737,12 +8204,12 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
7737
8204
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
7738
8205
|
if (/^approved/i.test(mockup)) {
|
|
7739
8206
|
if (!at) problems.push(`${spec.path}: \`mockup_at:\` is empty. Record where the approved drawing is.`);
|
|
7740
|
-
else if (!/^https?:/i.test(at) && !
|
|
8207
|
+
else if (!/^https?:/i.test(at) && !existsSync27(join43(root, at))) problems.push(`${spec.path}: \`mockup_at: ${at}\` does not exist.`);
|
|
7741
8208
|
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.`);
|
|
7742
8209
|
}
|
|
7743
|
-
if (at && /\.html?$/i.test(at) &&
|
|
8210
|
+
if (at && /\.html?$/i.test(at) && existsSync27(join43(root, at))) problems.push(...mockupDrawingProblems(root, spec.body, at));
|
|
7744
8211
|
}
|
|
7745
|
-
if (command === "tweak" && spec) problems.push(...tweakProblems(root, spec, owner));
|
|
8212
|
+
if (command === "tweak" && spec) problems.push(...tweakProblems(root, spec, owner, start));
|
|
7746
8213
|
if ((command === "spec" || command === "make") && spec) {
|
|
7747
8214
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
7748
8215
|
const mockup = /^\s*mockup\s*:\s*(\S+)/im.exec(front)?.[1] ?? "";
|
|
@@ -7768,7 +8235,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
7768
8235
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
7769
8236
|
const mockup = /^\s*mockup\s*:\s*(\S+)/im.exec(front)?.[1] ?? "";
|
|
7770
8237
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
7771
|
-
if (/^approved/i.test(mockup) && at && !/^https?:/i.test(at) &&
|
|
8238
|
+
if (/^approved/i.test(mockup) && at && !/^https?:/i.test(at) && existsSync27(join43(root, at))) {
|
|
7772
8239
|
const changedAfter = drawingChangedAfterApproval(root, spec.path, at);
|
|
7773
8240
|
if (changedAfter) {
|
|
7774
8241
|
problems.push(`${at} has changed since the owner approved it (${changedAfter}). An approval is of the drawing as the owner saw it: set \`mockup: pending\` in ${spec.path} and ask again.`);
|
|
@@ -7780,7 +8247,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
7780
8247
|
if (found) {
|
|
7781
8248
|
let now = "";
|
|
7782
8249
|
try {
|
|
7783
|
-
now =
|
|
8250
|
+
now = readText(join43(root, found));
|
|
7784
8251
|
} catch {
|
|
7785
8252
|
}
|
|
7786
8253
|
const then = fileAtSessionStart(root, found, start);
|
|
@@ -7794,13 +8261,13 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
7794
8261
|
if (found) {
|
|
7795
8262
|
let now = "";
|
|
7796
8263
|
try {
|
|
7797
|
-
now =
|
|
8264
|
+
now = readText(join43(root, found));
|
|
7798
8265
|
} catch {
|
|
7799
8266
|
}
|
|
7800
8267
|
const then = fileAtSessionStart(root, found, start);
|
|
7801
8268
|
if (now && now !== then) {
|
|
7802
8269
|
problems.push(...unsourcedReasons(now, then, { newWhysOnly: true }).map((p) => `${found}: ${p}`));
|
|
7803
|
-
const change = readTweak(
|
|
8270
|
+
const change = readTweak(join43(root, ".jig", "critique", spec.slug))?.change;
|
|
7804
8271
|
if (change) problems.push(...quotesNotFrom(now, then, change).map((p) => `${found}: ${p}`));
|
|
7805
8272
|
}
|
|
7806
8273
|
}
|
|
@@ -7809,7 +8276,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
7809
8276
|
for (const file of modeFiles(root)) {
|
|
7810
8277
|
let now = "";
|
|
7811
8278
|
try {
|
|
7812
|
-
now =
|
|
8279
|
+
now = readText(join43(root, file));
|
|
7813
8280
|
} catch {
|
|
7814
8281
|
continue;
|
|
7815
8282
|
}
|
|
@@ -7820,8 +8287,8 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
7820
8287
|
}
|
|
7821
8288
|
}
|
|
7822
8289
|
if (command === "critique") {
|
|
7823
|
-
const dir =
|
|
7824
|
-
const surfaces =
|
|
8290
|
+
const dir = join43(root, ".jig", "critique");
|
|
8291
|
+
const surfaces = existsSync27(dir) ? readdirSync16(dir).filter((s) => existsSync27(join43(dir, s, "screen.json")) || existsSync27(join43(dir, s, "code.json"))) : [];
|
|
7825
8292
|
for (const surface2 of surfaces) {
|
|
7826
8293
|
const v = verifyVerdicts({ projectRoot: root, surface: surface2 });
|
|
7827
8294
|
if (v.ok && v.decisions.state !== "ran" && v.decisions.total > 0) {
|
|
@@ -7832,7 +8299,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
7832
8299
|
}
|
|
7833
8300
|
}
|
|
7834
8301
|
const own = specFor(root, surface)?.slug;
|
|
7835
|
-
if (own && surfaces.includes(own)) problems.push(...reportOmissions(
|
|
8302
|
+
if (own && surfaces.includes(own)) problems.push(...reportOmissions(join43(dir, own), own));
|
|
7836
8303
|
if (surfaces.length === 0) {
|
|
7837
8304
|
problems.push("critique wrote no verdict files. Each reader arm writes .jig/critique/<surface>/screen.json or code.json, and `jig verdicts <surface>` \u2014 not your own count \u2014 decides whether the review is complete. A report without them is not a review.");
|
|
7838
8305
|
}
|
|
@@ -7845,7 +8312,7 @@ function drawingBeforeAsking(root, surface) {
|
|
|
7845
8312
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
7846
8313
|
const named = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
7847
8314
|
const at = named || `.jig/mockups/${spec.slug}.html`;
|
|
7848
|
-
if (/^https?:/i.test(at) || !/\.html?$/i.test(at) || !
|
|
8315
|
+
if (/^https?:/i.test(at) || !/\.html?$/i.test(at) || !existsSync27(join43(root, at))) return [];
|
|
7849
8316
|
const found = mockupDrawingProblems(root, spec.body, at);
|
|
7850
8317
|
return found.length ? [`Fix the drawing before you put it to the owner; they approve what the spec lists, drawn. ${found.join(" ")}`] : [];
|
|
7851
8318
|
}
|
|
@@ -7853,14 +8320,14 @@ function specChecked(root, spec, start, owner) {
|
|
|
7853
8320
|
const found = decisionsFile(root);
|
|
7854
8321
|
let decisions = "";
|
|
7855
8322
|
try {
|
|
7856
|
-
if (found) decisions =
|
|
8323
|
+
if (found) decisions = readText(join43(root, found));
|
|
7857
8324
|
} catch {
|
|
7858
8325
|
}
|
|
7859
8326
|
return specCheckProblems(root, spec, fileAtSessionStart(root, spec.path, start), owner, decisions);
|
|
7860
8327
|
}
|
|
7861
8328
|
function modeFiles(root) {
|
|
7862
8329
|
try {
|
|
7863
|
-
const state = JSON.parse(
|
|
8330
|
+
const state = JSON.parse(readFileSync36(join43(root, ".jig", "state.json"), "utf8"));
|
|
7864
8331
|
return Object.keys(state.files ?? {}).filter((f) => /(^|\/)mode\.[\w-]+\.css$/.test(f));
|
|
7865
8332
|
} catch {
|
|
7866
8333
|
return [];
|
|
@@ -7869,7 +8336,7 @@ function modeFiles(root) {
|
|
|
7869
8336
|
function reportOmissions(dir, surface) {
|
|
7870
8337
|
let report2;
|
|
7871
8338
|
try {
|
|
7872
|
-
report2 =
|
|
8339
|
+
report2 = readText(join43(dir, "REPORT.md")).toLowerCase();
|
|
7873
8340
|
} catch {
|
|
7874
8341
|
return [];
|
|
7875
8342
|
}
|
|
@@ -7877,7 +8344,7 @@ function reportOmissions(dir, surface) {
|
|
|
7877
8344
|
for (const f of VERDICT_FILES) {
|
|
7878
8345
|
let file;
|
|
7879
8346
|
try {
|
|
7880
|
-
file = JSON.parse(
|
|
8347
|
+
file = JSON.parse(readFileSync36(join43(dir, f), "utf8"));
|
|
7881
8348
|
} catch {
|
|
7882
8349
|
continue;
|
|
7883
8350
|
}
|
|
@@ -7890,7 +8357,7 @@ function reportOmissions(dir, surface) {
|
|
|
7890
8357
|
if (!missing.length) return [];
|
|
7891
8358
|
return [`.jig/critique/${surface}/REPORT.md leaves out ${missing.length === 1 ? "a finding" : `${missing.length} findings`} the verdict files hold: ${missing.slice(0, 8).join(", ")}. The report is what the owner and \`make\` read; name every finding in it, by id (a decision by its name).`];
|
|
7892
8359
|
}
|
|
7893
|
-
function tweakProblems(root, spec, owner = "") {
|
|
8360
|
+
function tweakProblems(root, spec, owner = "", start) {
|
|
7894
8361
|
const problems = [];
|
|
7895
8362
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
7896
8363
|
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\`.`);
|
|
@@ -7898,21 +8365,31 @@ function tweakProblems(root, spec, owner = "") {
|
|
|
7898
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\`.`);
|
|
7899
8366
|
else problems.push(...structureSinceApproval(root, spec, front));
|
|
7900
8367
|
const surface = spec.slug;
|
|
7901
|
-
const record = readTweak(
|
|
8368
|
+
const record = readTweak(join43(root, ".jig", "critique", surface));
|
|
7902
8369
|
if (!record) {
|
|
7903
8370
|
problems.push(`.jig/critique/${surface}/tweak.json is missing. It names the change in the owner's words (\`change\`), when it was made (\`at\`) and the rule ids and decisions re-judged for it (\`ids\`).`);
|
|
7904
8371
|
} else {
|
|
7905
8372
|
if (!record.change) problems.push(`.jig/critique/${surface}/tweak.json has no \`change\`: the owner's words for what changed.`);
|
|
8373
|
+
else if (owner && fileAtSessionStart(root, `.jig/critique/${surface}/tweak.json`, start) !== readText(join43(root, ".jig", "critique", surface, "tweak.json")) && !quoteHeld(record.change.replace(/^["“]|["”]$/g, ""), owner)) {
|
|
8374
|
+
problems.push(`.jig/critique/${surface}/tweak.json: \`change\` is "${record.change.length > 90 ? `${record.change.slice(0, 90)}\u2026` : record.change}", which the owner did not say in this session. It is their words for the change, as they gave them; what you made of them goes in the spec's Tweak entry.`);
|
|
8375
|
+
}
|
|
8376
|
+
try {
|
|
8377
|
+
const raw = JSON.parse(readFileSync36(join43(root, ".jig", "critique", surface, "tweak.json"), "utf8"));
|
|
8378
|
+
const extra = Object.keys(raw).filter((k) => !["at", "change", "ids", "deferred"].includes(k));
|
|
8379
|
+
if (extra.length) problems.push(`.jig/critique/${surface}/tweak.json: ${extra.map((k) => `\`${k}\``).join(", ")} ${extra.length === 1 ? "is not a field" : "are not fields"} Jig reads. tweak.json holds \`at\`, \`change\`, \`ids\` and \`deferred\`; what else you have to say goes in the spec's Tweak entry.`);
|
|
8380
|
+
} catch {
|
|
8381
|
+
}
|
|
7906
8382
|
if (!record.ids.length) problems.push(`.jig/critique/${surface}/tweak.json names nothing to re-judge. A change that no rule and no decision could see needs no tweak; name the ones it can.`);
|
|
7907
8383
|
if (record.deferred !== void 0) {
|
|
7908
|
-
|
|
8384
|
+
const said = typeof record.deferred === "string" ? ownerWordProblem("defer", record.deferred, owner, `.jig/critique/${surface}/tweak.json: \`deferred\``) : void 0;
|
|
8385
|
+
if (typeof record.deferred === "string" ? said !== void 0 : !critiqueAtShip(root, spec.body)) {
|
|
7909
8386
|
problems.push(
|
|
7910
|
-
`.jig/critique/${surface}/tweak.json defers its re-judge, and nobody said to. \`deferred\` quotes the owner telling you to leave it for later, or is \`true\` where the page's spec says \`critique: at-ship\` (or, the spec silent, jig.config.json says \`"critique": "at-ship"\`). Otherwise re-judge what it names.`
|
|
8387
|
+
`.jig/critique/${surface}/tweak.json defers its re-judge, and nobody said to. \`deferred\` quotes the owner telling you to leave it for later, or is \`true\` where the page's spec says \`critique: at-ship\` (or, the spec silent, jig.config.json says \`"critique": "at-ship"\`). Otherwise re-judge what it names.` + (said ? ` ${said}` : "")
|
|
7911
8388
|
);
|
|
7912
8389
|
}
|
|
7913
8390
|
return problems;
|
|
7914
8391
|
}
|
|
7915
|
-
const unjudged = unjudgedTweakIds(
|
|
8392
|
+
const unjudged = unjudgedTweakIds(join43(root, ".jig", "critique", surface), record);
|
|
7916
8393
|
if (unjudged.length) {
|
|
7917
8394
|
problems.push(
|
|
7918
8395
|
`.jig/critique/${surface}: tweak.json names ${unjudged.slice(0, 6).join(", ")}${unjudged.length > 6 ? " and more" : ""} but no verdict for ${unjudged.length === 1 ? "it" : "them"} carries \`"tweak": "${record.at}"\`. Each named verdict is re-judged on the changed page by a reader that did not make the change.`
|
|
@@ -7939,7 +8416,7 @@ function structureSinceApproval(root, spec, front) {
|
|
|
7939
8416
|
const problems = [];
|
|
7940
8417
|
let thenBody = "";
|
|
7941
8418
|
try {
|
|
7942
|
-
thenBody = git(["show", `${baseline}:./${spec.path}`]);
|
|
8419
|
+
thenBody = lf(git(["show", `${baseline}:./${spec.path}`]));
|
|
7943
8420
|
} catch {
|
|
7944
8421
|
return [];
|
|
7945
8422
|
}
|
|
@@ -7948,9 +8425,9 @@ function structureSinceApproval(root, spec, front) {
|
|
|
7948
8425
|
problems.push(`${spec.path}: the regions under \`sizes:\` differ from the ones the page had ${when}. A change to what the page holds is not a tweak: take it through \`spec\` and \`mockup\`.`);
|
|
7949
8426
|
}
|
|
7950
8427
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
|
|
7951
|
-
if (at && !/^https?:/i.test(at) &&
|
|
8428
|
+
if (at && !/^https?:/i.test(at) && existsSync27(join43(root, at))) {
|
|
7952
8429
|
try {
|
|
7953
|
-
if (git(["show", `${baseline}:./${at}`]) !==
|
|
8430
|
+
if (lf(git(["show", `${baseline}:./${at}`])) !== readText(join43(root, at))) {
|
|
7954
8431
|
problems.push(`${at} has changed since ${when.replace(/^when /, "")}. A tweak leaves the drawing as approved; a change the drawing must show goes through \`mockup\`.`);
|
|
7955
8432
|
}
|
|
7956
8433
|
} catch {
|
|
@@ -7960,7 +8437,7 @@ function structureSinceApproval(root, spec, front) {
|
|
|
7960
8437
|
}
|
|
7961
8438
|
function readTweak(dir) {
|
|
7962
8439
|
try {
|
|
7963
|
-
const raw = JSON.parse(
|
|
8440
|
+
const raw = JSON.parse(readFileSync36(join43(dir, "tweak.json"), "utf8"));
|
|
7964
8441
|
const deferred = typeof raw.deferred === "string" || raw.deferred === true ? raw.deferred : void 0;
|
|
7965
8442
|
return { at: String(raw.at ?? ""), change: String(raw.change ?? "").trim(), ids: Array.isArray(raw.ids) ? raw.ids.map(String) : [], ...deferred !== void 0 ? { deferred } : {} };
|
|
7966
8443
|
} catch {
|
|
@@ -7973,20 +8450,20 @@ function surfacePage(projectRoot, surface) {
|
|
|
7973
8450
|
const declared = /^\s*surface\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
|
|
7974
8451
|
const candidates = [declared, `${surface}.html`, declared ? `${declared.replace(/^\//, "")}.html` : void 0].filter((c) => !!c && /\.\w+$/.test(c) === (c === declared ? /\.\w+$/.test(c) : true));
|
|
7975
8452
|
for (const candidate of candidates) {
|
|
7976
|
-
if (candidate &&
|
|
8453
|
+
if (candidate && existsSync27(join43(projectRoot, candidate))) return candidate;
|
|
7977
8454
|
}
|
|
7978
8455
|
return void 0;
|
|
7979
8456
|
}
|
|
7980
8457
|
function gate(opts) {
|
|
7981
8458
|
const root = opts.projectRoot;
|
|
7982
|
-
if (!
|
|
8459
|
+
if (!existsSync27(join43(root, "jig.config.json")) && !existsSync27(join43(root, ".jig"))) {
|
|
7983
8460
|
return { block: false, reason: "" };
|
|
7984
8461
|
}
|
|
7985
8462
|
const invocation = lastJigInvocation(opts.input.transcript_path);
|
|
7986
8463
|
const command = invocation?.command;
|
|
7987
8464
|
const waiting = command !== void 0 && ASKS_THE_OWNER.has(command) && asksOwner(lastAssistantText(opts.input.transcript_path));
|
|
7988
8465
|
const start = sessionStart(opts.input.transcript_path);
|
|
7989
|
-
const owner = command === "spec" || command === "mockup" || command === "make" || command === "tweak" ? ownerWords(opts.input.transcript_path) : "";
|
|
8466
|
+
const owner = command === "decide" || command === "spec" || command === "mockup" || command === "make" || command === "tweak" ? ownerWords(opts.input.transcript_path) : "";
|
|
7990
8467
|
const problems = command && !waiting ? commandProblems(root, command, invocation?.surface, start, owner).map((p) => `/jig ${command}: ${p}`) : [];
|
|
7991
8468
|
if (waiting && command === "mockup") problems.push(...drawingBeforeAsking(root, invocation?.surface).map((p) => `/jig mockup: ${p}`));
|
|
7992
8469
|
if (command === "ship" && !asksOwner(lastAssistantText(opts.input.transcript_path))) {
|
|
@@ -8000,9 +8477,24 @@ ${result.report}`);
|
|
|
8000
8477
|
problems.push(`/jig make: ${spec.path} still says \`mockup: pending\`: nobody has said whether to draw it. Ask the owner, draw it with \`/jig mockup\` or skip it; if they skip it, write \`mockup: skipped \u2014 "<their words>"\` and build from the spec alone.`);
|
|
8001
8478
|
}
|
|
8002
8479
|
}
|
|
8480
|
+
if (command === "spec" && !waiting) {
|
|
8481
|
+
const spec = namedSpec(root, invocation?.surface);
|
|
8482
|
+
if (spec && isConfirmed(spec.body) && !isConfirmed(fileAtSessionStart(root, spec.path, start))) {
|
|
8483
|
+
const reply = ownerReplyAfter(opts.input.transcript_path, /\bconfirm/i);
|
|
8484
|
+
const said = reply === void 0 ? void 0 : ownerWordProblem("confirm", reply.slice(0, 400), reply, `${spec.path}: \`confirmed: true\``);
|
|
8485
|
+
if (reply === void 0) problems.push(`/jig spec: ${spec.path} is recorded as confirmed, and the owner was never asked to confirm it. Put it to them with its sheet and stop; record \`confirmed: true\` when they say yes.`);
|
|
8486
|
+
else if (said) problems.push(`/jig spec: ${spec.path} is recorded as confirmed, but the owner's reply to being asked does not confirm it ("${reply.trim().slice(0, 120)}"). Record \`confirmed: true\` only on their yes.`);
|
|
8487
|
+
}
|
|
8488
|
+
}
|
|
8003
8489
|
if (waiting && command === "spec") {
|
|
8004
|
-
const spec =
|
|
8490
|
+
const spec = namedSpec(root, invocation?.surface);
|
|
8491
|
+
if (!spec && invocation?.surface && /\bconfirm/i.test(lastAssistantText(opts.input.transcript_path) ?? "")) {
|
|
8492
|
+
problems.push(`/jig spec: you are asking the owner to confirm a spec that is not at .jig/specs/${invocation.surface}.spec.md. Write it there, with \`confirmed: false\`, have it checked (step 3c), and ask again: the checks the owner relies on read that file, and a draft kept anywhere else is checked by nothing.`);
|
|
8493
|
+
}
|
|
8005
8494
|
if (spec) problems.push(...specChecked(root, spec, start, owner).map((p) => `/jig spec: ${p}`));
|
|
8495
|
+
if (spec && existsSync27(join43(root, ".jig", "specs", `${spec.slug}.checked.json`))) {
|
|
8496
|
+
problems.push(...[...specProblems(spec), ...newFieldProblems(spec, fileAtSessionStart(root, spec.path, start))].map((p) => `/jig spec: ${p}`));
|
|
8497
|
+
}
|
|
8006
8498
|
}
|
|
8007
8499
|
const selection = selectFiles(root, false);
|
|
8008
8500
|
const changedUi = selection.mode === "changed" && selection.files.some((f) => isStyleBearing(f) || isReaderText(f));
|
|
@@ -8025,11 +8517,17 @@ ${result.report}`);
|
|
|
8025
8517
|
}
|
|
8026
8518
|
}
|
|
8027
8519
|
problems.push(...verdictGuard(root, command, surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)));
|
|
8028
|
-
|
|
8029
|
-
|
|
8520
|
+
problems.push(...probesLeftBehind(root, surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)));
|
|
8521
|
+
const touched = ["AGENTS.md", "CLAUDE.md"].some((f) => {
|
|
8522
|
+
const now = existsSync27(join43(root, f)) ? readText(join43(root, f)) : "";
|
|
8523
|
+
return fileAtSessionStart(root, f, start) !== now;
|
|
8524
|
+
});
|
|
8525
|
+
if (touched) problems.push(...agentFileProblems(root).map((p) => `Jig's agent instructions: ${p}`));
|
|
8526
|
+
const critiqueDir = join43(root, ".jig", "critique");
|
|
8527
|
+
if (existsSync27(critiqueDir)) {
|
|
8030
8528
|
for (const surface of surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)) {
|
|
8031
|
-
const dir =
|
|
8032
|
-
if (!
|
|
8529
|
+
const dir = join43(critiqueDir, surface);
|
|
8530
|
+
if (!existsSync27(join43(dir, "screen.json")) && !existsSync27(join43(dir, "code.json"))) continue;
|
|
8033
8531
|
const v = verifyVerdicts({ projectRoot: root, surface });
|
|
8034
8532
|
if (!v.ok) {
|
|
8035
8533
|
const shown = v.errors.slice(0, 6).map((e) => ` ${e}`);
|
|
@@ -8052,10 +8550,10 @@ ${problems.join("\n\n")}`
|
|
|
8052
8550
|
}
|
|
8053
8551
|
const session = opts.input.session_id ?? "unknown";
|
|
8054
8552
|
const key = `${session}:${createHash3("sha256").update(problems.join("\n")).digest("hex").slice(0, 12)}`;
|
|
8055
|
-
const stateFile =
|
|
8553
|
+
const stateFile = join43(root, ".jig", "gate.json");
|
|
8056
8554
|
let state = {};
|
|
8057
8555
|
try {
|
|
8058
|
-
state = JSON.parse(
|
|
8556
|
+
state = JSON.parse(readFileSync36(stateFile, "utf8"));
|
|
8059
8557
|
} catch {
|
|
8060
8558
|
}
|
|
8061
8559
|
if (problems.length === 0) {
|
|
@@ -8090,7 +8588,7 @@ function verdictDigests(dir) {
|
|
|
8090
8588
|
for (const f of VERDICT_FILES) {
|
|
8091
8589
|
let file;
|
|
8092
8590
|
try {
|
|
8093
|
-
file = JSON.parse(
|
|
8591
|
+
file = JSON.parse(readFileSync36(join43(dir, f), "utf8"));
|
|
8094
8592
|
} catch {
|
|
8095
8593
|
continue;
|
|
8096
8594
|
}
|
|
@@ -8105,7 +8603,7 @@ function writeLock(root, surface, lockPath, dir, now) {
|
|
|
8105
8603
|
const pageFile2 = recordedPage(root, surface);
|
|
8106
8604
|
let page;
|
|
8107
8605
|
try {
|
|
8108
|
-
if (pageFile2) page = { file: pageFile2, checksum: pageChecksum(
|
|
8606
|
+
if (pageFile2) page = { file: pageFile2, checksum: pageChecksum(readFileSync36(join43(root, pageFile2), "utf8")) };
|
|
8109
8607
|
} catch {
|
|
8110
8608
|
}
|
|
8111
8609
|
const tweakAt = readTweak(dir)?.at;
|
|
@@ -8118,7 +8616,7 @@ function verdictChecksum(dir) {
|
|
|
8118
8616
|
const parts = VERDICT_FILES.map((f) => {
|
|
8119
8617
|
try {
|
|
8120
8618
|
return `${f}
|
|
8121
|
-
${
|
|
8619
|
+
${readFileSync36(join43(dir, f), "utf8")}`;
|
|
8122
8620
|
} catch {
|
|
8123
8621
|
return "";
|
|
8124
8622
|
}
|
|
@@ -8126,15 +8624,15 @@ ${readFileSync35(join42(dir, f), "utf8")}`;
|
|
|
8126
8624
|
return parts.some(Boolean) ? checksum(parts.join("\n")) : void 0;
|
|
8127
8625
|
}
|
|
8128
8626
|
function verdictGuard(root, command, inPlay) {
|
|
8129
|
-
const critiqueDir =
|
|
8130
|
-
if (!
|
|
8627
|
+
const critiqueDir = join43(root, ".jig", "critique");
|
|
8628
|
+
if (!existsSync27(critiqueDir)) return [];
|
|
8131
8629
|
const problems = [];
|
|
8132
8630
|
for (const surface of readdirSync16(critiqueDir)) {
|
|
8133
8631
|
if (surface.startsWith("_")) continue;
|
|
8134
|
-
const dir =
|
|
8632
|
+
const dir = join43(critiqueDir, surface);
|
|
8135
8633
|
const now = verdictChecksum(dir);
|
|
8136
8634
|
if (!now) continue;
|
|
8137
|
-
const lockPath =
|
|
8635
|
+
const lockPath = join43(dir, LOCK);
|
|
8138
8636
|
if ((command === "critique" || command === "ship") && (!inPlay || inPlay.includes(surface))) {
|
|
8139
8637
|
writeLock(root, surface, lockPath, dir, now);
|
|
8140
8638
|
problems.push(...lockLeftBehind(root, surface));
|
|
@@ -8142,7 +8640,7 @@ function verdictGuard(root, command, inPlay) {
|
|
|
8142
8640
|
}
|
|
8143
8641
|
let lock = {};
|
|
8144
8642
|
try {
|
|
8145
|
-
lock = JSON.parse(
|
|
8643
|
+
lock = JSON.parse(readFileSync36(lockPath, "utf8"));
|
|
8146
8644
|
} catch {
|
|
8147
8645
|
continue;
|
|
8148
8646
|
}
|
|
@@ -8175,6 +8673,25 @@ function lockLeftBehind(root, surface) {
|
|
|
8175
8673
|
}
|
|
8176
8674
|
return [`${rel}/${LOCK} was written when you stopped, after the verdicts it records were committed. Commit it on its own (\`git add ${rel}/${LOCK} && git commit -m "chore: record ${surface}'s verdict lock"\`), and nothing else.`];
|
|
8177
8675
|
}
|
|
8676
|
+
function probesLeftBehind(root, surfaces) {
|
|
8677
|
+
if (!surfaces.length) return [];
|
|
8678
|
+
let lines;
|
|
8679
|
+
try {
|
|
8680
|
+
lines = execFileSync4("git", ["status", "--porcelain", "--untracked-files=all"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).split("\n").filter(Boolean);
|
|
8681
|
+
} catch {
|
|
8682
|
+
return [];
|
|
8683
|
+
}
|
|
8684
|
+
const path = (l) => l.slice(3).replace(/^"|"$/g, "");
|
|
8685
|
+
const isProbe = (p) => /^\.jig\/critique\/[^/]+\/probe-[^/]+\.json$/.test(p);
|
|
8686
|
+
const isLock = (p) => /^\.jig\/critique\/[^/]+\/verdicts\.lock$/.test(p);
|
|
8687
|
+
if (lines.some((l) => !isProbe(path(l)) && !isLock(path(l)))) return [];
|
|
8688
|
+
const left = lines.map(path).filter((p) => isProbe(p) && surfaces.some((s) => p.startsWith(`.jig/critique/${s}/`)));
|
|
8689
|
+
if (!left.length) return [];
|
|
8690
|
+
const bySurface = [...new Set(left.map((p) => p.split("/")[2]))];
|
|
8691
|
+
return [
|
|
8692
|
+
`The gate re-took ${left.length} probe file${left.length === 1 ? "" : "s"} of ${bySurface.join(", ")} when you stopped, after your last commit: the page changed since they were taken. Commit them on their own (\`git add ${bySurface.map((s) => `.jig/critique/${s}/probe-*.json`).join(" ")} && git commit -m "chore: re-take ${bySurface.join(", ")}'s probes"\`), and nothing else.`
|
|
8693
|
+
];
|
|
8694
|
+
}
|
|
8178
8695
|
function tweakVerdictProblems(dir, surface, lockedVerdicts) {
|
|
8179
8696
|
const record = readTweak(dir);
|
|
8180
8697
|
if (!record) {
|
|
@@ -8199,7 +8716,7 @@ function unjudgedTweakIds(dir, record) {
|
|
|
8199
8716
|
for (const f of VERDICT_FILES) {
|
|
8200
8717
|
let file;
|
|
8201
8718
|
try {
|
|
8202
|
-
file = JSON.parse(
|
|
8719
|
+
file = JSON.parse(readFileSync36(join43(dir, f), "utf8"));
|
|
8203
8720
|
} catch {
|
|
8204
8721
|
continue;
|
|
8205
8722
|
}
|
|
@@ -8211,7 +8728,7 @@ function unjudgedTweakIds(dir, record) {
|
|
|
8211
8728
|
}
|
|
8212
8729
|
function save(file, state) {
|
|
8213
8730
|
try {
|
|
8214
|
-
mkdirSync8(
|
|
8731
|
+
mkdirSync8(join43(file, ".."), { recursive: true });
|
|
8215
8732
|
writeFileSync6(file, JSON.stringify(state), "utf8");
|
|
8216
8733
|
} catch {
|
|
8217
8734
|
}
|
|
@@ -8219,7 +8736,7 @@ function save(file, state) {
|
|
|
8219
8736
|
|
|
8220
8737
|
// src/index.ts
|
|
8221
8738
|
var packageRoot = getPackageRoot();
|
|
8222
|
-
var { version } = JSON.parse(
|
|
8739
|
+
var { version } = JSON.parse(readFileSync37(join44(packageRoot, "package.json"), "utf8"));
|
|
8223
8740
|
function warnIfUnpublishedPin() {
|
|
8224
8741
|
if (isDevVersion(version)) {
|
|
8225
8742
|
console.warn(
|
|
@@ -8263,7 +8780,7 @@ program.command("install").description("Install Jig rules and the agent skill fi
|
|
|
8263
8780
|
projectRoot,
|
|
8264
8781
|
packageRoot: assetRoot(),
|
|
8265
8782
|
version,
|
|
8266
|
-
homeDir:
|
|
8783
|
+
homeDir: homedir3(),
|
|
8267
8784
|
hook
|
|
8268
8785
|
});
|
|
8269
8786
|
if (result.warning) {
|
|
@@ -8272,6 +8789,7 @@ program.command("install").description("Install Jig rules and the agent skill fi
|
|
|
8272
8789
|
}
|
|
8273
8790
|
if (result.hookOnly) {
|
|
8274
8791
|
console.log(result.hookOnly);
|
|
8792
|
+
for (const f of result.agentFiles ?? []) console.log(` + ${f} (Jig's block for your agent; your own text in it is untouched)`);
|
|
8275
8793
|
if (result.stopHook === true) console.log(" + .claude/settings.json (Stop hook: jig gate blocks finishing while check or a critique fails)");
|
|
8276
8794
|
if (result.stopHook === false) console.log(" ! .claude/settings.json is not valid JSON \u2014 the Stop hook was not added. Fix the file and run install again.");
|
|
8277
8795
|
return;
|
|
@@ -8280,6 +8798,7 @@ program.command("install").description("Install Jig rules and the agent skill fi
|
|
|
8280
8798
|
warnIfUnpublishedPin();
|
|
8281
8799
|
for (const f of result.written) console.log(` + ${f}`);
|
|
8282
8800
|
for (const f of result.skipped) console.log(` \xB7 ${f} (edited locally, left alone)`);
|
|
8801
|
+
for (const f of result.agentFiles ?? []) console.log(` + ${f} (Jig's block for your agent; your own text in it is untouched)`);
|
|
8283
8802
|
if (result.stopHook === true) console.log(" + .claude/settings.json (Stop hook: jig gate blocks finishing while check or a critique fails)");
|
|
8284
8803
|
if (result.stopHook === false) console.log(" ! .claude/settings.json is not valid JSON \u2014 the Stop hook was not added. Fix the file and run install again.");
|
|
8285
8804
|
if (result.stopHook === void 0 && opts.agent === "claude" && opts.scope === "project") {
|
|
@@ -8307,7 +8826,7 @@ program.command("update").description("Update vendored Jig rules, skipping files
|
|
|
8307
8826
|
projectRoot,
|
|
8308
8827
|
packageRoot: assetRoot(),
|
|
8309
8828
|
version,
|
|
8310
|
-
homeDir:
|
|
8829
|
+
homeDir: homedir3()
|
|
8311
8830
|
});
|
|
8312
8831
|
const label = result.targets.map((t) => `${t.agent} (${t.scope}, ${t.fromVersion})`).join(", ");
|
|
8313
8832
|
warnIfUnpublishedPin();
|
|
@@ -8326,6 +8845,16 @@ program.command("update").description("Update vendored Jig rules, skipping files
|
|
|
8326
8845
|
process.exit(1);
|
|
8327
8846
|
}
|
|
8328
8847
|
});
|
|
8848
|
+
program.command("checksum").description("Print a spec's checksum, the value its .checked.json records as `spec`.").argument("<spec>", "the spec by name (tokens-and-modes) or path (.jig/specs/tokens-and-modes.spec.md)").action((name) => {
|
|
8849
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
8850
|
+
const slug = name.replace(/^.*\//, "").replace(/\.spec\.md$/, "");
|
|
8851
|
+
const path = join44(projectRoot, ".jig", "specs", `${slug}.spec.md`);
|
|
8852
|
+
if (!existsSync28(path)) {
|
|
8853
|
+
console.error(` \u2717 no spec at .jig/specs/${slug}.spec.md`);
|
|
8854
|
+
process.exit(1);
|
|
8855
|
+
}
|
|
8856
|
+
console.log(specChecksum(readFileSync37(path, "utf8")));
|
|
8857
|
+
});
|
|
8329
8858
|
program.command("verdicts").description("Verify a critique's verdict files and compute its counts.").argument("<surface>", "the surface slug the critique wrote under .jig/critique/").option("--reprobe", "re-take every probe that is missing or was taken on an older version of the page, first (needs a browser)").action(async (surface, opts) => {
|
|
8330
8859
|
const projectRoot = findProjectRoot(process.cwd());
|
|
8331
8860
|
try {
|
|
@@ -8370,7 +8899,7 @@ program.command("check").description("Check the repo against Jig's mechanical +
|
|
|
8370
8899
|
try {
|
|
8371
8900
|
const result = check({
|
|
8372
8901
|
projectRoot,
|
|
8373
|
-
homeDir:
|
|
8902
|
+
homeDir: homedir3(),
|
|
8374
8903
|
version,
|
|
8375
8904
|
all: opts.all,
|
|
8376
8905
|
ci: opts.ci
|
|
@@ -8453,13 +8982,14 @@ program.command("probe").description("Print the render probe. With --save, read
|
|
|
8453
8982
|
program.command("gate").description("Run by the Claude Code Stop hook: block stopping while check or a critique fails.").action(async () => {
|
|
8454
8983
|
let input = {};
|
|
8455
8984
|
try {
|
|
8456
|
-
if (!process.stdin.isTTY) input = JSON.parse(
|
|
8985
|
+
if (!process.stdin.isTTY) input = JSON.parse(readFileSync37(0, "utf8") || "{}");
|
|
8457
8986
|
} catch {
|
|
8458
8987
|
}
|
|
8459
8988
|
const cwd = input.cwd ?? process.cwd();
|
|
8460
8989
|
try {
|
|
8461
8990
|
const projectRoot = findProjectRoot(cwd);
|
|
8462
|
-
const
|
|
8991
|
+
const byHand = !input.transcript_path;
|
|
8992
|
+
const critiqued = byHand ? [] : critiquedSurfaces(projectRoot);
|
|
8463
8993
|
for (const surface of surfacesToProbe(projectRoot, input).filter((s) => critiqued.includes(s))) {
|
|
8464
8994
|
const page = surfacePage(projectRoot, surface) ?? recordedPage(projectRoot, surface);
|
|
8465
8995
|
if (!page) continue;
|
|
@@ -8482,7 +9012,7 @@ program.command("init").description("Set the project up to use Jig: a brand file
|
|
|
8482
9012
|
await init({
|
|
8483
9013
|
projectRoot,
|
|
8484
9014
|
packageRoot: assetRoot(),
|
|
8485
|
-
homeDir:
|
|
9015
|
+
homeDir: homedir3(),
|
|
8486
9016
|
version,
|
|
8487
9017
|
yes: opts.yes
|
|
8488
9018
|
});
|
|
@@ -8493,7 +9023,7 @@ program.command("init").description("Set the project up to use Jig: a brand file
|
|
|
8493
9023
|
});
|
|
8494
9024
|
function resolveSurfaceMode(projectRoot) {
|
|
8495
9025
|
try {
|
|
8496
|
-
const config = JSON.parse(
|
|
9026
|
+
const config = JSON.parse(readFileSync37(join44(projectRoot, "jig.config.json"), "utf8"));
|
|
8497
9027
|
const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
|
|
8498
9028
|
return modes.length === 1 ? modes[0] : void 0;
|
|
8499
9029
|
} catch {
|