create-githolon 0.98.3 → 0.99.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/package.json +1 -1
- package/template/docs/03-client.md +3 -3
- package/template/docs/08-births.md +47 -194
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-githolon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.99.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Scaffold a Nomos domain package: the starter domain + compile config + live e2e. `npm create githolon my-app`.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -63,10 +63,10 @@ The four-layer split (the blessed shape):
|
|
|
63
63
|
final platform = await engine.open(cloud: cloud, workspace: platformWs);
|
|
64
64
|
final platformClient = await PlatformClient.bind(platform);
|
|
65
65
|
|
|
66
|
-
//
|
|
67
|
-
final
|
|
66
|
+
// One business birth; the parent is the bound session and Nomos owns the ceremony:
|
|
67
|
+
final projectRef = await platformClient.birth(project);
|
|
68
68
|
// NomosRef is THE cross-workspace primitive — follow it, bind the next client:
|
|
69
|
-
final project = await engine.follow(
|
|
69
|
+
final project = await engine.follow(projectRef);
|
|
70
70
|
final projectClient = await ProjectClient.bind(project);
|
|
71
71
|
```
|
|
72
72
|
|
|
@@ -1,213 +1,66 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Birth: give a business thing its own life
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Use `birth(Aggregate, spec)` when a business thing should be independently durable, portable, and
|
|
4
|
+
recoverable. Describe the business facts once; Nomos owns the workspace, identity proof, provenance,
|
|
5
|
+
persistence, and recovery beneath it.
|
|
6
6
|
|
|
7
|
-
## How it works (birth is a kernel concern, defined by the ledger)
|
|
8
|
-
|
|
9
|
-
Your directive declares `.births()` and, in its `plan`, authors the child's **genesis recipe** — the
|
|
10
|
-
ordered intents the child will run — then calls `birth({ workspace, genesisChain })`. That's it. When the
|
|
11
|
-
parent's offer is admitted, the kernel's birth offer-effect:
|
|
12
|
-
|
|
13
|
-
1. spawns the child's (empty) custody,
|
|
14
|
-
2. **installs the child's law in-chain** and folds the genesis recipe **through the child's own gate**,
|
|
15
|
-
3. so the child **self-validates from intent 0** — it is not trusted, it proves itself.
|
|
16
|
-
|
|
17
|
-
The host installs nothing and decides nothing. The recipe is a **ledger fact**, not a runtime trick.
|
|
18
|
-
|
|
19
|
-
## The leaf recipe = two installs + your seed
|
|
20
|
-
|
|
21
|
-
A leaf child (a home, a project) is born with exactly:
|
|
22
|
-
|
|
23
|
-
```
|
|
24
|
-
[ bootstrap/installDomain(<frameworkHash>), // the bootstrap controller (bytes-by-hash, kernel-resolved)
|
|
25
|
-
nomos/installDomain(<lawHash>), // YOUR child's domain law package
|
|
26
|
-
<your own seed step(s)> ] // e.g. seed the owner / initial state
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
`frameworkHash` + `lawHash` are the content hashes of the two packages, pinned at compile time (the kernel
|
|
30
|
-
resolves the bytes from custody by hash — they are never shipped on the wire).
|
|
31
|
-
|
|
32
|
-
## Every birth is **keyed** — so you can find the workspace again
|
|
33
|
-
|
|
34
|
-
A workspace exists to be **used**, and using it — sync it, open it, add a task to it — means **finding it
|
|
35
|
-
again**, often from another device or after a restart. So a born workspace's name is **never** opaque or
|
|
36
|
-
minted-and-forgotten: it is always **derived from a key you already own** (a business id like `projectId`, or
|
|
37
|
-
an owner uid). One verb: **`birth.keyed`**.
|
|
38
|
-
|
|
39
|
-
> **The one rule.** The name comes from a key you own → the client re-derives the same name from that same
|
|
40
|
-
> key with `deriveWorkspaceName(namespace, key)` → the workspace is addressable **forever, with zero
|
|
41
|
-
> persistence**. Same key ⇒ same workspace (re-birth is idempotent). You never hand-roll a naming scheme, and
|
|
42
|
-
> you never store a minted name to find it later.
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
import { directive, birth, create, z } from "@githolon/dsl";
|
|
46
|
-
|
|
47
|
-
export const birthProject = directive("birthProject")
|
|
48
|
-
.creates(ProjectBirth) // your lineage/audit row
|
|
49
|
-
.payload(z.object({
|
|
50
|
-
projectId: z.string().min(1), // THE KEY — an id you mint app-side; the workspace is FOR it
|
|
51
|
-
owner: z.string().min(1),
|
|
52
|
-
frameworkHash: z.string().min(1),
|
|
53
|
-
lawHash: z.string().min(1),
|
|
54
|
-
bornAt: z.string().min(1), // ISO-8601, caller-stamped (determinism)
|
|
55
|
-
}))
|
|
56
|
-
.plan((p) => {
|
|
57
|
-
const actor = "user:" + p.owner;
|
|
58
|
-
birth.keyed({
|
|
59
|
-
key: p.projectId, // ← the workspace name is derived from this
|
|
60
|
-
namespace: "project", // → project-<sha256(project ⏎ projectId)[:40]>
|
|
61
|
-
genesisChain: (ws) => [ // the builder receives the resolved name
|
|
62
|
-
{ domain: "bootstrap", directiveId: "installDomain", actor, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash, domainPackageB64: "" },
|
|
63
|
-
{ domain: "nomos", directiveId: "installDomain", actor, payload: { domainHash: p.lawHash }, domainHash: p.lawHash, domainPackageB64: "" },
|
|
64
|
-
{ domain: "project", directiveId: "seedProject", actor, payload: { projectId: p.projectId, workspaceName: ws, bornAt: p.bornAt } },
|
|
65
|
-
],
|
|
66
|
-
});
|
|
67
|
-
return [];
|
|
68
|
-
})
|
|
69
|
-
.births()
|
|
70
|
-
.requires("projectCreator");
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Then in the app you address that project from just its id — **no lookup table, no persisted name**:
|
|
74
|
-
|
|
75
|
-
```dart
|
|
76
|
-
// Dart (nomos_client)
|
|
77
|
-
final ws = deriveWorkspaceName('project', projectId);
|
|
78
|
-
final session = await engine.open(ws); // open / sync / add tasks — any device, any time
|
|
79
|
-
```
|
|
80
7
|
```ts
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
8
|
+
import { Lww, aggregate, birth, t, z } from "@githolon/dsl";
|
|
9
|
+
|
|
10
|
+
export const Project = aggregate("Project", {
|
|
11
|
+
reference: t.string().merge(Lww),
|
|
12
|
+
name: t.string().merge(Lww),
|
|
13
|
+
country: t.string().merge(Lww),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const project = birth(Project, {
|
|
17
|
+
payload: z.object({
|
|
18
|
+
reference: z.string().min(1),
|
|
19
|
+
name: z.string().min(1),
|
|
20
|
+
country: z.string().length(2),
|
|
21
|
+
}),
|
|
22
|
+
key: (facts) => facts.reference,
|
|
23
|
+
plan: (project, facts) => project
|
|
24
|
+
.set("reference", facts.reference)
|
|
25
|
+
.set("name", facts.name)
|
|
26
|
+
.set("country", facts.country),
|
|
27
|
+
});
|
|
84
28
|
```
|
|
85
29
|
|
|
86
|
-
|
|
87
|
-
across the engine, Dart, and TS). This is the helper that replaces every hand-rolled
|
|
88
|
-
`projectWorkspaceForProjectId` — delete yours and use this.
|
|
89
|
-
|
|
90
|
-
### One-per-user things (a home): `birth.singleton`
|
|
91
|
-
|
|
92
|
-
A "singleton" is just a keyed birth whose key is a natural single key — one home per user, so `key = owner`.
|
|
93
|
-
Sugar over `birth.keyed`:
|
|
30
|
+
The generated client has one birth method:
|
|
94
31
|
|
|
95
32
|
```ts
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
> workspace can only be reached via the birth result. There is no workspace you make and never want to find
|
|
102
|
-
> again. Always `birth.keyed` (or `birth.singleton`).
|
|
103
|
-
|
|
104
|
-
(Raw `birth({ workspace, genesisChain })` stays the low-level escape hatch for a name that is genuinely
|
|
105
|
-
externally-determined — but if you're deriving it from your own data, use `birth.keyed`.)
|
|
106
|
-
|
|
107
|
-
## From the app (Dart) — parentless, session-bound
|
|
108
|
-
|
|
109
|
-
The generated Dart client emits a typed method per `.births()` directive, bound to the SESSION whose
|
|
110
|
-
workspace IS the parent — no loose parent string, and the born child comes back as a followable ref:
|
|
111
|
-
|
|
112
|
-
```dart
|
|
113
|
-
final parentClient = await MyPlatformClient.bind(parentSession);
|
|
114
|
-
final birth = await parentClient.birthProjectChild(payload: order); // parent = the session workspace
|
|
115
|
-
final child = await engine.follow(birth.bornRefs.single); // mount the born child
|
|
116
|
-
final childClient = await ProjectClient.bind(child); // typed client on the child
|
|
33
|
+
const born = await app.birth({
|
|
34
|
+
reference: "PROJECT-001",
|
|
35
|
+
name: "Foundry retrofit",
|
|
36
|
+
country: "GB",
|
|
37
|
+
});
|
|
117
38
|
```
|
|
118
39
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
## Authority — who may birth
|
|
123
|
-
|
|
124
|
-
`.requires("creator")` gates the birth on a relation tuple. Grant the home owner `#creator` on their own
|
|
125
|
-
home (self-bound at birth, the same way the owner is seeded) so a verified owner can birth their project
|
|
126
|
-
children — and only their own. (Relations: `grant` / `writeTuple`, see `07-security.md`. Use whatever
|
|
127
|
-
relation models your policy; `creator` is the convention `birthChild` uses.)
|
|
40
|
+
`born` is the typed reference to the business root. The call returns only after the birth is committed
|
|
41
|
+
locally and persistence has been requested from the bound Nomos session.
|
|
128
42
|
|
|
129
|
-
|
|
43
|
+
That is the tenant contract. Do not create a birth directive, construct a child address, assemble setup
|
|
44
|
+
steps, or expose framework hashes in application code. Those are framework implementation details.
|
|
130
45
|
|
|
131
|
-
|
|
132
|
-
`birth.keyed` owns the naming and `deriveWorkspaceName` gives the client the identical name — hand-rolling
|
|
133
|
-
either half is how the two drift and a workspace becomes unfindable.
|
|
134
|
-
- **Don't use `birth.instance`.** It's deprecated — it mints a name you can't re-derive, so the workspace is
|
|
135
|
-
addressable only via the birth result. Every workspace needs to be found again; birth it keyed.
|
|
136
|
-
- **Don't ask for a "child-birth primitive."** It already exists — `.births()` + `birth.keyed`. A home is a
|
|
137
|
-
first-class workspace; it births children exactly as root births platforms.
|
|
138
|
-
- **Don't hand-roll a second recipe *builder*** across your domains — keep the recipe inline (it's small)
|
|
139
|
-
or share one tiny helper *in your own code*; the framework deliberately keeps **one** birth primitive.
|
|
140
|
-
- **Don't put a clock/random in the plan** — `bornAt` rides the payload (the timestamp doctrine).
|
|
46
|
+
## One root per domain
|
|
141
47
|
|
|
142
|
-
|
|
48
|
+
A domain declares at most one business birth. This keeps the generated method unambiguous and makes the
|
|
49
|
+
born aggregate the stable root of its workspace. Model other records beneath that root using ordinary
|
|
50
|
+
aggregates and directives.
|
|
143
51
|
|
|
144
|
-
|
|
145
|
-
directive exists? recipe payload satisfies its schema? every plan returns an **array** of ops) and prints
|
|
146
|
-
any problem as a `⚠ births` warning **containing the fix**. Then:
|
|
52
|
+
## Prove it locally
|
|
147
53
|
|
|
148
54
|
```bash
|
|
149
|
-
npx githolon compile
|
|
150
|
-
npx githolon proof
|
|
151
|
-
# child through its OWN gate, reads the seed back, verify_chain green
|
|
55
|
+
npx githolon compile
|
|
56
|
+
npx githolon proof
|
|
152
57
|
```
|
|
153
58
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
repo) — it births child workspaces with `birth.keyed` (addressable by key) and is exercised by
|
|
158
|
-
`bench/scale/home_delegated_estate.smoke.mjs`.
|
|
159
|
-
|
|
160
|
-
## Posture at birth
|
|
161
|
-
|
|
162
|
-
`birthChild` / `createWorkspace` take an optional `posture: "managed" | "sovereign"` (default **managed**)
|
|
163
|
-
— the child's declared governance posture, mirrored onto its lineage record. A **Managed** child can be
|
|
164
|
-
recovered by the operator on request (a rebirth heal = "ask us"); a **Sovereign** child's rebirth requires
|
|
165
|
-
the child's own consent fact first (a heal = consent first). See
|
|
166
|
-
[11-governance-postures.md](./11-governance-postures.md) for the two postures, the consent ceremony, and
|
|
167
|
-
the exit rights.
|
|
168
|
-
|
|
169
|
-
## Offline workspace creation via delegated authority (`delegatedRole` / `.delegatedFrom(parent)`)
|
|
170
|
-
|
|
171
|
-
Sometimes a CHILD must birth its own children **offline**, under authority a PARENT granted — with no
|
|
172
|
-
contact with the parent at authoring time. Example: a platform **P** lets a user **U** create projects; U
|
|
173
|
-
holds a **home** holon **H** on-device; U births a project **E** as a child of H, offline, gated by H's
|
|
174
|
-
law. The parent's grant cannot be an author-time attested read (that needs connectivity). Instead the
|
|
175
|
-
grant is **delivered into H as a signed cert and folded as a local authority fact** — then read offline.
|
|
176
|
-
|
|
177
|
-
Author the delivery directive with `.delegatedRole(relation).delegatedFrom(parent)`:
|
|
178
|
-
|
|
179
|
-
```ts
|
|
180
|
-
export const acceptDelegatedRole = directive("acceptDelegatedRole")
|
|
181
|
-
.creates(ProjectCreatorGrant)
|
|
182
|
-
.payload(z.object({ subject: roleSubject, delegationCert: z.string().min(1), grantedBy: roleSubject, grantedAt: z.string() }))
|
|
183
|
-
.plan((p) => {
|
|
184
|
-
create(ProjectCreatorGrant).set("subject", p.subject).set("grantedAt", p.grantedAt);
|
|
185
|
-
return writeTuple({ object: "workspace:self", relation: "projectCreator", subject: p.subject, grantedBy: p.grantedBy, grantedAt: p.grantedAt });
|
|
186
|
-
})
|
|
187
|
-
.grants("projectCreator")
|
|
188
|
-
.delegatedRole("projectCreator", { subjectField: "subject", certField: "delegationCert" })
|
|
189
|
-
.delegatedFrom({ parentKeyObject: "birth-attestation:self", parentKeyField: "parentKey" }); // H's parent P's key
|
|
190
|
-
|
|
191
|
-
// the birth itself is gated on the DELIVERED local tuple — read OFFLINE, no parent contact:
|
|
192
|
-
export const birthProjectWorkspace = directive("birthProjectWorkspace")
|
|
193
|
-
.creates(ProjectBirthOrder).payload(/* … */).plan(/* birth(...) */)
|
|
194
|
-
.births()
|
|
195
|
-
.requires("projectCreator"); // ← reads the delegated tuple folded by acceptDelegatedRole
|
|
196
|
-
```
|
|
59
|
+
The offline proof drives the real kernel. It checks that the child installs its law in-chain, authors the
|
|
60
|
+
business root, verifies its full history, packs its custody, discards the resident copy, reopens from that
|
|
61
|
+
custody, verifies again, and can still read the root.
|
|
197
62
|
|
|
198
|
-
|
|
199
|
-
- **The one gate re-verifies P's grant on every lane** (author / edge admission / verify_chain): the
|
|
200
|
-
carried `DelegationCert` must be signed by a grantor whose key chains to the pinned `K_root`, bind the
|
|
201
|
-
same `(relation, subject)` the tuple writes, and — `.delegatedFrom(parent)` — whose grantor key IS this
|
|
202
|
-
workspace's parent key. A forged / mis-bound / wrong-parent grant fails closed. This is the
|
|
203
|
-
`nomosDelegationGate` era key — a law that declares no `.delegatedRole()` is byte-identical.
|
|
204
|
-
- **The birth is authored OFFLINE**, gated only by H's LOCAL folded tuple; `verify_chain` re-verifies the
|
|
205
|
-
delegation to `K_root` with no cross-workspace read.
|
|
206
|
-
- **Revocation is forward-only + evidence-preserving.** P delivers a revoke (`removeTuple`, same gate);
|
|
207
|
-
the folded tuple flips revoked (`RemoveWins`), so U's NEXT birth refuses — but a project born WHILE the
|
|
208
|
-
grant was live re-verifies green forever (verify_chain re-folds the chain prefix at each intent's
|
|
209
|
-
position). Both the grant and the revoke are permanent ledger facts.
|
|
63
|
+
## Framework compatibility
|
|
210
64
|
|
|
211
|
-
The
|
|
212
|
-
|
|
213
|
-
`bench/scale/home_delegated_estate.smoke.mjs`; doctrine: `architecture/delegated_authority_births.md`.
|
|
65
|
+
The lower-level recipe APIs still exist for Nomos' built-in workspace laws and for older packages. They
|
|
66
|
+
are not the tenant authoring surface. New business domains use `birth(Aggregate, spec)`.
|