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 +56 -0
- package/package.json +1 -1
- package/plugins/super-ux/scripts/ux_lint.py +21 -21
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.
|
|
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)
|