super-ux 0.43.0 → 0.44.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,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.44.0 — 2026-08-17
4
+
5
+ **Three lint codes for the two layers whose claims nothing could check.** `U055`, `U056` and
6
+ `U057`; the linter goes 43 → **54** fixtures, each code carrying the defect it must catch and
7
+ the shapes it must **not**.
8
+
9
+ ### A `Coverage` claim is a claim about code (`U055`, `U056`) — closes #6
10
+
11
+ Measured in a real project: five screens carried `partial` in the index while their entries
12
+ named no file, and one said `none — no per-account memberships route exists` about a route a
13
+ task had built the day before. **Two fields of one record contradicted each other for a day,
14
+ and neither was checked against the other.**
15
+
16
+ - `U055` (warn) — a `Coverage` value other than `none` that names no file. A claim about code
17
+ citing no code cannot be checked by a script *or* by a reader, who has nowhere to go to
18
+ disagree.
19
+ - `U056` (error) — a cited path that does not exist. A stale citation is the same defect with
20
+ the means to notice.
21
+
22
+ **The path pattern is deliberately narrow and that was the design decision, not a detail.** It
23
+ requires a slash **and** an extension, so `src/routes/x.tsx:12` matches while *"partial —
24
+ client/server split"* and *"the route is built"* do not. A wider pattern was tried first and
25
+ flagged three correct prose entries — the false positive that gets a rule switched off inside a
26
+ day, taking the real check with it.
27
+
28
+ ### A flow's verdict must be measurable, not inherited (`U057`) — closes #7
29
+
30
+ The chain is foundation → flows → screens → scenarios, and audits in practice attach to the two
31
+ **ends**. Flows sit between and are the only layer with no artefact of their own: a flow is a
32
+ path across screens, so the cheap thing is to derive its verdict from theirs — and a derived
33
+ verdict presented as a measured one is how one project's `flows.md` carried **no code verdict
34
+ for 42 flows across three weeks**, its header delegating to an audit that had itself derived
35
+ them, and a later scenario walk refuting that audit on every clause without touching flows.
36
+
37
+ `U057` does not verdict a flow. It reports the flows for which **no verdict can be measured at
38
+ all** — the state that was invisible. A flow naming no screen stays `U010`'s subject, not this
39
+ one.
40
+
41
+ Run against this repository's own chain: clean.
42
+
3
43
  ## 0.43.0 — 2026-08-17
4
44
 
5
45
  **`ux-flows` has told agents to sweep real products before inventing a flow since 0.35.0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "super-ux",
3
- "version": "0.43.0",
3
+ "version": "0.44.0",
4
4
  "scripts": {
5
5
  "test": "python3 test/validate.py && python3 test/brand_lint_test.py && python3 test/ux_lint_test.py && python3 docs/ux/lint.py && python3 docs/brand/lint.py"
6
6
  },
@@ -102,6 +102,14 @@ def entry_blocks(text: str, prefix: str) -> dict[str, str]:
102
102
  return out
103
103
 
104
104
 
105
+ # A cited path, deliberately narrow: it must carry a slash AND an extension, so
106
+ # `src/routes/x.tsx:12` matches and prose like "partial — client/server split"
107
+ # or "the route is built" does not. Widening this to any slash-bearing token was
108
+ # tried and flagged three correct prose entries, which is the false positive that
109
+ # gets a rule switched off.
110
+ CITED_PATH = re.compile(r"\b([\w.-]+(?:/[\w.-]+)+\.[A-Za-z][\w]{0,4}(?::\d+)?)")
111
+
112
+
105
113
  def screen_blocks(text: str) -> dict[str, str]:
106
114
  """Map SCR-id -> its section body."""
107
115
  return entry_blocks(text, "SCR")
@@ -289,6 +297,33 @@ def main() -> int:
289
297
  for orphan in sorted(screen_ids - used):
290
298
  warn(f"[U011] screens.md: {orphan} is used by no flow (orphan)")
291
299
 
300
+ # --- A flow's verdict must be measurable, not inherited -------------
301
+ #
302
+ # The layer order is foundation → flows → screens → scenarios, and audits
303
+ # in practice attach to the two ENDS. Flows sit between and are the only
304
+ # layer with no artefact of their own to measure: a flow is a path across
305
+ # screens, so the cheap thing is to derive its verdict from theirs — and a
306
+ # derived verdict presented as a measured one is what let one project's
307
+ # `flows.md` carry no code verdict for 42 flows across three weeks, its
308
+ # header delegating to an audit that had itself derived them.
309
+ #
310
+ # This does not verdict a flow. It reports the flows for which no verdict
311
+ # can be measured at all, which is the state that was invisible.
312
+ for fid, fbody in sorted(entry_blocks(flows, "FLW").items()):
313
+ mine = [b for b in screen_blocks(screens).values()
314
+ if re.search(r"\*\*Used by:\*\*[^\n]*\b" + re.escape(fid) + r"\b", b)]
315
+ if not mine:
316
+ continue # a flow naming no screen is U010's subject, not this one
317
+ cited_anywhere = False
318
+ for b in mine:
319
+ m = re.search(r"\*\*Coverage:\*\*\s*(.+)", b)
320
+ if m and CITED_PATH.search(m.group(1)):
321
+ cited_anywhere = True
322
+ break
323
+ if not cited_anywhere:
324
+ warn(f"[U057] flows.md: {fid} has no screen naming an implementing "
325
+ f"file, so its coverage cannot be measured — only inherited")
326
+
292
327
  # --- Scenario traces resolve ---
293
328
  if ids(scenarios, "SCN"):
294
329
  story_ids = set(ids(foundation, "ST"))
@@ -317,6 +352,9 @@ def main() -> int:
317
352
  # --- Screen-level: Figma frames, coverage, drift status ---
318
353
  if has_screens:
319
354
  fig = figma_enabled(foundation)
355
+ # Cited paths are project-relative, so they resolve against the tree the
356
+ # ux directory sits in — the same derivation `check_links` already uses.
357
+ screens_root = ux.parent.parent if ux.name == "ux" else ux.parent
320
358
  for sid, body in screen_blocks(screens).items():
321
359
  status_m = re.search(r"\*\*Status:\*\*\s*(designed|built|drifted|retired)", body)
322
360
  status = status_m.group(1) if status_m else None
@@ -335,6 +373,22 @@ def main() -> int:
335
373
  cov = cov_m.group(1).strip() if cov_m else ""
336
374
  if status == "built" and (not cov or cov.lower().startswith("none")):
337
375
  warn(f"[U021] screens.md: {sid} is 'built' but has no Coverage")
376
+ # A Coverage value other than `none` is a CLAIM ABOUT CODE, and a claim
377
+ # about code that names no code is unfalsifiable — not by a script and
378
+ # not by a reader, who has nowhere to go to disagree. Measured in a real
379
+ # project: five screens carried `partial` in the index while their
380
+ # entries named no file, and one of them said `none — no route exists`
381
+ # about a route a task had built the day before. Two fields of one
382
+ # record contradicting each other, neither checked against the other.
383
+ if cov and not cov.lower().startswith("none"):
384
+ cited = CITED_PATH.findall(cov)
385
+ if not cited:
386
+ warn(f"[U055] screens.md: {sid} claims Coverage '{cov}' and names no file")
387
+ for rel in cited:
388
+ # The line suffix is part of a citation, not of the path.
389
+ target = (screens_root / rel.split(":", 1)[0])
390
+ if not target.exists():
391
+ err(f"[U056] screens.md: {sid} cites '{rel}', which does not exist")
338
392
 
339
393
  check_vision(ux, vision)
340
394
  check_web_surface(screens, flows)