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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-githolon",
3
- "version": "0.80.2",
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
- ## Copy `birthHome` — the canonical template
32
+ ## Every birth is **keyed** — so you can find the workspace again
33
33
 
34
- `birthHome` (in `@githolon/dsl`'s framework) is exactly this pattern for the **home** domain. To birth an
35
- **estate child** (or any leaf running your law), copy it and swap the seed:
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 birthEstateChild = directive("birthEstateChild")
41
- .creates(EstateChild) // the child lineage record on YOUR chain
47
+ export const birthEstate = directive("birthEstate")
48
+ .creates(EstateBirth) // your lineage/audit row
42
49
  .payload(z.object({
43
- child: z.string().min(1), // the child workspace name
44
- owner: z.string().min(1), // the verified owner (a user:<uid> subject)
45
- frameworkHash: z.string().min(1), // pinned at your governance compile
46
- lawHash: z.string().min(1), // your ESTATE domain law package hash
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
- workspace: p.child,
53
- genesisChain: [
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: "seedEstateChild", actor, payload: { owner: p.owner, bornAt: p.bornAt } },
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() // declares the birth — the kernel folds the recipe
62
- .requires("creator"); // the birth authority (see below)
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 ask for a "child-birth primitive."** It already exists — `.births()` + `birth(recipe)`. A home is
90
- a first-class workspace; it births children exactly as root births platforms. Recursion is the law.
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 verb.
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-birth` (nomos2 repo).
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`.