super-ux 0.61.0 → 0.63.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 CHANGED
@@ -1,3 +1,66 @@
1
+ ## 0.63.0 (2026-10-20): short descriptions, triggers in the body
2
+
3
+ - **Every skill description is a router of 279 to 393 characters** (was 332 to
4
+ 948). The operator's Claude Code skill listing measured 29,995 of about
5
+ 30,000 characters for 134 skills, and 81 skills showed no description at
6
+ all; a plugin skill cannot be hidden, so length is the only lever. Each
7
+ description is one "Use when" sentence, four to six English and Russian
8
+ triggers, one "Not for" boundary naming the sibling, and the refusal
9
+ phrase where the route has one. Total for the seven skills: 4,426 to 2,436.
10
+ - **Every removed trigger phrase moved verbatim into a `## Triggers` section**
11
+ of its SKILL.md body, so the family routing hook's words still appear in the
12
+ skill's own text.
13
+ - **`ux-flows` Improve procedure moved to `references/flow-improve.md`**, text
14
+ unchanged apart from link paths, so the body stays inside the house audit's
15
+ 4,750-token working limit with the new `## Triggers` section (4,712 tokens
16
+ before, 4,564 after).
17
+ - **Commands carry one short sentence each** (60 to 89 characters, was 99 to
18
+ 224; total 2,424 to 1,116).
19
+ - **`validate.py` holds the budget:** a skill description over 500 characters
20
+ or without a "Not for" boundary fails, a command description over 120 fails,
21
+ and the trigger-position check reads `Triggers:` as well as `Triggers - `.
22
+
23
+ ## 0.62.0 (2026-10-19): retrieval crawlers, the audit's brief, landing promises, Copylot sweep
24
+
25
+ - **`B050` judges retrieval agents only.** The AI-search contradiction is a block
26
+ of `OAI-SearchBot`, `ChatGPT-User`, `Claude-SearchBot`, `Claude-User`,
27
+ `PerplexityBot` or `Perplexity-User` while `channels.md` declares AI search a
28
+ target. Training and dataset tokens (`GPTBot`, `ClaudeBot`, `CCBot`,
29
+ `Google-Extended`) are a separate decision and are never reported; before
30
+ this release a `GPTBot` block was an error and an `OAI-SearchBot` block was
31
+ invisible. robots.txt is read in RFC 9309 groups: shared `User-agent` lines,
32
+ the `*` fallback for an agent with no group of its own, case-insensitive
33
+ tokens, and an equal-length `Allow: /` beating `Disallow: /`. Each purpose is
34
+ cited from the vendor's own crawler page in the code and the contract.
35
+ Fifteen new fixtures, each defect planted in the linter and watched red.
36
+ - **`seo-aeo-safety.md`:** a "Crawler policy: two decisions" section with the
37
+ vendor table; the technical floor replaced by a pointer to seo-aeo-audit's
38
+ brief mode (`references/build-time.md`, ships with seo-aeo-audit 0.28); no
39
+ `FAQPage`/`HowTo` markup for rich results (Google's changelog: HowTo removed
40
+ in 2023, FAQ rich results gone from 7 May 2026); the keyword-stuffing text now
41
+ states what `B051` enforces (pages of 40 or more words, a word repeated 5 or
42
+ more times above a 4% share, registered terms exempt, advisory) instead of
43
+ the retired 1% rule; Claude's row in the engine table reflects web search.
44
+ - **`landing-pages.md` LP-21..LP-24:** an offer condition next to its claim and
45
+ the same in the ad; lead with the problem and name AI only where it changes
46
+ the outcome; a "works with X" claim waits for a measured acceptance test in
47
+ `facts.md`; one job per page and creatives judged by the qualified-step rate,
48
+ not CTR. Readiness-check commands for LP-21, LP-23 and LP-24; LP-22 stays a
49
+ reading. Each rule cites the regulator or platform policy it rests on.
50
+ - **`copywriting`:** runs `copylot.check` beside the brand linter when the tool
51
+ is in the session (target `register: web`, `format: web-page`; until
52
+ Copylot's schema lists them, `marketing` and no format, said out loud) and
53
+ prints a fallback line when it is absent.
54
+ - **Synthetic respondents are hypotheses:** `research-evidence.md`,
55
+ `ux-foundation` and `ux-scenarios` say a synthetic persona or LLM "digital
56
+ twin" is never evidence for a persona or scenario decision (cited: Nielsen
57
+ Norman Group, 2024). `audit-depth.md` adds the quarterly fresh-user walk:
58
+ clean install or new account, from the ad or landing page to paid use with a
59
+ real payment method, on the smallest supported screen, findings recorded
60
+ against scenarios.
61
+ - Contract copies synchronized with `python3 test/sync_references.py`; seeded
62
+ `docs/brand/lint.py` re-copied. Discovery descriptions unchanged.
63
+
1
64
  ## 0.61.0 (2026-10-18): no em dash in new text
2
65
 
3
66
  Owner decision, 2026-10-18: text an agent writes carries no em dash (U+2014), in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "super-ux",
3
- "version": "0.61.0",
3
+ "version": "0.63.0",
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"
@@ -1794,7 +1794,79 @@ def _ios_keywords(path: str, fields: dict, raw: str) -> list[Finding]:
1794
1794
  return findings
1795
1795
 
1796
1796
 
1797
- AI_AGENTS = ("GPTBot", "ClaudeBot", "PerplexityBot", "Google-Extended")
1797
+ # B050 judges RETRIEVAL agents only: the ones that fetch a page to show, cite
1798
+ # or answer from it. Blocking one while AI search is a declared target is the
1799
+ # contradiction. Each purpose is the vendor's own wording, read 2026-10-19:
1800
+ # OAI-SearchBot "surface websites in search results in ChatGPT's search
1801
+ # features"; ChatGPT-User user-initiated fetches
1802
+ # (https://developers.openai.com/api/docs/bots)
1803
+ # Claude-SearchBot indexes content for search; Claude-User retrieves a
1804
+ # page in response to a user query
1805
+ # (https://support.claude.com/en/articles/8896518)
1806
+ # PerplexityBot "surface and link websites in search results on
1807
+ # Perplexity"; Perplexity-User fetches for a user's question
1808
+ # (https://docs.perplexity.ai/guides/bots)
1809
+ RETRIEVAL_AGENTS = (
1810
+ "OAI-SearchBot", "ChatGPT-User", "Claude-SearchBot", "Claude-User",
1811
+ "PerplexityBot", "Perplexity-User",
1812
+ )
1813
+ # TRAINING and dataset tokens: blocking them is a separate decision with no
1814
+ # effect on AI search, so B050 never reports them. GPTBot and ClaudeBot are
1815
+ # the vendors' training crawlers (same pages); Google-Extended controls Gemini
1816
+ # training and grounding and "does not impact a site's inclusion in Google
1817
+ # Search" (https://developers.google.com/crawling/docs/crawlers-fetchers/google-common-crawlers);
1818
+ # CCBot builds Common Crawl's open archive (https://commoncrawl.org/ccbot).
1819
+ TRAINING_AGENTS = ("GPTBot", "ClaudeBot", "CCBot", "Google-Extended")
1820
+ # Whole-site paths and their rule length. Google's matcher ignores a trailing
1821
+ # `*` (so `/*` and a bare `*` weigh what `/` weighs) and reads `$` as the end
1822
+ # of the path; the longer rule wins and Allow wins a tie (RFC 9309 2.2.2).
1823
+ # So `Disallow: /*` against `Allow: /` is a tie Allow wins, while
1824
+ # `Disallow: /*$` (3 octets) outranks `Allow: /`.
1825
+ FULL_PATHS = {"/": 1, "/*": 1, "*": 1, "/*$": 3}
1826
+
1827
+
1828
+ def robots_groups(text: str) -> list[tuple[list[str], list[tuple[str, str]]]]:
1829
+ """RFC 9309 groups: consecutive `User-agent` lines share the rules after
1830
+ them, and a `User-agent` line after a rule opens a new group. Tokens are
1831
+ lower-cased, because product tokens match case-insensitively, and cut to
1832
+ their leading [a-z_-] run, as Google's matcher does, so `OAI-SearchBot/1.0`
1833
+ still names OAI-SearchBot. A UTF-8 BOM before the first line is dropped."""
1834
+ text = text.lstrip("\ufeff")
1835
+ groups: list[tuple[list[str], list[tuple[str, str]]]] = []
1836
+ agents: list[str] = []
1837
+ rules: list[tuple[str, str]] = []
1838
+ for raw in text.splitlines():
1839
+ line = raw.split("#", 1)[0].strip()
1840
+ m = re.match(r"^(user-agent|allow|disallow)\s*:\s*(.*?)\s*$", line, re.I)
1841
+ if not m:
1842
+ continue
1843
+ key, value = m.group(1).lower(), m.group(2)
1844
+ if key == "user-agent":
1845
+ if rules:
1846
+ groups.append((agents, rules))
1847
+ agents, rules = [], []
1848
+ token = re.match(r"\*(?=\s|$)|[a-z_-]*", value.lower()).group(0)
1849
+ agents.append(token or value.lower())
1850
+ elif agents:
1851
+ rules.append((key, value))
1852
+ if agents:
1853
+ groups.append((agents, rules))
1854
+ return groups
1855
+
1856
+
1857
+ def robots_blocks(text: str, token: str) -> bool:
1858
+ """True when robots.txt shuts `token` out of the whole site. The agent
1859
+ obeys the groups naming it, or the `*` groups when none does; an
1860
+ equal-length `Allow` beats `Disallow` (RFC 9309 section 2.2.2)."""
1861
+ groups = robots_groups(text)
1862
+ own = [r for a, r in groups if token.lower() in a]
1863
+ rules = [rule for rs in (own or [r for a, r in groups if "*" in a])
1864
+ for rule in rs]
1865
+ def longest(kind: str) -> int:
1866
+ return max((FULL_PATHS[v] for k, v in rules
1867
+ if k == kind and v in FULL_PATHS), default=0)
1868
+ disallow, allow = longest("disallow"), longest("allow")
1869
+ return disallow > 0 and disallow > allow
1798
1870
 
1799
1871
  FILLER_OPENERS = (
1800
1872
  "in today's digital landscape", "in the ever-evolving world",
@@ -2159,21 +2231,15 @@ def check_bot_safety(brand_dir: Path, sources: dict) -> list[Finding]:
2159
2231
  (p, {}, "") for p in sources["robots"]
2160
2232
  ]:
2161
2233
  robots = read(brand_dir.parent.parent / path) or ""
2162
- blocks = []
2163
- agent = None
2164
- for line in robots.splitlines():
2165
- head = re.match(r"^User-agent:\s*(.+?)\s*$", line, re.I)
2166
- if head:
2167
- agent = head.group(1).strip()
2168
- elif re.match(r"^Disallow:\s*/\s*$", line, re.I) and agent:
2169
- if agent in AI_AGENTS:
2170
- blocks.append(agent)
2234
+ blocks = [t for t in RETRIEVAL_AGENTS if robots_blocks(robots, t)]
2171
2235
  if blocks:
2172
2236
  findings.append(Finding(
2173
2237
  "B050", SEVERITY_ERROR, path, 0,
2174
- f"channels.md declares AI search a target while "
2175
- f"{', '.join(blocks)} is blocked here -- content quality "
2176
- f"is irrelevant to a crawler that never arrives",
2238
+ f"channels.md declares AI search a target while the "
2239
+ f"retrieval agent(s) {', '.join(blocks)} are blocked here. "
2240
+ f"An answer engine cannot cite a page it may not fetch. "
2241
+ f"Training crawlers ({', '.join(TRAINING_AGENTS)}) are a "
2242
+ f"separate decision and are not judged here",
2177
2243
  ))
2178
2244
 
2179
2245
  records = surfaces(brand_dir)