@githolon/dsl 0.101.6 → 0.102.1

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/contract.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  // aggregate's domainHash moves only when its OWN fields change. Contracts ride the IR as ADDITIVE metadata
8
8
  // (like value objects — era-safe, omit-when-empty) and are stable-id tracked for evolution.
9
9
  //
10
- // const CatalogueListingContract = contract("CatalogueListing", { listingId: t.string(), name: t.string() });
10
+ // const CatalogueListingContract = recordContract("CatalogueListing", { listingId: t.string(), name: t.string() });
11
11
  // const CatalogueListing = aggregate("CatalogueListing", { listingId: t.string().merge(Lww), … })
12
12
  // .implements(CatalogueListingContract); // ← compile-checked: the aggregate SATISFIES the shape
13
13
  // const EstateAsset = aggregate("EstateAsset", {
@@ -39,21 +39,27 @@ export function isContractHandle(x: unknown): x is ContractHandle {
39
39
  return typeof x === "object" && x !== null && (x as { __isContract?: unknown }).__isContract === true;
40
40
  }
41
41
 
42
+ /** The generated public type name for a contract. Contract ids are already public business names;
43
+ * preserve an explicit `...Contract` suffix rather than producing `...ContractContract`. */
44
+ export function contractTypeName(id: string): string {
45
+ return id.endsWith("Contract") ? id : `${id}Contract`;
46
+ }
47
+
42
48
  /**
43
49
  * Declare a domain CONTRACT — a named public shape. Reuse the `t.*` field builders for the fields (the merge
44
50
  * driver is irrelevant — a contract is a shape). Export it; a concrete aggregate `.implements(...)` it, and
45
51
  * other domains reference it via `t.ref` / `t.foreignRef`. Purely additive: a package with no contract emits
46
52
  * byte-identically to before.
47
53
  */
48
- export function contract<const Id extends string, const F extends Record<string, ContractField>>(
54
+ export function recordContract<const Id extends string, const F extends Record<string, ContractField>>(
49
55
  id: Id,
50
56
  fields: F,
51
57
  ): ContractHandle<Id, F> {
52
58
  if (typeof id !== "string" || id.length === 0) {
53
- throw new Error("contract(id, fields): the id must be a non-empty string (the stable public name).");
59
+ throw new Error("recordContract(id, fields): the id must be a non-empty string (the stable public name).");
54
60
  }
55
61
  if (typeof fields !== "object" || fields === null || Object.keys(fields).length === 0) {
56
- throw new Error(`contract('${id}'): declare at least one field (a contract is a public shape).`);
62
+ throw new Error(`recordContract('${id}'): declare at least one field (a record contract is a public shape).`);
57
63
  }
58
64
  return { id, fields, __isContract: true };
59
65
  }
@@ -27,7 +27,7 @@ export interface BarrelExportedFile {
27
27
  readonly src: string;
28
28
  }
29
29
 
30
- /** Declared PUBLIC top-level type/typedef names in a Dart source (class/enum/mixin/extension type/typedef).
30
+ /** Declared PUBLIC top-level type/typedef/named-extension names in a Dart source.
31
31
  * Private (`_`-prefixed) names never leak through an `export`, so they cannot collide — excluded. */
32
32
  export function publicDartDeclarations(src: string): Set<string> {
33
33
  const names = new Set<string>();
@@ -39,6 +39,10 @@ export function publicDartDeclarations(src: string): Set<string> {
39
39
  for (let m = re.exec(src); m !== null; m = re.exec(src)) {
40
40
  if (!m[1]!.startsWith("_")) names.add(m[1]!);
41
41
  }
42
+ const extension = /^extension\s+([A-Za-z][A-Za-z0-9_]*)\s+(?:<[^\n>]+>\s+)?on\s+/gm;
43
+ for (let m = extension.exec(src); m !== null; m = extension.exec(src)) {
44
+ if (!m[1]!.startsWith("_")) names.add(m[1]!);
45
+ }
42
46
  return names;
43
47
  }
44
48
 
@@ -145,7 +145,7 @@ const publicAggregates = (mod: DomainModule): AggregateHandle[] =>
145
145
  );
146
146
 
147
147
  const publicDirectives = (mod: DomainModule): Directive<unknown>[] =>
148
- (mod.directives as Directive<unknown>[]).filter((directive) =>
148
+ ((mod.applicationIntents ?? mod.directives) as Directive<unknown>[]).filter((directive) =>
149
149
  directive.framework !== true
150
150
  && (directive as { businessBirthInternal?: unknown }).businessBirthInternal !== true
151
151
  );
@@ -181,7 +181,11 @@ const aggregateItem = (domain: string, aggregate: AggregateHandle): NomosDescrib
181
181
  };
182
182
  };
183
183
 
184
- const actionItem = (domain: string, directive: Directive<unknown>): NomosDescribeItemModel => {
184
+ const actionItem = (
185
+ domain: string,
186
+ directive: Directive<unknown>,
187
+ publicAggregateIds: ReadonlySet<string>,
188
+ ): NomosDescribeItemModel => {
185
189
  const businessBirth = businessBirthOf(directive);
186
190
  const actionId = businessBirth === undefined ? directive.id : "birth";
187
191
  const actionLabel = businessBirth === undefined
@@ -190,7 +194,7 @@ const actionItem = (domain: string, directive: Directive<unknown>): NomosDescrib
190
194
  const outputs = [
191
195
  { effect: directive.marker, type: directive.aggregateId },
192
196
  ...(directive.secondaryOutputs ?? []).map((output) => ({ effect: output.marker, type: output.aggregateId })),
193
- ];
197
+ ].filter((output) => publicAggregateIds.has(output.type));
194
198
  const effectVerb = (marker: string): string => ({
195
199
  creates: "create",
196
200
  mutates: "change",
@@ -207,7 +211,7 @@ const actionItem = (domain: string, directive: Directive<unknown>): NomosDescrib
207
211
  id: `${domain}.${actionId}`,
208
212
  label: actionLabel,
209
213
  summary: businessBirth === undefined
210
- ? directive.description ?? `${actionLabel} will ${effect}.`
214
+ ? directive.description ?? (effect === "" ? `${actionLabel} changes the business.` : `${actionLabel} will ${effect}.`)
211
215
  : `${actionLabel} will ${effect}.`,
212
216
  details: {
213
217
  domain,
@@ -407,7 +411,11 @@ export function buildNomosDescribeModel(
407
411
  });
408
412
  const questions = modules.flatMap(questionItems);
409
413
  const nouns = aggregates.map(({ domain, aggregate }) => aggregateItem(domain, aggregate));
410
- const verbs = actions.map(({ domain, directive }) => actionItem(domain, directive));
414
+ const publicByDomain = new Map(modules.map((mod) => [
415
+ mod.domain ?? mod.name,
416
+ new Set(publicAggregates(mod).map((aggregate) => aggregate.id)),
417
+ ]));
418
+ const verbs = actions.map(({ domain, directive }) => actionItem(domain, directive, publicByDomain.get(domain) ?? new Set()));
411
419
  const rules = modules.flatMap(ruleItems);
412
420
  const domainCount = new Set(modules.map((mod) => mod.domain ?? mod.name)).size;
413
421
 
package/src/directive.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import type { z } from "zod";
19
19
  import type { AggregateHandle, ReadModelRow } from "./aggregate.js";
20
+ import { isRuntimeLaw } from "./authoring_context.js";
20
21
  import type { Field } from "./fields.js";
21
22
  import type { PlannedOp } from "./ops.js";
22
23
  import type { Ports } from "./ctx.js";
@@ -24,6 +25,7 @@ import type { RelationDecl } from "./relation.js";
24
25
  import type { QueryDecl } from "./query.js";
25
26
  import { normalizeActivity, type ActivityInput, type ActivityDescriptor } from "./activity.js";
26
27
  import { BIRTH_METER_QUERIES } from "./birth_meter.js";
28
+ import { zodDef, zodKind, zodObjectShape } from "./zod_introspection.js";
27
29
  import {
28
30
  isFromPayloadRef,
29
31
  isTrustPolicyDecl,
@@ -56,6 +58,8 @@ export type ReferentialMarker = "creates" | "mutates" | "ensures" | "archives";
56
58
  export interface PreconditionDescriptor {
57
59
  /** The guarded aggregate's TYPE id (the ref type the synthesized invariant resolves). */
58
60
  readonly aggregateId: string;
61
+ /** Stored business fields used to distinguish an unresolved metadata shell from a real row. */
62
+ readonly fieldNames: readonly string[];
59
63
  /** Pure payload → target instance id (no clock, no IO — re-derived identically on every peer). */
60
64
  readonly idField: (payload: unknown) => string;
61
65
  /** The fixed-predicate specialization, or `predicate` for the general `.where` form. */
@@ -86,6 +90,43 @@ export interface PreconditionDescriptor {
86
90
  export interface SecondaryOutput {
87
91
  readonly marker: ReferentialMarker;
88
92
  readonly aggregateId: string;
93
+ /** This action may affect more than one record of this aggregate type. */
94
+ readonly many?: true;
95
+ }
96
+
97
+ /**
98
+ * Frontend-only application shaping for a foreign reference carried by a
99
+ * directive payload. Tenant code supplies the referenced business record;
100
+ * generated client state privately supplies its workspace locator and wire id.
101
+ * This does not change the law or its replayed payload shape.
102
+ */
103
+ export interface BusinessRecordInput {
104
+ /** Payload id field, which must also be a foreignRef field on the target aggregate. */
105
+ readonly field: string;
106
+ /** Optional business argument name; defaults to the id field without its `Id` suffix. */
107
+ readonly as?: string;
108
+ }
109
+
110
+ /**
111
+ * Application-only composition for a business action whose law performs a
112
+ * dynamic attested read. The generated stateful client first obtains the
113
+ * source's signed answer, privately seats its identity through an internal
114
+ * directive, then submits the business action carrying that attestation.
115
+ */
116
+ export interface BusinessSourceTrust {
117
+ /** Hidden law directive that records the accepted source identity. */
118
+ readonly viaDirectiveId: string;
119
+ /** Payload field routing the dynamic source read. */
120
+ readonly fromPayload: string;
121
+ /** Source query to attest. */
122
+ readonly queryId: string;
123
+ /** Source-query argument -> business payload field. */
124
+ readonly queryArgs: Readonly<Record<string, string>>;
125
+ /** Hidden trust payload field -> business payload field. */
126
+ readonly copy: Readonly<Record<string, string>>;
127
+ /** Hidden trust payload fields populated from the signed envelope. */
128
+ readonly genesisField: string;
129
+ readonly keyHashesField: string;
89
130
  }
90
131
 
91
132
  export interface Directive<P = unknown> {
@@ -101,7 +142,26 @@ export interface Directive<P = unknown> {
101
142
  */
102
143
  readonly secondaryOutputs?: readonly SecondaryOutput[];
103
144
  readonly payloadSchema: z.ZodType<P>;
145
+ /**
146
+ * One lawful sample of this directive's complete payload. Proof generation
147
+ * and author-time checks prefer it when constraints span several fields and
148
+ * therefore cannot be inferred from the individual Zod leaves. Validated at
149
+ * declaration time; non-law tooling metadata, omitted when undeclared.
150
+ */
151
+ readonly payloadExample?: Readonly<P>;
104
152
  readonly plan: (payload: P, ctx: Ports) => PlannedOp[];
153
+ /** Application-only typed-record inputs. Omitted from canonical law output. */
154
+ readonly businessRecordInputs?: readonly BusinessRecordInput[];
155
+ /** Application-only source-trust composition. Omitted from canonical law. */
156
+ readonly businessSourceTrust?: BusinessSourceTrust;
157
+ /** Application-only identity binding. The generated application removes this
158
+ * field from tenant input and fills it from its verified signed-in principal. */
159
+ readonly businessPrincipalField?: string;
160
+ /**
161
+ * Signed framework or lowering law retained by Nomos but omitted from tenant
162
+ * application APIs. Set only through `nomosImplementationDetail(...)`.
163
+ */
164
+ readonly businessBirthInternal?: true;
105
165
  /**
106
166
  * FRAMEWORK-SHARED brand (Dart-codegen hygiene). Set (by `framework/framework_aggregates.ts`) ONLY on
107
167
  * the framework-owned directive singletons declared in `dsl/src/framework/` (the relate/signer/enroll
@@ -353,6 +413,38 @@ export interface Directive<P = unknown> {
353
413
  * each `plan` — the kernel resolves it WITHIN the one shared `admit` gate.
354
414
  */
355
415
  export interface RequirableDirective<P> extends Directive<P> {
416
+ /**
417
+ * Supply one lawful payload for proof/check synthesis when the schema has
418
+ * cross-field refinements. The value is validated immediately against this
419
+ * directive's payload schema and is never sent unless a generated proof uses
420
+ * it. Returns a new immutable directive.
421
+ */
422
+ example(payload: P): RequirableDirective<P>;
423
+ /** Bind one payload field to the generated application's verified principal.
424
+ * The field remains signed law and may be checked by invariants, but disappears
425
+ * from tenant payload types so applications cannot claim to be another person. */
426
+ fromCurrentPrincipal(field: string): RequirableDirective<P>;
427
+ /**
428
+ * Replace a payload foreign-reference locator with the referenced business
429
+ * record on generated application surfaces. The target aggregate's
430
+ * `foreignRef({ workspace: ... })` declaration supplies the paired workspace
431
+ * field; generated client state supplies its value privately.
432
+ */
433
+ fromRecord(field: string, opts?: { as?: string }): RequirableDirective<P>;
434
+ /**
435
+ * Keep dynamic-source identity, key hashing and attestation carriage inside
436
+ * the generated application action. [via] remains signed law but must be
437
+ * wrapped in `nomosImplementationDetail(...)` at its export site.
438
+ */
439
+ trustsSource(opts: {
440
+ via: Directive<any>;
441
+ fromPayload: string;
442
+ query: { id: string };
443
+ queryArgs: Readonly<Record<string, string>>;
444
+ copy?: Readonly<Record<string, string>>;
445
+ genesisField?: string;
446
+ keyHashesField?: string;
447
+ }): RequirableDirective<P>;
356
448
  /**
357
449
  * MULTI-OUTPUT: declare an ADDITIONAL aggregate this directive writes (beyond its primary marker target),
358
450
  * so generated docs/proof/client metadata name every output ("writes A + B") instead of one by convention.
@@ -361,8 +453,11 @@ export interface RequirableDirective<P> extends Directive<P> {
361
453
  * kernel folds by the plan's ops regardless — this is descriptive law metadata, not a write-restriction.
362
454
  */
363
455
  alsoWrites(aggregate: AggregateHandle, marker: ReferentialMarker): RequirableDirective<P>;
456
+ alsoWritesMany(aggregate: AggregateHandle, marker: ReferentialMarker): RequirableDirective<P>;
364
457
  alsoEnsures(aggregate: AggregateHandle): RequirableDirective<P>;
365
458
  alsoCreates(aggregate: AggregateHandle): RequirableDirective<P>;
459
+ alsoEnsuresMany(aggregate: AggregateHandle): RequirableDirective<P>;
460
+ alsoCreatesMany(aggregate: AggregateHandle): RequirableDirective<P>;
366
461
  alsoMutates(aggregate: AggregateHandle): RequirableDirective<P>;
367
462
  alsoArchives(aggregate: AggregateHandle): RequirableDirective<P>;
368
463
  /**
@@ -618,6 +713,7 @@ class DirectivePlanStep<Id extends string, P> {
618
713
  return this.with(
619
714
  {
620
715
  aggregateId: this.aggregate.id,
716
+ fieldNames: Object.keys(this.aggregate.fields).sort(),
621
717
  idField,
622
718
  kind: "absent",
623
719
  predicate: (row) => row === undefined,
@@ -639,6 +735,7 @@ class DirectivePlanStep<Id extends string, P> {
639
735
  ): DirectivePlanStep<Id, P> {
640
736
  return this.with({
641
737
  aggregateId: aggregate.id,
738
+ fieldNames: Object.keys(aggregate.fields).sort(),
642
739
  idField: idFn as (payload: unknown) => string,
643
740
  kind: "exists",
644
741
  predicate: (row) => row !== undefined,
@@ -661,6 +758,7 @@ class DirectivePlanStep<Id extends string, P> {
661
758
  ): DirectivePlanStep<Id, P> {
662
759
  return this.with({
663
760
  aggregateId: aggregate.id,
761
+ fieldNames: Object.keys(aggregate.fields).sort(),
664
762
  idField: idFn as (payload: unknown) => string,
665
763
  kind: "predicate",
666
764
  predicate: predicate as (row: Record<string, unknown> | undefined) => boolean,
@@ -758,20 +856,148 @@ export function recordObject(
758
856
  * declaration site reads `directive(id)…plan(fn).requires("cap", p => […])`.
759
857
  */
760
858
  function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
761
- const alsoWrites = (aggregate: AggregateHandle, marker: ReferentialMarker): RequirableDirective<P> => {
859
+ const alsoWrites = (
860
+ aggregate: AggregateHandle,
861
+ marker: ReferentialMarker,
862
+ many = false,
863
+ ): RequirableDirective<P> => {
762
864
  // Dedup by (marker, aggregateId) + the primary target — declaring the same output twice is a no-op, and
763
865
  // re-declaring the primary aggregate is ignored (it is already the marker target). Sorted at emit time.
764
- const next: SecondaryOutput = { marker, aggregateId: aggregate.id };
866
+ const next: SecondaryOutput = {
867
+ marker,
868
+ aggregateId: aggregate.id,
869
+ ...(many ? { many: true as const } : {}),
870
+ };
765
871
  const isPrimary = aggregate.id === base.aggregateId;
766
- const already = (base.secondaryOutputs ?? []).some((s) => s.marker === marker && s.aggregateId === aggregate.id);
767
- if (isPrimary || already) return makeRequirable(base);
872
+ const existing = (base.secondaryOutputs ?? []).find(
873
+ (s) => s.marker === marker && s.aggregateId === aggregate.id,
874
+ );
875
+ if (isPrimary || (existing !== undefined && (!many || existing.many === true))) {
876
+ return makeRequirable(base);
877
+ }
878
+ if (existing !== undefined) {
879
+ return makeRequirable({
880
+ ...base,
881
+ secondaryOutputs: (base.secondaryOutputs ?? []).map((output) =>
882
+ output === existing ? next : output,
883
+ ),
884
+ });
885
+ }
768
886
  return makeRequirable({ ...base, secondaryOutputs: [...(base.secondaryOutputs ?? []), next] });
769
887
  };
770
888
  return {
771
889
  ...base,
890
+ example(payload: P): RequirableDirective<P> {
891
+ if (isRuntimeLaw()) {
892
+ // The Node authoring pass already proved this example. Executable law retains it as proof
893
+ // metadata but does not rerun host-shaped Zod checks when a lazy domain is first forced.
894
+ return makeRequirable({ ...base, payloadExample: payload });
895
+ }
896
+ const parsed = base.payloadSchema.safeParse(payload);
897
+ if (!parsed.success) throw new Error(
898
+ `directive('${base.id}').example(...) must satisfy its payload schema: ${parsed.error.message}`,
899
+ );
900
+ return makeRequirable({ ...base, payloadExample: parsed.data });
901
+ },
902
+ fromCurrentPrincipal(field: string): RequirableDirective<P> {
903
+ const name = field.trim();
904
+ if (name.length === 0) {
905
+ throw new Error("directive.fromCurrentPrincipal(field): field must be a non-empty payload field name");
906
+ }
907
+ if (zodKind(base.payloadSchema) !== "object") {
908
+ throw new Error(
909
+ `directive('${base.id}').fromCurrentPrincipal('${name}') needs an object payload schema`,
910
+ );
911
+ }
912
+ const principalSchema = zodObjectShape(base.payloadSchema)[name];
913
+ if (principalSchema === undefined) {
914
+ throw new Error(
915
+ `directive('${base.id}').fromCurrentPrincipal('${name}') names no payload field`,
916
+ );
917
+ }
918
+ let principalValue = principalSchema;
919
+ while (zodKind(principalValue) === "optional" || zodKind(principalValue) === "default") {
920
+ principalValue = zodDef(principalValue).innerType!;
921
+ }
922
+ if (zodKind(principalValue) !== "string") {
923
+ throw new Error(
924
+ `directive('${base.id}').fromCurrentPrincipal('${name}') must bind a string payload field`,
925
+ );
926
+ }
927
+ if (base.businessPrincipalField !== undefined && base.businessPrincipalField !== name) {
928
+ throw new Error(
929
+ `directive('${base.id}').fromCurrentPrincipal(...) may bind only one identity field`,
930
+ );
931
+ }
932
+ return makeRequirable({ ...base, businessPrincipalField: name });
933
+ },
934
+ fromRecord(field: string, opts?: { as?: string }): RequirableDirective<P> {
935
+ if (typeof field !== "string" || field.length === 0) {
936
+ throw new Error("directive.fromRecord(field): field must be a non-empty payload field name");
937
+ }
938
+ const prior = base.businessRecordInputs ?? [];
939
+ const next: BusinessRecordInput = {
940
+ field,
941
+ ...(opts?.as === undefined ? {} : { as: opts.as }),
942
+ };
943
+ return makeRequirable({
944
+ ...base,
945
+ businessRecordInputs: [...prior.filter((candidate) => candidate.field !== field), next],
946
+ });
947
+ },
948
+ trustsSource(opts): RequirableDirective<P> {
949
+ if (base.businessSourceTrust !== undefined) {
950
+ throw new Error(
951
+ `directive('${base.id}').trustsSource(...) may be declared only once`,
952
+ );
953
+ }
954
+ const fromPayload = opts.fromPayload.trim();
955
+ if (fromPayload.length === 0 || opts.query.id.trim().length === 0) {
956
+ throw new Error(
957
+ `directive('${base.id}').trustsSource(...) needs a source payload field and query`,
958
+ );
959
+ }
960
+ const matchingRead = (base.declaredForeignReads ?? []).some(
961
+ (edge) =>
962
+ !("from" in edge) &&
963
+ edge.fromPayload === fromPayload &&
964
+ edge.queryId === opts.query.id,
965
+ );
966
+ if (!matchingRead) {
967
+ throw new Error(
968
+ `directive('${base.id}').trustsSource(...) must follow its matching dynamic .readsFrom(...) declaration`,
969
+ );
970
+ }
971
+ const via = opts.via as Directive<unknown> & {
972
+ businessBirthInternal?: unknown;
973
+ };
974
+ if (via.businessBirthInternal !== true) {
975
+ throw new Error(
976
+ `directive('${base.id}').trustsSource(...): the trust directive must be wrapped in nomosImplementationDetail(...) so tenant code cannot call it`,
977
+ );
978
+ }
979
+ return makeRequirable({
980
+ ...base,
981
+ businessSourceTrust: {
982
+ viaDirectiveId: via.id,
983
+ fromPayload,
984
+ queryId: opts.query.id,
985
+ queryArgs: { ...opts.queryArgs },
986
+ copy: { ...(opts.copy ?? {}) },
987
+ genesisField: opts.genesisField ?? "sourceGenesis",
988
+ keyHashesField: opts.keyHashesField ?? "sourceKeyHashes",
989
+ },
990
+ });
991
+ },
772
992
  alsoWrites,
993
+ alsoWritesMany: (aggregate: AggregateHandle, marker: ReferentialMarker) =>
994
+ alsoWrites(aggregate, marker, true),
773
995
  alsoEnsures: (aggregate: AggregateHandle) => alsoWrites(aggregate, "ensures"),
774
996
  alsoCreates: (aggregate: AggregateHandle) => alsoWrites(aggregate, "creates"),
997
+ alsoEnsuresMany: (aggregate: AggregateHandle) =>
998
+ alsoWrites(aggregate, "ensures", true),
999
+ alsoCreatesMany: (aggregate: AggregateHandle) =>
1000
+ alsoWrites(aggregate, "creates", true),
775
1001
  alsoMutates: (aggregate: AggregateHandle) => alsoWrites(aggregate, "mutates"),
776
1002
  alsoArchives: (aggregate: AggregateHandle) => alsoWrites(aggregate, "archives"),
777
1003
  requires(
@@ -925,7 +1151,9 @@ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
925
1151
  entry = { queryId: query.id, from: source };
926
1152
  }
927
1153
  const canon = (e: ForeignReadEdge): string =>
928
- "from" in e ? `0${e.from}${e.queryId}` : `1${e.policy}${e.fromPayload}${e.queryId}`;
1154
+ "from" in e
1155
+ ? `0\\0${e.from}\\0${e.queryId}`
1156
+ : `1\\0${e.policy}\\0${e.fromPayload}\\0${e.queryId}`;
929
1157
  const prior = base.declaredForeignReads ?? [];
930
1158
  const byKey = new Map<string, ForeignReadEdge>(prior.map((e) => [canon(e), e]));
931
1159
  byKey.set(canon(entry), entry);
@@ -1009,3 +1237,14 @@ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
1009
1237
  export function directive<const Id extends string>(id: Id): DirectiveMarkerStep<Id> {
1010
1238
  return new DirectiveMarkerStep(id);
1011
1239
  }
1240
+
1241
+ /**
1242
+ * Keep a signed-law declaration available to Nomos while omitting it from every
1243
+ * generated tenant API. This is deliberately namespaced and explicit: it is for
1244
+ * framework lowering and temporary migration law, never a second application lane.
1245
+ */
1246
+ export function nomosImplementationDetail<T extends object>(
1247
+ declaration: T,
1248
+ ): T & { readonly businessBirthInternal: true } {
1249
+ return { ...declaration, businessBirthInternal: true };
1250
+ }
@@ -57,7 +57,7 @@ import { RelationTuple, __setDispatchContext } from "./framework/relations.js";
57
57
  import { BIRTH_METER_AGGREGATES } from "./birth_meter.js";
58
58
  import { AuthProvider as FrameworkAuthProvider, setAuthProvider as frameworkSetAuthProvider } from "./framework/auth_provider_law.js";
59
59
  import { namespacedAggregateIds, qualifyReadProjectionType, READ_PROJECTION_DELIMITER } from "./read_projection_namespace.js";
60
- import { isRelationTypeDecl } from "./relation_schema.js";
60
+ import { discoverRelationTypeDeclarations } from "./relation_schema.js";
61
61
  import { synthesizePreconditionInvariants } from "./precondition.js";
62
62
  import { synthesizeBirthQuotaInvariants } from "./birth_meter.js";
63
63
  import { withBirthMeterExports } from "./birth_meter_ensure.js";
@@ -80,9 +80,16 @@ import type { CombinedDecl } from "./combined.js";
80
80
  import type { InvariantBody, InvariantEvidence, InvariantVerdict } from "./relation.js";
81
81
  import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
82
82
  import type { QueryRow } from "./report.js";
83
- import { expandBusinessBirthExports } from "./business_birth.js";
83
+ import { expandBusinessBirthExports, isBusinessBirthDirective } from "./business_birth.js";
84
84
  import { withOwnedCustodyExports } from "./framework/owned_custody.js";
85
85
  import { withBusinessCorrectionExports } from "./framework/business_corrections.js";
86
+ import { withClearDirectives } from "./clear_directive.js";
87
+ import {
88
+ applicationLanguageOf,
89
+ assertApplicationLanguageCoversAuthoredDirectives,
90
+ executableDirectivesForLanguage,
91
+ remapWorkspaceInvariantsForLanguage,
92
+ } from "./intent.js";
86
93
 
87
94
  /** One bundled domain module: a bag of named exports the entry scans by SHAPE. */
88
95
  export type DomainModuleExports = Record<string, unknown>;
@@ -250,6 +257,14 @@ function combinedsOf(mod: DomainModuleExports): CombinedDecl[] {
250
257
  function mergeModules(mods: readonly DomainModuleExports[], domain: string): DomainModuleExports {
251
258
  let merged: DomainModuleExports = {};
252
259
  for (const m of mods) merged = { ...merged, ...m };
260
+ const applicationLanguage = applicationLanguageOf([merged], domain);
261
+ if (applicationLanguage !== undefined) {
262
+ assertApplicationLanguageCoversAuthoredDirectives(
263
+ applicationLanguage,
264
+ [...directivesOf(merged).values()],
265
+ domain,
266
+ );
267
+ }
253
268
  merged = expandBusinessBirthExports(merged, domain);
254
269
  merged = withOwnedCustodyExports(merged);
255
270
  merged = withBusinessCorrectionExports(merged);
@@ -261,7 +276,11 @@ function mergeModules(mods: readonly DomainModuleExports[], domain: string): Dom
261
276
  // the registry + `describe()` see the exact surface the composed manifest carries.
262
277
  // ROLE SUGAR: lift every `role(...).emit()` into its grant/revoke/seed directives at the SAME
263
278
  // engine merge point so the registry dispatches them (the compiler saw them via the same expander).
264
- return expandRoleExports(withAttestedReadExports(withBirthMeterExports(expandCapabilityExports(merged)))) as DomainModuleExports;
279
+ return expandRoleExports(
280
+ withClearDirectives(
281
+ withAttestedReadExports(withBirthMeterExports(expandCapabilityExports(merged))),
282
+ ),
283
+ ) as DomainModuleExports;
265
284
  }
266
285
 
267
286
  /**
@@ -298,11 +317,16 @@ interface DomainSlice {
298
317
  }
299
318
 
300
319
  /** Build one domain's slice from its merged module exports (the original scans). */
301
- function buildDomainSlice(mod: DomainModuleExports): DomainSlice {
320
+ function buildDomainSlice(mod: DomainModuleExports, domain: string): DomainSlice {
302
321
  // ── directiveId → {directive, agg} ────────────────────────────────────────────
303
322
  const registry = new Map<string, RegistryEntry>();
304
323
  const aggs = aggregatesOf(mod);
305
- const dirs = directivesOf(mod);
324
+ const applicationLanguage = applicationLanguageOf([mod], domain);
325
+ const discoveredDirectives = [...directivesOf(mod).values()];
326
+ const executableDirectives = applicationLanguage === undefined
327
+ ? discoveredDirectives
328
+ : executableDirectivesForLanguage(applicationLanguage, discoveredDirectives, domain);
329
+ const dirs = new Map(executableDirectives.map((directive) => [directive.id, directive]));
306
330
  // RELATIONS AUTO-ENSURE (mirror of composeDomainModule): a relate-family directive `.ensures(RelationTuple)`
307
331
  // needs the framework aggregate registered for DISPATCH too — without it the directive is silently dropped
308
332
  // below. Inject iff a directive targets it and the dev didn't already export the handle.
@@ -398,6 +422,7 @@ function buildDomainSlice(mod: DomainModuleExports): DomainSlice {
398
422
 
399
423
  // ── workspace-invariant id → declaration — from the SAME exports (#266, by shape) ──
400
424
  const wsInvariants = new Map<string, WorkspaceInvariantDecl>();
425
+ const declaredWorkspaceInvariants: WorkspaceInvariantDecl[] = [];
401
426
  for (const v of Object.values(mod)) {
402
427
  if (
403
428
  v &&
@@ -413,9 +438,14 @@ function buildDomainSlice(mod: DomainModuleExports): DomainSlice {
413
438
  `body — the gate would have nothing to evaluate (docs/workspace_invariant.md).`,
414
439
  );
415
440
  }
416
- wsInvariants.set(w.id, w);
441
+ declaredWorkspaceInvariants.push(w);
417
442
  }
418
443
  }
444
+ for (const w of applicationLanguage === undefined
445
+ ? declaredWorkspaceInvariants
446
+ : remapWorkspaceInvariantsForLanguage(declaredWorkspaceInvariants, applicationLanguage)) {
447
+ wsInvariants.set(w.id, w);
448
+ }
419
449
  // ── DESUGARED write-path preconditions (`.requiresExisting`/`.creates(Agg,idFn)`/`.where`) ──
420
450
  // The synthesized workspace invariants are NOT module exports (they are generated from the directive
421
451
  // markers), so the export-scan above never sees them. Synthesize them here — from the SAME discovered
@@ -438,8 +468,8 @@ function buildDomainSlice(mod: DomainModuleExports): DomainSlice {
438
468
  // objects stay bare; the synthesized read-visibility `workspace` caps are never authored relationTypes,
439
469
  // so they are absent here too (their bare grants stay bare — the read gate hardcodes `workspace:self`).
440
470
  const qualifiableRelations = new Set<string>();
441
- for (const v of Object.values(mod)) {
442
- if (!isRelationTypeDecl(v) || v.global === true) continue;
471
+ for (const v of discoverRelationTypeDeclarations(mod)) {
472
+ if (v.global === true) continue;
443
473
  for (const rel of v.relations.keys()) qualifiableRelations.add(`${v.type}#${rel}`);
444
474
  }
445
475
 
@@ -471,7 +501,7 @@ export function collectEngineRouting(
471
501
  if (!list.includes(domainName)) list.push(domainName);
472
502
  };
473
503
  for (const [domainName, mods] of Object.entries(domains)) {
474
- const slice = buildDomainSlice(mergeModules(mods, domainName));
504
+ const slice = buildDomainSlice(mergeModules(mods, domainName), domainName);
475
505
  // Type-keyed dispatches arrive with the ROW's type — QUALIFIED for a namespaced aggregate (the
476
506
  // runtime `__type` stamp), bare for a global one / a pre-namespacing row. Route BOTH keys so
477
507
  // either era resolves to the declaring domain (the slice lookup strips the qualifier back off).
@@ -557,7 +587,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
557
587
  const sources = config.domains[domainName];
558
588
  if (sources === undefined) return undefined;
559
589
  const forced = sources.map((m) => (typeof m === "function" ? m() : m));
560
- const slice = buildDomainSlice(mergeModules(forced, domainName));
590
+ const slice = buildDomainSlice(mergeModules(forced, domainName), domainName);
561
591
  sliceCache.set(domainName, slice);
562
592
  return slice;
563
593
  }
@@ -933,8 +963,8 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
933
963
 
934
964
  /**
935
965
  * THE SELF-DESCRIBE VERB (G1) — the lump's OWN OpenAPI, derived from the SAME modules it
936
- * dispatches. The kernel runs this over the FOLDED law (the lump IS `nomos:executable:
937
- * javascriptHex` in the package USDA in custody), so the workspace's API surface is a pure,
966
+ * dispatches. The kernel selects the content-addressed describe plan from the native USD
967
+ * package in custody, so the workspace's API surface is a pure,
938
968
  * deterministic function of installed law — NOT a host-staged blob. Its public `paths` reproduce
939
969
  * the `<name>.openapi.json` the compiler wrote; describe also carries a private, law-derived
940
970
  * framework routing manifest which the host consumes internally and never publishes.
@@ -951,7 +981,19 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
951
981
  // Aggregates dedup by wire id + RelationTuple auto-ensure (the compose contract) — openapi
952
982
  // reads aggregates only for the hasMany inverse indexes, so this matches composeDomainModule.
953
983
  const byId = aggregatesOf(merged);
954
- const directives = [...directivesOf(merged).values()];
984
+ const discoveredDirectives = [...directivesOf(merged).values()];
985
+ const applicationLanguage = applicationLanguageOf([merged], domainName);
986
+ const directives = applicationLanguage === undefined
987
+ ? discoveredDirectives
988
+ : executableDirectivesForLanguage(applicationLanguage, discoveredDirectives, domainName);
989
+ const applicationIntents = applicationLanguage === undefined
990
+ ? undefined
991
+ : [
992
+ ...applicationLanguage.intents,
993
+ ...directives.filter((directive) =>
994
+ isBusinessBirthDirective(directive) &&
995
+ !applicationLanguage.intents.some((intent) => intent.id === directive.id)),
996
+ ];
955
997
  if (!byId.has(RelationTuple.id) && directives.some((d) => d.aggregateId === RelationTuple.id)) {
956
998
  byId.set(RelationTuple.id, RelationTuple);
957
999
  }
@@ -961,6 +1003,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
961
1003
  domain: domainName,
962
1004
  aggregates: [...byId.values()],
963
1005
  directives,
1006
+ ...(applicationIntents === undefined ? {} : { applicationIntents }),
964
1007
  ...(decls.queries && decls.queries.length > 0 ? { queries: [...decls.queries] } : {}),
965
1008
  ...(decls.counts && decls.counts.length > 0 ? { counts: [...decls.counts] } : {}),
966
1009
  ...(decls.sums && decls.sums.length > 0 ? { sums: [...decls.sums] } : {}),
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Canonicalise esbuild's non-semantic install-topology noise before the executable law is hashed.
3
+ *
4
+ * A workspace dependency is labelled `../../dsl/src/...`; the same published dependency is labelled
5
+ * `node_modules/@githolon/dsl/src/...`. Esbuild can also leave either one or two newlines after its
6
+ * `"use strict"` prologue. None of those bytes describe business law, so they must not give the same
7
+ * law two identities.
8
+ *
9
+ * Replacements are deliberately restricted to esbuild's generated comment/function labels. Tenant
10
+ * strings and executable imports remain byte-significant.
11
+ */
12
+ export function canonicalizeEngineJavascript(source: string): string {
13
+ const body = source
14
+ .replace(/\r\n?/g, "\n")
15
+ .replace(/^[ \t]*"use strict";[ \t]*(?:\n|$)/gm, "")
16
+ .replace(/^(\s*\/\/ )(?:(?:\.\.\/)+dsl\/src\/|node_modules\/@githolon\/dsl\/src\/)/gm, "$1@githolon/dsl/src/")
17
+ .replace(/"(?:(?:\.\.\/)+dsl\/src\/|node_modules\/@githolon\/dsl\/src\/)([^"]+)"\(\) \{/g, '"@githolon/dsl/src/$1"() {')
18
+ .replace(/^(\s*\/\/ )(?:(?:\.\.\/)+node_modules\/zod\/|node_modules\/zod\/)/gm, "$1zod/")
19
+ .replace(/"(?:(?:\.\.\/)+node_modules\/zod\/|node_modules\/zod\/)([^"]+)"\(\) \{/g, '"zod/$1"() {')
20
+ .replace(/\n[ \t]*\n/g, "\n")
21
+ .trim();
22
+ return `"use strict";\n${body}\n`;
23
+ }
24
+
25
+ /** Remove the same install-topology label from the sealed generated application bundle. */
26
+ export function canonicalizeGeneratedClientJavascript(source: string): string {
27
+ return source.replace(
28
+ /^(\s*\/\/ )(?:(?:\.\.\/)+dsl\/runtime\/|node_modules\/@githolon\/dsl\/runtime\/)/gm,
29
+ "$1@githolon/dsl/runtime/",
30
+ );
31
+ }