@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
@@ -74,6 +74,7 @@ import { directive, type RequirableDirective } from "../directive.js";
74
74
  import { executeDirectiveToIntent } from "../wire_encode.js";
75
75
  import type { WireIntent } from "../wire.js";
76
76
  import type { Ports } from "../ctx.js";
77
+ import { idOf } from "../value_object.js";
77
78
 
78
79
  /**
79
80
  * The impure-capability lifecycle vocabulary — framework-fixed. A task is `requested` when
@@ -455,6 +456,12 @@ export function impureCapability<
455
456
  * the Rust reactor's `result_fields` (`HandlerSuccess::with_result_field`).
456
457
  */
457
458
  completeResult?: (taskInstanceId: string, resultRef: string) => PlannedOp[];
459
+ /**
460
+ * OPTIONAL validation for the result reference before the plan runs. Typed capabilities use this
461
+ * to make malformed encoded receipts impossible to admit at the payload boundary; receipt-free
462
+ * capabilities retain the ordinary opaque-string result reference.
463
+ */
464
+ completeResultSchema?: z.ZodString;
458
465
  /**
459
466
  * OPTIONAL: the capability-credential name the provider host must hold a
460
467
  * law-state `CapabilityBinding` for (see {@link ImpureCapability.capability}).
@@ -463,9 +470,19 @@ export function impureCapability<
463
470
  capability?: string;
464
471
  }): ImpureCapability<Id, ParamsShape, F> {
465
472
  const taskAggregate = impureCapabilityAggregate(spec.aggregateId, spec.params);
466
-
467
- // The obligation payload = the framework provenance/timing envelope ⊕ the tenant's params.
468
- const obligationPayload = obligationEnvelopeSchema.extend(spec.paramsSchema);
473
+ const typedRequestId = { requestId: idOf(taskAggregate) };
474
+ const obligationPayloadBase = obligationEnvelopeSchema.extend(spec.paramsSchema);
475
+ // `idOf()` deliberately has the same parsed value (`string`) as the schema it
476
+ // replaces; its extra aggregate identity lives in Zod metadata for lint/codegen.
477
+ // Preserve the generic payload type while installing that richer runtime schema.
478
+ const obligationPayload = obligationPayloadBase.extend(typedRequestId) as typeof obligationPayloadBase;
479
+ const typedCompleteBase = completeSchema.extend(typedRequestId);
480
+ const typedCompleteSchema = spec.completeResultSchema !== undefined
481
+ ? typedCompleteBase.extend({ resultRef: spec.completeResultSchema })
482
+ : typedCompleteBase;
483
+ const typedFailSchema = failSchema.extend(typedRequestId);
484
+ const typedBlockSchema = blockSchema.extend(typedRequestId);
485
+ const typedDeadLetterSchema = deadLetterSchema.extend(typedRequestId);
469
486
 
470
487
  const obligation: RequirableDirective<ObligationPayload<ParamsShape>> = directive(
471
488
  spec.obligationDirectiveId,
@@ -488,7 +505,7 @@ export function impureCapability<
488
505
 
489
506
  const complete = directive(spec.completeDirectiveId)
490
507
  .mutates(taskAggregate)
491
- .payload(completeSchema)
508
+ .payload(typedCompleteSchema)
492
509
  .plan((p) =>
493
510
  completeOps(
494
511
  spec.aggregateId,
@@ -500,17 +517,17 @@ export function impureCapability<
500
517
 
501
518
  const fail = directive(spec.failDirectiveId)
502
519
  .mutates(taskAggregate)
503
- .payload(failSchema)
520
+ .payload(typedFailSchema)
504
521
  .plan((p) => failOps(spec.aggregateId, p));
505
522
 
506
523
  const block = directive(spec.blockDirectiveId)
507
524
  .mutates(taskAggregate)
508
- .payload(blockSchema)
525
+ .payload(typedBlockSchema)
509
526
  .plan((p) => blockOps(spec.aggregateId, p));
510
527
 
511
528
  const deadLetter = directive(spec.deadLetterDirectiveId)
512
529
  .mutates(taskAggregate)
513
- .payload(deadLetterSchema)
530
+ .payload(typedDeadLetterSchema)
514
531
  .plan((p) => deadLetterOps(spec.aggregateId, p));
515
532
 
516
533
  return {
@@ -244,7 +244,10 @@ interface WriteTupleInput {
244
244
  * un-endorsed identity (`asAttribution()`) cannot be a subject (it won't typecheck), so identity
245
245
  * never SILENTLY decides a grant.
246
246
  */
247
- subject: string | EndorsedAuthority<string>;
247
+ /** A principal/userset wire subject, endorsed current principal, or an object token for a typed parent
248
+ * edge. `objectOf(Relations, id)` is the canonical parent-reference form: Nomos qualifies its type to the
249
+ * dispatching domain, so law authors never hand-assemble a namespaced relation identity. */
250
+ subject: string | EndorsedAuthority<string> | RelationObjectToken;
248
251
  /** ATTRIBUTION (free): who recorded the grant. A plain string OR `ctx.principal().asAttribution()`. */
249
252
  grantedBy: string | AttributionPrincipal;
250
253
  grantedAt: string;
@@ -261,7 +264,7 @@ interface WriteTupleInput {
261
264
  function lowerTuple(
262
265
  p: WriteTupleInput & {
263
266
  status: RelationStatus;
264
- revokedBy?: string | undefined;
267
+ revokedBy?: string | AttributionPrincipal | undefined;
265
268
  revokedAt?: string | undefined;
266
269
  },
267
270
  ): PlannedOp[] {
@@ -278,13 +281,18 @@ function lowerTuple(
278
281
  // host-stamped actor — the SAME form the write gate parses via `Subject::parse`), so an endorsed
279
282
  // authority subject lowers to that leaf verbatim and the gate's `engine.check` matches it.
280
283
  const subjectE = endorsementOf(p.subject);
281
- const subjectRaw = subjectE ? subjectE.principal : (p.subject as string);
284
+ const subjectRaw = subjectE
285
+ ? subjectE.principal
286
+ : isRelationObjectToken(p.subject)
287
+ ? resolveTupleObject(p.subject, p.relation)
288
+ : p.subject as string;
282
289
  const grantedByRaw = endorsementOf(p.grantedBy)?.principal ?? (p.grantedBy as string);
290
+ const revokedByRaw = endorsementOf(p.revokedBy)?.principal ?? (p.revokedBy as string | undefined);
283
291
  const leaf = compact({
284
292
  status: p.status,
285
293
  grantedBy: grantedByRaw,
286
294
  grantedAt: p.grantedAt,
287
- revokedBy: p.revokedBy,
295
+ revokedBy: revokedByRaw,
288
296
  revokedAt: p.revokedAt,
289
297
  // VA-core signer-provenance leaf fields — `compact` strips an `undefined`, so a non-signer relation
290
298
  // (these never set) stays byte-identical to a pre-VA leaf (the era rule on the wire shape).
@@ -316,7 +324,7 @@ export function writeTuple(p: WriteTupleInput): PlannedOp[] {
316
324
  * gate's remove-wins check (slice 4) refuses a later re-grant of an ever-revoked subject.
317
325
  */
318
326
  export function removeTuple(
319
- p: WriteTupleInput & { revokedBy: string; revokedAt: string },
327
+ p: WriteTupleInput & { revokedBy: string | AttributionPrincipal; revokedAt: string },
320
328
  ): PlannedOp[] {
321
329
  return lowerTuple({
322
330
  ...p,
@@ -78,6 +78,7 @@ import { workspaceInvariant, refAs, type WorkspaceInvariantVerdict } from "./wor
78
78
  import { writeTuple, removeTuple, RelationTuple, relationTupleInstanceId } from "./relations.js";
79
79
  import { relationType, self, user, key } from "../relation_schema.js";
80
80
  import { GENESIS_WARRANT_ID } from "./genesis.js";
81
+ import { idOf } from "../value_object.js";
81
82
  import { signerObject, SIGNER_RELATION } from "./signing_identity.js";
82
83
  // THE GENESIS BOOTSTRAP (platform-birth genesis bootstrap, Approach A): governance carries the
83
84
  // warrant-flip primitives so the birth door can lawfully bootstrap a virgin platform's founding admin —
@@ -812,7 +813,7 @@ export const provisionWorkspace = directive("provisionWorkspace")
812
813
  .mutates(CloudWorkspace)
813
814
  .payload(
814
815
  z.object({
815
- workspaceId: z.string().min(1),
816
+ workspaceId: idOf(CloudWorkspace),
816
817
  ownerGrantHash: sha256Hex,
817
818
  provisionedAt: epochMs,
818
819
  provisionedBy: z.string().min(1),
@@ -836,7 +837,7 @@ export const rotateWorkspaceOwner = directive("rotateWorkspaceOwner")
836
837
  .mutates(CloudWorkspace)
837
838
  .payload(
838
839
  z.object({
839
- workspaceId: z.string().min(1),
840
+ workspaceId: idOf(CloudWorkspace),
840
841
  ownerGrantHash: sha256Hex,
841
842
  rotatedBy: z.string().min(1),
842
843
  rotatedAt: epochMs,
@@ -857,7 +858,7 @@ export const setWorkspaceTier = directive("setWorkspaceTier")
857
858
  .mutates(CloudWorkspace)
858
859
  .payload(
859
860
  z.object({
860
- workspaceId: z.string().min(1),
861
+ workspaceId: idOf(CloudWorkspace),
861
862
  tier: z.enum(WORKSPACE_TIERS),
862
863
  changedBy: z.string().min(1),
863
864
  changedAt: epochMs,
@@ -878,7 +879,7 @@ export const retireWorkspace = directive("retireWorkspace")
878
879
  .mutates(CloudWorkspace)
879
880
  .payload(
880
881
  z.object({
881
- workspaceId: z.string().min(1),
882
+ workspaceId: idOf(CloudWorkspace),
882
883
  retiredBy: z.string().min(1),
883
884
  retiredAt: epochMs,
884
885
  }),
@@ -900,7 +901,7 @@ export const retireOwnWorkspace = directive("retireOwnWorkspace")
900
901
  .mutates(CloudWorkspace)
901
902
  .payload(
902
903
  z.object({
903
- workspaceId: z.string().min(1),
904
+ workspaceId: idOf(CloudWorkspace),
904
905
  retiredBy: z.string().min(1),
905
906
  retiredAt: epochMs,
906
907
  }),
@@ -956,7 +957,7 @@ export const reclaimWorkspace = directive("reclaimWorkspace")
956
957
  .mutates(CloudWorkspace)
957
958
  .payload(
958
959
  z.object({
959
- workspaceId: z.string().min(1),
960
+ workspaceId: idOf(CloudWorkspace),
960
961
  reclaimedBy: z.string().min(1),
961
962
  reclaimedAt: epochMs,
962
963
  }),
@@ -994,7 +995,7 @@ export const rebirthWorkspace = directive("rebirthWorkspace")
994
995
  .payload(
995
996
  z.object({
996
997
  /** The target CloudWorkspace AGGREGATE id (the minted lineage-record id; root-custody registry). */
997
- workspaceId: z.string().min(1),
998
+ workspaceId: idOf(CloudWorkspace),
998
999
  /** MANDATORY — the custodian's justification for superseding this chain (audit, on the ledger forever). */
999
1000
  reason: z.string().min(1),
1000
1001
  /** Epoch-ms the supersession was authored (caller-stamped — the timestamp doctrine). Its presence on the
@@ -1322,7 +1323,7 @@ export const setPlatformPool = directive("setPlatformPool")
1322
1323
  .mutates(CloudWorkspace)
1323
1324
  .payload(
1324
1325
  z.object({
1325
- workspaceId: z.string().min(1),
1326
+ workspaceId: idOf(CloudWorkspace),
1326
1327
  pool: z.number().int().positive(),
1327
1328
  setBy: z.string().min(1),
1328
1329
  setAt: epochMs,
@@ -1437,7 +1438,7 @@ export const releaseHostname = directive("releaseHostname")
1437
1438
  .mutates(HostnameClaim)
1438
1439
  .payload(
1439
1440
  z.object({
1440
- claimId: z.string().min(1),
1441
+ claimId: idOf(HostnameClaim),
1441
1442
  releasedBy: z.string().min(1),
1442
1443
  releasedAt: epochMs,
1443
1444
  }),
@@ -1499,7 +1500,7 @@ export const revokeCapability = directive("revokeCapability")
1499
1500
  .mutates(CapabilityBinding)
1500
1501
  .payload(
1501
1502
  z.object({
1502
- bindingId: z.string().min(1),
1503
+ bindingId: idOf(CapabilityBinding),
1503
1504
  revokedBy: z.string().min(1),
1504
1505
  revokedAt: epochMs,
1505
1506
  }),
@@ -1574,7 +1575,7 @@ export const setWorkspaceByteCap = directive("setWorkspaceByteCap")
1574
1575
  .mutates(CloudWorkspace)
1575
1576
  .payload(
1576
1577
  z.object({
1577
- workspaceId: z.string().min(1),
1578
+ workspaceId: idOf(CloudWorkspace),
1578
1579
  byteCap: z.number().int().positive(),
1579
1580
  setBy: z.string().min(1),
1580
1581
  setAt: epochMs,
@@ -1807,7 +1808,7 @@ export const founderSignerWriteOnce = workspaceInvariant("FounderSignerWriteOnce
1807
1808
  refAs(
1808
1809
  "tuple",
1809
1810
  "RelationTuple",
1810
- "pre:" + relationTupleInstanceId(signerObject(String((ctx.intent ?? {}).principal ?? "")), SIGNER_RELATION),
1811
+ "pre:" + relationTupleInstanceId(signerObject(String(ctx.principal ?? "")), SIGNER_RELATION),
1811
1812
  ),
1812
1813
  ])
1813
1814
  .assert(({ tuple }, ctx) => {
@@ -16,7 +16,45 @@ type ClientDomain = {
16
16
  const camel = (value: string): string =>
17
17
  value.replace(/[_\s-]+(\w)/g, (_match, letter: string) => letter.toUpperCase());
18
18
 
19
- /** Refuse tenant callables that would overwrite the generated `model` adoption concept. */
19
+ const lowerFirst = (value: string): string =>
20
+ value.length === 0 ? value : value[0]!.toLowerCase() + value.slice(1);
21
+
22
+ /** One shared public collection spelling for generators and their generated proof (`Entry` -> `entries`). */
23
+ export function generatedAggregateCollectionName(aggregateId: string): string {
24
+ const name = camel(aggregateId);
25
+ const plural = /[^aeiou]y$/i.test(name)
26
+ ? `${name.slice(0, -1)}ies`
27
+ : /(?:s|x|z|ch|sh)$/i.test(name)
28
+ ? `${name}es`
29
+ : `${name}s`;
30
+ return lowerFirst(plural);
31
+ }
32
+
33
+ const GENERATED_APPLICATION_MEMBERS = new Set([
34
+ "domain",
35
+ "domainHash",
36
+ "interface",
37
+ "model",
38
+ "encryption",
39
+ "recovery",
40
+ "devices",
41
+ "share",
42
+ "unshare",
43
+ "transfer",
44
+ "corrections",
45
+ "conflicts",
46
+ "past",
47
+ "watchActivity",
48
+ "activityLog",
49
+ "describe",
50
+ "describeHistory",
51
+ "currentStatus",
52
+ "watchStatus",
53
+ "currentUser",
54
+ "refresh",
55
+ ]);
56
+
57
+ /** Refuse tenant callables that would overwrite a generated application concept. */
20
58
  export function assertModelClientMemberAvailable(mod: ClientDomain): void {
21
59
  const domain = mod.domain ?? mod.name;
22
60
  const surfaces: readonly [string, readonly Named[]][] = [
@@ -28,9 +66,10 @@ export function assertModelClientMemberAvailable(mod: ClientDomain): void {
28
66
  ];
29
67
  for (const [kind, entries] of surfaces) {
30
68
  for (const entry of entries) {
31
- if (typeof entry.id !== "string" || camel(entry.id) !== "model") continue;
69
+ if (typeof entry.id !== "string" || !GENERATED_APPLICATION_MEMBERS.has(camel(entry.id))) continue;
70
+ const reserved = camel(entry.id);
32
71
  throw new Error(
33
- `Nomos generated client concept 'model' is already taken, but business ${kind} ` +
72
+ `Nomos generated application concept '${reserved}' is already taken, but business ${kind} ` +
34
73
  `'${entry.id}' in domain '${domain}' would use the same name. Sorry mate: Nomos will not ` +
35
74
  `silently reinterpret either concept; rename the business ${kind}.`,
36
75
  );
package/src/idof_lint.ts CHANGED
@@ -8,11 +8,11 @@
8
8
  /**
9
9
  * THE PAYLOAD-REF idOf LINT (`architecture/dart_codegen_parity.md` P2 — ref ergonomics). A
10
10
  * `t.ref(Target)` / `t.foreignRef(Target)` field on the AGGREGATE/read-model side already
11
- * generates a TYPED id class (`SiteId`, `codegen_dart.ts`). Nothing enforced the payload SIDE
11
+ * generates a canonical `NomosRef<Site>`. Nothing enforced the payload SIDE
12
12
  * carried the matching brand — a directive author can write `siteId: z.string()` in the
13
13
  * payload and never discover `idOf(Site)` exists, so the generated Dart payload class carries a
14
- * raw `String` while the read model carries `SiteId`: an asymmetry that forces manual
15
- * `.value` unwrap/rewrap at the UI boundary and lets a mistyped id compile silently.
14
+ * raw `String` while the read model carries `NomosRef<Site>`: an asymmetry that forces manual
15
+ * identity reconstruction at the UI boundary and lets a mistyped id compile silently.
16
16
  *
17
17
  * THIS PASS closes that gap at COMPILE time: for every directive, plan-PROBE it (reusing the
18
18
  * exact primitive the proof generator's own discovery legs use — `probePlanOps` /
@@ -37,17 +37,15 @@
37
37
  * - A directive whose plan can't be probed (unsynthesizable payload, or the plan throws under
38
38
  * the synthesized sample) is SKIPPED — best-effort, matching every other plan-probe
39
39
  * consumer in `codegen_proof.ts` (never a false refusal from an exotic shape).
40
- * - `idOf(Agg)` AND `brandedId(Name)` both satisfy the lint (either brand types the Dart/TS
41
- * side); only a truly BARE `z.string()` (through `.optional()`/`.default()`/`.nullable()`)
42
- * refuses.
40
+ * - Only `idOf(the exact target aggregate)` satisfies a reference edge. A standalone
41
+ * `brandedId(Name)` is intentionally insufficient because it has no workspace or aggregate type.
43
42
  *
44
43
  * NOT era-gated: this is a pure Dart/TS-codegen build-time guard (like the domain-contracts
45
44
  * circular-import guard in `compile_package_main.ts`) — it has no wire/manifest/kernel effect,
46
45
  * changes no hash, and never touches already-deployed law. It only judges whether `nomos-compile`
47
46
  * accepts a directive's SOURCE shape, exactly the same posture as that circular-dependency guard.
48
- * A domain already using `idOf`/`brandedId` (or one with no ref-fed payload field at all) compiles
49
- * exactly as before; only a directive whose payload feeds a ref field with a provably-bare string
50
- * newly refuses — which is the intended fail-closed behaviour, not an accidental regression.
47
+ * A domain already using the exact `idOf(Target)` (or one with no ref-fed payload field at all) compiles
48
+ * exactly as before; bare strings, standalone brands and `idOf(AnotherAggregate)` refuse.
51
49
  */
52
50
  import type { z } from "zod";
53
51
 
@@ -101,6 +99,12 @@ function refTargetOf(op: FieldOp): string | undefined {
101
99
  * (unsynthesizable payload / plan throws) is silently skipped — best-effort, no false refusal.
102
100
  */
103
101
  export function assertIdOfLintForDirective(dir: Directive<unknown>, where: string): void {
102
+ // Framework-authored implementation directives deliberately carry wire-level record ids.
103
+ // They are never emitted into the generated tenant API, whose public wrapper accepts and
104
+ // validates NomosRef<TAggregate> before privately lowering it to this payload. Applying the
105
+ // tenant-facing idOf lint here would force framework wire schemas to masquerade as public
106
+ // application schemas and, worse, reject every aggregate that opts into owned custody.
107
+ if ((dir as { businessBirthInternal?: unknown }).businessBirthInternal === true) return;
104
108
  const probed = probePlanOps(dir, where);
105
109
  if (probed === undefined) return;
106
110
  let shape: Record<string, z.ZodTypeAny>;
@@ -117,6 +121,31 @@ export function assertIdOfLintForDirective(dir: Directive<unknown>, where: strin
117
121
  if (typeof value === "string") byValue.set(value, name);
118
122
  }
119
123
  for (const op of probed.ops) {
124
+ // A bound mutation addresses one existing aggregate instance. The payload field carrying that
125
+ // address is therefore not "some string" or merely "some branded id": it must be idOf(the exact
126
+ // aggregate being mutated). This is the identity spine of the generated application API. Without
127
+ // this guard a model can compile `entryId: z.string()`, after which every generated caller can mix
128
+ // unrelated record ids and is encouraged to reconstruct identity as application state.
129
+ const effectiveMarker = op.marker ?? (op.aggregateType === dir.aggregateId ? dir.marker : "mutates");
130
+ if (op.aggregateType !== undefined && (effectiveMarker === "mutates" || effectiveMarker === "archives")) {
131
+ const payloadField = byValue.get(op.aggregateId);
132
+ if (payloadField !== undefined) {
133
+ const fieldSchema = shape[payloadField];
134
+ const actualTarget = fieldSchema === undefined ? undefined : idOfMetaOf(fieldSchema);
135
+ if (actualTarget !== op.aggregateType) {
136
+ const expected = shortAggName(op.aggregateType);
137
+ const actual = actualTarget === undefined
138
+ ? "an untyped string"
139
+ : `idOf(${shortAggName(actualTarget)})`;
140
+ const verb = effectiveMarker === "archives" ? "archive" : "mutate";
141
+ throw new IdOfLintError(
142
+ `directive '${dir.id}': payload field '${payloadField}' selects the ${expected} record to ${verb} ` +
143
+ `but carries ${actual} — declare it as idOf(${expected}) so create, update, find and watch use ` +
144
+ `the same generated NomosRef<${expected}>.`,
145
+ );
146
+ }
147
+ }
148
+ }
120
149
  if (op.kind !== "set" && op.kind !== "setEntry") continue;
121
150
  if (typeof op.value !== "string") continue;
122
151
  const targetAgg = refTargetOf(op);
@@ -124,13 +153,23 @@ export function assertIdOfLintForDirective(dir: Directive<unknown>, where: strin
124
153
  const payloadField = byValue.get(op.value);
125
154
  if (payloadField === undefined) continue; // not traceable to a top-level payload field — no verdict
126
155
  const fieldSchema = shape[payloadField];
127
- if (fieldSchema === undefined || !isBareString(fieldSchema)) continue; // already idOf/brandedId — fine
156
+ if (fieldSchema === undefined) continue;
128
157
  const onAgg = shortAggName(op.aggregateType ?? op.aggregateId);
129
158
  const targetName = shortAggName(targetAgg);
159
+ const actualTarget = idOfMetaOf(fieldSchema);
160
+ if (actualTarget === targetAgg) continue;
161
+ if (actualTarget !== undefined) {
162
+ throw new IdOfLintError(
163
+ `directive '${dir.id}': payload field '${payloadField}' feeds ref field '${onAgg}.${op.field}' ` +
164
+ `(-> ${targetName}) but carries idOf(${shortAggName(actualTarget)}) — declare it as ` +
165
+ `idOf(${targetName}) so generated callers carry NomosRef<${targetName}>.`,
166
+ );
167
+ }
168
+ if (!isBareString(fieldSchema) && brandedIdMetaOf(fieldSchema) === undefined) continue;
130
169
  throw new IdOfLintError(
131
170
  `directive '${dir.id}': payload field '${payloadField}' feeds ref field '${onAgg}.${op.field}' ` +
132
- `(-> ${targetName}) but is a plain z.string() — wrap it as idOf(${targetName}) so the generated ` +
133
- `Dart/TS carry the typed id, matching the read model. (dart_codegen_parity.md P2)`,
171
+ `(-> ${targetName}) but is not idOf(${targetName}) — declare it as idOf(${targetName}) so generated ` +
172
+ `Dart/TS carry NomosRef<${targetName}>, matching the read model. (dart_codegen_parity.md P2)`,
134
173
  );
135
174
  }
136
175
  }
@@ -138,7 +177,20 @@ export function assertIdOfLintForDirective(dir: Directive<unknown>, where: strin
138
177
  /** Run the idOf lint over every directive in a domain. `domain` names the module for the probe's
139
178
  * error-context strings (`synthesizePayload`'s `where`). Throws on the FIRST violation. */
140
179
  export function assertIdOfLint(directives: readonly Directive<unknown>[], domain: string): void {
180
+ const violations: string[] = [];
141
181
  for (const dir of directives) {
142
- assertIdOfLintForDirective(dir, `${domain}/${dir.id}`);
182
+ try {
183
+ assertIdOfLintForDirective(dir, `${domain}/${dir.id}`);
184
+ } catch (error) {
185
+ if (!(error instanceof IdOfLintError)) throw error;
186
+ violations.push(error.message);
187
+ }
188
+ }
189
+ if (violations.length === 1) throw new IdOfLintError(violations[0]!);
190
+ if (violations.length > 1) {
191
+ throw new IdOfLintError(
192
+ `${violations.length} typed identity violations in domain '${domain}':\n` +
193
+ violations.map((message) => ` - ${message}`).join("\n"),
194
+ );
143
195
  }
144
196
  }
package/src/index.ts CHANGED
@@ -60,6 +60,7 @@ export {
60
60
  unit,
61
61
  idOf,
62
62
  idOfMetaOf,
63
+ idOfTargetKindOf,
63
64
  brandedId,
64
65
  brandedIdMetaOf,
65
66
  voMetaOf,
@@ -110,6 +111,32 @@ export {
110
111
  type ConflictPolicy,
111
112
  type ConflictRule,
112
113
  } from "./conflicts.js";
114
+ export {
115
+ intent,
116
+ language,
117
+ isBusinessIntent,
118
+ isApplicationLanguage,
119
+ applicationLanguageOf,
120
+ executableDirectivesForLanguage,
121
+ assertApplicationLanguageCoversAuthoredDirectives,
122
+ remapWorkspaceInvariantsForLanguage,
123
+ type BusinessIntent,
124
+ type IntentContract,
125
+ type IntentContractDeclaration,
126
+ type IntentStep,
127
+ type ApplicationLanguage,
128
+ } from "./intent.js";
129
+ export {
130
+ workspaceSharing,
131
+ type WorkspaceSharingOptions,
132
+ type WorkspaceSharingPolicy,
133
+ type WorkspaceSharingReceipt,
134
+ } from "./workspace_sharing.js";
135
+ export {
136
+ recipient,
137
+ recipientMetaOf,
138
+ NOMOS_RECIPIENT_BRAND,
139
+ } from "./recipient.js";
113
140
  export {
114
141
  set,
115
142
  clear,
@@ -131,6 +158,7 @@ export {
131
158
  export { clearDirectiveId, makeClearDirective, withClearDirectives } from "./clear_directive.js";
132
159
  export {
133
160
  directive,
161
+ nomosImplementationDetail,
134
162
  recordObject,
135
163
  type Directive,
136
164
  type ReferentialMarker,
@@ -148,7 +176,7 @@ export {
148
176
  // — naming no id. A directive's `.plan` calls these and returns `[]` (the recorded graph is the write
149
177
  // surface). The dev never sees a "prim" — that is the GitHolon's internal OpenUSD IR.
150
178
  export { create, existing, ensure, type AggregateRef } from "./authoring.js";
151
- export { contract, isContractHandle, satisfiesContract, type ContractHandle, type ContractField, type ContractSatisfaction } from "./contract.js";
179
+ export { recordContract, isContractHandle, satisfiesContract, type ContractHandle, type ContractField, type ContractSatisfaction } from "./contract.js";
152
180
  export { normalizeActivity, renderActivityTemplate, activityKey, type ActivityInput, type ActivityDescriptor, type ActivityRefInput, type ActivitySeverity } from "./activity.js";
153
181
  // The REBIRTH-boundary repair DSL (architecture/repair_dsl.md) — distinct from framework/repair.js (the
154
182
  // IN-CHAIN compensating-intent lane): repair() binds a typed replay refusal to a declared re-fold remedy.
@@ -444,22 +472,28 @@ export {
444
472
  setAuthProvider,
445
473
  authProvider,
446
474
  } from "./framework/auth_provider_law.js";
447
- // THE HOME HOLON (home_holons_and_cross_custody.md §4) — the identity-role law: the shared-with-me access
448
- // index + the owner lane to maintain it (the key directory is the `signer` relation above).
475
+ // THE RECEIVING-GROUP HOLON — the framework-private shared signer roster + access manifest.
449
476
  export {
450
477
  SharedWithMe,
451
478
  recordShare,
452
479
  pruneShare,
453
480
  sharedByWorkspace,
454
- HomeOwner,
455
- HOME_OWNER_ID,
456
- HOME_NAME_PREFIX,
457
- HOME_NAME_HASH_LEN,
458
- seedHomeOwner,
459
- writeHomeOwner,
460
- homeNameBindsOwner,
461
- homeNameOf,
462
- birthHome,
481
+ ReceivingGroup,
482
+ ReceivingGroupPolicy,
483
+ RECEIVING_GROUP_ID,
484
+ RECEIVING_GROUP_NAME_PREFIX,
485
+ RECEIVING_GROUP_NAME_HASH_LEN,
486
+ RECEIVING_GROUP_POLICY_ID,
487
+ DEFAULT_RECEIVING_GROUP_POLICY,
488
+ seedReceivingGroup,
489
+ seedReceivingGroupPolicy,
490
+ writeReceivingGroup,
491
+ receivingGroupNameBindsPrincipal,
492
+ receivingGroupRejectsKeylessAdditionalDevice,
493
+ receivingGroupAdditionalDeviceFollowsPolicy,
494
+ receivingGroupRevocationFollowsPolicy,
495
+ receivingGroupNameOf,
496
+ birthReceivingGroup,
463
497
  // THE PEER-SOVEREIGN ROOT-OF-TRUST (the BirthAttestation cert chain).
464
498
  BirthAttestation,
465
499
  BIRTH_ATTESTATION_ID,