@githolon/dsl 0.2.3 → 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 +246 -5
- 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
|
@@ -82,6 +82,11 @@ import {
|
|
|
82
82
|
type Mod,
|
|
83
83
|
} from "./build_package.js";
|
|
84
84
|
import { collectEngineRouting } from "./engine_entry.js";
|
|
85
|
+
import { resolveWorkspaceTypes, type WorkspaceTypeDecl } from "./workspace_type.js";
|
|
86
|
+
import { canonicalTaxonomyFragment, validateWorkspaceTaxonomyAndRoutes } from "./workspace_routing.js";
|
|
87
|
+
import { shardingLawModule, type ShardingLawSpec } from "./workspace_sharding.js";
|
|
88
|
+
import type { AnyExtremum } from "./extremum.js";
|
|
89
|
+
import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
|
|
85
90
|
import {
|
|
86
91
|
emitLayeredDomain,
|
|
87
92
|
flattenLayeredUsd,
|
|
@@ -102,9 +107,15 @@ interface DomainConfig {
|
|
|
102
107
|
readonly queries?: readonly string[];
|
|
103
108
|
readonly counts?: readonly string[];
|
|
104
109
|
readonly sums?: readonly string[];
|
|
110
|
+
/** Maintained MIN/MAX reads (#47) — export names, config-named like sums. */
|
|
111
|
+
readonly mins?: readonly string[];
|
|
112
|
+
readonly maxes?: readonly string[];
|
|
105
113
|
readonly spatials?: readonly string[];
|
|
106
114
|
readonly deriveds?: readonly string[];
|
|
107
115
|
readonly combineds?: readonly string[];
|
|
116
|
+
readonly workspaceTypes?: readonly string[];
|
|
117
|
+
/** Export-name escape hatch for workspace invariants (auto-discovered by shape otherwise). */
|
|
118
|
+
readonly workspaceInvariants?: readonly string[];
|
|
108
119
|
readonly impureCapabilities?: readonly string[];
|
|
109
120
|
readonly extraAggregates?: readonly string[];
|
|
110
121
|
readonly readModelAggregates?: readonly string[];
|
|
@@ -176,6 +187,14 @@ function isQueryDecl(v: unknown): v is QueryDecl {
|
|
|
176
187
|
);
|
|
177
188
|
}
|
|
178
189
|
|
|
190
|
+
function isWorkspaceInvariantDecl(v: unknown): v is WorkspaceInvariantDecl {
|
|
191
|
+
return (
|
|
192
|
+
!!v &&
|
|
193
|
+
typeof v === "object" &&
|
|
194
|
+
(v as { __isWorkspaceInvariant?: unknown }).__isWorkspaceInvariant === true
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
179
198
|
function isSpatialDecl(v: unknown): v is SpatialDecl {
|
|
180
199
|
return (
|
|
181
200
|
!!v &&
|
|
@@ -223,7 +242,8 @@ function isCountDecl(v: unknown): v is AnyCount {
|
|
|
223
242
|
typeof o.on !== "string" && // spatial
|
|
224
243
|
typeof o.fn !== "function" && // derived/combined
|
|
225
244
|
!Array.isArray(o.key) && // query
|
|
226
|
-
!("field" in o) && //
|
|
245
|
+
!("field" in o) && // extremum
|
|
246
|
+
!("sumField" in o) && // sum (the SumDecl/Sum-builder mark — never a count)
|
|
227
247
|
!("orderBy" in o) && // ordered read
|
|
228
248
|
!("_existsMarker" in o) && // exists
|
|
229
249
|
typeof o.refField !== "string" && // combined
|
|
@@ -233,6 +253,28 @@ function isCountDecl(v: unknown): v is AnyCount {
|
|
|
233
253
|
);
|
|
234
254
|
}
|
|
235
255
|
|
|
256
|
+
/**
|
|
257
|
+
* Scan merged exports for `workspaceType(...)` decls (sharding §10 slice 1) by the
|
|
258
|
+
* `__isWorkspaceType` brand — individual exports AND exported arrays. Dedupe is BY
|
|
259
|
+
* REFERENCE here and BY ID (divergence-refused) in `resolveWorkspaceTypes`; the
|
|
260
|
+
* generic `discover()`'s JSON dedupe key is unusable for these decls (they carry
|
|
261
|
+
* aggregate handles whose zod schemas do not stringify).
|
|
262
|
+
*/
|
|
263
|
+
function discoverWorkspaceTypes(merged: Mod): WorkspaceTypeDecl[] {
|
|
264
|
+
const out: WorkspaceTypeDecl[] = [];
|
|
265
|
+
const isDecl = (v: unknown): v is WorkspaceTypeDecl =>
|
|
266
|
+
!!v && typeof v === "object" &&
|
|
267
|
+
(v as { __isWorkspaceType?: unknown }).__isWorkspaceType === true;
|
|
268
|
+
const push = (v: WorkspaceTypeDecl) => {
|
|
269
|
+
if (!out.includes(v)) out.push(v);
|
|
270
|
+
};
|
|
271
|
+
for (const v of Object.values(merged)) {
|
|
272
|
+
if (isDecl(v)) push(v);
|
|
273
|
+
else if (Array.isArray(v)) for (const el of v) if (isDecl(el)) push(el);
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
|
|
236
278
|
/** Scan merged exports for decls matching `match`: individual exports AND exported arrays. */
|
|
237
279
|
function discover<T>(merged: Mod, match: (v: unknown) => v is T): T[] {
|
|
238
280
|
const seen = new Set<string>();
|
|
@@ -421,6 +463,7 @@ async function main(): Promise<void> {
|
|
|
421
463
|
const eagerLump = process.env.NOMOS_LUMP_BOOT === "eager";
|
|
422
464
|
const entryImports: string[] = [];
|
|
423
465
|
const entryDomains: string[] = [];
|
|
466
|
+
const entryMintInstalls: string[] = []; // route-tagged in-plan mints (taxonomy packages only)
|
|
424
467
|
const routingInput: Record<string, Mod[]> = {};
|
|
425
468
|
const domainModules: DomainModule[] = [];
|
|
426
469
|
const layered = layeredFlag || cfg.layered === true;
|
|
@@ -446,11 +489,127 @@ async function main(): Promise<void> {
|
|
|
446
489
|
sourceExprs.push(`() => require(${JSON.stringify(abs)})`);
|
|
447
490
|
}
|
|
448
491
|
}
|
|
449
|
-
entryDomains.push(` ${JSON.stringify(d.key)}: [${sourceExprs.join(", ")}],`);
|
|
450
|
-
routingInput[d.key] = mods;
|
|
451
|
-
|
|
452
492
|
let merged: Mod = {};
|
|
453
493
|
for (const m of mods) merged = { ...merged, ...m };
|
|
494
|
+
let shardingSums: AnySum[] = [];
|
|
495
|
+
let shardingModForDomain: Mod | undefined;
|
|
496
|
+
let shardingLazyExpr: string | undefined;
|
|
497
|
+
|
|
498
|
+
// ── THE DERIVED PLACEMENT LAW (sharding slice 2): a taxonomy with PACKED axes
|
|
499
|
+
// appends the framework placement module (`workspace_sharding.ts`) to this
|
|
500
|
+
// domain — in the composed manifests (hash-bearing law) AND the generated
|
|
501
|
+
// entry (the plan ships in the lump). Taxonomy-free domains append NOTHING:
|
|
502
|
+
// their package bytes / hashes do not move (the slice-1 stability law). ──
|
|
503
|
+
const workspaceTypes = [
|
|
504
|
+
...discoverWorkspaceTypes(merged),
|
|
505
|
+
...resolveNamed<WorkspaceTypeDecl>(merged, d.workspaceTypes, "workspaceTypes"),
|
|
506
|
+
];
|
|
507
|
+
if (workspaceTypes.length > 0) {
|
|
508
|
+
let spec: ShardingLawSpec | undefined;
|
|
509
|
+
try {
|
|
510
|
+
const types = resolveWorkspaceTypes(workspaceTypes, d.name ?? d.key);
|
|
511
|
+
const axes = [...types.values()]
|
|
512
|
+
.filter((t) => t.mode === "packed")
|
|
513
|
+
.map((p) => {
|
|
514
|
+
const parent = [...types.values()].find((t) => t.childIds.includes(p.id));
|
|
515
|
+
// SLICE-2 STATIC MAP: shard count = the parent's .pool(n), default 1 (the
|
|
516
|
+
// degenerate coordinator+sole-shard layout, §6's no-flag-day migration).
|
|
517
|
+
return { axisType: p.id, axisRoot: p.rootId, shardCount: parent?.poolSize ?? 1 };
|
|
518
|
+
})
|
|
519
|
+
.sort((a, b) => (a.axisType < b.axisType ? -1 : 1));
|
|
520
|
+
if (axes.length > 0) spec = { axes };
|
|
521
|
+
} catch (e) {
|
|
522
|
+
fail((e as Error).message);
|
|
523
|
+
}
|
|
524
|
+
if (spec !== undefined) {
|
|
525
|
+
// SLICE 3a (#41): derive the directive ROUTES over the TENANT modules (the SAME
|
|
526
|
+
// derivation the canonical manifest records) and feed them into the placement
|
|
527
|
+
// law, so it emits the per-directive HOMING INVARIANT — the wrong-home gate
|
|
528
|
+
// moves from the edge bailiff into THE CHAIN GATE ITSELF.
|
|
529
|
+
// SLICE 3 (#42): also feed the PACKED HOMING TABLE (aggregate → axis) — the
|
|
530
|
+
// engine-side route-tag mint wrapper tags in-plan `create(Agg)` mints of
|
|
531
|
+
// packed-homed aggregates with the dispatched intent's home tag (§4).
|
|
532
|
+
try {
|
|
533
|
+
const fragment = canonicalTaxonomyFragment({
|
|
534
|
+
name: d.name ?? d.key,
|
|
535
|
+
aggregates: aggregatesOf(merged),
|
|
536
|
+
directives: directivesOf(merged),
|
|
537
|
+
workspaceTypes,
|
|
538
|
+
});
|
|
539
|
+
if (fragment.routes !== undefined && fragment.routes.length > 0) {
|
|
540
|
+
spec = { ...spec, routes: fragment.routes };
|
|
541
|
+
}
|
|
542
|
+
const packedAxes = new Set(spec.axes.map((a) => a.axisType));
|
|
543
|
+
const packedHomes = Object.fromEntries(
|
|
544
|
+
Object.entries(fragment.homes ?? {}).filter(([, home]) => packedAxes.has(home)),
|
|
545
|
+
);
|
|
546
|
+
if (Object.keys(packedHomes).length > 0) spec = { ...spec, homes: packedHomes };
|
|
547
|
+
} catch (e) {
|
|
548
|
+
fail((e as Error).message);
|
|
549
|
+
}
|
|
550
|
+
const shardingMod = shardingLawModule(spec) as Mod;
|
|
551
|
+
mods.push(shardingMod);
|
|
552
|
+
merged = { ...merged, ...shardingMod };
|
|
553
|
+
shardingModForDomain = shardingMod;
|
|
554
|
+
// The derived sharding law's MAINTAINED SUMS (`nomosEstateSummary` — the §5.1
|
|
555
|
+
// estate total over subtotal rows) must reach the read manifest + identity:
|
|
556
|
+
// sums are config-named (never auto-discovered — hash stability for existing
|
|
557
|
+
// packages), so the appended module's sums thread through explicitly here.
|
|
558
|
+
shardingSums = Object.values(shardingMod).filter(
|
|
559
|
+
(v): v is AnySum =>
|
|
560
|
+
!!v && typeof v === "object" &&
|
|
561
|
+
typeof (v as { id?: unknown }).id === "string" &&
|
|
562
|
+
typeof (v as { of?: unknown }).of === "string" &&
|
|
563
|
+
typeof (v as { sumField?: unknown }).sumField === "string",
|
|
564
|
+
);
|
|
565
|
+
const specJson = JSON.stringify(spec);
|
|
566
|
+
if (eagerLump) {
|
|
567
|
+
if (!entryImports.some((l) => l.includes("workspace-sharding"))) {
|
|
568
|
+
entryImports.push(
|
|
569
|
+
`import { shardingLawModule as __nomosShardingLaw, installRouteTaggedMint as __nomosInstallMint } from "@githolon/dsl/workspace-sharding";`,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
sourceExprs.push(`__nomosShardingLaw(${specJson})`);
|
|
573
|
+
} else {
|
|
574
|
+
shardingLazyExpr = `() => (require("@githolon/dsl/workspace-sharding") as any).shardingLawModule(${specJson})`;
|
|
575
|
+
sourceExprs.push(shardingLazyExpr);
|
|
576
|
+
}
|
|
577
|
+
// ROUTE-TAGGED IN-PLAN MINTS (§4, slice 3): installed AFTER registerEngine at
|
|
578
|
+
// the lump's top level — pre-freeze, taxonomy packages only (a taxonomy-free
|
|
579
|
+
// lump never imports the module: the slice-1 hash-stability law).
|
|
580
|
+
entryMintInstalls.push(
|
|
581
|
+
eagerLump
|
|
582
|
+
? `__nomosInstallMint(${specJson});`
|
|
583
|
+
: `(require("@githolon/dsl/workspace-sharding") as any).installRouteTaggedMint(${specJson});`,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
entryDomains.push(` ${JSON.stringify(d.key)}: [${sourceExprs.join(", ")}],`);
|
|
589
|
+
routingInput[d.key] = mods;
|
|
590
|
+
// ── THE AUX INVARIANT-ROUTING KEY (#47, sharding slice 7 — the invariant-gate
|
|
591
|
+
// flame pass): in a LAZY lump, the derived sharding law registers a SECOND
|
|
592
|
+
// time under its own domain key, and the ROUTING table points the sharding
|
|
593
|
+
// workspace-invariant ids at THAT key only. A per-write homing-invariant
|
|
594
|
+
// dispatch (`workspaceInvariantReads`/`workspaceInvariant`) then forces ONLY
|
|
595
|
+
// the small framework module — never the tenant domain's whole zod/module
|
|
596
|
+
// graph (the measured ~30 ms-per-dispatch floor on co2 estate_structures).
|
|
597
|
+
// Directive dispatch is untouched (the sharding directives stay registered
|
|
598
|
+
// under `d.key` — the wire domain every host lane already speaks); the
|
|
599
|
+
// canonical manifests are untouched (identity hashes do not move); eager
|
|
600
|
+
// lumps are untouched (no routing table — the pre-#34 equivalence shape);
|
|
601
|
+
// taxonomy-free packages emit NOTHING here (the slice-1 byte-stability law).
|
|
602
|
+
if (shardingModForDomain !== undefined && shardingLazyExpr !== undefined && !eagerLump) {
|
|
603
|
+
const auxKey = `${d.key}/nomos-sharding`;
|
|
604
|
+
if (cfg.domains.some((x) => x.key === auxKey) || routingInput[auxKey] !== undefined) {
|
|
605
|
+
fail(
|
|
606
|
+
`domain key '${auxKey}' is reserved for the derived sharding law's invariant routing — rename the domain`,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
entryDomains.push(` ${JSON.stringify(auxKey)}: [${shardingLazyExpr}],`);
|
|
610
|
+
routingInput[d.key] = mods.filter((m) => m !== shardingModForDomain);
|
|
611
|
+
routingInput[auxKey] = [shardingModForDomain];
|
|
612
|
+
}
|
|
454
613
|
|
|
455
614
|
const queries = [
|
|
456
615
|
...discover<QueryDecl>(merged, isQueryDecl),
|
|
@@ -472,7 +631,13 @@ async function main(): Promise<void> {
|
|
|
472
631
|
...discover<CombinedDecl>(merged, isCombinedDecl),
|
|
473
632
|
...resolveNamed<CombinedDecl>(merged, d.combineds, "combineds"),
|
|
474
633
|
];
|
|
475
|
-
const
|
|
634
|
+
const workspaceInvariants = [
|
|
635
|
+
...discover<WorkspaceInvariantDecl>(merged, isWorkspaceInvariantDecl),
|
|
636
|
+
...resolveNamed<WorkspaceInvariantDecl>(merged, d.workspaceInvariants, "workspaceInvariants"),
|
|
637
|
+
];
|
|
638
|
+
const sums = [...resolveNamed<AnySum>(merged, d.sums, "sums"), ...shardingSums];
|
|
639
|
+
const mins = resolveNamed<AnyExtremum>(merged, d.mins, "mins");
|
|
640
|
+
const maxes = resolveNamed<AnyExtremum>(merged, d.maxes, "maxes");
|
|
476
641
|
const impureCapabilities = resolveNamed<ImpureCapabilityDecl>(
|
|
477
642
|
merged,
|
|
478
643
|
d.impureCapabilities,
|
|
@@ -500,6 +665,10 @@ async function main(): Promise<void> {
|
|
|
500
665
|
...(combineds.length > 0 ? { combineds } : {}),
|
|
501
666
|
...(impureCapabilities.length > 0 ? { impureCapabilities } : {}),
|
|
502
667
|
...(sums.length > 0 ? { sums } : {}),
|
|
668
|
+
...(mins.length > 0 ? { mins } : {}),
|
|
669
|
+
...(maxes.length > 0 ? { maxes } : {}),
|
|
670
|
+
...(workspaceTypes.length > 0 ? { workspaceTypes } : {}),
|
|
671
|
+
...(workspaceInvariants.length > 0 ? { workspaceInvariants } : {}),
|
|
503
672
|
...(spatials.length > 0 ? { spatials } : {}),
|
|
504
673
|
...(d.dartImports !== undefined && d.dartImports.length > 0
|
|
505
674
|
? { dartImports: d.dartImports }
|
|
@@ -511,6 +680,67 @@ async function main(): Promise<void> {
|
|
|
511
680
|
}),
|
|
512
681
|
);
|
|
513
682
|
|
|
683
|
+
// ── THE TAXONOMY GATE (sharding §10 slice 1 + slice 2; fail-closed, BEFORE
|
|
684
|
+
// identity emission): resolve the declared workspace types, run the homing
|
|
685
|
+
// walk (nearest packed axis via t.ref chains), the directive cross-home
|
|
686
|
+
// check, AND the slice-2 route derivation (per-directive home keys). A
|
|
687
|
+
// no-path / ambiguous / cross-home / unroutable law is a NAMED COMPILE
|
|
688
|
+
// ERROR here — never a domain silently recorded as identity-excluded.
|
|
689
|
+
const composed = domainModules[domainModules.length - 1]!;
|
|
690
|
+
if (composed.workspaceTypes !== undefined && composed.workspaceTypes.length > 0) {
|
|
691
|
+
try {
|
|
692
|
+
validateWorkspaceTaxonomyAndRoutes(composed);
|
|
693
|
+
} catch (e) {
|
|
694
|
+
fail((e as Error).message);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
// ── THE SCOPE GATE (sharding §5, slice 3; fail-closed): every `scoped(read, Ws)`
|
|
698
|
+
// must name a DECLARED, DEDICATED workspace type of THIS domain's taxonomy
|
|
699
|
+
// (the coordinator type that holds the subtotal rows). A scoped read in a
|
|
700
|
+
// taxonomy-free domain, or scoped to a packed type, is a NAMED compile error.
|
|
701
|
+
{
|
|
702
|
+
const declaredScopes = new Map<string, string>(); // type id -> mode
|
|
703
|
+
const globalAggIds = new Set<string>(); // §5.4 — replicated reference data
|
|
704
|
+
if (workspaceTypes.length > 0) {
|
|
705
|
+
try {
|
|
706
|
+
for (const [id, ty] of resolveWorkspaceTypes(workspaceTypes, d.name ?? d.key)) {
|
|
707
|
+
declaredScopes.set(id, ty.mode);
|
|
708
|
+
for (const g of ty.globalIds) globalAggIds.add(g);
|
|
709
|
+
}
|
|
710
|
+
} catch { /* already failed above */ }
|
|
711
|
+
}
|
|
712
|
+
for (const read of [...(composed.counts ?? []), ...(composed.sums ?? [])]) {
|
|
713
|
+
const scope = (read as { scope?: unknown }).scope;
|
|
714
|
+
if (scope === undefined) continue;
|
|
715
|
+
const id = (read as { id?: unknown }).id;
|
|
716
|
+
if (typeof scope !== "string" || !declaredScopes.has(scope)) {
|
|
717
|
+
fail(
|
|
718
|
+
`domain '${d.key}': read '${String(id)}' is scoped to '${String(scope)}', which is not a ` +
|
|
719
|
+
`declared workspace type of this domain — declare the taxonomy (workspaceType(...)) ` +
|
|
720
|
+
`in the same domain, or drop the scope.`,
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
if (declaredScopes.get(scope) !== "dedicated") {
|
|
724
|
+
fail(
|
|
725
|
+
`domain '${d.key}': read '${String(id)}' is scoped to PACKED type '${scope}' — estate ` +
|
|
726
|
+
`scope must name the DEDICATED coordinator type (the holon that holds the subtotal rows).`,
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
// §5.4 (slice 4): a `.global(...)` aggregate is coordinator-homed reference
|
|
730
|
+
// data REPLICATED to every shard — an estate-scoped tally over it would sum
|
|
731
|
+
// the SAME rows once per shard. Its home-scope read on the coordinator IS
|
|
732
|
+
// the estate value already.
|
|
733
|
+
const ofType = (read as { of?: unknown }).of;
|
|
734
|
+
if (typeof ofType === "string" && globalAggIds.has(ofType)) {
|
|
735
|
+
fail(
|
|
736
|
+
`domain '${d.key}': read '${String(id)}' is scoped over GLOBAL aggregate '${ofType}' — ` +
|
|
737
|
+
`global reference data is replicated to every shard, so an estate-scoped tally would ` +
|
|
738
|
+
`count it once per shard. Drop the scope: the coordinator's home-scope read IS the estate value.`,
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
514
744
|
// ── opt-in: ONE USD LAYER PER MODULE (Φ applied per module, not once over the
|
|
515
745
|
// composed fold) — the layered emission `usd_layers.ts` proves flattens back
|
|
516
746
|
// to the EXACT canonical IR (asserted below, fail-closed). Per-module decls
|
|
@@ -645,6 +875,10 @@ async function main(): Promise<void> {
|
|
|
645
875
|
...(eagerLump ? [] : [` routing: ${JSON.stringify(routing)},`]),
|
|
646
876
|
...(zodSeedNames.length > 0 ? [` seeds: [${zodSeedNames.join(", ")}],`] : []),
|
|
647
877
|
`});`,
|
|
878
|
+
// ROUTE-TAGGED IN-PLAN MINTS (sharding §4, slice 3): wraps globalThis.plan +
|
|
879
|
+
// globalThis.nomos.mint at top-level eval — pre-freeze, after registerEngine's
|
|
880
|
+
// own assignments. Empty (and therefore byte-absent) for taxonomy-free packages.
|
|
881
|
+
...entryMintInstalls,
|
|
648
882
|
``,
|
|
649
883
|
].join("\n");
|
|
650
884
|
const entryPath = path.join(outDir, ".nomos_entry.ts");
|
|
@@ -882,6 +1116,13 @@ async function main(): Promise<void> {
|
|
|
882
1116
|
console.log(` identity ${rel(manifestsPath)} (${Object.keys(identity.manifests).length} domain(s)${identity.excluded.length ? `, ${identity.excluded.length} EXCLUDED (palette gap)` : ""})`);
|
|
883
1117
|
for (const [dom, h] of Object.entries(identity.hashes)) console.log(` ${dom.padEnd(20)} ${h}`);
|
|
884
1118
|
for (const ex of identity.excluded) console.log(` EXCLUDED ${ex.domain}: ${ex.reason}`);
|
|
1119
|
+
for (const m of domainModules) {
|
|
1120
|
+
if (m.workspaceTypes !== undefined && m.workspaceTypes.length > 0) {
|
|
1121
|
+
console.log(
|
|
1122
|
+
` taxonomy ${m.domain ?? m.name}: ${m.workspaceTypes.length} workspace type(s) — homing derived from t.ref chains; birth lanes typed in the client`,
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
885
1126
|
console.log(` client ${rel(clientPath)} (typed TS client — payloads, read models, query accessors)`);
|
|
886
1127
|
if (proofPath !== undefined) {
|
|
887
1128
|
console.log(` proof ${rel(proofPath)} (a runnable e2e GENERATED from your law — run: githolon proof)`);
|
package/src/engine_entry.ts
CHANGED
|
@@ -59,6 +59,7 @@ import type { WireEvent, WireHlc } from "./wire.js";
|
|
|
59
59
|
import type { DerivedDecl } from "./derived.js";
|
|
60
60
|
import type { CombinedDecl } from "./combined.js";
|
|
61
61
|
import type { InvariantBody, InvariantEvidence, InvariantVerdict } from "./relation.js";
|
|
62
|
+
import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
|
|
62
63
|
import type { QueryRow } from "./report.js";
|
|
63
64
|
|
|
64
65
|
/** One bundled domain module: a bag of named exports the entry scans by SHAPE. */
|
|
@@ -91,6 +92,8 @@ export interface EngineRouting {
|
|
|
91
92
|
readonly combinedOf?: Record<string, readonly string[]>;
|
|
92
93
|
readonly aggregateInvariantOf?: Record<string, readonly string[]>;
|
|
93
94
|
readonly relationOf?: Record<string, readonly string[]>;
|
|
95
|
+
/** workspace-invariant id → declaring domain keys (#266 — the gate's reads/assert dispatches). */
|
|
96
|
+
readonly workspaceInvariantOf?: Record<string, readonly string[]>;
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
/** The one declarative input: dispatch key → ORDERED module list (later wins). */
|
|
@@ -216,6 +219,8 @@ interface DomainSlice {
|
|
|
216
219
|
invariants: Map<string, InvariantBody>;
|
|
217
220
|
/** aggregate type → aggregate-invariant body. */
|
|
218
221
|
aggInvariants: Map<string, AggregateInvariantFn>;
|
|
222
|
+
/** workspace-invariant id → its declaration (#266 — `reads`/`assert` bodies ship HERE). */
|
|
223
|
+
wsInvariants: Map<string, WorkspaceInvariantDecl>;
|
|
219
224
|
}
|
|
220
225
|
|
|
221
226
|
/** Build one domain's slice from its merged module exports (the original scans). */
|
|
@@ -288,7 +293,28 @@ function buildDomainSlice(mod: DomainModuleExports): DomainSlice {
|
|
|
288
293
|
}
|
|
289
294
|
}
|
|
290
295
|
|
|
291
|
-
|
|
296
|
+
// ── workspace-invariant id → declaration — from the SAME exports (#266, by shape) ──
|
|
297
|
+
const wsInvariants = new Map<string, WorkspaceInvariantDecl>();
|
|
298
|
+
for (const v of Object.values(mod)) {
|
|
299
|
+
if (
|
|
300
|
+
v &&
|
|
301
|
+
typeof v === "object" &&
|
|
302
|
+
(v as { __isWorkspaceInvariant?: boolean }).__isWorkspaceInvariant === true
|
|
303
|
+
) {
|
|
304
|
+
const w = v as WorkspaceInvariantDecl;
|
|
305
|
+
if (typeof w.reads !== "function" || typeof w.assert !== "function") {
|
|
306
|
+
// A declaration MUST ship both executable halves — fail-closed at boot, exactly
|
|
307
|
+
// like a relation/aggregate invariant with a missing body.
|
|
308
|
+
throw new Error(
|
|
309
|
+
`engine bundle: workspace invariant "${w.id}" declares no executable reads/assert ` +
|
|
310
|
+
`body — the gate would have nothing to evaluate (docs/workspace_invariant.md).`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
wsInvariants.set(w.id, w);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { registry, deriveds, combineds, invariants, aggInvariants, wsInvariants };
|
|
292
318
|
}
|
|
293
319
|
|
|
294
320
|
/**
|
|
@@ -306,6 +332,7 @@ export function collectEngineRouting(
|
|
|
306
332
|
const combinedOf: Record<string, string[]> = {};
|
|
307
333
|
const aggregateInvariantOf: Record<string, string[]> = {};
|
|
308
334
|
const relationOf: Record<string, string[]> = {};
|
|
335
|
+
const workspaceInvariantOf: Record<string, string[]> = {};
|
|
309
336
|
const add = (map: Record<string, string[]>, key: string, domainName: string) => {
|
|
310
337
|
const list = map[key] ?? (map[key] = []);
|
|
311
338
|
if (!list.includes(domainName)) list.push(domainName);
|
|
@@ -316,8 +343,9 @@ export function collectEngineRouting(
|
|
|
316
343
|
for (const type of slice.combineds.keys()) add(combinedOf, type, domainName);
|
|
317
344
|
for (const type of slice.aggInvariants.keys()) add(aggregateInvariantOf, type, domainName);
|
|
318
345
|
for (const relationId of slice.invariants.keys()) add(relationOf, relationId, domainName);
|
|
346
|
+
for (const wsInvariantId of slice.wsInvariants.keys()) add(workspaceInvariantOf, wsInvariantId, domainName);
|
|
319
347
|
}
|
|
320
|
-
return { derivedOf, combinedOf, aggregateInvariantOf, relationOf };
|
|
348
|
+
return { derivedOf, combinedOf, aggregateInvariantOf, relationOf, workspaceInvariantOf };
|
|
321
349
|
}
|
|
322
350
|
|
|
323
351
|
/**
|
|
@@ -458,6 +486,80 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
458
486
|
return JSON.stringify(verdict);
|
|
459
487
|
}
|
|
460
488
|
|
|
489
|
+
/** Every domain key whose merged module declares a given workspace invariant (routed when lazy). */
|
|
490
|
+
function wsInvariantDomains(id?: string): readonly string[] {
|
|
491
|
+
if (routing.workspaceInvariantOf !== undefined && id !== undefined) {
|
|
492
|
+
return routing.workspaceInvariantOf[id] ?? [];
|
|
493
|
+
}
|
|
494
|
+
return domainNames;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** Resolve ONE declared workspace invariant by id (later domain wins — the one fold order). */
|
|
498
|
+
function wsInvariantById(id: string): WorkspaceInvariantDecl | undefined {
|
|
499
|
+
let found: WorkspaceInvariantDecl | undefined;
|
|
500
|
+
for (const domainName of wsInvariantDomains(id)) {
|
|
501
|
+
const hit = sliceOf(domainName)?.wsInvariants.get(id);
|
|
502
|
+
if (hit !== undefined) found = hit;
|
|
503
|
+
}
|
|
504
|
+
return found;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ── the three WORKSPACE-INVARIANT dispatches (#266 — wire shapes are the gate oracle's
|
|
508
|
+
// contract, EXACT: see admission-peer EngineWorkspaceInvariant + plan_workspace_invariant*) ──
|
|
509
|
+
|
|
510
|
+
/** `{workspaceInvariantList:{}}` → the DECLARED set `[{id,on},…]` sorted by id (discovery). */
|
|
511
|
+
function planWorkspaceInvariantList(): string {
|
|
512
|
+
const declared = new Map<string, { id: string; on: string }>();
|
|
513
|
+
const domains =
|
|
514
|
+
routing.workspaceInvariantOf !== undefined
|
|
515
|
+
? [...new Set(Object.values(routing.workspaceInvariantOf).flat())]
|
|
516
|
+
: domainNames;
|
|
517
|
+
for (const domainName of domains) {
|
|
518
|
+
const slice = sliceOf(domainName);
|
|
519
|
+
if (slice === undefined) continue;
|
|
520
|
+
for (const w of slice.wsInvariants.values()) declared.set(w.id, { id: w.id, on: w.on });
|
|
521
|
+
}
|
|
522
|
+
return JSON.stringify([...declared.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** `{workspaceInvariantReads:{id}, payload}` → the bounded ref-set `[{name,aggregate,id},…]`. */
|
|
526
|
+
function planWorkspaceInvariantReads(job: {
|
|
527
|
+
intent?: { workspaceInvariantReads?: { id?: string }; payload?: unknown };
|
|
528
|
+
}): string {
|
|
529
|
+
const id = job.intent?.workspaceInvariantReads?.id;
|
|
530
|
+
if (typeof id !== "string") {
|
|
531
|
+
throw new Error(
|
|
532
|
+
`engine workspaceInvariantReads: job.intent.workspaceInvariantReads must carry {id}; got ${JSON.stringify(job.intent)}`,
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
const decl = wsInvariantById(id);
|
|
536
|
+
if (decl === undefined) {
|
|
537
|
+
throw new Error(`engine workspaceInvariantReads: no workspace invariant registered for "${id}"`);
|
|
538
|
+
}
|
|
539
|
+
const intent = (job.intent?.payload ?? {}) as Record<string, unknown>;
|
|
540
|
+
return JSON.stringify(decl.reads({ intent }));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/** `{workspaceInvariant:{id,on}, payload?}` + priorState (named snapshots) → {accept}|{reject}. */
|
|
544
|
+
function planWorkspaceInvariantAssert(job: {
|
|
545
|
+
intent?: { workspaceInvariant?: { id?: string }; payload?: unknown };
|
|
546
|
+
priorState?: Record<string, unknown>;
|
|
547
|
+
}): string {
|
|
548
|
+
const id = job.intent?.workspaceInvariant?.id;
|
|
549
|
+
if (typeof id !== "string") {
|
|
550
|
+
throw new Error(
|
|
551
|
+
`engine workspaceInvariant: job.intent.workspaceInvariant must carry {id}; got ${JSON.stringify(job.intent)}`,
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
const decl = wsInvariantById(id);
|
|
555
|
+
if (decl === undefined) {
|
|
556
|
+
throw new Error(`engine workspaceInvariant: no workspace invariant registered for "${id}"`);
|
|
557
|
+
}
|
|
558
|
+
const snapshots = (job.priorState ?? {}) as Record<string, Record<string, unknown>>;
|
|
559
|
+
const intent = (job.intent?.payload ?? {}) as Record<string, unknown>;
|
|
560
|
+
return JSON.stringify(decl.assert(snapshots, { intent }));
|
|
561
|
+
}
|
|
562
|
+
|
|
461
563
|
function planDerive(job: {
|
|
462
564
|
intent?: { derive?: { of?: string } };
|
|
463
565
|
priorState?: Record<string, unknown>;
|
|
@@ -527,8 +629,26 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
527
629
|
queryRows?: unknown[];
|
|
528
630
|
priorState?: Record<string, unknown>;
|
|
529
631
|
}): string {
|
|
530
|
-
// Branch order mirrors emit_engine.ts:
|
|
531
|
-
// combine → report (the gate's most recent dispatch key wins
|
|
632
|
+
// Branch order mirrors emit_engine.ts: workspace-invariant trio → aggregateInvariant →
|
|
633
|
+
// invariant → derive → combine → report (the gate's most recent dispatch key wins).
|
|
634
|
+
const wsJob = job as {
|
|
635
|
+
intent?: {
|
|
636
|
+
workspaceInvariantList?: unknown;
|
|
637
|
+
workspaceInvariantReads?: { id?: string };
|
|
638
|
+
workspaceInvariant?: { id?: string };
|
|
639
|
+
payload?: unknown;
|
|
640
|
+
};
|
|
641
|
+
priorState?: Record<string, unknown>;
|
|
642
|
+
};
|
|
643
|
+
if (wsJob.intent?.workspaceInvariantList !== undefined) {
|
|
644
|
+
return planWorkspaceInvariantList();
|
|
645
|
+
}
|
|
646
|
+
if (wsJob.intent?.workspaceInvariantReads !== undefined) {
|
|
647
|
+
return planWorkspaceInvariantReads(wsJob);
|
|
648
|
+
}
|
|
649
|
+
if (wsJob.intent?.workspaceInvariant !== undefined) {
|
|
650
|
+
return planWorkspaceInvariantAssert(wsJob);
|
|
651
|
+
}
|
|
532
652
|
if (job.intent?.aggregateInvariant !== undefined) {
|
|
533
653
|
return planAggInvariant(
|
|
534
654
|
job as { intent?: { aggregateInvariant?: { of?: string } }; priorState?: Record<string, unknown> },
|
|
@@ -75,9 +75,16 @@ export type WorkspaceInvariantReads = (ctx: { intent: Record<string, unknown> })
|
|
|
75
75
|
/**
|
|
76
76
|
* The post-apply predicate over the resolved snapshots, keyed by each ref's binding `name`.
|
|
77
77
|
* PURE over the named map (no clock, no IO, no live reads) — runs in the sealed engine.
|
|
78
|
+
*
|
|
79
|
+
* The OPTIONAL second argument carries the authored intent payload (`ctx.intent`) — the
|
|
80
|
+
* SAME committed fact the `reads` body derived its ref-set from (never a live read), so a
|
|
81
|
+
* predicate may compare a payload-carried value against the resolved snapshots (the
|
|
82
|
+
* wrong-home homing invariant compares the payload's home key against the shard's own
|
|
83
|
+
* declared identity). Predicates that ignore it are unchanged (#266 signature, additive).
|
|
78
84
|
*/
|
|
79
85
|
export type WorkspaceInvariantAssert = (
|
|
80
86
|
snapshots: Record<string, Record<string, unknown>>,
|
|
87
|
+
ctx?: { intent: Record<string, unknown> },
|
|
81
88
|
) => WorkspaceInvariantVerdict;
|
|
82
89
|
|
|
83
90
|
/** A complete, registered workspace-invariant declaration (the `.assert` terminal yields this). */
|
package/src/index.ts
CHANGED
|
@@ -52,6 +52,12 @@ export {
|
|
|
52
52
|
type StrikeOp,
|
|
53
53
|
} from "./ops.js";
|
|
54
54
|
export { directive, type Directive, type ReferentialMarker } from "./directive.js";
|
|
55
|
+
// FIRST-CLASS WORKSPACE TYPES (sharding §10 RATIFIED, slice 1) live on the SUBPATH
|
|
56
|
+
// `@githolon/dsl/workspace-type` (`workspaceType("estate").root(Estate).hasMany(...)`),
|
|
57
|
+
// NOT this runtime barrel — the barrel is bundled into EVERY tenant's engine lump, and
|
|
58
|
+
// a taxonomy-free domain's package bytes must not move (the hash-stability law; same
|
|
59
|
+
// reason `build-package` is subpath-only). The decls are compile-lane: the manifest
|
|
60
|
+
// lowering + homing walk + derived birth lanes consume them; the sealed engine never does.
|
|
55
61
|
// THE AGGREGATE AUTHORING SURFACE (Nomos owns every birth): `create(agg)` mints + records and returns
|
|
56
62
|
// a DDD-fluent `AggregateRef`; the dev `.set`/`.add`(collection)/`.setEntry`(map)/`.relate`(reference)
|
|
57
63
|
// — naming no id. A directive's `.plan` calls these and returns `[]` (the recorded graph is the write
|
package/src/manifest.ts
CHANGED
|
@@ -43,6 +43,11 @@ import type { DerivedDecl } from "./derived.js";
|
|
|
43
43
|
import type { CombinedDecl } from "./combined.js";
|
|
44
44
|
import type { CertifiedReadDecl } from "./certified_read.js";
|
|
45
45
|
import type { RelationDecl } from "./relation.js";
|
|
46
|
+
import { type CanonicalWorkspaceType } from "./workspace_type.js";
|
|
47
|
+
import {
|
|
48
|
+
canonicalTaxonomyFragment,
|
|
49
|
+
type CanonicalDirectiveRoute,
|
|
50
|
+
} from "./workspace_routing.js";
|
|
46
51
|
|
|
47
52
|
/** One captured `[fieldName, zodKind]` payload field, with optionality. */
|
|
48
53
|
export interface CanonicalPayloadField {
|
|
@@ -221,6 +226,13 @@ export interface CanonicalCount {
|
|
|
221
226
|
readonly where?: CanonicalPred;
|
|
222
227
|
/** The group-by field, in DECLARED form. OMITTED for a grand-total count. */
|
|
223
228
|
readonly by?: string;
|
|
229
|
+
/**
|
|
230
|
+
* THE ESTATE SCOPE (sharding §5, slice 3): the workspace-type id this read is
|
|
231
|
+
* lifted to via `scoped(read, Ws)` — its per-shard values ride the §5.2 delta
|
|
232
|
+
* lane as gate-recomputed coordinator subtotals. OMITTED for home-scope reads
|
|
233
|
+
* (every pre-slice-3 count is byte-identical — the `cap(n)` discipline).
|
|
234
|
+
*/
|
|
235
|
+
readonly scope?: string;
|
|
224
236
|
}
|
|
225
237
|
|
|
226
238
|
/**
|
|
@@ -263,6 +275,8 @@ export interface CanonicalSum {
|
|
|
263
275
|
readonly where?: CanonicalPred;
|
|
264
276
|
/** The group-by field. OMITTED for a grand-total sum. */
|
|
265
277
|
readonly by?: string;
|
|
278
|
+
/** THE ESTATE SCOPE (sharding §5, slice 3) — see {@link CanonicalCount.scope}. */
|
|
279
|
+
readonly scope?: string;
|
|
266
280
|
}
|
|
267
281
|
|
|
268
282
|
/**
|
|
@@ -428,6 +442,29 @@ export interface CanonicalManifest {
|
|
|
428
442
|
* (non-optional at both TS and manifest levels), optional predicate, and `limit`.
|
|
429
443
|
*/
|
|
430
444
|
readonly orderedReads?: CanonicalOrderedRead[];
|
|
445
|
+
/**
|
|
446
|
+
* The domain's FIRST-CLASS WORKSPACE TYPES (sharding §10 RATIFIED, slice 1), SORTED
|
|
447
|
+
* by id. OMITTED ENTIRELY when the domain declares none — so a taxonomy-free domain
|
|
448
|
+
* is byte-identical to before this key existed (pinned golden hashes UNCHANGED; the
|
|
449
|
+
* `cap(n)` discipline). The taxonomy IS law: a type's root/mode/children/globals/
|
|
450
|
+
* pool/cap all move the domain hash (per-type optional keys omitted-when-absent).
|
|
451
|
+
*/
|
|
452
|
+
readonly workspaceTypes?: CanonicalWorkspaceType[];
|
|
453
|
+
/**
|
|
454
|
+
* The DERIVED HOMING TABLE (aggregate wire id → workspace-type id) — present iff
|
|
455
|
+
* {@link workspaceTypes} is. Derived (never hand-written) by the fail-closed homing
|
|
456
|
+
* walk over the aggregates' `t.ref` chains, and HASH-BEARING: a homing move is a
|
|
457
|
+
* law change (placement of FACTS is custody, but which chain a fact is BORN into
|
|
458
|
+
* is law — the shard gate's `wrong-home` invariant pins to this table).
|
|
459
|
+
*/
|
|
460
|
+
readonly homes?: Record<string, string>;
|
|
461
|
+
/**
|
|
462
|
+
* THE DERIVED DIRECTIVE ROUTING TABLE (sharding slice 2) — present only when the
|
|
463
|
+
* domain declares a taxonomy with at least one packed-homed directive, SORTED by
|
|
464
|
+
* directive id, HASH-BEARING (the client routes by it; the shard gate's typed
|
|
465
|
+
* `wrong-home` refusal pins to it). OMITTED ENTIRELY otherwise (hash-stable).
|
|
466
|
+
*/
|
|
467
|
+
readonly routes?: CanonicalDirectiveRoute[];
|
|
431
468
|
}
|
|
432
469
|
|
|
433
470
|
/** Minimal structural view of Zod 4 schema internals we read deterministically. */
|
|
@@ -690,15 +727,16 @@ function canonicalCounts(
|
|
|
690
727
|
): { counts?: CanonicalCount[] } {
|
|
691
728
|
if (declared === undefined || declared.length === 0) return {};
|
|
692
729
|
const counts: CanonicalCount[] = [...declared]
|
|
693
|
-
.map(finishCount)
|
|
694
|
-
.map((c) => ({
|
|
730
|
+
.map((raw) => ({ finished: finishCount(raw), scope: (raw as { scope?: unknown }).scope }))
|
|
731
|
+
.map(({ finished: c, scope }) => ({
|
|
695
732
|
id: c.id,
|
|
696
733
|
of: c.of,
|
|
697
|
-
// OMIT-WHEN-ABSENT: `where` and `
|
|
698
|
-
// predicate-free
|
|
699
|
-
// (hash-stable — the same discipline as every other optional
|
|
734
|
+
// OMIT-WHEN-ABSENT: `where`, `by` and `scope` are omitted when not declared so
|
|
735
|
+
// a predicate-free / grand-total / home-scope count is byte-identical to the
|
|
736
|
+
// legacy form (hash-stable — the same discipline as every other optional key).
|
|
700
737
|
...(c.where !== undefined ? { where: c.where } : {}),
|
|
701
738
|
...(c.by !== undefined ? { by: c.by } : {}),
|
|
739
|
+
...(typeof scope === "string" ? { scope } : {}),
|
|
702
740
|
}))
|
|
703
741
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
704
742
|
return { counts };
|
|
@@ -733,13 +771,14 @@ function canonicalSpatials(
|
|
|
733
771
|
function canonicalSums(declared: AnySum[] | undefined): { sums?: CanonicalSum[] } {
|
|
734
772
|
if (declared === undefined || declared.length === 0) return {};
|
|
735
773
|
const sums: CanonicalSum[] = [...declared]
|
|
736
|
-
.map(finishSum)
|
|
737
|
-
.map((s) => ({
|
|
774
|
+
.map((raw) => ({ finished: finishSum(raw), scope: (raw as { scope?: unknown }).scope }))
|
|
775
|
+
.map(({ finished: s, scope }) => ({
|
|
738
776
|
id: s.id,
|
|
739
777
|
of: s.of,
|
|
740
778
|
sumField: s.sumField,
|
|
741
779
|
...(s.where !== undefined ? { where: s.where } : {}),
|
|
742
780
|
...(s.by !== undefined ? { by: s.by } : {}),
|
|
781
|
+
...(typeof scope === "string" ? { scope } : {}),
|
|
743
782
|
}))
|
|
744
783
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
745
784
|
return { sums };
|
|
@@ -901,6 +940,16 @@ export function domainManifest(mod: DomainModule): CanonicalManifest {
|
|
|
901
940
|
...(canonicalExtrema(mod.mins, "mins") as { mins?: CanonicalExtremum[] }),
|
|
902
941
|
...(canonicalExtrema(mod.maxes, "maxes") as { maxes?: CanonicalExtremum[] }),
|
|
903
942
|
...canonicalOrderedReads(mod.orderedReads),
|
|
943
|
+
// Omit-when-empty: a taxonomy-free domain contributes NO taxonomy key (hash-
|
|
944
|
+
// stable). When declared, this RUNS the fail-closed homing walk AND the slice-2
|
|
945
|
+
// route derivation — an unhomeable or unroutable taxonomy never produces an
|
|
946
|
+
// identity (workspace_type.ts + workspace_routing.ts).
|
|
947
|
+
...canonicalTaxonomyFragment({
|
|
948
|
+
name: mod.name,
|
|
949
|
+
aggregates: mod.aggregates,
|
|
950
|
+
directives: mod.directives,
|
|
951
|
+
...(mod.workspaceTypes !== undefined ? { workspaceTypes: mod.workspaceTypes } : {}),
|
|
952
|
+
}),
|
|
904
953
|
};
|
|
905
954
|
}
|
|
906
955
|
|