@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/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 `by` are omitted when not declared so a
698
- // predicate-free or grand-total count is byte-identical to the legacy form
699
- // (hash-stable — the same discipline as every other optional manifest key).
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
 
package/src/usd.ts CHANGED
@@ -170,6 +170,24 @@ export interface UsdLayer {
170
170
  * same discipline as `queries`/`references`/`reads`/`emits`).
171
171
  */
172
172
  readonly variantSets?: Record<string, Record<string, UsdPrim[]>>;
173
+ /**
174
+ * THE INVARIANT-GATE DECLARATION (#41 — "the law is the era"). Present IFF the layer's
175
+ * module declares ≥1 invariant (aggregate or workspace): the law's EXPLICIT opt-in to
176
+ * invariant EXECUTION at the one gate. The wasm gate runs the engine-backed invariant
177
+ * oracles for an intent only when the law resolved for that intent carries this key —
178
+ * every bundle compiled before #41 (whose declared invariants the gate held as hashed-
179
+ * but-inert law) lacks it, so chains sealed under the inert gate replay green forever.
180
+ * OMITTED (`undefined`) for invariant-free law — byte-identical hash, same discipline
181
+ * as `queries`/`references`. Hand-adding it to a foreign bundle only opts IN to
182
+ * stricter checking; removing it makes new law, for that law's own chains (status quo).
183
+ */
184
+ readonly nomosInvariantGate?: {
185
+ readonly v: 1;
186
+ /** Aggregate types whose declared `invariant` bodies the gate executes, sorted. */
187
+ readonly aggregates?: string[];
188
+ /** Declared workspace invariants `{id, on}` the gate executes, sorted by id. */
189
+ readonly workspaceInvariants?: { id: string; on: string }[];
190
+ };
173
191
  }
174
192
 
175
193
  /** The reserved variant name declaring a set's DEFAULT (USD has a default variant). */
@@ -239,12 +257,31 @@ export function emitUsd(
239
257
  // which already applies omit-when-empty: `queries` is present here ONLY when the
240
258
  // module declared them, so a query-free layer is byte-identical to before.
241
259
  const manifest = domainManifest(l.module);
260
+ // THE INVARIANT-GATE DECLARATION (#41): presence-only, derived from the SAME module
261
+ // the lump's executable bodies come from — sorted, omit-when-empty (hash-stable for
262
+ // invariant-free law; see the `UsdLayer.nomosInvariantGate` banner for the era rule).
263
+ const invariantAggregates = l.module.aggregates
264
+ .filter((a) => a.hasInvariant === true)
265
+ .map((a) => a.id)
266
+ .sort();
267
+ const wsInvariants = [...(l.module.workspaceInvariants ?? [])]
268
+ .map((w) => ({ id: w.id, on: w.on }))
269
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
242
270
  return {
243
271
  path: l.path,
244
272
  prims: encodeModuleToPrims(l.path, l.module),
245
273
  ...(manifest.queries !== undefined ? { queries: manifest.queries } : {}),
246
274
  ...(manifest.deriveds !== undefined ? { deriveds: manifest.deriveds } : {}),
247
275
  ...(manifest.combineds !== undefined ? { combineds: manifest.combineds } : {}),
276
+ ...(invariantAggregates.length > 0 || wsInvariants.length > 0
277
+ ? {
278
+ nomosInvariantGate: {
279
+ v: 1 as const,
280
+ ...(invariantAggregates.length > 0 ? { aggregates: invariantAggregates } : {}),
281
+ ...(wsInvariants.length > 0 ? { workspaceInvariants: wsInvariants } : {}),
282
+ },
283
+ }
284
+ : {}),
248
285
  };
249
286
  })
250
287
  .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));