jig-ui 0.22.0 → 0.23.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 CHANGED
@@ -1,5 +1,89 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.23.0 (2026-09-29)
4
+
5
+ Rules for the prose a page ships and for how it moves, a spec that says what
6
+ moves and why, and a critique that reads the page against its spec both ways.
7
+
8
+ ### Added
9
+
10
+ - **Rules for the prose a page ships.** The copy rules were written for
11
+ interface strings: a label, a button, an error. A docs chapter, a guide or a
12
+ blog post was held only to those, and a paragraph could claim a figure nobody
13
+ measured, open with "In today's rapidly changing world" and restate its
14
+ introduction as its conclusion, and pass. Nine rules now cover prose longer
15
+ than a paragraph (`I-148` to `I-156`): a claim with no source, a sentence
16
+ specific in sound and empty in fact, formula openers and closers, saying it
17
+ twice, a rhetorical shape on repeat, enthusiasm the content has not earned,
18
+ stacked hedges, emoji in headings, and structure imposed on the content.
19
+ Four more cover writing a person puts their name to, a blog post or a case
20
+ study (`I-157` to `I-160`): nothing only the author could say, no point of
21
+ view, an even rhythm, and a feeling named rather than shown.
22
+ - **They judge the writing, never the writer.** A finding says the copy reads
23
+ as generic; it never says a model wrote it. Nothing is measured: rhythm and
24
+ voice have no number a check could hold, and a score would reward prose that
25
+ games it.
26
+ - **`check` catches two of them.** `I-150` warns on a short list of formula
27
+ phrases ("it is important to note", "let's dive in", "in conclusion") in the
28
+ text a reader sees, and leaves a quotation alone. `I-155` warns on an emoji
29
+ in a Markdown page's heading; in markup, `A-05` already reports it.
30
+ - **The copy checklist asks three more questions** (`L-06`): whether every
31
+ figure traces to a source, whether prose says something specific once, and
32
+ whether a long-form piece holds something only its author could say.
33
+
34
+ - **Three motion rules.** `G-161`: a layout change the user caused shows
35
+ where things went (a deleted row's neighbours slide into the gap) rather than
36
+ jumping, and stays instant when the user did not cause it. `G-162`: animate
37
+ `transform` and `opacity`, never layout, and never `all`; `check` warns on a
38
+ transition or keyframe that names a layout property or `all`, and on
39
+ Tailwind's `transition-all`. `G-163`: one thing moves at a time. Moving
40
+ within the screen takes `--ease-in-out` (`T-04`). No new tokens.
41
+ - **A spec says what moves, and why.** `motion:` lists each movement: what
42
+ moves, its trigger, and what it tells the reader. One that cannot say is cut
43
+ before it is built. The owner reads it on the confirmation sheet, `make`
44
+ builds that motion and no other, and `critique` judges the page against it.
45
+ A spec confirmed before it has none, and is judged by the rules alone.
46
+
47
+ ### Changed
48
+
49
+ - **A critique reads the page against the spec, not only the spec against the
50
+ page.** A section, a paragraph or a claim the page carries that no line of the
51
+ spec asks for, and no deviation records, is now a finding. On jig-site a
52
+ chapter carried an accurate paragraph the owner never confirmed, and two
53
+ critiques saw it and filed nothing, because it broke no rule. The fix is a
54
+ tweak that writes it into the spec for the owner to confirm, or removes it.
55
+ - **Each critique arm is told the shape of a `ruled` verdict.** An arm wrote
56
+ four that named the owner's decision only in their reason, and `verdicts`
57
+ refused them all.
58
+ - **A CLI run from a checkout reads the checkout's rules.** `prepack` copies the
59
+ rules beside the CLI package for each publish, and the copies stayed behind:
60
+ a clone that had published read the rules as they were then, and Jig's own
61
+ first test run after each rule change failed 14 tests. An installed package
62
+ still reads its own copies.
63
+ - **`jig ship` leaves a replaced spec to the spec that replaced it.**
64
+ `superseded_by: <spec>` in a spec's front matter marks it, and `ship` reports
65
+ it apart instead of as never critiqued, once it has checked the spec it names
66
+ exists. On jig-site two replaced specs were owed a critique at every `ship`,
67
+ each marked a different way by the agent that replaced it.
68
+ - **A tweak that records a decision applies it to the whole page.** The owner
69
+ rules on one instance, and the decision states a kind; the tweak now finds the
70
+ other instances on the page and fixes them in the same change. On jig-site a
71
+ decision to set every quotation mark curly fixed the one named, and four
72
+ straight marks survived to the next critique.
73
+ - **A spec's second check covers what changed.** The reader gets the earlier
74
+ record and the diff; a quotation or fact on an unchanged line, whose source
75
+ has not changed either, carries over. On jig-site every re-check reopened
76
+ every source, about $10 a round.
77
+ - **A critique has a place for what no rule names.** A difference between the
78
+ page and its spec, or a source it teaches, goes under `differences` in the
79
+ arm's verdict file and counts as a finding. On jig-site both arms of one
80
+ critique invented rule ids to carry one, and `verdicts` refused them. The
81
+ report now says what the page-against-spec pass found, "none" included, and
82
+ `verdicts` explains that a decision is judged, never `ruled`.
83
+ - **Every page is owed a critique again.** A critique judges every rule, so one
84
+ written before these sixteen is incomplete, and `jig ship` lists each page
85
+ until it is critiqued on this release.
86
+
3
87
  ## 0.22.0 (2026-09-29)
4
88
 
5
89
  A spec and a mockup are checked before the owner says yes, a critique can wait,
package/README.md CHANGED
@@ -12,13 +12,13 @@ Installed as `npx jig-ui` — the bare name was taken on npm.
12
12
  Jig is **a skill your coding agent reads**, and **a CLI you can run yourself**.
13
13
  They are two halves of the same thing, and the split is not arbitrary:
14
14
 
15
- - Of the 143 rules, **36 can be decided by a machine** — a hard-coded colour, a
15
+ - Of the 159 rules, **39 can be decided by a machine** — a hard-coded colour, a
16
16
  contrast ratio below the floor, a removed focus ring. The CLI decides those.
17
- - The other **107 are judgment** — whether an empty state says anything useful,
17
+ - The other **120 are judgment** — whether an empty state says anything useful,
18
18
  whether a label reads as an instruction, whether motion earns its place. No
19
19
  regex settles those. An agent reads the rules and applies them.
20
20
 
21
- Running only the CLI gets you the 36. Running only the agent gets you the 107 with
21
+ Running only the CLI gets you the 39. Running only the agent gets you the 120 with
22
22
  no verification. **A clean `jig check` is not a clean review**, and the skill
23
23
  says so to every agent that reads it.
24
24
 
@@ -407,7 +407,7 @@ on the result — the CLI reports, the agent applies the judgment half.
407
407
  | Slash command | Equivalent |
408
408
  | --- | --- |
409
409
  | `/jig init` | `jig init` — then states the mode it chose and what it wired |
410
- | `/jig check` | `jig check` — then applies the 107 judgment rules and reports both halves |
410
+ | `/jig check` | `jig check` — then applies the 120 judgment rules and reports both halves |
411
411
  | `/jig explain C-19` | `jig explain C-19` — prints the rule as-is, without paraphrasing it |
412
412
  | `/jig explain contrast` | `jig explain contrast` — every rule matching a word, when you do not have an id |
413
413
  | `/jig install --agent cursor` | `jig install --agent cursor` |
@@ -489,6 +489,9 @@ before you say yes:
489
489
  the navigation you would expect there.
490
490
  - **States cover what the page will meet.** Empty, one, a lot, loading, failure:
491
491
  whichever the page can actually be in.
492
+ - **Everything that moves has a reason.** Each line of `motion:` names what
493
+ moves, what sets it off, and what it tells the reader. Cut any line that
494
+ can't say, and expect nothing on the page to move that the list leaves out.
492
495
  - **Open questions were asked, not answered for you.** A spec touching an item
493
496
  under `Unresolved` in `DECISIONS.md` carries your answer, and a field reading
494
497
  `unspecified — make chooses one it can defend` is one you can decide now.
@@ -746,12 +749,12 @@ treatment.
746
749
 
747
750
  | File | Contents |
748
751
  | --- | --- |
749
- | `rules/00-anti-patterns.md` | 125 universal rules with corrections |
752
+ | `rules/00-anti-patterns.md` | 128 universal rules with corrections |
750
753
  | `rules/01-modes.md` | `editorial` / `product` / `operator` profiles |
751
754
  | `rules/02-tokens.md` | Token contract, naming, consumption |
752
755
  | `rules/03-patterns.md` | Component anatomy and behaviour |
753
756
  | `rules/04-principles.md` | Five frames + seven tiebreakers |
754
- | `rules/05-copy.md` | Interface text rules |
757
+ | `rules/05-copy.md` | Interface text rules, and the prose a page ships |
755
758
  | `examples/<ID>.html` | For every rule, a small dont and do: self-contained HTML fragments that render in a sandboxed frame. `jig explain <ID>` names the file. |
756
759
  | `<css dir>/jig/brand.*.css` | Identity. One per project. |
757
760
  | `<css dir>/jig/mode.*.css` | Density, scale, rhythm, motion |
package/dist/index.js CHANGED
@@ -28,7 +28,7 @@ function assetRoot(startDir = getPackageRoot()) {
28
28
  let current = startDir;
29
29
  const { root } = parse(startDir);
30
30
  while (true) {
31
- if (existsSync(join(current, ASSET_MARKER))) return current;
31
+ if (existsSync(join(current, ASSET_MARKER))) return sourceOf(current);
32
32
  if (current === root) {
33
33
  throw new Error(
34
34
  `assetRoot(): could not find an ancestor of "${startDir}" containing "${ASSET_MARKER}"`
@@ -37,6 +37,11 @@ function assetRoot(startDir = getPackageRoot()) {
37
37
  current = dirname(current);
38
38
  }
39
39
  }
40
+ function sourceOf(found) {
41
+ if (isPublishedBuild(found)) return found;
42
+ const repo = join(found, "..", "..");
43
+ return existsSync(join(repo, ASSET_MARKER)) && existsSync(join(repo, "packages", "cli", "package.json")) ? repo : found;
44
+ }
40
45
  function isPublishedBuild(packageRoot2) {
41
46
  return packageRoot2.split(/[\\/]/).includes("node_modules");
42
47
  }
@@ -1522,8 +1527,9 @@ Example: ${example} (a dont and a do, each a self-contained HTML fragment)` : re
1522
1527
  const idBody = ids.filter((e) => !idTitle.includes(e) && matches(e.text, needle));
1523
1528
  const secTitle = secs.filter((e) => matches(e.title, needle));
1524
1529
  const secBody = secs.filter((e) => !secTitle.includes(e) && matches(e.text, needle));
1530
+ const opens = (e) => e.title.toLowerCase().startsWith(needle.toLowerCase()) ? 0 : 1;
1525
1531
  const hits = [
1526
- ...idTitle.sort((a, b) => byNumber(a.id, b.id)),
1532
+ ...idTitle.sort((a, b) => opens(a) - opens(b) || byNumber(a.id, b.id)),
1527
1533
  ...idBody.sort((a, b) => byNumber(a.id, b.id)),
1528
1534
  ...secTitle,
1529
1535
  ...secBody
@@ -3505,19 +3511,21 @@ function indentedProse(source) {
3505
3511
  const blank3 = (m) => " ".repeat(m.length);
3506
3512
  return source.split("\n").map((line) => /^\s*(-|\/\/|#(?!\w)|=)/.test(line) ? blank3(line) : line).join("\n");
3507
3513
  }
3514
+ function readerSpans(file, raw) {
3515
+ const markdown = MARKDOWN.test(file);
3516
+ const indented = INDENTED.test(file);
3517
+ const masked = markdown ? markdownProse(raw) : indented ? indentedProse(maskNonProse(raw)) : maskNonProse(raw);
3518
+ const script = SCRIPT.test(file);
3519
+ return markdown || indented ? [{ index: 0, text: masked }] : proseSpans(masked, !script).filter((span) => !script || !CODEY.test(span.text));
3520
+ }
3508
3521
  var emDash = {
3509
3522
  name: "em-dash",
3510
3523
  appliesTo: (file) => isReaderText(file),
3511
3524
  run(_source, file, ctx) {
3512
- const markdown = MARKDOWN.test(file);
3513
- const indented = INDENTED.test(file);
3514
- const masked = markdown ? markdownProse(ctx.raw) : indented ? indentedProse(maskNonProse(ctx.raw)) : maskNonProse(ctx.raw);
3515
3525
  const starts = buildLineIndex(ctx.raw);
3516
3526
  const findings = [];
3517
3527
  const seen = /* @__PURE__ */ new Set();
3518
- const script = SCRIPT.test(file);
3519
- const spans = markdown || indented ? [{ index: 0, text: masked }] : proseSpans(masked, !script).filter((span) => !script || !CODEY.test(span.text));
3520
- for (const span of spans) {
3528
+ for (const span of readerSpans(file, ctx.raw)) {
3521
3529
  if (!EM_DASH.test(span.text)) continue;
3522
3530
  for (const hit of span.text.matchAll(/\u2014/g)) {
3523
3531
  const line = lineForOffset(starts, span.index + hit.index);
@@ -3539,6 +3547,135 @@ var emDash = {
3539
3547
  }
3540
3548
  };
3541
3549
 
3550
+ // src/check/detectors/prose.ts
3551
+ var FORMULA = new RegExp(
3552
+ [
3553
+ String.raw`in today['’]s (?:fast[- ]paced|rapidly|ever[- ]|increasingly|digital|modern|busy)`,
3554
+ String.raw`in an (?:increasingly|ever[- ](?:changing|evolving)) \w+ (?:world|landscape|age|marketplace|era)`,
3555
+ String.raw`as technology continues to evolve`,
3556
+ String.raw`it(?: is|['’]s) (?:important|worth) (?:to note|noting)`,
3557
+ String.raw`let['’]s (?:dive|delve)\b`,
3558
+ String.raw`here['’]s the thing\b`,
3559
+ String.raw`without further ado`,
3560
+ String.raw`in conclusion\b`,
3561
+ String.raw`only time will tell`,
3562
+ String.raw`the future (?:looks|is) (?:bright|incredibly promising)`
3563
+ ].map((p) => `\\b${p}`).join("|"),
3564
+ "gi"
3565
+ );
3566
+ function maskQuotations(raw) {
3567
+ const blank3 = (m) => m.replace(/[^\n]/g, " ");
3568
+ return raw.replace(/<blockquote\b[\s\S]*?<\/blockquote\s*>/gi, blank3).replace(/<q\b[\s\S]*?<\/q\s*>/gi, blank3).replace(/^ {0,3}>[^\n]*/gm, blank3);
3569
+ }
3570
+ var formulaPhrase = {
3571
+ name: "formula-phrase",
3572
+ appliesTo: (file) => isReaderText(file),
3573
+ run(_source, file, ctx) {
3574
+ const starts = buildLineIndex(ctx.raw);
3575
+ const findings = [];
3576
+ const seen = /* @__PURE__ */ new Set();
3577
+ for (const span of readerSpans(file, maskQuotations(ctx.raw))) {
3578
+ for (const hit of span.text.matchAll(FORMULA)) {
3579
+ const line = lineForOffset(starts, span.index + hit.index);
3580
+ if (seen.has(line)) continue;
3581
+ seen.add(line);
3582
+ findings.push(
3583
+ mkFinding(
3584
+ ctx,
3585
+ "formula-phrase",
3586
+ file,
3587
+ line,
3588
+ `a formula phrase ("${hit[0]}"): start with the point, and end when it has been made`,
3589
+ sourceLine(ctx.raw, line)
3590
+ )
3591
+ );
3592
+ }
3593
+ }
3594
+ return findings.sort((a, b) => a.line - b.line);
3595
+ }
3596
+ };
3597
+ var HEADING3 = /^ {0,3}#{1,6}[ \t]+(.+)$/gm;
3598
+ var FENCE = /^ {0,3}(`{3,}|~{3,})[\s\S]*?^ {0,3}\1[^\n]*$/gm;
3599
+ var headingEmoji = {
3600
+ name: "heading-emoji",
3601
+ appliesTo: (file) => isReaderText(file) && hasExtension(file, [".md", ".markdown"]) && !isStyleBearing(file),
3602
+ run(_source, file, ctx) {
3603
+ const masked = ctx.raw.replace(FENCE, (m) => m.replace(/[^\n]/g, " "));
3604
+ const starts = buildLineIndex(ctx.raw);
3605
+ const findings = [];
3606
+ for (const heading of masked.matchAll(HEADING3)) {
3607
+ const glyph = [...heading[1].matchAll(EMOJI_RE)].find((m) => !TEXTUAL.has(m[0]));
3608
+ if (!glyph) continue;
3609
+ const line = lineForOffset(starts, heading.index);
3610
+ findings.push(
3611
+ mkFinding(
3612
+ ctx,
3613
+ "heading-emoji",
3614
+ file,
3615
+ line,
3616
+ `an emoji in a heading ("${glyph[0]}"): let the words carry it; a screen reader reads the emoji's name first`,
3617
+ sourceLine(ctx.raw, line)
3618
+ )
3619
+ );
3620
+ }
3621
+ return findings;
3622
+ }
3623
+ };
3624
+
3625
+ // src/check/detectors/layout-motion.ts
3626
+ var LAYOUT = String.raw`(?:(?:min-|max-)?(?:width|height)|(?:inline|block)-size|top|left|right|bottom|inset(?:-[a-z-]+)?|margin(?:-[a-z-]+)?|padding(?:-[a-z-]+)?)`;
3627
+ var LAYOUT_NAME = new RegExp(`^${LAYOUT}$`, "i");
3628
+ var TRANSITION = /(?<![-\w])transition(-property)?\s*:\s*([^;}]+)/gi;
3629
+ var KEYFRAMES = /@(?:-webkit-)?keyframes\b[^{]*\{/gi;
3630
+ var KEYFRAME_DECL = new RegExp(`(?<![-\\w])(${LAYOUT})\\s*:`, "gi");
3631
+ var TAILWIND = new RegExp(`(?<![-\\w\\[])transition-(all|\\[[^\\]\\s]*?\\b${LAYOUT}\\b[^\\]\\s]*\\])(?![-\\w])`, "gi");
3632
+ function namedProperties(value, longhand) {
3633
+ return value.split(",").map((item) => {
3634
+ const words = item.trim().split(/\s+/);
3635
+ return longhand ? words[0] : words.find((w) => /^[a-z-]+$/i.test(w) && !/^(ease|ease-in|ease-out|ease-in-out|linear|step-start|step-end|allow-discrete|normal|none)$/i.test(w)) ?? "";
3636
+ }).filter(Boolean);
3637
+ }
3638
+ function blockEnd(source, open) {
3639
+ let depth = 0;
3640
+ for (let i = open; i < source.length; i++) {
3641
+ if (source[i] === "{") depth++;
3642
+ else if (source[i] === "}" && --depth === 0) return i;
3643
+ }
3644
+ return source.length;
3645
+ }
3646
+ var layoutMotion = {
3647
+ name: "layout-motion",
3648
+ appliesTo: (file) => isStyleBearing(file),
3649
+ run(source, file, ctx) {
3650
+ const findings = [];
3651
+ const seen = /* @__PURE__ */ new Set();
3652
+ const report2 = (text, offset, message) => {
3653
+ const line = lineForOffset(buildLineIndex(text), offset);
3654
+ if (seen.has(line)) return;
3655
+ seen.add(line);
3656
+ findings.push(mkFinding(ctx, "layout-motion", file, line, message, sourceLine(text, line)));
3657
+ };
3658
+ for (const m of source.matchAll(TRANSITION)) {
3659
+ const names = namedProperties(m[2], Boolean(m[1]));
3660
+ const bad = names.find((n) => n.toLowerCase() === "all" || LAYOUT_NAME.test(n));
3661
+ if (!bad) continue;
3662
+ report2(source, m.index, bad.toLowerCase() === "all" ? "a transition on `all`: name the properties it covers, and animate transform and opacity" : `a transition on \`${bad}\`, which lays the page out again every frame: animate transform or opacity instead`);
3663
+ }
3664
+ for (const k of source.matchAll(KEYFRAMES)) {
3665
+ const open = k.index + k[0].length - 1;
3666
+ const body = source.slice(open, blockEnd(source, open));
3667
+ for (const d of body.matchAll(KEYFRAME_DECL)) {
3668
+ report2(source, open + d.index, `a keyframe animates \`${d[1]}\`, which lays the page out again every frame: animate transform or opacity instead`);
3669
+ }
3670
+ }
3671
+ const raw = maskProseComments(ctx.raw);
3672
+ for (const t of raw.matchAll(TAILWIND)) {
3673
+ report2(ctx.raw, t.index, `\`${t[0]}\` animates ${t[1] === "all" ? "every property that changes" : "layout"}: use \`transition\`, \`transition-opacity\` or \`transition-transform\``);
3674
+ }
3675
+ return findings.sort((a, b) => a.line - b.line);
3676
+ }
3677
+ };
3678
+
3542
3679
  // src/check/detectors/semantic-element.ts
3543
3680
  var BODY = /<body\b/i;
3544
3681
  var MAIN = /<main\b|role\s*=\s*["']main["']/i;
@@ -4094,6 +4231,9 @@ var DETECTORS = [
4094
4231
  menuState,
4095
4232
  undeclaredToken,
4096
4233
  emDash,
4234
+ formulaPhrase,
4235
+ headingEmoji,
4236
+ layoutMotion,
4097
4237
  semanticElement,
4098
4238
  metadata,
4099
4239
  interfaceSafety,
@@ -6525,7 +6665,7 @@ function probeContradictions(probes, verdictOf, indexable = true) {
6525
6665
  // src/check/decisions.ts
6526
6666
  import { existsSync as existsSync21, readFileSync as readFileSync28 } from "fs";
6527
6667
  import { join as join35, posix as posix3 } from "path";
6528
- var HEADING3 = /^(#{2,3})\s+(.+?)\s*$/gm;
6668
+ var HEADING4 = /^(#{2,3})\s+(.+?)\s*$/gm;
6529
6669
  var NOT_A_DECISION = /^(unresolved|open questions?|undecided|contents?|index)$/i;
6530
6670
  function decisionsFile(projectRoot) {
6531
6671
  const candidates = ["jig/DECISIONS.md", "DECISIONS.md", "src/jig/DECISIONS.md", "src/styles/jig/DECISIONS.md", ".jig/DECISIONS.md"];
@@ -6552,7 +6692,7 @@ function decisionHeadings(projectRoot) {
6552
6692
  } catch {
6553
6693
  return headings;
6554
6694
  }
6555
- for (const match of body.matchAll(HEADING3)) {
6695
+ for (const match of body.matchAll(HEADING4)) {
6556
6696
  const name = match[2].replace(/[`*]/g, "").trim();
6557
6697
  if (!name || NOT_A_DECISION.test(name)) continue;
6558
6698
  if (!headings.has(name)) headings.set(name, match[0].trim());
@@ -6660,6 +6800,18 @@ function specProblems(spec) {
6660
6800
  if (critique !== void 0 && !/^(each|at-ship)$/i.test(critique)) {
6661
6801
  problems.push(`${spec.path}: \`critique: ${critique}\` is neither \`each\` nor \`at-ship\`. Leave it out to follow the project's default.`);
6662
6802
  }
6803
+ const motion = /^motion\s*:[ \t]*(.*)\n?((?:[ \t]+-.*\n?)*)/im.exec(front);
6804
+ if (motion) {
6805
+ const inline = motion[1].replace(/\s+#.*$/, "").trim();
6806
+ const items = motion[2].split("\n").map((l) => l.replace(/^[ \t]+-\s*/, "").trim()).filter(Boolean);
6807
+ if (!items.length && !/^none$/i.test(inline)) {
6808
+ problems.push(`${spec.path}: \`motion:\` is neither \`none\` nor a list. List each movement as \`- <what moves>: <how>, on <trigger>; tells <what the reader learns>\`, or write \`motion: none\`.`);
6809
+ }
6810
+ const unsaid = items.filter((i) => !/\bon\b/i.test(i) || !/\btells?\b/i.test(i));
6811
+ if (unsaid.length) {
6812
+ problems.push(`${spec.path}: ${unsaid.length === 1 ? "a movement in `motion:` does" : `${unsaid.length} movements in \`motion:\` do`} not say what triggers it and what it tells the reader ("${unsaid[0]}"). Write \`on <trigger>; tells <what>\`, or cut the movement.`);
6813
+ }
6814
+ }
6663
6815
  for (const field of ["feature", "surface", "mode", "sizes", "confirmed", "mockup"]) {
6664
6816
  if (!has(field)) problems.push(`${spec.path} frontmatter has no \`${field}:\`.`);
6665
6817
  }
@@ -6803,7 +6955,7 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
6803
6955
  seen.add(id);
6804
6956
  const pass = otherPass.get(id);
6805
6957
  if (!required.includes(id) && !extraAllowed.has(id)) {
6806
- errors.push(pass === "mechanical" ? `${name}.json: ${id} is a mechanical rule \u2014 \`jig check\` decides it, so it has no verdict here. Remove it.` : pass ? `${name}.json: ${id} is a pass: ${pass} rule \u2014 it belongs to the other arm.` : `${name}.json: ${written} is not a rule or spec in this corpus. Run \`jig explain ${written}\`; an id that does not resolve is not a verdict.`);
6958
+ errors.push(pass === "mechanical" ? `${name}.json: ${id} is a mechanical rule \u2014 \`jig check\` decides it, so it has no verdict here. Remove it.` : pass ? `${name}.json: ${id} is a pass: ${pass} rule \u2014 it belongs to the other arm.` : `${name}.json: ${written} is not a rule or spec in this corpus. Run \`jig explain ${written}\`; an id that does not resolve is not a verdict. A difference from the spec, or from a source the page quotes, is not a rule: list it under \`differences\` in the same file.`);
6807
6959
  continue;
6808
6960
  }
6809
6961
  if (typeof v.verdict !== "string" || !RULE_VERDICTS.includes(v.verdict)) {
@@ -6824,6 +6976,16 @@ function checkArm(name, file, required, otherPass, extraAllowed, extraRequired,
6824
6976
  if (v.verdict === "finding") findings++;
6825
6977
  }
6826
6978
  reasonProblems(`${name}.json`, reasons, errors);
6979
+ const differences = file.differences;
6980
+ if (differences !== void 0) {
6981
+ if (!Array.isArray(differences)) errors.push(`${name}.json: \`differences\` is not a list.`);
6982
+ else for (const d of differences) {
6983
+ const said = (x) => typeof x === "string" && x.trim() !== "";
6984
+ if (!said(d?.what) || !said(d?.where) || !said(d?.against)) {
6985
+ errors.push(`${name}.json: a difference needs \`what\` (what differs), \`where\` (the page's file and line) and \`against\` (the spec line or source it differs from).`);
6986
+ } else findings++;
6987
+ }
6988
+ }
6827
6989
  const missing = [...required, ...extraRequired].filter((id) => !seen.has(id));
6828
6990
  if (missing.length) {
6829
6991
  errors.push(`${name}.json: ${missing.length} of ${total} ids have no verdict: ${missing.join(", ")}. Re-run the arm; never report a short pass.`);
@@ -6863,7 +7025,7 @@ function checkDecisions(projectRoot, dir, errors) {
6863
7025
  }
6864
7026
  seen.add(match);
6865
7027
  if (typeof v.verdict !== "string" || !VERDICTS.includes(v.verdict)) {
6866
- errors.push(`decisions.json: "${match}" has verdict ${JSON.stringify(v.verdict)} \u2014 it must be ok, finding or n/a.`);
7028
+ errors.push(v.verdict === "ruled" ? `decisions.json: "${match}" is marked ruled. A decision is not excused by a decision: judge whether the page follows it, ok, finding or n/a. \`ruled\` is for a rule in screen.json or code.json that a decision overrides.` : `decisions.json: "${match}" has verdict ${JSON.stringify(v.verdict)} \u2014 it must be ok, finding or n/a.`);
6867
7029
  continue;
6868
7030
  }
6869
7031
  const reason = typeof v.reason === "string" ? v.reason.trim() : "";
@@ -7397,12 +7559,21 @@ function changedSinceLock(projectRoot, dir) {
7397
7559
  return void 0;
7398
7560
  }
7399
7561
  }
7562
+ function supersededBy(front) {
7563
+ const named = /^\s*superseded_by\s*:\s*(.*)$/im.exec(front)?.[1]?.replace(/\s+#.*$/, "").trim();
7564
+ return named ? named : void 0;
7565
+ }
7400
7566
  function pageStatus(projectRoot, surface) {
7401
7567
  const spec = readFileSync33(join40(projectRoot, ".jig", "specs", `${surface}.spec.md`), "utf8");
7402
7568
  const front = spec.split(/^---\s*$/m)[1] ?? "";
7403
7569
  if (!/^\s*confirmed\s*:\s*true\b/im.test(front)) {
7404
7570
  return { surface, state: "in-progress", detail: "its spec is not confirmed, so nothing of it is built to ship" };
7405
7571
  }
7572
+ const successor = supersededBy(front);
7573
+ if (successor !== void 0) {
7574
+ const file = successor.replace(/(\.spec\.md)?$/, ".spec.md");
7575
+ return existsSync24(join40(projectRoot, ".jig", "specs", file)) ? { surface, state: "superseded", detail: `superseded by ${file}, whose critique judges its page` } : { surface, state: "incomplete", detail: `\`superseded_by: ${successor}\` names a spec that does not exist in .jig/specs` };
7576
+ }
7406
7577
  const dir = join40(projectRoot, ".jig", "critique", surface);
7407
7578
  if (!existsSync24(join40(dir, "screen.json")) && !existsSync24(join40(dir, "code.json"))) {
7408
7579
  return { surface, state: "never", detail: "never critiqued" };
@@ -7433,11 +7604,11 @@ function ship(opts) {
7433
7604
  const specsDir = join40(root, ".jig", "specs");
7434
7605
  const surfaces = existsSync24(specsDir) ? readdirSync15(specsDir).filter((f) => f.endsWith(".spec.md") && !f.startsWith("_")).map((f) => f.replace(/\.spec\.md$/, "")).sort() : [];
7435
7606
  const pages = surfaces.map((s) => pageStatus(root, s));
7436
- const owed = pages.filter((p) => p.state !== "judged" && p.state !== "in-progress");
7607
+ const owed = pages.filter((p) => p.state !== "judged" && p.state !== "in-progress" && p.state !== "superseded");
7437
7608
  const ready = mechanicalErrors === 0 && seoErrors === 0 && owed.length === 0;
7438
7609
  const count = (state) => pages.filter((p) => p.state === state).length;
7439
- const line = `JIG_SHIP: ready=${ready ? "yes" : "no"} mechanical=${mechanicalErrors} seo=${seoErrors} pages=${pages.length} judged=${count("judged")} owed=${owed.length} in-progress=${count("in-progress")}`;
7440
- const mark = { judged: "\u2713", never: "\u2717", changed: "\u2717", reprobe: "\u2717", deferred: "\u2717", incomplete: "\u2717", findings: "\u2717", "in-progress": "\xB7" };
7610
+ const line = `JIG_SHIP: ready=${ready ? "yes" : "no"} mechanical=${mechanicalErrors} seo=${seoErrors} pages=${pages.length} judged=${count("judged")} owed=${owed.length} in-progress=${count("in-progress")} superseded=${count("superseded")}`;
7611
+ const mark = { judged: "\u2713", never: "\u2717", changed: "\u2717", reprobe: "\u2717", deferred: "\u2717", incomplete: "\u2717", findings: "\u2717", "in-progress": "\xB7", superseded: "\xB7" };
7441
7612
  const report2 = [
7442
7613
  ` ${mechanicalErrors === 0 ? "\u2713" : "\u2717"} check --all --ci: ${mechanicalErrors} mechanical error${mechanicalErrors === 1 ? "" : "s"}`,
7443
7614
  ` ${seoErrors === 0 ? "\u2713" : "\u2717"} seo: ${seoErrors} error${seoErrors === 1 ? "" : "s"}`,
@@ -0,0 +1,7 @@
1
+ <!-- G-161 · A layout change that jumps -->
2
+ <figure data-example="dont">
3
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:6px;"><p style="margin:0;font-size:12px;color:#57534e;">Row 2 deleted. In the next frame:</p><div style="background:#fff;border:1px solid #d6d3d1;border-radius:4px;padding:6px 10px;font-size:13px;">Pay the invoice</div><div style="background:#fff;border:1px solid #d6d3d1;border-radius:4px;padding:6px 10px;font-size:13px;">Book the venue</div><div style="background:#fff;border:1px solid #d6d3d1;border-radius:4px;padding:6px 10px;font-size:13px;">Send the agenda</div><p style="margin:0;font-size:12px;color:#57534e;">The rows below snapped up. Nothing shows what went where.</p></div></div>
4
+ </figure>
5
+ <figure data-example="do">
6
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:6px;"><p style="margin:0;font-size:12px;color:#57534e;">Row 2 deleted. Over the next 150ms:</p><div style="background:#fff;border:1px solid #d6d3d1;border-radius:4px;padding:6px 10px;font-size:13px;">Pay the invoice</div><div style="background:#fff;border:1px solid #d6d3d1;border-radius:4px;padding:6px 10px;font-size:13px;box-shadow:0 0 0 2px #a8a29e;">Book the venue ↑</div><div style="background:#fff;border:1px solid #d6d3d1;border-radius:4px;padding:6px 10px;font-size:13px;box-shadow:0 0 0 2px #a8a29e;">Send the agenda ↑</div><p style="margin:0;font-size:12px;color:#57534e;">The rows below slide up into the gap, so the eye follows them.</p></div></div>
7
+ </figure>
@@ -0,0 +1,18 @@
1
+ <!-- G-162 · Animating layout, or everything -->
2
+ <figure data-example="dont">
3
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:6px;"><pre style="margin:0;font-family:ui-monospace,Menlo,monospace;font-size:12px;background:#fff;border:1px solid #d6d3d1;border-radius:4px;padding:8px;white-space:pre-wrap;">.panel {
4
+ transition: all 200ms;
5
+ }
6
+ .row {
7
+ transition: height 200ms;
8
+ }</pre></div></div>
9
+ </figure>
10
+ <figure data-example="do">
11
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:6px;"><pre style="margin:0;font-family:ui-monospace,Menlo,monospace;font-size:12px;background:#fff;border:1px solid #d6d3d1;border-radius:4px;padding:8px;white-space:pre-wrap;">.panel {
12
+ transition: opacity 150ms var(--ease-out),
13
+ transform 150ms var(--ease-out);
14
+ }
15
+ .row {
16
+ transition: transform 150ms var(--ease-in-out);
17
+ }</pre></div></div>
18
+ </figure>
@@ -0,0 +1,7 @@
1
+ <!-- G-163 · Several things moving at once -->
2
+ <figure data-example="dont">
3
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:6px;"><div style="background:#fff;border:1px solid #d6d3d1;border-radius:4px;padding:6px 10px;font-size:13px;">Gradient drifting behind the hero</div><div style="background:#fff;border:1px solid #d6d3d1;border-radius:4px;padding:6px 10px;font-size:13px;">Scroll arrow bouncing</div><div style="background:#fff;border:1px solid #d6d3d1;border-radius:4px;padding:6px 10px;font-size:13px;">Logo ticker scrolling</div><div style="background:#fff;border:1px solid #d6d3d1;border-radius:4px;padding:6px 10px;font-size:13px;">"New" badge pulsing</div><p style="margin:0;font-size:12px;color:#57534e;">Four loops at once. The saved message arrives and nobody sees it.</p></div></div>
4
+ </figure>
5
+ <figure data-example="do">
6
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:6px;"><div style="background:#fff;border:1px solid #d6d3d1;border-radius:4px;padding:6px 10px;font-size:13px;">Hero, still</div><div style="background:#fff;border:1px solid #d6d3d1;border-radius:4px;padding:6px 10px;font-size:13px;">Scroll arrow, still</div><div style="background:#fff;border:1px solid #d6d3d1;border-radius:4px;padding:6px 10px;font-size:13px;">Logo row, still</div><div style="background:#fff;border:1px solid #d6d3d1;border-radius:4px;padding:6px 10px;font-size:13px;box-shadow:0 0 0 2px #a8a29e;">Saved ✓</div><p style="margin:0;font-size:12px;color:#57534e;">One thing moves: the message the user caused.</p></div></div>
7
+ </figure>
@@ -0,0 +1,7 @@
1
+ <!-- I-148 · A claim with no source -->
2
+ <figure data-example="dont">
3
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">Teams using Acme ship 37% faster.</p><p style="margin:0;font-size:14px;">A 2024 study found developers save 11 hours a week.</p></div></div>
4
+ </figure>
5
+ <figure data-example="do">
6
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">In our own use, releases went from weekly to twice a week.</p><p style="margin:0;font-size:12px;color:#57534e;">Source: our release log, January to June 2026</p></div></div>
7
+ </figure>
@@ -0,0 +1,7 @@
1
+ <!-- I-149 · Specific in sound, empty in fact -->
2
+ <figure data-example="dont">
3
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">Organisations that adopt these practices often see significant gains in efficiency.</p></div></div>
4
+ </figure>
5
+ <figure data-example="do">
6
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">The billing team closed March in two days instead of five.</p></div></div>
7
+ </figure>
@@ -0,0 +1,7 @@
1
+ <!-- I-150 · Formula openers, closers and signposts -->
2
+ <figure data-example="dont">
3
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">In today's rapidly changing world, it is important to note that backups matter.</p><p style="margin:0;font-size:14px;">In conclusion, only time will tell.</p></div></div>
4
+ </figure>
5
+ <figure data-example="do">
6
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">Back up before you migrate.</p><p style="margin:0;font-size:14px;">A failed migration cannot be undone.</p></div></div>
7
+ </figure>
@@ -0,0 +1,7 @@
1
+ <!-- I-151 · Saying it twice -->
2
+ <figure data-example="dont">
3
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">Jig checks a page before it ships.</p><p style="margin:0;font-size:14px;">It reads the rules and applies them.</p><p style="margin:0;font-size:14px;">In short, Jig makes sure every page is checked before shipping.</p></div></div>
4
+ </figure>
5
+ <figure data-example="do">
6
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">Jig checks a page before it ships.</p><p style="margin:0;font-size:14px;">It reads the rules and applies them.</p><p style="margin:0;font-size:14px;">A machine decides 39 of them; a reviewer judges the rest.</p></div></div>
7
+ </figure>
@@ -0,0 +1,7 @@
1
+ <!-- I-152 · A rhetorical shape on repeat -->
2
+ <figure data-example="dont">
3
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">It's not a tool, it's a workflow.</p><p style="margin:0;font-size:14px;">It's not a checklist, it's a habit.</p><p style="margin:0;font-size:14px;">It's not a rule, it's a promise.</p></div></div>
4
+ </figure>
5
+ <figure data-example="do">
6
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">It runs in your editor.</p><p style="margin:0;font-size:14px;">Before you ship, it checks each page and names the rule a line breaks.</p></div></div>
7
+ </figure>
@@ -0,0 +1,7 @@
1
+ <!-- I-153 · Enthusiasm the content has not earned -->
2
+ <figure data-example="dont">
3
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">An incredibly exciting, truly transformative update. The possibilities are endless.</p></div></div>
4
+ </figure>
5
+ <figure data-example="do">
6
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">Exports now include every column, so the monthly report takes one download instead of four.</p></div></div>
7
+ </figure>
@@ -0,0 +1,7 @@
1
+ <!-- I-154 · Stacked hedges -->
2
+ <figure data-example="dont">
3
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">It may potentially suggest that this could, in some cases, help.</p></div></div>
4
+ </figure>
5
+ <figure data-example="do">
6
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">The evidence suggests it helps on pages over 2,000 words.</p></div></div>
7
+ </figure>
@@ -0,0 +1,7 @@
1
+ <!-- I-155 · Emoji in headings -->
2
+ <figure data-example="dont">
3
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><div style="font-size:16px;font-weight:700;line-height:1.2;">🚀 Getting started</div><p style="margin:0;font-size:14px;">Install it with one command.</p><div style="font-size:16px;font-weight:700;line-height:1.2;">✨ Key features</div><p style="margin:0;font-size:14px;">It checks each page before you ship.</p></div></div>
4
+ </figure>
5
+ <figure data-example="do">
6
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><div style="font-size:16px;font-weight:700;line-height:1.2;">Install Jig in one command</div><p style="margin:0;font-size:14px;">Install it with one command.</p><div style="font-size:16px;font-weight:700;line-height:1.2;">What each check reports</div><p style="margin:0;font-size:14px;">It checks each page before you ship.</p></div></div>
7
+ </figure>
@@ -0,0 +1,7 @@
1
+ <!-- I-156 · Structure imposed rather than earned -->
2
+ <figure data-example="dont">
3
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><div style="font-size:14px;font-weight:700;line-height:1.2;">Overview</div><p style="margin:0;font-size:14px;">Jig checks pages.</p><div style="font-size:14px;font-weight:700;line-height:1.2;">Key benefits</div><p style="margin:0;font-size:14px;">It is fast.</p><div style="font-size:14px;font-weight:700;line-height:1.2;">Conclusion</div><p style="margin:0;font-size:14px;">Jig checks pages.</p></div></div>
4
+ </figure>
5
+ <figure data-example="do">
6
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><div style="font-size:14px;font-weight:700;line-height:1.2;">Install Jig in one command</div><p style="margin:0;font-size:14px;">Run npx jig-ui init in your project.</p><div style="font-size:14px;font-weight:700;line-height:1.2;">Run your first check</div><p style="margin:0;font-size:14px;">Run npx jig-ui check. Each finding names its rule.</p></div></div>
7
+ </figure>
@@ -0,0 +1,7 @@
1
+ <!-- I-157 · Nothing only the author could say -->
2
+ <figure data-example="dont">
3
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">The migration was challenging, but the team learned valuable lessons.</p></div></div>
4
+ </figure>
5
+ <figure data-example="do">
6
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">The migration looked like a weekend's work. It took three weeks, because two services wrote the same table and nobody had written that down.</p></div></div>
7
+ </figure>
@@ -0,0 +1,7 @@
1
+ <!-- I-158 · No point of view -->
2
+ <figure data-example="dont">
3
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">There are several factors to consider, each with its own trade-offs.</p></div></div>
4
+ </figure>
5
+ <figure data-example="do">
6
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">The technology was not the hard part. Changing how procurement worked was.</p></div></div>
7
+ </figure>
@@ -0,0 +1,7 @@
1
+ <!-- I-159 · An even rhythm -->
2
+ <figure data-example="dont">
3
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">We started the project in May. We chose a small team for it. We wrote the plan in one week. We shipped the first page in June. We learned a lot along the way.</p></div></div>
4
+ </figure>
5
+ <figure data-example="do">
6
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">We started in May with three people. The plan took a week. The first page took six, because nobody had asked support what customers searched for.</p></div></div>
7
+ </figure>
@@ -0,0 +1,7 @@
1
+ <!-- I-160 · Feeling named, not shown -->
2
+ <figure data-example="dont">
3
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">I was fascinated by the results, which provided valuable insights.</p></div></div>
4
+ </figure>
5
+ <figure data-example="do">
6
+ <div style="font-family:system-ui,-apple-system,'Segoe UI',sans-serif;font-size:13px;line-height:1.45;color:#1f1f1f;background:#f5f5f4;padding:16px;min-height:176px;box-sizing:border-box;"><div style="display:flex;flex-direction:column;gap:8px;"><p style="margin:0;font-size:14px;">I did not expect the result. We had tested it three times, and it failed in exactly the same place.</p></div></div>
7
+ </figure>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jig-ui",
3
- "version": "0.22.0",
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.",
3
+ "version": "0.23.0",
4
+ "description": "A design system for coding agents. 159 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",
7
7
  "bin": {
@@ -590,6 +590,25 @@ A pulse says "look here, something is happening". On a status that has not chang
590
590
 
591
591
  **Scope: this is about motion that answers an input or carries a state change.** It is not a ceiling on every animation on the page. Slow decorative looping motion — see `P-13` — runs for seconds by design, and is not covered here. The reason the two differ is the reason the numbers differ: interaction motion sits between the user and their task, so it must get out of the way; ambient motion is never in the way, so speed would only make it noticeable.
592
592
 
593
+ ### G-161 A layout change that jumps
594
+ ❌ A row deleted, and the rows below snap up into the gap in one frame. A filter applied, and the grid reshuffles with nothing to follow. A card opened into its detail view, with nothing connecting the two.
595
+ ✅ When the user's own action moves things, show where they went. The rows below slide up into the gap. Items that stay keep their identity and move to their new place; items that go fade out where they stood. A card grows into its detail view (`view-transition-name` on both, where the browser has View Transitions). Moves within the screen take `--ease-in-out` and `--duration-base`; a whole view changing, `--duration-slow`.
596
+ A jump makes the user find their place again, and they cannot tell a row that moved from a row that changed. The move answers "where did it go?" before they have to ask.
597
+ **Keep it instant** when the user did not cause the change: a live table updating under someone's pointer must not slide the row they were about to click. Also when most of the view changes at once, since a cut is clearer than twenty moves. Under reduced motion the change is instant, and focus or a live region says where things went (`G-43`).
598
+ Technique, for a layout change CSS cannot transition: record each item's position before the change, apply it, then animate each item's `transform` from the old position to zero (first, last, invert, play). Never animate the layout properties themselves (`G-162`).
599
+
600
+ ### G-162 Animating layout, or everything
601
+ ❌ `transition: height 200ms`, `transition: all 150ms`, a keyframe that moves `top` or `margin-left`, Tailwind's `transition-all`
602
+ ✅ Animate `transform` and `opacity`. To move, `translate`; to grow, `scale` or a clip; to appear, `opacity`. Name the properties a transition covers: `transition: opacity var(--duration-fast) var(--ease-out), transform var(--duration-fast) var(--ease-out)`.
603
+ `width`, `height`, `margin`, `padding`, `top`, `left` and their kin change layout, so the browser works out the position of everything around the element again on every frame, and on a slow phone the motion stutters. `transform` and `opacity` do not move anything else. `all` animates every property that changes, including a layout one someone adds next month, and nobody reading the rule can tell which.
604
+ **One accepted case:** a disclosure opening to its content's height (`P-11`). Animate `grid-template-rows` from `0fr` to `1fr` on a wrapper, which the detector does not flag, or write the `height` transition with `<!-- jig-allow G-162: a disclosure grows to its content -->`.
605
+ The detector warns on `transition` or `transition-property` naming `all` or a layout property; a keyframe step setting one; Tailwind's `transition-all` and `transition-[<layout property>]`. `P-13`'s "Animate `transform` and `opacity`" becomes a pointer here.
606
+
607
+ ### G-163 Several things moving at once
608
+ ❌ A hero with a drifting gradient, a bouncing scroll arrow, a logo ticker and a pulsing "New" badge, all running together.
609
+ ✅ At a time, one thing moves: the one the user caused, or the one they need to notice now. Things that move as one count as one: a list reordering, `P-13`'s ambient layers on one surface.
610
+ Motion is the strongest pull on the eye a page has. Two things moving split it; four make the page restless, and the one that matters (a saved state, an error arriving) competes with decoration and loses. `G-145` is the one-dot case of this rule.
611
+
593
612
  ---
594
613
 
595
614
  ## H. Code-level
package/rules/01-modes.md CHANGED
@@ -190,7 +190,7 @@ Attempting to vary these by mode is a category error:
190
190
  - **Accessibility floors.** Contrast, focus indication, target size, semantic markup. Identical in all three. `operator` being dense does not license a 24px tap target or a 3:1 body contrast.
191
191
  - **Brand identity.** Palette, typeface, logo, voice.
192
192
  - **State completeness.** Every mode renders loading, empty, error and disabled.
193
- - **The anti-pattern file.** All 125 rules in it apply everywhere.
193
+ - **The anti-pattern file.** All 128 rules in it apply everywhere.
194
194
 
195
195
  ---
196
196
 
@@ -243,6 +243,9 @@ away. That maps onto the tokens:
243
243
  the closer analogue, and we do not ship one: exits in this system fade or
244
244
  collapse in place rather than fly off screen, and a third easing token bought
245
245
  only that one case.
246
+ - **Moving within the screen**, from one place to another: `--ease-in-out`. The
247
+ element is on screen at the start and the end, so it speeds up leaving its
248
+ place and slows into the new one (`G-161`).
246
249
  - **Never linear** for anything that moves. Linear reads as mechanical because
247
250
  nothing physical moves that way. Colour and opacity are the exception — a
248
251
  simple curve is enough there, and often linear is fine.
@@ -429,7 +429,8 @@ it that they did not cause. Motion in those modes always means something changed
429
429
  - **Animate `transform` and `opacity`.** These run on the compositor. A loop that
430
430
  runs forever on every frame the page is open cannot afford animated `blur`,
431
431
  `box-shadow`, or anything that triggers layout — the cost is not paid once, it is
432
- paid continuously, on whatever device the reader has.
432
+ paid continuously, on whatever device the reader has. `G-162` holds this for all motion;
433
+ here the cost is simply paid forever.
433
434
 
434
435
  **Anti-pattern:** ambient motion used to direct attention. It is atmosphere, not a
435
436
  signal. The moment it points at something it has become interaction motion badly
package/rules/05-copy.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # 05 · Copy
2
2
 
3
3
  **Status:** draft v0.1
4
- **Scope:** universal. Interface text in every mode.
5
- **Load when:** writing or reviewing any user-facing string — labels, buttons, headings, errors, empty states, help text.
4
+ **Scope:** universal. Interface text in every mode, and the prose a page ships: docs, guides, marketing pages, blog posts.
5
+ **Load when:** writing or reviewing any user-facing string — labels, buttons, headings, errors, empty states, help text — or any prose longer than a paragraph.
6
6
 
7
7
  Interface text is interface design. A screen with perfect spacing and a vague button label is a broken screen. Most of what follows costs nothing to apply and is invisible when done well.
8
8
 
@@ -142,6 +142,89 @@ See `P-01` for *where* the message goes and `F-37` for field-level validation te
142
142
 
143
143
  ---
144
144
 
145
+ ## Prose
146
+
147
+ **Scope:** prose longer than a paragraph that a page ships: docs, guides,
148
+ marketing pages, and everything under *Long-form* below. Interface strings stay
149
+ under the rules above; `I-79`'s 20-word sentence limit is for them, not for
150
+ prose. A page with no prose longer than a paragraph judges these `n/a`.
151
+
152
+ These rules judge whether prose is specific, sourced and authored. They never
153
+ judge who wrote it: a finding says the copy reads as generic, never that a
154
+ model wrote it.
155
+
156
+ ### I-148 A claim with no source
157
+ ❌ "Teams using Acme ship 37% faster." "A 2024 Stanford study found…" A feature described that the product does not have.
158
+ ✅ Every figure, study, quotation, date and feature a page states traces to something in the project: its data, its docs, a link to the source, the product itself. Otherwise it is cut, or written as what it is: "in our own use", "we expect".
159
+ A made-up figure reads exactly like a real one, and it is the claim a reader is most likely to repeat. Of everything here, it costs the most when it is wrong.
160
+
161
+ ### I-149 Specific in sound, empty in fact
162
+ ❌ "Organisations that adopt these practices often see significant gains in efficiency."
163
+ ✅ Name who, how many, how much, compared with what. "The billing team closed the month in two days instead of five."
164
+ The test, as `I-81` has it for headlines: if the sentence would still be true after swapping in any other product, team or year, it says nothing.
165
+
166
+ ### I-150 Formula openers, closers and signposts
167
+ ❌ "In today's rapidly changing world…" "It is important to note that…" "Let's dive in." "Here's the thing:" "In conclusion…" "Only time will tell."
168
+ ✅ Start with the point; end when it has been made. Signpost only where a reader would otherwise be lost.
169
+ `check` warns on a short, fixed list of phrases that are almost never needed; the judgment half covers the same habit in other words. A quotation keeps its own words, as with `I-118`.
170
+
171
+ ### I-151 Saying it twice
172
+ ❌ A conclusion that restates the introduction. A lead sentence repeated as the next section's opening. The same point made again in new words two paragraphs later.
173
+ ✅ Each paragraph adds something. Where a reader needs a reminder, point back to where it was said.
174
+
175
+ ### I-152 A rhetorical shape on repeat
176
+ ❌ "It's not X, it's Y" in every section; three adjectives, three verbs, three clauses, sentence after sentence; every paragraph built claim, example, takeaway.
177
+ ✅ Any one of these is fine. The finding is density: the same shape often enough that a reader starts to hear it.
178
+
179
+ ### I-153 Enthusiasm the content has not earned
180
+ ❌ "An incredibly exciting, truly transformative opportunity." "The possibilities are endless."
181
+ ✅ Let the thing described make the case. If it is impressive, the specifics show it; if they do not, the adjectives will not.
182
+
183
+ ### I-154 Stacked hedges
184
+ ❌ "It may potentially suggest that this could, in some cases, help."
185
+ ✅ One qualifier, where the uncertainty is real: "The evidence suggests…"
186
+ Exception: legal, medical and scientific text, and any claim that is genuinely uncertain, may need more.
187
+
188
+ ### I-155 Emoji in headings
189
+ ❌ "🚀 Getting started", "✨ Key features"
190
+ ✅ The words carry the heading. An emoji is read aloud by a screen reader as its name, and at the head of every section it reads as a template.
191
+ In markup, `A-05` already reports every emoji, a heading's included; `check` reports this rule for a Markdown page's headings, which `A-05` does not read.
192
+
193
+ ### I-156 Structure imposed rather than earned
194
+ ❌ Overview, Key benefits, Challenges, Best practices, Conclusion, whatever the subject. A heading every two paragraphs. Every bullet opening with a bold phrase. A numbered list for things with no order.
195
+ ✅ Let the content decide the structure: a heading where a reader would look for one, a numbered list for a sequence, bold only where a reader scanning must stop.
196
+
197
+ ---
198
+
199
+ ## Long-form
200
+
201
+ **Scope:** authored writing a person puts their name to: blog posts, essays,
202
+ case studies, release notes written as a story. Everything under *Prose* applies
203
+ as well. Judged by a reader, never measured: rhythm and voice have no number a
204
+ check could hold, and a score would reward prose that games it.
205
+
206
+ ### I-157 Nothing only the author could say
207
+ ❌ "The migration was challenging, but the team learned valuable lessons."
208
+ ✅ "The migration looked like a weekend's work. It took three weeks, because two services wrote the same table and nobody had written that down."
209
+ A post worth reading holds something the reader could not have written themselves: a case, a number, a mistake, a decision and its reason.
210
+
211
+ ### I-158 No point of view
212
+ ❌ "There are several factors to consider, each with its own trade-offs."
213
+ ✅ Say which factor mattered, and why: "The technology was not the hard part. Changing how procurement worked was."
214
+ Balance is fine when the question is open. A post that never commits to anything has not said what its author thinks.
215
+
216
+ ### I-159 An even rhythm
217
+ ❌ Sentence after sentence of the same length and build; paragraphs of identical size down the page.
218
+ ✅ Short sentences where something lands, longer ones where an idea needs room. Read it aloud: an even rhythm is audible long before it is visible.
219
+ Not a length rule, and not a count: `I-79`'s limit is for interface strings.
220
+
221
+ ### I-160 Feeling named, not shown
222
+ ❌ "I was fascinated by the results, which provided valuable insights."
223
+ ✅ "I did not expect the result. We had tested it three times, and it failed in exactly the same place."
224
+ Where a post reports a reaction, the specifics carry it; a named emotion with nothing behind it reads as flat.
225
+
226
+ ---
227
+
145
228
  ## L-06 · Copy checklist
146
229
 
147
230
  1. Sentence case throughout? (`I-53`)
@@ -152,3 +235,6 @@ See `P-01` for *where* the message goes and `F-37` for field-level validation te
152
235
  6. Numerals as figures, formatted consistently? (`I-83`)
153
236
  7. One word per concept across the whole product? (`I-87`)
154
237
  8. Every error saying what happened and what to do next? (`I-90`)
238
+ 9. Every figure, study, quotation and feature traceable to a source? (`I-148`)
239
+ 10. Prose saying something specific, once, without formula? (`I-149`, `I-150`, `I-151`)
240
+ 11. For long-form: something only the author could say, and a view? (`I-157`, `I-158`)
package/rules.index.json CHANGED
@@ -1013,5 +1013,119 @@
1013
1013
  "severity": "warning",
1014
1014
  "since": "0.17.0",
1015
1015
  "pass": "screen"
1016
+ },
1017
+ {
1018
+ "id": "I-148",
1019
+ "bucket": "judgment",
1020
+ "severity": "warning",
1021
+ "since": "0.23.0",
1022
+ "pass": "code"
1023
+ },
1024
+ {
1025
+ "id": "I-149",
1026
+ "bucket": "judgment",
1027
+ "severity": "note",
1028
+ "since": "0.23.0",
1029
+ "pass": "code"
1030
+ },
1031
+ {
1032
+ "id": "I-150",
1033
+ "bucket": "hybrid",
1034
+ "severity": "warning",
1035
+ "since": "0.23.0",
1036
+ "detector": "formula-phrase",
1037
+ "pass": "code"
1038
+ },
1039
+ {
1040
+ "id": "I-151",
1041
+ "bucket": "judgment",
1042
+ "severity": "note",
1043
+ "since": "0.23.0",
1044
+ "pass": "code"
1045
+ },
1046
+ {
1047
+ "id": "I-152",
1048
+ "bucket": "judgment",
1049
+ "severity": "note",
1050
+ "since": "0.23.0",
1051
+ "pass": "code"
1052
+ },
1053
+ {
1054
+ "id": "I-153",
1055
+ "bucket": "judgment",
1056
+ "severity": "note",
1057
+ "since": "0.23.0",
1058
+ "pass": "code"
1059
+ },
1060
+ {
1061
+ "id": "I-154",
1062
+ "bucket": "judgment",
1063
+ "severity": "note",
1064
+ "since": "0.23.0",
1065
+ "pass": "code"
1066
+ },
1067
+ {
1068
+ "id": "I-155",
1069
+ "bucket": "mechanical",
1070
+ "severity": "warning",
1071
+ "since": "0.23.0",
1072
+ "detector": "heading-emoji"
1073
+ },
1074
+ {
1075
+ "id": "I-156",
1076
+ "bucket": "judgment",
1077
+ "severity": "note",
1078
+ "since": "0.23.0",
1079
+ "pass": "code"
1080
+ },
1081
+ {
1082
+ "id": "I-157",
1083
+ "bucket": "judgment",
1084
+ "severity": "warning",
1085
+ "since": "0.23.0",
1086
+ "pass": "code"
1087
+ },
1088
+ {
1089
+ "id": "I-158",
1090
+ "bucket": "judgment",
1091
+ "severity": "note",
1092
+ "since": "0.23.0",
1093
+ "pass": "code"
1094
+ },
1095
+ {
1096
+ "id": "I-159",
1097
+ "bucket": "judgment",
1098
+ "severity": "note",
1099
+ "since": "0.23.0",
1100
+ "pass": "code"
1101
+ },
1102
+ {
1103
+ "id": "I-160",
1104
+ "bucket": "judgment",
1105
+ "severity": "note",
1106
+ "since": "0.23.0",
1107
+ "pass": "code"
1108
+ },
1109
+ {
1110
+ "id": "G-161",
1111
+ "bucket": "judgment",
1112
+ "severity": "note",
1113
+ "since": "0.23.0",
1114
+ "pass": "code"
1115
+ },
1116
+ {
1117
+ "id": "G-162",
1118
+ "bucket": "hybrid",
1119
+ "severity": "warning",
1120
+ "since": "0.23.0",
1121
+ "detector": "layout-motion",
1122
+ "pass": "code"
1123
+ },
1124
+ {
1125
+ "id": "G-163",
1126
+ "bucket": "judgment",
1127
+ "severity": "note",
1128
+ "since": "0.23.0",
1129
+ "pass": "code"
1016
1130
  }
1017
1131
  ]
@@ -369,6 +369,8 @@ sizes: # phone first. Each size is a whole composition, not
369
369
  why: "content is capped at 1200px, so 1600 adds margin and nothing else"
370
370
  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
371
371
  states: [default, loading, error, success]
372
+ motion: # every movement the page has, or `none`: what moves, on its trigger; tells what
373
+ - deleted row: rows below slide into the gap, on delete; tells where the list went
372
374
  decisions: [The Engineer Reads First] # every DECISIONS.md entry this screen implements, by name
373
375
  later: [social sign-in, remember this device] # cut from V1, by name — the next specs start here
374
376
  indexable: true # from the mode — editorial yes, product/operator no. Say why when you override it.
@@ -378,11 +380,19 @@ mockup: pending # approved or skipped, quoting the user: approved by
378
380
  mockup_at: # where the approved drawing is: a .jig/mockups path, or a Figma or Stitch link
379
381
  deviations: [] # `make` writes here; `spec` leaves it empty
380
382
  critique: each # optional: each, or at-ship; left out, the project's default in {{config_file}}
383
+ # superseded_by: reference.spec.md # only when another spec replaces this one; `ship` then leaves this page to that spec's critique
381
384
  ---
382
385
  ```
383
386
 
384
387
  - **`phone` is written first, and in full.** It is the most common screen, and the
385
388
  one a derived composition fails on worst.
389
+ - **`motion:` lists every movement, and why it moves.** Each names what moves, its
390
+ trigger, and what it tells the reader. The trigger is the user's input, a state
391
+ change, arrival (once per visitor, editorial only, `G-42`) or ambient (editorial
392
+ only, `P-13`). A movement that cannot say what it tells the reader is cut here,
393
+ before it is built. Write it from the composition; it is not a question for the
394
+ owner, who reads it on the sheet beside `states:`. A page with nothing moving
395
+ says `none`.
386
396
  - **`indexable:`, `title:` and `description:` are copy, decided here.** The mode
387
397
  sets the default — `editorial` is first-visit content and is indexable,
388
398
  `product` and `operator` are what somebody reaches after signing in and are
@@ -521,7 +531,15 @@ not your summary of what the owner meant. It writes
521
531
  touches.
522
532
 
523
533
  Fix what it found, then have the spec checked again: the record is of the spec
524
- you show, and the gate compares its `spec` checksum to the file. **If you cannot
534
+ you show, and the gate compares its `spec` checksum to the file.
535
+
536
+ **A second check covers what changed, not the whole spec again.** Give the
537
+ reader the earlier record and the spec's diff since it. A quotation or fact whose
538
+ line did not change, and whose source has not changed since (`git diff` on that
539
+ file), carries over as recorded; the reader checks the changed lines, the new
540
+ ones, and any fact whose source moved. It still writes the whole record, with
541
+ the new checksum. On jig-site each re-check opened every source again, about $10
542
+ a round, to confirm facts on lines nobody had touched. **If you cannot
525
543
  delegate, write the record yourself, with `"reader": "the writer"` in it, and say
526
544
  in the confirmation message that nobody else checked the spec.** That is an
527
545
  honest gap the owner can close by reading more closely; a check you ran on your
@@ -536,6 +554,8 @@ Show the spec, and with it the sheet the owner checks it by, from
536
554
  - **Facts:** each claim and its source, the ones that could not be checked
537
555
  first.
538
556
  - **Copy the page will show as written.**
557
+ - **What moves:** each line of `motion:`, with its trigger and what it tells
558
+ the reader, or that nothing moves.
539
559
  - **Your conditions**, beside the lines that carry them.
540
560
  - **Left for you:** the `open` items, which they can decide now.
541
561
 
@@ -896,6 +916,10 @@ indexable carries `noindex` instead, from its own metadata rather than from
896
916
  Then the ordinary rules apply: tokens by semantic name, the relevant
897
917
  `03-patterns.md` section for each component, `05-copy.md` for every string.
898
918
 
919
+ **Build the motion `motion:` lists, and no other.** A movement the spec does not
920
+ list is a deviation, recorded below like any other. A spec with no `motion:`
921
+ (one confirmed before it existed) leaves motion to the `G-` rules alone.
922
+
899
923
  ### Record what you changed
900
924
 
901
925
  Building reveals that a spec was wrong somewhere — that is normal and expected.
@@ -1060,7 +1084,11 @@ owner ruled icon-only, a menu the owner put at every phone width. The verdict
1060
1084
  names that decision in `ruling`, exactly as its heading reads, and `verdicts`
1061
1085
  refuses a `ruled` with no ruling or with one the file does not hold. A ruled
1062
1086
  verdict is reported, and counted apart (`ruled=`); it is not a finding, and it
1063
- does not go back to `make`, which could only leave it as it is. On jig-site a
1087
+ does not go back to `make`, which could only leave it as it is. **Put the field
1088
+ in each arm's brief, with its shape:** `{ "id": …, "verdict": "ruled", "ruling":
1089
+ "<the decision's heading>", "reason": … }`. On jig-site an arm wrote four ruled
1090
+ verdicts that named the decision only in `reason`, `verdicts` refused all four,
1091
+ and the parent had to write the field into a file it was not to edit. On jig-site a
1064
1092
  header critique counted 15 findings, five of them rulings labelled "owner-ruled"
1065
1093
  in prose: every count was inflated, and the real findings were harder to see.
1066
1094
  A rule the page breaks with no decision behind it is a `finding`, however sure
@@ -1129,11 +1157,21 @@ Each reader arm writes its own file. The arm reports back only that it wrote it.
1129
1157
  { "id": "P-14", "verdict": "finding", "reason": "menu button at 360 does not open; aria-expanded never set" },
1130
1158
  { "id": "A-60", "verdict": "n/a", "reason": "A-60 is about competing icons; this page has none" },
1131
1159
  { "id": "E-51", "verdict": "ruled", "ruling": "The theme toggle is icon-only", "reason": "the toggle shows only a sun or a moon; the owner ruled it icon-only" }
1160
+ ],
1161
+ "differences": [
1162
+ { "what": "the checklist gives nine checks", "where": "src/pages/guide.astro:245", "against": "README.md, 'Before you confirm a spec': ten" }
1132
1163
  ]
1133
1164
  }
1134
1165
  ```
1135
1166
 
1136
1167
  `screen.json` carries `rendered` and `artefacts`; `code.json` needs only `verdicts`.
1168
+ **`differences` holds what no rule names:** the page against its spec (step 3),
1169
+ or against a source it quotes or teaches. Each counts as a finding. Never carry
1170
+ one as a verdict under an id you made up: `verdicts` refuses an id the corpus
1171
+ does not hold, and on jig-site both arms of one critique invented one
1172
+ (`page-vs-corpus-1`), which the parent then had to move by hand. `ruled` is for a
1173
+ rule a decision overrides; a decision in `decisions.json` is judged `ok`,
1174
+ `finding` or `n/a`, never ruled.
1137
1175
 
1138
1176
  **And the project's own decisions, one verdict each**, in
1139
1177
  `.jig/critique/<surface>/decisions.json`:
@@ -1280,6 +1318,26 @@ grouped with what, what stacks or moves on the phone. Not the colour, type or
1280
1318
  polish: the mockup is grayscale and low-fidelity on purpose, so its appearance is
1281
1319
  not a target.
1282
1320
 
1321
+ **Motion, against `motion:`.** Each movement on the page is judged against its
1322
+ line: one the spec does not list is a `G-42` finding, and one that tells the
1323
+ reader something other than its line says is a finding under the rule it
1324
+ breaks. With no `motion:` in the spec, the `G-` rules alone judge it.
1325
+
1326
+ **Then the other way: the page against the spec.** Each section, paragraph,
1327
+ control or claim the page carries that no line of the spec asks for is a
1328
+ difference too, however accurate it is. The owner confirmed the spec, not the
1329
+ page; content the spec never held is content they never approved, and every
1330
+ later tweak and critique works from a spec that no longer says what is there.
1331
+ On jig-site a chapter carried an accurate paragraph with no spec line and no
1332
+ deviation, and two critiques in a row saw it and filed nothing, because it broke
1333
+ no rule. Report it among the findings as a difference from the spec, naming the
1334
+ content and where it is: the fix is a tweak that writes it into the spec for the
1335
+ owner to confirm, or removes it.
1336
+
1337
+ **Say in the report what each direction found**, "none" included: a report that
1338
+ does not mention the page-against-spec pass cannot be told apart from one that
1339
+ skipped it.
1340
+
1283
1341
  A difference from either is a finding **unless** `deviations:` already explains it
1284
1342
  — that is what the recorded deviation is for. A difference explained by neither is
1285
1343
  the more serious finding, because it means the spec has quietly stopped describing
@@ -1410,6 +1468,15 @@ jig-site a tweak shown a picture of a copy button wrote an exception to `E-51`
1410
1468
  gave, which every later critique would have judged the page by. The gate stops a
1411
1469
  tweak whose new `**Why:**` quotes words tweak.json does not hold.
1412
1470
 
1471
+ **A decision you record or amend covers the whole page, not the one place it
1472
+ was raised.** The owner rules on an instance ("keep this apostrophe curly") and
1473
+ the decision states a kind ("every apostrophe, curly"). Before you build, look
1474
+ through the page, and the chrome it shares, for every other instance of that
1475
+ kind, and bring each one in line in the same change; name them in your report.
1476
+ On jig-site a tweak recorded "every apostrophe and quotation mark curly" and
1477
+ fixed the one apostrophe the critique had named; three straight ones and a pair
1478
+ of straight quotation marks on the same page survived to the next critique.
1479
+
1413
1480
  ### 2. Bring the spec in line, if it disagrees
1414
1481
 
1415
1482
  If the spec says something the change contradicts, or the page will no longer
@@ -1482,7 +1549,10 @@ is owed a critique when it has none, when its page changed after it was judged,
1482
1549
  when a tweak deferred its re-judge, when its verdicts are incomplete, or when a
1483
1550
  finding stands that the owner has not ruled on. It prints each page's state and
1484
1551
  a `JIG_SHIP:` line, and exits non-zero until the project is ready. A spec that
1485
- is not confirmed yet is in progress and not held against the ship.
1552
+ is not confirmed yet is in progress and not held against the ship. Nor is a spec
1553
+ another has replaced: `superseded_by: <spec>` in its front matter leaves its page
1554
+ to that spec's critique, and `ship` checks the spec it names exists. Write that
1555
+ line, in those words, when a spec is replaced; a marker of your own is not read.
1486
1556
 
1487
1557
  ### 2. Clear what it names
1488
1558
 
@@ -42,7 +42,7 @@ cite the number when you follow or deliberately break one.
42
42
  selects it and nothing else will. (`explain L-01` prints it too, but the
43
43
  file is the source — do not skip the step if the command is unavailable.)
44
44
  4. Load the relevant section of `{{rules_path}}/03-patterns.md` for the component you are building.
45
- 5. Load `{{rules_path}}/05-copy.md` whenever you write a label, button, heading, error or empty state.
45
+ 5. Load `{{rules_path}}/05-copy.md` whenever you write a label, button, heading, error or empty state, or prose a page ships (docs, a guide, a blog post).
46
46
  6. Consume tokens by semantic name only. Never write a raw colour or pixel value
47
47
  at a call site, and never resolve a name yourself: if a token you need has no
48
48
  value, that is a finding to report, not a number to supply.