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:

classDiagram
    class BaseFrontmatter {
        +string type
        +string status
        +date created
        +list tags
        +string abstract
        +date updated
        +string author
    }
    class ADR {
        +string number
        +integer revision
        +list supersedes
        +list amends
        +string decided_by
    }
    class Review {
        +string verdict
        +integer pr
        +string reviewer
        +string subject
        +string head
    }
    class Synthesis {
        +string kind
        +list sources
        +list related
    }
    class Convention {
        +string scope
        +string area
        +integer revision
        +list applies_to
    }
    BaseFrontmatter <|-- ADR
    BaseFrontmatter <|-- Review
    BaseFrontmatter <|-- Synthesis
    BaseFrontmatter <|-- Convention

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:

  1. discriminator: type: Names a string field of common whose value selects the document's specialized type definition.
  2. 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).
  3. types.<name>: (Per-Type Specializations): Declares fields specific to each discriminator value and optionally narrows shared common fields.

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 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:

Engineering Knowledge Base Document Distribution by Schema v2 Type0100200300400500600700Indexed Document CountsynthesisreviewadrreferencenoteconventionplaybookSchema v2 Document Type (discriminator: type)Document Typeadrconventionnoteplaybookreferencereviewsynthesis

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.

CategoryNameSchema_KeyAllowed_Values_or_SyntaxDiagnostic_RuleDescription
Field Typestringtype: stringUTF-8 scalar texttypeArbitrary text scalar; supports enum restriction and default value
Field Typeintegertype: integer64-bit integer (decimal, 0x hex, 0o octal)typeExact integer scalar; coerced for numeric facet comparisons
Field Typenumbertype: numberIEEE-754 float or integertypeFloating-point numeric scalar; rejects NaN and Inf
Field Typebooleantype: booleantrue or falsetypeYAML Core boolean scalar
Field Typedatetype: dateYYYY-MM-DD or RFC 3339 timestamptypeCalendar date; RFC 3339 timestamps index their YYYY-MM-DD date as the facet
Field Typeenumenum: [...] / type: enumNon-empty list of scalar values (max 1024)enumRestricts scalar or list items to a closed set of allowed values
Field Typelisttype: listitem_type: string|integer|number|boolean|date|maptypeSequence of items (a single scalar in YAML is promoted to a 1-item list)
Field Typereftype: ref / relation listDocument stem or workspace-relative path or $KB_ROOT/...unresolved_refTyped document reference; resolved into the workspace knowledge graph
Constraintrequiredrequired: trueboolean (cannot combine with default)requiredReports a diagnostic when the field is absent or empty in frontmatter
Constraintvalues / enumenum: [v1, v2]List of unique valid strings or scalarsenumNarrows allowed values; v2 types may narrow a base enum subset
Constraintdefaultdefault: valueScalar or list matching the field type—Facet value answered when a document omits the optional field
Constraintmanagedmanaged: truebooleanmanagedReserved for daemon-owned lifecycle fields; rejects client edits
Constraintstrictstrict: trueTop-level boolean in schema fileunknown_fieldFlags any frontmatter key not declared in common/base or the active type
Diagnostic Codeyamlrule: yamlMalformed YAML frontmatter blockyamlUnclosed fence or syntax error inside the leading --- block
Diagnostic Codeunknown_typerule: unknown_typeUnrecognized discriminator valueunknown_typeDocument's discriminator value is not declared under types.<name>
Diagnostic Coderequiredrule: requiredMissing required fieldrequiredDeclared required field is missing or null
Diagnostic Codetyperule: typeMismatched scalar or item typetypeValue cannot be parsed as the declared field type
Diagnostic Codeenumrule: enumValue outside allowed enum listenumValue is not a member of the field's declared enum set
Diagnostic Codeunknown_fieldrule: unknown_fieldUndeclared key when strict: trueunknown_fieldFrontmatter contains a key not declared for this document type

Notable Type Behaviors


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)):

  1. Zero Extra Configuration: The moment you add verdict: {type: string, enum: [approved, rejected]} under types.review in _schema/frontmatter.yaml, both verdict:approved inline query tokens and {"facets": {"verdict": ["approved"]}} RPC filters become active across the workspace.
  2. Type-Aware Normalization: Integer fields (pr: 249 or pr: 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.
  3. Scoped Type Matching: Filtering by a type-specific field such as verdict:approved naturally narrows results to documents whose discriminator type declares and populates verdict.

← Previous: Chapter 06: Configuring Embeddings & Rerankers | Index | Next: Chapter 08: Knowledge Graphs & ADR Lifecycle →