super-ux 0.59.2 → 0.59.3
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 +13 -0
- package/README.md +1 -1
- package/cursor/rules/copywriting.mdc +4 -0
- package/package.json +1 -1
- package/plugins/super-ux/scripts/brand_lint.py +41 -1
- package/plugins/super-ux/scripts/ux_lint.py +11 -0
- package/templates/brand/README.md +1 -0
- package/templates/brand/copy-brief.md +10 -0
- package/templates/foundation.md +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## 0.59.3 — accountable journeys and explicit copy inputs
|
|
2
|
+
|
|
3
|
+
- Journeys record an Owner, with `unknown` supported. `U086` advises migration
|
|
4
|
+
for missing/blank/duplicate owners without assigning anyone automatically.
|
|
5
|
+
- Marketing writing uses a current `copy-brief.md` with success metric,
|
|
6
|
+
constraints and a reference to the voice pair alongside the original inputs.
|
|
7
|
+
`B074` advises existing packs; `B075` checks present records. Unresolved values
|
|
8
|
+
remain `unknown`; unrelated tasks do not load a brief.
|
|
9
|
+
- Seeds and every per-skill contract copy are synchronized. Discovery descriptions
|
|
10
|
+
are unchanged. No installation or provider operation was performed.
|
|
11
|
+
- CI uses nightly runs and deliberate ready-for-review promotion, preserving jobs.
|
|
12
|
+
[FW05 source receipt](docs/evidence/runs/2026-10-13-fw05-journey-copy.md).
|
|
13
|
+
|
|
1
14
|
## 0.59.2 — brand-lint names the line, and a release refuses a lightweight tag
|
|
2
15
|
|
|
3
16
|
- **`B022` reports the line.** An unregistered literal was reported at line 0 —
|
package/README.md
CHANGED
|
@@ -161,7 +161,7 @@ Commands: `/brand` (status → one recommended action), `/brand-init`,
|
|
|
161
161
|
python3 docs/brand/lint.py
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
48 deterministic checks (`B001`..`B073`): banned words, one action under two names, a figure
|
|
165
165
|
with no sourced fact, a field over its limit with the locale coefficient
|
|
166
166
|
applied, blocked AI crawlers, keyword stuffing, humor on a billing screen,
|
|
167
167
|
a rhetorical dash, a title that ends in a full stop, a locale that lags
|
|
@@ -21,6 +21,10 @@ locale policy. Missing terms or facts are reported, never invented. This pass
|
|
|
21
21
|
owns product text and `strings.md` rows (`Status: proposed`); claim coordinated
|
|
22
22
|
files before editing. A file living in `docs/brand/` does not forbid the edit.
|
|
23
23
|
|
|
24
|
+
For marketing writing only, use the copywriting skill’s `marketing-copy.md`
|
|
25
|
+
→ Before writing contract for the current brief; keep unknown inputs explicit.
|
|
26
|
+
Other tasks do not load this brief.
|
|
27
|
+
|
|
24
28
|
## Rules that hold in every voice
|
|
25
29
|
|
|
26
30
|
- **No decorative full stop in headings or labels**, even when inline markup
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "super-ux",
|
|
3
|
-
"version": "0.59.
|
|
3
|
+
"version": "0.59.3",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"test": "python3 test/validate.py && python3 test/brand_lint_test.py && python3 test/display_copy_test.py && python3 test/ux_lint_test.py && python3 docs/ux/lint.py && python3 docs/brand/lint.py && node test/installer_test.js && npm run test:audit",
|
|
6
6
|
"test:audit": "for t in test/audit_regressions/*.py; do python3 \"$t\" || exit 1; done"
|
|
@@ -81,7 +81,7 @@ STRING_KINDS = ("copy", "layout")
|
|
|
81
81
|
|
|
82
82
|
SEVERITY_ERROR = "error"
|
|
83
83
|
SEVERITY_WARN = "warn"
|
|
84
|
-
WARNING_CODES = set("B003 B005 B007 B022 B025 B026 B031 B043 B044 B045 B046 B051 B053 B054 B060 B063 B064 B066 B071 B072".split())
|
|
84
|
+
WARNING_CODES = set("B003 B005 B007 B022 B025 B026 B031 B043 B044 B045 B046 B051 B053 B054 B060 B063 B064 B066 B071 B072 B074".split())
|
|
85
85
|
|
|
86
86
|
Finding = namedtuple("Finding", "code severity path line message")
|
|
87
87
|
|
|
@@ -2574,6 +2574,45 @@ def apply_fixes(brand_dir: Path, findings: list[Finding]) -> int:
|
|
|
2574
2574
|
return rewritten
|
|
2575
2575
|
|
|
2576
2576
|
|
|
2577
|
+
COPY_BRIEF_FIELDS = ("Surface", "Action", "Reader", "Customer words", "Proof",
|
|
2578
|
+
"Success metric", "Constraints", "Voice reference")
|
|
2579
|
+
|
|
2580
|
+
|
|
2581
|
+
def check_copy_brief(brand_dir: Path, sources: dict) -> list[Finding]:
|
|
2582
|
+
"""B074/B075: one current marketing brief, with explicit unknowns permitted."""
|
|
2583
|
+
path = brand_dir / "copy-brief.md"
|
|
2584
|
+
if not path.exists():
|
|
2585
|
+
return [Finding("B074", SEVERITY_WARN, "copy-brief.md", 0,
|
|
2586
|
+
"marketing sources have no current copy brief; seed one "
|
|
2587
|
+
"before the next writing task, retaining unknown inputs")] if sources.get("marketing") else []
|
|
2588
|
+
text = read(path)
|
|
2589
|
+
if text is None:
|
|
2590
|
+
return [Finding("B075", SEVERITY_ERROR, "copy-brief.md", 0,
|
|
2591
|
+
"current copy brief cannot be read")]
|
|
2592
|
+
text = re.sub(r"<!--.*?-->", "", text, flags=re.S)
|
|
2593
|
+
fields = {}
|
|
2594
|
+
findings = []
|
|
2595
|
+
for field in COPY_BRIEF_FIELDS:
|
|
2596
|
+
values = re.findall(rf"(?m)^{re.escape(field)}:[ \t]*(.*)$", text)
|
|
2597
|
+
if len(values) != 1 or unfilled(values[0]):
|
|
2598
|
+
findings.append(Finding("B075", SEVERITY_ERROR, "copy-brief.md", 0,
|
|
2599
|
+
f"needs one nonempty {field}: value; use unknown for an unresolved input"))
|
|
2600
|
+
else:
|
|
2601
|
+
fields[field] = values[0].strip()
|
|
2602
|
+
ref = fields.get("Voice reference", "unknown")
|
|
2603
|
+
if ref.lower() != "unknown":
|
|
2604
|
+
voice = re.sub(r"<!--.*?-->", "", read(brand_dir / "voice.md") or "", flags=re.S)
|
|
2605
|
+
section = re.search(r"(?ms)^## Voice references[ \t]*\n(.*?)(?=^## |\Z)", voice)
|
|
2606
|
+
pair = [re.search(rf"(?m)^[ \t]*(?:-[ \t]*)?\*\*{field}:\*\*[ \t]*(.*)$", section[1] if section else "")
|
|
2607
|
+
for field in ("Admired", "Refused")]
|
|
2608
|
+
if (ref != "voice.md#voice-references" or not section
|
|
2609
|
+
or any(not m or unfilled(m[1]) or m[1].strip().lower() == "unknown" for m in pair)):
|
|
2610
|
+
findings.append(Finding("B075", SEVERITY_ERROR, "copy-brief.md", 0,
|
|
2611
|
+
"Voice reference must be unknown or voice.md#voice-references "
|
|
2612
|
+
"with a filled Admired/Refused pair in voice.md"))
|
|
2613
|
+
return findings
|
|
2614
|
+
|
|
2615
|
+
|
|
2577
2616
|
def run(brand_dir: Path, fix: bool = False) -> list[Finding]:
|
|
2578
2617
|
"""Every check, in order. `fix` is applied by the caller via apply_fixes."""
|
|
2579
2618
|
sources = load_sources(brand_dir)
|
|
@@ -2584,6 +2623,7 @@ def run(brand_dir: Path, fix: bool = False) -> list[Finding]:
|
|
|
2584
2623
|
findings.extend(check_consistency(brand_dir, sources))
|
|
2585
2624
|
findings.extend(check_facts(brand_dir, sources))
|
|
2586
2625
|
findings.extend(check_channels(brand_dir, sources))
|
|
2626
|
+
findings.extend(check_copy_brief(brand_dir, sources))
|
|
2587
2627
|
findings.extend(check_short_video(brand_dir, sources))
|
|
2588
2628
|
findings.extend(check_bot_safety(brand_dir, sources))
|
|
2589
2629
|
findings.extend(check_ai_tells(brand_dir, sources))
|
|
@@ -735,6 +735,16 @@ def check_jobs(foundation: str, scenarios: str = "", flows: str = "",
|
|
|
735
735
|
f"{', '.join(JOB_FIELDS)}")
|
|
736
736
|
|
|
737
737
|
|
|
738
|
+
def check_journey_owners(foundation: str) -> None:
|
|
739
|
+
"""U086: absence is migration advice; unknown is an explicit unassigned owner."""
|
|
740
|
+
for sid, body in sorted(entry_blocks(foundation, "JRN").items()):
|
|
741
|
+
owners = re.findall(r"(?m)^[ \t]*(?:-[ \t]*)?\*\*Owner:\*\*[ \t]*(.*)$", body)
|
|
742
|
+
if len(owners) != 1 or _unfilled(owners[0]):
|
|
743
|
+
warn(f"[U086] foundation.md: {sid} needs one **Owner:** value — "
|
|
744
|
+
"name the accountable role/person or write unknown; "
|
|
745
|
+
"do not invent an assignment")
|
|
746
|
+
|
|
747
|
+
|
|
738
748
|
WEB_SURFACE_FIELDS = ("Route", "Answers", "Indexable", "Without JS", "Entity")
|
|
739
749
|
|
|
740
750
|
|
|
@@ -1251,6 +1261,7 @@ def main() -> int:
|
|
|
1251
1261
|
check_field_vocabulary(scenarios, foundation)
|
|
1252
1262
|
check_status_enums(scenarios, foundation, screens, flows, vision)
|
|
1253
1263
|
check_jobs(foundation, scenarios, flows, screens)
|
|
1264
|
+
check_journey_owners(foundation)
|
|
1254
1265
|
check_vision(ux, vision)
|
|
1255
1266
|
check_web_surface(screens, flows)
|
|
1256
1267
|
check_onboarding(flows, ux, project_root)
|
|
@@ -15,6 +15,7 @@ Owned by the `brand-voice` skill. Written by `copywriting`. Checked by
|
|
|
15
15
|
| `terminology.md` | our words, banned words, entity and tier names |
|
|
16
16
|
| `facts.md` | canonical numbers and proof — the only source of a figure |
|
|
17
17
|
| `channels.md` | one record per surface: register, limits, bans |
|
|
18
|
+
| `copy-brief.md` | current marketing task inputs; unknowns stay explicit |
|
|
18
19
|
| `strings.md` | interface string registry → `file:line` → scenario |
|
|
19
20
|
| `locales/<code>.md` | per-locale delta |
|
|
20
21
|
|
package/templates/foundation.md
CHANGED
|
@@ -23,6 +23,7 @@ data/observation, recognizable by a real user. -->
|
|
|
23
23
|
## Customer journeys
|
|
24
24
|
|
|
25
25
|
<!-- ### JRN-01: <persona> — <job> (JTBD-01)
|
|
26
|
+
- **Owner:** unknown
|
|
26
27
|
| # | Stage | User action | Touchpoint | Emotion (1-5) | Pain | Opportunity |
|
|
27
28
|
|---|-------|------------|------------|---------------|------|-------------|
|
|
28
29
|
-->
|