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.
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 prose body 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 prose is 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 prose: 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 withinMAX_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 underkb/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:
- A derived copy of recomputable truth must be checked or absent — rationale: why a referential check is obligatory rather than optional, and why a false copy fails where an absent one warns
- ADR 046 — Verbatim quotes are validated against their cited source — evidence: the decision that added the second referential check and surfaced the class
- ADR 024 — Schema severity is per-constraint, fail by default — evidence: how the
schemasource assigns its own severities - Commands — see-also: the
commonplace-validatecommand surface - ADR 038 — Type-conformance reviews use the type spec as the gate — see-also: the third verification mechanism, where a type spec's prose binds as an LLM-judged criterion at review time
- ADR 050 — Validation runs share parsed artifacts and collection indexes — implemented-by: artifact-local and collection-indexed checks share one execution context without a generic dependency engine