Document Types
Table of Contents
Every .org file in ORE Studio carries a #+type: field that places it
in a shared taxonomy. The taxonomy exists for three reasons. First,
structural contracts: each type defines exactly which sections,
frontmatter fields, and lifecycle (if any) a document must carry — an
LLM or human loading any file immediately knows what shape it is and
what rules apply. Second, queryability: #+type: is the primary
filter for compass list, grep, and the audit scripts, making it
possible to ask "all knowledge docs on security" or "every in-flight
task" without noise from unrelated file shapes. Third, level
alignment: types map to cybernetic levels — task to S1, story to S2,
sprint to S3, version to S4, product_identity to S5 — so the
type of a document tells you who owns it and at what time horizon. The
#+description: field, capped at 120 characters, is the load-bearing
summary every index and search reads first; it is as important as the
type itself.
The full vocabulary of types is in * Document types below. The rules
for standard frontmatter, state, linking, and tags apply across all
types and are defined in the sections that precede it.
Creating documents
Never hand-write frontmatter or IDs. Use the generator:
projects/ores.codegen/generate_doc.sh --type <type> --slug <slug> \ --parent-dir <dir> --title "..." --description "..." --tags "..."
See How do I create a new doc? for copy-pasteable invocations per type. The contract each generated file follows is described below.
Standard frontmatter
Every document under doc/ begins with this block. Fields marked
required must be present.
,:PROPERTIES: ,:ID: <UUID4 from `uuidgen`> ; required — drives org-roam ,:END: #+title: <human-readable title> ; required #+description: <one-liner, ≤ 120 chars> ; required #+type: <one of the types below> ; required #+filetags: :tag1:tag2: ; required (filetags, not tags) #+created: YYYY-MM-DD ; required #+updated: YYYY-MM-DD ; required #+todo: DISCOVERED BACKLOG STARTED BLOCKED | DONE ABANDONED ; required for stateful #+owner: <handle> ; required for tasks #+branch: <git branch> ; for executing tasks #+pr: <number-or-url> ; once a PR is open #+blocked_on: <review|ci|local_build|external|none> ; for executing tasks #+blocked_since: <YYYY-MM-DD HH:MM> ; when blocked_on != none
State is not a frontmatter field. State lives as an org-mode TODO
keyword on the * Status headline inside the document (see State as
TODO below). Folders never encode state.
The #+description line is the load-bearing field. It is what every
grep, index, and search reads. Write it as if it is the only line anyone
reads.
Dates are absolute (2026-05-18), never relative. If you find yourself
writing "yesterday" or "next week", convert it.
Why these specific forms
:PROPERTIES: :ID:— org-roam tracks documents by ID, not by path. Every document needs one; generate withuuidgen. Paths can change; IDs do not. Every UUID — both the:ID:property and every[[id:UUID]]link — is uppercase hex. The codegen enforces this for new documents (doc_generate.pyuppercases the freshly minted ID, the templates carry uppercase concept-link UUIDs, and--parent-id/--predecessor-idare uppercased defensively); any hand-edited or copy-pasted UUID must follow the same rule. The Sprint 18 story Lock down uppercase UUID invariant records the rationale and the audit trail.#+filetags:— the standard org-mode keyword for applying tags to a whole file.#+tags:in standard org-mode declares the allowed tag vocabulary for a file, which is a different concern and not what we want here.#+todo:— declares the TODO state vocabulary used by the document. Stateful documents use TODO keywords on their* Statusheadline; the folder structure carries no state information.- No
#+links:keyword — links live in the document body as id: links (see Linking between documents below). Org-roam picks them up automatically.
State as TODO
State lives inside the document, as an org-mode TODO keyword on the
* Status headline. The folder structure encodes composition (which
task belongs to which story, which story to which sprint, etc.) but
never state.
The vocabulary
#+todo: DISCOVERED BACKLOG STARTED BLOCKED | DONE ABANDONED
Active states are left of the bar; terminal states are right.
DISCOVERED— found in passing while executing another task; not yet triaged.BACKLOG— triaged, scheduled, not started.STARTED— an agent is actively working on it. Org auto-stamps the transition viaLOGBOOK.BLOCKED— paused waiting on something. The reason lives in#+blocked_on(one ofreview,ci,local_build,external).DONE— terminal success. Org auto-stampsCLOSED:on the headline.ABANDONED— terminal failure. The reason lives in the Status** Notesor similar block; audit flags abandoned tasks without one.
Where the TODO state lives
Every stateful document has a * Status section formatted as a table.
The state lives in the State row (greppable), and the row order is
fixed: State / Parent <type> / Now / Waiting on / Next / Last touched.
Versions are top-level so the Parent row is dropped.
* Status , ,| Field | Value | ,|--------------+------------------------------------| ,| State | STARTED | ,| Parent story | Currencies end-to-end | ,| Now | Implementing the XML serialiser. | ,| Waiting on | Nothing. | ,| Next | Add round-trip tests. | ,| Last touched | 2026-05-21 |
The Status section sits after the document's "what is this?" section
(* Goal for tasks/stories, * Mission for sprints, * Identity for
versions) so the reader knows what the doc is before reading its
state. Operational detail of in-flight work goes in Now / Waiting on
/ Next / Last touched and is updated on every agent or orchestrator
action.
Why state-on-headline, not state-in-folder
- Shallower trees. The folder structure mirrors composition only (version → sprint → story → task), no state subdivisions.
- Native org-mode tooling. Org-agenda, the org-mode mode line, and
TODO cycling (
C-c C-t) all work out of the box on the state of the* Statusheading. - Atomic transitions. Changing state is one edit; the diff is small.
- State-by-grep.
grep -lR "^| State.*STARTED"finds every in-flight document in seconds.
Linking between documents
Every link between documents uses org-roam's id: form:
This is what makes the org-roam graph work. [[file:...]] links are
standard org-mode but invisible to org-roam, so we do not use them.
Conventions:
- Every stateful document carries an outbound link to its direct parent
(a task to its story, a story to its sprint, a sprint to its version).
The link sits in the
Parent <type>row of the* Statustable and in the top-of-page blurb, both pointing at the same:ID:. - Every stateful document opens with a one-sentence blurb above the
first section that hyperlinks the type word (
task,story,sprint,version) to the glossary entry for that type. The blurb is the reader's orientation: what kind of document this is and who its parent is. - Sibling links (related tasks, related stories) live in a
* See alsosection or inline as the prose calls for them.
Tags
Tags are required on every document. They are the primary cross-cutting index — every grep for "all knowledge about Qt" or "every task touching the SQL component" goes through tags. If tags rot, the system loses its lateral navigation.
Format
- Standard org-mode tag syntax:
:tag1:tag2:tag3:— colons on both sides of each tag, no spaces inside. - All lowercase. Multi-word tags use
snake_case. - At least one tag is required. Two to four is typical. More than six is usually a sign the document needs to be split.
Tag dimensions
A document's tags should cover the dimensions below where applicable. Tags are flat (no hierarchy), but readers should be able to infer the dimension from the tag itself.
- Component — which area of the codebase:
trading,sql,qt,cli,http,compute,shell,codegen,modeling, … - Domain topic — what the content is about:
fx_forward,pricing,reporting,observability,ci,build,release,security,ux, … - Activity — what kind of work:
refactor,new_feature,bug_fix,documentation,migration,cleanup,testing, … - Status modifier (optional) —
blocked,needs_review,urgent,stale.
Examples
- A task to add an FX Forward import test:
:trading:fx_forward:testing:new_feature: - A recipe for running the build:
:cmake:build:recipe: - A knowledge doc on the codegen architecture:
:codegen:architecture:knowledge: - A sprint focused on Qt UI work:
:qt:ux:sprint:
Ancestor tags
Every stateful document carries one tag for each of its ancestors in
the composition tree (immediate parent first). This duplicates the
#+links relationship in a grep-friendly form: #+links drives the
org-roam graph, the ancestor tags drive the shell.
- A task tags with its story's slug, and its sprint's slug, and its version's slug.
- A story tags with its sprint's slug, and its version's slug.
- A sprint tags with its version's slug.
Examples (filetags only, content tags omitted):
- A task in
versions/v0/sprint_03/login_and_sessions/→:login_and_sessions:sprint_03:v0:. - A story in
versions/v0/sprint_03/→:sprint_03:v0:. - A sprint in
versions/v0/→:v0:.
Single-step lateral queries:
# every task / story / file in sprint_03 grep -lR ":sprint_03:" doc/agile/versions/ # every story in story currencies_end_to_end (which is one, but you # can grep for siblings on the same theme by switching the tag) grep -lR ":currencies_end_to_end:" doc/agile/versions/ # every file in version v0 grep -lR ":v0:" doc/agile/versions/
The generator at projects/ores.codegen/generate_doc.sh derives
the ancestor slugs from the --parent-dir path (following the
versions/<version>/<sprint>/<story>/ layout) and adds them all to
#+filetags automatically, on top of any --tags the user supplies.
Story categories
Stories carry a #+category: frontmatter field (not a tag) that
classifies their intent. Categories are used to slice velocity metrics:
feature throughput, housekeeping load, and enabler investment are
tracked separately so the team can see whether sprint time is going to
the right places.
| Category | Meaning |
|---|---|
feature |
Adds or changes user-visible behaviour. Counted in feature velocity. |
housekeeping |
Cleanup, refactoring, dependency upgrades, docs, tooling fixes. Does not advance features but is necessary work. |
enabler |
Infrastructure, scaffolding, CI, build, or platform work that unblocks future features but delivers nothing user-visible on its own. |
Rules:
- Every story must carry exactly one category.
- When a story spans multiple categories, assign the dominant intent.
A story that mostly refactors but incidentally adds an endpoint is
still
housekeeping. - Sprints should aim for ≥ 50%
featurestories by count. A sprint skewed towardhousekeepingorenablershould be noted in the health review focus signal.
Governance
- The audit job (System 3*) lists every tag in use and flags singletons (tags used exactly once) for review — they are usually typos or one-offs that should fold into an existing tag.
- The canonical Tag inventory (
doc/meta/tag_inventory.org) lists every valid tag grouped by dimension;compass lintvalidates against it. - Audit also verifies the parent-tag invariant: every task must carry exactly one story slug as a tag; every story must carry exactly one sprint slug; every sprint must carry exactly one version slug.
Document types
Each type's contract lives on its own page:
- task
- story
- sprint
- version
- capture
- memory
- product_identity
- function
- recipe
- runbook
- component
- skill
- manual
- knowledge
- plan
- decision
- retrospective, release_note, audit_report
- investigation
- meta
Codegen model types live with their component under
projects/ores.<component>/modeling/ and are consumed by
ores.codegen rather than published; their contract is defined by the
loader (org_loader.py) and the scaffolding templates:
entity_org(#+type: ores.codegen.entity) — a full entity model driving C++ domain class, mapper, repository, service, Qt UI and SQL generation. Scaffold:compass add entity_org.field_group(#+type: ores.codegen.field_group) — a named group of related fields generated as a plain C++ sub-struct, composed by a hand-written parent (the C1202 decomposition pattern). Filenames end_field_group.org. Scaffold:compass add field_group.dataset_overview— seeder dataset description. Scaffold:compass add dataset_overview.