@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
"./usd-layers": "./src/usd_layers.ts",
|
|
23
23
|
"./usd-state": "./src/usd_state.ts",
|
|
24
24
|
"./engine-entry": "./src/engine_entry.ts",
|
|
25
|
+
"./workspace-type": "./src/workspace_type.ts",
|
|
26
|
+
"./workspace-routing": "./src/workspace_routing.ts",
|
|
27
|
+
"./workspace-sharding": "./src/workspace_sharding.ts",
|
|
25
28
|
"./build-package": "./src/build_package.ts",
|
|
26
29
|
"./compile-engine": "./src/compile_engine.ts",
|
|
27
30
|
"./codegen-dart": "./src/codegen_dart.ts",
|
package/src/build_package.ts
CHANGED
|
@@ -45,10 +45,13 @@ import type { DomainModule, PermissionVocabulary, DartImport } from "./codegen_d
|
|
|
45
45
|
import { finishCount, type AnyCount, type CountDecl } from "./count.js";
|
|
46
46
|
import type { QueryDecl } from "./query.js";
|
|
47
47
|
import type { SpatialDecl } from "./spatial.js";
|
|
48
|
-
import type
|
|
48
|
+
import { finishSum, type AnySum } from "./sum.js";
|
|
49
|
+
import { finishExtremum, type AnyExtremum } from "./extremum.js";
|
|
49
50
|
import type { DerivedDecl } from "./derived.js";
|
|
50
51
|
import type { CombinedDecl } from "./combined.js";
|
|
51
52
|
import type { ImpureCapabilityDecl } from "./codegen_provider_dart.js";
|
|
53
|
+
import type { WorkspaceTypeDecl } from "./workspace_type.js";
|
|
54
|
+
import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
|
|
52
55
|
import { domainHash, emitManifestBytes } from "./manifest.js";
|
|
53
56
|
import { emitUsd } from "./usd.js";
|
|
54
57
|
|
|
@@ -108,7 +111,14 @@ export interface DomainModuleSpec {
|
|
|
108
111
|
readonly combineds?: readonly CombinedDecl[];
|
|
109
112
|
readonly impureCapabilities?: readonly ImpureCapabilityDecl[];
|
|
110
113
|
readonly sums?: readonly AnySum[];
|
|
114
|
+
/** Maintained MIN/MAX reads (#47 — the extremum RPC's manifest half). Config-named like sums. */
|
|
115
|
+
readonly mins?: readonly AnyExtremum[];
|
|
116
|
+
readonly maxes?: readonly AnyExtremum[];
|
|
111
117
|
readonly spatials?: readonly SpatialDecl[];
|
|
118
|
+
/** First-class workspace types (sharding §10 slice 1) — the law-declared taxonomy. */
|
|
119
|
+
readonly workspaceTypes?: readonly WorkspaceTypeDecl[];
|
|
120
|
+
/** Declared workspace invariants (#266) — presence is hashed; bodies ship in the lump. */
|
|
121
|
+
readonly workspaceInvariants?: readonly WorkspaceInvariantDecl[];
|
|
112
122
|
readonly dartImports?: readonly DartImport[];
|
|
113
123
|
readonly dartRefImports?: DomainModule["dartRefImports"];
|
|
114
124
|
readonly permissionVocabulary?: PermissionVocabulary;
|
|
@@ -149,6 +159,10 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
149
159
|
const combineds = [...(spec.combineds ?? [])];
|
|
150
160
|
const impureCapabilities = [...(spec.impureCapabilities ?? [])];
|
|
151
161
|
const sums = [...(spec.sums ?? [])];
|
|
162
|
+
const mins = [...(spec.mins ?? [])];
|
|
163
|
+
const maxes = [...(spec.maxes ?? [])];
|
|
164
|
+
const workspaceTypes = [...(spec.workspaceTypes ?? [])];
|
|
165
|
+
const workspaceInvariants = [...(spec.workspaceInvariants ?? [])];
|
|
152
166
|
const dartImports = [...(spec.dartImports ?? [])];
|
|
153
167
|
|
|
154
168
|
return {
|
|
@@ -164,6 +178,15 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
164
178
|
...(readModels.length > 0 ? { readModelAggregates: [...readModels] } : {}),
|
|
165
179
|
...(impureCapabilities.length > 0 ? { impureCapabilities } : {}),
|
|
166
180
|
...(sums.length > 0 ? { sums } : {}),
|
|
181
|
+
// Omit-when-empty — extremum-free domains' canonical manifests stay byte-identical.
|
|
182
|
+
...(mins.length > 0 ? { mins } : {}),
|
|
183
|
+
...(maxes.length > 0 ? { maxes } : {}),
|
|
184
|
+
// Omit-when-empty — HASH-LOAD-BEARING like every optional key here: a taxonomy-
|
|
185
|
+
// free domain's canonical manifest must stay byte-identical (sharding §10 slice 1).
|
|
186
|
+
...(workspaceTypes.length > 0 ? { workspaceTypes } : {}),
|
|
187
|
+
// Omit-when-empty — presence-only hashed law (#266): an invariant-free domain's
|
|
188
|
+
// canonical manifest must stay byte-identical (adding/removing an invariant moves it).
|
|
189
|
+
...(workspaceInvariants.length > 0 ? { workspaceInvariants } : {}),
|
|
167
190
|
...(dartImports.length > 0 ? { dartImports } : {}),
|
|
168
191
|
...(spec.dartRefImports !== undefined && spec.dartRefImports.length > 0
|
|
169
192
|
? { dartRefImports: [...spec.dartRefImports] }
|
|
@@ -199,6 +222,10 @@ export interface CountDescriptor {
|
|
|
199
222
|
id: string;
|
|
200
223
|
of: string;
|
|
201
224
|
by: string | null;
|
|
225
|
+
/** The canonical predicate (`WirePred` in the Rust parser — `{kind:"eq"|"ne"|"and"|"or",…}`).
|
|
226
|
+
* OMITTED entirely when the count declares none: predicate-free read manifests stay
|
|
227
|
+
* byte-identical (the Rust side reads it `#[serde(rename = "where", default)]`). */
|
|
228
|
+
where?: unknown;
|
|
202
229
|
}
|
|
203
230
|
export interface SpatialDescriptor {
|
|
204
231
|
id: string;
|
|
@@ -223,6 +250,26 @@ export interface CombinedDescriptor {
|
|
|
223
250
|
returns: ProjectionReturnDescriptor;
|
|
224
251
|
}
|
|
225
252
|
|
|
253
|
+
/** One maintained-sum descriptor (the projection's `WireSum` shape — note `field`). */
|
|
254
|
+
export interface SumDescriptor {
|
|
255
|
+
id: string;
|
|
256
|
+
of: string;
|
|
257
|
+
field: string;
|
|
258
|
+
by: string | null;
|
|
259
|
+
/** The canonical predicate — same omit-when-absent discipline as {@link CountDescriptor}. */
|
|
260
|
+
where?: unknown;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** One min/max read-manifest descriptor — the Rust `WireExtremum` shape (#47): identical
|
|
264
|
+
* to {@link SumDescriptor}; the KIND is implicit in which array it rides (`mins` vs `maxes`). */
|
|
265
|
+
export interface ExtremumDescriptor {
|
|
266
|
+
id: string;
|
|
267
|
+
of: string;
|
|
268
|
+
field: string;
|
|
269
|
+
by: string | null;
|
|
270
|
+
where?: unknown;
|
|
271
|
+
}
|
|
272
|
+
|
|
226
273
|
/** The combined read-manifest artifact (the EXACT wire shape the Rust read engine parses). */
|
|
227
274
|
export interface ReadManifest {
|
|
228
275
|
aggregateFieldKinds: AggregateFieldKinds;
|
|
@@ -231,6 +278,11 @@ export interface ReadManifest {
|
|
|
231
278
|
spatials: SpatialDescriptor[];
|
|
232
279
|
deriveds: DerivedDescriptor[];
|
|
233
280
|
combineds: CombinedDescriptor[];
|
|
281
|
+
/** OMITTED when no domain declares a sum (read manifests predating sums stay byte-identical). */
|
|
282
|
+
sums?: SumDescriptor[];
|
|
283
|
+
/** OMITTED when no domain declares a min/max (extremum-free read manifests stay byte-identical). */
|
|
284
|
+
mins?: ExtremumDescriptor[];
|
|
285
|
+
maxes?: ExtremumDescriptor[];
|
|
234
286
|
}
|
|
235
287
|
|
|
236
288
|
type ZodLike = {
|
|
@@ -332,7 +384,9 @@ function describeCount(byType: AggregateFieldKinds, c: CountDecl): CountDescript
|
|
|
332
384
|
`the of-type must be an aggregate emitted into the manifest.`,
|
|
333
385
|
);
|
|
334
386
|
}
|
|
335
|
-
|
|
387
|
+
// `where` rides to the projection (WireCount parses it) — OMITTED when absent, so
|
|
388
|
+
// predicate-free read manifests keep their exact pre-slice-4 bytes.
|
|
389
|
+
return { id: c.id, of: c.of, by: c.by ?? null, ...(c.where !== undefined ? { where: c.where } : {}) };
|
|
336
390
|
}
|
|
337
391
|
|
|
338
392
|
function describeSpatial(byType: AggregateFieldKinds, s: SpatialDecl): SpatialDescriptor {
|
|
@@ -443,6 +497,63 @@ export function buildReadManifest(modules: readonly DomainModule[]): ReadManifes
|
|
|
443
497
|
}
|
|
444
498
|
const counts = [...countsById.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
445
499
|
|
|
500
|
+
// Maintained SUMS (sharding slice 3 routes the derived `nomosEstateSummary` through
|
|
501
|
+
// here; tenant sums ride the same lane). The projection's `WireSum` names the summed
|
|
502
|
+
// field `field`. Omitted entirely when none — sum-free read manifests stay byte-identical.
|
|
503
|
+
const sumsById = new Map<string, SumDescriptor>();
|
|
504
|
+
for (const mod of modules) {
|
|
505
|
+
for (const raw of mod.sums ?? []) {
|
|
506
|
+
const s = finishSum(raw);
|
|
507
|
+
if (sorted[s.of] === undefined) {
|
|
508
|
+
throw new Error(
|
|
509
|
+
`build-package: sum '${s.id}' tallies '${s.of}', which has no field schema — ` +
|
|
510
|
+
`the of-type must be an aggregate emitted into the manifest.`,
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
const descriptor: SumDescriptor = { id: s.id, of: s.of, field: s.sumField, by: s.by ?? null, ...(s.where !== undefined ? { where: s.where } : {}) };
|
|
514
|
+
const existing = sumsById.get(s.id);
|
|
515
|
+
if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(descriptor)) {
|
|
516
|
+
throw new Error(
|
|
517
|
+
`build-package: sum id '${s.id}' is declared with DIVERGENT maintenance across ` +
|
|
518
|
+
`domains — one id cannot map to two totals. Reconcile the sum declaration.`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
sumsById.set(s.id, descriptor);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const sums = [...sumsById.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
525
|
+
|
|
526
|
+
// Maintained MIN/MAX reads (#47 — the extremum RPC's manifest half): the SAME lane
|
|
527
|
+
// as sums, two arrays (the kind is implicit in which array a descriptor rides —
|
|
528
|
+
// the Rust `WireExtremum` has parsed them since the read-engine slice). Omitted
|
|
529
|
+
// entirely when none — extremum-free read manifests stay byte-identical.
|
|
530
|
+
const lowerExtrema = (key: "mins" | "maxes"): ExtremumDescriptor[] => {
|
|
531
|
+
const byId = new Map<string, ExtremumDescriptor>();
|
|
532
|
+
for (const mod of modules) {
|
|
533
|
+
for (const raw of mod[key] ?? []) {
|
|
534
|
+
const e = finishExtremum(raw);
|
|
535
|
+
if (sorted[e.of] === undefined) {
|
|
536
|
+
throw new Error(
|
|
537
|
+
`build-package: ${key === "mins" ? "min" : "max"} '${e.id}' reads '${e.of}', which has no field schema — ` +
|
|
538
|
+
`the of-type must be an aggregate emitted into the manifest.`,
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
const descriptor: ExtremumDescriptor = { id: e.id, of: e.of, field: e.valueField, by: e.by ?? null, ...(e.where !== undefined ? { where: e.where } : {}) };
|
|
542
|
+
const existing = byId.get(e.id);
|
|
543
|
+
if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(descriptor)) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`build-package: ${key === "mins" ? "min" : "max"} id '${e.id}' is declared with DIVERGENT ` +
|
|
546
|
+
`maintenance across domains — one id cannot map to two extrema. Reconcile the declaration.`,
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
byId.set(e.id, descriptor);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
553
|
+
};
|
|
554
|
+
const mins = lowerExtrema("mins");
|
|
555
|
+
const maxes = lowerExtrema("maxes");
|
|
556
|
+
|
|
446
557
|
const spatialsById = new Map<string, SpatialDescriptor>();
|
|
447
558
|
for (const mod of modules) {
|
|
448
559
|
for (const s of mod.spatials ?? []) {
|
|
@@ -497,7 +608,17 @@ export function buildReadManifest(modules: readonly DomainModule[]): ReadManifes
|
|
|
497
608
|
(a, b) => a.of.localeCompare(b.of) || a.id.localeCompare(b.id),
|
|
498
609
|
);
|
|
499
610
|
|
|
500
|
-
return {
|
|
611
|
+
return {
|
|
612
|
+
aggregateFieldKinds: sorted,
|
|
613
|
+
queries,
|
|
614
|
+
counts,
|
|
615
|
+
spatials,
|
|
616
|
+
deriveds,
|
|
617
|
+
combineds,
|
|
618
|
+
...(sums.length > 0 ? { sums } : {}),
|
|
619
|
+
...(mins.length > 0 ? { mins } : {}),
|
|
620
|
+
...(maxes.length > 0 ? { maxes } : {}),
|
|
621
|
+
};
|
|
501
622
|
}
|
|
502
623
|
|
|
503
624
|
// ─────────────────────────────────────────────────────────────────────────────────
|
package/src/codegen_dart.ts
CHANGED
|
@@ -45,6 +45,7 @@ import type { OrderedReadDecl } from "./ordered_read.js";
|
|
|
45
45
|
import type { DerivedDecl } from "./derived.js";
|
|
46
46
|
import type { CombinedDecl } from "./combined.js";
|
|
47
47
|
import { emitProviderSdk, type ImpureCapabilityDecl } from "./codegen_provider_dart.js";
|
|
48
|
+
import type { WorkspaceTypeDecl } from "./workspace_type.js";
|
|
48
49
|
|
|
49
50
|
/** A directive of any payload type. `Directive<P>` is invariant in `P`, so the
|
|
50
51
|
* generator (which only introspects shapes) takes the `any`-payload form. */
|
|
@@ -2407,6 +2408,20 @@ export interface DomainModule {
|
|
|
2407
2408
|
* aggregate `hasInvariant` / a directive `plan`). See `docs/workspace_invariant.md`.
|
|
2408
2409
|
*/
|
|
2409
2410
|
workspaceInvariants?: WorkspaceInvariantDecl[];
|
|
2411
|
+
/**
|
|
2412
|
+
* The domain's FIRST-CLASS WORKSPACE TYPES (`workspace_type.ts`; sharding §10
|
|
2413
|
+
* RATIFIED, slice 1) — the law-declared workspace taxonomy
|
|
2414
|
+
* (`workspaceType("estate").root(Estate).hasMany(() => SiteHome)`). ADDITIVE +
|
|
2415
|
+
* OPTIONAL: a domain that declares none omits this entirely and is byte-identical
|
|
2416
|
+
* in the canonical manifest to before this key existed (taxonomy-free domains are
|
|
2417
|
+
* hash-stable). When declared, `manifest.ts` lowers the taxonomy AND the derived
|
|
2418
|
+
* homing table (nearest packed axis via the aggregates' `t.ref` chains) into the
|
|
2419
|
+
* canonical manifest — hash-bearing law — after the FAIL-CLOSED homing walk
|
|
2420
|
+
* (no-path / ambiguous / cross-home = compile error with a named remedy). Birth
|
|
2421
|
+
* lanes are DERIVED from `hasMany` over the EXISTING platform machinery
|
|
2422
|
+
* (`codegen_ts.ts` emits the typed helpers); domain devs never see a compound key.
|
|
2423
|
+
*/
|
|
2424
|
+
workspaceTypes?: WorkspaceTypeDecl[];
|
|
2410
2425
|
/**
|
|
2411
2426
|
* The domain's ENGINE-PROJECTED DERIVED read fields (read-engine: derived read fields).
|
|
2412
2427
|
* Each is a PURE fn of ONE aggregate's folded fields, computed BY THE ENGINE during the
|
package/src/codegen_ts.ts
CHANGED
|
@@ -45,6 +45,8 @@ import { finishCount } from "./count.js";
|
|
|
45
45
|
import { finishSum } from "./sum.js";
|
|
46
46
|
import type { DerivedDecl } from "./derived.js";
|
|
47
47
|
import type { CombinedDecl } from "./combined.js";
|
|
48
|
+
import { resolveWorkspaceTypes, type ResolvedWorkspaceType, type WorkspaceTypeDecl } from "./workspace_type.js";
|
|
49
|
+
import { deriveClientMintPlan, type ClientMintPlanEntry } from "./workspace_routing.js";
|
|
48
50
|
|
|
49
51
|
const cap = (s: string) => (s.length ? s[0]!.toUpperCase() + s.slice(1) : s);
|
|
50
52
|
const camel = (s: string) => s.replace(/[_\s-]+(\w)/g, (_m, c: string) => c.toUpperCase());
|
|
@@ -187,18 +189,33 @@ export function autoStampFields(schema: z.ZodTypeAny): string[] {
|
|
|
187
189
|
return out;
|
|
188
190
|
}
|
|
189
191
|
|
|
190
|
-
/** The payload type text, with auto-stamped
|
|
191
|
-
function tsTypeOfPayload(
|
|
192
|
-
|
|
192
|
+
/** The payload type text, with auto-stamped / front-door-minted / auto-placed fields optional (`?`). */
|
|
193
|
+
function tsTypeOfPayload(
|
|
194
|
+
schema: z.ZodTypeAny,
|
|
195
|
+
autoStamped: readonly string[],
|
|
196
|
+
minted: readonly string[] = [],
|
|
197
|
+
autoPlaced: readonly string[] = [],
|
|
198
|
+
): string {
|
|
199
|
+
if (zodKind(schema) !== "object" || (autoStamped.length === 0 && minted.length === 0 && autoPlaced.length === 0)) {
|
|
200
|
+
return tsTypeOfZod(schema, "");
|
|
201
|
+
}
|
|
193
202
|
const stamped = new Set(autoStamped);
|
|
203
|
+
const mintedSet = new Set(minted);
|
|
204
|
+
const placedSet = new Set(autoPlaced);
|
|
194
205
|
const fields = Object.entries(zodObjectShape(schema)).map(([name, raw]) => {
|
|
195
206
|
let f = raw as z.ZodTypeAny;
|
|
196
|
-
let optional = stamped.has(name);
|
|
207
|
+
let optional = stamped.has(name) || mintedSet.has(name) || placedSet.has(name);
|
|
197
208
|
while (zodKind(f) === "optional" || zodKind(f) === "default") {
|
|
198
209
|
optional = true;
|
|
199
210
|
f = zodDef(f).innerType!;
|
|
200
211
|
}
|
|
201
|
-
const note = stamped.has(name)
|
|
212
|
+
const note = stamped.has(name)
|
|
213
|
+
? " // auto-stamped with ISO now() at dispatch when omitted"
|
|
214
|
+
: mintedSet.has(name)
|
|
215
|
+
? " // kernel-minted at dispatch when omitted (the front-door mint; returned on the result)"
|
|
216
|
+
: placedSet.has(name)
|
|
217
|
+
? " // picked LEAST-LOADED over the coordinator's law-state at dispatch when omitted (sharding §3)"
|
|
218
|
+
: "";
|
|
202
219
|
return ` ${name}${optional ? "?" : ""}: ${tsTypeOfZod(f, " ")};${note}`;
|
|
203
220
|
});
|
|
204
221
|
return fields.length ? `{\n${fields.join("\n")}\n}` : "Record<string, never>";
|
|
@@ -266,6 +283,21 @@ export interface TsClientOptions {
|
|
|
266
283
|
readonly domainHash: string;
|
|
267
284
|
}
|
|
268
285
|
|
|
286
|
+
/** TRUE when any module declares a PACKED workspace taxonomy (the routed-holon shape).
|
|
287
|
+
* Gates every slice-4 paged-surface emission: taxonomy-free clients stay BYTE-IDENTICAL. */
|
|
288
|
+
function anyPackedTaxonomy(modules: readonly DomainModule[]): boolean {
|
|
289
|
+
for (const mod of modules) {
|
|
290
|
+
const types = (mod as { workspaceTypes?: readonly WorkspaceTypeDecl[] }).workspaceTypes;
|
|
291
|
+
if (!types || types.length === 0) continue;
|
|
292
|
+
try {
|
|
293
|
+
for (const t of resolveWorkspaceTypes(types, mod.name).values()) {
|
|
294
|
+
if (t.mode === "packed") return true;
|
|
295
|
+
}
|
|
296
|
+
} catch { /* an unresolvable taxonomy is the compile gate's verdict, never codegen's */ }
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
269
301
|
export function generateTsClient(modules: readonly DomainModule[], opts: TsClientOptions): string {
|
|
270
302
|
const hashConst = tsHashConstName(opts.packageName);
|
|
271
303
|
const out: string[] = [];
|
|
@@ -294,6 +326,12 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
294
326
|
` /** Declared-count / declared-sum O(1) reads (maintained by the read engine). */`,
|
|
295
327
|
` count(countId: string, groupKey?: string): Promise<number>;`,
|
|
296
328
|
` sum(sumId: string, groupKey?: string): Promise<number>;`,
|
|
329
|
+
...(anyPackedTaxonomy(modules)
|
|
330
|
+
? [
|
|
331
|
+
` /** THE LOGICAL PAGED ROW SURFACE (sharding §5.3, ruling 2) — present on a ROUTED holon. */`,
|
|
332
|
+
` pages?(queryId: string, params?: Record<string, unknown>, page?: { pageSize?: number; cursor?: string | null }): Promise<{ rows: unknown[]; cursor: string | null; done: boolean }>;`,
|
|
333
|
+
]
|
|
334
|
+
: []),
|
|
297
335
|
` /** The DEAD-LETTER QUEUE — refused-but-legitimate intents the app must handle`,
|
|
298
336
|
` * explicitly (inspect → retry after a law fix → or discard). Work is never lost. */`,
|
|
299
337
|
` deadLetters(): Promise<{ id?: string; oid: string; source: string; error: string; domain?: string | null; directiveId?: string | null; at: string }[]>;`,
|
|
@@ -351,6 +389,149 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
351
389
|
out.push(`}`, ``);
|
|
352
390
|
}
|
|
353
391
|
|
|
392
|
+
// ── DERIVED BIRTH LANES (sharding §10 RATIFIED, slice 1) ──
|
|
393
|
+
// A `workspaceType(...).hasMany(...)` taxonomy DERIVES typed birth helpers over
|
|
394
|
+
// the EXISTING platform machinery (`POST /v1/platforms/...` — derived, never
|
|
395
|
+
// forked). Fully typesafe per Jack's ruling: the caller passes LOGICAL names
|
|
396
|
+
// (parent param named after the parent TYPE); the `p--ws` composition is internal
|
|
397
|
+
// plumbing — the composed workspace name comes back on the result for `connect()`.
|
|
398
|
+
// PACKED child types derive NO holon-birth helper: a packed birth is a shard-map
|
|
399
|
+
// PLACEMENT (slice 2's lane).
|
|
400
|
+
const birthModules = modules.filter(
|
|
401
|
+
(m) => m.workspaceTypes !== undefined && m.workspaceTypes.length > 0,
|
|
402
|
+
);
|
|
403
|
+
// THE FRONT-DOOR MINT PLAN (sharding slice 2; taxonomy packages ONLY — a
|
|
404
|
+
// taxonomy-free package's generated client is byte-identical to before this
|
|
405
|
+
// existed): routed `.creates` directives whose target id rides the payload mint
|
|
406
|
+
// it through the holon's kernel mint with the home's ROUTE TAG folded in
|
|
407
|
+
// (self-routing ids, §4); the derived placement directives mint the axis-root id.
|
|
408
|
+
const mintPlans = new Map<DomainModule, Map<string, ClientMintPlanEntry>>();
|
|
409
|
+
for (const mod of birthModules) {
|
|
410
|
+
const plan = deriveClientMintPlan(
|
|
411
|
+
mod as {
|
|
412
|
+
name: string;
|
|
413
|
+
aggregates: AggregateHandle[];
|
|
414
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
415
|
+
directives: any[];
|
|
416
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
417
|
+
workspaceTypes?: any[];
|
|
418
|
+
},
|
|
419
|
+
);
|
|
420
|
+
if (plan.length > 0) mintPlans.set(mod, new Map(plan.map((e) => [e.directive, e])));
|
|
421
|
+
}
|
|
422
|
+
if (mintPlans.size > 0) {
|
|
423
|
+
out.push(
|
|
424
|
+
`// ── the front-door kernel mint (routed creates — ids are internal plumbing) ──`,
|
|
425
|
+
``,
|
|
426
|
+
`/** The ROUTED holon surface (@githolon/client with a taxonomy law): the kernel mint. */`,
|
|
427
|
+
`export interface NomosRoutedHolon extends NomosHolon {`,
|
|
428
|
+
` /** Reserve a kernel-minted typed id; \`homeKey\`/\`tagHex\` fold the home's route tag in. */`,
|
|
429
|
+
` mint(typeTag: string, opts?: { homeKey?: string; tagHex?: string }): Promise<string>;`,
|
|
430
|
+
`}`,
|
|
431
|
+
``,
|
|
432
|
+
`/** The 48-bit route-tag slot of a kernel-minted id (the UUIDv7 leading 12 hex chars). */`,
|
|
433
|
+
`const routeTagOfId = (id: string): string | undefined => {`,
|
|
434
|
+
` const i = id.indexOf("_");`,
|
|
435
|
+
` if (i <= 0) return undefined;`,
|
|
436
|
+
` const body = id.slice(i + 1).replace(/-/g, "").toLowerCase();`,
|
|
437
|
+
` return /^[0-9a-f]{32}$/.test(body) ? body.slice(0, 12) : undefined;`,
|
|
438
|
+
`};`,
|
|
439
|
+
``,
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
if (birthModules.length > 0) {
|
|
443
|
+
out.push(
|
|
444
|
+
`// ── derived birth lanes (workspace taxonomy → the EXISTING platform lanes) ──`,
|
|
445
|
+
``,
|
|
446
|
+
`/** Auth a birth presents — every birth needs a principal (the cloud's law). */`,
|
|
447
|
+
`export interface NomosBirthAuth {`,
|
|
448
|
+
` /** Auth-provider access token, sent as \`x-nomos-auth\` (verified by the cloud). */`,
|
|
449
|
+
` authToken?: string;`,
|
|
450
|
+
` /** Transitional bare principal uid, sent as \`x-nomos-principal\` (no token). */`,
|
|
451
|
+
` principal?: string;`,
|
|
452
|
+
` /** Bearer credential when the lane is gated (platform admin key / invite key). */`,
|
|
453
|
+
` bearer?: string;`,
|
|
454
|
+
`}`,
|
|
455
|
+
``,
|
|
456
|
+
`/** A birth lane's response — \`workspace\` is the born holon's addressable name. */`,
|
|
457
|
+
`export interface NomosBirthResult {`,
|
|
458
|
+
` ok: boolean;`,
|
|
459
|
+
` /** The addressable workspace name (pass it to \`connect({ workspace })\`). */`,
|
|
460
|
+
` workspace?: string;`,
|
|
461
|
+
` /** Returned ONCE at birth — the workspace's deploy credential. SAVE IT. */`,
|
|
462
|
+
` workspaceSecret?: string;`,
|
|
463
|
+
` /** Platform births only — the platform's own governance credential, ONCE. */`,
|
|
464
|
+
` platformAdminKey?: string;`,
|
|
465
|
+
` error?: string;`,
|
|
466
|
+
` [k: string]: unknown;`,
|
|
467
|
+
`}`,
|
|
468
|
+
``,
|
|
469
|
+
`const nomosBirth = async (cloud: string, path: string, auth: NomosBirthAuth): Promise<NomosBirthResult> => {`,
|
|
470
|
+
` const headers: Record<string, string> = {};`,
|
|
471
|
+
` if (auth.authToken !== undefined) headers["x-nomos-auth"] = auth.authToken;`,
|
|
472
|
+
` if (auth.principal !== undefined) headers["x-nomos-principal"] = auth.principal;`,
|
|
473
|
+
` if (auth.bearer !== undefined) headers["authorization"] = \`Bearer \${auth.bearer}\`;`,
|
|
474
|
+
` const res = await fetch(\`\${cloud.replace(/\\/$/, "")}\${path}\`, { method: "POST", headers });`,
|
|
475
|
+
` return (await res.json()) as NomosBirthResult;`,
|
|
476
|
+
`};`,
|
|
477
|
+
``,
|
|
478
|
+
);
|
|
479
|
+
const emittedBirths = new Set<string>();
|
|
480
|
+
for (const mod of birthModules) {
|
|
481
|
+
let types: Map<string, ResolvedWorkspaceType>;
|
|
482
|
+
try {
|
|
483
|
+
types = resolveWorkspaceTypes(mod.workspaceTypes!, mod.name);
|
|
484
|
+
} catch (e) {
|
|
485
|
+
// The compile gate (compile_package_main) already failed a bad taxonomy
|
|
486
|
+
// loudly; reaching here means codegen was driven directly — same verdict.
|
|
487
|
+
throw new Error(`codegen-ts: ${(e as Error).message}`);
|
|
488
|
+
}
|
|
489
|
+
const childIds = new Set([...types.values()].flatMap((t) => [...t.childIds]));
|
|
490
|
+
const fnName = (typeId: string) => `birth${cap(camel(typeId))}`;
|
|
491
|
+
const emit = (name: string, doc: string, params: string[], pathExpr: string) => {
|
|
492
|
+
if (emittedBirths.has(name)) return;
|
|
493
|
+
emittedBirths.add(name);
|
|
494
|
+
out.push(
|
|
495
|
+
`/** ${doc} */`,
|
|
496
|
+
`export const ${name} = (opts: NomosBirthAuth & { cloud: string; ${params.join("; ")} }): Promise<NomosBirthResult> =>`,
|
|
497
|
+
` nomosBirth(opts.cloud, ${pathExpr}, opts);`,
|
|
498
|
+
``,
|
|
499
|
+
);
|
|
500
|
+
};
|
|
501
|
+
for (const t of types.values()) {
|
|
502
|
+
if (t.mode === "packed") continue; // placement, not a holon birth (slice 2)
|
|
503
|
+
const isPlatform = t.childIds.length > 0;
|
|
504
|
+
if (!childIds.has(t.id)) {
|
|
505
|
+
// A TOP-LEVEL type: births directly under the cloud.
|
|
506
|
+
emit(
|
|
507
|
+
fnName(t.id),
|
|
508
|
+
`Birth a '${t.id}' workspace${isPlatform ? " (a platform-kind coordinator: hosts its own subtree)" : ""} — derived from the taxonomy over the existing ${isPlatform ? "platform" : "workspace"} lane.`,
|
|
509
|
+
isPlatform ? [`name: string`] : [`name: string`, `tier?: "edge" | "heavy"`],
|
|
510
|
+
isPlatform
|
|
511
|
+
? `\`/v1/platforms/\${encodeURIComponent(opts.name)}\``
|
|
512
|
+
: `\`/v1/workspaces/\${encodeURIComponent(opts.name)}\${opts.tier === "heavy" ? "?tier=heavy" : ""}\``,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
for (const childId of t.childIds) {
|
|
516
|
+
const child = types.get(childId)!;
|
|
517
|
+
if (child.mode === "packed") continue;
|
|
518
|
+
const parentParam = lcFirst(camel(t.id));
|
|
519
|
+
const childIsPlatform = child.childIds.length > 0;
|
|
520
|
+
emit(
|
|
521
|
+
fnName(child.id),
|
|
522
|
+
`Birth a '${child.id}' under a '${t.id}' — the taxonomy's hasMany edge over the existing platform lane (lineage + pool enforced by the parent's law).`,
|
|
523
|
+
childIsPlatform
|
|
524
|
+
? [`${parentParam}: string`, `name: string`]
|
|
525
|
+
: [`${parentParam}: string`, `name: string`, `tier?: "edge" | "heavy"`],
|
|
526
|
+
childIsPlatform
|
|
527
|
+
? `\`/v1/platforms/\${encodeURIComponent(opts.${parentParam})}/platforms/\${encodeURIComponent(opts.name)}\``
|
|
528
|
+
: `\`/v1/platforms/\${encodeURIComponent(opts.${parentParam})}/workspaces/\${encodeURIComponent(opts.name)}\${opts.tier === "heavy" ? "?tier=heavy" : ""}\``,
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
354
535
|
// ── payload types + per-domain client factories ──
|
|
355
536
|
const usedPayloadNames = new Set<string>();
|
|
356
537
|
for (const mod of modules) {
|
|
@@ -360,6 +541,18 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
360
541
|
);
|
|
361
542
|
out.push(`// ── domain "${domain}" ──`, ``);
|
|
362
543
|
|
|
544
|
+
const mintPlan = mintPlans.get(mod);
|
|
545
|
+
// PLACEMENT directives (packed taxonomy births): their `shard` is AUTO-PICKED
|
|
546
|
+
// least-loaded by the routed holon at dispatch — optional on the typed surface
|
|
547
|
+
// (the §7 transparency contract: a dev never names a shard).
|
|
548
|
+
const placementDirs = new Set<string>();
|
|
549
|
+
if (mod.workspaceTypes !== undefined && mod.workspaceTypes.length > 0) {
|
|
550
|
+
try {
|
|
551
|
+
for (const t of resolveWorkspaceTypes(mod.workspaceTypes, mod.name).values()) {
|
|
552
|
+
if (t.mode === "packed") placementDirs.add(`birth${cap(camel(t.id))}`);
|
|
553
|
+
}
|
|
554
|
+
} catch { /* the compile gate already failed a bad taxonomy loudly */ }
|
|
555
|
+
}
|
|
363
556
|
const methods: string[] = [];
|
|
364
557
|
for (const d of dirs) {
|
|
365
558
|
let payloadName = `${cap(d.id)}Payload`;
|
|
@@ -367,11 +560,34 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
367
560
|
usedPayloadNames.add(payloadName);
|
|
368
561
|
const schema = (d as unknown as { payloadSchema: z.ZodTypeAny }).payloadSchema;
|
|
369
562
|
const stamps = autoStampFields(schema);
|
|
563
|
+
const mint = mintPlan?.get(d.id);
|
|
564
|
+
const placed = placementDirs.has(d.id) ? ["shard"] : [];
|
|
370
565
|
out.push(
|
|
371
|
-
`/** \`${domain}/${d.id}\` payload (mirrors the engine's zod schema).${stamps.length ? ` Omitted ${stamps.map((s) => `\`${s}\``).join("/")} auto-stamp(s) ISO now() at dispatch.` : ""} */`,
|
|
372
|
-
`export type ${payloadName} = ${tsTypeOfPayload(schema, stamps)};`,
|
|
566
|
+
`/** \`${domain}/${d.id}\` payload (mirrors the engine's zod schema).${stamps.length ? ` Omitted ${stamps.map((s) => `\`${s}\``).join("/")} auto-stamp(s) ISO now() at dispatch.` : ""}${mint ? ` Omitted \`${mint.field}\` is KERNEL-MINTED at dispatch (returned on the result).` : ""}${placed.length ? " Omitted `shard` is picked LEAST-LOADED at dispatch (the routed holon)." : ""} */`,
|
|
567
|
+
`export type ${payloadName} = ${tsTypeOfPayload(schema, stamps, mint ? [mint.field] : [], placed)};`,
|
|
373
568
|
``,
|
|
374
569
|
);
|
|
570
|
+
if (mint !== undefined) {
|
|
571
|
+
const mintOpts =
|
|
572
|
+
mint.homeKeyField !== undefined
|
|
573
|
+
? `, { homeKey: payload.${mint.homeKeyField} }`
|
|
574
|
+
: mint.tagFromIdField !== undefined
|
|
575
|
+
? `, { tagHex: routeTagOfId(payload.${mint.tagFromIdField}) }`
|
|
576
|
+
: "";
|
|
577
|
+
const body = stamps.length
|
|
578
|
+
? `stampNow({ ...payload, ${mint.field} }, ${JSON.stringify(stamps)})`
|
|
579
|
+
: `{ ...payload, ${mint.field} }`;
|
|
580
|
+
methods.push(
|
|
581
|
+
` /** Author \`${domain}/${d.id}\`: the front-door MINTS \`${mint.field}\` (route-tagged) when omitted`,
|
|
582
|
+
` * — ids stay internal plumbing — and returns it with the new local head. */`,
|
|
583
|
+
` ${d.id}: async (payload: ${payloadName}): Promise<{ head: string; ${mint.field}: string }> => {`,
|
|
584
|
+
` const ${mint.field} = payload.${mint.field} ?? (await (holon as NomosRoutedHolon).mint(${JSON.stringify(mint.mintType)}${mintOpts}));`,
|
|
585
|
+
` const head = await holon.dispatch(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${body}, { domainHash });`,
|
|
586
|
+
` return { head, ${mint.field} };`,
|
|
587
|
+
` },`,
|
|
588
|
+
);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
375
591
|
methods.push(
|
|
376
592
|
` /** Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head. */`,
|
|
377
593
|
stamps.length
|
|
@@ -394,6 +610,18 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
394
610
|
` /** Declared query \`${q.id}\` (indexed probe — routed by the read manifest). */`,
|
|
395
611
|
` ${lcFirst(camel(q.id))}: (params: { ${params} }) => holon.query(${JSON.stringify(q.id)}, params) as Promise<Row<${q.returns}Data>[]>,`,
|
|
396
612
|
);
|
|
613
|
+
// THE LOGICAL PAGED ROW SURFACE (sharding §5.3, ruling 2 — slice 4): emitted
|
|
614
|
+
// ONLY for packed-taxonomy domains (taxonomy-free clients stay byte-identical).
|
|
615
|
+
// The dev sees ordinary pagination; the shard walk is the implementation.
|
|
616
|
+
if (anyPackedTaxonomy(modules)) {
|
|
617
|
+
methods.push(
|
|
618
|
+
` /** \`${q.id}\`, paged at estate width (ruling 2): ordinary pagination, stable cursor. */`,
|
|
619
|
+
` ${lcFirst(camel(q.id))}Pages: (params: { ${params} }, page?: { pageSize?: number; cursor?: string | null }) => {`,
|
|
620
|
+
` if (!holon.pages) throw new Error("pages() needs the ROUTED holon - connect() to the logical (coordinator) workspace, not a shard");`,
|
|
621
|
+
` return holon.pages(${JSON.stringify(q.id)}, params, page) as Promise<{ rows: Row<${q.returns}Data>[]; cursor: string | null; done: boolean }>;`,
|
|
622
|
+
` },`,
|
|
623
|
+
);
|
|
624
|
+
}
|
|
397
625
|
}
|
|
398
626
|
|
|
399
627
|
for (const c of (mod.counts ?? []).map((raw) => finishCount(raw))) {
|