@githolon/dsl 0.101.6 → 0.102.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.
Files changed (120) hide show
  1. package/dart/CHANGELOG.md +13 -0
  2. package/dart/lib/nomos_client.dart +3 -52
  3. package/dart/lib/src/dev_cloud.dart +5 -0
  4. package/dart/lib/src/home.dart +10 -15
  5. package/dart/lib/src/nms_conformance_runtime.dart +12 -0
  6. package/dart/lib/src/nms_generated_runtime.dart +29 -0
  7. package/dart/lib/src/nms_test_environment.dart +8 -0
  8. package/dart/lib/src/node_runner_transport.dart +56 -18
  9. package/dart/lib/src/provider.dart +5 -1
  10. package/dart/lib/src/record_origin.dart +55 -0
  11. package/dart/lib/src/runner/node-entry.mjs +9105 -0
  12. package/dart/lib/src/runner/offline-install.mjs +4729 -0
  13. package/dart/lib/src/session_bound.dart +23 -10
  14. package/dart/lib/src/source_trust.dart +51 -0
  15. package/dart/lib/src/test_harness.dart +490 -58
  16. package/dart/lib/src/webview_bridge.dart +167 -58
  17. package/dart/lib/src/workspace_name.dart +7 -1
  18. package/dart/pubspec.lock +3 -3
  19. package/dart/pubspec.yaml +4 -2
  20. package/dart/test/acquire_session_credentials_test.dart +34 -17
  21. package/dart/test/acquire_session_test.dart +109 -24
  22. package/dart/test/adv_realm_interference_test.dart +227 -99
  23. package/dart/test/bundle_version_pairing_test.dart +1 -1
  24. package/dart/test/cloud_bound_topology_test.dart +1 -1
  25. package/dart/test/cluster_attested_test.dart +52 -35
  26. package/dart/test/cluster_births_test.dart +5 -2
  27. package/dart/test/cluster_device_keyed_acceptance_test.dart +5 -2
  28. package/dart/test/cluster_local_autosync_test.dart +5 -2
  29. package/dart/test/cluster_scale_test.dart +5 -2
  30. package/dart/test/compat_test.dart +1 -1
  31. package/dart/test/dlq_session_surface_test.dart +2 -1
  32. package/dart/test/errors_test.dart +1 -1
  33. package/dart/test/first_app_gauntlet_test.dart +78 -51
  34. package/dart/test/four_layer_nested_topology_test.dart +1 -1
  35. package/dart/test/harness_phase_timeout_test.dart +1 -1
  36. package/dart/test/home_parity_test.dart +9 -5
  37. package/dart/test/local_sync_inert_test.dart +22 -14
  38. package/dart/test/node_runner_transport_test.dart +9 -5
  39. package/dart/test/null_slot_test.dart +1 -1
  40. package/dart/test/parent_signer_recovery_test.dart +5 -2
  41. package/dart/test/schema_validation_test.dart +1 -1
  42. package/dart/test/sealing_client_test.dart +2 -1
  43. package/dart/test/session_bound_test.dart +92 -119
  44. package/dart/test/stale_bundle_test.dart +1 -1
  45. package/dart/test/test_harness_test.dart +77 -22
  46. package/dart/test/vo_read_typing_test.dart +32 -14
  47. package/dart/test/watch_after_offer_stall_test.dart +2 -1
  48. package/dart/test/webview_bridge_test.dart +17 -4
  49. package/dart/test/wire_test.dart +1 -1
  50. package/dart_types/CHANGELOG.md +8 -0
  51. package/dart_types/lib/nomos_types.dart +7 -6
  52. package/dart_types/lib/src/activity.dart +23 -13
  53. package/dart_types/lib/src/foreign_ref.dart +61 -112
  54. package/dart_types/lib/src/invitation.dart +96 -0
  55. package/dart_types/lib/src/nomos_ref.dart +97 -59
  56. package/dart_types/pubspec.yaml +1 -1
  57. package/dart_types/test/surfaced_test.dart +125 -19
  58. package/package.json +7 -3
  59. package/runtime/generated-application-host.js +8851 -0
  60. package/src/activity.ts +2 -2
  61. package/src/aggregate.ts +24 -1
  62. package/src/application_composition.ts +282 -0
  63. package/src/application_topology.ts +151 -0
  64. package/src/attested_core.ts +2 -2
  65. package/src/authoring_context.ts +9 -0
  66. package/src/birth_meter_ensure.ts +1 -1
  67. package/src/births.ts +53 -5
  68. package/src/build_package.ts +79 -71
  69. package/src/business_birth.ts +93 -10
  70. package/src/clear_directive.ts +3 -1
  71. package/src/codegen_dart.ts +369 -420
  72. package/src/codegen_dart_application.ts +1508 -0
  73. package/src/codegen_dart_client.ts +156 -95
  74. package/src/codegen_dart_scenario.ts +299 -0
  75. package/src/codegen_openapi.ts +8 -3
  76. package/src/codegen_proof.ts +1113 -1137
  77. package/src/codegen_provider_dart.ts +12 -1
  78. package/src/codegen_ts.ts +1176 -496
  79. package/src/compat_package_main.ts +22 -10
  80. package/src/compile_package_main.ts +1536 -337
  81. package/src/contract.ts +10 -4
  82. package/src/dart_barrel_guard.ts +5 -1
  83. package/src/describe_model.ts +13 -5
  84. package/src/directive.ts +244 -5
  85. package/src/engine_entry.ts +56 -13
  86. package/src/engine_javascript.ts +31 -0
  87. package/src/evolve_preview.ts +3 -12
  88. package/src/framework/application_home.ts +24 -10
  89. package/src/framework/capability.ts +15 -1
  90. package/src/framework/domain_lifecycle.ts +8 -0
  91. package/src/framework/framework_aggregates.ts +2 -2
  92. package/src/framework/genesis.ts +11 -1
  93. package/src/framework/home.ts +178 -75
  94. package/src/framework/impure_capability.ts +24 -7
  95. package/src/framework/relations.ts +13 -5
  96. package/src/framework/workspaces.ts +13 -12
  97. package/src/generated_client_names.ts +42 -3
  98. package/src/idof_lint.ts +65 -13
  99. package/src/index.ts +46 -12
  100. package/src/intent.ts +587 -0
  101. package/src/law_check.ts +64 -16
  102. package/src/precondition.ts +16 -4
  103. package/src/read_projection_namespace.ts +2 -7
  104. package/src/recipient.ts +33 -0
  105. package/src/relation_schema.ts +93 -0
  106. package/src/secondary_ensure.ts +11 -2
  107. package/src/shape_compat.ts +11 -1
  108. package/src/usd.ts +30 -8
  109. package/src/usd_layers.ts +447 -246
  110. package/src/usd_state.ts +1 -1
  111. package/src/value_object.ts +49 -13
  112. package/src/wire_encode.ts +124 -26
  113. package/src/workspace_sharing.ts +269 -0
  114. package/src/zod_introspection.ts +84 -0
  115. package/dart/example/nomos_secure_key_store.dart +0 -82
  116. package/dart/example/nomos_webview_transport.dart +0 -63
  117. package/dart/lib/nomos_headless.dart +0 -50
  118. package/dart/lib/testing.dart +0 -14
  119. package/dart/test/co2_product_gauntlet_test.dart +0 -244
  120. package/src/framework/identity.ts +0 -537
package/src/law_check.ts CHANGED
@@ -33,7 +33,12 @@ import type { DomainModule } from "./codegen_dart.js";
33
33
  import { admitsNull, type Field, type ValueObjectDescriptor, type ValueObjectFieldDescriptor } from "./fields.js";
34
34
  import type { FieldOp, PlannedOp } from "./ops.js";
35
35
  import { deterministicPorts } from "./ctx.js";
36
- import { synthesizePayload, withBirthProbeGlobals, type SynthesizedPayload } from "./codegen_proof.js";
36
+ import {
37
+ synthesizeDirectivePayload,
38
+ synthesizePayload,
39
+ withBirthProbeGlobals,
40
+ type SynthesizedPayload,
41
+ } from "./codegen_proof.js";
37
42
  import { __drainAuthoring, __resetAuthoring } from "./authoring.js";
38
43
  import { __drainReads, __resetReads } from "./read.js";
39
44
  import { __drainBirths, __resetBirths } from "./births.js";
@@ -83,6 +88,9 @@ interface ProbeResult {
83
88
  readonly error?: unknown;
84
89
  /** The FIELD ops the plan emitted (authoring sink + returned ops), when it didn't throw. */
85
90
  readonly ops: FieldOp[];
91
+ /** Captured reads reached before return/throw. An empty-state read may make a valid stateful plan
92
+ * deliberately reject this isolated probe; the generated prerequisite journey is its positive proof. */
93
+ readonly reads: readonly { readonly queryId: string }[];
86
94
  /** The top-level payload keys the plan actually READ (via the tracking proxy). */
87
95
  readonly readFields: ReadonlySet<string>;
88
96
  }
@@ -93,7 +101,7 @@ interface ProbeResult {
93
101
  function probeDirective(dir: Directive<unknown>, where: string): ProbeResult | undefined {
94
102
  let synth: SynthesizedPayload;
95
103
  try {
96
- synth = synthesizePayload(dir.payloadSchema as z.ZodTypeAny, where);
104
+ synth = synthesizeDirectivePayload(dir, where);
97
105
  } catch {
98
106
  return undefined;
99
107
  }
@@ -118,6 +126,7 @@ function probeDirective(dir: Directive<unknown>, where: string): ProbeResult | u
118
126
  let threw = false;
119
127
  let error: unknown;
120
128
  let ops: FieldOp[] = [];
129
+ let reads: readonly { readonly queryId: string }[] = [];
121
130
  withBirthProbeGlobals(() => {
122
131
  __resetAuthoring();
123
132
  __resetReads();
@@ -126,9 +135,11 @@ function probeDirective(dir: Directive<unknown>, where: string): ProbeResult | u
126
135
  returned = dir.plan(tracked as never, deterministicPorts({ physical: 1, replica: 1, principal: "principal:probe" }));
127
136
  const drained: PlannedOp[] = [...__drainAuthoring(), ...(Array.isArray(returned) ? returned : [])];
128
137
  ops = drained.filter((o): o is FieldOp => (o as { kind?: string }).kind !== "strike");
138
+ reads = __drainReads();
129
139
  } catch (e) {
130
140
  threw = true;
131
141
  error = e;
142
+ reads = __drainReads();
132
143
  } finally {
133
144
  // Drain the sinks on every path so a throwing probe never leaks state into the next.
134
145
  try { __drainAuthoring(); } catch { /* drained */ }
@@ -136,7 +147,7 @@ function probeDirective(dir: Directive<unknown>, where: string): ProbeResult | u
136
147
  try { __drainBirths(); } catch { /* drained */ }
137
148
  }
138
149
  });
139
- return { synth, probePayload, returned, threw, error, ops, readFields };
150
+ return { synth, probePayload, returned, threw, error, ops, reads, readFields };
140
151
  }
141
152
 
142
153
  /** `zodObjectShape` with a swallow — a non-object payload yields `{}` (the return twin handles it). */
@@ -335,8 +346,24 @@ function deletedAmbientFindings(domain: string, dir: Directive<unknown>, probe:
335
346
  // `return []` after `create(...).set(...)`; a plain object) throws "not iterable" at the FIRST
336
347
  // dispatch on every peer. This is payload-INDEPENDENT (the return TYPE is fixed) ⇒ REFUSE.
337
348
  // A plan that THREW on the sample is payload-DEPENDENT (can't prove) ⇒ WARN naming the sample.
338
- function planReturnFindings(domain: string, dir: Directive<unknown>, probe: ProbeResult): Finding[] {
349
+ function planReturnFindings(
350
+ domain: string,
351
+ dir: Directive<unknown>,
352
+ probe: ProbeResult,
353
+ generatedJourneyProvesDirective: boolean,
354
+ ): Finding[] {
339
355
  if (probe.threw) {
356
+ const declaredReadIds = new Set([
357
+ ...(dir.declaredQueryReads ?? []).map((query) => query.id),
358
+ ...(dir.declaredForeignReads ?? []).map((read) => read.queryId),
359
+ ]);
360
+ const threwAfterDeclaredRead = probe.reads.length > 0 &&
361
+ probe.reads.every((read) => declaredReadIds.has(read.queryId));
362
+ // An isolated empty projection is the wrong positive fixture for a stateful business action. When the
363
+ // generator has built a real prerequisite journey for this exact public directive, that journey—not a
364
+ // fabricated checker row—proves the plan's return shape against actual folded state. CI executes it.
365
+ // Keep warning for private/unproved directives and for throws before any declared read.
366
+ if (threwAfterDeclaredRead && generatedJourneyProvesDirective) return [];
340
367
  return [
341
368
  {
342
369
  domain,
@@ -393,6 +420,17 @@ function unreadPayloadFindings(domain: string, dir: Directive<unknown>, probe: P
393
420
  gateConsumed.add(firstDeviceGate.keyHashField);
394
421
  gateConsumed.add(firstDeviceGate.verifiedViaField);
395
422
  }
423
+ // A cross-custody attestation gate verifies the signed quote directly from
424
+ // the payload. `publicKey` and `signature` are deliberately verification
425
+ // inputs rather than stored business fields, so the plan must not pretend to
426
+ // consume them merely to placate this lint. All field names are law-declared;
427
+ // do not hardcode the recordShare convention here.
428
+ const attestationGate = (dir as {
429
+ attestationAuthorizedGate?: { fields?: Readonly<Record<string, string>> };
430
+ }).attestationAuthorizedGate;
431
+ for (const field of Object.values(attestationGate?.fields ?? {})) {
432
+ if (typeof field === "string") gateConsumed.add(field);
433
+ }
396
434
  // A dynamic .readsFrom(fromPayload("<wsField>"), …) edge routes the foreign read by a payload field the
397
435
  // ROUTING lane consumes (the plan may pass it straight to read(…, {from}) without a tracked property
398
436
  // read on the sampled path) — a declared trust-edge field is never a dead field.
@@ -1037,7 +1075,7 @@ function topLevelPortReadFindings(mod: DomainModule, portReadingDomains: Readonl
1037
1075
  function foreignReadsPerformed(dir: Directive<unknown>, where: string): Set<string> | undefined {
1038
1076
  let synth: SynthesizedPayload;
1039
1077
  try {
1040
- synth = synthesizePayload(dir.payloadSchema as z.ZodTypeAny, where);
1078
+ synth = synthesizeDirectivePayload(dir, where);
1041
1079
  } catch {
1042
1080
  return undefined;
1043
1081
  }
@@ -1144,30 +1182,40 @@ export function checkDomainModules(
1144
1182
  ): Finding[] {
1145
1183
  const findings: Finding[] = [];
1146
1184
  const portReadingDomains = opts?.portReadingDomains ?? new Set<string>();
1185
+ const generatedJourneyDirectiveIds = new Map<string, Set<string>>();
1186
+ try {
1187
+ const index = proofLegsIndex(modules as DomainModule[]);
1188
+ for (const [domain, legs] of Object.entries(index?.domains ?? {})) {
1189
+ const ids = generatedJourneyDirectiveIds.get(domain) ?? new Set<string>();
1190
+ if (legs.directiveId !== undefined) ids.add(legs.directiveId);
1191
+ for (const create of legs.extraCreates ?? []) ids.add(create.directiveId);
1192
+ for (const birth of legs.childBirths ?? (legs.childBirth === undefined ? [] : [legs.childBirth])) {
1193
+ ids.add(birth.birthDirectiveId);
1194
+ }
1195
+ for (const bootstrap of legs.relationBootstraps ?? (legs.relationBootstrap === undefined ? [] : [legs.relationBootstrap])) {
1196
+ ids.add(bootstrap.gatedDirectiveId);
1197
+ }
1198
+ generatedJourneyDirectiveIds.set(domain, ids);
1199
+ }
1200
+ } catch {
1201
+ // The ordinary findings and proof diagnostics retain their own exact failures. Never suppress from an
1202
+ // index we could not derive.
1203
+ }
1147
1204
 
1148
1205
  for (const mod of modules) {
1149
1206
  const domain = mod.domain ?? mod.name;
1207
+ const generatedJourney = generatedJourneyDirectiveIds.get(domain) ?? new Set<string>();
1150
1208
  const aggById = new Map<string, AggregateHandle>();
1151
1209
  for (const a of mod.aggregates) aggById.set(a.id, a);
1152
1210
 
1153
1211
  // Slice-2 twin: the module top level read the captured ports (pristine-pool ineligibility).
1154
1212
  findings.push(...topLevelPortReadFindings(mod, portReadingDomains));
1155
1213
 
1156
- // The NOTE twin (unify): the auto-declared secondary ensures composeDomainModule injected.
1157
- for (const ty of mod.autoDeclaredEnsures ?? []) {
1158
- findings.push({
1159
- domain,
1160
- level: "note",
1161
- what: `auto-declared Ensure→${ty} from a plan's secondary ensure(...) — the id-mint gate was opened for it`,
1162
- fix: `no action needed; to make it explicit, export a directive that \`.ensures(${ty})\``,
1163
- });
1164
- }
1165
-
1166
1214
  for (const dir of mod.directives as Directive<unknown>[]) {
1167
1215
  const where = `${domain}/${dir.id}`;
1168
1216
  const probe = probeDirective(dir, where);
1169
1217
  if (probe === undefined) continue; // unsynthesizable payload — proven by hand, not by check
1170
- findings.push(...planReturnFindings(domain, dir, probe));
1218
+ findings.push(...planReturnFindings(domain, dir, probe, generatedJourney.has(dir.id)));
1171
1219
  findings.push(...unreadPayloadFindings(domain, dir, probe));
1172
1220
  findings.push(...opVsSchemaFindings(domain, dir, probe, aggById));
1173
1221
  findings.push(...deletedAmbientFindings(domain, dir, probe));
@@ -78,12 +78,24 @@ export function synthesizePreconditionInvariants<P>(directive: Directive<P>): Wo
78
78
  id,
79
79
  on: directive.id,
80
80
  reads: (ctx: { intent: Record<string, unknown> }): InvariantRef[] => [
81
- refAs(REF_NAME, desc.aggregateId, desc.idField(ctx.intent)),
81
+ // Preconditions judge the state the directive is attempting to extend.
82
+ // The gate's reserved `pre:` namespace resolves the committed pre-apply
83
+ // aggregate; an ordinary ref would resolve the directive's trial result
84
+ // and therefore make every successful create appear to be a duplicate.
85
+ refAs(REF_NAME, desc.aggregateId, `pre:${desc.idField(ctx.intent)}`),
82
86
  ],
83
87
  assert: (snapshots: Record<string, Record<string, unknown>>) => {
84
- // The kernel resolves the ref into `snapshots[REF_NAME]`; an ABSENT instance leaves the key
85
- // undefined (proven safe — `homeNameBindsOwner`), so `undefined` is the "row does not exist" signal.
86
- const row = snapshots[REF_NAME] as Record<string, unknown> | undefined;
88
+ // Hosts historically represented an unresolved ref either by omitting
89
+ // the key or by returning an empty projection. Treat both identically;
90
+ // an admitted aggregate has at least one folded field after its atomic
91
+ // create intent, so an empty object is not a durable record.
92
+ const projected = snapshots[REF_NAME] as
93
+ | Record<string, unknown>
94
+ | undefined;
95
+ const row = projected !== undefined &&
96
+ desc.fieldNames.some((field) => Object.hasOwn(projected, field))
97
+ ? projected
98
+ : undefined;
87
99
  return desc.predicate(row) ? { accept: true } : { reject: desc.rejectCode };
88
100
  },
89
101
  ...(desc.declassify !== undefined ? { declassify: desc.declassify } : {}),
@@ -89,15 +89,10 @@ export const GLOBAL_READ_PROJECTION_AGGREGATE_IDS: ReadonlySet<string> = new Set
89
89
  "BirthPolicy",
90
90
  "BirthGrant",
91
91
  // home holons
92
- "HomeOwner",
93
92
  "SharedWithMe",
94
93
  "BirthAttestation",
95
- // identity
96
- "Actor",
97
- "User",
98
- "DeviceKey",
99
- "Organization",
100
- "PublicUser",
94
+ "NomosInternalReceivingGroup7c1f5e",
95
+ "NomosInternalReceivingGroupPolicy7c1f5e",
101
96
  // native relations (the one authz spine)
102
97
  "RelationTuple",
103
98
  // repair
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ import { zodDef, zodKind } from "./zod_introspection.js";
3
+
4
+ /**
5
+ * A business invitation recipient. It is wire-compatible with a string because
6
+ * law records the durable address, but generated applications expose the value
7
+ * as `Recipient<User>` so a UID, email address, principal, or record id cannot
8
+ * accidentally be passed in its place.
9
+ */
10
+ export const NOMOS_RECIPIENT_BRAND: unique symbol = Symbol.for("nomos.recipient");
11
+
12
+ export function recipient(): z.ZodType<string> {
13
+ const schema = z.string().min(1);
14
+ Object.defineProperty(schema, NOMOS_RECIPIENT_BRAND, {
15
+ value: "User",
16
+ enumerable: false,
17
+ });
18
+ return schema;
19
+ }
20
+
21
+ /** Resolve a recipient annotation through Zod's omission/null wrappers. */
22
+ export function recipientMetaOf(zt: z.ZodTypeAny): "User" | undefined {
23
+ let current: z.ZodTypeAny | undefined = zt;
24
+ for (let hops = 0; current !== undefined && hops < 16; hops += 1) {
25
+ if ((current as unknown as Record<symbol, unknown>)[NOMOS_RECIPIENT_BRAND] === "User") {
26
+ return "User";
27
+ }
28
+ const kind = zodKind(current);
29
+ if (kind !== "optional" && kind !== "default" && kind !== "nullable") return undefined;
30
+ current = zodDef(current).innerType;
31
+ }
32
+ return undefined;
33
+ }
@@ -112,6 +112,99 @@ export function isRelationTypeDecl(v: unknown): v is RelationTypeDecl {
112
112
  return typeof v === "object" && v !== null && (v as { __isRelationType?: unknown }).__isRelationType === true;
113
113
  }
114
114
 
115
+ /**
116
+ * Discover authored relation fragments without the generic declaration scanner's JSON
117
+ * deduplication. A relation declaration stores its relations in a Map, and every Map
118
+ * stringifies as `{}`; JSON deduplication therefore made two distinct fragments of the
119
+ * same type look identical and silently discarded one of them.
120
+ */
121
+ export function discoverRelationTypeDeclarations(
122
+ bag: Readonly<Record<string, unknown>>,
123
+ ): RelationTypeDecl[] {
124
+ const seen = new Set<RelationTypeDecl>();
125
+ const declarations: RelationTypeDecl[] = [];
126
+ const add = (value: unknown): void => {
127
+ if (!isRelationTypeDecl(value) || seen.has(value)) return;
128
+ seen.add(value);
129
+ declarations.push(value);
130
+ };
131
+ for (const value of Object.values(bag)) {
132
+ if (Array.isArray(value)) for (const entry of value) add(entry);
133
+ else add(value);
134
+ }
135
+ return declarations;
136
+ }
137
+
138
+ /**
139
+ * Compose independently authored fragments of one relation type into one complete
140
+ * declaration. Distinct relation names union; duplicate names and conflicting caps
141
+ * hard-fail. Validation happens on the complete type so a policy fragment may safely
142
+ * use `computed("owner")` when the owner relation is declared by another module in the
143
+ * same domain.
144
+ */
145
+ export function mergeRelationTypeDeclarations(
146
+ declarations: readonly RelationTypeDecl[],
147
+ ): RelationTypeDecl[] {
148
+ const unique = [...new Set(declarations)];
149
+ const groups = new Map<string, RelationTypeDecl[]>();
150
+ for (const declaration of unique) {
151
+ const key = `${declaration.global === true ? "global" : "local"}:${declaration.type}`;
152
+ const group = groups.get(key) ?? [];
153
+ group.push(declaration);
154
+ groups.set(key, group);
155
+ }
156
+
157
+ const merged: RelationTypeDecl[] = [];
158
+ for (const group of groups.values()) {
159
+ const first = group[0]!;
160
+ if (group.length === 1) {
161
+ validateRelationType(first);
162
+ merged.push(first);
163
+ continue;
164
+ }
165
+ const relations = new Map<string, RelationDefAst>();
166
+ let caps: RelationCaps | undefined;
167
+ for (const declaration of group) {
168
+ if (declaration.caps !== undefined) {
169
+ if (
170
+ caps !== undefined &&
171
+ (caps.maxDepth !== declaration.caps.maxDepth || caps.maxFanout !== declaration.caps.maxFanout)
172
+ ) {
173
+ throw new Error(`relationType("${declaration.type}"): conflicting caps across declarations`);
174
+ }
175
+ caps = declaration.caps;
176
+ }
177
+ for (const [relation, definition] of declaration.relations) {
178
+ const existing = relations.get(relation);
179
+ if (existing !== undefined) {
180
+ // Framework-owned declarations can cross the published-CLI/package bundle boundary as
181
+ // distinct object identities. The private brand plus byte-identical definition identifies
182
+ // that one canonical concept; tenant-authored collisions remain a loud hard failure.
183
+ const repeatedFrameworkDeclaration =
184
+ (first as RelationTypeDecl & { businessBirthInternal?: boolean }).businessBirthInternal === true &&
185
+ (declaration as RelationTypeDecl & { businessBirthInternal?: boolean }).businessBirthInternal === true &&
186
+ JSON.stringify(existing) === JSON.stringify(definition);
187
+ if (repeatedFrameworkDeclaration) continue;
188
+ throw new Error(
189
+ `relationType("${declaration.type}"): relation "${relation}" declared in two relationType() blocks`,
190
+ );
191
+ }
192
+ relations.set(relation, definition);
193
+ }
194
+ }
195
+ const declaration: RelationTypeDecl = {
196
+ type: first.type,
197
+ relations,
198
+ ...(caps === undefined ? {} : { caps }),
199
+ ...(first.global === true ? { global: true } : {}),
200
+ __isRelationType: true,
201
+ };
202
+ validateRelationType(declaration);
203
+ merged.push(declaration);
204
+ }
205
+ return merged;
206
+ }
207
+
115
208
  // ─────────────────────────────── relation-type NAMESPACING ───────────────────────────────
116
209
 
117
210
  /**
@@ -57,6 +57,10 @@ export function sanctioningEnsureDirective(agg: AggregateHandle): Directive<unkn
57
57
  .ensures(agg)
58
58
  .payload(z.object({}).strict())
59
59
  .plan(() => []) as unknown as Directive<unknown>;
60
+ // This declaration exists only to certify the id-mint lane. It is never a
61
+ // tenant action and must not appear in generated clients, applications, or
62
+ // OpenAPI, irrespective of whether the aggregate itself is framework-owned.
63
+ (dir as { businessBirthInternal?: true }).businessBirthInternal = true;
60
64
  // FRAMEWORK-SHARED DEDUP (Dart codegen): when the sanctioned aggregate is a FRAMEWORK singleton
61
65
  // (e.g. RelationTuple — auto-ensured into EVERY relation-using domain), this directive is injected
62
66
  // per-domain but its payload class (`NomosEnsure_<Agg>Payload`, zero-field) is byte-identical across
@@ -73,8 +77,10 @@ export function sanctioningEnsureDirective(agg: AggregateHandle): Directive<unkn
73
77
 
74
78
  /**
75
79
  * Discover the secondary-ensured types a domain's directives introduce that NO directive already
76
- * declares `.ensures()` for — the SORTED gap set the compose seam must sanction. Pure discovery:
77
- * probes every directive, unions the ensured types, subtracts the domain-wide DECLARED-ensures set.
80
+ * declares as its PRIMARY `.ensures()` target — the SORTED gap set the compose seam must sanction.
81
+ * Declared `.alsoEnsures()` outputs are included directly as well as plan-probed ensures. New kernels
82
+ * understand `secondaryOutputs`, but older supported kernels only open the deterministic-id lane from
83
+ * a primary Ensure marker; the hidden sanction keeps one compiled package portable across both.
78
84
  */
79
85
  export function discoverUnsanctionedEnsures(
80
86
  directives: readonly Directive<unknown>[],
@@ -87,6 +93,9 @@ export function discoverUnsanctionedEnsures(
87
93
  }
88
94
  const gap = new Set<string>();
89
95
  for (const d of directives) {
96
+ for (const output of (d as { secondaryOutputs?: readonly { aggregateId: string; marker: string }[] }).secondaryOutputs ?? []) {
97
+ if (output.marker === "ensures" && !declared.has(output.aggregateId)) gap.add(output.aggregateId);
98
+ }
90
99
  // A `.births()` directive's genesis-recipe plan runs under a different probe surface; its meter
91
100
  // record is sanctioned by `birth_meter_ensure.ts`, so leave it to that lane (avoid double probing).
92
101
  if ((d as { birthsChild?: boolean }).birthsChild === true) continue;
@@ -26,6 +26,7 @@ import { z } from "zod";
26
26
  import { introspectPayload, type DomainModule, type PayloadFieldSpec } from "./codegen_dart.js";
27
27
  import type { StableIds } from "./stable_ids_types.js";
28
28
  import type { Field } from "./fields.js";
29
+ import { isBusinessIntent } from "./intent.js";
29
30
 
30
31
  /** One payload field's normalized shape: its kind + whether the schema makes it optional. */
31
32
  export interface PayloadFieldShape {
@@ -126,6 +127,12 @@ function payloadFieldKind(spec: PayloadFieldSpec): string {
126
127
  case "wrapped":
127
128
  // typed-id (string wire) or unit (number wire) — the interface sees the WIRE kind.
128
129
  return spec.dartType.endsWith("Id") ? "string" : "double";
130
+ case "ref":
131
+ // NomosRef<T> is aggregate-typed in the client and remains a string id on the wire.
132
+ return "string";
133
+ case "recipient":
134
+ // Recipient<User> is nominal in clients and an opaque delivery address on the law wire.
135
+ return "string";
129
136
  case "vo":
130
137
  case "voList":
131
138
  // Interface kinds stay at the base lane for now (slice D of vo_authoring_end_to_end.md
@@ -192,7 +199,10 @@ export function domainInterface(mod: DomainModule, stableIds?: StableIds): Domai
192
199
  fieldEntries.push([spec.name, { kind: payloadFieldKind(spec), optional: !!spec.optional }]);
193
200
  }
194
201
  }
195
- dirEntries.push([d.businessBirth === undefined ? d.id : "birth", { fields: sortedRec(fieldEntries) }]);
202
+ dirEntries.push([
203
+ d.businessBirth === undefined || isBusinessIntent(d) ? d.id : "birth",
204
+ { fields: sortedRec(fieldEntries) },
205
+ ]);
196
206
  }
197
207
  Object.assign(directives, sortedRec(dirEntries));
198
208
  const aggregates: Record<string, AggregateReadShape> = {};
package/src/usd.ts CHANGED
@@ -72,7 +72,7 @@ export type RefMode = WireRefMode;
72
72
  * (an aggregate is a single-source schema); they pass through by path.
73
73
  * - `directive`: a composable law unit. `requires` is the capability set (#137:
74
74
  * add/narrow, never silently remove); `scope` is the optional composable scope
75
- * path (#137: narrow-not-widen). `payloadRef` is the certified-artifact handle
75
+ * path (#137: narrow-not-widen). `artifactRef` is the certified-artifact handle
76
76
  * — left `undefined` here (stage 2).
77
77
  */
78
78
  export type UsdPrim =
@@ -123,7 +123,7 @@ export type UsdPrim =
123
123
  */
124
124
  readonly payloadJsonSchema?: unknown;
125
125
  /** Certified-artifact payload reference — `undefined` for now (stage 2). */
126
- readonly payloadRef?: string;
126
+ readonly artifactRef?: string;
127
127
  };
128
128
 
129
129
  /** One captured query declaration on a USD layer (read-side closure). `key` is the
@@ -753,7 +753,7 @@ function sortPrims(prims: UsdPrim[]): UsdPrim[] {
753
753
  * `manifest.ts`'s `domainManifest` (and therefore its `encodeKernelSchema` driver encoding)
754
754
  * — no driver-encoding logic is duplicated here. Aggregate prims carry the exact
755
755
  * encoded schema; directive prims map `requiresCapability` → `requires: [cap]`, leave
756
- * `scope`/`payloadRef` `undefined` (no static scope today; payload is stage 2).
756
+ * `scope`/`artifactRef` `undefined` (no static scope or certified artifact today).
757
757
  */
758
758
  function encodeModuleToPrims(layerPath: string, manifest: CanonicalManifest): UsdPrim[] {
759
759
  const prims: UsdPrim[] = [];
@@ -789,7 +789,7 @@ function encodeModuleToPrims(layerPath: string, manifest: CanonicalManifest): Us
789
789
  // Omit-when-empty from the manifest, so a single-output prim is byte-identical to before.
790
790
  ...(d.secondaryOutputs !== undefined ? { secondaryOutputs: d.secondaryOutputs } : {}),
791
791
  payloadJsonSchema: d.payloadJsonSchema,
792
- // `scope`/`payloadRef` deliberately omitted (undefined): no static scope in
792
+ // `scope`/`artifactRef` deliberately omitted (undefined): no static scope in
793
793
  // the DSL today, payload is the stage-2 certified artifact.
794
794
  });
795
795
  }
@@ -930,6 +930,7 @@ function serializeRelationSchema(
930
930
  domainKey: string,
931
931
  ): UsdLayer["nomosRelations"] {
932
932
  if (decls.length === 0) return undefined;
933
+ const namespacedTypes = new Set(decls.filter((decl) => decl.global !== true).map((decl) => decl.type));
933
934
  type AllowedW = { kind: string; type?: string; ns?: string; relation?: string };
934
935
  const allowedKey = (a: AllowedW): string =>
935
936
  a.kind === "direct" ? `direct:${a.type}` : a.kind === "wildcard" ? "wildcard" : `userset:${a.type}#${a.relation}`;
@@ -952,7 +953,18 @@ function serializeRelationSchema(
952
953
  }
953
954
  for (const [rel, def] of d.relations) {
954
955
  if (entry.relations.has(rel)) throw new Error(`relationType("${d.type}"): relation "${rel}" declared in two relationType() blocks`);
955
- const allowed = [...def.allowed].sort((x, y) => (allowedKey(x as AllowedW) < allowedKey(y as AllowedW) ? -1 : 1));
956
+ // An allowed object/userset subject names another relation type. When that target is local to this
957
+ // domain it must carry the SAME qualifier as the schema key and objectOf(...) tuple subject; otherwise
958
+ // a perfectly typed parent edge is stored under `<domain>__Parent` while the gate only permits bare
959
+ // `Parent`, making tuple-to-userset inheritance silently dead. Framework-global user/key types stay bare.
960
+ const allowed = [...def.allowed]
961
+ .map((subject) => {
962
+ if ((subject.kind === "direct" || subject.kind === "userset") && namespacedTypes.has(subject.type)) {
963
+ return { ...subject, type: qualifyRelationType(domainKey, subject.type) };
964
+ }
965
+ return subject;
966
+ })
967
+ .sort((x, y) => (allowedKey(x as AllowedW) < allowedKey(y as AllowedW) ? -1 : 1));
956
968
  entry.relations.set(rel, { rewrite: def.rewrite, allowed });
957
969
  }
958
970
  }
@@ -982,6 +994,16 @@ function synthesizeReadRelationTypes(
982
994
  for (const a of manifest.aggregates) {
983
995
  if (a.requiresRead !== undefined && a.owned === undefined) caps.add(a.requiresRead);
984
996
  }
997
+ // Pack acquisition happens before per-record filtering/decryption. Any law with owned state therefore
998
+ // needs the same obscure capability at workspace grain, and the receiving-group genesis always writes
999
+ // that seat so a standalone group law remains self-consistent too. This grants ciphertext transport;
1000
+ // each owned record still has its separately synthesized aggregate-grain relation below.
1001
+ if (
1002
+ manifest.aggregates.some((aggregate) => aggregate.owned !== undefined) ||
1003
+ manifest.aggregates.some((aggregate) => aggregate.id === "NomosInternalReceivingGroup7c1f5e")
1004
+ ) {
1005
+ caps.add(OWNED_READ_RELATION);
1006
+ }
985
1007
  const out: import("./relation_schema.js").RelationTypeDecl[] = [];
986
1008
  // GLOBAL (un-namespaced): the read gate hardcodes `ObjectRef::new("workspace","self")` in Rust
987
1009
  // (`wasm_git_holon.rs`), so the synthesized read-visibility `workspace` type MUST stay bare — it is
@@ -1542,7 +1564,7 @@ export function emitUsd(
1542
1564
  // DOMAIN CONTRACTS: the additive `nomosContracts` descriptor (hash-inert, omit-when-empty).
1543
1565
  const nomosContracts = buildNomosContracts(l.module);
1544
1566
  const flowEndorse = [...l.module.directives]
1545
- .flatMap((d) => harvestPrincipalFlows(d as never).map((f) => ({ directiveId: d.id, sink: f.sink, facet: f.facet, reason: f.reason })))
1567
+ .flatMap((d) => harvestPrincipalFlows(d as never, l.module.domain ?? l.module.name).map((f) => ({ directiveId: d.id, sink: f.sink, facet: f.facet, reason: f.reason })))
1546
1568
  .sort((a, b) =>
1547
1569
  a.directiveId !== b.directiveId ? (a.directiveId < b.directiveId ? -1 : 1)
1548
1570
  : a.sink !== b.sink ? (a.sink < b.sink ? -1 : 1)
@@ -1645,7 +1667,7 @@ function aggregateEqual(
1645
1667
  * Merge a stronger `over` prim onto a `base` prim at the SAME path, per the #137
1646
1668
  * typed rules — reusing `compose.ts`'s `mergeRequiresScope` for directive prims
1647
1669
  * (requires add/narrow-not-remove; scope narrow-not-widen; widen/remove need
1648
- * `authority`). An uncertified payload would always be refused — but `payloadRef`
1670
+ * `authority`). An uncertified artifact would always be refused — but `artifactRef`
1649
1671
  * is unset here (stage 2), so there is nothing to refuse yet.
1650
1672
  *
1651
1673
  * Mixing prim KINDS at one path, or redefining an aggregate's schema across layers,
@@ -1720,7 +1742,7 @@ function mergeUsdPrim(base: UsdPrim, over: UsdPrim, opts: ComposeOptions): UsdPr
1720
1742
  : base.emits !== undefined
1721
1743
  ? { emits: base.emits }
1722
1744
  : {}),
1723
- // payloadRef stays unset (stage 2).
1745
+ // artifactRef stays unset (a later certified-artifact slice).
1724
1746
  };
1725
1747
  return out;
1726
1748
  }