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.
@@ -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>