ASK KNOX
beta
LESSON 602

Docs as Dashboards: Registries & Status Ingestion

The registry is the index over every doc that exists — who owns it, when it was last verified, and whether the file the registry claims is there is actually there.

8 min read·Self-Maintaining Documentation

The Index Problem

Each lesson in this track introduces an automation that depends on knowing which docs exist. The drift checker from the drift-detection lesson needs to know which docs contain countable facts. The runbook drift checker from the runbooks lesson needs to know which docs are runbooks so it can verify their steps. The doc-bot from the doc-bot lesson needs to know which docs are affected by a code change and who owns them. The freshness gate from the freshness-SLO lesson needs to know each doc's SLO.

Without a registry, every automation solves this problem independently — typically with directory conventions and glob patterns that diverge over time. The registry is the canonical answer: a structured index of every doc that exists, with the metadata that makes every other automation possible.

The Canonical DocRecord Shape

Every lesson and challenge in this track uses exactly one DocRecord shape. It is not a suggestion — it is the contract.

interface DocRecord {
  doc_id: string       // stable id, e.g. "readme", "runbook-deploy"
  path: string         // repo-relative path
  doc_type: string     // 'readme' | 'runbook' | 'adr' | 'reference' | 'context-file' | 'changelog'
  owner: string        // team or role, not a person's real name
  checksum: string     // sha256 of current content
  verified_at: string  // ISO date a human/agent last confirmed accuracy
  max_age_days: number // freshness SLO for this doc type
  generated: boolean   // true = produced by a generator; hand-edits are drift
}

The generated field is load-bearing: a doc-bot scanning the registry for docs it should update only touches records where generated: true. A hand-edited doc with generated: false is flagged if the bot sees drift from the generator's output — that is a human-override situation, not a bot-auto-fix situation. The boundary between mechanical and semantic changes (covered fully in the doc-bot-safety lesson) starts here.

The max_age_days field is the freshness SLO. A runbook has a shorter SLO than an ADR because runbooks reference operational procedures that change with tooling; an ADR documents a decision that is unlikely to be reversed. The freshness gate in the freshness-SLO lesson reads this field per-record and fails CI when verified_at plus max_age_days is in the past.

The checksum field is the registry-side equivalent of the CI diff gate from the generated-references lesson. It is computed at registration time and recomputed every time a generator regenerates the doc — so for a generated: true record, the stored checksum should always match the sha256 of what is currently on disk. A mismatch means the file changed since the generator last wrote it: a hand-edit slipped into a generated doc. The generated-references lesson catches this with a git diff after regeneration; the registry catches the same drift class without running the generator, by comparing one hash. The capstone's verify step is exactly three lines per record:

function checksumMatches(record: DocRecord, currentContent: string): boolean {
  const onDisk = crypto.createHash('sha256').update(currentContent).digest('hex')
  return record.checksum === onDisk  // false on a generated doc = hand-edit drift
}

A failing checksumMatches on a generated: true record is a drift violation; on a generated: false record the checksum is informational (it records what was last registered, not an enforced contract), so the gate skips it.

The Registry Scanner

The scanner performs reconciliation between two sets: docs on disk and docs in the registry.

export function scanRegistry(
  diskPaths: string[],
  registryRows: DocRecord[]
): ScanResult {
  const registeredPaths = new Set(registryRows.map((r) => r.path))
  const diskPathSet = new Set(diskPaths)

  const unregistered = diskPaths.filter((p) => !registeredPaths.has(p))
  const missing = registryRows.filter((r) => !diskPathSet.has(r.path)).map((r) => r.path)
  const registered = registryRows.filter((r) => diskPathSet.has(r.path))

  return {
    unregistered,
    missing,
    registered,
    summary: {
      totalOnDisk: diskPaths.length,
      totalInRegistry: registryRows.length,
      unregisteredCount: unregistered.length,
      missingCount: missing.length,
    },
  }
}

Set lookups are O(1), which matters when the docs tree is large. The scan reports two categories of debt:

Unregistered (on disk, no DocRecord). These docs exist but are invisible to automation. The action is to create DocRecord rows for them: assign a doc_id, classify the doc_type, assign an owner, set a max_age_days based on type, and initialize verified_at to today.

Missing from disk (DocRecord exists, file does not). These are false claims in the registry. The file was deleted or moved without updating the registry. The action is to either restore the file, update the path field, or delete the registry row if the doc was intentionally retired. A stale DocRecord pointing to a nonexistent path is as harmful as a stale doc — it will cause every automation that reads the path field to fail silently or with a confusing error.

Status Ingestion: Turning Docs Into Dashboard Rows

The registry scanner identifies what exists and what is registered. Status ingestion goes further: it parses the content of doc files to extract structured status data and surfaces it on a dashboard where it becomes actionable.

The mechanism is straightforward. A spec file has frontmatter:

---
status: in-review
owner: platform-team
updated_at: "2026-03-15"
---

The ingest pipeline reads this with gray-matter, reconciles the status and updated_at fields against the registry, and writes a dashboard row. The dashboard shows the spec title, its current status badge, its owner, and its SLO position. Engineers who check the dashboard weekly see specs that are still marked draft three months after implementation — and update them.

The key insight is that the surface, not the ingestion, is what drives accuracy. A doc file in docs/specs/ is opened once and then forgotten. A spec whose status appears on a dashboard that is checked as part of the weekly engineering review cycle gets maintained because the stale badge is socially visible.

What Gets Registered and What Does Not

The registry tracks docs that have operational significance: docs that automation reads, docs that agents load as context, docs that gate CI, docs that describe how the system works or how to operate it. It does not track ephemeral notes, drafts in progress, or files that are explicitly excluded.

The doc_type field encodes the kind of significance:

  • runbook: operational procedures with executable steps — highest verification priority
  • adr: architectural decisions — lower volatility, longer SLO
  • reference: generated tables, API docs, key-files sections — must match their source
  • context-file: CLAUDE.md, AGENTS.md — agent session context, high operational impact
  • changelog: generated per-release — tight SLO (7 days), because a generated doc has no excuse for staleness
  • readme: project overview — moderate SLO, typically hand-authored

Each type carries a different max_age_days value. The registry scanner populates these values from the doc_type at registration time, using the defaults for the track. The freshness gate in the freshness-SLO lesson reads them at check time.

Building the Registry from Scratch

For a repo with no existing registry, the bootstrap process is:

  1. Run the scanner against the current docs tree to discover all unregistered paths.
  2. For each unregistered path, create a DocRecord with doc_type inferred from the file path pattern, owner set to the team responsible for that directory, verified_at initialized to today, max_age_days set from the type default, and generated set based on whether the file is known to be bot-produced.
  3. Commit the initial registry file.
  4. Add the scanner to CI so that new docs that skip registration are caught on the next PR.

The most common gap during bootstrap: context files (CLAUDE.md, AGENTS.md) are rarely registered, but they are among the highest-impact docs for agent sessions. Every agent that reads an unregistered, unverified context file is operating from a doc that no SLO watches.

What's Next

The next lesson builds the freshness gate — the CI job that reads verified_at and max_age_days from each DocRecord and fails the build when any doc has exceeded its SLO. The registry you built here is the data source that gate reads. A runbook verified 110 days ago on its 90-day SLO blocks the PR. An ADR verified 150 days ago on its 180-day SLO passes. The gate is what makes the SLO a real constraint rather than an advisory note.