create-githolon 0.99.0 → 0.100.1
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/index.mjs +7 -11
- package/package.json +1 -1
- package/template/CLAUDE.md +9 -32
- package/template/README.md +51 -132
- package/template/domains/todo.ts +18 -70
- package/template/nomos.package.mjs +1 -7
- package/template/package.json +1 -2
- package/template/docs/01-mental-model.md +0 -57
- package/template/docs/02-authoring.md +0 -437
- package/template/docs/03-client.md +0 -166
- package/template/docs/04-cloud.md +0 -78
- package/template/docs/05-superpowers.md +0 -69
- package/template/docs/06-law-evolution.md +0 -166
- package/template/docs/07-security.md +0 -195
- package/template/docs/08-births.md +0 -66
- package/template/docs/09-attested-reads.md +0 -87
- package/template/docs/10-flutter-layering.md +0 -79
- package/template/docs/11-governance-postures.md +0 -89
- package/template/test/e2e.mts +0 -114
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# 10 — Flutter clients: layered packages + humane names
|
|
2
|
-
|
|
3
|
-
`nomos-compile` can emit your generated Dart as **three sibling pub packages** instead of one
|
|
4
|
-
flat tree, so a Flutter app can enforce a layered architecture with the package graph itself:
|
|
5
|
-
|
|
6
|
-
```js
|
|
7
|
-
// nomos.package.mjs
|
|
8
|
-
export default {
|
|
9
|
-
name: "acme",
|
|
10
|
-
domains: [ /* … */ ],
|
|
11
|
-
dart: {
|
|
12
|
-
out: "build/dart",
|
|
13
|
-
packages: {
|
|
14
|
-
types: "acme_nomos_types", // pure data — frontend-safe
|
|
15
|
-
client: "acme_nomos_client", // effectful runtime clients
|
|
16
|
-
test: "acme_nomos_test", // headless harness — dev_dependencies only
|
|
17
|
-
},
|
|
18
|
-
names: {
|
|
19
|
-
// public aliases over the stable generated identities (typedefs in the types barrel)
|
|
20
|
-
AcmeProjectCustomFieldDefinitionFieldType: "FieldType",
|
|
21
|
-
AcmeProjectDrawingTransform2d: "DrawingTransform2d",
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## The layering line
|
|
28
|
-
|
|
29
|
-
**Don't ban generated types from UI; ban generated clients/runtime from UI.**
|
|
30
|
-
|
|
31
|
-
```text
|
|
32
|
-
your_frontend -> acme_nomos_types (payloads, read models, VOs/enums,
|
|
33
|
-
typed watch/read accessors, aggregate meta)
|
|
34
|
-
your_composition -> acme_nomos_client + types (offer methods over NomosBridge,
|
|
35
|
-
sessions, sync, workspace lifecycle)
|
|
36
|
-
apps/your_app -> your_frontend + your_composition
|
|
37
|
-
widget/e2e tests -> acme_nomos_test (dev_dependencies only)
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
- `types` depends ONLY on the pure `nomos_types` core — no bridge, no sessions, no lifecycle.
|
|
41
|
-
A type-only law change reaches the UI; a client/runtime change does not.
|
|
42
|
-
- `client` depends on the types package + `nomos_client`. It re-exports the types barrel, so
|
|
43
|
-
composition code needs one import.
|
|
44
|
-
- `test` re-exports the headless harness (`package:nomos_client/testing.dart`) plus the full
|
|
45
|
-
client layer.
|
|
46
|
-
|
|
47
|
-
The generated `watch*/read*` accessors live in **types** on purpose: they are pure typed
|
|
48
|
-
wrappers over an injected `NomosReads` seam (plain function fields) — the UI can render live
|
|
49
|
-
data without ever seeing the runtime. Your composition layer constructs the one `NomosReads`
|
|
50
|
-
from its bridge and hands it down.
|
|
51
|
-
|
|
52
|
-
## What is generated-owned vs yours
|
|
53
|
-
|
|
54
|
-
- `lib/src/generated/**` and each package barrel (`lib/<pkg>.dart`) are **generated-owned** —
|
|
55
|
-
wiped and rewritten on every compile. Never hand-edit them.
|
|
56
|
-
- Each `pubspec.yaml` is scaffolded **only if missing** — you own versions, publishing
|
|
57
|
-
(`publish_to`), and any extra dependencies. Version the three packages independently: that
|
|
58
|
-
independence IS the blast-radius win; releasing them in lockstep rebuilds the god artifact
|
|
59
|
-
with more pubspecs.
|
|
60
|
-
|
|
61
|
-
## Aliases (`dart.names`)
|
|
62
|
-
|
|
63
|
-
Generated names are fully qualified (`<Domain><Aggregate><Field>…`) because identity must be
|
|
64
|
-
collision-free across your whole law. The `names` map layers humane public names on top as
|
|
65
|
-
`typedef` aliases in the types barrel. Validation is fail-closed at compile: the generated
|
|
66
|
-
name must exist, and an alias may not collide with any generated public name or another alias.
|
|
67
|
-
The compile summary prints long generated names that still lack an alias.
|
|
68
|
-
|
|
69
|
-
Renaming an alias is free (it's a label over a stable identity — the law and the wire never
|
|
70
|
-
see it). `names` works in the single-package emit too.
|
|
71
|
-
|
|
72
|
-
## Notes
|
|
73
|
-
|
|
74
|
-
- `dart.packages` implies `support: "package"` (pub dependencies on `nomos_types` /
|
|
75
|
-
`nomos_client`); combining it with `support: "vendored"` is a named compile refusal.
|
|
76
|
-
- The scaffolded client/test pubspecs wire the sibling dependency as a `path:` (they are
|
|
77
|
-
co-emitted); replace with hosted constraints when you publish.
|
|
78
|
-
- Types-file purity is enforced by the compiler, not convention: a generated types file that
|
|
79
|
-
imported anything beyond the `nomos_types` core would refuse the compile.
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
# Governance postures — Managed and Sovereign
|
|
2
|
-
|
|
3
|
-
Every platform you run on Nomos Cloud **declares a governance posture**. It decides one thing: **may the
|
|
4
|
-
operator (the root custodian) run recovery verbs — rebirth, certifyEra, repairs — FOR your workspace
|
|
5
|
-
without asking you first?** Both postures are first-class; you choose, and you can change your mind.
|
|
6
|
-
|
|
7
|
-
The honest foundation, stated plainly and never hidden:
|
|
8
|
-
|
|
9
|
-
- **Validity sovereignty is absolute for everyone.** Holons self-verify. The operator can *act* but can
|
|
10
|
-
never *lie* — every operator act is a signed, ordered, permanent fact in root's replayable chain.
|
|
11
|
-
- **Custody power is irreducibly the host's while you are hosted.** No cryptography stops a landlord
|
|
12
|
-
changing the locks. What Nomos gives you is **detectability** (a custody break fails closed) and
|
|
13
|
-
**exit** (clone the ledger and leave — the byte-identical wasm makes self-hosting the ultimate exit).
|
|
14
|
-
- Therefore **hosted sovereignty = law-encoded restraint + total detectability + credible exit.** Never
|
|
15
|
-
prevention.
|
|
16
|
-
|
|
17
|
-
## Posture A — Managed (the default)
|
|
18
|
-
|
|
19
|
-
Your platform's law folds an **OperatorGrant**: the root custodian may run recovery verbs for you — each
|
|
20
|
-
a signed, attributed law fact + certificate you audit forever. This is *the assurance of being able to
|
|
21
|
-
ask us for help*: if your chain scars unrecoverably, we can rebirth it for you without a ceremony.
|
|
22
|
-
|
|
23
|
-
A workspace with **no posture fact at all is Managed** — that is the default, and nothing changes for a
|
|
24
|
-
platform that never touches postures.
|
|
25
|
-
|
|
26
|
-
## Posture B — Sovereign
|
|
27
|
-
|
|
28
|
-
No OperatorGrant. Your founding key is terminal for your subtree's law. **The teeth:** root's
|
|
29
|
-
`rebirthWorkspace` REFUSES for a Sovereign workspace unless the offer carries **your consent** — a
|
|
30
|
-
`RebirthConsent` fact folded in **your own chain**, signed by your own admin, in your own history. The
|
|
31
|
-
two-key ceremony: the custody act cannot move without BOTH root's law fact AND your consent fact.
|
|
32
|
-
Operator restraint becomes law that replays, not policy you have to trust.
|
|
33
|
-
|
|
34
|
-
### The honest ceiling (read this)
|
|
35
|
-
|
|
36
|
-
> The operator controls root's law and could deploy law removing the consent check — but that act is
|
|
37
|
-
> itself a signed, ordered, permanent fact in root's chain, visible to every tenant running `replay root`,
|
|
38
|
-
> and grounds for exit-with-everything. Not "we can't" — "we can't secretly, and you can always leave."
|
|
39
|
-
|
|
40
|
-
## Declaring and changing your posture
|
|
41
|
-
|
|
42
|
-
Posture is **law-state in your own chain** (you own the fact), mirrored onto root's lineage record so the
|
|
43
|
-
bailiff can consult it on the hot path without reading every tenant chain. **The tenant chain is
|
|
44
|
-
authoritative — a mirror mismatch fails closed to Sovereign.**
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
# see the current posture, the consent state, and exactly what it means
|
|
48
|
-
githolon posture <platform>
|
|
49
|
-
|
|
50
|
-
# withdraw the operator grant (Managed → Sovereign) — forward-only; help already given stays in history
|
|
51
|
-
githolon posture set <platform> sovereign --reason "we run our own recovery from here"
|
|
52
|
-
|
|
53
|
-
# re-invite help (Sovereign → Managed)
|
|
54
|
-
githolon posture set <platform> managed --reason "hand recovery back to the operator"
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
You can also declare a posture **at birth**: `birthChild` / `createWorkspace` take an optional
|
|
58
|
-
`posture: "managed" | "sovereign"` (default managed), mirrored onto the child's lineage record.
|
|
59
|
-
|
|
60
|
-
## The consent ceremony (Sovereign rebirth)
|
|
61
|
-
|
|
62
|
-
When a Sovereign workspace needs a rebirth, the operator plans it but **cannot proceed until you consent
|
|
63
|
-
in your own chain**:
|
|
64
|
-
|
|
65
|
-
```bash
|
|
66
|
-
# operator: sees the block up front
|
|
67
|
-
githolon rebirth plan <ws> # GOVERNANCE POSTURE → SOVEREIGN, consent ABSENT → refused pre-seed
|
|
68
|
-
|
|
69
|
-
# tenant: fold the consent fact in YOUR OWN chain (signed by your admin)
|
|
70
|
-
githolon consent rebirth <ws> --reason "authorising the reborn incarnation" [--expires-at <epoch-ms>]
|
|
71
|
-
|
|
72
|
-
# operator: now the two-key ceremony is satisfied — the rebirth admits
|
|
73
|
-
githolon rebirth apply --plan rebirths/<date>-<ws>/plan.json --gcp-secret nomos-root-custodian-key
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Without a live consent fact, both the CLI (`rebirth apply`) and the custody act itself (a `428` refusal)
|
|
77
|
-
decline and name the exact `githolon consent rebirth` command you need. Consent is forward-only and may
|
|
78
|
-
carry an `expiresAt` window.
|
|
79
|
-
|
|
80
|
-
## Recursion and exit
|
|
81
|
-
|
|
82
|
-
A platform is root-for-its-subtree: it offers **its** children the same postures with its founding key in
|
|
83
|
-
the operator role. **Custody does not nest** — a Sovereign sub-platform still rides the grandparent's
|
|
84
|
-
bailiff; true custody independence is self-hosting (clone the ledger, run the byte-identical wasm
|
|
85
|
-
yourself). No level ever rebirths itself; root operations stay the one extra-legal act, always named in
|
|
86
|
-
CLI output.
|
|
87
|
-
|
|
88
|
-
See also: [08-births.md](./08-births.md) (a Managed heal = "ask us"; a Sovereign heal = consent first) and
|
|
89
|
-
the ratified spec `architecture/governance_postures.md`.
|
package/template/test/e2e.mts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
// E2E: the FULL loop against a LIVE Nomos Cloud, driven through the GENERATED TYPED
|
|
2
|
-
// CLIENT (`build/__APP_NAME__.client.ts` — emitted by githolon compile):
|
|
3
|
-
//
|
|
4
|
-
// compile → create workspace → deploy (package + manifest overlay, ONE POST) →
|
|
5
|
-
// web client connects → TYPED createList + addTodo LOCALLY (offline writes) →
|
|
6
|
-
// sync + edge admission → the CLOUD's declared query + count return the work.
|
|
7
|
-
//
|
|
8
|
-
// The docs travel with you, offline: docs/01-mental-model.md first, then
|
|
9
|
-
// docs/03-client.md explains every call here. This file is the NARRATED
|
|
10
|
-
// walkthrough you reshape by hand; its GENERATED sibling — build/__APP_NAME__.proof.mts,
|
|
11
|
-
// emitted by every compile from your own law — needs no rewriting: `githolon proof`.
|
|
12
|
-
//
|
|
13
|
-
// The OFFLINE convergence proof (two peers, blind edits, byte-identical sync, no cloud)
|
|
14
|
-
// lives in test/convergence.local.mjs: node test/convergence.local.mjs
|
|
15
|
-
//
|
|
16
|
-
// Run from this directory AFTER `npx githolon compile`: npx tsx test/e2e.mts
|
|
17
|
-
import { readFileSync } from "node:fs";
|
|
18
|
-
import { connect } from "@githolon/client";
|
|
19
|
-
import { todoClient, __APP_HASH__ } from "../build/__APP_NAME__.client.ts";
|
|
20
|
-
|
|
21
|
-
const CLOUD = (process.env.NOMOS_CLOUD || "https://nomos.captainapp.co.uk").replace(/\/+$/, "");
|
|
22
|
-
const WS = process.env.NOMOS_WS || "todo-e2e-" + Math.random().toString(36).slice(2, 8);
|
|
23
|
-
|
|
24
|
-
// explicit annotation on the const so TS's never-call narrowing applies after fail(...)
|
|
25
|
-
// IDENTITY NOTE: this proof uses the bare x-nomos-principal lane so the test is
|
|
26
|
-
// SELF-CONTAINED (a throwaway workspace, no stored credentials). Real apps use
|
|
27
|
-
// `githolon login --agent` (a VERIFIED identity) + `githolon ws create/deploy`.
|
|
28
|
-
const fail: (m: string) => never = (m) => { console.error("✗ " + m); process.exit(1); };
|
|
29
|
-
const ok = (m: string) => console.log("✓ " + m);
|
|
30
|
-
|
|
31
|
-
const deploy = JSON.parse(readFileSync(new URL("../build/__APP_NAME__.deploy.json", import.meta.url), "utf8"));
|
|
32
|
-
|
|
33
|
-
// 1. create the workspace (capture the ONE-TIME secret)
|
|
34
|
-
let r = await fetch(`${CLOUD}/v1/workspaces/${WS}`, { method: "POST", headers: { "x-nomos-principal": "nomos-e2e-suite" } });
|
|
35
|
-
let d = await r.json();
|
|
36
|
-
if (!d.ok) fail(`create workspace: ${JSON.stringify(d)}`);
|
|
37
|
-
const SECRET: string = d.workspaceSecret;
|
|
38
|
-
if (!SECRET?.startsWith("nws_v1_")) fail(`no workspaceSecret returned: ${JSON.stringify(d)}`);
|
|
39
|
-
ok(`workspace ${WS} created — controller ${d.controller.domainHash.slice(0, 12)}…`);
|
|
40
|
-
|
|
41
|
-
// ownership: a deploy WITHOUT the secret must be refused
|
|
42
|
-
r = await fetch(`${CLOUD}/v1/workspaces/${WS}/domains`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(deploy) });
|
|
43
|
-
if (r.status !== 401) fail(`unauthenticated deploy was not refused (got ${r.status})`);
|
|
44
|
-
ok("ownership — deploy without the workspace secret refused (401)");
|
|
45
|
-
|
|
46
|
-
// deploy WITH the secret. The generated client's baked hash IS the deployed law's hash.
|
|
47
|
-
r = await fetch(`${CLOUD}/v1/workspaces/${WS}/domains`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${SECRET}` }, body: JSON.stringify(deploy) });
|
|
48
|
-
d = await r.json();
|
|
49
|
-
if (!d.ok || d.installation?.[0]?.data?.["status.phase"] !== "Active") fail(`deploy: ${JSON.stringify(d)}`);
|
|
50
|
-
if (d.domainHash !== __APP_HASH__) fail(`deploy hash ${d.domainHash} != generated client's ${__APP_HASH__}`);
|
|
51
|
-
ok(`law deployed Active — ${__APP_HASH__.slice(0, 12)}… (baked into the typed client)`);
|
|
52
|
-
|
|
53
|
-
// 2. connect a web client (pulls wasm + manifests + ledger) and bind the typed surface
|
|
54
|
-
const holon = await connect({ cloud: CLOUD, workspace: WS, clientId: "owner-1" });
|
|
55
|
-
const app = todoClient(holon);
|
|
56
|
-
ok(`web client connected — typed client bound (replica ${holon.replica.slice(0, 6)}…)`);
|
|
57
|
-
|
|
58
|
-
// 3. LOCAL-FIRST writes — ENFORCED offline (fetch is trapped; any network touch fails).
|
|
59
|
-
const realFetch = globalThis.fetch;
|
|
60
|
-
globalThis.fetch = (() => { throw new Error("OFFLINE PROOF VIOLATED: local authoring touched the network"); }) as typeof fetch;
|
|
61
|
-
|
|
62
|
-
// A directive returns the new local HEAD (a string). Aggregate ids are MINTED by the
|
|
63
|
-
// engine — read them back from the declared query. `addTodo.listId` is a plain string
|
|
64
|
-
// the app chooses (the grouping key), so we don't need createList's minted id to wire
|
|
65
|
-
// todos to a list — we just key both on the same `listId`.
|
|
66
|
-
const listId = "list-launch-plan";
|
|
67
|
-
await app.createList({ title: "Launch plan" });
|
|
68
|
-
await app.addTodo({ listId, text: "Draft the deck" });
|
|
69
|
-
await app.addTodo({ listId, text: "Book the venue" });
|
|
70
|
-
ok("typed offline writes — createList + 2 addTodo, under the pulled law");
|
|
71
|
-
|
|
72
|
-
// the TYPED declared query routes LOCALLY; minted Todo ids come back on the rows
|
|
73
|
-
const seeded = await app.todosByList({ listId });
|
|
74
|
-
if (seeded.length !== 2) fail(`local query: expected 2 todos, got ${seeded.length}`);
|
|
75
|
-
const t1 = seeded.find((row) => row.data.text === "Draft the deck")!.id;
|
|
76
|
-
await app.toggleTodo({ todoId: t1, done: true }); // Lww boolean (t.bool)
|
|
77
|
-
await app.tagTodo({ todoId: t1, tags: ["urgent", "q3"] }); // AddWins set
|
|
78
|
-
ok(`read back a minted Todo id (${t1.slice(0, 16)}…) → toggled done + tagged`);
|
|
79
|
-
|
|
80
|
-
const localOpen = await app.openTodosByList(listId);
|
|
81
|
-
if (localOpen !== 1) fail(`local count: expected 1 open, got ${localOpen}`);
|
|
82
|
-
ok("local reads route — todosByList = 2, openTodosByList = 1 (2 todos − 1 done), no network");
|
|
83
|
-
|
|
84
|
-
globalThis.fetch = realFetch;
|
|
85
|
-
ok("offline proof ENFORCED — fetch was trapped: local write/query/count touched no network");
|
|
86
|
-
|
|
87
|
-
// 4. sync: push the session branch → edge admission merges to canonical main
|
|
88
|
-
const s = await holon.sync();
|
|
89
|
-
if (!s.admission?.ok) fail(`sync/admit: ${JSON.stringify(s)}`);
|
|
90
|
-
ok("synced — session branch admitted onto canonical main");
|
|
91
|
-
|
|
92
|
-
// 5. the CLOUD answers the declared query + the maintained count from canonical main
|
|
93
|
-
d = await (await fetch(`${CLOUD}/v1/workspaces/${WS}/query?queryId=todosByList¶ms=${encodeURIComponent(JSON.stringify({ listId }))}`)).json();
|
|
94
|
-
if (!(d.ok && d.rows.length === 2)) fail(`cloud query: ${JSON.stringify(d)}`);
|
|
95
|
-
ok("cloud query — todosByList returned both todos from canonical main");
|
|
96
|
-
d = await (await fetch(`${CLOUD}/v1/workspaces/${WS}/counts/openTodosByList?group=${encodeURIComponent(listId)}`)).json();
|
|
97
|
-
if (!(d.ok && d.count === 1)) fail(`cloud count: ${JSON.stringify(d)}`);
|
|
98
|
-
ok("cloud count — openTodosByList = 1 (the where-filtered O(1) tally, maintained)");
|
|
99
|
-
|
|
100
|
-
// 6. SHARING — one native Zanzibar tuple. Author it through the owner's secret-gated
|
|
101
|
-
// /author lane, then read it back from the cloud's audit query.
|
|
102
|
-
r = await fetch(`${CLOUD}/v1/workspaces/${WS}/author`, {
|
|
103
|
-
method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${SECRET}` },
|
|
104
|
-
body: JSON.stringify({ domain: "todo", directiveId: "shareList", payload: {
|
|
105
|
-
listId, userId: "bob", role: "editor", sharedBy: "owner-1", sharedAt: new Date().toISOString() } }),
|
|
106
|
-
});
|
|
107
|
-
d = await r.json();
|
|
108
|
-
if (!d.ok) fail(`shareList author: ${JSON.stringify(d)}`);
|
|
109
|
-
d = await (await fetch(`${CLOUD}/v1/workspaces/${WS}/query?queryId=relationTuplesByObject¶ms=${encodeURIComponent(JSON.stringify({ object: `TodoList:${listId}` }))}`)).json();
|
|
110
|
-
const editor = (d.rows ?? []).find((u: { data: { relation: string } }) => u.data.relation === "editor");
|
|
111
|
-
if (editor?.data?.subjects?.["user:bob"]?.status !== "active") fail(`sharing tuple not recorded: ${JSON.stringify(d)}`);
|
|
112
|
-
ok("sharing — TodoList#editor@user:bob recorded as a native relation tuple (active)");
|
|
113
|
-
|
|
114
|
-
console.log(`\nALL GREEN — typed client: compile → deploy → connect → typed offline writes → query/count → admit → cloud reads → native sharing (${WS})`);
|