super-ux 0.29.0 → 0.30.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.
@@ -166,6 +166,44 @@ def fix(ux: Path, d: dict) -> list[str]:
166
166
  return done
167
167
 
168
168
 
169
+ def brand_contract_state(root: Path) -> list[str]:
170
+ """The brand pack's contract version, or why there is nothing to report.
171
+
172
+ Same blind spot as the chain's: a pack written to an old contract is
173
+ internally consistent, so `brand_lint.py` stays quiet about it. Only a
174
+ marker comparison notices, which is why the doctor reads it too.
175
+ """
176
+ brand = root / "docs" / "brand"
177
+ if not brand.is_dir():
178
+ return []
179
+ versions: dict[str, str] = {}
180
+ unmarked: list[str] = []
181
+ for path in sorted(brand.rglob("*.md")):
182
+ text = path.read_text(encoding="utf-8", errors="replace")
183
+ found = re.search(r"^Contract:\s*brand-contract\s*(v\d+)\s*$", text, re.M)
184
+ rel = path.relative_to(brand).as_posix()
185
+ if found:
186
+ versions[rel] = found.group(1)
187
+ else:
188
+ unmarked.append(rel)
189
+ out = []
190
+ distinct = set(versions.values())
191
+ if len(distinct) > 1:
192
+ out.append(
193
+ "docs/brand: mixed contract versions -- "
194
+ + ", ".join(f"{k} {v}" for k, v in sorted(versions.items()))
195
+ )
196
+ elif distinct and distinct != {"v1"}:
197
+ out.append(
198
+ f"docs/brand: written to brand-contract {distinct.pop()}, current is v1"
199
+ )
200
+ if unmarked:
201
+ out.append(
202
+ "docs/brand: no contract marker on " + ", ".join(unmarked[:4])
203
+ )
204
+ return out
205
+
206
+
169
207
  def main() -> int:
170
208
  args = [a for a in sys.argv[1:] if not a.startswith("--")]
171
209
  ux = find_ux_dir(args[0] if args else None)
@@ -188,6 +226,14 @@ def main() -> int:
188
226
  return 0
189
227
 
190
228
  problems = report(ux, d)
229
+
230
+ brand = brand_contract_state(ux.parent.parent)
231
+ if brand:
232
+ print("\nBrand pack:")
233
+ for line in brand:
234
+ print(f" {line}")
235
+ problems = True
236
+
191
237
  if "--fix" in sys.argv:
192
238
  applied = fix(ux, d)
193
239
  print("\nApplied:" if applied else "\nNothing to apply automatically.")
@@ -0,0 +1,53 @@
1
+ Contract: brand-contract v1
2
+
3
+ # docs/brand/ — how this product speaks
4
+
5
+ `docs/ux/` decides **what the product does**. This folder decides **how it
6
+ speaks** — in the interface, on the landing page, in the store listing, in a
7
+ post. One voice, many registers.
8
+
9
+ Owned by the `brand-voice` skill. Written by `copywriting`. Checked by
10
+ `python3 docs/brand/lint.py` and by `/ux-audit copy`.
11
+
12
+ | File | Holds |
13
+ |---|---|
14
+ | `voice.md` | the identity: pack, five axes, narrative, invariants |
15
+ | `terminology.md` | our words, banned words, entity and tier names |
16
+ | `facts.md` | canonical numbers and proof — the only source of a figure |
17
+ | `channels.md` | one record per surface: register, limits, bans |
18
+ | `strings.md` | interface string registry → `file:line` → scenario |
19
+ | `locales/<code>.md` | per-locale delta |
20
+
21
+ ## Sources
22
+
23
+ **Fill this in before anything else.** The linter scans nothing outside these
24
+ paths, and refuses to report a clean run over a surface it never read
25
+ (`B006`). Delete the keys this project does not have — a declared-but-absent
26
+ source is worse than an omitted one.
27
+
28
+ ```
29
+ Sources:
30
+ ui: src/**/*.{ts,tsx,js,jsx,vue,svelte}
31
+ marketing: content/**/*.{md,mdx}
32
+ store: store/{ios,android}/*.md
33
+ robots: public/robots.txt
34
+ locales: src/locales/*.json
35
+ ```
36
+
37
+ `ui` and `marketing` also classify findings — several checks apply to only
38
+ one of the two.
39
+
40
+ ## The hard rule
41
+
42
+ Any change to public-facing text updates this folder in the same change, and
43
+ `python3 docs/brand/lint.py` exits clean before the work is called done.
44
+
45
+ ## Commands
46
+
47
+ | Command | Does |
48
+ |---|---|
49
+ | `/brand` | status across every file, then one recommended action |
50
+ | `/brand-init` | pick a voice pack and calibrate it to this product |
51
+ | `/brand-update` | recalibrate after positioning or personas changed |
52
+ | `/brand-lint` | run the linter |
53
+ | `/copy` | write or edit copy for a named surface |
@@ -0,0 +1,129 @@
1
+ Contract: brand-contract v1
2
+
3
+ # Channels
4
+
5
+ One record per surface. `Register` is deltas against the five axes in
6
+ `voice.md` — a register moves the axes, never the invariants.
7
+
8
+ `Forbidden` always carries both halves, even when one is `none`. Platform
9
+ physics and brand choice written on one line become indistinguishable within
10
+ a quarter, and then nobody can tell which is safe to revisit when the
11
+ platform changes.
12
+
13
+ Delete the surfaces this product does not have. Do not rename one: the
14
+ linter and the audit address surfaces by the names in the contract.
15
+
16
+ ---
17
+
18
+ ## Product surfaces
19
+
20
+ ### primary action
21
+
22
+ ```
23
+ Register: <humor -1>
24
+ Format: <verb phrase naming the outcome; one primary per screen>
25
+ Limits: <24 characters>
26
+ Forbidden: physics: none | brand: <"Submit", "OK", bare nouns>
27
+ CTA: <this surface is the CTA>
28
+ Proof: none
29
+ Locales: <coefficient applies to the character limit>
30
+ ```
31
+
32
+ ### error
33
+
34
+ ```
35
+ Register: humor -3, density +1, distance -1
36
+ Format: what happened, what was not affected, one next step
37
+ Limits: <…>
38
+ Forbidden: physics: none | brand: humor, "unexpected", blame, bare codes
39
+ CTA: the recovery action
40
+ Proof: none
41
+ Locales: <…>
42
+ ```
43
+
44
+ ### empty state
45
+
46
+ ```
47
+ Register: <unchanged>
48
+ Format: what belongs here, why it is worth it, the one starting action
49
+ Limits: <…>
50
+ Forbidden: physics: none | brand: apology, "Nothing here yet" alone
51
+ CTA: one
52
+ Proof: none
53
+ Locales: <…>
54
+ ```
55
+
56
+ ### paywall and upgrade
57
+
58
+ ```
59
+ Register: humor -3, distance +1
60
+ Format: what is sold, what it costs, what happens at term end, how to leave
61
+ Limits: <…>
62
+ Forbidden: physics: none | brand: humor, manufactured urgency, hidden terms
63
+ CTA: one primary, one way out
64
+ Proof: one sourced figure
65
+ Locales: <legal differences per locale>
66
+ ```
67
+
68
+ ### destructive confirm
69
+
70
+ ```
71
+ Register: humor -3
72
+ Format: object, consequence, reversibility; verb repeats the action
73
+ Limits: <…>
74
+ Forbidden: physics: none | brand: humor, "OK" as the confirming verb
75
+ CTA: the destructive verb
76
+ Proof: none
77
+ Locales: <…>
78
+ ```
79
+
80
+ <add the remaining product surfaces this product has: loading,
81
+ success/toast, onboarding, billing and receipts, settings and legal,
82
+ transactional email and push, docs and help>
83
+
84
+ ---
85
+
86
+ ## Marketing surfaces
87
+
88
+ ### landing hero
89
+
90
+ ```
91
+ Register: confidence +1, density -1
92
+ Format: one headline, one subhead, one primary action
93
+ Limits: title 60, meta description 160
94
+ Forbidden: physics: none | brand: superlatives with no facts.md row
95
+ CTA: one, verb plus outcome
96
+ Proof: one number, sourced
97
+ Locales: <coefficient applies to both limits>
98
+ ```
99
+
100
+ ### X
101
+
102
+ ```
103
+ Register: density +1, distance -1
104
+ Format: one idea; threads 5-12 posts, each standing alone
105
+ Limits: 280
106
+ Forbidden: physics: link in body suppresses reach; >2 hashtags penalised;
107
+ editing within 30 minutes resets distribution
108
+ | brand: <engagement bait, fake urgency>
109
+ CTA: <in the first reply, not the post>
110
+ Proof: <one figure, sourced>
111
+ Locales: <…>
112
+ ```
113
+
114
+ ### Reddit
115
+
116
+ ```
117
+ Register: distance -2, humor +1
118
+ Format: a statement, not a headline; the comments are the content
119
+ Limits: <per subreddit>
120
+ Forbidden: physics: <subreddit rules outrank everything here>
121
+ | brand: undisclosed affiliation, CTA of any kind
122
+ CTA: none
123
+ Proof: <first-hand only>
124
+ Locales: <…>
125
+ ```
126
+
127
+ <add the remaining marketing surfaces this product uses: landing body,
128
+ pricing, blog, changelog, LinkedIn, HN and Product Hunt, App Store,
129
+ Google Play, ads, lifecycle email>
@@ -0,0 +1,36 @@
1
+ Contract: brand-contract v1
2
+
3
+ # Facts
4
+
5
+ **The only source of any figure in public copy.** A number on a public
6
+ surface with no row here is `B030` and blocks. A row with no `Source`, or one
7
+ past its `Review by`, is `B031` and warns.
8
+
9
+ A missing fact is reported, never invented to close a gap.
10
+
11
+ | Fact | Value | Source | Checked | Review by | Public |
12
+ |---|---|---|---|---|---|
13
+ | <what the number counts> | <448> | <api/catalog.json> | <2026-08-05> | <2026-11-05> | <yes> |
14
+ | <internal figure> | <$3.10> | <billing export> | <2026-08-05> | <2026-11-05> | <no> |
15
+
16
+ `Public: no` marks figures that exist and must never be quoted — internal
17
+ margins, unreleased counts, anything under embargo. The linter treats
18
+ quoting one as the same failure as quoting a number that does not exist.
19
+
20
+ ## Proof that is not a number
21
+
22
+ Testimonials, awards, certifications and press. Same rules: attributed,
23
+ dated, and re-checked.
24
+
25
+ | Claim | Attribution | Source | Checked | Review by | Public |
26
+ |---|---|---|---|---|---|
27
+ | <…> | <name, role, company> | <where it was said> | <YYYY-MM-DD> | <YYYY-MM-DD> | <yes> |
28
+
29
+ ## Required disclaimers
30
+
31
+ Text that must appear alongside specific claims — regulatory, contractual, or
32
+ promised. Locale differences live in `locales/<code>.md`.
33
+
34
+ | Claim it attaches to | Required text |
35
+ |---|---|
36
+ | <…> | <…> |
@@ -0,0 +1,68 @@
1
+ Contract: brand-contract v1
2
+ Locale: <de>
3
+ Primary: <no>
4
+ Address form: <Sie>
5
+ Length coefficient: <1.30>
6
+ Humor: <-1 from base>
7
+ Never translated: <product name, entity names, tier names>
8
+ Keywords: <own research — see below>
9
+ Reviewed by: <name of someone who reads this language daily, or `unreviewed`>
10
+
11
+ # Locale delta
12
+
13
+ Copy this file to `locales/<code>.md`, one per declared locale. The invariant
14
+ half of the voice does not appear here — it is in `voice.md` and holds in
15
+ every language. This file carries only what is decided again.
16
+
17
+ ## Address form
18
+
19
+ <The single choice, applied everywhere. This decision changes how the brand
20
+ is perceived more than any word choice, so it is made once and recorded, not
21
+ per string.>
22
+
23
+ ## Dead idioms
24
+
25
+ Idiom and wordplay from the primary locale that does not survive. The
26
+ replacement does the same **job**; it is not a translation of the joke.
27
+
28
+ | Primary | Replacement | Job it does |
29
+ |---|---|---|
30
+ | <"ship it"> | <"raus damit"> | <permission to stop polishing> |
31
+
32
+ ## Keywords
33
+
34
+ Researched in this market, never translated from the primary. The
35
+ translation of a high-volume English term is routinely a term with no volume,
36
+ while the term people actually type is a different word.
37
+
38
+ | Term | Volume | Where it is used |
39
+ |---|---|---|
40
+ | <…> | <…> | <…> |
41
+
42
+ ## Legal differences
43
+
44
+ Required copy this locale has and others do not. Treated as required strings:
45
+ a missing one is a defect even when parity is otherwise complete.
46
+
47
+ | Requirement | Text or reference |
48
+ |---|---|
49
+ | <Impressum> | <…> |
50
+ | <VAT-inclusive pricing> | <…> |
51
+
52
+ ## Length notes
53
+
54
+ Surfaces where the coefficient bites hardest in this locale — usually
55
+ buttons, tab labels, store title and subtitle. Recorded here so the
56
+ constraint reaches the primary-locale original, where it can still be
57
+ designed around.
58
+
59
+ | Surface | Primary limit | Effective limit | Note |
60
+ |---|---|---|---|
61
+ | <App Store title> | <30> | <23> | <…> |
62
+
63
+ ## Parity
64
+
65
+ Computed by the linter against `strings.md`; not maintained by hand. Below
66
+ the threshold in `voice.md` it warns (`B071`) with the percentage. A partial
67
+ locale that declares itself is a known state; one that does not is a surprise
68
+ for a user who does not speak the fallback.
@@ -0,0 +1,41 @@
1
+ Contract: brand-contract v1
2
+
3
+ # Interface strings
4
+
5
+ A **decision registry**, not a message catalog. It holds no translations and
6
+ does not replace i18n keys. It records which strings have been reconciled
7
+ with the voice, which scenario each serves, and where each one lives.
8
+
9
+ This is what makes "one action, two names" (`B020`) checkable at all. Without
10
+ it, that defect is only findable by reading the entire interface — which is
11
+ why it survives in every product that has not written one down.
12
+
13
+ Populated by an inventory sweep at init, not by hand.
14
+
15
+ | Key | Text (primary) | Location | Scenario | Status |
16
+ |---|---|---|---|---|
17
+ | <action.project.publish> | <Publish> | <src/ui/ProjectBar.tsx:47> | <SCN-014> | <agreed> |
18
+
19
+ ## Columns
20
+
21
+ - **Key** — dot-separated, stable, names the action rather than the screen.
22
+ Two rows sharing a `Key` with different `Text` is `B020`.
23
+ - **Text (primary)** — the string in the primary locale. Other locales live
24
+ in the project's own i18n files; parity is computed against this column.
25
+ - **Location** — `file:line`. A location that no longer resolves is `B023`.
26
+ - **Scenario** — the `SCN-NNN` this string serves. A string serving no
27
+ scenario is a question for `ux-scenarios`, not a copy problem.
28
+ - **Status** — `agreed` · `proposed` · `drifted` · `orphan`.
29
+
30
+ ## Statuses
31
+
32
+ | Status | Means |
33
+ |---|---|
34
+ | `agreed` | reconciled with the voice and approved |
35
+ | `proposed` | written, not yet approved |
36
+ | `drifted` | the code no longer matches this row (`B021`) |
37
+ | `orphan` | in the registry, no longer in the code |
38
+
39
+ A string found in code with no row here is `B022` — a warning, not an error,
40
+ so adopting this registry on an existing product does not block from day one.
41
+ It becomes an error only once someone agrees the row.
@@ -0,0 +1,45 @@
1
+ Contract: brand-contract v1
2
+
3
+ # Terminology
4
+
5
+ The dictionary the linter reads. Column one of **Product terms** and
6
+ **Banned** drives `B010` and `B011`; **Entity and tier names** drives `B012`.
7
+
8
+ ## Product terms — always
9
+
10
+ Where the product has its own word, the generic one is a defect, not a
11
+ synonym.
12
+
13
+ | Our term | Never write | Applies to |
14
+ |---|---|---|
15
+ | <Run> | <Execution, Job> | <what the product performs> |
16
+
17
+ ## Entity and tier names — exact spelling
18
+
19
+ One spelling, everywhere: interface, marketing, billing, support, docs.
20
+
21
+ | Name | Wrong forms seen |
22
+ |---|---|
23
+ | <Pro> | <PRO, Pro plan, pro> |
24
+
25
+ ## Banned
26
+
27
+ Seeded at init from weak verbs, hedging chains and the marker vocabulary in
28
+ `ai-tells.md`. Calibration adds what is specific to this product.
29
+
30
+ | Word or phrase | Why | Use instead |
31
+ |---|---|---|
32
+ | leverage | filler verb | use |
33
+ | seamless | claims what it cannot show | name the step that disappeared |
34
+ | utilize | longer word, same meaning | use |
35
+ | robust | hides the number | the number |
36
+ | <…> | <…> | <…> |
37
+
38
+ ## Glossary
39
+
40
+ Terms this product uses in a specific sense. If a term is here, it is
41
+ grounded before it is leaned on.
42
+
43
+ | Term | Meaning |
44
+ |---|---|
45
+ | <…> | <…> |
@@ -0,0 +1,58 @@
1
+ Contract: brand-contract v1
2
+ Voice pack: <pack id from voice-packs.md, or `custom`>
3
+ Locales: <en (primary)>
4
+ Locale parity threshold: 80%
5
+ Derived-from: <PER-NN, JTBD-NN from docs/ux/foundation.md — or `inferred`>
6
+ Status: draft
7
+ Last calibrated: <YYYY-MM-DD>
8
+
9
+ # Voice
10
+
11
+ The pack was the starting position. This file is the truth.
12
+
13
+ ## Axes
14
+
15
+ The five axes are fixed. A project sharpens the wording; it does not add or
16
+ remove an axis, because `channels.md` expresses every register as a delta
17
+ against exactly these five.
18
+
19
+ | Axis | The product IS | The product IS NOT |
20
+ |---|---|---|
21
+ | Confidence | <…> | <…> |
22
+ | Register | <…> | <…> |
23
+ | Distance | <…> | <…> |
24
+ | Humor | <…> | <…> |
25
+ | Density | <…> | <…> |
26
+
27
+ ## Narrative
28
+
29
+ ```
30
+ Hero: <who the reader is, in the role this product meets them in>
31
+ Enemy: <the state of the world that costs them — not a competitor>
32
+ Product role: <instrument | guide | weapon>
33
+ Promise: <one line, checkable against a row in facts.md>
34
+ ```
35
+
36
+ ## Invariant in every language
37
+
38
+ What survives translation. Breaking one of these is a finding on any surface,
39
+ in any locale.
40
+
41
+ - <e.g. never hedges>
42
+ - <e.g. never claims a number that is not in facts.md>
43
+
44
+ ## Reconsidered per locale
45
+
46
+ Conventions rather than character. Each is decided again in
47
+ `locales/<code>.md`.
48
+
49
+ - address form
50
+ - humor level
51
+ - idiom and wordplay
52
+ - <…>
53
+
54
+ ## Failure mode
55
+
56
+ Copied from the pack, kept here so the audit can look for it by name.
57
+
58
+ <the degenerate form this voice collapses into when overdone>
@@ -29,3 +29,23 @@
29
29
  - Use `/ux` as the entry point; skills: `ux-foundation`, `ux-flows`
30
30
  (flows + Figma mockups), `ux-scenarios` for maintenance, `ux-audit` for
31
31
  evidence-backed verification. Full map: the plugin's system-map reference.
32
+
33
+ ## Brand voice — hard rule (super-ux)
34
+
35
+ - `docs/brand/` is the source of truth for how the product speaks:
36
+ `voice.md` (axes, narrative, invariants), `terminology.md` (our words and
37
+ the banned ones), `facts.md` (the only source of any public figure),
38
+ `channels.md` (one record per surface), `strings.md` (the interface string
39
+ registry), `locales/<code>.md`.
40
+ - Any change to public-facing text — an interface string, a landing page, a
41
+ post, a store listing, an ad, an email — updates `docs/brand/` in the SAME
42
+ change. A new string with no registry row is drift, not a detail.
43
+ - **Never quote a number that has no row in `facts.md`,** and never invent a
44
+ fact, statistic, quote or expert to fill a gap. Report the gap instead.
45
+ - **One action keeps one name** across button, confirmation, toast, history,
46
+ notification and accessible name. Search `strings.md` before naming one.
47
+ - **No humor, exclamation marks or emoji** on error, destructive confirm,
48
+ billing or paywall surfaces — in any voice.
49
+ - Run `python3 docs/brand/lint.py` after any text change and before calling
50
+ work done. It must exit clean; wire it into CI or pre-commit alongside the
51
+ UX linter so copy drift cannot merge.