@githolon/dsl 0.102.4 → 0.103.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 (131) hide show
  1. package/compile_package.mjs +43 -2
  2. package/compile_proof.d.mts +47 -0
  3. package/compile_proof.mjs +343 -0
  4. package/dart/CHANGELOG.md +7 -0
  5. package/dart/lib/src/dev_cloud.dart +8 -8
  6. package/dart/lib/src/nms_generated_runtime.dart +2 -0
  7. package/dart/lib/src/runner/node-entry.mjs +5504 -1236
  8. package/dart/lib/src/runner/offline-install.mjs +4578 -528
  9. package/dart/lib/src/test_harness.dart +206 -152
  10. package/dart/lib/src/webview_bridge.dart +196 -89
  11. package/dart/pubspec.lock +1 -1
  12. package/dart/pubspec.yaml +2 -2
  13. package/dart/test/acquire_session_credentials_test.dart +4 -3
  14. package/dart/test/adv_realm_interference_test.dart +7 -6
  15. package/dart/test/cloud_bound_topology_test.dart +3 -3
  16. package/dart/test/cluster_births_test.dart +4 -3
  17. package/dart/test/cluster_device_keyed_acceptance_test.dart +10 -16
  18. package/dart/test/cluster_local_autosync_test.dart +3 -2
  19. package/dart/test/cluster_scale_test.dart +3 -2
  20. package/dart/test/dlq_session_surface_test.dart +7 -6
  21. package/dart/test/first_app_gauntlet_test.dart +14 -21
  22. package/dart/test/fixtures/wedge/fake-install.mjs +1 -1
  23. package/dart/test/four_layer_nested_topology_test.dart +4 -4
  24. package/dart/test/harness_phase_timeout_test.dart +2 -2
  25. package/dart/test/local_sync_inert_test.dart +1 -1
  26. package/dart/test/parent_signer_recovery_test.dart +2 -2
  27. package/dart/test/sealing_client_test.dart +4 -3
  28. package/dart/test/session_bound_test.dart +8 -7
  29. package/dart/test/test_harness_test.dart +13 -12
  30. package/dart/test/vo_read_typing_test.dart +4 -3
  31. package/dart/test/watch_after_offer_stall_test.dart +4 -3
  32. package/dart/test/webview_bridge_test.dart +102 -5
  33. package/dart_types/CHANGELOG.md +5 -0
  34. package/dart_types/lib/nomos_types.dart +9 -0
  35. package/dart_types/lib/src/nomos_ref.dart +20 -0
  36. package/dart_types/lib/src/optional_ref.dart +85 -0
  37. package/dart_types/lib/src/sha256_digest.dart +41 -0
  38. package/dart_types/lib/src/versioned_nomos_ref.dart +81 -0
  39. package/dart_types/pubspec.yaml +1 -1
  40. package/dart_types/test/optional_ref_test.dart +44 -0
  41. package/dart_types/test/sha256_digest_test.dart +18 -0
  42. package/package.json +9 -5
  43. package/runtime/generated-application-host.js +5365 -1105
  44. package/src/aggregate.ts +18 -0
  45. package/src/application_composition.ts +6 -32
  46. package/src/attested_core.ts +6 -3
  47. package/src/birth_meter.ts +1 -0
  48. package/src/birth_meter_ensure.ts +28 -2
  49. package/src/births.ts +1 -1
  50. package/src/build_package.ts +162 -538
  51. package/src/business_birth.ts +19 -3
  52. package/src/canonical_json.ts +21 -0
  53. package/src/check_package_main.ts +76 -35
  54. package/src/clear_directive.ts +19 -8
  55. package/src/codegen_dart.ts +305 -22
  56. package/src/codegen_dart_application.ts +844 -292
  57. package/src/codegen_dart_client.ts +1084 -407
  58. package/src/codegen_dart_scenario.ts +12 -12
  59. package/src/codegen_proof.ts +169 -39
  60. package/src/codegen_provider_dart.ts +17 -1
  61. package/src/codegen_ts.ts +298 -106
  62. package/src/compat_manifest.ts +2 -2
  63. package/src/compat_package_main.ts +8 -12
  64. package/src/compile_package_main.ts +1866 -597
  65. package/src/compose.ts +1 -1
  66. package/src/directive.ts +208 -15
  67. package/src/engine_entry.ts +165 -111
  68. package/src/evolve_preview.ts +43 -81
  69. package/src/extremum.ts +1 -1
  70. package/src/fields.ts +183 -4
  71. package/src/flow_gate.ts +75 -2
  72. package/src/framework/application_home.ts +2 -0
  73. package/src/framework/auth_provider_law.ts +2 -2
  74. package/src/framework/birth_meter.ts +7 -19
  75. package/src/framework/bootstrap.ts +0 -2
  76. package/src/framework/business_corrections.ts +9 -5
  77. package/src/framework/domain_lifecycle.ts +15 -29
  78. package/src/framework/encryption_scope.ts +2 -1
  79. package/src/framework/framework_aggregates.ts +2 -0
  80. package/src/framework/genesis.ts +16 -14
  81. package/src/framework/home.ts +20 -14
  82. package/src/framework/owned_custody.ts +9 -3
  83. package/src/framework/platform_admin.ts +3 -3
  84. package/src/framework/relations.ts +21 -13
  85. package/src/framework/signing_identity.ts +45 -40
  86. package/src/framework/workspace_invariant.ts +245 -0
  87. package/src/framework/workspaces.ts +23 -29
  88. package/src/index.ts +36 -16
  89. package/src/intent.ts +178 -6
  90. package/src/law_check.ts +32 -11
  91. package/src/law_graph.ts +2902 -0
  92. package/src/law_graph_usda.ts +1799 -0
  93. package/src/law_identity.ts +435 -0
  94. package/src/law_value_type.ts +230 -0
  95. package/src/lsp_server.ts +61 -31
  96. package/src/ops.ts +7 -0
  97. package/src/package_config.ts +141 -0
  98. package/src/package_dependencies.ts +108 -0
  99. package/src/package_dependency_usage.ts +150 -0
  100. package/src/package_source_imports.ts +71 -0
  101. package/src/plan_source_slice.ts +335 -0
  102. package/src/point_guard.ts +192 -0
  103. package/src/precondition.ts +44 -9
  104. package/src/read_projection_namespace.ts +1 -1
  105. package/src/runtime_zod.ts +44 -0
  106. package/src/secondary_ensure.ts +67 -14
  107. package/src/shape_compat.ts +1 -1
  108. package/src/stable_id_identity_bridge.ts +149 -0
  109. package/src/stable_id_lockfile.ts +278 -0
  110. package/src/stable_ids.ts +7 -2
  111. package/src/stable_ids_for_module.ts +348 -0
  112. package/src/stable_ids_types.ts +1 -1
  113. package/src/sum.ts +1 -1
  114. package/src/usd_state.ts +1 -1
  115. package/src/usda_relationship_integrity.ts +66 -0
  116. package/src/usda_text.ts +18 -0
  117. package/src/usdz.ts +78 -0
  118. package/src/value_object.ts +95 -0
  119. package/src/wire.ts +19 -0
  120. package/src/wire_encode.ts +22 -0
  121. package/src/workspace_sharding.ts +52 -36
  122. package/src/workspace_sharing.ts +28 -4
  123. package/src/zod_introspection.ts +1 -1
  124. package/dart/test/cluster_attested_test.dart +0 -201
  125. package/src/compile_engine.ts +0 -595
  126. package/src/manifest.ts +0 -1476
  127. package/src/read_projection_composition.ts +0 -117
  128. package/src/read_projection_manifest.ts +0 -115
  129. package/src/relation_composition.ts +0 -286
  130. package/src/usd.ts +0 -1967
  131. package/src/usd_layers.ts +0 -1776
package/src/compose.ts CHANGED
@@ -24,7 +24,7 @@
24
24
  * `CompositionViolation`, never silently coerced.
25
25
  */
26
26
  import { createHash } from "node:crypto";
27
- import { canonicalJson } from "./manifest.js";
27
+ import { canonicalJson } from "./canonical_json.js";
28
28
 
29
29
  /**
30
30
  * A composable law unit (modelled on a directive/role). The typed-rule fields:
package/src/directive.ts CHANGED
@@ -29,11 +29,24 @@ import { zodDef, zodKind, zodObjectShape } from "./zod_introspection.js";
29
29
  import {
30
30
  isFromPayloadRef,
31
31
  isTrustPolicyDecl,
32
+ NomosSourceFrontier,
32
33
  __registerMonotoneForeignQuery,
33
34
  type FromPayloadRef,
34
35
  type TrustPolicyDecl,
35
36
  type ForeignReadEdge,
36
37
  } from "./attested_core.js";
38
+ import {
39
+ buildPointGuard,
40
+ evaluatePointGuard,
41
+ type PointGuardContext,
42
+ type PointGuardDeclaration,
43
+ type PointGuardSpec,
44
+ } from "./point_guard.js";
45
+ import {
46
+ resolveInvariantReadId,
47
+ type InvariantReadId,
48
+ type InvariantReadIdPart,
49
+ } from "./framework/workspace_invariant.js";
37
50
 
38
51
  export type ReferentialMarker = "creates" | "mutates" | "ensures" | "archives";
39
52
 
@@ -62,12 +75,20 @@ export interface PreconditionDescriptor {
62
75
  readonly fieldNames: readonly string[];
63
76
  /** Pure payload → target instance id (no clock, no IO — re-derived identically on every peer). */
64
77
  readonly idField: (payload: unknown) => string;
78
+ /** Typed payload path for native USDA read selection. Legacy callback selectors omit this and
79
+ * remain compiler-visible migration debt; new code should pass a payload key or `.precondition`. */
80
+ readonly idPayloadPath?: readonly string[];
81
+ /** Complete typed USDA id expression. Present for keys, literals and templates; absent only
82
+ * for legacy arbitrary callbacks that still require migration. */
83
+ readonly readId?: InvariantReadId;
65
84
  /** The fixed-predicate specialization, or `predicate` for the general `.where` form. */
66
85
  readonly kind: "exists" | "absent" | "predicate";
67
86
  /** The row predicate — `row` is the resolved snapshot or `undefined` when the instance is absent. */
68
87
  readonly predicate: (row: Record<string, unknown> | undefined) => boolean;
69
88
  /** The typed refusal code emitted when the predicate is false. */
70
89
  readonly rejectCode: string;
90
+ /** Generic point-state algebra lowered from typed USDA directly into the Rust gate. */
91
+ readonly pointGuard?: PointGuardSpec;
71
92
  /**
72
93
  * Why this precondition may intentionally make a fact about private state observable through its
73
94
  * public refusal. Set by chaining `.declassify(reason)` directly after the precondition marker;
@@ -76,6 +97,56 @@ export interface PreconditionDescriptor {
76
97
  readonly declassify?: string;
77
98
  }
78
99
 
100
+ type PayloadIdSelector<P> =
101
+ | Extract<keyof P, string>
102
+ | ((payload: P) => string)
103
+ | InvariantReadIdPart
104
+ | readonly InvariantReadIdPart[];
105
+
106
+ function payloadIdSelector<P>(selector: PayloadIdSelector<P>): {
107
+ readonly field: (payload: unknown) => string;
108
+ readonly path?: readonly string[];
109
+ readonly readId?: InvariantReadId;
110
+ } {
111
+ if (typeof selector === "string") {
112
+ const readId: InvariantReadId = {
113
+ source: "payload",
114
+ path: [selector],
115
+ prefix: "",
116
+ suffix: "",
117
+ optional: false,
118
+ };
119
+ return {
120
+ field: (payload) => resolveInvariantReadId(readId, payload as Record<string, unknown>) ?? "",
121
+ path: [selector],
122
+ readId,
123
+ };
124
+ }
125
+ if (Array.isArray(selector)) {
126
+ const readId: InvariantReadId = { source: "template", parts: [...selector], optional: false };
127
+ return {
128
+ field: (payload) => resolveInvariantReadId(readId, payload as Record<string, unknown>) ?? "",
129
+ readId,
130
+ };
131
+ }
132
+ if (typeof selector === "object" && selector !== null && "source" in selector) {
133
+ if (selector.source === "item") {
134
+ throw new Error("directive precondition ids cannot use item-relative read parts");
135
+ }
136
+ const readId: InvariantReadId = selector.source === "literal"
137
+ ? selector
138
+ : selector.transform === "none"
139
+ ? { source: "payload", path: selector.path, prefix: "", suffix: "", optional: false }
140
+ : { source: "template", parts: [selector], optional: false };
141
+ return {
142
+ field: (payload) => resolveInvariantReadId(readId, payload as Record<string, unknown>) ?? "",
143
+ ...(readId.source === "payload" ? { path: readId.path } : {}),
144
+ readId,
145
+ };
146
+ }
147
+ return { field: selector as (payload: unknown) => string };
148
+ }
149
+
79
150
  /**
80
151
  * A pure, replay-stable derivation of the authz SCOPE an authored intent targets,
81
152
  * from its payload — e.g. `(p) => ["site", p.siteId]`. It MUST be a pure function
@@ -90,8 +161,12 @@ export interface PreconditionDescriptor {
90
161
  export interface SecondaryOutput {
91
162
  readonly marker: ReferentialMarker;
92
163
  readonly aggregateId: string;
164
+ /** Process-local compiler link; canonical law emits the referenced identity, never this object. */
165
+ readonly aggregate?: AggregateHandle;
93
166
  /** This action may affect more than one record of this aggregate type. */
94
167
  readonly many?: true;
168
+ /** This action affects zero or one record of this aggregate type. */
169
+ readonly optional?: true;
95
170
  }
96
171
 
97
172
  /**
@@ -133,6 +208,21 @@ export interface Directive<P = unknown> {
133
208
  readonly id: string;
134
209
  readonly marker: ReferentialMarker;
135
210
  readonly aggregateId: string;
211
+ /**
212
+ * This declaration contributes certified law metadata but is not an
213
+ * executable offer. The compiler must never publish a plan route for it.
214
+ * Used for structural sanctions such as the generated secondary-Ensure
215
+ * declaration; ordinary tenant and framework actions leave it unset.
216
+ */
217
+ readonly manifestOnly?: true;
218
+ /** Typed declaration target retained for compiler composition; omitted by every wire/law serializer. */
219
+ readonly aggregate?: AggregateHandle;
220
+ /**
221
+ * Business-result cardinality for the primary marker target. The default is
222
+ * exactly one; `.affectsMany()` and `.affectsOptional()` opt into the other
223
+ * two generated client shapes without changing the directive's replay law.
224
+ */
225
+ readonly outputCardinality?: "many" | "optional";
136
226
  /**
137
227
  * MULTI-OUTPUT DIRECTIVES: additional aggregates this directive declares it writes, beyond the primary
138
228
  * marker target. Present iff the directive chained `.alsoEnsures/.alsoCreates/.alsoMutates/.alsoArchives`
@@ -162,6 +252,13 @@ export interface Directive<P = unknown> {
162
252
  * application APIs. Set only through `nomosImplementationDetail(...)`.
163
253
  */
164
254
  readonly businessBirthInternal?: true;
255
+ /**
256
+ * Compiler-generated child seed produced from `birth(..., { initialise })`.
257
+ * Its effect signature is owned by the lowering pass: tenant authors never
258
+ * receive this directive and therefore cannot honestly call `.alsoWrites()`
259
+ * on it themselves. Omitted from canonical law output.
260
+ */
261
+ readonly businessBirthSeed?: true;
165
262
  /**
166
263
  * FRAMEWORK-SHARED brand (Dart-codegen hygiene). Set (by `framework/framework_aggregates.ts`) ONLY on
167
264
  * the framework-owned directive singletons declared in `dsl/src/framework/` (the relate/signer/enroll
@@ -445,21 +542,33 @@ export interface RequirableDirective<P> extends Directive<P> {
445
542
  genesisField?: string;
446
543
  keyHashesField?: string;
447
544
  }): RequirableDirective<P>;
545
+ /** Declare that this action can affect several records of its primary aggregate. */
546
+ affectsMany(): RequirableDirective<P>;
547
+ /** Declare that this action can affect zero or one record of its primary aggregate. */
548
+ affectsOptional(): RequirableDirective<P>;
448
549
  /**
449
550
  * MULTI-OUTPUT: declare an ADDITIONAL aggregate this directive writes (beyond its primary marker target),
450
551
  * so generated docs/proof/client metadata name every output ("writes A + B") instead of one by convention.
451
552
  * `.alsoEnsures/.alsoCreates/.alsoMutates/.alsoArchives` are the marker-specific sugar; `.alsoWrites` takes
452
553
  * an explicit marker. Chainable + additive (a directive that never calls these is byte-identical). The
453
- * kernel folds by the plan's ops regardless — this is descriptive law metadata, not a write-restriction.
554
+ * The compiler checks observable marked plan outputs against this certified effect signature; an
555
+ * undeclared secondary output is a compile error rather than a runtime-only projection surprise.
454
556
  */
455
557
  alsoWrites(aggregate: AggregateHandle, marker: ReferentialMarker): RequirableDirective<P>;
456
558
  alsoWritesMany(aggregate: AggregateHandle, marker: ReferentialMarker): RequirableDirective<P>;
559
+ alsoWritesOptional(aggregate: AggregateHandle, marker: ReferentialMarker): RequirableDirective<P>;
457
560
  alsoEnsures(aggregate: AggregateHandle): RequirableDirective<P>;
458
561
  alsoCreates(aggregate: AggregateHandle): RequirableDirective<P>;
459
562
  alsoEnsuresMany(aggregate: AggregateHandle): RequirableDirective<P>;
460
563
  alsoCreatesMany(aggregate: AggregateHandle): RequirableDirective<P>;
564
+ alsoEnsuresOptional(aggregate: AggregateHandle): RequirableDirective<P>;
565
+ alsoCreatesOptional(aggregate: AggregateHandle): RequirableDirective<P>;
461
566
  alsoMutates(aggregate: AggregateHandle): RequirableDirective<P>;
462
567
  alsoArchives(aggregate: AggregateHandle): RequirableDirective<P>;
568
+ alsoMutatesMany(aggregate: AggregateHandle): RequirableDirective<P>;
569
+ alsoArchivesMany(aggregate: AggregateHandle): RequirableDirective<P>;
570
+ alsoMutatesOptional(aggregate: AggregateHandle): RequirableDirective<P>;
571
+ alsoArchivesOptional(aggregate: AggregateHandle): RequirableDirective<P>;
463
572
  /**
464
573
  * Declare the authz pre-condition: the `capability` the actor must hold (defaults
465
574
  * to the directive id) at the scope `scopeFrom(payload)` derives (defaults to root).
@@ -702,19 +811,22 @@ class DirectivePlanStep<Id extends string, P> {
702
811
  * function of the inferred payload. ONLY valid on a `.creates` directive (it defines the created id).
703
812
  * Additive; `.creates(Agg)` with no `.withId` is the minted path (byte-identical).
704
813
  */
705
- withId(idFn: (payload: P) => string): DirectivePlanStep<Id, P> {
814
+ withId(idSelector: PayloadIdSelector<P>): DirectivePlanStep<Id, P> {
706
815
  if (this.marker !== "creates") {
707
816
  throw new Error(
708
817
  `directive('${this.id}').withId(...) is only valid on a .creates(...) directive — it fixes the ` +
709
818
  `CREATED aggregate's caller-supplied id (a .${this.marker} directive targets an existing id).`,
710
819
  );
711
820
  }
712
- const idField = idFn as (payload: unknown) => string;
821
+ const selected = payloadIdSelector(idSelector);
822
+ const idField = selected.field;
713
823
  return this.with(
714
824
  {
715
825
  aggregateId: this.aggregate.id,
716
826
  fieldNames: Object.keys(this.aggregate.fields).sort(),
717
827
  idField,
828
+ ...(selected.path === undefined ? {} : { idPayloadPath: selected.path }),
829
+ ...(selected.readId === undefined ? {} : { readId: selected.readId }),
718
830
  kind: "absent",
719
831
  predicate: (row) => row === undefined,
720
832
  rejectCode: `${shortName(this.aggregate.id)}-exists`,
@@ -730,13 +842,16 @@ class DirectivePlanStep<Id extends string, P> {
730
842
  */
731
843
  requiresExisting<F extends Record<string, Field>>(
732
844
  aggregate: AggregateHandle<string, F>,
733
- idFn: (payload: P) => string,
845
+ idSelector: PayloadIdSelector<P>,
734
846
  rejectCode?: string,
735
847
  ): DirectivePlanStep<Id, P> {
848
+ const selected = payloadIdSelector(idSelector);
736
849
  return this.with({
737
850
  aggregateId: aggregate.id,
738
851
  fieldNames: Object.keys(aggregate.fields).sort(),
739
- idField: idFn as (payload: unknown) => string,
852
+ idField: selected.field,
853
+ ...(selected.path === undefined ? {} : { idPayloadPath: selected.path }),
854
+ ...(selected.readId === undefined ? {} : { readId: selected.readId }),
740
855
  kind: "exists",
741
856
  predicate: (row) => row !== undefined,
742
857
  rejectCode: rejectCode ?? `${shortName(aggregate.id)}-not-found`,
@@ -752,20 +867,65 @@ class DirectivePlanStep<Id extends string, P> {
752
867
  */
753
868
  where<F extends Record<string, Field>>(
754
869
  aggregate: AggregateHandle<string, F>,
755
- idFn: (payload: P) => string,
870
+ idSelector: PayloadIdSelector<P>,
756
871
  predicate: (row: ReadModelRow<F> | undefined) => boolean,
757
872
  rejectCode: string,
758
873
  ): DirectivePlanStep<Id, P> {
874
+ const selected = payloadIdSelector(idSelector);
759
875
  return this.with({
760
876
  aggregateId: aggregate.id,
761
877
  fieldNames: Object.keys(aggregate.fields).sort(),
762
- idField: idFn as (payload: unknown) => string,
878
+ idField: selected.field,
879
+ ...(selected.path === undefined ? {} : { idPayloadPath: selected.path }),
880
+ ...(selected.readId === undefined ? {} : { readId: selected.readId }),
763
881
  kind: "predicate",
764
882
  predicate: predicate as (row: Record<string, unknown> | undefined) => boolean,
765
883
  rejectCode,
766
884
  });
767
885
  }
768
886
 
887
+ /**
888
+ * A kernel-lowerable point precondition. Unlike `.where(...)`, this callback builds a small typed
889
+ * algebraic value; it is not tenant code executed at admission. The resulting aggregate lookup,
890
+ * predicate tree, and refusal code are preserved as typed USDA and evaluated directly by Rust.
891
+ *
892
+ * .precondition(Site, ({ row, intent, all }) => ({
893
+ * id: intent.field("siteId"),
894
+ * holds: all(row.exists(), row.field("status").ne("deleted")),
895
+ * }), "site-not-found-or-deleted")
896
+ */
897
+ precondition<F extends Record<string, Field>>(
898
+ aggregate: AggregateHandle<string, F>,
899
+ build: (ctx: PointGuardContext<P, F>) => PointGuardDeclaration,
900
+ rejectCode: string,
901
+ ): DirectivePlanStep<Id, P> {
902
+ const pointGuard = buildPointGuard(aggregate, build, rejectCode);
903
+ return this.with({
904
+ aggregateId: aggregate.id,
905
+ fieldNames: Object.keys(aggregate.fields).sort(),
906
+ idField: (payload) => {
907
+ let current: unknown = payload;
908
+ for (const part of pointGuard.idPayloadPath) {
909
+ if (current === null || typeof current !== "object") return "";
910
+ current = (current as Record<string, unknown>)[part];
911
+ }
912
+ return typeof current === "string" ? current : "";
913
+ },
914
+ idPayloadPath: pointGuard.idPayloadPath,
915
+ readId: {
916
+ source: "payload",
917
+ path: pointGuard.idPayloadPath,
918
+ prefix: "",
919
+ suffix: "",
920
+ optional: false,
921
+ },
922
+ kind: "predicate",
923
+ predicate: (row) => evaluatePointGuard(pointGuard.predicate, row, undefined),
924
+ rejectCode,
925
+ pointGuard,
926
+ });
927
+ }
928
+
769
929
  /**
770
930
  * Acknowledge that the MOST RECENT write-path precondition may intentionally reveal one fact about
771
931
  * private state through its typed refusal. This is business policy, so it stays beside the condition
@@ -808,6 +968,7 @@ class DirectivePlanStep<Id extends string, P> {
808
968
  id: this.id,
809
969
  marker: this.marker,
810
970
  aggregateId: this.aggregate.id,
971
+ aggregate: this.aggregate,
811
972
  payloadSchema: this.payloadSchema,
812
973
  plan: fn,
813
974
  declaredReads: [],
@@ -859,20 +1020,32 @@ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
859
1020
  const alsoWrites = (
860
1021
  aggregate: AggregateHandle,
861
1022
  marker: ReferentialMarker,
862
- many = false,
1023
+ cardinality: "one" | "many" | "optional" = "one",
863
1024
  ): RequirableDirective<P> => {
864
1025
  // Dedup by (marker, aggregateId) + the primary target — declaring the same output twice is a no-op, and
865
1026
  // re-declaring the primary aggregate is ignored (it is already the marker target). Sorted at emit time.
866
1027
  const next: SecondaryOutput = {
867
1028
  marker,
868
1029
  aggregateId: aggregate.id,
869
- ...(many ? { many: true as const } : {}),
1030
+ aggregate,
1031
+ ...(cardinality === "many" ? { many: true as const } : {}),
1032
+ ...(cardinality === "optional" ? { optional: true as const } : {}),
870
1033
  };
871
- const isPrimary = aggregate.id === base.aggregateId;
1034
+ // Only the exact primary EFFECT is already declared. The same aggregate under a
1035
+ // different marker is a distinct lawful consequence: a `.mutates(A)` directive
1036
+ // may deliberately emit an Archive for A and must retain `.alsoArchives(A)` in
1037
+ // its typed effect signature.
1038
+ const isPrimary = aggregate.id === base.aggregateId && marker === base.marker;
872
1039
  const existing = (base.secondaryOutputs ?? []).find(
873
1040
  (s) => s.marker === marker && s.aggregateId === aggregate.id,
874
1041
  );
875
- if (isPrimary || (existing !== undefined && (!many || existing.many === true))) {
1042
+ if (
1043
+ isPrimary ||
1044
+ (existing !== undefined &&
1045
+ (cardinality === "one" ||
1046
+ (cardinality === "many" && existing.many === true) ||
1047
+ (cardinality === "optional" && existing.optional === true)))
1048
+ ) {
876
1049
  return makeRequirable(base);
877
1050
  }
878
1051
  if (existing !== undefined) {
@@ -887,6 +1060,8 @@ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
887
1060
  };
888
1061
  return {
889
1062
  ...base,
1063
+ affectsMany: () => makeRequirable({ ...base, outputCardinality: "many" }),
1064
+ affectsOptional: () => makeRequirable({ ...base, outputCardinality: "optional" }),
890
1065
  example(payload: P): RequirableDirective<P> {
891
1066
  if (isRuntimeLaw()) {
892
1067
  // The Node authoring pass already proved this example. Executable law retains it as proof
@@ -991,15 +1166,29 @@ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
991
1166
  },
992
1167
  alsoWrites,
993
1168
  alsoWritesMany: (aggregate: AggregateHandle, marker: ReferentialMarker) =>
994
- alsoWrites(aggregate, marker, true),
1169
+ alsoWrites(aggregate, marker, "many"),
1170
+ alsoWritesOptional: (aggregate: AggregateHandle, marker: ReferentialMarker) =>
1171
+ alsoWrites(aggregate, marker, "optional"),
995
1172
  alsoEnsures: (aggregate: AggregateHandle) => alsoWrites(aggregate, "ensures"),
996
1173
  alsoCreates: (aggregate: AggregateHandle) => alsoWrites(aggregate, "creates"),
997
1174
  alsoEnsuresMany: (aggregate: AggregateHandle) =>
998
- alsoWrites(aggregate, "ensures", true),
1175
+ alsoWrites(aggregate, "ensures", "many"),
999
1176
  alsoCreatesMany: (aggregate: AggregateHandle) =>
1000
- alsoWrites(aggregate, "creates", true),
1177
+ alsoWrites(aggregate, "creates", "many"),
1178
+ alsoEnsuresOptional: (aggregate: AggregateHandle) =>
1179
+ alsoWrites(aggregate, "ensures", "optional"),
1180
+ alsoCreatesOptional: (aggregate: AggregateHandle) =>
1181
+ alsoWrites(aggregate, "creates", "optional"),
1001
1182
  alsoMutates: (aggregate: AggregateHandle) => alsoWrites(aggregate, "mutates"),
1002
1183
  alsoArchives: (aggregate: AggregateHandle) => alsoWrites(aggregate, "archives"),
1184
+ alsoMutatesMany: (aggregate: AggregateHandle) =>
1185
+ alsoWrites(aggregate, "mutates", "many"),
1186
+ alsoArchivesMany: (aggregate: AggregateHandle) =>
1187
+ alsoWrites(aggregate, "archives", "many"),
1188
+ alsoMutatesOptional: (aggregate: AggregateHandle) =>
1189
+ alsoWrites(aggregate, "mutates", "optional"),
1190
+ alsoArchivesOptional: (aggregate: AggregateHandle) =>
1191
+ alsoWrites(aggregate, "archives", "optional"),
1003
1192
  requires(
1004
1193
  relation: string,
1005
1194
  opts?: { object?: string; objectFrom?: { recordType: string; idField: string } },
@@ -1158,7 +1347,11 @@ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
1158
1347
  const byKey = new Map<string, ForeignReadEdge>(prior.map((e) => [canon(e), e]));
1159
1348
  byKey.set(canon(entry), entry);
1160
1349
  const merged = [...byKey.values()].sort((a, b) => (canon(a) < canon(b) ? -1 : canon(a) > canon(b) ? 1 : 0));
1161
- return makeRequirable({ ...base, declaredForeignReads: merged });
1350
+ // Every admitted foreign read advances the reader-local source frontier in-band.
1351
+ // This is a framework consequence of `.readsFrom`, so make it part of the typed
1352
+ // effect signature here; tenant authors must not know the bookkeeping aggregate.
1353
+ return makeRequirable({ ...base, declaredForeignReads: merged })
1354
+ .alsoEnsures(NomosSourceFrontier);
1162
1355
  },
1163
1356
  emits(eventType: string, opts?: { max?: number }): RequirableDirective<P> {
1164
1357
  // Accumulate one event type onto any prior emits; a `max` bound is recorded only