create-githolon 0.80.2 → 0.82.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 +115 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-githolon",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.82.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,39 +29,81 @@ 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
|
+
## Every birth is **keyed** — so you can find the workspace again
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
A workspace exists to be **used**, and using it — sync it, open it, place an asset in 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 `estateId`, 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.
|
|
36
43
|
|
|
37
44
|
```ts
|
|
38
|
-
import { directive, birth, z } from "@githolon/dsl";
|
|
45
|
+
import { directive, birth, create, z } from "@githolon/dsl";
|
|
39
46
|
|
|
40
|
-
export const
|
|
41
|
-
.creates(
|
|
47
|
+
export const birthEstate = directive("birthEstate")
|
|
48
|
+
.creates(EstateBirth) // your lineage/audit row
|
|
42
49
|
.payload(z.object({
|
|
43
|
-
|
|
44
|
-
owner: z.string().min(1),
|
|
45
|
-
frameworkHash: z.string().min(1),
|
|
46
|
-
lawHash: z.string().min(1),
|
|
50
|
+
estateId: 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),
|
|
47
54
|
bornAt: z.string().min(1), // ISO-8601, caller-stamped (determinism)
|
|
48
55
|
}))
|
|
49
56
|
.plan((p) => {
|
|
50
57
|
const actor = "user:" + p.owner;
|
|
51
|
-
birth({
|
|
52
|
-
|
|
53
|
-
|
|
58
|
+
birth.keyed({
|
|
59
|
+
key: p.estateId, // ← the workspace name is derived from this
|
|
60
|
+
namespace: "estate", // → estate-<sha256(estate ⏎ estateId)[:40]>
|
|
61
|
+
genesisChain: (ws) => [ // the builder receives the resolved name
|
|
54
62
|
{ domain: "bootstrap", directiveId: "installDomain", actor, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash, domainPackageB64: "" },
|
|
55
63
|
{ domain: "nomos", directiveId: "installDomain", actor, payload: { domainHash: p.lawHash }, domainHash: p.lawHash, domainPackageB64: "" },
|
|
56
|
-
{ domain: "estate", directiveId: "
|
|
64
|
+
{ domain: "estate", directiveId: "seedEstate", actor, payload: { estateId: p.estateId, workspaceName: ws, bornAt: p.bornAt } },
|
|
57
65
|
],
|
|
58
66
|
});
|
|
59
67
|
return [];
|
|
60
68
|
})
|
|
61
|
-
.births()
|
|
62
|
-
.requires("
|
|
69
|
+
.births()
|
|
70
|
+
.requires("estateCreator");
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Then in the app you address that estate from just its id — **no lookup table, no persisted name**:
|
|
74
|
+
|
|
75
|
+
```dart
|
|
76
|
+
// Dart (nomos_client)
|
|
77
|
+
final ws = deriveWorkspaceName('estate', estateId);
|
|
78
|
+
final session = await engine.open(ws); // open / sync / place assets — any device, any time
|
|
79
|
+
```
|
|
80
|
+
```ts
|
|
81
|
+
// TS (@githolon/client)
|
|
82
|
+
const ws = await deriveWorkspaceName('estate', estateId);
|
|
83
|
+
const holon = await connect({ cloud, workspace: ws, clientId });
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`deriveWorkspaceName` computes the byte-identical name the law's `birth.keyed` produced (contract-tested
|
|
87
|
+
across the engine, Dart, and TS). This is the helper that replaces every hand-rolled
|
|
88
|
+
`estateWorkspaceForEstateId` — 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`:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
birth.singleton({ keyedBy: p.owner, namespace: "home", genesisChain: (home) => [ /* installs + seed */ ] });
|
|
97
|
+
// app: deriveWorkspaceName('home', owner)
|
|
63
98
|
```
|
|
64
99
|
|
|
100
|
+
> ⚠️ **`birth.instance` is deprecated — don't use it.** It mints a name you throw away, so the born
|
|
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
|
+
|
|
65
107
|
## From the app (Dart) — parentless, session-bound
|
|
66
108
|
|
|
67
109
|
The generated Dart client emits a typed method per `.births()` directive, bound to the SESSION whose
|
|
@@ -86,10 +128,15 @@ relation models your policy; `creator` is the convention `birthChild` uses.)
|
|
|
86
128
|
|
|
87
129
|
## What NOT to do
|
|
88
130
|
|
|
89
|
-
- **Don't
|
|
90
|
-
|
|
131
|
+
- **Don't hand-roll the child's name** (`estate-<sha256(owner)>`, or your own `estateWorkspaceForEstateId`).
|
|
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.
|
|
91
138
|
- **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
|
|
139
|
+
or share one tiny helper *in your own code*; the framework deliberately keeps **one** birth primitive.
|
|
93
140
|
- **Don't put a clock/random in the plan** — `bornAt` rides the payload (the timestamp doctrine).
|
|
94
141
|
|
|
95
142
|
## Prove it — the 30-second loop
|
|
@@ -106,7 +153,9 @@ npx githolon proof # OFFLINE: drives your .births() directive on the real
|
|
|
106
153
|
|
|
107
154
|
A red proof names the exact genesis step the child's gate refused. The most common first-timer halt: a
|
|
108
155
|
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
|
|
156
|
+
A complete working two-domain births package to copy: `bench/scale/fixtures/home-delegated-estate` (nomos2
|
|
157
|
+
repo) — it births estates with `birth.keyed` (addressable by estate key) and is exercised by
|
|
158
|
+
`bench/scale/home_delegated_estate.smoke.mjs`.
|
|
110
159
|
|
|
111
160
|
## Posture at birth
|
|
112
161
|
|
|
@@ -116,3 +165,49 @@ recovered by the operator on request (a rebirth heal = "ask us"); a **Sovereign*
|
|
|
116
165
|
the child's own consent fact first (a heal = consent first). See
|
|
117
166
|
[11-governance-postures.md](./11-governance-postures.md) for the two postures, the consent ceremony, and
|
|
118
167
|
the exit rights.
|
|
168
|
+
|
|
169
|
+
## Offline estate 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 estates; U
|
|
173
|
+
holds a **home** holon **H** on-device; U births an estate **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(EstateCreatorGrant)
|
|
182
|
+
.payload(z.object({ subject: roleSubject, delegationCert: z.string().min(1), grantedBy: roleSubject, grantedAt: z.string() }))
|
|
183
|
+
.plan((p) => {
|
|
184
|
+
create(EstateCreatorGrant).set("subject", p.subject).set("grantedAt", p.grantedAt);
|
|
185
|
+
return writeTuple({ object: "workspace:self", relation: "estateCreator", subject: p.subject, grantedBy: p.grantedBy, grantedAt: p.grantedAt });
|
|
186
|
+
})
|
|
187
|
+
.grants("estateCreator")
|
|
188
|
+
.delegatedRole("estateCreator", { 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 birthEstateWorkspace = directive("birthEstateWorkspace")
|
|
193
|
+
.creates(EstateBirthOrder).payload(/* … */).plan(/* birth(...) */)
|
|
194
|
+
.births()
|
|
195
|
+
.requires("estateCreator"); // ← reads the delegated tuple folded by acceptDelegatedRole
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
How it works:
|
|
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 an estate 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.
|
|
210
|
+
|
|
211
|
+
The platform produces the cert with the `delegation_cert_sign` holon op (grantor secret injected per call,
|
|
212
|
+
never persisted), carrying its lineage to `K_root`; the bailiff delivers it host-free. Worked end-to-end:
|
|
213
|
+
`bench/scale/home_delegated_estate.smoke.mjs`; doctrine: `architecture/delegated_authority_births.md`.
|