super-ux 0.28.0 → 0.29.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 +39 -0
- package/README.md +2 -1
- package/bin/super-ux.js +17 -10
- package/cursor/rules/super-ux.mdc +4 -0
- package/package.json +2 -1
- package/plugins/super-ux/scripts/ux_doctor.py +205 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.29.0 — 2026-08-05
|
|
4
|
+
|
|
5
|
+
A pass aimed at the installed base rather than the catalog. Run across the
|
|
6
|
+
twelve projects using super-ux, the tooling turned out to be blind to the
|
|
7
|
+
most common failure: only two are cleanly on contract v4, five carry no
|
|
8
|
+
marker at all — including the two largest bases, at 119 and 120 scenarios —
|
|
9
|
+
one holds v2, v3 and v4 across four artifacts simultaneously, and one keeps
|
|
10
|
+
its base under a name the contract does not own, so four audit reports were
|
|
11
|
+
produced against scenarios the tooling could not find.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **`/ux-doctor` and `docs/ux/doctor.py`.** `ux_lint` checks a chain against
|
|
15
|
+
itself, so a base written entirely to an old contract passes it — from the
|
|
16
|
+
inside such a chain is consistent. The doctor reports the effective
|
|
17
|
+
contract version, what each version since it introduced, mixed versions
|
|
18
|
+
across artifacts, files the tooling cannot find under their contract
|
|
19
|
+
names, audits produced against a base that is not there, and which
|
|
20
|
+
additive sections a project has not adopted. Read-only unless `--fix`,
|
|
21
|
+
which does only what cannot be wrong: renames, and moving loose audit
|
|
22
|
+
reports into `audits/`. Contract upgrades stay content decisions for
|
|
23
|
+
`/ux-update`. Wired into `/ux` status and the Cursor rule.
|
|
24
|
+
- **Information architecture — BP-180, BP-181.** BP-052 requires navigation
|
|
25
|
+
to be visible and BP-049 puts it in reach; neither judges whether the
|
|
26
|
+
*groups* make sense. Card sorting for grouping, tree testing for labels —
|
|
27
|
+
the one navigation decision that cannot be judged from the inside, because
|
|
28
|
+
the team already knows where everything is.
|
|
29
|
+
- **Moderated test tasks generated from `scenarios.md`.** A scenario is
|
|
30
|
+
already the shape a test task wants — situation, goal, observable success
|
|
31
|
+
— so the tasks are a rewrite of the base rather than a new artifact, and
|
|
32
|
+
what comes back grades against the same base.
|
|
33
|
+
- **`benchmark:<competitor>` audit scope.** Every other scope measures the
|
|
34
|
+
product against its own chain and cannot report that a flow is two steps
|
|
35
|
+
longer than everyone else's. Same axes both sides, observable from outside
|
|
36
|
+
only, and gaps recorded as opportunities for the foundation rather than as
|
|
37
|
+
defects in the report.
|
|
38
|
+
- **Reviews and support tickets as WHY-layer evidence** in `ux-foundation` —
|
|
39
|
+
the cheapest input the layer has, and the one most often skipped because
|
|
40
|
+
it does not feel like research.
|
|
41
|
+
|
|
3
42
|
## 0.28.0 — 2026-08-05
|
|
4
43
|
|
|
5
44
|
Closes the carry-over ledger the 0.27 audit opened. Twenty-three practices,
|
package/README.md
CHANGED
|
@@ -185,9 +185,10 @@ The contracts every skill reads:
|
|
|
185
185
|
|---|---|
|
|
186
186
|
| [scenario-format.md](plugins/super-ux/skills/references/scenario-format.md) | **The contract (ux-contract v4).** File layout, every field name, stable IDs (`P` `JTBD` `JRN` `ST` `FLW` `SCR` `SCN`), completeness checklists, the `draft → validated → implemented` lifecycle, audit verdicts and severities, the UX-plan format |
|
|
187
187
|
| [system-map.md](plugins/super-ux/skills/references/system-map.md) | The whole system on one page — pipeline, files, skills, companions, and the four sync rules; every skill points here |
|
|
188
|
+
| [ux_doctor.py](plugins/super-ux/scripts/ux_doctor.py) | Contract doctor — reports mixed or stale contract versions across a project's artifacts, files the tooling cannot find under their contract names, and audits produced against a base that is not there. `/ux-lint` checks a chain against itself; this checks it against the contract. Installed as `docs/ux/doctor.py`, read-only unless `--fix` |
|
|
188
189
|
| [best-practices-index.md](plugins/super-ux/skills/references/best-practices-index.md) | Generated tag index over the catalog — tag → ids, id → title. Read it to decide which entries to open; regenerated by `plugins/super-ux/scripts/bp_index.py` and checked for drift by the validator |
|
|
189
190
|
| [ux-design-principles.md](plugins/super-ux/skills/references/ux-design-principles.md) | How the agent thinks: the design pipeline (forward and backwards), task analysis, flow rules, heuristics PRN-01..21, the improvement procedure, anti-patterns |
|
|
190
|
-
| [best-practices.md](plugins/super-ux/skills/references/best-practices.md) | Living, tag-indexed catalog of
|
|
191
|
+
| [best-practices.md](plugins/super-ux/skills/references/best-practices.md) | Living, tag-indexed catalog of 181 proven practices — subscription-app laws, mobile/web/voice guidance (Apple HIG 2025, M3 Expressive, NN/g, Baymard, WCAG 2.2), monetization economics (RevenueCat/PLG 2025 benchmarks, ASO, freemium boundaries), web funnels end to end (landing, pricing, checkout, dunning, cancel) and web2app (paid handoff, deferred deep links, storefront rules), motion and page weight (HTTP Archive field data, W3C sustainability), accessibility as it actually fails (WebAIM Million, EAA/ADA exposure), frustration telemetry, gamification and trend governance, growth loops and referral mechanics, empty states, authentication (NIST SP 800-63B rev 4) and form recovery, motion craft and perceived quality, the defaults that make an interface read as generated, interface state, locale and platform surfaces (Web Interface Guidelines), visual craft, Figma structure |
|
|
191
192
|
| [practice-selection.md](plugins/super-ux/skills/references/practice-selection.md) | The deterministic bridge: product profile → mandatory consideration sets → per-artifact checklists → a compliance table where every pulled practice gets a verdict. No silent skips, no cargo cult |
|
|
192
193
|
| [component-guidelines.md](plugins/super-ux/skills/references/component-guidelines.md) | Which control for which job (radios/select/switch, sheet/alert, modal/disclosure, combobox, nav bar/rail, FAB, dates, toasts) and the platform rules — Apple HIG, Material 3, W3C ARIA APG, GOV.UK |
|
|
193
194
|
| [visual-identity.md](plugins/super-ux/skills/references/visual-identity.md) | The visual layer and its owner: one style pack for the whole product, where it's recorded, how it meets Figma and code, and the division of labor with the craft floors |
|
package/bin/super-ux.js
CHANGED
|
@@ -89,16 +89,23 @@ function installCursor(target, force) {
|
|
|
89
89
|
// The linter is code, not a template — refresh it to the shipped version.
|
|
90
90
|
// Shipped via package.json files[]; if that ever regresses, warn instead of
|
|
91
91
|
// dying on an ENOENT stack trace after the rules are already installed.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
92
|
+
// Paths stay literal so test/validate.py can read them out of this source and
|
|
93
|
+
// check them against package.json files[] — a variable segment here silently
|
|
94
|
+
// turns that check into a directory prefix nobody ships.
|
|
95
|
+
for (const [src, from, dst] of [
|
|
96
|
+
['ux_lint.py', path.join(ROOT, 'plugins', 'super-ux', 'scripts', 'ux_lint.py'), 'lint.py'],
|
|
97
|
+
['ux_doctor.py', path.join(ROOT, 'plugins', 'super-ux', 'scripts', 'ux_doctor.py'), 'doctor.py'],
|
|
98
|
+
]) {
|
|
99
|
+
const to = path.join(target, 'docs', 'ux', dst);
|
|
100
|
+
if (fs.existsSync(from)) {
|
|
101
|
+
fs.copyFileSync(from, to);
|
|
102
|
+
console.log(`sync: ${to}`);
|
|
103
|
+
} else {
|
|
104
|
+
console.error(
|
|
105
|
+
`warning: ${src} not found in this package (${from}); docs/ux/${dst} was not installed.\n` +
|
|
106
|
+
` Get it from https://github.com/${REPO}/blob/main/plugins/super-ux/scripts/${src}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
console.log(`done: ${installed} installed, ${skipped} skipped`);
|
|
@@ -14,6 +14,10 @@ alwaysApply: true
|
|
|
14
14
|
- Run the linter after any UX change and before calling work done:
|
|
15
15
|
`python3 docs/ux/lint.py`. It must pass — drift must not merge; wire it
|
|
16
16
|
into CI/pre-commit.
|
|
17
|
+
- Run `python3 docs/ux/doctor.py` when picking this base up after a while.
|
|
18
|
+
The linter checks the chain against itself and stays silent when the whole
|
|
19
|
+
base is written to an old contract; the doctor is what notices. Fix a
|
|
20
|
+
mixed or stale contract before designing on top of it.
|
|
17
21
|
- Any change that touches user-facing behavior MUST update
|
|
18
22
|
`docs/ux/scenarios.md` in the same change (add/adjust scenarios, statuses,
|
|
19
23
|
coverage). New user-facing behavior with no scenario is a blocker, not a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "super-ux",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
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"
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"cursor",
|
|
11
11
|
"templates",
|
|
12
12
|
"plugins/super-ux/scripts/ux_lint.py",
|
|
13
|
+
"plugins/super-ux/scripts/ux_doctor.py",
|
|
13
14
|
"README.md",
|
|
14
15
|
"LICENSE",
|
|
15
16
|
"CHANGELOG.md"
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Diagnose a project's UX chain against the current contract (stdlib only).
|
|
3
|
+
|
|
4
|
+
`ux_lint.py` checks a chain against itself -- ids, links, orphans. It cannot
|
|
5
|
+
tell you that the whole base is written to a contract three versions old,
|
|
6
|
+
because from the inside everything is consistent. That is the drift this
|
|
7
|
+
finds: between the project and the contract it was written to.
|
|
8
|
+
|
|
9
|
+
Read-only by default. `--fix` applies only the changes that cannot be wrong:
|
|
10
|
+
renaming a file the contract owns and stamping a marker onto an artifact whose
|
|
11
|
+
shape already matches. Everything else is reported for a human to decide.
|
|
12
|
+
|
|
13
|
+
python3 ux_doctor.py [path] # report
|
|
14
|
+
python3 ux_doctor.py [path] --fix # apply the safe subset
|
|
15
|
+
python3 ux_doctor.py [path] --brief # one line, for sweeping many projects
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
CURRENT = 4
|
|
25
|
+
|
|
26
|
+
# What each contract version introduced, so a report can say what is missing
|
|
27
|
+
# rather than only that something is old.
|
|
28
|
+
HISTORY = {
|
|
29
|
+
2: "`Traces:` on every scenario + traceability rules; `foundation.md` (personas, JTBD, journeys, stories)",
|
|
30
|
+
3: "scenarios become use cases (`user action -> system response`), `Alt paths`, `Traces` includes `FLW-NN`; `flows.md`; `plans/`",
|
|
31
|
+
4: "`screens.md` — one `SCR-NN` entry per screen with per-state Figma frames; flows reference screens by id instead of respecifying them",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Additive since v4 — absence is not a version problem, but it is worth naming.
|
|
35
|
+
ADDITIVE = [
|
|
36
|
+
("foundation.md", "## Product mechanics", "Product mechanics section (personalization, engagement, a11y regime) — 0.26.1"),
|
|
37
|
+
("scenarios.md", "**Telemetry:**", "`Telemetry` on scenarios — the bridge to analytics practices, 0.28.0"),
|
|
38
|
+
("foundation.md", "**Kill criteria:**", "`Kill criteria` on stories — gives `dropped` a definition, 0.28.0"),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
ARTIFACTS = ["foundation.md", "flows.md", "screens.md", "scenarios.md"]
|
|
42
|
+
|
|
43
|
+
# Names the contract owns, and the near-misses seen in the wild.
|
|
44
|
+
RENAMES = {
|
|
45
|
+
"ux-scenarios.md": "scenarios.md",
|
|
46
|
+
"ux-flows.md": "flows.md",
|
|
47
|
+
"ux-foundation.md": "foundation.md",
|
|
48
|
+
"ux-screens.md": "screens.md",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def find_ux_dir(arg: str | None) -> Path | None:
|
|
53
|
+
base = Path(arg) if arg else Path.cwd()
|
|
54
|
+
for cand in (base, base / "docs" / "ux", base.parent if base.name else base):
|
|
55
|
+
if any((cand / a).exists() for a in ARTIFACTS) or (cand / "ux-scenarios.md").exists():
|
|
56
|
+
return cand
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def marker(path: Path) -> int | None:
|
|
61
|
+
try:
|
|
62
|
+
head = path.read_text(encoding="utf-8", errors="ignore")[:2000]
|
|
63
|
+
except OSError:
|
|
64
|
+
return None
|
|
65
|
+
m = re.search(r"ux-contract v(\d+)", head)
|
|
66
|
+
return int(m.group(1)) if m else None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def diagnose(ux: Path) -> dict:
|
|
70
|
+
present = {a: (ux / a).exists() for a in ARTIFACTS}
|
|
71
|
+
markers = {a: marker(ux / a) for a in ARTIFACTS if present[a]}
|
|
72
|
+
known = [v for v in markers.values() if v is not None]
|
|
73
|
+
|
|
74
|
+
misnamed = {
|
|
75
|
+
wrong: right
|
|
76
|
+
for wrong, right in RENAMES.items()
|
|
77
|
+
if (ux / wrong).exists() and not (ux / right).exists()
|
|
78
|
+
}
|
|
79
|
+
# Audit reports loose in docs/ux instead of docs/ux/audits/
|
|
80
|
+
loose = sorted(
|
|
81
|
+
p.name for p in ux.glob("*.md")
|
|
82
|
+
if re.search(r"audit", p.name, re.I) and p.name not in ARTIFACTS
|
|
83
|
+
)
|
|
84
|
+
audits = sorted(p.name for p in (ux / "audits").glob("*.md")) if (ux / "audits").is_dir() else []
|
|
85
|
+
|
|
86
|
+
missing_additive = []
|
|
87
|
+
for fname, needle, label in ADDITIVE:
|
|
88
|
+
f = ux / fname
|
|
89
|
+
if f.exists() and needle not in f.read_text(encoding="utf-8", errors="ignore"):
|
|
90
|
+
missing_additive.append(label)
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"present": present,
|
|
94
|
+
"markers": markers,
|
|
95
|
+
"effective": min(known) if known else None,
|
|
96
|
+
"mixed": len(set(known)) > 1,
|
|
97
|
+
"misnamed": misnamed,
|
|
98
|
+
"loose_audits": loose,
|
|
99
|
+
"audits": audits,
|
|
100
|
+
"missing_additive": missing_additive,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def report(ux: Path, d: dict) -> int:
|
|
105
|
+
problems = 0
|
|
106
|
+
print(f"docs/ux: {ux}")
|
|
107
|
+
|
|
108
|
+
eff, markers = d["effective"], d["markers"]
|
|
109
|
+
unmarked = [a for a, v in markers.items() if v is None]
|
|
110
|
+
|
|
111
|
+
if d["mixed"]:
|
|
112
|
+
problems += 1
|
|
113
|
+
detail = ", ".join(f"{a} v{v}" for a, v in sorted(markers.items()) if v)
|
|
114
|
+
print(f" MIXED CONTRACT: {detail}")
|
|
115
|
+
print(" Each artifact was last touched by a different version and nothing reconciled them.")
|
|
116
|
+
elif eff is None:
|
|
117
|
+
problems += 1
|
|
118
|
+
print(" NO CONTRACT MARKER on any artifact — the chain is unmanaged.")
|
|
119
|
+
elif eff < CURRENT:
|
|
120
|
+
problems += 1
|
|
121
|
+
print(f" BEHIND: contract v{eff}, current v{CURRENT}")
|
|
122
|
+
|
|
123
|
+
if unmarked and not d["mixed"]:
|
|
124
|
+
problems += 1
|
|
125
|
+
print(f" UNMARKED: {', '.join(sorted(unmarked))}")
|
|
126
|
+
|
|
127
|
+
lowest = eff if eff is not None else 1
|
|
128
|
+
for v in range(lowest + 1, CURRENT + 1):
|
|
129
|
+
if v in HISTORY:
|
|
130
|
+
print(f" v{v} added — {HISTORY[v]}")
|
|
131
|
+
|
|
132
|
+
for wrong, right in d["misnamed"].items():
|
|
133
|
+
problems += 1
|
|
134
|
+
print(f" MISNAMED: {wrong} -> {right} (the tooling looks for the contract name and finds nothing)")
|
|
135
|
+
|
|
136
|
+
missing_core = [a for a, ok in d["present"].items() if not ok]
|
|
137
|
+
if missing_core:
|
|
138
|
+
print(f" ABSENT: {', '.join(missing_core)}")
|
|
139
|
+
|
|
140
|
+
if d["loose_audits"]:
|
|
141
|
+
problems += 1
|
|
142
|
+
print(f" AUDITS OUTSIDE audits/: {', '.join(d['loose_audits'])}")
|
|
143
|
+
|
|
144
|
+
if d["audits"] and not d["present"].get("scenarios.md"):
|
|
145
|
+
problems += 1
|
|
146
|
+
print(f" {len(d['audits'])} audit report(s) but no scenarios.md — audited against a base that is not there")
|
|
147
|
+
|
|
148
|
+
for label in d["missing_additive"]:
|
|
149
|
+
print(f" optional, absent: {label}")
|
|
150
|
+
|
|
151
|
+
print(" OK — on the current contract" if problems == 0 else f" {problems} problem(s)")
|
|
152
|
+
return problems
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def fix(ux: Path, d: dict) -> list[str]:
|
|
156
|
+
"""Only the changes that cannot be wrong."""
|
|
157
|
+
done = []
|
|
158
|
+
for wrong, right in d["misnamed"].items():
|
|
159
|
+
(ux / wrong).rename(ux / right)
|
|
160
|
+
done.append(f"renamed {wrong} -> {right}")
|
|
161
|
+
if d["loose_audits"]:
|
|
162
|
+
(ux / "audits").mkdir(exist_ok=True)
|
|
163
|
+
for name in d["loose_audits"]:
|
|
164
|
+
(ux / name).rename(ux / "audits" / name)
|
|
165
|
+
done.append(f"moved {name} -> audits/")
|
|
166
|
+
return done
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def main() -> int:
|
|
170
|
+
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
|
171
|
+
ux = find_ux_dir(args[0] if args else None)
|
|
172
|
+
if ux is None:
|
|
173
|
+
print("no UX docs found (docs/ux/). Run /ux to set one up.")
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
d = diagnose(ux)
|
|
177
|
+
if "--brief" in sys.argv:
|
|
178
|
+
eff = d["effective"]
|
|
179
|
+
state = "mixed" if d["mixed"] else (f"v{eff}" if eff else "unmarked")
|
|
180
|
+
flags = []
|
|
181
|
+
if d["misnamed"]:
|
|
182
|
+
flags.append("misnamed")
|
|
183
|
+
if d["loose_audits"]:
|
|
184
|
+
flags.append("loose-audits")
|
|
185
|
+
if d["audits"] and not d["present"].get("scenarios.md"):
|
|
186
|
+
flags.append("audits-without-base")
|
|
187
|
+
print(f"{ux} {state}{' ' + ','.join(flags) if flags else ''}")
|
|
188
|
+
return 0
|
|
189
|
+
|
|
190
|
+
problems = report(ux, d)
|
|
191
|
+
if "--fix" in sys.argv:
|
|
192
|
+
applied = fix(ux, d)
|
|
193
|
+
print("\nApplied:" if applied else "\nNothing to apply automatically.")
|
|
194
|
+
for line in applied:
|
|
195
|
+
print(f" {line}")
|
|
196
|
+
if applied:
|
|
197
|
+
print(" Re-run without --fix to see what is left for a human.")
|
|
198
|
+
elif problems:
|
|
199
|
+
print("\n --fix applies the mechanical subset (renames, moving audit reports).")
|
|
200
|
+
print(" Contract upgrades are content decisions: run /ux-update for those.")
|
|
201
|
+
return 0
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
if __name__ == "__main__":
|
|
205
|
+
sys.exit(main())
|