super-ux 0.38.2 → 0.40.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,160 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.40.0 — 2026-08-14
4
+
5
+ A 42-page practitioner guide on building web2app funnels was read against the
6
+ catalog, and most of what it teaches was already here: BP-001 on adapting rather
7
+ than copying, BP-005 on loading screens that sell, BP-010 and BP-029 on echoing
8
+ the stated goal, BP-116..123 on the surfaces, BP-124..129 on the web2app chain.
9
+ What was missing had one thing in common. **Every gap was something that fails
10
+ without changing what the funnel looks like.**
11
+
12
+ Five practices, `BP-211..215`, in a section named for that property:
13
+
14
+ - **BP-211 — personalize the wording, never the price.** The catalog said to
15
+ branch the offer on the quiz answer and never said where the branch stops.
16
+ Two rules: the product and its price are identical across every branch, and
17
+ the branch has a default, because the person who skips the question currently
18
+ meets an empty offer at the moment of highest intent.
19
+ - **BP-212 — publicly addressable before it takes money, instrumented before it
20
+ takes traffic.** Both orderings are forced rather than tidy. A provider
21
+ confirms a charge by calling a public address, so the whole post-payment path
22
+ is untestable on a laptop; and traffic bought before instrumentation cannot be
23
+ read afterwards, because the sessions are spent and the losing step was never
24
+ recorded.
25
+ - **BP-213 — a collected answer carries three decisions no screen shows:** who
26
+ may read the row, when the person was told, how they get it deleted. `[GDPR]`
27
+ Art. 13 fixes the timing at *the moment the data is obtained*, which is why
28
+ this is a design-time decision and not a launch-week chore, and Art. 17 is why
29
+ a funnel with no deletion route has promised something it cannot do.
30
+ - **BP-214 — the legal text is sourced, never generated.** A policy is a
31
+ statement about your own processing, so generated prose is a fabricated claim
32
+ about it: BP-194's failure with a regulator for a reader.
33
+ - **BP-215 — access after payment is a ladder, and the link carries a token
34
+ rather than a person.** Each rung above the first adds a service that fails
35
+ independently of the funnel, so the rung below stays reachable; and a URL is a
36
+ bearer credential, so what is encoded in it is readable by everyone it is
37
+ forwarded to.
38
+
39
+ **BP-118 gained the unit web funnels actually anchor on.** It stopped at the
40
+ monthly equivalent of an annual plan. The per-day figure is the same mechanism
41
+ one step further, and it ships beside the billed amount rather than instead of
42
+ it, because a price the buyer is never charged is the shape a dark pattern takes
43
+ here.
44
+
45
+ **`funnel-research.md` is new**, and it is a method rather than a practice:
46
+ `FR-01..FR-07`, from finding the funnels running in a category to landing each
47
+ finding in the chain. It leads with the constraint that makes it a method at
48
+ all — you cannot see anyone's revenue, so every signal is spend, and spend is
49
+ somebody else's judgement you cannot inspect. Its last section names the step
50
+ chain a corpus keeps producing and the practice specifying each step; its final
51
+ section is what the method **cannot** do. Carried by `ux-foundation` and
52
+ `ux-flows`, and by neither of the other five, because a link in a skill is a
53
+ shipping instruction.
54
+
55
+ **Two new gates, because a numbered set with nothing counting it is a promise.**
56
+
57
+ - `validate_reference_contents` — every `## Contents` anchor in a reference
58
+ resolves to a heading in that file. The Contents list is the one part of the
59
+ shelf that goes stale by somebody else's edit: rename a heading and the entry
60
+ above it still looks right. 150 checks over 21 files, and it recorded the
61
+ slug rule it needed to get right, because a checker that collapses the double
62
+ hyphen an em dash leaves behind reports 22 failures on a clean shelf.
63
+ - The `FR-01..NN` range check, in the form `PRN-01..NN` already had, so a step
64
+ added without updating its carriers goes red in all three of them.
65
+
66
+ Both were watched failing against planted defects, each isolated so only the
67
+ branch under test could fire it. Disarming the anchor loop drops the count from
68
+ 3500 to 3354, which the floor ratchet refuses on its own.
69
+
70
+ `docs/brand/facts.md` was recomputed rather than edited: `B030` went red on
71
+ `215` in the README before this run had touched the table, which is the sequence
72
+ the check exists for.
73
+
74
+ ## 0.39.0 — 2026-08-14
75
+
76
+ The em-dash reflex has been in `ai-tells.md` since the verbal identity layer
77
+ shipped, graded S2 and worded "one or two in a piece is normal". The linter
78
+ never looked for it: `S1_MARKERS` held twelve string literals, none of them a
79
+ dash. A title ending in a full stop was caught only inside the string
80
+ registry, by `B026`, so every heading and page title outside it went
81
+ unchecked. Both are now checks.
82
+
83
+ **The rule is a distinction, not a ban.** A dash standing in for a full stop,
84
+ a comma or a colon is out. A dash the language requires stays, because a
85
+ global ban makes Russian ungrammatical on its first line: the copula
86
+ («Москва — столица»), numeric ranges and direct speech are orthography.
87
+
88
+ - **`B062`** errors on what can be established without parsing grammar: a
89
+ dash before a coordinating conjunction, which is always a comma's job and
90
+ which a copula dash never takes, and paired dashes bracketing an aside
91
+ inside one sentence. In a locale with no grammatical dash it errors on
92
+ every dash that is not a range. Where a locale has one, it reports and
93
+ leaves the judgement to the doctrine rather than claiming a distinction it
94
+ cannot measure. Every finding quotes the dash in context with its line,
95
+ because forty findings reading "a dash stands in for a full stop" is a
96
+ report nobody can act on.
97
+ - **`B063`** carries `B026`'s rule to document titles and headings, allowing
98
+ what is not the defect: a question mark, an ellipsis, a trailing
99
+ abbreviation, and a title that is genuinely several sentences.
100
+
101
+ `ai-tells.md` was rewritten around them. Every marker now carries an id
102
+ (`AT-01`..`AT-15`), so coverage over the set is computable rather than
103
+ asserted, and four markers were added: `AT-07` the full-stopped title,
104
+ `AT-11` "not just X, but Y", `AT-12` the bold reflex, `AT-13` the colon hook.
105
+ The dash rule gets its own section with a replacement table, because a comma,
106
+ a colon and a full stop state three different relationships and
107
+ find-and-replace picks the wrong one.
108
+
109
+ The rule also enters the **Brand voice hard rule**, so it reads in every
110
+ session of every project that installs it rather than only when an agent
111
+ opens the reference.
112
+
113
+ **Seven planted defects, each turning exactly one fixture red.** Two found
114
+ holes in the fixtures rather than in the code: an English conjunction case
115
+ stayed green when the conjunction branch was deleted, because the strict
116
+ branch produced the same code by another path and a set comparison cannot
117
+ tell them apart; and the fenced-block case was masked by the inline-code
118
+ stripper. Both were rewritten to isolate their branch, and both plants then
119
+ landed.
120
+
121
+ **Dogfood went from red to clean.** `docs/brand/lint.py` had been failing on
122
+ `B030` before any of this, because `facts.md` was itself three counts stale:
123
+ 206 practices against 210, 33 lint checks against 37, 3107 validator checks
124
+ against 3240. Every row is recomputed and re-dated, and the file's preamble
125
+ now records that naming the command is not the same as running it.
126
+
127
+ The doctrine prose lost **144 of its 158 dashes** across the eleven
128
+ copywriting and brand-voice references; the fourteen that remain are quoted
129
+ examples and table cells standing for "no value". The README and the
130
+ installer's interface strings were swept with them, both being declared
131
+ public surfaces in this project's own pack.
132
+
133
+ **This project's own chain and brand pack now run in CI**, which they never
134
+ did. `validate.yml` ran the validator and the two fixture suites and neither
135
+ `docs/ux/lint.py` nor `docs/brand/lint.py`, which is how the pack sat red with
136
+ nothing reporting it. The hard rule this repository installs into other
137
+ projects has always required exactly that wiring.
138
+
139
+ Adding it found a second defect within one run. `B005` asks whether
140
+ `foundation.md` changed after the voice was last calibrated, and answered from
141
+ the file's **mtime**, which in a fresh clone is the checkout time: every file
142
+ reads as "changed today", so the check fired on every CI run about a file
143
+ nobody had touched. It now answers from `git log`, falling back to mtime only
144
+ outside a repository, and the checkout uses full history so the commit is there
145
+ to read. An eighth plant covers it.
146
+
147
+ And a third, found by the second. `docs/brand/lint.py` is a **copy** of
148
+ `brand_lint.py` seeded by `/brand-init`, and it was 227 lines behind: the pack
149
+ was being linted by a file that had neither `B062` nor `B063` in it.
150
+ `validate_seeded_scripts` verified that a command *instructs* the copy, never
151
+ that the copy is current, so it compares bytes now and a planted two-line
152
+ append turns it red.
153
+
154
+ Gates, each run alone: `validate.py` 3252, `brand_lint_test.py` 62,
155
+ `ux_lint_test.py` 43, `docs/ux/lint.py` and `docs/brand/lint.py` clean.
156
+ Floors raised to match.
157
+
3
158
  ## 0.38.2 — 2026-08-14
4
159
 
5
160
  A red `validate` could not stop a publish anywhere in this family, and one member
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![CI](https://github.com/ssheleg/super-ux/actions/workflows/validate.yml/badge.svg)](https://github.com/ssheleg/super-ux/actions/workflows/validate.yml)
5
5
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
6
6
 
7
- **Scenario-driven UI development for AI agents** Claude Code, Cursor, and
7
+ **Scenario-driven UI development for AI agents.** Claude Code, Cursor, and
8
8
  70+ other agents.
9
9
 
10
10
  Coding agents build bad interfaces for one reason: they write UI without a
@@ -32,7 +32,7 @@ flowchart LR
32
32
 
33
33
  Every layer traces to the one above it. New product? Build it forward. Existing
34
34
  codebase? The same artifacts get filled in backwards from the code, tagged
35
- `inferred` until you confirm them the gap between "is" and "should" becomes
35
+ `inferred` until you confirm them, so the gap between "is" and "should" becomes
36
36
  your improvement backlog.
37
37
 
38
38
  ## What you get
@@ -41,14 +41,14 @@ your improvement backlog.
41
41
  job it does once; every later prompt inherits that instead of re-deriving it
42
42
  from the diff.
43
43
  - **Scenarios become acceptance criteria.** "Make it nicer" can no longer mean
44
- "silently change the error handling" a file says what the error handling
44
+ "silently change the error handling": a file says what the error handling
45
45
  does, and the audit checks the code against it.
46
46
  - **Drift gets caught, deterministically.** A linter fails on missing Figma
47
47
  frames, broken traces, orphan screens, and index desync; audits report what
48
48
  no longer matches with `file:line` evidence. That is the review pass you'd
49
49
  otherwise never run.
50
50
  - **Designer artifacts without being a designer.** Personas, jobs to be done,
51
- journeys, flows, screen states, wireframes, Figma frames produced in your
51
+ journeys, flows, screen states, wireframes, Figma frames, produced in your
52
52
  repo, in the vocabulary a design review actually uses.
53
53
 
54
54
  ## Quick start
@@ -62,7 +62,7 @@ your improvement backlog.
62
62
 
63
63
  Then in your project, run `/ux` and answer in plain words. First run installs
64
64
  the hard rule, seeds `docs/ux/`, and builds the chain; every later run reports
65
- status and recommends one next action. You never pick a skill or a layer
65
+ status and recommends one next action. You never pick a skill or a layer;
66
66
  routing is the agent's job.
67
67
 
68
68
  ### Cursor
@@ -86,7 +86,7 @@ npx skills add ssheleg/super-ux --skill ux-audit # one skill
86
86
 
87
87
  [vercel-labs/skills](https://github.com/vercel-labs/skills) discovers the
88
88
  skills through this repo's marketplace manifest and installs them for Claude
89
- Code, Cursor, Codex, OpenCode and others. This channel ships the skills only
89
+ Code, Cursor, Codex, OpenCode and others. This channel ships the skills only;
90
90
  the `/ux` commands come with the plugin, the always-on hard rule with the
91
91
  Cursor install.
92
92
 
@@ -98,11 +98,11 @@ npx super-ux
98
98
 
99
99
  Multi-select menu (space toggles, `a` selects everything, enter installs):
100
100
  skills for any of 70+ agents, Cursor rules into a project, and the Claude Code
101
- plugin user-globally any combination in one run. Also works straight from
101
+ plugin user-globally, in any combination in one run. Also works straight from
102
102
  GitHub: `npx github:ssheleg/super-ux --cursor <dir>`, or clone and run
103
103
  `./install.sh --cursor <dir>`.
104
104
 
105
- ## The brand layer how the product speaks
105
+ ## The brand layer: how the product speaks
106
106
 
107
107
  `docs/ux/` decides what the product does. **`docs/brand/`** decides how it
108
108
  speaks, under `brand-contract v1`: one voice, many registers, and a linter
@@ -112,14 +112,14 @@ that makes copy drift as findable as chain drift.
112
112
  |---|---|
113
113
  | `voice.md` | the pack, five fixed axes, narrative, invariants, locales |
114
114
  | `terminology.md` | our words, banned words, entity and tier names |
115
- | `facts.md` | canonical figures the only source of a number in public copy |
115
+ | `facts.md` | canonical figures, the only source of a number in public copy |
116
116
  | `channels.md` | one record per surface: register deltas, limits, bans |
117
117
  | `strings.md` | the interface string registry → `file:line` → scenario |
118
118
  | `locales/<code>.md` | address form, length coefficient, dead idioms, keywords |
119
119
 
120
120
  Two skills: **`brand-voice`** defines and holds the identity (six shipped
121
121
  voice packs, each declaring the degeneration it collapses into when overdone);
122
- **`copywriting`** writes in it and never writes *to* it a missing term or an
122
+ **`copywriting`** writes in it and never writes *to* it. A missing term or an
123
123
  unsourced number is reported, never invented.
124
124
 
125
125
  Commands: `/brand` (status → one recommended action), `/brand-init`,
@@ -129,10 +129,11 @@ Commands: `/brand` (status → one recommended action), `/brand-init`,
129
129
  python3 docs/brand/lint.py
130
130
  ```
131
131
 
132
- 35 deterministic checks (`B001`..`B073`) banned words, one action under two names, a figure
132
+ 37 deterministic checks (`B001`..`B073`): banned words, one action under two names, a figure
133
133
  with no sourced fact, a field over its limit with the locale coefficient
134
134
  applied, blocked AI crawlers, keyword stuffing, humor on a billing screen,
135
- a locale that lags without saying so. Exit 0 clean, 1 warnings, 2 errors.
135
+ a rhetorical dash, a title that ends in a full stop, a locale that lags
136
+ without saying so. Exit 0 clean, 1 warnings, 2 errors.
136
137
 
137
138
  Clean means *checkable*, not *good*: tone drift, unproven claims and a voice
138
139
  that has overshot its own failure mode are judged by `/ux-audit copy`.
@@ -146,39 +147,39 @@ Installed into your project's `CLAUDE.md` (and as the always-on Cursor rule):
146
147
  traces to.
147
148
  - Any change touching user-facing behavior or interface updates **in the same
148
149
  change**: scenarios, affected flows, the affected screens in
149
- `docs/ux/screens.md`, and when Figma is on the frames plus their links.
150
+ `docs/ux/screens.md`, and, when Figma is on, the frames plus their links.
150
151
  Code that diverges from a screen's record, or a stale Figma link, is drift
151
152
  the audit flags.
152
153
  - Any new feature or project **starts** with the chain: which job, which
153
- journey stage, which story then flows, screens, and scenarios, validated
154
+ journey stage, which story, then flows, screens, and scenarios, validated
154
155
  against the existing base and approved.
155
- - **Do not write interface code until that workflow is done** chain designed
156
+ - **Do not write interface code until that workflow is done.** Chain designed
156
157
  and approved, and (Figma on, the default) the UI mocked up with every screen
157
158
  linked to its frame. Building UI before this is the mistake super-ux exists
158
159
  to prevent.
159
160
  - One **style pack** is the visual identity for the whole product, recorded in
160
161
  `docs/ux/screens.md` → Design system. Inventing a palette, type pairing, or
161
162
  motion per screen is drift too.
162
- - Run `python3 docs/ux/lint.py` after any UX change and in CI it must pass.
163
+ - Run `python3 docs/ux/lint.py` after any UX change and in CI. It must pass.
163
164
 
164
165
  ## Typical cycle
165
166
 
166
- 1. **`/ux`** first run sets everything up: foundation first (greenfield:
167
+ 1. **`/ux`** sets everything up on the first run: foundation first (greenfield:
167
168
  an interview about personas, jobs, journeys; existing code:
168
169
  reverse-engineering them), then flows, screens, and scenarios derived from
169
170
  the stories with full traceability.
170
171
  2. **Work normally.** Every user-facing change updates the chain in the same
171
- change the always-on rule catches it, `/ux-update` gives manual control.
172
+ change; the always-on rule catches it, and `/ux-update` gives manual control.
172
173
  New feature ideas get validated against the chain first: which job, which
173
174
  journey stage, which story. An idea serving no job is challenged, not
174
175
  silently built.
175
- 3. **`/ux-audit`** batched verification of code against every scenario plus
176
+ 3. **`/ux-audit`** is batched verification of code against every scenario plus
176
177
  its story's acceptance criteria. `deep` adds heuristic, practice, and chain
177
178
  coverage passes; `coverage` audits the chain itself. Reports land in
178
179
  `docs/ux/audits/YYYY-MM-DD.md`.
179
180
  4. **Fix plan.** Findings become `docs/ux/plans/…`: the target interface per
180
181
  screen plus a traced CREATE/MODIFY/DELETE table, prioritized by Frequency ×
181
- Severity × Solvability written to be executable without the conversation
182
+ Severity × Solvability, written to be executable without the conversation
182
183
  that produced it. Build, then re-audit.
183
184
 
184
185
  ## Companions (recommended, never required)
@@ -189,52 +190,53 @@ fine without either.
189
190
 
190
191
  | When | Companion | What it adds |
191
192
  |---|---|---|
192
- | At VISUALIZE / BUILD a frame or a screen is about to be drawn | **[sheleg-design](https://github.com/ssheleg/sheleg-design-skill)** | The look: one locked style pack (palette, type, texture, motion tokens, bans) with ready token CSS `workbench` for product UI, dashboards and tools; `instrument-console`; `editorial-luxury`; or a new pack on its contract. Plus the motion methodology for cinematic scroll-driven landings. The pack is recorded in `screens.md`; its tokens become the Figma variables *and* the code tokens. `npx sheleg-design-skill` |
193
+ | At VISUALIZE / BUILD, a frame or a screen is about to be drawn | **[sheleg-design](https://github.com/ssheleg/sheleg-design-skill)** | The look: one locked style pack (palette, type, texture, motion tokens, bans) with ready token CSS: `workbench` for product UI, dashboards and tools; `instrument-console`; `editorial-luxury`; or a new pack on its contract. Plus the motion methodology for cinematic scroll-driven landings. The pack is recorded in `screens.md`; its tokens become the Figma variables *and* the code tokens. `npx sheleg-design-skill` |
193
194
  | After an audit or an Improve pass produced a UX plan | **[task-pipeline](https://github.com/ssheleg/task-pipeline)** | Executes the plan end-to-end through gated stages: spec → plan → subagent build → tests → deploy → docs. `/task-pipeline docs/ux/plans/<file>` |
194
195
 
195
196
  The boundary that keeps them from fighting: BP-079..090 and BP-130..135 are
196
197
  craft **floors** (contrast, line length, tap targets, spacing rhythm, a motion
197
198
  token scale, reduced motion, the narrow viewport) and always win on safety;
198
199
  the style pack owns **identity** and wins on look. Whether a trend is adopted
199
- at all its mechanism, its cost, its review date is BP-145/BP-146. Both decisions land in the
200
+ at all (its mechanism, its cost, its review date) is BP-145/BP-146. Both decisions land in the
200
201
  compliance table. Full protocol:
201
202
  [visual-identity.md](plugins/super-ux/skills/references/visual-identity.md).
202
203
 
203
204
  ## What's inside
204
205
 
205
206
  Seven skills, one entry point, and a set of contracts they all obey. Every
206
- one of them is reachable from `/ux` a skill the entry point cannot route
207
+ one of them is reachable from `/ux`. A skill the entry point cannot route
207
208
  to is a skill nobody runs.
208
209
 
209
210
  | Piece | Purpose |
210
211
  |---|---|
211
- | skill `vision` | What the product **is** (`docs/ux/vision.md`) the layer above the chain, never to be confused with `scenarios.md`, which says what it **does**: essence, core idea, system behaviour, the user's role, principles with a rejected side, the **anti-vision**, horizon, one sentence, and an alignment test later features are checked against. Installs that check into the project's own instruction file |
212
+ | skill `vision` | What the product **is** (`docs/ux/vision.md`), the layer above the chain, never to be confused with `scenarios.md`, which says what it **does**: essence, core idea, system behaviour, the user's role, principles with a rejected side, the **anti-vision**, horizon, one sentence, and an alignment test later features are checked against. Installs that check into the project's own instruction file |
212
213
  | skill `ux-foundation` | The WHY layer (`docs/ux/foundation.md`): personas, jobs to be done with forces, customer journey maps, user stories with Given/When/Then acceptance criteria, the monetization model |
213
- | skill `ux-flows` | The HOW layer + the UI map: `docs/ux/flows.md` (task analysis, mermaid flows referencing screens by ID) and `docs/ux/screens.md` every screen and state with its Figma frame, wireframe, code coverage, scenarios and resources. Also heuristic evaluation and traced redesign proposals |
214
+ | skill `ux-flows` | The HOW layer + the UI map: `docs/ux/flows.md` (task analysis, mermaid flows referencing screens by ID) and `docs/ux/screens.md`, holding every screen and state with its Figma frame, wireframe, code coverage, scenarios and resources. Also heuristic evaluation and traced redesign proposals |
214
215
  | skill `ux-scenarios` | `docs/ux/scenarios.md`: use-case scenarios (action → observable response, alt and error paths) covering every flow node and edge, `Traces:` to stories and flows, validated for conflicts, coverage and traceability |
215
216
  | skill `ux-audit` | Batched audit with full context: code vs every scenario plus its story's acceptance criteria; verdicts PASS / PARTIAL / FAIL / BLOCKED with `file:line` evidence; depths `quick` / `standard` / `deep`; a `coverage` scope that audits the chain itself |
216
- | skill `brand-voice` | `docs/brand/`: the pack and its five axes, the words the product owns and bans, canonical facts, the per-surface register, locales six shipped voice packs, each declaring the degeneration it collapses into when overdone |
217
+ | skill `brand-voice` | `docs/brand/`: the pack and its five axes, the words the product owns and bans, canonical facts, the per-surface register, locales, plus six shipped voice packs, each declaring the degeneration it collapses into when overdone |
217
218
  | skill `copywriting` | Writes in that voice and never writes *to* it: interface strings, errors, empty states, landing and pricing pages, posts, changelogs, store listings, ads, lifecycle email. A missing term or an unsourced number is reported, never invented |
218
219
  | `/ux` | **The one command**: sets up whatever is missing, reports status across every layer, then offers only the applicable actions with one marked recommended. Idempotent |
219
220
  | `/vision` `/ux-init` `/ux-foundation` `/ux-flows` `/ux-update` `/ux-audit` `/ux-rule` `/ux-lint` `/ux-doctor` · `/brand` `/brand-init` `/brand-update` `/brand-lint` `/copy` | Direct controls for when you know exactly what you want; `/ux-rule` installs both hard rules and seeds `lint.py` + `doctor.py`; `/brand-init` seeds `docs/brand/` and its linter |
220
- | `docs/ux/lint.py` + `/ux-lint` | The deterministic half: missing Figma frames, unresolved SCR/story traces, orphans, built screens without coverage, index desync, ID gaps, broken links. Stdlib-only, exit 1 on problems wire it into CI so drift can't merge |
221
+ | `docs/ux/lint.py` + `/ux-lint` | The deterministic half: missing Figma frames, unresolved SCR/story traces, orphans, built screens without coverage, index desync, ID gaps, broken links. Stdlib-only, exit 1 on problems, so wire it into CI and drift can't merge |
221
222
  | `cursor/rules/*.mdc` | The same methodology for Cursor: one always-on hard rule + seven agent-requested rules (vision, foundation, flows, scenarios, audit, brand voice, copywriting) |
222
- | `templates/` | Seeds for `docs/ux/`: the vision skeleton, foundation, flows, screens, scenario base, the folder README, and the audit-report skeleton. Both hard-rule snippets live here as their single source `claude-rule.md` (scenario-first) and `vision-rule.md` (vision alignment) and the validator fails if a command's embedded copy drifts from them. Seeds for `docs/brand/`: voice, terminology, facts, channels, the string registry, a locale delta, and its folder README |
223
+ | `templates/` | Seeds for `docs/ux/`: the vision skeleton, foundation, flows, screens, scenario base, the folder README, and the audit-report skeleton. Both hard-rule snippets live here as their single source, `claude-rule.md` (scenario-first) and `vision-rule.md` (vision alignment), and the validator fails if a command's embedded copy drifts from them. Seeds for `docs/brand/`: voice, terminology, facts, channels, the string registry, a locale delta, and its folder README |
223
224
 
224
225
  The contracts every skill reads:
225
226
 
226
227
  | Reference | Holds |
227
228
  |---|---|
228
229
  | [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 |
229
- | [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 |
230
- | [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` |
231
- | [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 |
230
+ | [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 |
231
+ | [ux_doctor.py](plugins/super-ux/scripts/ux_doctor.py) | Contract doctor. It 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` |
232
+ | [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 |
232
233
  | [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..24, the improvement procedure, anti-patterns |
233
- | [best-practices.md](plugins/super-ux/skills/references/best-practices.md) | Living, tag-indexed catalog of 210 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 |
234
+ | [best-practices.md](plugins/super-ux/skills/references/best-practices.md) | Living, tag-indexed catalog of 215 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), web2app (paid handoff, deferred deep links, storefront rules) and the funnel wiring that fails invisibly (what a personalization branch may vary, stand-up order, the three decisions a stored answer carries, GDPR Art. 13/17 timing, the access ladder), 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 |
234
235
  | [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 |
235
- | [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 |
236
+ | [funnel-research.md](plugins/super-ux/skills/references/funnel-research.md) | Reading a funnel market before designing one, `FR-01..FR-07`: where competitor funnels are visible, the four signals that survive when revenue is invisible, the fields that make a corpus comparable, which adjacent categories transfer, the stop before copying, and where each finding lands in the chain. Carried by `ux-foundation` and `ux-flows` |
237
+ | [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 of Apple HIG, Material 3, W3C ARIA APG and GOV.UK |
236
238
  | [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 |
237
- | [figma-integration.md](plugins/super-ux/skills/references/figma-integration.md) · [figma-structure.md](plugins/super-ux/skills/references/figma-structure.md) | The optional Figma surface (on by default): when and how to mock up, and how to structure the file so frames named `SCR-NN/<Screen>/<state>` map 1:1 to `screens.md` deterministic lookup, checkable drift |
239
+ | [figma-integration.md](plugins/super-ux/skills/references/figma-integration.md) · [figma-structure.md](plugins/super-ux/skills/references/figma-structure.md) | The optional Figma surface (on by default): when and how to mock up, and how to structure the file so frames named `SCR-NN/<Screen>/<state>` map 1:1 to `screens.md`, giving deterministic lookup and checkable drift |
238
240
 
239
241
  ## Keeping installs current
240
242
 
@@ -261,7 +263,7 @@ plugin and serves its frozen version forever. Nothing reports this: the plugin
261
263
  updates, the copy does not, and the copy is what loads.
262
264
 
263
265
  Cursor rules and the seeded `docs/ux/lint.py` are per-project (Cursor has no
264
- global rules directory) refresh each project you use:
266
+ global rules directory), so refresh each project you use:
265
267
 
266
268
  ```sh
267
269
  npx super-ux@latest --cursor /path/to/your/project --force
@@ -273,7 +275,7 @@ rest of `docs/ux/` are never touched. Check the published version with
273
275
 
274
276
  ## Contributing
275
277
 
276
- Issues and pull requests are welcome see [CONTRIBUTING.md](CONTRIBUTING.md)
278
+ Issues and pull requests are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md)
277
279
  for the repo layout, the validator, and the release checklist. Everyone taking
278
280
  part is expected to follow the [Code of Conduct](CODE_OF_CONDUCT.md); to report a
279
281
  vulnerability, see [SECURITY.md](SECURITY.md). In short:
@@ -283,19 +285,19 @@ edits to `plugins/super-ux/skills/references/` need
283
285
 
284
286
  ## Author
285
287
 
286
- Built by ssheleg [sshlg.me](https://sshlg.me)
288
+ Built by ssheleg · [sshlg.me](https://sshlg.me)
287
289
 
288
- - X / Twitter [@sshlg93](https://x.com/sshlg93)
289
- - Telegram [@sshlg](https://t.me/sshlg)
290
+ - X / Twitter · [@sshlg93](https://x.com/sshlg93)
291
+ - Telegram · [@sshlg](https://t.me/sshlg)
290
292
 
291
293
  Part of the [ssheleg skill family](https://github.com/ssheleg/sshlg-skills):
292
294
  `super-ux`, `task-pipeline`, `agent-sync`, `make-skill`, `sheleg-design`, `seo-aeo-audit`.
293
- **The family installs and updates as one package**, for every agent you use a bundle with one
295
+ **The family installs and updates as one package**, for every agent you use, a bundle with one
294
296
  member current and the rest stale is a combination nobody tested:
295
297
 
296
298
  ```bash
297
- npx sshlg-skills install # nothing installed yet the whole family, any agent
298
- npx sshlg-skills update # installed but behind updates everything
299
+ npx sshlg-skills install # nothing installed yet: the whole family, any agent
300
+ npx sshlg-skills update # installed but behind: updates everything
299
301
  npx --yes sshlg-skills@latest list # what the current release of each member is
300
302
  ```
301
303
 
package/bin/super-ux.js CHANGED
@@ -19,7 +19,7 @@ const ROOT = path.resolve(__dirname, '..');
19
19
  const REPO = 'ssheleg/super-ux';
20
20
 
21
21
  const MENU_ITEMS = [
22
- { key: 'skills', label: 'Skills for any AI agent (Claude Code, Codex, Cursor, 70+ opens agent picker)' },
22
+ { key: 'skills', label: 'Skills for any AI agent (Claude Code, Codex, Cursor, 70+; opens agent picker)' },
23
23
  { key: 'cursor', label: 'Cursor rules + docs/ux skeleton + docs/brand pack + linters, into a project' },
24
24
  { key: 'claude', label: 'Claude Code plugin (skills + /ux commands, user-global)' },
25
25
  ];
@@ -168,12 +168,12 @@ function installClaudePlugin() {
168
168
  return;
169
169
  }
170
170
  if (run('claude', ['plugin', 'marketplace', 'add', REPO]) !== 'ok') {
171
- console.log('(marketplace may already be added continuing)');
171
+ console.log('(marketplace may already be added, continuing)');
172
172
  }
173
173
  if (run('claude', ['plugin', 'install', 'super-ux@super-ux']) === 'ok') {
174
174
  console.log('Claude Code plugin installed (scope: user). Restart sessions to pick it up; then run /ux in any project.');
175
175
  } else {
176
- console.error('warning: claude plugin install failed see output above');
176
+ console.error('warning: claude plugin install failed, see output above');
177
177
  }
178
178
  }
179
179
 
@@ -306,7 +306,7 @@ async function selectFallback(items, prompter) {
306
306
  }
307
307
 
308
308
  async function menu() {
309
- console.log('super-ux scenario-driven UI development. Select what to install:\n');
309
+ console.log('super-ux: scenario-driven UI development. Select what to install:\n');
310
310
  const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
311
311
 
312
312
  // ONE prompter for the whole flow: with piped stdin, all pending lines are
@@ -333,7 +333,7 @@ async function menu() {
333
333
  let cursorDir = null;
334
334
  if (keys.includes('cursor')) {
335
335
  if (!prompter) prompter = makePrompter();
336
- const dir = (await prompter.ask('Cursor rules project directory [.]: ')).trim() || '.';
336
+ const dir = (await prompter.ask('Cursor rules, project directory [.]: ')).trim() || '.';
337
337
  cursorDir = path.resolve(dir);
338
338
  }
339
339
  if (prompter) prompter.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "super-ux",
3
- "version": "0.38.2",
3
+ "version": "0.40.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"
@@ -71,6 +71,35 @@ def read(path: Path) -> str | None:
71
71
  return None
72
72
 
73
73
 
74
+ def content_date(path: Path) -> str | None:
75
+ """The date this file's content last changed, as `YYYY-MM-DD`.
76
+
77
+ Git first, mtime only as a fallback. A fresh clone stamps every file with
78
+ the checkout time, so an mtime-based answer says "changed today" about a
79
+ file nobody has touched in months -- which made `B005` fire on every CI
80
+ run the moment this project put its own linter in CI, and would have
81
+ trained a reader to ignore the one gate it was added to enforce.
82
+ """
83
+ try:
84
+ import subprocess
85
+
86
+ out = subprocess.run(
87
+ ["git", "log", "-1", "--format=%cs", "--", path.name],
88
+ cwd=path.parent, capture_output=True, text=True, timeout=10,
89
+ )
90
+ stamp = out.stdout.strip()
91
+ if out.returncode == 0 and re.fullmatch(r"\d{4}-\d{2}-\d{2}", stamp):
92
+ return stamp
93
+ except Exception:
94
+ pass
95
+ try:
96
+ import datetime
97
+
98
+ return datetime.date.fromtimestamp(path.stat().st_mtime).isoformat()
99
+ except OSError:
100
+ return None
101
+
102
+
74
103
  def header_field(text: str, key: str) -> str | None:
75
104
  """A `Key: value` line from a file's header block."""
76
105
  match = re.search(rf"^{re.escape(key)}:\s*(.+?)\s*$", text, re.M)
@@ -180,11 +209,8 @@ def check_contract(brand_dir: Path) -> list[Finding]:
180
209
  calibrated = header_field(voice, "Last calibrated")
181
210
  if foundation is not None and calibrated:
182
211
  try:
183
- stamp = (brand_dir.parent / "ux" / "foundation.md").stat().st_mtime
184
- import datetime
185
-
186
- changed = datetime.date.fromtimestamp(stamp).isoformat()
187
- if changed > calibrated:
212
+ changed = content_date(brand_dir.parent / "ux" / "foundation.md")
213
+ if changed and changed > calibrated:
188
214
  findings.append(Finding(
189
215
  "B005", SEVERITY_WARN, "voice.md", 1,
190
216
  f"foundation.md changed on {changed}, after the voice "
@@ -757,6 +783,155 @@ EMOJI_RE = re.compile(
757
783
  "[\U0001F300-\U0001FAFF☀-➿️]"
758
784
  )
759
785
 
786
+ # AT-06, the rhetorical dash. The rule is a distinction, not a ban: a dash
787
+ # standing in for a full stop, a comma or a colon is the machine-drafting
788
+ # marker, and a dash the language requires is grammar. Stripping both
789
+ # produces ungrammatical Russian, so the checks below are ordered by what
790
+ # can actually be established without parsing the sentence.
791
+ DASH = "—"
792
+
793
+ # Never a finding. A range is arithmetic and direct speech is a convention.
794
+ DASH_RANGE_RE = re.compile(rf"\d\s*{DASH}\s*\d")
795
+ DASH_SPEECH_RE = re.compile(rf"^\s*{DASH}\s")
796
+
797
+ # Rhetorical in every language: a dash cannot introduce a coordinating
798
+ # conjunction, because that is a comma's job. The copula dash is never
799
+ # followed by one, which is what makes this safe to run on Russian.
800
+ DASH_CONJ_RE = re.compile(
801
+ rf"{DASH}\s+(?:и|а|но|или|да|and|but|or|so|yet|nor)\s",
802
+ re.I,
803
+ )
804
+
805
+ # Locales whose orthography requires a dash between subject and predicate
806
+ # when the verb is absent (<<Москва — столица>>). Deliberately short: a
807
+ # language belongs here only when the construction is a rule of its
808
+ # orthography rather than a stylistic option, and a wrong entry silently
809
+ # switches the strict check off for a whole language.
810
+ COPULA_LOCALES = ("ru", "uk", "be")
811
+ CYRILLIC_RE = re.compile("[Ѐ-ӿ]")
812
+
813
+ # A title is a name. `.` ends a statement, so it does not belong; `?` and
814
+ # `…` do, because a title may genuinely ask or genuinely trail off.
815
+ ABBREVIATION_RE = re.compile(r"(?:\b[A-Za-z]\.[A-Za-z]\.|\b(?:etc|vs|Inc|Ltd|Co|jr|sr|no)\.|\.[a-z]{2,4})$", re.I)
816
+
817
+
818
+ def prose_only(text: str) -> str:
819
+ """Fenced blocks and inline code are not prose and never carry a tell."""
820
+ text = re.sub(r"```.*?```", "", text, flags=re.S)
821
+ text = re.sub(r"`[^`]*`", "", text)
822
+ return text
823
+
824
+
825
+ def sentences(text: str) -> list[str]:
826
+ """Crude split, sufficient to count dashes inside one sentence."""
827
+ return [p for p in re.split(r"(?<=[.!?])\s+|\n\s*\n", text) if p.strip()]
828
+
829
+
830
+ def grammatical_dash_language(text: str, primary: str | None) -> bool:
831
+ """Does a grammatical dash exist in the language this text is written in?
832
+
833
+ Two signals, and either is enough, because the consequence of guessing
834
+ wrong in one direction is a linter that calls correct Russian an error.
835
+ A missed rhetorical dash is a style note; a false error on grammar is
836
+ how a check gets switched off.
837
+ """
838
+ if primary and primary.split("-")[0].lower() in COPULA_LOCALES:
839
+ return True
840
+ return bool(CYRILLIC_RE.search(text))
841
+
842
+
843
+ def _around(sentence: str, width: int = 34) -> str:
844
+ """The dash with enough either side to find it and decide the fix."""
845
+ flat = " ".join(sentence.split())
846
+ at = flat.find(DASH)
847
+ if at < 0:
848
+ return flat[:width * 2]
849
+ start, end = max(0, at - width), min(len(flat), at + width)
850
+ return ("…" if start else "") + flat[start:end] + ("…" if end < len(flat) else "")
851
+
852
+
853
+ def dash_findings(code: str, path: str, text: str, strict: bool) -> list[Finding]:
854
+ """AT-06 over one body of prose. `strict` bans every non-range dash.
855
+
856
+ Every finding quotes the dash in context and carries the line it sits on.
857
+ Thirty-seven findings reading "a dash stands in for a full stop" is a
858
+ report nobody can act on, and a check nobody can act on gets switched
859
+ off rather than obeyed.
860
+ """
861
+ findings: list[Finding] = []
862
+ body = prose_only(text)
863
+ if DASH not in body:
864
+ return findings
865
+
866
+ # Line numbers come from the stripped body, so a dash inside a fenced
867
+ # block cannot shift the number of one in the prose after it.
868
+ offsets = {}
869
+ cursor = 0
870
+ for number, line in enumerate(body.splitlines(keepends=True), start=1):
871
+ offsets[cursor] = number
872
+ cursor += len(line)
873
+
874
+ def line_of(fragment: str) -> int:
875
+ at = body.find(fragment[:40])
876
+ if at < 0:
877
+ return 0
878
+ best = 0
879
+ for start, number in offsets.items():
880
+ if start <= at:
881
+ best = number
882
+ else:
883
+ break
884
+ return best
885
+
886
+ for sentence in sentences(body):
887
+ if DASH not in sentence:
888
+ continue
889
+ if DASH_SPEECH_RE.match(sentence):
890
+ continue
891
+
892
+ line = line_of(sentence)
893
+ quoted = _around(sentence)
894
+
895
+ conj = DASH_CONJ_RE.search(sentence)
896
+ if conj:
897
+ findings.append(Finding(
898
+ code, SEVERITY_ERROR, path, line,
899
+ f'a dash introduces "{conj.group(0).strip()}", which is a '
900
+ f"comma's job; no language puts a grammatical dash before a "
901
+ f'conjunction: "{quoted}"',
902
+ ))
903
+ continue
904
+
905
+ bare = sentence.count(DASH) - len(DASH_RANGE_RE.findall(sentence))
906
+ if bare >= 2:
907
+ findings.append(Finding(
908
+ code, SEVERITY_ERROR, path, line,
909
+ f"{bare} dashes bracket an aside in one sentence, which is "
910
+ f'the parenthetical reflex; use commas or brackets: "{quoted}"',
911
+ ))
912
+ continue
913
+
914
+ if strict and bare > 0:
915
+ findings.append(Finding(
916
+ code, SEVERITY_ERROR, path, line,
917
+ f"a dash stands in for a full stop, a comma or a colon, and "
918
+ f"this locale has no grammatical dash; pick the mark that "
919
+ f'states the real relationship: "{quoted}"',
920
+ ))
921
+ return findings
922
+
923
+
924
+ def title_full_stop(title: str) -> bool:
925
+ """AT-07. True when a title ends in a full stop that is a full stop."""
926
+ text = title.strip()
927
+ if not text.endswith("."):
928
+ return False
929
+ if text.endswith("..") or text.endswith("…"):
930
+ return False
931
+ if ". " in text: # several sentences -- a different defect
932
+ return False
933
+ return not ABBREVIATION_RE.search(text)
934
+
760
935
 
761
936
  def check_bot_safety(brand_dir: Path, sources: dict) -> list[Finding]:
762
937
  """B050-B054 -- do not write text that looks like gaming a crawler."""
@@ -838,8 +1013,16 @@ def check_bot_safety(brand_dir: Path, sources: dict) -> list[Finding]:
838
1013
 
839
1014
 
840
1015
  def check_ai_tells(brand_dir: Path, sources: dict) -> list[Finding]:
841
- """B060-B061 -- machine-drafting markers, and the one absolute ban."""
1016
+ """B060-B063 -- machine-drafting markers, and the one absolute ban.
1017
+
1018
+ B060 counts vocabulary markers, B061 bans levity where the user is
1019
+ losing something, B062 is AT-06 (the rhetorical dash) and B063 is AT-07
1020
+ (a title that ends in a full stop) outside the string registry, which
1021
+ B026 already covers. The grades and the reasoning are in
1022
+ `references/ai-tells.md`; this file carries only the provable subset.
1023
+ """
842
1024
  findings: list[Finding] = []
1025
+ primary, _others = declared_locales(brand_dir)
843
1026
 
844
1027
  for path, _fields, body in documents(brand_dir, sources, "marketing"):
845
1028
  lowered = body.lower()
@@ -854,6 +1037,38 @@ def check_ai_tells(brand_dir: Path, sources: dict) -> list[Finding]:
854
1037
  f"Naturalness grade {grade}",
855
1038
  ))
856
1039
 
1040
+ # B062 -- the rhetorical dash, in every surface that ships prose.
1041
+ for key in ("marketing", "store"):
1042
+ for path, fields, body in documents(brand_dir, sources, key):
1043
+ text = f"{fields.get('title', '')}\n{body}"
1044
+ strict = not grammatical_dash_language(text, primary)
1045
+ findings.extend(dash_findings("B062", path, body, strict))
1046
+
1047
+ for row in registry(brand_dir):
1048
+ strict = not grammatical_dash_language(row["text"], primary)
1049
+ findings.extend(dash_findings("B062", row["location"], row["text"], strict))
1050
+
1051
+ # B063 -- AT-07 outside the registry: document titles and the headings
1052
+ # inside them. B026 owns the same rule for `strings.md`, and the two are
1053
+ # split by artifact rather than by rule so that neither can be satisfied
1054
+ # by fixing the other.
1055
+ for key in ("marketing", "store"):
1056
+ for path, fields, body in documents(brand_dir, sources, key):
1057
+ title = fields.get("title", "")
1058
+ if title and title_full_stop(title):
1059
+ findings.append(Finding(
1060
+ "B063", SEVERITY_WARN, path, 0,
1061
+ f'the title ends in a full stop: "{title.strip()}". '
1062
+ f"A title is a name, not a statement",
1063
+ ))
1064
+ for heading in re.findall(r"^#{1,6}\s+(.+?)\s*$", prose_only(body), re.M):
1065
+ if title_full_stop(heading):
1066
+ findings.append(Finding(
1067
+ "B063", SEVERITY_WARN, path, 0,
1068
+ f'a heading ends in a full stop: "{heading.strip()}". '
1069
+ f"A heading is a name, not a statement",
1070
+ ))
1071
+
857
1072
  for row in registry(brand_dir):
858
1073
  if not row["key"].startswith(SENSITIVE_PREFIXES):
859
1074
  continue
@@ -39,15 +39,23 @@
39
39
  the banned ones), `facts.md` (the only source of any public figure),
40
40
  `channels.md` (one record per surface), `strings.md` (the interface string
41
41
  registry), `locales/<code>.md`.
42
- - Any change to public-facing text an interface string, a landing page, a
43
- post, a store listing, an ad, an email updates `docs/brand/` in the SAME
42
+ - Any change to public-facing text (an interface string, a landing page, a
43
+ post, a store listing, an ad, an email) updates `docs/brand/` in the SAME
44
44
  change. A new string with no registry row is drift, not a detail.
45
45
  - **Never quote a number that has no row in `facts.md`,** and never invent a
46
46
  fact, statistic, quote or expert to fill a gap. Report the gap instead.
47
47
  - **One action keeps one name** across button, confirmation, toast, history,
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
- billing or paywall surfaces in any voice.
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
59
  - Run `python3 docs/brand/lint.py` after any text change and before calling
52
60
  work done. It must exit clean; wire it into CI or pre-commit alongside the
53
61
  UX linter so copy drift cannot merge.