@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,145 @@
1
+ ---
2
+ name: graview-permissions
3
+ description: Declare who may do what in a Graview app once, so the store enforces it, the actions strip narrows and an agent seat narrows with it — verified by graview check and by trying it.
4
+ ---
5
+
6
+ # Declare who may do what
7
+
8
+ Permission is another input to a derivation the framework already does. What
9
+ can be done is already DERIVED from the schema and the invariants, so adding a
10
+ principal and some grants narrows the interface and the agent surface at the
11
+ same time, from one declaration.
12
+
13
+ Two things must not be got wrong, and they are the reason to follow this rather
14
+ than improvise.
15
+
16
+ **Enforcement belongs at the STORE.** The tool runtime calls the same mutations
17
+ a person does, so a check inside a React component is not a permission system,
18
+ it is a suggestion — and the agent seat is the bypass.
19
+
20
+ **An action you may not take should SAY SO rather than vanish.** Hiding it
21
+ teaches people the software is broken: they watched a colleague do this
22
+ yesterday and now the button is gone.
23
+
24
+ ## Do this
25
+
26
+ 1. **Write the policy where the checker can read it.** In your domain layer,
27
+ not your UI:
28
+
29
+ ```ts
30
+ export const policy: Policy = {
31
+ roles: ["coach", "analyst", "player"],
32
+ grants: [
33
+ { roles: ["coach"], mutations: "*" },
34
+ { roles: ["analyst"], mutations: ["plan-drill", "cut-drill", "design-drill"] },
35
+ // The line can be drawn by SUBJECT KIND on the same mutation: an
36
+ // analyst may rename a drill and not a position, because a position's
37
+ // name is part of the formation and the formation is selection.
38
+ { roles: ["analyst"], mutations: ["rename"], kinds: ["drill", "session"] },
39
+ { roles: ["player"], mutations: ["end-unavailability", "explain"] },
40
+ ],
41
+ };
42
+ ```
43
+
44
+ 2. **Hand it to both `defineApp` and the `Store`.** `defineApp` is what
45
+ `graview check` reads; the `Store` is what enforces.
46
+
47
+ The derived acts ride your grants rather than needing their own line:
48
+ `edit-<kind>` is permitted to whoever may change or make the kind,
49
+ `remove-<kind>` to whoever may make it — or to any grant that names the
50
+ act or says `*`. A refusal names who could, so a seat told no learns what
51
+ it lacks rather than concluding the capability is missing.
52
+
53
+ 3. **Give the provider a principal.** `<GraviewProvider principal={{ kind:
54
+ "human", id: me.id, roles: me.roles }}>`. The interface reads it to decide
55
+ what to OFFER; the store decides what to allow; the log attributes to it.
56
+ Three readings of one object, which is why they cannot drift.
57
+
58
+ 4. **Do not filter anything yourself.** The narrowing happens once, in
59
+ `deriveAffordances`. A provider written tomorrow inherits it.
60
+
61
+ 5. **Remember the absence of a policy permits everything.** That keeps
62
+ permission opt-in rather than a tax every app pays before it has decided it
63
+ has users.
64
+
65
+ 6. **The derived edits ride your grants.** `edit-<kind>` — the act the
66
+ framework derives for fields nobody writes — is permitted to whoever may
67
+ already run an act that writes a field of that kind or creates one, on
68
+ that kind. An analyst who may `resize-drill` may change a drill's other
69
+ fields; a player who may not, may not. Nothing to add to the policy; a
70
+ grant may still name `edit-drill` outright, and `*` reaches it. When no
71
+ role may write or create a kind at all, `graview check` says so per
72
+ field (`field-without-writer`): grant an act, or mark the field `fixed`.
73
+
74
+ 7. **Put the installation in the graph.** Who may use the app, who has been
75
+ asked to, and what each holds are nodes and acts, not a second app:
76
+
77
+ ```ts
78
+ const installation = declareInstallation({ roles: ["coach", "analyst", "player"], admin: "coach" });
79
+ createSchema([...yours, ...installation.kinds]); // user, invitation — typed, so
80
+ // an edge may say to: ["user"]
81
+ mutations: [...yours, ...installation.mutations]; // invite, welcome, remove-user, grant, revoke, revoke-invitation
82
+ modules: installation.modules; // drawn only for those who administer it
83
+ policy: installation.withPolicy(policy); // the admin's grants, and "you, on yours" for a profile
84
+ ```
85
+
86
+ The coach sees "Show the installation" in the bar and on an embed's strip
87
+ and the people and invitations rise as ordinary districts; nobody else
88
+ ever sees them. A person's record page is their profile, and the derived
89
+ `edit-user` is theirs alone through a `self: true` grant. Register
90
+ `reachLens` over the people with a title and the policy is a picture:
91
+ roles down the side, acts across the top, a mark where the store would
92
+ say yes. Chapter 14 of `apps/seedbed` is the worked example.
93
+
94
+ **Pointing at a person?** `declareInstallation({ …, required: true })`.
95
+ An edge into a module the app can turn off earns a `module-edge-leak`
96
+ warning, and for people the only answer is "it is never off" — an
97
+ installation without people is a household of one, not a disabled module.
98
+ Say it once and the checker stops asking.
99
+
100
+ ## Worked examples
101
+
102
+ - `packages/core/tests/unit/permissions.test.ts` — grants by role, by mutation
103
+ and by subject kind, and the refusal that names who could
104
+ - `packages/tools/tests/unit/affordances.test.ts` — a guarded store: what a
105
+ principal is offered, what is withheld and said, and how the agent seat
106
+ narrows with it
107
+
108
+ ## Then find out whether it worked
109
+
110
+ ```sh
111
+ pnpm build && npx graview check ./dist/domain/app.js
112
+ ```
113
+
114
+ The checker reports `mutation-unreachable-by-any-role` (declared and
115
+ unreachable), `role-may-do-nothing`, `grant-unknown-mutation` and
116
+ `grant-unknown-kind`. All four are silent at runtime and obvious at build time,
117
+ and the person who finds them otherwise is the person standing in front of a
118
+ button they cannot press. Report the output.
119
+
120
+ **And try it.** Two tests that matter more than the check:
121
+
122
+ ```ts
123
+ it("refuses at the store, where nothing can go around it", () => {
124
+ expect(() => store.apply(call, { author: player })).toThrow(PermissionDeniedError);
125
+ expect(store.log.length).toBe(0);
126
+ });
127
+
128
+ it("narrows the agent seat from the same policy", () => {
129
+ const seat = createToolRuntime(store, { author: { kind: "agent", roles: ["player"] } });
130
+ expect(seat.definitions.map((t) => t.name)).not.toContain("select-player");
131
+ });
132
+ ```
133
+
134
+ Undo is a change and is judged like one: what you may undo is what you may have
135
+ done. If you have a custom undo path, check it goes through `store.undo`.
136
+
137
+ ## What the check cannot see
138
+
139
+ - Whether the roles match how the organisation actually works. That is a
140
+ conversation, not a declaration.
141
+ - Whether a withheld action's message helps. It names the roles that could;
142
+ whether that is useful depends on whether a person knows who holds them.
143
+ - Whether the principal is who they say they are. Graview authorises; it does
144
+ not authenticate. Wire that to your own identity provider and pass the result
145
+ in as the principal.
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: graview-port-app
3
+ description: Port an existing application onto Graview — deciding what is a node, what is a field and what is an edge — and proving the port with a parity test rather than an assertion.
4
+ ---
5
+
6
+ # Port an existing application
7
+
8
+ The hard part of a port is not the code. It is deciding what your existing
9
+ schema was really saying, because a relational schema hides three different
10
+ things inside a foreign key: a real relationship, an implementation detail, and
11
+ a thing that should have been a node all along.
12
+
13
+ ## The decisions, in order
14
+
15
+ 1. **What is a NODE?** The test: does anything point at it, and does it have a
16
+ life of its own? A `status` string is a field. A `rationale` — a reason
17
+ someone recorded, that outlives the person who recorded it and that other
18
+ things refer to — is a node, and turning it into one is usually the moment a
19
+ port starts paying for itself.
20
+
21
+ 2. **What is an EDGE?** A relationship a person would name out loud. `assigned-to`
22
+ is an edge. `created_at` is a field. A join table with columns on it is a
23
+ node, not an edge, and pretending otherwise is the mistake that costs most
24
+ later.
25
+
26
+ 3. **What is a MUTATION?** Every change, named as an act rather than as a write.
27
+ Not `updateDuty` but "Reassign run". The title is the label a person reads
28
+ in the strip AND the instruction an agent reads in its tool schema, so an
29
+ opaque one costs twice. If you find yourself writing `patch`, you have
30
+ skipped this step.
31
+
32
+ 4. **What is an INVARIANT?** Everything your current code validates, plus
33
+ everything it was supposed to. Give each one repairs — see
34
+ `graview-invariant`. Rules that name their repairs are where the interface
35
+ stops needing you to design it.
36
+
37
+ 5. **What is a LENS?** Your primary screen, described without your nouns. A
38
+ calendar is intervals in columns. A checklist grid is a bipartite mapping. A
39
+ formation is positions with domain-given coordinates. If one of the three
40
+ shipped lenses fits, bind roles to your fields and write no picture at all.
41
+
42
+ ## Do this
43
+
44
+ 1. Build the domain tier first, with no UI. Get `graview check` clean before
45
+ anything renders.
46
+ 2. Load your real data through a `snapshot`, not a fixture you invented. A port
47
+ that works on twelve made-up nodes and falls over on the real graph has
48
+ proved nothing.
49
+ 3. Register default views, look at it, and only then write custom ones.
50
+ 4. Port the rules LAST, and port them as tests first.
51
+
52
+ ## Worked example
53
+
54
+ - The household product (now in its own repository) is a port. Its parity
55
+ fixture was generated by running the ORIGINAL app's code over the same data,
56
+ and its test asserts the framework's invariants reproduce every violation —
57
+ 90 across 9 provocations. That fixture-and-test pair is the reason anyone
58
+ should believe a port, and the shape to copy.
59
+
60
+ ## Then find out whether it worked
61
+
62
+ ```sh
63
+ pnpm build && npx graview check ./dist/domain/app.js
64
+ ```
65
+
66
+ **And write the parity test.** This is the one thing that makes a port
67
+ trustworthy, and it is worth more than every other check here combined: run the
68
+ OLD system's validation and the new invariants over the same graphs, and assert
69
+ they agree.
70
+
71
+ ```ts
72
+ // Generated by running the old code over N provocations, then committed.
73
+ import expected from "./fixtures/legacy-violations.json";
74
+
75
+ it("agrees with the system it replaces, violation for violation", () => {
76
+ for (const provocation of provocations) {
77
+ const store = createStore({ snapshot: provocation.graph });
78
+ const mine = store.violations().map((v) => v.message).sort();
79
+ expect(mine).toEqual(expected[provocation.name]);
80
+ }
81
+ });
82
+ ```
83
+
84
+ The framework's own the household example port does this across 90 violations and nine
85
+ provocations, against a fixture generated by running the original code. If you
86
+ cannot generate that fixture, say so — and say what you are relying on instead,
87
+ because "I read both and they look the same" is a different claim.
88
+
89
+ ## What the check cannot see
90
+
91
+ - Whether you modelled the domain or transcribed the database. The symptom is
92
+ edges named after columns.
93
+ - Whether behaviour the old system had is gone. Only the parity test knows.
94
+ - Whether the port is worth finishing. A port that has not made anything
95
+ clearer by the third kind is telling you something.
@@ -0,0 +1,103 @@
1
+ ---
2
+ name: graview-seed
3
+ description: Get a Graview app from a blank graph to a useful one — the order the declaration already states, a plan a model proposes, and one turn that can be taken back.
4
+ ---
5
+
6
+ # Fill a blank graph
7
+
8
+ Every product ships empty once, and it is the state its author never sees:
9
+ your own graph has had data in it since the first afternoon. A ten-kind app
10
+ on a blank installation is one door and nine silent districts, and the person
11
+ who finds that out is the first one to open it.
12
+
13
+ Two things make it a surface rather than a wall: the app already declares the
14
+ ORDER things must be made in, and what a model proposes can be an object
15
+ rather than a list of calls somebody applies by hand.
16
+
17
+ ## What the declaration already says
18
+
19
+ `creates: ["feature"]` on an act, and `zoneId: nodeRef(["zone"])` in its
20
+ input, are together a chain: a feature cannot be made until a zone exists.
21
+
22
+ ```ts
23
+ import { beginning } from "@graview/core";
24
+ const chain = beginning(app);
25
+ chain.roots; // kinds that can begin with nothing in the graph
26
+ chain.doors; // the acts that are the way in
27
+ chain.order; // every kind, with what it waits for and how deep
28
+ chain.unreachable; // what nothing here can make — often right, always worth seeing
29
+ ```
30
+
31
+ `graview check` says it as a note (`blank-graph-unreachable`), and
32
+ `graview describe` reads the whole thing out. Run it before you believe your
33
+ app onboards anybody:
34
+
35
+ ```sh
36
+ pnpm build:domain && npx graview describe ./dist/domain/app.js
37
+ ```
38
+
39
+ ## Do this
40
+
41
+ 1. **Mount `<Begin>`.** It is derived — the chain, the acts that can run now,
42
+ what everything else is waiting for, in your own plurals. Put it on the
43
+ home surface of the routed face, or anywhere a person lands first. It
44
+ stands down on its own once every kind has something in it:
45
+ ```tsx
46
+ <Begin whenFull={<YourOwnHome />} />
47
+ ```
48
+
49
+ 2. **Give the root kind an act that needs nothing.** One act with no required
50
+ `nodeRef` is the difference between an app that onboards itself and one
51
+ that cannot be started. If every creating act needs a node, `graview check`
52
+ says `blank-graph-has-no-door` — and if the data really arrives by seed,
53
+ migration or sync, that note is the answer rather than a fault.
54
+
55
+ 3. **Let a model propose, and make it a plan.** A plan is ordered by the
56
+ chain, judged before any of it runs, reviewable, and applied as ONE turn:
57
+ ```ts
58
+ import { applyPlan, planFrom } from "@graview/tools";
59
+ const plan = planFrom(store, proposals, { app, principal });
60
+ const done = applyPlan(store, plan, { author: surveyor });
61
+ store.undo(done.batch); // the whole seeding, back the way it arrived
62
+ ```
63
+ A call can name what it is about to make — `as: "lawn"` — and a later call
64
+ points at it with `{ $plan: "lawn" }`. That is the only way a model can
65
+ refer to a node that does not exist yet, and it is what turns forty
66
+ proposals into one graph. It is all-or-nothing and says where it stopped;
67
+ `keepWhatRan` is for a caller who would rather have the half.
68
+
69
+ 4. **Show it before you run it.** `<PlanReview plan={plan} declinable />`
70
+ draws the plan in the order it will run, counts what it makes, and strikes
71
+ refusals through with their reason. `declinable` lets a person drop one —
72
+ and declining the area declines the tree standing in it, said beside the
73
+ entry BEFORE the press (`dependentsOf`, `without`). A seeding nobody read
74
+ is a seeding nobody can trust, and a review nobody can disagree with is
75
+ not a review.
76
+
77
+ 5. **Write the prompt from the graph, not from your head.** The model needs
78
+ the kinds, the acts and their arguments — which `generateLlmsTxt(app)`
79
+ already writes — plus what is already there, so it does not propose a
80
+ second Back Lawn.
81
+
82
+ ## Then find out whether it worked
83
+
84
+ ```sh
85
+ pnpm build:domain && npx graview check ./dist/domain/app.js
86
+ npx graview describe ./dist/domain/app.js
87
+ ```
88
+
89
+ Then open it with an empty store (`?fresh=1` with a ship browser adapter) and
90
+ go through your own front door. A test holds it: derive the affordances of
91
+ each root kind on an empty graph and assert the way in is offered, and that a
92
+ kind deeper in the chain offers nothing and says what it waits for.
93
+
94
+ ## What the check cannot see
95
+
96
+ - Whether the order is the order a PERSON would want. The chain says what is
97
+ possible, not what is kind: an app may be able to start with practices and
98
+ still want to ask about the ground first.
99
+ - Whether the model's proposals are any good. They are typed, ordered,
100
+ permitted and reversible — none of which makes them right, which is why
101
+ the review is a surface and not a formality.
102
+ - Whether a seeded graph reads as somebody's. Ten plausible rows a model
103
+ invented look exactly like ten rows that matter, until someone reads them.
@@ -0,0 +1,154 @@
1
+ ---
2
+ name: graview-ship
3
+ description: Deploy a Graview app — persistence, op-log-native migrations, export and health — with one declaration and one adapter, self-hosted or behind a service.
4
+ ---
5
+
6
+ # Ship a Graview app
7
+
8
+ `@graview/ship` holds what every deployment needs. One `defineApp`
9
+ declaration plus one persistence adapter is a running deployment; nothing
10
+ here requires a service.
11
+
12
+ ## Do this
13
+
14
+ 1. **Open the store through ship**, not by hand:
15
+
16
+ ```ts
17
+ import { createFileAdapter, openStore } from "@graview/ship";
18
+
19
+ const opened = await openStore({ app, adapter: createFileAdapter("./data") });
20
+ // opened.store is an ordinary Store; every applied diff is appended to
21
+ // the log and the snapshot rewritten, serialised in order. Hand off with
22
+ // `await opened.flush()` before close/exit — writes are async.
23
+ // One writer per scope: opening it twice interleaves and clobbers.
24
+ ```
25
+
26
+ The file adapter is deliberately readable: `snapshot.json`, append-only
27
+ `log.jsonl`, `meta.json` with the stored schema version. The core's
28
+ sqlite adapter is the scale answer.
29
+
30
+ **In the page, with no server**, the browser adapter stores the same
31
+ three things in `localStorage` and slots into the same call — import it
32
+ from `@graview/ship/browser` so the bundler never meets `node:fs`:
33
+
34
+ ```ts
35
+ import { browserStartsFresh, createBrowserAdapter, forgetFreshParam, openStore }
36
+ from "@graview/ship/browser";
37
+
38
+ const opened = await openStore({
39
+ app, adapter: createBrowserAdapter(), seed,
40
+ fresh: browserStartsFresh(), // ?fresh=1 asks; a driven browser gets it unless ?remember=1
41
+ });
42
+ forgetFreshParam(); // the seed is the FIRST load, not every load
43
+ ```
44
+
45
+ Reopened, the store carries its persisted history: earlier edits are in
46
+ the activity, attributed, and undoable. Give the person a visible way
47
+ back — `StartFresh` from `@graview/primitives`, or `remembers: true` on
48
+ the pages context — because a demo that can be edited into a corner with
49
+ no exit teaches distrust. The sample apps' `main.tsx` files are the
50
+ worked examples; `pnpm remember` is the harness that holds them to it.
51
+
52
+ **The seed is read once**, into an empty store. When the default content
53
+ moves afterwards, do not delete the store to see it — diff and land it:
54
+
55
+ ```sh
56
+ graview sync-seed ./dist/domain/app.js --seed ./src/data/example.json --data ./data
57
+ graview sync-seed ./dist/domain/app.js --seed ./src/data/example.json --data ./data --apply
58
+ ```
59
+
60
+ The first prints the content steps — `task t-van is put`, `today is
61
+ patched: label`, `today holds t-van is tied` — and writes nothing; the
62
+ second lands them as one operation authored `system · ship:sync-seed`,
63
+ logged, undoable, leaving what people made alone unless `--prune` says
64
+ otherwise. The same five steps (`put-node`, `patch-node`, `drop-node`,
65
+ `put-edge`, `drop-edge`) go in `migrations[]` through `stepsMigration`
66
+ when every installation should get them.
67
+
68
+ 2. **Version the declaration, and migrate in primitives.** When the schema
69
+ changes shape:
70
+
71
+ ```ts
72
+ defineApp({
73
+ ...,
74
+ version: 2,
75
+ migrations: [{
76
+ from: 1, to: 2,
77
+ title: "size words become bed counts",
78
+ apply: (snapshot) => snapshot.nodes
79
+ .filter((node) => node.kind === "plot")
80
+ .map((node) => ({ op: "patch-node", id: node.id,
81
+ before: { size: node.size, beds: undefined },
82
+ after: { size: undefined, beds: node.size === "large" ? 6 : 2 } })),
83
+ }],
84
+ })
85
+ ```
86
+
87
+ A migration answers in the op log's own five words, so running one appends
88
+ ordinary operations — authored `system · ship:migration`, stating intent,
89
+ carrying their inverse. `openStore` runs the pending chain on load.
90
+ `graview check` refuses a chain with a hole or a multi-version jump
91
+ (`migration-gap`, `migration-not-single-step`) before deploy time finds it.
92
+
93
+ 3. **Export is the exit.** `exportBundle(app, store)` — graph, attributed
94
+ history and version in one JSON shape; `assertBundle` refuses someone
95
+ else's app or a newer version, plainly. A tenant who cannot leave was
96
+ never a customer.
97
+
98
+ 4. **Health is coherence, not liveness.** `health(store)` reports sizes,
99
+ standing and dangling edges — poll it per deployment, curl it self-hosted.
100
+
101
+ 5. **The machine under the dev server is a door.** A locally-run product
102
+ very often has a coding agent installed, logged in and paid for; the
103
+ browser cannot spawn it and the dev server can.
104
+
105
+ ```ts
106
+ // vite.config.ts
107
+ import { localIntelligence } from "@graview/ship/dev";
108
+ plugins: [localIntelligence({ path: "/__graview/local", budgetUsd: 2 })]
109
+ ```
110
+
111
+ Declare it — `intelligence: [{ …, reach: ["paste", "local"], bridge:
112
+ "/__graview/local" }]` — and read it with `useLocalIntelligence(path)`
113
+ from `@graview/react`. The spawned session gets Read and only Read, a
114
+ turn per photograph plus three, a dollar budget, a closed stdin and an
115
+ environment with every `CLAUDE*` variable stripped. `apply: "serve"` means
116
+ a build carries no door: a deployed copy probes, gets nothing, and reads
117
+ as **closed** — which is a state, not a fault.
118
+
119
+ ## Then find out whether it worked
120
+
121
+ Write, close, reopen, and read: the graph must survive the round trip and
122
+ the persisted log must carry your ops with their authors. The framework's
123
+ own rehearsal (`pnpm smoke`) does exactly this from packed tarballs —
124
+ `theDeploymentShipped` is the verdict to mimic.
125
+
126
+ ## What the check cannot see
127
+
128
+ - Whether your migration is the RIGHT transform — the chain being unbroken
129
+ says nothing about the data arriving meaningful. Migrate a copy of real
130
+ data and read it before shipping the step.
131
+ - Whether the adapter's storage location survives your deployment story
132
+ (containers with ephemeral disks lose a file adapter's whole point).
133
+ - Whether the export actually round-trips: rehearse import into a fresh
134
+ deployment, the way the framework's smoke run does — do not assume it.
135
+
136
+ 6. **Serve it, and let an agent in.** `graview serve <entry> --data ./data`
137
+ puts the store behind HTTP with the op log as the wire; `graview mcp
138
+ <entry> --remote-url <url>` and `graview apply … --remote-url <url>` are
139
+ the same commands an agent uses against a folder, now against the server,
140
+ judged under the seat the request carries. Put `serve` and `mcp` in the
141
+ app's scripts. The routes are `WIRE`, exported and pinned by a test, and
142
+ `serveStore({ seatOf })` is where a host reads its own credential —
143
+ `openRemote({ headers })` carries whatever it asks for.
144
+
145
+ ## The boundary
146
+
147
+ Anything ONE deployment needs belongs in ship: the op log, the snapshot,
148
+ migrating on open, the wire, a principal on every apply, the seed at first
149
+ install, content steps, and the agent's door. Tenancy, provisioning,
150
+ deploy-to-URL, billing, quotas and fleet upgrades belong to the operator of
151
+ many — a separate service consuming ship like any customer, with its own
152
+ `seatOf` mapping people and keys to principals. The concern table is in
153
+ `@graview/ship`'s README; a third party can stand up their own host from
154
+ it without forking anything.
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: graview-studio
3
+ description: Open an app's declaration as a graph in Graview's own interface, change it with ordinary acts, let graview check judge the result before it is applied, migrate a stored graph, and write the declaration back as the files graview create writes.
4
+ ---
5
+
6
+ # Work the declaration in the studio
7
+
8
+ The declaration — kinds, fields, edges, acts, rules, roles, grants, lenses,
9
+ brand — is itself a graph. `@graview/studio` declares that graph with the
10
+ same `defineNode` an app uses, so a change to the declaration is an act with
11
+ an author, an intent and an inverse, judged by the same checker, undone like
12
+ any other.
13
+
14
+ ## Do this
15
+
16
+ 1. **Open the studio over the app**, in a test, a script or a page:
17
+
18
+ ```ts
19
+ import { createStudio, createStudioLens } from "@graview/studio";
20
+ const studio = createStudio(app); // the declaration, as a store
21
+ ```
22
+
23
+ The store's nodes have stable ids: `declared:plot` (not `kind:plot`,
24
+ which is the layout's name for the district), `field:plot.label`,
25
+ `edge:plot.tended-by`, `act:tend`, `rule:every-plot-tended`,
26
+ `role:coordinator`, `grant:2`, `lens:coverage`, `brand`.
27
+
28
+ 2. **Change it with acts**, not by hand:
29
+
30
+ ```ts
31
+ studio.store.apply({ name: "add-field", args: { kind: "declared:plot", label: "soil", type: "enum", required: true, options: ["clay", "loam"] } }, { author: june, intent: "Plots have soil" });
32
+ studio.store.apply({ name: "add-act", args: { kind: "declared:plot", label: "resize", title: "Resize", writes: ["size"] } });
33
+ studio.store.apply({ name: "add-rule", args: { kind: "declared:plot", label: "sized", description: "A plot has a size." } });
34
+ studio.store.apply({ name: "name-repair", args: { rule: "rule:sized", act: "act:resize" } });
35
+ ```
36
+
37
+ The acts: `add-kind`, `rename-kind`, `remove-kind`, `add-field`,
38
+ `remove-field`, `add-edge`, `remove-edge`, `add-act`, `remove-act`,
39
+ `add-rule`, `remove-rule`, `name-repair`, `forget-repair`, `add-role`,
40
+ `grant`, `revoke-grant`, and the derived `edit-<kind>` for every field
41
+ (a description, an inverse, a title, a lifecycle).
42
+
43
+ 3. **Check before you apply.** `studio.check()` is `graview check` on the
44
+ declaration as it now stands. `studio.apply()` refuses while there are
45
+ errors and says which; otherwise it hands back `{ app, migration }` —
46
+ the migration is `null` when a stored graph needs nothing, else a
47
+ `{ from, to, title, apply }` to append to the app's migrations (the new
48
+ app already carries it and the bumped version).
49
+
50
+ 4. **Write it back.** `studio.files({ schemaVar: "gardenSchema" })` is
51
+ `src/domain/schema.ts`, `mutations.ts`, `invariants.ts` and, with roles,
52
+ `policy.ts` — the files `graview create` writes. Shape is what a graph
53
+ carries: an act the studio declared gets a body from what it says
54
+ (create, connect, sever, write); an act the checkout wrote keeps the
55
+ checkout's body under the studio's declaration; a rule the studio
56
+ declared judges nothing until the checkout gives it an `evaluate`. Read
57
+ the files before you commit them, and keep a hand-written body where the
58
+ comment says so.
59
+
60
+ 5. **Let an agent propose.** `studio.propose(call, agentPrincipal, intent)`
61
+ applies under the agent's seat in a batch of its own; `studio.proposals()`
62
+ lists what stands; `studio.decline(batchId)` undoes one; accepting is
63
+ applying. In the interface, the trail shows the agent's turn with its
64
+ undo, like any turn.
65
+
66
+ 6. **Put it on a page.** The studio is an app: mount `studioApp()` with
67
+ `declarationToGraph(app)` as the seed, and register
68
+ `createStudioLens(app).View` over `kind` with a title — a place, "What
69
+ the checker says", one press from anywhere.
70
+
71
+ ## Then find out whether it worked
72
+
73
+ Run `graview check` on the app the studio applied, and the checkout's own
74
+ verify on the files it wrote. Say what they said.
75
+
76
+ ## What the check cannot see
77
+
78
+ - Whether a body the studio wrote does what the person meant. It does what
79
+ the act declares — create, connect, sever, write — and nothing more.
80
+ - Whether a rule the studio declared is right. It judges nothing until the
81
+ checkout gives it an `evaluate`; the checker sees a rule, not a judgement.
82
+ - Whether a migration is safe for data it has not met. It is computed
83
+ against the stored graph when it runs; look at the primitives on a copy.
84
+ - Whether the files should replace the checkout's. Read them; a hand-written
85
+ body is the checkout's to keep.