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 +84 -0
- package/README.md +9 -6
- package/dist/index.js +186 -15
- package/examples/G-161.html +7 -0
- package/examples/G-162.html +18 -0
- package/examples/G-163.html +7 -0
- package/examples/I-148.html +7 -0
- package/examples/I-149.html +7 -0
- package/examples/I-150.html +7 -0
- package/examples/I-151.html +7 -0
- package/examples/I-152.html +7 -0
- package/examples/I-153.html +7 -0
- package/examples/I-154.html +7 -0
- package/examples/I-155.html +7 -0
- package/examples/I-156.html +7 -0
- package/examples/I-157.html +7 -0
- package/examples/I-158.html +7 -0
- package/examples/I-159.html +7 -0
- package/examples/I-160.html +7 -0
- package/package.json +2 -2
- package/rules/00-anti-patterns.md +19 -0
- package/rules/01-modes.md +1 -1
- package/rules/02-tokens.md +3 -0
- package/rules/03-patterns.md +2 -1
- package/rules/05-copy.md +88 -2
- package/rules.index.json +114 -0
- package/templates/COMMAND.md.tmpl +73 -3
- package/templates/SKILL.md.tmpl +1 -1
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
|
|
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 **
|
|
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
|
|
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
|
|
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` |
|
|
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
|
|
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
|
|
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(
|
|
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.
|
|
4
|
-
"description": "A design system for coding agents.
|
|
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
|
|
193
|
+
- **The anti-pattern file.** All 128 rules in it apply everywhere.
|
|
194
194
|
|
|
195
195
|
---
|
|
196
196
|
|
package/rules/02-tokens.md
CHANGED
|
@@ -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.
|
package/rules/03-patterns.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
package/templates/SKILL.md.tmpl
CHANGED
|
@@ -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.
|