---
title: "Docs Validation Plan"
description: "The docs guards wired today, what each one actually enforces, and how the docs program hardens them."
engineVersion: v1.0.234
date: 2026-09-28
license: "(c) Gessa, proprietary. Cite with attribution to https://gessa.ai/docs/. Terms: https://gessa.ai/terms/."
canonical: https://gessa.ai/docs/docs-system/DOCS_VALIDATION_PLAN/
---

# Docs Validation Plan

Documentation is a product contract, so it is guarded like code: with deterministic, file-walking checks that exit non-zero and print actionable findings. This page describes the validation suite that runs today, states honestly what each guard enforces (which is sometimes narrower than the rule it defends), and points at the [docs program](../cycles/docs-program/SPEC.md) work that hardens it. Every check walks files, compares against an in-repo source of truth, and fails loudly; none touch the network.

For the three-tier source-of-truth model these checks defend, see the [Documentation Contract Architecture](./DOCUMENTATION_CONTRACT_ARCHITECTURE.md).

## Guards Wired Today

Six guards are wired. One is the generated-drift guard, run standalone; the other five run under `npm run check:docs`. Both `gen-docs:check` and `check:docs` are part of the global `npm run check` chain.

### Generated-docs drift (`npm run gen-docs:check`)

- **Guards:** that every Tier-2 generated file is a byte-exact projection of its Tier-1 source.
- **How it works:** runs each `scripts/gen-*-docs.ts` generator in `--check` mode (plus `scripts/check-spec-drift.ts` and the AI-context manifest generator), regenerating the output in memory and failing if the committed file differs. This is the mechanism behind the `GENERATED FILE: do not edit by hand.` banner and the real drift guard for the whole generated surface.
- **Status:** implemented and in the global chain.

### Scaffold existence (`npm run check:docs-contract-scaffold`)

- **Guards:** that the documentation contract scaffolding exists and is structurally intact: every required scaffold file (the four docs-system pages, all `docs/product/v1/` pages, and all `docs/ai-context/v1/` files), a set of load-bearing exact H1 titles (`index.md` H1 "Gessa Product Documentation (v1)", `README.md` H1 "Gessa AI Context (v1)", `llms.txt` first line "# Gessa", and the four docs-system H1s), and that `ai-context-manifest.json` parses as JSON and carries the literal `engineVersion` string `v1` with a `files` array.
- **How it works:** a deterministic file walk (`scripts/check-docs-contract-scaffold.ts`) that asserts each required path exists, parses the manifest, and verifies each critical heading appears verbatim; exits non-zero listing any missing path, invalid manifest, or mismatched title.
- **Enforcement gap:** it checks the manifest against the literal string `v1`, not against `ENGINE_VERSION_MAJOR`. It is an existence and shape gate, not a dynamic version check.
- **Status:** implemented and, because it runs inside `check:docs`, in the global chain.

### Relative links (`npm run check:docs-links`)

- **Guards:** that every relative Markdown link in the hand-authored surface (`docs/docs-system`, `docs/product/v1`, `docs/ai-context/v1`) resolves to a real path on disk.
- **How it works:** `scripts/check-docs-links.ts` walks the `.md` and `.txt` files, extracts each link, skips external and pure-anchor links, strips titles and anchor fragments, and fails on any target that does not exist.
- **Status:** implemented and in the global chain via `check:docs`.

### Markdoc tags (`npm run check:docs-markdoc`)

- **Guards:** that every custom Markdoc tag used in prose is one of the eleven, is well-formed (required attributes present, no unknown attributes, enumerated values in range), and, for a binding tag, resolves into its generated file. Tags shown inside fenced code blocks are skipped.
- **How it works:** `scripts/check-docs-markdoc.ts` parses each page with Markdoc, walks the tree, and checks each prose tag against the `TAG_SCHEMAS` and `BINDING_TAGS` exports in `scripts/markdoc/tags.ts`.
- **Enforcement gap:** a binding tag resolves by naive substring `.includes()` over the generated file, so a `name` or `id` that is a substring of unrelated text resolves even when there is no matching table row. The `version` tag's `coordinate` and `hash` are free strings that no check validates against `packages/engine-version`.
- **Status:** implemented and in the global chain via `check:docs`.

### Example markers (`npm run check:docs-examples`)

- **Guards:** that every fenced `ts`, `tsx`, `typescript`, `js`, `javascript`, or `json` block in the hand-authored surface carries an `<!-- example:conceptual -->` or `<!-- example:verbatim source=... -->` marker in the three lines directly above the opening fence. Blocks in `text`, `bash`, `sh`, `mermaid`, `yaml`, or `markdoc` are exempt.
- **How it works:** `scripts/check-docs-examples.ts` scans line by line for fences and their languages and fails on an unmarked marked-language block.
- **Enforcement gap:** this is a marker-presence check only. It does not compile, type-check, or byte-compare any block against a source file; the marker is a promise the guard does not verify.
- **Status:** implemented and in the global chain via `check:docs`.

### Public claims (`npm run check:docs-claims`)

- **Guards:** over `docs/product/v1` and `docs/ai-context/v1`, that a fixed list of banned over-claim phrases never appears in prose, and that a `{% contract %}` or `{% proof %}` tag does not carry an accepting status while the v1 readiness ledger has zero accepted rows.
- **How it works:** `scripts/check-docs-claims.ts` scans the claim roots for the banned phrases and, while the ledger has no accepted rows, parses each page and fails on a `contract` with an accepting status or a `proof` with status `present`.
- **Enforcement gap:** the accepting-status rule has an accepted-rows bypass. It counts accepted rows in `docs/cycles/v1-engine-readiness-loop/FEATURE_LEDGER.json` and, the moment the first row is accepted, stops checking tag status entirely. It protects only the pre-launch window and does not tie a specific claim to the specific ledger row that would justify it. The banned-phrase list stays in force regardless.
- **Status:** implemented and in the global chain via `check:docs`.

## What the Docs Program Hardens

The [docs program](../cycles/docs-program/SPEC.md) is the cycle that turns the guards above from shape checks into contract checks. Its planned additions and upgrades:

### Docs version equals engine version (`check:docs-version-freeze`)

- **Guards:** the flagship invariant that nothing enforces today. It asserts that the export manifest, the version archives (`versions.json`), the changelog, and the since-ledger agree with `ENGINE_VERSION` and `releases.json`, that the docs folder set equals `v${ENGINE_VERSION_MAJOR}`, and that a removed coordinate carries a deletion tombstone.
- **Status:** added by the docs program; wired into the chain and into `deploy.sh`.

### Closure in both directions (`check:docs-closure`)

- **Guards:** code to docs (every registry entry's anchor resolves as a heading in its generated file at the pinned version) and docs to code (every identifier-shaped token in prose resolves to a registry id, every Markdoc binding tag resolves to a heading rather than a substring, and no integer sits next to a catalog noun outside a count tag).
- **Status:** added by the docs program in warn mode with a sorted shrink-only baseline, then flipped to error for new and edited nodes.

### Examples that earn their marker (`check:docs-examples`, upgraded; `docs:examples:nightly`)

- **Guards:** that a `verbatim` block byte-matches its cited symbol span, that a `replay` example passes preflight, and, on a nightly lane, that a `compile` block type-checks against the SDK and that tutorials run in Playwright against the QA stack.
- **Status:** upgrades today's marker-presence check into a content check.

### Binding resolution by row, not substring (`check:docs-markdoc`, hardened)

- **Guards:** that a `component`, `action`, `capability`, or `mcp-tool` reference resolves to an actual table row or heading in its generated file, and that a `version` coordinate resolves against `packages/engine-version`.
- **Status:** replaces the substring resolution described above.

### Claims tied to ledger rows (`check:docs-claims`, extended)

- **Guards:** that a capability claim names the specific readiness-ledger row that justifies it, rather than relying on the global accepted-rows bypass. The banned-phrase list is retained.
- **Status:** extended by the docs program alongside the parity-page binding resolver.

### Registry-side DocContract parsing

- **Guards:** that no surface loads without a purpose, anchor, stability, and audience. `DocContractSchema` (added to `packages/engine-version`) is spread into the five registries, and each registry's import fails when an entry omits a contract field.
- **Status:** landed as warn behind the closure baseline, then flipped per registry when its baseline reaches zero.

### Behavioral tier stays current (`check:docs-dossier-currency`)

Dossiers under `docs/spec/dossiers` are snapshots of engine behavior, so they are policed for
staleness instead of regenerated. Each unit carries a `symbols` twin of every citation and a
receipt with the sha256 of every cited source file. The gate fails hard when a cited symbol no
longer resolves or a unit has no receipt, and reports a unit as stale when a cited file changed
since its stamp. The doctor collects the strict form; `check:docs-version-freeze` refuses to pin
a release while any unit is stale or broken. Units re-trace against a paused tree at the release
point, then re-stamp with `--stamp <unit>` (decision D26 in the docs program).

### Zero em dashes (`check:docs-em-dash`)

The corpus trees carry no U+2014, code fences included, because a fence is copied verbatim by
readers and by the examples gate. Chained into `check:docs` with a self-test; the engineering
record under `docs/cycles` is out of scope by design.

## Wiring Discipline

Checks join the build incrementally, never as a big-bang flip.

- `gen-docs:check` is already part of `npm run check`. Any new generator registers a `--check` mode and is appended to the `gen-docs:check` chain (and to `gen-docs`).
- A new content check lands first as a standalone `scripts/check-*` gate that can be run in isolation, is validated deterministic and green on the current tree, and only then is appended to the global chain. Warn-mode gates carry a sorted shrink-only baseline so two lanes burning down different entries both pass.
- `check:docs` (which runs the five hand-authored-surface guards) is in the global chain today. The earlier plan to keep `check:docs-contract-scaffold` out of the global chain no longer holds: it runs inside `check:docs` and therefore inside `npm run check`.

## Status

Six guards run today: `gen-docs:check` plus the five under `check:docs` (contract-scaffold, links, markdoc, examples, claims), all in the global `npm run check` chain. Their enforcement is honest but narrow in three places: binding tags resolve by substring, example markers are not verified against source, and the public-claims status rule bypasses once the ledger accepts a row. The [docs program](../cycles/docs-program/SPEC.md) hardens each of these and adds the docs-version freeze that no check enforces today. The [Markdoc Schema Plan](./MARKDOC_SCHEMA_PLAN.md) defines the tags several of these checks validate, and the [Documentation Contract Architecture](./DOCUMENTATION_CONTRACT_ARCHITECTURE.md) defines the three-tier model they defend.
