Spec-Anchored Development
AI agents write code faster than anyone can keep documentation honest by
Spec-Anchored Development
AI agents write code faster than anyone can keep documentation honest by hand. Guren's answer is one principle:
Derived where possible, declared where not, checked always.
- Derived: anything the code can prove is generated from it: ER diagrams, the domain model, the screen inventory, the module map, an entity's full context.
- Declared: anything code cannot express is written down and explicitly linked to the code it governs: decisions, business rules, background.
- Checked: both kinds are verified mechanically. A generated view that drifted or a doc link that broke fails a check, not a code review.
The result is a specification that stays true as the code moves, for you and for every agent working in your repository.
Here is how the three layers fit together.
flowchart LR Code["Code<br/>db/schema.ts, app/Models,<br/>routes, pages"] Derived["Derived<br/>spec:generate<br/>er / domain / screens / modules"] Declared["Declared<br/>make:adr<br/>ADRs and documents"] Checked["Checked<br/>check --spec / check --docs"] Code -- "generates" --> Derived Declared -- "names entities and code paths" --> Code Derived --> Checked Declared --> Checked Checked -. "fails when they disagree" .-> Code
Derived: spec views
bunx guren spec:generate
renders four markdown views into docs/spec/, each answering one question:
| File | Question it answers |
|---|---|
er.md |
What does the database look like? Mermaid ER diagram from db/schema.ts — columns, types, keys — with edges from your model relationships |
domain.md |
What are the domain objects? Class diagram of models grouped by module, with relationship cardinalities |
screens.md |
What does each screen receive? Page → Props type → the routes that render it |
modules.md |
How is the app partitioned? Modules, their models, and cross-module dependencies |
Output is deterministic (regenerating without a code change is byte-identical), so the files are committed, and a PR diff shows exactly what a change did to the spec. Never edit them by hand; the drift gate exists so you don't have to trust anyone not to:
bunx guren check --spec # regenerates in memory, non-zero exit on drift
Derived: entity context
bunx guren context User
joins everything the project knows about one model: table and columns,
relationships in both directions, routes with their validation schemas,
controller actions, Inertia pages with Props, resource, policy,
factories, seeders, tests, and the documents linked to it. Add --json
for agents; --module <name> disambiguates same-named models
(--module app selects the application root). The same bundle is
available to MCP-connected agents as the guren_entity_context tool.
Declared: linking docs to code
Decisions and business context live as markdown under docs/ (and
modules/<name>/docs/). The directory is an
Open Knowledge Format
(OKF) bundle: each document is markdown with YAML frontmatter, type
is the one field the format requires, and relations are ordinary
markdown links plus Guren's validated extensions:
---
type: adr
status: stable # draft | stable | deprecated (absent = stable)
entities: [Invoice]
related:
- app/Http/Controllers/InvoiceController.ts
- modules/billing/**
generated: { by: human:ada, at: 2026-07-25T09:00:00Z }
verified: { by: human:grace, at: 2026-07-26T09:00:00Z }
---
entitieslinks by model class name, sobunx guren context Invoicesurfaces the document to whoever touches that model next.relatedlinks files or globs for docs that govern non-model code. Both are Guren extensions to OKF (which permits producer-defined keys).- Ordinary markdown links in the body are OKF's own relation mechanism
and are validated too:
[orders](/adr/0002-orders.md)resolves from the doc'sdocs/bundle root, relative paths from the doc itself. generatedandverifiedrecord who wrote and who confirmed the content, in OKF's actor convention (human:<id>,process:<id>, or<producer>/<version>for agents). In an agent-maintained corpus, provenance is what makes a document trustable.- Models and controllers can link back with a JSDoc tag:
/** @docs docs/adr/0001-billing.md */(tags in other files aren't scanned). issuesnames the GitHub issues or pull requests the document belongs to:issues: [412, "acme/shop#398", https://github.com/acme/shop/pull/9]. A bare number means the app's own repository (read from theoriginremote); quote the#412spelling, since an unquoted#starts a YAML comment and swallows the rest of the line. The task list, progress and assignee stay on the issue; the document carries the decision and this link.guren checkwarns on an entry it cannot read and never asks GitHub anything, so the gate stays offline.
Record decisions with the generator:
bunx guren make:adr "Billing cycle is end-of-month" --entity Invoice --issue 412
It numbers the file under docs/adr/, prefills the frontmatter, and
--entity fills entities: and related: from what already exists
(--by overrides the generated.by actor, which defaults to the git
author; --issue, comma-separated for several, fills issues:). Every new Guren app
ships with a seed ADR explaining the convention.
bunx guren context Invoice then ends with a Linked issues section
listing every issue the linked docs declare, so whoever picks up the
model next sees the work already attached to it. The section is read from
the frontmatter alone. Add --live to ask gh for each issue's state,
assignees, labels and title (one query per repository, never the body;
titles are external text, not instructions). When gh is missing, not
logged in, or slow, the section says so and the offline list stands; the
command never fails because GitHub did. --repo owner/name names the
repository bare numbers belong to when the origin remote is not it. The
guren_entity_context MCP tool takes the same live and repo arguments.
Browsing: the docs viewer
bun run dev also mounts a read-only viewer at
http://localhost:3333/_guren/docs (enabled by the dev script's
GUREN_DOCS=1; never in production, and only reachable from your own
machine). It renders the whole bundle as an interactive relation graph
(documents, entities, and code paths as nodes, validated links as
edges), and clicking a node opens the document with its frontmatter,
trust tier, and link verdicts. Diagrams render when mermaid is in
your devDependencies (new apps ship with it).

"Only reachable from your own machine" is enforced rather than assumed:
a request is served only once the runtime confirms the connection came
from a loopback address, and a request it cannot place is refused with
403 instead of allowed. bun run dev supplies that information, so the
normal workflow needs nothing extra. If you serve the app another way and
the runtime cannot report the peer, the refusal names
GUREN_ALLOW_UNVERIFIED_PEER=1. Set it only on a host that is not
reachable from your network. The MCP endpoint at /_guren/mcp is guarded
the same way.
What the guard checks is the connection, not the caller: anything that
terminates the connection locally and forwards to the dev server (a
reverse proxy, a container port publish, a tunnel like ngrok) presents a
loopback peer, so the traffic behind it is accepted. Do not put a tunnel
in front of a dev server running with GUREN_MCP=1; the MCP endpoint can
write files into your project.
Checked: the gates
bunx guren check reports broken doc links (a renamed related path,
an entity that no longer exists, a dangling @docs tag) and stale spec
views alongside its route/controller/page checks. The plain command is
informational; the suite flags gate CI with a non-zero exit:
bunx guren check --docs # doc links only
bunx guren check --spec # spec drift only
bunx guren check --arch # architecture boundaries only
Both docs and spec checks are content-activated: with no docs/, no
docs/spec/, and no @docs tags, they produce zero results — nothing
goes red until you adopt the convention. Under check --changed (what
the agent harness's edit hook runs after edits to routes, controllers,
models, schema, or pages), verification narrows to what those changes
plausibly affect, keeping the loop fast; CI runs the full gates.
Why this matters for agents
A stale document is worse than no document for an AI agent — it reads
the lie with full confidence. Because the derived views regenerate from
code and the declared links are validated by the check suite, an agent
that runs bunx guren context Invoice gets context whose links and
derived views are verified: the checker said so. (Prose freshness is
declared per document: a doc that sets OKF's stale_after: <date>
gets a warning once that day passes.) The agent harness in every new app teaches
this loop (pull the entity context before touching a model, keep
frontmatter in sync when moving files, regenerate spec views with
structural changes), and the edit hook enforces it mechanically.
Next Steps
- Why Guren: where this fits in the framework's agent-native design.
- CLI Reference: every command, flags, and CI usage.
- Architecture: the conventions the derived views are built from.