@githolon/dsl 0.2.3 → 0.4.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
  /**
@@ -320,6 +334,12 @@ export interface CanonicalExtremum {
320
334
  readonly where?: CanonicalPred;
321
335
  /** The group-by field. OMITTED for a grand-total extremum. */
322
336
  readonly by?: string;
337
+ /** THE ESTATE SCOPE (sharding §5, slice 8) — see {@link CanonicalCount.scope}:
338
+ * per-shard extremums ride the §5.2 delta lane as gate-adjudicated coordinator
339
+ * subtotals (extremize-on-monotone; retractions re-derive). OMITTED for
340
+ * home-scope extremums, so an unscoped declaration is byte-identical to before
341
+ * this key existed (golden hashes UNCHANGED). */
342
+ readonly scope?: string;
323
343
  }
324
344
 
325
345
  /**
@@ -428,6 +448,29 @@ export interface CanonicalManifest {
428
448
  * (non-optional at both TS and manifest levels), optional predicate, and `limit`.
429
449
  */
430
450
  readonly orderedReads?: CanonicalOrderedRead[];
451
+ /**
452
+ * The domain's FIRST-CLASS WORKSPACE TYPES (sharding §10 RATIFIED, slice 1), SORTED
453
+ * by id. OMITTED ENTIRELY when the domain declares none — so a taxonomy-free domain
454
+ * is byte-identical to before this key existed (pinned golden hashes UNCHANGED; the
455
+ * `cap(n)` discipline). The taxonomy IS law: a type's root/mode/children/globals/
456
+ * pool/cap all move the domain hash (per-type optional keys omitted-when-absent).
457
+ */
458
+ readonly workspaceTypes?: CanonicalWorkspaceType[];
459
+ /**
460
+ * The DERIVED HOMING TABLE (aggregate wire id → workspace-type id) — present iff
461
+ * {@link workspaceTypes} is. Derived (never hand-written) by the fail-closed homing
462
+ * walk over the aggregates' `t.ref` chains, and HASH-BEARING: a homing move is a
463
+ * law change (placement of FACTS is custody, but which chain a fact is BORN into
464
+ * is law — the shard gate's `wrong-home` invariant pins to this table).
465
+ */
466
+ readonly homes?: Record<string, string>;
467
+ /**
468
+ * THE DERIVED DIRECTIVE ROUTING TABLE (sharding slice 2) — present only when the
469
+ * domain declares a taxonomy with at least one packed-homed directive, SORTED by
470
+ * directive id, HASH-BEARING (the client routes by it; the shard gate's typed
471
+ * `wrong-home` refusal pins to it). OMITTED ENTIRELY otherwise (hash-stable).
472
+ */
473
+ readonly routes?: CanonicalDirectiveRoute[];
431
474
  }
432
475
 
433
476
  /** Minimal structural view of Zod 4 schema internals we read deterministically. */
@@ -690,15 +733,16 @@ function canonicalCounts(
690
733
  ): { counts?: CanonicalCount[] } {
691
734
  if (declared === undefined || declared.length === 0) return {};
692
735
  const counts: CanonicalCount[] = [...declared]
693
- .map(finishCount)
694
- .map((c) => ({
736
+ .map((raw) => ({ finished: finishCount(raw), scope: (raw as { scope?: unknown }).scope }))
737
+ .map(({ finished: c, scope }) => ({
695
738
  id: c.id,
696
739
  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).
740
+ // OMIT-WHEN-ABSENT: `where`, `by` and `scope` are omitted when not declared so
741
+ // a predicate-free / grand-total / home-scope count is byte-identical to the
742
+ // legacy form (hash-stable — the same discipline as every other optional key).
700
743
  ...(c.where !== undefined ? { where: c.where } : {}),
701
744
  ...(c.by !== undefined ? { by: c.by } : {}),
745
+ ...(typeof scope === "string" ? { scope } : {}),
702
746
  }))
703
747
  .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
704
748
  return { counts };
@@ -733,13 +777,14 @@ function canonicalSpatials(
733
777
  function canonicalSums(declared: AnySum[] | undefined): { sums?: CanonicalSum[] } {
734
778
  if (declared === undefined || declared.length === 0) return {};
735
779
  const sums: CanonicalSum[] = [...declared]
736
- .map(finishSum)
737
- .map((s) => ({
780
+ .map((raw) => ({ finished: finishSum(raw), scope: (raw as { scope?: unknown }).scope }))
781
+ .map(({ finished: s, scope }) => ({
738
782
  id: s.id,
739
783
  of: s.of,
740
784
  sumField: s.sumField,
741
785
  ...(s.where !== undefined ? { where: s.where } : {}),
742
786
  ...(s.by !== undefined ? { by: s.by } : {}),
787
+ ...(typeof scope === "string" ? { scope } : {}),
743
788
  }))
744
789
  .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
745
790
  return { sums };
@@ -802,14 +847,17 @@ function canonicalExtrema(
802
847
  ): { mins?: CanonicalExtremum[] } | { maxes?: CanonicalExtremum[] } {
803
848
  if (declared === undefined || declared.length === 0) return {};
804
849
  const items: CanonicalExtremum[] = [...declared]
805
- .map(finishExtremum)
806
- .map((e) => ({
850
+ .map((raw) => ({ finished: finishExtremum(raw), scope: (raw as { scope?: unknown }).scope }))
851
+ .map(({ finished: e, scope }) => ({
807
852
  id: e.id,
808
853
  kind: e.kind,
809
854
  of: e.of,
810
855
  valueField: e.valueField,
811
856
  ...(e.where !== undefined ? { where: e.where } : {}),
812
857
  ...(e.by !== undefined ? { by: e.by } : {}),
858
+ // OMIT-WHEN-ABSENT (the canonicalSums discipline): a home-scope extremum is
859
+ // byte-identical to before the scope key existed — hash-stable by default.
860
+ ...(typeof scope === "string" ? { scope } : {}),
813
861
  }))
814
862
  .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
815
863
  return { [keyName]: items };
@@ -901,6 +949,16 @@ export function domainManifest(mod: DomainModule): CanonicalManifest {
901
949
  ...(canonicalExtrema(mod.mins, "mins") as { mins?: CanonicalExtremum[] }),
902
950
  ...(canonicalExtrema(mod.maxes, "maxes") as { maxes?: CanonicalExtremum[] }),
903
951
  ...canonicalOrderedReads(mod.orderedReads),
952
+ // Omit-when-empty: a taxonomy-free domain contributes NO taxonomy key (hash-
953
+ // stable). When declared, this RUNS the fail-closed homing walk AND the slice-2
954
+ // route derivation — an unhomeable or unroutable taxonomy never produces an
955
+ // identity (workspace_type.ts + workspace_routing.ts).
956
+ ...canonicalTaxonomyFragment({
957
+ name: mod.name,
958
+ aggregates: mod.aggregates,
959
+ directives: mod.directives,
960
+ ...(mod.workspaceTypes !== undefined ? { workspaceTypes: mod.workspaceTypes } : {}),
961
+ }),
904
962
  };
905
963
  }
906
964
 
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));