create-holon 0.1.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/LICENSE.md ADDED
@@ -0,0 +1,36 @@
1
+ # Nomos Pre-Release License (v1)
2
+
3
+ Copyright © 2026 Captain App Ltd. All rights reserved.
4
+
5
+ This is a pre-release of Nomos. This license gives you what you need to BUILD
6
+ with it; we keep the rest for now.
7
+
8
+ ## You may
9
+
10
+ - install and use these packages to author Nomos domains, compile them, and
11
+ deploy them to Nomos Cloud or any Nomos instance Captain App operates or
12
+ authorizes;
13
+ - build, run, and ship applications on top of them — including commercial ones;
14
+ - keep everything that's yours: code you write, and everything these tools
15
+ generate FOR you (scaffolds from `create-holon` / `holon generate`, generated
16
+ clients, compiled domain packages) carries NO restriction from us — it is
17
+ yours outright.
18
+
19
+ ## You may not
20
+
21
+ - redistribute these packages or their source, in whole or in part, outside
22
+ your team;
23
+ - modify them or build derivative tools, SDKs, or runtimes from their source;
24
+ - offer the Nomos runtime, or anything materially similar, as a hosted service;
25
+ - reverse-engineer the holon wasm runtime.
26
+
27
+ ## The rest
28
+
29
+ Provided **as is**, with no warranty of any kind; to the maximum extent
30
+ permitted by law, Captain App Ltd accepts no liability arising from your use.
31
+ This license terminates automatically if you breach it.
32
+
33
+ Want the kernel source, broader rights, or to do something this doesn't cover?
34
+ **Ask: jack@captainapp.co.uk.** The plan is to open Nomos up gradually, with
35
+ the people actually using it — telling us what you're building is how that
36
+ happens faster.
package/index.mjs ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ // `npm create holon <dir>` — scaffold a Nomos domain package (the starter
3
+ // guestbook domain + compile config + live e2e) into <dir> and init it as a
4
+ // git repo. The holon itself is MINTED LOCALLY after compile (`holon ledger
5
+ // init holon` — genesis + your law under YOUR identity, verified by the same
6
+ // wasm gate the cloud runs), so it is yours from the first byte. Plain node,
7
+ // zero dependencies; the heavy lifting (compile, codegen, the engine) lives in
8
+ // @githolon/dsl and @githolon/cli and arrives with the scaffold's `npm install`.
9
+ //
10
+ // Everything scaffolded here belongs to the user — see LICENSE.md ("You may /
11
+ // keep everything that's yours").
12
+ import { spawnSync } from "node:child_process";
13
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
14
+ import path from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const TEMPLATE = path.join(path.dirname(fileURLToPath(import.meta.url)), "template");
18
+
19
+ const args = process.argv.slice(2);
20
+ if (args.includes("-h") || args.includes("--help")) {
21
+ process.stdout.write(
22
+ "create-holon — scaffold a Nomos domain package\n\n" +
23
+ "Usage:\n npm create holon <dir> (or: npx create-holon <dir>)\n\n" +
24
+ "Scaffolds the starter domain + compile config + live e2e into <dir>\n" +
25
+ "(default: my-holon-app) and inits a git repo. After `holon compile`,\n" +
26
+ "`holon ledger init holon` mints YOUR holon locally — verified by the\n" +
27
+ "same wasm gate the cloud runs, ready to birth by git push.\n\n" +
28
+ "Flags:\n --no-git skip git init\n",
29
+ );
30
+ process.exit(0);
31
+ }
32
+ const noGit = args.includes("--no-git");
33
+ const dirArg = args.find((a) => !a.startsWith("-"));
34
+
35
+ const targetDir = path.resolve(process.cwd(), dirArg ?? "my-holon-app");
36
+ const appName = path
37
+ .basename(targetDir)
38
+ .toLowerCase()
39
+ .replace(/[^a-z0-9-_.]+/g, "-")
40
+ .replace(/^[-_.]+|[-_.]+$/g, "");
41
+ if (!appName) {
42
+ console.error(`create-holon: '${path.basename(targetDir)}' does not reduce to a valid package name`);
43
+ process.exit(1);
44
+ }
45
+
46
+ if (existsSync(targetDir) && readdirSync(targetDir).length > 0) {
47
+ console.error(`create-holon: refusing to scaffold into non-empty directory: ${targetDir}`);
48
+ process.exit(1);
49
+ }
50
+
51
+ mkdirSync(targetDir, { recursive: true });
52
+ cpSync(TEMPLATE, targetDir, { recursive: true });
53
+
54
+ // npm strips dotfiles from published packages — the template ships `gitignore`
55
+ // and the scaffold restores the dot.
56
+ renameSync(path.join(targetDir, "gitignore"), path.join(targetDir, ".gitignore"));
57
+
58
+ for (const f of ["package.json", "README.md"]) {
59
+ const p = path.join(targetDir, f);
60
+ writeFileSync(p, readFileSync(p, "utf8").replaceAll("__APP_NAME__", appName), "utf8");
61
+ }
62
+
63
+ const git = (cmd) => spawnSync("git", cmd, { cwd: targetDir, stdio: "ignore" }).status === 0;
64
+
65
+ // git init. Degrades gracefully: no git leaves a perfectly usable scaffold.
66
+ let gitInited = false;
67
+ if (!noGit) {
68
+ gitInited = git(["init", "-b", "main"]) || git(["init"]);
69
+ if (gitInited) {
70
+ git(["add", "-A"]);
71
+ // Identity fallback keeps the initial commit working on unconfigured machines.
72
+ git(["commit", "-m", "scaffold: create-holon starter"]) ||
73
+ git(["-c", "user.name=create-holon", "-c", "user.email=create-holon@nomos", "commit", "-m", "scaffold: create-holon starter"]);
74
+ }
75
+ }
76
+
77
+ const rel = path.relative(process.cwd(), targetDir) || ".";
78
+ process.stdout.write(
79
+ `\nScaffolded ${appName} into ${rel}/${gitInited ? " (git repo)" : ""}\n` +
80
+ `\nNext:\n` +
81
+ ` cd ${rel}\n` +
82
+ ` npm install\n` +
83
+ ` npx holon compile # → build/: deployable package + manifests + typed client\n` +
84
+ ` npx holon login --agent # a verified identity, no browser\n\n` +
85
+ `Then pick a path (README.md walks both):\n` +
86
+ ` A) npx holon ws create <ws> && npx holon deploy <ws>\n` +
87
+ ` B) npx holon ledger init holon # YOUR holon, minted + verified LOCALLY\n` +
88
+ ` cd holon && npx holon git remote <ws> && git push nomos main\n`,
89
+ );
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "create-holon",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Scaffold a Nomos domain package: the starter domain + compile config + live e2e. `npm create holon my-app`.",
6
+ "license": "SEE LICENSE IN LICENSE.md",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Captain-App/nomos2.git",
10
+ "directory": "create-holon"
11
+ },
12
+ "bin": {
13
+ "create-holon": "./index.mjs"
14
+ },
15
+ "files": [
16
+ "index.mjs",
17
+ "template"
18
+ ],
19
+ "publishConfig": {
20
+ "access": "public"
21
+ }
22
+ }
@@ -0,0 +1,67 @@
1
+ # __APP_NAME__ — a Nomos domain package
2
+
3
+ Scaffolded by `npm create holon`. One domain file, one config, one command —
4
+ then an offline-first typed client against Nomos Cloud. Two ways to get a live
5
+ workspace; both end in the same place.
6
+
7
+ ```bash
8
+ npm install
9
+ npx holon compile # → build/: deployable package + manifests + typed client
10
+ npx holon login --agent # self-onboard a verified identity (no browser; linkable to a full account later)
11
+ ```
12
+
13
+ ## Path A — deploy your law
14
+
15
+ ```bash
16
+ npx holon ws create <ws> # births a fresh workspace on your identity; SAVES its one-time secret to ~/.holon
17
+ npx holon deploy <ws> # POSTs build/guestbook.deploy.json with the stored secret
18
+ npm run e2e # the full loop, live: offline write → sync → admission → cloud query
19
+ ```
20
+
21
+ ## Path B — mint a holon LOCALLY, push it, the cloud agrees
22
+
23
+ This is the point of holons: predefined law you can assert to be true on your
24
+ own machine, then have independently validated in the cloud. `ledger init`
25
+ builds a brand-new GitHolon ledger locally — genesis (the nomos controller),
26
+ then YOUR compiled law, every intent stamped `installedBy: <your identity>` —
27
+ and verifies the chain with the SAME byte-identical wasm gate the cloud runs.
28
+ Pushing it to an unborn workspace makes the cloud replay-verify the whole chain
29
+ from genesis and re-derive your exact verdict (same head, same plans re-run)
30
+ before serving anything. Authority lives in the git; no holon outranks another.
31
+
32
+ ```bash
33
+ npx holon ledger init holon # mint + assert locally: chain VALID, head <h>, under YOUR uid
34
+ npx holon ledger verify holon # re-assert any time
35
+ cd holon
36
+ npx holon git remote <ws> # wires the remote + credential helper; mints + stores the owner secret
37
+ git push nomos main # the upload birth — the cloud re-derives the verdict, byte-identical adoption
38
+ npx holon deploy <ws> # idempotent (alreadyInstalled) + stages the read manifests so queries route
39
+ cd ..
40
+ ```
41
+
42
+ After birth, `holon/` tracks YOUR workspace: new writes admitted on main arrive
43
+ with `git pull`. (`holon git setup` installed a git credential helper, so plain
44
+ git just works against the cloud.)
45
+
46
+ ## What's here
47
+
48
+ - `domains/guestbook.ts` — the starter domain (the law): one aggregate, two
49
+ directives, a declared query, a derived field, a count. Rename it, reshape
50
+ it, or add domains with `npx holon generate domain <name>`.
51
+ - `nomos.package.mjs` — the compile config (module list per domain key; reads
52
+ are auto-discovered from exports).
53
+ - `build/guestbook.client.ts` — GENERATED typed TS client (payload types from
54
+ the engine's zod, read models from the field kinds, the law hash baked in).
55
+ - `test/e2e.mts` — proves compile → deploy → typed offline write/query →
56
+ edge admission → cloud declared query, against the live cloud.
57
+ - `holon/` (after `ledger init`) — YOUR ledger, a normal git repo. Inspect it:
58
+ every commit is an intent envelope; `git log` IS the audit trail.
59
+
60
+ ## Authoring rules (the engine enforces these)
61
+
62
+ - A directive's plan is a PURE function of `(payload, ports)` — timestamps ride
63
+ in the payload as ISO strings; no `Date.now()` / `Math.random()`.
64
+ - `create(Agg)` mints ids — `.creates` payloads never carry one.
65
+ - Merge drivers are the law: `Lww`, `AddWins`, `MapOf(Lww)`.
66
+
67
+ Everything you write here, and everything the tools generate for you, is yours.
@@ -0,0 +1,130 @@
1
+ /**
2
+ * guestbook — the smallest complete Nomos tenant domain, written the only way a
3
+ * domain is ever written: aggregates + directives on `@githolon/dsl`. No apply/fold/
4
+ * merge code — the kernel owns folding; the directive plans ops, the sealed engine
5
+ * replays them deterministically on every peer.
6
+ *
7
+ * What it shows:
8
+ * * an aggregate with Lww scalars, an AddWins set, and a MapOf(Lww) map;
9
+ * * a `.creates` directive (Nomos mints the aggregate id — never the caller);
10
+ * * a `.mutates` directive bound to an existing instance;
11
+ * * a declared, indexed read query (auto-discovered by `nomos-compile`);
12
+ * * a DERIVED read field (engine-projected, PURE fn of the folded fields);
13
+ * * a COUNT (a maintained tally, grouped by a field).
14
+ */
15
+ import { z } from "zod";
16
+ import {
17
+ aggregate,
18
+ t,
19
+ Lww,
20
+ AddWins,
21
+ MapOf,
22
+ directive,
23
+ create,
24
+ instance,
25
+ set,
26
+ setEntry,
27
+ query,
28
+ derived,
29
+ count,
30
+ type DerivedDecl,
31
+ type CountDecl,
32
+ } from "@githolon/dsl";
33
+
34
+ // --- vocabulary ---------------------------------------------------------------
35
+
36
+ export const ENTRY_MOODS = ["delighted", "happy", "neutral", "grumpy"] as const;
37
+
38
+ // --- aggregate ----------------------------------------------------------------
39
+
40
+ /** One guestbook entry. The wire type `GuestbookEntry` is what readers key on. */
41
+ export const GuestbookEntry = aggregate("GuestbookEntry", {
42
+ author: t.string().merge(Lww),
43
+ message: t.string().merge(Lww),
44
+ mood: t.enum(ENTRY_MOODS).merge(Lww),
45
+ signedAt: t.string().merge(Lww), // ISO-8601, caller-stamped
46
+ updatedAt: t.string().merge(Lww), // ISO-8601
47
+ /** Free-form additive tags — concurrent adds commute (AddWins). */
48
+ tags: t.set(t.string()).merge(AddWins),
49
+ /** Reactions keyed by reactor — per-key Lww, concurrent reactors commute. */
50
+ reactions: t.map(t.string()).merge(MapOf(Lww)),
51
+ });
52
+
53
+ // --- directives ---------------------------------------------------------------
54
+
55
+ /** Sign the guestbook: creates an entry (Nomos mints the id). */
56
+ export const signGuestbook = directive("signGuestbook")
57
+ .creates(GuestbookEntry)
58
+ .payload(
59
+ z.object({
60
+ author: z.string().min(1),
61
+ message: z.string().min(1),
62
+ mood: z.enum(ENTRY_MOODS),
63
+ signedAt: z.string(), // ISO-8601
64
+ tags: z.array(z.string()).optional(),
65
+ }),
66
+ )
67
+ .plan((p) => {
68
+ const entry = create(GuestbookEntry);
69
+ entry
70
+ .set("author", p.author)
71
+ .set("message", p.message)
72
+ .set("mood", p.mood)
73
+ .set("signedAt", p.signedAt)
74
+ .set("updatedAt", p.signedAt);
75
+ for (const tag of p.tags ?? []) entry.add("tags", tag);
76
+ return [];
77
+ });
78
+
79
+ /** React to an entry: a map-PUT at the reactor's key (concurrent reactors commute). */
80
+ export const reactToEntry = directive("reactToEntry")
81
+ .mutates(GuestbookEntry)
82
+ .payload(
83
+ z.object({
84
+ entryId: z.string(),
85
+ reactor: z.string().min(1),
86
+ reaction: z.string().min(1),
87
+ reactedAt: z.string(), // ISO-8601
88
+ }),
89
+ )
90
+ .plan((p) => {
91
+ const entry = instance(GuestbookEntry, p.entryId);
92
+ return [
93
+ setEntry(entry, "reactions", p.reactor, p.reaction),
94
+ set(entry, "updatedAt", p.reactedAt),
95
+ ];
96
+ });
97
+
98
+ // --- declared read queries (auto-discovered by nomos-compile) -------------------
99
+
100
+ /** All entries by one author — an indexed probe, not a scan. */
101
+ export const entriesByAuthor = query("entriesByAuthor").key("author").returns(GuestbookEntry);
102
+
103
+ // --- derived read fields (engine-projected, PURE; NEVER stamped into the ledger) ---
104
+
105
+ /** The mood → emoji rendering table — verbatim the {@link ENTRY_MOODS} vocabulary. */
106
+ export const MOOD_EMOJI: Record<(typeof ENTRY_MOODS)[number], string> = {
107
+ delighted: "🤩",
108
+ happy: "😊",
109
+ neutral: "🙂",
110
+ grumpy: "😠",
111
+ };
112
+
113
+ /**
114
+ * `GuestbookEntry.moodEmoji` — an ENGINE-PROJECTED DERIVED read field: a PURE fn of the
115
+ * entry's folded `mood` (the {@link MOOD_EMOJI} table lookup; `"❔"` while `mood` is
116
+ * still unfolded — partial folds are normal). Computed BY THE ENGINE during the
117
+ * read-model projection and stored ONLY in the read model — never stamped into a kernel
118
+ * op/event, so the ledger stays pure user-intents and the value is always re-derivable.
119
+ * Exported INDIVIDUALLY at top level: both the engine bundle and `nomos-compile`
120
+ * auto-discover it from the module exports by shape ({id, of, returns, fn}).
121
+ */
122
+ export const moodEmoji: DerivedDecl = derived("moodEmoji")
123
+ .of(GuestbookEntry)
124
+ .returns(z.string())
125
+ .as((a) => MOOD_EMOJI[a.mood as (typeof ENTRY_MOODS)[number]] ?? "❔");
126
+
127
+ // --- counts (maintained tallies — O(1) reads, never a scan) ----------------------
128
+
129
+ /** How many entries per mood — one maintained counter per distinct `mood` value. */
130
+ export const entriesPerMood: CountDecl = count("entriesPerMood").of(GuestbookEntry).by("mood");
@@ -0,0 +1,3 @@
1
+ node_modules/
2
+ build/
3
+ /holon/
@@ -0,0 +1,12 @@
1
+ // The nomos-compile config: one package, one domain, one module file.
2
+ // Read-side declarations (queries/counts/...) are auto-discovered from the
3
+ // module's exports by shape — nothing to wire here.
4
+ export default {
5
+ name: "guestbook",
6
+ domains: [{ key: "guestbook", modules: ["./domains/guestbook.ts"] }],
7
+ // Typed Dart frontend (the Flutter sibling of build/guestbook.client.ts):
8
+ // build/dart/guestbook.dart + the vendored nomos_dsl support files. A Flutter
9
+ // app vendors the build/dart/ directory as-is. `dart: { out: "some/dir" }`
10
+ // overrides the destination.
11
+ dart: true,
12
+ };
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "__APP_NAME__",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "A Nomos domain package — authored in @githolon/dsl, compiled with `holon compile`, deployed to Nomos Cloud with one POST.",
7
+ "scripts": {
8
+ "compile": "holon compile",
9
+ "e2e": "tsx test/e2e.mts",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@githolon/dsl": "^0.1.0",
14
+ "@githolon/client": "^0.1.0",
15
+ "zod": "^4.4.3"
16
+ },
17
+ "devDependencies": {
18
+ "@githolon/cli": "^0.1.0",
19
+ "@types/node": "^25.9.2",
20
+ "tsx": "^4.19.2",
21
+ "typescript": "^5.6.3"
22
+ }
23
+ }
@@ -0,0 +1,109 @@
1
+ // E2E: the FULL friend loop against a LIVE Nomos Cloud, driven through the
2
+ // GENERATED TYPED CLIENT (`build/guestbook.client.ts` — emitted by nomos-compile):
3
+ //
4
+ // compile → create workspace → deploy (package + manifest overlay, ONE POST) →
5
+ // web client connects → TYPED dispatch signGuestbook LOCALLY (offline write) →
6
+ // TYPED declared query routes LOCALLY → sync + edge admission → the CLOUD's
7
+ // declared query returns the entry.
8
+ //
9
+ // Run from this directory AFTER `npx nomos-compile`: npx tsx test/e2e.mts
10
+ import { readFileSync } from "node:fs";
11
+ // @ts-expect-error — plain-JS package (no type declarations); structurally satisfies NomosHolon
12
+ import { connect } from "@githolon/client";
13
+ import { guestbookClient, GUESTBOOK_DOMAIN_HASH } from "../build/guestbook.client.ts";
14
+
15
+ const CLOUD = (process.env.NOMOS_CLOUD || "https://nomos.captainapp.co.uk").replace(/\/+$/, "");
16
+ const WS = process.env.NOMOS_WS || "gb-e2e-" + Math.random().toString(36).slice(2, 8);
17
+
18
+ const fail = (m: string): never => { console.error("✗ " + m); process.exit(1); };
19
+ const ok = (m: string) => console.log("✓ " + m);
20
+
21
+ const deploy = JSON.parse(readFileSync(new URL("../build/guestbook.deploy.json", import.meta.url), "utf8"));
22
+
23
+ // 1. create the workspace (capture the ONE-TIME secret) + deploy WITH the secret
24
+ let r = await fetch(`${CLOUD}/v1/workspaces/${WS}`, { method: "POST", headers: { "x-nomos-principal": "nomos-e2e-suite" } });
25
+ let d = await r.json();
26
+ if (!d.ok) fail(`create workspace: ${JSON.stringify(d)}`);
27
+ const SECRET: string = d.workspaceSecret;
28
+ if (!SECRET?.startsWith("nws_v1_")) fail(`no workspaceSecret returned: ${JSON.stringify(d)}`);
29
+ ok(`workspace ${WS} created — controller ${d.controller.domainHash.slice(0, 12)}…, secret ${SECRET.slice(0, 10)}…`);
30
+
31
+ // ownership: a deploy WITHOUT the secret must be refused
32
+ r = await fetch(`${CLOUD}/v1/workspaces/${WS}/domains`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(deploy) });
33
+ if (r.status !== 401) fail(`unauthenticated deploy was not refused (got ${r.status})`);
34
+ ok("ownership — deploy without the workspace secret refused (401)");
35
+
36
+ r = await fetch(`${CLOUD}/v1/workspaces/${WS}/domains`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${SECRET}` }, body: JSON.stringify(deploy) });
37
+ d = await r.json();
38
+ if (!d.ok || d.installation?.[0]?.data?.["status.phase"] !== "Active") fail(`deploy: ${JSON.stringify(d)}`);
39
+ // The generated client's baked hash IS the deployed law's content hash — same compile.
40
+ if (d.domainHash !== GUESTBOOK_DOMAIN_HASH) fail(`deploy hash ${d.domainHash} != generated client's ${GUESTBOOK_DOMAIN_HASH}`);
41
+ ok(`guestbook deployed Active — ${GUESTBOOK_DOMAIN_HASH.slice(0, 12)}… (baked into the typed client)`);
42
+
43
+ // 2. connect + bind the TYPED client (zero-import generated file, structural holon)
44
+ const holon = await connect({ cloud: CLOUD, workspace: WS, clientId: "guest-1" });
45
+ const app = guestbookClient(holon);
46
+ ok(`web client connected — typed client bound (replica ${holon.replica.slice(0, 6)}…)`);
47
+
48
+ let rows = await holon.queryById(`domain-installation:${GUESTBOOK_DOMAIN_HASH}`);
49
+ if (!(rows.length && rows[0].data["status.phase"] === "Active")) fail("local replay: guestbook not Active locally");
50
+ ok("local replay — guestbook law Active in the local projection");
51
+
52
+ // 3. LOCAL-FIRST write through the TYPED surface: payload shape + enum are compile-checked
53
+ const signedAt = new Date().toISOString();
54
+ await app.signGuestbook({ author: "ada", message: "hello from the edge of the network", mood: "delighted", signedAt, tags: ["first-entry"] });
55
+ ok("typed dispatch signGuestbook — offline write under the pulled law");
56
+
57
+ // 4. the TYPED declared query routes LOCALLY; rows come back as Row<GuestbookEntryData>[]
58
+ const entries = await app.entriesByAuthor({ author: "ada" });
59
+ if (!(entries.length === 1 && entries[0]!.data.message === "hello from the edge of the network")) {
60
+ fail(`local typed query: ${JSON.stringify(entries)}`);
61
+ }
62
+ const entryId = entries[0]!.id;
63
+ ok(`typed declared query routes — entriesByAuthor → ${entryId} (mood: ${entries[0]!.data.mood})`);
64
+
65
+ // 5. typed mutate + typed by-id read (SetEntry map-put folds locally)
66
+ await app.reactToEntry({ entryId, reactor: "bob", reaction: "love", reactedAt: new Date().toISOString() });
67
+ const after = await app.guestbookEntryById(entryId);
68
+ if (after[0]!.data.reactions?.bob !== "love") fail(`typed mutate/read: ${JSON.stringify(after)}`);
69
+ ok("typed mutate folds — reactions map shows bob → love locally");
70
+
71
+ // 5b. DERIVED read field: the engine projects moodEmoji from the law's pure fn
72
+ if (after[0]!.data.moodEmoji !== "🤩") fail(`derived moodEmoji not projected locally: ${JSON.stringify(after[0]!.data)}`);
73
+ ok("derived field projects locally — moodEmoji 🤩 (engine-computed, never in the ledger)");
74
+
75
+ // 5c. DECLARED COUNT: the maintained O(1) tally, typed accessor, local projection
76
+ const localCount = await app.entriesPerMood("delighted");
77
+ if (localCount !== 1) fail(`local declared count: ${localCount}`);
78
+ ok("declared count maintained locally — entriesPerMood('delighted') = 1");
79
+
80
+ // 6. sync: push the session branch + edge admission merges to main
81
+ const s = await holon.sync({ admit: true });
82
+ if (!s.admission?.ok) fail(`sync/admit: ${JSON.stringify(s)}`);
83
+ const admitted = (s.admission.sessions || []).flatMap((x: { admitted?: unknown[] }) => x.admitted || []);
84
+ if (admitted.length < 2) fail(`edge admitted ${admitted.length} intent(s), expected 2: ${JSON.stringify(s.admission)}`);
85
+ ok(`synced — edge admission merged ${admitted.length} intents to main`);
86
+
87
+ // 7. the CLOUD's declared query (read-manifest overlay, edge projection) returns the entry
88
+ d = await (await fetch(`${CLOUD}/v1/workspaces/${WS}/query?queryId=entriesByAuthor&params=${encodeURIComponent(JSON.stringify({ author: "ada" }))}`)).json();
89
+ if (!(d.ok && d.rows.length === 1 && d.rows[0].data.reactions?.bob === "love")) {
90
+ fail(`cloud declared query: ${JSON.stringify(d)}`);
91
+ }
92
+ ok("cloud declared query returns the admitted entry WITH the admitted reaction — full loop closed");
93
+
94
+ // 8. the EDGE projects the derived field + maintains the count too (same wasm, same law)
95
+ d = await (await fetch(`${CLOUD}/v1/workspaces/${WS}/aggregates/${encodeURIComponent(entryId)}`)).json();
96
+ if (d.rows?.[0]?.data?.moodEmoji !== "🤩") fail(`edge derived projection: ${JSON.stringify(d.rows?.[0]?.data)}`);
97
+ ok("derived field projects at the edge — moodEmoji 🤩");
98
+ d = await (await fetch(`${CLOUD}/v1/workspaces/${WS}/counts/entriesPerMood?group=delighted`)).json();
99
+ if (!(d.ok && d.count === 1)) fail(`cloud count: ${JSON.stringify(d)}`);
100
+ ok("cloud count endpoint — entriesPerMood[delighted] = 1 (O(1) maintained read)");
101
+
102
+ // 9. CONVERGENCE: the same instance adopts canonical main inside sync() — no reconnect
103
+ const head = await holon.head();
104
+ const refsAdv = await (await fetch(`${CLOUD}/v1/workspaces/${WS}/git/info/refs?service=git-upload-pack`)).text();
105
+ const remoteMain = (refsAdv.match(/([0-9a-f]{40}) refs\/heads\/main/) || [])[1];
106
+ if (head !== remoteMain) fail(`not converged in place: local ${head?.slice(0, 10)} vs main ${remoteMain?.slice(0, 10)}`);
107
+ ok("converged in place — local head == canonical main, no reconnect");
108
+
109
+ console.log(`\nALL GREEN — typed client: compile → deploy → connect → typed write/query/mutate/derive/count → admit → converge → cloud reads (${WS})`);
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "nodenext",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "skipLibCheck": true,
9
+ "allowImportingTsExtensions": true,
10
+ "verbatimModuleSyntax": true
11
+ },
12
+ "include": [
13
+ "domains/**/*.ts",
14
+ "build/*.client.ts",
15
+ "test/**/*.mts"
16
+ ]
17
+ }