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 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
- 46 deterministic checks (`B001`..`B073`): banned words, one action under two names, a figure
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.2",
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
 
@@ -0,0 +1,10 @@
1
+ Contract: brand-contract v1
2
+
3
+ Surface: unknown
4
+ Action: unknown
5
+ Reader: unknown
6
+ Customer words: unknown
7
+ Proof: unknown
8
+ Success metric: unknown
9
+ Constraints: unknown
10
+ Voice reference: unknown
@@ -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
  -->