super-ux 0.27.1 → 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 CHANGED
@@ -1,5 +1,107 @@
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
+
42
+ ## 0.28.0 — 2026-08-05
43
+
44
+ Closes the carry-over ledger the 0.27 audit opened. Twenty-three practices,
45
+ five principles, three optional contract fields, a prototype step, and an
46
+ index over a catalog that had grown past the point of being read whole.
47
+
48
+ ### Added
49
+ - **Motion craft — BP-157..164.** BP-130 said durations and easings should be
50
+ tokens and stopped there. These make the decisions it does not: whether the
51
+ element animates at all (frequency decides — a hundred times a day means
52
+ never), what the durations actually are, why `ease-in` is wrong on
53
+ interface motion, entering from a visible state rather than `scale(0)`,
54
+ anchored surfaces growing from their trigger, asymmetric enter/exit,
55
+ interruptibility for anything retriggerable, and hover gated behind a
56
+ hover-capable pointer.
57
+ - **Perceived quality — BP-165..168.** Why a technically correct interface
58
+ still reads as unfinished: mixed icon families and stroke weights, emoji
59
+ used as structural icons, pressed states that reflow their neighbours, and
60
+ a scrim too weak to separate the layers it exists to separate. None of
61
+ these fail an automated check, which is why they survive audits that only
62
+ measure.
63
+ - **Generated-default tells — BP-169..172.** super-ux is a tool agents build
64
+ interfaces with, so it should know the signature of its own defaults: fake
65
+ product screenshots built from markup, implausible placeholder data, the
66
+ three-equal-cards row that means nobody decided the hierarchy, and the
67
+ cluster (`#000000`, two accents, `100vh`, no max-width) that marks a
68
+ question never asked.
69
+ - **Interface state, platform surfaces and locale — BP-173..179**, from the
70
+ Web Interface Guidelines: state in the URL, undo for reversible actions
71
+ instead of a confirmation nobody reads, unsaved work that survives,
72
+ dark mode covering the surfaces the browser draws, localization as a
73
+ design constraint rather than a translation step, scroll containment and
74
+ touch defaults in overlays, and lists virtualized before they ship long.
75
+ - **Motivation principles — PRN-17..21.** Goal-gradient, Zeigarnik, IKEA,
76
+ endowment, zero-price. Each carries a fourth column naming where it turns
77
+ coercive, and a rule that any of them recorded as `applied` says which side
78
+ of that line it is on. They are motivation mechanisms; the same levers
79
+ aimed at the product's interest are dark patterns.
80
+ - **`Telemetry` on a scenario (optional).** The bridge the chain was missing:
81
+ BP-139, BP-140 and BP-129 all assume events exist, and nothing tied a named
82
+ event to the behavior it measures — so renaming a step silently re-pointed
83
+ a dashboard. `object_action`, snake_case, verb last.
84
+ - **`Kill criteria` on a story (optional).** `dropped` has been a valid
85
+ status since v1 with nothing defining when it applies, so it was only ever
86
+ reached by someone losing interest.
87
+ - **A verdict on audit reports — REFINE / REDESIGN / NEW.** Findings alone
88
+ read as a to-do list, and a surface that should be rebuilt gets patched
89
+ indefinitely, one true finding at a time.
90
+ - **A prototype step in `ux-flows`** for the question documents cannot
91
+ settle, plus *diverge before converging*: two genuinely different shapes
92
+ before picking one, and a line on why the loser lost.
93
+ - **`best-practices-index.md`**, generated by
94
+ `plugins/super-ux/scripts/bp_index.py` — 274 lines of tag → ids over a
95
+ catalog that is now 1400+. The validator fails when it drifts.
96
+
97
+ ### Changed
98
+ - Taxonomy gains `i18n`; source keys gain `[EmilK]` and `[WIG]`.
99
+ - `PRN-01..16` becomes `PRN-01..21` everywhere it is referenced.
100
+
101
+ The contract stays **v4**. All three new fields are optional and additive, on
102
+ the same precedent as 0.26.1 — no existing `docs/ux` file changes shape and
103
+ there is nothing to migrate.
104
+
3
105
  ## 0.27.1 — 2026-08-05
4
106
 
5
107
  Re-cut of 0.27.0 on the correct base. The 0.27.0 tag was pushed from a tree
package/README.md CHANGED
@@ -185,8 +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-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..16, the improvement procedure, anti-patterns |
189
- | [best-practices.md](plugins/super-ux/skills/references/best-practices.md) | Living, tag-indexed catalog of 156 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, visual craft, Figma structure |
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` |
189
+ | [best-practices-index.md](plugins/super-ux/skills/references/best-practices-index.md) | Generated tag index over the catalogtag 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 |
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 |
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 |
190
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 |
191
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 |
192
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
- const lintSrc = path.join(ROOT, 'plugins', 'super-ux', 'scripts', 'ux_lint.py');
93
- const lintDst = path.join(target, 'docs', 'ux', 'lint.py');
94
- if (fs.existsSync(lintSrc)) {
95
- fs.copyFileSync(lintSrc, lintDst);
96
- console.log(`sync: ${lintDst}`);
97
- } else {
98
- console.error(
99
- `warning: linter not found in this package (${lintSrc}); docs/ux/lint.py was not installed.\n` +
100
- ` Get it from https://github.com/${REPO}/blob/main/plugins/super-ux/scripts/ux_lint.py`
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
@@ -18,6 +18,11 @@ covered, what was left out and why, what could not be verified, and what the
18
18
  code cannot answer. An audit reads a slice; absence from the report never
19
19
  means PASS.
20
20
 
21
+ **Verdict (required):** REFINE — fix the findings; REDESIGN — they cluster on
22
+ a structure patching will not converge on, name it; NEW — the chain does not
23
+ describe what was built, start upstream. Findings without a verdict read as a
24
+ to-do list, and the surface that needs rebuilding gets patched forever.
25
+
21
26
  ## Loop
22
27
 
23
28
  1. Read the base (and foundation/flows/screens when they exist); scope =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "super-ux",
3
- "version": "0.27.1",
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())
@@ -39,6 +39,10 @@ report never means PASS. -->
39
39
  - **Could not verify:**
40
40
  - **Open questions:**
41
41
 
42
+ ## Verdict
43
+
44
+ <REFINE | REDESIGN | NEW> — <one line of reasoning, and the scope it covers>
45
+
42
46
  ## Practice compliance (deep audits)
43
47
 
44
48
  | Practice | Verdict | How / why not |
@@ -65,5 +65,6 @@ this section just records the on/off choice and the file location.
65
65
  - **Acceptance criteria:**
66
66
  - Given <precondition>, when <action>, then <observable result>.
67
67
  - **Priority:** must | should | could
68
+ - **Kill criteria:** <metric below threshold by date -> drop | iterate, or omit>
68
69
  - **Status:** proposed
69
70
  -->
@@ -32,6 +32,7 @@ action per step paired with the observable system response:
32
32
  - **UI elements:** <every button, field, link, dialog, toast involved>
33
33
  - **States covered:** <loading | empty | error | success>
34
34
  - **Errors & recovery:** <each failure: what the user sees, how they recover>
35
+ - **Telemetry:** <`object_action` event + params, or omit if none>
35
36
  - **Status:** draft
36
37
  - **Coverage:** none yet
37
38
  -->