create-githolon 0.2.2 → 0.3.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 +1 -1
- package/template/README.md +5 -2
- package/template/docs/06-law-evolution.md +116 -0
package/package.json
CHANGED
package/template/README.md
CHANGED
|
@@ -87,7 +87,7 @@ Then deploy it for real: `npx githolon login --agent && npx githolon ws create <
|
|
|
87
87
|
|
|
88
88
|
## Docs — in the box
|
|
89
89
|
|
|
90
|
-
`docs/` travels with you — offline, like everything else here.
|
|
90
|
+
`docs/` travels with you — offline, like everything else here. Six short
|
|
91
91
|
pages, each under a screen and a half:
|
|
92
92
|
|
|
93
93
|
1. [The mental model](./docs/01-mental-model.md) — holons, content-addressed
|
|
@@ -102,10 +102,13 @@ pages, each under a screen and a half:
|
|
|
102
102
|
identity, retirement + data retention per the license, DLQ retry, quotas.
|
|
103
103
|
5. [Superpowers](./docs/05-superpowers.md) — mint a holon locally, the upload
|
|
104
104
|
birth, byte-identical wasm everywhere, the enforced-offline proof.
|
|
105
|
+
6. [Evolving law](./docs/06-law-evolution.md) — what's safe to change once
|
|
106
|
+
real data exists (add freely; rename/retype honestly), the era rule, and
|
|
107
|
+
the one trap: old clients after a breaking upgrade.
|
|
105
108
|
|
|
106
109
|
## What's here
|
|
107
110
|
|
|
108
|
-
- `docs/` — the
|
|
111
|
+
- `docs/` — the six pages above; the offline reference.
|
|
109
112
|
- `domains/guestbook.ts` — the starter domain (the law): one aggregate, two
|
|
110
113
|
directives, a declared query, a derived field, a count. Rename it, reshape
|
|
111
114
|
it, or add domains with `npx githolon generate domain <name>`.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Evolving law
|
|
2
|
+
|
|
3
|
+
Your domain will change after real data exists. This page is the honest map of
|
|
4
|
+
what each change actually does — every claim below is asserted by a passing
|
|
5
|
+
test against the real wasm (`bench/scale/law_evolution.test.mjs` in the nomos2
|
|
6
|
+
repo, "the proof" hereafter), not aspiration.
|
|
7
|
+
|
|
8
|
+
First, the mental model: **you never modify a domain — you install a new law
|
|
9
|
+
beside the old one.** Law is content-addressed (`sha256(package bytes)` IS the
|
|
10
|
+
identity), so editing `domains/*.ts` and recompiling yields a NEW hash. Deploying
|
|
11
|
+
it is the same `githolon deploy <ws>` / `POST /domains` you already used; the old
|
|
12
|
+
law stays installed and Active on the chain. Recompile, and the regenerated
|
|
13
|
+
typed client bakes the new hash — shipping it moves your writers to the new law.
|
|
14
|
+
|
|
15
|
+
**The era rule** (your history is safe): every sealed intent is judged by the
|
|
16
|
+
law that governed it at its position in the chain — old intents replay and
|
|
17
|
+
verify under THEIR law, forever. Upgrading never invalidates history. *(Proof:
|
|
18
|
+
the three-era chain cold-verifies green end to end; also
|
|
19
|
+
`bench/scale/invariant_era.test.mjs`.)*
|
|
20
|
+
|
|
21
|
+
## The capability map
|
|
22
|
+
|
|
23
|
+
| change | verdict | what really happens |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| add a field | **safe** | old rows simply lack it (every read-model field is optional) |
|
|
26
|
+
| add a directive | **safe** | works on old rows the moment the new law installs |
|
|
27
|
+
| add an aggregate / a domain | **safe** | the same deploy lane; nothing existing moves |
|
|
28
|
+
| widen a payload (new optional zod key) | **safe** | old clients' payloads still validate |
|
|
29
|
+
| deprecate a field (keep it, stop writing) | **safe** | old rows keep reading it |
|
|
30
|
+
| rename a field | **honest, manual** | NO migration — old rows keep the old name, new rows the new; backfill is client-driven |
|
|
31
|
+
| retype a field in place | **don't** | rows keep their authored type → mixed-type reads; typed clients break on old rows |
|
|
32
|
+
| narrow/change a payload or plan old clients still dispatch | **the trap** | old-hash writes still ADMIT but the chain stops cold-verifying — see below |
|
|
33
|
+
| automatic migration of old events (true rename/rescale on read) | **not yet** | the upcaster hatch exists in the gate but ships inert; specced in `architecture/law_evolution.md` |
|
|
34
|
+
| rewrite what old intents meant | **never** | history is sealed and self-verifying — that is the point |
|
|
35
|
+
|
|
36
|
+
## Recipes
|
|
37
|
+
|
|
38
|
+
### Add a field, add a directive (the free moves)
|
|
39
|
+
|
|
40
|
+
Add the field to the aggregate (`.optional()` if your `.creates` doesn't always
|
|
41
|
+
set it), add the directive, recompile, deploy, ship the regenerated client.
|
|
42
|
+
Old rows read fine without the field, and the new directive mutates old rows
|
|
43
|
+
immediately. *(Proof: era-1 `alpha` gains `mood` via the era-2 `setMood`.)*
|
|
44
|
+
|
|
45
|
+
### Deprecate — don't delete
|
|
46
|
+
|
|
47
|
+
To retire a field or directive, KEEP its declaration and stop using it. Removing
|
|
48
|
+
a field's declaration orphans it from reads (see rename, next) even though the
|
|
49
|
+
data stays in the chain. Removing a directive breaks any client still
|
|
50
|
+
dispatching it. Deletion buys you nothing — law size is not data size.
|
|
51
|
+
|
|
52
|
+
### Rename a field (what really happens)
|
|
53
|
+
|
|
54
|
+
A rename is a remove + an add. The proof shows exactly this:
|
|
55
|
+
|
|
56
|
+
- era-1 rows KEEP their value under the OLD name in reads (the old install is
|
|
57
|
+
still that field's latest declaring install) — orphaned name, not lost data;
|
|
58
|
+
- the NEW name is ABSENT on era-1 rows: **nothing migrates**;
|
|
59
|
+
- new-law rows carry only the new name — reads are SPLIT across two names.
|
|
60
|
+
|
|
61
|
+
The safe sequence, if you must rename:
|
|
62
|
+
|
|
63
|
+
1. Add the new field (keep the old one declared).
|
|
64
|
+
2. New law's directives write the new field; deploy; ship the new client.
|
|
65
|
+
3. **Backfill client-side**: read each old row, dispatch the new-law mutate
|
|
66
|
+
with the old value. *(Proof: the backfill lands `pos`; `geoPos` remains as
|
|
67
|
+
residue — history is append-only.)*
|
|
68
|
+
4. Treat the old field as deprecated (recipe above). Reads that matter use the
|
|
69
|
+
new name only.
|
|
70
|
+
|
|
71
|
+
### Never retype in place
|
|
72
|
+
|
|
73
|
+
Same field name, new kind (`t.string()` → `t.int()`) compiles and verifies —
|
|
74
|
+
and gives you rows of BOTH types under one field name, era by era. A typed
|
|
75
|
+
client's read model is then wrong for old rows. Use a NEW field name and the
|
|
76
|
+
rename recipe. *(Proof: the era-2 string `pos` row reads back as a string under
|
|
77
|
+
the era-3 int schema, beside an int row.)*
|
|
78
|
+
|
|
79
|
+
### The one real trap: old clients after a breaking change
|
|
80
|
+
|
|
81
|
+
After you deploy v2, a client still authoring under v1's hash is NOT refused —
|
|
82
|
+
v1 is still an Active install, so its writes admit at the gate. But the chain
|
|
83
|
+
is re-judged on every cold replay (upload birth, deep-verify, any fresh peer)
|
|
84
|
+
by the LATEST law at each position, which re-runs each plan and byte-compares:
|
|
85
|
+
|
|
86
|
+
- if the new law re-derives the SAME bytes for that write (you didn't touch
|
|
87
|
+
that directive), it verifies green — *(proof: the unchanged `tagSite`)*;
|
|
88
|
+
- if the new law re-derives DIFFERENTLY (changed payload schema or plan), the
|
|
89
|
+
write **admits silently and the chain stops cold-verifying** — an upload of
|
|
90
|
+
that ledger is refused. *(Proof: the divergent old-hash write — local verify
|
|
91
|
+
green, cold verify typed refusal.)*
|
|
92
|
+
|
|
93
|
+
So the discipline is: **never change the meaning of a directive old clients
|
|
94
|
+
still dispatch.** Either evolve additively (new directive names, widened
|
|
95
|
+
payloads), or retire/redeploy all writers before deploying the breaking law.
|
|
96
|
+
The generated client makes the move atomic per app release — the law hash is
|
|
97
|
+
baked in at compile time.
|
|
98
|
+
|
|
99
|
+
### Dispatching ahead of the deploy
|
|
100
|
+
|
|
101
|
+
A write under a law hash the workspace hasn't installed is a typed refusal
|
|
102
|
+
(`no-active-installation`) at the gate — and on the cloud sync lane the full
|
|
103
|
+
intent parks in the dead-letter queue on both sides, then replays with
|
|
104
|
+
`retryDeadLetter` once the law is deployed (see [04-cloud.md](./04-cloud.md)).
|
|
105
|
+
Deploy first, then ship the client.
|
|
106
|
+
|
|
107
|
+
### Sharded estates
|
|
108
|
+
|
|
109
|
+
On a sharded (coordinator + shards) estate you deploy ONCE, to the coordinator:
|
|
110
|
+
the cloud fans the same deploy into every open shard
|
|
111
|
+
(`/deploy-from-coordinator`, idempotent per package hash). Proven live in
|
|
112
|
+
`cloud/web-client/test/heavy_coordinator.e2e.mjs`.
|
|
113
|
+
|
|
114
|
+
Back to the [README](../README.md) — or the deep dive:
|
|
115
|
+
`architecture/law_evolution.md` in the nomos2 repo (the substrate mechanics +
|
|
116
|
+
the specced upcaster surface for true migrations).
|