super-ux 0.28.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.
- package/CHANGELOG.md +111 -0
- package/README.md +37 -1
- package/bin/super-ux.js +40 -10
- package/cursor/rules/brand-voice.mdc +37 -0
- package/cursor/rules/super-ux.mdc +4 -0
- package/package.json +3 -1
- package/plugins/super-ux/scripts/brand_lint.py +957 -0
- package/plugins/super-ux/scripts/ux_doctor.py +251 -0
- package/templates/brand/README.md +53 -0
- package/templates/brand/channels.md +129 -0
- package/templates/brand/facts.md +36 -0
- package/templates/brand/locale.md +68 -0
- package/templates/brand/strings.md +41 -0
- package/templates/brand/terminology.md +45 -0
- package/templates/brand/voice.md +58 -0
- package/templates/claude-rule.md +20 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Diagnose a project's UX chain against the current contract (stdlib only).
|
|
3
|
+
|
|
4
|
+
`ux_lint.py` checks a chain against itself -- ids, links, orphans. It cannot
|
|
5
|
+
tell you that the whole base is written to a contract three versions old,
|
|
6
|
+
because from the inside everything is consistent. That is the drift this
|
|
7
|
+
finds: between the project and the contract it was written to.
|
|
8
|
+
|
|
9
|
+
Read-only by default. `--fix` applies only the changes that cannot be wrong:
|
|
10
|
+
renaming a file the contract owns and stamping a marker onto an artifact whose
|
|
11
|
+
shape already matches. Everything else is reported for a human to decide.
|
|
12
|
+
|
|
13
|
+
python3 ux_doctor.py [path] # report
|
|
14
|
+
python3 ux_doctor.py [path] --fix # apply the safe subset
|
|
15
|
+
python3 ux_doctor.py [path] --brief # one line, for sweeping many projects
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
CURRENT = 4
|
|
25
|
+
|
|
26
|
+
# What each contract version introduced, so a report can say what is missing
|
|
27
|
+
# rather than only that something is old.
|
|
28
|
+
HISTORY = {
|
|
29
|
+
2: "`Traces:` on every scenario + traceability rules; `foundation.md` (personas, JTBD, journeys, stories)",
|
|
30
|
+
3: "scenarios become use cases (`user action -> system response`), `Alt paths`, `Traces` includes `FLW-NN`; `flows.md`; `plans/`",
|
|
31
|
+
4: "`screens.md` — one `SCR-NN` entry per screen with per-state Figma frames; flows reference screens by id instead of respecifying them",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Additive since v4 — absence is not a version problem, but it is worth naming.
|
|
35
|
+
ADDITIVE = [
|
|
36
|
+
("foundation.md", "## Product mechanics", "Product mechanics section (personalization, engagement, a11y regime) — 0.26.1"),
|
|
37
|
+
("scenarios.md", "**Telemetry:**", "`Telemetry` on scenarios — the bridge to analytics practices, 0.28.0"),
|
|
38
|
+
("foundation.md", "**Kill criteria:**", "`Kill criteria` on stories — gives `dropped` a definition, 0.28.0"),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
ARTIFACTS = ["foundation.md", "flows.md", "screens.md", "scenarios.md"]
|
|
42
|
+
|
|
43
|
+
# Names the contract owns, and the near-misses seen in the wild.
|
|
44
|
+
RENAMES = {
|
|
45
|
+
"ux-scenarios.md": "scenarios.md",
|
|
46
|
+
"ux-flows.md": "flows.md",
|
|
47
|
+
"ux-foundation.md": "foundation.md",
|
|
48
|
+
"ux-screens.md": "screens.md",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def find_ux_dir(arg: str | None) -> Path | None:
|
|
53
|
+
base = Path(arg) if arg else Path.cwd()
|
|
54
|
+
for cand in (base, base / "docs" / "ux", base.parent if base.name else base):
|
|
55
|
+
if any((cand / a).exists() for a in ARTIFACTS) or (cand / "ux-scenarios.md").exists():
|
|
56
|
+
return cand
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def marker(path: Path) -> int | None:
|
|
61
|
+
try:
|
|
62
|
+
head = path.read_text(encoding="utf-8", errors="ignore")[:2000]
|
|
63
|
+
except OSError:
|
|
64
|
+
return None
|
|
65
|
+
m = re.search(r"ux-contract v(\d+)", head)
|
|
66
|
+
return int(m.group(1)) if m else None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def diagnose(ux: Path) -> dict:
|
|
70
|
+
present = {a: (ux / a).exists() for a in ARTIFACTS}
|
|
71
|
+
markers = {a: marker(ux / a) for a in ARTIFACTS if present[a]}
|
|
72
|
+
known = [v for v in markers.values() if v is not None]
|
|
73
|
+
|
|
74
|
+
misnamed = {
|
|
75
|
+
wrong: right
|
|
76
|
+
for wrong, right in RENAMES.items()
|
|
77
|
+
if (ux / wrong).exists() and not (ux / right).exists()
|
|
78
|
+
}
|
|
79
|
+
# Audit reports loose in docs/ux instead of docs/ux/audits/
|
|
80
|
+
loose = sorted(
|
|
81
|
+
p.name for p in ux.glob("*.md")
|
|
82
|
+
if re.search(r"audit", p.name, re.I) and p.name not in ARTIFACTS
|
|
83
|
+
)
|
|
84
|
+
audits = sorted(p.name for p in (ux / "audits").glob("*.md")) if (ux / "audits").is_dir() else []
|
|
85
|
+
|
|
86
|
+
missing_additive = []
|
|
87
|
+
for fname, needle, label in ADDITIVE:
|
|
88
|
+
f = ux / fname
|
|
89
|
+
if f.exists() and needle not in f.read_text(encoding="utf-8", errors="ignore"):
|
|
90
|
+
missing_additive.append(label)
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"present": present,
|
|
94
|
+
"markers": markers,
|
|
95
|
+
"effective": min(known) if known else None,
|
|
96
|
+
"mixed": len(set(known)) > 1,
|
|
97
|
+
"misnamed": misnamed,
|
|
98
|
+
"loose_audits": loose,
|
|
99
|
+
"audits": audits,
|
|
100
|
+
"missing_additive": missing_additive,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def report(ux: Path, d: dict) -> int:
|
|
105
|
+
problems = 0
|
|
106
|
+
print(f"docs/ux: {ux}")
|
|
107
|
+
|
|
108
|
+
eff, markers = d["effective"], d["markers"]
|
|
109
|
+
unmarked = [a for a, v in markers.items() if v is None]
|
|
110
|
+
|
|
111
|
+
if d["mixed"]:
|
|
112
|
+
problems += 1
|
|
113
|
+
detail = ", ".join(f"{a} v{v}" for a, v in sorted(markers.items()) if v)
|
|
114
|
+
print(f" MIXED CONTRACT: {detail}")
|
|
115
|
+
print(" Each artifact was last touched by a different version and nothing reconciled them.")
|
|
116
|
+
elif eff is None:
|
|
117
|
+
problems += 1
|
|
118
|
+
print(" NO CONTRACT MARKER on any artifact — the chain is unmanaged.")
|
|
119
|
+
elif eff < CURRENT:
|
|
120
|
+
problems += 1
|
|
121
|
+
print(f" BEHIND: contract v{eff}, current v{CURRENT}")
|
|
122
|
+
|
|
123
|
+
if unmarked and not d["mixed"]:
|
|
124
|
+
problems += 1
|
|
125
|
+
print(f" UNMARKED: {', '.join(sorted(unmarked))}")
|
|
126
|
+
|
|
127
|
+
lowest = eff if eff is not None else 1
|
|
128
|
+
for v in range(lowest + 1, CURRENT + 1):
|
|
129
|
+
if v in HISTORY:
|
|
130
|
+
print(f" v{v} added — {HISTORY[v]}")
|
|
131
|
+
|
|
132
|
+
for wrong, right in d["misnamed"].items():
|
|
133
|
+
problems += 1
|
|
134
|
+
print(f" MISNAMED: {wrong} -> {right} (the tooling looks for the contract name and finds nothing)")
|
|
135
|
+
|
|
136
|
+
missing_core = [a for a, ok in d["present"].items() if not ok]
|
|
137
|
+
if missing_core:
|
|
138
|
+
print(f" ABSENT: {', '.join(missing_core)}")
|
|
139
|
+
|
|
140
|
+
if d["loose_audits"]:
|
|
141
|
+
problems += 1
|
|
142
|
+
print(f" AUDITS OUTSIDE audits/: {', '.join(d['loose_audits'])}")
|
|
143
|
+
|
|
144
|
+
if d["audits"] and not d["present"].get("scenarios.md"):
|
|
145
|
+
problems += 1
|
|
146
|
+
print(f" {len(d['audits'])} audit report(s) but no scenarios.md — audited against a base that is not there")
|
|
147
|
+
|
|
148
|
+
for label in d["missing_additive"]:
|
|
149
|
+
print(f" optional, absent: {label}")
|
|
150
|
+
|
|
151
|
+
print(" OK — on the current contract" if problems == 0 else f" {problems} problem(s)")
|
|
152
|
+
return problems
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def fix(ux: Path, d: dict) -> list[str]:
|
|
156
|
+
"""Only the changes that cannot be wrong."""
|
|
157
|
+
done = []
|
|
158
|
+
for wrong, right in d["misnamed"].items():
|
|
159
|
+
(ux / wrong).rename(ux / right)
|
|
160
|
+
done.append(f"renamed {wrong} -> {right}")
|
|
161
|
+
if d["loose_audits"]:
|
|
162
|
+
(ux / "audits").mkdir(exist_ok=True)
|
|
163
|
+
for name in d["loose_audits"]:
|
|
164
|
+
(ux / name).rename(ux / "audits" / name)
|
|
165
|
+
done.append(f"moved {name} -> audits/")
|
|
166
|
+
return done
|
|
167
|
+
|
|
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
|
+
|
|
207
|
+
def main() -> int:
|
|
208
|
+
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
|
209
|
+
ux = find_ux_dir(args[0] if args else None)
|
|
210
|
+
if ux is None:
|
|
211
|
+
print("no UX docs found (docs/ux/). Run /ux to set one up.")
|
|
212
|
+
return 0
|
|
213
|
+
|
|
214
|
+
d = diagnose(ux)
|
|
215
|
+
if "--brief" in sys.argv:
|
|
216
|
+
eff = d["effective"]
|
|
217
|
+
state = "mixed" if d["mixed"] else (f"v{eff}" if eff else "unmarked")
|
|
218
|
+
flags = []
|
|
219
|
+
if d["misnamed"]:
|
|
220
|
+
flags.append("misnamed")
|
|
221
|
+
if d["loose_audits"]:
|
|
222
|
+
flags.append("loose-audits")
|
|
223
|
+
if d["audits"] and not d["present"].get("scenarios.md"):
|
|
224
|
+
flags.append("audits-without-base")
|
|
225
|
+
print(f"{ux} {state}{' ' + ','.join(flags) if flags else ''}")
|
|
226
|
+
return 0
|
|
227
|
+
|
|
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
|
+
|
|
237
|
+
if "--fix" in sys.argv:
|
|
238
|
+
applied = fix(ux, d)
|
|
239
|
+
print("\nApplied:" if applied else "\nNothing to apply automatically.")
|
|
240
|
+
for line in applied:
|
|
241
|
+
print(f" {line}")
|
|
242
|
+
if applied:
|
|
243
|
+
print(" Re-run without --fix to see what is left for a human.")
|
|
244
|
+
elif problems:
|
|
245
|
+
print("\n --fix applies the mechanical subset (renames, moving audit reports).")
|
|
246
|
+
print(" Contract upgrades are content decisions: run /ux-update for those.")
|
|
247
|
+
return 0
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
sys.exit(main())
|
|
@@ -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>
|