create-githolon 0.93.0 → 0.94.2

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.93.0",
3
+ "version": "0.94.2",
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",
@@ -16,13 +16,27 @@ export const Book = aggregate("Book", {
16
16
  });
17
17
  ```
18
18
 
19
- Field kinds: `t.string()` · `t.int()` · `t.enum([...] as const)` (typos squeal
19
+ Field kinds: `t.string()` · `t.int()` · `t.bool()` (a true/false flag —
20
+ count/filter key on it; NOT a 2-value `t.enum`) · `t.enum([...] as const)` (typos squeal
20
21
  at compile time) · `t.set(t.string())` · `t.map(inner)` · `t.json()` /
21
22
  `t.jsonObject()` (JSON-string leaf; `jsonObject` asserts an object and reads
22
23
  back structured) · `t.ref(Agg)` (an id-valued reference) ·
23
24
  `t.hasMany(Child).via("parent")` (the virtual read side of a child's ref —
24
25
  nothing stored on the parent). Add `.optional()` where absence is legal.
25
26
 
27
+ **Cross-workspace links are TYPED — never a bare string.** A reference into ANOTHER
28
+ workspace (its own app-specific ledger — a Task pointing at a Space, a Project at a
29
+ catalogue) is `t.foreignRef({ workspace: "<sibling field holding the foreign workspace
30
+ id>", target: SpaceOwner })`. It stores the foreign aggregate id, types the edge as
31
+ `ForeignRef<SpaceOwnerData>`, and the generated client emits `.resolve(nomos)` /
32
+ `.watch(nomos)` to open the other holon on demand. Cross-domain imports are first-class
33
+ (`import { SpaceOwner } from "./space.ts"`). It is a typed LOCATOR, not a gate-enforced
34
+ foreign key (separate ledgers = no cross-workspace referential integrity at the gate);
35
+ law that must depend on the foreign FACT opts into attested evidence via `trustSource`
36
+ + `.readsFrom(...)`. Worked four-layer example (home → Space + Project with a typed
37
+ cross-ref): `bench/scale/fixtures/four-layer-nested-topology`. **Do NOT store a foreign
38
+ workspace name in a plain `t.string()` — that throws away the type and the resolver.**
39
+
26
40
  ## Relationships: reference aggregates, don't hand-roll composite ids
27
41
 
28
42
  When one aggregate points at another — a listing's primary image, its many
@@ -75,20 +89,20 @@ const PostalAddress = valueObject("PostalAddress", {
75
89
  streetAddress: z.string(), city: z.string(), countryCode: z.string(),
76
90
  });
77
91
  const GeoPoint = geo.point("GeoPoint"); // RFC 7946 Point
78
- const SiteLocation = valueObject("SiteLocation", {
92
+ const TaskLocation = valueObject("TaskLocation", {
79
93
  address: PostalAddress.optional(), // VOs nest in VOs
80
94
  point: GeoPoint.optional(),
81
95
  });
82
96
 
83
- export const EstateSite = aggregate("EstateSite", {
97
+ export const ProjectTask = aggregate("ProjectTask", {
84
98
  name: t.string().merge(Lww),
85
- location: SiteLocation.field().optional(), // ONE json-object leaf
99
+ location: TaskLocation.field().optional(), // ONE json-object leaf
86
100
  point: GeoPoint.field().optional(), // give a geometry its OWN field…
87
101
  });
88
- export const sitesByBounds = spatial("sitesByBounds").of(EstateSite).on("point"); // …to index it
89
- export const recordSite = directive("recordSite").creates(EstateSite)
90
- .payload(z.object({ name: z.string(), location: SiteLocation.zod })) // the SAME zod validates
91
- .plan((p) => { create(EstateSite).set("name", p.name).set("location", p.location); return []; });
102
+ export const tasksByBounds = spatial("tasksByBounds").of(ProjectTask).on("point"); // …to index it
103
+ export const recordTask = directive("recordTask").creates(ProjectTask)
104
+ .payload(z.object({ name: z.string(), location: TaskLocation.zod })) // the SAME zod validates
105
+ .plan((p) => { create(ProjectTask).set("name", p.name).set("location", p.location); return []; });
92
106
  ```
93
107
 
94
108
  ONE definition yields the zod (payload validation — a malformed VO is the
@@ -128,29 +142,29 @@ no behavior:
128
142
 
129
143
  ```ts
130
144
  // contracts.ts — the shared public shapes
131
- export const CatalogueListingContract = contract("CatalogueListing", {
132
- listingId: t.string(),
145
+ export const SpaceLabelContract = contract("SpaceLabel", {
146
+ labelId: t.string(),
133
147
  name: t.string(),
134
148
  });
135
149
 
136
- // catalogue.ts — the concrete owner declares it satisfies the shape (compile-checked;
150
+ // space.ts — the concrete owner declares it satisfies the shape (compile-checked;
137
151
  // a missing/mismatched field fails the build naming it). Private fields stay private.
138
- export const CatalogueListing = aggregate("CatalogueListing", {
139
- listingId: t.string().merge(Lww),
152
+ export const SpaceLabel = aggregate("SpaceLabel", {
153
+ labelId: t.string().merge(Lww),
140
154
  name: t.string().merge(Lww),
141
155
  internalNotes: t.string().merge(Lww), // not in the contract — behind the boundary
142
- }).implements(CatalogueListingContract);
156
+ }).implements(SpaceLabelContract);
143
157
 
144
- // estate.ts — reference the CONTRACT, never the concrete aggregate
145
- export const EstateAsset = aggregate("EstateAsset", {
146
- catalogueWorkspaceName: t.string().merge(Lww),
147
- listing: t.foreignRef({ workspace: "catalogueWorkspaceName", target: CatalogueListingContract, domain: "catalogue" }),
158
+ // project.ts — reference the CONTRACT, never the concrete aggregate
159
+ export const ProjectTask = aggregate("ProjectTask", {
160
+ spaceWorkspaceName: t.string().merge(Lww),
161
+ label: t.foreignRef({ workspace: "spaceWorkspaceName", target: SpaceLabelContract, domain: "space" }),
148
162
  });
149
163
  ```
150
164
 
151
- The generated client types the reference as `ForeignRef<CatalogueListingContract>`;
152
- the Dart DTO class lives in a shared `contracts.dart` every domain imports, so `estate`
153
- never imports `catalogue`'s implementation. If two domains ever import each other's
165
+ The generated client types the reference as `ForeignRef<SpaceLabelContract>`;
166
+ the Dart DTO class lives in a shared `contracts.dart` every domain imports, so `project`
167
+ never imports `space`'s implementation. If two domains ever import each other's
154
168
  concrete types, the compiler fails with a cycle diagnostic pointing you here. Contracts
155
169
  are pure typing metadata — they never change an aggregate's law hash.
156
170
 
@@ -161,9 +175,9 @@ each deliberately (the names below are the DEFAULTS, derived from your ids):
161
175
 
162
176
  | tier | what it is | generated name |
163
177
  |---|---|---|
164
- | **Command payloads** | the typed input to a directive (`toPayloadJson()`) | `<Cap(directiveId)>Payload` — e.g. `CreateSitePayload` |
165
- | **Aggregate state** | the concrete decoded aggregate class | `<Cap(aggId)>` — e.g. `EstateSite` |
166
- | **Read models / projections** | the typed projection row (incl. derived/combined fields) | `<stem(aggId)>ReadModel` — e.g. `EstateSiteReadModel` |
178
+ | **Command payloads** | the typed input to a directive (`toPayloadJson()`) | `<Cap(directiveId)>Payload` — e.g. `RecordTaskPayload` |
179
+ | **Aggregate state** | the concrete decoded aggregate class | `<Cap(aggId)>` — e.g. `ProjectTask` |
180
+ | **Read models / projections** | the typed projection row (incl. derived/combined fields) | `<stem(aggId)>ReadModel` — e.g. `ProjectTaskReadModel` |
167
181
  | **Value objects** | a named structured value (`valueObject`/`geo.*`) | the VO's own declared name, unprefixed |
168
182
 
169
183
  The wire identity (aggregate id, stable ids, projection type tag) is separate from ALL of
@@ -171,34 +185,34 @@ these — names are labels, identity is minted. Renaming below never touches the
171
185
 
172
186
  ### `.dartName()` — declare the product name (Dart codegen only)
173
187
 
174
- An aggregate id is often long for uniqueness (`EstateSite`); the Dart product name you want
175
- is short (`Site`). `.dartName("Site")` makes `Site` the PRIMARY generated Dart identity —
188
+ An aggregate id is often long for uniqueness (`ProjectTask`); the Dart product name you want
189
+ is short (`Task`). `.dartName("Task")` makes `Task` the PRIMARY generated Dart identity —
176
190
  not a post-hoc alias:
177
191
 
178
192
  ```ts
179
- export const EstateSite = aggregate("EstateSite", {
193
+ export const ProjectTask = aggregate("ProjectTask", {
180
194
  owner: t.string().merge(Lww),
181
195
  name: t.string().merge(Lww),
182
- }).public().dartName("Site");
196
+ }).public().dartName("Task");
183
197
  ```
184
198
 
185
- Now EVERY Dart site renames consistently: the state class `Site`, the read model
186
- `SiteReadModel`, the typed id `SiteId`, the by-id accessors `readSiteById`/`watchSiteById`,
187
- `listSites`, and any `ForeignRef<Site>`. The WIRE id stays `EstateSite` (unchanged law,
188
- unchanged stable ids — the minted-id tag is still `EstateSite_<uuid>`). It is validated
199
+ Now EVERY Dart site renames consistently: the state class `Task`, the read model
200
+ `TaskReadModel`, the typed id `TaskId`, the by-id accessors `readTaskById`/`watchTaskById`,
201
+ `listTasks`, and any `ForeignRef<Task>`. The WIRE id stays `ProjectTask` (unchanged law,
202
+ unchanged stable ids — the minted-id tag is still `ProjectTask_<uuid>`). It is validated
189
203
  at author time (must be a Dart type identifier `^[A-Z][A-Za-z0-9_]*$`; a collision with
190
204
  another aggregate's name fails the compile). **TS codegen is deliberately unaffected** —
191
- the generated `.client.ts` stays id-derived (`EstateSiteData`), so there is no
205
+ the generated `.client.ts` stays id-derived (`ProjectTaskData`), so there is no
192
206
  cross-language identity confusion; `.dartName()` is a Dart-only presentation choice.
193
207
 
194
208
  ### Namespaces, not prefixes (multi-domain packages)
195
209
 
196
- Each domain compiles to its OWN generated `.dart` library (`estate.dart`, `catalogue.dart`).
197
- That IS Dart's namespace mechanism: two domains can BOTH declare a `Site` and stay
198
- unambiguous — `import 'package:x/src/generated/estate.dart' as estate;` then `estate.Site`.
210
+ Each domain compiles to its OWN generated `.dart` library (`project.dart`, `space.dart`).
211
+ That IS Dart's namespace mechanism: two domains can BOTH declare a `Comment` and stay
212
+ unambiguous — `import 'package:x/src/generated/project.dart' as project;` then `project.Comment`.
199
213
  The compiler never smashes a domain word into your class name. The top-level package barrel
200
214
  flat-re-exports every domain's classes by default (`dart.packages.barrelExports: 'all'`);
201
- for a multi-domain package where you'd rather force prefixed imports (so two `Site`s never
215
+ for a multi-domain package where you'd rather force prefixed imports (so two `Comment`s never
202
216
  silently share one flat scope), set `barrelExports: 'explicit'` — the barrel then exports
203
217
  only the shared core and you import each domain with an `as` prefix. A single-domain package
204
218
  is unaffected either way.
@@ -215,24 +229,24 @@ generated name via `dart.names`, and route it to the compat package:
215
229
  dart: {
216
230
  support: "package",
217
231
  packages: {
218
- types: "co2_estate_types",
219
- client: "co2_estate_client",
220
- test: "co2_estate_test",
221
- compat: "co2_estate_compat_names", // ← the migration-only shim
232
+ types: "task_tracker_types",
233
+ client: "task_tracker_client",
234
+ test: "task_tracker_test",
235
+ compat: "task_tracker_compat_names", // ← the migration-only shim
222
236
  },
223
- // EstateSite.dartName("Site") → the read model is now `SiteReadModel`. Callers that
224
- // imported the old `SiteRootAggregate` name keep compiling via the compat typedef.
225
- names: { SiteReadModel: "SiteRootAggregate" },
237
+ // ProjectTask.dartName("Task") → the read model is now `TaskReadModel`. Callers that
238
+ // imported the old `TaskRootAggregate` name keep compiling via the compat typedef.
239
+ names: { TaskReadModel: "TaskRootAggregate" },
226
240
  }
227
241
  ```
228
242
 
229
- The `co2_estate_compat_names` package then contains exactly:
243
+ The `task_tracker_compat_names` package then contains exactly:
230
244
 
231
245
  ```dart
232
246
  // MIGRATION-ONLY — delete this package once callers have moved to the real names in
233
- // `co2_estate_types`. …
234
- import 'package:co2_estate_types/co2_estate_types.dart';
235
- typedef SiteRootAggregate = SiteReadModel;
247
+ // `task_tracker_types`. …
248
+ import 'package:task_tracker_types/task_tracker_types.dart';
249
+ typedef TaskRootAggregate = TaskReadModel;
236
250
  ```
237
251
 
238
252
  When `compat` is UNSET, the typedefs land in the types barrel exactly as before.
@@ -36,7 +36,7 @@ and the semantics live in the doc comments.
36
36
  ## Many workspaces at once (the normal shape)
37
37
 
38
38
  Everything about Nomos is multi-workspace: a real app holds a dozen+ live
39
- local workspaces (the user's home, each estate, each catalogue). Don't build
39
+ local workspaces (the user's home, each space, each project). Don't build
40
40
  on a single-connection assumption — hold one PLANE and many handles on it.
41
41
  The four-layer split (the blessed shape):
42
42
 
@@ -50,7 +50,7 @@ The four-layer split (the blessed shape):
50
50
 
51
51
  - **TS/JS**: `openRealm` mounts N workspaces in ONE engine; give each
52
52
  generated client its own handle:
53
- `const realm = await openRealm({ cloud }); const estate = estateClient(await realm.workspace("estate-…"));`
53
+ `const realm = await openRealm({ cloud }); const project = projectClient(await realm.workspace("project-…"));`
54
54
  (`connect()` remains the one-workspace sugar over the same machinery.)
55
55
  - **Flutter/Dart** — session-bound clients, preflighted at construction:
56
56
 
@@ -61,13 +61,13 @@ The four-layer split (the blessed shape):
61
61
  final homeClient = await HomeClient.bind(home); // ASYNC preflight (see below)
62
62
 
63
63
  final platform = await engine.open(cloud: cloud, workspace: platformWs);
64
- final platformClient = await Co2PlatformClient.bind(platform);
64
+ final platformClient = await PlatformClient.bind(platform);
65
65
 
66
66
  // A `.births()` directive takes NO parent — the parent IS the session workspace:
67
- final birth = await platformClient.birthEstateWorkspace(payload: order);
67
+ final birth = await platformClient.birthProjectWorkspace(payload: order);
68
68
  // NomosRef is THE cross-workspace primitive — follow it, bind the next client:
69
- final estate = await engine.follow(birth.bornRefs.single);
70
- final estateClient = await EstateClient.bind(estate);
69
+ final project = await engine.follow(birth.bornRefs.single);
70
+ final projectClient = await ProjectClient.bind(project);
71
71
  ```
72
72
 
73
73
  `XClient.bind(session)` verifies the session's workspace is reachable, holds
@@ -127,6 +127,37 @@ AddWins union keeps every add. No locks, no "last writer wins the whole
127
127
  record", no merge code — the field's declared driver did it
128
128
  (see [02-authoring.md](./02-authoring.md)).
129
129
 
130
+ ## Typed-client patterns (the three gotchas)
131
+
132
+ Building with the generated typed client (`XClient.bind(session)` — no string
133
+ directive ids), three things trip people up once and never again:
134
+
135
+ - **Success = `outcome.ok`.** A directive method returns a `NomosOfferOutcome`;
136
+ `outcome.ok` is true when it committed (a refusal *throws* / surfaces its typed
137
+ law error, it doesn't return a false outcome). For a `.creates` you also get the
138
+ minted id back with no query round-trip; for a `.births` you get `outcome.born`.
139
+ - **Timestamps are caller-stamped — pass them.** Payload fields like `createdAt` /
140
+ `bornAt` are `required` BY DESIGN, not an oversight: a plan is a pure function of
141
+ its payload and reads no clock (determinism — every peer replays the same bytes).
142
+ Stamp `DateTime.now().toUtc().toIso8601String()` at the call site. This is the
143
+ timestamp doctrine, not friction to remove.
144
+ - **Apps hold MANY workspaces open at once — that is the model, not an edge case.**
145
+ `bridge.session(ws)` returns a live session from the REALM (one engine, N mounted
146
+ workspaces, LRU park/remount above a budget). A Space AND a Project AND the home
147
+ are all open concurrently; there is no singleton "current workspace" (that surface
148
+ would be the bug). See [10-flutter-layering.md](./10-flutter-layering.md).
149
+ - **A cross-workspace `ForeignRef` has TWO read paths — don't confuse them.**
150
+ `task.space` (a `ForeignRef<SpaceOwnerData>`) is a typed LOCATOR.
151
+ - **App/presentation read:** `await task.space.resolve(nomos)` / `.watch(nomos)`
152
+ reads the target THROUGH the open realm — instant when that Space is already a
153
+ live session (it usually is), a mount otherwise. This is live UI data, not law.
154
+ - **Law read (a plan's DECISION depends on the foreign fact):** never a live
155
+ resolve — declare `trustSource` + `.readsFrom(...)` so the fact is CAPTURED onto
156
+ the intent and re-verified on every replay (the captured-ports / attested-read
157
+ doctrine, deterministic). `foreignRef` also generates `.attest(nomos)` for that
158
+ evidence. Separate ledgers mean readability is policy, not a gate-checked FK —
159
+ see [09-attested-reads.md](./09-attested-reads.md).
160
+
130
161
  ## Persistence
131
162
 
132
163
  `holon.export()` → bytes; `connect({ restoreFrom: bytes })` restores. Pending
@@ -153,9 +153,9 @@ intent parks in the dead-letter queue on both sides, then replays with
153
153
  `retryDeadLetter` once the law is deployed (see [04-cloud.md](./04-cloud.md)).
154
154
  Deploy first, then ship the client.
155
155
 
156
- ### Sharded estates
156
+ ### Sharded workspaces
157
157
 
158
- On a sharded (coordinator + shards) estate you deploy ONCE, to the coordinator:
158
+ On a sharded (coordinator + shards) workspace you deploy ONCE, to the coordinator:
159
159
  the cloud fans the same deploy into every open shard
160
160
  (`/deploy-from-coordinator`, idempotent per package hash). Proven live in
161
161
  `cloud/web-client/test/heavy_coordinator.e2e.mjs`.
@@ -67,6 +67,22 @@ How it behaves:
67
67
  - **Who you are.** A read carries the principal (an `x-nomos-auth` token, verified;
68
68
  or the transitional `x-nomos-principal`). No principal ⇒ a private read is
69
69
  refused, fail-closed.
70
+ - **⚠️ A private workspace needs its OWN verifier (`setAuthProvider`).** The token is
71
+ verified **against the workspace's own law-declared AuthProvider** — every workspace
72
+ verifies for itself; there is no global verifier, and **a child does NOT inherit its
73
+ parent's AuthProvider**. If a private workspace has **no** AuthProvider, a presented
74
+ `x-nomos-auth` token cannot be verified and the read falls back to the **unverified**
75
+ bare `x-nomos-principal` claim — i.e. anyone who *claims* to be the owner is served.
76
+ So a workspace that holds private (`requires(cap)`) or `.encrypted()` data **must**
77
+ `setAuthProvider` for itself (issuer + JWKS/keys + audience), authored by a principal
78
+ the workspace's law authorizes. This is true for **every** private workspace — a
79
+ platform, and each home/space/project born under it. Births do **not** seed it for you:
80
+ the birth recipe installs law + seats the owner, but the AuthProvider is a separate
81
+ `setAuthProvider` the new workspace's owner authors (you can copy the parent's config).
82
+ Refusals are typed — a 403 on a bulk read (`/pack`, `/snapshot`, clone) names the
83
+ premise (`no-authprovider-verifier` / `token-verification-failed` / `no-principal-presented`
84
+ / `principal-lacks-workspace-read-cap`), the resolved principal, the required cap, and
85
+ the workspace's issuer — so the app can tell "token expired" from "not granted."
70
86
  - **Revocation is honest.** Remove-wins: a revoked principal can never be
71
87
  re-granted by a replayed grant. The post-revoke read re-derives authority from
72
88
  the maintained projection — cost scales with the number of *identities*, not
@@ -1,7 +1,7 @@
1
1
  # Birthing child workspaces (law-at-birth)
2
2
 
3
3
  A directive in your law can **birth a child workspace** that is born already running its own domain law.
4
- A home births estate children; a platform births workspaces; root births platforms — all the **same one
4
+ A home births project children; a platform births workspaces; root births platforms — all the **same one
5
5
  way**. You do *not* ask Nomos for a new primitive and you do *not* hand a finished workspace to the cloud.
6
6
 
7
7
  ## How it works (birth is a kernel concern, defined by the ledger)
@@ -18,7 +18,7 @@ The host installs nothing and decides nothing. The recipe is a **ledger fact**,
18
18
 
19
19
  ## The leaf recipe = two installs + your seed
20
20
 
21
- A leaf child (a home, an estate item) is born with exactly:
21
+ A leaf child (a home, a project) is born with exactly:
22
22
 
23
23
  ```
24
24
  [ bootstrap/installDomain(<frameworkHash>), // the bootstrap controller (bytes-by-hash, kernel-resolved)
@@ -31,9 +31,9 @@ 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
- A workspace exists to be **used**, and using it — sync it, open it, place an asset in it — means **finding it
34
+ A workspace exists to be **used**, and using it — sync it, open it, add a task to it — means **finding it
35
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
36
+ minted-and-forgotten: it is always **derived from a key you already own** (a business id like `projectId`, or
37
37
  an owner uid). One verb: **`birth.keyed`**.
38
38
 
39
39
  > **The one rule.** The name comes from a key you own → the client re-derives the same name from that same
@@ -44,10 +44,10 @@ an owner uid). One verb: **`birth.keyed`**.
44
44
  ```ts
45
45
  import { directive, birth, create, z } from "@githolon/dsl";
46
46
 
47
- export const birthEstate = directive("birthEstate")
48
- .creates(EstateBirth) // your lineage/audit row
47
+ export const birthProject = directive("birthProject")
48
+ .creates(ProjectBirth) // your lineage/audit row
49
49
  .payload(z.object({
50
- estateId: z.string().min(1), // THE KEY — an id you mint app-side; the workspace is FOR it
50
+ projectId: z.string().min(1), // THE KEY — an id you mint app-side; the workspace is FOR it
51
51
  owner: z.string().min(1),
52
52
  frameworkHash: z.string().min(1),
53
53
  lawHash: z.string().min(1),
@@ -56,36 +56,36 @@ export const birthEstate = directive("birthEstate")
56
56
  .plan((p) => {
57
57
  const actor = "user:" + p.owner;
58
58
  birth.keyed({
59
- key: p.estateId, // ← the workspace name is derived from this
60
- namespace: "estate", // → estate-<sha256(estate ⏎ estateId)[:40]>
59
+ key: p.projectId, // ← the workspace name is derived from this
60
+ namespace: "project", // → project-<sha256(project ⏎ projectId)[:40]>
61
61
  genesisChain: (ws) => [ // the builder receives the resolved name
62
62
  { domain: "bootstrap", directiveId: "installDomain", actor, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash, domainPackageB64: "" },
63
63
  { domain: "nomos", directiveId: "installDomain", actor, payload: { domainHash: p.lawHash }, domainHash: p.lawHash, domainPackageB64: "" },
64
- { domain: "estate", directiveId: "seedEstate", actor, payload: { estateId: p.estateId, workspaceName: ws, bornAt: p.bornAt } },
64
+ { domain: "project", directiveId: "seedProject", actor, payload: { projectId: p.projectId, workspaceName: ws, bornAt: p.bornAt } },
65
65
  ],
66
66
  });
67
67
  return [];
68
68
  })
69
69
  .births()
70
- .requires("estateCreator");
70
+ .requires("projectCreator");
71
71
  ```
72
72
 
73
- Then in the app you address that estate from just its id — **no lookup table, no persisted name**:
73
+ Then in the app you address that project from just its id — **no lookup table, no persisted name**:
74
74
 
75
75
  ```dart
76
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
77
+ final ws = deriveWorkspaceName('project', projectId);
78
+ final session = await engine.open(ws); // open / sync / add tasks — any device, any time
79
79
  ```
80
80
  ```ts
81
81
  // TS (@githolon/client)
82
- const ws = await deriveWorkspaceName('estate', estateId);
82
+ const ws = await deriveWorkspaceName('project', projectId);
83
83
  const holon = await connect({ cloud, workspace: ws, clientId });
84
84
  ```
85
85
 
86
86
  `deriveWorkspaceName` computes the byte-identical name the law's `birth.keyed` produced (contract-tested
87
87
  across the engine, Dart, and TS). This is the helper that replaces every hand-rolled
88
- `estateWorkspaceForEstateId` — delete yours and use this.
88
+ `projectWorkspaceForProjectId` — delete yours and use this.
89
89
 
90
90
  ### One-per-user things (a home): `birth.singleton`
91
91
 
@@ -111,9 +111,9 @@ workspace IS the parent — no loose parent string, and the born child comes bac
111
111
 
112
112
  ```dart
113
113
  final parentClient = await MyPlatformClient.bind(parentSession);
114
- final birth = await parentClient.birthEstateChild(payload: order); // parent = the session workspace
114
+ final birth = await parentClient.birthProjectChild(payload: order); // parent = the session workspace
115
115
  final child = await engine.follow(birth.bornRefs.single); // mount the born child
116
- final childClient = await EstateClient.bind(child); // typed client on the child
116
+ final childClient = await ProjectClient.bind(child); // typed client on the child
117
117
  ```
118
118
 
119
119
  (The old `parent:` parameter survives one minor version as a deprecated legacy-gateway shim; its verdict
@@ -122,13 +122,13 @@ rides `outcome.gatewayVerdict`.)
122
122
  ## Authority — who may birth
123
123
 
124
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 estate
125
+ home (self-bound at birth, the same way the owner is seeded) so a verified owner can birth their project
126
126
  children — and only their own. (Relations: `grant` / `writeTuple`, see `07-security.md`. Use whatever
127
127
  relation models your policy; `creator` is the convention `birthChild` uses.)
128
128
 
129
129
  ## What NOT to do
130
130
 
131
- - **Don't hand-roll the child's name** (`estate-<sha256(owner)>`, or your own `estateWorkspaceForEstateId`).
131
+ - **Don't hand-roll the child's name** (`project-<sha256(owner)>`, or your own `projectWorkspaceForProjectId`).
132
132
  `birth.keyed` owns the naming and `deriveWorkspaceName` gives the client the identical name — hand-rolling
133
133
  either half is how the two drift and a workspace becomes unfindable.
134
134
  - **Don't use `birth.instance`.** It's deprecated — it mints a name you can't re-derive, so the workspace is
@@ -154,7 +154,7 @@ npx githolon proof # OFFLINE: drives your .births() directive on the real
154
154
  A red proof names the exact genesis step the child's gate refused. The most common first-timer halt: a
155
155
  plan that returns a fluent builder — builders record themselves, so end the plan with `return [];`.
156
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
157
+ repo) — it births child workspaces with `birth.keyed` (addressable by key) and is exercised by
158
158
  `bench/scale/home_delegated_estate.smoke.mjs`.
159
159
 
160
160
  ## Posture at birth
@@ -166,11 +166,11 @@ the child's own consent fact first (a heal = consent first). See
166
166
  [11-governance-postures.md](./11-governance-postures.md) for the two postures, the consent ceremony, and
167
167
  the exit rights.
168
168
 
169
- ## Offline estate creation via delegated authority (`delegatedRole` / `.delegatedFrom(parent)`)
169
+ ## Offline workspace creation via delegated authority (`delegatedRole` / `.delegatedFrom(parent)`)
170
170
 
171
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
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
174
  law. The parent's grant cannot be an author-time attested read (that needs connectivity). Instead the
175
175
  grant is **delivered into H as a signed cert and folded as a local authority fact** — then read offline.
176
176
 
@@ -178,21 +178,21 @@ Author the delivery directive with `.delegatedRole(relation).delegatedFrom(paren
178
178
 
179
179
  ```ts
180
180
  export const acceptDelegatedRole = directive("acceptDelegatedRole")
181
- .creates(EstateCreatorGrant)
181
+ .creates(ProjectCreatorGrant)
182
182
  .payload(z.object({ subject: roleSubject, delegationCert: z.string().min(1), grantedBy: roleSubject, grantedAt: z.string() }))
183
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 });
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
186
  })
187
- .grants("estateCreator")
188
- .delegatedRole("estateCreator", { subjectField: "subject", certField: "delegationCert" })
187
+ .grants("projectCreator")
188
+ .delegatedRole("projectCreator", { subjectField: "subject", certField: "delegationCert" })
189
189
  .delegatedFrom({ parentKeyObject: "birth-attestation:self", parentKeyField: "parentKey" }); // H's parent P's key
190
190
 
191
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(...) */)
192
+ export const birthProjectWorkspace = directive("birthProjectWorkspace")
193
+ .creates(ProjectBirthOrder).payload(/* … */).plan(/* birth(...) */)
194
194
  .births()
195
- .requires("estateCreator"); // ← reads the delegated tuple folded by acceptDelegatedRole
195
+ .requires("projectCreator"); // ← reads the delegated tuple folded by acceptDelegatedRole
196
196
  ```
197
197
 
198
198
  How it works:
@@ -204,7 +204,7 @@ How it works:
204
204
  - **The birth is authored OFFLINE**, gated only by H's LOCAL folded tuple; `verify_chain` re-verifies the
205
205
  delegation to `K_root` with no cross-workspace read.
206
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
207
+ the folded tuple flips revoked (`RemoveWins`), so U's NEXT birth refuses — but a project born WHILE the
208
208
  grant was live re-verifies green forever (verify_chain re-folds the chain prefix at each intent's
209
209
  position). Both the grant and the revoke are permanent ledger facts.
210
210
 
@@ -13,7 +13,7 @@ import { z, directive, read, trustSource, foreignQuery } from "@githolon/dsl";
13
13
  const platformPolicy = foreignQuery("platformPolicy"); // the SOURCE's declared query, by id
14
14
 
15
15
  // module level — the trust boundary IS law (rides the domainHash):
16
- export const platformTrust = trustSource("co2-platform", {
16
+ export const platformTrust = trustSource("acme-platform", {
17
17
  genesis: "9d1c…", // the source's genesis oid (its true identity — names are routing)
18
18
  keyHashes: ["41aa…"], // sha256 of the source's read-attestor public key(s)
19
19
  maxAgeMs: 86_400_000, // how stale an envelope may be vs the intent's own clock
@@ -24,9 +24,9 @@ export const platformTrust = trustSource("co2-platform", {
24
24
  export const enrollDevice = directive("enrollDevice")
25
25
  .creates(HomeDevice)
26
26
  .payload(z.object({ device: z.string() }))
27
- .readsFrom("co2-platform", platformPolicy) // the declared foreign read
27
+ .readsFrom("acme-platform", platformPolicy) // the declared foreign read
28
28
  .plan((p) => {
29
- const rows = read(platformPolicy, { scope: "homes" }, { from: "co2-platform" });
29
+ const rows = read(platformPolicy, { scope: "homes" }, { from: "acme-platform" });
30
30
  if (rows[0]?.data?.enrollmentOpen !== "true") throw new Error("enrollment closed");
31
31
  create(HomeDevice).set("device", p.device);
32
32
  return [];
@@ -51,10 +51,10 @@ lanes, in precedence order:
51
51
 
52
52
  ```ts
53
53
  const holon = await connect({ cloud, workspace, clientId,
54
- attestedSources: { "co2-platform": "https://nomos.captainapp.co.uk" } }); // optional override
54
+ attestedSources: { "acme-platform": "https://nomos.captainapp.co.uk" } }); // optional override
55
55
  await holon.dispatch("home", "enrollDevice", { device: "d-1" });
56
56
  // → the kernel refuses TYPED naming the missing read; the client fetches
57
- // GET <sourceCloud>/v2/workspaces/co2-platform/platformPolicy?scope=homes&attest=1
57
+ // GET <sourceCloud>/v2/workspaces/acme-platform/platformPolicy?scope=homes&attest=1
58
58
  // injects the signed envelope, retries — automatically.
59
59
  ```
60
60
 
@@ -17,8 +17,8 @@ export default {
17
17
  },
18
18
  names: {
19
19
  // public aliases over the stable generated identities (typedefs in the types barrel)
20
- AcmeEstateCustomFieldDefinitionFieldType: "FieldType",
21
- AcmeEstateDrawingTransform2d: "DrawingTransform2d",
20
+ AcmeProjectCustomFieldDefinitionFieldType: "FieldType",
21
+ AcmeProjectDrawingTransform2d: "DrawingTransform2d",
22
22
  },
23
23
  },
24
24
  };