feat(seo-schema-validator): back the upgraded SKILL.md with a working 5-layer pipeline
The "Upgrade Schema Validator" commit added SKILL.md referencing files that did not exist. Implement them so the skill actually runs: - scripts/validate_schema.py — 5-layer offline validator (L0 coverage, L1 syntax, L2 vocabulary/value-format, L3 rich-result, L4 consistency) with xlsx/csv/jsonl/ json/dir/live-URL adapters. Gate = zero P0; exits 1 on failure. - scripts/schema_rules.json — curated hotel-focused, offline rule set (edit-only extension point). - scripts/make_sample.py + fixtures/sample_schema.csv — deliberately flawed fixture seeding ≥1 defect per layer; used to self-test. - references/ — validation-methodology, defect-taxonomy (25 codes), hotel-type-map. - templates/ — client-qa-report, decision-log. - code/CLAUDE.md — redirect legacy single-URL tool to the new pipeline. Noise control: MISSING_RECOMMENDED aggregated one-line-per-node; unexpected-property checks opt-in via --strict. Generalized client-specific shilla-type-map → hotel-type-map. Self-tested: default P0=5/P1=4/P2=14 FAIL, --strict --no-recommended P2=0, adapters verified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
# Validation Methodology
|
||||
|
||||
The reasoning behind the 5 layers, and the type → requirement matrix the validator
|
||||
enforces. The matrix is the human-readable mirror of `scripts/schema_rules.json` —
|
||||
if you change one, change the other.
|
||||
|
||||
## Why a machine gate before human review
|
||||
|
||||
At a few dozen entries, a person can eyeball JSON-LD. At hundreds (a multi-language,
|
||||
multi-device, multi-property hotel site easily reaches 2,000+ URLs), eyeballing
|
||||
fails in a predictable way: the reviewer drowns in *mechanical* errors (a missing
|
||||
required field, a bad date format, a typo'd URL) and never reaches the *judgement*
|
||||
errors that actually need a human (is this the right schema type for this page? is
|
||||
this description accurate?).
|
||||
|
||||
The fix is not "review harder." It is to split the work by who is best at it:
|
||||
|
||||
| Error class | Best checker | This skill |
|
||||
|---|---|---|
|
||||
| Mechanical (parse, required-present, value format, duplicate, consistency) | A script, every time | Layers 0–4, automated |
|
||||
| Judgement (type choice, copy accuracy, intent) | A human, once | Client reviews only P0-free entries |
|
||||
|
||||
So the gate runs first. **An entry reaches client review only when it has zero P0.**
|
||||
The client then reviews a clean set against a defect report — never raw JSON in a meeting.
|
||||
|
||||
## The layers, in order
|
||||
|
||||
Each layer assumes the previous one passed for that entry. A fatal L1 failure
|
||||
(unparseable JSON, no `@type`) stops deeper layers for that entry — there is nothing
|
||||
to inspect.
|
||||
|
||||
### L0 — Coverage (needs `--url-list`)
|
||||
Compares the canonical URL inventory against the URLs that actually have an entry.
|
||||
- `COVERAGE_MISSING` (P1): inventory URL with no authored entry — a gap to fill.
|
||||
- `COVERAGE_ORPHAN` (P2): entry whose URL isn't in the inventory — a typo, a stale
|
||||
path, or a list that's out of date. (Expect many orphans if your inventory is a
|
||||
subset; expect ~zero when it's the real canonical list.)
|
||||
|
||||
### L1 — Syntax
|
||||
The cheapest, hardest blockers. If these fail, nothing downstream is trustworthy.
|
||||
- `INVALID_JSON` (P0), `NO_SCHEMA_IN_HTML` (P0, live mode).
|
||||
- `MISSING_CONTEXT` / `WRONG_CONTEXT` / `NO_TYPE` / `ENCODING_CORRUPTION` (P1).
|
||||
|
||||
### L2 — Vocabulary & value formats
|
||||
Is the type known, and are values well-formed?
|
||||
- `UNKNOWN_TYPE` (P2; P1 in `--strict`): type isn't in the curated rule set. A
|
||||
*warning*, not an error — add it to `schema_rules.json` if it's intended.
|
||||
- `BAD_URL` / `BAD_DATE` / `BAD_LANG` / `BAD_CURRENCY` / `BAD_NUMBER` (P2; P1 strict).
|
||||
- `UNEXPECTED_PROPERTY` (P1, `--strict` only): a property not known for a known type.
|
||||
**Off by default** — flagging every unexpected property offline produces exactly the
|
||||
false-positive flood that makes reviewers distrust the tool.
|
||||
|
||||
### L3 — Rich-result requirements
|
||||
The contract Google enforces for eligibility.
|
||||
- `MISSING_REQUIRED` (P0): a required property is absent → the rich result is blocked.
|
||||
- `MISSING_RECOMMENDED` (P2): recommended properties absent. **Aggregated to one line
|
||||
per node** (never one defect per property) — this is the single most important
|
||||
noise-control decision in the tool.
|
||||
|
||||
### L4 — Consistency (cross-node / cross-entry)
|
||||
The errors a per-entry check can't see.
|
||||
- `PLACEHOLDER_TEXT` (P0): boilerplate that escaped authoring (`예시`, `수정필요`,
|
||||
`lorem`, `{{`, …). Almost always a real, embarrassing leak.
|
||||
- `NAP_PHONE_MISMATCH` / `NAP_ADDRESS_MISMATCH` (P0): the same business shows
|
||||
different Name/Address/Phone across entries — a local-SEO and trust problem.
|
||||
- `DUPLICATE_ID` (P1): one `@id` defined twice with different content.
|
||||
- `DANGLING_ID` (P1): a `{"@id": …}` reference points at a node never defined.
|
||||
- `GEO_SWAPPED` / `GEO_OUT_OF_RANGE` (P1): latitude/longitude transposed or impossible.
|
||||
- `DUPLICATE_DESCRIPTION` (P1): the same description reused across ≥3 entries.
|
||||
|
||||
## Severity → gate
|
||||
|
||||
| Severity | Meaning | Gate effect |
|
||||
|---|---|---|
|
||||
| **P0** | Blocker. Breaks parsing, blocks the rich result, or publishes wrong data. | **Fails the gate.** Process exits 1. Entry must not reach client review. |
|
||||
| **P1** | Fix before launch. Real defect, doesn't block the rich result. | Triage backlog. |
|
||||
| **P2** | Optimization. Recommended props, style, orphan URLs. | Optimization backlog. |
|
||||
|
||||
Full code list: `defect-taxonomy.md`.
|
||||
|
||||
## Type → requirement matrix (mirror of `schema_rules.json`)
|
||||
|
||||
`required` missing → **P0**. `recommended` missing → **P2** (aggregated). Anything in
|
||||
`allowed` is accepted silently. Properties outside all three are flagged only in `--strict`.
|
||||
|
||||
| Type | Required (P0 if missing) | Recommended (P2 if missing) |
|
||||
|---|---|---|
|
||||
| Organization | name, url | logo, sameAs, contactPoint, address |
|
||||
| WebSite | name, url | publisher, potentialAction, inLanguage |
|
||||
| WebPage | name | url, isPartOf, primaryImageOfPage, breadcrumb, datePublished, dateModified |
|
||||
| Hotel / LodgingBusiness / Resort | name, address | telephone, image, priceRange, geo, url, starRating, aggregateRating |
|
||||
| LocalBusiness | name, address | telephone, openingHoursSpecification, geo, image, url, priceRange, aggregateRating |
|
||||
| Restaurant / FoodEstablishment | name, address | servesCuisine, priceRange, telephone, menu, openingHoursSpecification |
|
||||
| FAQPage | mainEntity | — |
|
||||
| Question | name, acceptedAnswer | — |
|
||||
| Answer | text | — |
|
||||
| BreadcrumbList / ItemList | itemListElement | — |
|
||||
| ListItem | position | item, name |
|
||||
| Product | name | image, offers, brand, aggregateRating, review, description, sku |
|
||||
| Offer | price, priceCurrency | availability, url, validFrom, priceValidUntil |
|
||||
| Article / NewsArticle / BlogPosting | headline | author, datePublished, image, dateModified, publisher |
|
||||
| Event | name, startDate, location | endDate, offers, performer, image, eventStatus, eventAttendanceMode, organizer |
|
||||
| Review | reviewRating, author | datePublished, reviewBody, itemReviewed |
|
||||
| AggregateRating | ratingValue | reviewCount, ratingCount, bestRating |
|
||||
| MemberProgram | name | hasTiers, hostingOrganization, url |
|
||||
|
||||
**Container types** (validated for value formats, but *not* for required/recommended,
|
||||
because they only ever appear nested): PostalAddress, GeoCoordinates, ImageObject,
|
||||
ContactPoint, OpeningHoursSpecification, Rating, Brand, EntryPoint, Place, OfferCatalog,
|
||||
ReserveAction, MeetingRoom, Room/HotelRoom/Suite, MemberProgramTier, Menu/MenuItem, … (full
|
||||
list in `schema_rules.json` → `container_types`).
|
||||
|
||||
## Extending the rules
|
||||
|
||||
Add a type, tighten a requirement, or recognize a new container by editing
|
||||
`scripts/schema_rules.json` **only** — no Python change needed:
|
||||
- New rich-result type → add to `known_types` with `required` / `recommended` / `allowed`.
|
||||
- New nested type to stop "unknown type" warnings → add to `container_types`.
|
||||
- New value-format property → add to the relevant `value_formats` group.
|
||||
- New placeholder token to catch → add to `placeholder_tokens`.
|
||||
|
||||
After any edit, re-run `make_sample.py` + `validate_schema.py` against the fixture to
|
||||
confirm you didn't regress.
|
||||
Reference in New Issue
Block a user