jig-ui 0.10.0 → 0.11.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 +29 -0
- package/README.md +19 -7
- package/dist/index.js +144 -50
- package/package.json +1 -1
- package/templates/COMMAND.md.tmpl +10 -2
- package/templates/command-metadata.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
One release, one lesson: a measurement an agent can type is not a
|
|
6
|
+
measurement. Everything here came from two live runs at the capability
|
|
7
|
+
floor the day 0.10.0 shipped.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`jig probe --save <surface>`.** The CLI writes the probe file now. It
|
|
12
|
+
reads what the probe returned on stdin, checks it is probe output for a
|
|
13
|
+
page inside the project, and stamps it with that page's checksum and the
|
|
14
|
+
time. A run had written its own probe file by hand — five links and an
|
|
15
|
+
Escape key that closed a menu with thirteen links and no Escape handler —
|
|
16
|
+
and its review passed on those numbers.
|
|
17
|
+
- **`install --hook` / `--no-hook`.** The Stop hook is opt-in. An
|
|
18
|
+
interactive install offers it once and takes silence as no; `--yes` never
|
|
19
|
+
adds it, because that is the path an agent takes and nobody is there to
|
|
20
|
+
consent. `update` moves an existing hook and never adds one.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **Probe files are version 2.** `jig verdicts` rejects a probe with no
|
|
25
|
+
stamp — nothing measured it — and one whose page changed after it was
|
|
26
|
+
taken. Version 1 files no longer validate.
|
|
27
|
+
- **README and `init` say which directory to commit, and why.** The tokens
|
|
28
|
+
live in `<css dir>/jig/`; `.jig/` holds `state.json`, specs, mockups and
|
|
29
|
+
critique verdicts. Both are warned about separately, each with what
|
|
30
|
+
ignoring it costs. Since 0.7.0 both had said `.jig/` holds the tokens.
|
|
31
|
+
|
|
3
32
|
## 0.10.0
|
|
4
33
|
|
|
5
34
|
Mobile-first, and a design loop that a weak model cannot skip. Everything here
|
package/README.md
CHANGED
|
@@ -149,13 +149,23 @@ Set `brand` in `jig.config.json` to put it somewhere else. Projects set up
|
|
|
149
149
|
before 0.7.0 keep their `.jig/tokens/` layout; `update` does not move them, and
|
|
150
150
|
`init` offers to.
|
|
151
151
|
|
|
152
|
-
The mode file is the one
|
|
153
|
-
edge in a build graph and has to resolve locally, on
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
152
|
+
The mode file is the one token file copied verbatim from the package: a
|
|
153
|
+
stylesheet `@import` is an edge in a build graph and has to resolve locally, on
|
|
154
|
+
every machine that builds. The brand file and the barrel are generated for your
|
|
155
|
+
project, not copied.
|
|
156
|
+
|
|
157
|
+
**Commit the token directory** — `<css dir>/jig/`. It holds the files your
|
|
158
|
+
stylesheet `@import`s, so ignoring it means the design system does not exist for
|
|
159
|
+
anyone who did not run `init` themselves: their build breaks on a missing import,
|
|
160
|
+
and CI's `jig check` sees no token layer at all.
|
|
161
|
+
|
|
162
|
+
**Commit `.jig/` too.** It is not the tokens (it was, before 0.7.0). It holds
|
|
163
|
+
`state.json`, which records the version and checksum of every file `init` wrote
|
|
164
|
+
and is what `update` reads to know what it may refresh, plus the durable record of
|
|
165
|
+
the design loop: your specs, approved mockups and critique verdicts. Ignored, a
|
|
166
|
+
teammate's `update` cannot tell a file you edited from one it wrote, and every
|
|
167
|
+
spec the project agreed is invisible to the next agent. `init` warns if it finds
|
|
168
|
+
either directory ignored.
|
|
159
169
|
|
|
160
170
|
Add `--yes` to accept every derived default non-interactively — the mode CI and
|
|
161
171
|
agents run in. It states the mode it chose and where to change it, because
|
|
@@ -370,6 +380,8 @@ treatment.
|
|
|
370
380
|
| `<css dir>/jig/brand.*.css` | Identity. One per project. |
|
|
371
381
|
| `<css dir>/jig/mode.*.css` | Density, scale, rhythm, motion |
|
|
372
382
|
| `<css dir>/jig/theme.css` | The barrel — brand + mode. This is what you import. |
|
|
383
|
+
| `.jig/state.json` | What `init` wrote, with checksums. `update` reads it to leave your edits alone. |
|
|
384
|
+
| `.jig/specs/`, `.jig/mockups/`, `.jig/critique/` | The design loop's record: what was agreed, what was drawn, what the review found. |
|
|
373
385
|
|
|
374
386
|
`rules/*` and `rules.index.json` live beside your installed skill file, not
|
|
375
387
|
in the project — see above.
|
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 readFileSync28 } from "fs";
|
|
6
6
|
import { homedir } from "os";
|
|
7
|
-
import { join as
|
|
7
|
+
import { join as join34 } from "path";
|
|
8
8
|
|
|
9
9
|
// src/paths.ts
|
|
10
10
|
import { existsSync } from "fs";
|
|
@@ -4847,9 +4847,16 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
4847
4847
|
log(` ${snippet.split("\n").join("\n ")}`);
|
|
4848
4848
|
}
|
|
4849
4849
|
}
|
|
4850
|
+
const tokenDirRel = tokensRelDir.join("/");
|
|
4851
|
+
if (isIgnored(opts.projectRoot, tokenDirRel)) {
|
|
4852
|
+
log(
|
|
4853
|
+
`
|
|
4854
|
+
WARNING: ${tokenDirRel}/ is gitignored, but it holds this project's tokens \u2014 the files your stylesheet @imports. Committed, teammates and CI get the same design system; ignored, their builds break on a missing import. Remove the ${tokenDirRel}/ rule from .gitignore, or commit the directory explicitly.`
|
|
4855
|
+
);
|
|
4856
|
+
}
|
|
4850
4857
|
if (isIgnored(opts.projectRoot, ".jig")) {
|
|
4851
4858
|
log(
|
|
4852
|
-
"\n WARNING: .jig/ is gitignored
|
|
4859
|
+
"\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."
|
|
4853
4860
|
);
|
|
4854
4861
|
}
|
|
4855
4862
|
const baselineResult = check({ projectRoot: opts.projectRoot, homeDir: opts.homeDir, version: opts.version, all: true, ci: false });
|
|
@@ -4873,8 +4880,8 @@ Could not write to ${wireTarget}: ${err.message}`);
|
|
|
4873
4880
|
}
|
|
4874
4881
|
|
|
4875
4882
|
// src/commands/verdicts.ts
|
|
4876
|
-
import { existsSync as
|
|
4877
|
-
import { join as
|
|
4883
|
+
import { existsSync as existsSync18, readFileSync as readFileSync25 } from "fs";
|
|
4884
|
+
import { join as join31, resolve as resolve4 } from "path";
|
|
4878
4885
|
|
|
4879
4886
|
// src/rules/citations.ts
|
|
4880
4887
|
import { join as join28 } from "path";
|
|
@@ -4888,11 +4895,16 @@ function citableIds(packageRoot2) {
|
|
|
4888
4895
|
}
|
|
4889
4896
|
|
|
4890
4897
|
// src/probe/check.ts
|
|
4891
|
-
import { existsSync as
|
|
4892
|
-
import { join as
|
|
4898
|
+
import { existsSync as existsSync17, readdirSync as readdirSync11, readFileSync as readFileSync24 } from "fs";
|
|
4899
|
+
import { join as join30 } from "path";
|
|
4900
|
+
|
|
4901
|
+
// src/probe/save.ts
|
|
4902
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync23, writeFileSync as writeFileSync6 } from "fs";
|
|
4903
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4904
|
+
import { join as join29, relative as relative3, resolve as resolve3 } from "path";
|
|
4893
4905
|
|
|
4894
4906
|
// src/probe/script.ts
|
|
4895
|
-
var PROBE_VERSION =
|
|
4907
|
+
var PROBE_VERSION = 2;
|
|
4896
4908
|
var PROBE_SCRIPT = `(async () => {
|
|
4897
4909
|
const doc = document.documentElement;
|
|
4898
4910
|
const vis = (e) => { const r = e.getBoundingClientRect(); const s = getComputedStyle(e); return r.width > 0 && r.height > 0 && s.visibility !== 'hidden' && s.display !== 'none'; };
|
|
@@ -4962,17 +4974,79 @@ var PROBE_SCRIPT = `(async () => {
|
|
|
4962
4974
|
});
|
|
4963
4975
|
})()`;
|
|
4964
4976
|
|
|
4977
|
+
// src/probe/save.ts
|
|
4978
|
+
function saveProbe(opts) {
|
|
4979
|
+
let probe;
|
|
4980
|
+
try {
|
|
4981
|
+
probe = JSON.parse(opts.json);
|
|
4982
|
+
} catch (e) {
|
|
4983
|
+
throw new Error(`That is not the probe's output: ${e.message}. Evaluate \`jig probe\` in the browser and pipe what it returns straight in.`);
|
|
4984
|
+
}
|
|
4985
|
+
if (probe.jigProbe !== PROBE_VERSION) {
|
|
4986
|
+
throw new Error(`That JSON is not version ${PROBE_VERSION} probe output (jigProbe: ${JSON.stringify(probe.jigProbe)}). Re-read \`jig probe\` and evaluate it; do not write the file yourself.`);
|
|
4987
|
+
}
|
|
4988
|
+
const width = typeof probe.width === "number" ? probe.width : NaN;
|
|
4989
|
+
if (!Number.isFinite(width)) throw new Error("The probe output has no width. Evaluate it in a browser, at the width you are judging.");
|
|
4990
|
+
const url = typeof probe.url === "string" ? probe.url : "";
|
|
4991
|
+
const page = pageFile(opts.projectRoot, url);
|
|
4992
|
+
if (!page) {
|
|
4993
|
+
throw new Error(`The probe ran at ${url || "no url"}, which is not a file in this project. Open the page you built \u2014 a file:// URL under the project \u2014 and probe that.`);
|
|
4994
|
+
}
|
|
4995
|
+
probe.pageFile = relative3(opts.projectRoot, page).split("\\").join("/");
|
|
4996
|
+
probe.pageChecksum = checksum(readFileSync23(page, "utf8"));
|
|
4997
|
+
probe.recordedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4998
|
+
const dir = join29(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
4999
|
+
mkdirSync7(dir, { recursive: true });
|
|
5000
|
+
const path = join29(dir, `probe-${width}.json`);
|
|
5001
|
+
writeFileSync6(path, JSON.stringify(probe), "utf8");
|
|
5002
|
+
return { path: `.jig/critique/${opts.surface}/probe-${width}.json`, width, page: probe.pageFile };
|
|
5003
|
+
}
|
|
5004
|
+
function pageFile(projectRoot, url) {
|
|
5005
|
+
if (!url.startsWith("file://")) return void 0;
|
|
5006
|
+
let path;
|
|
5007
|
+
try {
|
|
5008
|
+
path = fileURLToPath2(url.split("#")[0].split("?")[0]);
|
|
5009
|
+
} catch {
|
|
5010
|
+
return void 0;
|
|
5011
|
+
}
|
|
5012
|
+
const abs = resolve3(path);
|
|
5013
|
+
if (!abs.startsWith(resolve3(projectRoot))) return void 0;
|
|
5014
|
+
return existsSync16(abs) ? abs : void 0;
|
|
5015
|
+
}
|
|
5016
|
+
|
|
4965
5017
|
// src/probe/check.ts
|
|
4966
|
-
function
|
|
4967
|
-
if (!
|
|
5018
|
+
function stampProblem(projectRoot, file, p) {
|
|
5019
|
+
if (!p.pageChecksum || !p.pageFile) {
|
|
5020
|
+
return `${file} was not written by \`jig probe --save\`, so nothing measured it. Evaluate \`jig probe\` in the browser and pipe its output into \`jig probe --save <surface>\`.`;
|
|
5021
|
+
}
|
|
5022
|
+
const page = pageFile(projectRoot, p.url ?? "");
|
|
5023
|
+
if (!page) return `${file} names a page outside this project (${p.url ?? "no url"}).`;
|
|
5024
|
+
let current;
|
|
5025
|
+
try {
|
|
5026
|
+
current = readFileSync24(page, "utf8");
|
|
5027
|
+
} catch {
|
|
5028
|
+
return `${file} was taken on ${p.pageFile}, which no longer exists.`;
|
|
5029
|
+
}
|
|
5030
|
+
if (checksum(current) !== p.pageChecksum) {
|
|
5031
|
+
return `${file} was taken on an older ${p.pageFile} \u2014 the page changed after it. Render and probe again at that width.`;
|
|
5032
|
+
}
|
|
5033
|
+
return void 0;
|
|
5034
|
+
}
|
|
5035
|
+
function readProbes(projectRoot, dir, errors) {
|
|
5036
|
+
if (!existsSync17(dir)) return [];
|
|
4968
5037
|
const probes = [];
|
|
4969
5038
|
for (const f of readdirSync11(dir).filter((n) => /^probe-\d+\.json$/.test(n)).sort()) {
|
|
4970
5039
|
try {
|
|
4971
|
-
const p = JSON.parse(
|
|
5040
|
+
const p = JSON.parse(readFileSync24(join30(dir, f), "utf8"));
|
|
4972
5041
|
if (p.jigProbe !== PROBE_VERSION || typeof p.width !== "number") {
|
|
4973
5042
|
errors.push(`${f} is not output of \`jig probe\` (version ${PROBE_VERSION}). Re-run the probe; do not write it by hand.`);
|
|
4974
5043
|
continue;
|
|
4975
5044
|
}
|
|
5045
|
+
const stale = stampProblem(projectRoot, f, p);
|
|
5046
|
+
if (stale) {
|
|
5047
|
+
errors.push(stale);
|
|
5048
|
+
continue;
|
|
5049
|
+
}
|
|
4976
5050
|
probes.push(p);
|
|
4977
5051
|
} catch (e) {
|
|
4978
5052
|
errors.push(`${f}: not valid JSON (${e.message}).`);
|
|
@@ -5033,18 +5107,18 @@ function probeContradictions(probes, verdictOf) {
|
|
|
5033
5107
|
var VERDICTS = ["ok", "finding", "n/a"];
|
|
5034
5108
|
var ABSENCE = /\b(rule (not found|does not exist)|context unavailable|cannot (find|read|access) (the )?rule|not in (the )?(accessible )?corpus)\b/i;
|
|
5035
5109
|
function readJson(path, errors) {
|
|
5036
|
-
if (!
|
|
5110
|
+
if (!existsSync18(path)) return null;
|
|
5037
5111
|
try {
|
|
5038
|
-
return JSON.parse(
|
|
5112
|
+
return JSON.parse(readFileSync25(path, "utf8"));
|
|
5039
5113
|
} catch (e) {
|
|
5040
5114
|
errors.push(`${path}: not valid JSON (${e.message})`);
|
|
5041
5115
|
return {};
|
|
5042
5116
|
}
|
|
5043
5117
|
}
|
|
5044
5118
|
function specNeedsNav(projectRoot, surface) {
|
|
5045
|
-
const path =
|
|
5046
|
-
if (!
|
|
5047
|
-
const front =
|
|
5119
|
+
const path = join31(projectRoot, ".jig", "specs", `${surface}.spec.md`);
|
|
5120
|
+
if (!existsSync18(path)) return false;
|
|
5121
|
+
const front = readFileSync25(path, "utf8").split(/^---\s*$/m)[1] ?? "";
|
|
5048
5122
|
const navField = [...front.matchAll(/^\s*nav:\s*(.+)$/gim)].some((m) => !/^\s*(none|n\/a|-)\b/i.test(m[1]));
|
|
5049
5123
|
const navRegion = /^\s*-\s*(nav|navigation)\s*:/im.test(front);
|
|
5050
5124
|
return navField || navRegion;
|
|
@@ -5095,7 +5169,7 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
|
|
|
5095
5169
|
function verifyVerdicts(opts) {
|
|
5096
5170
|
const root = opts.packageRoot ?? assetRoot();
|
|
5097
5171
|
const errors = [];
|
|
5098
|
-
const index = JSON.parse(
|
|
5172
|
+
const index = JSON.parse(readFileSync25(join31(root, "rules.index.json"), "utf8"));
|
|
5099
5173
|
const judgment = index.filter((r) => r.bucket === "judgment");
|
|
5100
5174
|
const screenIds = judgment.filter((r) => r.pass === "screen").map((r) => r.id);
|
|
5101
5175
|
const codeIds = judgment.filter((r) => r.pass === "code").map((r) => r.id);
|
|
@@ -5104,21 +5178,21 @@ function verifyVerdicts(opts) {
|
|
|
5104
5178
|
...judgment.map((r) => [r.id, r.pass ?? ""])
|
|
5105
5179
|
]);
|
|
5106
5180
|
const specIds = new Set(citableIds(root).filter((id) => /^[PMLRT]-\d+$/.test(id)));
|
|
5107
|
-
const dir =
|
|
5108
|
-
const screenFile = readJson(
|
|
5109
|
-
const codeFile = readJson(
|
|
5181
|
+
const dir = join31(opts.projectRoot, ".jig", "critique", opts.surface);
|
|
5182
|
+
const screenFile = readJson(join31(dir, "screen.json"), errors);
|
|
5183
|
+
const codeFile = readJson(join31(dir, "code.json"), errors);
|
|
5110
5184
|
const screenExtraRequired = specNeedsNav(opts.projectRoot, opts.surface) ? ["P-14"] : [];
|
|
5111
5185
|
const screen = checkArm("screen", screenFile, screenIds, passOf, specIds, screenExtraRequired, errors);
|
|
5112
5186
|
const code = checkArm("code", codeFile, codeIds, passOf, specIds, [], errors);
|
|
5113
5187
|
let rendered = false;
|
|
5114
5188
|
if (screenFile && screenFile.rendered === true) {
|
|
5115
5189
|
const artefacts = Array.isArray(screenFile.artefacts) ? screenFile.artefacts.filter((a) => typeof a === "string") : [];
|
|
5116
|
-
const absent = artefacts.filter((a) => !
|
|
5190
|
+
const absent = artefacts.filter((a) => !existsSync18(resolve4(opts.projectRoot, a)));
|
|
5117
5191
|
if (artefacts.length === 0) errors.push("screen.json says rendered: true but lists no artefacts. A render leaves a screenshot.");
|
|
5118
5192
|
else if (absent.length) errors.push(`screen.json says rendered: true, but these artefacts do not exist: ${absent.join(", ")}.`);
|
|
5119
5193
|
else rendered = true;
|
|
5120
5194
|
}
|
|
5121
|
-
const probes = readProbes(dir, errors);
|
|
5195
|
+
const probes = readProbes(opts.projectRoot, dir, errors);
|
|
5122
5196
|
if (screenFile && screenFile.rendered === true) {
|
|
5123
5197
|
const widths = new Set(probes.map((p) => p.width));
|
|
5124
5198
|
const missing = [360, 768, 1280].filter((w) => !widths.has(w));
|
|
@@ -5140,19 +5214,19 @@ function verifyVerdicts(opts) {
|
|
|
5140
5214
|
}
|
|
5141
5215
|
|
|
5142
5216
|
// src/commands/gate.ts
|
|
5143
|
-
import { existsSync as
|
|
5144
|
-
import { join as
|
|
5217
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync8, readdirSync as readdirSync13, readFileSync as readFileSync27, writeFileSync as writeFileSync7 } from "fs";
|
|
5218
|
+
import { join as join33 } from "path";
|
|
5145
5219
|
|
|
5146
5220
|
// src/check/spec-shape.ts
|
|
5147
|
-
import { existsSync as
|
|
5148
|
-
import { join as
|
|
5221
|
+
import { existsSync as existsSync19, readFileSync as readFileSync26, readdirSync as readdirSync12, statSync as statSync2 } from "fs";
|
|
5222
|
+
import { join as join32 } from "path";
|
|
5149
5223
|
function newestSpec(projectRoot) {
|
|
5150
|
-
const dir =
|
|
5151
|
-
if (!
|
|
5224
|
+
const dir = join32(projectRoot, ".jig", "specs");
|
|
5225
|
+
if (!existsSync19(dir)) return void 0;
|
|
5152
5226
|
const files = readdirSync12(dir).filter((f) => f.endsWith(".md"));
|
|
5153
5227
|
if (files.length === 0) return void 0;
|
|
5154
|
-
const newest = files.map((f) => ({ f, at: statSync2(
|
|
5155
|
-
return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body:
|
|
5228
|
+
const newest = files.map((f) => ({ f, at: statSync2(join32(dir, f)).mtimeMs })).sort((a, b) => b.at - a.at)[0].f;
|
|
5229
|
+
return { path: `.jig/specs/${newest}`, slug: newest.replace(/\.spec\.md$|\.md$/, ""), body: readFileSync26(join32(dir, newest), "utf8") };
|
|
5156
5230
|
}
|
|
5157
5231
|
function specProblems(spec) {
|
|
5158
5232
|
const parts = spec.body.split(/^---\s*$/m);
|
|
@@ -5221,10 +5295,10 @@ function navProblems(spec) {
|
|
|
5221
5295
|
// src/commands/gate.ts
|
|
5222
5296
|
var MAX_BLOCKS = 3;
|
|
5223
5297
|
function lastJigCommand(transcriptPath) {
|
|
5224
|
-
if (!transcriptPath || !
|
|
5298
|
+
if (!transcriptPath || !existsSync20(transcriptPath)) return void 0;
|
|
5225
5299
|
let text;
|
|
5226
5300
|
try {
|
|
5227
|
-
text =
|
|
5301
|
+
text = readFileSync27(transcriptPath, "utf8");
|
|
5228
5302
|
} catch {
|
|
5229
5303
|
return void 0;
|
|
5230
5304
|
}
|
|
@@ -5242,10 +5316,10 @@ function commandProblems(root, command) {
|
|
|
5242
5316
|
const problems = [];
|
|
5243
5317
|
const spec = newestSpec(root);
|
|
5244
5318
|
if (command === "decide") {
|
|
5245
|
-
const file = ["jig/DECISIONS.md", "DECISIONS.md", ".jig/DECISIONS.md"].map((p) =>
|
|
5319
|
+
const file = ["jig/DECISIONS.md", "DECISIONS.md", ".jig/DECISIONS.md"].map((p) => join33(root, p)).find((p) => existsSync20(p));
|
|
5246
5320
|
if (!file) problems.push("decide wrote no DECISIONS.md beside the token layer.");
|
|
5247
5321
|
else {
|
|
5248
|
-
const body =
|
|
5322
|
+
const body = readFileSync27(file, "utf8");
|
|
5249
5323
|
if (!/^##\s+Unresolved\s*$/im.test(body)) {
|
|
5250
5324
|
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.`");
|
|
5251
5325
|
}
|
|
@@ -5266,13 +5340,13 @@ function commandProblems(root, command) {
|
|
|
5266
5340
|
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
5267
5341
|
if (/^approved/i.test(mockup)) {
|
|
5268
5342
|
if (!at) problems.push(`${spec.path}: \`mockup_at:\` is empty. Record where the approved drawing is.`);
|
|
5269
|
-
else if (!/^https?:/i.test(at) && !
|
|
5343
|
+
else if (!/^https?:/i.test(at) && !existsSync20(join33(root, at))) problems.push(`${spec.path}: \`mockup_at: ${at}\` does not exist.`);
|
|
5270
5344
|
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.`);
|
|
5271
5345
|
}
|
|
5272
5346
|
}
|
|
5273
5347
|
if (command === "critique") {
|
|
5274
|
-
const dir =
|
|
5275
|
-
const surfaces =
|
|
5348
|
+
const dir = join33(root, ".jig", "critique");
|
|
5349
|
+
const surfaces = existsSync20(dir) ? readdirSync13(dir).filter((s) => existsSync20(join33(dir, s, "screen.json")) || existsSync20(join33(dir, s, "code.json"))) : [];
|
|
5276
5350
|
for (const surface of surfaces) {
|
|
5277
5351
|
const v = verifyVerdicts({ projectRoot: root, surface });
|
|
5278
5352
|
if (v.ok && v.screen.state === "skipped") {
|
|
@@ -5287,7 +5361,7 @@ function commandProblems(root, command) {
|
|
|
5287
5361
|
}
|
|
5288
5362
|
function gate(opts) {
|
|
5289
5363
|
const root = opts.projectRoot;
|
|
5290
|
-
if (!
|
|
5364
|
+
if (!existsSync20(join33(root, "jig.config.json")) && !existsSync20(join33(root, ".jig"))) {
|
|
5291
5365
|
return { block: false, reason: "" };
|
|
5292
5366
|
}
|
|
5293
5367
|
const command = lastJigCommand(opts.input.transcript_path);
|
|
@@ -5306,11 +5380,11 @@ function gate(opts) {
|
|
|
5306
5380
|
);
|
|
5307
5381
|
}
|
|
5308
5382
|
}
|
|
5309
|
-
const critiqueDir =
|
|
5310
|
-
if (
|
|
5383
|
+
const critiqueDir = join33(root, ".jig", "critique");
|
|
5384
|
+
if (existsSync20(critiqueDir)) {
|
|
5311
5385
|
for (const surface of readdirSync13(critiqueDir)) {
|
|
5312
|
-
const dir =
|
|
5313
|
-
if (!
|
|
5386
|
+
const dir = join33(critiqueDir, surface);
|
|
5387
|
+
if (!existsSync20(join33(dir, "screen.json")) && !existsSync20(join33(dir, "code.json"))) continue;
|
|
5314
5388
|
const v = verifyVerdicts({ projectRoot: root, surface });
|
|
5315
5389
|
if (!v.ok) {
|
|
5316
5390
|
const shown = v.errors.slice(0, 6).map((e) => ` ${e}`);
|
|
@@ -5323,10 +5397,10 @@ function gate(opts) {
|
|
|
5323
5397
|
}
|
|
5324
5398
|
}
|
|
5325
5399
|
const session = opts.input.session_id ?? "unknown";
|
|
5326
|
-
const stateFile =
|
|
5400
|
+
const stateFile = join33(root, ".jig", "gate.json");
|
|
5327
5401
|
let state = {};
|
|
5328
5402
|
try {
|
|
5329
|
-
state = JSON.parse(
|
|
5403
|
+
state = JSON.parse(readFileSync27(stateFile, "utf8"));
|
|
5330
5404
|
} catch {
|
|
5331
5405
|
}
|
|
5332
5406
|
if (problems.length === 0) {
|
|
@@ -5355,15 +5429,15 @@ ${problems.join("\n\n")}`
|
|
|
5355
5429
|
}
|
|
5356
5430
|
function save(file, state) {
|
|
5357
5431
|
try {
|
|
5358
|
-
|
|
5359
|
-
|
|
5432
|
+
mkdirSync8(join33(file, ".."), { recursive: true });
|
|
5433
|
+
writeFileSync7(file, JSON.stringify(state), "utf8");
|
|
5360
5434
|
} catch {
|
|
5361
5435
|
}
|
|
5362
5436
|
}
|
|
5363
5437
|
|
|
5364
5438
|
// src/index.ts
|
|
5365
5439
|
var packageRoot = getPackageRoot();
|
|
5366
|
-
var { version } = JSON.parse(
|
|
5440
|
+
var { version } = JSON.parse(readFileSync28(join34(packageRoot, "package.json"), "utf8"));
|
|
5367
5441
|
function warnIfUnpublishedPin() {
|
|
5368
5442
|
if (isPublishedBuild(packageRoot)) return;
|
|
5369
5443
|
console.warn(
|
|
@@ -5489,13 +5563,33 @@ program.command("check").description("Check the repo against Jig's mechanical +
|
|
|
5489
5563
|
process.exit(1);
|
|
5490
5564
|
}
|
|
5491
5565
|
});
|
|
5492
|
-
program.command("probe").description("Print the render probe
|
|
5493
|
-
|
|
5566
|
+
program.command("probe").description("Print the render probe. With --save, read what it returned on stdin and record it for `jig verdicts`.").option("--save <surface>", "record the probe's output (piped in) under .jig/critique/<surface>/").action(async (opts) => {
|
|
5567
|
+
if (!opts.save) {
|
|
5568
|
+
console.log(PROBE_SCRIPT);
|
|
5569
|
+
return;
|
|
5570
|
+
}
|
|
5571
|
+
const projectRoot = findProjectRoot(process.cwd());
|
|
5572
|
+
try {
|
|
5573
|
+
const json = process.stdin.isTTY ? "" : await new Promise((done, fail) => {
|
|
5574
|
+
let text = "";
|
|
5575
|
+
process.stdin.setEncoding("utf8");
|
|
5576
|
+
process.stdin.on("data", (chunk) => {
|
|
5577
|
+
text += chunk;
|
|
5578
|
+
});
|
|
5579
|
+
process.stdin.on("end", () => done(text));
|
|
5580
|
+
process.stdin.on("error", fail);
|
|
5581
|
+
});
|
|
5582
|
+
const result = saveProbe({ projectRoot, surface: opts.save, json });
|
|
5583
|
+
console.log(` Recorded ${result.path} \u2014 ${result.page} at ${result.width}px.`);
|
|
5584
|
+
} catch (err) {
|
|
5585
|
+
console.error(` \u2717 ${err.message}`);
|
|
5586
|
+
process.exit(1);
|
|
5587
|
+
}
|
|
5494
5588
|
});
|
|
5495
5589
|
program.command("gate").description("Run by the Claude Code Stop hook: block stopping while check or a critique fails.").action(() => {
|
|
5496
5590
|
let input = {};
|
|
5497
5591
|
try {
|
|
5498
|
-
if (!process.stdin.isTTY) input = JSON.parse(
|
|
5592
|
+
if (!process.stdin.isTTY) input = JSON.parse(readFileSync28(0, "utf8") || "{}");
|
|
5499
5593
|
} catch {
|
|
5500
5594
|
}
|
|
5501
5595
|
const cwd = input.cwd ?? process.cwd();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jig-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "A design system for coding agents. 113 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",
|
|
@@ -853,10 +853,18 @@ probe in the browser and save what it returns beside the verdicts:
|
|
|
853
853
|
|
|
854
854
|
```
|
|
855
855
|
{{scripts_path}} probe > .jig/probe.js
|
|
856
|
-
# at each width: set the viewport, open the page, evaluate .jig/probe.js,
|
|
857
|
-
#
|
|
856
|
+
# at each width: set the viewport, open the page, evaluate .jig/probe.js, and pipe
|
|
857
|
+
# exactly what it returned into:
|
|
858
|
+
{{scripts_path}} probe --save <surface>
|
|
858
859
|
```
|
|
859
860
|
|
|
861
|
+
**The CLI writes the probe file, you do not.** `--save` reads the probe's output,
|
|
862
|
+
stamps it with the page's checksum, and stores it. That is what makes the file a
|
|
863
|
+
measurement: `verdicts` recomputes the checksum, so a file written by hand, or one
|
|
864
|
+
taken before the page's last edit, is rejected. A live run wrote a probe file itself
|
|
865
|
+
— five links and Escape closing a menu that has thirteen links and no Escape handler
|
|
866
|
+
— and its review passed on those numbers.
|
|
867
|
+
|
|
860
868
|
The probe opens the phone menu, presses Escape, measures sideways scroll, and reads
|
|
861
869
|
whether the styles and tokens actually applied. `verdicts` refuses `rendered: true`
|
|
862
870
|
without a probe at each width, and refuses an `ok` the probe contradicts — a `P-14`
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"status": "available"
|
|
31
31
|
},
|
|
32
32
|
"probe": {
|
|
33
|
-
"description": "Print the render probe: a script the critique runs in a browser at 360, 768 and 1280px.
|
|
34
|
-
"argumentHint": "",
|
|
33
|
+
"description": "Print the render probe: a script the critique runs in a browser at 360, 768 and 1280px. With --save it records what the probe returned, stamped with the page's checksum, so `verdicts` can tell a measurement from a claim.",
|
|
34
|
+
"argumentHint": "[--save <surface>]",
|
|
35
35
|
"status": "available"
|
|
36
36
|
},
|
|
37
37
|
"gate": {
|