@graview/skills 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,195 @@
1
+ ---
2
+ name: graview-new-app
3
+ description: Start a product on Graview in its own repository — the shape of the declaration, the shell that comes for free, and the CI that keeps it honest afterwards.
4
+ ---
5
+
6
+ # Start a product on Graview
7
+
8
+ Graview ships as eleven packages. A product built on it lives in **its own
9
+ repository** and depends on them the way any other consumer does. This is the
10
+ setup that gets you from nothing to something that can tell you when you have
11
+ broken it.
12
+
13
+ ## Start with the scaffolder
14
+
15
+ ```sh
16
+ # from a framework checkout (the packages are not published yet):
17
+ pnpm install && pnpm build
18
+ pnpm graview create ../my-app --link . --name "My App" --kind thing
19
+ # once published, from anywhere:
20
+ npm create graview@latest my-app # or: pnpm create graview my-app
21
+ ```
22
+
23
+ Put the product BESIDE the framework, never inside its git tree; `--link`
24
+ names it relative to where you run the command. `--workspace` writes the
25
+ layout every real product ends up with (a root, the app under `app/`, the
26
+ harnesses beside it); `--merge` starts in a repository that already has a
27
+ README, naming collisions rather than writing over them.
28
+
29
+ It writes exactly the shape below — one kind with `creates`, `connects`,
30
+ `writes` and a `lifecycle`, one rule with its repair, the shell, the routed
31
+ face, a headless test, a CI workflow — installs it, and installs these skills
32
+ into it. Run its `verify`, then replace the first kind with the product's own.
33
+ The rest of this skill is what each part is for.
34
+
35
+ ## The shape
36
+
37
+ ```
38
+ src/
39
+ domain/ # No React in here. This is what `graview check` reads.
40
+ schema.ts # defineNode × n, createSchema (z comes from @graview/core)
41
+ mutations.ts # defineMutation × n — every change is a named, typed act
42
+ invariants.ts # defineInvariant × n — rules that name their repairs
43
+ policy.ts # who may do what, if anyone
44
+ brand.ts # name, mark, typeface, palette
45
+ app.ts # defineApp({ ... }) — one object, the whole surface
46
+ ui/
47
+ views.tsx # registerDefaultViews, then your own where you care
48
+ app.tsx # the provider, the scene, the workbench
49
+ main.tsx
50
+ ```
51
+
52
+ The domain/ui split keeps the declaration inspectable by a build, a CLI and
53
+ an agent: `graview check` reads `defineApp`, and a React import there would
54
+ drag a UI package into the checker.
55
+
56
+ ## Do this
57
+
58
+ 1. **Install.** The scaffold already did. Reach for `@graview/render/gpu`
59
+ only if you want the experimental capture path; the DOM path is what ships.
60
+
61
+ 2. **Declare one kind, one mutation, one rule.** Not the whole domain. The
62
+ loop you want on day one is: declare → `graview check` → look → declare
63
+ more.
64
+
65
+ 3. **Open it empty before you believe in it.** `graview describe
66
+ ./dist/domain/app.js` reads out what a blank installation meets; `<Begin>`
67
+ is that as a surface, wired into the scaffolded home. See `graview-seed`.
68
+
69
+ 4. **Declare the seams that make the interface smart.** These are one-line
70
+ declarations on mutations and kinds, and every derived surface reads them:
71
+ - `creates: ["<kind>"]` on every mutation that adds a kind — the chain
72
+ `Begin` and `graview describe` read, and the empty card's own way in.
73
+ - `connects: [...]` / `severs: [...]` naming the edge kinds a mutation
74
+ makes or breaks — what makes drawn LINES selectable, offers the act from
75
+ either end, and hides a severing act with nothing to sever.
76
+ - `lifecycle: { field, retired }` on kinds whose members expire — counts
77
+ say "+N past".
78
+ - `subject: { kinds, arg }` on every mutation that acts on a thing.
79
+
80
+ 5. **Take the shell.** `Inspector`, `Standing`, `ActivityRail`, `ChatPanel`,
81
+ `QuickRelations`, `RelationKey`, `BackOut`, `Trail`, `OverviewButton` and
82
+ `Wordmark` from `@graview/primitives` are the parts of an interface that
83
+ are not about your domain — including a chat seat that answers from the
84
+ graph with no API key. A shell is about eighty lines; if yours is longer,
85
+ you are probably rebuilding something derived.
86
+
87
+ Search comes with `Shell`: its `FindBox` answers `/` or ⌘K from
88
+ anywhere, lights what the words find in whatever picture is open and
89
+ dims the rest, and `#q=` makes a search a stop Back returns to. A shell
90
+ of your own puts `<FindBox />` in its bar; nothing is declared per kind.
91
+
92
+ The routed face is one branch in `main.tsx`: when the path starts with
93
+ `/pages`, render `<PagesApp basename="/pages" context={{ store, brand,
94
+ views: views(), settings }} />` from `@graview/pages`. It lands on a
95
+ gallery of the app's pictures — every kind drawn as a card until you title
96
+ a lens, then the lens by its name — with a list, a record and a form per
97
+ kind, the problems and the map, at phone widths. Hand it the same `views`
98
+ the scene draws from, or it has no pictures to land on.
99
+
100
+ 6. **Register default views first, override later.** `registerDefaultViews`
101
+ means a new kind renders sensibly at all three fidelities before you write
102
+ anything. Write a custom view for a kind when the generic one is genuinely
103
+ wrong, not on principle.
104
+
105
+ 7. **Add the check to your build.** `"check": "graview check
106
+ ./dist/domain/app.js"`, and a `verify` that runs typecheck, test, build
107
+ and check in that order. The scaffold writes both.
108
+
109
+ ## What to make yours next
110
+
111
+ The scaffold is deliberately the framework's own face. A product replaces it
112
+ in this order, and each step has a skill and a worked chapter in
113
+ `apps/seedbed` behind it:
114
+
115
+ 1. **The words.** Every edge gets `description` and `inverse` — how it reads
116
+ from each end — or `graview check` says `edge-without-inverse` and the
117
+ far end is captioned with the edge kind's name. Every mutation gets a
118
+ `title` and `description`; they are the button and the tool schema.
119
+ (`graview-node-kind`.)
120
+ 2. **A lens with a name.** A lens registered over a group with a `title` is
121
+ a PLACE — in the bar, on an embed's strip, one press from anywhere. Start
122
+ from the three that ship; write your own when the domain has a picture of
123
+ itself, the way the garden has a map. (`graview-lens`, chapters 10-13.)
124
+ 3. **The pages.** One page in the product's words first, then, when the
125
+ product needs to look like a product, every surface: the shell, the home,
126
+ the lists, the records, the problems — over the same store, acts, rules
127
+ and permissions. (`graview-pages`, chapters 9 and 13.)
128
+ 4. **A seat and a policy.** Who may do what, declared once; the strip, the
129
+ pages and the agent's tools all narrow from it. (`graview-permissions`,
130
+ `graview-agent-seat`, chapters 5 and 7.)
131
+ 5. **A brand, and shipping.** The name, the mark, the typefaces, the palette
132
+ the checker holds to AA; a version and a migration so a stored graph is
133
+ carried forward. (`graview-brand`, `graview-ship`, chapters 8 and 12.)
134
+
135
+ Work with an agent beside the declaration: describe a kind, let it draft the
136
+ edges, acts and rule, run `pnpm verify`, look, declare more.
137
+
138
+ ## The CI a product on Graview needs
139
+
140
+ Copy the shape from the framework's own `.github/workflows/ci.yml`, minus the
141
+ packaging steps you do not need. What earns its place:
142
+
143
+ - **`tsc`** — an edge to an undeclared kind is a typecheck failure, so this
144
+ catches a whole class before anything runs.
145
+ - **`graview check`** — everything `tsc` cannot see: a repair naming a mutation
146
+ nobody registered, a lens role bound to a missing field, a role that may do
147
+ nothing, a palette pair below AA. **Fail the build on errors.** Warnings are
148
+ a judgement call; errors are not.
149
+ - **Headless tests** — the domain tier has no DOM in it. Test that your rules
150
+ fire on graphs that break them and that their repairs resolve them; that is
151
+ the test that catches a real regression.
152
+ - **An accessibility run, in BOTH schemes.** Copy
153
+ `apps/todo/scripts/run-a11y.mjs`: the real accessibility tree through CDP,
154
+ plus axe-core. A light palette that clears AA in the dark is what it catches.
155
+
156
+ Worth stealing later: a browser harness that walks your own acceptance
157
+ criteria and writes a JSON verdict (the framework has several; each names
158
+ the claim that stopped being true), and a persistence adapter test.
159
+
160
+ ## Supported browsers
161
+
162
+ Build for the DOM path: Chromium, WebKit and Firefox, all three verified by
163
+ the framework (`pnpm engines`). The floor is `document.adoptedStyleSheets`
164
+ (Safari 16.4+, Firefox 101+, Chromium 99+). The altitude morph rides
165
+ `@property` and degrades to a clean cut where that is missing — write no
166
+ fallback. The chat's local-model rung needs WebGPU or Chrome's Prompt API;
167
+ without either the graph still answers and the header says why. The GPU
168
+ capture path is Chromium-only, experimental and opt-in (`attachRenderer`
169
+ from `@graview/render/gpu`; there is no URL switch) — never a requirement.
170
+
171
+ ## Then find out whether it worked
172
+
173
+ ```sh
174
+ pnpm verify # typecheck, tests, build, graview check
175
+ ```
176
+
177
+ Report the real output. On a new app the useful early findings are
178
+ `mutation-untitled`, `mutation-undescribed`, `edge-without-inverse` and
179
+ `required-invariant-unregistered` — things that look fine until somebody
180
+ reads the interface or an agent reads a tool schema.
181
+
182
+ ## What the check cannot see
183
+
184
+ - Whether your kinds are the right kinds. The test for each: does anything
185
+ point AT it, and does it have a life of its own? A colour is a field. A
186
+ fixture is a kind.
187
+ - Whether your mutations are the acts a person would name. They are the labels
188
+ in the strip and the instructions in an agent's tool schema, so an opaque one
189
+ costs twice.
190
+ - Whether the interface is any good. Run it. Run it at the size it will have: seed a
191
+ real catalogue, not a dozen rows. The scene draws what a person can read — a
192
+ relation too long for its band is grouped by its best arrangement or closes
193
+ on "+N more", and a thumbnail draws its 12 most relevant members — so a
194
+ thousand records are a picture, not a smear. What that hides is yours to
195
+ judge: whether the groups are the ones a person would ask for.
@@ -0,0 +1,159 @@
1
+ ---
2
+ name: graview-node-kind
3
+ description: Add a node kind to a Graview app — fields, edges, label, plural and field roles — and verify it with graview check rather than claiming it worked.
4
+ ---
5
+
6
+ # Add a node kind
7
+
8
+ A kind is the unit of everything here. Declaring one gets you, without another
9
+ line: a place in the layout, a card at three fidelities, an aggregate view, an
10
+ accessibility label, a hue, a slot in the kinds plane, and inclusion in every
11
+ agent tool that walks the graph.
12
+
13
+ ## Do this
14
+
15
+ 1. **Find the schema.** It is the file calling `createSchema`. Usually
16
+ `src/domain/schema.ts`.
17
+
18
+ 2. **Declare the kind.** Zod is the single source of runtime validation,
19
+ TypeScript types and JSON Schema — there is no second place to describe a
20
+ field.
21
+
22
+ ```ts
23
+ export const fixture = defineNode("fixture", {
24
+ fields: z.object({
25
+ // A NAME, and bounded. Every derived surface draws `label` — on a
26
+ // card, in a list, on a plan — and `z.string().min(1)` permits a
27
+ // paragraph. That is fine while a person is typing it; a model asked
28
+ // to survey a garden wrote "Pea-gravel corner with river-rock
29
+ // border, log seats and a fire bowl", which is a true sentence and a
30
+ // terrible name, and it was never told otherwise because the prompt
31
+ // is generated from this file. `graview check` notes
32
+ // `label-unbounded` on any kind a declared provider may create.
33
+ label: z.string().min(1).max(60),
34
+ kickOff: z.string(),
35
+ opponent: z.string(),
36
+ }),
37
+ plural: "Fixtures",
38
+ description: "A match, and the team you intend to put out for it.",
39
+ edges: {
40
+ // One edge, two readings: the fixture's page says who is in the
41
+ // team; the player's page says which fixtures they are picked for.
42
+ "picked-for": {
43
+ to: ["player"],
44
+ description: "who is in the team",
45
+ inverse: "the fixtures they are picked for",
46
+ },
47
+ },
48
+ // Lets a lens ask for "the start time" without knowing your field names.
49
+ fieldRoles: { start: "kickOff" },
50
+ });
51
+ ```
52
+
53
+ 3. **Add it to `createSchema([...])`.** An edge pointing at a kind that is not
54
+ in that array is a *typecheck* failure, not a runtime one — `createSchema`'s
55
+ `ValidateEdgeTargets` sees it.
56
+
57
+ 4. **Say what a node of it is called.** `label` defaults to a `label` field and
58
+ then to the id. An id in the interface is a bug you shipped, not a
59
+ placeholder. And say what ONE of the kind is called where its id does not:
60
+ `noun: "staff member"` on a kind called `staff`, or every sentence about
61
+ one reads "Change the staff".
62
+
63
+ An id is still a name a caller may choose: every act that declares
64
+ `creates: ["fixture"]` takes an optional `id` argument the framework adds,
65
+ so a seed being synced or an agent that will refer to the node next call
66
+ gets exactly the id it asked for, or a refusal by name. Never hardcode a
67
+ bootstrap id — `"today"`, `"u-nick"` — inside a mutation body or an
68
+ invariant; bind a role, a flag or an edge instead, so a store seeded
69
+ differently still works. And every kind gets `remove-<kind>` derived,
70
+ permitted through the acts that create it.
71
+
72
+ 5. **Give it verbs.** A kind with no mutation naming it as a subject renders
73
+ fine and can have nothing done to it, and the actions strip will say so out
74
+ loud. If that is not what you meant, see `graview-invariant` for the rule
75
+ shape and add at least one mutation whose `subject.kinds` includes it.
76
+
77
+ 6. **Let the fields be changed, or say why not.** A field you could set at
78
+ creation, you can change: every settable field no mutation writes is
79
+ covered by a derived act per kind (`edit-<kind>`, "Change the drill"),
80
+ editable where it is shown and logged like any other. Two declarations
81
+ shape it. `writes: ["done"]` on a mutation says which fields it sets when
82
+ its arguments do not (`finish()` writing `done`) — and a mutation whose
83
+ argument merely shares a field's name should say `writes: []`. `fixed:
84
+ { text: "the client's words, as sent" }` on the kind says a field never
85
+ changes, and why; the sentence is the documentation. The checker warns
86
+ `field-without-writer` when a field is still out of everyone's reach,
87
+ and refuses `writes-unknown-field`, `fixed-unknown-field` and a
88
+ `fixed-but-written` contradiction.
89
+
90
+ ## Worked examples
91
+
92
+ - `apps/todo/src/domain/schema.ts` — four kinds, including a `rule` kind whose
93
+ `spec` makes the rules a domain enforces into data, and `fieldRoles` binding
94
+ the timeline lens to a task's own field names
95
+ - `apps/seedbed/src/domain/schema.ts` — a kind with a declared `lifecycle`, so
96
+ the past is a horizon rather than a delete
97
+
98
+ ## The declarations that keep paying
99
+
100
+ - **`creates`** on the mutation that adds this kind (`creates: ["fixture"]`):
101
+ the empty kind card then offers "Add a fixture" by derivation — the blank
102
+ graph onboards itself, FROM THE ROOT OF THE CHAIN. An act that also takes
103
+ a `nodeRef` has no candidates on an empty graph, so it is withheld (a
104
+ picker with nothing in it is worse than no button) and the district says
105
+ what it is waiting for instead: *"Place a feature" cannot begin until there
106
+ is a zone.* Expect exactly one way in on a blank installation, and check
107
+ that it is the one you meant — this only ever shows up on the graph nobody
108
+ tests against.
109
+ - **`fromTheOtherEnd`** on the act that makes or breaks the edge. An act
110
+ declaring `connects` or `severs` is offered from BOTH ends of the tie, and
111
+ `title` is written from the subject's side: "Name a caretaker", offered on
112
+ the gardener, reads as naming hers. Say how it reads standing there
113
+ (`fromTheOtherEnd: "Take on a plot"`) — `graview check` warns
114
+ `act-without-far-end-reading` and names the end it has no words for.
115
+ - **`lifecycle`** when members expire — `{ field: "status", retired:
116
+ ["played"] }` or `{ field: "until", retired: "date" }`. Every count then
117
+ aggregates over the horizon ("4, +12 past") instead of drowning, and
118
+ `graview check` refuses a lifecycle reading a missing field.
119
+ - **A figure**, which is line art of the THING at the city's own three-quarter
120
+ angle — a person, a plot of ground, a gutter — drawn wherever the kind is
121
+ drawn. Nine ship; any domain that is not an abstract tracker runs out of
122
+ them at once, so draw the rest:
123
+ `graview figure ./dist/domain/app.js --kind gutter --from "a gutter along a
124
+ roof edge"` prints the brief (the rules, the angle, a shipped figure as the
125
+ style), and `--judge <file>` reads the answer back, holds it to the rules
126
+ `graview check` holds a figure to, and prints the line to paste. Then look
127
+ at it at twenty pixels, which is the size a chip gives it.
128
+ - **A declared hue** in the brand (`accents: { fixture: 210 }`) if this kind
129
+ should wear a chosen colour rather than a stable hash — every chip dot,
130
+ district roof and the focus tag follow.
131
+
132
+ ## Then find out whether it worked
133
+
134
+ ```sh
135
+ pnpm build && npx graview check ./dist/domain/app.js
136
+ ```
137
+
138
+ Report **the actual output**, including warnings. What it catches here:
139
+
140
+ - `edge-target-undeclared` — an edge to a kind nobody declared
141
+ - `edge-without-inverse` — a relation with words for one of its two readings
142
+ - `edge-name-shared` — one edge name declared on two kinds in two sets of
143
+ words (`by` on a song and on an album); every surface treats a name as ONE
144
+ relation, so name each its own
145
+ - `act-without-far-end-reading` — an act offered on an end it has no words for
146
+ - `field-role-missing-field` — a role pointing at a field that is not there
147
+ - `required-invariant-unregistered` — `requiresInvariant` naming no rule
148
+ - `mutation-untitled` / `mutation-undescribed` — a verb nobody can read
149
+
150
+ ## What the check cannot see
151
+
152
+ Say so rather than implying otherwise:
153
+
154
+ - Whether the kind is a *kind* at all, or should have been a field on an
155
+ existing one. The test: does anything point AT it, and does it have a life of
156
+ its own? A colour is a field. A fixture is a kind.
157
+ - Whether the default views read well. Run the app and look.
158
+ - Whether the plural reads naturally in a sentence — "3 Fixtures" is fine,
159
+ "3 Person" is not.
@@ -0,0 +1,206 @@
1
+ ---
2
+ name: graview-pages
3
+ description: Give a Graview app the routed face it wants — from the derived pages, to one page in the app's own words, to a product design that replaces every surface.
4
+ ---
5
+
6
+ # The pages face
7
+
8
+ A Graview app has two faces over one store. The scene is the picture. The
9
+ pages face is the same declaration routed as an ordinary web application:
10
+ a home, a list per kind, a record per node, forms for every act, and a
11
+ problems page — derived, then replaceable one surface at a time, all the way
12
+ to a product design of its own. The ladder ends where the framework's own
13
+ example does: `apps/seedbed`, chapters nine and thirteen.
14
+
15
+ ## What comes for free
16
+
17
+ ```tsx
18
+ // main.tsx — one branch: the path decides the face
19
+ if (location.pathname.startsWith("/pages")) {
20
+ root.render(<PagesApp basename="/pages" context={{ store, brand, principal, sceneHref: "/" }} />);
21
+ }
22
+ ```
23
+
24
+ `PagesApp` renders with no registry at all:
25
+
26
+ - `/` — a GALLERY. The standing as the headline ("2 gardeners, 3 plots and
27
+ 1 planting.", or "Nothing here yet." and which act begins it), then every
28
+ picture as a large live card, two across at a desk, one on a phone, then
29
+ the kinds as a row of counts, a line to the map, and Recently. A kind
30
+ with no titled lens gets a contact sheet of its members, so a new app
31
+ lands on a gallery; titling a lens replaces that card with the
32
+ lens by its name. An empty picture names the act that fills it.
33
+ - `/<plural>` — a list per kind, marking trouble, with the creating acts
34
+ beneath it.
35
+ - `/<plural>/<id>` — a record: its facts, its relations captioned in the
36
+ declaration's words, what can be done, what has happened.
37
+ - `/problems` — every broken rule with its repairs.
38
+
39
+ The shell is one row — pictures (home), kinds, Map, Problems — scrolling
40
+ sideways on a phone. Shell and gallery are 1160px wide; read pages, 760.
41
+
42
+ **Hand it `views`** (the scene's registry, plus `settings`/`presence`) —
43
+ `graview create` does — and the face puts the scene's provider
44
+ under its routes, which buys three things at once:
45
+
46
+ - `/places`, `/places/<as>` — every named lens as a page (fullscreen, over
47
+ the kind's members, siblings one press away, the beginning acts beneath)
48
+ and the gallery again. A kind's page lists its pictures; a pick in a lens
49
+ travels to the record.
50
+ - `/map` — `kindMap(store)`: every declared relation in its words with
51
+ its live count, also a section on the home page. A kind's list says what
52
+ it relates to and arranges itself in the shared words (`?sort=due:desc`,
53
+ `?filter=done:false`, `?group=due:month`, `?q=tape`) a lens carries in
54
+ its fragment, so an arrangement is a link; `?by=`, `?<edge>=<id>`,
55
+ `?with=` and `?past=1` still land. A record links back.
56
+ - `/search?q=` — the Find box's matcher: hits by kind, each with why; a
57
+ nav box narrows a list, else lands here. Find and the way back ("Take
58
+ back “…”", ⌘Z) are on every face: a shell that places `<PageFind>` or
59
+ `<PageUndo>` says where, one that does not gets them drawn around it,
60
+ and `surface("shell", Shell, { without: ["find"] })` goes without.
61
+ - **The assistant**, on every route: one control opens the scene's own
62
+ `Companion` in a drawer, and the ROUTE is what "this" means. Grounded
63
+ questions before anybody types; proposals apply through the same runtime,
64
+ attributed and undoable, withheld ones struck through. Open questions are
65
+ listed on `/problems`; the rung that answers is chosen in the footer.
66
+
67
+ Everything a page shows is a derivation the scene also uses: `recordFacts`,
68
+ `deriveAffordances`, `store.permits`. **A page never decides what an act is
69
+ or who may take it** — it strikes through what the seat may not, and says why.
70
+
71
+ **At a phone's width this face is the answer.** The scene still holds there —
72
+ districts stay legible, panels scroll — but
73
+ a 134px card in a 390px viewport is a city through a letterbox. `Shell`
74
+ carries `pagesHref`.
75
+
76
+ ## Rung one: a page in the app's own words
77
+
78
+ `createPageRegistry(schema)` replaces pages per kind and surfaces per app:
79
+
80
+ ```tsx
81
+ import { createPageRegistry, DerivedForm, kindFacts, PageMain, pageStyles, recordFacts, spatialHref, useStoreTick } from "@graview/pages";
82
+
83
+ function PlotPage({ context }: { context: PageContext<S> }) {
84
+ const { store, principal } = context;
85
+ useStoreTick(store); // re-render on every op
86
+ const id = decodeURIComponent(useParams()["id"] ?? "");
87
+ const facts = recordFacts(store, id, { principal }); // the same derivations
88
+ return (
89
+ <PageMain context={context}>
90
+ <h1 style={pageStyles.h1}>{facts.label}</h1>
91
+ <a href={spatialHref(id)}>See it in the scene ↗</a>
92
+ <DerivedForm store={store} mutation={sow} prefilled={{ plotId: id }} />
93
+ </PageMain>
94
+ );
95
+ }
96
+
97
+ export const pages = createPageRegistry<S, PageComponent<S>>(schema)
98
+ .register("plot", "record", PlotPage)
99
+ .route("/survey", SurveyDesk); // about nothing in the schema
100
+ ```
101
+
102
+ `.route(path, Component)` gives a page that is NOT about a kind an address —
103
+ onboarding, settings, import — matched before `/:plural`. **Every `to` is
104
+ basename-relative** (`to="/survey"`, never `to="/pages/survey"`), and so is
105
+ `initialPath`, so a test renders the hrefs a browser will.
106
+
107
+ Rules for a page at this rung:
108
+
109
+ 1. **`PageMain`, not `<main>`** — inside an embed the host owns the one
110
+ `main`, and a hand-written one is a duplicate landmark.
111
+ 2. **`useStoreTick(store)`** at the top of every page that reads the graph,
112
+ or it goes stale after the first act.
113
+ 3. **`pageStyles`** for the parts you did not design, so one custom page
114
+ still reads as the same face as the derived ones.
115
+ 4. **Prefill, never wire.** `DerivedForm` with `prefilled` is how a record
116
+ offers an act about itself; the form asks for the rest.
117
+ 5. **Read permission before drawing an act.** `store.permits({ name, args },
118
+ principal)`, struck through with `verdict.refusal.message`. A form that
119
+ refuses on submit is the bug this prevents.
120
+ 6. **Link to a PICTURE, not only to a node.** `spatialHref(id)` opens the
121
+ scene on one thing; `placeHref(as)` — `/#view=the-grounds`, the title
122
+ through `placeSlug` — opens it on one named place, group in focus, not on
123
+ a default view that says "press The grounds".
124
+ 7. **Take the act LIST from the derivation, not from the mutations.**
125
+ `recordFacts(store, id, { principal }).actions` is the same `AffordanceSet`
126
+ the scene's strip reads: `affordances` are the acts that can actually act
127
+ here, each with its `args` already decided and its `open` questions left,
128
+ and `withheld` are the ones this seat may not take, with the reason.
129
+ Filtering `store.allMutations()` is not equivalent: it offers acts with
130
+ nothing to act on. A **list** page asks it of a kind instead —
131
+ `kindFacts(store, kind, { principal }).actions`, the acts that can BEGIN it.
132
+
133
+ ## Rung two: a product design
134
+
135
+ The registry has three surfaces and two page types, and replacing all of them
136
+ is a product:
137
+
138
+ ```tsx
139
+ createPageRegistry<S, PageComponent<S>>(schema)
140
+ .surface("shell", Shell) // the frame around every route: nav, masthead, standing
141
+ .surface("home", Home)
142
+ .surface("problems", Problems)
143
+ .register("plot", "list", Plots).register("plot", "record", PlotRecord)
144
+ .register("gardener", "list", Gardeners).register("gardener", "record", GardenerRecord)
145
+ // ... every kind
146
+ ```
147
+
148
+ The shell surface receives `{ context, children }`. Two worked examples. `apps/todo/src/ui/design.tsx` is the FINISHED one, and
149
+ what makes it finished is not the type — it is that everything a person
150
+ tries there works: sort, filter and group come from `ArrangeBar` and live
151
+ in `useSearchParams` in the shared words (a list you arranged is a link); a record edits where it is
152
+ shown, heading included, through `editableFields(store, id)`; an act's form
153
+ opens where the act is, prefilled, with the store's own refusal said at the
154
+ press; the problems page is an inbox, its repairs ordered by
155
+ `rankedRepairs`; and every empty state offers the way out of itself.
156
+ `apps/seedbed/src/ui/design.tsx` is the other end — an almanac whose own
157
+ drawing has two homes, the page and the scene's lens reading one model.
158
+
159
+ What every design must keep doing:
160
+
161
+ - **Read the graph through one model.** `readGarden(store)` turns nodes,
162
+ edges and violations into the design's words; every page reads it and none
163
+ reaches for `store.graph`. The scene's lens reads the same model.
164
+ - **Style through the theme's tokens** (`--graview-ground`, `-panel`, `-ink`,
165
+ `-edge`, `-warn`, `-accent`, `-font-display`, `-font-body`), tinted with
166
+ `color-mix` for the design's own paper. Never a colour that works in one
167
+ scheme only.
168
+ - **Keep landmarks and targets honest.** One `main` — a `section` when
169
+ `context.embedded`, and UNNAMED, or a page holding two embeds has two
170
+ regions with one name. Controls at least 24px, AA contrast.
171
+ Every size in `rem`: at 200% text a flex or grid item's automatic minimum
172
+ is its CONTENT's, so one un-wrappable row pushes the whole column off the
173
+ screen — `minmax(0, 1fr)` and `min-width: 0` on the column, `flex-wrap` on
174
+ the row. Measure rather than trust: `-ink-faint` fails AA on an 11px
175
+ label, and the accent fails on the warning ground. `node
176
+ scripts/verify-pages.mjs` runs axe over every route, both widths, both
177
+ schemes.
178
+ - **Withhold, do not hide.** Rule 7 again, at every surface: struck through
179
+ with the policy's own sentence rather than dropped.
180
+
181
+ ## On somebody else's page
182
+
183
+ The embed mounts the app — this face included (`face: "pages"`, `path`) —
184
+ into one element of any page. That is its own skill: `graview-embed`.
185
+
186
+ ## Then find out whether it worked
187
+
188
+ ```sh
189
+ pnpm build && npx graview check ./dist/domain/app.js # the declaration is still whole
190
+ pnpm test # render every page you registered
191
+ ```
192
+
193
+ Render each registered page with `PagesApp` and `initialPath` in a test —
194
+ `apps/seedbed/tests/integration/chapters.test.ts` does. Then open it: a
195
+ design that passes its tests and reads like an admin panel has replaced
196
+ nothing.
197
+
198
+ ## What the check cannot see
199
+
200
+ - Whether the design's words are the domain's. The derived pages use the
201
+ declaration's `description` and `inverse`; a design that writes its own
202
+ sentences must keep them true as the declaration changes.
203
+ - Whether a page still offers everything the seat may do. Listing acts by
204
+ NAME misses the one declared after it was written; scanning the mutations
205
+ offers acts that cannot act. `facts.actions` is neither — but only a
206
+ person can see whether the page gives them room.