Maintain product specs¶
This guide is for the person who writes and curates specs: capturing a durable product rule as a spec node, and keeping the corpus correct over time. If you only need to find and read a spec, see Read and cite product specs. If you own the rules but would rather a coding agent drive these commands, see Manage product specs with a coding agent.
hadron spec is the opinionated layer over node/edge: it allocates the
next citation, scaffolds the required rubric, wires the edges, and enforces
the stability rules so a citation is a permanent handle. The full command
surface is in the
hadron CLI reference.
Prerequisites¶
- The hadron CLI installed and signed in — see
Install the hadron CLI. Check with
hadron auth status. - Write access to a memory that follows the loc-as-citation convention. The
flat examples below use
acme.com:specs; the product-rooted ones useacme.com:platform-specs. - Familiarity with how a citation is structured — Read a citation covers the two schemes and the tier names used throughout this page.
Set your corpus once so you can drop -m from every command:
The examples below keep -m explicit, since a maintainer often works across
more than one corpus.
Check (or declare) the scheme¶
Most single-product memories are flat (<module>:<feature>:<rule>) and
need no setup — skip ahead. Use the product-rooted scheme
(<product>:<module>:<feature>:<rule>) when one memory spans several
products, so each citation names its product.
spec describe reports which scheme a memory uses, the products and modules
present, and the per-tier counts:
The scheme is derived from the live specs, and a memory can also declare
it in its data — handy for an empty memory that wants to state its arity up
front, after which describe flags any drift. Declare it once, before the
first spec:
A memory should be all-flat or all-product; spec lint warns if it mixes the
two.
Decide whether it belongs¶
Write a spec when a change introduces a product-level rule a product manager would need to read — a threshold, a time window, a categorical rule, or a lifecycle / eligibility / delivery contract. Pure implementation details (file paths, query shapes, caching) do not belong here.
Pick the module (a frozen three-letter code, e.g. api for messaging) —
and, in a product-rooted corpus, the product (the shippable artifact,
e.g. srv) — then the feature the rule belongs to. See what already
exists and which numbers are free:
spec register prints the ledger derived from the live nodes, including the
next free feature per module and next free rule per feature.
Scaffold a spec into an existing feature¶
When the module and feature already exist, you're just allocating the next
rule. Preview it first — --dry-run writes nothing:
hadron spec new -m acme.com:specs --module api --feature 010 \
--title "W4 — 7-day check-in" --dry-run
The output shows the citation it would allocate (the next free rule under
api:010 — here api:010:04), the node name and tags, and the
table-of-contents and inheritance edges it would create. When it looks
right, create it — supplying the abstract and body up front so the spec is
complete:
hadron spec new -m acme.com:specs --module api --feature 010 \
--title "W4 — 7-day check-in" \
--abstract "Re-engage users who signed up but never finished setup, 7 days in." \
--content-file w4.md
Without --abstract / --content-file, the spec is created from a rubric
template — a placeholder abstract and the four mandatory section
headings (Definition, Rule & examples, Durable vs tunable, What invalidates
this spec) — which you fill in afterward.
To change a spec that already exists, see Amend a spec: it
covers the amend-vs-supersede decision and spec edit, which opens the body
and abstract together because they are one logical unit.
Scaffold a whole branch¶
A new rule usually needs a new feature, and sometimes a new module or product
above it. --new-path creates the citation you name plus every missing
ancestor in one call:
✓ created srv:api:010:01 — srv:api:010:01 — Place new order
edge: Place new order → srv:api:010
edge: inherits the shared contract (general provisions) → srv:api:010:00
✓ created srv — srv — srv
✓ created srv:gen — srv:gen — srv general provisions
edge: srv general provisions → srv
✓ created srv:api — srv:api — api
edge: api → srv
edge: inherits the shared contract (general provisions) → srv:gen
✓ created srv:api:000 — srv:api:000 — api general provisions
edge: api general provisions → srv:api
✓ created srv:api:010 — srv:api:010 — 010
edge: 010 → srv:api
edge: inherits the shared contract (general provisions) → srv:api:000
✓ created srv:api:010:00 — srv:api:010:00 — 010 general provisions
edge: 010 general provisions → srv:api:010
Your --title goes on the citation you named. The ancestors get
placeholder titles taken from their own citation segment — srv:api is
titled api, srv:api:010 is titled 010. They lint clean (the name leads
with the citation), so nothing will remind you. Rename them afterward:
--dry-run previews the whole chain the same way.
Every root also gets its contract¶
Creating a root — with --new-path, or with --new-product /
--new-module / --new-feature — also scaffolds that tier's
general-provisions contract, which is why seven nodes appear above for one
rule. Each root's children inherit that contract, and creating it up front is
what lets the inheritance edges wire themselves:
| Creating this root | also creates |
|---|---|
a product (srv) |
srv:gen |
a module (srv:api) |
srv:api:000 |
a feature (srv:api:010) |
srv:api:010:00 |
Pass --no-contract to suppress it. You rarely want to — a tier without its
contract is the case the retrofit below exists to clean up.
Building a corpus one tier at a time¶
--new-path is the short form of a top-down build. The explicit form is
still there when you want to title and populate each tier as you go:
M=acme.com:platform-specs
hadron spec new -m $M --new-product --product srv --title "Server"
hadron spec new -m $M --product srv --new-module --module api --title "Ordering API"
hadron spec new -m $M --product srv --module api --new-feature --title "Order placement"
hadron spec new -m $M --product srv --module api --feature 010 --title "Place new order"
Each level must exist before its children, and each of the first three calls
also creates its contract, exactly as above. Product and module codes are
frozen once created — spec new refuses to mint one that already exists
(exit 5).
Everything else — list --prefix srv, lint, find, supersede — works
the same in a product-rooted corpus; the citations simply carry the product
(srv:api:010:01).
When scaffolding fails¶
Two different failures look similar and are worth telling apart. Both exit non-zero, but only one of them writes anything.
A missing parent tier is rejected up front (exit 4). Nothing is created:
Create the parent tier and re-run — or use --new-path, which mints the
missing ancestors for you.
An edge that can't be wired fails after the node is created (exit 1). A
spec without its table-of-contents and inheritance edges is silently
orphaned, so spec new refuses to report ✓ created and tells you what's
missing instead:
hadron: created srv:api:010:01 but failed to wire 1 required edge(s) to
srv:api:010 — the node is orphaned; fix the target(s) and re-run, or wire
the edge(s) with `hadron edge add`
This one genuinely half-worked: the node exists. Fix the edge target and either re-run or wire the edge by hand — don't assume a clean slate.
Share provisions across siblings¶
When a provision is shared by every sibling at a tier — every rule of a
feature, every feature of a module, or every module of a product — put it in
that tier's general-provisions contract instead of repeating it. Siblings
inherit it automatically. Roots created by spec new already have theirs;
to add one to a tier that doesn't, use --contract at the deepest level you
name:
| Shared across | Contract citation | Create with |
|---|---|---|
| every rule of a feature | api:010:00 |
spec new --module api --feature 010 --contract |
| every feature of a module | api:000 |
spec new --module api --contract |
| every module of a product | srv:gen |
spec new --product srv --contract |
The numeric tiers use their reserved zero (00, 000); the alpha product
tier uses the reserved code gen.
Add a contract to a tier that already has siblings¶
Introducing a contract into a tier that already has siblings is a
retroactive constraint: the moment api:000 exists, every sibling at that
tier must inherit it — and the pre-existing ones carry no such edge. spec
lint fails for each:
CITATION SEVERITY RULE MESSAGE
api:010 error inheritance-edge no inheritance edge to general-provisions contract api:000
api:020 error inheritance-edge no inheritance edge to general-provisions contract api:000
api:030 error inheritance-edge no inheritance edge to general-provisions contract api:000
The remedy isn't in the message: back-wire each sibling to the contract with
the inheritance edge spec new would have added. Use hadron edge add (or the
hadron_create_edge MCP tool), with the name inherits the shared contract (general
provisions):
for f in api:010 api:020 api:030; do
hadron edge add --from "hrn:node:acme.com:specs:$f" --to hrn:node:acme.com:specs:api:000 \
--name "inherits the shared contract (general provisions)"
done
Re-lint to confirm the tier is clean:
The same retrofit applies at all three tiers — feature siblings back-wired to a
module's :000, or module contracts to a product's :gen — not just the module
tier shown here.
An outlier sibling still inherits¶
The rule is all-or-nothing per tier, which can feel forced when a tier is heterogeneous — say a module whose contract states a scoring model, but one of its features is an A/B assignment mechanism rather than a scoring rule. That feature still needs the edge. Read the inheritance edge as "governed by the tier's general provisions," not "is another instance of the same kind." Keep each contract broad enough to genuinely cover every sibling; an edge on a true outlier means only that it is bound by the shared provisions.
Lint it¶
spec lint checks a spec against the rubric and the stability rules. A
freshly scaffolded spec fails until you replace the placeholder abstract and
state what invalidates it:
hadron spec lint api:010:04 -m acme.com:specs # one spec
hadron spec lint --prefix api:010 -m acme.com:specs # a feature and its rules
hadron spec lint --module api -m acme.com:specs # a whole module
hadron spec lint --product srv -m acme.com:platform-specs # a whole product
hadron spec lint --all -m acme.com:specs # the corpus
Errors — a missing abstract, no "what invalidates" statement, a malformed
citation, a name that doesn't lead with the citation, or a missing inheritance
edge to a tier's
general-provisions contract
— exit with code 5. Warnings (a missing data.version, an absent edge, a
mixed-arity corpus) do not, unless you pass --strict. Fix the findings and
re-run until it is clean.
Restructure without breaking citations¶
Three commands cover the common reshaping moves, all of which preserve existing numbers:
spec extractsplits a sub-rule out of a fat parent into its own citation under another feature, piping the moved chunk in as the new body and auto-wiring a cross-reference edge back.--strip-sourcealso trims the chunk from the source, but only when it matches verbatim.spec linkcross-references one spec from another by bare citation, validating that both are specs in the same corpus. The convention is that the more specific spec cites the more general one.spec replacedoes a citation-aware find/replace across every spec's body and abstract. It is word-boundary-aware by default, so renamingh-read-nodenever touchesh-read-nodes. Preview with--dry-run, cap the blast radius with--max-specs N, and pass--yesnon-interactively; changed specs are re-linted afterward.
Replace a spec without renumbering¶
A citation is a permanent handle, so you never change a number in place. To
replace a rule, supersede it: spec mints the next free number, links
the old spec to the new one with a superseded-by edge, and tags the old
spec superseded (its loc is untouched). Like the other destructive
commands, it requires --yes when run non-interactively:
--copy-body seeds the replacement from the old spec's content and abstract.
Afterward, mark the old number retired in the register ledger — the command
prints a reminder, and never edits the register itself.
Keep the corpus honest¶
Two checks are worth running in CI; both exit 5 on findings.
hadron spec lint --all -m acme.com:specs # rubric + stability rules
hadron spec check-tools -m acme.com:specs # hadron_* tool references
hadron spec register --check -m acme.com:specs # ledger drift vs live nodes
spec check-tools flags hadron_* tool names in spec prose that aren't real
registered tools — the drift that lets stale shorthand rot unnoticed.
spec register --check diffs the hand-written register ledger against the
live nodes; the register is advisory and spec never writes it.
Related¶
- Read and cite product specs — the read side: search, resolve a citation, and follow inherited provisions.
- hadron CLI reference → Product specs — the full command surface, numbering policy, and semantics.
- Use Obsidian to view/edit memory — open the synced or exported repo as an Obsidian vault, and make citation-numbered nodes readable.