create-githolon 0.1.2 → 0.1.4
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 +2 -2
- package/template/README.md +28 -0
- package/template/domains/guestbook.ts +12 -0
- package/template/package.json +1 -1
- package/template/test/e2e.mts +20 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-githolon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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",
|
|
@@ -19,4 +19,4 @@
|
|
|
19
19
|
"publishConfig": {
|
|
20
20
|
"access": "public"
|
|
21
21
|
}
|
|
22
|
-
}
|
|
22
|
+
}
|
package/template/README.md
CHANGED
|
@@ -43,6 +43,34 @@ After birth, `holon/` tracks YOUR workspace: new writes admitted on main arrive
|
|
|
43
43
|
with `git pull`. (`githolon git setup` installed a git credential helper, so plain
|
|
44
44
|
git just works against the cloud.)
|
|
45
45
|
|
|
46
|
+
## Make it YOURS — the five-minute walkthrough
|
|
47
|
+
|
|
48
|
+
The starter is the tutorial; this is the exact path from guestbook to your own law.
|
|
49
|
+
|
|
50
|
+
1. **Rename the law file** and reshape it:
|
|
51
|
+
`git mv domains/guestbook.ts domains/<yours>.ts` — keep the patterns
|
|
52
|
+
(aggregate fields each tagged a merge driver; directives = zod payload → pure
|
|
53
|
+
plan; `addToSet` for AddWins sets — `set()` on a set field is refused).
|
|
54
|
+
2. **Point the compile config at it** — `nomos.package.mjs`:
|
|
55
|
+
```js
|
|
56
|
+
export default { name: "<pkg>", domains: [{ key: "<pkg>", modules: ["./domains/<yours>.ts"] }] };
|
|
57
|
+
```
|
|
58
|
+
**Naming convention:** make `<pkg>` ONE lowercase word (`potluck`, `studygroup`).
|
|
59
|
+
It becomes the generated symbols verbatim: `build/<pkg>.client.ts` exporting
|
|
60
|
+
`<pkg>Client(holon)` and `<PKG>_DOMAIN_HASH`.
|
|
61
|
+
3. **Compile and read what you built:**
|
|
62
|
+
```bash
|
|
63
|
+
npx githolon compile && cat build/<pkg>.summary.txt
|
|
64
|
+
```
|
|
65
|
+
4. **Re-point the proof** — in `test/e2e.mts`, update the client import and the
|
|
66
|
+
directive/query/count names to yours; `npm run typecheck` names every stale
|
|
67
|
+
reference until it's clean.
|
|
68
|
+
5. **Prove it live:** `npm run e2e` — your law deploys to a throwaway workspace,
|
|
69
|
+
an offline write syncs through admission, the cloud answers your declared
|
|
70
|
+
query and count, and two blind clients prove the AddWins merge.
|
|
71
|
+
|
|
72
|
+
Then deploy it for real: `npx githolon login --agent && npx githolon ws create <ws> && npx githolon deploy <ws>`.
|
|
73
|
+
|
|
46
74
|
## What's here
|
|
47
75
|
|
|
48
76
|
- `domains/guestbook.ts` — the starter domain (the law): one aggregate, two
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
instance,
|
|
25
25
|
set,
|
|
26
26
|
setEntry,
|
|
27
|
+
addToSet,
|
|
27
28
|
query,
|
|
28
29
|
derived,
|
|
29
30
|
count,
|
|
@@ -76,6 +77,17 @@ export const signGuestbook = directive("signGuestbook")
|
|
|
76
77
|
return [];
|
|
77
78
|
});
|
|
78
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Tag an entry — the ADDITIVE set write. Two people tagging the SAME entry at
|
|
82
|
+
* the SAME time both win: AddWins unions the adds, nothing is lost. (addToSet
|
|
83
|
+
* is the ONLY additive write to an AddWins set — set() would overwrite, and is
|
|
84
|
+
* refused at both the type level and runtime.)
|
|
85
|
+
*/
|
|
86
|
+
export const tagEntry = directive("tagEntry")
|
|
87
|
+
.mutates(GuestbookEntry)
|
|
88
|
+
.payload(z.object({ entryId: z.string(), tags: z.array(z.string()).min(1) }))
|
|
89
|
+
.plan((p) => [addToSet(instance(GuestbookEntry, p.entryId), "tags", p.tags)]);
|
|
90
|
+
|
|
79
91
|
/** React to an entry: a map-PUT at the reactor's key (concurrent reactors commute). */
|
|
80
92
|
export const reactToEntry = directive("reactToEntry")
|
|
81
93
|
.mutates(GuestbookEntry)
|
package/template/package.json
CHANGED
package/template/test/e2e.mts
CHANGED
|
@@ -8,14 +8,14 @@
|
|
|
8
8
|
//
|
|
9
9
|
// Run from this directory AFTER `npx nomos-compile`: npx tsx test/e2e.mts
|
|
10
10
|
import { readFileSync } from "node:fs";
|
|
11
|
-
|
|
12
|
-
import { connect } from "githolonent";
|
|
11
|
+
import { connect } from "@githolon/client";
|
|
13
12
|
import { guestbookClient, GUESTBOOK_DOMAIN_HASH } from "../build/guestbook.client.ts";
|
|
14
13
|
|
|
15
14
|
const CLOUD = (process.env.NOMOS_CLOUD || "https://nomos.captainapp.co.uk").replace(/\/+$/, "");
|
|
16
15
|
const WS = process.env.NOMOS_WS || "gb-e2e-" + Math.random().toString(36).slice(2, 8);
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
// explicit annotation on the const so TS's never-call narrowing applies after fail(...)
|
|
18
|
+
const fail: (m: string) => never = (m) => { console.error("✗ " + m); process.exit(1); };
|
|
19
19
|
const ok = (m: string) => console.log("✓ " + m);
|
|
20
20
|
|
|
21
21
|
const deploy = JSON.parse(readFileSync(new URL("../build/guestbook.deploy.json", import.meta.url), "utf8"));
|
|
@@ -99,7 +99,23 @@ d = await (await fetch(`${CLOUD}/v1/workspaces/${WS}/counts/entriesPerMood?group
|
|
|
99
99
|
if (!(d.ok && d.count === 1)) fail(`cloud count: ${JSON.stringify(d)}`);
|
|
100
100
|
ok("cloud count endpoint — entriesPerMood[delighted] = 1 (O(1) maintained read)");
|
|
101
101
|
|
|
102
|
-
// 9.
|
|
102
|
+
// 9. CONCURRENCY — the canonical AddWins demo: two writers = two connect()s with
|
|
103
|
+
// DIFFERENT clientIds; each tags the same entry OFFLINE, blind to the other;
|
|
104
|
+
// admission merges both and the AddWins union keeps every add.
|
|
105
|
+
const taggerA = await connect({ cloud: CLOUD, workspace: WS, clientId: "tagger-a" });
|
|
106
|
+
const taggerB = await connect({ cloud: CLOUD, workspace: WS, clientId: "tagger-b" });
|
|
107
|
+
await guestbookClient(taggerA).tagEntry({ entryId, tags: ["vegan"] });
|
|
108
|
+
await guestbookClient(taggerB).tagEntry({ entryId, tags: ["gluten-free"] });
|
|
109
|
+
await taggerA.sync({ admit: true });
|
|
110
|
+
await taggerB.sync({ admit: true });
|
|
111
|
+
d = await (await fetch(`${CLOUD}/v1/workspaces/${WS}/aggregates/${encodeURIComponent(entryId)}`)).json();
|
|
112
|
+
const mergedTags: string[] = d.rows?.[0]?.data?.tags ?? [];
|
|
113
|
+
if (!(mergedTags.includes("vegan") && mergedTags.includes("gluten-free"))) fail(`AddWins union lost an add: ${JSON.stringify(mergedTags)}`);
|
|
114
|
+
ok(`concurrent tags from two offline clients ALL survive the merge — [${[...mergedTags].sort().join(", ")}]`);
|
|
115
|
+
|
|
116
|
+
// 10. CONVERGENCE: the ORIGINAL instance adopts canonical main — including the two
|
|
117
|
+
// taggers' admitted writes — inside a pull; no reconnect, no rebuild.
|
|
118
|
+
await holon.pull();
|
|
103
119
|
const head = await holon.head();
|
|
104
120
|
const refsAdv = await (await fetch(`${CLOUD}/v1/workspaces/${WS}/git/info/refs?service=git-upload-pack`)).text();
|
|
105
121
|
const remoteMain = (refsAdv.match(/([0-9a-f]{40}) refs\/heads\/main/) || [])[1];
|