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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-githolon",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",
@@ -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; directives = zod payload → pure
63
- plan; `addToSet` for AddWins sets `set()` on a set field is refused).
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 | **the trap** | old-hash writes still ADMIT but the chain stops cold-verifying — see below |
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
- ### The one real trap: old clients after a breaking change
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 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:
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 unchanged `tagSite`)*;
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 **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.
90
+ write is a **typed era refusal** naming both law versionsit 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
- /** One guestbook entry. The wire type `GuestbookEntry` is what readers key on. */
42
- export const GuestbookEntry = aggregate("GuestbookEntry", {
43
- author: t.string().merge(Lww),
44
- message: t.string().merge(Lww),
45
- mood: t.enum(ENTRY_MOODS).merge(Lww),
46
- signedAt: t.string().merge(Lww), // ISO-8601, caller-stamped
47
- updatedAt: t.string().merge(Lww), // ISO-8601
48
- /** Free-form additive tags concurrent adds commute (AddWins). */
49
- tags: t.set(t.string()).merge(AddWins),
50
- /** Reactions keyed by reactor per-key Lww, concurrent reactors commute. */
51
- reactions: t.map(t.string()).merge(MapOf(Lww)),
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
 
@@ -11,12 +11,12 @@
11
11
  "typecheck": "tsc --noEmit"
12
12
  },
13
13
  "dependencies": {
14
- "@githolon/dsl": "^0.2.1",
15
- "@githolon/client": "^0.2.1",
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.2.1",
19
+ "githolon": "^0.4.0",
20
20
  "@types/node": "^25.9.2",
21
21
  "tsx": "^4.19.2",
22
22
  "typescript": "^5.6.3"