docs(principles): a front end displays state; it does not reimplement behaviour

User ruling 2026-09-20: minimise the amount of new code a new UI framework
needs. Centralise logic in a redux-like store so a second or third front end
is a function from state to pixels plus a wire for events, not a second
implementation of how the application behaves.

Names the pieces that already exist as what they are -- Command is the
action, reduce_view and WidgetCache::apply are the reducer, ViewState plus
the caches are the store, Sessions::apply is dispatch -- and gives the
question to ask of any front-end code: would a third front end have to write
this again?

Three things make it concrete: the measure is lines of framework-specific
code and should trend to 'paint and wire'; a derivation done twice is the
defect even when both copies are correct today (improvise-edp is the worked
example, gotcha 44 is the same defect still open); and sharing a table beats
testing two copies of it, which is why the keymap is shared rather than
mirrored. Also says what legitimately stays local: painting, event wiring,
and a semantic-value-to-framework-type lookup like AxisColor -> axis_fg.

Sections 5-7 renumbered; the §6 testing references in repo-map.md and the
code-review skill updated with them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
This commit is contained in:
Edward Langley
2026-09-19 21:31:58 -07:00
parent db70bb161c
commit d175b9934f
4 changed files with 62 additions and 9 deletions
+4 -4
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -24,7 +24,7 @@ them here would let the copy drift:
| Source | What it holds |
|---|---|
| `context/design-principles.md` | The "why" — functional-first, polymorphism over conditionals, correctness by construction, layering, §6 testing doctrine |
| `context/design-principles.md` | The "why" — functional-first, polymorphism over conditionals, correctness by construction, layering, §7 testing doctrine |
| `context/repo-map.md` | The "where", plus **numbered gotchas** — read the ones your diff touches |
| `prolog-sessions/kb.pl` | Executable knowledge: `invariant/2`, `doctrine/2`, `forbidden/2`, `decision/3`, and prior `work_done/3` |
| `AGENTS.md` | Working rules: bug-first tests, `Option`/`Result`/`Box` at the edges, merges over rebases |
@@ -75,7 +75,7 @@ Wire tokens are not UI labels and must not follow them.
## Testing doctrine
From `design-principles.md` §6 and `doctrine/2`:
From `design-principles.md` §7 and `doctrine/2`:
- **A bug fix needs a test that failed before it.** If the diff claims a fix
with no such test, that is the finding. Ask whether it was run against the
+55 -2
View File
@@ -212,7 +212,60 @@ discarded. No partial writes reach the model.
---
## 5. Guidelines for New Code
## 5. A Front End Displays State; It Does Not Reimplement Behaviour
**The goal is to minimise the amount of new code a new UI framework needs.**
Logic is centralised in a redux-like store, so a second or third front end is
mostly a function from state to pixels plus a wire for events — not a second
implementation of how the application behaves.
The pieces already exist and are worth naming as what they are:
| redux | here |
|---|---|
| action | `Command` / `ModelCommand` (`improvise-protocol`) |
| reducer | `reduce_view`, and `WidgetCache::apply` per widget |
| store | `ViewState` + the per-widget caches |
| dispatch | `Sessions::apply` — the one door (gotcha 36) |
So the question to ask of any code in a front end is: **would a third front end
have to write this again?** If yes, it is behaviour and it belongs in the shared
layer. Three things make this concrete rather than aspirational:
**The measure is lines of framework-specific code, and it should trend to
"paint and wire".** `web/app.js` is the current benchmark: a few hundred lines
that know no protocol, reaching the application through a handful of string
calls. A front end that needs to understand `CellKey` coordinates, or which
category a row label belongs to, or what a keypress means, is one that has been
handed a job the store should have done.
**A derivation done twice is the defect, even when both copies are correct
today.** Two front ends deriving the same fact will diverge, and the divergence
is invisible until someone compares them side by side. `improvise-edp` is the
worked example — the category tree was flattened twice, two different ways, and
the fix put `viewmodel::flatten_cat_tree` where both ends call it. Gotcha 44 is
the same defect still open: the TUI derives the grid's *frame* from
`GridLayout`, the browser cannot, and so the browser renders `Food/Alice` where
the TUI renders two header columns.
**Sharing the table beats testing two copies of it.** Where a front end must
turn input into actions, share the mapping rather than mirroring it: two
hand-kept copies held in step by a test are one definition waiting to be made
(`doctrine(a_consolidation_that_keeps_its_compatibility_test_is_unfinished)`).
That is why the keymap is shared rather than reimplemented per front end, and
why `doctrine(mirror_the_cmd_line_for_line)` is a rule about *resolvers* — the
narrow, unavoidable residue — and never a licence to restate a table.
**What legitimately stays per-framework.** Painting, event wiring, and the
mapping from a semantic value to a framework type: `AxisColor → ratatui::Color`
in `draw.rs::axis_fg`, `MinibufColor → minibuf_fg`. That mapping is the shape
to imitate — the *decision* is in the shared layer as a semantic enum, and only
the last translation is local. Anything larger than a lookup table is a
candidate for the store.
---
## 6. Guidelines for New Code
- **Add a command, not a special case.** If you need new behavior on a keypress,
implement `Cmd`, register it, and bind it in the keymap. Do not add an
@@ -236,7 +289,7 @@ discarded. No partial writes reach the model.
---
## 6. Testing
## 7. Testing
### Coverage and ambition
+1 -1
View File
@@ -484,7 +484,7 @@ Import flags: `--category`, `--measure`, `--time`, `--skip`, `--extract`, `--axi
## Testing — the short version
Full guidance lives in `context/design-principles.md` §6. Quick reminders:
Full guidance lives in `context/design-principles.md` §7. Quick reminders:
- Suite runs in <2s. Don't let that drift.
- Commands: build a `CmdContext`, call `execute`, assert on returned effects. No terminal needed.