@githolon/dsl 0.108.0 → 0.108.2

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 (95) hide show
  1. package/dart/CHANGELOG.md +5 -0
  2. package/dart/lib/src/application_framework_calls.dart +299 -0
  3. package/dart/lib/src/generated_call.dart +96 -0
  4. package/dart/lib/src/nms_generated_runtime.dart +29 -0
  5. package/dart/lib/src/node_runner_transport.dart +77 -1
  6. package/dart/lib/src/result_usda.dart +487 -0
  7. package/dart/lib/src/runner/node-entry.mjs +6401 -2102
  8. package/dart/lib/src/runner/offline-install.mjs +1538 -163
  9. package/dart/lib/src/runtime_v2_gateway.dart +54 -0
  10. package/dart/lib/src/runtime_v2_transport.dart +117 -0
  11. package/dart/lib/src/session_bound.dart +7 -2
  12. package/dart/lib/src/test_harness.dart +193 -112
  13. package/dart/lib/src/webview_bridge.dart +2120 -569
  14. package/dart/pubspec.yaml +2 -2
  15. package/dart/test/acquire_session_credentials_test.dart +1 -1
  16. package/dart/test/acquire_session_test.dart +68 -14
  17. package/dart/test/adv_realm_interference_test.dart +6 -4
  18. package/dart/test/cluster_births_test.dart +11 -8
  19. package/dart/test/cluster_local_autosync_test.dart +1 -1
  20. package/dart/test/cluster_scale_test.dart +3 -2
  21. package/dart/test/dlq_session_surface_test.dart +52 -35
  22. package/dart/test/first_app_gauntlet_test.dart +1 -1
  23. package/dart/test/local_sync_inert_test.dart +23 -26
  24. package/dart/test/offer_authentication_proof_test.dart +193 -0
  25. package/dart/test/result_usda_test.dart +49 -0
  26. package/dart/test/session_bound_test.dart +8 -11
  27. package/dart/test/stale_bundle_test.dart +18 -3
  28. package/dart/test/test_harness_test.dart +14 -27
  29. package/dart/test/webview_bridge_test.dart +504 -83
  30. package/dart_types/CHANGELOG.md +7 -0
  31. package/dart_types/lib/nomos_kernel_protocol.dart +3 -2
  32. package/dart_types/lib/src/custody.dart +64 -19
  33. package/dart_types/lib/src/local_work_rejected.dart +30 -45
  34. package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pb.dart +169 -0
  35. package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pbenum.dart +11 -0
  36. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +2021 -234
  37. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +175 -8
  38. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +8664 -0
  39. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pbenum.dart +369 -0
  40. package/dart_types/lib/src/surfaced.dart +30 -25
  41. package/dart_types/pubspec.yaml +1 -1
  42. package/dart_types/test/surfaced_test.dart +2 -1
  43. package/package.json +1 -1
  44. package/runtime/generated-application-host.js +5736 -2047
  45. package/src/authoring.ts +3 -2
  46. package/src/births.ts +21 -0
  47. package/src/build_package.ts +30 -0
  48. package/src/business_birth.ts +39 -35
  49. package/src/codegen_call_usda.ts +1111 -0
  50. package/src/codegen_dart.ts +207 -282
  51. package/src/codegen_dart_application.ts +195 -252
  52. package/src/codegen_dart_client.ts +1978 -593
  53. package/src/codegen_dart_framework_calls.ts +291 -0
  54. package/src/codegen_dart_scenario.ts +7 -0
  55. package/src/codegen_proof.ts +3 -4
  56. package/src/codegen_provider_dart.ts +21 -7
  57. package/src/codegen_query_usda.ts +214 -0
  58. package/src/codegen_result_usda.ts +685 -0
  59. package/src/codegen_ts.ts +3945 -1111
  60. package/src/compile_package_main.ts +360 -32
  61. package/src/directive.ts +19 -4
  62. package/src/engine_entry.ts +45 -4
  63. package/src/framework/application_home.ts +2 -0
  64. package/src/framework/auth_provider_law.ts +262 -20
  65. package/src/framework/home.ts +32 -28
  66. package/src/framework/owned_custody.ts +19 -11
  67. package/src/framework/owned_custody_value.ts +51 -0
  68. package/src/framework/relations.ts +57 -21
  69. package/src/framework/signing_identity.ts +34 -0
  70. package/src/framework/workspaces.ts +31 -9
  71. package/src/framework_call_definitions.ts +53 -0
  72. package/src/framework_read_definitions.ts +45 -0
  73. package/src/identity_frontier.ts +58 -0
  74. package/src/index.ts +20 -1
  75. package/src/intent.ts +216 -16
  76. package/src/law_check.ts +19 -1
  77. package/src/law_graph.ts +57 -8
  78. package/src/law_graph_usda.ts +73 -5
  79. package/src/law_value_type.ts +106 -19
  80. package/src/read.ts +47 -0
  81. package/src/read_projection_namespace.ts +7 -0
  82. package/src/shape_compat.ts +38 -39
  83. package/src/stable_ids.ts +37 -2
  84. package/src/stable_ids_for_module.ts +64 -4
  85. package/src/test_call_usda.ts +83 -0
  86. package/src/wire.ts +36 -2
  87. package/src/wire_encode.ts +45 -1
  88. package/src/workspace_sharing.ts +27 -10
  89. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbjson.dart +0 -6096
  90. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbserver.dart +0 -50
  91. package/dart_types/lib/src/proto/nomos/kernel/v1/progress.pbjson.dart +0 -178
  92. package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pb.dart +0 -828
  93. package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pbenum.dart +0 -465
  94. package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pbjson.dart +0 -573
  95. package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pbserver.dart +0 -50
package/src/intent.ts CHANGED
@@ -19,6 +19,7 @@ import type { Ports } from "./ctx.js";
19
19
  import type { PlannedOp } from "./ops.js";
20
20
  import type { Field } from "./fields.js";
21
21
  import type { AggregateHandle } from "./aggregate.js";
22
+ import type { QueryDecl } from "./query.js";
22
23
  import type {
23
24
  InvariantReadBinding,
24
25
  InvariantReadId,
@@ -45,6 +46,18 @@ export interface IntentStep<I = unknown, P = unknown> {
45
46
  readonly payload: (facts: I) => P;
46
47
  }
47
48
 
49
+ /** One cross-workspace consequence authored by source law. The destination mapper supplies only
50
+ * the target workspace address; the nominal target directive and payload type remain sealed by the
51
+ * compiled source graph and its generated USDA encoder. */
52
+ export interface WorkspaceEffectStep<I = unknown, P = unknown> {
53
+ readonly target: Directive<P>;
54
+ readonly destination:
55
+ | { readonly kind: "workspace"; readonly address: (facts: I) => string }
56
+ | { readonly kind: "verifiedParent" }
57
+ | { readonly kind: "verifiedSource" };
58
+ readonly payload: (facts: I) => P;
59
+ }
60
+
48
61
  const PAYLOAD_TRACE_PATH = Symbol("nomos.intent.payload-trace-path");
49
62
 
50
63
  type PayloadTrace = {
@@ -175,6 +188,10 @@ export interface BusinessIntent<I = unknown> extends Directive<I> {
175
188
  readonly [INTENT_BRAND]: true;
176
189
  readonly [BUSINESS_BIRTH_POLICY_BINDING]?: (domains: readonly string[]) => void;
177
190
  readonly steps: readonly IntentStep<I, any>[];
191
+ readonly workspaceEffects: readonly WorkspaceEffectStep<I, any>[];
192
+ /** Law-selected proof for this exact public offer. Omitted means the ordinary session proof
193
+ * posture. Tenant callers never choose this at invocation time. */
194
+ readonly offerAuthenticationProof?: "enrolledDevice";
178
195
  /** The complete public business boundary. Runtime lowering still uses Directive.payloadSchema privately. */
179
196
  readonly contract: IntentContract<I>;
180
197
  }
@@ -311,6 +328,11 @@ class IntentBuilder<I> {
311
328
  private readonly id: string,
312
329
  private readonly contractDeclaration: IntentContractDeclaration<I>,
313
330
  private readonly steps: readonly IntentStep<I, any>[] = [],
331
+ private readonly workspaceEffects: readonly WorkspaceEffectStep<I, any>[] = [],
332
+ private readonly intentReads: readonly (string | QueryDecl)[] = [],
333
+ private readonly acceptsWorkspaceEffects = false,
334
+ private readonly intentRelationGate?: NonNullable<Directive["requiresRelationGate"]>,
335
+ private readonly offerAuthenticationProof?: "enrolledDevice",
314
336
  ) {}
315
337
 
316
338
  /** Add one private directive consequence to this business intent. */
@@ -324,7 +346,148 @@ class IntentBuilder<I> {
324
346
  return new IntentBuilder(this.id, this.contractDeclaration, [
325
347
  ...this.steps,
326
348
  { directive, payload },
327
- ]);
349
+ ], this.workspaceEffects, this.intentReads, this.acceptsWorkspaceEffects, this.intentRelationGate, this.offerAuthenticationProof);
350
+ }
351
+
352
+ /** Author one exact nominal call into another workspace. The caller chooses only the target
353
+ * workspace address. Passing the target Directive object preserves the application type and lets
354
+ * the compiler bind its stable USDA identity; no operation string or host-selected mapping exists. */
355
+ directsWorkspace<P>(
356
+ target: Directive<P>,
357
+ effect: { readonly workspace: (facts: I) => string; readonly payload: (facts: I) => P },
358
+ ): IntentBuilder<I> {
359
+ if (isBusinessIntent(target) && target.acceptsWorkspaceEffectGate !== true) {
360
+ throw new Error(
361
+ `intent('${this.id}') workspace effect may target a public intent only when target law explicitly accepts workspace effects`,
362
+ );
363
+ }
364
+ return new IntentBuilder(this.id, this.contractDeclaration, this.steps, [
365
+ ...this.workspaceEffects,
366
+ {
367
+ target,
368
+ destination: { kind: "workspace", address: effect.workspace },
369
+ payload: effect.payload,
370
+ },
371
+ ], this.intentReads, this.acceptsWorkspaceEffects, this.intentRelationGate, this.offerAuthenticationProof);
372
+ }
373
+
374
+ /** Send an exact nominal request to this workspace's verified immediate parent. The route is
375
+ * resolved by the kernel from birth custody after the source intent admits; no workspace string
376
+ * enters the public payload or the JavaScript execution boundary. */
377
+ directsParentWorkspace<P>(
378
+ target: Directive<P>,
379
+ effect: { readonly payload: (facts: I) => P },
380
+ ): IntentBuilder<I> {
381
+ if (isBusinessIntent(target) && target.acceptsWorkspaceEffectGate !== true) {
382
+ throw new Error(
383
+ `intent('${this.id}') parent request may target a public intent only when target law explicitly accepts workspace effects`,
384
+ );
385
+ }
386
+ return new IntentBuilder(this.id, this.contractDeclaration, this.steps, [
387
+ ...this.workspaceEffects,
388
+ {
389
+ target,
390
+ destination: { kind: "verifiedParent" },
391
+ payload: effect.payload,
392
+ },
393
+ ], this.intentReads, this.acceptsWorkspaceEffects, this.intentRelationGate, this.offerAuthenticationProof);
394
+ }
395
+
396
+ /** Reply to the exact workspace whose carried effect invoked this intent. The destination is
397
+ * kernel-captured only after the source custody proof succeeds; it is neither public payload nor
398
+ * an authority subject. This keeps request/reply routing out of application data while target law
399
+ * still authorizes the verified workspace actor through its ordinary relation gate. */
400
+ repliesWorkspace<P>(
401
+ target: Directive<P>,
402
+ effect: { readonly payload: (facts: I) => P },
403
+ ): IntentBuilder<I> {
404
+ if (isBusinessIntent(target)) {
405
+ throw new Error(
406
+ `intent('${this.id}') workspace reply must target a private accepted directive, not another public intent`,
407
+ );
408
+ }
409
+ return new IntentBuilder(this.id, this.contractDeclaration, this.steps, [
410
+ ...this.workspaceEffects,
411
+ {
412
+ target,
413
+ destination: { kind: "verifiedSource" },
414
+ payload: effect.payload,
415
+ },
416
+ ], this.intentReads, this.acceptsWorkspaceEffects, this.intentRelationGate, this.offerAuthenticationProof);
417
+ }
418
+
419
+ /** Admit this public request only through the cross-workspace effect door. Authority remains the
420
+ * intent's ordinary static `.requires(..., {object:"workspace:self"})` relation gate. */
421
+ acceptsWorkspaceEffect(): IntentBuilder<I> {
422
+ return new IntentBuilder(
423
+ this.id,
424
+ this.contractDeclaration,
425
+ this.steps,
426
+ this.workspaceEffects,
427
+ this.intentReads,
428
+ true,
429
+ this.intentRelationGate,
430
+ this.offerAuthenticationProof,
431
+ );
432
+ }
433
+
434
+ /** Require this law-declared offer to prove the author through the workspace's enrolled device
435
+ * relation. Generated clients therefore withhold any session provider token for this intent;
436
+ * callers cannot downgrade or override the proof at invocation time. */
437
+ authenticatesWithEnrolledDevice(): IntentBuilder<I> {
438
+ return new IntentBuilder(
439
+ this.id,
440
+ this.contractDeclaration,
441
+ this.steps,
442
+ this.workspaceEffects,
443
+ this.intentReads,
444
+ this.acceptsWorkspaceEffects,
445
+ this.intentRelationGate,
446
+ "enrolledDevice",
447
+ );
448
+ }
449
+
450
+ /** Put the authorization requirement on the business action itself. This is the honest shape for
451
+ * a pure request/reply action that has no local storage consequence on which to hang a gate. */
452
+ requires(
453
+ relation: string,
454
+ options: { readonly object: "workspace:self" } = { object: "workspace:self" },
455
+ ): IntentBuilder<I> {
456
+ if (!relation.trim()) throw new Error(`intent('${this.id}') requires a non-empty relation`);
457
+ const gate = { relation, object: options.object };
458
+ if (this.intentRelationGate !== undefined && stable(this.intentRelationGate) !== stable(gate)) {
459
+ throw new Error(`intent('${this.id}') already has a different authorization gate`);
460
+ }
461
+ return new IntentBuilder(
462
+ this.id,
463
+ this.contractDeclaration,
464
+ this.steps,
465
+ this.workspaceEffects,
466
+ this.intentReads,
467
+ this.acceptsWorkspaceEffects,
468
+ gate,
469
+ this.offerAuthenticationProof,
470
+ );
471
+ }
472
+
473
+ /** Declare the bounded source reads used to author this business action's consequences.
474
+ * This belongs on the intent because a pure cross-workspace effect has no honest local
475
+ * directive on which to hang its captured-read contract. */
476
+ reads(...reads: readonly (string | QueryDecl)[]): IntentBuilder<I> {
477
+ const byKey = new Map<string, string | QueryDecl>();
478
+ for (const read of [...this.intentReads, ...reads]) {
479
+ byKey.set(typeof read === "string" ? `ref:${read}` : `query:${read.id}`, read);
480
+ }
481
+ return new IntentBuilder(
482
+ this.id,
483
+ this.contractDeclaration,
484
+ this.steps,
485
+ this.workspaceEffects,
486
+ [...byKey.values()],
487
+ this.acceptsWorkspaceEffects,
488
+ this.intentRelationGate,
489
+ this.offerAuthenticationProof,
490
+ );
328
491
  }
329
492
 
330
493
  /** Add one declared child birth as a consequence; Nomos owns the private genesis recipe. */
@@ -336,12 +499,12 @@ class IntentBuilder<I> {
336
499
  return new IntentBuilder(this.id, this.contractDeclaration, [
337
500
  ...this.steps,
338
501
  { directive, payload },
339
- ]);
502
+ ], this.workspaceEffects, this.intentReads, this.acceptsWorkspaceEffects, this.intentRelationGate, this.offerAuthenticationProof);
340
503
  }
341
504
 
342
505
  /** Finish the intent and lower its directive graph to one atomic offer. */
343
506
  atomically(): BusinessIntent<I> {
344
- if (this.steps.length === 0) {
507
+ if (this.steps.length === 0 && this.workspaceEffects.length === 0) {
345
508
  throw new Error(`intent('${this.id}') needs at least one directed consequence`);
346
509
  }
347
510
  const duplicate = this.steps.find(
@@ -364,7 +527,8 @@ class IntentBuilder<I> {
364
527
  }
365
528
 
366
529
  const primary = this.steps.find((step) => step.directive.birthsChild !== true)?.directive
367
- ?? this.steps[0]!.directive;
530
+ ?? this.steps[0]?.directive
531
+ ?? this.workspaceEffects[0]!.target;
368
532
  const allOutputs: SecondaryOutput[] = [];
369
533
  for (const step of this.steps) {
370
534
  const candidate = step.directive;
@@ -392,19 +556,21 @@ class IntentBuilder<I> {
392
556
  ? { optional: true }
393
557
  : {}),
394
558
  });
395
- const primaryOutput = composedOutput(
396
- outputGroups.get(`${primary.marker}\0${primary.aggregateId}`) ?? [{
397
- aggregateId: primary.aggregateId,
398
- marker: primary.marker,
399
- }],
400
- );
559
+ const primaryOutput = this.steps.length === 0
560
+ ? undefined
561
+ : composedOutput(
562
+ outputGroups.get(`${primary.marker}\0${primary.aggregateId}`) ?? [{
563
+ aggregateId: primary.aggregateId,
564
+ marker: primary.marker,
565
+ }],
566
+ );
401
567
  const secondaryOutputs = [...outputGroups.values()]
402
568
  .map(composedOutput)
403
569
  .filter(
404
570
  (output) => !(output.aggregateId === primary.aggregateId && output.marker === primary.marker),
405
571
  );
406
572
  const inferredReturns = [
407
- primary.aggregateId,
573
+ ...(primaryOutput === undefined ? [] : [primary.aggregateId]),
408
574
  ...secondaryOutputs.map((output) => output.aggregateId),
409
575
  ...birthSteps.flatMap((step) => {
410
576
  const aggregateId = (step.directive as BusinessBirthDirective).businessBirth?.aggregateId;
@@ -451,8 +617,14 @@ class IntentBuilder<I> {
451
617
  };
452
618
  }),
453
619
  );
620
+ const intentQueryReads = this.intentReads.filter(
621
+ (read): read is QueryDecl => typeof read !== "string",
622
+ );
623
+ const intentRefReads = this.intentReads.filter(
624
+ (read): read is string => typeof read === "string",
625
+ );
454
626
  const queryReads = dedupeBy(
455
- this.steps.flatMap((step) => step.directive.declaredQueryReads ?? []),
627
+ [...intentQueryReads, ...this.steps.flatMap((step) => step.directive.declaredQueryReads ?? [])],
456
628
  (query) => query.id,
457
629
  );
458
630
  const foreignReads = dedupeBy(
@@ -472,7 +644,24 @@ class IntentBuilder<I> {
472
644
  }
473
645
  }
474
646
  }
475
- const relationGate = oneStaticRelationGate(this.steps);
647
+ const inheritedRelationGate = oneStaticRelationGate(this.steps);
648
+ if (
649
+ this.intentRelationGate !== undefined &&
650
+ inheritedRelationGate !== undefined &&
651
+ stable(this.intentRelationGate) !== stable(inheritedRelationGate)
652
+ ) {
653
+ throw new Error(`intent('${this.id}') has conflicting intent and consequence authorization gates`);
654
+ }
655
+ const relationGate = this.intentRelationGate ?? inheritedRelationGate;
656
+ if (
657
+ this.acceptsWorkspaceEffects &&
658
+ (relationGate === undefined || relationGate.object !== "workspace:self" || relationGate.objectFrom !== undefined)
659
+ ) {
660
+ throw new Error(
661
+ `intent('${this.id}') .acceptsWorkspaceEffect() requires one static ` +
662
+ `.requires(relation, { object: "workspace:self" }) consequence gate`,
663
+ );
664
+ }
476
665
  const sourceTrusts = this.steps.flatMap((step) =>
477
666
  step.directive.businessSourceTrust === undefined
478
667
  ? []
@@ -520,16 +709,26 @@ class IntentBuilder<I> {
520
709
  // engine cannot rebuild its aggregate registry entry and silently drops
521
710
  // compound intents that also birth a child.
522
711
  ...(primary.aggregate === undefined ? {} : { aggregate: primary.aggregate }),
523
- ...(primaryOutput.many === true
712
+ ...(primaryOutput?.many === true
524
713
  ? { outputCardinality: "many" as const }
525
- : primaryOutput.optional === true
714
+ : primaryOutput?.optional === true
526
715
  ? { outputCardinality: "optional" as const }
527
716
  : {}),
528
717
  ...(secondaryOutputs.length > 0 ? { secondaryOutputs } : {}),
529
718
  payloadSchema: (birthSteps[0]?.directive.payloadSchema ?? this.contractDeclaration.accepts) as unknown as z.ZodType<I>,
530
719
  plan,
531
720
  steps,
532
- declaredReads: [...new Set(this.steps.flatMap((step) => step.directive.declaredReads))].sort(),
721
+ workspaceEffects: this.workspaceEffects,
722
+ ...(this.offerAuthenticationProof === undefined
723
+ ? {}
724
+ : { offerAuthenticationProof: this.offerAuthenticationProof }),
725
+ ...(this.acceptsWorkspaceEffects ? { acceptsWorkspaceEffectGate: true as const } : {}),
726
+ declaredReads: [
727
+ ...new Set([
728
+ ...intentRefReads,
729
+ ...this.steps.flatMap((step) => step.directive.declaredReads),
730
+ ]),
731
+ ].sort(),
533
732
  ...(queryReads.length > 0 ? { declaredQueryReads: queryReads } : {}),
534
733
  ...(foreignReads.length > 0 ? { declaredForeignReads: foreignReads } : {}),
535
734
  declaredEmits: emits,
@@ -597,6 +796,7 @@ class IntentContractStep {
597
796
  refuses: Object.freeze([]),
598
797
  },
599
798
  steps: [step],
799
+ workspaceEffects: [],
600
800
  plan: (facts, ctx) => effect
601
801
  .plan(effect.payloadSchema.parse(facts), ctx)
602
802
  .map((op) => composePrimaryConsequenceOp(effect, op)),
package/src/law_check.ts CHANGED
@@ -50,6 +50,7 @@ import { finishExtremum } from "./extremum.js";
50
50
  import { finishExists } from "./exists.js";
51
51
  import { clearDirectiveId } from "./clear_directive.js";
52
52
  import type { ForeignReadEdge } from "./attested_core.js";
53
+ import { isBusinessIntent } from "./intent.js";
53
54
 
54
55
  /** One author-time finding — the gate refusal pre-fired at compile, or a courtesy note. */
55
56
  export interface Finding {
@@ -116,7 +117,7 @@ function probeDirective(dir: Directive<unknown>, where: string): ProbeResult | u
116
117
  const probePayload: Record<string, unknown> = { ...synth.value };
117
118
  for (const f of synth.autoStamped) probePayload[f] = "2026-01-01T00:00:00Z";
118
119
  if ((dir as { businessBirth?: unknown }).businessBirth !== undefined) {
119
- probePayload[BUSINESS_BIRTH_SEED_PAYLOAD_FIELD] = "{}";
120
+ probePayload[BUSINESS_BIRTH_SEED_PAYLOAD_FIELD] = { ...probePayload };
120
121
  }
121
122
 
122
123
  const shapeKeys = new Set(Object.keys(safeShape(dir.payloadSchema as z.ZodTypeAny)));
@@ -141,6 +142,23 @@ function probeDirective(dir: Directive<unknown>, where: string): ProbeResult | u
141
142
  returned = dir.plan(tracked as never, deterministicPorts({ physical: 1, replica: 1, principal: "principal:probe" }));
142
143
  const drained: PlannedOp[] = [...__drainAuthoring(), ...(Array.isArray(returned) ? returned : [])];
143
144
  ops = drained.filter((o): o is FieldOp => (o as { kind?: string }).kind !== "strike");
145
+ if (isBusinessIntent(dir)) {
146
+ for (const effect of dir.workspaceEffects) {
147
+ // These two mappers are source law just as surely as the local plan. Run them against
148
+ // the same tracking payload so route/payload fields are not falsely diagnosed as dead.
149
+ // A captured read may deliberately refuse this isolated empty-state probe; that does not
150
+ // turn a successful local plan into a plan-return warning, but reads reached before the
151
+ // refusal are still drained and checked below.
152
+ try {
153
+ if (effect.destination.kind === "workspace") {
154
+ effect.destination.address(tracked as never);
155
+ }
156
+ effect.payload(tracked as never);
157
+ } catch {
158
+ // Inconclusive mapper branch on the synthesized fixture; runtime/generator proof remains.
159
+ }
160
+ }
161
+ }
144
162
  reads = __drainReads();
145
163
  } catch (e) {
146
164
  threw = true;
package/src/law_graph.ts CHANGED
@@ -34,6 +34,7 @@ import {
34
34
  type LawLiteral,
35
35
  type LawValueMember,
36
36
  type LawValueConstraint,
37
+ type LawValueSemantic,
37
38
  type LawValueType,
38
39
  } from "./law_value_type.js";
39
40
  import { harvestPrincipalFlows } from "./wire_encode.js";
@@ -50,6 +51,7 @@ import {
50
51
  import { isNamespacedAggregate, qualifyReadProjectionType } from "./read_projection_namespace.js";
51
52
  import { assertFlowGateModule } from "./flow_gate.js";
52
53
  import type { InvariantReadBinding } from "./framework/workspace_invariant.js";
54
+ import { OwnedCustodyValue } from "./framework/owned_custody_value.js";
53
55
 
54
56
  declare const LAW_SID: unique symbol;
55
57
  export type LawSid<K extends LawEntityKind> = string & { readonly [LAW_SID]: K };
@@ -195,7 +197,7 @@ const OWNED_AUTHORING_PAYLOAD_MEMBERS = [
195
197
  {
196
198
  name: OWNED_CUSTODY_PAYLOAD_FIELD,
197
199
  required: false,
198
- value: { kind: "string", constraints: [] },
200
+ value: lowerLawValueType(OwnedCustodyValue.zod),
199
201
  },
200
202
  {
201
203
  name: OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD,
@@ -378,7 +380,7 @@ export interface LawRelationPredicateNode {
378
380
  readonly expression: LawRelationPredicateExpression;
379
381
  }
380
382
 
381
- export type LawAnonymousValueTypeExpression =
383
+ type LawAnonymousValueTypeShape =
382
384
  | {
383
385
  readonly kind: "string" | "number" | "integer";
384
386
  readonly constraints: readonly LawValueConstraint[];
@@ -398,6 +400,10 @@ export type LawAnonymousValueTypeExpression =
398
400
  }
399
401
  | { readonly kind: "recursive"; readonly value: LawStructuralRef<"value-type"> };
400
402
 
403
+ export type LawAnonymousValueTypeExpression = LawAnonymousValueTypeShape & {
404
+ readonly semantic?: LawValueSemantic;
405
+ };
406
+
401
407
  export interface LawAnonymousValueTypeNode {
402
408
  readonly kind: "structural";
403
409
  readonly structuralKind: "value-type";
@@ -587,6 +593,13 @@ export interface LawIntentNode {
587
593
  readonly foreignReads: readonly LawStructuralRef<"foreign-read">[];
588
594
  /** Attested cross-workspace contribution contracts which may propose this intent. */
589
595
  readonly workspaceRelations: readonly LawStructuralRef<"workspace-relation">[];
596
+ /** Exact signed parent-workspace effects may propose this call; authority is still `.requires`. */
597
+ readonly acceptsWorkspaceEffect: boolean;
598
+ /** Client proof selected by law for this exact public offer. Absence retains the ordinary
599
+ * session-credential posture. */
600
+ readonly offerAuthenticationProof?: "enrolledDevice";
601
+ /** Exact nominal target directives this source intent may author into another workspace. */
602
+ readonly directsWorkspaceEffects: readonly LawTargetRef<"intent">[];
590
603
  /** Zanzibar relation required to author this intent. */
591
604
  readonly authorization?: LawIntentAuthorization;
592
605
  readonly requiresHostCapabilities: readonly string[];
@@ -1172,6 +1185,16 @@ export function discoverLawGraph(
1172
1185
  ): LawTargetRef<"aggregate"> => {
1173
1186
  const local = aggregateByLabel.get(aggregateId);
1174
1187
  if (local !== undefined) return ref(local.node.entity);
1188
+ if (
1189
+ isBusinessIntent(intent) &&
1190
+ intent.steps.length === 0 &&
1191
+ intent.workspaceEffects.some((effect) => effect.target.aggregateId === aggregateId)
1192
+ ) {
1193
+ // A workspace-effect-only intent has no local aggregate consequence. Its nominal target
1194
+ // aggregate belongs to the destination package; retain that as a dependency export instead
1195
+ // of fabricating a local audit row merely to satisfy the older directive-shaped IR.
1196
+ return { kind: "dependency-export", entityKind: "aggregate", label: aggregateId };
1197
+ }
1175
1198
  const birth = (intent as { businessBirth?: { aggregateId?: unknown; domain?: unknown } }).businessBirth;
1176
1199
  if (birth?.aggregateId === aggregateId && typeof birth.domain === "string") {
1177
1200
  return { kind: "dependency-export", entityKind: "aggregate", label: aggregateId, domain: birth.domain };
@@ -1318,11 +1341,11 @@ export function discoverLawGraph(
1318
1341
  }
1319
1342
  payloadFieldsByIntent.set(intent.id, payloadFields);
1320
1343
  const outputs: Array<Pick<LawIntentEffectNode, "marker" | "aggregate" | "cardinality">> = [
1321
- {
1344
+ ...(isBusinessIntent(intent) && intent.steps.length === 0 ? [] : [{
1322
1345
  marker: intent.marker,
1323
1346
  aggregate: intentAggregateTarget(intent, intent.aggregateId),
1324
- cardinality: intent.outputCardinality ?? "one",
1325
- },
1347
+ cardinality: intent.outputCardinality ?? "one" as const,
1348
+ }]),
1326
1349
  ...(intent.secondaryOutputs ?? []).map((output) => ({
1327
1350
  marker: output.marker,
1328
1351
  aggregate: aggregateRef(aggregateByLabel, output.aggregateId, `intent '${intent.id}'`),
@@ -1460,6 +1483,20 @@ export function discoverLawGraph(
1460
1483
  reads: [],
1461
1484
  foreignReads: [],
1462
1485
  workspaceRelations,
1486
+ acceptsWorkspaceEffect: intent.acceptsWorkspaceEffectGate === true,
1487
+ ...(isBusinessIntent(intent) && intent.offerAuthenticationProof !== undefined
1488
+ ? { offerAuthenticationProof: intent.offerAuthenticationProof }
1489
+ : {}),
1490
+ directsWorkspaceEffects: (isBusinessIntent(intent) ? intent.workspaceEffects : []).map((effect) => {
1491
+ const local = intentEntities.get(effect.target.id);
1492
+ return local === undefined
1493
+ ? {
1494
+ kind: "dependency-export" as const,
1495
+ entityKind: "intent" as const,
1496
+ label: effect.target.id,
1497
+ }
1498
+ : ref(local);
1499
+ }),
1463
1500
  requiresHostCapabilities: [...new Set(intent.declaredHostCapabilities ?? [])].sort(compareText),
1464
1501
  ...(resolvedField === undefined ? {} : { resolves: ref(resolvedField.entity) }),
1465
1502
  births: intent.birthsChild === true,
@@ -1667,6 +1704,14 @@ export function discoverLawGraph(
1667
1704
  for (const [index, intentNode] of intentNodes.entries()) {
1668
1705
  const declaration = intents.find((intent) => intent.id === intentNode.entity.label)!;
1669
1706
  const gate = declaration.requiresRelationGate;
1707
+ if (declaration.acceptsWorkspaceEffectGate === true) {
1708
+ if (gate === undefined || gate.object !== "workspace:self" || gate.objectFrom !== undefined) {
1709
+ fail(
1710
+ `intent '${declaration.id}' .acceptsWorkspaceEffect() requires a static ` +
1711
+ `.requires(relation, { object: "workspace:self" }) authorization gate`,
1712
+ );
1713
+ }
1714
+ }
1670
1715
  if (gate === undefined) continue;
1671
1716
  const objectType = gate.objectFrom?.recordType ?? gate.object.split(":", 1)[0]!;
1672
1717
  const exactLabel = gate.relation.includes("#") ? gate.relation : `${objectType}#${gate.relation}`;
@@ -2783,6 +2828,7 @@ export function lowerAnonymousValueGraph<K extends LawEntityKind>(
2783
2828
  case "recursive-ref":
2784
2829
  throw new Error("unreachable recursive-ref lowering");
2785
2830
  }
2831
+ if (value.semantic !== undefined) expression = { ...expression, semantic: value.semantic };
2786
2832
  nodes.set(entity.id, {
2787
2833
  kind: "structural",
2788
2834
  structuralKind: "value-type",
@@ -2831,25 +2877,28 @@ export function lowerAnonymousContractFields(
2831
2877
  }
2832
2878
 
2833
2879
  function canonicalValueType(value: LawValueType): LawValueType {
2880
+ const semantic = value.semantic === undefined ? {} : { semantic: value.semantic };
2834
2881
  switch (value.kind) {
2835
2882
  case "record":
2836
2883
  return {
2884
+ ...semantic,
2837
2885
  kind: "record",
2838
2886
  fields: value.fields.map((field) => ({ ...field, value: canonicalValueType(field.value) }))
2839
2887
  .sort((a, b) => compareText(a.name, b.name)),
2840
2888
  };
2841
2889
  case "sum":
2842
2890
  return {
2891
+ ...semantic,
2843
2892
  kind: "sum",
2844
2893
  options: value.options.map(canonicalValueType)
2845
2894
  .sort((a, b) => compareText(JSON.stringify(a), JSON.stringify(b))),
2846
2895
  };
2847
2896
  case "list":
2848
- return { kind: "list", element: canonicalValueType(value.element) };
2897
+ return { ...semantic, kind: "list", element: canonicalValueType(value.element) };
2849
2898
  case "map":
2850
- return { kind: "map", value: canonicalValueType(value.value) };
2899
+ return { ...semantic, kind: "map", value: canonicalValueType(value.value) };
2851
2900
  case "nullable":
2852
- return { kind: "nullable", value: canonicalValueType(value.value) };
2901
+ return { ...semantic, kind: "nullable", value: canonicalValueType(value.value) };
2853
2902
  case "default":
2854
2903
  return { ...value, value: canonicalValueType(value.value) };
2855
2904
  case "recursive":
@@ -159,12 +159,20 @@ function entityLabelOpinions(
159
159
  ];
160
160
  }
161
161
 
162
- /** First W4.4 vertical slice: render domain, aggregate, stored-field and reusable-contract opinions. */
163
- export function emitSparseLawGraphUsda(
162
+ /** Exact path index produced while rendering one sparse law layer. Generated domain clients consume
163
+ * these paths in-memory during the same compile so their call USDA cannot drift from the law text.
164
+ * The index is compiler data, not a sidecar artifact. */
165
+ export interface CompiledSparseLawGraphUsda {
166
+ readonly text: string;
167
+ readonly paths: ReadonlyMap<string, string>;
168
+ }
169
+
170
+ /** Render one sparse law layer and retain its exact identity-path projection for client codegen. */
171
+ export function compileSparseLawGraphUsda(
164
172
  graph: LawGraph,
165
173
  logicalPath: string,
166
174
  closure: readonly LawGraph[] = [graph],
167
- ): string {
175
+ ): CompiledSparseLawGraphUsda {
168
176
  const domain = graph.domain;
169
177
  const aggregates = graph.nodes.filter((node): node is LawAggregateNode => node.kind === "aggregate");
170
178
  const contracts = graph.nodes.filter((node): node is LawContractNode => node.kind === "contract");
@@ -675,7 +683,16 @@ export function emitSparseLawGraphUsda(
675
683
  (flow) => `w_${flow.entity.sid}`);
676
684
  }
677
685
  lines.push(" }", " }", "}", "");
678
- return lines.join("\n");
686
+ return { text: lines.join("\n"), paths: new Map(pathById) };
687
+ }
688
+
689
+ /** First W4.4 vertical slice: render domain, aggregate, stored-field and reusable-contract opinions. */
690
+ export function emitSparseLawGraphUsda(
691
+ graph: LawGraph,
692
+ logicalPath: string,
693
+ closure: readonly LawGraph[] = [graph],
694
+ ): string {
695
+ return compileSparseLawGraphUsda(graph, logicalPath, closure).text;
679
696
  }
680
697
 
681
698
  function emitRetirement(retirement: LawRetirement, indent: number): string[] {
@@ -966,6 +983,12 @@ function emitIntent(
966
983
  `${pad}custom uniform bool nomos:active = true`,
967
984
  `${pad}custom uniform bool nomos:public = ${intent.public}`,
968
985
  `${pad}custom uniform string nomos:publicRoute = ${quote(intent.publicRoute ?? intent.entity.label)}`,
986
+ ...(intent.acceptsWorkspaceEffect
987
+ ? [`${pad}custom uniform bool nomos:acceptsWorkspaceEffect = true`]
988
+ : []),
989
+ ...(intent.offerAuthenticationProof === "enrolledDevice"
990
+ ? [`${pad}custom uniform token nomos:offerAuthenticationProof = "EnrolledDevice"`]
991
+ : []),
969
992
  `${pad}custom rel nomos:target = ${targets([intent.target], paths, resolveDependency)}`,
970
993
  `${pad}custom rel nomos:accepts = <${requiredPath(intent.accepts.id, paths)}>`,
971
994
  ...(intent.publicAccepts === undefined
@@ -975,6 +998,11 @@ function emitIntent(
975
998
  if (intent.reads.length > 0) {
976
999
  body.push(`${pad}custom rel nomos:reads = ${targets(intent.reads, paths, resolveDependency)}`);
977
1000
  }
1001
+ if ((intent.directsWorkspaceEffects ?? []).length > 0) {
1002
+ body.push(
1003
+ `${pad}custom rel nomos:directsWorkspaceEffect = ${targets(intent.directsWorkspaceEffects, paths, resolveDependency)}`,
1004
+ );
1005
+ }
978
1006
  if (intent.authorization !== undefined) {
979
1007
  body.push(
980
1008
  `${pad}custom rel nomos:requires = ${targets([intent.authorization.relation], paths, resolveDependency)}`,
@@ -1471,7 +1499,9 @@ function emitAnonymousType(
1471
1499
  ): string[] {
1472
1500
  const pad = " ".repeat(indent);
1473
1501
  const expression = type.expression;
1474
- const body: string[] = [];
1502
+ const body: string[] = expression.semantic === undefined
1503
+ ? []
1504
+ : emitAnonymousValueSemantic(expression.semantic, indent);
1475
1505
  switch (expression.kind) {
1476
1506
  case "string":
1477
1507
  case "number":
@@ -1605,6 +1635,44 @@ function emitAnonymousType(
1605
1635
  return body;
1606
1636
  }
1607
1637
 
1638
+ function emitAnonymousValueSemantic(
1639
+ semantic: NonNullable<LawAnonymousValueTypeNode["expression"]["semantic"]>,
1640
+ indent: number,
1641
+ ): string[] {
1642
+ const pad = " ".repeat(indent);
1643
+ const body = [`${pad}custom uniform token nomos:semantic = ${quote(semantic.kind)}`];
1644
+ switch (semantic.kind) {
1645
+ case "sha256-digest":
1646
+ case "git-object-id":
1647
+ break;
1648
+ case "branded-id":
1649
+ case "unit":
1650
+ case "value-object":
1651
+ body.push(`${pad}custom uniform string nomos:semanticName = ${quote(semantic.name)}`);
1652
+ break;
1653
+ case "recipient":
1654
+ body.push(`${pad}custom uniform token nomos:subject = ${quote(semantic.subject)}`);
1655
+ break;
1656
+ case "id-reference":
1657
+ case "address-reference":
1658
+ body.push(
1659
+ `${pad}custom uniform token nomos:targetKind = ${quote(semantic.targetKind)}`,
1660
+ `${pad}custom uniform string nomos:target = ${quote(semantic.target)}`,
1661
+ );
1662
+ break;
1663
+ case "versioned-reference":
1664
+ body.push(
1665
+ `${pad}custom uniform token nomos:targetKind = ${quote(semantic.targetKind)}`,
1666
+ `${pad}custom uniform string nomos:target = ${quote(semantic.target)}`,
1667
+ `${pad}custom uniform token nomos:seriesKind = ${quote(semantic.seriesKind)}`,
1668
+ `${pad}custom uniform string nomos:series = ${quote(semantic.series)}`,
1669
+ `${pad}custom uniform string nomos:currentField = ${quote(semantic.currentField)}`,
1670
+ );
1671
+ break;
1672
+ }
1673
+ return body;
1674
+ }
1675
+
1608
1676
  function emitAnonymousField(
1609
1677
  field: LawAnonymousValueFieldNode,
1610
1678
  types: ReadonlyMap<string, LawAnonymousValueTypeNode>,