@githolon/dsl 0.33.0 → 0.34.1

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/usd_layers.ts CHANGED
@@ -58,6 +58,7 @@
58
58
  * the runtime barrel.
59
59
  */
60
60
  import type { AggregateHandle } from "./aggregate.js";
61
+ import { READ_PROJECTION_DELIMITER } from "./read_projection_namespace.js";
61
62
  import type { Field } from "./fields.js";
62
63
  import type { DomainModule } from "./codegen_dart.js";
63
64
  import type { WireDriver, WireRefMode, WireSchema } from "./wire.js";
@@ -175,6 +176,9 @@ function domainLayerAttrLines(domain: string, layer: UsdLayer, indent: string):
175
176
  ...(layer.nomosRelationNamespace !== undefined
176
177
  ? [`${indent}custom string nomos:relationNamespaceHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosRelationNamespace)))}`]
177
178
  : []),
179
+ ...(layer.nomosReadProjectionNamespace !== undefined
180
+ ? [`${indent}custom string nomos:readProjectionNamespaceHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosReadProjectionNamespace)))}`]
181
+ : []),
178
182
  ...(layer.nomosRelationGate !== undefined
179
183
  ? [`${indent}custom string nomos:relationGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosRelationGate)))}`]
180
184
  : []),
@@ -184,6 +188,9 @@ function domainLayerAttrLines(domain: string, layer: UsdLayer, indent: string):
184
188
  ...(layer.nomosBirthGate !== undefined
185
189
  ? [`${indent}custom string nomos:birthGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosBirthGate)))}`]
186
190
  : []),
191
+ ...(layer.nomosBirthRecordGate !== undefined
192
+ ? [`${indent}custom string nomos:birthRecordGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosBirthRecordGate)))}`]
193
+ : []),
187
194
  ...(layer.nomosParentAttestedBirth !== undefined
188
195
  ? [`${indent}custom string nomos:parentAttestedBirthHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosParentAttestedBirth)))}`]
189
196
  : []),
@@ -779,6 +786,18 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
779
786
  }
780
787
  if (ns.size > 0) relationNamespace = { v: 1, delimiter: "__", types: [...ns].sort() };
781
788
  }
789
+ // THE READ-PROJECTION NAMESPACE ERA KEY (read-projection namespacing): the SORTED union of every
790
+ // layer's qualified aggregate type names. Present iff ANY layer namespaced >=1 aggregate;
791
+ // omit-when-empty (byte-identical for global-only packages). Delimiter is the ratified "__".
792
+ let readProjectionNamespace: UsdLayer["nomosReadProjectionNamespace"];
793
+ {
794
+ const ns = new Set<string>();
795
+ for (const l of stack) {
796
+ if (l.nomosReadProjectionNamespace === undefined) continue;
797
+ for (const id of l.nomosReadProjectionNamespace.types) ns.add(id);
798
+ }
799
+ if (ns.size > 0) readProjectionNamespace = { v: 1, delimiter: READ_PROJECTION_DELIMITER, types: [...ns].sort() };
800
+ }
782
801
  // THE RELATION GATE (P4): directiveId → {relation, object} merged later-wins, SORTED. Omit-when-empty.
783
802
  let relationGate: UsdLayer["nomosRelationGate"];
784
803
  {
@@ -820,6 +839,16 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
820
839
  }
821
840
  if (b.size > 0) birthGate = { v: 1, directives: [...b].sort() };
822
841
  }
842
+ // THE BIRTH-RECORD GATE (metered birth, A1): present iff ANY layer declares it (once declared,
843
+ // declared). Carry the FIRST layer's aggregate/prefix verbatim (the framework constants — do NOT
844
+ // reconstruct). Omit-when-absent (era rule).
845
+ let birthRecordGate: UsdLayer["nomosBirthRecordGate"];
846
+ for (const l of stack) {
847
+ if (l.nomosBirthRecordGate !== undefined) {
848
+ birthRecordGate = l.nomosBirthRecordGate;
849
+ break;
850
+ }
851
+ }
823
852
  // THE PEER-SOVEREIGN ROOT-OF-TRUST: the era key is present iff ANY layer declares it (once required,
824
853
  // required). Carry the FIRST layer's declared owner/cert locators verbatim (preserve them — do NOT
825
854
  // reconstruct {v:1}, which would drop the locators the kernel reads). Omit-when-absent (era rule).
@@ -846,6 +875,29 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
846
875
  verifiedFirstEnroll = { v: 1, directives: rec };
847
876
  }
848
877
  }
878
+ // FIRST-CLASS VALUE OBJECTS (+ the VO↔field usage pointers): descriptors merged
879
+ // later-wins per NAME, usages later-wins per (aggregate, field), both SORTED —
880
+ // so the layered fold stays byte-homomorphic with `emitUsd`'s composed emission.
881
+ // Omit-when-empty (the era rule); `usages` omitted when none (0.33.0 tolerance).
882
+ let valueObjects: UsdLayer["nomosValueObjects"];
883
+ {
884
+ type VoDesc = NonNullable<UsdLayer["nomosValueObjects"]>["valueObjects"][number];
885
+ type VoUse = NonNullable<NonNullable<UsdLayer["nomosValueObjects"]>["usages"]>[number];
886
+ const byName = new Map<string, VoDesc>();
887
+ const byField = new Map<string, VoUse>();
888
+ for (const l of stack) {
889
+ if (l.nomosValueObjects === undefined) continue;
890
+ for (const d of l.nomosValueObjects.valueObjects) byName.set(d.name, d);
891
+ for (const u of l.nomosValueObjects.usages ?? []) byField.set(`${u.aggregate}\u0000${u.field}`, u);
892
+ }
893
+ if (byName.size > 0) {
894
+ const descs = [...byName.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
895
+ const uses = [...byField.values()].sort((a, b) =>
896
+ a.aggregate !== b.aggregate ? (a.aggregate < b.aggregate ? -1 : 1) : a.field < b.field ? -1 : a.field > b.field ? 1 : 0,
897
+ );
898
+ valueObjects = { v: 1, valueObjects: descs, ...(uses.length > 0 ? { usages: uses } : {}) };
899
+ }
900
+ }
849
901
  return {
850
902
  path,
851
903
  prims: [...prims.keys()].sort().map((p) => prims.get(p)!),
@@ -861,11 +913,14 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
861
913
  ...(encryption !== undefined ? { nomosEncryption: encryption } : {}),
862
914
  ...(relations !== undefined ? { nomosRelations: relations } : {}),
863
915
  ...(relationNamespace !== undefined ? { nomosRelationNamespace: relationNamespace } : {}),
916
+ ...(readProjectionNamespace !== undefined ? { nomosReadProjectionNamespace: readProjectionNamespace } : {}),
864
917
  ...(relationGate !== undefined ? { nomosRelationGate: relationGate } : {}),
865
918
  ...(attestationGate !== undefined ? { nomosAttestationGate: attestationGate } : {}),
866
919
  ...(birthGate !== undefined ? { nomosBirthGate: birthGate } : {}),
920
+ ...(birthRecordGate !== undefined ? { nomosBirthRecordGate: birthRecordGate } : {}),
867
921
  ...(parentAttestedBirth !== undefined ? { nomosParentAttestedBirth: parentAttestedBirth } : {}),
868
922
  ...(verifiedFirstEnroll !== undefined ? { nomosVerifiedFirstEnroll: verifiedFirstEnroll } : {}),
923
+ ...(valueObjects !== undefined ? { nomosValueObjects: valueObjects } : {}),
869
924
  };
870
925
  });
871
926
 
@@ -996,8 +1051,10 @@ export function parseUsdaDocument(text: string): UsdDocument {
996
1051
  const relationsByDomain = new Map<string, UsdLayer["nomosRelations"]>();
997
1052
  const relationGateByDomain = new Map<string, UsdLayer["nomosRelationGate"]>();
998
1053
  const relationNamespaceByDomain = new Map<string, UsdLayer["nomosRelationNamespace"]>();
1054
+ const readProjectionNamespaceByDomain = new Map<string, UsdLayer["nomosReadProjectionNamespace"]>();
999
1055
  const attestationGateByDomain = new Map<string, UsdLayer["nomosAttestationGate"]>();
1000
1056
  const birthGateByDomain = new Map<string, UsdLayer["nomosBirthGate"]>();
1057
+ const birthRecordGateByDomain = new Map<string, UsdLayer["nomosBirthRecordGate"]>();
1001
1058
  const parentAttestedBirthByDomain = new Map<string, UsdLayer["nomosParentAttestedBirth"]>();
1002
1059
  const verifiedFirstEnrollByDomain = new Map<string, UsdLayer["nomosVerifiedFirstEnroll"]>();
1003
1060
  const valueObjectsByDomain = new Map<string, UsdLayer["nomosValueObjects"]>();
@@ -1052,6 +1109,11 @@ export function parseUsdaDocument(text: string): UsdDocument {
1052
1109
  if (typeof relNsHex === "string") {
1053
1110
  relationNamespaceByDomain.set(domain, JSON.parse(unhexUtf8(relNsHex)) as UsdLayer["nomosRelationNamespace"]);
1054
1111
  }
1112
+ // THE READ-PROJECTION-NAMESPACE ERA KEY: the hex-carried qualified-aggregate witness.
1113
+ const rpNsHex = p.attrs.get("nomos:readProjectionNamespaceHex");
1114
+ if (typeof rpNsHex === "string") {
1115
+ readProjectionNamespaceByDomain.set(domain, JSON.parse(unhexUtf8(rpNsHex)) as UsdLayer["nomosReadProjectionNamespace"]);
1116
+ }
1055
1117
  // THE ATTESTATION GATE (home_holons §3/§5): the hex-carried attestation-authorized directive set.
1056
1118
  const attGateHex = p.attrs.get("nomos:attestationGateHex");
1057
1119
  if (typeof attGateHex === "string") {
@@ -1062,6 +1124,11 @@ export function parseUsdaDocument(text: string): UsdDocument {
1062
1124
  if (typeof birthGateHex === "string") {
1063
1125
  birthGateByDomain.set(domain, JSON.parse(unhexUtf8(birthGateHex)) as UsdLayer["nomosBirthGate"]);
1064
1126
  }
1127
+ // THE BIRTH-RECORD GATE (metered birth, A1): the hex-carried record declaration.
1128
+ const birthRecordGateHex = p.attrs.get("nomos:birthRecordGateHex");
1129
+ if (typeof birthRecordGateHex === "string") {
1130
+ birthRecordGateByDomain.set(domain, JSON.parse(unhexUtf8(birthRecordGateHex)) as UsdLayer["nomosBirthRecordGate"]);
1131
+ }
1065
1132
  // THE PEER-SOVEREIGN ROOT-OF-TRUST: the hex-carried parent-attested-birth era marker.
1066
1133
  const pabHex = p.attrs.get("nomos:parentAttestedBirthHex");
1067
1134
  if (typeof pabHex === "string") {
@@ -1174,8 +1241,10 @@ export function parseUsdaDocument(text: string): UsdDocument {
1174
1241
  const rel = relationsByDomain.get(domain);
1175
1242
  const relGate = relationGateByDomain.get(domain);
1176
1243
  const relNs = relationNamespaceByDomain.get(domain);
1244
+ const rpNs = readProjectionNamespaceByDomain.get(domain);
1177
1245
  const attGate = attestationGateByDomain.get(domain);
1178
1246
  const birthGate = birthGateByDomain.get(domain);
1247
+ const birthRecordGate = birthRecordGateByDomain.get(domain);
1179
1248
  const parentAttestedBirth = parentAttestedBirthByDomain.get(domain);
1180
1249
  const verifiedFirstEnroll = verifiedFirstEnrollByDomain.get(domain);
1181
1250
  const valueObjects = valueObjectsByDomain.get(domain);
@@ -1194,9 +1263,11 @@ export function parseUsdaDocument(text: string): UsdDocument {
1194
1263
  ...(enc !== undefined ? { nomosEncryption: enc } : {}),
1195
1264
  ...(rel !== undefined ? { nomosRelations: rel } : {}),
1196
1265
  ...(relNs !== undefined ? { nomosRelationNamespace: relNs } : {}),
1266
+ ...(rpNs !== undefined ? { nomosReadProjectionNamespace: rpNs } : {}),
1197
1267
  ...(relGate !== undefined ? { nomosRelationGate: relGate } : {}),
1198
1268
  ...(attGate !== undefined ? { nomosAttestationGate: attGate } : {}),
1199
1269
  ...(birthGate !== undefined ? { nomosBirthGate: birthGate } : {}),
1270
+ ...(birthRecordGate !== undefined ? { nomosBirthRecordGate: birthRecordGate } : {}),
1200
1271
  ...(parentAttestedBirth !== undefined ? { nomosParentAttestedBirth: parentAttestedBirth } : {}),
1201
1272
  ...(verifiedFirstEnroll !== undefined ? { nomosVerifiedFirstEnroll: verifiedFirstEnroll } : {}),
1202
1273
  ...(valueObjects !== undefined ? { nomosValueObjects: valueObjects } : {}),
@@ -411,3 +411,46 @@ export function collectValueObjectDescriptors(
411
411
  }
412
412
  return [...byName.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
413
413
  }
414
+
415
+ /**
416
+ * ONE VO↔FIELD ATTACHMENT (the IR pointer): aggregate `aggregate`'s field `field`
417
+ * stores value object `vo`. Rides the additive `nomosValueObjects` USD key as
418
+ * `usages` so tooling (the scene projector, the explorer glyphs) can attach a VO
419
+ * molecule to the exact field that carries it. NEVER in the canonical
420
+ * manifest/domainHash — a VO field's wire schema stays a plain `jsonObject` leaf.
421
+ */
422
+ export interface ValueObjectUsage {
423
+ readonly aggregate: string;
424
+ readonly field: string;
425
+ readonly vo: string;
426
+ }
427
+
428
+ /**
429
+ * Collect the sorted VO↔field usage pointers of a set of aggregates (one entry per
430
+ * VO-typed field; sorted by aggregate then field — byte-deterministic). Returns `[]`
431
+ * when no field carries a VO — the omit-when-empty input to the `usages` entry of
432
+ * the `nomosValueObjects` era key.
433
+ */
434
+ export function collectValueObjectUsages(
435
+ aggregates: readonly { id: string; fields: Record<string, Field> }[],
436
+ ): ValueObjectUsage[] {
437
+ const usages: ValueObjectUsage[] = [];
438
+ for (const agg of aggregates) {
439
+ for (const fieldName of Object.keys(agg.fields)) {
440
+ const meta = agg.fields[fieldName]!.valueObject;
441
+ if (meta === undefined) continue;
442
+ usages.push({ aggregate: agg.id, field: fieldName, vo: meta.name });
443
+ }
444
+ }
445
+ return usages.sort((a, b) =>
446
+ a.aggregate !== b.aggregate
447
+ ? a.aggregate < b.aggregate
448
+ ? -1
449
+ : 1
450
+ : a.field < b.field
451
+ ? -1
452
+ : a.field > b.field
453
+ ? 1
454
+ : 0,
455
+ );
456
+ }
@@ -20,9 +20,16 @@
20
20
  * LATER coordinated migration — we target the current serde_json shape now.
21
21
  */
22
22
  import { encodeDriver } from "./drivers.js";
23
+ import { __qualifyAggregateType } from "./framework/relations.js";
23
24
  import { __resetAuthoring, __drainAuthoring } from "./authoring.js";
24
25
  import { __resetReads, __drainReads } from "./read.js";
25
26
  import { __resetBirths, __drainBirths } from "./births.js";
27
+ import {
28
+ birthMeterSubject,
29
+ birthMeterBornAt,
30
+ captureBirthMeterReads,
31
+ planBirthRecordOps,
32
+ } from "./birth_meter.js";
26
33
  import type { AggregateHandle } from "./aggregate.js";
27
34
  import type { Field, EvidenceRefValue } from "./fields.js";
28
35
  import type { Directive, ReferentialMarker } from "./directive.js";
@@ -203,13 +210,33 @@ export function executeDirectiveToIntent<P>(
203
210
  __resetReads();
204
211
  __resetBirths();
205
212
  const returned = directive.plan(parsed, ctx);
206
- const planned = [...__drainAuthoring(), ...returned];
207
- const reads = __drainReads();
208
213
  // THE BIRTH EFFECT (G3 — birth is a declared kernel primitive): a `.births()` directive's plan records
209
214
  // the child's genesis recipe via `birth(recipe)`; it rides the intent as a DECLARED output (like a
210
215
  // captured read), drained here. Empty for every non-birth directive — OMITTED below so a birth-free
211
216
  // intent is byte-identical (the era rule).
212
217
  const births = __drainBirths();
218
+ // METERED BIRTH (slice A — architecture/metered_birth_and_attested_reads.md). For every PLANNED birth,
219
+ // the library — HERE, in the ONE plan-output assembly both lanes (client authoring + the sealed engine's
220
+ // `plan`) funnel through, so the halves are byte-identical by construction — does two things the tenant
221
+ // never writes:
222
+ // A1 THE AUTO-RECORD: append the `NomosBirthRecord` field ops (deterministic id per child; Lww upsert)
223
+ // to the SAME intent that carries the birth effect. The kernel's `nomosBirthRecordGate` era arm
224
+ // refuses a new-era birth whose intent carries no record — this is what satisfies it.
225
+ // A2 THE AUTO-CAPTURE: capture the three meter reads (records-by-subject + grant + policy — the #58
226
+ // lane `.births()` auto-declares) so the synthesized birth-quota invariant judges over COMMITTED,
227
+ // CAS-re-derived pre-apply state on every lane.
228
+ // A `.births()` dispatch that planned NO birth (e.g. birthChild without a genesis) appends/captures
229
+ // nothing — byte-identical to before (the era rule); pre-era bundles ship an older lump and are untouched.
230
+ const meterOps: PlannedOp[] = [];
231
+ if (births.length > 0) {
232
+ const payloadRecord = parsed as unknown as Record<string, unknown>;
233
+ const subject = birthMeterSubject(payloadRecord);
234
+ captureBirthMeterReads(subject);
235
+ const bornAt = birthMeterBornAt(payloadRecord);
236
+ for (const b of births) meterOps.push(...planBirthRecordOps(b, subject, bornAt));
237
+ }
238
+ const planned = [...__drainAuthoring(), ...returned, ...meterOps];
239
+ const reads = __drainReads();
213
240
 
214
241
  // Partition the plan output: STRIKE ops route onto the intent's `strikes` channel
215
242
  // (the kernel's strikeout / revert facet, folded by parity); FIELD ops group into
@@ -293,7 +320,11 @@ export function executeDirectiveToIntent<P>(
293
320
  aggregate,
294
321
  marker: eventMarker,
295
322
  ops: [
296
- { field: "__type", op: { Set: { Str: ty } } },
323
+ // READ-PROJECTION NAMESPACING: the stamped TYPE is qualified by the dispatching domain iff
324
+ // the domain declares it non-global — so the folded row's `__type` equals the qualified
325
+ // schema key the SAME compile wrote (rows and read manifest agree by construction). Outside
326
+ // a plan dispatch (codegen probes) the type passes through bare.
327
+ { field: "__type", op: { Set: { Str: __qualifyAggregateType(ty) } } },
297
328
  { field: "__id", op: { Set: { Str: aggregate } } },
298
329
  ...ops,
299
330
  ],