feat(seo-data): content_quality verb — deterministic filler/AI-slop signal

Cherry-picked from claude-seo (github.com/AgriciDaniel/claude-seo, MIT)
content_quality.py, rewritten to the lib/seo-data contract per BDR-070. The
Content Shape axis was 100% LLM judgement; this gives it a measured input.

fetch.sh content_quality (stdin or --file) → {filler_score, ai_pattern_score,
information_density, overall_quality, flags[], matches{}}. 100% deterministic:
QRG §4.6 filler list (26 phrases) + AI-pattern list (46) kept intact, regex
matching, no LLM. Stdlib only (argparse/json/re/sys/collections/typing).

Advisory, NOT a verdict — the point of the wiring. It never claims a page "is
AI-written" (LRN-131/133); flags are candidates for human review. geo-analyzer
STEP 8 Check 10 makes it a deterministic input that INFORMS checks 1-9, never
replaces them, never scored on its own. A low number is not an automatic
finding.

Detection proven both directions (a detector that always- or never-flags is
useless): filler+slop text → flags [filler, low-density], overall 34-49; clean
dense factual text (dates/EUR/percentages) → no flags, overall 90. Empty input →
degraded/empty_input, never zeros-as-a-result.

Verified: GATE 1 verifier CONFORME 10/10 (both directions exercised live, lists
diffed intact vs source, advisory language confirmed); GATE 2 self-scan clean
(only sink is read-only open() for --file); seo-data 190 → 210 pass, 0 fail;
full suite green; shellcheck + py_compile clean.
This commit is contained in:
Bastien Chanot
2026-07-17 19:06:58 +02:00
parent fb0b587240
commit b271e83fb6
5 changed files with 371 additions and 1 deletions
+35
View File
@@ -248,6 +248,41 @@ fetch.sh schema_gen <reservation|order|discussion|profile> [flags] [--script-tag
empty (argparse cannot catch that) → `{"status":"degraded",...}`,
exit 0 — fail-open, never a traceback.
fetch.sh content_quality [--file <path.txt>] < text_on_stdin
→ {"status":"ok","source":"content_quality","filler_score":0,"ai_pattern_score":0,
"information_density":1.0,"overall_quality":90,"flags":[],
"matches":{"filler":[],"ai_patterns":[]}}
→ {"status":"degraded","reason":"empty_input"|"<file error>"}
fetch.sh content_quality --file article.txt
printf '%s' "$BODY_TEXT" | fetch.sh content_quality
Adapted from claude-seo's `content_quality.py` (MIT) into this contract.
100% deterministic — regex/word-lists (QRG §4.6 filler phrases + a
Wikipedia "AI Cleanup" catalogue of LLM-typical phrasings, CC BY-SA 4.0),
no LLM call, no network. Reads the text to score from `--file <path>` or,
when `--file` is `-` or omitted, from stdin — the same idiom `score.py`
uses for `--findings`.
• **ADVISORY, NOT A VERDICT.** The output never claims "this text is
AI-written" — modern generative tools can pass every heuristic here,
and human writers use some of these phrases too. `flags` are
candidates for HUMAN REVIEW, never an automatic finding. geo-analyzer
STEP 8 (Content Shape for AI) treats `overall_quality`/`flags` as ONE
measured input that INFORMS the axis; the axis itself stays an LLM
judgement (30/70, Definition Lead), never replaced by this score.
• `filler_score`/`ai_pattern_score` (0-100, higher = worse) count
phrase-list hits scaled per 1000 tokens; `information_density`
(0.0-1.0) is entities + numbers per 100 tokens; `overall_quality`
(0-100, higher is better) is the weighted composite (also folds in a
bigram-repetition penalty even though that score isn't itself a
top-level field). `flags` fires at fixed thresholds: `filler`,
`ai-patterns`, `low-density`, `repetitive`.
• Stdlib only (argparse/json/re/sys/collections/typing) — runs even
without the venv. Empty/whitespace-only input degrades rather than
returning a false zero-value "ok": an empty analysis is not a result.
• This is filler/AI-pattern SHAPE, not fact-checking — a text can be
dense and well-cited yet still wrong; that stays a human/LLM call.
fetch.sh drift --url https://ex.com/sitemap.xml [--max 500]
→ {"status":"ok","baseline":true,"captured":"…","pages":24,"store":"…"}
→ {"status":"ok","baseline":false,"since":"…","gone":[…],"new":[…],