jig-ui 0.20.1 → 0.21.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 +70 -0
- package/dist/index.js +264 -25
- package/package.json +1 -1
- package/templates/COMMAND.md.tmpl +27 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.0 (2026-09-28)
|
|
4
|
+
|
|
5
|
+
Dev builds, so a fix is tried on a real project before it ships, and the gate
|
|
6
|
+
fixes that trial on jig-site turned up.
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **Dev builds.** A version carrying `-dev` (`0.21.0-dev.1`) is a local build
|
|
11
|
+
of a fix, tried on a real project before it is released. npm has no such
|
|
12
|
+
version, so its skill, its commands and its Stop hook run the `jig` on PATH
|
|
13
|
+
rather than `npx jig-ui@<version>`; a hook that could not run would let every
|
|
14
|
+
agent stop unchecked. `scripts/pack-dev.mjs` packs one. Installing a release
|
|
15
|
+
afterwards puts the `npx` hook back in place of the dev one.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **The Stop hook refreshes only the probes it judges.** It re-rendered every
|
|
20
|
+
critiqued page on every stop, so a link added to jig-site's header left sixty
|
|
21
|
+
probe files changed on pages no one was working on, and three sessions
|
|
22
|
+
committed them. A page's probes are refreshed when its own critique or tweak
|
|
23
|
+
runs.
|
|
24
|
+
- **A mockup is checked before it goes to the owner.** The pause for approval
|
|
25
|
+
skipped the drawing check, so on jig-site two mockup sessions put a drawing
|
|
26
|
+
the check refused to the owner, reported the gate's blocks as "waiting on
|
|
27
|
+
owner review", and the owner approved a drawing the gate then refused `make`
|
|
28
|
+
for.
|
|
29
|
+
- **A region label may say its name with spaces.** "on this page" now labels the
|
|
30
|
+
spec's `on-this-page`; hyphens, underscores and spaces compare as one.
|
|
31
|
+
- **A comment after a size is a comment.** `phone: # judged at 360px` over a
|
|
32
|
+
full composition was refused as a one-line size.
|
|
33
|
+
- **A critique's report names every finding its verdict files hold.** jig-site's
|
|
34
|
+
Guide critique held seven and listed six; make reads the report. Checked
|
|
35
|
+
where the project keeps a `REPORT.md` beside the verdicts.
|
|
36
|
+
- **A session cannot change Jig's mode file.** A jig-site make round narrowed
|
|
37
|
+
`--size-rail` in `mode.editorial.css` to make three columns fit; `update`
|
|
38
|
+
owns that file, and a project's own values go in its brand file or stylesheet.
|
|
39
|
+
- **A decide amendment answers for its own reason.** Like a tweak's, only the
|
|
40
|
+
`**Why…:**` paragraphs it adds must quote the owner; three jig-site amendments
|
|
41
|
+
had to relabel an earlier round's reason before the gate let them stop.
|
|
42
|
+
- **A finding is fixed only where the source it cited changed.** `verdicts`
|
|
43
|
+
told jig-site's fourth home critique that seven findings were fixed; four had
|
|
44
|
+
flipped because its readers read the same unchanged lines differently. A
|
|
45
|
+
finding judged ok is now fixed when a line it cited (`file:line`) changed, or
|
|
46
|
+
its file when it cited no line, or any source when it cited nothing. The rest
|
|
47
|
+
are reported as judged ok though the lines they cited did not change: a
|
|
48
|
+
different reading, or a fix made where the finding did not point.
|
|
49
|
+
- **The gate asks for the verdict lock to be committed.** It writes
|
|
50
|
+
`verdicts.lock` when a critique or tweak stops, after the agent has committed,
|
|
51
|
+
so every such session on jig-site left it behind. When the verdicts are
|
|
52
|
+
committed and the lock is not, it stops the session once to commit it.
|
|
53
|
+
- **A tweak's decision quotes the owner's recorded words.** A jig-site tweak
|
|
54
|
+
shown a screenshot wrote an exception to `E-51` "given directly by the owner
|
|
55
|
+
… by reference rather than words". A tweak's new `**Why:**` now meets
|
|
56
|
+
`decide`'s rule (the owner's words in quotation marks, `not given`, or
|
|
57
|
+
`**Why (inferred):**`), and its quotations must be in tweak.json's `change`.
|
|
58
|
+
A reason `decide` wrote earlier is not the tweak's to answer for.
|
|
59
|
+
- **`spec`, `mockup` and `critique` write under `.jig/` only.** On jig-site a
|
|
60
|
+
critique swapped the rule its page demonstrates to get past a block, then
|
|
61
|
+
re-judged the page it had changed, and a mockup session wrote the site's
|
|
62
|
+
stylesheet. The gate stops either.
|
|
63
|
+
- **An approved drawing changed afterwards goes back to pending.** The session
|
|
64
|
+
that recorded a jig-site approval then redrew frames and moved a switch.
|
|
65
|
+
- **Quoted tool output is not the page's copy.** The probe's em-dash scan skips
|
|
66
|
+
`<pre>`, `<code>`, `<samp>` and `<kbd>`, as it skips quotations: jig-site's
|
|
67
|
+
home page quotes `jig check`'s real output, whose wording has one. Probe
|
|
68
|
+
version 9.
|
|
69
|
+
- **A drawing's size frames are the class `frame`, not any class containing
|
|
70
|
+
it.** jig-site's home drawing captioned its rule specimens `spec-frame`, and
|
|
71
|
+
the gate split every size frame at each one and reported regions missing.
|
|
72
|
+
|
|
3
73
|
## 0.20.1 (2026-09-27)
|
|
4
74
|
|
|
5
75
|
`jig gate` run by hand no longer tells an agent that a passing tweak failed.
|
package/dist/index.js
CHANGED
|
@@ -40,6 +40,14 @@ function assetRoot(startDir = getPackageRoot()) {
|
|
|
40
40
|
function isPublishedBuild(packageRoot2) {
|
|
41
41
|
return packageRoot2.split(/[\\/]/).includes("node_modules");
|
|
42
42
|
}
|
|
43
|
+
function isDevVersion(version2) {
|
|
44
|
+
return !!version2 && /-dev(\.|$)/.test(version2);
|
|
45
|
+
}
|
|
46
|
+
function cliInvocation(version2) {
|
|
47
|
+
if (!version2) return "npx jig-ui";
|
|
48
|
+
return isDevVersion(version2) ? "jig" : `npx jig-ui@${version2}`;
|
|
49
|
+
}
|
|
50
|
+
var JIG_GATE_HOOK = /(?:\bjig-ui@[^\s"]+|^jig) gate\b/;
|
|
43
51
|
|
|
44
52
|
// src/commands/install.ts
|
|
45
53
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
@@ -578,7 +586,7 @@ function buildSkillBody(packageRoot2, rulesPath, version2) {
|
|
|
578
586
|
);
|
|
579
587
|
return render(template, {
|
|
580
588
|
command_prefix: "/jig ",
|
|
581
|
-
scripts_path: version2
|
|
589
|
+
scripts_path: cliInvocation(version2),
|
|
582
590
|
// `update` must NOT carry the pin. Pinned, it refreshes to the version
|
|
583
591
|
// already installed — reporting "Updated Jig → <same version>" for a no-op
|
|
584
592
|
// — so a reader following the skill could never upgrade.
|
|
@@ -597,7 +605,7 @@ function buildCommandBody(packageRoot2, rulesPath, version2, argsPlaceholder) {
|
|
|
597
605
|
const templatePath = join7(packageRoot2, "templates", "COMMAND.md.tmpl");
|
|
598
606
|
if (!existsSync5(templatePath)) return { body: "", subcommands };
|
|
599
607
|
const template = readFileSync4(templatePath, "utf8");
|
|
600
|
-
const body = template.replace(/\{\{command_prefix\}\}/g, "/jig ").replace(/\{\{args_placeholder\}\}/g, argsPlaceholder).replace(/\{\{subcommand_list\}\}/g, subcommands.join(", ")).replace(/\{\{scripts_path\}\}/g,
|
|
608
|
+
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");
|
|
601
609
|
const unresolved = [...new Set(body.match(/\{\{[a-z_]+\}\}/g) ?? [])];
|
|
602
610
|
if (unresolved.length > 0) {
|
|
603
611
|
throw new Error(
|
|
@@ -724,7 +732,8 @@ function install(opts) {
|
|
|
724
732
|
function hasStopHook(projectRoot) {
|
|
725
733
|
try {
|
|
726
734
|
const settings = readFileSync4(join7(projectRoot, ".claude", "settings.json"), "utf8");
|
|
727
|
-
|
|
735
|
+
const hooks = JSON.parse(settings).hooks?.Stop ?? [];
|
|
736
|
+
return hooks.some((group) => (group.hooks ?? []).some((h) => JIG_GATE_HOOK.test(h.command ?? "")));
|
|
728
737
|
} catch {
|
|
729
738
|
return false;
|
|
730
739
|
}
|
|
@@ -739,9 +748,9 @@ function installStopHook(projectRoot, version2) {
|
|
|
739
748
|
return false;
|
|
740
749
|
}
|
|
741
750
|
}
|
|
742
|
-
const command = `npx --yes jig-ui@${version2} gate`;
|
|
751
|
+
const command = isDevVersion(version2) ? "jig gate" : `npx --yes jig-ui@${version2} gate`;
|
|
743
752
|
const hooks = settings.hooks ?? {};
|
|
744
|
-
const isJig = (h) =>
|
|
753
|
+
const isJig = (h) => JIG_GATE_HOOK.test(h.command ?? "");
|
|
745
754
|
const stop = (hooks.Stop ?? []).map((group) => ({ ...group, hooks: group.hooks.filter((h) => !isJig(h)) })).filter((group) => group.hooks.length > 0);
|
|
746
755
|
stop.push({ hooks: [{ type: "command", command }] });
|
|
747
756
|
settings.hooks = { ...hooks, Stop: stop };
|
|
@@ -5771,7 +5780,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
|
|
|
5771
5780
|
import { join as join33, relative as relative3, resolve as resolve4, sep as sep5 } from "path";
|
|
5772
5781
|
|
|
5773
5782
|
// src/probe/script.ts
|
|
5774
|
-
var PROBE_VERSION =
|
|
5783
|
+
var PROBE_VERSION = 9;
|
|
5775
5784
|
var PROBE_SCRIPT = `(async () => {
|
|
5776
5785
|
const doc = document.documentElement;
|
|
5777
5786
|
// checkVisibility also sees content a closed <details> hides: Chromium hides
|
|
@@ -5786,9 +5795,12 @@ var PROBE_SCRIPT = `(async () => {
|
|
|
5786
5795
|
const text = document.body.innerText || '';
|
|
5787
5796
|
// I-118: a verbatim quotation keeps its own punctuation. Text marked as a
|
|
5788
5797
|
// quotation is the source's words, so it is taken out before the dash scan;
|
|
5789
|
-
// the page's own copy around it still counts.
|
|
5798
|
+
// the page's own copy around it still counts. So is a tool's output or a
|
|
5799
|
+
// command shown in <pre>, <code>, <samp> or <kbd>: jig-site's home page
|
|
5800
|
+
// quotes \`jig check\`'s real output, whose own wording has an em dash, and
|
|
5801
|
+
// the probe blocked a page for words it had rightly not rewritten.
|
|
5790
5802
|
let ownText = text;
|
|
5791
|
-
for (const q of document.querySelectorAll('blockquote, q')) {
|
|
5803
|
+
for (const q of document.querySelectorAll('blockquote, q, pre, code, samp, kbd')) {
|
|
5792
5804
|
const quoted = (q.innerText || '').trim();
|
|
5793
5805
|
if (quoted) ownText = ownText.split(quoted).join('\\n');
|
|
5794
5806
|
}
|
|
@@ -6557,12 +6569,22 @@ function decisionSections(body) {
|
|
|
6557
6569
|
close();
|
|
6558
6570
|
return sections;
|
|
6559
6571
|
}
|
|
6560
|
-
function
|
|
6572
|
+
function changedWhys(current, before, newOnly = false) {
|
|
6561
6573
|
const then = decisionSections(before);
|
|
6562
|
-
const
|
|
6574
|
+
const paragraphs = (text) => text.split(/\n\s*\n/).map((p) => p.trim()).filter((p) => /^\*\*Why\b[^*]*:\*\*/.test(p));
|
|
6575
|
+
const out = [];
|
|
6563
6576
|
for (const [name, text] of decisionSections(current)) {
|
|
6564
|
-
|
|
6565
|
-
|
|
6577
|
+
const old = then.get(name);
|
|
6578
|
+
if (old === text) continue;
|
|
6579
|
+
const kept = newOnly && old !== void 0 ? new Set(paragraphs(old)) : /* @__PURE__ */ new Set();
|
|
6580
|
+
const whys = paragraphs(text).filter((p) => !kept.has(p));
|
|
6581
|
+
if (whys.length) out.push([name, whys]);
|
|
6582
|
+
}
|
|
6583
|
+
return out;
|
|
6584
|
+
}
|
|
6585
|
+
function unsourcedReasons(current, before, opts = {}) {
|
|
6586
|
+
const problems = [];
|
|
6587
|
+
for (const [name, whys] of changedWhys(current, before, opts.newWhysOnly)) {
|
|
6566
6588
|
for (const why of whys) {
|
|
6567
6589
|
const label = /^\s*\*\*(Why\b[^*]*):\*\*/.exec(why)[1];
|
|
6568
6590
|
const said = why.replace(/^\s*\*\*Why\b[^*]*:\*\*/, "").trim();
|
|
@@ -6575,6 +6597,23 @@ function unsourcedReasons(current, before) {
|
|
|
6575
6597
|
}
|
|
6576
6598
|
return problems;
|
|
6577
6599
|
}
|
|
6600
|
+
function quotesNotFrom(current, before, words) {
|
|
6601
|
+
const norm = (t) => t.toLowerCase().replace(/[‘’]/g, "'").replace(/\s+/g, " ").trim();
|
|
6602
|
+
const said = norm(words);
|
|
6603
|
+
const problems = [];
|
|
6604
|
+
for (const [name, whys] of changedWhys(current, before, true)) {
|
|
6605
|
+
for (const why of whys) {
|
|
6606
|
+
const label = /^\s*\*\*(Why\b[^*]*):\*\*/.exec(why)[1];
|
|
6607
|
+
if (/inferred/i.test(label)) continue;
|
|
6608
|
+
const missing = [...why.matchAll(/["“]([^"”]{3,})["”]/g)].map((m) => m[1]).filter((q) => q.split(/…|\.\.\./).map((part) => norm(part).replace(/^[\s.,;:]+|[\s.,;:!?]+$/g, "")).filter(Boolean).some((part) => !said.includes(part)));
|
|
6609
|
+
if (missing.length) {
|
|
6610
|
+
problems.push(`"${name}": its \`**${label}:**\` quotes "${missing[0]}", which the owner's words in tweak.json (\`change\`) do not say. Quote the owner as tweak.json records them, or put your reading under \`**Why (inferred):**\`.`);
|
|
6611
|
+
break;
|
|
6612
|
+
}
|
|
6613
|
+
}
|
|
6614
|
+
}
|
|
6615
|
+
return problems;
|
|
6616
|
+
}
|
|
6578
6617
|
|
|
6579
6618
|
// src/check/spec-shape.ts
|
|
6580
6619
|
import { existsSync as existsSync22, readFileSync as readFileSync29, readdirSync as readdirSync14, statSync as statSync4 } from "fs";
|
|
@@ -6615,7 +6654,7 @@ function specProblems(spec) {
|
|
|
6615
6654
|
problems.push(`${spec.path} has no \`${size}:\` composition under \`sizes:\`. Every size is written in full, phone first; \`same-as:\` needs a \`why:\`.`);
|
|
6616
6655
|
continue;
|
|
6617
6656
|
}
|
|
6618
|
-
if (line[1].trim()) {
|
|
6657
|
+
if (line[1].replace(/(^|\s)#.*$/, "").trim()) {
|
|
6619
6658
|
problems.push(`${spec.path}: \`${size}:\` is a one-line description. A size is a composition \u2014 \`regions:\` in order, \`hierarchy:\`, \`nav:\` \u2014 each on its own line, or \`same-as:\` with a \`why:\`.`);
|
|
6620
6659
|
continue;
|
|
6621
6660
|
}
|
|
@@ -6918,10 +6957,12 @@ function previousFindings(projectRoot, dir) {
|
|
|
6918
6957
|
const files = ["screen.json", "code.json", "decisions.json"];
|
|
6919
6958
|
const paths = files.map((f) => `${rel}/${f}`);
|
|
6920
6959
|
let commit;
|
|
6960
|
+
let target;
|
|
6921
6961
|
try {
|
|
6922
6962
|
const dirty = git(["status", "--porcelain", "--", ...paths]).trim() !== "";
|
|
6923
6963
|
const touched = git(["log", "--format=%H", "-2", "--", ...paths]).split("\n").filter(Boolean);
|
|
6924
6964
|
commit = dirty ? touched[0] ?? "" : touched[1] ?? "";
|
|
6965
|
+
target = dirty ? void 0 : touched[0];
|
|
6925
6966
|
} catch {
|
|
6926
6967
|
return void 0;
|
|
6927
6968
|
}
|
|
@@ -6942,7 +6983,7 @@ function previousFindings(projectRoot, dir) {
|
|
|
6942
6983
|
}
|
|
6943
6984
|
for (const v of Array.isArray(body.verdicts) ? body.verdicts : []) {
|
|
6944
6985
|
const key = String(v.id ?? v.decision ?? "").trim();
|
|
6945
|
-
if (key && typeof v.verdict === "string") out.set(f === "decisions.json" ? `"${key}"` : key.toUpperCase(), v.verdict);
|
|
6986
|
+
if (key && typeof v.verdict === "string") out.set(f === "decisions.json" ? `"${key}"` : key.toUpperCase(), { verdict: v.verdict, reason: String(v.reason ?? "") });
|
|
6946
6987
|
}
|
|
6947
6988
|
return out;
|
|
6948
6989
|
};
|
|
@@ -6952,17 +6993,64 @@ function previousFindings(projectRoot, dir) {
|
|
|
6952
6993
|
for (const [k, v] of read(f, commit)) before.set(k, v);
|
|
6953
6994
|
for (const [k, v] of read(f)) now.set(k, v);
|
|
6954
6995
|
}
|
|
6955
|
-
const
|
|
6996
|
+
const changed = sourceChanges(git, commit, target);
|
|
6997
|
+
const result = { commit: commit.slice(0, 7), fixed: [], unchanged: [], open: [], added: [], ruled: [] };
|
|
6956
6998
|
for (const [k, v] of before) {
|
|
6957
|
-
if (v !== "finding") continue;
|
|
6958
|
-
const after = now.get(k);
|
|
6999
|
+
if (v.verdict !== "finding") continue;
|
|
7000
|
+
const after = now.get(k)?.verdict;
|
|
6959
7001
|
if (after === "finding") result.open.push(k);
|
|
6960
7002
|
else if (after === "ruled") result.ruled.push(k);
|
|
6961
|
-
else if (after === "ok" || after === "n/a") result.fixed.push(k);
|
|
7003
|
+
else if (after === "ok" || after === "n/a") (changed(v.reason) ? result.fixed : result.unchanged).push(k);
|
|
6962
7004
|
}
|
|
6963
|
-
for (const [k, v] of now) if (v === "finding" && before.get(k) !== "finding") result.added.push(k);
|
|
7005
|
+
for (const [k, v] of now) if (v.verdict === "finding" && before.get(k)?.verdict !== "finding") result.added.push(k);
|
|
6964
7006
|
return result;
|
|
6965
7007
|
}
|
|
7008
|
+
var CITATION = /([\w@.\-[\]/]*[\w\]-]\.[a-z][a-z0-9]{0,5})(?::(\d+(?:-\d+)?(?:,\s*\d+(?:-\d+)?)*))?/gi;
|
|
7009
|
+
function sourceChanges(git, from, to) {
|
|
7010
|
+
const range = to ? [from, to] : [from];
|
|
7011
|
+
let tracked = [];
|
|
7012
|
+
let changedFiles = /* @__PURE__ */ new Set();
|
|
7013
|
+
try {
|
|
7014
|
+
tracked = git(["ls-tree", "-r", "--name-only", from]).split("\n").filter((f) => f && !f.startsWith(".jig/"));
|
|
7015
|
+
changedFiles = new Set(git(["diff", "--name-only", ...range, "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean));
|
|
7016
|
+
} catch {
|
|
7017
|
+
}
|
|
7018
|
+
const hunks = /* @__PURE__ */ new Map();
|
|
7019
|
+
const oldRanges = (file) => {
|
|
7020
|
+
let out = hunks.get(file);
|
|
7021
|
+
if (out) return out;
|
|
7022
|
+
out = [];
|
|
7023
|
+
try {
|
|
7024
|
+
for (const m of git(["diff", "-U0", ...range, "--", file]).matchAll(/^@@ -(\d+)(?:,(\d+))? /gm)) {
|
|
7025
|
+
const start = Number(m[1]);
|
|
7026
|
+
const count = m[2] === void 0 ? 1 : Number(m[2]);
|
|
7027
|
+
out.push(count === 0 ? [start, start + 1] : [start, start + count - 1]);
|
|
7028
|
+
}
|
|
7029
|
+
} catch {
|
|
7030
|
+
}
|
|
7031
|
+
hunks.set(file, out);
|
|
7032
|
+
return out;
|
|
7033
|
+
};
|
|
7034
|
+
return (reason) => {
|
|
7035
|
+
let cited = false;
|
|
7036
|
+
for (const m of reason.matchAll(CITATION)) {
|
|
7037
|
+
const path = m[1].replace(/^\.\//, "");
|
|
7038
|
+
const matches2 = tracked.filter((f) => f === path || f.endsWith(`/${path}`));
|
|
7039
|
+
if (!matches2.length) continue;
|
|
7040
|
+
cited = true;
|
|
7041
|
+
const lines = (m[2] ?? "").split(",").map((r) => r.trim()).filter(Boolean).map((r) => {
|
|
7042
|
+
const [a, b] = r.split("-").map(Number);
|
|
7043
|
+
return [a, b ?? a];
|
|
7044
|
+
});
|
|
7045
|
+
for (const file of matches2) {
|
|
7046
|
+
if (!changedFiles.has(file)) continue;
|
|
7047
|
+
if (!lines.length) return true;
|
|
7048
|
+
if (oldRanges(file).some(([hs, he]) => lines.some(([s, e]) => hs <= e && he >= s))) return true;
|
|
7049
|
+
}
|
|
7050
|
+
}
|
|
7051
|
+
return cited ? false : changedFiles.size > 0;
|
|
7052
|
+
};
|
|
7053
|
+
}
|
|
6966
7054
|
|
|
6967
7055
|
// src/commands/gate.ts
|
|
6968
7056
|
import { existsSync as existsSync24, mkdirSync as mkdirSync8, readdirSync as readdirSync15, readFileSync as readFileSync32, statSync as statSync5, writeFileSync as writeFileSync6 } from "fs";
|
|
@@ -7053,7 +7141,7 @@ function specSwitches(specBody) {
|
|
|
7053
7141
|
return value.replace(/^\[|\]$/g, "").split(",").map((n) => n.trim().replace(/^["']|["']$/g, "").replace(/^(--)?breakpoint-/, "")).filter(Boolean);
|
|
7054
7142
|
}
|
|
7055
7143
|
function readFrames(html) {
|
|
7056
|
-
const opens = [...html.matchAll(/<\w+\b[^>]*\bclass\s*=\s*["'][^"']
|
|
7144
|
+
const opens = [...html.matchAll(/<\w+\b[^>]*\bclass\s*=\s*["']([^"']*)["'][^>]*>/gi)].filter((m) => m[1].split(/\s+/).includes("frame"));
|
|
7057
7145
|
return opens.map((open, i) => {
|
|
7058
7146
|
const tag = open[0];
|
|
7059
7147
|
const body = html.slice(open.index + tag.length, opens[i + 1]?.index ?? html.length);
|
|
@@ -7119,7 +7207,9 @@ function regionDrawn(labels, region) {
|
|
|
7119
7207
|
const found = (name) => labels.some((label) => labelMatches(label, name) || labelMatches(stripQualifiers(label), name));
|
|
7120
7208
|
return found(region.name) || !!region.parts && region.parts.every(found);
|
|
7121
7209
|
}
|
|
7122
|
-
function labelMatches(
|
|
7210
|
+
function labelMatches(rawLabel, rawName) {
|
|
7211
|
+
const label = rawLabel.replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim();
|
|
7212
|
+
const name = rawName.replace(/[-_]+/g, " ").replace(/\s+/g, " ").trim();
|
|
7123
7213
|
return label === name || label.startsWith(`${name} `) || label.startsWith(`${name}:`) || new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(label);
|
|
7124
7214
|
}
|
|
7125
7215
|
function normalise(text) {
|
|
@@ -7228,6 +7318,10 @@ function surfacesInPlay(root, command, transcriptPath, surface) {
|
|
|
7228
7318
|
const current = command === "critique" || command === "tweak" ? specFor(root, surface)?.slug : void 0;
|
|
7229
7319
|
return all.filter((s) => s === current || verdictsMtime(join39(critiqueDir, s)) >= start - 1e3);
|
|
7230
7320
|
}
|
|
7321
|
+
function surfacesToProbe(root, input) {
|
|
7322
|
+
const invocation = lastJigInvocation(input.transcript_path);
|
|
7323
|
+
return surfacesInPlay(root, invocation?.command, input.transcript_path, invocation?.surface);
|
|
7324
|
+
}
|
|
7231
7325
|
function fileAtSessionStart(root, path, start) {
|
|
7232
7326
|
const git = (args) => execFileSync4("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
7233
7327
|
try {
|
|
@@ -7237,6 +7331,35 @@ function fileAtSessionStart(root, path, start) {
|
|
|
7237
7331
|
return "";
|
|
7238
7332
|
}
|
|
7239
7333
|
}
|
|
7334
|
+
function changedOutsideRecords(root, start) {
|
|
7335
|
+
const git = (args) => execFileSync4("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
7336
|
+
try {
|
|
7337
|
+
const base = start === void 0 ? "HEAD" : git(["rev-list", "-1", `--before=@${Math.floor(start / 1e3)}`, "HEAD"]).trim();
|
|
7338
|
+
if (!base) return [];
|
|
7339
|
+
const changed = git(["diff", "--name-only", base, "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean);
|
|
7340
|
+
const created = git(["ls-files", "--others", "--exclude-standard", "--", ".", ":(exclude).jig"]).split("\n").filter(Boolean).filter((f) => {
|
|
7341
|
+
try {
|
|
7342
|
+
return start === void 0 || statSync5(join39(root, f)).mtimeMs >= start - 1e3;
|
|
7343
|
+
} catch {
|
|
7344
|
+
return false;
|
|
7345
|
+
}
|
|
7346
|
+
});
|
|
7347
|
+
return [.../* @__PURE__ */ new Set([...changed, ...created])].sort();
|
|
7348
|
+
} catch {
|
|
7349
|
+
return [];
|
|
7350
|
+
}
|
|
7351
|
+
}
|
|
7352
|
+
function drawingChangedAfterApproval(root, specPath, at) {
|
|
7353
|
+
const git = (args) => execFileSync4("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
7354
|
+
try {
|
|
7355
|
+
const approvedIn = git(["log", "-1", "--format=%H", "-G", "^mockup[[:space:]]*:[[:space:]]*approved", "--", specPath]).trim();
|
|
7356
|
+
if (!approvedIn) return void 0;
|
|
7357
|
+
const then = git(["show", `${approvedIn}:./${at}`]);
|
|
7358
|
+
return then !== readFileSync32(join39(root, at), "utf8") ? approvedIn.slice(0, 7) : void 0;
|
|
7359
|
+
} catch {
|
|
7360
|
+
return void 0;
|
|
7361
|
+
}
|
|
7362
|
+
}
|
|
7240
7363
|
function commandProblems(root, command, surface, start) {
|
|
7241
7364
|
const problems = [];
|
|
7242
7365
|
const spec = specFor(root, surface);
|
|
@@ -7249,7 +7372,7 @@ function commandProblems(root, command, surface, start) {
|
|
|
7249
7372
|
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.`");
|
|
7250
7373
|
}
|
|
7251
7374
|
if (/\[TODO\]/.test(body)) problems.push("DECISIONS.md still contains [TODO] markers.");
|
|
7252
|
-
problems.push(...unsourcedReasons(body, fileAtSessionStart(root, found, start)).map((p) => `DECISIONS.md: ${p}`));
|
|
7375
|
+
problems.push(...unsourcedReasons(body, fileAtSessionStart(root, found, start), { newWhysOnly: true }).map((p) => `DECISIONS.md: ${p}`));
|
|
7253
7376
|
}
|
|
7254
7377
|
}
|
|
7255
7378
|
if (command === "spec" || command === "mockup" || command === "make" || command === "critique" || command === "tweak") {
|
|
@@ -7285,6 +7408,25 @@ function commandProblems(root, command, surface, start) {
|
|
|
7285
7408
|
}
|
|
7286
7409
|
}
|
|
7287
7410
|
}
|
|
7411
|
+
if (command === "spec" || command === "mockup" || command === "critique") {
|
|
7412
|
+
const outside = changedOutsideRecords(root, start);
|
|
7413
|
+
if (outside.length) {
|
|
7414
|
+
problems.push(
|
|
7415
|
+
`${command} writes under .jig/ only, and this session changed ${outside.slice(0, 6).join(", ")}${outside.length > 6 ? ` and ${outside.length - 6} more` : ""}. Put them back as they were when the session began, and hand the change to \`/jig make\` (or \`/jig tweak\` for a small one to a built page). ` + (command === "critique" ? "A critique that changes the page it judges is grading its own work; report the finding instead." : "The build is make's.")
|
|
7416
|
+
);
|
|
7417
|
+
}
|
|
7418
|
+
}
|
|
7419
|
+
if (command === "mockup" && spec) {
|
|
7420
|
+
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
7421
|
+
const mockup = /^\s*mockup\s*:\s*(\S+)/im.exec(front)?.[1] ?? "";
|
|
7422
|
+
const at = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
7423
|
+
if (/^approved/i.test(mockup) && at && !/^https?:/i.test(at) && existsSync24(join39(root, at))) {
|
|
7424
|
+
const changedAfter = drawingChangedAfterApproval(root, spec.path, at);
|
|
7425
|
+
if (changedAfter) {
|
|
7426
|
+
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.`);
|
|
7427
|
+
}
|
|
7428
|
+
}
|
|
7429
|
+
}
|
|
7288
7430
|
if (command === "make") {
|
|
7289
7431
|
const found = decisionsFile(root);
|
|
7290
7432
|
if (found) {
|
|
@@ -7299,6 +7441,36 @@ function commandProblems(root, command, surface, start) {
|
|
|
7299
7441
|
}
|
|
7300
7442
|
}
|
|
7301
7443
|
}
|
|
7444
|
+
if (command === "tweak" && spec) {
|
|
7445
|
+
const found = decisionsFile(root);
|
|
7446
|
+
if (found) {
|
|
7447
|
+
let now = "";
|
|
7448
|
+
try {
|
|
7449
|
+
now = readFileSync32(join39(root, found), "utf8");
|
|
7450
|
+
} catch {
|
|
7451
|
+
}
|
|
7452
|
+
const then = fileAtSessionStart(root, found, start);
|
|
7453
|
+
if (now && now !== then) {
|
|
7454
|
+
problems.push(...unsourcedReasons(now, then, { newWhysOnly: true }).map((p) => `${found}: ${p}`));
|
|
7455
|
+
const change = readTweak(join39(root, ".jig", "critique", spec.slug))?.change;
|
|
7456
|
+
if (change) problems.push(...quotesNotFrom(now, then, change).map((p) => `${found}: ${p}`));
|
|
7457
|
+
}
|
|
7458
|
+
}
|
|
7459
|
+
}
|
|
7460
|
+
if (command !== "update" && command !== "init" && command !== "install") {
|
|
7461
|
+
for (const file of modeFiles(root)) {
|
|
7462
|
+
let now = "";
|
|
7463
|
+
try {
|
|
7464
|
+
now = readFileSync32(join39(root, file), "utf8");
|
|
7465
|
+
} catch {
|
|
7466
|
+
continue;
|
|
7467
|
+
}
|
|
7468
|
+
const then = fileAtSessionStart(root, file, start);
|
|
7469
|
+
if (then && now !== then) {
|
|
7470
|
+
problems.push(`${file} changed in this session. A mode file is Jig's, refreshed by \`update\`; a value this project needs of its own goes in its brand file or its own stylesheet. Restore it (\`git checkout -- ${file}\`, or \`git show <commit>:${file}\` if the change is committed).`);
|
|
7471
|
+
}
|
|
7472
|
+
}
|
|
7473
|
+
}
|
|
7302
7474
|
if (command === "critique") {
|
|
7303
7475
|
const dir = join39(root, ".jig", "critique");
|
|
7304
7476
|
const surfaces = existsSync24(dir) ? readdirSync15(dir).filter((s) => existsSync24(join39(dir, s, "screen.json")) || existsSync24(join39(dir, s, "code.json"))) : [];
|
|
@@ -7311,12 +7483,56 @@ function commandProblems(root, command, surface, start) {
|
|
|
7311
7483
|
problems.push(`${surface2}: the screen pass judged ${v.screen.judged} rules with rendered: false. Those rules are judged on a render \u2014 open the page at 360, 768 and 1280, run \`jig probe\` at each, and judge them there.`);
|
|
7312
7484
|
}
|
|
7313
7485
|
}
|
|
7486
|
+
const own = specFor(root, surface)?.slug;
|
|
7487
|
+
if (own && surfaces.includes(own)) problems.push(...reportOmissions(join39(dir, own), own));
|
|
7314
7488
|
if (surfaces.length === 0) {
|
|
7315
7489
|
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.");
|
|
7316
7490
|
}
|
|
7317
7491
|
}
|
|
7318
7492
|
return problems;
|
|
7319
7493
|
}
|
|
7494
|
+
function drawingBeforeAsking(root, surface) {
|
|
7495
|
+
const spec = specFor(root, surface);
|
|
7496
|
+
if (!spec) return [];
|
|
7497
|
+
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
7498
|
+
const named = /^\s*mockup_at\s*:\s*(.+)$/im.exec(front)?.[1]?.trim().replace(/^["']|["']$/g, "") ?? "";
|
|
7499
|
+
const at = named || `.jig/mockups/${spec.slug}.html`;
|
|
7500
|
+
if (/^https?:/i.test(at) || !/\.html?$/i.test(at) || !existsSync24(join39(root, at))) return [];
|
|
7501
|
+
const found = mockupDrawingProblems(root, spec.body, at);
|
|
7502
|
+
return found.length ? [`Fix the drawing before you put it to the owner; they approve what the spec lists, drawn. ${found.join(" ")}`] : [];
|
|
7503
|
+
}
|
|
7504
|
+
function modeFiles(root) {
|
|
7505
|
+
try {
|
|
7506
|
+
const state = JSON.parse(readFileSync32(join39(root, ".jig", "state.json"), "utf8"));
|
|
7507
|
+
return Object.keys(state.files ?? {}).filter((f) => /(^|\/)mode\.[\w-]+\.css$/.test(f));
|
|
7508
|
+
} catch {
|
|
7509
|
+
return [];
|
|
7510
|
+
}
|
|
7511
|
+
}
|
|
7512
|
+
function reportOmissions(dir, surface) {
|
|
7513
|
+
let report2;
|
|
7514
|
+
try {
|
|
7515
|
+
report2 = readFileSync32(join39(dir, "REPORT.md"), "utf8").toLowerCase();
|
|
7516
|
+
} catch {
|
|
7517
|
+
return [];
|
|
7518
|
+
}
|
|
7519
|
+
const missing = [];
|
|
7520
|
+
for (const f of VERDICT_FILES) {
|
|
7521
|
+
let file;
|
|
7522
|
+
try {
|
|
7523
|
+
file = JSON.parse(readFileSync32(join39(dir, f), "utf8"));
|
|
7524
|
+
} catch {
|
|
7525
|
+
continue;
|
|
7526
|
+
}
|
|
7527
|
+
for (const v of Array.isArray(file.verdicts) ? file.verdicts : []) {
|
|
7528
|
+
if (v.verdict !== "finding") continue;
|
|
7529
|
+
const key = String(v.id ?? v.decision ?? "").trim();
|
|
7530
|
+
if (key && !report2.includes(key.toLowerCase())) missing.push(v.id ? key : `"${key}"`);
|
|
7531
|
+
}
|
|
7532
|
+
}
|
|
7533
|
+
if (!missing.length) return [];
|
|
7534
|
+
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).`];
|
|
7535
|
+
}
|
|
7320
7536
|
function tweakProblems(root, spec) {
|
|
7321
7537
|
const problems = [];
|
|
7322
7538
|
const front = spec.body.split(/^---\s*$/m)[1] ?? "";
|
|
@@ -7404,6 +7620,7 @@ function gate(opts) {
|
|
|
7404
7620
|
const command = invocation?.command;
|
|
7405
7621
|
const waiting = command !== void 0 && ASKS_THE_OWNER.has(command) && asksOwner(lastAssistantText(opts.input.transcript_path));
|
|
7406
7622
|
const problems = command && !waiting ? commandProblems(root, command, invocation?.surface, sessionStart(opts.input.transcript_path)).map((p) => `/jig ${command}: ${p}`) : [];
|
|
7623
|
+
if (waiting && command === "mockup") problems.push(...drawingBeforeAsking(root, invocation?.surface).map((p) => `/jig mockup: ${p}`));
|
|
7407
7624
|
const selection = selectFiles(root, false);
|
|
7408
7625
|
const changedUi = selection.mode === "changed" && selection.files.some((f) => isStyleBearing(f) || isReaderText(f));
|
|
7409
7626
|
if (changedUi) {
|
|
@@ -7530,6 +7747,7 @@ function verdictGuard(root, command, inPlay) {
|
|
|
7530
7747
|
const lockPath = join39(dir, LOCK);
|
|
7531
7748
|
if (command === "critique" && (!inPlay || inPlay.includes(surface))) {
|
|
7532
7749
|
writeLock(lockPath, dir, now);
|
|
7750
|
+
problems.push(...lockLeftBehind(root, surface));
|
|
7533
7751
|
continue;
|
|
7534
7752
|
}
|
|
7535
7753
|
let lock = {};
|
|
@@ -7542,7 +7760,10 @@ function verdictGuard(root, command, inPlay) {
|
|
|
7542
7760
|
if (command === "tweak" && (!inPlay || inPlay.includes(surface)) && locked !== now) {
|
|
7543
7761
|
const own = tweakVerdictProblems(dir, surface, lock.verdicts);
|
|
7544
7762
|
problems.push(...own);
|
|
7545
|
-
if (own.length === 0)
|
|
7763
|
+
if (own.length === 0) {
|
|
7764
|
+
writeLock(lockPath, dir, now);
|
|
7765
|
+
problems.push(...lockLeftBehind(root, surface));
|
|
7766
|
+
}
|
|
7546
7767
|
continue;
|
|
7547
7768
|
}
|
|
7548
7769
|
if (locked && locked !== now) {
|
|
@@ -7553,6 +7774,17 @@ function verdictGuard(root, command, inPlay) {
|
|
|
7553
7774
|
}
|
|
7554
7775
|
return problems;
|
|
7555
7776
|
}
|
|
7777
|
+
function lockLeftBehind(root, surface) {
|
|
7778
|
+
const rel = `.jig/critique/${surface}`;
|
|
7779
|
+
try {
|
|
7780
|
+
const status = (paths) => execFileSync4("git", ["status", "--porcelain", "--", ...paths], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
7781
|
+
if (!status([`${rel}/${LOCK}`])) return [];
|
|
7782
|
+
if (status(VERDICT_FILES.map((f) => `${rel}/${f}`))) return [];
|
|
7783
|
+
} catch {
|
|
7784
|
+
return [];
|
|
7785
|
+
}
|
|
7786
|
+
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.`];
|
|
7787
|
+
}
|
|
7556
7788
|
function tweakVerdictProblems(dir, surface, lockedVerdicts) {
|
|
7557
7789
|
const record = readTweak(dir);
|
|
7558
7790
|
if (!record) {
|
|
@@ -7696,6 +7928,12 @@ function seo(opts) {
|
|
|
7696
7928
|
var packageRoot = getPackageRoot();
|
|
7697
7929
|
var { version } = JSON.parse(readFileSync34(join41(packageRoot, "package.json"), "utf8"));
|
|
7698
7930
|
function warnIfUnpublishedPin() {
|
|
7931
|
+
if (isDevVersion(version)) {
|
|
7932
|
+
console.warn(
|
|
7933
|
+
`Dev build ${version}: the skill, its commands and the Stop hook run the \`jig\` on your PATH, not npm, which has no such version. Keep this build installed while the project uses it, and install a published version to go back.`
|
|
7934
|
+
);
|
|
7935
|
+
return;
|
|
7936
|
+
}
|
|
7699
7937
|
if (isPublishedBuild(packageRoot)) return;
|
|
7700
7938
|
console.warn(
|
|
7701
7939
|
`Note: this is a source build, so the skill pins 'npx jig-ui@${version}' \u2014 a version that may not be published. Agents reading it will not be able to run the CLI until it is. Re-run install or update from a published build to correct the pin.`
|
|
@@ -7819,7 +8057,7 @@ program.command("verdicts").description("Verify a critique's verdict files and c
|
|
|
7819
8057
|
const p = result.previous;
|
|
7820
8058
|
if (p) {
|
|
7821
8059
|
const list = (ids) => ids.length ? ` (${ids.slice(0, 8).join(", ")}${ids.length > 8 ? ", \u2026" : ""})` : "";
|
|
7822
|
-
console.log(` Since the critique before this one (${p.commit}): ${p.fixed.length} fixed${list(p.fixed)}, ${p.open.length} still open${list(p.open)}, ${p.ruled.length} ruled by the owner${list(p.ruled)}, ${p.added.length} new${list(p.added)}.`);
|
|
8060
|
+
console.log(` Since the critique before this one (${p.commit}): ${p.fixed.length} fixed${list(p.fixed)}, ${p.unchanged.length} judged ok though the lines they cited did not change${list(p.unchanged)}, ${p.open.length} still open${list(p.open)}, ${p.ruled.length} ruled by the owner${list(p.ruled)}, ${p.added.length} new${list(p.added)}.`);
|
|
7823
8061
|
}
|
|
7824
8062
|
console.log(` ${result.line}`);
|
|
7825
8063
|
process.exit(result.ok ? 0 : 1);
|
|
@@ -7917,7 +8155,8 @@ program.command("gate").description("Run by the Claude Code Stop hook: block sto
|
|
|
7917
8155
|
const cwd = input.cwd ?? process.cwd();
|
|
7918
8156
|
try {
|
|
7919
8157
|
const projectRoot = findProjectRoot(cwd);
|
|
7920
|
-
|
|
8158
|
+
const critiqued = critiquedSurfaces(projectRoot);
|
|
8159
|
+
for (const surface of surfacesToProbe(projectRoot, input).filter((s) => critiqued.includes(s))) {
|
|
7921
8160
|
const page = surfacePage(projectRoot, surface) ?? recordedPage(projectRoot, surface);
|
|
7922
8161
|
if (!page) continue;
|
|
7923
8162
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jig-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
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",
|
|
@@ -657,7 +657,10 @@ down the spec's `regions:` and its `nav:` and find each one in that frame. A reg
|
|
|
657
657
|
the spec names and the frame lacks is drawn now, not raised in review. The gate
|
|
658
658
|
checks an HTML drawing when you stop: a frame for each of the four sizes, each
|
|
659
659
|
size's regions labelled in its frame (by name where the spec names them), a
|
|
660
|
-
frame either side of each recorded switch, and no icon or image in any frame.
|
|
660
|
+
frame either side of each recorded switch, and no icon or image in any frame. It checks
|
|
661
|
+
the same while you wait for the user's word, so a drawing it refuses cannot be put to
|
|
662
|
+
them. When it stops you, say what it refused and fix it; never report a gate block as
|
|
663
|
+
waiting on the user. This matters
|
|
661
664
|
most on the phone, where a region dropped for space is easy to miss and the reviewer
|
|
662
665
|
is looking at what is there, not at what is not.
|
|
663
666
|
|
|
@@ -1159,11 +1162,16 @@ preserve while fixing them.
|
|
|
1159
1162
|
|
|
1160
1163
|
**Say what happened to the last round's findings.** `verdicts` compares this
|
|
1161
1164
|
critique with the one before it in git and prints `Since the critique before
|
|
1162
|
-
this one: <n> fixed, <n>
|
|
1163
|
-
|
|
1164
|
-
The owner should not have to set two reports side
|
|
1165
|
-
make round worked. The comparison is made after both
|
|
1166
|
-
tells the arms nothing.
|
|
1165
|
+
this one: <n> fixed, <n> judged ok though the lines they cited did not change, <n>
|
|
1166
|
+
still open, <n> ruled by the owner, <n> new`, with the ids. Put that line in the
|
|
1167
|
+
report, as `verdicts` printed it, above the findings. The owner should not have to set two reports side
|
|
1168
|
+
by side to learn whether the make round worked. The comparison is made after both
|
|
1169
|
+
arms have written, so it tells the arms nothing.
|
|
1170
|
+
|
|
1171
|
+
A finding counts as fixed only where the lines it cited changed. One judged ok
|
|
1172
|
+
where nothing it pointed at changed is either two readers disagreeing about the
|
|
1173
|
+
same code, or a fix made somewhere the finding did not point. Look at which, and
|
|
1174
|
+
say so: the reading you stand by and why, or where the fix landed.
|
|
1167
1175
|
|
|
1168
1176
|
List `ruled` verdicts after the findings, under their own heading, each with the
|
|
1169
1177
|
decision it cites. They are not findings, and they are not hidden either.
|
|
@@ -1256,6 +1264,14 @@ colour for a mark, how versions are written), record it in `DECISIONS.md` the wa
|
|
|
1256
1264
|
applies a rule, or answers a finding the rules already settle (a word stranded on
|
|
1257
1265
|
its own line, `B-106`), there is nothing to decide; do not invent a decision.
|
|
1258
1266
|
|
|
1267
|
+
The owner's words are the ones tweak.json records as `change`. A `**Why:**` you
|
|
1268
|
+
add quotes them, in quotation marks, and nothing else; what you read into them,
|
|
1269
|
+
or into a screenshot the owner pointed at, goes under `**Why (inferred):**`. On
|
|
1270
|
+
jig-site a tweak shown a picture of a copy button wrote an exception to `E-51`
|
|
1271
|
+
"given directly by the owner … by reference rather than words": a ruling nobody
|
|
1272
|
+
gave, which every later critique would have judged the page by. The gate stops a
|
|
1273
|
+
tweak whose new `**Why:**` quotes words tweak.json does not hold.
|
|
1274
|
+
|
|
1259
1275
|
### 2. Bring the spec in line, if it disagrees
|
|
1260
1276
|
|
|
1261
1277
|
If the spec says something the change contradicts, or the page will no longer
|
|
@@ -1341,6 +1357,11 @@ blocking. It also refuses when a critique's verdict files changed after `critiqu
|
|
|
1341
1357
|
wrote them, in a session that did not run `critique`: a fix is judged by the next
|
|
1342
1358
|
review, never by the builder editing the last one.
|
|
1343
1359
|
|
|
1360
|
+
When a `critique` or `tweak` session stops, the gate records its verdicts in
|
|
1361
|
+
`.jig/critique/<surface>/verdicts.lock`. That happens after you have committed, so
|
|
1362
|
+
if your verdicts are committed it stops you once more to commit the lock on its
|
|
1363
|
+
own; the next stop passes.
|
|
1364
|
+
|
|
1344
1365
|
It judges the critiques this session touched: any with a file changed since the
|
|
1345
1366
|
session began, and the current spec's surface when you ran `critique`. Another
|
|
1346
1367
|
page's older critique does not hold you back. To set a critique aside for good,
|