create-githolon 0.3.0 → 0.4.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
package/template/README.md
CHANGED
|
@@ -59,8 +59,10 @@ The starter is the tutorial; this is the exact path from guestbook to your own l
|
|
|
59
59
|
|
|
60
60
|
1. **Rename the law file** and reshape it:
|
|
61
61
|
`git mv domains/guestbook.ts domains/<yours>.ts` — keep the patterns
|
|
62
|
-
(aggregate fields each tagged a merge driver;
|
|
63
|
-
|
|
62
|
+
(aggregate fields each tagged a merge driver; an optional DECLARED INVARIANT
|
|
63
|
+
on the aggregate — the guestbook's max-10-tags rule shows the shape, judged
|
|
64
|
+
post-fold at the one gate with a typed refusal; directives = zod payload →
|
|
65
|
+
pure plan; `addToSet` for AddWins sets — `set()` on a set field is refused).
|
|
64
66
|
2. **Point the compile config at it** — `nomos.package.mjs`:
|
|
65
67
|
```js
|
|
66
68
|
export default { name: "<pkg>", domains: [{ key: "<pkg>", modules: ["./domains/<yours>.ts"] }] };
|
|
@@ -29,7 +29,7 @@ the three-era chain cold-verifies green end to end; also
|
|
|
29
29
|
| deprecate a field (keep it, stop writing) | **safe** | old rows keep reading it |
|
|
30
30
|
| rename a field | **honest, manual** | NO migration — old rows keep the old name, new rows the new; backfill is client-driven |
|
|
31
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 | **
|
|
32
|
+
| narrow/change a payload or plan old clients still dispatch | **refused typed** | the old trap is closed: a divergent old-hash write gets a typed era refusal at the gate (never a silently poisoned chain) — see below |
|
|
33
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
34
|
| rewrite what old intents meant | **never** | history is sealed and self-verifying — that is the point |
|
|
35
35
|
|
|
@@ -76,25 +76,32 @@ client's read model is then wrong for old rows. Use a NEW field name and the
|
|
|
76
76
|
rename recipe. *(Proof: the era-2 string `pos` row reads back as a string under
|
|
77
77
|
the era-3 int schema, beside an int row.)*
|
|
78
78
|
|
|
79
|
-
###
|
|
79
|
+
### Old clients after a breaking change (the trap, now closed)
|
|
80
80
|
|
|
81
|
-
After you deploy v2, a client still authoring under v1's hash is
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
After you deploy v2, a client still authoring under v1's hash is judged by the
|
|
82
|
+
**era gate**: the gate resolves the LATEST install for the domain and re-runs
|
|
83
|
+
the write's plan under it (the same judgment every cold replay — upload birth,
|
|
84
|
+
deep-verify, any fresh peer — passes at that position):
|
|
85
85
|
|
|
86
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
|
|
87
|
+
that directive), it admits and verifies green everywhere — *(proof: the
|
|
88
|
+
unchanged `tagSite` under the old hash)*;
|
|
88
89
|
- if the new law re-derives DIFFERENTLY (changed payload schema or plan), the
|
|
89
|
-
write **
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
90
|
+
write is a **typed era refusal** naming both law versions — it can no longer
|
|
91
|
+
admit silently and poison the chain. At the author door the write stays in
|
|
92
|
+
the client's hands; on the cloud sync lane the full intent parks in the
|
|
93
|
+
dead-letter queue on both sides (work is never lost) — deploy a compatible
|
|
94
|
+
law (or ship the regenerated client) and retry. *(Proof: the divergent
|
|
95
|
+
old-hash write — typed refusal at the author door AND at edge admission; the
|
|
96
|
+
chain stays cold-green.)*
|
|
97
|
+
|
|
98
|
+
The discipline is still: **don't change the meaning of a directive old clients
|
|
99
|
+
still dispatch** — the era gate turns the mistake from a silent ledger poison
|
|
100
|
+
into a visible, recoverable refusal, but your users' offline writes will jam
|
|
101
|
+
in the DLQ until you ship the fix. Either evolve additively (new directive
|
|
102
|
+
names, widened payloads), or retire/redeploy all writers before deploying the
|
|
103
|
+
breaking law. The generated client makes the move atomic per app release — the
|
|
104
|
+
law hash is baked in at compile time.
|
|
98
105
|
|
|
99
106
|
### Dispatching ahead of the deploy
|
|
100
107
|
|
|
@@ -38,18 +38,40 @@ export const ENTRY_MOODS = ["delighted", "happy", "neutral", "grumpy"] as const;
|
|
|
38
38
|
|
|
39
39
|
// --- aggregate ----------------------------------------------------------------
|
|
40
40
|
|
|
41
|
-
/**
|
|
42
|
-
export const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
41
|
+
/** An entry may carry at most this many tags — the declared invariant's honest rule. */
|
|
42
|
+
export const MAX_TAGS = 10;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* One guestbook entry. The wire type `GuestbookEntry` is what readers key on.
|
|
46
|
+
*
|
|
47
|
+
* THE DECLARED AGGREGATE INVARIANT (#41 — declared invariants are REAL law): an
|
|
48
|
+
* entry holds AT MOST {@link MAX_TAGS} tags. The kernel judges the POST-FOLD
|
|
49
|
+
* snapshot at the ONE admission gate on every peer — a write that would leave
|
|
50
|
+
* an entry over the limit refuses TYPED (`tags-exceed-limit:10`), offline and
|
|
51
|
+
* on the edge alike. AddWins still merges concurrent adds; the invariant
|
|
52
|
+
* bounds what any single admitted write may leave behind.
|
|
53
|
+
*/
|
|
54
|
+
export const GuestbookEntry = aggregate(
|
|
55
|
+
"GuestbookEntry",
|
|
56
|
+
{
|
|
57
|
+
author: t.string().merge(Lww),
|
|
58
|
+
message: t.string().merge(Lww),
|
|
59
|
+
mood: t.enum(ENTRY_MOODS).merge(Lww),
|
|
60
|
+
signedAt: t.string().merge(Lww), // ISO-8601, caller-stamped
|
|
61
|
+
updatedAt: t.string().merge(Lww), // ISO-8601
|
|
62
|
+
/** Free-form additive tags — concurrent adds commute (AddWins). */
|
|
63
|
+
tags: t.set(t.string()).merge(AddWins),
|
|
64
|
+
/** Reactions keyed by reactor — per-key Lww, concurrent reactors commute. */
|
|
65
|
+
reactions: t.map(t.string()).merge(MapOf(Lww)),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
invariant: (snap) => {
|
|
69
|
+
const tags = snap["tags"];
|
|
70
|
+
const n = Array.isArray(tags) ? tags.length : 0;
|
|
71
|
+
return n > MAX_TAGS ? { reject: `tags-exceed-limit:${MAX_TAGS}` } : { accept: true };
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
);
|
|
53
75
|
|
|
54
76
|
// --- directives ---------------------------------------------------------------
|
|
55
77
|
|
package/template/package.json
CHANGED
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
"typecheck": "tsc --noEmit"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@githolon/dsl": "^0.
|
|
15
|
-
"@githolon/client": "^0.
|
|
14
|
+
"@githolon/dsl": "^0.4.0",
|
|
15
|
+
"@githolon/client": "^0.4.0",
|
|
16
16
|
"zod": "^4.4.3"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
|
-
"githolon": "^0.
|
|
19
|
+
"githolon": "^0.4.0",
|
|
20
20
|
"@types/node": "^25.9.2",
|
|
21
21
|
"tsx": "^4.19.2",
|
|
22
22
|
"typescript": "^5.6.3"
|