three-part-body¶
A spec body has two labelled parts — raw source (human) · expanded spec (agent); current state is DERIVED, never narrated.
raw source¶
A spec body is hard to keep honest when one blob of prose mixes the human's intent and the agent's detailed reading of it. Split it into two parts with two owners. (1) RAW SOURCE is the human's raw intent/decisions — very short, rarely changed, and changing it needs human approval. (2) EXPANDED SPEC is the agent's detailed behavioral understanding — not implementation — versioned freely, but it must always still match the raw source.
There is deliberately no agent-authored "current state" part. An agent narrating what's-done
hallucinates completion, so the node's progress must be DERIVED, never narrated: the derived
4-state status (pending/active/merged/drift), the version count, and the drift figure already answer
"what's done" from git — facts, not prose. Keep the body a living document — no ## vN headings.
expanded spec¶
The two parts are clearly-delimited markdown sections in spec.md, so a human reading the raw file
and an agent reading /api/specs see the same structure:
## raw source— part 1.## expanded spec— part 2. May carry its own###subsections for structure; they are content of this part, not new parts.
The backend parser (parseParts in specs.ts) is fence-aware and matches exactly two-hash
headings for the two part names. A body that uses neither heading parses to null, and every existing
one-blob spec keeps rendering whole — the structure is opt-in, never forced. Because the part names
are structure headings (not ## vN changelog headings), spex lint's living rule stays satisfied
and the feature ships at 0 errors.
loadSpecs() attaches parts ({ rawSource, expandedSpec } or null) to each node, and /api/specs
exposes it verbatim. The dashboard NodeView renders each part as its own card with an owner badge
(human vs agent) and a stability note, so the reader sees who owns each part and how often it changes;
legacy null-parts nodes fall back to the whole-body view. The NodeView meta line already carries the
derived status, version, and drift — that is where "what's done" is read, so there are no progress or
self-assessment cards to render and the parser carries no currentState field.
This is a cross-cutting contract, not a code-owning node: it governs no source file of its own. Its
two halves live where their primary concern does — parseParts rides in specs.ts (source-of-truth,
the loader/aggregator) and the TwoPart card rendering in NodeView.jsx (work-pane, the node popup).
Listing neither here is deliberate: a change to the loader or the popup is their node's drift, never a
phantom warning on this body-structure contract.