graview-node-kind
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.
Installed into a project by graview skills install ., and read by whichever assistant is working beside you.
The skill
Add a node kind
A kind is the unit of everything here. Declaring one gets you, without another line: a place in the layout, a card at three fidelities, an aggregate view, an accessibility label, a hue, a slot in the kinds plane, and inclusion in every agent tool that walks the graph.
Do this
- Find the schema. It is the file calling
createSchema. Usuallysrc/domain/schema.ts.
- Declare the kind. Zod is the single source of runtime validation, TypeScript types and JSON Schema — there is no second place to describe a field.
``ts export const fixture = defineNode("fixture", { fields: z.object({ // A NAME, and bounded. Every derived surface draws label — on a // card, in a list, on a plan — and z.string().min(1) permits a // paragraph. That is fine while a person is typing it; a model asked // to survey a garden wrote "Pea-gravel corner with river-rock // border, log seats and a fire bowl", which is a true sentence and a // terrible name, and it was never told otherwise because the prompt // is generated from this file. graview check notes // label-unbounded on any kind a declared provider may create. label: z.string().min(1).max(60), kickOff: z.string(), opponent: z.string(), }), plural: "Fixtures", description: "A match, and the team you intend to put out for it.", edges: { // One edge, two readings: the fixture's page says who is in the // team; the player's page says which fixtures they are picked for. "picked-for": { to: ["player"], description: "who is in the team", inverse: "the fixtures they are picked for", }, }, // Lets a lens ask for "the start time" without knowing your field names. fieldRoles: { start: "kickOff" }, }); ``
- Add it to
createSchema([...]). An edge pointing at a kind that is not in that array is a *typecheck* failure, not a runtime one —createSchema'sValidateEdgeTargetssees it.
- Say what a node of it is called.
labeldefaults to alabelfield and then to the id. An id in the interface is a bug you shipped, not a placeholder. And say what ONE of the kind is called where its id does not:noun: "staff member"on a kind calledstaff, or every sentence about one reads "Change the staff".
An id is still a name a caller may choose: every act that declares creates: ["fixture"] takes an optional id argument the framework adds, so a seed being synced or an agent that will refer to the node next call gets exactly the id it asked for, or a refusal by name. Never hardcode a bootstrap id — "today", "u-nick" — inside a mutation body or an invariant; bind a role, a flag or an edge instead, so a store seeded differently still works. And every kind gets remove-<kind> derived, permitted through the acts that create it.
- Give it verbs. A kind with no mutation naming it as a subject renders fine and can have nothing done to it, and the actions strip will say so out loud. If that is not what you meant, see
graview-invariantfor the rule shape and add at least one mutation whosesubject.kindsincludes it.
- Let the fields be changed, or say why not. A field you could set at creation, you can change: every settable field no mutation writes is covered by a derived act per kind (
edit-<kind>, "Change the drill"), editable where it is shown and logged like any other. Two declarations shape it.writes: ["done"]on a mutation says which fields it sets when its arguments do not (finish()writingdone) — and a mutation whose argument merely shares a field's name should saywrites: []. `fixed: { text: "the client's words, as sent" }` on the kind says a field never changes, and why; the sentence is the documentation. The checker warnsfield-without-writerwhen a field is still out of everyone's reach, and refuseswrites-unknown-field,fixed-unknown-fieldand afixed-but-writtencontradiction.
Worked examples
apps/todo/src/domain/schema.ts— four kinds, including arulekind whosespecmakes the rules a domain enforces into data, andfieldRolesbinding the timeline lens to a task's own field namesapps/seedbed/src/domain/schema.ts— a kind with a declaredlifecycle, so the past is a horizon rather than a delete
The declarations that keep paying
createson the mutation that adds this kind (creates: ["fixture"]): the empty kind card then offers "Add a fixture" by derivation — the blank graph onboards itself, FROM THE ROOT OF THE CHAIN. An act that also takes anodeRefhas no candidates on an empty graph, so it is withheld (a picker with nothing in it is worse than no button) and the district says what it is waiting for instead: *"Place a feature" cannot begin until there is a zone.* Expect exactly one way in on a blank installation, and check that it is the one you meant — this only ever shows up on the graph nobody tests against.fromTheOtherEndon the act that makes or breaks the edge. An act declaringconnectsorseversis offered from BOTH ends of the tie, andtitleis written from the subject's side: "Name a caretaker", offered on the gardener, reads as naming hers. Say how it reads standing there (fromTheOtherEnd: "Take on a plot") —graview checkwarnsact-without-far-end-readingand names the end it has no words for.lifecyclewhen members expire — `{ field: "status", retired: ["played"] }or{ field: "until", retired: "date" }`. Every count then aggregates over the horizon ("4, +12 past") instead of drowning, andgraview checkrefuses a lifecycle reading a missing field.- A figure, which is line art of the THING at the city's own three-quarter angle — a person, a plot of ground, a gutter — drawn wherever the kind is drawn. Nine ship; any domain that is not an abstract tracker runs out of them at once, so draw the rest: `graview figure ./dist/domain/app.js --kind gutter --from "a gutter along a roof edge"` prints the brief (the rules, the angle, a shipped figure as the style), and
--judge <file>reads the answer back, holds it to the rulesgraview checkholds a figure to, and prints the line to paste. Then look at it at twenty pixels, which is the size a chip gives it. - A declared hue in the brand (
accents: { fixture: 210 }) if this kind should wear a chosen colour rather than a stable hash — every chip dot, district roof and the focus tag follow.
Then find out whether it worked
pnpm build && npx graview check ./dist/domain/app.jsReport the actual output, including warnings. What it catches here:
edge-target-undeclared— an edge to a kind nobody declarededge-without-inverse— a relation with words for one of its two readingsedge-name-shared— one edge name declared on two kinds in two sets of words (byon a song and on an album); every surface treats a name as ONE relation, so name each its ownact-without-far-end-reading— an act offered on an end it has no words forfield-role-missing-field— a role pointing at a field that is not thererequired-invariant-unregistered—requiresInvariantnaming no rulemutation-untitled/mutation-undescribed— a verb nobody can read
What the check cannot see
Say so rather than implying otherwise:
- Whether the kind is a *kind* at all, or should have been a field on an existing one. The test: does anything point AT it, and does it have a life of its own? A colour is a field. A fixture is a kind.
- Whether the default views read well. Run the app and look.
- Whether the plural reads naturally in a sentence — "3 Fixtures" is fine, "3 Person" is not.