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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-githolon",
3
- "version": "0.80.1",
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
- ## Copy `birthHome` — the canonical template
32
+ ## FIRST decide the child's cardinality: **instance** or **singleton**
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 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(EstateChild) // the child lineage record on YOUR chain
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
- birth({
52
- workspace: p.child,
53
- genesisChain: [
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 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.
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 verb.
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-birth` (nomos2 repo).
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`.