jig-ui 0.11.0 → 0.12.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,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.0
4
+
5
+ ### Added
6
+
7
+ - **`I-118` Em dashes in interface text.** Use a full stop, a comma, a colon
8
+ or a second element. The mark is ambiguous on its own: it stands in for
9
+ four different ones, and the reader only learns which after reading past
10
+ it. It is also the clearest tell of machine-written copy, and copy that
11
+ reads as generated is copy the reader trusts less. The `em-dash` detector
12
+ reads element text and the attributes a reader hears or sees; scripts,
13
+ styles, comments, class names and documentation are not interface text.
14
+ The en dash keeps its one job, a range read as "to" (`2-10 seats`).
15
+
16
+ ## 0.11.1
17
+
18
+ Two holes in the gate, both found by running the loop twice on 0.11.0 —
19
+ once on Haiku, once on Sonnet.
20
+
21
+ ### Fixed
22
+
23
+ - **The Stop hook's block budget is per failure, not per session.** A
24
+ headless session keeps one session id across every `/jig` step, so a run
25
+ that spent its three blocks on `init`, `spec` and `make` reached
26
+ `critique` with none left: it reported "complete and approved, zero
27
+ findings" having written no verdict files, and the gate that would have
28
+ caught it had already let go. Fixing a failure now returns its attempts,
29
+ a new failure starts fresh, and an agent that cannot fix a given failure
30
+ still gets out after three tries.
31
+ - **`nav: horizontal bar (no menu)` is no longer read as a menu button**
32
+ at a width where the links fit. A false positive is how a gate gets
33
+ ignored.
34
+
3
35
  ## 0.11.0
4
36
 
5
37
  One release, one lesson: a measurement an agent can type is not a
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 113 rules, **16 can be decided by a machine** — a hard-coded colour, a
15
+ - Of the 114 rules, **17 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
17
  - The other **97 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 16. Running only the agent gets you the 97 with
21
+ Running only the CLI gets you the 17. Running only the agent gets you the 97 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
 
package/dist/index.js CHANGED
@@ -3299,6 +3299,59 @@ var undeclaredToken = {
3299
3299
  }
3300
3300
  };
3301
3301
 
3302
+ // src/check/detectors/em-dash.ts
3303
+ var MARKUP = [".html", ".htm", ".vue", ".svelte", ".astro", ".jsx", ".tsx", ".php", ".erb", ".twig", ".hbs"];
3304
+ var TEXT_ATTRS = /\b(aria-label|aria-description|aria-placeholder|title|placeholder|alt|label|value|content)\s*=\s*("([^"]*)"|'([^']*)')/gi;
3305
+ var EM_DASH = /—/;
3306
+ function maskNonProse(source) {
3307
+ const blank3 = (m) => " ".repeat(m.length);
3308
+ return source.replace(/<script\b[\s\S]*?<\/script\s*>/gi, blank3).replace(/<style\b[\s\S]*?<\/style\s*>/gi, blank3).replace(/<!--[\s\S]*?-->/g, blank3).replace(/\/\*[\s\S]*?\*\//g, blank3).replace(/(^|[^:])\/\/[^\n]*/g, (m, p1) => p1 + " ".repeat(m.length - p1.length));
3309
+ }
3310
+ function proseSpans(masked) {
3311
+ const spans = [];
3312
+ let last = 0;
3313
+ for (const tag of masked.matchAll(/<[^>]*>/g)) {
3314
+ const between = masked.slice(last, tag.index);
3315
+ if (between.trim()) spans.push({ index: last, text: between });
3316
+ for (const attr of tag[0].matchAll(TEXT_ATTRS)) {
3317
+ const value = attr[3] ?? attr[4] ?? "";
3318
+ spans.push({ index: tag.index + attr.index + attr[0].indexOf(value), text: value });
3319
+ }
3320
+ last = tag.index + tag[0].length;
3321
+ }
3322
+ const tail = masked.slice(last);
3323
+ if (tail.trim()) spans.push({ index: last, text: tail });
3324
+ return spans;
3325
+ }
3326
+ var emDash = {
3327
+ name: "em-dash",
3328
+ appliesTo: (file) => hasExtension(file, MARKUP),
3329
+ run(_source, file, ctx) {
3330
+ const masked = maskNonProse(ctx.raw);
3331
+ const starts = buildLineIndex(ctx.raw);
3332
+ const findings = [];
3333
+ const seen = /* @__PURE__ */ new Set();
3334
+ for (const span of proseSpans(masked)) {
3335
+ if (!EM_DASH.test(span.text)) continue;
3336
+ const offset = span.index + span.text.indexOf("\u2014");
3337
+ const line = lineForOffset(starts, offset);
3338
+ if (seen.has(line)) continue;
3339
+ seen.add(line);
3340
+ findings.push(
3341
+ mkFinding(
3342
+ ctx,
3343
+ "em-dash",
3344
+ file,
3345
+ line,
3346
+ "an em dash in interface text: use a full stop, a comma, a colon, or a second element",
3347
+ sourceLine(ctx.raw, line)
3348
+ )
3349
+ );
3350
+ }
3351
+ return findings;
3352
+ }
3353
+ };
3354
+
3302
3355
  // src/check/registry.ts
3303
3356
  var DETECTORS = [
3304
3357
  gradientText,
@@ -3316,7 +3369,8 @@ var DETECTORS = [
3316
3369
  inputZoom,
3317
3370
  safeArea,
3318
3371
  menuState,
3319
- undeclaredToken
3372
+ undeclaredToken,
3373
+ emDash
3320
3374
  ];
3321
3375
  function getDetector(name) {
3322
3376
  return DETECTORS.find((d) => d.name === name);
@@ -5215,6 +5269,7 @@ function verifyVerdicts(opts) {
5215
5269
 
5216
5270
  // src/commands/gate.ts
5217
5271
  import { existsSync as existsSync20, mkdirSync as mkdirSync8, readdirSync as readdirSync13, readFileSync as readFileSync27, writeFileSync as writeFileSync7 } from "fs";
5272
+ import { createHash as createHash2 } from "crypto";
5218
5273
  import { join as join33 } from "path";
5219
5274
 
5220
5275
  // src/check/spec-shape.ts
@@ -5275,7 +5330,7 @@ function sizeBlock(sizes, size) {
5275
5330
  }
5276
5331
  return lines.join("\n");
5277
5332
  }
5278
- var MENU_RE = /\b(menu|hamburger|drawer|burger)\b/i;
5333
+ var MENU_RE = /(?<!\b(?:no|not|without|never)\s)(?<!\bno\s\w{1,12}\s)\b(menu|hamburger|drawer|burger)\b/i;
5279
5334
  var NONE_RE = /^\s*(none|no navigation|n\/a|-)\b/i;
5280
5335
  function navProblems(spec) {
5281
5336
  const front = spec.body.split(/^---\s*$/m)[1] ?? "";
@@ -5397,6 +5452,7 @@ function gate(opts) {
5397
5452
  }
5398
5453
  }
5399
5454
  const session = opts.input.session_id ?? "unknown";
5455
+ const key = `${session}:${createHash2("sha256").update(problems.join("\n")).digest("hex").slice(0, 12)}`;
5400
5456
  const stateFile = join33(root, ".jig", "gate.json");
5401
5457
  let state = {};
5402
5458
  try {
@@ -5404,13 +5460,14 @@ function gate(opts) {
5404
5460
  } catch {
5405
5461
  }
5406
5462
  if (problems.length === 0) {
5407
- if (state[session]) {
5408
- delete state[session];
5463
+ const mine = Object.keys(state).filter((k) => k.startsWith(`${session}:`));
5464
+ if (mine.length) {
5465
+ for (const k of mine) delete state[k];
5409
5466
  save(stateFile, state);
5410
5467
  }
5411
5468
  return { block: false, reason: "" };
5412
5469
  }
5413
- const count = (state[session] ?? 0) + 1;
5470
+ const count = (state[key] ?? 0) + 1;
5414
5471
  if (count > MAX_BLOCKS) {
5415
5472
  return {
5416
5473
  block: false,
@@ -5418,7 +5475,7 @@ function gate(opts) {
5418
5475
  ${problems.join("\n\n")}`
5419
5476
  };
5420
5477
  }
5421
- state[session] = count;
5478
+ state[key] = count;
5422
5479
  save(stateFile, state);
5423
5480
  return {
5424
5481
  block: true,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "jig-ui",
3
- "version": "0.11.0",
4
- "description": "A design system for coding agents. 113 numbered UI rules, brand x mode design tokens, and an installer for Claude Code, Codex, Cursor and opencode.",
3
+ "version": "0.12.0",
4
+ "description": "A design system for coding agents. 114 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": {
package/rules/05-copy.md CHANGED
@@ -83,6 +83,14 @@ The one legitimate use is a **short label** distinguishing itself from nearby te
83
83
  ✅ Most interface text is too short to need them. Use a full stop only where the text is a complete sentence containing commas.
84
84
  Whichever you choose, be consistent across sibling elements — a list where three items end in a stop and two do not looks like a mistake, because it is one.
85
85
 
86
+ ### I-118 Em dashes in interface text
87
+ ❌ "Your plan — including every seat — renews monthly", "Free — forever", "Deleted — this cannot be undone"
88
+ ✅ A full stop, a comma, a colon or a new element. "Your plan renews monthly, including every seat." "Free forever." "Deleted. This cannot be undone."
89
+ Interface text is read in fragments, at a glance, in a space someone else's content has to fit too. An em dash is a pause the reader has to interpret: it stands in for a comma, a colon, a bracket or a full stop, and which one it is only becomes clear after reading past it. The punctuation that says exactly one thing is faster.
90
+ It is also the clearest tell of machine-written copy. Generated text reaches for the em dash far more often than a person does, and readers have learned to notice. Copy that reads as generated is copy the reader trusts less, whatever it says.
91
+ This is about interface strings — labels, buttons, headings, errors, empty states, help text, and the prose a page ships. It is not a rule about your documentation, your commit messages or this file.
92
+ The en dash keeps its one job: ranges, where it is read as "to" (`2–10 seats`, `Mon–Fri`). That is not a pause, and it is not affected.
93
+
86
94
  ### I-87 Inconsistent vocabulary
87
95
  ❌ "Add to cart" beside a "Bag" icon; "Sign up" on the page and "Register" in the nav
88
96
  ✅ One word per concept, everywhere. Keep a term list in the project and follow it.
package/rules.index.json CHANGED
@@ -793,5 +793,12 @@
793
793
  "severity": "error",
794
794
  "since": "0.10.0",
795
795
  "detector": "undeclared-token"
796
+ },
797
+ {
798
+ "id": "I-118",
799
+ "bucket": "mechanical",
800
+ "severity": "warning",
801
+ "since": "0.12.0",
802
+ "detector": "em-dash"
796
803
  }
797
804
  ]