From cfdd89e73b4f61c9504cc28acb38aaf36ceb219b Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Fri, 17 Jul 2026 14:30:31 +0200 Subject: [PATCH 1/4] =?UTF-8?q?feat(seo-data):=20schema=5Fgen=20verb=20?= =?UTF-8?q?=E2=80=94=20generate=20JSON-LD,=20not=20just=20audit=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-picked from claude-seo (github.com/AgriciDaniel/claude-seo, MIT) schema_generate.py, rewritten to the lib/seo-data contract per BDR-070 — adapt, never copy. The system audited JSON-LD but could not generate it; geo-analyzer's G2 batch hand-wrote markup. Now it calls the verb. fetch.sh schema_gen {reservation|order|discussion|profile} → fail-open envelope {"status":"ok","source":"schema_gen","type":…,"jsonld":{…}}. Types: Reservation (7 subtypes), OrderAction, DiscussionForumPosting, ProfilePage (sameAs/knowsAbout for the entity graph). Stdlib only (import argparse, json — zero third-party), _strip_nones so a null is never emitted, --script-tag wraps for direct paste. Fail-open mirrors score.py's _cli exactly (the contract's named pattern): a flag-omitted required field → argparse exit 2 + {"status":"error","bad_usage"}; a flag-present-but-empty field → {"status":"degraded","reason":"missing required field: …"} exit 0. Never a traceback, never empty stdout (LRN-133: the can't-generate case stays legible). geo-analyzer G2 wiring preserves the data-integrity rule — the verb generates STRUCTURE, unknown values stay [À COMPLÉTER], never invented. Verified: GATE 1 verifier CONFORME 10/10 (exercised the fail-open edge cases + diffed field surface against the source); GATE 2 self-scan clean (no network/shell/secret/eval sink); seo-data 167 → 190 pass, 0 fail; full suite green; shellcheck + py_compile clean. --- agents/geo-analyzer.md | 9 +- lib/seo-data/README.md | 33 ++++ lib/seo-data/fetch.sh | 4 +- lib/seo-data/schema_gen.py | 301 ++++++++++++++++++++++++++++++++++ lib/seo-data/seo-data.test.sh | 59 +++++++ 5 files changed, 404 insertions(+), 2 deletions(-) create mode 100644 lib/seo-data/schema_gen.py diff --git a/agents/geo-analyzer.md b/agents/geo-analyzer.md index 66f845c..ad26ddf 100644 --- a/agents/geo-analyzer.md +++ b/agents/geo-analyzer.md @@ -813,7 +813,14 @@ to act without your audit context. Embed per item: - **Templates + context** — G2/G6 paste the expected JSON-LD from `geo-schemas.md` + business context (entity name, sameAs, @id canonical) + framework note. G4 follows `llms-txt-template.md` exactly. G1 pastes - the correct variant from `ai-crawlers-2026.md`. + the correct variant from `ai-crawlers-2026.md`. When a G2 item needs a + `Reservation`/`OrderAction`/`DiscussionForumPosting`/`ProfilePage` block, + generate the skeleton via `fetch.sh schema_gen + [flags]` + (`~/.claude/lib/seo-data/fetch.sh`) and fill in the real values, rather + than hand-writing that markup. The data-integrity rule still applies on + top of it: `schema_gen` only generates STRUCTURE — unknown field values + stay `[À COMPLÉTER]`, never invented to fill a flag the verb needs. - **PERMISSIVE default** on G1 unless the client flagged premium/regulated. ### Output shape diff --git a/lib/seo-data/README.md b/lib/seo-data/README.md index 747a244..3b457d0 100644 --- a/lib/seo-data/README.md +++ b/lib/seo-data/README.md @@ -215,6 +215,39 @@ fetch.sh score --findings • Malformed input is an error, never a silently wrong number — unlike the fetch verbs, a degrade here would mean bad input, not a network fact. +fetch.sh schema_gen [flags] [--script-tag] + → {"status":"ok","source":"schema_gen","type":"<@type>","jsonld":{…}} + → {"status":"error","reason":"bad_usage"} # a REQUIRED flag omitted + → {"status":"degraded","reason":"…"} # a required flag given, empty + + fetch.sh schema_gen reservation --provider "Marea NYC" \ + --start 2026-06-04T19:30:00-04:00 --party-size 4 + fetch.sh schema_gen order --merchant "Acme Pizza" --order-url https://acme.example/order + fetch.sh schema_gen discussion --headline "…" --author "Sara Park" \ + --url https://forum.example.com/t/123 --date 2026-05-12T14:00:00Z + fetch.sh schema_gen profile --name "Daniel Agrici" --url https://agricidaniel.com/about \ + --same-as https://github.com/AgriciDaniel --knows-about "SEO" "Schema markup" + + Adapted from claude-seo's `schema_generate.py` (MIT) into this contract. + Our system only AUDITS existing markup elsewhere; this is the one verb + that GENERATES it — deterministic JSON-LD skeletons for the four v2 + high-leverage Schema.org types, so geo-analyzer's G2 batch stops + hand-writing markup by hand. It only generates STRUCTURE: unknown field + VALUES are the caller's job, `[À COMPLÉTER]` for anything unconfirmed — + this verb never invents a sameAs, an email, or a business name. + • Stdlib only, no network, no auth — runs even without the venv. + • `--script-tag` wraps the cleaned jsonld in + `` under a `script` key, + still inside the `ok` envelope. It must be given AFTER the type + (`schema_gen reservation … --script-tag`, not before) — argparse + subcommand flags only parse after their subcommand. + • Never emits a JSON `null`: fields left unset are omitted from the + `jsonld` object entirely rather than serialised as `null`. + • A REQUIRED flag omitted → `{"status":"error","reason":"bad_usage"}`, + exit 2 (bad usage, like every other verb). A required flag GIVEN but + empty (argparse cannot catch that) → `{"status":"degraded",...}`, + exit 0 — fail-open, never a traceback. + 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":[…], diff --git a/lib/seo-data/fetch.sh b/lib/seo-data/fetch.sh index e2291be..9934db9 100644 --- a/lib/seo-data/fetch.sh +++ b/lib/seo-data/fetch.sh @@ -34,6 +34,8 @@ case "$cmd" in exec "$PY" "$HERE/sitemap.py" --store "$STORE" "$@" ;; score) exec "$PY" "$HERE/score.py" --store "$STORE" "$@" ;; + schema_gen) + exec "$PY" "$HERE/schema_gen.py" --store "$STORE" "$@" ;; drift) exec "$PY" "$HERE/drift.py" --store "$STORE" "$@" ;; rendercheck) @@ -52,6 +54,6 @@ case "$cmd" in fi echo '{"status":"error","reason":"usage: fetch.sh forget {--label