@githolon/dsl 0.101.5 → 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/usd_state.ts CHANGED
@@ -474,7 +474,7 @@ export function stateScrubStageUsda(opts: {
474
474
  if (opts.lawLayerRelPathsWeakToStrong.length === 0) {
475
475
  fail(
476
476
  `scrub stage for '${opts.domain}' has no law layers — emit them first ` +
477
- `(nomos-compile --layered, spike 1) so state has law to compose over`,
477
+ `(nomos-compile always emits it) so state has law to compose over`,
478
478
  );
479
479
  }
480
480
  const strongestFirst = [opts.stateLayerRelPath, ...[...opts.lawLayerRelPathsWeakToStrong].reverse()];
@@ -52,8 +52,10 @@
52
52
  * and `.optional()` on any of them (arrays/records of optionals excluded). Tuples, dates,
53
53
  * non-string-keyed records etc. are deliberately OUT — grow this surface deliberately.
54
54
  */
55
+
55
56
  import { z } from "zod";
56
57
 
58
+ import { isRuntimeLaw } from "./authoring_context.js";
57
59
  import type { Driver } from "./drivers.js";
58
60
  import {
59
61
  valueObjectField,
@@ -251,28 +253,36 @@ export function unit<Z extends z.ZodTypeAny>(name: string, inner: Z): Z {
251
253
  }
252
254
 
253
255
  /**
254
- * THE TYPED-ID BRAND (dart_codegen_parity.md P2) — `idOf(Site)` in a directive payload is
255
- * wire-identical to `z.string()` but the generated Dart field is the aggregate's TYPED id
256
- * class (`SiteId`), and the generated TS references the branded alias. Passing another
257
- * aggregate's id where a SiteId belongs becomes a COMPILE error. Payload-side only — the
256
+ * THE AGGREGATE-REFERENCE BRAND (dart_codegen_parity.md P2) — `idOf(Site)` in a directive payload is
257
+ * wire-identical to `z.string()` but both generated languages expose it as `NomosRef<Site>`.
258
+ * Passing a bare string or `NomosRef<AnotherAggregate>` where the Site reference belongs becomes a
259
+ * COMPILE error. Payload-side only — the
258
260
  * kernel keeps seeing a string; validation and the aggregate field contract are untouched
259
261
  * because this is a directive-payload annotation.
260
262
  */
261
263
  export const NOMOS_IDOF_BRAND: unique symbol = Symbol.for("nomos.idOf");
262
264
 
263
- /** A payload field holding an EXISTING aggregate's id, typed: `siteId: idOf(Site)`. */
264
- export function idOf(aggregate: { id: string } | string): z.ZodType<string> {
265
+ /** A payload field holding an EXISTING aggregate reference: `siteId: idOf(Site)` -> `NomosRef<Site>`. */
266
+ export function idOf(aggregate: { id: string; __isContract?: true } | string): z.ZodType<string> {
265
267
  const aggregateType = typeof aggregate === "string" ? aggregate : aggregate.id;
266
268
  if (typeof aggregateType !== "string" || aggregateType === "") {
267
269
  throw new Error("idOf: pass the aggregate handle (idOf(Site)) or its type name.");
268
270
  }
269
271
  const schema = z.string().min(1);
270
- Object.defineProperty(schema, NOMOS_IDOF_BRAND, { value: aggregateType, enumerable: false });
272
+ Object.defineProperty(schema, NOMOS_IDOF_BRAND, {
273
+ value: {
274
+ aggregateType,
275
+ targetKind: typeof aggregate === "object" && aggregate.__isContract === true
276
+ ? "contract"
277
+ : "aggregate",
278
+ },
279
+ enumerable: false,
280
+ });
271
281
  return schema;
272
282
  }
273
283
 
274
284
  /**
275
- * THE BRANDED-ID BRAND (dart_codegen_parity.md P2 — the standalone twin of {@link idOf}) —
285
+ * THE BRANDED-ID BRAND (dart_codegen_parity.md P2 — deliberately NOT an aggregate reference) —
276
286
  * `brandedId("SiteId")` is a NOMINAL string: wire-identical to `z.string()` but the generated TS is a branded alias
277
287
  * (`type SiteId = string & { readonly __brand }`) and the generated Dart a zero-cost
278
288
  * extension type (`extension type const SiteId(String value) {}`), so passing a bare string
@@ -313,6 +323,27 @@ export function idOfMetaOf(zt: z.ZodTypeAny): string | undefined {
313
323
  for (let hops = 0; cur !== undefined && hops < 16; hops++) {
314
324
  const branded = (cur as unknown as Record<symbol, unknown>)[NOMOS_IDOF_BRAND];
315
325
  if (typeof branded === "string") return branded;
326
+ if (typeof branded === "object" && branded !== null) {
327
+ const aggregateType = (branded as { aggregateType?: unknown }).aggregateType;
328
+ if (typeof aggregateType === "string") return aggregateType;
329
+ }
330
+ const kind = zodKind(cur);
331
+ if (kind !== "optional" && kind !== "default" && kind !== "nullable") return undefined;
332
+ cur = zodDef(cur).innerType;
333
+ }
334
+ return undefined;
335
+ }
336
+
337
+ /** Whether an `idOf(...)` target is a concrete aggregate or a shared aggregate contract. */
338
+ export function idOfTargetKindOf(zt: z.ZodTypeAny): "aggregate" | "contract" | undefined {
339
+ let cur: z.ZodTypeAny | undefined = zt;
340
+ for (let hops = 0; cur !== undefined && hops < 16; hops++) {
341
+ const branded = (cur as unknown as Record<symbol, unknown>)[NOMOS_IDOF_BRAND];
342
+ if (typeof branded === "object" && branded !== null) {
343
+ const targetKind = (branded as { targetKind?: unknown }).targetKind;
344
+ if (targetKind === "aggregate" || targetKind === "contract") return targetKind;
345
+ }
346
+ if (typeof branded === "string") return "aggregate";
316
347
  const kind = zodKind(cur);
317
348
  if (kind !== "optional" && kind !== "default" && kind !== "nullable") return undefined;
318
349
  cur = zodDef(cur).innerType;
@@ -366,10 +397,9 @@ function describeZod(
366
397
  case "string": {
367
398
  const brandedBrand = (schema as unknown as Record<symbol, unknown>)[NOMOS_BRANDED_ID_BRAND];
368
399
  const idOfAgg = (schema as unknown as Record<symbol, unknown>)[NOMOS_IDOF_BRAND];
369
- // idOf(Agg) REFERENCES the aggregate's canonical typed-id class `<Agg>Id` (emitted ONCE by the
370
- // aggregate) — it must NOT re-declare a standalone extension type (that duplicated the id symbol and
371
- // stripped its `.fromRow`, co2). Carry `idOf` so the emitter references + imports it and skips the
372
- // brand-extension-type emission. A standalone `brandedId("X")` still declares its own extension type.
400
+ // idOf(Agg) REFERENCES the aggregate's canonical `NomosRef<Agg>`. Carry `idOf` in the stable
401
+ // descriptor so every emitter can recover the aggregate type while the wire remains a string.
402
+ // A standalone `brandedId("X")` remains an unrelated nominal string and declares its own wrapper.
373
403
  if (typeof idOfAgg === "string") {
374
404
  return { type: "string", brand: `${idOfAgg}Id`, idOf: idOfAgg };
375
405
  }
@@ -545,13 +575,19 @@ function makeValueObject<T>(
545
575
  geometry?: GeometryKind,
546
576
  example?: T,
547
577
  ): ValueObject<T> {
578
+ if (name === "User" || name === "Identity") {
579
+ throw new Error(
580
+ `valueObject '${name}': '${name}' is a first-class Nomos authority type, not application state. ` +
581
+ `Sorry mate, that concept is already taken by the framework; choose a business-specific name.`,
582
+ );
583
+ }
548
584
  if (!VO_NAME_RE.test(name)) {
549
585
  throw new Error(
550
586
  `valueObject: name '${name}' is not a valid identifier ([A-Za-z][A-Za-z0-9_]*) — it becomes the ` +
551
587
  `generated TS interface / Dart class name.`,
552
588
  );
553
589
  }
554
- if (example !== undefined) {
590
+ if (example !== undefined && !isRuntimeLaw()) {
555
591
  const v = zodSchema.safeParse(example);
556
592
  if (!v.success) {
557
593
  throw new Error(
@@ -19,8 +19,10 @@
19
19
  * one event; a multi-aggregate one yields several. Proto/numeric-tag wire is a
20
20
  * LATER coordinated migration — we target the current serde_json shape now.
21
21
  */
22
+ import type { z } from "zod";
23
+
22
24
  import { encodeDriver } from "./drivers.js";
23
- import { __qualifyAggregateType } from "./framework/relations.js";
25
+ import { __qualifyAggregateType, __setDispatchDomain } from "./framework/relations.js";
24
26
  import { __resetAuthoring, __drainAuthoring } from "./authoring.js";
25
27
  import { __resetReads, __drainReads } from "./read.js";
26
28
  import { __resetBirths, __drainBirths } from "./births.js";
@@ -51,6 +53,15 @@ import type {
51
53
  WireSchema,
52
54
  WireValue,
53
55
  } from "./wire.js";
56
+ import {
57
+ zodArrayElement,
58
+ zodDef,
59
+ zodEnumValues,
60
+ zodKind,
61
+ zodObjectShape,
62
+ zodSampleNumber,
63
+ zodSampleString,
64
+ } from "./zod_introspection.js";
54
65
 
55
66
  /** Map a directive's referential marker onto the kernel `RefMode` wire string. */
56
67
  export function encodeRefMode(marker: ReferentialMarker): WireRefMode {
@@ -103,6 +114,14 @@ function describeShape(value: unknown): string {
103
114
  * `op` is passed purely for a NAMED diagnostic (aggregate + field + expected VO type) when a value
104
115
  * can't be lowered — never `field '?'`. */
105
116
  function encodeKernelValue(field: Field | undefined, value: unknown, op?: FieldOp): WireValue {
117
+ if (value === undefined) {
118
+ const aggName = op?.aggregateType ?? op?.aggregateId ?? "?";
119
+ const fieldName = op?.field ?? field?.kind ?? "?";
120
+ throw new Error(
121
+ `${aggName}.${fieldName} attempted to write undefined. Omit the operation (for example with ` +
122
+ `setOptional(...)) or declare an explicit null/clear policy; undefined can never be ledger state.`,
123
+ );
124
+ }
106
125
  // An `evidence` field encodes as the FIRST-CLASS kernel `Value::Evidence` (NOT a JSON
107
126
  // string): the authored {hash, mediaType, byteLength, storageClass} maps to the
108
127
  // kernel's snake_case serde shape + the `Git`/`External` unit-variant. Checked BEFORE
@@ -380,12 +399,14 @@ export function executeDirectiveToIntent<P>(
380
399
  }
381
400
  }
382
401
 
383
- // Group ops by aggregate id, preserving first-seen order (authored order).
384
- // `aggregateId` is the address: for an unbound op it is the TYPE (legacy
385
- // one-per-workspace shape); for an instance-bound op (`instance(h, id)`) it is
386
- // the concrete INSTANCE id, with `aggregateType` carrying the TYPE (#105).
402
+ // Group ops by the complete aggregate address `(type, id)`, preserving first-seen order.
403
+ // `id` alone is never an address: two aggregate types may intentionally share a business key
404
+ // (Draft(id) + DraftPredecessorSelection(id)). Collapsing those buckets turns a lawful atomic
405
+ // Create+Ensure fan-out into one contradictory event and is exactly the sort of stringly identity
406
+ // bug `NomosRef<T>` exists to make impossible.
387
407
  const order: string[] = [];
388
408
  const byAggregate = new Map<string, WireFieldOp[]>();
409
+ const idOf = new Map<string, string>();
389
410
  // Per-bucket aggregate TYPE — only present for instance-bound ops. When set, we
390
411
  // auto-stamp `__type`/`__id` provenance field-ops so `view()` (api.rs
391
412
  // `project_view`) can recover the aggregate's TYPE off a per-instance id.
@@ -403,31 +424,33 @@ export function executeDirectiveToIntent<P>(
403
424
  const explicitMarker = new Map<string, WireRefMode>();
404
425
  const fieldsOf = agg.fields as Record<string, Field>;
405
426
  for (const op of ops) {
406
- let bucket = byAggregate.get(op.aggregateId);
427
+ const bucketKey = JSON.stringify([op.aggregateType ?? null, op.aggregateId]);
428
+ let bucket = byAggregate.get(bucketKey);
407
429
  if (bucket === undefined) {
408
430
  bucket = [];
409
- byAggregate.set(op.aggregateId, bucket);
410
- order.push(op.aggregateId);
431
+ byAggregate.set(bucketKey, bucket);
432
+ idOf.set(bucketKey, op.aggregateId);
433
+ order.push(bucketKey);
411
434
  }
412
- if (op.aggregateType !== undefined) typeOf.set(op.aggregateId, op.aggregateType);
435
+ if (op.aggregateType !== undefined) typeOf.set(bucketKey, op.aggregateType);
413
436
  if (op.marker !== undefined) {
414
437
  const encoded = encodeRefMode(op.marker);
415
- const existing = explicitMarker.get(op.aggregateId);
438
+ const existing = explicitMarker.get(bucketKey);
416
439
  if (existing !== undefined && existing !== encoded) {
417
440
  throw new Error(
418
441
  `Directive '${directive.id}' emits conflicting markers for aggregate ` +
419
- `'${op.aggregateId}': ${existing} vs ${encoded}.`,
442
+ `'${op.aggregateType ?? "?"}:${op.aggregateId}': ${existing} vs ${encoded}.`,
420
443
  );
421
444
  }
422
- explicitMarker.set(op.aggregateId, encoded);
445
+ explicitMarker.set(bucketKey, encoded);
423
446
  }
424
447
  // Field kinds are known for ops targeting the directive's own aggregate — by
425
448
  // TYPE for a bound op (`aggregateType`), else by id for an unbound op.
426
449
  const isOwnTarget =
427
450
  op.aggregateType !== undefined ? op.aggregateType === agg.id : op.aggregateId === agg.id;
428
451
  // A bucket is the own target if ANY of its ops target the directive's own aggregate.
429
- if (isOwnTarget) ownTarget.set(op.aggregateId, true);
430
- else if (!ownTarget.has(op.aggregateId)) ownTarget.set(op.aggregateId, false);
452
+ if (isOwnTarget) ownTarget.set(bucketKey, true);
453
+ else if (!ownTarget.has(bucketKey)) ownTarget.set(bucketKey, false);
431
454
  // Resolve the field's schema from the OP'S OWN TARGET aggregate, not the dispatch primary: a fanned-out
432
455
  // sibling op carries its target's Field (`op.targetField`, captured at op-construction where the handle
433
456
  // knew its schema). For an own-target op `op.targetField` IS `fieldsOf[op.field]` (same Field object), so
@@ -437,13 +460,14 @@ export function executeDirectiveToIntent<P>(
437
460
  bucket.push(encodeKernelFieldOp(op, field));
438
461
  }
439
462
 
440
- const events: WireEvent[] = order.map((aggregate) => {
441
- const ops = byAggregate.get(aggregate)!;
442
- const ty = typeOf.get(aggregate);
463
+ const events: WireEvent[] = order.map((bucketKey) => {
464
+ const aggregate = idOf.get(bucketKey)!;
465
+ const ops = byAggregate.get(bucketKey)!;
466
+ const ty = typeOf.get(bucketKey);
443
467
  // The directive's marker applies to its OWN target; a FAN-OUT bucket is a Mutate of an
444
468
  // existing sibling aggregate (so the id-mint gate never sees a spurious Create for it).
445
469
  const eventMarker: WireRefMode =
446
- explicitMarker.get(aggregate) ?? (ownTarget.get(aggregate) ? marker : "Mutate");
470
+ explicitMarker.get(bucketKey) ?? (ownTarget.get(bucketKey) ? marker : "Mutate");
447
471
  if (ty !== undefined) {
448
472
  // Stamp provenance FIRST so the fold carries the TYPE + canonical id the
449
473
  // structured `view()` projects (the same `__type`/`__id` reserved fields the
@@ -487,6 +511,53 @@ export interface PrincipalFlow {
487
511
  readonly reason: string | null;
488
512
  }
489
513
 
514
+ /**
515
+ * Build a schema-valid-enough value for security discovery without importing a
516
+ * code generator into the wire layer. Business intents validate their payload
517
+ * again while lowering private effects, so the old property-only Proxy could
518
+ * never reach their plans: Zod enumerates object keys before reading them.
519
+ * Unsupported/exotic schemas still fail closed at runtime exactly as before.
520
+ */
521
+ function principalFlowProbeValue(
522
+ schema: z.ZodTypeAny,
523
+ visiting: ReadonlySet<z.ZodTypeAny> = new Set(),
524
+ hint = "value",
525
+ ): unknown {
526
+ if (visiting.has(schema)) return {};
527
+ const next = new Set(visiting).add(schema);
528
+ const def = zodDef(schema);
529
+ switch (zodKind(schema)) {
530
+ case "string": return zodSampleString(schema, hint);
531
+ case "number": return zodSampleNumber(schema);
532
+ case "boolean": return false;
533
+ case "enum": return zodEnumValues(schema)[0];
534
+ case "literal": return def.value ?? def.values?.[0];
535
+ case "array": return [principalFlowProbeValue(zodArrayElement(schema), next, "item")];
536
+ case "object": {
537
+ const value: Record<string, unknown> = {};
538
+ for (const [name, field] of Object.entries(zodObjectShape(schema))) {
539
+ const kind = zodKind(field);
540
+ if (kind === "optional" || kind === "default") continue;
541
+ value[name] = principalFlowProbeValue(field, next, name);
542
+ }
543
+ return value;
544
+ }
545
+ case "record": return {};
546
+ case "union": {
547
+ const first = def.options?.[0];
548
+ return first === undefined ? undefined : principalFlowProbeValue(first, next, hint);
549
+ }
550
+ case "nullable": return null;
551
+ case "optional": return undefined;
552
+ case "default": return def.innerType === undefined ? undefined : principalFlowProbeValue(def.innerType, next, hint);
553
+ case "lazy": return def.getter === undefined ? {} : principalFlowProbeValue(def.getter(), next, hint);
554
+ case "pipe": return def.in === undefined ? {} : principalFlowProbeValue(def.in, next, hint);
555
+ case "unknown":
556
+ case "any": return {};
557
+ default: return {};
558
+ }
559
+ }
560
+
490
561
  /** Extract the principal flows carried by a plan's field ops (those with an `endorsement`). The ONE
491
562
  * op→flow mapping shared by the discovery harvest (compile) and the runtime footprint (Slice B). */
492
563
  function flowsFromOps(ops: readonly FieldOp[]): PrincipalFlow[] {
@@ -509,23 +580,49 @@ function flowsFromOps(ops: readonly FieldOp[]): PrincipalFlow[] {
509
580
  * the omission is SAFE because the runtime/replay witness (Slice B) refuses any observed flow absent
510
581
  * from the manifest. The reasons are static law, so they are intent-independent. SORTED for determinism.
511
582
  */
512
- export function harvestPrincipalFlows(directive: Directive<unknown>): PrincipalFlow[] {
583
+ export function harvestPrincipalFlows(directive: Directive<unknown>, domain?: string): PrincipalFlow[] {
513
584
  const ctx: Ports = deterministicPorts({ principal: "principal:probe" });
514
- // A probe payload: any field access yields a benign string. Plans that need a typed shape (an
515
- // array .map, etc.) may throw — caught below; that directive's flows are then witnessed at runtime.
516
- const probe = new Proxy({}, { get: () => "probe" });
585
+ let candidate: unknown;
586
+ try {
587
+ candidate = principalFlowProbeValue(directive.payloadSchema as z.ZodTypeAny);
588
+ } catch (error) {
589
+ const detail = error instanceof Error ? error.message : String(error);
590
+ throw new Error(`principal-flow discovery for '${directive.id}' could not construct a lawful probe: ${detail}`, { cause: error });
591
+ }
592
+ const parsed = (directive.payloadSchema as z.ZodTypeAny).safeParse(candidate);
593
+ // Keep the historical property proxy as a fallback for unusual schemas whose plan does not validate
594
+ // its payload. A BusinessIntent does validate and therefore needs the enumerable parsed object above.
595
+ const probe = parsed.success ? parsed.data : new Proxy({}, { get: () => "probe-value" });
517
596
  // A plan that MINTS (`create(Agg)` — the common `.creates` shape) calls `globalThis.nomos.mint`; the
518
597
  // harvest runs at compile, where the engine's real shim is absent. Install a deterministic stub so the
519
598
  // probe plan runs to completion and its flows ARE harvested (an un-harvested authority flow would be
520
599
  // refused at runtime by the Slice B witness — fail-closed, but a false jam). The minted VALUE is
521
600
  // irrelevant — a flow's sink is the field path, not the id. Restore any pre-existing shim after.
522
- const g = globalThis as unknown as { nomos?: { mint?: (t: string) => string; read?: unknown } };
601
+ const g = globalThis as unknown as {
602
+ nomos?: {
603
+ mint?: (t: string) => string;
604
+ sha256?: (text: string) => string;
605
+ read?: unknown;
606
+ };
607
+ };
523
608
  const hadNomos = "nomos" in g;
524
- const prior: { mint?: (t: string) => string; read?: unknown } | undefined = g.nomos;
525
- if (!hadNomos || typeof prior?.mint !== "function") {
526
- g.nomos = { ...(prior ?? {}), mint: (typeTag: string) => `${typeTag}_0192f00d-0000-7000-8000-000000000000` };
609
+ const prior = g.nomos;
610
+ if (
611
+ !hadNomos ||
612
+ typeof prior?.mint !== "function" ||
613
+ typeof prior?.sha256 !== "function"
614
+ ) {
615
+ g.nomos = {
616
+ ...(prior ?? {}),
617
+ mint: prior?.mint ?? ((typeTag: string) => `${typeTag}_0192f00d-0000-7000-8000-000000000000`),
618
+ // Discovery needs only to drive the pure plan far enough to observe its
619
+ // declared flows. The address is irrelevant here; the runtime provides
620
+ // the real deterministic hash and separately verifies the plan bytes.
621
+ sha256: prior?.sha256 ?? (() => "0".repeat(64)),
622
+ };
527
623
  }
528
624
  let ops: FieldOp[];
625
+ if (domain !== undefined) __setDispatchDomain(domain);
529
626
  __resetAuthoring(ctx, String(ctx.clock().physical));
530
627
  __resetReads();
531
628
  try {
@@ -535,6 +632,7 @@ export function harvestPrincipalFlows(directive: Directive<unknown>): PrincipalF
535
632
  try { __drainAuthoring(); __drainReads(); } catch { /* sink already drained */ }
536
633
  return [];
537
634
  } finally {
635
+ if (domain !== undefined) __setDispatchDomain(undefined);
538
636
  if (hadNomos && prior !== undefined) g.nomos = prior; else delete g.nomos;
539
637
  }
540
638
  __drainReads();
@@ -0,0 +1,269 @@
1
+ // Durable, framework-owned delivery of a business application's access record.
2
+ //
3
+ // The domain records what should be shared and the business receipts that close
4
+ // that order. Generated stateful clients perform and retry the cross-workspace
5
+ // delivery. Tenant application code never sees a bridge, signer, home index, or
6
+ // delivery protocol.
7
+
8
+ import { aggregate, instance, type AggregateHandle } from "./aggregate.js";
9
+ import { directive, nomosImplementationDetail, type Directive } from "./directive.js";
10
+ import type { BusinessIntent } from "./intent.js";
11
+ import { query, type QueryDecl } from "./query.js";
12
+ import { z } from "zod";
13
+ import { set, withMarker } from "./ops.js";
14
+ import { t } from "./fields.js";
15
+ import { Lww } from "./drivers.js";
16
+ import { endorsementOf } from "./principal_evidence.js";
17
+ import { RelationTuple, writeTuple } from "./framework/relations.js";
18
+ import {
19
+ refAs,
20
+ workspaceInvariant,
21
+ type WorkspaceInvariantDecl,
22
+ } from "./framework/workspace_invariant.js";
23
+ import { recipientMetaOf } from "./recipient.js";
24
+ import { zodKind, zodObjectShape } from "./zod_introspection.js";
25
+
26
+ export type WorkspaceSharingPresence = "present" | "absent";
27
+
28
+ export interface WorkspaceSharingWhen {
29
+ readonly field: string;
30
+ readonly is: WorkspaceSharingPresence;
31
+ }
32
+
33
+ export interface WorkspaceSharingReceipt {
34
+ readonly when?: WorkspaceSharingWhen;
35
+ /** Receipt payload field -> durable order field, when the business names differ. */
36
+ readonly payload?: Readonly<Record<string, string>>;
37
+ readonly records: Directive<any>;
38
+ }
39
+
40
+ export interface WorkspaceSharingPolicy {
41
+ readonly orders: AggregateHandle;
42
+ readonly initiatedBy: readonly BusinessIntent<any>[];
43
+ readonly pending: QueryDecl;
44
+ readonly pendingValue: string;
45
+ readonly person: string;
46
+ readonly role: string;
47
+ readonly scope: string;
48
+ readonly delivered: readonly WorkspaceSharingReceipt[];
49
+ readonly refused: readonly WorkspaceSharingReceipt[];
50
+ /** Generated law behind `Invitation<User>`; never exported to tenant code. */
51
+ readonly invitationLaw: WorkspaceSharingInvitationLaw;
52
+ }
53
+
54
+ export interface WorkspaceSharingInvitationLaw {
55
+ readonly claim: AggregateHandle;
56
+ readonly redeem: Directive<any>;
57
+ readonly gate: WorkspaceInvariantDecl;
58
+ readonly pendingByRecipient: QueryDecl;
59
+ }
60
+
61
+ /** One workspace-wide consumption record makes a bearer invitation single-use across every policy. */
62
+ const NomosInvitationClaim = {
63
+ ...aggregate("NomosInternalInvitationClaim7c1f5e", {
64
+ publicKey: t.string().merge(Lww),
65
+ redeemedBy: t.string().merge(Lww),
66
+ redeemedAt: t.string().merge(Lww),
67
+ }, { global: true }).public(),
68
+ businessBirthInternal: true as const,
69
+ } as AggregateHandle;
70
+
71
+ export interface WorkspaceSharingOptions {
72
+ readonly orders: AggregateHandle;
73
+ /** Business actions after which the generated application owns delivery. */
74
+ readonly initiatedBy: readonly BusinessIntent<any>[];
75
+ /** An indexed, single-key query whose value selects pending delivery orders. */
76
+ readonly pending: QueryDecl;
77
+ readonly pendingValue?: string;
78
+ readonly person: string;
79
+ readonly role: string;
80
+ readonly scope: string;
81
+ readonly delivered: readonly WorkspaceSharingReceipt[];
82
+ readonly refused: readonly WorkspaceSharingReceipt[];
83
+ }
84
+
85
+ const requireOrderField = (orders: AggregateHandle, field: string, purpose: string): void => {
86
+ if (orders.fields[field] === undefined) {
87
+ throw new Error(
88
+ `workspaceSharing(${orders.id}): ${purpose} field '${field}' is not declared on the delivery order`,
89
+ );
90
+ }
91
+ };
92
+
93
+ /**
94
+ * Declare one durable business sharing policy. The order aggregate is ordinary
95
+ * domain state; delivery and retry are generated framework behaviour.
96
+ *
97
+ * This is the canonical access-delivery lane. Model the pending order and its
98
+ * delivered/refused business receipts here; never call a bridge from application
99
+ * code or mirror pending delivery in UI state.
100
+ *
101
+ * @example
102
+ * language("estate", intents, {
103
+ * workspaceSharing: [workspaceSharing({
104
+ * orders: EstateAccessShareOrder,
105
+ * initiatedBy: [grantEstateAccess, acceptEstateInvitation],
106
+ * pending: estateAccessShareOrdersByStatus,
107
+ * person: "subject",
108
+ * role: "relation",
109
+ * scope: "object",
110
+ * delivered: [{ records: recordEstateAccessDelivered }],
111
+ * refused: [{ records: recordEstateAccessRefused }],
112
+ * })],
113
+ * })
114
+ */
115
+ export function workspaceSharing(options: WorkspaceSharingOptions): WorkspaceSharingPolicy {
116
+ const { orders, pending } = options;
117
+ if (pending.returns !== orders.id) {
118
+ throw new Error(
119
+ `workspaceSharing(${orders.id}): pending query '${pending.id}' returns '${pending.returns}'`,
120
+ );
121
+ }
122
+ if (pending.key.length !== 1) {
123
+ throw new Error(
124
+ `workspaceSharing(${orders.id}): pending query '${pending.id}' must have exactly one indexed key`,
125
+ );
126
+ }
127
+ if (options.initiatedBy.length === 0) {
128
+ throw new Error(
129
+ `workspaceSharing(${orders.id}): initiatedBy must name at least one business action`,
130
+ );
131
+ }
132
+ for (const action of options.initiatedBy) {
133
+ const schema = action.payloadSchema as z.ZodTypeAny;
134
+ const hasTypedRecipient = zodKind(schema) === "object" &&
135
+ Object.values(zodObjectShape(schema)).some((field) => recipientMetaOf(field) !== undefined);
136
+ if (!hasTypedRecipient) {
137
+ throw new Error(
138
+ `workspaceSharing(${orders.id}): business action '${action.id}' must accept a recipient() field; ` +
139
+ `plain uid/email/principal strings are not invitation recipients`,
140
+ );
141
+ }
142
+ }
143
+ requireOrderField(orders, options.person, "person");
144
+ requireOrderField(orders, options.role, "role");
145
+ requireOrderField(orders, options.scope, "scope");
146
+ if (options.delivered.length === 0 || options.refused.length === 0) {
147
+ throw new Error(
148
+ `workspaceSharing(${orders.id}): both delivered and refused business receipts are required`,
149
+ );
150
+ }
151
+ for (const receipt of [...options.delivered, ...options.refused]) {
152
+ if (receipt.when !== undefined) {
153
+ requireOrderField(orders, receipt.when.field, "receipt condition");
154
+ }
155
+ for (const [target, source] of Object.entries(receipt.payload ?? {})) {
156
+ requireOrderField(orders, source, `receipt payload '${target}'`);
157
+ }
158
+ }
159
+ const pendingValue = options.pendingValue ?? "pending";
160
+ const stem = options.orders.id.replace(/[^A-Za-z0-9]/g, "");
161
+ const redeemId = `nomosInternalRedeem${stem}Invitation7c1f5e`;
162
+ const pendingByRecipient = {
163
+ ...query(`nomosInternal${stem}InvitationsByRecipient7c1f5e`)
164
+ .key(options.person)
165
+ .returns(options.orders),
166
+ businessBirthInternal: true as const,
167
+ };
168
+ const b64 = z.string().regex(/^[A-Za-z0-9+/]+={0,2}$/, "expected base64");
169
+ const redeem = nomosImplementationDetail(
170
+ directive(redeemId)
171
+ .mutates(options.orders)
172
+ .payload(z.object({
173
+ orderId: z.string().min(1),
174
+ object: z.string().min(1),
175
+ relation: z.string().min(1),
176
+ invitationPublicKey: b64,
177
+ invitationSignature: b64,
178
+ invitationId: z.string().regex(/^nomos-invitation-[0-9a-f]{64}$/),
179
+ redeemedAt: z.string().min(1),
180
+ }))
181
+ .plan((facts, ctx) => {
182
+ const principal = endorsementOf(ctx.principal().asAttribution())?.principal ?? "";
183
+ const claim = instance(NomosInvitationClaim, facts.invitationId);
184
+ return [
185
+ ...writeTuple({
186
+ object: facts.object,
187
+ relation: facts.relation,
188
+ subject: ctx.principal().asAuthoritySubject(
189
+ "redeem a possession-proven, single-use business invitation",
190
+ ),
191
+ grantedBy: ctx.principal().asAttribution(),
192
+ grantedAt: facts.redeemedAt,
193
+ }),
194
+ set(instance(options.orders, facts.orderId), options.person, principal),
195
+ withMarker(set(claim, "publicKey", facts.invitationPublicKey), "ensures"),
196
+ set(claim, "redeemedBy", principal),
197
+ set(claim, "redeemedAt", facts.redeemedAt),
198
+ ];
199
+ })
200
+ .alsoEnsures(RelationTuple)
201
+ .alsoEnsures(NomosInvitationClaim),
202
+ );
203
+ const gate = workspaceInvariant(`NomosInvitationGate:${options.orders.id}`)
204
+ .on(redeemId)
205
+ .reads(({ intent }) => [
206
+ refAs("order", options.orders.id, `pre:${String(intent.orderId ?? "")}`),
207
+ refAs("claim", NomosInvitationClaim.id, `pre:${String(intent.invitationId ?? "")}`),
208
+ ])
209
+ .assert((snapshots, ctx) => {
210
+ const intent = ctx?.intent ?? {};
211
+ const order = snapshots.order ?? {};
212
+ const claim = snapshots.claim ?? {};
213
+ const publicKey = String(intent.invitationPublicKey ?? "");
214
+ const principal = String(ctx?.principal ?? "");
215
+ const invitationAddress = `nomos-invitation:v1:${publicKey}`;
216
+ if (ctx?.principalVerified !== true || !principal.startsWith("user:")) {
217
+ return { reject: "nomos.invitation.verified_user_required" };
218
+ }
219
+ if (claim.publicKey !== undefined) {
220
+ return { reject: "nomos.invitation.already_redeemed" };
221
+ }
222
+ const invitationId = `nomos-invitation-${(globalThis as unknown as { nomos: { sha256(value: string): string } }).nomos.sha256(publicKey)}`;
223
+ if (intent.invitationId !== invitationId) {
224
+ return { reject: "nomos.invitation.invalid_proof" };
225
+ }
226
+ if (
227
+ order[options.person] !== invitationAddress ||
228
+ order[options.role] !== intent.relation ||
229
+ order[options.scope] !== intent.object ||
230
+ order[pending.key[0]!] !== pendingValue
231
+ ) {
232
+ return { reject: "nomos.invitation.not_pending" };
233
+ }
234
+ const verifier = (globalThis as unknown as {
235
+ nomos?: {
236
+ attestationVerify?: (
237
+ publicKey: string,
238
+ signature: string,
239
+ asserterWorkspace: string,
240
+ object: string,
241
+ relation: string,
242
+ subject: string,
243
+ attestedAt: string,
244
+ ) => boolean;
245
+ };
246
+ }).nomos?.attestationVerify;
247
+ const valid = verifier?.(
248
+ publicKey,
249
+ String(intent.invitationSignature ?? ""),
250
+ "nomos-invitation-v1",
251
+ String(intent.orderId ?? ""),
252
+ String(intent.relation ?? ""),
253
+ principal,
254
+ String(intent.redeemedAt ?? ""),
255
+ ) === true;
256
+ return valid
257
+ ? { accept: true }
258
+ : { reject: "nomos.invitation.invalid_proof" };
259
+ });
260
+ const invitationLaw = Object.freeze({ claim: NomosInvitationClaim, redeem, gate, pendingByRecipient });
261
+
262
+ return Object.freeze({
263
+ ...options,
264
+ pendingValue,
265
+ delivered: Object.freeze([...options.delivered]),
266
+ refused: Object.freeze([...options.refused]),
267
+ invitationLaw,
268
+ });
269
+ }