Chapter 7: Frontmatter Schemas and Validation
Markdown documents in an engineering knowledge base carry structured metadata—document type, lifecycle status, creation date, author, supersession links, and PR verdicts—inside their leading YAML frontmatter block (---). Without a formal schema, frontmatter quickly drifts: one author writes status: done, another writes status: completed, and a third forgets created altogether, breaking automated queries.
AutoDoc (core/schema) solves metadata drift without turning your text editor into a rigid database form:
- Declarative YAML Schemas (
_schema/frontmatter.yaml): Supports both flat (version: 1) and discriminated polymorphic (version: 2) workspace schemas up to256 KiBand256declared fields (core/schema/schema.go). - Non-Blocking Live Diagnostics: Validation runs continuously as you type in the TUI/GUI editor and on every file change in the indexer. Problems appear in a live diagnostic banner above the page and in
index.status(diagnosed), never blocking a file save or excluding a document's prose from search. - Automatic Search Facet Registration: Every field declared in the workspace schema is automatically indexed as a typed search facet (
type:adr,verdict:approved,-status:superseded).
1. Schema version: 1 (Flat) vs. Schema version: 2 (Discriminated Types)
A workspace binds its schema file via Go › Manage workspaces… › Edit… or the workspace.set_schema RPC verb. The form suggests <root>/.autodoc/schema.yaml; a knowledge base typically uses _schema/frontmatter.yaml.
Schema version: 1: Flat Workspace Metadata
A version: 1 schema validates every Markdown file in the workspace against a single flat frontmatter: field map. It is ideal for homogeneous note collections or runbook repositories:
version: 1
strict: false # true: report fields the schema does not declare
frontmatter:
type: {type: string, enum: [note, adr, review], required: true}
status: {type: string, enum: [draft, active, archived], default: active}
tags: {type: list, item_type: string}
created: {type: date} # YYYY-MM-DD, or an RFC 3339 time (its date is the facet)
count: {type: integer}
The types are string, integer, number, boolean, date and list (of item_type, strings by default; a lone value is a list of one). A required field has no default.
A fuller one, with a writer order and stricter fields:
# .autodoc/schema.yaml (Schema Version 1)
version: 1
strict: true
order: [type, status, created, tags, abstract]
frontmatter:
type: {type: string, required: true, enum: [note, runbook, postmortem]}
status: {type: string, enum: [draft, active, archived], default: draft}
created: {type: date, required: true}
tags: {type: list, item_type: string, required: true}
abstract: {type: string, required: true}
severity: {type: integer, enum: [1, 2, 3]}
Schema version: 2: Polymorphic Document Types (discriminator: type)
In a multi-artifact engineering knowledge base, an Architecture Decision Record (adr) requires number and allows status: [proposed, accepted, rejected, superseded, deprecated], whereas a Code Review (review) requires reviewer and subject and restricts verdict to [approved, approved_with_notes, change_requested, rejected, commented].
Schema version: 2 expresses this cleanly in a single file using three top-level sections:
discriminator: type: Names astringfield ofcommonwhose value selects the document's specialized type definition.common:(Base Shared Fields): Declares fields shared by every document in the workspace (type,status,created,tags,abstract,updated,author,sources,related,supersedes,superseded_by,amends).types.<name>:(Per-Type Specializations): Declares fields specific to each discriminator value and optionally narrows sharedcommonfields.
Version 2 declares fields by document type, still in one file that validates every document. The discriminator names a string field of common; a file is checked against common merged with the types entry its value names:
version: 2
strict: false
discriminator: type # the field whose value picks the type
common: # every document
type: {type: string, required: true}
status: {type: string, enum: [draft, proposed, accepted, approved], default: draft}
tags: {type: list}
types: # merged over common when type has this value
adr:
number: {type: string, required: true}
status: {type: string, enum: [proposed, accepted], default: proposed}
review:
verdict: {type: string, enum: [approved, rejected], required: true}
note: # common alone
- A file whose type the schema does not name is diagnosed (
unknown type "memo", ruleunknown_type) and checked againstcommonalone. strict: truereports the fields the file's own type does not declare: averdicton an ADR.
A production knowledge base's version 2 schema:
# _schema/frontmatter.yaml (Schema Version 2)
version: 2
strict: false
discriminator: type
common:
type: {type: string, required: true, enum: [adr, adr-rationale, convention, playbook, reference, synthesis, review, pr, note, about, kb]}
status: {type: string, required: true}
created: {type: date, required: true}
tags: {type: list, required: true}
abstract: {type: string, required: true}
title: {type: string}
updated: {type: date}
author: {type: string}
sources: {type: list}
related: {type: list}
supersedes: {type: list}
superseded_by: {type: list}
amends: {type: list}
types:
adr:
status: {type: string, enum: [proposed, accepted, rejected, superseded, deprecated]}
number: {type: string, required: true}
revision: {type: integer}
decided_by: {type: string}
review:
status: {type: string, enum: [in-progress, completed]}
reviewer: {type: string, required: true}
subject: {type: string, required: true}
verdict: {type: string, enum: [approved, approved_with_notes, change_requested, rejected, commented]}
pr: {type: integer}
head: {type: string}
synthesis:
status: {type: string, enum: [draft, final, superseded]}
kind: {type: string, enum: [analysis, investigation, lesson, bug-fix, benchmark, incident, design]}
convention:
status: {type: string, enum: [draft, active, retired]}
area: {type: string}
applies_to: {type: list}
Type Narrowing Invariants in core/schema
When core/schema/schema.go merges types.<name> over common, it enforces strict covariance rules at schema load time:
- Aliases: a field with an
enummay name legacy spellings of its values,aliases: {changes_requested: change_requested}. An alias validates with no diagnostic and is faceted as its value, soverdict:change_requestedfinds both spellings. An alias can't be a value of the enum itself, and each names one of its values. - Exempt Paths: a top-level
exemptlist of root-relative globs, asincludeandexcludetake them, names files the schema doesn't judge. Such a file keeps its facets and has no diagnostics. The KB schema AutoDoc ships exemptsraw/**(immutable source),_templates/**(AutoDoc's own),archive/**(retired), and two tools' directories,.auto-agents/**and.junie/**. - Narrowing Only: A specialized type may narrow a
commonfield'senumto a subset, promote an optionalcommonfield torequired: true, or supply a type-specificdefault. - No Widening or Type Mutation: A type may never change a field's scalar
type(for example,number: integerinadrandnumber: stringinpris rejected with the offending line number) or add enum values disallowed bycommon. - Safe Schema Reloads: Editing
_schema/frontmatter.yamlimmediately re-validates all workspace documents without re-embedding their text vectors. If an edit introduces a syntax error or violates a narrowing rule, AutoDoc reports the line number inworkspace.list(schema.error,schema.line) and keeps the last valid schema active until you fix the file.
2. Field Types, Constraints, and Diagnostic Rules
AutoDoc resolves frontmatter using the YAML 1.2 Core schema (golib/yaml) and checks every field against its declared type and constraints.
Notable Type Behaviors
- Automatic List Promotion: For any field declared as
type: list, a single scalar in YAML (tags: sqlite) is automatically treated as a one-element list (["sqlite"]) for both validation and facet indexing. - Structured Map Lists (
item_type: map): Alistfield can declareitem_type: mapwith anitems:schema map to validate nested object arrays inside frontmatter. - Canonical Writer Ordering (
order): When a schema declares a top-levelorder: [type, status, created, ...]list, AutoDoc's frontmatter writer emits keys in that exact sequence. - Defaults Answer Facets Without Touching Disk: When an optional field declares
default: active, documents omitting that field answerstatus:activein facet queries without AutoDoc ever rewriting your Markdown files on disk.
3. Live Editor Banner and Workspace-Wide Validation (doc.validate & index.documents)
Non-Blocking Live Editor Banner
A task in a todo store is not a document type, so the workspace's schema never checks it: the todo format does, the way todos.list reads it. A task the format can't read has the diagnostic todo-malformed, and each entry of its errors[] (an adr that names no file, say) has todo-<code> on the field it names. Its facets are the format's own fields, so status:open and priority:high filter tasks.
When you open or edit a Markdown file in the TUI or GUI (autodoc --ui / --gui), AutoDoc validates the buffer's YAML frontmatter on every keystroke against the workspace's active schema. If a required field is missing or an enum value is misspelled (status: aproved), a diagnostic banner appears directly above the top ruler showing the line number, field name, and rule violation—while allowing Ctrl+S saves at any time.
Pre-Flight Validation via doc.validate
AI agents and CI hooks can validate draft Markdown content before or after writing to disk by calling doc.validate. Because doc.validate accepts raw content bytes, you can check unsaved buffers:
# Validate an unsaved ADR draft against workspace "kb"'s v2 schema
autodoc --call doc.validate '[
"kb",
"adrs/0220-schema-facets.md",
"---\ntype: adr\nstatus:invalid_state\ncreated: 2026-10-17\ntags: [kb]\n---\n# ADR 0220\n"
]'
Response:
{
"diagnostics": [
{
"field": "status",
"line": 3,
"rule": "enum",
"message": "status must be one of: proposed, accepted, rejected, superseded, deprecated"
},
{
"field": "abstract",
"line": 1,
"rule": "required",
"message": "missing required field: abstract"
},
{
"field": "number",
"line": 1,
"rule": "required",
"message": "missing required field: number"
}
]
}
Auditing All Diagnosed Documents Across a Workspace
To audit an entire knowledge base for frontmatter drift, check diagnosed in index.status and list the offending documents with index.documents passing "diagnosed": true (or "missing": ["abstract"]):
# 1. Attach or update the workspace's frontmatter schema
autodoc --call workspace.set_schema '["kb", "_schema/frontmatter.yaml"]'
# 2. Check total diagnosed document count in index.status
autodoc --call index.status '["kb"]'
# 3. List every document in "kb" that has one or more schema diagnostics
autodoc --call index.documents '[
"kb",
{
"diagnosed": true,
"sort": "updated",
"limit": 50
}
]'
# 4. Find documents missing specific optional fields (e.g. "author" or "updated")
autodoc --call index.documents '[
"kb",
{
"missing": ["author", "updated"],
"paths": ["adrs"]
}
]'
4. How Schema Fields Become Indexed Search Facets
In core/schema/schema.go:68-71, Schema.Fields constructs the ordered union of common and every types.<name> map. The search engine (core/index) registers this union as the workspace's facet vocabulary (Schema.Declared(name)):
- Zero Extra Configuration: The moment you add
verdict: {type: string, enum: [approved, rejected]}undertypes.reviewin_schema/frontmatter.yaml, bothverdict:approvedinline query tokens and{"facets": {"verdict": ["approved"]}}RPC filters become active across the workspace. - Type-Aware Normalization: Integer fields (
pr: 249orpr: 0xf9), boolean flags, and ISO/RFC-3339 dates (created: 2026-10-17) are normalized to their canonical typed representation at index time and query parse time. - Scoped Type Matching: Filtering by a type-specific field such as
verdict:approvednaturally narrows results to documents whose discriminator type declares and populatesverdict.
← Previous: Chapter 06: Configuring Embeddings & Rerankers | Index | Next: Chapter 08: Knowledge Graphs & ADR Lifecycle →