create-githolon 0.80.1 → 0.81.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/08-births.md +104 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-githolon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.81.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",
|
|
@@ -29,18 +29,36 @@ A leaf child (a home, an estate item) is born with exactly:
|
|
|
29
29
|
`frameworkHash` + `lawHash` are the content hashes of the two packages, pinned at compile time (the kernel
|
|
30
30
|
resolves the bytes from custody by hash — they are never shipped on the wire).
|
|
31
31
|
|
|
32
|
-
##
|
|
32
|
+
## FIRST decide the child's cardinality: **instance** or **singleton**
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
A born child's workspace name **is its identity**, so before you write the birth you must answer one
|
|
35
|
+
question: *can a parent have MANY of these children, or exactly ONE per key?* The DSL makes you state the
|
|
36
|
+
answer at the call site — the same way a field states its merge driver — so you can't get it wrong by
|
|
37
|
+
accident. Do **not** hand-roll the name.
|
|
38
|
+
|
|
39
|
+
| You want… | Use | Naming | Re-birth |
|
|
40
|
+
|---|---|---|---|
|
|
41
|
+
| **Many children per parent** (a user has many estates, a project has many docs) | `birth.instance({ from, … })` | Nomos **mints** the child id from a `create()`d record — unique, captured, unforgeable | each call is a **new** child |
|
|
42
|
+
| **Exactly one child per key** (one home per user, one settings-workspace per team) | `birth.singleton({ keyedBy, … })` | **derived** deterministically from the key | idempotent — same key ⇒ **same** child |
|
|
43
|
+
|
|
44
|
+
> ⚠️ **The trap:** births are deterministic (no `Math.random`/`Date.now` in a plan), so the tempting move
|
|
45
|
+
> is to derive the name from a stable field — `estate-<sha256(owner)>`. That silently caps the owner at
|
|
46
|
+
> **one** estate forever. If you find yourself hashing a field to name a child, you almost always want
|
|
47
|
+
> `birth.instance` (many) — reach for a derived name **only** when one-per-key is the actual intent, and
|
|
48
|
+
> then let `birth.singleton` derive it for you.
|
|
49
|
+
|
|
50
|
+
### `birth.instance` — many children per parent (the common case)
|
|
51
|
+
|
|
52
|
+
The child's id is **minted**, exactly like `create(Agg)` mints an aggregate id: you `create()` a record for
|
|
53
|
+
the birth (the audit/lineage row you'd want anyway) and its captured id names the child. It's unique by
|
|
54
|
+
construction and reproduces byte-identically at `verify_chain` (the minted id rides the committed intent).
|
|
36
55
|
|
|
37
56
|
```ts
|
|
38
|
-
import { directive, birth, z } from "@githolon/dsl";
|
|
57
|
+
import { directive, birth, create, z } from "@githolon/dsl";
|
|
39
58
|
|
|
40
59
|
export const birthEstateChild = directive("birthEstateChild")
|
|
41
|
-
.creates(
|
|
60
|
+
.creates(EstateOrder) // the birth's lineage record on YOUR chain
|
|
42
61
|
.payload(z.object({
|
|
43
|
-
child: z.string().min(1), // the child workspace name
|
|
44
62
|
owner: z.string().min(1), // the verified owner (a user:<uid> subject)
|
|
45
63
|
frameworkHash: z.string().min(1), // pinned at your governance compile
|
|
46
64
|
lawHash: z.string().min(1), // your ESTATE domain law package hash
|
|
@@ -48,20 +66,41 @@ export const birthEstateChild = directive("birthEstateChild")
|
|
|
48
66
|
}))
|
|
49
67
|
.plan((p) => {
|
|
50
68
|
const actor = "user:" + p.owner;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
69
|
+
const order = create(EstateOrder).set("owner", p.owner).set("bornAt", p.bornAt);
|
|
70
|
+
const child = birth.instance({
|
|
71
|
+
from: order, // the child's id IS this record's minted id (no name to invent)
|
|
72
|
+
prefix: "estate-", // optional, human-legible
|
|
73
|
+
genesisChain: (estate) => [ // the builder receives the resolved child name
|
|
54
74
|
{ domain: "bootstrap", directiveId: "installDomain", actor, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash, domainPackageB64: "" },
|
|
55
75
|
{ domain: "nomos", directiveId: "installDomain", actor, payload: { domainHash: p.lawHash }, domainHash: p.lawHash, domainPackageB64: "" },
|
|
56
|
-
{ domain: "estate", directiveId: "seedEstateChild", actor, payload: { owner: p.owner, bornAt: p.bornAt } },
|
|
76
|
+
{ domain: "estate", directiveId: "seedEstateChild", actor, payload: { owner: p.owner, estateName: estate, bornAt: p.bornAt } },
|
|
57
77
|
],
|
|
58
78
|
});
|
|
79
|
+
order.set("workspaceName", child); // record the minted name on the lineage row
|
|
59
80
|
return [];
|
|
60
81
|
})
|
|
61
82
|
.births() // declares the birth — the kernel folds the recipe
|
|
62
83
|
.requires("creator"); // the birth authority (see below)
|
|
63
84
|
```
|
|
64
85
|
|
|
86
|
+
### `birth.singleton` — exactly one child per key (e.g. a home)
|
|
87
|
+
|
|
88
|
+
The framework's own `birthHome` is a singleton: one home per user, `home-<hash(owner)>`, idempotent. That is
|
|
89
|
+
the *correct* use of a derived name — and `birth.singleton` derives it for you:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const home = birth.singleton({
|
|
93
|
+
keyedBy: p.owner, // one home per owner; re-birth is a no-op (same name)
|
|
94
|
+
namespace: "home", // → home-<hash>
|
|
95
|
+
genesisChain: (home) => [ /* installs + seed the owner, using `home` as the name */ ],
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`keyedBy` may be a compound key (`["user:ada", "team-7"]`, order-sensitive) for one-child-per-pair.
|
|
100
|
+
|
|
101
|
+
(Raw `birth({ workspace, genesisChain })` remains the low-level escape hatch for a name that is genuinely
|
|
102
|
+
externally-determined — but if you're deriving it from your own data, use one of the two verbs above.)
|
|
103
|
+
|
|
65
104
|
## From the app (Dart) — parentless, session-bound
|
|
66
105
|
|
|
67
106
|
The generated Dart client emits a typed method per `.births()` directive, bound to the SESSION whose
|
|
@@ -86,10 +125,13 @@ relation models your policy; `creator` is the convention `birthChild` uses.)
|
|
|
86
125
|
|
|
87
126
|
## What NOT to do
|
|
88
127
|
|
|
89
|
-
- **Don't
|
|
90
|
-
|
|
128
|
+
- **Don't hand-roll the child's name from a field** (`estate-<sha256(owner)>`). That is the cardinality
|
|
129
|
+
trap — it caps the parent at one child per key. Use `birth.instance` (many) or `birth.singleton` (one);
|
|
130
|
+
they own the naming so you can't collapse it by accident.
|
|
131
|
+
- **Don't ask for a "child-birth primitive."** It already exists — `.births()` + `birth.instance` /
|
|
132
|
+
`birth.singleton`. A home is a first-class workspace; it births children exactly as root births platforms.
|
|
91
133
|
- **Don't hand-roll a second recipe *builder*** across your domains — keep the recipe inline (it's small)
|
|
92
|
-
or share one tiny helper *in your own code*; the framework deliberately keeps **one** birth
|
|
134
|
+
or share one tiny helper *in your own code*; the framework deliberately keeps **one** birth primitive.
|
|
93
135
|
- **Don't put a clock/random in the plan** — `bornAt` rides the payload (the timestamp doctrine).
|
|
94
136
|
|
|
95
137
|
## Prove it — the 30-second loop
|
|
@@ -106,7 +148,9 @@ npx githolon proof # OFFLINE: drives your .births() directive on the real
|
|
|
106
148
|
|
|
107
149
|
A red proof names the exact genesis step the child's gate refused. The most common first-timer halt: a
|
|
108
150
|
plan that returns a fluent builder — builders record themselves, so end the plan with `return [];`.
|
|
109
|
-
A complete working two-domain births package to copy: `bench/scale/fixtures/estate
|
|
151
|
+
A complete working two-domain births package to copy: `bench/scale/fixtures/home-delegated-estate` (nomos2
|
|
152
|
+
repo) — it births estates with `birth.instance` (many per owner) and is exercised by
|
|
153
|
+
`bench/scale/home_delegated_estate.smoke.mjs`.
|
|
110
154
|
|
|
111
155
|
## Posture at birth
|
|
112
156
|
|
|
@@ -116,3 +160,49 @@ recovered by the operator on request (a rebirth heal = "ask us"); a **Sovereign*
|
|
|
116
160
|
the child's own consent fact first (a heal = consent first). See
|
|
117
161
|
[11-governance-postures.md](./11-governance-postures.md) for the two postures, the consent ceremony, and
|
|
118
162
|
the exit rights.
|
|
163
|
+
|
|
164
|
+
## Offline estate creation via delegated authority (`delegatedRole` / `.delegatedFrom(parent)`)
|
|
165
|
+
|
|
166
|
+
Sometimes a CHILD must birth its own children **offline**, under authority a PARENT granted — with no
|
|
167
|
+
contact with the parent at authoring time. Example: a platform **P** lets a user **U** create estates; U
|
|
168
|
+
holds a **home** holon **H** on-device; U births an estate **E** as a child of H, offline, gated by H's
|
|
169
|
+
law. The parent's grant cannot be an author-time attested read (that needs connectivity). Instead the
|
|
170
|
+
grant is **delivered into H as a signed cert and folded as a local authority fact** — then read offline.
|
|
171
|
+
|
|
172
|
+
Author the delivery directive with `.delegatedRole(relation).delegatedFrom(parent)`:
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
export const acceptDelegatedRole = directive("acceptDelegatedRole")
|
|
176
|
+
.creates(EstateCreatorGrant)
|
|
177
|
+
.payload(z.object({ subject: roleSubject, delegationCert: z.string().min(1), grantedBy: roleSubject, grantedAt: z.string() }))
|
|
178
|
+
.plan((p) => {
|
|
179
|
+
create(EstateCreatorGrant).set("subject", p.subject).set("grantedAt", p.grantedAt);
|
|
180
|
+
return writeTuple({ object: "workspace:self", relation: "estateCreator", subject: p.subject, grantedBy: p.grantedBy, grantedAt: p.grantedAt });
|
|
181
|
+
})
|
|
182
|
+
.grants("estateCreator")
|
|
183
|
+
.delegatedRole("estateCreator", { subjectField: "subject", certField: "delegationCert" })
|
|
184
|
+
.delegatedFrom({ parentKeyObject: "birth-attestation:self", parentKeyField: "parentKey" }); // H's parent P's key
|
|
185
|
+
|
|
186
|
+
// the birth itself is gated on the DELIVERED local tuple — read OFFLINE, no parent contact:
|
|
187
|
+
export const birthEstateWorkspace = directive("birthEstateWorkspace")
|
|
188
|
+
.creates(EstateBirthOrder).payload(/* … */).plan(/* birth(...) */)
|
|
189
|
+
.births()
|
|
190
|
+
.requires("estateCreator"); // ← reads the delegated tuple folded by acceptDelegatedRole
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
How it works:
|
|
194
|
+
- **The one gate re-verifies P's grant on every lane** (author / edge admission / verify_chain): the
|
|
195
|
+
carried `DelegationCert` must be signed by a grantor whose key chains to the pinned `K_root`, bind the
|
|
196
|
+
same `(relation, subject)` the tuple writes, and — `.delegatedFrom(parent)` — whose grantor key IS this
|
|
197
|
+
workspace's parent key. A forged / mis-bound / wrong-parent grant fails closed. This is the
|
|
198
|
+
`nomosDelegationGate` era key — a law that declares no `.delegatedRole()` is byte-identical.
|
|
199
|
+
- **The birth is authored OFFLINE**, gated only by H's LOCAL folded tuple; `verify_chain` re-verifies the
|
|
200
|
+
delegation to `K_root` with no cross-workspace read.
|
|
201
|
+
- **Revocation is forward-only + evidence-preserving.** P delivers a revoke (`removeTuple`, same gate);
|
|
202
|
+
the folded tuple flips revoked (`RemoveWins`), so U's NEXT birth refuses — but an estate born WHILE the
|
|
203
|
+
grant was live re-verifies green forever (verify_chain re-folds the chain prefix at each intent's
|
|
204
|
+
position). Both the grant and the revoke are permanent ledger facts.
|
|
205
|
+
|
|
206
|
+
The platform produces the cert with the `delegation_cert_sign` holon op (grantor secret injected per call,
|
|
207
|
+
never persisted), carrying its lineage to `K_root`; the bailiff delivers it host-free. Worked end-to-end:
|
|
208
|
+
`bench/scale/home_delegated_estate.smoke.mjs`; doctrine: `architecture/delegated_authority_births.md`.
|