super-ux 0.59.3 → 0.61.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,3 +1,42 @@
1
+ ## 0.61.0 (2026-10-18): no em dash in new text
2
+
3
+ Owner decision, 2026-10-18: text an agent writes carries no em dash (U+2014), in
4
+ any language, and no substitute for one. The family hub carries the same rule as
5
+ an always-on line for every host; this release makes product text agree with it.
6
+
7
+ - `ai-tells.md` AT-06 is now a hard ban, not a frequency target. The dash rule has
8
+ two layers: the glyph (no em dash in newly authored text) and the role (the
9
+ rhetorical dash is banned in every spelling; the grammatical Russian dash is
10
+ allowed only where no rewrite reads naturally). Ranges take an en dash.
11
+ - New lint code `B067` (`brand_lint.py`, seeded `docs/brand/lint.py`): an em dash
12
+ joining a range errors; an em dash opening a line where the locale has no
13
+ dialogue dash errors; a lone dash in a locale with a grammatical one warns, so
14
+ the writer decides whether the grammar needs it. Never a second finding beside
15
+ `B062`. Five fixtures, each watched failing with `B067` stubbed out.
16
+ - `copywriting`: the em dash is a Non-negotiable and the one exception to the
17
+ advisory humanization pass; the status-line examples no longer use a dash.
18
+ `brand-voice` seeds the invariant into `voice.md`; the brand hard rule
19
+ (`templates/claude-rule.md` and its embedded copies) and the Cursor rule say so.
20
+ - Contract copies synchronized with `python3 test/sync_references.py`.
21
+
22
+ ## 0.60.0 — a rendered screen is checked in WebKit (Safari) first
23
+
24
+ - New shared contract `references/browser-engine.md`: Safari through
25
+ `safaridriver`, Playwright `webkit` (`--browser webkit`,
26
+ `npx playwright install webkit`), Safari Web Inspector, and
27
+ `open -a Safari <url>` on macOS rather than the system default browser.
28
+ Chromium is a stated fallback for a task that needs Chrome itself (a Chrome-only
29
+ API, Lighthouse, a CDP trace, the chrome-devtools MCP), on Chrome Dev, with a
30
+ temporary profile and every started process closed. A screen checked only in
31
+ Chromium is reported as exactly that.
32
+ - `ux-audit`: a RUNTIME browser check runs in WebKit first and names its engine;
33
+ the live pass walks scenarios in WebKit first; the capture record gains `engine`.
34
+ - `funnel-research.md`: the funnel walk uses Playwright `webkit` or Safari first;
35
+ `chrome-devtools` / `claude-in-chrome` only as a stated fallback.
36
+ - Per-skill copies synchronized (`ux-audit`, `ux-flows`, `ux-foundation`). Discovery
37
+ descriptions unchanged. New regression `test/audit_regressions/safari-first.py`
38
+ with drop mutations; text integrity only, no model eval.
39
+
1
40
  ## 0.59.3 — accountable journeys and explicit copy inputs
2
41
 
3
42
  - Journeys record an Owner, with `unknown` supported. `U086` advises migration
package/README.md CHANGED
@@ -161,10 +161,10 @@ Commands: `/brand` (status → one recommended action), `/brand-init`,
161
161
  python3 docs/brand/lint.py
162
162
  ```
163
163
 
164
- 48 deterministic checks (`B001`..`B073`): banned words, one action under two names, a figure
164
+ 49 deterministic checks (`B001`..`B073`): banned words, one action under two names, a figure
165
165
  with no sourced fact, a field over its limit with the locale coefficient
166
166
  applied, blocked AI crawlers, keyword stuffing, humor on a billing screen,
167
- a rhetorical dash, a title that ends in a full stop, a locale that lags
167
+ a rhetorical dash or an em dash in new text, a title that ends in a full stop, a locale that lags
168
168
  without saying so, a short-video hook the filter calls weak, a caption that
169
169
  spends the feed window or asks twice, an invisible character pasted in. Exit 0 clean **or warnings only**, 1 warnings under `--strict`, 2 any error. That is the policy `docs/ux/lint.py` has always had, and one pack cannot hold two opposite meanings for a warning: this linter returned 1 on warnings alone until 2026-08-20, so 13 of its 39 codes turned a build red while printing `0 error(s), 1 warning(s)`.
170
170
 
@@ -54,9 +54,14 @@ Could this sentence sit unchanged on a competitor's page? Then it says
54
54
  nothing. Rewrite it until it could not.
55
55
 
56
56
  Machine-made tells to strip: tricolons that add nothing, "seamless /
57
- effortless / unlock / elevate / robust", em-dash cadence in every paragraph,
58
- "it's not just X, it's Y", hedges stacked two deep, and a closing sentence
59
- that summarizes what was just said.
57
+ effortless / unlock / elevate / robust", "it's not just X, it's Y", hedges
58
+ stacked two deep, and a closing sentence that summarizes what was just said.
59
+
60
+ **No em dash (U+2014) in text you write, in any language** (owner decision,
61
+ 2026-10-18). A hard ban, not a count: no substitute either (`--`, an en dash
62
+ or a spaced hyphen doing a dash's job). Use a full stop, a comma, a colon or
63
+ brackets, or rewrite. The rare exception is a dash Russian grammar needs where
64
+ no rewrite reads naturally; quotations, code, commands and paths keep theirs.
60
65
 
61
66
  ## Short video
62
67
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "super-ux",
3
- "version": "0.59.3",
3
+ "version": "0.61.0",
4
4
  "scripts": {
5
5
  "test": "python3 test/validate.py && python3 test/brand_lint_test.py && python3 test/display_copy_test.py && python3 test/ux_lint_test.py && python3 docs/ux/lint.py && python3 docs/brand/lint.py && node test/installer_test.js && npm run test:audit",
6
6
  "test:audit": "for t in test/audit_regressions/*.py; do python3 \"$t\" || exit 1; done"
@@ -2037,6 +2037,75 @@ def dash_findings(code: str, path: str, text: str, strict: bool) -> list[Finding
2037
2037
  return findings
2038
2038
 
2039
2039
 
2040
+ # AT-06's hard layer, owner-decided 2026-10-18: newly authored text carries no
2041
+ # em dash (U+2014) in any language. B062 above judges the dash's ROLE; this
2042
+ # judges the glyph B062 leaves alone. It never reports a sentence B062
2043
+ # already reports, so one dash is one finding.
2044
+ EM_DASH = "—"
2045
+ EM_DASH_RANGE_RE = re.compile(rf"\d\s*{EM_DASH}\s*\d")
2046
+
2047
+
2048
+ def em_dash_findings(code: str, path: str, text: str, strict: bool) -> list[Finding]:
2049
+ """The em dash the rhetorical check cannot see, and the Russian dash it allows.
2050
+
2051
+ Three cases, each one the B062 branches pass over in silence:
2052
+
2053
+ - a range written with an em dash, in any language: an error, because a
2054
+ range takes an en dash (U+2013) and a rewrite never costs anything;
2055
+ - an em dash opening a line where the locale has no dialogue dash: an
2056
+ error, because there it is decoration, not the speech convention;
2057
+ - a lone dash, in any spelling, where the locale has a grammatical dash:
2058
+ a warning. The copula (AT-06-E1) is allowed only where no rewrite reads
2059
+ naturally, and only the writer can judge that, so the linter asks and
2060
+ never blocks on grammar it cannot parse.
2061
+ """
2062
+ findings: list[Finding] = []
2063
+ raw = prose_only(text)
2064
+ # Length-preserving like the B062 normaliser, so quotes come from `raw`.
2065
+ cells = TABLE_CELL_DASH_RE.sub(r"\1 \2", raw) # AT-06-E4
2066
+ body = normalise_dash_spelling(raw)
2067
+ if DASH not in body:
2068
+ return findings
2069
+
2070
+ def line_of(sentence: str) -> int:
2071
+ at = body.find(sentence[:40])
2072
+ return body.count("\n", 0, at) + 1 if at >= 0 else 0
2073
+
2074
+ for sentence, cell, original in zip(sentences(body), sentences(cells), sentences(raw)):
2075
+ if DASH not in sentence:
2076
+ continue
2077
+ line = line_of(sentence)
2078
+ if EM_DASH_RANGE_RE.search(cell):
2079
+ findings.append(Finding(
2080
+ code, SEVERITY_ERROR, path, line,
2081
+ f"an em dash joins a range; write the range with an en dash "
2082
+ f'(U+2013) or the word "to": "{_around(sentence, original)}"',
2083
+ ))
2084
+ continue
2085
+ speech = DASH_SPEECH_RE.match(sentence)
2086
+ if strict:
2087
+ # B062 reports every other bare dash in this locale.
2088
+ if speech and cell.lstrip().startswith(EM_DASH):
2089
+ findings.append(Finding(
2090
+ code, SEVERITY_ERROR, path, line,
2091
+ f"an em dash opens the line, and this locale has no dialogue "
2092
+ f'dash; drop it or use a list: "{_around(sentence, original)}"',
2093
+ ))
2094
+ continue
2095
+ if speech: # AT-06-E3
2096
+ continue
2097
+ bare = sentence.count(DASH) - len(DASH_RANGE_RE.findall(sentence))
2098
+ if bare <= 0 or bare >= 2 or DASH_CONJ_RE.search(sentence):
2099
+ continue # a range, or B062's own error
2100
+ findings.append(Finding(
2101
+ code, SEVERITY_WARN, path, line,
2102
+ f"a dash in new text is allowed only where the grammar needs it "
2103
+ f"(AT-06-E1) and no rewrite reads naturally; otherwise use a full "
2104
+ f'stop, a comma, a colon or brackets: "{_around(sentence, original)}"',
2105
+ ))
2106
+ return findings
2107
+
2108
+
2040
2109
  def markdown_title(text: str) -> str:
2041
2110
  """Read common inline heading markup as the displayed label."""
2042
2111
  text = re.sub(r"!?\[([^\]]+)\]\([^)]*\)", r"\1", text)
@@ -2197,10 +2266,11 @@ def check_bot_safety(brand_dir: Path, sources: dict) -> list[Finding]:
2197
2266
 
2198
2267
 
2199
2268
  def check_ai_tells(brand_dir: Path, sources: dict) -> list[Finding]:
2200
- """B060-B063 -- machine-drafting markers, and the one absolute ban.
2269
+ """B060-B063 and B067 -- machine-drafting markers, and the absolute bans.
2201
2270
 
2202
2271
  B060 counts vocabulary markers, B061 bans levity where the user is
2203
- losing something, B062 is AT-06 (the rhetorical dash) and B063 is AT-07
2272
+ losing something, B062 is AT-06 (the rhetorical dash), B067 is AT-06's
2273
+ hard layer (the em dash glyph in new text) and B063 is AT-07
2204
2274
  (a title that ends in a full stop) outside the string registry, which
2205
2275
  B026 already covers. The grades and the reasoning are in
2206
2276
  `references/ai-tells.md`; this file carries only the provable subset.
@@ -2231,12 +2301,15 @@ def check_ai_tells(brand_dir: Path, sources: dict) -> list[Finding]:
2231
2301
  text = f"{fields.get('title', '')}\n{body}"
2232
2302
  strict = not grammatical_dash_language(text, primary)
2233
2303
  findings.extend(dash_findings("B062", path, body, strict))
2304
+ # B067 -- the em dash glyph B062 does not judge (AT-06, hard layer).
2305
+ findings.extend(em_dash_findings("B067", path, body, strict))
2234
2306
 
2235
2307
  for row in registry(brand_dir):
2236
2308
  if row["kind"] == "layout":
2237
2309
  continue
2238
2310
  strict = not grammatical_dash_language(row["text"], primary)
2239
2311
  findings.extend(dash_findings("B062", row["location"], row["text"], strict))
2312
+ findings.extend(em_dash_findings("B067", row["location"], row["text"], strict))
2240
2313
 
2241
2314
  # B063 -- AT-07 outside the registry: document titles and the headings
2242
2315
  # inside them. B026 owns the same rule for `strings.md`, and the two are
@@ -40,6 +40,8 @@ Promise: <one line, checkable against a row in facts.md>
40
40
  What survives translation. Breaking one of these is a finding on any surface,
41
41
  in any locale.
42
42
 
43
+ - No em dash (U+2014) in new text, in any language; the grammatical Russian
44
+ dash only where no rewrite reads naturally (owner decision, 2026-10-18)
43
45
  - <e.g. never hedges>
44
46
  - <e.g. never claims a number that is not in facts.md>
45
47
 
@@ -48,14 +48,16 @@
48
48
  notification and accessible name. Search `strings.md` before naming one.
49
49
  - **No humor, exclamation marks or emoji** on error, destructive confirm,
50
50
  billing or paywall surfaces, in any voice.
51
- - **No rhetorical dash, and no full stop after a title.** A dash standing in
52
- for a full stop, a comma or a colon is the loudest machine-drafting marker
53
- the pack has, and a heading, button, menu item or page title is a name
54
- rather than a statement. The dash a language requires stays: the Russian
55
- copula, numeric ranges, direct speech. Choose the replacement from the
56
- meaning, because a comma, a colon and a full stop state three different
57
- relationships and find-and-replace picks the wrong one. `B062` and `B063`
58
- catch what a machine can prove; the rest is in the skill's `ai-tells.md`.
51
+ - **No em dash in new text, no rhetorical dash, and no full stop after a
52
+ title.** Text you write carries no em dash (U+2014) in any language (owner
53
+ decision, 2026-10-18), and no dash in any spelling stands in for a full
54
+ stop, a comma or a colon. A heading, button, menu item or page title is a
55
+ name rather than a statement. The dash Russian grammar needs (the copula,
56
+ direct speech) stays only where no rewrite reads naturally; a range takes
57
+ an en dash. Choose the replacement from the meaning, because a comma, a
58
+ colon and a full stop state three different relationships and
59
+ find-and-replace picks the wrong one. `B062`, `B063` and `B067` catch what
60
+ a machine can prove; the rest is in the skill's `ai-tells.md`.
59
61
  - Run `python3 docs/brand/lint.py` after any text change and before calling
60
62
  work done. It must exit 0 — an error blocks and a warning is advice, which
61
63
  is the same policy the UX linter has; `--strict` makes warnings block too.