@githolon/dsl 0.2.2 → 0.3.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 +4 -1
- package/src/build_package.ts +124 -3
- package/src/codegen_dart.ts +15 -0
- package/src/codegen_ts.ts +235 -7
- package/src/compile_package_main.ts +342 -6
- package/src/engine_entry.ts +124 -4
- package/src/framework/workspace_invariant.ts +7 -0
- package/src/index.ts +6 -0
- package/src/manifest.ts +56 -7
- package/src/usd.ts +37 -0
- package/src/workspace_routing.ts +585 -0
- package/src/workspace_sharding.ts +1179 -0
- package/src/workspace_type.ts +609 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded
|
|
4
|
+
// QuickJS engine} on V8 + WASI-shim, byte-identical everywhere. V8 = portability; the one
|
|
5
|
+
// wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
|
|
6
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `workspaceType(...)` — FIRST-CLASS WORKSPACE TYPES, the tenant DSL surface
|
|
10
|
+
* (`architecture/workspace_types_and_sharding.md`, §10 RATIFIED; slice 1).
|
|
11
|
+
*
|
|
12
|
+
* A workspace type is a law-declared node kind in the workspace tree — the
|
|
13
|
+
* genericization of the hand-written governance taxonomy (`framework/workspaces.ts`
|
|
14
|
+
* `CloudWorkspace.kind: "workspace" | "platform"`, `birthPlatform`, pools, `cap(n)`)
|
|
15
|
+
* into a feature any tenant declares. IMPORTED FROM THE SUBPATH
|
|
16
|
+
* `@githolon/dsl/workspace-type` — NOT the runtime barrel: the barrel is bundled
|
|
17
|
+
* into every tenant's engine lump, and a taxonomy-free domain's package bytes must
|
|
18
|
+
* not move (the hash-stability law; the `build-package` precedent). The decls are
|
|
19
|
+
* COMPILE-LANE — manifest lowering, the homing walk, and the derived birth lanes
|
|
20
|
+
* consume them; the sealed engine never does:
|
|
21
|
+
*
|
|
22
|
+
* import { workspaceType } from "@githolon/dsl/workspace-type";
|
|
23
|
+
*
|
|
24
|
+
* export const EstateWs = workspaceType("estate")
|
|
25
|
+
* .root(Estate) // exactly one Estate aggregate per instance
|
|
26
|
+
* .hasMany(() => SiteHome) // the taxonomy: an Estate has Sites
|
|
27
|
+
* .global(Catalogue); // estate-wide reference data
|
|
28
|
+
*
|
|
29
|
+
* export const SiteHome = workspaceType("site")
|
|
30
|
+
* .root(Site)
|
|
31
|
+
* .packed(); // the shard axis (vs .dedicated())
|
|
32
|
+
*
|
|
33
|
+
* Everything else is DERIVED — Jack's ratified requirement is a FULLY TYPESAFE
|
|
34
|
+
* surface: a domain dev NEVER sees or manages a compound key. Homing is derived
|
|
35
|
+
* from the aggregates' existing `t.ref` chains (zero annotation); birth lanes are
|
|
36
|
+
* derived from `hasMany` over the EXISTING platform machinery
|
|
37
|
+
* (`POST /v1/platforms/...` — derived, never forked); minted-id home keys are
|
|
38
|
+
* internal plumbing (slice 2).
|
|
39
|
+
*
|
|
40
|
+
* THE HOMING WALK (this file): `home(aggregate)` = the NEAREST PACKED AXIS its
|
|
41
|
+
* `t.ref` chain reaches (`TrackableAsset.siteId → Site` ⇒ assets home on their
|
|
42
|
+
* site; `Room → Building → Site` transitively). An aggregate whose chain stops at
|
|
43
|
+
* a dedicated root homes on that root's type (the coordinator). FAIL-CLOSED, with
|
|
44
|
+
* named remedies:
|
|
45
|
+
* * no path to any axis → compile error (give it a ref / `.global(...)`
|
|
46
|
+
* / `.coordinatorLocal(...)`);
|
|
47
|
+
* * ambiguous nearest axis → compile error (pin it, or break a ref chain);
|
|
48
|
+
* * a directive whose declared surface (target + `.reads(...)`) spans two homes
|
|
49
|
+
* → compile error (model it as the Order/Receipt
|
|
50
|
+
* PR pair — `cross_workspace.md`).
|
|
51
|
+
*
|
|
52
|
+
* MANIFEST LOWERING (`manifest.ts` calls {@link canonicalWorkspaceTypesFragment}):
|
|
53
|
+
* the taxonomy AND the derived homing table are HASH-BEARING law — and OMITTED
|
|
54
|
+
* ENTIRELY when the domain declares no workspace type, so a taxonomy-free domain
|
|
55
|
+
* is byte-identical in the canonical manifest to before this feature existed
|
|
56
|
+
* (the `cap(n)` discipline; guestbook + co2 hashes PROVEN unmoved).
|
|
57
|
+
*/
|
|
58
|
+
import type { AggregateHandle } from "./aggregate.js";
|
|
59
|
+
import type { Directive } from "./directive.js";
|
|
60
|
+
|
|
61
|
+
/** A directive of any payload type (`Directive<P>` is invariant in `P` — same alias
|
|
62
|
+
* convention as `codegen_dart.ts`). */
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
type AnyDirective = Directive<any>;
|
|
65
|
+
|
|
66
|
+
/** How a workspace type's instances are hosted: one holon each, or packed into shards. */
|
|
67
|
+
export type WorkspaceTypeMode = "dedicated" | "packed";
|
|
68
|
+
|
|
69
|
+
/** A child reference — the decl itself, or a thunk for forward/circular declaration order. */
|
|
70
|
+
export type WorkspaceTypeRef = WorkspaceTypeDecl | (() => WorkspaceTypeDecl);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* One declared workspace type. Chainable + immutable (every method returns a NEW
|
|
74
|
+
* decl), and discoverable by SHAPE at any stage via `__isWorkspaceType` — the same
|
|
75
|
+
* duck-typing discipline as `AggregateHandle`/`Directive`. Validation (a missing
|
|
76
|
+
* `.root`, a packed type with children, …) is FAIL-CLOSED at compile/lowering,
|
|
77
|
+
* never silently defaulted.
|
|
78
|
+
*/
|
|
79
|
+
export interface WorkspaceTypeDecl {
|
|
80
|
+
/** Brand for by-shape discovery (compile auto-discovery, like aggregates). */
|
|
81
|
+
readonly __isWorkspaceType: true;
|
|
82
|
+
/** The type's id — instance workspace names are composed from it by the lanes. */
|
|
83
|
+
readonly id: string;
|
|
84
|
+
/** The ROOT aggregate: exactly one instance per workspace of this type. */
|
|
85
|
+
readonly rootAggregate?: AggregateHandle;
|
|
86
|
+
/** `dedicated` (one holon per instance — default) or `packed` (the shard axis). */
|
|
87
|
+
readonly mode: WorkspaceTypeMode;
|
|
88
|
+
/** The taxonomy's `hasMany` edges (child workspace types). */
|
|
89
|
+
readonly childRefs: readonly WorkspaceTypeRef[];
|
|
90
|
+
/** Reference data homed HERE and replicated to descendant shards (slice 4 lane). */
|
|
91
|
+
readonly globalAggregates: readonly AggregateHandle[];
|
|
92
|
+
/** Aggregates PINNED to this (coordinator) type — the no-path named remedy. */
|
|
93
|
+
readonly coordinatorLocalAggregates: readonly AggregateHandle[];
|
|
94
|
+
/** The pool: how many live children this type's instances may parent. */
|
|
95
|
+
readonly poolSize?: number;
|
|
96
|
+
/** The law-declared instance limit for this type (the `cap(n)` discipline). */
|
|
97
|
+
readonly capCount?: number;
|
|
98
|
+
/** Declare the root aggregate (required — validated fail-closed at compile). */
|
|
99
|
+
root(agg: AggregateHandle): WorkspaceTypeDecl;
|
|
100
|
+
/** Declare a `hasMany` child type (the taxonomy edge a birth lane derives from). */
|
|
101
|
+
hasMany(child: WorkspaceTypeRef): WorkspaceTypeDecl;
|
|
102
|
+
/** Mark instances PACKED into shard holons (the shard axis). */
|
|
103
|
+
packed(): WorkspaceTypeDecl;
|
|
104
|
+
/** Mark instances DEDICATED (one holon each — the default, explicit form). */
|
|
105
|
+
dedicated(): WorkspaceTypeDecl;
|
|
106
|
+
/** Declare reference-data aggregates homed here, replicated to descendant shards. */
|
|
107
|
+
global(...aggs: AggregateHandle[]): WorkspaceTypeDecl;
|
|
108
|
+
/** PIN aggregates to this type (the named remedy for a no-path/ambiguous home). */
|
|
109
|
+
coordinatorLocal(...aggs: AggregateHandle[]): WorkspaceTypeDecl;
|
|
110
|
+
/** Declare the child pool (quota law: how many live children instances may parent). */
|
|
111
|
+
pool(n: number): WorkspaceTypeDecl;
|
|
112
|
+
/** Declare the instance cap (law-declared instance limit, like aggregate `cap`). */
|
|
113
|
+
cap(n: number): WorkspaceTypeDecl;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface WorkspaceTypeState {
|
|
117
|
+
readonly id: string;
|
|
118
|
+
readonly rootAggregate?: AggregateHandle;
|
|
119
|
+
readonly mode: WorkspaceTypeMode;
|
|
120
|
+
readonly childRefs: readonly WorkspaceTypeRef[];
|
|
121
|
+
readonly globalAggregates: readonly AggregateHandle[];
|
|
122
|
+
readonly coordinatorLocalAggregates: readonly AggregateHandle[];
|
|
123
|
+
readonly poolSize?: number;
|
|
124
|
+
readonly capCount?: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function makeWorkspaceType(state: WorkspaceTypeState): WorkspaceTypeDecl {
|
|
128
|
+
return {
|
|
129
|
+
__isWorkspaceType: true,
|
|
130
|
+
...state,
|
|
131
|
+
root(agg: AggregateHandle): WorkspaceTypeDecl {
|
|
132
|
+
return makeWorkspaceType({ ...state, rootAggregate: agg });
|
|
133
|
+
},
|
|
134
|
+
hasMany(child: WorkspaceTypeRef): WorkspaceTypeDecl {
|
|
135
|
+
return makeWorkspaceType({ ...state, childRefs: [...state.childRefs, child] });
|
|
136
|
+
},
|
|
137
|
+
packed(): WorkspaceTypeDecl {
|
|
138
|
+
return makeWorkspaceType({ ...state, mode: "packed" });
|
|
139
|
+
},
|
|
140
|
+
dedicated(): WorkspaceTypeDecl {
|
|
141
|
+
return makeWorkspaceType({ ...state, mode: "dedicated" });
|
|
142
|
+
},
|
|
143
|
+
global(...aggs: AggregateHandle[]): WorkspaceTypeDecl {
|
|
144
|
+
return makeWorkspaceType({
|
|
145
|
+
...state,
|
|
146
|
+
globalAggregates: [...state.globalAggregates, ...aggs],
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
coordinatorLocal(...aggs: AggregateHandle[]): WorkspaceTypeDecl {
|
|
150
|
+
return makeWorkspaceType({
|
|
151
|
+
...state,
|
|
152
|
+
coordinatorLocalAggregates: [...state.coordinatorLocalAggregates, ...aggs],
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
pool(n: number): WorkspaceTypeDecl {
|
|
156
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
157
|
+
throw new Error(`workspaceType '${state.id}': pool must be a positive integer (got ${n})`);
|
|
158
|
+
}
|
|
159
|
+
return makeWorkspaceType({ ...state, poolSize: n });
|
|
160
|
+
},
|
|
161
|
+
cap(n: number): WorkspaceTypeDecl {
|
|
162
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
163
|
+
throw new Error(`workspaceType '${state.id}': cap must be a positive integer (got ${n})`);
|
|
164
|
+
}
|
|
165
|
+
return makeWorkspaceType({ ...state, capCount: n });
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Declare a workspace type. The id must be a lawful workspace-name segment
|
|
172
|
+
* (instances ride the existing `p--ws` name composition), so the same shape the
|
|
173
|
+
* cloud's name law accepts — and never `--`, the composer's reserved separator.
|
|
174
|
+
*/
|
|
175
|
+
export function workspaceType(id: string): WorkspaceTypeDecl {
|
|
176
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(id) || id.includes("--")) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`workspaceType '${id}': the id must start alphanumeric and use only letters, ` +
|
|
179
|
+
`digits, '-' and '_' (it seeds instance workspace names), and must not ` +
|
|
180
|
+
`contain '--' (the reserved name-composition separator). Rename the type.`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return makeWorkspaceType({
|
|
184
|
+
id,
|
|
185
|
+
mode: "dedicated",
|
|
186
|
+
childRefs: [],
|
|
187
|
+
globalAggregates: [],
|
|
188
|
+
coordinatorLocalAggregates: [],
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── resolution ────────────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
/** One RESOLVED workspace type: thunks forced, aggregates reduced to wire ids. */
|
|
195
|
+
export interface ResolvedWorkspaceType {
|
|
196
|
+
readonly id: string;
|
|
197
|
+
readonly rootId: string;
|
|
198
|
+
readonly mode: WorkspaceTypeMode;
|
|
199
|
+
readonly childIds: readonly string[];
|
|
200
|
+
readonly globalIds: readonly string[];
|
|
201
|
+
readonly coordinatorLocalIds: readonly string[];
|
|
202
|
+
readonly poolSize?: number;
|
|
203
|
+
readonly capCount?: number;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Resolve a set of declared workspace types into the closed taxonomy: force child
|
|
208
|
+
* thunks (forward refs), union in referenced child decls, dedupe by id, and
|
|
209
|
+
* validate FAIL-CLOSED with named remedies:
|
|
210
|
+
* * a type without `.root(...)` → error (declare the root aggregate);
|
|
211
|
+
* * two divergent decls under one id → error (one id, one law);
|
|
212
|
+
* * a PACKED type with `hasMany` children → error (shard-hosted subtrees are not
|
|
213
|
+
* law yet — make it `.dedicated()` or flatten the taxonomy);
|
|
214
|
+
* * a child id colliding with its parent → error.
|
|
215
|
+
*/
|
|
216
|
+
export function resolveWorkspaceTypes(
|
|
217
|
+
declared: readonly WorkspaceTypeDecl[],
|
|
218
|
+
domainName: string,
|
|
219
|
+
): Map<string, ResolvedWorkspaceType> {
|
|
220
|
+
// Force the closure over child refs first (a thunked child need not be exported).
|
|
221
|
+
const seen = new Map<string, WorkspaceTypeDecl>();
|
|
222
|
+
const queue: WorkspaceTypeDecl[] = [...declared];
|
|
223
|
+
while (queue.length > 0) {
|
|
224
|
+
const decl = queue.shift()!;
|
|
225
|
+
const prior = seen.get(decl.id);
|
|
226
|
+
if (prior !== undefined) {
|
|
227
|
+
if (prior !== decl && !sameDecl(prior, decl)) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`domain '${domainName}': workspaceType '${decl.id}' is declared twice with ` +
|
|
230
|
+
`DIVERGENT shapes — one id is one law. Reconcile the declarations.`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
seen.set(decl.id, decl);
|
|
236
|
+
for (const ref of decl.childRefs) queue.push(typeof ref === "function" ? ref() : ref);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const out = new Map<string, ResolvedWorkspaceType>();
|
|
240
|
+
for (const decl of seen.values()) {
|
|
241
|
+
if (decl.rootAggregate === undefined) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`domain '${domainName}': workspaceType '${decl.id}' declares no root aggregate — ` +
|
|
244
|
+
`every workspace type needs exactly one root. Fix: chain .root(<Aggregate>) ` +
|
|
245
|
+
`on the declaration.`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
const childIds = decl.childRefs.map((ref) => (typeof ref === "function" ? ref() : ref).id);
|
|
249
|
+
if (decl.mode === "packed" && childIds.length > 0) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`domain '${domainName}': workspaceType '${decl.id}' is .packed() but declares ` +
|
|
252
|
+
`hasMany children (${childIds.join(", ")}) — a packed (shard-axis) type cannot ` +
|
|
253
|
+
`parent child workspaces. Fix: make '${decl.id}' .dedicated(), or flatten the ` +
|
|
254
|
+
`taxonomy so the children hang off a dedicated ancestor.`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
if (childIds.includes(decl.id)) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`domain '${domainName}': workspaceType '${decl.id}' declares itself as its own ` +
|
|
260
|
+
`child — the taxonomy is a tree. Remove the self-edge.`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
out.set(decl.id, {
|
|
264
|
+
id: decl.id,
|
|
265
|
+
rootId: decl.rootAggregate.id,
|
|
266
|
+
mode: decl.mode,
|
|
267
|
+
childIds: [...new Set(childIds)].sort(),
|
|
268
|
+
globalIds: [...new Set(decl.globalAggregates.map((a) => a.id))].sort(),
|
|
269
|
+
coordinatorLocalIds: [...new Set(decl.coordinatorLocalAggregates.map((a) => a.id))].sort(),
|
|
270
|
+
...(decl.poolSize !== undefined ? { poolSize: decl.poolSize } : {}),
|
|
271
|
+
...(decl.capCount !== undefined ? { capCount: decl.capCount } : {}),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// One root aggregate ⇒ one type (two types claiming one root is two laws on one record).
|
|
276
|
+
const byRoot = new Map<string, string>();
|
|
277
|
+
for (const t of out.values()) {
|
|
278
|
+
const prior = byRoot.get(t.rootId);
|
|
279
|
+
if (prior !== undefined) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`domain '${domainName}': aggregate '${t.rootId}' is the root of BOTH workspaceType ` +
|
|
282
|
+
`'${prior}' and '${t.id}' — one root aggregate anchors one type. Split the roots.`,
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
byRoot.set(t.rootId, t.id);
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Structural equality of two decls under one id (reference dedupe's slow path). */
|
|
291
|
+
function sameDecl(a: WorkspaceTypeDecl, b: WorkspaceTypeDecl): boolean {
|
|
292
|
+
const ids = (refs: readonly WorkspaceTypeRef[]) =>
|
|
293
|
+
refs.map((r) => (typeof r === "function" ? r() : r).id).sort().join(",");
|
|
294
|
+
return (
|
|
295
|
+
a.rootAggregate?.id === b.rootAggregate?.id &&
|
|
296
|
+
a.mode === b.mode &&
|
|
297
|
+
ids(a.childRefs) === ids(b.childRefs) &&
|
|
298
|
+
a.globalAggregates.map((x) => x.id).sort().join(",") ===
|
|
299
|
+
b.globalAggregates.map((x) => x.id).sort().join(",") &&
|
|
300
|
+
a.coordinatorLocalAggregates.map((x) => x.id).sort().join(",") ===
|
|
301
|
+
b.coordinatorLocalAggregates.map((x) => x.id).sort().join(",") &&
|
|
302
|
+
a.poolSize === b.poolSize &&
|
|
303
|
+
a.capCount === b.capCount
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ── the homing walk ───────────────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
const NO_PATH_REMEDY =
|
|
310
|
+
"Fix one of: give it a t.ref chain that reaches an axis root; mark it " +
|
|
311
|
+
".global(...) on the type that owns it (replicated reference data); or pin it " +
|
|
312
|
+
"with .coordinatorLocal(...) on the coordinator type.";
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Derive the homing table: aggregate wire id → workspace-type id. ZERO ANNOTATION
|
|
316
|
+
* in the common case — the walk follows the aggregates' existing same-workspace
|
|
317
|
+
* `t.ref` edges (cross-workspace refs are PR-tier, never homing edges) to the
|
|
318
|
+
* NEAREST PACKED AXIS root; an aggregate reaching only dedicated roots homes on the
|
|
319
|
+
* nearest one (the coordinator). TOTAL over the module's aggregates, FAIL-CLOSED:
|
|
320
|
+
* a no-path or ambiguous aggregate refuses to compile with a named remedy.
|
|
321
|
+
*/
|
|
322
|
+
export function deriveHoming(
|
|
323
|
+
aggregates: readonly AggregateHandle[],
|
|
324
|
+
types: ReadonlyMap<string, ResolvedWorkspaceType>,
|
|
325
|
+
domainName: string,
|
|
326
|
+
): Record<string, string> {
|
|
327
|
+
const byId = new Map<string, AggregateHandle>();
|
|
328
|
+
for (const agg of aggregates) byId.set(agg.id, agg);
|
|
329
|
+
|
|
330
|
+
// Pins first: roots anchor their own type; globals/coordinatorLocals are explicit.
|
|
331
|
+
const homes: Record<string, string> = {};
|
|
332
|
+
const pin = (aggId: string, typeId: string, why: string) => {
|
|
333
|
+
const prior = homes[aggId];
|
|
334
|
+
if (prior !== undefined && prior !== typeId) {
|
|
335
|
+
throw new Error(
|
|
336
|
+
`domain '${domainName}': aggregate '${aggId}' is ${why} of workspaceType ` +
|
|
337
|
+
`'${typeId}' but already homes on '${prior}' — one aggregate, one home. ` +
|
|
338
|
+
`Remove one of the pins.`,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
homes[aggId] = typeId;
|
|
342
|
+
};
|
|
343
|
+
for (const t of types.values()) {
|
|
344
|
+
if (!byId.has(t.rootId)) {
|
|
345
|
+
throw new Error(
|
|
346
|
+
`domain '${domainName}': workspaceType '${t.id}' roots on aggregate '${t.rootId}', ` +
|
|
347
|
+
`which this domain does not declare — the root must be one of the domain's ` +
|
|
348
|
+
`aggregates. Export it from a composed module (or extraAggregates).`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
pin(t.rootId, t.id, "the root");
|
|
352
|
+
}
|
|
353
|
+
for (const t of types.values()) {
|
|
354
|
+
for (const g of t.globalIds) pin(g, t.id, "a .global(...)");
|
|
355
|
+
for (const c of t.coordinatorLocalIds) pin(c, t.id, "a .coordinatorLocal(...)");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// THE FRAMEWORK SHARDING LAW homes on the COORDINATOR (sharding slices 2+3): the
|
|
359
|
+
// derived `Nomos*` aggregates (`workspace_sharding.ts` — the shard map, receipts,
|
|
360
|
+
// registry, policy, the §5.2 subtotal/frontier rows, deep-verify verdicts) carry
|
|
361
|
+
// no homing t.ref chain BY DESIGN (they reference homes as DATA, never as a
|
|
362
|
+
// homing edge), so they pin to the taxonomy's unique top dedicated type. (The
|
|
363
|
+
// receipt + identity rows physically FOLD in shard chains too — placement is
|
|
364
|
+
// custody; the pin only says their DIRECTIVES are coordinator law, never routed.)
|
|
365
|
+
// With NO unique coordinator the pin is a FAIL-CLOSED error, never a guess.
|
|
366
|
+
const FRAMEWORK_SHARDING_AGGREGATES = new Set([
|
|
367
|
+
"NomosShardAssignment", "NomosShardIdentity", "NomosHomeReceipt", "NomosShardRegistry",
|
|
368
|
+
"NomosShardPolicy", "NomosSummarySubtotal", "NomosSummaryFrontier", "NomosDeepVerify",
|
|
369
|
+
"NomosCheckpointSeal",
|
|
370
|
+
]);
|
|
371
|
+
const childIdsAll = new Set([...types.values()].flatMap((t) => [...t.childIds]));
|
|
372
|
+
const coordinators = [...types.values()]
|
|
373
|
+
.filter((t) => t.mode === "dedicated" && !childIdsAll.has(t.id))
|
|
374
|
+
.map((t) => t.id)
|
|
375
|
+
.sort();
|
|
376
|
+
for (const agg of aggregates) {
|
|
377
|
+
if (!FRAMEWORK_SHARDING_AGGREGATES.has(agg.id) || homes[agg.id] !== undefined) continue;
|
|
378
|
+
if (coordinators.length !== 1) {
|
|
379
|
+
throw new Error(
|
|
380
|
+
`domain '${domainName}': framework aggregate '${agg.id}' homes on the coordinator, ` +
|
|
381
|
+
`but the taxonomy has ${coordinators.length === 0 ? "no" : "more than one"} top ` +
|
|
382
|
+
`dedicated type (${coordinators.join(", ") || "none"}) — pin it with ` +
|
|
383
|
+
`.coordinatorLocal(...) on the intended coordinator type.`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
pin(agg.id, coordinators[0]!, "the framework placement law (coordinator-pinned)");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const rootType = new Map<string, ResolvedWorkspaceType>();
|
|
390
|
+
for (const t of types.values()) rootType.set(t.rootId, t);
|
|
391
|
+
|
|
392
|
+
// BFS per unpinned aggregate over same-workspace ref edges, shortest first.
|
|
393
|
+
for (const agg of aggregates) {
|
|
394
|
+
if (homes[agg.id] !== undefined) continue;
|
|
395
|
+
const dist = new Map<string, number>([[agg.id, 0]]);
|
|
396
|
+
const frontier: string[] = [agg.id];
|
|
397
|
+
const packedHits = new Map<string, number>(); // typeId → distance
|
|
398
|
+
const dedicatedHits = new Map<string, number>();
|
|
399
|
+
while (frontier.length > 0) {
|
|
400
|
+
const cur = frontier.shift()!;
|
|
401
|
+
const d = dist.get(cur)!;
|
|
402
|
+
const t = rootType.get(cur);
|
|
403
|
+
if (t !== undefined && cur !== agg.id) {
|
|
404
|
+
(t.mode === "packed" ? packedHits : dedicatedHits).set(t.id, d);
|
|
405
|
+
continue; // a root is an axis terminus — the walk stops at it
|
|
406
|
+
}
|
|
407
|
+
const handle = byId.get(cur);
|
|
408
|
+
if (handle === undefined) continue; // a ref out of this domain — not a homing edge
|
|
409
|
+
for (const field of Object.values(handle.fields)) {
|
|
410
|
+
if (field.kind !== "ref" || field.refAggregateId === undefined) continue;
|
|
411
|
+
if (field.refWorkspace !== undefined) continue; // PR-tier edge, never homing
|
|
412
|
+
if (!dist.has(field.refAggregateId)) {
|
|
413
|
+
dist.set(field.refAggregateId, d + 1);
|
|
414
|
+
frontier.push(field.refAggregateId);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const pick = (hits: Map<string, number>, axisKind: string): string | undefined => {
|
|
419
|
+
if (hits.size === 0) return undefined;
|
|
420
|
+
const min = Math.min(...hits.values());
|
|
421
|
+
const nearest = [...hits.entries()].filter(([, d]) => d === min).map(([id]) => id).sort();
|
|
422
|
+
if (nearest.length > 1) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
`domain '${domainName}': aggregate '${agg.id}' reaches ${axisKind} axes ` +
|
|
425
|
+
`${nearest.map((n) => `'${n}'`).join(" and ")} at the same ref distance — ` +
|
|
426
|
+
`its home is ambiguous. Fix: pin it (.coordinatorLocal(...) / .global(...)), ` +
|
|
427
|
+
`or restructure its t.ref chain so one axis is strictly nearer.`,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
return nearest[0];
|
|
431
|
+
};
|
|
432
|
+
const home = pick(packedHits, "packed") ?? pick(dedicatedHits, "dedicated");
|
|
433
|
+
if (home === undefined) {
|
|
434
|
+
throw new Error(
|
|
435
|
+
`domain '${domainName}': aggregate '${agg.id}' has NO t.ref path to any ` +
|
|
436
|
+
`workspace-type root — it cannot be homed. ${NO_PATH_REMEDY}`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
homes[agg.id] = home;
|
|
440
|
+
}
|
|
441
|
+
return homes;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ── directive cross-home check ────────────────────────────────────────────────────
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* FAIL-CLOSED cross-home check over each directive's DECLARED surface (its target
|
|
448
|
+
* aggregate + its `.reads(...)` boundary): every declared-read type must be
|
|
449
|
+
* available at the target's home — same home, or `.global(...)` reference data
|
|
450
|
+
* (replicated to shards), or an ancestor coordinator's globals. A plan spanning two
|
|
451
|
+
* homes must be modeled as the Order/Receipt PR pair (`cross_workspace.md`), never
|
|
452
|
+
* as one intent the sharding layer would have to tear in half. (Instance-level
|
|
453
|
+
* wrong-home is the shard gate's runtime invariant — slice 2.)
|
|
454
|
+
*/
|
|
455
|
+
export function checkDirectiveHoming(
|
|
456
|
+
directives: readonly AnyDirective[],
|
|
457
|
+
homes: Readonly<Record<string, string>>,
|
|
458
|
+
types: ReadonlyMap<string, ResolvedWorkspaceType>,
|
|
459
|
+
domainName: string,
|
|
460
|
+
): void {
|
|
461
|
+
const globalIds = new Set<string>();
|
|
462
|
+
for (const t of types.values()) for (const g of t.globalIds) globalIds.add(g);
|
|
463
|
+
for (const d of directives) {
|
|
464
|
+
const targetHome = homes[d.aggregateId];
|
|
465
|
+
if (targetHome === undefined) continue; // a foreign-domain target — not this law's walk
|
|
466
|
+
for (const read of d.declaredReads) {
|
|
467
|
+
const readHome = homes[read];
|
|
468
|
+
if (readHome === undefined || readHome === targetHome) continue;
|
|
469
|
+
if (globalIds.has(read)) continue; // replicated reference data — readable at any home
|
|
470
|
+
throw new Error(
|
|
471
|
+
`domain '${domainName}': directive '${d.id}' targets '${d.aggregateId}' ` +
|
|
472
|
+
`(home '${targetHome}') but declares a read of '${read}' (home '${readHome}') — ` +
|
|
473
|
+
`a plan cannot span two homes. Fix: mark '${read}' .global(...) on its owning ` +
|
|
474
|
+
`type (replicated reference data), or model the cross-home effect as an ` +
|
|
475
|
+
`Order/Receipt PR pair (cross_workspace.md).`,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ── canonical-manifest lowering (hash-bearing, omitted-when-absent) ───────────────
|
|
482
|
+
|
|
483
|
+
/** One captured workspace type in the canonical manifest — the hashed taxonomy law. */
|
|
484
|
+
export interface CanonicalWorkspaceType {
|
|
485
|
+
readonly id: string;
|
|
486
|
+
/** The root aggregate's wire id. */
|
|
487
|
+
readonly root: string;
|
|
488
|
+
readonly mode: WorkspaceTypeMode;
|
|
489
|
+
/** Child type ids, SORTED. OMITTED when the type declares no children. */
|
|
490
|
+
readonly children?: string[];
|
|
491
|
+
/** Replicated reference-data aggregate ids, SORTED. OMITTED when none. */
|
|
492
|
+
readonly globals?: string[];
|
|
493
|
+
/** Pinned aggregate ids, SORTED. OMITTED when none. */
|
|
494
|
+
readonly coordinatorLocals?: string[];
|
|
495
|
+
/** The child pool. OMITTED when undeclared. */
|
|
496
|
+
readonly pool?: number;
|
|
497
|
+
/** The instance cap. OMITTED when undeclared. */
|
|
498
|
+
readonly cap?: number;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** The manifest fragment the taxonomy lowers to: the types + the derived homing table. */
|
|
502
|
+
export interface CanonicalWorkspaceTypesFragment {
|
|
503
|
+
workspaceTypes?: CanonicalWorkspaceType[];
|
|
504
|
+
homes?: Record<string, string>;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Lower a module's declared workspace types into its canonical-manifest fragment.
|
|
509
|
+
* OMIT-WHEN-EMPTY: returns `{}` (NEITHER key) when the module declares none — the
|
|
510
|
+
* omission, not an empty array, is what keeps taxonomy-free domains byte-identical
|
|
511
|
+
* to before this feature existed (guestbook + co2 hashes PROVEN unmoved). When
|
|
512
|
+
* declared: `workspaceTypes` SORTED by id (per-type optional keys omitted-when-absent,
|
|
513
|
+
* the `cap(n)` discipline) plus `homes` — the DERIVED homing table (aggregate →
|
|
514
|
+
* type id; `canonicalJson` sorts the record keys). Both are LAW: the domain hash
|
|
515
|
+
* moves when the taxonomy or a homing assignment moves. Runs the full fail-closed
|
|
516
|
+
* validation (resolution, the homing walk, the directive cross-home check) — an
|
|
517
|
+
* unhomeable taxonomy never produces an identity.
|
|
518
|
+
*/
|
|
519
|
+
export function canonicalWorkspaceTypesFragment(mod: {
|
|
520
|
+
readonly name: string;
|
|
521
|
+
readonly aggregates: readonly AggregateHandle[];
|
|
522
|
+
readonly directives: readonly AnyDirective[];
|
|
523
|
+
readonly workspaceTypes?: readonly WorkspaceTypeDecl[];
|
|
524
|
+
}): CanonicalWorkspaceTypesFragment {
|
|
525
|
+
if (mod.workspaceTypes === undefined || mod.workspaceTypes.length === 0) return {};
|
|
526
|
+
const types = resolveWorkspaceTypes(mod.workspaceTypes, mod.name);
|
|
527
|
+
const homes = deriveHoming(mod.aggregates, types, mod.name);
|
|
528
|
+
checkDirectiveHoming(mod.directives, homes, types, mod.name);
|
|
529
|
+
const workspaceTypes: CanonicalWorkspaceType[] = [...types.values()]
|
|
530
|
+
.map((t) => ({
|
|
531
|
+
id: t.id,
|
|
532
|
+
root: t.rootId,
|
|
533
|
+
mode: t.mode,
|
|
534
|
+
...(t.childIds.length > 0 ? { children: [...t.childIds] } : {}),
|
|
535
|
+
...(t.globalIds.length > 0 ? { globals: [...t.globalIds] } : {}),
|
|
536
|
+
...(t.coordinatorLocalIds.length > 0
|
|
537
|
+
? { coordinatorLocals: [...t.coordinatorLocalIds] }
|
|
538
|
+
: {}),
|
|
539
|
+
...(t.poolSize !== undefined ? { pool: t.poolSize } : {}),
|
|
540
|
+
...(t.capCount !== undefined ? { cap: t.capCount } : {}),
|
|
541
|
+
}))
|
|
542
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
543
|
+
return { workspaceTypes, homes };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Validate a module's taxonomy FAIL-CLOSED (the compile gate `nomos-compile` runs
|
|
548
|
+
* BEFORE identity emission, so a homing error is a named COMPILE error — never a
|
|
549
|
+
* domain silently recorded as identity-excluded). Same machinery as the lowering;
|
|
550
|
+
* throwing is the verdict.
|
|
551
|
+
*/
|
|
552
|
+
export function validateWorkspaceTaxonomy(mod: {
|
|
553
|
+
readonly name: string;
|
|
554
|
+
readonly aggregates: readonly AggregateHandle[];
|
|
555
|
+
readonly directives: readonly AnyDirective[];
|
|
556
|
+
readonly workspaceTypes?: readonly WorkspaceTypeDecl[];
|
|
557
|
+
}): void {
|
|
558
|
+
canonicalWorkspaceTypesFragment(mod);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ── estate-scope reads (sharding §2/§5 — slice 3) ─────────────────────────────────
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Lift a declared `count(...)`/`sum(...)` read to ESTATE SCOPE (sharding §5): the
|
|
565
|
+
* read's per-shard values ride the §5.2 delta lane as gate-recomputed committed
|
|
566
|
+
* subtotals on the coordinator, and the logical workspace answers it O(1) from the
|
|
567
|
+
* maintained estate total — never a scatter-gather.
|
|
568
|
+
*
|
|
569
|
+
* export const fxSiteCount = scoped(count("fxSiteCount").of(Site), EstateWs);
|
|
570
|
+
*
|
|
571
|
+
* Lives on THIS subpath (never the runtime barrel, and never a method on the
|
|
572
|
+
* count/sum builders) by the slice-1 hash-stability law: the builders' bytes ride
|
|
573
|
+
* every tenant's engine lump, and a taxonomy-free domain's package bytes must not
|
|
574
|
+
* move. The marker is HASH-BEARING (the canonical manifest's `scope` key,
|
|
575
|
+
* omitted-when-absent): scoping a read is a law change.
|
|
576
|
+
*
|
|
577
|
+
* PREDICATE-BEARING reads (slice 4): a `.where(...)`-filtered count/sum MAY be
|
|
578
|
+
* estate-scoped — the shard-side capture's oracle is the projection's MAINTAINED
|
|
579
|
+
* (predicate-aware) tally, and the suffix re-derivation evaluates the canonical
|
|
580
|
+
* predicate in its host fold (`engine.mjs evalCanonicalPred`). The predicate is
|
|
581
|
+
* hash-bearing through the canonical manifest's `where` key exactly as before.
|
|
582
|
+
*
|
|
583
|
+
* FAIL-CLOSED boundary (named): the scope must be a DECLARED workspace type (the
|
|
584
|
+
* compile gate additionally pins it to the coordinator type of a packed taxonomy).
|
|
585
|
+
*/
|
|
586
|
+
export function scoped<T extends { readonly id: string; readonly of: string }>(
|
|
587
|
+
read: T,
|
|
588
|
+
scope: WorkspaceTypeRef,
|
|
589
|
+
): T & { readonly scope: string } {
|
|
590
|
+
const decl = typeof scope === "function" ? scope() : scope;
|
|
591
|
+
if (!decl || (decl as { __isWorkspaceType?: unknown }).__isWorkspaceType !== true) {
|
|
592
|
+
throw new Error(
|
|
593
|
+
`scoped(read, scope): the scope must be a workspaceType(...) declaration (got ${typeof scope})`,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
const r = read as { id?: unknown; of?: unknown; _where?: unknown };
|
|
597
|
+
if (typeof r.id !== "string" || typeof r.of !== "string") {
|
|
598
|
+
throw new Error(
|
|
599
|
+
`scoped(read, scope): the read must be a count/sum with .of(...) declared (a named, maintained read)`,
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
if ((r.id as string).includes("|")) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`estate-scoped read '${String(r.id)}' contains '|' — the estate-summary bucket separator. ` +
|
|
605
|
+
`Rename the read (scoped read ids may not contain '|').`,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
return Object.freeze({ ...(read as object), scope: decl.id }) as T & { readonly scope: string };
|
|
609
|
+
}
|