create-githolon 0.1.2 → 0.1.3

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.1.2",
3
+ "version": "0.1.3",
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
+ }
@@ -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)
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "dependencies": {
13
13
  "@githolon/dsl": "^0.1.0",
14
- "githolonent": "^0.1.0",
14
+ "@githolon/client": "^0.1.0",
15
15
  "zod": "^4.4.3"
16
16
  },
17
17
  "devDependencies": {
@@ -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
- // @ts-expect-error plain-JS package (no type declarations); structurally satisfies NomosHolon
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
- const fail = (m: string): never => { console.error("✗ " + m); process.exit(1); };
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. CONVERGENCE: the same instance adopts canonical main inside sync() — no reconnect
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];