@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.
- package/LICENSE +96 -0
- package/README.md +46 -2
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +54 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -3
- package/skills/graview-agent-seat/SKILL.md +159 -0
- package/skills/graview-brand/SKILL.md +137 -0
- package/skills/graview-desk/SKILL.md +84 -0
- package/skills/graview-embed/SKILL.md +86 -0
- package/skills/graview-invariant/SKILL.md +112 -0
- package/skills/graview-lens/SKILL.md +187 -0
- package/skills/graview-new-app/SKILL.md +195 -0
- package/skills/graview-node-kind/SKILL.md +159 -0
- package/skills/graview-pages/SKILL.md +206 -0
- package/skills/graview-permissions/SKILL.md +145 -0
- package/skills/graview-port-app/SKILL.md +95 -0
- package/skills/graview-seed/SKILL.md +103 -0
- package/skills/graview-ship/SKILL.md +154 -0
- package/skills/graview-studio/SKILL.md +85 -0
|
@@ -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.
|