title: "The spec editor" description: "A hierarchical, typed specification tree with references, status tracking, AI assistance and markdown generation." nodes: 21 progress: 71% generated: ohm spec editor#
The spec editor#
A hierarchical, typed specification tree with references, status tracking, AI assistance and markdown generation.
Contents#
☑ Overview#
☑ Scope#
note
packages/core/src/specs.ts, apps/ui/src/views/SpecEditor.tsx and the
specs/*.spec.json documents. This very document is written in that format —
docs/specs/ is generated from data and published as both .spec.json and
.spec.md.
☑ Specs are a tree of typed nodes, not a document#
decision
Prose in a wiki drifts from the code. A spec node has a kind, a status, a priority and typed references that point at real things in the project — a prompt, a curve, an experiment, a script line, another node — so the spec can be navigated, filtered and progress-tracked, and generating markdown is a rendering step rather than the source of truth.
☑ Model#
☑ SpecDoc and SpecNode#
data-model
SpecDoc { version: 1, name, description?, updatedAt?, nodes: SpecNode[] }
SpecNode { id, title, kind, body?, status?, priority?, tags?, refs?,
prompt?, collapsed?, children: SpecNode[] }
Kinds (15): section, requirement, api, component, data-model, test, flow, state, task, prompt, reference, note, question, decision, risk.
Statuses (7): none, todo, doing, blocked, review, done, cancelled — rendered as ☐ ◔ ⛔ 👁 ☑ ✗ (none is blank).
Priority: low, normal, high, urgent — only non-normal is shown.
prompt is { text?, profile?, temperature?, maxTokens?, lastOutput?, lastRunAt? }
and is only meaningful on a prompt node.
☑ References#
data-model
{ kind, target, line?, note?, blocking? } over eleven kinds: prompt, curve, graph,
experiment, spec, doc, data, script, link, url, node.
"Blocking" is declarative: it renders as depends on and draws a solid rather than dotted edge in the generated graph. Nothing propagates status or detects cycles — deliberately, for now.
Opening a reference navigates: urls and links open a browser tab, docs/data/scripts
open in the text editor, node selects that node, and the rest open the
corresponding item editor.
☑ Progress#
data-model
tracked = total − none − cancelled; progress = done / tracked, and 0 when
nothing is tracked. So notes and cancelled work do not dilute the number.
☑ The editor#
☑ Keyboard-first tree editing#
requirement
| Key | Action |
|---|---|
| ↑ / ↓ | move selection through the visible rows |
| → / ← | expand / collapse |
| Enter | new sibling; Ctrl+Enter new child |
| Tab / Shift+Tab | indent (becomes last child of the previous sibling) / outdent |
| Alt+↑ / Alt+↓ | move within siblings |
| Delete | remove |
A new node inherits the selected node's kind unless that is section. Every
structural action is also a toolbar button whose tooltip names the shortcut.
Drag and drop reorders; holding Alt or Shift drops inside the target. Dropping onto self or a descendant is rejected.
☑ Detail pane#
component
Breadcrumbs, title, kind/status/priority, tags (committed on blur), a markdown body, the reference editor, the inline prompt block for prompt nodes, and History — the item version list with a Restore that loads the snapshot and leaves the document dirty.
- spec:
08-projects-and-research— version snapshots
☑ AI actions#
component
Expand into children — asks for 3–6 items as
title — one sentencegiven the spec name and the ancestor path, parses the reply line by line (stripping list markers, capped at 8), and appends them astodochildren whose kind isrequirementunder a section and the parent's kind otherwise.Draft body — a short specification paragraph, appended to the existing body.
Run on a prompt node — generates with that node's profile and parameters and stores
lastOutput/lastRunAt, with a separate action to copy the output into the body.spec:
04-models-and-settings
☑ Markdown generation#
flow
specs.markdown(name, write) renders, and with write writes
docs/<name>.spec.md and returns the path. The structure is fixed and test-pinned:
- YAML front matter — title, description, project, node count, progress percentage, generator and timestamp.
# nameand the description.## Contents— a table of contents of the top two levels, only when there is more than one entry.- Each node depth-first as
#### <a id="…"></a>☑ Title(heading level capped at 6), then a badge line of kind, non-normal priority and#tags, the body, a ```prompt fence, the last model output in a<details>, and one bullet per reference. ## Links between nodes— a mermaidgraph LRof node-to-node references, solid for blocking and dotted otherwise, emitted only when such references exist.
- node:
refs
☑ Script API#
api
specs.list(), specs.get(name), specs.save(name, doc),
specs.markdown(name, write) — so a spec can be generated or rewritten by an Ohm
script, which is how docs/specs could be kept in sync from inside the app.
- spec:
02-ohm-language
☑ Templates#
component
Feature spec (Overview → Goals/Non-goals/Open questions; Design → Data model, API, Flow, Risks; Plan → Implement, Tests), Research plan (Question, Sub-questions, Sources, Findings, Report), and Prompt suite (a draft prompt node plus an evaluation test referencing an experiment).
☑ Review findings#
☐ The key handler hijacks the filter select#
risk
The tree's key handler only exempts INPUT elements, and the status filter
<select> lives inside the same container — so arrow keys pressed while it is
focused move the tree selection and are preventDefaulted, breaking the select's
own keyboard behaviour. Exempt SELECT and TEXTAREA too.
- node:
keyboard
☐ Navigation and structure disagree under a filter#
risk
Arrow navigation walks the filtered rows while indent, outdent and move operate on the real tree, so with a filter active a move can produce a result the user cannot see. Either disable structural edits while filtering or operate on the filtered view consistently.
☐ Delete is refused when everything is collapsed#
risk low
The guard counts the collapse-filtered rows, so with a single collapsed root it refuses to delete even though children exist; conversely the post-delete selection can point past the end and the pane shows "Select a node."
☐ Done nodes are struck through#
risk
.spec-row.done .spec-title applies text-decoration: line-through. In a to-do
list that reads correctly; in a specification it does not — "done" there means
"this is how the product works", and striking it through makes an implemented
requirement look cancelled or obsolete.
Found by opening this repository's own specs in the editor, where most nodes are
done and the whole tree appeared crossed out. Dim the row, or reserve the
strikethrough for cancelled.
- node:
spec-model
☐ Markdown generation overwrites silently#
risk
It always writes docs/<name>.spec.md with no collision check, so a hand-written
document of that name is replaced without warning.
- node:
markdown
☐ Duplicated logic between core and the editor#
note low
The status-mark table, the node-id generator and the progress computation each exist
twice — once in packages/core/src/specs.ts and once in the editor. They agree
today; they should be imported from one place.
The drop hint says Shift while the code accepts Alt or Shift.
Links between nodes#
graph LR
markdown["Markdown generation"] -.-> refs["References"]
the_key_handler_hijacks_the_filter_select["The key handler hijacks the filter select"] -.-> keyboard["Keyboard-first tree editing"]
done_nodes_are_struck_through["Done nodes are struck through"] -.-> spec_model["SpecDoc and SpecNode"]
markdown_generation_overwrites_silently["Markdown generation overwrites silently"] -.-> markdown["Markdown generation"]