The validation contract

Type: kb/types/note.md · Tags: type-system

commonplace-validate enforces one contract on a note, but the clauses come from three places. Every finding is labelled with the source that produced it, because a reader who knows only the type spec would otherwise get failures from rules that spec never mentions:

PASS:
- [base] link health: all local relative links resolve
- [schema] type schema: note requirements satisfied
FAIL:
- [type: tag-readme] complete mark: missing entry for kb/notes/foo.md

In one sentence: a type declares what the document must contain; the framework checks that what it points at is really there.

The explicit landings and redirects targets are outside this note pipeline. Both emit [repository] findings. landings checks that every top-level collection has a README.md and that no sibling index.md shadows it. redirects compares properdocs.yml with the live docs_dir: targets resolve, keys do not shadow pages, and the map is flat. Keeping these checks explicit prevents validation of one note or collection from failing on unrelated site configuration while still making the published-tree invariants deterministic.

Scope: this is the deterministic half only

A type is verified by three mechanisms, and only the first two are the validator's:

Mechanism When Judges meaning?
1 schema — declarative JSON Schema validate time no
2 type_rule — imperative code the type registers validate time no
3 type-conformance review gate — the type spec's body text is the criterion, read by an LLM (ADR 038) review time yes

This page documents 1 and 2. The third is not a lesser mechanism: a type spec's natural-language instructions are an executable criterion, not documentation. Everything a schema cannot express still binds — it binds at review time. That is why type-spec.md tells authors not to restate schema rules in body text: the schema already enforces them, so a restatement only spends reviewer judgment re-confirming what is already guaranteed, instead of on the properties only a reviewer can check.

The three sources of a validator finding

Source Owner Mechanism Can dereference?
base framework imperative, applies to every typed note; repository-boundary rules may also cover bare library text yes — link health, archive boundary, verbatim quotes
type: <name> the type imperative rules registered for that type yes — tag-readme marks re-derive from the collection; type specs resolve their declared schemas
schema the type declarative JSON Schema over the parsed document no

A type is not verified by its schema alone. tag-readme proves it: its complete mark is checked by re-walking the collection and re-deriving membership from every tagged note — imperative, dereferencing, and impossible to express in a schema. type-spec supplies a smaller example: type-spec.schema.yaml can constrain the schema: field's shape, but an imperative rule must follow a non-null path and load the declared schema. So who owns a rule and whether it dereferences are independent axes, which is why the table has no empty cells and why "schema versus everything else" is the wrong mental model.

Type-spec documents are ordinary validation artifacts. Collection validation includes local type specs, and commonplace-validate types runs the same base, type-rule, and schema pipeline over the complete global and local type inventory. There is no separate type-system validation pass.

Imperative rules select types by canonical path, not by the type spec's bare name. An installed framework path under kb/commonplace/ normalizes to its source identity, while a same-named collection-local type remains distinct (ADR 048). Reports still use the shorter type: <name> label because a display label is not an identity key.

What the schema can and cannot express

The schema is not limited to frontmatter. ParsedDocument.to_validation_object() hands it frontmatter, body, headings, links, and body_dates, so a type can require a ## Reasoning heading or constrain body content declaratively, and several do.

What a schema cannot do is dereference — it has no way to say follow this path and look inside the artifact it names. JSON Schema validates one instance document; the referent is another file. This is an inherited limit of the substrate, not a gap worth closing, and it is the whole reason a second, imperative check mechanism exists at all.

So the dividing line is not frontmatter/body. It is intra-document (declarable) versus referential (must be executed). A referential check's ground truth lives in a second artifact, which is precondition 3 of a derived copy of recomputable truth must be checked or absent — the rule that makes these checks obligatory rather than optional.

The base contract

Every note with frontmatter is checked for the following, whatever its type. A type spec does not declare these and cannot opt out of them.

  • Frontmatter parses — valid delimiters, well-formed YAML.
  • Title length within MAX_NOTE_TITLE_LENGTH; filename slug length within MAX_NOTE_SLUG_LENGTH (derived-artifact types are exempt from the slug limit).
  • Link health — every local relative link resolves to an existing target. Warns.
  • Proposal archive boundary — no library artifact links to a file under kb/reference/proposals/archive/. The archive README is a permitted target and may link to archived files itself; workshop files under kb/work/ may also link in. Violations fail.
  • Verbatim quotes — every verbatim-marked quotation resolves against the source it links (ADR 046). A quote absent from its cited source fails; an unpairable verbatim citation warns, but only in notes that demonstrably use the convention.

Bare text opts out of structural checks. A file with no frontmatter is typed text and gets no title, slug, link-health, quote, type, or schema requirements — deliberate, because text keeps capture friction at zero and may hold imported material whose relative links are broken by construction. The proposal archive boundary is the one repository-level exception: bare library READMEs can otherwise make archived designs load-bearing just as readily as typed notes. Non-library outputs such as kb/reports/ remain outside that rule.

Why the two referential checks have different severities

Link health warns; a false verbatim quote fails. That asymmetry is the derived-copy rule, not an inconsistency. A dangling link costs the reader a bounded, recoverable search. A false verbatim quote tells the reader the text was checked when it was not — it suppresses the verification it claims to have done, which is silent and unbounded. Absence degrades; a false copy corrupts.

Why the base checks are not type-configurable

Letting a type opt out of link health or verbatim-quote resolution would be a knob that can only ever be set wrong: a broken link is broken in every type, and a false verbatim claim is false in every type. The checks are already self-selecting — a note that makes no verbatim claim produces no candidates — so a type gate would add configuration without adding reach.

Open

ADR 050 gives referential checks a shared execution context: one validation run caches parsed artifacts and collection tag indexes and builds the authored-link graph once. It does not give Markdown elements a shared positioned representation. Link health and verbatim-quote resolution remain separate hand-written passes; they share one notion of code (note_parser.blank_fenced_code_blocks), but ParsedDocument.links is still a tuple of URLs with no spans, so the quote checker carries its own link regex. A third positioned referential check could still mean a third parser. Tracked in the kb-graph-loader workshop: a LoadedNote carrying positioned elements is what would retire those private parsers.


Relevant Notes: