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>
124 lines
6.8 KiB
Markdown
124 lines
6.8 KiB
Markdown
# 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.
|