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 +155 -0
- package/README.md +44 -42
- package/bin/super-ux.js +5 -5
- package/package.json +1 -1
- package/plugins/super-ux/scripts/brand_lint.py +221 -6
- package/templates/claude-rule.md +11 -3
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
|
[](https://github.com/ssheleg/super-ux/actions/workflows/validate.yml)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
**Scenario-driven UI development for AI agents
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`**
|
|
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
|
|
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`**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
230
|
-
| [ux_doctor.py](plugins/super-ux/scripts/ux_doctor.py) | Contract doctor
|
|
231
|
-
| [best-practices-index.md](plugins/super-ux/skills/references/best-practices-index.md) | Generated tag index over the catalog
|
|
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
|
|
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
|
-
| [
|
|
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
|
|
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)
|
|
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
|
|
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
|
|
288
|
+
Built by ssheleg · [sshlg.me](https://sshlg.me)
|
|
287
289
|
|
|
288
|
-
- X / Twitter
|
|
289
|
-
- Telegram
|
|
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
|
|
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
|
|
298
|
-
npx sshlg-skills update # installed but behind
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
184
|
-
|
|
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-
|
|
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
|
package/templates/claude-rule.md
CHANGED
|
@@ -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
|
|
43
|
-
post, a store listing, an ad, an email
|
|
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
|
|
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.
|