jig-ui 0.18.3 → 0.18.6
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 +119 -51
- package/dist/index.js +164 -33
- package/package.json +1 -1
- package/templates/COMMAND.md.tmpl +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.6 (2026-09-25)
|
|
4
|
+
|
|
5
|
+
A mockup's frames must hold each size's regions, and the drawing shows either
|
|
6
|
+
side of each switch the project records.
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **The gate checks what a mockup's frames hold, not only that they exist.**
|
|
11
|
+
0.18.5 required a frame per size; a frame can still be empty, or hold the
|
|
12
|
+
desktop composition narrower. The procedure's self-check, "go down the spec's
|
|
13
|
+
`regions:` and find each one in that frame", was prose only. Now each size's
|
|
14
|
+
regions must be labelled in its frame: by name where the spec gives a short
|
|
15
|
+
one (`nav`, or `h1: "Versions"`), and by count where it writes a description,
|
|
16
|
+
so wording never trips it.
|
|
17
|
+
- **A mockup draws either side of each switch the project records.** The probe
|
|
18
|
+
has measured one pixel either side of every `--breakpoint-*` since 0.18.0, but
|
|
19
|
+
the drawing the owner approves showed only the four sizes; jig-site's header
|
|
20
|
+
changes at 540, between phone and tablet, and no frame showed 539 beside 540.
|
|
21
|
+
A new spec field, `switches:`, names the switches a page crosses (`none` if it
|
|
22
|
+
crosses none); absent, the mockup draws every one.
|
|
23
|
+
- **A spec's `indexable:` reads past a YAML comment.** The spec template itself
|
|
24
|
+
writes `indexable: true # from the mode ...`; a spec that copied it was
|
|
25
|
+
reported unreadable.
|
|
26
|
+
|
|
27
|
+
## 0.18.5 (2026-09-25)
|
|
28
|
+
|
|
29
|
+
The gate holds an HTML mockup that does not draw the page at every size.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **The gate holds a mockup that does not show every size.** On jig-site three
|
|
34
|
+
HTML mockups in a row were single responsive pages, so the owner saw only the
|
|
35
|
+
width of the window they opened and asked twice where the other sizes were.
|
|
36
|
+
The procedure's wireframe template draws the page once per size, side by
|
|
37
|
+
side, in `.frame[data-size]` boxes, and nothing checked a drawing used them.
|
|
38
|
+
When a `mockup` session stops with an HTML drawing, the gate now requires a
|
|
39
|
+
frame for phone (360), tablet (768), desktop (1280) and wide (1600).
|
|
40
|
+
|
|
41
|
+
## 0.18.4 (2026-09-25)
|
|
42
|
+
|
|
43
|
+
Every release summary is one or two lines, for jig-site's Versions page.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- **Every release summary is one or two lines, with no em dash.** jig-site's
|
|
48
|
+
Versions page shows the summary under each release, so it is interface text.
|
|
49
|
+
Nine ran to three to thirteen lines; their detail now sits under
|
|
50
|
+
`### Upgrading` or `### Notes` in the same release. A test keeps summaries
|
|
51
|
+
short.
|
|
52
|
+
|
|
3
53
|
## 0.18.3 (2026-09-25)
|
|
4
54
|
|
|
5
55
|
Recording a decision no longer undoes the critiques written before it, and an
|
|
@@ -72,9 +122,8 @@ menu, and `A-05` missed an arrow that renders as an emoji.
|
|
|
72
122
|
|
|
73
123
|
## 0.18.0 (2026-09-25)
|
|
74
124
|
|
|
75
|
-
A page's frame gets tokens, a project
|
|
76
|
-
|
|
77
|
-
it found building jig-site's header and Reference with the full loop.
|
|
125
|
+
A page's frame gets tokens, a project records where its layout switches, and
|
|
126
|
+
keyboard access a rule requires fits every script budget.
|
|
78
127
|
|
|
79
128
|
### Added
|
|
80
129
|
|
|
@@ -181,8 +230,12 @@ The gate judges only the critiques a session touched.
|
|
|
181
230
|
## 0.17.0 (2026-09-24)
|
|
182
231
|
|
|
183
232
|
Thirteen more tells of a generated page, ten of them caught by `check`, and a
|
|
184
|
-
picture for every rule.
|
|
185
|
-
|
|
233
|
+
picture for every rule.
|
|
234
|
+
|
|
235
|
+
### Notes
|
|
236
|
+
|
|
237
|
+
Also carries the probe and verdict fixes tagged as 0.16.2, which was not
|
|
238
|
+
published to npm.
|
|
186
239
|
|
|
187
240
|
### Added
|
|
188
241
|
|
|
@@ -468,8 +521,8 @@ One new rule: no em dashes in interface text (`I-118`), with a detector.
|
|
|
468
521
|
|
|
469
522
|
## 0.11.1 (2026-09-18)
|
|
470
523
|
|
|
471
|
-
Two holes in the gate,
|
|
472
|
-
|
|
524
|
+
Two holes in the gate, found by running the loop twice on 0.11.0: once on
|
|
525
|
+
Haiku, once on Sonnet.
|
|
473
526
|
|
|
474
527
|
### Fixed
|
|
475
528
|
|
|
@@ -487,9 +540,8 @@ once on Haiku, once on Sonnet.
|
|
|
487
540
|
|
|
488
541
|
## 0.11.0 (2026-09-18)
|
|
489
542
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
floor the day 0.10.0 shipped.
|
|
543
|
+
A measurement an agent can type is not a measurement. Everything here came
|
|
544
|
+
from two live runs at the capability floor.
|
|
493
545
|
|
|
494
546
|
### Added
|
|
495
547
|
|
|
@@ -516,9 +568,8 @@ floor the day 0.10.0 shipped.
|
|
|
516
568
|
|
|
517
569
|
## 0.10.0 (2026-09-18)
|
|
518
570
|
|
|
519
|
-
Mobile-first, and a design loop that a weak model cannot skip
|
|
520
|
-
|
|
521
|
-
because an instruction the agent could choose to ignore was ignored.
|
|
571
|
+
Mobile-first, and a design loop that a weak model cannot skip, measured on live
|
|
572
|
+
runs with Haiku.
|
|
522
573
|
|
|
523
574
|
### Added
|
|
524
575
|
|
|
@@ -582,9 +633,8 @@ because an instruction the agent could choose to ignore was ignored.
|
|
|
582
633
|
|
|
583
634
|
## 0.9.0 (2026-09-13)
|
|
584
635
|
|
|
585
|
-
One new rule and one amended correction, both
|
|
586
|
-
|
|
587
|
-
at a token, so an agent reading it literally always produced one.
|
|
636
|
+
One new rule and one amended correction, both about the same blind spot: H-47's
|
|
637
|
+
correction always pointed an agent at a token.
|
|
588
638
|
|
|
589
639
|
### Added
|
|
590
640
|
|
|
@@ -627,12 +677,12 @@ at a token, so an agent reading it literally always produced one.
|
|
|
627
677
|
|
|
628
678
|
## 0.8.2 (2026-09-12)
|
|
629
679
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
Tailwind, which is the first time Jig's Tailwind guidance had been followed
|
|
633
|
-
end to end by anything other than its own tests.
|
|
680
|
+
Four fixes found by following Jig's Tailwind guidance end to end on its own
|
|
681
|
+
documentation site.
|
|
634
682
|
|
|
635
|
-
|
|
683
|
+
### Notes
|
|
684
|
+
|
|
685
|
+
Three of the four are the same shape: two places answering one question, and
|
|
636
686
|
disagreeing without either knowing the other existed.
|
|
637
687
|
|
|
638
688
|
### Fixed
|
|
@@ -706,10 +756,8 @@ disagreeing without either knowing the other existed.
|
|
|
706
756
|
|
|
707
757
|
## 0.8.1 (2026-09-12)
|
|
708
758
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
verification that had only checked the file it had just edited. Both were found
|
|
712
|
-
by inspecting the published tarball rather than the working tree.
|
|
759
|
+
Two fixes where 0.8.0 corrected one instance and left the class alone, both
|
|
760
|
+
found by inspecting the published tarball.
|
|
713
761
|
|
|
714
762
|
### Fixed
|
|
715
763
|
|
|
@@ -766,9 +814,10 @@ by inspecting the published tarball rather than the working tree.
|
|
|
766
814
|
|
|
767
815
|
## 0.8.0 (2026-09-11)
|
|
768
816
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
817
|
+
Fixes found by agents new to Jig building its documentation site. The test
|
|
818
|
+
suite passed throughout and caught none of them.
|
|
819
|
+
|
|
820
|
+
### Notes
|
|
772
821
|
|
|
773
822
|
Minor rather than patch: `explain` prints lines it did not print before, and
|
|
774
823
|
`init` can write a file it did not write before.
|
|
@@ -904,6 +953,8 @@ Minor rather than patch: `explain` prints lines it did not print before, and
|
|
|
904
953
|
|
|
905
954
|
A rule file that contradicted the tool, and the guard that kept it that way.
|
|
906
955
|
|
|
956
|
+
### Notes
|
|
957
|
+
|
|
907
958
|
Patch: the shipped rules change, but no command writes anything different.
|
|
908
959
|
|
|
909
960
|
### Fixed
|
|
@@ -956,7 +1007,9 @@ already knew where the tokens were.
|
|
|
956
1007
|
|
|
957
1008
|
## 0.7.0 (2026-09-11)
|
|
958
1009
|
|
|
959
|
-
A silent no-op, fixed,
|
|
1010
|
+
A silent no-op, fixed, and the missing half of 0.6.0's token-layer move.
|
|
1011
|
+
|
|
1012
|
+
### Notes
|
|
960
1013
|
|
|
961
1014
|
Minor rather than patch, and the call is arguable. The non-TTY fix is a
|
|
962
1015
|
correction; the relocation offer is new behaviour that can move files, though
|
|
@@ -998,19 +1051,25 @@ to move their token layer should read these notes.
|
|
|
998
1051
|
|
|
999
1052
|
## 0.6.0 (2026-09-11)
|
|
1000
1053
|
|
|
1001
|
-
The token layer
|
|
1054
|
+
The token layer leaves its dotfolder, and `check` reads the layer's own
|
|
1055
|
+
declarations back.
|
|
1056
|
+
|
|
1057
|
+
### Upgrading
|
|
1002
1058
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
and no command ever looked again. A generated file edited afterwards, by a person
|
|
1007
|
-
or an agent, went unexamined: `--color-text-weak` dropped to 22% opacity and
|
|
1008
|
-
`check` reported "No findings". With that closed, where the files live and who
|
|
1009
|
-
writes them become ordinary decisions rather than load-bearing ones.
|
|
1059
|
+
Run `npx jig-ui@latest update`, then `npx jig-ui@latest init`. Existing installs
|
|
1060
|
+
keep their `.jig/tokens/` layout. Nothing moves unless you move it, because
|
|
1061
|
+
relocating files could break an import you wrote yourself.
|
|
1010
1062
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1063
|
+
### Notes
|
|
1064
|
+
|
|
1065
|
+
Both changes came from the same question: who should write the tokens. The
|
|
1066
|
+
answer turned out to depend on something that did not exist: **nothing
|
|
1067
|
+
validated the token layer's own declarations.** `init` checked a brand colour
|
|
1068
|
+
once, at write time, and no command ever looked again. A generated file edited
|
|
1069
|
+
afterwards, by a person or an agent, went unexamined: `--color-text-weak`
|
|
1070
|
+
dropped to 22% opacity and `check` reported "No findings". With that closed,
|
|
1071
|
+
where the files live and who writes them become ordinary decisions rather than
|
|
1072
|
+
load-bearing ones.
|
|
1014
1073
|
|
|
1015
1074
|
### Added
|
|
1016
1075
|
|
|
@@ -1092,18 +1151,24 @@ move it, because relocating files could break an import you wrote yourself.
|
|
|
1092
1151
|
|
|
1093
1152
|
## 0.5.0 (2026-09-10)
|
|
1094
1153
|
|
|
1095
|
-
Four things
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
every numeric default against an external reference is finished — 0 rows open —
|
|
1100
|
-
which is where most of the rest of this release came from.
|
|
1154
|
+
Four things 0.4.0 reported as working did not, and the reconciliation of every
|
|
1155
|
+
numeric default against an external reference is finished.
|
|
1156
|
+
|
|
1157
|
+
### Upgrading
|
|
1101
1158
|
|
|
1102
1159
|
**If you are on 0.4.0, run `npx jig-ui@latest update` from a terminal.** The
|
|
1103
1160
|
`/jig update` fix cannot deliver itself: your command file is the broken one, so
|
|
1104
1161
|
the slash command will keep reporting a successful no-op until the CLI replaces
|
|
1105
1162
|
it. Once, from the terminal, and the slash command works from then on.
|
|
1106
1163
|
|
|
1164
|
+
### Notes
|
|
1165
|
+
|
|
1166
|
+
The four: `/jig update` refreshed to the version already installed and said
|
|
1167
|
+
"Updated Jig → 0.4.0". Dark mode could not be reached by choosing it. `check`
|
|
1168
|
+
skipped nearly every colour in a project that had not run `init`. And the
|
|
1169
|
+
reconciliation ended with 0 rows open, which is where most of the rest of this
|
|
1170
|
+
release came from.
|
|
1171
|
+
|
|
1107
1172
|
### Fixed
|
|
1108
1173
|
|
|
1109
1174
|
- **`/jig update` could never upgrade anyone.** Every subcommand is invoked at
|
|
@@ -1186,11 +1251,14 @@ them, and nothing at all checked that a token cited in the rules exists.
|
|
|
1186
1251
|
|
|
1187
1252
|
## 0.4.0 (2026-09-04)
|
|
1188
1253
|
|
|
1189
|
-
Jig stops copying itself into your project.
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1254
|
+
Jig stops copying itself into your project. A single-mode project now gets
|
|
1255
|
+
**three** files, all of them its own.
|
|
1256
|
+
|
|
1257
|
+
### Notes
|
|
1258
|
+
|
|
1259
|
+
Jig is a skill an agent reads, and 0.3.0 wrote 220KB across 17 files into every
|
|
1260
|
+
consuming repo to deliver it. Roughly 200KB of that was Jig's own, read by an
|
|
1261
|
+
agent that already had it from the skill install.
|
|
1194
1262
|
|
|
1195
1263
|
### Changed
|
|
1196
1264
|
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import { readFileSync as
|
|
5
|
+
import { readFileSync as readFileSync34 } from "fs";
|
|
6
6
|
import { homedir as homedir2 } from "os";
|
|
7
|
-
import { join as
|
|
7
|
+
import { join as join41 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/paths.ts
|
|
10
10
|
import { existsSync } from "fs";
|
|
@@ -6471,7 +6471,7 @@ function navProblems(spec) {
|
|
|
6471
6471
|
function specIndexableField(front) {
|
|
6472
6472
|
const line = /^\s*indexable\s*:(.*)$/im.exec(front);
|
|
6473
6473
|
if (!line) return "absent";
|
|
6474
|
-
const value = line[1].trim().replace(/^["']|["']$/g, "").toLowerCase();
|
|
6474
|
+
const value = line[1].replace(/\s+#.*$/, "").trim().replace(/^["']|["']$/g, "").toLowerCase();
|
|
6475
6475
|
if (value === "true" || value === "yes") return true;
|
|
6476
6476
|
if (value === "false" || value === "no") return false;
|
|
6477
6477
|
return "unreadable";
|
|
@@ -6685,15 +6685,145 @@ function verifyVerdicts(opts) {
|
|
|
6685
6685
|
}
|
|
6686
6686
|
|
|
6687
6687
|
// src/commands/gate.ts
|
|
6688
|
-
import { existsSync as existsSync24, mkdirSync as mkdirSync8, readdirSync as readdirSync15, readFileSync as
|
|
6688
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync8, readdirSync as readdirSync15, readFileSync as readFileSync32, statSync as statSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
6689
6689
|
import { createHash as createHash2 } from "crypto";
|
|
6690
|
+
import { join as join39 } from "path";
|
|
6691
|
+
|
|
6692
|
+
// src/check/mockup-drawing.ts
|
|
6693
|
+
import { readFileSync as readFileSync31 } from "fs";
|
|
6690
6694
|
import { join as join38 } from "path";
|
|
6695
|
+
var FRAMES = [["phone", 360], ["tablet", 768], ["desktop", 1280], ["wide", 1600]];
|
|
6696
|
+
function mockupDrawingProblems(root, specBody, at) {
|
|
6697
|
+
let html;
|
|
6698
|
+
try {
|
|
6699
|
+
html = readFileSync31(join38(root, at), "utf8");
|
|
6700
|
+
} catch {
|
|
6701
|
+
return [];
|
|
6702
|
+
}
|
|
6703
|
+
const frames = readFrames(html);
|
|
6704
|
+
const problems = [];
|
|
6705
|
+
const missing = FRAMES.filter(([size]) => !frames.some((f) => f.size === size));
|
|
6706
|
+
if (missing.length) {
|
|
6707
|
+
problems.push(
|
|
6708
|
+
`${at} has no frame for ${missing.map(([size, px2]) => `${size} (${px2}px)`).join(", ")}. A mockup draws the page once per size, side by side, in the wireframe template's \`<section class="frame" data-size="\u2026">\` boxes, so the owner sees every size at once. A single responsive page shows only the width of the window it is opened in.`
|
|
6709
|
+
);
|
|
6710
|
+
}
|
|
6711
|
+
const regions = specRegions(specBody);
|
|
6712
|
+
for (const [size] of FRAMES) {
|
|
6713
|
+
const listed = regions[size];
|
|
6714
|
+
const inFrame = frames.filter((f) => f.size === size).flatMap((f) => f.labels);
|
|
6715
|
+
if (!listed || listed.length === 0 || !frames.some((f) => f.size === size)) continue;
|
|
6716
|
+
const named = listed.filter((r) => r.name).map((r) => r.name);
|
|
6717
|
+
const absent = named.filter((name) => !inFrame.some((label) => labelMatches(label, name)));
|
|
6718
|
+
if (absent.length) {
|
|
6719
|
+
problems.push(
|
|
6720
|
+
`${at}: the ${size} frame has no labelled region for ${absent.map((n) => `"${n}"`).join(", ")}. Every region the spec lists for a size is drawn in that size's frame, with its name in a \`.name\` label.`
|
|
6721
|
+
);
|
|
6722
|
+
} else if (inFrame.length < listed.length) {
|
|
6723
|
+
problems.push(
|
|
6724
|
+
`${at}: the ${size} frame labels ${inFrame.length} region(s) and the spec lists ${listed.length} for ${size}. Draw each one in the frame, with its name in a \`.name\` label.`
|
|
6725
|
+
);
|
|
6726
|
+
}
|
|
6727
|
+
}
|
|
6728
|
+
const widths = new Set(frames.map((f) => f.width).filter((w) => w !== void 0));
|
|
6729
|
+
const crossed = specSwitches(specBody);
|
|
6730
|
+
for (const s of declaredSwitches(root).filter((sw) => !crossed || crossed.includes(sw.name))) {
|
|
6731
|
+
const absent = [s.px - 1, s.px].filter((w) => !widths.has(w));
|
|
6732
|
+
if (absent.length) {
|
|
6733
|
+
problems.push(
|
|
6734
|
+
`${at} does not draw either side of the \`--breakpoint-${s.name}\` switch at ${s.px}px (no frame at ${absent.join(" or ")}px). The layout changes there, between the four sizes, so the owner sees the last width before it and the first at it: a frame with \`data-width="${s.px - 1}"\` and one with \`data-width="${s.px}"\`, each that wide. A page that never crosses it says so in its spec: \`switches:\` lists the ones it does, or \`none\`.`
|
|
6735
|
+
);
|
|
6736
|
+
}
|
|
6737
|
+
}
|
|
6738
|
+
return problems;
|
|
6739
|
+
}
|
|
6740
|
+
function specSwitches(specBody) {
|
|
6741
|
+
const front = specBody.split(/^---\s*$/m)[1] ?? "";
|
|
6742
|
+
const value = /^switches\s*:\s*(.+)$/im.exec(front)?.[1]?.replace(/#.*$/, "").trim();
|
|
6743
|
+
if (value === void 0) return void 0;
|
|
6744
|
+
if (/^(none|\[\s*\])$/i.test(value)) return [];
|
|
6745
|
+
return value.replace(/^\[|\]$/g, "").split(",").map((n) => n.trim().replace(/^["']|["']$/g, "").replace(/^--breakpoint-/, "")).filter(Boolean);
|
|
6746
|
+
}
|
|
6747
|
+
function readFrames(html) {
|
|
6748
|
+
const opens = [...html.matchAll(/<\w+\b[^>]*\bclass\s*=\s*["'][^"']*\bframe\b[^"']*["'][^>]*>/gi)];
|
|
6749
|
+
return opens.map((open, i) => {
|
|
6750
|
+
const tag = open[0];
|
|
6751
|
+
const body = html.slice(open.index + tag.length, opens[i + 1]?.index ?? html.length);
|
|
6752
|
+
const size = /\bdata-size\s*=\s*["']([\w-]+)["']/i.exec(tag)?.[1]?.toLowerCase();
|
|
6753
|
+
const width = Number(/\bdata-width\s*=\s*["'](\d+)["']/i.exec(tag)?.[1]) || void 0;
|
|
6754
|
+
const labels = [...body.matchAll(/<\w+\b[^>]*\bclass\s*=\s*["'][^"']*\bname\b[^"']*["'][^>]*>([^<]*)/gi)].map((m) => normalise(m[1])).filter(Boolean);
|
|
6755
|
+
return { size, width, labels };
|
|
6756
|
+
});
|
|
6757
|
+
}
|
|
6758
|
+
function specRegions(specBody) {
|
|
6759
|
+
const front = specBody.split(/^---\s*$/m)[1] ?? "";
|
|
6760
|
+
const sizes = front.split(/^sizes\s*:/im)[1] ?? "";
|
|
6761
|
+
const out = {};
|
|
6762
|
+
const own = {};
|
|
6763
|
+
const sameAs = {};
|
|
6764
|
+
for (const [size] of [...FRAMES, ["landscape", 900]]) {
|
|
6765
|
+
const block = sizeBlock2(sizes, size);
|
|
6766
|
+
if (!block) continue;
|
|
6767
|
+
const alias = /^\s*same-as\s*:\s*([\w-]+)/im.exec(block)?.[1];
|
|
6768
|
+
if (alias) {
|
|
6769
|
+
sameAs[size] = alias.toLowerCase();
|
|
6770
|
+
continue;
|
|
6771
|
+
}
|
|
6772
|
+
own[size] = regionEntries(block).map(regionName);
|
|
6773
|
+
}
|
|
6774
|
+
for (const size of [...Object.keys(own), ...Object.keys(sameAs)]) {
|
|
6775
|
+
let at = size;
|
|
6776
|
+
for (let hops = 0; sameAs[at] && hops < 5; hops++) at = sameAs[at];
|
|
6777
|
+
if (own[at]) out[size] = own[at];
|
|
6778
|
+
}
|
|
6779
|
+
return out;
|
|
6780
|
+
}
|
|
6781
|
+
function regionEntries(block) {
|
|
6782
|
+
const inline = /^\s*regions\s*:\s*\[(.*)\]\s*$/im.exec(block);
|
|
6783
|
+
if (inline) return inline[1].split(",").map((s) => s.trim()).filter(Boolean);
|
|
6784
|
+
const start = /^(\s*)regions\s*:\s*$/im.exec(block);
|
|
6785
|
+
if (!start) return [];
|
|
6786
|
+
const indent = start[1].length;
|
|
6787
|
+
const entries = [];
|
|
6788
|
+
for (const line of block.slice(start.index + start[0].length).split("\n")) {
|
|
6789
|
+
if (!line.trim()) continue;
|
|
6790
|
+
const lead = /^\s*/.exec(line)[0].length;
|
|
6791
|
+
if (lead <= indent) break;
|
|
6792
|
+
const item = /^\s*-\s*(.+)$/.exec(line);
|
|
6793
|
+
if (item) entries.push(item[1]);
|
|
6794
|
+
}
|
|
6795
|
+
return entries;
|
|
6796
|
+
}
|
|
6797
|
+
function regionName(entry) {
|
|
6798
|
+
const text = entry.trim().replace(/^["']|["']$/g, "").replace(/\\"/g, '"');
|
|
6799
|
+
const head = normalise(text.split(":")[0]);
|
|
6800
|
+
return head && head.split(" ").length <= 4 ? { name: head } : {};
|
|
6801
|
+
}
|
|
6802
|
+
function labelMatches(label, name) {
|
|
6803
|
+
return label === name || label.startsWith(`${name} `) || label.startsWith(`${name}:`) || new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(label);
|
|
6804
|
+
}
|
|
6805
|
+
function normalise(text) {
|
|
6806
|
+
return text.toLowerCase().replace(/[`"'“”]/g, "").replace(/\s+/g, " ").trim();
|
|
6807
|
+
}
|
|
6808
|
+
function sizeBlock2(sizes, size) {
|
|
6809
|
+
const start = new RegExp(`^(\\s+)${size}\\s*:.*$`, "im").exec(sizes);
|
|
6810
|
+
if (!start) return "";
|
|
6811
|
+
const indent = start[1].length;
|
|
6812
|
+
const lines = [];
|
|
6813
|
+
for (const line of sizes.slice(start.index + start[0].length).split("\n")) {
|
|
6814
|
+
if (line.trim() && /^\s*/.exec(line)[0].length <= indent) break;
|
|
6815
|
+
lines.push(line);
|
|
6816
|
+
}
|
|
6817
|
+
return lines.join("\n");
|
|
6818
|
+
}
|
|
6819
|
+
|
|
6820
|
+
// src/commands/gate.ts
|
|
6691
6821
|
var MAX_BLOCKS = 3;
|
|
6692
6822
|
function lastJigCommand(transcriptPath) {
|
|
6693
6823
|
if (!transcriptPath || !existsSync24(transcriptPath)) return void 0;
|
|
6694
6824
|
let text;
|
|
6695
6825
|
try {
|
|
6696
|
-
text =
|
|
6826
|
+
text = readFileSync32(transcriptPath, "utf8");
|
|
6697
6827
|
} catch {
|
|
6698
6828
|
return void 0;
|
|
6699
6829
|
}
|
|
@@ -6711,7 +6841,7 @@ function lastAssistantText(transcriptPath) {
|
|
|
6711
6841
|
if (!transcriptPath || !existsSync24(transcriptPath)) return void 0;
|
|
6712
6842
|
let found;
|
|
6713
6843
|
try {
|
|
6714
|
-
for (const line of
|
|
6844
|
+
for (const line of readFileSync32(transcriptPath, "utf8").split("\n")) {
|
|
6715
6845
|
if (!line.includes('"assistant"')) continue;
|
|
6716
6846
|
let entry;
|
|
6717
6847
|
try {
|
|
@@ -6737,7 +6867,7 @@ var ASKS_THE_OWNER = /* @__PURE__ */ new Set(["decide", "spec", "mockup"]);
|
|
|
6737
6867
|
function sessionStart(transcriptPath) {
|
|
6738
6868
|
if (!transcriptPath || !existsSync24(transcriptPath)) return void 0;
|
|
6739
6869
|
try {
|
|
6740
|
-
for (const line of
|
|
6870
|
+
for (const line of readFileSync32(transcriptPath, "utf8").split("\n")) {
|
|
6741
6871
|
const m = /"timestamp"\s*:\s*"([^"]+)"/.exec(line);
|
|
6742
6872
|
if (m) {
|
|
6743
6873
|
const t = Date.parse(m[1]);
|
|
@@ -6752,20 +6882,20 @@ function verdictsMtime(dir) {
|
|
|
6752
6882
|
let newest = 0;
|
|
6753
6883
|
for (const name of VERDICT_FILES) {
|
|
6754
6884
|
try {
|
|
6755
|
-
newest = Math.max(newest, statSync4(
|
|
6885
|
+
newest = Math.max(newest, statSync4(join39(dir, name)).mtimeMs);
|
|
6756
6886
|
} catch {
|
|
6757
6887
|
}
|
|
6758
6888
|
}
|
|
6759
6889
|
return newest;
|
|
6760
6890
|
}
|
|
6761
6891
|
function surfacesInPlay(root, command, transcriptPath) {
|
|
6762
|
-
const critiqueDir =
|
|
6892
|
+
const critiqueDir = join39(root, ".jig", "critique");
|
|
6763
6893
|
if (!existsSync24(critiqueDir)) return [];
|
|
6764
6894
|
const all = readdirSync15(critiqueDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith("_") && !d.name.startsWith(".")).map((d) => d.name);
|
|
6765
6895
|
const start = sessionStart(transcriptPath);
|
|
6766
6896
|
if (start === void 0) return all;
|
|
6767
6897
|
const current = command === "critique" ? /^\s*surface\s*:\s*(.+)$/im.exec(newestSpec(root)?.body.split(/^---\s*$/m)[1] ?? "")?.[1]?.trim().replace(/^["']|["']$/g, "") : void 0;
|
|
6768
|
-
return all.filter((s) => s === current || verdictsMtime(
|
|
6898
|
+
return all.filter((s) => s === current || verdictsMtime(join39(critiqueDir, s)) >= start - 1e3);
|
|
6769
6899
|
}
|
|
6770
6900
|
function commandProblems(root, command) {
|
|
6771
6901
|
const problems = [];
|
|
@@ -6774,7 +6904,7 @@ function commandProblems(root, command) {
|
|
|
6774
6904
|
const found = decisionsFile(root);
|
|
6775
6905
|
if (!found) problems.push("decide wrote no DECISIONS.md beside the token layer.");
|
|
6776
6906
|
else {
|
|
6777
|
-
const body =
|
|
6907
|
+
const body = readFileSync32(join39(root, found), "utf8");
|
|
6778
6908
|
if (!/^##\s+Unresolved\s*$/im.test(body)) {
|
|
6779
6909
|
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.`");
|
|
6780
6910
|
}
|
|
@@ -6795,13 +6925,14 @@ function commandProblems(root, command) {
|
|
|
6795
6925
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
6796
6926
|
if (/^approved/i.test(mockup)) {
|
|
6797
6927
|
if (!at) problems.push(`${spec.path}: \`mockup_at:\` is empty. Record where the approved drawing is.`);
|
|
6798
|
-
else if (!/^https?:/i.test(at) && !existsSync24(
|
|
6928
|
+
else if (!/^https?:/i.test(at) && !existsSync24(join39(root, at))) problems.push(`${spec.path}: \`mockup_at: ${at}\` does not exist.`);
|
|
6799
6929
|
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.`);
|
|
6800
6930
|
}
|
|
6931
|
+
if (at && /\.html?$/i.test(at) && existsSync24(join39(root, at))) problems.push(...mockupDrawingProblems(root, spec.body, at));
|
|
6801
6932
|
}
|
|
6802
6933
|
if (command === "critique") {
|
|
6803
|
-
const dir =
|
|
6804
|
-
const surfaces = existsSync24(dir) ? readdirSync15(dir).filter((s) => existsSync24(
|
|
6934
|
+
const dir = join39(root, ".jig", "critique");
|
|
6935
|
+
const surfaces = existsSync24(dir) ? readdirSync15(dir).filter((s) => existsSync24(join39(dir, s, "screen.json")) || existsSync24(join39(dir, s, "code.json"))) : [];
|
|
6805
6936
|
for (const surface of surfaces) {
|
|
6806
6937
|
const v = verifyVerdicts({ projectRoot: root, surface });
|
|
6807
6938
|
if (v.ok && v.decisions.state !== "ran" && v.decisions.total > 0) {
|
|
@@ -6823,13 +6954,13 @@ function surfacePage(projectRoot, surface) {
|
|
|
6823
6954
|
const declared = /^\s*surface\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "");
|
|
6824
6955
|
const candidates = [declared, `${surface}.html`, declared ? `${declared.replace(/^\//, "")}.html` : void 0].filter((c) => !!c && /\.\w+$/.test(c) === (c === declared ? /\.\w+$/.test(c) : true));
|
|
6825
6956
|
for (const candidate of candidates) {
|
|
6826
|
-
if (candidate && existsSync24(
|
|
6957
|
+
if (candidate && existsSync24(join39(projectRoot, candidate))) return candidate;
|
|
6827
6958
|
}
|
|
6828
6959
|
return void 0;
|
|
6829
6960
|
}
|
|
6830
6961
|
function gate(opts) {
|
|
6831
6962
|
const root = opts.projectRoot;
|
|
6832
|
-
if (!existsSync24(
|
|
6963
|
+
if (!existsSync24(join39(root, "jig.config.json")) && !existsSync24(join39(root, ".jig"))) {
|
|
6833
6964
|
return { block: false, reason: "" };
|
|
6834
6965
|
}
|
|
6835
6966
|
const command = lastJigCommand(opts.input.transcript_path);
|
|
@@ -6856,11 +6987,11 @@ function gate(opts) {
|
|
|
6856
6987
|
}
|
|
6857
6988
|
}
|
|
6858
6989
|
problems.push(...verdictGuard(root, command, surfacesInPlay(root, command, opts.input.transcript_path)));
|
|
6859
|
-
const critiqueDir =
|
|
6990
|
+
const critiqueDir = join39(root, ".jig", "critique");
|
|
6860
6991
|
if (existsSync24(critiqueDir)) {
|
|
6861
6992
|
for (const surface of surfacesInPlay(root, command, opts.input.transcript_path)) {
|
|
6862
|
-
const dir =
|
|
6863
|
-
if (!existsSync24(
|
|
6993
|
+
const dir = join39(critiqueDir, surface);
|
|
6994
|
+
if (!existsSync24(join39(dir, "screen.json")) && !existsSync24(join39(dir, "code.json"))) continue;
|
|
6864
6995
|
const v = verifyVerdicts({ projectRoot: root, surface });
|
|
6865
6996
|
if (!v.ok) {
|
|
6866
6997
|
const shown = v.errors.slice(0, 6).map((e) => ` ${e}`);
|
|
@@ -6874,10 +7005,10 @@ function gate(opts) {
|
|
|
6874
7005
|
}
|
|
6875
7006
|
const session = opts.input.session_id ?? "unknown";
|
|
6876
7007
|
const key = `${session}:${createHash2("sha256").update(problems.join("\n")).digest("hex").slice(0, 12)}`;
|
|
6877
|
-
const stateFile =
|
|
7008
|
+
const stateFile = join39(root, ".jig", "gate.json");
|
|
6878
7009
|
let state = {};
|
|
6879
7010
|
try {
|
|
6880
|
-
state = JSON.parse(
|
|
7011
|
+
state = JSON.parse(readFileSync32(stateFile, "utf8"));
|
|
6881
7012
|
} catch {
|
|
6882
7013
|
}
|
|
6883
7014
|
if (problems.length === 0) {
|
|
@@ -6911,7 +7042,7 @@ function verdictChecksum(dir) {
|
|
|
6911
7042
|
const parts = VERDICT_FILES.map((f) => {
|
|
6912
7043
|
try {
|
|
6913
7044
|
return `${f}
|
|
6914
|
-
${
|
|
7045
|
+
${readFileSync32(join39(dir, f), "utf8")}`;
|
|
6915
7046
|
} catch {
|
|
6916
7047
|
return "";
|
|
6917
7048
|
}
|
|
@@ -6919,15 +7050,15 @@ ${readFileSync31(join38(dir, f), "utf8")}`;
|
|
|
6919
7050
|
return parts.some(Boolean) ? checksum(parts.join("\n")) : void 0;
|
|
6920
7051
|
}
|
|
6921
7052
|
function verdictGuard(root, command, inPlay) {
|
|
6922
|
-
const critiqueDir =
|
|
7053
|
+
const critiqueDir = join39(root, ".jig", "critique");
|
|
6923
7054
|
if (!existsSync24(critiqueDir)) return [];
|
|
6924
7055
|
const problems = [];
|
|
6925
7056
|
for (const surface of readdirSync15(critiqueDir)) {
|
|
6926
7057
|
if (surface.startsWith("_")) continue;
|
|
6927
|
-
const dir =
|
|
7058
|
+
const dir = join39(critiqueDir, surface);
|
|
6928
7059
|
const now = verdictChecksum(dir);
|
|
6929
7060
|
if (!now) continue;
|
|
6930
|
-
const lockPath =
|
|
7061
|
+
const lockPath = join39(dir, LOCK);
|
|
6931
7062
|
if (command === "critique" && (!inPlay || inPlay.includes(surface))) {
|
|
6932
7063
|
try {
|
|
6933
7064
|
writeFileSync7(lockPath, JSON.stringify({ checksum: now }) + "\n", "utf8");
|
|
@@ -6937,7 +7068,7 @@ function verdictGuard(root, command, inPlay) {
|
|
|
6937
7068
|
}
|
|
6938
7069
|
let locked;
|
|
6939
7070
|
try {
|
|
6940
|
-
locked = JSON.parse(
|
|
7071
|
+
locked = JSON.parse(readFileSync32(lockPath, "utf8")).checksum;
|
|
6941
7072
|
} catch {
|
|
6942
7073
|
continue;
|
|
6943
7074
|
}
|
|
@@ -6951,15 +7082,15 @@ function verdictGuard(root, command, inPlay) {
|
|
|
6951
7082
|
}
|
|
6952
7083
|
function save(file, state) {
|
|
6953
7084
|
try {
|
|
6954
|
-
mkdirSync8(
|
|
7085
|
+
mkdirSync8(join39(file, ".."), { recursive: true });
|
|
6955
7086
|
writeFileSync7(file, JSON.stringify(state), "utf8");
|
|
6956
7087
|
} catch {
|
|
6957
7088
|
}
|
|
6958
7089
|
}
|
|
6959
7090
|
|
|
6960
7091
|
// src/commands/seo.ts
|
|
6961
|
-
import { readFileSync as
|
|
6962
|
-
import { join as
|
|
7092
|
+
import { readFileSync as readFileSync33 } from "fs";
|
|
7093
|
+
import { join as join40 } from "path";
|
|
6963
7094
|
var SITEMAP_FILE = /(^|\/)sitemap(-index)?\.(xml|ts|js|mjs|tsx|jsx|rb|php|py)$/i;
|
|
6964
7095
|
var ROBOTS_FILE = /(^|\/)robots\.(txt|ts|js|mjs|tsx|jsx)$/i;
|
|
6965
7096
|
var ROUTE_IN_SITEMAP = /(?:<loc>\s*([^<\s]+)\s*<\/loc>)|(?:url\s*:\s*["'`]([^"'`]+)["'`])/gi;
|
|
@@ -7000,7 +7131,7 @@ function seo(opts) {
|
|
|
7000
7131
|
for (const file of files) {
|
|
7001
7132
|
let raw;
|
|
7002
7133
|
try {
|
|
7003
|
-
raw =
|
|
7134
|
+
raw = readFileSync33(join40(root, file), "utf8");
|
|
7004
7135
|
} catch {
|
|
7005
7136
|
continue;
|
|
7006
7137
|
}
|
|
@@ -7056,7 +7187,7 @@ function seo(opts) {
|
|
|
7056
7187
|
|
|
7057
7188
|
// src/index.ts
|
|
7058
7189
|
var packageRoot = getPackageRoot();
|
|
7059
|
-
var { version } = JSON.parse(
|
|
7190
|
+
var { version } = JSON.parse(readFileSync34(join41(packageRoot, "package.json"), "utf8"));
|
|
7060
7191
|
function warnIfUnpublishedPin() {
|
|
7061
7192
|
if (isPublishedBuild(packageRoot)) return;
|
|
7062
7193
|
console.warn(
|
|
@@ -7250,7 +7381,7 @@ program.command("probe").description("Print the render probe. With --save, read
|
|
|
7250
7381
|
program.command("gate").description("Run by the Claude Code Stop hook: block stopping while check or a critique fails.").action(async () => {
|
|
7251
7382
|
let input = {};
|
|
7252
7383
|
try {
|
|
7253
|
-
if (!process.stdin.isTTY) input = JSON.parse(
|
|
7384
|
+
if (!process.stdin.isTTY) input = JSON.parse(readFileSync34(0, "utf8") || "{}");
|
|
7254
7385
|
} catch {
|
|
7255
7386
|
}
|
|
7256
7387
|
const cwd = input.cwd ?? process.cwd();
|
|
@@ -7289,7 +7420,7 @@ program.command("init").description("Set the project up to use Jig: a brand file
|
|
|
7289
7420
|
});
|
|
7290
7421
|
function resolveSurfaceMode(projectRoot) {
|
|
7291
7422
|
try {
|
|
7292
|
-
const config = JSON.parse(
|
|
7423
|
+
const config = JSON.parse(readFileSync34(join41(projectRoot, "jig.config.json"), "utf8"));
|
|
7293
7424
|
const modes = [...new Set((config.surfaces ?? []).map((s) => s.mode).filter(Boolean))];
|
|
7294
7425
|
return modes.length === 1 ? modes[0] : void 0;
|
|
7295
7426
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jig-ui",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.6",
|
|
4
4
|
"description": "A design system for coding agents. 143 numbered UI rules, brand x mode design tokens, and an installer for Claude Code, Codex, Cursor and opencode.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -343,6 +343,7 @@ sizes: # phone first. Each size is a whole composition, not
|
|
|
343
343
|
wide: # judged at 1600px — where a layout with no upper bound shows itself
|
|
344
344
|
same-as: desktop
|
|
345
345
|
why: "content is capped at 1200px, so 1600 adds margin and nothing else"
|
|
346
|
+
switches: none # the recorded switches (--breakpoint-*, T-04) this page crosses, by name; none if it crosses none. Absent, the mockup draws either side of every one
|
|
346
347
|
states: [default, loading, error, success]
|
|
347
348
|
decisions: [The Engineer Reads First] # every DECISIONS.md entry this screen implements, by name
|
|
348
349
|
later: [social sign-in, remember this device] # cut from V1, by name — the next specs start here
|
|
@@ -561,6 +562,14 @@ look. So the drawing contains:
|
|
|
561
562
|
detail: a phone composition that is only the desktop one narrower is the failure
|
|
562
563
|
specs are written per size to prevent. A `same-as:` size is drawn anyway, so the
|
|
563
564
|
claim can be seen to hold.
|
|
565
|
+
- **Either side of each switch the project records.** Where the project has
|
|
566
|
+
measured and recorded a width its layout changes at (`--breakpoint-<what>`,
|
|
567
|
+
`T-04`), draw the last width before it and the first at it, after the four
|
|
568
|
+
sizes: `<section class="frame" data-size="switch" data-width="539"
|
|
569
|
+
style="width:539px">` and the same at 540. The spec's `switches:` names the
|
|
570
|
+
ones this page crosses; absent, draw every one. The four sizes are where a page is
|
|
571
|
+
judged, not where it changes, and a switch between them is otherwise never
|
|
572
|
+
seen before it is built.
|
|
564
573
|
- **Navigation at every size, exactly as the spec's `nav:` says for that size.** On
|
|
565
574
|
the phone that usually means the Menu button, drawn where `DECISIONS.md` puts it —
|
|
566
575
|
and, because an open menu changes the structure, a second phone frame labelled
|
|
@@ -634,7 +643,10 @@ for is review: is the header taking a sixth of the phone? Is a tap target under
|
|
|
634
643
|
|
|
635
644
|
**Check the drawing against the spec before anyone sees it.** For each size, go
|
|
636
645
|
down the spec's `regions:` and its `nav:` and find each one in that frame. A region
|
|
637
|
-
the spec names and the frame lacks is drawn now, not raised in review.
|
|
646
|
+
the spec names and the frame lacks is drawn now, not raised in review. The gate
|
|
647
|
+
checks an HTML drawing when you stop: a frame for each of the four sizes, each
|
|
648
|
+
size's regions labelled in its frame (by name where the spec names them), and a
|
|
649
|
+
frame either side of each recorded switch. This matters
|
|
638
650
|
most on the phone, where a region dropped for space is easy to miss and the reviewer
|
|
639
651
|
is looking at what is there, not at what is not.
|
|
640
652
|
|