super-ux 0.33.0 → 0.34.2

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,61 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.34.2 — 2026-08-11
4
+
5
+ ### Fixed
6
+
7
+ - **The 0.34.1 heading was `## [0.34.1]` and the release workflow extracts
8
+ `## <version>`.** The notes came back empty, the job failed before creating
9
+ anything, and the tag sat there looking delivered — the same shape that kept
10
+ `agent-sync` off npm for three releases. This repo's own history uses the
11
+ bare form; only the new entry deviated.
12
+
13
+ ## 0.34.1 — 2026-08-11
14
+
15
+ ### Changed
16
+
17
+ - **Seventy-six references over 100 lines now open with a `## Contents` list**,
18
+ generated from each file's own `##` headings.
19
+
20
+ `best-practices.md` and `best-practices-index.md` are deliberately untouched
21
+ in all five skills that carry them: the catalog already routes through a
22
+ generated tag index that the validator keeps in sync with it, which is a
23
+ better answer to the same problem than a heading list would be.
24
+
25
+ ## 0.34.0 — 2026-08-10
26
+
27
+ B-010 and B-002, and the gate that stops both coming back. The UX linter is
28
+ older and more central than the brand one and had neither codes nor fixtures;
29
+ this release gives it both, and then gates the fixtures so the harness cannot
30
+ fall behind the linter the way the linter fell behind the contract.
31
+
32
+ ### Added
33
+
34
+ - **Every UX linter rule has a code, `U001`..`U054`.** Twenty-one of them, in
35
+ the message itself, so a rule can be searched, cited in review and gated on.
36
+ The full table with severities is in `references/scenario-format.md` — the
37
+ contract, not the source, is where a rule's meaning lives.
38
+ - **`test/ux_lint_test.py` covers all twenty-one.** 43 checks: every code with
39
+ its planted defect, and a clean twin wherever silence is the interesting
40
+ half. Three defects were planted in the linter itself to confirm the harness
41
+ bites — a duplicate-id check that never fires, a Figma-frame check short-
42
+ circuited, a coverage check keyed to a status that does not exist — and each
43
+ turned exactly one case red.
44
+ - **`validate_ux_lint_coverage`** — every emitted code needs a fixture **and** a
45
+ contract row. It went red on all twenty-one on its first run, which is what a
46
+ coverage gate is supposed to do the day it is added.
47
+ - **`validate_run_instructions`** — closes B-002 from the other side. The
48
+ existing gate asked *for each known destination, does a command seed it?*;
49
+ this one asks *for each path an instruction tells the reader to run, is it a
50
+ destination anything seeds?* That is the direction a rename breaks: an
51
+ instruction naming `docs/ux/linter.py` while commands seed `docs/ux/lint.py`
52
+ passed the old gate and failed the reader.
53
+
54
+ ### Changed
55
+
56
+ - Linter output now carries the code before the message. Exit codes, severities
57
+ and behaviour are unchanged; anything keying off exit status is unaffected.
58
+
3
59
  ## 0.33.0 — 2026-08-10
4
60
 
5
61
  The chain designed landing pages and had nowhere to record that a landing is a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "super-ux",
3
- "version": "0.33.0",
3
+ "version": "0.34.2",
4
4
  "description": "Scenario-driven UI development for AI agents (Claude Code, Cursor, 70+ agents): a versioned design chain in docs/ux/, a scenario-first hard rule, a deterministic drift linter, and evidence-backed UX audits. This package is the installer CLI.",
5
5
  "bin": {
6
6
  "super-ux": "bin/super-ux.js"
@@ -73,12 +73,12 @@ def check_unique_and_gaps(entry_ids: list[str], label: str) -> None:
73
73
  seen[i] = seen.get(i, 0) + 1
74
74
  for i, n in seen.items():
75
75
  if n > 1:
76
- err(f"{label}: duplicate id {i} ({n} entries)")
76
+ err(f"[U001] {label}: duplicate id {i} ({n} entries)")
77
77
  nums = sorted(int(i.split("-")[1]) for i in seen)
78
78
  if nums:
79
79
  missing = [n for n in range(1, max(nums) + 1) if n not in nums]
80
80
  if missing:
81
- warn(f"{label}: id gaps (retired entries should stay): {missing}")
81
+ warn(f"[U002] {label}: id gaps (retired entries should stay): {missing}")
82
82
 
83
83
 
84
84
  def figma_enabled(foundation: str) -> bool | None:
@@ -136,26 +136,26 @@ def check_web_surface(screens: str, flows: str) -> None:
136
136
 
137
137
  if declared is None:
138
138
  warn(
139
- "screens.md: no **Web surfaces:** declaration — answer yes or no once "
139
+ "[U050] screens.md: no **Web surfaces:** declaration — answer yes or no once "
140
140
  "(a public page a search or answer engine reads?). An unanswered "
141
141
  "question reads as no, and this one cannot be fixed after launch"
142
142
  )
143
143
  elif declared is False and blocks:
144
144
  for sid in sorted(blocks):
145
145
  err(
146
- f"screens.md: declares no web surfaces but {sid} carries a "
146
+ f"[U051] screens.md: declares no web surfaces but {sid} carries a "
147
147
  f"**Web surface:** block — one of the two is wrong"
148
148
  )
149
149
  elif declared is True and not blocks:
150
150
  warn(
151
- "screens.md: declares web surfaces but no screen carries a "
151
+ "[U052] screens.md: declares web surfaces but no screen carries a "
152
152
  "**Web surface:** block"
153
153
  )
154
154
 
155
155
  for sid, body in sorted(blocks.items()):
156
156
  for field in WEB_SURFACE_FIELDS:
157
157
  if f"**{field}:**" not in body:
158
- err(f"screens.md: {sid} web surface block is missing **{field}:**")
158
+ err(f"[U053] screens.md: {sid} web surface block is missing **{field}:**")
159
159
 
160
160
  # A declaration of "no" is silence, so it must not be able to hide a flow
161
161
  # that plainly starts on the web. This is the one contradiction the
@@ -168,7 +168,7 @@ def check_web_surface(screens: str, flows: str) -> None:
168
168
  entry = m.group(1).strip()
169
169
  if re.match(r"https?://|/\S", entry):
170
170
  warn(
171
- f"flows.md: {fid} starts at a URL ({entry.split()[0]}) while "
171
+ f"[U054] flows.md: {fid} starts at a URL ({entry.split()[0]}) while "
172
172
  f"screens.md declares no web surfaces — one of the two is wrong"
173
173
  )
174
174
 
@@ -200,7 +200,7 @@ def check_vision(ux: Path, vision: str) -> None:
200
200
  return
201
201
  for section in VISION_SECTIONS:
202
202
  if not re.search(rf"^##\s+{re.escape(section)}\s*$", vision, re.MULTILINE):
203
- err(f"vision.md: missing section '## {section}'")
203
+ err(f"[U030] vision.md: missing section '## {section}'")
204
204
  # Emptiness is a defect only once the document claims to be finished.
205
205
  # A freshly seeded template is all headings and no content by design, and
206
206
  # a linter that fails on its own seed teaches people to skip the linter.
@@ -212,17 +212,17 @@ def check_vision(ux: Path, vision: str) -> None:
212
212
  if len(body) == 2:
213
213
  tail = re.split(r"^##\s", body[1], maxsplit=1, flags=re.MULTILINE)[0]
214
214
  if not tail.strip():
215
- err(f"vision.md: approved but '## {section}' is empty — "
215
+ err(f"[U031] vision.md: approved but '## {section}' is empty — "
216
216
  f"the section that settles arguments cannot be blank")
217
217
 
218
218
  root = ux.parent.parent if ux.name == "ux" else ux.parent
219
219
  present = [root / n for n in INSTRUCTION_FILES if (root / n).is_file()]
220
220
  if not present:
221
- warn("vision.md exists but the project has no CLAUDE.md / AGENTS.md / "
221
+ warn("[U032] vision.md exists but the project has no CLAUDE.md / AGENTS.md / "
222
222
  "GEMINI.md — the alignment rule has nowhere to live")
223
223
  return
224
224
  if not any(VISION_RULE_HEADING in read(p) for p in present):
225
- warn(f"vision.md exists but no '{VISION_RULE_HEADING}' block in "
225
+ warn(f"[U033] vision.md exists but no '{VISION_RULE_HEADING}' block in "
226
226
  f"{', '.join(p.name for p in present)} — nothing ever reads the vision "
227
227
  f"(run the `vision` skill's step 4)")
228
228
 
@@ -236,7 +236,7 @@ def check_links(ux: Path) -> None:
236
236
  continue
237
237
  resolved = (md.parent / target.split("#", 1)[0]).resolve()
238
238
  if not resolved.exists():
239
- warn(f"{md.name}: broken link -> {target}")
239
+ warn(f"[U040] {md.name}: broken link -> {target}")
240
240
 
241
241
 
242
242
  def main() -> int:
@@ -276,18 +276,18 @@ def main() -> int:
276
276
  continue
277
277
  idx = index_ids(text, pref)
278
278
  for missing in sorted(entries - idx):
279
- warn(f"{name}: {missing} has no index row")
279
+ warn(f"[U003] {name}: {missing} has no index row")
280
280
  for ghost in sorted(idx - entries):
281
- err(f"{name}: index lists {ghost} but no entry exists")
281
+ err(f"[U004] {name}: index lists {ghost} but no entry exists")
282
282
 
283
283
  # --- Flows reference existing screens ---
284
284
  if has_flows and has_screens:
285
285
  screen_ids = set(ids(screens, "SCR"))
286
286
  used = refs(flows, "SCR")
287
287
  for miss in sorted(used - screen_ids):
288
- err(f"flows.md references {miss} but screens.md has no such screen")
288
+ err(f"[U010] flows.md references {miss} but screens.md has no such screen")
289
289
  for orphan in sorted(screen_ids - used):
290
- warn(f"screens.md: {orphan} is used by no flow (orphan)")
290
+ warn(f"[U011] screens.md: {orphan} is used by no flow (orphan)")
291
291
 
292
292
  # --- Scenario traces resolve ---
293
293
  if ids(scenarios, "SCN"):
@@ -297,10 +297,10 @@ def main() -> int:
297
297
  traced_flw = refs(scenarios, "FLW")
298
298
  if has_stories:
299
299
  for miss in sorted(traced_st - story_ids):
300
- warn(f"scenarios.md: traces to {miss} which is not in foundation.md")
300
+ warn(f"[U012] scenarios.md: traces to {miss} which is not in foundation.md")
301
301
  if has_flows:
302
302
  for miss in sorted(traced_flw - flow_ids):
303
- warn(f"scenarios.md: traces to {miss} which is not in flows.md")
303
+ warn(f"[U013] scenarios.md: traces to {miss} which is not in flows.md")
304
304
 
305
305
  # --- must/should stories have a scenario ---
306
306
  if has_stories and ids(scenarios, "SCN"):
@@ -312,7 +312,7 @@ def main() -> int:
312
312
  tail = re.split(r"^#{2,3}\s", foundation[m.end():], maxsplit=1, flags=re.MULTILINE)[0]
313
313
  if re.search(r"\*\*Priority:\*\*\s*(must|should)", tail, re.IGNORECASE):
314
314
  if sid not in traced:
315
- warn(f"foundation.md: {sid} (must/should) has no scenario tracing to it")
315
+ warn(f"[U014] foundation.md: {sid} (must/should) has no scenario tracing to it")
316
316
 
317
317
  # --- Screen-level: Figma frames, coverage, drift status ---
318
318
  if has_screens:
@@ -330,11 +330,11 @@ def main() -> int:
330
330
  cells = [c.strip() for c in rest.split("|")]
331
331
  frame = cells[1] if len(cells) >= 2 else ""
332
332
  if not frame or frame in ("-", "—", "<frame deep-link>", "<frame link>"):
333
- err(f"screens.md: {sid} state '{state}' has no Figma frame link")
333
+ err(f"[U020] screens.md: {sid} state '{state}' has no Figma frame link")
334
334
  cov_m = re.search(r"\*\*Coverage:\*\*\s*(.+)", body)
335
335
  cov = cov_m.group(1).strip() if cov_m else ""
336
336
  if status == "built" and (not cov or cov.lower().startswith("none")):
337
- warn(f"screens.md: {sid} is 'built' but has no Coverage")
337
+ warn(f"[U021] screens.md: {sid} is 'built' but has no Coverage")
338
338
 
339
339
  check_vision(ux, vision)
340
340
  check_web_surface(screens, flows)