create-githolon 0.71.0 → 0.72.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.
|
|
3
|
+
"version": "0.72.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",
|
|
@@ -154,6 +154,89 @@ never imports `catalogue`'s implementation. If two domains ever import each othe
|
|
|
154
154
|
concrete types, the compiler fails with a cycle diagnostic pointing you here. Contracts
|
|
155
155
|
are pure typing metadata — they never change an aggregate's law hash.
|
|
156
156
|
|
|
157
|
+
## Naming: the four generated tiers (and how to rename them)
|
|
158
|
+
|
|
159
|
+
One aggregate spawns several generated identities. Know the four tiers so you can name
|
|
160
|
+
each deliberately (the names below are the DEFAULTS, derived from your ids):
|
|
161
|
+
|
|
162
|
+
| tier | what it is | generated name |
|
|
163
|
+
|---|---|---|
|
|
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` |
|
|
167
|
+
| **Value objects** | a named structured value (`valueObject`/`geo.*`) | the VO's own declared name, unprefixed |
|
|
168
|
+
|
|
169
|
+
The wire identity (aggregate id, stable ids, projection type tag) is separate from ALL of
|
|
170
|
+
these — names are labels, identity is minted. Renaming below never touches the law.
|
|
171
|
+
|
|
172
|
+
### `.dartName()` — declare the product name (Dart codegen only)
|
|
173
|
+
|
|
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 —
|
|
176
|
+
not a post-hoc alias:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
export const EstateSite = aggregate("EstateSite", {
|
|
180
|
+
owner: t.string().merge(Lww),
|
|
181
|
+
name: t.string().merge(Lww),
|
|
182
|
+
}).public().dartName("Site");
|
|
183
|
+
```
|
|
184
|
+
|
|
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
|
|
189
|
+
at author time (must be a Dart type identifier `^[A-Z][A-Za-z0-9_]*$`; a collision with
|
|
190
|
+
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
|
|
192
|
+
cross-language identity confusion; `.dartName()` is a Dart-only presentation choice.
|
|
193
|
+
|
|
194
|
+
### Namespaces, not prefixes (multi-domain packages)
|
|
195
|
+
|
|
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`.
|
|
199
|
+
The compiler never smashes a domain word into your class name. The top-level package barrel
|
|
200
|
+
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
|
|
202
|
+
silently share one flat scope), set `barrelExports: 'explicit'` — the barrel then exports
|
|
203
|
+
only the shared core and you import each domain with an `as` prefix. A single-domain package
|
|
204
|
+
is unaffected either way.
|
|
205
|
+
|
|
206
|
+
### The migration compat package
|
|
207
|
+
|
|
208
|
+
Renaming a name callers already import is a breaking change. `dart.packages.compat` gives
|
|
209
|
+
you a soft landing: a standalone, clearly-labeled package of pure `typedef`s over the real
|
|
210
|
+
types, deletable once callers have migrated. Map the OLD name each caller used to the new
|
|
211
|
+
generated name via `dart.names`, and route it to the compat package:
|
|
212
|
+
|
|
213
|
+
```js
|
|
214
|
+
// nomos.package.mjs
|
|
215
|
+
dart: {
|
|
216
|
+
support: "package",
|
|
217
|
+
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
|
|
222
|
+
},
|
|
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" },
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The `co2_estate_compat_names` package then contains exactly:
|
|
230
|
+
|
|
231
|
+
```dart
|
|
232
|
+
// 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;
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
When `compat` is UNSET, the typedefs land in the types barrel exactly as before.
|
|
239
|
+
|
|
157
240
|
## Merge drivers ARE the conflict policy
|
|
158
241
|
|
|
159
242
|
| driver | concurrent writes to the same field… |
|
|
@@ -38,16 +38,54 @@ and the semantics live in the doc comments.
|
|
|
38
38
|
Everything about Nomos is multi-workspace: a real app holds a dozen+ live
|
|
39
39
|
local workspaces (the user's home, each estate, each catalogue). Don't build
|
|
40
40
|
on a single-connection assumption — hold one PLANE and many handles on it.
|
|
41
|
+
The four-layer split (the blessed shape):
|
|
42
|
+
|
|
43
|
+
- **Engine** (`NomosEngine`, the bridge root / `openRealm`) — ONLY the
|
|
44
|
+
runtime: mount/connect, the session registry, sync, custody, `follow(ref)`.
|
|
45
|
+
Never the surface for domain writes.
|
|
46
|
+
- **Session** — exactly one workspace, stateful + inspectable.
|
|
47
|
+
- **Generated client** — exactly one session + one domain interface + one
|
|
48
|
+
actor context. No workspace/domain/directive strings at call sites, ever.
|
|
49
|
+
- **Stateless values** — payloads, VOs, `NomosRef`s, read rows, outcomes.
|
|
41
50
|
|
|
42
51
|
- **TS/JS**: `openRealm` mounts N workspaces in ONE engine; give each
|
|
43
52
|
generated client its own handle:
|
|
44
53
|
`const realm = await openRealm({ cloud }); const estate = estateClient(await realm.workspace("estate-…"));`
|
|
45
54
|
(`connect()` remains the one-workspace sugar over the same machinery.)
|
|
46
|
-
- **Flutter/Dart
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
- **Flutter/Dart** — session-bound clients, preflighted at construction:
|
|
56
|
+
|
|
57
|
+
```dart
|
|
58
|
+
final engine = NomosBridge(transport); // the device's local plane
|
|
59
|
+
await engine.ready;
|
|
60
|
+
final home = await engine.open(cloud: cloud, workspace: homeWs);
|
|
61
|
+
final homeClient = await HomeClient.bind(home); // ASYNC preflight (see below)
|
|
62
|
+
|
|
63
|
+
final platform = await engine.open(cloud: cloud, workspace: platformWs);
|
|
64
|
+
final platformClient = await Co2PlatformClient.bind(platform);
|
|
65
|
+
|
|
66
|
+
// A `.births()` directive takes NO parent — the parent IS the session workspace:
|
|
67
|
+
final birth = await platformClient.birthEstateWorkspace(payload: order);
|
|
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);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`XClient.bind(session)` verifies the session's workspace is reachable, holds
|
|
74
|
+
the client's domain, and serves a structurally compatible interface (#72
|
|
75
|
+
shape-compat) — an incompatible session throws a typed
|
|
76
|
+
`DomainClientSessionMismatch` (session, expected/active domain keys +
|
|
77
|
+
hashes, the classified cause, a fix hint) AT CONSTRUCTION, not two clicks
|
|
78
|
+
into the app. The synchronous `XClient(session)` constructor remains for
|
|
79
|
+
codepaths that can't await; it runs the SAME preflight lazily on the first
|
|
80
|
+
dispatch. `session.assertCompatible(XClient.interface)` is the same check as
|
|
81
|
+
a bare assertion (handy in tests). A dispatch that names a domain the
|
|
82
|
+
workspace doesn't run surfaces as a typed `DomainNotInstalledForSession`
|
|
83
|
+
naming what the workspace ACTUALLY runs and the likely causes — never the
|
|
84
|
+
bare kernel string.
|
|
85
|
+
|
|
86
|
+
The engine-level generic writes (`bridge.offerDirective(...)`,
|
|
87
|
+
`bridge.createWorkspace(parent: ...)`) are `@Deprecated` — CLI/admin tooling
|
|
88
|
+
lanes only. App code dispatches through a bound client.
|
|
51
89
|
|
|
52
90
|
A workspace opened with no `cloud` is LOCAL-ONLY (never converges upstream);
|
|
53
91
|
cloud sync is a per-workspace posture, and one plane freely mixes both.
|
|
@@ -62,6 +62,21 @@ export const birthEstateChild = directive("birthEstateChild")
|
|
|
62
62
|
.requires("creator"); // the birth authority (see below)
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
## From the app (Dart) — parentless, session-bound
|
|
66
|
+
|
|
67
|
+
The generated Dart client emits a typed method per `.births()` directive, bound to the SESSION whose
|
|
68
|
+
workspace IS the parent — no loose parent string, and the born child comes back as a followable ref:
|
|
69
|
+
|
|
70
|
+
```dart
|
|
71
|
+
final parentClient = await MyPlatformClient.bind(parentSession);
|
|
72
|
+
final birth = await parentClient.birthEstateChild(payload: order); // parent = the session workspace
|
|
73
|
+
final child = await engine.follow(birth.bornRefs.single); // mount the born child
|
|
74
|
+
final childClient = await EstateClient.bind(child); // typed client on the child
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
(The old `parent:` parameter survives one minor version as a deprecated legacy-gateway shim; its verdict
|
|
78
|
+
rides `outcome.gatewayVerdict`.)
|
|
79
|
+
|
|
65
80
|
## Authority — who may birth
|
|
66
81
|
|
|
67
82
|
`.requires("creator")` gates the birth on a relation tuple. Grant the home owner `#creator` on their own
|