The validation contract
Type: 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, redirects, and lifecycle targets are outside this
note pipeline. landings and redirects 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.
lifecycle inspects the support surfaces under kb/work/ and kb/tasks/. It
fails a non-empty top-level workshop with no framing file and warns about an
unregistered non-empty workshop, a backlog checklist whose tasks are all
complete, or a recurring task whose declared non-glob output does not exist.
The warnings raise reconciliation decisions; they do not authorize registration,
movement, creation, or deletion. Keeping these checks explicit prevents
validation of one note or collection from failing on unrelated repository state
while still making each broader invariant deterministic.
The command presents these findings through two views. The default compact view
prints counts plus every warning, failure, and material notice. --full retains
the per-artifact PASS/WARN/FAIL/INFO transcript for deliberate inspection.
--json emits the versioned commonplace.validation.v1 envelope with stable
diagnostic rule IDs, subjects, reasons, the path and detected type of every
analysed artifact, and the detailed drill-down command. A caller that requires
one particular typed artifact checks both summary.files_analysed and the
corresponding analysed_artifacts[].type; text_files counts standalone frontmatter-free targets, excluding members
grouped under directory artifacts. It is not the total number of targets.
Presentation does not change severities or exit behavior: warnings exit zero;
failures exit nonzero.
Directory artifacts
ARTIFACT.yaml makes its directory one additional validation unit. Its
type selects a type spec and shared schema. The schema receives
{manifest, members}: parsed manifest metadata and a map from each visible
direct Markdown filename to its ordinary parsed-document representation.
It sees actual files, including ones without manifest entries. Descendants
remain independent traversal targets.
Required members use required; optional members appear in properties
without required. additionalProperties: false gives closed membership;
omitting it gives open membership. Expected types constrain each member's
frontmatter.type, independently of whether the member is required.
A type may permit bare Markdown with no type declaration.
The manifest can contain only type when no metadata is required. Optional
members entries map filenames to {sha256: <digest>}. Each entry asserts
that its file exists; each supplied digest must match exact bytes. The
shared schema decides when entries and hashes are mandatory. Metadata
cannot authorize a member forbidden by the schema.
Explicit directory validation runs set checks alongside ordinary file checks,
even with a malformed manifest. A collection sweep groups direct members
under their directory and counts the directory once. In JSON,
analysed_artifacts uses the directory path and full type-spec path; member
diagnostics use the member's path. The existing files_analysed field counts
these validation units. Explicit member-file validation checks only that
file. A workflow calls ValidationRun.validate(directory) to check the set
without starting traversal; repeated requests reuse results and active
cycles fail.
Analysis set checks resolve references across all five members, including amendments in the reconciliation, and check the overview's amendment index. Before synthesis, the workflow checks the four record members and their shared boundary directly; it does not require a provisional overview.
See ADR 095 for the boundary and alternatives, and the analysis set type for the first production contract.
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 tag space; 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 participating collections declared in kb/tags/COLLECTION.md and re-deriving membership from every tagged artifact — 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 every type spec under the project's kb/, skipping subtrees under a validation-ignore marker. In this checkout that includes the library's global types; in an installed project the read-only library is not validated. There is no separate type-system validation pass.
Imperative rules select types by type identity, not by the type spec's name field. A type's identity is its type: value as written, its spec's path under a KB root such as types/tag-readme.md or articles/types/article.md, so a same-named collection-local type remains distinct. ADR 048 introduced this keying. 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 inside the supplied schema instance (declarable) versus outside that instance (must be loaded and checked). Directory schemas can compare the member data their loader supplies; they cannot perform filesystem reads themselves. 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, including write briefs, are exempt from the slug limit). Git-ignored artifacts are not authored library artifacts, so explicit validation still runs their other structural checks but exempts both authored length limits. If Git is unavailable or the project is not a worktree, validation applies the limits. - Link health — every local relative link resolves to an existing target. Warns.
- Write-brief pairing — a
brief:value must be the document's own<stem>.brief.md, and that sibling must exist withtype: types/write-brief.md. Every file named*.brief.mdmust carry that type and be declared by its sibling<stem>.md, and every write brief must be so named (ADR 092). Violations fail. - 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 or repeated in its eligible source region fails; an unpairable verbatim citation warns, but only in notes that demonstrably use the convention. - Ingest snapshot pairing — when an ignored local snapshot is retained, its name-paired path, source URL, and exact-byte checksum must agree with the tracked ingest. A mismatch warns; when the recorded bytes exist under another filename, the warning locates them without treating that path as mutation authority. For a legacy ingest without a checksum, the source URL may locate a related capture but cannot establish exact-byte identity. Complete cache absence is silent.
- Source quotes — every attributed blockquote in a tracked ingest's
## Quotessection names its exact snapshot path and checksum, and occurs exactly once in that snapshot or within its supplied line range. Absent, ambiguous, or out-of-range extracts fail. Missing or mismatched local bytes are reported as unverified source errors, without judging the extract. Notes citing an ingest match only retained blockquote bodies, never attribution text or analytical prose. Both the snapshot check and citations of retained extracts use the ingest's source-kind normalization: repository sources preserve code operators.
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 output collections such as kb/reports/ remain outside that rule.
A kb/sources collection sweep also audits the retained local snapshot cache.
It indexes exact ingest source URL values independently of checksums so legacy
checksum-less ingests and changed observations remain visibly related. A
derived ingest may also own exact precursor bytes through
original_snapshot_sha256; this accounts for a retained translation input
without pretending it is the ingest's primary observation. The sweep warns
about a Markdown snapshot that has no same-stem ingest and no URL or checksum
match, and about redundant alternate copies of an already valid pair. A
checksum-owned alternate that reveals path drift is reported on the affected
ingest instead, so one condition produces one warning.
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 — rests-on: 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 — decided-by: the decision that added the second referential check and surfaced the class
- ADR 024 — Schema severity is per-constraint, fail by default — decided-by: 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 natural-language instructions bind 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