diff --git a/docs/design.md b/docs/design.md
index 3730e77..c7226f4 100644
--- a/docs/design.md
+++ b/docs/design.md
@@ -188,11 +188,11 @@ The two had the identical effect on every daemon pump — one capability with tw
## 8. System artifacts (project scope)
-Most artifacts are working memory: a plan matters for the lifetime of its goal, then survives only as historical context. Some deliverables outlive any goal — an architecture overview, an inventory of what the system is made of, a glossary of the names the project uses for those parts, decision records (ADRs), conventions and onboarding docs. These are **system artifacts**: ordinary artifacts whose *type* declares `scope: project` in the registry. Nothing else about them is special — same request lifecycle, same `prev` versioning, same reviews (auto-review included; a wrong architecture doc is the most expensive artifact to leave unreviewed), same conversation — messages can anchor to an artifact directly (§4), so a system artifact carries its own thread without any goal.
+Most artifacts are working memory: a plan matters for the lifetime of its goal, then survives only as historical context. Some deliverables outlive any goal — an architecture overview, an inventory of what the system is made of, a glossary of the names the project uses for those parts, decision records (ADRs), feature design records (FDRs), conventions and onboarding docs. These are **system artifacts**: ordinary artifacts whose *type* declares `scope: project` in the registry. Nothing else about them is special — same request lifecycle, same `prev` versioning, same reviews (auto-review included; a wrong architecture doc is the most expensive artifact to leave unreviewed), same conversation — messages can anchor to an artifact directly (§4), so a system artifact carries its own thread without any goal.
The UI's **System section** is just "the current version of each project-scoped artifact chain" — a view the fold already computes. It collects two durability patterns with different maintenance stories:
-- **Decision records** (ADR-style) are append-only and immutable: never edited, only superseded by a later record (`prev`). No staleness problem — the chain is the value.
+- **Decision records** (ADR-style — the seeded `adr` for a decision about the system's shape, `fdr` for the design of one feature) are append-only and immutable: never edited, only superseded by a later record (`prev`). No staleness problem — the chain is the value.
- **Living documents** (architecture, architecture-inventory, glossary, conventions) are revised across many goals as `prev`-linked versions. Durability's real cost lives here: not generation, but maintenance.
Generation surfaces:
diff --git a/docs/radial-json.md b/docs/radial-json.md
index 5f70aa5..42a64ea 100644
--- a/docs/radial-json.md
+++ b/docs/radial-json.md
@@ -55,7 +55,8 @@ the directory it resolved and which rung chose it, and warns when
"architecture-inventory",
"glossary",
"conventions",
- "adr"
+ "adr",
+ "fdr"
]
},
"implementer": { "artifactTypes": ["implementation"] },
@@ -482,10 +483,10 @@ This is the one field whose correct value is not knowable from this file.
Artifact types are **per-space registry data**, not lexicon (design §4). A space
admin writes an `artifactType` record and every client grows a button for it — no
-deploy, no schema change. `radial space create` seeds seven: `plan` and
+deploy, no schema change. `radial space create` seeds eight: `plan` and
`implementation` (goal-scoped), plus the system types `architecture`,
-`architecture-inventory`, `glossary`, `conventions`, and `adr` (project-scoped,
-§8), whose current versions ride in every turn's bundle.
+`architecture-inventory`, `glossary`, `conventions`, `adr`, and `fdr`
+(project-scoped, §8), whose current versions ride in every turn's bundle.
What no client can grow is an agent. Three things have to line up:
@@ -522,7 +523,7 @@ space's agent records say, not only its own.
# 2. republish that profile's agent record
radiald init planner --update
✓ planner did:plc:… plan, architecture, architecture-inventory, glossary,
- conventions, adr (republished)
+ conventions, adr, fdr (republished)
```
`radiald init` prints what each profile now publishes, so the list is visible
diff --git a/docs/running-an-agent.md b/docs/running-an-agent.md
index 5705fef..8644e26 100644
--- a/docs/running-an-agent.md
+++ b/docs/running-an-agent.md
@@ -57,7 +57,7 @@ radiald config init # or: radiald config init agent.example
This prompts for the agent's atproto handle and writes
`~/.config/radial/radial.json` with three profiles on that one identity:
`planner` (plan plus the system types `architecture`,
-`architecture-inventory`, `glossary`, `conventions`, and `adr`),
+`architecture-inventory`, `glossary`, `conventions`, `adr`, and `fdr`),
`implementer`, and `reviewer` (which also carries `answer`, the built-in that
replies in a goal's thread). Profiles inherit the top-level `pds`, `harness`,
and `models`, and each publishes its own agent record, so one identity can
@@ -76,7 +76,8 @@ offer several capability sets:
"architecture-inventory",
"glossary",
"conventions",
- "adr"
+ "adr",
+ "fdr"
]
},
"impl": {
@@ -140,7 +141,7 @@ publishes:
```sh
printf '%s\n' "$AGENT_APP_PASSWORD" | radiald init --password-stdin
✓ planner did:plc:… plan, architecture, architecture-inventory, glossary,
- conventions, adr
+ conventions, adr, fdr
✓ implementer did:plc:… implementation
✓ reviewer did:plc:… review, answer
diff --git a/packages/core/test/validation.test.mjs b/packages/core/test/validation.test.mjs
index f749a9d..f75c848 100644
--- a/packages/core/test/validation.test.mjs
+++ b/packages/core/test/validation.test.mjs
@@ -47,6 +47,7 @@ describe('record validation', () => {
['glossary', 'project', 'living'],
['conventions', 'project', 'living'],
['adr', 'project', 'append-only'],
+ ['fdr', 'project', 'append-only'],
],
)
for (const record of builtins) {
diff --git a/packages/daemon/src/cli.ts b/packages/daemon/src/cli.ts
index 791781d..777fa93 100644
--- a/packages/daemon/src/cli.ts
+++ b/packages/daemon/src/cli.ts
@@ -790,7 +790,7 @@ const profileOf = (uri: string): string => uri.slice(uri.lastIndexOf('/') + 1)
*
* - `uncovered` — the registry has a type NO agent in the space accepts. Seeded system types are the
* common case: a space created today registers `architecture`, `architecture-inventory`,
- * `glossary`, `conventions` and `adr`, and a
+ * `glossary`, `conventions`, `adr` and `fdr`, and a
* `radial.json` written before them names none.
* - `stale` — a profile's `artifactTypes` in the config no longer match the agent record it
* published. The record is what an assignee menu reads, so editing the config alone changes
diff --git a/packages/daemon/src/config.ts b/packages/daemon/src/config.ts
index 446647e..a4d09d9 100644
--- a/packages/daemon/src/config.ts
+++ b/packages/daemon/src/config.ts
@@ -769,8 +769,8 @@ export function extraIdentities(config: RadialConfig, profiles: string[]): strin
* profiles are what let an operator route a different model to reviews.
*
* The planner also carries the system types `space create` seeds (§8) — writing an architecture doc,
- * keeping the inventory and glossary current, or distilling an ADR is the same register of work as
- * planning, and a seeded type no profile publishes is a ⊕ entry whose assignee list is empty.
+ * keeping the inventory and glossary current, or distilling an ADR or an FDR is the same register of
+ * work as planning, and a seeded type no profile publishes is a ⊕ entry whose assignee list is empty.
*
* The `run` block is emitted with the forge already wired. It is the one setting an operator has
* no way to guess is load bearing: without it the daemon cannot confirm a predecessor's pull
@@ -804,6 +804,7 @@ export function buildDefaultConfig(
'glossary',
'conventions',
'adr',
+ 'fdr',
],
},
implementer: { artifactTypes: ['implementation'] },
diff --git a/packages/daemon/test/config.test.mjs b/packages/daemon/test/config.test.mjs
index b3d4ac4..9001cfd 100644
--- a/packages/daemon/test/config.test.mjs
+++ b/packages/daemon/test/config.test.mjs
@@ -169,7 +169,7 @@ it('scaffolds a config that round-trips straight into agent registration', async
assert.deepEqual(
inits.map((init) => init.artifactTypes.join()),
[
- 'plan,architecture,architecture-inventory,glossary,conventions,adr',
+ 'plan,architecture,architecture-inventory,glossary,conventions,adr,fdr',
'implementation',
// The reviewer also answers: `answer` is a built-in turn type like `review`, and an agent that
// publishes neither cannot be asked for either.
diff --git a/packages/lexicons/fixtures/builtins.json b/packages/lexicons/fixtures/builtins.json
index 39a34e3..5640495 100644
--- a/packages/lexicons/fixtures/builtins.json
+++ b/packages/lexicons/fixtures/builtins.json
@@ -87,5 +87,18 @@
"scope": "project",
"durability": "append-only",
"createdAt": "2026-01-01T00:00:07Z"
+ },
+ {
+ "$type": "com.disnetdev.radial.artifactType",
+ "space": {"uri": "at://did:plc:root/com.disnetdev.radial.space/space", "cid": "cid-space"},
+ "name": "fdr",
+ "brief": "Distil the design of one feature from the supplied provenance. Write in the register of a design record — the problem, the behaviour being committed to, the design that delivers it, and the alternatives weighed against it — not as a plan with a new title. Keep it to this feature: a decision about the shape of the system belongs in an ADR. Name any earlier record this supersedes.",
+ "outputSpec": {
+ "format": "markdown",
+ "description": "A feature design record: status, problem, the behaviour it commits to, the design, alternatives considered, and anything left open."
+ },
+ "scope": "project",
+ "durability": "append-only",
+ "createdAt": "2026-01-01T00:00:08Z"
}
]
diff --git a/packages/sidecar/README.md b/packages/sidecar/README.md
index 0944aec..bf7cd83 100644
--- a/packages/sidecar/README.md
+++ b/packages/sidecar/README.md
@@ -84,7 +84,8 @@ requires it.
`space create` also publishes the built-in artifact-type registry records: the
goal-scoped `plan` and `implementation`, and the project-scoped system
artifacts `architecture`, `architecture-inventory`, `glossary`, and
-`conventions` (all living), and `adr` (append-only). Use `--no-builtins` only
+`conventions` (all living), and `adr` and `fdr` (append-only — a decision about
+the system's shape and the design of one feature). Use `--no-builtins` only
if another caller will seed them. `--json` returns structured refs for
scripts.
diff --git a/packages/sidecar/src/commands.ts b/packages/sidecar/src/commands.ts
index f84b4a4..eb3c10e 100644
--- a/packages/sidecar/src/commands.ts
+++ b/packages/sidecar/src/commands.ts
@@ -105,19 +105,25 @@ export interface CommandResult {
}
/**
- * What `space create` seeds into the registry (§4). Two goal-scoped types produce the work, five
+ * What `space create` seeds into the registry (§4). Two goal-scoped types produce the work, six
* project-scoped ones — system artifacts (§8) — hold what outlives it: the current version of each
* rides in every turn's bundle, so a space that seeds none starts its agents with no orientation and
- * no obvious place to put one. Durability is stated on the project-scoped five because it is what
+ * no obvious place to put one. Durability is stated on the project-scoped six because it is what
* decides whether a successor is "v3" or "a record that supersedes this one" (§3.3); the goal-scoped
* two leave it off and take the `living` default.
*
- * The project-scoped five are deliberately different shapes of orientation, because prose is the
+ * The project-scoped six are deliberately different shapes of orientation, because prose is the
* wrong container for some of it. `architecture` explains and `adr` justifies, but a fresh turn also
* needs to *look things up*: what the space calls a thing (`glossary`) and what the system is made of
* (`architecture-inventory`). Folded into the architecture doc, both decay — a list of every queue and
* component either bloats the prose or gets quietly dropped from it — so each is its own chain, with
* its own drift badge and its own request button.
+ *
+ * `fdr` is `adr`'s analogue one altitude down, and the same append-only register: an ADR settles why
+ * the SYSTEM is shaped as it is, an FDR settles what one FEATURE does and why it is designed that
+ * way. They are separate chains rather than one because they are superseded by different things — a
+ * structural decision outlives the feature that provoked it, and a feature's design is replaced by
+ * the next design of that feature, not by the next decision about the system.
*/
const builtinTypes = [
{
@@ -202,6 +208,18 @@ const builtinTypes = [
'A decision record: status, context, the decision taken, and its consequences.',
},
},
+ {
+ name: 'fdr',
+ scope: 'project' as const,
+ durability: 'append-only' as const,
+ brief:
+ 'Distil the design of one feature from the supplied provenance. Write in the register of a design record — the problem, the behaviour being committed to, the design that delivers it, and the alternatives weighed against it — not as a plan with a new title. Keep it to this feature: a decision about the shape of the system belongs in an ADR. Name any earlier record this supersedes.',
+ outputSpec: {
+ format: 'markdown' as const,
+ description:
+ 'A feature design record: status, problem, the behaviour it commits to, the design, alternatives considered, and anything left open.',
+ },
+ },
]
/**
diff --git a/packages/sidecar/test/space-create.test.mjs b/packages/sidecar/test/space-create.test.mjs
index 5e2cad5..d76af75 100644
--- a/packages/sidecar/test/space-create.test.mjs
+++ b/packages/sidecar/test/space-create.test.mjs
@@ -46,6 +46,7 @@ describe('space create seeds the registry', () => {
['glossary', 'project', 'living'],
['conventions', 'project', 'living'],
['adr', 'project', 'append-only'],
+ ['fdr', 'project', 'append-only'],
],
)
// Every seed points at the space that was just written, which is what makes them registry
diff --git a/packages/ui/src/lib/admin.ts b/packages/ui/src/lib/admin.ts
index 1a3766a..4a33ae0 100644
--- a/packages/ui/src/lib/admin.ts
+++ b/packages/ui/src/lib/admin.ts
@@ -577,7 +577,7 @@ export interface SpaceDraft {
/**
* `radial space create`. Seeding the built-ins — `plan` and `implementation`, plus the project-scoped
- * `architecture`, `architecture-inventory`, `glossary`, `conventions` and `adr` — is what
+ * `architecture`, `architecture-inventory`, `glossary`, `conventions`, `adr` and `fdr` — is what
* `--no-builtins` turns off and this never does: a
* space with an empty registry has a ⊕ menu with nothing in it and a System section that can never
* fill, and "add your own types" reads very differently on a screen that already has some than on one
diff --git a/packages/ui/src/lib/components/Glyph.svelte b/packages/ui/src/lib/components/Glyph.svelte
index 8a4a9d2..8b99a64 100644
--- a/packages/ui/src/lib/components/Glyph.svelte
+++ b/packages/ui/src/lib/components/Glyph.svelte
@@ -14,6 +14,9 @@
review: 'review',
'security-review': 'shield',
adr: 'adr',
+ // Kin to `adr` in register, deliberately not in shape: the two sit in the same System list, and
+ // a decision and a feature design that draw alike are two rows a person has to read to tell apart.
+ fdr: 'fdr',
architecture: 'arch',
conventions: 'book',
}
@@ -43,6 +46,9 @@
{:else if glyph === 'adr'}
+ {:else if glyph === 'fdr'}
+
+
{:else if glyph === 'arch'}
diff --git a/packages/ui/src/lib/requests.ts b/packages/ui/src/lib/requests.ts
index 0ce33ed..91afd77 100644
--- a/packages/ui/src/lib/requests.ts
+++ b/packages/ui/src/lib/requests.ts
@@ -1,8 +1,8 @@
// Asking the space for an artifact.
//
// This is the core bet of the product as a piece of UI. Nothing in this file knows a type name:
-// `plan`, `implementation`, `architecture`, `architecture-inventory`, `glossary`, `conventions` and
-// `adr` are records `space create` seeds
+// `plan`, `implementation`, `architecture`, `architecture-inventory`, `glossary`, `conventions`,
+// `adr` and `fdr` are records `space create` seeds
// into the registry and are otherwise no different from a `security-review` an admin adds tomorrow
// (design §4). So the ⊕ menu, the
// buttons under a landed artifact, the assignee list, and the words on the compose card are all
diff --git a/packages/ui/src/lib/units.test.ts b/packages/ui/src/lib/units.test.ts
index b110e2a..0b294db 100644
--- a/packages/ui/src/lib/units.test.ts
+++ b/packages/ui/src/lib/units.test.ts
@@ -21,6 +21,7 @@ import {
requestState,
unitActorDid,
unitBadges,
+ typeLabel,
unitSearchText,
unitSummary,
versionTitle,
@@ -39,6 +40,17 @@ const systemUnits = unitsOf(space.index, project)
// 0004 does not make an untitled `current` anywhere in System.
const goalUnits = space.index.goals.flatMap((goal) => unitsOf(space.index, goal))
+describe('typeLabel', () => {
+ it('capitalises the seeded acronyms and leaves every other registry name alone', () => {
+ // Typography, not semantics: the two seeded record types are acronyms when spoken, and a name the
+ // list has never heard of — a type an admin adds tomorrow — still has to draw as itself.
+ expect(typeLabel('adr')).toBe('ADR')
+ expect(typeLabel('fdr')).toBe('FDR')
+ expect(typeLabel('architecture-inventory')).toBe('architecture-inventory')
+ expect(typeLabel('security-review')).toBe('security-review')
+ })
+})
+
describe('versionTitle', () => {
it('answers per version, so a renamed document keeps each version’s own name', () => {
const architecture = systemUnits.find((unit) => unit.type === 'architecture')
diff --git a/packages/ui/src/lib/units.ts b/packages/ui/src/lib/units.ts
index db9ecc2..773251f 100644
--- a/packages/ui/src/lib/units.ts
+++ b/packages/ui/src/lib/units.ts
@@ -133,10 +133,13 @@ export const isGoalView = (view: GoalView | ProjectView): view is GoalView =>
/**
* A registry name as it is written in prose. Registry names are lowercase identifiers and are shown
- * as-is; `adr` is the one that is an acronym when spoken, so it is capitalised. This is typography,
- * not semantics — nothing anywhere decides what a type *means* from its name (§3.3).
+ * as-is; `adr` and `fdr` are the ones that are acronyms when spoken, so they are capitalised. This is
+ * typography, not semantics — nothing anywhere decides what a type *means* from its name (§3.3), and
+ * a name absent from this list still draws its row.
*/
-export const typeLabel = (name: string): string => (name === 'adr' ? 'ADR' : name)
+const SPOKEN_ACRONYMS = new Set(['adr', 'fdr'])
+export const typeLabel = (name: string): string =>
+ SPOKEN_ACRONYMS.has(name) ? name.toUpperCase() : name
/**
* Where a row hangs off, as the row says it: the goal's title, or `System · `.
diff --git a/packages/ui/src/lib/write.test.ts b/packages/ui/src/lib/write.test.ts
index ab1b169..b3d5fe8 100644
--- a/packages/ui/src/lib/write.test.ts
+++ b/packages/ui/src/lib/write.test.ts
@@ -1011,7 +1011,7 @@ describe('creating a space from a browser that has never opened one', () => {
// need the space transport it happens to be able to borrow here.
const result = await createSpace(spaceArgs({ name: 'Second', description: 'A space of its own.' }))
expect(parseAtUri(result.primary.uri).collection).toBe(COLLECTIONS.space)
- expect(result.refs.seededArtifactTypes).toHaveLength(7)
+ expect(result.refs.seededArtifactTypes).toHaveLength(8)
// Opened by URI, over `listRecords`, exactly as a space somebody handed you would be — nothing is
// carried across from the write, so if this tab can read it so can everyone else.
@@ -1028,6 +1028,7 @@ describe('creating a space from a browser that has never opened one', () => {
'architecture',
'architecture-inventory',
'conventions',
+ 'fdr',
'glossary',
'implementation',
'plan',