@githolon/dsl 0.55.1 → 0.57.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.
@@ -54,10 +54,13 @@ export interface DomainInterface {
54
54
  readonly directives: Record<string, DirectivePayloadShape>;
55
55
  /** keyed by AGGREGATE SID. */
56
56
  readonly aggregates: Record<string, AggregateReadShape>;
57
- /** ATTESTED READS (slice B): directive id → its declared `(queryId, from)` trust edges —
58
- * the cross-workspace dependencies this law consumes on its write path. OMITTED when the
59
- * law declares none (the era rule). */
60
- readonly foreignReads?: Record<string, { queryId: string; from: string }[]>;
57
+ /** ATTESTED READS (slice B + dynamic policies): directive id → its declared foreign-read edges —
58
+ * a STATIC `{queryId, from}` edge or a dynamic `{queryId, policy, fromPayload}` edge — the
59
+ * cross-workspace dependencies this law consumes on its write path. OMITTED when none (era rule). */
60
+ readonly foreignReads?: Record<
61
+ string,
62
+ { queryId: string; from?: string; policy?: string; fromPayload?: string }[]
63
+ >;
61
64
  }
62
65
 
63
66
  /** The verdict: compatible iff nothing is missing and nothing is type-incompatible.
@@ -161,8 +164,9 @@ export function domainInterface(mod: DomainModule, stableIds?: StableIds): Domai
161
164
  Object.assign(aggregates, sortedRec(aggEntries));
162
165
  // ATTESTED READS (slice B): the declared trust edges — so shape-compat + client codegen can
163
166
  // SEE the cross-workspace dependency (typed clients don't call it; it's write-path-internal).
164
- const foreignReadEntries: Array<[string, { queryId: string; from: string }[]]> = [];
165
- for (const d of (mod.directives ?? []) as Array<{ id?: unknown; declaredForeignReads?: { queryId: string; from: string }[] }>) {
167
+ type ForeignEdge = { queryId: string; from?: string; policy?: string; fromPayload?: string };
168
+ const foreignReadEntries: Array<[string, ForeignEdge[]]> = [];
169
+ for (const d of (mod.directives ?? []) as Array<{ id?: unknown; declaredForeignReads?: ForeignEdge[] }>) {
166
170
  if (!d || typeof d.id !== "string") continue;
167
171
  if (d.declaredForeignReads !== undefined && d.declaredForeignReads.length > 0) {
168
172
  foreignReadEntries.push([d.id, d.declaredForeignReads.map((e) => ({ ...e }))]);
package/src/usd.ts CHANGED
@@ -44,6 +44,7 @@ import type { DomainModule } from "./codegen_dart.js";
44
44
  import { domainInterface } from "./shape_compat.js";
45
45
  import { relationType, user, key, anyUser, qualifiedTypeId, qualifyRelationType } from "./relation_schema.js";
46
46
  import { harvestPrincipalFlows } from "./wire_encode.js";
47
+ import { zodKind, zodObjectShape } from "./codegen_ts.js";
47
48
  import { composeSharedRelationTypes } from "./relation_composition.js";
48
49
  import { collectValueObjectUsages } from "./value_object.js";
49
50
  import {
@@ -459,7 +460,29 @@ export interface UsdLayer {
459
460
  onDishonor: "refuse" | "record";
460
461
  }
461
462
  >;
462
- readonly directives: Record<string, { queryId: string; from: string }[]>;
463
+ /**
464
+ * DYNAMIC-SOURCE trust POLICIES (dynamic_source_attested_reads.md): the acceptable-source SET is
465
+ * law-state — a reader-side allow-list aggregate. A directive edge may route through a policy
466
+ * (`{queryId, policy}`) instead of a fixed workspace; the gate resolves the envelope's genesis
467
+ * against the folded `allowList` rows (genesis field) → the row's `keysField` accepted keys.
468
+ * OMITTED when no policy is declared (byte-identical to a policy-free bundle — the era rule).
469
+ */
470
+ readonly policies?: Record<
471
+ string,
472
+ {
473
+ allowList: string;
474
+ genesisField: string;
475
+ keysField: string;
476
+ maxAgeMs?: number;
477
+ minSourceEpoch?: "birth" | number;
478
+ monotone?: boolean;
479
+ onDishonor?: "refuse" | "record";
480
+ }
481
+ >;
482
+ readonly directives: Record<
483
+ string,
484
+ ({ queryId: string; from: string } | { queryId: string; policy: string })[]
485
+ >;
463
486
  };
464
487
  /**
465
488
  * THE READ-VISIBILITY GATE DECLARATION (the native-relations read gate).
@@ -566,6 +589,27 @@ export interface UsdDocument {
566
589
  readonly layers: UsdLayer[];
567
590
  }
568
591
 
592
+ /** The payload field kinds of a directive — `"string-required"` for a required `z.string()`, else the
593
+ * zod kind (`"optional"`, `"number"`, …). Used to validate a `fromPayload` routing field. Empty when
594
+ * the payload isn't a plain object (a shape the check can't reason about — the runtime gate arbitrates). */
595
+ function payloadFieldKinds(dir: { payloadSchema?: unknown } | undefined): Map<string, string> {
596
+ const m = new Map<string, string>();
597
+ const schema = dir?.payloadSchema as Parameters<typeof zodKind>[0] | undefined;
598
+ if (schema === undefined) return m;
599
+ let shape: Record<string, Parameters<typeof zodKind>[0]>;
600
+ try {
601
+ if (zodKind(schema) !== "object") return m;
602
+ shape = zodObjectShape(schema);
603
+ } catch {
604
+ return m;
605
+ }
606
+ for (const [name, f] of Object.entries(shape)) {
607
+ const k = zodKind(f);
608
+ m.set(name, k === "string" ? "string-required" : k);
609
+ }
610
+ return m;
611
+ }
612
+
569
613
  /** Sort prims by their `path` (canonical structure). */
570
614
  function sortPrims(prims: UsdPrim[]): UsdPrim[] {
571
615
  return [...prims].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
@@ -984,47 +1028,122 @@ export function emitUsd(
984
1028
  // trustSource declarations. Emitted iff ≥1 directive declares a foreign read (the era
985
1029
  // rule). A declared `from` with NO matching trustSource is a NAMED COMPILE ERROR — the
986
1030
  // kernel would fail it closed anyway; refuse loudly at build instead.
987
- const foreignDirectives: Record<string, { queryId: string; from: string }[]> = {};
1031
+ type ForeignEdgeInternal =
1032
+ | { queryId: string; from: string }
1033
+ | { queryId: string; policy: string; fromPayload: string };
1034
+ type ForeignEdgeEmit = { queryId: string; from: string } | { queryId: string; policy: string };
1035
+ const foreignDirectives: Record<string, ForeignEdgeInternal[]> = {};
988
1036
  for (const d of [...l.module.directives].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
989
- const fr = (d as { declaredForeignReads?: { queryId: string; from: string }[] }).declaredForeignReads;
1037
+ const fr = (d as { declaredForeignReads?: ForeignEdgeInternal[] }).declaredForeignReads;
990
1038
  if (fr !== undefined && fr.length > 0) foreignDirectives[d.id] = fr.map((e) => ({ ...e }));
991
1039
  }
992
1040
  let nomosAttestedReadGate: UsdLayer["nomosAttestedReadGate"];
993
1041
  if (Object.keys(foreignDirectives).length > 0) {
994
1042
  const trust = (l.module as { trustSources?: readonly import("./attested.js").TrustSourceDecl[] }).trustSources ?? [];
995
1043
  const trustByWs = new Map(trust.map((tsrc) => [tsrc.workspace, tsrc]));
1044
+ const policies = (l.module as { trustPolicies?: readonly import("./attested.js").TrustPolicyDecl[] }).trustPolicies ?? [];
1045
+ const policyById = new Map(policies.map((p) => [p.policyId, p]));
1046
+ const aggIds = new Set(l.module.aggregates.map((a) => a.id));
1047
+ // A policy-referenced allow-list must be a real aggregate of THIS module (the fold happens in
1048
+ // the reader's own state) — else the gate could never resolve it (fail-closed at build).
1049
+ for (const p of policies) {
1050
+ if (!aggIds.has(p.allowListType)) {
1051
+ throw new Error(
1052
+ `trustPolicy('${p.policyId}'): its allowList aggregate '${p.allowListType}' is not an ` +
1053
+ `aggregate of this domain — the acceptable-source set is the reader's OWN folded state, ` +
1054
+ `so the allow-list must be declared (and written by a gated enrolment directive) here.`,
1055
+ );
1056
+ }
1057
+ }
996
1058
  const sourcesRec: NonNullable<UsdLayer["nomosAttestedReadGate"]>["sources"] = {};
1059
+ const policiesRec: NonNullable<NonNullable<UsdLayer["nomosAttestedReadGate"]>["policies"]> = {};
1060
+ const emitDirectives: Record<string, ForeignEdgeEmit[]> = {};
997
1061
  for (const [dirId, edges] of Object.entries(foreignDirectives)) {
1062
+ const dir = l.module.directives.find((d) => d.id === dirId);
1063
+ const payloadShape = payloadFieldKinds(dir);
1064
+ const emitted: ForeignEdgeEmit[] = [];
998
1065
  for (const e of edges) {
999
- const tsrc = trustByWs.get(e.from);
1000
- if (tsrc === undefined) {
1001
- throw new Error(
1002
- `directive '${dirId}' declares .readsFrom('${e.from}', '${e.queryId}') but the ` +
1003
- `module exports no trustSource('${e.from}', …) — the trust edge must be declared ` +
1004
- `in law (attested_reads_design.md §3.1)`,
1005
- );
1006
- }
1007
- if (sourcesRec[e.from] === undefined) {
1008
- sourcesRec[e.from] = {
1009
- genesis: tsrc.genesis,
1010
- ...(tsrc.keyHashes !== undefined && tsrc.keyHashes.length > 0
1011
- ? { keyHashes: [...tsrc.keyHashes].sort() }
1012
- : {}),
1013
- ...(tsrc.maxAgeMs !== undefined ? { maxAgeMs: tsrc.maxAgeMs } : {}),
1014
- ...(tsrc.minSourceEpoch !== undefined ? { minSourceEpoch: tsrc.minSourceEpoch } : {}),
1015
- ...(tsrc.monotone === true ? { monotone: true } : {}),
1016
- roster: tsrc.roster ?? "pinned",
1017
- onDishonor: tsrc.onDishonor ?? "refuse",
1018
- };
1066
+ if ("from" in e) {
1067
+ const tsrc = trustByWs.get(e.from);
1068
+ if (tsrc === undefined) {
1069
+ throw new Error(
1070
+ `directive '${dirId}' declares .readsFrom('${e.from}', '${e.queryId}') but the ` +
1071
+ `module exports no trustSource('${e.from}', …) — the trust edge must be declared ` +
1072
+ `in law (attested_reads_design.md §3.1)`,
1073
+ );
1074
+ }
1075
+ if (sourcesRec[e.from] === undefined) {
1076
+ sourcesRec[e.from] = {
1077
+ genesis: tsrc.genesis,
1078
+ ...(tsrc.keyHashes !== undefined && tsrc.keyHashes.length > 0
1079
+ ? { keyHashes: [...tsrc.keyHashes].sort() }
1080
+ : {}),
1081
+ ...(tsrc.maxAgeMs !== undefined ? { maxAgeMs: tsrc.maxAgeMs } : {}),
1082
+ ...(tsrc.minSourceEpoch !== undefined ? { minSourceEpoch: tsrc.minSourceEpoch } : {}),
1083
+ ...(tsrc.monotone === true ? { monotone: true } : {}),
1084
+ roster: tsrc.roster ?? "pinned",
1085
+ onDishonor: tsrc.onDishonor ?? "refuse",
1086
+ };
1087
+ }
1088
+ emitted.push({ queryId: e.queryId, from: e.from });
1089
+ } else {
1090
+ const pol = policyById.get(e.policy);
1091
+ if (pol === undefined) {
1092
+ throw new Error(
1093
+ `directive '${dirId}' declares .readsFrom(fromPayload('${e.fromPayload}'), '${e.queryId}', ` +
1094
+ `{ policy: '${e.policy}' }) but the module exports no trustPolicy('${e.policy}', …) — the ` +
1095
+ `policy must be declared in law (dynamic_source_attested_reads.md).`,
1096
+ );
1097
+ }
1098
+ // The routing field must be a REQUIRED string in the directive's payload (the workspace
1099
+ // name is chosen at runtime; a missing/optional/non-string field could never route).
1100
+ const kind = payloadShape.get(e.fromPayload);
1101
+ if (kind === undefined) {
1102
+ throw new Error(
1103
+ `directive '${dirId}': .readsFrom(fromPayload('${e.fromPayload}'), …) names a payload ` +
1104
+ `field that is not declared in the directive's payload — add a required z.string() ` +
1105
+ `'${e.fromPayload}' to the payload (it carries the routed source workspace name).`,
1106
+ );
1107
+ }
1108
+ if (kind !== "string-required") {
1109
+ throw new Error(
1110
+ `directive '${dirId}': the fromPayload routing field '${e.fromPayload}' must be a ` +
1111
+ `REQUIRED z.string() in the payload (it names the routed source workspace) — got '${kind}'.`,
1112
+ );
1113
+ }
1114
+ if (policiesRec[e.policy] === undefined) {
1115
+ policiesRec[e.policy] = {
1116
+ // Qualify the allow-list type by the owning domain iff it is namespaced — the
1117
+ // gate scans rows by their stored `__type`, which the engine stamps qualified
1118
+ // (`qaType`, the SAME read-projection namespacing rows/schema keys use). A bare
1119
+ // (unqualified) allowList would match no folded row → every dynamic read refuses.
1120
+ allowList: qaType(pol.allowListType),
1121
+ genesisField: pol.genesisField,
1122
+ keysField: pol.keysField,
1123
+ ...(pol.maxAgeMs !== undefined ? { maxAgeMs: pol.maxAgeMs } : {}),
1124
+ ...(pol.minSourceEpoch !== undefined ? { minSourceEpoch: pol.minSourceEpoch } : {}),
1125
+ ...(pol.monotone === true ? { monotone: true } : {}),
1126
+ ...(pol.onDishonor !== undefined ? { onDishonor: pol.onDishonor } : {}),
1127
+ };
1128
+ }
1129
+ emitted.push({ queryId: e.queryId, policy: e.policy });
1019
1130
  }
1020
1131
  }
1132
+ emitDirectives[dirId] = emitted;
1021
1133
  }
1022
1134
  // SORTED-KEY records (byte-determinism — matching the layered flatten).
1023
1135
  const sortedSources: typeof sourcesRec = {};
1024
1136
  for (const k of Object.keys(sourcesRec).sort()) sortedSources[k] = sourcesRec[k]!;
1025
- const sortedDirectives: Record<string, { queryId: string; from: string }[]> = {};
1026
- for (const k of Object.keys(foreignDirectives).sort()) sortedDirectives[k] = foreignDirectives[k]!;
1027
- nomosAttestedReadGate = { v: 1 as const, sources: sortedSources, directives: sortedDirectives };
1137
+ const sortedPolicies: typeof policiesRec = {};
1138
+ for (const k of Object.keys(policiesRec).sort()) sortedPolicies[k] = policiesRec[k]!;
1139
+ const sortedDirectives: Record<string, ForeignEdgeEmit[]> = {};
1140
+ for (const k of Object.keys(emitDirectives).sort()) sortedDirectives[k] = emitDirectives[k]!;
1141
+ nomosAttestedReadGate = {
1142
+ v: 1 as const,
1143
+ sources: sortedSources,
1144
+ ...(Object.keys(sortedPolicies).length > 0 ? { policies: sortedPolicies } : {}),
1145
+ directives: sortedDirectives,
1146
+ };
1028
1147
  }
1029
1148
  // THE BIRTH GATE (G3 — birth is a declared kernel primitive): the SORTED set of directive ids
1030
1149
  // declaring `.births()`. The kernel reads this off the manifest (never a domain aggregate) to know an
package/src/usd_layers.ts CHANGED
@@ -835,20 +835,32 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
835
835
  // layers, SORTED keys (matching the composed emission). Omit-when-empty (era rule).
836
836
  let attestedReadGate: UsdLayer["nomosAttestedReadGate"];
837
837
  {
838
- type SrcSpec = NonNullable<UsdLayer["nomosAttestedReadGate"]>["sources"][string];
838
+ type Gate = NonNullable<UsdLayer["nomosAttestedReadGate"]>;
839
+ type SrcSpec = Gate["sources"][string];
840
+ type PolSpec = NonNullable<Gate["policies"]>[string];
841
+ type EdgeList = Gate["directives"][string];
839
842
  const srcs = new Map<string, SrcSpec>();
840
- const dirs = new Map<string, { queryId: string; from: string }[]>();
843
+ const pols = new Map<string, PolSpec>();
844
+ const dirs = new Map<string, EdgeList>();
841
845
  for (const l of stack) {
842
846
  if (l.nomosAttestedReadGate === undefined) continue;
843
847
  for (const [ws, spec] of Object.entries(l.nomosAttestedReadGate.sources)) srcs.set(ws, spec);
848
+ for (const [id, spec] of Object.entries(l.nomosAttestedReadGate.policies ?? {})) pols.set(id, spec);
844
849
  for (const [id, edges] of Object.entries(l.nomosAttestedReadGate.directives)) dirs.set(id, edges);
845
850
  }
846
851
  if (dirs.size > 0) {
847
852
  const sourcesRec: Record<string, SrcSpec> = {};
848
853
  for (const k of [...srcs.keys()].sort()) sourcesRec[k] = srcs.get(k)!;
849
- const directivesRec: Record<string, { queryId: string; from: string }[]> = {};
854
+ const policiesRec: Record<string, PolSpec> = {};
855
+ for (const k of [...pols.keys()].sort()) policiesRec[k] = pols.get(k)!;
856
+ const directivesRec: Record<string, EdgeList> = {};
850
857
  for (const k of [...dirs.keys()].sort()) directivesRec[k] = dirs.get(k)!;
851
- attestedReadGate = { v: 1, sources: sourcesRec, directives: directivesRec };
858
+ attestedReadGate = {
859
+ v: 1,
860
+ sources: sourcesRec,
861
+ ...(Object.keys(policiesRec).length > 0 ? { policies: policiesRec } : {}),
862
+ directives: directivesRec,
863
+ };
852
864
  }
853
865
  }
854
866
  // THE BIRTH GATE (G3): the SORTED union of every layer's `.births()` directive ids. Omit-when-empty