super-ux 0.61.0 → 0.62.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 +41 -0
- package/package.json +1 -1
- package/plugins/super-ux/scripts/brand_lint.py +79 -13
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
## 0.62.0 (2026-10-19): retrieval crawlers, the audit's brief, landing promises, Copylot sweep
|
|
2
|
+
|
|
3
|
+
- **`B050` judges retrieval agents only.** The AI-search contradiction is a block
|
|
4
|
+
of `OAI-SearchBot`, `ChatGPT-User`, `Claude-SearchBot`, `Claude-User`,
|
|
5
|
+
`PerplexityBot` or `Perplexity-User` while `channels.md` declares AI search a
|
|
6
|
+
target. Training and dataset tokens (`GPTBot`, `ClaudeBot`, `CCBot`,
|
|
7
|
+
`Google-Extended`) are a separate decision and are never reported; before
|
|
8
|
+
this release a `GPTBot` block was an error and an `OAI-SearchBot` block was
|
|
9
|
+
invisible. robots.txt is read in RFC 9309 groups: shared `User-agent` lines,
|
|
10
|
+
the `*` fallback for an agent with no group of its own, case-insensitive
|
|
11
|
+
tokens, and an equal-length `Allow: /` beating `Disallow: /`. Each purpose is
|
|
12
|
+
cited from the vendor's own crawler page in the code and the contract.
|
|
13
|
+
Fifteen new fixtures, each defect planted in the linter and watched red.
|
|
14
|
+
- **`seo-aeo-safety.md`:** a "Crawler policy: two decisions" section with the
|
|
15
|
+
vendor table; the technical floor replaced by a pointer to seo-aeo-audit's
|
|
16
|
+
brief mode (`references/build-time.md`, ships with seo-aeo-audit 0.28); no
|
|
17
|
+
`FAQPage`/`HowTo` markup for rich results (Google's changelog: HowTo removed
|
|
18
|
+
in 2023, FAQ rich results gone from 7 May 2026); the keyword-stuffing text now
|
|
19
|
+
states what `B051` enforces (pages of 40 or more words, a word repeated 5 or
|
|
20
|
+
more times above a 4% share, registered terms exempt, advisory) instead of
|
|
21
|
+
the retired 1% rule; Claude's row in the engine table reflects web search.
|
|
22
|
+
- **`landing-pages.md` LP-21..LP-24:** an offer condition next to its claim and
|
|
23
|
+
the same in the ad; lead with the problem and name AI only where it changes
|
|
24
|
+
the outcome; a "works with X" claim waits for a measured acceptance test in
|
|
25
|
+
`facts.md`; one job per page and creatives judged by the qualified-step rate,
|
|
26
|
+
not CTR. Readiness-check commands for LP-21, LP-23 and LP-24; LP-22 stays a
|
|
27
|
+
reading. Each rule cites the regulator or platform policy it rests on.
|
|
28
|
+
- **`copywriting`:** runs `copylot.check` beside the brand linter when the tool
|
|
29
|
+
is in the session (target `register: web`, `format: web-page`; until
|
|
30
|
+
Copylot's schema lists them, `marketing` and no format, said out loud) and
|
|
31
|
+
prints a fallback line when it is absent.
|
|
32
|
+
- **Synthetic respondents are hypotheses:** `research-evidence.md`,
|
|
33
|
+
`ux-foundation` and `ux-scenarios` say a synthetic persona or LLM "digital
|
|
34
|
+
twin" is never evidence for a persona or scenario decision (cited: Nielsen
|
|
35
|
+
Norman Group, 2024). `audit-depth.md` adds the quarterly fresh-user walk:
|
|
36
|
+
clean install or new account, from the ad or landing page to paid use with a
|
|
37
|
+
real payment method, on the smallest supported screen, findings recorded
|
|
38
|
+
against scenarios.
|
|
39
|
+
- Contract copies synchronized with `python3 test/sync_references.py`; seeded
|
|
40
|
+
`docs/brand/lint.py` re-copied. Discovery descriptions unchanged.
|
|
41
|
+
|
|
1
42
|
## 0.61.0 (2026-10-18): no em dash in new text
|
|
2
43
|
|
|
3
44
|
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.
|
|
3
|
+
"version": "0.62.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
|
-
|
|
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)}
|
|
2176
|
-
f"
|
|
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)
|