jig-ui 0.23.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 +101 -0
- package/README.md +98 -52
- package/dist/index.js +882 -523
- package/package.json +3 -2
- package/templates/COMMAND.md.tmpl +73 -48
- package/templates/SKILL.md.tmpl +12 -0
- 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";
|
|
@@ -55,8 +55,8 @@ function cliInvocation(version2) {
|
|
|
55
55
|
var JIG_GATE_HOOK = /(?:\bjig-ui@[^\s"]+|^jig) gate\b/;
|
|
56
56
|
|
|
57
57
|
// src/commands/install.ts
|
|
58
|
-
import { existsSync as
|
|
59
|
-
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";
|
|
60
60
|
|
|
61
61
|
// src/adapters/types.ts
|
|
62
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.";
|
|
@@ -88,6 +88,236 @@ function agentsPointer(skillPath) {
|
|
|
88
88
|
].join("\n");
|
|
89
89
|
}
|
|
90
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
|
+
|
|
91
321
|
// src/adapters/codex.ts
|
|
92
322
|
var codex = {
|
|
93
323
|
name: "codex",
|
|
@@ -96,7 +326,7 @@ var codex = {
|
|
|
96
326
|
const skillPath = ".agents/skills/jig/SKILL.md";
|
|
97
327
|
const files = [{ relPath: skillPath, content: ctx.skillBody }];
|
|
98
328
|
const agentsPath = ctx.scope === "global" ? ".codex/AGENTS.md" : "AGENTS.md";
|
|
99
|
-
files.push({ relPath: agentsPath, content: agentsPointer(skillPath) });
|
|
329
|
+
files.push({ relPath: agentsPath, content: ctx.scope === "global" ? agentsPointer(skillPath) : agentsGuide() });
|
|
100
330
|
if (ctx.commandBody && ctx.scope === "global") {
|
|
101
331
|
files.push({ relPath: ".codex/prompts/jig.md", content: `${ctx.commandBody}
|
|
102
332
|
` });
|
|
@@ -258,22 +488,22 @@ function skillFilesFor(adapter, ctx) {
|
|
|
258
488
|
}
|
|
259
489
|
|
|
260
490
|
// src/install/references.ts
|
|
261
|
-
import { existsSync as
|
|
262
|
-
import { join as
|
|
491
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
492
|
+
import { join as join5, posix as posix2 } from "path";
|
|
263
493
|
function referenceFiles(packageRoot2) {
|
|
264
|
-
const root =
|
|
265
|
-
if (!
|
|
494
|
+
const root = join5(packageRoot2, "references");
|
|
495
|
+
if (!existsSync3(root)) return [];
|
|
266
496
|
const found = [];
|
|
267
497
|
const walk2 = (dir, prefix) => {
|
|
268
498
|
for (const entry of readdirSync(dir, { withFileTypes: true }).sort(
|
|
269
499
|
(a, b) => a.name.localeCompare(b.name)
|
|
270
500
|
)) {
|
|
271
|
-
const child =
|
|
272
|
-
const rel = prefix ?
|
|
501
|
+
const child = join5(dir, entry.name);
|
|
502
|
+
const rel = prefix ? posix2.join(prefix, entry.name) : entry.name;
|
|
273
503
|
if (entry.isDirectory()) {
|
|
274
504
|
walk2(child, rel);
|
|
275
505
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
276
|
-
found.push({ relPath: rel, content:
|
|
506
|
+
found.push({ relPath: rel, content: readFileSync3(child, "utf8") });
|
|
277
507
|
}
|
|
278
508
|
}
|
|
279
509
|
};
|
|
@@ -283,32 +513,11 @@ function referenceFiles(packageRoot2) {
|
|
|
283
513
|
|
|
284
514
|
// src/install/manifest.ts
|
|
285
515
|
import { createHash } from "crypto";
|
|
286
|
-
import { closeSync, existsSync as
|
|
287
|
-
import { dirname as dirname3, join as
|
|
288
|
-
|
|
289
|
-
// src/install/atomic.ts
|
|
290
|
-
import { mkdirSync, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
291
|
-
import { dirname as dirname2, join as join3 } from "path";
|
|
292
|
-
function writeFileAtomic(path, content) {
|
|
293
|
-
const dir = dirname2(path);
|
|
294
|
-
mkdirSync(dir, { recursive: true });
|
|
295
|
-
const tmp = join3(dir, `.${process.pid}-${Date.now()}.tmp`);
|
|
296
|
-
try {
|
|
297
|
-
writeFileSync(tmp, content, "utf8");
|
|
298
|
-
renameSync(tmp, path);
|
|
299
|
-
} catch (err) {
|
|
300
|
-
try {
|
|
301
|
-
unlinkSync(tmp);
|
|
302
|
-
} catch {
|
|
303
|
-
}
|
|
304
|
-
throw err;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// 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";
|
|
309
518
|
var REGENERATE_HINT = "Re-run 'jig install --agent <name>' to fix it.";
|
|
310
519
|
function manifestPath(root, manifestDir) {
|
|
311
|
-
return
|
|
520
|
+
return join6(root, ...manifestDir.split("/"), "manifest.json");
|
|
312
521
|
}
|
|
313
522
|
function checksum(content) {
|
|
314
523
|
const normalized = content.replace(/\r\n/g, "\n");
|
|
@@ -346,10 +555,10 @@ function validateManifest(raw, path) {
|
|
|
346
555
|
}
|
|
347
556
|
function readManifest(root, manifestDir = ".jig") {
|
|
348
557
|
const path = manifestPath(root, manifestDir);
|
|
349
|
-
if (!
|
|
558
|
+
if (!existsSync4(path)) return null;
|
|
350
559
|
let raw;
|
|
351
560
|
try {
|
|
352
|
-
raw = JSON.parse(
|
|
561
|
+
raw = JSON.parse(readFileSync4(path, "utf8"));
|
|
353
562
|
} catch {
|
|
354
563
|
throw new Error(`Jig's manifest at ${path} is not valid JSON. ${REGENERATE_HINT}`);
|
|
355
564
|
}
|
|
@@ -379,12 +588,9 @@ function withLock(lock, fn) {
|
|
|
379
588
|
}
|
|
380
589
|
break;
|
|
381
590
|
} catch (err) {
|
|
382
|
-
if (err.code
|
|
591
|
+
if (!BUSY.has(err.code ?? "")) throw err;
|
|
383
592
|
if (lockIsStale(lock) || Date.now() > deadline) {
|
|
384
|
-
|
|
385
|
-
unlinkSync2(lock);
|
|
386
|
-
} catch {
|
|
387
|
-
}
|
|
593
|
+
unlinkRetrying(lock);
|
|
388
594
|
continue;
|
|
389
595
|
}
|
|
390
596
|
sleep(10 + Math.random() * 20);
|
|
@@ -393,15 +599,25 @@ function withLock(lock, fn) {
|
|
|
393
599
|
try {
|
|
394
600
|
fn();
|
|
395
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++) {
|
|
396
608
|
try {
|
|
397
609
|
unlinkSync2(lock);
|
|
398
|
-
|
|
610
|
+
return;
|
|
611
|
+
} catch (err) {
|
|
612
|
+
const code = err.code ?? "";
|
|
613
|
+
if (code === "ENOENT" || !BUSY.has(code)) return;
|
|
614
|
+
sleep(5 + attempt * 5);
|
|
399
615
|
}
|
|
400
616
|
}
|
|
401
617
|
}
|
|
402
618
|
function lockIsStale(lock) {
|
|
403
619
|
try {
|
|
404
|
-
const [pid, at] =
|
|
620
|
+
const [pid, at] = readFileSync4(lock, "utf8").trim().split(/\s+/).map(Number);
|
|
405
621
|
if (at && Date.now() - at > LOCK_STALE_MS) return true;
|
|
406
622
|
if (pid && pid !== process.pid) {
|
|
407
623
|
try {
|
|
@@ -421,7 +637,7 @@ function sleep(ms) {
|
|
|
421
637
|
}
|
|
422
638
|
function readManifestQuietly(path) {
|
|
423
639
|
try {
|
|
424
|
-
const parsed = JSON.parse(
|
|
640
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
425
641
|
if (!isPlainObject(parsed) || !isPlainObject(parsed.files)) return null;
|
|
426
642
|
return parsed;
|
|
427
643
|
} catch {
|
|
@@ -431,9 +647,9 @@ function readManifestQuietly(path) {
|
|
|
431
647
|
function isModified(projectRoot, relPath, m) {
|
|
432
648
|
const recorded = m.files[relPath];
|
|
433
649
|
if (!recorded) return false;
|
|
434
|
-
const abs =
|
|
435
|
-
if (!
|
|
436
|
-
return checksum(
|
|
650
|
+
const abs = join6(projectRoot, relPath);
|
|
651
|
+
if (!existsSync4(abs)) return false;
|
|
652
|
+
return checksum(readFileSync4(abs, "utf8")) !== recorded;
|
|
437
653
|
}
|
|
438
654
|
|
|
439
655
|
// src/template/render.ts
|
|
@@ -460,97 +676,16 @@ function renderCommandTable(metadata2) {
|
|
|
460
676
|
return ["| Command | Description | Status |", "| --- | --- | --- |", ...rows].join("\n");
|
|
461
677
|
}
|
|
462
678
|
|
|
463
|
-
// src/install/vendor.ts
|
|
464
|
-
import { posix as posix2 } from "path";
|
|
465
|
-
var { join: join5 } = posix2;
|
|
466
|
-
var DELIMITERS = {
|
|
467
|
-
html: ["<!--", "-->"],
|
|
468
|
-
css: ["/*", "*/"]
|
|
469
|
-
};
|
|
470
|
-
function licencePathFor(relPath) {
|
|
471
|
-
const depth = relPath.split("/").length - 1;
|
|
472
|
-
return depth === 0 ? "." : Array(depth).fill("..").join("/");
|
|
473
|
-
}
|
|
474
|
-
function vendorHeader(file, version2, style = "html", licencePath = ".") {
|
|
475
|
-
const [open, close] = DELIMITERS[style];
|
|
476
|
-
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")}.`;
|
|
477
|
-
return [
|
|
478
|
-
`${open} ${file} \u2014 vendored from Jig v${version2}.`,
|
|
479
|
-
attribution,
|
|
480
|
-
` Edit freely: \`jig update\` will not overwrite a file you have changed. ${close}`,
|
|
481
|
-
"",
|
|
482
|
-
""
|
|
483
|
-
].join("\n");
|
|
484
|
-
}
|
|
485
|
-
function findMarkerRegions(existing) {
|
|
486
|
-
const regions = [];
|
|
487
|
-
let searchFrom = 0;
|
|
488
|
-
let openStart = null;
|
|
489
|
-
while (true) {
|
|
490
|
-
const nextStart = existing.indexOf(BLOCK_START, searchFrom);
|
|
491
|
-
const nextEnd = existing.indexOf(BLOCK_END, searchFrom);
|
|
492
|
-
if (nextStart === -1 && nextEnd === -1) break;
|
|
493
|
-
const endIsNext = nextEnd !== -1 && (nextStart === -1 || nextEnd < nextStart);
|
|
494
|
-
if (endIsNext) {
|
|
495
|
-
if (openStart !== null) {
|
|
496
|
-
regions.push({ start: openStart, end: nextEnd + BLOCK_END.length });
|
|
497
|
-
openStart = null;
|
|
498
|
-
} else {
|
|
499
|
-
regions.push({ start: nextEnd, end: nextEnd + BLOCK_END.length });
|
|
500
|
-
}
|
|
501
|
-
searchFrom = nextEnd + BLOCK_END.length;
|
|
502
|
-
} else {
|
|
503
|
-
if (openStart !== null) {
|
|
504
|
-
regions.push({ start: openStart, end: openStart + BLOCK_START.length });
|
|
505
|
-
}
|
|
506
|
-
openStart = nextStart;
|
|
507
|
-
searchFrom = nextStart + BLOCK_START.length;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
if (openStart !== null) {
|
|
511
|
-
regions.push({ start: openStart, end: existing.length });
|
|
512
|
-
}
|
|
513
|
-
regions.sort((a, b) => a.start - b.start);
|
|
514
|
-
return regions;
|
|
515
|
-
}
|
|
516
|
-
function stripMarkerRegions(existing) {
|
|
517
|
-
const regions = findMarkerRegions(existing);
|
|
518
|
-
if (regions.length === 0) {
|
|
519
|
-
return { text: existing, insertAt: existing.length, removed: false };
|
|
520
|
-
}
|
|
521
|
-
let text = "";
|
|
522
|
-
let cursor = 0;
|
|
523
|
-
let insertAt = -1;
|
|
524
|
-
regions.forEach((region, i) => {
|
|
525
|
-
let chunk = existing.slice(cursor, region.start);
|
|
526
|
-
if (i > 0) chunk = chunk.replace(/^\n/, "");
|
|
527
|
-
text += chunk;
|
|
528
|
-
if (insertAt === -1) insertAt = text.length;
|
|
529
|
-
cursor = region.end;
|
|
530
|
-
});
|
|
531
|
-
const tail = existing.slice(cursor).replace(/^\n/, "");
|
|
532
|
-
text += tail;
|
|
533
|
-
return { text, insertAt, removed: true };
|
|
534
|
-
}
|
|
535
|
-
function upsertBlock(existing, block) {
|
|
536
|
-
const { text, insertAt, removed } = stripMarkerRegions(existing);
|
|
537
|
-
if (!removed) {
|
|
538
|
-
const separator = text.length && !text.endsWith("\n") ? "\n\n" : text.length ? "\n" : "";
|
|
539
|
-
return `${text}${separator}${block}`;
|
|
540
|
-
}
|
|
541
|
-
return `${text.slice(0, insertAt)}${block}${text.slice(insertAt)}`;
|
|
542
|
-
}
|
|
543
|
-
|
|
544
679
|
// src/install/writer.ts
|
|
545
|
-
import { existsSync as
|
|
546
|
-
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";
|
|
547
682
|
|
|
548
683
|
// src/install/line-endings.ts
|
|
549
684
|
function dominantEnding(existing) {
|
|
550
685
|
const crlf = (existing.match(/\r\n/g) ?? []).length;
|
|
551
|
-
const
|
|
552
|
-
if (crlf === 0 &&
|
|
553
|
-
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";
|
|
554
689
|
}
|
|
555
690
|
function matchLineEndings(content, existing) {
|
|
556
691
|
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
@@ -567,9 +702,9 @@ function createWriter(root, seed = {}) {
|
|
|
567
702
|
return {
|
|
568
703
|
files,
|
|
569
704
|
write(key, content) {
|
|
570
|
-
const abs =
|
|
705
|
+
const abs = join7(root, ...key.split("/"));
|
|
571
706
|
mkdirSync3(dirname4(abs), { recursive: true });
|
|
572
|
-
const existing =
|
|
707
|
+
const existing = existsSync5(abs) ? readFileSync5(abs, "utf8") : void 0;
|
|
573
708
|
const toWrite = matchLineEndings(content, existing);
|
|
574
709
|
writeFileAtomic(abs, toWrite);
|
|
575
710
|
files[key] = checksum(toWrite);
|
|
@@ -579,15 +714,15 @@ function createWriter(root, seed = {}) {
|
|
|
579
714
|
}
|
|
580
715
|
function bundleFiles(packageRoot2, dir) {
|
|
581
716
|
const ext = dir === "rules" ? ".md" : ".css";
|
|
582
|
-
return readdirSync2(
|
|
717
|
+
return readdirSync2(join7(packageRoot2, dir)).filter((f) => f.endsWith(ext)).sort();
|
|
583
718
|
}
|
|
584
719
|
|
|
585
720
|
// src/commands/install.ts
|
|
586
721
|
var ASK_INSTRUCTION = "Ask the user directly in chat, one question at a time, and wait for an answer before continuing.";
|
|
587
722
|
function buildSkillBody(packageRoot2, rulesPath, version2) {
|
|
588
|
-
const template =
|
|
723
|
+
const template = readFileSync6(join8(packageRoot2, "templates", "SKILL.md.tmpl"), "utf8");
|
|
589
724
|
const metadata2 = JSON.parse(
|
|
590
|
-
|
|
725
|
+
readFileSync6(join8(packageRoot2, "templates", "command-metadata.json"), "utf8")
|
|
591
726
|
);
|
|
592
727
|
return render(template, {
|
|
593
728
|
command_prefix: "/jig ",
|
|
@@ -604,12 +739,12 @@ function buildSkillBody(packageRoot2, rulesPath, version2) {
|
|
|
604
739
|
}
|
|
605
740
|
function buildCommandBody(packageRoot2, rulesPath, version2, argsPlaceholder) {
|
|
606
741
|
const metadata2 = JSON.parse(
|
|
607
|
-
|
|
742
|
+
readFileSync6(join8(packageRoot2, "templates", "command-metadata.json"), "utf8")
|
|
608
743
|
);
|
|
609
744
|
const subcommands = Object.entries(metadata2).filter(([, meta]) => meta.status === "available" || meta.status === "agent").map(([name]) => name).sort();
|
|
610
|
-
const templatePath =
|
|
611
|
-
if (!
|
|
612
|
-
const template =
|
|
745
|
+
const templatePath = join8(packageRoot2, "templates", "COMMAND.md.tmpl");
|
|
746
|
+
if (!existsSync6(templatePath)) return { body: "", subcommands };
|
|
747
|
+
const template = readFileSync6(templatePath, "utf8");
|
|
613
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");
|
|
614
749
|
const unresolved = [...new Set(body.match(/\{\{[a-z_]+\}\}/g) ?? [])];
|
|
615
750
|
if (unresolved.length > 0) {
|
|
@@ -637,6 +772,7 @@ function install(opts) {
|
|
|
637
772
|
written: [],
|
|
638
773
|
skipped: [],
|
|
639
774
|
stopHook: installStopHook(opts.projectRoot, opts.version),
|
|
775
|
+
agentFiles: ensureAgentFiles(opts.projectRoot, { claude: true }),
|
|
640
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.`
|
|
641
777
|
};
|
|
642
778
|
}
|
|
@@ -657,9 +793,9 @@ function install(opts) {
|
|
|
657
793
|
existingManifest = null;
|
|
658
794
|
}
|
|
659
795
|
const planned = [];
|
|
660
|
-
const rulesDir =
|
|
796
|
+
const rulesDir = join8(opts.packageRoot, "rules");
|
|
661
797
|
for (const file of bundleFiles(opts.packageRoot, "rules")) {
|
|
662
|
-
const body =
|
|
798
|
+
const body = readFileSync6(join8(rulesDir, file), "utf8");
|
|
663
799
|
planned.push({
|
|
664
800
|
key: relKey(referenceDir, "rules", file),
|
|
665
801
|
content: vendorHeader(file, opts.version, "html", licencePathFor(`rules/${file}`)) + body,
|
|
@@ -675,13 +811,13 @@ function install(opts) {
|
|
|
675
811
|
}
|
|
676
812
|
planned.push({
|
|
677
813
|
key: relKey(referenceDir, "rules.index.json"),
|
|
678
|
-
content:
|
|
814
|
+
content: readFileSync6(join8(opts.packageRoot, "rules.index.json"), "utf8"),
|
|
679
815
|
checkable: true
|
|
680
816
|
});
|
|
681
817
|
for (const src of ["LICENSE", "NOTICE"]) {
|
|
682
818
|
planned.push({
|
|
683
819
|
key: relKey(referenceDir, src),
|
|
684
|
-
content:
|
|
820
|
+
content: readFileSync6(join8(opts.packageRoot, src), "utf8"),
|
|
685
821
|
checkable: false
|
|
686
822
|
});
|
|
687
823
|
}
|
|
@@ -703,9 +839,9 @@ function install(opts) {
|
|
|
703
839
|
});
|
|
704
840
|
for (const file of skillFiles) {
|
|
705
841
|
const key = file.relPath;
|
|
706
|
-
const abs =
|
|
842
|
+
const abs = join8(installRoot, ...key.split("/"));
|
|
707
843
|
const isBlockFile = file.content.includes(BLOCK_START);
|
|
708
|
-
const content = isBlockFile &&
|
|
844
|
+
const content = isBlockFile && existsSync6(abs) ? upsertBlock(readFileSync6(abs, "utf8"), file.content) : file.content;
|
|
709
845
|
planned.push({ key, content, checkable: !isBlockFile });
|
|
710
846
|
}
|
|
711
847
|
const writer = createWriter(installRoot);
|
|
@@ -732,11 +868,12 @@ function install(opts) {
|
|
|
732
868
|
writeManifest(installRoot, manifest, referenceDir);
|
|
733
869
|
written.push(relKey(referenceDir, "manifest.json"));
|
|
734
870
|
const stopHook = opts.hook && opts.agent === "claude" && opts.scope === "project" ? installStopHook(installRoot, opts.version) : void 0;
|
|
735
|
-
|
|
871
|
+
const agentFiles = opts.scope === "project" ? ensureAgentFiles(opts.projectRoot, { claude: opts.agent === "claude" || claudeInstalled(opts.projectRoot, opts.homeDir) }) : [];
|
|
872
|
+
return { written, skipped, stopHook, agentFiles };
|
|
736
873
|
}
|
|
737
874
|
function hasStopHook(projectRoot) {
|
|
738
875
|
try {
|
|
739
|
-
const settings =
|
|
876
|
+
const settings = readFileSync6(join8(projectRoot, ".claude", "settings.json"), "utf8");
|
|
740
877
|
const hooks = JSON.parse(settings).hooks?.Stop ?? [];
|
|
741
878
|
return hooks.some((group) => (group.hooks ?? []).some((h) => JIG_GATE_HOOK.test(h.command ?? "")));
|
|
742
879
|
} catch {
|
|
@@ -744,11 +881,11 @@ function hasStopHook(projectRoot) {
|
|
|
744
881
|
}
|
|
745
882
|
}
|
|
746
883
|
function installStopHook(projectRoot, version2) {
|
|
747
|
-
const path =
|
|
884
|
+
const path = join8(projectRoot, ".claude", "settings.json");
|
|
748
885
|
let settings = {};
|
|
749
|
-
if (
|
|
886
|
+
if (existsSync6(path)) {
|
|
750
887
|
try {
|
|
751
|
-
settings = JSON.parse(
|
|
888
|
+
settings = JSON.parse(readFileSync6(path, "utf8"));
|
|
752
889
|
} catch {
|
|
753
890
|
return false;
|
|
754
891
|
}
|
|
@@ -759,14 +896,14 @@ function installStopHook(projectRoot, version2) {
|
|
|
759
896
|
const stop = (hooks.Stop ?? []).map((group) => ({ ...group, hooks: group.hooks.filter((h) => !isJig(h)) })).filter((group) => group.hooks.length > 0);
|
|
760
897
|
stop.push({ hooks: [{ type: "command", command }] });
|
|
761
898
|
settings.hooks = { ...hooks, Stop: stop };
|
|
762
|
-
mkdirSync4(
|
|
899
|
+
mkdirSync4(join8(projectRoot, ".claude"), { recursive: true });
|
|
763
900
|
writeFileSync2(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
764
901
|
return true;
|
|
765
902
|
}
|
|
766
903
|
|
|
767
904
|
// src/commands/update.ts
|
|
768
|
-
import { join as
|
|
769
|
-
import { existsSync as
|
|
905
|
+
import { join as join11 } from "path";
|
|
906
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
770
907
|
|
|
771
908
|
// src/install/target.ts
|
|
772
909
|
import { resolve } from "path";
|
|
@@ -809,14 +946,14 @@ function resolveAllInstalled(projectRoot, homeDir) {
|
|
|
809
946
|
}
|
|
810
947
|
|
|
811
948
|
// src/init/state.ts
|
|
812
|
-
import { existsSync as
|
|
813
|
-
import { dirname as dirname5, join as
|
|
814
|
-
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");
|
|
815
952
|
function readInitManifest(projectRoot) {
|
|
816
|
-
const path =
|
|
817
|
-
if (!
|
|
953
|
+
const path = join9(projectRoot, INIT_MANIFEST_REL);
|
|
954
|
+
if (!existsSync7(path)) return null;
|
|
818
955
|
try {
|
|
819
|
-
const raw = JSON.parse(
|
|
956
|
+
const raw = JSON.parse(readFileSync7(path, "utf8"));
|
|
820
957
|
if (typeof raw !== "object" || raw === null) return null;
|
|
821
958
|
const { files, version: version2, modes } = raw;
|
|
822
959
|
if (typeof files !== "object" || files === null) return null;
|
|
@@ -831,7 +968,7 @@ function readInitManifest(projectRoot) {
|
|
|
831
968
|
}
|
|
832
969
|
var MERGE_ATTEMPTS = 5;
|
|
833
970
|
function writeInitManifest(projectRoot, m) {
|
|
834
|
-
const path =
|
|
971
|
+
const path = join9(projectRoot, INIT_MANIFEST_REL);
|
|
835
972
|
mkdirSync5(dirname5(path), { recursive: true });
|
|
836
973
|
for (let attempt = 0; attempt < MERGE_ATTEMPTS; attempt++) {
|
|
837
974
|
const onDisk = readInitManifest(projectRoot);
|
|
@@ -852,15 +989,15 @@ function isInitFileModified(projectRoot, relPath, m) {
|
|
|
852
989
|
}
|
|
853
990
|
|
|
854
991
|
// src/init/migrate.ts
|
|
855
|
-
import { existsSync as
|
|
856
|
-
import { join as
|
|
992
|
+
import { existsSync as existsSync8, readFileSync as readFileSync8, readdirSync as readdirSync3, rmSync } from "fs";
|
|
993
|
+
import { join as join10 } from "path";
|
|
857
994
|
var LEGACY_RULE_RE = /^\d{2}-.*\.md$/;
|
|
858
995
|
function isLegacyInstallArtifact(name) {
|
|
859
996
|
return LEGACY_RULE_RE.test(name) || name === "rules.index.json" || name === "LICENSE" || name === "NOTICE" || name === "manifest.json";
|
|
860
997
|
}
|
|
861
998
|
function readLegacyManifest(dirAbs) {
|
|
862
999
|
try {
|
|
863
|
-
const raw = JSON.parse(
|
|
1000
|
+
const raw = JSON.parse(readFileSync8(join10(dirAbs, "manifest.json"), "utf8"));
|
|
864
1001
|
if (raw && typeof raw === "object" && typeof raw.files === "object") {
|
|
865
1002
|
return raw;
|
|
866
1003
|
}
|
|
@@ -872,8 +1009,8 @@ function classify(projectRoot, relPath, legacyManifest) {
|
|
|
872
1009
|
return legacyManifest && relPath in legacyManifest.files ? isModified(projectRoot, relPath, legacyManifest) : "unknown";
|
|
873
1010
|
}
|
|
874
1011
|
function detectLegacyRules(projectRoot) {
|
|
875
|
-
const jigDir =
|
|
876
|
-
if (!
|
|
1012
|
+
const jigDir = join10(projectRoot, ".jig");
|
|
1013
|
+
if (!existsSync8(jigDir)) return { present: false, files: [] };
|
|
877
1014
|
const legacyManifest = readLegacyManifest(jigDir);
|
|
878
1015
|
let entries;
|
|
879
1016
|
try {
|
|
@@ -889,12 +1026,12 @@ function detectLegacyRules(projectRoot) {
|
|
|
889
1026
|
}
|
|
890
1027
|
function detectLegacyCursorRules(projectRoot) {
|
|
891
1028
|
const bundleDirRel = ".cursor/rules/jig";
|
|
892
|
-
const bundleDir =
|
|
1029
|
+
const bundleDir = join10(projectRoot, ...bundleDirRel.split("/"));
|
|
893
1030
|
const mdcRel = ".cursor/rules/jig.mdc";
|
|
894
|
-
const mdcAbs =
|
|
1031
|
+
const mdcAbs = join10(projectRoot, ...mdcRel.split("/"));
|
|
895
1032
|
const legacyManifest = readLegacyManifest(bundleDir);
|
|
896
1033
|
const files = [];
|
|
897
|
-
if (
|
|
1034
|
+
if (existsSync8(mdcAbs)) {
|
|
898
1035
|
files.push({ relPath: mdcRel, modified: classify(projectRoot, mdcRel, legacyManifest) });
|
|
899
1036
|
}
|
|
900
1037
|
let entries;
|
|
@@ -948,7 +1085,7 @@ function removableLegacyFiles(report2) {
|
|
|
948
1085
|
function removeLegacyFiles(projectRoot, relPaths) {
|
|
949
1086
|
for (const relPath of relPaths) {
|
|
950
1087
|
try {
|
|
951
|
-
rmSync(
|
|
1088
|
+
rmSync(join10(projectRoot, ...relPath.split("/")), { force: true });
|
|
952
1089
|
} catch {
|
|
953
1090
|
}
|
|
954
1091
|
}
|
|
@@ -991,6 +1128,9 @@ function update(opts) {
|
|
|
991
1128
|
const initResult = updateInitFiles(opts);
|
|
992
1129
|
updated.push(...initResult.updated);
|
|
993
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
|
+
}
|
|
994
1134
|
return {
|
|
995
1135
|
updated,
|
|
996
1136
|
skipped,
|
|
@@ -1013,14 +1153,14 @@ function updateTarget(opts, resolved) {
|
|
|
1013
1153
|
const write = (key, content) => {
|
|
1014
1154
|
updated.push(writer.write(key, content));
|
|
1015
1155
|
};
|
|
1016
|
-
const rulesDir =
|
|
1156
|
+
const rulesDir = join11(opts.packageRoot, "rules");
|
|
1017
1157
|
for (const file of bundleFiles(opts.packageRoot, "rules")) {
|
|
1018
1158
|
const key = relKey(referenceDir, "rules", file);
|
|
1019
1159
|
if (isModified(installRoot, key, existing)) {
|
|
1020
1160
|
skipped.push(key);
|
|
1021
1161
|
continue;
|
|
1022
1162
|
}
|
|
1023
|
-
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"));
|
|
1024
1164
|
}
|
|
1025
1165
|
for (const ref of referenceFiles(opts.packageRoot)) {
|
|
1026
1166
|
const key = relKey(referenceDir, ...ref.relPath.split("/"));
|
|
@@ -1034,7 +1174,7 @@ function updateTarget(opts, resolved) {
|
|
|
1034
1174
|
if (isModified(installRoot, indexKey, existing)) {
|
|
1035
1175
|
skipped.push(indexKey);
|
|
1036
1176
|
} else {
|
|
1037
|
-
write(indexKey,
|
|
1177
|
+
write(indexKey, readFileSync9(join11(opts.packageRoot, "rules.index.json"), "utf8"));
|
|
1038
1178
|
}
|
|
1039
1179
|
const rulesPath = rulesPathFor(referenceDir, discoveredScope);
|
|
1040
1180
|
const skillBody = buildSkillBody(opts.packageRoot, rulesPath, opts.version);
|
|
@@ -1056,8 +1196,8 @@ function updateTarget(opts, resolved) {
|
|
|
1056
1196
|
const key = file.relPath;
|
|
1057
1197
|
const isBlockFile = file.content.includes(BLOCK_START);
|
|
1058
1198
|
if (isBlockFile) {
|
|
1059
|
-
const abs =
|
|
1060
|
-
const current =
|
|
1199
|
+
const abs = join11(installRoot, ...key.split("/"));
|
|
1200
|
+
const current = existsSync9(abs) ? readFileSync9(abs, "utf8") : "";
|
|
1061
1201
|
write(key, upsertBlock(current, file.content));
|
|
1062
1202
|
continue;
|
|
1063
1203
|
}
|
|
@@ -1068,7 +1208,7 @@ function updateTarget(opts, resolved) {
|
|
|
1068
1208
|
write(key, file.content);
|
|
1069
1209
|
}
|
|
1070
1210
|
for (const src of ["LICENSE", "NOTICE"]) {
|
|
1071
|
-
write(relKey(referenceDir, src),
|
|
1211
|
+
write(relKey(referenceDir, src), readFileSync9(join11(opts.packageRoot, src), "utf8"));
|
|
1072
1212
|
}
|
|
1073
1213
|
const manifest = {
|
|
1074
1214
|
...existing,
|
|
@@ -1084,7 +1224,7 @@ function updateInitFiles(opts) {
|
|
|
1084
1224
|
const updated = [];
|
|
1085
1225
|
const skipped = [];
|
|
1086
1226
|
const tokens = [];
|
|
1087
|
-
const tokensDir =
|
|
1227
|
+
const tokensDir = join11(opts.packageRoot, "tokens");
|
|
1088
1228
|
const initManifest = readInitManifest(opts.projectRoot);
|
|
1089
1229
|
if (initManifest) {
|
|
1090
1230
|
const initFiles = { ...initManifest.files };
|
|
@@ -1096,10 +1236,10 @@ function updateInitFiles(opts) {
|
|
|
1096
1236
|
for (const key of keys) {
|
|
1097
1237
|
let before = "";
|
|
1098
1238
|
try {
|
|
1099
|
-
before =
|
|
1239
|
+
before = readFileSync9(join11(opts.projectRoot, ...key.split("/")), "utf8");
|
|
1100
1240
|
} catch {
|
|
1101
1241
|
}
|
|
1102
|
-
const fresh =
|
|
1242
|
+
const fresh = readFileSync9(join11(tokensDir, file), "utf8");
|
|
1103
1243
|
if (isInitFileModified(opts.projectRoot, key, initManifest)) {
|
|
1104
1244
|
skipped.push(key);
|
|
1105
1245
|
const change2 = tokenChange(key, before, fresh, true);
|
|
@@ -1120,12 +1260,12 @@ function updateInitFiles(opts) {
|
|
|
1120
1260
|
}
|
|
1121
1261
|
|
|
1122
1262
|
// src/commands/explain.ts
|
|
1123
|
-
import { existsSync as
|
|
1124
|
-
import { join as
|
|
1263
|
+
import { existsSync as existsSync10 } from "fs";
|
|
1264
|
+
import { join as join16 } from "path";
|
|
1125
1265
|
|
|
1126
1266
|
// src/rules/load.ts
|
|
1127
|
-
import { readFileSync as
|
|
1128
|
-
import { join as
|
|
1267
|
+
import { readFileSync as readFileSync10, readdirSync as readdirSync4 } from "fs";
|
|
1268
|
+
import { join as join12 } from "path";
|
|
1129
1269
|
|
|
1130
1270
|
// src/rules/parse.ts
|
|
1131
1271
|
var HEADING = /^###\s+([A-Z])-(\d+)\s+(.+?)\s*$/;
|
|
@@ -1221,9 +1361,9 @@ function loadRules(rulesDir, indexPath) {
|
|
|
1221
1361
|
const files = readdirSync4(rulesDir).filter((f) => f.endsWith(".md")).sort();
|
|
1222
1362
|
const parsed = [];
|
|
1223
1363
|
for (const file of files) {
|
|
1224
|
-
parsed.push(...parseRules(
|
|
1364
|
+
parsed.push(...parseRules(readFileSync10(join12(rulesDir, file), "utf8"), file));
|
|
1225
1365
|
}
|
|
1226
|
-
const entries = validateIndex(JSON.parse(
|
|
1366
|
+
const entries = validateIndex(JSON.parse(readFileSync10(indexPath, "utf8")));
|
|
1227
1367
|
const byId = new Map(entries.map((e) => [e.id, e]));
|
|
1228
1368
|
const missing = parsed.filter((r) => !byId.has(r.id)).map((r) => r.id);
|
|
1229
1369
|
if (missing.length) {
|
|
@@ -1245,8 +1385,8 @@ function loadRules(rulesDir, indexPath) {
|
|
|
1245
1385
|
}
|
|
1246
1386
|
|
|
1247
1387
|
// src/rules/specs.ts
|
|
1248
|
-
import { readFileSync as
|
|
1249
|
-
import { join as
|
|
1388
|
+
import { readFileSync as readFileSync11, readdirSync as readdirSync5 } from "fs";
|
|
1389
|
+
import { join as join13 } from "path";
|
|
1250
1390
|
var KINDS = {
|
|
1251
1391
|
P: "pattern",
|
|
1252
1392
|
M: "mode",
|
|
@@ -1305,14 +1445,14 @@ function parseSpecs(markdown, sourceFile) {
|
|
|
1305
1445
|
function loadSpecs(rulesDir) {
|
|
1306
1446
|
const specs = [];
|
|
1307
1447
|
for (const file of readdirSync5(rulesDir).filter((f) => f.endsWith(".md")).sort()) {
|
|
1308
|
-
specs.push(...parseSpecs(
|
|
1448
|
+
specs.push(...parseSpecs(readFileSync11(join13(rulesDir, file), "utf8"), file));
|
|
1309
1449
|
}
|
|
1310
1450
|
return specs;
|
|
1311
1451
|
}
|
|
1312
1452
|
|
|
1313
1453
|
// src/rules/sections.ts
|
|
1314
|
-
import { readFileSync as
|
|
1315
|
-
import { join as
|
|
1454
|
+
import { readFileSync as readFileSync12, readdirSync as readdirSync6 } from "fs";
|
|
1455
|
+
import { join as join14 } from "path";
|
|
1316
1456
|
var RULE_HEADING = /^###\s+[A-Z]-\d+\b/;
|
|
1317
1457
|
var SPEC_HEADING = /^##\s+[PMLRT]-\d+\b/;
|
|
1318
1458
|
var ANY_HEADING = /^(#{1,6})\s+(.*?)\s*$/;
|
|
@@ -1353,16 +1493,16 @@ function parseSections(markdown, file) {
|
|
|
1353
1493
|
function loadSections(rulesDir) {
|
|
1354
1494
|
const out = [];
|
|
1355
1495
|
for (const file of readdirSync6(rulesDir).filter((f) => f.endsWith(".md")).sort()) {
|
|
1356
|
-
out.push(...parseSections(
|
|
1496
|
+
out.push(...parseSections(readFileSync12(join14(rulesDir, file), "utf8"), file));
|
|
1357
1497
|
}
|
|
1358
1498
|
return out;
|
|
1359
1499
|
}
|
|
1360
1500
|
|
|
1361
1501
|
// src/rules/layers.ts
|
|
1362
|
-
import { readFileSync as
|
|
1363
|
-
import { join as
|
|
1502
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
1503
|
+
import { join as join15 } from "path";
|
|
1364
1504
|
function loadLayers(packageRoot2) {
|
|
1365
|
-
return JSON.parse(
|
|
1505
|
+
return JSON.parse(readFileSync13(join15(packageRoot2, "layers.json"), "utf8"));
|
|
1366
1506
|
}
|
|
1367
1507
|
|
|
1368
1508
|
// src/commands/explain.ts
|
|
@@ -1444,8 +1584,8 @@ function renderSection(s) {
|
|
|
1444
1584
|
function explain(opts) {
|
|
1445
1585
|
const raw = opts.ruleId.trim().toUpperCase();
|
|
1446
1586
|
const root = opts.packageRoot ?? assetRoot();
|
|
1447
|
-
const rulesDir =
|
|
1448
|
-
const rules = loadRules(rulesDir,
|
|
1587
|
+
const rulesDir = join16(root, "rules");
|
|
1588
|
+
const rules = loadRules(rulesDir, join16(root, "rules.index.json"));
|
|
1449
1589
|
const specs = loadSpecs(rulesDir);
|
|
1450
1590
|
const sections = loadSections(rulesDir);
|
|
1451
1591
|
const entries = searchEntries(rules, specs, sections);
|
|
@@ -1501,8 +1641,8 @@ function explain(opts) {
|
|
|
1501
1641
|
const id = `${shape[1]}-${shape[2]}`;
|
|
1502
1642
|
const rule = rules.find((r) => r.id === id);
|
|
1503
1643
|
if (rule) {
|
|
1504
|
-
const example =
|
|
1505
|
-
return
|
|
1644
|
+
const example = join16(root, "examples", `${id}.html`);
|
|
1645
|
+
return existsSync10(example) ? `${renderRule(rule)}
|
|
1506
1646
|
|
|
1507
1647
|
Example: ${example} (a dont and a do, each a self-contained HTML fragment)` : renderRule(rule);
|
|
1508
1648
|
}
|
|
@@ -1559,13 +1699,13 @@ function sectionsOf(entries) {
|
|
|
1559
1699
|
}
|
|
1560
1700
|
|
|
1561
1701
|
// src/commands/check.ts
|
|
1562
|
-
import { existsSync as
|
|
1563
|
-
import { join as
|
|
1702
|
+
import { existsSync as existsSync16, readFileSync as readFileSync21 } from "fs";
|
|
1703
|
+
import { join as join25 } from "path";
|
|
1564
1704
|
|
|
1565
1705
|
// src/check/files.ts
|
|
1566
1706
|
import { execFileSync } from "child_process";
|
|
1567
1707
|
import { readdirSync as readdirSync7, realpathSync, statSync as statSync2 } from "fs";
|
|
1568
|
-
import { join as
|
|
1708
|
+
import { join as join17, relative, sep } from "path";
|
|
1569
1709
|
var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
|
|
1570
1710
|
"node_modules",
|
|
1571
1711
|
"dist",
|
|
@@ -1640,7 +1780,7 @@ function walk(root, dir, out, seen) {
|
|
|
1640
1780
|
}
|
|
1641
1781
|
for (const entry of entries) {
|
|
1642
1782
|
if (EXCLUDE_DIRS.has(entry.name)) continue;
|
|
1643
|
-
const abs =
|
|
1783
|
+
const abs = join17(dir, entry.name);
|
|
1644
1784
|
let isDir = entry.isDirectory();
|
|
1645
1785
|
let isFile = entry.isFile();
|
|
1646
1786
|
if (entry.isSymbolicLink()) {
|
|
@@ -1782,6 +1922,10 @@ function formatReport(findings, meta) {
|
|
|
1782
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.`
|
|
1783
1923
|
);
|
|
1784
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
|
+
}
|
|
1785
1929
|
if (meta.exemptPatterns && meta.exemptPatterns.length > 0) {
|
|
1786
1930
|
const n = meta.exempt?.length ?? 0;
|
|
1787
1931
|
lines.push(` ${n} file(s) exempt via jig.config.json and not scanned:`);
|
|
@@ -1789,6 +1933,9 @@ function formatReport(findings, meta) {
|
|
|
1789
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" : ""}`;
|
|
1790
1934
|
lines.push(` ${pattern} (${note})`);
|
|
1791
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
|
+
}
|
|
1792
1939
|
if (n > 0) {
|
|
1793
1940
|
lines.push(` ${meta.exempt.slice(0, 8).join(", ")}${n > 8 ? `, and ${n - 8} more` : ""}`);
|
|
1794
1941
|
}
|
|
@@ -1952,8 +2099,8 @@ function isReaderText(file) {
|
|
|
1952
2099
|
}
|
|
1953
2100
|
|
|
1954
2101
|
// src/check/run.ts
|
|
1955
|
-
import { readFileSync as
|
|
1956
|
-
import { join as
|
|
2102
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
2103
|
+
import { join as join19 } from "path";
|
|
1957
2104
|
|
|
1958
2105
|
// src/check/css.ts
|
|
1959
2106
|
function maskComments(source) {
|
|
@@ -3372,8 +3519,8 @@ var menuState = {
|
|
|
3372
3519
|
};
|
|
3373
3520
|
|
|
3374
3521
|
// src/check/declared-properties.ts
|
|
3375
|
-
import { existsSync as
|
|
3376
|
-
import { join as
|
|
3522
|
+
import { existsSync as existsSync11, readFileSync as readFileSync14 } from "fs";
|
|
3523
|
+
import { join as join18 } from "path";
|
|
3377
3524
|
var DECLARATION_RE = /(?<![\w-])['"]?(--[A-Za-z_][\w-]*)['"]?\s*:/g;
|
|
3378
3525
|
var AT_PROPERTY_RE = /@property\s+(--[\w-]+)/g;
|
|
3379
3526
|
var SET_PROPERTY_RE = /setProperty\(\s*['"`](--[\w-]+)/g;
|
|
@@ -3385,16 +3532,16 @@ function addAll(names, text) {
|
|
|
3385
3532
|
}
|
|
3386
3533
|
}
|
|
3387
3534
|
function readPackageImport(projectRoot, spec) {
|
|
3388
|
-
const base =
|
|
3535
|
+
const base = join18(projectRoot, "node_modules", spec);
|
|
3389
3536
|
for (const candidate of [base, `${base}.css`]) {
|
|
3390
3537
|
try {
|
|
3391
|
-
if (
|
|
3538
|
+
if (existsSync11(candidate) && candidate.endsWith(".css")) return readFileSync14(candidate, "utf8");
|
|
3392
3539
|
} catch {
|
|
3393
3540
|
}
|
|
3394
3541
|
}
|
|
3395
3542
|
try {
|
|
3396
|
-
const pkg = JSON.parse(
|
|
3397
|
-
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");
|
|
3398
3545
|
} catch {
|
|
3399
3546
|
}
|
|
3400
3547
|
return void 0;
|
|
@@ -3406,7 +3553,7 @@ function collectDeclaredProperties(projectRoot, files, tokenNames = []) {
|
|
|
3406
3553
|
for (const file of files) {
|
|
3407
3554
|
let raw;
|
|
3408
3555
|
try {
|
|
3409
|
-
raw =
|
|
3556
|
+
raw = readFileSync14(join18(projectRoot, file), "utf8");
|
|
3410
3557
|
} catch {
|
|
3411
3558
|
continue;
|
|
3412
3559
|
}
|
|
@@ -4256,7 +4403,7 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
|
|
|
4256
4403
|
if (applicable.length === 0) continue;
|
|
4257
4404
|
let raw;
|
|
4258
4405
|
try {
|
|
4259
|
-
raw =
|
|
4406
|
+
raw = readFileSync15(join19(projectRoot, file), "utf8");
|
|
4260
4407
|
} catch {
|
|
4261
4408
|
continue;
|
|
4262
4409
|
}
|
|
@@ -4270,8 +4417,8 @@ function runChecks(projectRoot, files, index, tokens, bucketFilter, projectParti
|
|
|
4270
4417
|
}
|
|
4271
4418
|
|
|
4272
4419
|
// src/check/tokens.ts
|
|
4273
|
-
import { existsSync as
|
|
4274
|
-
import { join as
|
|
4420
|
+
import { existsSync as existsSync12, readdirSync as readdirSync8, readFileSync as readFileSync16 } from "fs";
|
|
4421
|
+
import { join as join20 } from "path";
|
|
4275
4422
|
function rootDeclarations(source) {
|
|
4276
4423
|
const out = {};
|
|
4277
4424
|
for (const block of splitRuleBlocks(source)) {
|
|
@@ -4286,20 +4433,20 @@ function rootDeclarations(source) {
|
|
|
4286
4433
|
}
|
|
4287
4434
|
function loadTokenMap(projectRoot, consumerFiles = []) {
|
|
4288
4435
|
const jig = {};
|
|
4289
|
-
const tokensDir =
|
|
4290
|
-
if (
|
|
4436
|
+
const tokensDir = join20(projectRoot, ".jig", "tokens");
|
|
4437
|
+
if (existsSync12(tokensDir)) {
|
|
4291
4438
|
for (const file of readdirSync8(tokensDir).filter((f) => f.endsWith(".css")).sort()) {
|
|
4292
|
-
Object.assign(jig, rootDeclarations(
|
|
4439
|
+
Object.assign(jig, rootDeclarations(readFileSync16(join20(tokensDir, file), "utf8")));
|
|
4293
4440
|
}
|
|
4294
4441
|
}
|
|
4295
4442
|
const consumer = {};
|
|
4296
4443
|
const ambiguous = /* @__PURE__ */ new Set();
|
|
4297
|
-
const vendored =
|
|
4444
|
+
const vendored = join20(".jig", "tokens");
|
|
4298
4445
|
for (const file of consumerFiles) {
|
|
4299
4446
|
if (file.includes(vendored)) continue;
|
|
4300
4447
|
let source;
|
|
4301
4448
|
try {
|
|
4302
|
-
source =
|
|
4449
|
+
source = readFileSync16(join20(projectRoot, file), "utf8");
|
|
4303
4450
|
} catch {
|
|
4304
4451
|
continue;
|
|
4305
4452
|
}
|
|
@@ -4318,8 +4465,8 @@ function loadTokenMap(projectRoot, consumerFiles = []) {
|
|
|
4318
4465
|
}
|
|
4319
4466
|
|
|
4320
4467
|
// src/check/exempt.ts
|
|
4321
|
-
import { existsSync as
|
|
4322
|
-
import { join as
|
|
4468
|
+
import { existsSync as existsSync13, readFileSync as readFileSync17 } from "fs";
|
|
4469
|
+
import { join as join21 } from "path";
|
|
4323
4470
|
function globToRegExp(glob) {
|
|
4324
4471
|
let out = "";
|
|
4325
4472
|
for (let i = 0; i < glob.length; i++) {
|
|
@@ -4338,18 +4485,37 @@ function globToRegExp(glob) {
|
|
|
4338
4485
|
return new RegExp(`^${out}$`);
|
|
4339
4486
|
}
|
|
4340
4487
|
function readExemptions(projectRoot) {
|
|
4341
|
-
const
|
|
4342
|
-
|
|
4488
|
+
const none = { patterns: [], ignored: [] };
|
|
4489
|
+
const configPath = join21(projectRoot, "jig.config.json");
|
|
4490
|
+
if (!existsSync13(configPath)) return none;
|
|
4343
4491
|
let declared;
|
|
4344
4492
|
try {
|
|
4345
|
-
declared = JSON.parse(
|
|
4493
|
+
declared = JSON.parse(readFileSync17(configPath, "utf8")).exempt;
|
|
4346
4494
|
} catch {
|
|
4347
|
-
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
|
+
}
|
|
4348
4513
|
}
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
);
|
|
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);
|
|
4353
4519
|
}
|
|
4354
4520
|
var LIKELY_TOO_BROAD = 3;
|
|
4355
4521
|
function applyExemptions(files, patterns) {
|
|
@@ -4374,8 +4540,8 @@ function applyExemptions(files, patterns) {
|
|
|
4374
4540
|
}
|
|
4375
4541
|
|
|
4376
4542
|
// src/check/waiver.ts
|
|
4377
|
-
import { readFileSync as
|
|
4378
|
-
import { join as
|
|
4543
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
4544
|
+
import { join as join22 } from "path";
|
|
4379
4545
|
var WAIVER = /jig-allow\s+([A-Z]-\d+(?:\s*,\s*[A-Z]-\d+)*)\s*:(.*)$/;
|
|
4380
4546
|
function readWaiver(line) {
|
|
4381
4547
|
const m = WAIVER.exec(line);
|
|
@@ -4390,7 +4556,7 @@ function applyWaivers(root, findings) {
|
|
|
4390
4556
|
let cached = lines.get(file);
|
|
4391
4557
|
if (!cached) {
|
|
4392
4558
|
try {
|
|
4393
|
-
cached =
|
|
4559
|
+
cached = readFileSync18(join22(root, file), "utf8").split("\n");
|
|
4394
4560
|
} catch {
|
|
4395
4561
|
cached = [];
|
|
4396
4562
|
}
|
|
@@ -4455,8 +4621,8 @@ function hasMenuToggle(raw) {
|
|
|
4455
4621
|
}
|
|
4456
4622
|
|
|
4457
4623
|
// src/check/mode-wiring.ts
|
|
4458
|
-
import { existsSync as
|
|
4459
|
-
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";
|
|
4460
4626
|
var MODES = ["editorial", "product", "operator"];
|
|
4461
4627
|
function configProblems(config) {
|
|
4462
4628
|
const problems = [];
|
|
@@ -4478,10 +4644,10 @@ function configProblems(config) {
|
|
|
4478
4644
|
}
|
|
4479
4645
|
function modeWiringProblems(projectRoot) {
|
|
4480
4646
|
let config;
|
|
4481
|
-
const path =
|
|
4482
|
-
if (!
|
|
4647
|
+
const path = join23(projectRoot, "jig.config.json");
|
|
4648
|
+
if (!existsSync14(path)) return [];
|
|
4483
4649
|
try {
|
|
4484
|
-
config = JSON.parse(
|
|
4650
|
+
config = JSON.parse(readFileSync19(path, "utf8"));
|
|
4485
4651
|
} catch {
|
|
4486
4652
|
return [{ mode: "", barrel: "jig.config.json", message: "jig.config.json is not valid JSON, so nothing in it applies" }];
|
|
4487
4653
|
}
|
|
@@ -4494,10 +4660,10 @@ function modeWiringProblems(projectRoot) {
|
|
|
4494
4660
|
const rel = (f) => dir === "." ? f : `${dir}/${f}`;
|
|
4495
4661
|
const multi = modes.length > 1;
|
|
4496
4662
|
const barrelOf = (mode) => multi ? `theme.${mode}.css` : "theme.css";
|
|
4497
|
-
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))));
|
|
4498
4664
|
if (!anyBarrel) return [];
|
|
4499
4665
|
const problems = [];
|
|
4500
|
-
if (multi &&
|
|
4666
|
+
if (multi && existsSync14(join23(projectRoot, rel("theme.css")))) {
|
|
4501
4667
|
problems.push({
|
|
4502
4668
|
mode: modes[0],
|
|
4503
4669
|
barrel: rel("theme.css"),
|
|
@@ -4509,7 +4675,7 @@ function modeWiringProblems(projectRoot) {
|
|
|
4509
4675
|
const modeFile = `mode.${mode}.css`;
|
|
4510
4676
|
let body;
|
|
4511
4677
|
try {
|
|
4512
|
-
body =
|
|
4678
|
+
body = readFileSync19(join23(projectRoot, barrel), "utf8");
|
|
4513
4679
|
} catch {
|
|
4514
4680
|
problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${barrel} does not exist` });
|
|
4515
4681
|
return;
|
|
@@ -4521,7 +4687,7 @@ function modeWiringProblems(projectRoot) {
|
|
|
4521
4687
|
barrel,
|
|
4522
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`
|
|
4523
4689
|
});
|
|
4524
|
-
} else if (!
|
|
4690
|
+
} else if (!existsSync14(join23(projectRoot, rel(modeFile)))) {
|
|
4525
4691
|
problems.push({ mode, barrel, message: `jig.config.json declares ${mode}, but ${rel(modeFile)} does not exist` });
|
|
4526
4692
|
}
|
|
4527
4693
|
});
|
|
@@ -4529,8 +4695,8 @@ function modeWiringProblems(projectRoot) {
|
|
|
4529
4695
|
}
|
|
4530
4696
|
|
|
4531
4697
|
// src/check/token-audit.ts
|
|
4532
|
-
import { existsSync as
|
|
4533
|
-
import { join as
|
|
4698
|
+
import { existsSync as existsSync15, readdirSync as readdirSync9, readFileSync as readFileSync20 } from "fs";
|
|
4699
|
+
import { join as join24 } from "path";
|
|
4534
4700
|
var TEXT_FLOOR = 4.5;
|
|
4535
4701
|
var UI_FLOOR = 3;
|
|
4536
4702
|
var FOREGROUNDS = [
|
|
@@ -4586,24 +4752,24 @@ function floorFor(token) {
|
|
|
4586
4752
|
function tokenFiles(projectRoot) {
|
|
4587
4753
|
const read = (rel) => {
|
|
4588
4754
|
try {
|
|
4589
|
-
return
|
|
4755
|
+
return readFileSync20(join24(projectRoot, ...rel.split("/")), "utf8");
|
|
4590
4756
|
} catch {
|
|
4591
4757
|
return null;
|
|
4592
4758
|
}
|
|
4593
4759
|
};
|
|
4594
|
-
const statePath =
|
|
4595
|
-
if (
|
|
4760
|
+
const statePath = join24(projectRoot, ".jig", "state.json");
|
|
4761
|
+
if (existsSync15(statePath)) {
|
|
4596
4762
|
try {
|
|
4597
|
-
const state = JSON.parse(
|
|
4763
|
+
const state = JSON.parse(readFileSync20(statePath, "utf8"));
|
|
4598
4764
|
const recorded = Object.keys(state.files ?? {}).filter((f) => f.endsWith(".css")).sort();
|
|
4599
4765
|
const out = recorded.map((path) => ({ path, source: read(path) })).filter((f) => f.source !== null);
|
|
4600
4766
|
if (out.length > 0) return out;
|
|
4601
4767
|
} catch {
|
|
4602
4768
|
}
|
|
4603
4769
|
}
|
|
4604
|
-
const legacy =
|
|
4605
|
-
if (!
|
|
4606
|
-
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") }));
|
|
4607
4773
|
}
|
|
4608
4774
|
function auditTokenLayer(projectRoot) {
|
|
4609
4775
|
const files = tokenFiles(projectRoot);
|
|
@@ -4671,15 +4837,15 @@ function auditTokenLayer(projectRoot) {
|
|
|
4671
4837
|
}
|
|
4672
4838
|
|
|
4673
4839
|
// src/commands/check.ts
|
|
4674
|
-
var LEGACY_INDEX_REL =
|
|
4840
|
+
var LEGACY_INDEX_REL = join25(".jig", "rules.index.json");
|
|
4675
4841
|
function resolveIndexPath(projectRoot) {
|
|
4676
|
-
const legacy =
|
|
4677
|
-
if (
|
|
4678
|
-
return
|
|
4842
|
+
const legacy = join25(projectRoot, LEGACY_INDEX_REL);
|
|
4843
|
+
if (existsSync16(legacy)) return legacy;
|
|
4844
|
+
return join25(assetRoot(), "rules.index.json");
|
|
4679
4845
|
}
|
|
4680
4846
|
function resolveMode(projectRoot) {
|
|
4681
4847
|
try {
|
|
4682
|
-
const config = JSON.parse(
|
|
4848
|
+
const config = JSON.parse(readFileSync21(join25(projectRoot, "jig.config.json"), "utf8"));
|
|
4683
4849
|
const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
|
|
4684
4850
|
if (modes.length === 1) return modes[0];
|
|
4685
4851
|
return modes.length > 1 ? "mixed" : "unknown";
|
|
@@ -4696,24 +4862,25 @@ function summariseUnscanned(files) {
|
|
|
4696
4862
|
}
|
|
4697
4863
|
function countSpecs() {
|
|
4698
4864
|
try {
|
|
4699
|
-
return loadSpecs(
|
|
4865
|
+
return loadSpecs(join25(assetRoot(), "rules")).length;
|
|
4700
4866
|
} catch {
|
|
4701
4867
|
return 0;
|
|
4702
4868
|
}
|
|
4703
4869
|
}
|
|
4704
4870
|
function check(opts) {
|
|
4705
4871
|
const indexPath = resolveIndexPath(opts.projectRoot);
|
|
4706
|
-
const index = validateIndex(JSON.parse(
|
|
4872
|
+
const index = validateIndex(JSON.parse(readFileSync21(indexPath, "utf8")));
|
|
4707
4873
|
const selection = selectFiles(opts.projectRoot, opts.all);
|
|
4708
|
-
const exemptions = readExemptions(opts.projectRoot);
|
|
4709
|
-
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) : [] }));
|
|
4710
4877
|
const bucketFilter = opts.ci ? (b) => b === "mechanical" : void 0;
|
|
4711
4878
|
const stylesheets = selection.mode === "all" ? files : selectFiles(opts.projectRoot, true).files;
|
|
4712
4879
|
const tokens = loadTokenMap(opts.projectRoot, stylesheets);
|
|
4713
4880
|
const projectParticipates = stylesheets.some((f) => {
|
|
4714
4881
|
if (!hasExtension(f, CSS_EXTENSIONS)) return false;
|
|
4715
4882
|
try {
|
|
4716
|
-
return participatesInTokenLayer(
|
|
4883
|
+
return participatesInTokenLayer(readFileSync21(join25(opts.projectRoot, f), "utf8"), tokens);
|
|
4717
4884
|
} catch {
|
|
4718
4885
|
return false;
|
|
4719
4886
|
}
|
|
@@ -4721,7 +4888,7 @@ function check(opts) {
|
|
|
4721
4888
|
const projectResponsive = stylesheets.some((f) => {
|
|
4722
4889
|
if (!isStyleBearing(f)) return false;
|
|
4723
4890
|
try {
|
|
4724
|
-
const raw =
|
|
4891
|
+
const raw = readFileSync21(join25(opts.projectRoot, f), "utf8");
|
|
4725
4892
|
return isResponsive(maskComments(maskNonStyleRegions(raw, f)), raw);
|
|
4726
4893
|
} catch {
|
|
4727
4894
|
return false;
|
|
@@ -4730,14 +4897,14 @@ function check(opts) {
|
|
|
4730
4897
|
const viewportFitCover = stylesheets.some((f) => {
|
|
4731
4898
|
if (!isStyleBearing(f)) return false;
|
|
4732
4899
|
try {
|
|
4733
|
-
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"));
|
|
4734
4901
|
} catch {
|
|
4735
4902
|
return false;
|
|
4736
4903
|
}
|
|
4737
4904
|
});
|
|
4738
4905
|
const projectMenuToggle = stylesheets.some((f) => {
|
|
4739
4906
|
try {
|
|
4740
|
-
return hasMenuToggle(
|
|
4907
|
+
return hasMenuToggle(readFileSync21(join25(opts.projectRoot, f), "utf8"));
|
|
4741
4908
|
} catch {
|
|
4742
4909
|
return false;
|
|
4743
4910
|
}
|
|
@@ -4745,7 +4912,7 @@ function check(opts) {
|
|
|
4745
4912
|
const declaredProperties = collectDeclaredProperties(opts.projectRoot, stylesheets, Object.keys(tokens));
|
|
4746
4913
|
const withStyles = files.filter((f) => {
|
|
4747
4914
|
try {
|
|
4748
|
-
const src =
|
|
4915
|
+
const src = readFileSync21(join25(opts.projectRoot, f), "utf8");
|
|
4749
4916
|
return hasExtension(f, CSS_EXTENSIONS) ? src.trim().length > 0 : maskNonStyleRegions(src, f).trim().length > 0;
|
|
4750
4917
|
} catch {
|
|
4751
4918
|
return false;
|
|
@@ -4778,7 +4945,7 @@ function check(opts) {
|
|
|
4778
4945
|
// told to add a token @import it has nowhere to put.
|
|
4779
4946
|
cssFiles.length > 0 && cssFiles.every((f) => {
|
|
4780
4947
|
try {
|
|
4781
|
-
return !participatesInTokenLayer(
|
|
4948
|
+
return !participatesInTokenLayer(readFileSync21(join25(opts.projectRoot, f), "utf8"), tokens);
|
|
4782
4949
|
} catch {
|
|
4783
4950
|
return true;
|
|
4784
4951
|
}
|
|
@@ -4798,20 +4965,22 @@ function check(opts) {
|
|
|
4798
4965
|
withStyles,
|
|
4799
4966
|
exempt,
|
|
4800
4967
|
exemptPatterns: byPattern,
|
|
4968
|
+
exemptIgnored,
|
|
4801
4969
|
scope: selection.mode,
|
|
4802
4970
|
modeUnwired: modeWiringProblems(opts.projectRoot),
|
|
4971
|
+
agentFiles: agentFileProblems(opts.projectRoot, opts.homeDir || void 0),
|
|
4803
4972
|
waived
|
|
4804
4973
|
});
|
|
4805
4974
|
return { findings, report: report2, hasError, waived };
|
|
4806
4975
|
}
|
|
4807
4976
|
|
|
4808
4977
|
// src/commands/init.ts
|
|
4809
|
-
import { existsSync as
|
|
4810
|
-
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";
|
|
4811
4980
|
|
|
4812
4981
|
// src/init/detect.ts
|
|
4813
|
-
import { readFileSync as
|
|
4814
|
-
import { join as
|
|
4982
|
+
import { readFileSync as readFileSync22 } from "fs";
|
|
4983
|
+
import { join as join26 } from "path";
|
|
4815
4984
|
var TAILWIND_CONFIG_RE = /^tailwind\.config\.(js|ts|mjs|cjs)$/;
|
|
4816
4985
|
var TAILWIND_V4_IMPORT_RE = /@import\s+["']tailwindcss["']/;
|
|
4817
4986
|
var TAILWIND_V4_THEME_RE = /@theme\b/;
|
|
@@ -4828,7 +4997,7 @@ var FRAMEWORK_DEPS = [
|
|
|
4828
4997
|
function detectFramework(projectRoot) {
|
|
4829
4998
|
let pkg;
|
|
4830
4999
|
try {
|
|
4831
|
-
pkg = JSON.parse(
|
|
5000
|
+
pkg = JSON.parse(readFileSync22(join26(projectRoot, "package.json"), "utf8"));
|
|
4832
5001
|
} catch {
|
|
4833
5002
|
return void 0;
|
|
4834
5003
|
}
|
|
@@ -4846,7 +5015,7 @@ function detect(projectRoot) {
|
|
|
4846
5015
|
for (const f of cssFiles) {
|
|
4847
5016
|
let source;
|
|
4848
5017
|
try {
|
|
4849
|
-
source =
|
|
5018
|
+
source = readFileSync22(join26(projectRoot, f), "utf8");
|
|
4850
5019
|
} catch {
|
|
4851
5020
|
continue;
|
|
4852
5021
|
}
|
|
@@ -4881,8 +5050,8 @@ function isIgnored(projectRoot, relPath) {
|
|
|
4881
5050
|
}
|
|
4882
5051
|
|
|
4883
5052
|
// src/init/derive.ts
|
|
4884
|
-
import { readFileSync as
|
|
4885
|
-
import { join as
|
|
5053
|
+
import { readFileSync as readFileSync23 } from "fs";
|
|
5054
|
+
import { join as join27 } from "path";
|
|
4886
5055
|
var DEFAULT_PROPOSAL = {
|
|
4887
5056
|
h: 264,
|
|
4888
5057
|
s: 0,
|
|
@@ -4915,7 +5084,7 @@ function fromCssCustomProperties(projectRoot, cssFiles) {
|
|
|
4915
5084
|
for (const file of cssFiles) {
|
|
4916
5085
|
let source;
|
|
4917
5086
|
try {
|
|
4918
|
-
source =
|
|
5087
|
+
source = readFileSync23(join27(projectRoot, file), "utf8");
|
|
4919
5088
|
} catch {
|
|
4920
5089
|
continue;
|
|
4921
5090
|
}
|
|
@@ -4964,7 +5133,7 @@ function balancedBraceBody(source, openIdx) {
|
|
|
4964
5133
|
function fromTailwindConfig(projectRoot, tailwindConfigFile) {
|
|
4965
5134
|
let source;
|
|
4966
5135
|
try {
|
|
4967
|
-
source =
|
|
5136
|
+
source = readFileSync23(join27(projectRoot, tailwindConfigFile), "utf8");
|
|
4968
5137
|
} catch {
|
|
4969
5138
|
return null;
|
|
4970
5139
|
}
|
|
@@ -5020,7 +5189,7 @@ function fromLiteralFrequency(projectRoot, cssFiles) {
|
|
|
5020
5189
|
for (const file of cssFiles) {
|
|
5021
5190
|
let source;
|
|
5022
5191
|
try {
|
|
5023
|
-
source =
|
|
5192
|
+
source = readFileSync23(join27(projectRoot, file), "utf8");
|
|
5024
5193
|
} catch {
|
|
5025
5194
|
continue;
|
|
5026
5195
|
}
|
|
@@ -5180,8 +5349,8 @@ function relativeImportPath(fromAbsDir, toAbsFile) {
|
|
|
5180
5349
|
}
|
|
5181
5350
|
|
|
5182
5351
|
// src/init/project-name.ts
|
|
5183
|
-
import { readFileSync as
|
|
5184
|
-
import { basename, join as
|
|
5352
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
5353
|
+
import { basename, join as join28 } from "path";
|
|
5185
5354
|
function slugify(name) {
|
|
5186
5355
|
const withoutScope = name.replace(/^@[^/]+\//, "");
|
|
5187
5356
|
const slug = withoutScope.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -5189,7 +5358,7 @@ function slugify(name) {
|
|
|
5189
5358
|
}
|
|
5190
5359
|
function deriveProjectSlug(projectRoot) {
|
|
5191
5360
|
try {
|
|
5192
|
-
const pkg = JSON.parse(
|
|
5361
|
+
const pkg = JSON.parse(readFileSync24(join28(projectRoot, "package.json"), "utf8"));
|
|
5193
5362
|
if (typeof pkg.name === "string" && pkg.name.trim()) return slugify(pkg.name);
|
|
5194
5363
|
} catch {
|
|
5195
5364
|
}
|
|
@@ -5330,7 +5499,7 @@ function parseSurfaces(answer, fallback) {
|
|
|
5330
5499
|
return parsed.length > 0 ? parsed : fallback;
|
|
5331
5500
|
}
|
|
5332
5501
|
function fileState(projectRoot, absPath, relPath, initManifest) {
|
|
5333
|
-
const existsOnDisk =
|
|
5502
|
+
const existsOnDisk = existsSync17(absPath);
|
|
5334
5503
|
const tracked = !!initManifest && relPath in initManifest.files;
|
|
5335
5504
|
const modified = tracked && isInitFileModified(projectRoot, relPath, initManifest);
|
|
5336
5505
|
return { existsOnDisk, tracked, modified };
|
|
@@ -5341,11 +5510,11 @@ function isValidSurfaceArray(v) {
|
|
|
5341
5510
|
);
|
|
5342
5511
|
}
|
|
5343
5512
|
function loadEffectiveConfig(projectRoot, configAbsPath, fallbackBrand, fallbackSurfaces) {
|
|
5344
|
-
if (!
|
|
5513
|
+
if (!existsSync17(configAbsPath)) return { brand: fallbackBrand, surfaces: fallbackSurfaces };
|
|
5345
5514
|
try {
|
|
5346
|
-
const parsed = JSON.parse(
|
|
5515
|
+
const parsed = JSON.parse(readFileSync25(configAbsPath, "utf8"));
|
|
5347
5516
|
const brandCandidate = typeof parsed.brand === "string" && parsed.brand ? parsed.brand : void 0;
|
|
5348
|
-
const brandResolves = brandCandidate ?
|
|
5517
|
+
const brandResolves = brandCandidate ? existsSync17(join29(projectRoot, ...brandCandidate.split("/"))) : false;
|
|
5349
5518
|
return {
|
|
5350
5519
|
brand: brandResolves ? brandCandidate : fallbackBrand,
|
|
5351
5520
|
surfaces: isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : fallbackSurfaces
|
|
@@ -5355,10 +5524,10 @@ function loadEffectiveConfig(projectRoot, configAbsPath, fallbackBrand, fallback
|
|
|
5355
5524
|
}
|
|
5356
5525
|
}
|
|
5357
5526
|
function configuredBrandPath(projectRoot, configAbsPath) {
|
|
5358
|
-
if (!
|
|
5527
|
+
if (!existsSync17(configAbsPath)) return { path: null };
|
|
5359
5528
|
let declared;
|
|
5360
5529
|
try {
|
|
5361
|
-
declared = JSON.parse(
|
|
5530
|
+
declared = JSON.parse(readFileSync25(configAbsPath, "utf8")).brand;
|
|
5362
5531
|
} catch {
|
|
5363
5532
|
return { path: null };
|
|
5364
5533
|
}
|
|
@@ -5386,9 +5555,9 @@ function defaultTokenDir(detection) {
|
|
|
5386
5555
|
return ["jig"];
|
|
5387
5556
|
}
|
|
5388
5557
|
function readConfiguredSurfaces(configAbsPath) {
|
|
5389
|
-
if (!
|
|
5558
|
+
if (!existsSync17(configAbsPath)) return null;
|
|
5390
5559
|
try {
|
|
5391
|
-
const parsed = JSON.parse(
|
|
5560
|
+
const parsed = JSON.parse(readFileSync25(configAbsPath, "utf8"));
|
|
5392
5561
|
return isValidSurfaceArray(parsed.surfaces) ? parsed.surfaces : null;
|
|
5393
5562
|
} catch {
|
|
5394
5563
|
return null;
|
|
@@ -5398,8 +5567,8 @@ function isCssModule(path) {
|
|
|
5398
5567
|
return /\.module\.(css|scss)$/i.test(path);
|
|
5399
5568
|
}
|
|
5400
5569
|
function legacyBrandFile(projectRoot) {
|
|
5401
|
-
const dir =
|
|
5402
|
-
if (!
|
|
5570
|
+
const dir = join29(projectRoot, ".jig", "tokens");
|
|
5571
|
+
if (!existsSync17(dir)) return void 0;
|
|
5403
5572
|
const found = readdirSync10(dir).find((f) => /^brand\.[\w.-]+\.css$/.test(f));
|
|
5404
5573
|
return found ? relKey(".jig", "tokens", found) : void 0;
|
|
5405
5574
|
}
|
|
@@ -5430,7 +5599,7 @@ var LEGACY_PAIR_RE = new RegExp(
|
|
|
5430
5599
|
`@import\\s+["'][^"']*brand\\.[\\w.-]+\\.css["'];?\\r?\\n?\\s*@import\\s+["'][^"']*mode\\.(?:${MODES2.join("|")})\\.css["'];?\\r?\\n?`
|
|
5431
5600
|
);
|
|
5432
5601
|
function wireBarrel(absPath, barrelImport) {
|
|
5433
|
-
const content =
|
|
5602
|
+
const content = existsSync17(absPath) ? readFileSync25(absPath, "utf8") : "";
|
|
5434
5603
|
const line = `@import "${barrelImport}";`;
|
|
5435
5604
|
if (content.includes(line)) return { status: "already-present" };
|
|
5436
5605
|
const legacy = LEGACY_PAIR_RE.exec(content);
|
|
@@ -5525,7 +5694,7 @@ async function init(opts) {
|
|
|
5525
5694
|
else if (critiqueAnswer) log(` Could not read '${critiqueAnswer}' \u2014 leaving it page by page; each spec will ask.`);
|
|
5526
5695
|
}
|
|
5527
5696
|
if (opts.yes) {
|
|
5528
|
-
const declared = readConfiguredSurfaces(
|
|
5697
|
+
const declared = readConfiguredSurfaces(join29(opts.projectRoot, "jig.config.json"));
|
|
5529
5698
|
const inEffect = declared ?? surfaces;
|
|
5530
5699
|
const map = inEffect.map((x) => `'${x.match}' \u2192 ${x.mode}`).join(", ");
|
|
5531
5700
|
log(
|
|
@@ -5544,7 +5713,7 @@ Brand colour adjusted to clear the 4.5:1 contrast floor: l:${originalL}% \u2192
|
|
|
5544
5713
|
const finalColor = { h: proposal.h, s: proposal.s, l: proposal.l };
|
|
5545
5714
|
const projectSlug = deriveProjectSlug(opts.projectRoot);
|
|
5546
5715
|
const configRelPath = "jig.config.json";
|
|
5547
|
-
const configAbsPath =
|
|
5716
|
+
const configAbsPath = join29(opts.projectRoot, configRelPath);
|
|
5548
5717
|
const configuredBrand = configuredBrandPath(opts.projectRoot, configAbsPath);
|
|
5549
5718
|
if (configuredBrand.refused) {
|
|
5550
5719
|
log(
|
|
@@ -5578,7 +5747,7 @@ The token layer is at ${currentDir}/, and this project's layout suggests ${would
|
|
|
5578
5747
|
}
|
|
5579
5748
|
}
|
|
5580
5749
|
const brandRelPath = relocateTo ?? configuredBrand.path ?? priorBrand ?? relKey(...defaultTokenDir(detection), brandFileName(projectSlug));
|
|
5581
|
-
const brandAbsPath =
|
|
5750
|
+
const brandAbsPath = join29(opts.projectRoot, ...brandRelPath.split("/"));
|
|
5582
5751
|
const tokensRelDir = brandRelPath.split("/").slice(0, -1);
|
|
5583
5752
|
log(
|
|
5584
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.")
|
|
@@ -5611,7 +5780,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5611
5780
|
if (configAction !== "written") log(` ${configRelPath} exists and is not jig-tracked \u2014 leaving it alone (re-run without --yes to refresh).`);
|
|
5612
5781
|
}
|
|
5613
5782
|
if (brandAction === "written") {
|
|
5614
|
-
const defaultCssRaw =
|
|
5783
|
+
const defaultCssRaw = readFileSync25(join29(opts.packageRoot, "tokens", "brand.default.css"), "utf8");
|
|
5615
5784
|
const content = renderBrandFile(defaultCssRaw, projectSlug, opts.version, proposal);
|
|
5616
5785
|
mkdirSync6(dirname7(brandAbsPath), { recursive: true });
|
|
5617
5786
|
writeFileSync4(brandAbsPath, content, "utf8");
|
|
@@ -5619,7 +5788,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5619
5788
|
}
|
|
5620
5789
|
if (relocateTo && configAction !== "written") {
|
|
5621
5790
|
try {
|
|
5622
|
-
const existing = JSON.parse(
|
|
5791
|
+
const existing = JSON.parse(readFileSync25(configAbsPath, "utf8"));
|
|
5623
5792
|
const content = `${JSON.stringify({ ...existing, brand: brandRelPath }, null, 2)}
|
|
5624
5793
|
`;
|
|
5625
5794
|
writeFileSync4(configAbsPath, content, "utf8");
|
|
@@ -5638,12 +5807,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5638
5807
|
} else if (configAction === "merged") {
|
|
5639
5808
|
let existing = {};
|
|
5640
5809
|
try {
|
|
5641
|
-
existing = JSON.parse(
|
|
5810
|
+
existing = JSON.parse(readFileSync25(configAbsPath, "utf8"));
|
|
5642
5811
|
} catch {
|
|
5643
5812
|
existing = {};
|
|
5644
5813
|
}
|
|
5645
5814
|
const existingBrand = typeof existing.brand === "string" ? existing.brand : void 0;
|
|
5646
|
-
const brandStillResolves = existingBrand ?
|
|
5815
|
+
const brandStillResolves = existingBrand ? existsSync17(join29(opts.projectRoot, ...existingBrand.split("/"))) : false;
|
|
5647
5816
|
const existingSurfaces = Array.isArray(existing.surfaces) && existing.surfaces.length > 0 ? existing.surfaces : void 0;
|
|
5648
5817
|
const merged = {
|
|
5649
5818
|
...existing,
|
|
@@ -5660,12 +5829,12 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5660
5829
|
}
|
|
5661
5830
|
const effectiveConfig = loadEffectiveConfig(opts.projectRoot, configAbsPath, brandRelPath, surfaces);
|
|
5662
5831
|
const primaryMode = effectiveConfig.surfaces[0]?.mode ?? "product";
|
|
5663
|
-
const wiringBrandAbsPath =
|
|
5832
|
+
const wiringBrandAbsPath = join29(opts.projectRoot, ...effectiveConfig.brand.split("/"));
|
|
5664
5833
|
const declaredModes = [...new Set(effectiveConfig.surfaces.map((s) => s.mode))];
|
|
5665
5834
|
const modeAbsPaths = {};
|
|
5666
5835
|
for (const mode of declaredModes) {
|
|
5667
5836
|
const modeFileName = `mode.${mode}.css`;
|
|
5668
|
-
const modeAbsPath2 =
|
|
5837
|
+
const modeAbsPath2 = join29(opts.projectRoot, ...tokensRelDir, modeFileName);
|
|
5669
5838
|
modeAbsPaths[mode] = modeAbsPath2;
|
|
5670
5839
|
const modeRelPath = relKey(...tokensRelDir, modeFileName);
|
|
5671
5840
|
const modeState = fileState(opts.projectRoot, modeAbsPath2, modeRelPath, initManifest);
|
|
@@ -5673,7 +5842,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5673
5842
|
if (modeState.existsOnDisk && !modeState.tracked) modeAction = "skipped-untracked";
|
|
5674
5843
|
else if (modeState.modified) modeAction = "skipped-edited";
|
|
5675
5844
|
if (modeAction === "written") {
|
|
5676
|
-
const modeSourceRaw =
|
|
5845
|
+
const modeSourceRaw = readFileSync25(join29(opts.packageRoot, "tokens", modeFileName), "utf8");
|
|
5677
5846
|
const content = vendorHeader(modeFileName, opts.version, "css", null) + modeSourceRaw;
|
|
5678
5847
|
mkdirSync6(dirname7(modeAbsPath2), { recursive: true });
|
|
5679
5848
|
writeFileSync4(modeAbsPath2, content, "utf8");
|
|
@@ -5688,7 +5857,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5688
5857
|
const barrelFor = (mode) => multiMode ? `theme.${mode}.css` : "theme.css";
|
|
5689
5858
|
for (const mode of declaredModes) {
|
|
5690
5859
|
const rel = relKey(...tokensRelDir, barrelFor(mode));
|
|
5691
|
-
const abs =
|
|
5860
|
+
const abs = join29(opts.projectRoot, ...tokensRelDir, barrelFor(mode));
|
|
5692
5861
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
5693
5862
|
if (state.existsOnDisk && (!state.tracked || state.modified)) {
|
|
5694
5863
|
log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
|
|
@@ -5701,8 +5870,8 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5701
5870
|
}
|
|
5702
5871
|
if (multiMode) {
|
|
5703
5872
|
const rel = relKey(...tokensRelDir, "theme.css");
|
|
5704
|
-
const abs =
|
|
5705
|
-
if (
|
|
5873
|
+
const abs = join29(opts.projectRoot, ...tokensRelDir, "theme.css");
|
|
5874
|
+
if (existsSync17(abs)) {
|
|
5706
5875
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
5707
5876
|
if (state.tracked && !state.modified) {
|
|
5708
5877
|
rmSync2(abs);
|
|
@@ -5717,7 +5886,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5717
5886
|
const declared = declaredTokenNames(
|
|
5718
5887
|
[brandAbsPath, modeAbsPath].map((f) => {
|
|
5719
5888
|
try {
|
|
5720
|
-
return
|
|
5889
|
+
return readFileSync25(f, "utf8");
|
|
5721
5890
|
} catch {
|
|
5722
5891
|
return "";
|
|
5723
5892
|
}
|
|
@@ -5742,7 +5911,7 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5742
5911
|
log(` This changes how components are written. Both styles are correct.`);
|
|
5743
5912
|
const answer = (await prompt(" Generate it? [y/N]: ")).toLowerCase();
|
|
5744
5913
|
if (answer === "y" || answer === "yes") {
|
|
5745
|
-
const abs =
|
|
5914
|
+
const abs = join29(opts.projectRoot, ...tokensRelDir, "utilities.css");
|
|
5746
5915
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
5747
5916
|
if (state.existsOnDisk && (!state.tracked || state.modified)) {
|
|
5748
5917
|
log(` ${rel} exists and is not jig-tracked (or has been edited) \u2014 leaving it alone.`);
|
|
@@ -5753,8 +5922,8 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5753
5922
|
files[rel] = checksum(content);
|
|
5754
5923
|
const target = findWireTarget(detection);
|
|
5755
5924
|
const importPath = target ? relativeImportPath(
|
|
5756
|
-
dirname7(
|
|
5757
|
-
|
|
5925
|
+
dirname7(join29(opts.projectRoot, target)),
|
|
5926
|
+
join29(opts.projectRoot, ...tokensRelDir, "utilities.css")
|
|
5758
5927
|
) : `./${rel}`;
|
|
5759
5928
|
log(` Wrote ${rel}. Import it in the stylesheet that imports Tailwind:`);
|
|
5760
5929
|
log(` @import "tailwindcss";`);
|
|
@@ -5773,11 +5942,11 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5773
5942
|
writeInitManifest(opts.projectRoot, { version: opts.version, modes: declaredModes, files });
|
|
5774
5943
|
if (relocateTo && currentBrand) {
|
|
5775
5944
|
const oldDir = currentBrand.split("/").slice(0, -1);
|
|
5776
|
-
const oldAbsDir =
|
|
5777
|
-
if (
|
|
5945
|
+
const oldAbsDir = join29(opts.projectRoot, ...oldDir);
|
|
5946
|
+
if (existsSync17(oldAbsDir)) {
|
|
5778
5947
|
for (const name of readdirSync10(oldAbsDir).filter((f) => f.endsWith(".css"))) {
|
|
5779
5948
|
const rel = relKey(...oldDir, name);
|
|
5780
|
-
const abs =
|
|
5949
|
+
const abs = join29(oldAbsDir, name);
|
|
5781
5950
|
const state = fileState(opts.projectRoot, abs, rel, initManifest);
|
|
5782
5951
|
if (!state.tracked || state.modified) {
|
|
5783
5952
|
log(` ${rel} was edited \u2014 left in place. Delete it yourself once you have moved anything you need.`);
|
|
@@ -5791,11 +5960,11 @@ ${configRelPath} already exists and was not created by 'jig init' (or has been e
|
|
|
5791
5960
|
const wireTarget = findWireTarget(detection);
|
|
5792
5961
|
let wiring;
|
|
5793
5962
|
if (multiMode) {
|
|
5794
|
-
const bare2 =
|
|
5963
|
+
const bare2 = join29(opts.projectRoot, ...tokensRelDir, "theme.css");
|
|
5795
5964
|
for (const file of detection.cssFiles.filter((f) => !isTokenLayerFile(f))) {
|
|
5796
|
-
const abs =
|
|
5965
|
+
const abs = join29(opts.projectRoot, file);
|
|
5797
5966
|
try {
|
|
5798
|
-
const before =
|
|
5967
|
+
const before = readFileSync25(abs, "utf8");
|
|
5799
5968
|
const after = before.split("\n").filter((l) => {
|
|
5800
5969
|
const m = /^\s*@import\s+["']([^"']+)["'];?\s*$/.exec(l);
|
|
5801
5970
|
return !(m && !/^[a-z]+:|^\//i.test(m[1]) && resolve2(dirname7(abs), m[1]) === bare2);
|
|
@@ -5817,21 +5986,21 @@ Could not edit ${file}: ${err.message}. Remove its import of ${relKey(...tokensR
|
|
|
5817
5986
|
snippet: effectiveConfig.surfaces.map((s) => `${s.match} \u2192 ${relKey(...tokensRelDir, barrelFor(s.mode))}`).join("\n")
|
|
5818
5987
|
};
|
|
5819
5988
|
} else if (wireTarget) {
|
|
5820
|
-
const targetAbsDir = dirname7(
|
|
5989
|
+
const targetAbsDir = dirname7(join29(opts.projectRoot, wireTarget));
|
|
5821
5990
|
const barrelImport = relativeImportPath(
|
|
5822
5991
|
targetAbsDir,
|
|
5823
|
-
|
|
5992
|
+
join29(opts.projectRoot, ...tokensRelDir, "theme.css")
|
|
5824
5993
|
);
|
|
5825
5994
|
const snippet = `@import "${barrelImport}";`;
|
|
5826
5995
|
try {
|
|
5827
5996
|
if (relocateTo && currentBrand) {
|
|
5828
|
-
const abs =
|
|
5997
|
+
const abs = join29(opts.projectRoot, wireTarget);
|
|
5829
5998
|
const oldDirName = currentBrand.split("/").slice(0, -1).join("/");
|
|
5830
|
-
const before =
|
|
5999
|
+
const before = existsSync17(abs) ? readFileSync25(abs, "utf8") : "";
|
|
5831
6000
|
const after = before.split("\n").filter((l) => !(l.includes("@import") && l.includes(oldDirName))).join("\n");
|
|
5832
6001
|
if (after !== before) writeFileSync4(abs, after, "utf8");
|
|
5833
6002
|
}
|
|
5834
|
-
const outcome = wireBarrel(
|
|
6003
|
+
const outcome = wireBarrel(join29(opts.projectRoot, wireTarget), barrelImport);
|
|
5835
6004
|
wiring = { target: wireTarget, status: outcome.status, snippet };
|
|
5836
6005
|
const verb = outcome.status === "wired" ? "Wired" : outcome.status === "rewired" ? "Rewired" : "Already present in";
|
|
5837
6006
|
log(`
|
|
@@ -5848,7 +6017,7 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
5848
6017
|
} else {
|
|
5849
6018
|
const barrelImport = relativeImportPath(
|
|
5850
6019
|
opts.projectRoot,
|
|
5851
|
-
|
|
6020
|
+
join29(opts.projectRoot, ...tokensRelDir, "theme.css")
|
|
5852
6021
|
);
|
|
5853
6022
|
const snippet = `@import "${barrelImport}";`;
|
|
5854
6023
|
wiring = { target: null, status: "print-only", snippet };
|
|
@@ -5857,7 +6026,7 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
5857
6026
|
if (pasteTargets.length > 0) {
|
|
5858
6027
|
log("Paste the block for whichever of these is your global stylesheet:");
|
|
5859
6028
|
for (const target of pasteTargets) {
|
|
5860
|
-
const dir = dirname7(
|
|
6029
|
+
const dir = dirname7(join29(opts.projectRoot, target));
|
|
5861
6030
|
const forTarget = `@import "${relativeImportPath(dir, wiringBrandAbsPath)}";
|
|
5862
6031
|
@import "${relativeImportPath(dir, modeAbsPath)}";`;
|
|
5863
6032
|
log(`
|
|
@@ -5883,6 +6052,9 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
5883
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."
|
|
5884
6053
|
);
|
|
5885
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.`);
|
|
5886
6058
|
const baselineResult = check({ projectRoot: opts.projectRoot, homeDir: opts.homeDir, version: opts.version, all: true, ci: false });
|
|
5887
6059
|
log("\nBaseline:");
|
|
5888
6060
|
log(baselineResult.report);
|
|
@@ -5905,28 +6077,28 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
5905
6077
|
|
|
5906
6078
|
// src/commands/verdicts.ts
|
|
5907
6079
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
5908
|
-
import { existsSync as
|
|
5909
|
-
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";
|
|
5910
6082
|
|
|
5911
6083
|
// src/rules/citations.ts
|
|
5912
|
-
import { join as
|
|
6084
|
+
import { join as join30 } from "path";
|
|
5913
6085
|
function citableIds(packageRoot2) {
|
|
5914
|
-
const rulesDir =
|
|
6086
|
+
const rulesDir = join30(packageRoot2, "rules");
|
|
5915
6087
|
const ids = [
|
|
5916
|
-
...loadRules(rulesDir,
|
|
6088
|
+
...loadRules(rulesDir, join30(packageRoot2, "rules.index.json")).map((r) => r.id),
|
|
5917
6089
|
...loadSpecs(rulesDir).map((s) => s.id)
|
|
5918
6090
|
];
|
|
5919
6091
|
return [...new Set(ids)].sort();
|
|
5920
6092
|
}
|
|
5921
6093
|
|
|
5922
6094
|
// src/probe/check.ts
|
|
5923
|
-
import { existsSync as
|
|
5924
|
-
import { join as
|
|
6095
|
+
import { existsSync as existsSync21, readdirSync as readdirSync13, readFileSync as readFileSync29 } from "fs";
|
|
6096
|
+
import { join as join35 } from "path";
|
|
5925
6097
|
|
|
5926
6098
|
// src/probe/save.ts
|
|
5927
|
-
import { existsSync as
|
|
6099
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync7, readdirSync as readdirSync12, readFileSync as readFileSync28, writeFileSync as writeFileSync5 } from "fs";
|
|
5928
6100
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5929
|
-
import { join as
|
|
6101
|
+
import { join as join34, relative as relative3, resolve as resolve4, sep as sep5 } from "path";
|
|
5930
6102
|
|
|
5931
6103
|
// src/probe/script.ts
|
|
5932
6104
|
var PROBE_VERSION = 9;
|
|
@@ -6158,24 +6330,28 @@ var PROBE_SCRIPT = `(async () => {
|
|
|
6158
6330
|
|
|
6159
6331
|
// src/probe/browser.ts
|
|
6160
6332
|
import { spawn } from "child_process";
|
|
6161
|
-
import { existsSync as
|
|
6162
|
-
import { homedir, tmpdir } from "os";
|
|
6163
|
-
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";
|
|
6164
6336
|
var CHROME_ENV = ["JIG_CHROME", "CHROME_PATH", "PUPPETEER_EXECUTABLE_PATH", "CHROMIUM_PATH"];
|
|
6165
|
-
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"];
|
|
6166
6338
|
var CHROME_APPS = [
|
|
6167
6339
|
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
6168
6340
|
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
6169
6341
|
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
6170
6342
|
"C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
|
|
6171
|
-
"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")] : []
|
|
6172
6348
|
];
|
|
6173
6349
|
function cachedBrowsers() {
|
|
6174
6350
|
const roots = [
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
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")
|
|
6179
6355
|
];
|
|
6180
6356
|
const found = [];
|
|
6181
6357
|
for (const root of roots) {
|
|
@@ -6195,8 +6371,8 @@ function cachedBrowsers() {
|
|
|
6195
6371
|
["chrome-win", "chrome.exe"],
|
|
6196
6372
|
["chrome-win64", "chrome.exe"]
|
|
6197
6373
|
]) {
|
|
6198
|
-
const candidate =
|
|
6199
|
-
if (
|
|
6374
|
+
const candidate = join31(root, entry, ...rel);
|
|
6375
|
+
if (existsSync18(candidate)) found.push(candidate);
|
|
6200
6376
|
}
|
|
6201
6377
|
}
|
|
6202
6378
|
}
|
|
@@ -6205,21 +6381,25 @@ function cachedBrowsers() {
|
|
|
6205
6381
|
function findChrome() {
|
|
6206
6382
|
for (const key of CHROME_ENV) {
|
|
6207
6383
|
const value = process.env[key];
|
|
6208
|
-
if (value &&
|
|
6384
|
+
if (value && existsSync18(value)) return value;
|
|
6209
6385
|
}
|
|
6210
6386
|
for (const command of CHROME_COMMANDS) {
|
|
6211
6387
|
const resolved = which(command);
|
|
6212
6388
|
if (resolved) return resolved;
|
|
6213
6389
|
}
|
|
6214
|
-
for (const app of CHROME_APPS) if (
|
|
6390
|
+
for (const app of CHROME_APPS) if (existsSync18(app)) return app;
|
|
6215
6391
|
return cachedBrowsers()[0];
|
|
6216
6392
|
}
|
|
6217
6393
|
function which(command) {
|
|
6218
|
-
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) : [""];
|
|
6219
6397
|
for (const dir of dirs) {
|
|
6220
6398
|
if (!dir) continue;
|
|
6221
|
-
const
|
|
6222
|
-
|
|
6399
|
+
for (const ext of exts) {
|
|
6400
|
+
const candidate = join31(dir, command + ext);
|
|
6401
|
+
if (existsSync18(candidate)) return candidate;
|
|
6402
|
+
}
|
|
6223
6403
|
}
|
|
6224
6404
|
return void 0;
|
|
6225
6405
|
}
|
|
@@ -6230,7 +6410,7 @@ async function runProbe(opts) {
|
|
|
6230
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."
|
|
6231
6411
|
);
|
|
6232
6412
|
}
|
|
6233
|
-
const profile = mkdtempSync(
|
|
6413
|
+
const profile = mkdtempSync(join31(tmpdir(), "jig-probe-"));
|
|
6234
6414
|
const child = spawn(chrome, [
|
|
6235
6415
|
"--headless=new",
|
|
6236
6416
|
"--remote-debugging-port=0",
|
|
@@ -6274,6 +6454,9 @@ async function runProbe(opts) {
|
|
|
6274
6454
|
}
|
|
6275
6455
|
}
|
|
6276
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
|
+
}
|
|
6277
6460
|
const socket = new WebSocket(endpoint);
|
|
6278
6461
|
let nextId = 1;
|
|
6279
6462
|
const pending = /* @__PURE__ */ new Map();
|
|
@@ -6331,8 +6514,8 @@ async function evaluate(endpoint, url, width, height, timeoutMs, expression) {
|
|
|
6331
6514
|
|
|
6332
6515
|
// src/probe/serve.ts
|
|
6333
6516
|
import { createServer } from "http";
|
|
6334
|
-
import { existsSync as
|
|
6335
|
-
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";
|
|
6336
6519
|
var TYPES = {
|
|
6337
6520
|
".html": "text/html; charset=utf-8",
|
|
6338
6521
|
".htm": "text/html; charset=utf-8",
|
|
@@ -6359,19 +6542,19 @@ async function serveDirectory(dir) {
|
|
|
6359
6542
|
const root = resolve3(dir);
|
|
6360
6543
|
const server = createServer((req, res) => {
|
|
6361
6544
|
let path = decodeURIComponent((req.url ?? "/").split("?")[0].split("#")[0]);
|
|
6362
|
-
let file = resolve3(
|
|
6545
|
+
let file = resolve3(join32(root, path));
|
|
6363
6546
|
if (file !== root && !file.startsWith(root + sep4)) {
|
|
6364
6547
|
res.writeHead(403).end();
|
|
6365
6548
|
return;
|
|
6366
6549
|
}
|
|
6367
|
-
if (
|
|
6368
|
-
else if (!
|
|
6369
|
-
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)) {
|
|
6370
6553
|
res.writeHead(404).end();
|
|
6371
6554
|
return;
|
|
6372
6555
|
}
|
|
6373
6556
|
res.writeHead(200, { "content-type": TYPES[extname(file).toLowerCase()] ?? "application/octet-stream" });
|
|
6374
|
-
res.end(
|
|
6557
|
+
res.end(readFileSync26(file));
|
|
6375
6558
|
});
|
|
6376
6559
|
await new Promise((done) => server.listen(0, "127.0.0.1", done));
|
|
6377
6560
|
const address = server.address();
|
|
@@ -6383,8 +6566,8 @@ async function serveDirectory(dir) {
|
|
|
6383
6566
|
}
|
|
6384
6567
|
|
|
6385
6568
|
// src/probe/switches.ts
|
|
6386
|
-
import { readFileSync as
|
|
6387
|
-
import { join as
|
|
6569
|
+
import { readFileSync as readFileSync27 } from "fs";
|
|
6570
|
+
import { join as join33 } from "path";
|
|
6388
6571
|
var DECLARATION = /--breakpoint-([a-z0-9-]+)\s*:\s*(\d+(?:\.\d+)?)(px|rem)\b/g;
|
|
6389
6572
|
function declaredSwitches(projectRoot) {
|
|
6390
6573
|
const found = /* @__PURE__ */ new Map();
|
|
@@ -6392,7 +6575,7 @@ function declaredSwitches(projectRoot) {
|
|
|
6392
6575
|
if (!/\.(css|scss|astro|vue|svelte|html)$/i.test(file)) continue;
|
|
6393
6576
|
let text;
|
|
6394
6577
|
try {
|
|
6395
|
-
text =
|
|
6578
|
+
text = readFileSync27(join33(projectRoot, file), "utf8");
|
|
6396
6579
|
} catch {
|
|
6397
6580
|
continue;
|
|
6398
6581
|
}
|
|
@@ -6427,18 +6610,18 @@ function saveProbe(opts) {
|
|
|
6427
6610
|
}
|
|
6428
6611
|
probe.pageFile = relative3(opts.projectRoot, page).split("\\").join("/");
|
|
6429
6612
|
if (opts.serveRoot) probe.serveRoot = relative3(opts.projectRoot, resolve4(opts.projectRoot, opts.serveRoot)).split("\\").join("/") || ".";
|
|
6430
|
-
probe.pageChecksum = checksum(
|
|
6613
|
+
probe.pageChecksum = checksum(readFileSync28(page, "utf8"));
|
|
6431
6614
|
probe.recordedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6432
|
-
const dir =
|
|
6615
|
+
const dir = join34(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
6433
6616
|
mkdirSync7(dir, { recursive: true });
|
|
6434
|
-
const path =
|
|
6617
|
+
const path = join34(dir, `probe-${width}.json`);
|
|
6435
6618
|
writeFileSync5(path, JSON.stringify(probe), "utf8");
|
|
6436
6619
|
return { path: `.jig/critique/${opts.surface}/probe-${width}.json`, width, page: probe.pageFile };
|
|
6437
6620
|
}
|
|
6438
6621
|
function projectFile(projectRoot, path) {
|
|
6439
6622
|
const abs = resolve4(projectRoot, path);
|
|
6440
6623
|
if (abs !== resolve4(projectRoot) && !abs.startsWith(resolve4(projectRoot) + sep5)) return void 0;
|
|
6441
|
-
return
|
|
6624
|
+
return existsSync20(abs) ? abs : void 0;
|
|
6442
6625
|
}
|
|
6443
6626
|
function pageFile(projectRoot, url) {
|
|
6444
6627
|
if (!url.startsWith("file://")) return void 0;
|
|
@@ -6450,12 +6633,12 @@ function pageFile(projectRoot, url) {
|
|
|
6450
6633
|
}
|
|
6451
6634
|
const abs = resolve4(path);
|
|
6452
6635
|
if (!abs.startsWith(resolve4(projectRoot))) return void 0;
|
|
6453
|
-
return
|
|
6636
|
+
return existsSync20(abs) ? abs : void 0;
|
|
6454
6637
|
}
|
|
6455
6638
|
var PROBE_WIDTHS = [360, 768, 1280, 1600];
|
|
6456
6639
|
async function runAndSaveProbes(opts) {
|
|
6457
6640
|
const abs = resolve4(opts.projectRoot, opts.page);
|
|
6458
|
-
if (!
|
|
6641
|
+
if (!existsSync20(abs)) throw new Error(`${opts.page} does not exist, so there is nothing to render.`);
|
|
6459
6642
|
const saved = [];
|
|
6460
6643
|
const widths = opts.widths ?? probeWidths(PROBE_WIDTHS, declaredSwitches(opts.projectRoot));
|
|
6461
6644
|
await withPageUrl(opts.projectRoot, abs, opts.serve, async (url) => {
|
|
@@ -6469,7 +6652,7 @@ async function runAndSaveProbes(opts) {
|
|
|
6469
6652
|
async function withPageUrl(projectRoot, abs, serve, use) {
|
|
6470
6653
|
if (!serve) return use(`file://${abs}`);
|
|
6471
6654
|
const root = resolve4(projectRoot, serve);
|
|
6472
|
-
if (!
|
|
6655
|
+
if (!existsSync20(root)) throw new Error(`${serve} does not exist, so there is nothing to serve.`);
|
|
6473
6656
|
if (!abs.startsWith(root + sep5)) throw new Error(`The page is not inside ${serve}, so serving ${serve} cannot show it.`);
|
|
6474
6657
|
const server = await serveDirectory(root);
|
|
6475
6658
|
try {
|
|
@@ -6480,31 +6663,31 @@ async function withPageUrl(projectRoot, abs, serve, use) {
|
|
|
6480
6663
|
}
|
|
6481
6664
|
}
|
|
6482
6665
|
function recordedPage(projectRoot, surface) {
|
|
6483
|
-
const dir =
|
|
6484
|
-
if (!
|
|
6666
|
+
const dir = join34(projectRoot, ".jig", "critique", surface);
|
|
6667
|
+
if (!existsSync20(dir)) return void 0;
|
|
6485
6668
|
for (const f of readdirSync12(dir).filter((name) => /^probe-\d+\.json$/.test(name)).sort()) {
|
|
6486
6669
|
try {
|
|
6487
|
-
const probe = JSON.parse(
|
|
6488
|
-
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;
|
|
6489
6672
|
} catch {
|
|
6490
6673
|
}
|
|
6491
6674
|
}
|
|
6492
6675
|
return void 0;
|
|
6493
6676
|
}
|
|
6494
6677
|
async function ensureProbes(opts) {
|
|
6495
|
-
const dir =
|
|
6678
|
+
const dir = join34(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
6496
6679
|
const abs = resolve4(opts.projectRoot, opts.page);
|
|
6497
6680
|
let current;
|
|
6498
6681
|
try {
|
|
6499
|
-
current = checksum(
|
|
6682
|
+
current = checksum(readFileSync28(abs, "utf8"));
|
|
6500
6683
|
} catch {
|
|
6501
6684
|
return { recorded: [], reason: `${opts.page} could not be read` };
|
|
6502
6685
|
}
|
|
6503
|
-
const recorded =
|
|
6686
|
+
const recorded = existsSync20(dir) ? readdirSync12(dir).map((f) => /^probe-(\d+)\.json$/.exec(f)?.[1]).filter((w) => !!w).map(Number) : [];
|
|
6504
6687
|
const widths = [.../* @__PURE__ */ new Set([...probeWidths(PROBE_WIDTHS, declaredSwitches(opts.projectRoot)), ...recorded])].sort((a, b) => a - b);
|
|
6505
6688
|
const missing = widths.filter((width) => {
|
|
6506
6689
|
try {
|
|
6507
|
-
const probe = JSON.parse(
|
|
6690
|
+
const probe = JSON.parse(readFileSync28(join34(dir, `probe-${width}.json`), "utf8"));
|
|
6508
6691
|
return probe.jigProbe !== PROBE_VERSION || probe.pageChecksum !== current;
|
|
6509
6692
|
} catch {
|
|
6510
6693
|
return true;
|
|
@@ -6515,7 +6698,7 @@ async function ensureProbes(opts) {
|
|
|
6515
6698
|
let serve;
|
|
6516
6699
|
for (const width of widths) {
|
|
6517
6700
|
try {
|
|
6518
|
-
const probe = JSON.parse(
|
|
6701
|
+
const probe = JSON.parse(readFileSync28(join34(dir, `probe-${width}.json`), "utf8"));
|
|
6519
6702
|
if (probe.serveRoot) {
|
|
6520
6703
|
serve = probe.serveRoot;
|
|
6521
6704
|
break;
|
|
@@ -6531,9 +6714,9 @@ async function ensureProbes(opts) {
|
|
|
6531
6714
|
return { recorded: missing };
|
|
6532
6715
|
}
|
|
6533
6716
|
function critiquedSurfaces(projectRoot) {
|
|
6534
|
-
const root =
|
|
6535
|
-
if (!
|
|
6536
|
-
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"))));
|
|
6537
6720
|
}
|
|
6538
6721
|
|
|
6539
6722
|
// src/probe/check.ts
|
|
@@ -6545,7 +6728,7 @@ function stampProblem(projectRoot, file, p) {
|
|
|
6545
6728
|
if (!page) return `${file} names a page outside this project (${p.serveRoot ? p.pageFile : p.url ?? "no url"}).`;
|
|
6546
6729
|
let current;
|
|
6547
6730
|
try {
|
|
6548
|
-
current =
|
|
6731
|
+
current = readFileSync29(page, "utf8");
|
|
6549
6732
|
} catch {
|
|
6550
6733
|
return `${file} was taken on ${p.pageFile}, which no longer exists.`;
|
|
6551
6734
|
}
|
|
@@ -6555,11 +6738,11 @@ function stampProblem(projectRoot, file, p) {
|
|
|
6555
6738
|
return void 0;
|
|
6556
6739
|
}
|
|
6557
6740
|
function readProbes(projectRoot, dir, errors) {
|
|
6558
|
-
if (!
|
|
6741
|
+
if (!existsSync21(dir)) return [];
|
|
6559
6742
|
const probes = [];
|
|
6560
6743
|
for (const f of readdirSync13(dir).filter((n) => /^probe-\d+\.json$/.test(n)).sort()) {
|
|
6561
6744
|
try {
|
|
6562
|
-
const p = JSON.parse(
|
|
6745
|
+
const p = JSON.parse(readFileSync29(join35(dir, f), "utf8"));
|
|
6563
6746
|
if (typeof p.jigProbe === "number" && p.jigProbe < PROBE_VERSION && typeof p.width === "number") {
|
|
6564
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\`.`);
|
|
6565
6748
|
continue;
|
|
@@ -6663,19 +6846,19 @@ function probeContradictions(probes, verdictOf, indexable = true) {
|
|
|
6663
6846
|
}
|
|
6664
6847
|
|
|
6665
6848
|
// src/check/decisions.ts
|
|
6666
|
-
import { existsSync as
|
|
6667
|
-
import { join as
|
|
6849
|
+
import { existsSync as existsSync22, readFileSync as readFileSync30 } from "fs";
|
|
6850
|
+
import { join as join36, posix as posix3 } from "path";
|
|
6668
6851
|
var HEADING4 = /^(#{2,3})\s+(.+?)\s*$/gm;
|
|
6669
6852
|
var NOT_A_DECISION = /^(unresolved|open questions?|undecided|contents?|index)$/i;
|
|
6670
6853
|
function decisionsFile(projectRoot) {
|
|
6671
6854
|
const candidates = ["jig/DECISIONS.md", "DECISIONS.md", "src/jig/DECISIONS.md", "src/styles/jig/DECISIONS.md", ".jig/DECISIONS.md"];
|
|
6672
6855
|
try {
|
|
6673
|
-
const brand = JSON.parse(
|
|
6856
|
+
const brand = JSON.parse(readFileSync30(join36(projectRoot, "jig.config.json"), "utf8")).brand;
|
|
6674
6857
|
if (typeof brand === "string" && brand.includes("/")) candidates.unshift(`${posix3.dirname(brand.replace(/^\.\//, ""))}/DECISIONS.md`);
|
|
6675
6858
|
} catch {
|
|
6676
6859
|
}
|
|
6677
6860
|
for (const candidate of candidates) {
|
|
6678
|
-
if (
|
|
6861
|
+
if (existsSync22(join36(projectRoot, candidate))) return candidate;
|
|
6679
6862
|
}
|
|
6680
6863
|
return void 0;
|
|
6681
6864
|
}
|
|
@@ -6688,7 +6871,7 @@ function decisionHeadings(projectRoot) {
|
|
|
6688
6871
|
if (!path) return headings;
|
|
6689
6872
|
let body;
|
|
6690
6873
|
try {
|
|
6691
|
-
body =
|
|
6874
|
+
body = readText(join36(projectRoot, path));
|
|
6692
6875
|
} catch {
|
|
6693
6876
|
return headings;
|
|
6694
6877
|
}
|
|
@@ -6746,7 +6929,7 @@ function unsourcedReasons(current, before, opts = {}) {
|
|
|
6746
6929
|
}
|
|
6747
6930
|
return problems;
|
|
6748
6931
|
}
|
|
6749
|
-
function quotesNotFrom(current, before, words) {
|
|
6932
|
+
function quotesNotFrom(current, before, words, source = "the owner's words in tweak.json (`change`)") {
|
|
6750
6933
|
const problems = [];
|
|
6751
6934
|
for (const [name, whys] of changedWhys(current, before, true)) {
|
|
6752
6935
|
for (const why of whys) {
|
|
@@ -6754,7 +6937,7 @@ function quotesNotFrom(current, before, words) {
|
|
|
6754
6937
|
if (/inferred/i.test(label)) continue;
|
|
6755
6938
|
const missing = [...why.matchAll(/["“]([^"”]{3,})["”]/g)].map((m) => m[1]).filter((q) => !quoteHeld(q, words));
|
|
6756
6939
|
if (missing.length) {
|
|
6757
|
-
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):**\`.`);
|
|
6758
6941
|
break;
|
|
6759
6942
|
}
|
|
6760
6943
|
}
|
|
@@ -6768,25 +6951,25 @@ function quoteHeld(quote, words) {
|
|
|
6768
6951
|
}
|
|
6769
6952
|
|
|
6770
6953
|
// src/check/spec-shape.ts
|
|
6771
|
-
import { existsSync as
|
|
6772
|
-
import { join as
|
|
6954
|
+
import { existsSync as existsSync23, readdirSync as readdirSync14, statSync as statSync4 } from "fs";
|
|
6955
|
+
import { join as join37 } from "path";
|
|
6773
6956
|
function newestSpec(projectRoot) {
|
|
6774
|
-
const dir =
|
|
6775
|
-
if (!
|
|
6957
|
+
const dir = join37(projectRoot, ".jig", "specs");
|
|
6958
|
+
if (!existsSync23(dir)) return void 0;
|
|
6776
6959
|
const files = readdirSync14(dir).filter((f) => f.endsWith(".md"));
|
|
6777
6960
|
if (files.length === 0) return void 0;
|
|
6778
|
-
const newest = files.map((f) => ({ f, at: statSync4(
|
|
6779
|
-
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)) };
|
|
6780
6963
|
}
|
|
6781
6964
|
function specFor(projectRoot, surface) {
|
|
6782
6965
|
if (surface) {
|
|
6783
|
-
const path =
|
|
6784
|
-
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) };
|
|
6785
6968
|
}
|
|
6786
6969
|
return newestSpec(projectRoot);
|
|
6787
6970
|
}
|
|
6788
6971
|
function specProblems(spec) {
|
|
6789
|
-
const parts = spec.body.split(/^---\s*$/m);
|
|
6972
|
+
const parts = lf(spec.body).split(/^---\s*$/m);
|
|
6790
6973
|
const front = parts.length >= 3 ? parts[1] : "";
|
|
6791
6974
|
if (!front.trim()) {
|
|
6792
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.`];
|
|
@@ -6804,7 +6987,7 @@ function specProblems(spec) {
|
|
|
6804
6987
|
if (motion) {
|
|
6805
6988
|
const inline = motion[1].replace(/\s+#.*$/, "").trim();
|
|
6806
6989
|
const items = motion[2].split("\n").map((l) => l.replace(/^[ \t]+-\s*/, "").trim()).filter(Boolean);
|
|
6807
|
-
if (!items.length && !/^none
|
|
6990
|
+
if (!items.length && !/^none\b(\s*$|\s*[—–:,;(-])/i.test(inline)) {
|
|
6808
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\`.`);
|
|
6809
6992
|
}
|
|
6810
6993
|
const unsaid = items.filter((i) => !/\bon\b/i.test(i) || !/\btells?\b/i.test(i));
|
|
@@ -6875,6 +7058,41 @@ function specIndexableField(front) {
|
|
|
6875
7058
|
if (value === "false" || value === "no") return false;
|
|
6876
7059
|
return "unreadable";
|
|
6877
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
|
+
}
|
|
6878
7096
|
|
|
6879
7097
|
// src/commands/verdicts.ts
|
|
6880
7098
|
var VERDICTS = ["ok", "finding", "n/a"];
|
|
@@ -6901,19 +7119,19 @@ function reasonProblems(file, judged, errors) {
|
|
|
6901
7119
|
}
|
|
6902
7120
|
}
|
|
6903
7121
|
function readJson(path, errors) {
|
|
6904
|
-
if (!
|
|
7122
|
+
if (!existsSync24(path)) return null;
|
|
6905
7123
|
try {
|
|
6906
|
-
return JSON.parse(
|
|
7124
|
+
return JSON.parse(readFileSync32(path, "utf8"));
|
|
6907
7125
|
} catch (e) {
|
|
6908
7126
|
errors.push(`${path}: not valid JSON (${e.message})`);
|
|
6909
7127
|
return {};
|
|
6910
7128
|
}
|
|
6911
7129
|
}
|
|
6912
7130
|
function specIndexable(projectRoot, surface) {
|
|
6913
|
-
const path =
|
|
7131
|
+
const path = join38(projectRoot, ".jig", "specs", `${surface}.spec.md`);
|
|
6914
7132
|
let front = "";
|
|
6915
7133
|
try {
|
|
6916
|
-
front =
|
|
7134
|
+
front = readText(path).split(/^---\s*$/m)[1] ?? "";
|
|
6917
7135
|
} catch {
|
|
6918
7136
|
}
|
|
6919
7137
|
const declared = specIndexableField(front);
|
|
@@ -6922,13 +7140,17 @@ function specIndexable(projectRoot, surface) {
|
|
|
6922
7140
|
return mode !== "product" && mode !== "operator";
|
|
6923
7141
|
}
|
|
6924
7142
|
function specNeedsNav(projectRoot, surface) {
|
|
6925
|
-
const path =
|
|
6926
|
-
if (!
|
|
6927
|
-
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] ?? "";
|
|
6928
7146
|
const navField = [...front.matchAll(/^\s*nav:\s*(.+)$/gim)].some((m) => !/^\s*(none|n\/a|-)\b/i.test(m[1]));
|
|
6929
7147
|
const navRegion = /^\s*-\s*(nav|navigation)\s*:/im.test(front);
|
|
6930
7148
|
return navField || navRegion;
|
|
6931
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
|
+
}
|
|
6932
7154
|
function checkArm(name, file, required, otherPass, extraAllowed, extraRequired, errors, decisionList = []) {
|
|
6933
7155
|
const total = required.length + extraRequired.length;
|
|
6934
7156
|
if (file === null) {
|
|
@@ -6986,6 +7208,7 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
|
|
|
6986
7208
|
} else findings++;
|
|
6987
7209
|
}
|
|
6988
7210
|
}
|
|
7211
|
+
errors.push(...unreadFields(`${name}.json`, file, ["verdicts", "differences", "rendered", "artefacts"]));
|
|
6989
7212
|
const missing = [...required, ...extraRequired].filter((id) => !seen.has(id));
|
|
6990
7213
|
if (missing.length) {
|
|
6991
7214
|
errors.push(`${name}.json: ${missing.length} of ${total} ids have no verdict: ${missing.join(", ")}. Re-run the arm; never report a short pass.`);
|
|
@@ -6996,7 +7219,7 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
|
|
|
6996
7219
|
function checkDecisions(projectRoot, dir, errors) {
|
|
6997
7220
|
const required = decisionNames(projectRoot);
|
|
6998
7221
|
if (required.length === 0) return { state: "ran", judged: 0, total: 0, findings: 0 };
|
|
6999
|
-
const file = readJson(
|
|
7222
|
+
const file = readJson(join38(dir, "decisions.json"), errors);
|
|
7000
7223
|
if (!file) {
|
|
7001
7224
|
errors.push(
|
|
7002
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.`
|
|
@@ -7005,6 +7228,7 @@ function checkDecisions(projectRoot, dir, errors) {
|
|
|
7005
7228
|
}
|
|
7006
7229
|
const list = Array.isArray(file.verdicts) ? file.verdicts : [];
|
|
7007
7230
|
if (!Array.isArray(file.verdicts)) errors.push('decisions.json has no "verdicts" array.');
|
|
7231
|
+
errors.push(...unreadFields("decisions.json", file, ["verdicts"]));
|
|
7008
7232
|
const seen = /* @__PURE__ */ new Set();
|
|
7009
7233
|
const reasons = [];
|
|
7010
7234
|
let findings = 0;
|
|
@@ -7050,7 +7274,7 @@ function decisionsSince(projectRoot, dir, unjudged) {
|
|
|
7050
7274
|
if (!path) return [];
|
|
7051
7275
|
const git = (args) => execFileSync3("git", args, { cwd: projectRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
7052
7276
|
try {
|
|
7053
|
-
const verdictFile = relative4(projectRoot,
|
|
7277
|
+
const verdictFile = relative4(projectRoot, join38(dir, "decisions.json")).split(sep6).join("/");
|
|
7054
7278
|
if (git(["status", "--porcelain", "--", verdictFile])) return [];
|
|
7055
7279
|
const judgedIn = git(["log", "-1", "--format=%H", "--", verdictFile]);
|
|
7056
7280
|
if (!judgedIn) return [];
|
|
@@ -7074,7 +7298,7 @@ function decisionsSince(projectRoot, dir, unjudged) {
|
|
|
7074
7298
|
function verifyVerdicts(opts) {
|
|
7075
7299
|
const root = opts.packageRoot ?? assetRoot();
|
|
7076
7300
|
const errors = [];
|
|
7077
|
-
const index = JSON.parse(
|
|
7301
|
+
const index = JSON.parse(readFileSync32(join38(root, "rules.index.json"), "utf8"));
|
|
7078
7302
|
const judgment = index.filter((r) => r.bucket === "judgment" || r.bucket === "hybrid" && r.pass !== void 0);
|
|
7079
7303
|
const screenIds = judgment.filter((r) => r.pass === "screen").map((r) => r.id);
|
|
7080
7304
|
const codeIds = judgment.filter((r) => r.pass === "code").map((r) => r.id);
|
|
@@ -7083,9 +7307,9 @@ function verifyVerdicts(opts) {
|
|
|
7083
7307
|
...judgment.map((r) => [r.id, r.pass ?? ""])
|
|
7084
7308
|
]);
|
|
7085
7309
|
const specIds = new Set(citableIds(root).filter((id) => /^[PMLRT]-\d+$/.test(id)));
|
|
7086
|
-
const dir =
|
|
7087
|
-
const screenFile = readJson(
|
|
7088
|
-
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);
|
|
7089
7313
|
const screenExtraRequired = specNeedsNav(opts.projectRoot, opts.surface) ? ["P-14"] : [];
|
|
7090
7314
|
const decisionList = decisionNames(opts.projectRoot);
|
|
7091
7315
|
const screen = checkArm("screen", screenFile, screenIds, passOf, specIds, screenExtraRequired, errors, decisionList);
|
|
@@ -7093,7 +7317,7 @@ function verifyVerdicts(opts) {
|
|
|
7093
7317
|
let rendered = false;
|
|
7094
7318
|
if (screenFile && screenFile.rendered === true) {
|
|
7095
7319
|
const artefacts = Array.isArray(screenFile.artefacts) ? screenFile.artefacts.filter((a) => typeof a === "string") : [];
|
|
7096
|
-
const absent = artefacts.filter((a) => !
|
|
7320
|
+
const absent = artefacts.filter((a) => !existsSync24(resolve5(opts.projectRoot, a)));
|
|
7097
7321
|
if (artefacts.length === 0) errors.push("screen.json says rendered: true but lists no artefacts. A render leaves a screenshot.");
|
|
7098
7322
|
else if (absent.length) errors.push(`screen.json says rendered: true, but these artefacts do not exist: ${absent.join(", ")}.`);
|
|
7099
7323
|
else rendered = true;
|
|
@@ -7114,7 +7338,7 @@ function verifyVerdicts(opts) {
|
|
|
7114
7338
|
};
|
|
7115
7339
|
let specFront = "";
|
|
7116
7340
|
try {
|
|
7117
|
-
specFront =
|
|
7341
|
+
specFront = readText(join38(opts.projectRoot, ".jig", "specs", `${opts.surface}.spec.md`)).split(/^---\s*$/m)[1] ?? "";
|
|
7118
7342
|
} catch {
|
|
7119
7343
|
}
|
|
7120
7344
|
if (specIndexableField(specFront) === "unreadable") {
|
|
@@ -7149,7 +7373,7 @@ function previousFindings(projectRoot, dir) {
|
|
|
7149
7373
|
const out = /* @__PURE__ */ new Map();
|
|
7150
7374
|
let text;
|
|
7151
7375
|
try {
|
|
7152
|
-
text = at ? git(["show", `${at}:./${rel}/${f}`]) :
|
|
7376
|
+
text = at ? lf(git(["show", `${at}:./${rel}/${f}`])) : readText(join38(dir, f));
|
|
7153
7377
|
} catch {
|
|
7154
7378
|
return out;
|
|
7155
7379
|
}
|
|
@@ -7231,18 +7455,17 @@ function sourceChanges(git, from, to) {
|
|
|
7231
7455
|
}
|
|
7232
7456
|
|
|
7233
7457
|
// src/commands/gate.ts
|
|
7234
|
-
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";
|
|
7235
7459
|
import { createHash as createHash3 } from "crypto";
|
|
7236
|
-
import { join as
|
|
7460
|
+
import { join as join43 } from "path";
|
|
7237
7461
|
|
|
7238
7462
|
// src/check/mockup-drawing.ts
|
|
7239
|
-
import {
|
|
7240
|
-
import { join as join38 } from "path";
|
|
7463
|
+
import { join as join39 } from "path";
|
|
7241
7464
|
var FRAMES = [["phone", 360], ["tablet", 768], ["desktop", 1280], ["wide", 1600]];
|
|
7242
7465
|
function mockupDrawingProblems(root, specBody, at) {
|
|
7243
7466
|
let html;
|
|
7244
7467
|
try {
|
|
7245
|
-
html =
|
|
7468
|
+
html = readText(join39(root, at));
|
|
7246
7469
|
} catch {
|
|
7247
7470
|
return [];
|
|
7248
7471
|
}
|
|
@@ -7303,7 +7526,7 @@ function frameRegionProblems(frames, specBody, at) {
|
|
|
7303
7526
|
function approvedDrawingProblems(root, specBody, at) {
|
|
7304
7527
|
let html;
|
|
7305
7528
|
try {
|
|
7306
|
-
html =
|
|
7529
|
+
html = readText(join39(root, at));
|
|
7307
7530
|
} catch {
|
|
7308
7531
|
return [];
|
|
7309
7532
|
}
|
|
@@ -7409,12 +7632,12 @@ function sizeBlock2(sizes, size) {
|
|
|
7409
7632
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
7410
7633
|
|
|
7411
7634
|
// src/commands/ship.ts
|
|
7412
|
-
import { existsSync as
|
|
7413
|
-
import { join as
|
|
7635
|
+
import { existsSync as existsSync25, readdirSync as readdirSync15, readFileSync as readFileSync34 } from "fs";
|
|
7636
|
+
import { join as join41 } from "path";
|
|
7414
7637
|
|
|
7415
7638
|
// src/commands/seo.ts
|
|
7416
|
-
import { readFileSync as
|
|
7417
|
-
import { join as
|
|
7639
|
+
import { readFileSync as readFileSync33 } from "fs";
|
|
7640
|
+
import { join as join40 } from "path";
|
|
7418
7641
|
var SITEMAP_FILE = /(^|\/)sitemap(-index)?\.(xml|ts|js|mjs|tsx|jsx|rb|php|py)$/i;
|
|
7419
7642
|
var ROBOTS_FILE = /(^|\/)robots\.(txt|ts|js|mjs|tsx|jsx)$/i;
|
|
7420
7643
|
var ROUTE_IN_SITEMAP = /(?:<loc>\s*([^<\s]+)\s*<\/loc>)|(?:url\s*:\s*["'`]([^"'`]+)["'`])/gi;
|
|
@@ -7455,7 +7678,7 @@ function seo(opts) {
|
|
|
7455
7678
|
for (const file of files) {
|
|
7456
7679
|
let raw;
|
|
7457
7680
|
try {
|
|
7458
|
-
raw =
|
|
7681
|
+
raw = readFileSync33(join40(root, file), "utf8");
|
|
7459
7682
|
} catch {
|
|
7460
7683
|
continue;
|
|
7461
7684
|
}
|
|
@@ -7520,7 +7743,7 @@ function critiqueAtShip(projectRoot, specBody) {
|
|
|
7520
7743
|
const own = specCritique(specBody);
|
|
7521
7744
|
if (own) return own === "at-ship";
|
|
7522
7745
|
try {
|
|
7523
|
-
return JSON.parse(
|
|
7746
|
+
return JSON.parse(readFileSync34(join41(projectRoot, "jig.config.json"), "utf8")).critique === "at-ship";
|
|
7524
7747
|
} catch {
|
|
7525
7748
|
return false;
|
|
7526
7749
|
}
|
|
@@ -7528,14 +7751,14 @@ function critiqueAtShip(projectRoot, specBody) {
|
|
|
7528
7751
|
function tweakDeferred(dir) {
|
|
7529
7752
|
let t;
|
|
7530
7753
|
try {
|
|
7531
|
-
t = JSON.parse(
|
|
7754
|
+
t = JSON.parse(readFileSync34(join41(dir, "tweak.json"), "utf8"));
|
|
7532
7755
|
} catch {
|
|
7533
7756
|
return false;
|
|
7534
7757
|
}
|
|
7535
7758
|
if (t.deferred === void 0 || t.deferred === false) return false;
|
|
7536
7759
|
let judged;
|
|
7537
7760
|
try {
|
|
7538
|
-
judged = JSON.parse(
|
|
7761
|
+
judged = JSON.parse(readFileSync34(join41(dir, "verdicts.lock"), "utf8")).tweak;
|
|
7539
7762
|
} catch {
|
|
7540
7763
|
}
|
|
7541
7764
|
return judged !== t.at;
|
|
@@ -7546,15 +7769,16 @@ function pageChecksum(html) {
|
|
|
7546
7769
|
function changedSinceLock(projectRoot, dir) {
|
|
7547
7770
|
let lock;
|
|
7548
7771
|
try {
|
|
7549
|
-
lock = JSON.parse(
|
|
7772
|
+
lock = JSON.parse(readFileSync34(join41(dir, "verdicts.lock"), "utf8"));
|
|
7550
7773
|
} catch {
|
|
7551
7774
|
return void 0;
|
|
7552
7775
|
}
|
|
7553
7776
|
const file = lock.page?.file;
|
|
7554
7777
|
if (!file || !lock.page?.checksum) return void 0;
|
|
7555
7778
|
try {
|
|
7556
|
-
const html =
|
|
7557
|
-
|
|
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;
|
|
7558
7782
|
} catch {
|
|
7559
7783
|
return void 0;
|
|
7560
7784
|
}
|
|
@@ -7564,7 +7788,7 @@ function supersededBy(front) {
|
|
|
7564
7788
|
return named ? named : void 0;
|
|
7565
7789
|
}
|
|
7566
7790
|
function pageStatus(projectRoot, surface) {
|
|
7567
|
-
const spec =
|
|
7791
|
+
const spec = readText(join41(projectRoot, ".jig", "specs", `${surface}.spec.md`));
|
|
7568
7792
|
const front = spec.split(/^---\s*$/m)[1] ?? "";
|
|
7569
7793
|
if (!/^\s*confirmed\s*:\s*true\b/im.test(front)) {
|
|
7570
7794
|
return { surface, state: "in-progress", detail: "its spec is not confirmed, so nothing of it is built to ship" };
|
|
@@ -7572,10 +7796,10 @@ function pageStatus(projectRoot, surface) {
|
|
|
7572
7796
|
const successor = supersededBy(front);
|
|
7573
7797
|
if (successor !== void 0) {
|
|
7574
7798
|
const file = successor.replace(/(\.spec\.md)?$/, ".spec.md");
|
|
7575
|
-
return
|
|
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` };
|
|
7576
7800
|
}
|
|
7577
|
-
const dir =
|
|
7578
|
-
if (!
|
|
7801
|
+
const dir = join41(projectRoot, ".jig", "critique", surface);
|
|
7802
|
+
if (!existsSync25(join41(dir, "screen.json")) && !existsSync25(join41(dir, "code.json"))) {
|
|
7579
7803
|
return { surface, state: "never", detail: "never critiqued" };
|
|
7580
7804
|
}
|
|
7581
7805
|
if (tweakDeferred(dir)) return { surface, state: "deferred", detail: "a tweak deferred its re-judge" };
|
|
@@ -7585,7 +7809,7 @@ function pageStatus(projectRoot, surface) {
|
|
|
7585
7809
|
const stale = v.errors.find((e) => /was taken (on|by) an older/.test(e));
|
|
7586
7810
|
let judgedPage = false;
|
|
7587
7811
|
try {
|
|
7588
|
-
judgedPage = !!JSON.parse(
|
|
7812
|
+
judgedPage = !!JSON.parse(readFileSync34(join41(dir, "verdicts.lock"), "utf8")).page?.checksum;
|
|
7589
7813
|
} catch {
|
|
7590
7814
|
}
|
|
7591
7815
|
if (stale && !judgedPage) return { surface, state: "changed", detail: "the page changed after its probes were taken" };
|
|
@@ -7601,17 +7825,19 @@ function ship(opts) {
|
|
|
7601
7825
|
const mechanicalErrors = mechanical.findings.filter((f) => f.bucket === "mechanical" && f.severity === "error").length;
|
|
7602
7826
|
const seoResult = seo({ projectRoot: root });
|
|
7603
7827
|
const seoErrors = seoResult.findings.filter((f) => f.severity === "error").length;
|
|
7604
|
-
const specsDir =
|
|
7605
|
-
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() : [];
|
|
7606
7830
|
const pages = surfaces.map((s) => pageStatus(root, s));
|
|
7607
7831
|
const owed = pages.filter((p) => p.state !== "judged" && p.state !== "in-progress" && p.state !== "superseded");
|
|
7608
|
-
const
|
|
7832
|
+
const agentFiles = agentFileProblems(root);
|
|
7833
|
+
const ready = mechanicalErrors === 0 && seoErrors === 0 && owed.length === 0 && agentFiles.length === 0;
|
|
7609
7834
|
const count = (state) => pages.filter((p) => p.state === state).length;
|
|
7610
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")}`;
|
|
7611
7836
|
const mark = { judged: "\u2713", never: "\u2717", changed: "\u2717", reprobe: "\u2717", deferred: "\u2717", incomplete: "\u2717", findings: "\u2717", "in-progress": "\xB7", superseded: "\xB7" };
|
|
7612
7837
|
const report2 = [
|
|
7613
7838
|
` ${mechanicalErrors === 0 ? "\u2713" : "\u2717"} check --all --ci: ${mechanicalErrors} mechanical error${mechanicalErrors === 1 ? "" : "s"}`,
|
|
7614
7839
|
` ${seoErrors === 0 ? "\u2713" : "\u2717"} seo: ${seoErrors} error${seoErrors === 1 ? "" : "s"}`,
|
|
7840
|
+
...agentFiles.map((p) => ` \u2717 ${p}`),
|
|
7615
7841
|
...pages.map((p) => ` ${mark[p.state]} ${p.surface}: ${p.detail}`),
|
|
7616
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.` : "",
|
|
7617
7843
|
` Not checked by Jig, here or anywhere: ${NOT_COVERED}.`,
|
|
@@ -7621,11 +7847,37 @@ function ship(opts) {
|
|
|
7621
7847
|
}
|
|
7622
7848
|
|
|
7623
7849
|
// src/check/spec-checked.ts
|
|
7624
|
-
import { existsSync as
|
|
7850
|
+
import { existsSync as existsSync26, readFileSync as readFileSync35 } from "fs";
|
|
7625
7851
|
import { createHash as createHash2 } from "crypto";
|
|
7626
|
-
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
|
|
7627
7879
|
function specChecksum(body) {
|
|
7628
|
-
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");
|
|
7629
7881
|
}
|
|
7630
7882
|
var mockupLine = (body) => /^\s*mockup\s*:\s*(.*)$/im.exec(body.split(/^---\s*$/m)[1] ?? "")?.[1]?.trim() ?? "";
|
|
7631
7883
|
function mockupWordProblems(spec, then, owner) {
|
|
@@ -7638,10 +7890,8 @@ function mockupWordProblems(spec, then, owner) {
|
|
|
7638
7890
|
if (!quoted) {
|
|
7639
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"}.`];
|
|
7640
7892
|
}
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
}
|
|
7644
|
-
return [];
|
|
7893
|
+
const problem = ownerWordProblem(word === "approved" ? "approve" : "skip", quoted, owner, `${spec.path}: \`mockup: ${word}\``);
|
|
7894
|
+
return problem ? [problem] : [];
|
|
7645
7895
|
}
|
|
7646
7896
|
function mockupPending(body) {
|
|
7647
7897
|
const now = mockupLine(body);
|
|
@@ -7676,7 +7926,7 @@ function specCheckProblems(root, spec, then, owner, decisions) {
|
|
|
7676
7926
|
const recordPath = `.jig/specs/${spec.slug}.checked.json`;
|
|
7677
7927
|
let record;
|
|
7678
7928
|
try {
|
|
7679
|
-
record = JSON.parse(
|
|
7929
|
+
record = JSON.parse(readFileSync35(join42(root, recordPath), "utf8"));
|
|
7680
7930
|
} catch {
|
|
7681
7931
|
problems.push(
|
|
7682
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.`
|
|
@@ -7705,16 +7955,16 @@ function specCheckProblems(root, spec, then, owner, decisions) {
|
|
|
7705
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.`);
|
|
7706
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) => {
|
|
7707
7957
|
const [, file, line] = /^(.+?)(?::(\d+)(?:-\d+)?)?$/.exec(s) ?? [];
|
|
7708
|
-
if (!file || !
|
|
7958
|
+
if (!file || !existsSync26(join42(root, file))) return true;
|
|
7709
7959
|
if (!line) return false;
|
|
7710
7960
|
try {
|
|
7711
|
-
return Number(line) >
|
|
7961
|
+
return Number(line) > readFileSync35(join42(root, file), "utf8").split("\n").length;
|
|
7712
7962
|
} catch {
|
|
7713
7963
|
return true;
|
|
7714
7964
|
}
|
|
7715
7965
|
});
|
|
7716
7966
|
if (missing.length) {
|
|
7717
|
-
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\`.`);
|
|
7718
7968
|
}
|
|
7719
7969
|
return problems;
|
|
7720
7970
|
}
|
|
@@ -7722,10 +7972,10 @@ function specCheckProblems(root, spec, then, owner, decisions) {
|
|
|
7722
7972
|
// src/commands/gate.ts
|
|
7723
7973
|
var MAX_BLOCKS = 3;
|
|
7724
7974
|
function lastJigInvocation(transcriptPath) {
|
|
7725
|
-
if (!transcriptPath || !
|
|
7975
|
+
if (!transcriptPath || !existsSync27(transcriptPath)) return void 0;
|
|
7726
7976
|
let text;
|
|
7727
7977
|
try {
|
|
7728
|
-
text =
|
|
7978
|
+
text = readFileSync36(transcriptPath, "utf8");
|
|
7729
7979
|
} catch {
|
|
7730
7980
|
return void 0;
|
|
7731
7981
|
}
|
|
@@ -7750,10 +8000,10 @@ function lastJigInvocation(transcriptPath) {
|
|
|
7750
8000
|
return found;
|
|
7751
8001
|
}
|
|
7752
8002
|
function ownerWords(transcriptPath) {
|
|
7753
|
-
if (!transcriptPath || !
|
|
8003
|
+
if (!transcriptPath || !existsSync27(transcriptPath)) return "";
|
|
7754
8004
|
const said = [];
|
|
7755
8005
|
try {
|
|
7756
|
-
for (const line of
|
|
8006
|
+
for (const line of readFileSync36(transcriptPath, "utf8").split("\n")) {
|
|
7757
8007
|
if (!line.includes('"user"')) continue;
|
|
7758
8008
|
let entry;
|
|
7759
8009
|
try {
|
|
@@ -7773,11 +8023,46 @@ function ownerWords(transcriptPath) {
|
|
|
7773
8023
|
}
|
|
7774
8024
|
return said.join("\n");
|
|
7775
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
|
+
}
|
|
7776
8061
|
function lastAssistantText(transcriptPath) {
|
|
7777
|
-
if (!transcriptPath || !
|
|
8062
|
+
if (!transcriptPath || !existsSync27(transcriptPath)) return void 0;
|
|
7778
8063
|
let found;
|
|
7779
8064
|
try {
|
|
7780
|
-
for (const line of
|
|
8065
|
+
for (const line of readFileSync36(transcriptPath, "utf8").split("\n")) {
|
|
7781
8066
|
if (!line.includes('"assistant"')) continue;
|
|
7782
8067
|
let entry;
|
|
7783
8068
|
try {
|
|
@@ -7801,9 +8086,9 @@ function asksOwner(text) {
|
|
|
7801
8086
|
}
|
|
7802
8087
|
var ASKS_THE_OWNER = /* @__PURE__ */ new Set(["decide", "spec", "mockup", "tweak"]);
|
|
7803
8088
|
function sessionStart(transcriptPath) {
|
|
7804
|
-
if (!transcriptPath || !
|
|
8089
|
+
if (!transcriptPath || !existsSync27(transcriptPath)) return void 0;
|
|
7805
8090
|
try {
|
|
7806
|
-
for (const line of
|
|
8091
|
+
for (const line of readFileSync36(transcriptPath, "utf8").split("\n")) {
|
|
7807
8092
|
const m = /"timestamp"\s*:\s*"([^"]+)"/.exec(line);
|
|
7808
8093
|
if (m) {
|
|
7809
8094
|
const t = Date.parse(m[1]);
|
|
@@ -7818,20 +8103,20 @@ function verdictsMtime(dir) {
|
|
|
7818
8103
|
let newest = 0;
|
|
7819
8104
|
for (const name of VERDICT_FILES) {
|
|
7820
8105
|
try {
|
|
7821
|
-
newest = Math.max(newest, statSync5(
|
|
8106
|
+
newest = Math.max(newest, statSync5(join43(dir, name)).mtimeMs);
|
|
7822
8107
|
} catch {
|
|
7823
8108
|
}
|
|
7824
8109
|
}
|
|
7825
8110
|
return newest;
|
|
7826
8111
|
}
|
|
7827
8112
|
function surfacesInPlay(root, command, transcriptPath, surface) {
|
|
7828
|
-
const critiqueDir =
|
|
7829
|
-
if (!
|
|
8113
|
+
const critiqueDir = join43(root, ".jig", "critique");
|
|
8114
|
+
if (!existsSync27(critiqueDir)) return [];
|
|
7830
8115
|
const all = readdirSync16(critiqueDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("_") && !d.name.startsWith(".")).map((d) => d.name);
|
|
7831
8116
|
const start = sessionStart(transcriptPath);
|
|
7832
8117
|
if (start === void 0) return all;
|
|
7833
8118
|
const current = command === "critique" || command === "tweak" ? specFor(root, surface)?.slug : void 0;
|
|
7834
|
-
return all.filter((s) => s === current || verdictsMtime(
|
|
8119
|
+
return all.filter((s) => s === current || verdictsMtime(join43(critiqueDir, s)) >= start - 1e3);
|
|
7835
8120
|
}
|
|
7836
8121
|
function surfacesToProbe(root, input) {
|
|
7837
8122
|
const invocation = lastJigInvocation(input.transcript_path);
|
|
@@ -7839,9 +8124,13 @@ function surfacesToProbe(root, input) {
|
|
|
7839
8124
|
}
|
|
7840
8125
|
function fileAtSessionStart(root, path, start) {
|
|
7841
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
|
+
}
|
|
7842
8131
|
try {
|
|
7843
8132
|
const base = start === void 0 ? "HEAD" : git(["rev-list", "-1", `--before=@${Math.floor(start / 1e3)}`, "HEAD"]).trim();
|
|
7844
|
-
return base ? git(["show", `${base}:./${path}`]) : "";
|
|
8133
|
+
return base ? lf(git(["show", `${base}:./${path}`])) : "";
|
|
7845
8134
|
} catch {
|
|
7846
8135
|
return "";
|
|
7847
8136
|
}
|
|
@@ -7854,7 +8143,7 @@ function changedOutsideRecords(root, start) {
|
|
|
7854
8143
|
const changed = git(["diff", "--name-only", base, "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean);
|
|
7855
8144
|
const created = git(["ls-files", "--others", "--exclude-standard", "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean).filter((f) => {
|
|
7856
8145
|
try {
|
|
7857
|
-
return start === void 0 || statSync5(
|
|
8146
|
+
return start === void 0 || statSync5(join43(root, f)).mtimeMs >= start - 1e3;
|
|
7858
8147
|
} catch {
|
|
7859
8148
|
return false;
|
|
7860
8149
|
}
|
|
@@ -7869,8 +8158,8 @@ function drawingChangedAfterApproval(root, specPath, at) {
|
|
|
7869
8158
|
try {
|
|
7870
8159
|
const approvedIn = git(["log", "-1", "--format=%H", "-G", "^mockup[[:space:]]*:[[:space:]]*approved", "--", specPath]).trim();
|
|
7871
8160
|
if (!approvedIn) return void 0;
|
|
7872
|
-
const then = git(["show", `${approvedIn}:./${at}`]);
|
|
7873
|
-
return then !==
|
|
8161
|
+
const then = lf(git(["show", `${approvedIn}:./${at}`]));
|
|
8162
|
+
return then !== readText(join43(root, at)) ? approvedIn.slice(0, 7) : void 0;
|
|
7874
8163
|
} catch {
|
|
7875
8164
|
return void 0;
|
|
7876
8165
|
}
|
|
@@ -7882,18 +8171,25 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
7882
8171
|
const found = decisionsFile(root);
|
|
7883
8172
|
if (!found) problems.push("decide wrote no DECISIONS.md beside the token layer.");
|
|
7884
8173
|
else {
|
|
7885
|
-
const body =
|
|
8174
|
+
const body = readText(join43(root, found));
|
|
7886
8175
|
if (!/^##\s+Unresolved\s*$/im.test(body)) {
|
|
7887
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.`");
|
|
7888
8177
|
}
|
|
7889
8178
|
if (/\[TODO\]/.test(body)) problems.push("DECISIONS.md still contains [TODO] markers.");
|
|
7890
|
-
|
|
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}`));
|
|
7891
8182
|
}
|
|
7892
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
|
+
}
|
|
7893
8188
|
if (command === "spec" || command === "mockup" || command === "make" || command === "critique" || command === "tweak") {
|
|
7894
8189
|
if (!spec) problems.push(`${command} needs a spec: there is no file in .jig/specs/.`);
|
|
7895
8190
|
else {
|
|
7896
8191
|
problems.push(...specProblems(spec));
|
|
8192
|
+
problems.push(...newFieldProblems(spec, fileAtSessionStart(root, spec.path, start)));
|
|
7897
8193
|
if (problems.length === 0) problems.push(...navProblems(spec));
|
|
7898
8194
|
}
|
|
7899
8195
|
}
|
|
@@ -7908,12 +8204,12 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
7908
8204
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
7909
8205
|
if (/^approved/i.test(mockup)) {
|
|
7910
8206
|
if (!at) problems.push(`${spec.path}: \`mockup_at:\` is empty. Record where the approved drawing is.`);
|
|
7911
|
-
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.`);
|
|
7912
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.`);
|
|
7913
8209
|
}
|
|
7914
|
-
if (at && /\.html?$/i.test(at) &&
|
|
8210
|
+
if (at && /\.html?$/i.test(at) && existsSync27(join43(root, at))) problems.push(...mockupDrawingProblems(root, spec.body, at));
|
|
7915
8211
|
}
|
|
7916
|
-
if (command === "tweak" && spec) problems.push(...tweakProblems(root, spec, owner));
|
|
8212
|
+
if (command === "tweak" && spec) problems.push(...tweakProblems(root, spec, owner, start));
|
|
7917
8213
|
if ((command === "spec" || command === "make") && spec) {
|
|
7918
8214
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
7919
8215
|
const mockup = /^\s*mockup\s*:\s*(\S+)/im.exec(front)?.[1] ?? "";
|
|
@@ -7939,7 +8235,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
7939
8235
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
7940
8236
|
const mockup = /^\s*mockup\s*:\s*(\S+)/im.exec(front)?.[1] ?? "";
|
|
7941
8237
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
7942
|
-
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))) {
|
|
7943
8239
|
const changedAfter = drawingChangedAfterApproval(root, spec.path, at);
|
|
7944
8240
|
if (changedAfter) {
|
|
7945
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.`);
|
|
@@ -7951,7 +8247,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
7951
8247
|
if (found) {
|
|
7952
8248
|
let now = "";
|
|
7953
8249
|
try {
|
|
7954
|
-
now =
|
|
8250
|
+
now = readText(join43(root, found));
|
|
7955
8251
|
} catch {
|
|
7956
8252
|
}
|
|
7957
8253
|
const then = fileAtSessionStart(root, found, start);
|
|
@@ -7965,13 +8261,13 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
7965
8261
|
if (found) {
|
|
7966
8262
|
let now = "";
|
|
7967
8263
|
try {
|
|
7968
|
-
now =
|
|
8264
|
+
now = readText(join43(root, found));
|
|
7969
8265
|
} catch {
|
|
7970
8266
|
}
|
|
7971
8267
|
const then = fileAtSessionStart(root, found, start);
|
|
7972
8268
|
if (now && now !== then) {
|
|
7973
8269
|
problems.push(...unsourcedReasons(now, then, { newWhysOnly: true }).map((p) => `${found}: ${p}`));
|
|
7974
|
-
const change = readTweak(
|
|
8270
|
+
const change = readTweak(join43(root, ".jig", "critique", spec.slug))?.change;
|
|
7975
8271
|
if (change) problems.push(...quotesNotFrom(now, then, change).map((p) => `${found}: ${p}`));
|
|
7976
8272
|
}
|
|
7977
8273
|
}
|
|
@@ -7980,7 +8276,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
7980
8276
|
for (const file of modeFiles(root)) {
|
|
7981
8277
|
let now = "";
|
|
7982
8278
|
try {
|
|
7983
|
-
now =
|
|
8279
|
+
now = readText(join43(root, file));
|
|
7984
8280
|
} catch {
|
|
7985
8281
|
continue;
|
|
7986
8282
|
}
|
|
@@ -7991,8 +8287,8 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
7991
8287
|
}
|
|
7992
8288
|
}
|
|
7993
8289
|
if (command === "critique") {
|
|
7994
|
-
const dir =
|
|
7995
|
-
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"))) : [];
|
|
7996
8292
|
for (const surface2 of surfaces) {
|
|
7997
8293
|
const v = verifyVerdicts({ projectRoot: root, surface: surface2 });
|
|
7998
8294
|
if (v.ok && v.decisions.state !== "ran" && v.decisions.total > 0) {
|
|
@@ -8003,7 +8299,7 @@ function commandProblems(root, command, surface, start, owner = "") {
|
|
|
8003
8299
|
}
|
|
8004
8300
|
}
|
|
8005
8301
|
const own = specFor(root, surface)?.slug;
|
|
8006
|
-
if (own && surfaces.includes(own)) problems.push(...reportOmissions(
|
|
8302
|
+
if (own && surfaces.includes(own)) problems.push(...reportOmissions(join43(dir, own), own));
|
|
8007
8303
|
if (surfaces.length === 0) {
|
|
8008
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.");
|
|
8009
8305
|
}
|
|
@@ -8016,7 +8312,7 @@ function drawingBeforeAsking(root, surface) {
|
|
|
8016
8312
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
8017
8313
|
const named = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
8018
8314
|
const at = named || `.jig/mockups/${spec.slug}.html`;
|
|
8019
|
-
if (/^https?:/i.test(at) || !/\.html?$/i.test(at) || !
|
|
8315
|
+
if (/^https?:/i.test(at) || !/\.html?$/i.test(at) || !existsSync27(join43(root, at))) return [];
|
|
8020
8316
|
const found = mockupDrawingProblems(root, spec.body, at);
|
|
8021
8317
|
return found.length ? [`Fix the drawing before you put it to the owner; they approve what the spec lists, drawn. ${found.join(" ")}`] : [];
|
|
8022
8318
|
}
|
|
@@ -8024,14 +8320,14 @@ function specChecked(root, spec, start, owner) {
|
|
|
8024
8320
|
const found = decisionsFile(root);
|
|
8025
8321
|
let decisions = "";
|
|
8026
8322
|
try {
|
|
8027
|
-
if (found) decisions =
|
|
8323
|
+
if (found) decisions = readText(join43(root, found));
|
|
8028
8324
|
} catch {
|
|
8029
8325
|
}
|
|
8030
8326
|
return specCheckProblems(root, spec, fileAtSessionStart(root, spec.path, start), owner, decisions);
|
|
8031
8327
|
}
|
|
8032
8328
|
function modeFiles(root) {
|
|
8033
8329
|
try {
|
|
8034
|
-
const state = JSON.parse(
|
|
8330
|
+
const state = JSON.parse(readFileSync36(join43(root, ".jig", "state.json"), "utf8"));
|
|
8035
8331
|
return Object.keys(state.files ?? {}).filter((f) => /(^|\/)mode\.[\w-]+\.css$/.test(f));
|
|
8036
8332
|
} catch {
|
|
8037
8333
|
return [];
|
|
@@ -8040,7 +8336,7 @@ function modeFiles(root) {
|
|
|
8040
8336
|
function reportOmissions(dir, surface) {
|
|
8041
8337
|
let report2;
|
|
8042
8338
|
try {
|
|
8043
|
-
report2 =
|
|
8339
|
+
report2 = readText(join43(dir, "REPORT.md")).toLowerCase();
|
|
8044
8340
|
} catch {
|
|
8045
8341
|
return [];
|
|
8046
8342
|
}
|
|
@@ -8048,7 +8344,7 @@ function reportOmissions(dir, surface) {
|
|
|
8048
8344
|
for (const f of VERDICT_FILES) {
|
|
8049
8345
|
let file;
|
|
8050
8346
|
try {
|
|
8051
|
-
file = JSON.parse(
|
|
8347
|
+
file = JSON.parse(readFileSync36(join43(dir, f), "utf8"));
|
|
8052
8348
|
} catch {
|
|
8053
8349
|
continue;
|
|
8054
8350
|
}
|
|
@@ -8061,7 +8357,7 @@ function reportOmissions(dir, surface) {
|
|
|
8061
8357
|
if (!missing.length) return [];
|
|
8062
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).`];
|
|
8063
8359
|
}
|
|
8064
|
-
function tweakProblems(root, spec, owner = "") {
|
|
8360
|
+
function tweakProblems(root, spec, owner = "", start) {
|
|
8065
8361
|
const problems = [];
|
|
8066
8362
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
8067
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\`.`);
|
|
@@ -8069,21 +8365,31 @@ function tweakProblems(root, spec, owner = "") {
|
|
|
8069
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\`.`);
|
|
8070
8366
|
else problems.push(...structureSinceApproval(root, spec, front));
|
|
8071
8367
|
const surface = spec.slug;
|
|
8072
|
-
const record = readTweak(
|
|
8368
|
+
const record = readTweak(join43(root, ".jig", "critique", surface));
|
|
8073
8369
|
if (!record) {
|
|
8074
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\`).`);
|
|
8075
8371
|
} else {
|
|
8076
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
|
+
}
|
|
8077
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.`);
|
|
8078
8383
|
if (record.deferred !== void 0) {
|
|
8079
|
-
|
|
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)) {
|
|
8080
8386
|
problems.push(
|
|
8081
|
-
`.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}` : "")
|
|
8082
8388
|
);
|
|
8083
8389
|
}
|
|
8084
8390
|
return problems;
|
|
8085
8391
|
}
|
|
8086
|
-
const unjudged = unjudgedTweakIds(
|
|
8392
|
+
const unjudged = unjudgedTweakIds(join43(root, ".jig", "critique", surface), record);
|
|
8087
8393
|
if (unjudged.length) {
|
|
8088
8394
|
problems.push(
|
|
8089
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.`
|
|
@@ -8110,7 +8416,7 @@ function structureSinceApproval(root, spec, front) {
|
|
|
8110
8416
|
const problems = [];
|
|
8111
8417
|
let thenBody = "";
|
|
8112
8418
|
try {
|
|
8113
|
-
thenBody = git(["show", `${baseline}:./${spec.path}`]);
|
|
8419
|
+
thenBody = lf(git(["show", `${baseline}:./${spec.path}`]));
|
|
8114
8420
|
} catch {
|
|
8115
8421
|
return [];
|
|
8116
8422
|
}
|
|
@@ -8119,9 +8425,9 @@ function structureSinceApproval(root, spec, front) {
|
|
|
8119
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\`.`);
|
|
8120
8426
|
}
|
|
8121
8427
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
|
|
8122
|
-
if (at && !/^https?:/i.test(at) &&
|
|
8428
|
+
if (at && !/^https?:/i.test(at) && existsSync27(join43(root, at))) {
|
|
8123
8429
|
try {
|
|
8124
|
-
if (git(["show", `${baseline}:./${at}`]) !==
|
|
8430
|
+
if (lf(git(["show", `${baseline}:./${at}`])) !== readText(join43(root, at))) {
|
|
8125
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\`.`);
|
|
8126
8432
|
}
|
|
8127
8433
|
} catch {
|
|
@@ -8131,7 +8437,7 @@ function structureSinceApproval(root, spec, front) {
|
|
|
8131
8437
|
}
|
|
8132
8438
|
function readTweak(dir) {
|
|
8133
8439
|
try {
|
|
8134
|
-
const raw = JSON.parse(
|
|
8440
|
+
const raw = JSON.parse(readFileSync36(join43(dir, "tweak.json"), "utf8"));
|
|
8135
8441
|
const deferred = typeof raw.deferred === "string" || raw.deferred === true ? raw.deferred : void 0;
|
|
8136
8442
|
return { at: String(raw.at ?? ""), change: String(raw.change ?? "").trim(), ids: Array.isArray(raw.ids) ? raw.ids.map(String) : [], ...deferred !== void 0 ? { deferred } : {} };
|
|
8137
8443
|
} catch {
|
|
@@ -8144,20 +8450,20 @@ function surfacePage(projectRoot, surface) {
|
|
|
8144
8450
|
const declared = /^\s*surface\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
|
|
8145
8451
|
const candidates = [declared, `${surface}.html`, declared ? `${declared.replace(/^\//, "")}.html` : void 0].filter((c) => !!c && /\.\w+$/.test(c) === (c === declared ? /\.\w+$/.test(c) : true));
|
|
8146
8452
|
for (const candidate of candidates) {
|
|
8147
|
-
if (candidate &&
|
|
8453
|
+
if (candidate && existsSync27(join43(projectRoot, candidate))) return candidate;
|
|
8148
8454
|
}
|
|
8149
8455
|
return void 0;
|
|
8150
8456
|
}
|
|
8151
8457
|
function gate(opts) {
|
|
8152
8458
|
const root = opts.projectRoot;
|
|
8153
|
-
if (!
|
|
8459
|
+
if (!existsSync27(join43(root, "jig.config.json")) && !existsSync27(join43(root, ".jig"))) {
|
|
8154
8460
|
return { block: false, reason: "" };
|
|
8155
8461
|
}
|
|
8156
8462
|
const invocation = lastJigInvocation(opts.input.transcript_path);
|
|
8157
8463
|
const command = invocation?.command;
|
|
8158
8464
|
const waiting = command !== void 0 && ASKS_THE_OWNER.has(command) && asksOwner(lastAssistantText(opts.input.transcript_path));
|
|
8159
8465
|
const start = sessionStart(opts.input.transcript_path);
|
|
8160
|
-
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) : "";
|
|
8161
8467
|
const problems = command && !waiting ? commandProblems(root, command, invocation?.surface, start, owner).map((p) => `/jig ${command}: ${p}`) : [];
|
|
8162
8468
|
if (waiting && command === "mockup") problems.push(...drawingBeforeAsking(root, invocation?.surface).map((p) => `/jig mockup: ${p}`));
|
|
8163
8469
|
if (command === "ship" && !asksOwner(lastAssistantText(opts.input.transcript_path))) {
|
|
@@ -8171,9 +8477,24 @@ ${result.report}`);
|
|
|
8171
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.`);
|
|
8172
8478
|
}
|
|
8173
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
|
+
}
|
|
8174
8489
|
if (waiting && command === "spec") {
|
|
8175
|
-
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
|
+
}
|
|
8176
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
|
+
}
|
|
8177
8498
|
}
|
|
8178
8499
|
const selection = selectFiles(root, false);
|
|
8179
8500
|
const changedUi = selection.mode === "changed" && selection.files.some((f) => isStyleBearing(f) || isReaderText(f));
|
|
@@ -8196,11 +8517,17 @@ ${result.report}`);
|
|
|
8196
8517
|
}
|
|
8197
8518
|
}
|
|
8198
8519
|
problems.push(...verdictGuard(root, command, surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)));
|
|
8199
|
-
|
|
8200
|
-
|
|
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)) {
|
|
8201
8528
|
for (const surface of surfacesInPlay(root, command, opts.input.transcript_path, invocation?.surface)) {
|
|
8202
|
-
const dir =
|
|
8203
|
-
if (!
|
|
8529
|
+
const dir = join43(critiqueDir, surface);
|
|
8530
|
+
if (!existsSync27(join43(dir, "screen.json")) && !existsSync27(join43(dir, "code.json"))) continue;
|
|
8204
8531
|
const v = verifyVerdicts({ projectRoot: root, surface });
|
|
8205
8532
|
if (!v.ok) {
|
|
8206
8533
|
const shown = v.errors.slice(0, 6).map((e) => ` ${e}`);
|
|
@@ -8223,10 +8550,10 @@ ${problems.join("\n\n")}`
|
|
|
8223
8550
|
}
|
|
8224
8551
|
const session = opts.input.session_id ?? "unknown";
|
|
8225
8552
|
const key = `${session}:${createHash3("sha256").update(problems.join("\n")).digest("hex").slice(0, 12)}`;
|
|
8226
|
-
const stateFile =
|
|
8553
|
+
const stateFile = join43(root, ".jig", "gate.json");
|
|
8227
8554
|
let state = {};
|
|
8228
8555
|
try {
|
|
8229
|
-
state = JSON.parse(
|
|
8556
|
+
state = JSON.parse(readFileSync36(stateFile, "utf8"));
|
|
8230
8557
|
} catch {
|
|
8231
8558
|
}
|
|
8232
8559
|
if (problems.length === 0) {
|
|
@@ -8261,7 +8588,7 @@ function verdictDigests(dir) {
|
|
|
8261
8588
|
for (const f of VERDICT_FILES) {
|
|
8262
8589
|
let file;
|
|
8263
8590
|
try {
|
|
8264
|
-
file = JSON.parse(
|
|
8591
|
+
file = JSON.parse(readFileSync36(join43(dir, f), "utf8"));
|
|
8265
8592
|
} catch {
|
|
8266
8593
|
continue;
|
|
8267
8594
|
}
|
|
@@ -8276,7 +8603,7 @@ function writeLock(root, surface, lockPath, dir, now) {
|
|
|
8276
8603
|
const pageFile2 = recordedPage(root, surface);
|
|
8277
8604
|
let page;
|
|
8278
8605
|
try {
|
|
8279
|
-
if (pageFile2) page = { file: pageFile2, checksum: pageChecksum(
|
|
8606
|
+
if (pageFile2) page = { file: pageFile2, checksum: pageChecksum(readFileSync36(join43(root, pageFile2), "utf8")) };
|
|
8280
8607
|
} catch {
|
|
8281
8608
|
}
|
|
8282
8609
|
const tweakAt = readTweak(dir)?.at;
|
|
@@ -8289,7 +8616,7 @@ function verdictChecksum(dir) {
|
|
|
8289
8616
|
const parts = VERDICT_FILES.map((f) => {
|
|
8290
8617
|
try {
|
|
8291
8618
|
return `${f}
|
|
8292
|
-
${
|
|
8619
|
+
${readFileSync36(join43(dir, f), "utf8")}`;
|
|
8293
8620
|
} catch {
|
|
8294
8621
|
return "";
|
|
8295
8622
|
}
|
|
@@ -8297,15 +8624,15 @@ ${readFileSync35(join42(dir, f), "utf8")}`;
|
|
|
8297
8624
|
return parts.some(Boolean) ? checksum(parts.join("\n")) : void 0;
|
|
8298
8625
|
}
|
|
8299
8626
|
function verdictGuard(root, command, inPlay) {
|
|
8300
|
-
const critiqueDir =
|
|
8301
|
-
if (!
|
|
8627
|
+
const critiqueDir = join43(root, ".jig", "critique");
|
|
8628
|
+
if (!existsSync27(critiqueDir)) return [];
|
|
8302
8629
|
const problems = [];
|
|
8303
8630
|
for (const surface of readdirSync16(critiqueDir)) {
|
|
8304
8631
|
if (surface.startsWith("_")) continue;
|
|
8305
|
-
const dir =
|
|
8632
|
+
const dir = join43(critiqueDir, surface);
|
|
8306
8633
|
const now = verdictChecksum(dir);
|
|
8307
8634
|
if (!now) continue;
|
|
8308
|
-
const lockPath =
|
|
8635
|
+
const lockPath = join43(dir, LOCK);
|
|
8309
8636
|
if ((command === "critique" || command === "ship") && (!inPlay || inPlay.includes(surface))) {
|
|
8310
8637
|
writeLock(root, surface, lockPath, dir, now);
|
|
8311
8638
|
problems.push(...lockLeftBehind(root, surface));
|
|
@@ -8313,7 +8640,7 @@ function verdictGuard(root, command, inPlay) {
|
|
|
8313
8640
|
}
|
|
8314
8641
|
let lock = {};
|
|
8315
8642
|
try {
|
|
8316
|
-
lock = JSON.parse(
|
|
8643
|
+
lock = JSON.parse(readFileSync36(lockPath, "utf8"));
|
|
8317
8644
|
} catch {
|
|
8318
8645
|
continue;
|
|
8319
8646
|
}
|
|
@@ -8346,6 +8673,25 @@ function lockLeftBehind(root, surface) {
|
|
|
8346
8673
|
}
|
|
8347
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.`];
|
|
8348
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
|
+
}
|
|
8349
8695
|
function tweakVerdictProblems(dir, surface, lockedVerdicts) {
|
|
8350
8696
|
const record = readTweak(dir);
|
|
8351
8697
|
if (!record) {
|
|
@@ -8370,7 +8716,7 @@ function unjudgedTweakIds(dir, record) {
|
|
|
8370
8716
|
for (const f of VERDICT_FILES) {
|
|
8371
8717
|
let file;
|
|
8372
8718
|
try {
|
|
8373
|
-
file = JSON.parse(
|
|
8719
|
+
file = JSON.parse(readFileSync36(join43(dir, f), "utf8"));
|
|
8374
8720
|
} catch {
|
|
8375
8721
|
continue;
|
|
8376
8722
|
}
|
|
@@ -8382,7 +8728,7 @@ function unjudgedTweakIds(dir, record) {
|
|
|
8382
8728
|
}
|
|
8383
8729
|
function save(file, state) {
|
|
8384
8730
|
try {
|
|
8385
|
-
mkdirSync8(
|
|
8731
|
+
mkdirSync8(join43(file, ".."), { recursive: true });
|
|
8386
8732
|
writeFileSync6(file, JSON.stringify(state), "utf8");
|
|
8387
8733
|
} catch {
|
|
8388
8734
|
}
|
|
@@ -8390,7 +8736,7 @@ function save(file, state) {
|
|
|
8390
8736
|
|
|
8391
8737
|
// src/index.ts
|
|
8392
8738
|
var packageRoot = getPackageRoot();
|
|
8393
|
-
var { version } = JSON.parse(
|
|
8739
|
+
var { version } = JSON.parse(readFileSync37(join44(packageRoot, "package.json"), "utf8"));
|
|
8394
8740
|
function warnIfUnpublishedPin() {
|
|
8395
8741
|
if (isDevVersion(version)) {
|
|
8396
8742
|
console.warn(
|
|
@@ -8434,7 +8780,7 @@ program.command("install").description("Install Jig rules and the agent skill fi
|
|
|
8434
8780
|
projectRoot,
|
|
8435
8781
|
packageRoot: assetRoot(),
|
|
8436
8782
|
version,
|
|
8437
|
-
homeDir:
|
|
8783
|
+
homeDir: homedir3(),
|
|
8438
8784
|
hook
|
|
8439
8785
|
});
|
|
8440
8786
|
if (result.warning) {
|
|
@@ -8443,6 +8789,7 @@ program.command("install").description("Install Jig rules and the agent skill fi
|
|
|
8443
8789
|
}
|
|
8444
8790
|
if (result.hookOnly) {
|
|
8445
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)`);
|
|
8446
8793
|
if (result.stopHook === true) console.log(" + .claude/settings.json (Stop hook: jig gate blocks finishing while check or a critique fails)");
|
|
8447
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.");
|
|
8448
8795
|
return;
|
|
@@ -8451,6 +8798,7 @@ program.command("install").description("Install Jig rules and the agent skill fi
|
|
|
8451
8798
|
warnIfUnpublishedPin();
|
|
8452
8799
|
for (const f of result.written) console.log(` + ${f}`);
|
|
8453
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)`);
|
|
8454
8802
|
if (result.stopHook === true) console.log(" + .claude/settings.json (Stop hook: jig gate blocks finishing while check or a critique fails)");
|
|
8455
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.");
|
|
8456
8804
|
if (result.stopHook === void 0 && opts.agent === "claude" && opts.scope === "project") {
|
|
@@ -8478,7 +8826,7 @@ program.command("update").description("Update vendored Jig rules, skipping files
|
|
|
8478
8826
|
projectRoot,
|
|
8479
8827
|
packageRoot: assetRoot(),
|
|
8480
8828
|
version,
|
|
8481
|
-
homeDir:
|
|
8829
|
+
homeDir: homedir3()
|
|
8482
8830
|
});
|
|
8483
8831
|
const label = result.targets.map((t) => `${t.agent} (${t.scope}, ${t.fromVersion})`).join(", ");
|
|
8484
8832
|
warnIfUnpublishedPin();
|
|
@@ -8497,6 +8845,16 @@ program.command("update").description("Update vendored Jig rules, skipping files
|
|
|
8497
8845
|
process.exit(1);
|
|
8498
8846
|
}
|
|
8499
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
|
+
});
|
|
8500
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) => {
|
|
8501
8859
|
const projectRoot = findProjectRoot(process.cwd());
|
|
8502
8860
|
try {
|
|
@@ -8541,7 +8899,7 @@ program.command("check").description("Check the repo against Jig's mechanical +
|
|
|
8541
8899
|
try {
|
|
8542
8900
|
const result = check({
|
|
8543
8901
|
projectRoot,
|
|
8544
|
-
homeDir:
|
|
8902
|
+
homeDir: homedir3(),
|
|
8545
8903
|
version,
|
|
8546
8904
|
all: opts.all,
|
|
8547
8905
|
ci: opts.ci
|
|
@@ -8624,13 +8982,14 @@ program.command("probe").description("Print the render probe. With --save, read
|
|
|
8624
8982
|
program.command("gate").description("Run by the Claude Code Stop hook: block stopping while check or a critique fails.").action(async () => {
|
|
8625
8983
|
let input = {};
|
|
8626
8984
|
try {
|
|
8627
|
-
if (!process.stdin.isTTY) input = JSON.parse(
|
|
8985
|
+
if (!process.stdin.isTTY) input = JSON.parse(readFileSync37(0, "utf8") || "{}");
|
|
8628
8986
|
} catch {
|
|
8629
8987
|
}
|
|
8630
8988
|
const cwd = input.cwd ?? process.cwd();
|
|
8631
8989
|
try {
|
|
8632
8990
|
const projectRoot = findProjectRoot(cwd);
|
|
8633
|
-
const
|
|
8991
|
+
const byHand = !input.transcript_path;
|
|
8992
|
+
const critiqued = byHand ? [] : critiquedSurfaces(projectRoot);
|
|
8634
8993
|
for (const surface of surfacesToProbe(projectRoot, input).filter((s) => critiqued.includes(s))) {
|
|
8635
8994
|
const page = surfacePage(projectRoot, surface) ?? recordedPage(projectRoot, surface);
|
|
8636
8995
|
if (!page) continue;
|
|
@@ -8653,7 +9012,7 @@ program.command("init").description("Set the project up to use Jig: a brand file
|
|
|
8653
9012
|
await init({
|
|
8654
9013
|
projectRoot,
|
|
8655
9014
|
packageRoot: assetRoot(),
|
|
8656
|
-
homeDir:
|
|
9015
|
+
homeDir: homedir3(),
|
|
8657
9016
|
version,
|
|
8658
9017
|
yes: opts.yes
|
|
8659
9018
|
});
|
|
@@ -8664,7 +9023,7 @@ program.command("init").description("Set the project up to use Jig: a brand file
|
|
|
8664
9023
|
});
|
|
8665
9024
|
function resolveSurfaceMode(projectRoot) {
|
|
8666
9025
|
try {
|
|
8667
|
-
const config = JSON.parse(
|
|
9026
|
+
const config = JSON.parse(readFileSync37(join44(projectRoot, "jig.config.json"), "utf8"));
|
|
8668
9027
|
const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
|
|
8669
9028
|
return modes.length === 1 ? modes[0] : void 0;
|
|
8670
9029
|
} catch {
|