@githolon/dsl 0.101.5 → 0.102.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/dart/CHANGELOG.md +13 -0
  2. package/dart/lib/nomos_client.dart +3 -52
  3. package/dart/lib/src/dev_cloud.dart +5 -0
  4. package/dart/lib/src/home.dart +10 -15
  5. package/dart/lib/src/nms_conformance_runtime.dart +12 -0
  6. package/dart/lib/src/nms_generated_runtime.dart +29 -0
  7. package/dart/lib/src/nms_test_environment.dart +8 -0
  8. package/dart/lib/src/node_runner_transport.dart +56 -18
  9. package/dart/lib/src/provider.dart +5 -1
  10. package/dart/lib/src/record_origin.dart +55 -0
  11. package/dart/lib/src/runner/node-entry.mjs +9105 -0
  12. package/dart/lib/src/runner/offline-install.mjs +4729 -0
  13. package/dart/lib/src/session_bound.dart +23 -10
  14. package/dart/lib/src/source_trust.dart +51 -0
  15. package/dart/lib/src/test_harness.dart +490 -58
  16. package/dart/lib/src/webview_bridge.dart +167 -58
  17. package/dart/lib/src/workspace_name.dart +7 -1
  18. package/dart/pubspec.lock +3 -3
  19. package/dart/pubspec.yaml +4 -2
  20. package/dart/test/acquire_session_credentials_test.dart +34 -17
  21. package/dart/test/acquire_session_test.dart +109 -24
  22. package/dart/test/adv_realm_interference_test.dart +227 -99
  23. package/dart/test/bundle_version_pairing_test.dart +1 -1
  24. package/dart/test/cloud_bound_topology_test.dart +1 -1
  25. package/dart/test/cluster_attested_test.dart +52 -35
  26. package/dart/test/cluster_births_test.dart +5 -2
  27. package/dart/test/cluster_device_keyed_acceptance_test.dart +5 -2
  28. package/dart/test/cluster_local_autosync_test.dart +5 -2
  29. package/dart/test/cluster_scale_test.dart +5 -2
  30. package/dart/test/compat_test.dart +1 -1
  31. package/dart/test/dlq_session_surface_test.dart +2 -1
  32. package/dart/test/errors_test.dart +1 -1
  33. package/dart/test/first_app_gauntlet_test.dart +78 -51
  34. package/dart/test/four_layer_nested_topology_test.dart +1 -1
  35. package/dart/test/harness_phase_timeout_test.dart +1 -1
  36. package/dart/test/home_parity_test.dart +9 -5
  37. package/dart/test/local_sync_inert_test.dart +22 -14
  38. package/dart/test/node_runner_transport_test.dart +9 -5
  39. package/dart/test/null_slot_test.dart +1 -1
  40. package/dart/test/parent_signer_recovery_test.dart +5 -2
  41. package/dart/test/schema_validation_test.dart +1 -1
  42. package/dart/test/sealing_client_test.dart +2 -1
  43. package/dart/test/session_bound_test.dart +92 -119
  44. package/dart/test/stale_bundle_test.dart +1 -1
  45. package/dart/test/test_harness_test.dart +77 -22
  46. package/dart/test/vo_read_typing_test.dart +32 -14
  47. package/dart/test/watch_after_offer_stall_test.dart +2 -1
  48. package/dart/test/webview_bridge_test.dart +17 -4
  49. package/dart/test/wire_test.dart +1 -1
  50. package/dart_types/CHANGELOG.md +8 -0
  51. package/dart_types/lib/nomos_types.dart +7 -6
  52. package/dart_types/lib/src/activity.dart +23 -13
  53. package/dart_types/lib/src/foreign_ref.dart +61 -112
  54. package/dart_types/lib/src/invitation.dart +96 -0
  55. package/dart_types/lib/src/nomos_ref.dart +97 -59
  56. package/dart_types/pubspec.yaml +1 -1
  57. package/dart_types/test/surfaced_test.dart +125 -19
  58. package/package.json +7 -3
  59. package/runtime/generated-application-host.js +8851 -0
  60. package/src/activity.ts +2 -2
  61. package/src/aggregate.ts +24 -1
  62. package/src/application_composition.ts +282 -0
  63. package/src/application_topology.ts +151 -0
  64. package/src/attested_core.ts +2 -2
  65. package/src/authoring_context.ts +9 -0
  66. package/src/birth_meter_ensure.ts +1 -1
  67. package/src/births.ts +53 -5
  68. package/src/build_package.ts +79 -71
  69. package/src/business_birth.ts +93 -10
  70. package/src/clear_directive.ts +3 -1
  71. package/src/codegen_dart.ts +369 -420
  72. package/src/codegen_dart_application.ts +1508 -0
  73. package/src/codegen_dart_client.ts +156 -95
  74. package/src/codegen_dart_scenario.ts +299 -0
  75. package/src/codegen_openapi.ts +8 -3
  76. package/src/codegen_proof.ts +1113 -1137
  77. package/src/codegen_provider_dart.ts +12 -1
  78. package/src/codegen_ts.ts +1176 -496
  79. package/src/compat_package_main.ts +22 -10
  80. package/src/compile_package_main.ts +1536 -337
  81. package/src/contract.ts +10 -4
  82. package/src/dart_barrel_guard.ts +5 -1
  83. package/src/describe_model.ts +13 -5
  84. package/src/directive.ts +244 -5
  85. package/src/engine_entry.ts +56 -13
  86. package/src/engine_javascript.ts +31 -0
  87. package/src/evolve_preview.ts +3 -12
  88. package/src/framework/application_home.ts +24 -10
  89. package/src/framework/capability.ts +15 -1
  90. package/src/framework/domain_lifecycle.ts +8 -0
  91. package/src/framework/framework_aggregates.ts +2 -2
  92. package/src/framework/genesis.ts +11 -1
  93. package/src/framework/home.ts +178 -75
  94. package/src/framework/impure_capability.ts +24 -7
  95. package/src/framework/relations.ts +13 -5
  96. package/src/framework/workspaces.ts +13 -12
  97. package/src/generated_client_names.ts +42 -3
  98. package/src/idof_lint.ts +65 -13
  99. package/src/index.ts +46 -12
  100. package/src/intent.ts +587 -0
  101. package/src/law_check.ts +64 -16
  102. package/src/precondition.ts +16 -4
  103. package/src/read_projection_namespace.ts +2 -7
  104. package/src/recipient.ts +33 -0
  105. package/src/relation_schema.ts +93 -0
  106. package/src/secondary_ensure.ts +11 -2
  107. package/src/shape_compat.ts +11 -1
  108. package/src/usd.ts +30 -8
  109. package/src/usd_layers.ts +447 -246
  110. package/src/usd_state.ts +1 -1
  111. package/src/value_object.ts +49 -13
  112. package/src/wire_encode.ts +124 -26
  113. package/src/workspace_sharing.ts +269 -0
  114. package/src/zod_introspection.ts +84 -0
  115. package/dart/example/nomos_secure_key_store.dart +0 -82
  116. package/dart/example/nomos_webview_transport.dart +0 -63
  117. package/dart/lib/nomos_headless.dart +0 -50
  118. package/dart/lib/testing.dart +0 -14
  119. package/dart/test/co2_product_gauntlet_test.dart +0 -244
  120. package/src/framework/identity.ts +0 -537
package/src/intent.ts ADDED
@@ -0,0 +1,587 @@
1
+ // NOMOS — business intent composition.
2
+ //
3
+ // A tenant application publishes intentions, not storage directives. One intent
4
+ // lowers to one kernel offer while composing any number of private directive
5
+ // plans; those plans may fan out again across aggregate types. The offer is
6
+ // therefore atomic inside one workspace without making an aggregate the owner of
7
+ // the business action.
8
+
9
+ import type { z } from "zod";
10
+
11
+ import type {
12
+ Directive,
13
+ PreconditionDescriptor,
14
+ ReferentialMarker,
15
+ SecondaryOutput,
16
+ } from "./directive.js";
17
+ import type { Ports } from "./ctx.js";
18
+ import type { PlannedOp } from "./ops.js";
19
+ import type { Field } from "./fields.js";
20
+ import type { AggregateHandle } from "./aggregate.js";
21
+ import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
22
+ import { businessBirthConsequence, type BusinessBirthDirective } from "./business_birth.js";
23
+ import type { BusinessBirthDecl } from "./births.js";
24
+ import type { WorkspaceSharingPolicy } from "./workspace_sharing.js";
25
+
26
+ const INTENT_BRAND = "__nomos_business_intent_v1" as const;
27
+ const LANGUAGE_BRAND = "__nomos_application_language_v1" as const;
28
+
29
+ export interface IntentStep<I = unknown, P = unknown> {
30
+ readonly directive: Directive<P>;
31
+ readonly payload: (facts: I) => P;
32
+ }
33
+
34
+ export interface BusinessIntent<I = unknown> extends Directive<I> {
35
+ readonly [INTENT_BRAND]: true;
36
+ readonly steps: readonly IntentStep<I, any>[];
37
+ /** The complete public business boundary. Runtime lowering still uses Directive.payloadSchema privately. */
38
+ readonly contract: IntentContract<I>;
39
+ }
40
+
41
+ /** The public promise of one business action. Outputs are inferred from its composed consequences unless
42
+ * the author names them explicitly, in which case the compiler verifies the promise exactly. */
43
+ export interface IntentContract<I = unknown> {
44
+ readonly accepts: z.ZodType<I>;
45
+ readonly returns: readonly string[];
46
+ readonly refuses: readonly string[];
47
+ }
48
+
49
+ export interface IntentContractDeclaration<I> {
50
+ readonly accepts: z.ZodType<I>;
51
+ readonly returns?: readonly AggregateHandle[];
52
+ readonly refuses?: readonly string[];
53
+ }
54
+
55
+ export interface ApplicationLanguage {
56
+ readonly [LANGUAGE_BRAND]: true;
57
+ readonly domain: string;
58
+ readonly intents: readonly BusinessIntent<any>[];
59
+ readonly workspaceSharing: readonly WorkspaceSharingPolicy[];
60
+ }
61
+
62
+ export interface ApplicationLanguageOptions {
63
+ readonly workspaceSharing?: readonly WorkspaceSharingPolicy[];
64
+ }
65
+
66
+ const stable = (value: unknown): string => {
67
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
68
+ if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`;
69
+ return `{${Object.entries(value as Record<string, unknown>)
70
+ .filter(([, child]) => child !== undefined)
71
+ .sort(([left], [right]) => left.localeCompare(right))
72
+ .map(([key, child]) => `${JSON.stringify(key)}:${stable(child)}`)
73
+ .join(",")}}`;
74
+ };
75
+
76
+ function oneStaticRelationGate<I>(steps: readonly IntentStep<I, any>[]): Directive["requiresRelationGate"] {
77
+ const gates = steps
78
+ .map((step) => step.directive.requiresRelationGate)
79
+ .filter((gate): gate is NonNullable<Directive["requiresRelationGate"]> => gate !== undefined);
80
+ if (gates.length === 0) return undefined;
81
+ if (gates.some((gate) => gate.objectFrom !== undefined)) {
82
+ throw new Error(
83
+ "compound intent cannot yet remap a per-record access gate — bind that gate on the intent itself",
84
+ );
85
+ }
86
+ const first = stable(gates[0]);
87
+ if (gates.some((gate) => stable(gate) !== first)) {
88
+ throw new Error(
89
+ "compound intent directives require different access gates — declare one jointly satisfiable intent gate",
90
+ );
91
+ }
92
+ return gates[0];
93
+ }
94
+
95
+ function unsupportedKernelGate(directive: Directive<any>): string | undefined {
96
+ if (directive.attestationAuthorizedGate !== undefined) return "attestation authorization";
97
+ if (directive.delegatedRoleGate !== undefined) return "delegated authority";
98
+ if (directive.firstDeviceEnrollGate !== undefined) return "first-device enrollment";
99
+ if (directive.certifiesEraGate !== undefined) return "era certification";
100
+ if (directive.businessSourceTrust !== undefined) return "source-trust preflight";
101
+ if ((directive.businessRecordInputs?.length ?? 0) > 0) return "record-input binding";
102
+ return undefined;
103
+ }
104
+
105
+ const dedupeBy = <T>(values: readonly T[], key: (value: T) => string): T[] => {
106
+ const out: T[] = [];
107
+ const seen = new Set<string>();
108
+ for (const value of values) {
109
+ const k = key(value);
110
+ if (seen.add(k)) out.push(value);
111
+ }
112
+ return out;
113
+ };
114
+
115
+ /**
116
+ * Carry a private consequence's declared target onto its primary operations.
117
+ *
118
+ * A bare-handle `set(Aggregate, ...)` intentionally contains no instance/type plumbing. When that
119
+ * consequence is an Ensure inside a public intent, however, the wire gate must see the declared aggregate
120
+ * type so it can distinguish a sanctioned deterministic ensure from an invented raw id. The declaration is
121
+ * already authoritative; composing it here keeps domain authors out of `ensure(type, handWrittenId)` recipes.
122
+ */
123
+ function composePrimaryConsequenceOp(
124
+ directive: Directive<any>,
125
+ op: PlannedOp,
126
+ ): PlannedOp {
127
+ if (op.kind === "strike") return op;
128
+ const targetsPrimary = op.aggregateType !== undefined
129
+ ? op.aggregateType === directive.aggregateId
130
+ : op.aggregateId === directive.aggregateId;
131
+ if (!targetsPrimary) return op;
132
+ return {
133
+ ...op,
134
+ ...(directive.marker === "ensures" && op.aggregateType === undefined
135
+ ? { aggregateType: directive.aggregateId }
136
+ : {}),
137
+ marker: directive.marker,
138
+ };
139
+ }
140
+
141
+ class IntentBuilder<I> {
142
+ constructor(
143
+ private readonly id: string,
144
+ private readonly contractDeclaration: IntentContractDeclaration<I>,
145
+ private readonly steps: readonly IntentStep<I, any>[] = [],
146
+ ) {}
147
+
148
+ /** Add one private directive consequence to this business intent. */
149
+ directs<P>(directive: Directive<P>, payload: (facts: I) => P): IntentBuilder<I> {
150
+ if (isBusinessIntent(directive)) {
151
+ throw new Error(`intent('${this.id}') cannot direct another intent — compose its private directives instead`);
152
+ }
153
+ if (directive.birthsChild === true) {
154
+ throw new Error(`intent('${this.id}') composes a declared birth with .births(...), never a raw birth directive`);
155
+ }
156
+ return new IntentBuilder(this.id, this.contractDeclaration, [
157
+ ...this.steps,
158
+ { directive, payload },
159
+ ]);
160
+ }
161
+
162
+ /** Add one declared child birth as a consequence; Nomos owns the private genesis recipe. */
163
+ births<P extends Record<string, unknown>, F extends Record<string, Field>>(
164
+ declaration: BusinessBirthDecl<P, F>,
165
+ payload: (facts: I) => P,
166
+ ): IntentBuilder<I> {
167
+ const directive = businessBirthConsequence(declaration);
168
+ return new IntentBuilder(this.id, this.contractDeclaration, [
169
+ ...this.steps,
170
+ { directive, payload },
171
+ ]);
172
+ }
173
+
174
+ /** Finish the intent and lower its directive graph to one atomic offer. */
175
+ atomically(): BusinessIntent<I> {
176
+ if (this.steps.length === 0) {
177
+ throw new Error(`intent('${this.id}') needs at least one directed consequence`);
178
+ }
179
+ const duplicate = this.steps.find(
180
+ (step, index) => this.steps.findIndex((candidate) => candidate.directive.id === step.directive.id) !== index,
181
+ );
182
+ if (duplicate !== undefined) {
183
+ throw new Error(`intent('${this.id}') directs '${duplicate.directive.id}' more than once`);
184
+ }
185
+ const birthSteps = this.steps.filter((step) => step.directive.birthsChild === true);
186
+ if (birthSteps.length > 1) {
187
+ throw new Error(`intent('${this.id}') cannot birth more than one durable life atomically`);
188
+ }
189
+ for (const step of this.steps) {
190
+ const unsupported = unsupportedKernelGate(step.directive);
191
+ if (unsupported !== undefined) {
192
+ throw new Error(
193
+ `intent('${this.id}') cannot compose '${step.directive.id}' yet: ${unsupported} needs intent-level lowering`,
194
+ );
195
+ }
196
+ }
197
+
198
+ const primary = this.steps.find((step) => step.directive.birthsChild !== true)?.directive
199
+ ?? this.steps[0]!.directive;
200
+ const outputs: SecondaryOutput[] = [];
201
+ for (const [index, step] of this.steps.entries()) {
202
+ const candidate = step.directive;
203
+ if (
204
+ candidate.birthsChild !== true &&
205
+ candidate !== primary &&
206
+ !(candidate.aggregateId === primary.aggregateId && candidate.marker === primary.marker)
207
+ ) {
208
+ outputs.push({ aggregateId: candidate.aggregateId, marker: candidate.marker });
209
+ }
210
+ if (candidate.birthsChild !== true) outputs.push(...(candidate.secondaryOutputs ?? []));
211
+ }
212
+ const secondaryOutputs = dedupeBy(
213
+ outputs.filter(
214
+ (output) => !(output.aggregateId === primary.aggregateId && output.marker === primary.marker),
215
+ ),
216
+ (output) => `${output.marker}\0${output.aggregateId}`,
217
+ );
218
+ const inferredReturns = [
219
+ primary.aggregateId,
220
+ ...secondaryOutputs.map((output) => output.aggregateId),
221
+ ...birthSteps.flatMap((step) => {
222
+ const aggregateId = (step.directive as BusinessBirthDirective).businessBirth?.aggregateId;
223
+ return aggregateId === undefined ? [] : [aggregateId];
224
+ }),
225
+ ].filter((value, index, values) => values.indexOf(value) === index);
226
+ const declaredReturns = this.contractDeclaration.returns?.map((aggregate) => aggregate.id);
227
+ if (
228
+ declaredReturns !== undefined &&
229
+ stable([...new Set(declaredReturns)].sort()) !== stable([...inferredReturns].sort())
230
+ ) {
231
+ throw new Error(
232
+ `intent('${this.id}') contract returns ${declaredReturns.join(", ") || "nothing"}, but its ` +
233
+ `consequences affect ${inferredReturns.join(", ") || "nothing"}`,
234
+ );
235
+ }
236
+
237
+ const preconditions: PreconditionDescriptor[] = this.steps.flatMap((step) =>
238
+ (step.directive.preconditions ?? []).map((condition) => ({
239
+ ...condition,
240
+ idField: (facts: unknown) => condition.idField(step.payload(facts as I)),
241
+ })),
242
+ );
243
+ const queryReads = dedupeBy(
244
+ this.steps.flatMap((step) => step.directive.declaredQueryReads ?? []),
245
+ (query) => query.id,
246
+ );
247
+ const foreignReads = dedupeBy(
248
+ this.steps.flatMap((step) => step.directive.declaredForeignReads ?? []),
249
+ (read) => stable(read),
250
+ );
251
+ const emits: Record<string, { max?: number }> = {};
252
+ for (const step of this.steps) {
253
+ for (const [event, declaration] of Object.entries(step.directive.declaredEmits)) {
254
+ const prior = emits[event];
255
+ if (prior === undefined) {
256
+ emits[event] = declaration.max === undefined ? {} : { max: declaration.max };
257
+ } else if (prior.max === undefined || declaration.max === undefined) {
258
+ emits[event] = {};
259
+ } else {
260
+ emits[event] = { max: prior.max + declaration.max };
261
+ }
262
+ }
263
+ }
264
+ const relationGate = oneStaticRelationGate(this.steps);
265
+ const steps = [...this.steps];
266
+ const plan = (facts: I, ctx: Ports): PlannedOp[] => {
267
+ const returned: PlannedOp[] = [];
268
+ for (const step of steps) {
269
+ const mapped = step.directive.payloadSchema.parse(step.payload(facts));
270
+ const result = step.directive.plan(mapped, ctx);
271
+ if (!Array.isArray(result)) {
272
+ throw new Error(
273
+ `intent '${this.id}' consequence '${step.directive.id}' did not return an operation array`,
274
+ );
275
+ }
276
+ returned.push(...result.map((op) => composePrimaryConsequenceOp(step.directive, op)));
277
+ }
278
+ return returned;
279
+ };
280
+
281
+ const businessBirth = (birthSteps[0]?.directive as BusinessBirthDirective | undefined)?.businessBirth;
282
+ return {
283
+ [INTENT_BRAND]: true,
284
+ id: this.id,
285
+ contract: {
286
+ accepts: this.contractDeclaration.accepts,
287
+ returns: Object.freeze(inferredReturns),
288
+ refuses: Object.freeze([...(this.contractDeclaration.refuses ?? [])]),
289
+ },
290
+ marker: primary.marker as ReferentialMarker,
291
+ aggregateId: primary.aggregateId,
292
+ ...(secondaryOutputs.length > 0 ? { secondaryOutputs } : {}),
293
+ payloadSchema: (birthSteps[0]?.directive.payloadSchema ?? this.contractDeclaration.accepts) as unknown as z.ZodType<I>,
294
+ plan,
295
+ steps,
296
+ declaredReads: [...new Set(this.steps.flatMap((step) => step.directive.declaredReads))].sort(),
297
+ ...(queryReads.length > 0 ? { declaredQueryReads: queryReads } : {}),
298
+ ...(foreignReads.length > 0 ? { declaredForeignReads: foreignReads } : {}),
299
+ declaredEmits: emits,
300
+ declaredRelations: dedupeBy(
301
+ this.steps.flatMap((step) => step.directive.declaredRelations),
302
+ (relation) => stable(relation),
303
+ ),
304
+ declaredHostCapabilities: [
305
+ ...new Set(this.steps.flatMap((step) => step.directive.declaredHostCapabilities)),
306
+ ].sort(),
307
+ ...(preconditions.length > 0 ? { preconditions } : {}),
308
+ ...(relationGate !== undefined ? { requiresRelationGate: relationGate } : {}),
309
+ ...(businessBirth === undefined
310
+ ? {}
311
+ : {
312
+ birthsChild: true,
313
+ businessBirth: {
314
+ ...businessBirth,
315
+ publicPayloadSchema: this.contractDeclaration.accepts,
316
+ },
317
+ }),
318
+ };
319
+ }
320
+ }
321
+
322
+ class IntentContractStep {
323
+ constructor(private readonly id: string) {}
324
+
325
+ /**
326
+ * Publish one existing effect as a business intent without repeating its
327
+ * payload schema. Useful while moving an established domain from routable
328
+ * directives to a closed application language.
329
+ */
330
+ performs<P>(effect: Directive<P>): BusinessIntent<P> {
331
+ if (isBusinessIntent(effect)) {
332
+ throw new Error(`intent('${this.id}').performs(...) expects a private directive effect, not an intent`);
333
+ }
334
+ const step: IntentStep<P, P> = { directive: effect, payload: (facts) => facts };
335
+ // A one-effect publication does not need to synthesize or approximate any
336
+ // kernel policy. Preserve the effect's complete metadata (birth, access,
337
+ // custody, recovery, preconditions, activity...) and change only the public
338
+ // route identity. Multi-effect intents still use `.directs(...).atomically()`
339
+ // and its explicit composition checks.
340
+ return {
341
+ ...effect,
342
+ [INTENT_BRAND]: true,
343
+ id: this.id,
344
+ contract: {
345
+ accepts: effect.payloadSchema,
346
+ returns: Object.freeze([
347
+ effect.aggregateId,
348
+ ...(effect.secondaryOutputs ?? []).map((output) => output.aggregateId),
349
+ ].filter((value, index, values) => values.indexOf(value) === index)),
350
+ refuses: Object.freeze([]),
351
+ },
352
+ steps: [step],
353
+ plan: (facts, ctx) => effect
354
+ .plan(effect.payloadSchema.parse(facts), ctx)
355
+ .map((op) => composePrimaryConsequenceOp(effect, op)),
356
+ };
357
+ }
358
+
359
+ /** Begin an intent from one declared business birth without repeating its payload schema. */
360
+ births<P extends Record<string, unknown>, F extends Record<string, Field>>(
361
+ declaration: BusinessBirthDecl<P, F>,
362
+ ): IntentBuilder<P> {
363
+ return new IntentBuilder<P>(this.id, { accepts: declaration.payloadSchema as z.ZodType<P> }, [
364
+ { directive: businessBirthConsequence(declaration), payload: (facts) => facts },
365
+ ]);
366
+ }
367
+
368
+ contract<S extends z.ZodTypeAny>(
369
+ declaration: IntentContractDeclaration<z.infer<S>> & { readonly accepts: S },
370
+ ): IntentBuilder<z.infer<S>> {
371
+ return new IntentBuilder(this.id, declaration as IntentContractDeclaration<z.infer<S>>);
372
+ }
373
+ }
374
+
375
+ /** Begin one public business intent. */
376
+ export function intent<const Id extends string>(id: Id): IntentContractStep {
377
+ if (id.trim().length === 0) throw new Error("intent id must not be empty");
378
+ if (id.startsWith("_nms")) {
379
+ throw new Error(`intent id '${id}' uses Nomos's reserved internal prefix '_nms'`);
380
+ }
381
+ return new IntentContractStep(id);
382
+ }
383
+
384
+ export function isBusinessIntent(value: unknown): value is BusinessIntent<any> {
385
+ return (
386
+ value !== null &&
387
+ typeof value === "object" &&
388
+ (value as Record<string, unknown>)[INTENT_BRAND] === true
389
+ );
390
+ }
391
+
392
+ /** Declare the complete public language for one domain. */
393
+ export function language(
394
+ domain: string,
395
+ intents: readonly BusinessIntent<any>[],
396
+ options: ApplicationLanguageOptions = {},
397
+ ): ApplicationLanguage {
398
+ if (domain.trim().length === 0) throw new Error("language domain must not be empty");
399
+ const seen = new Set<string>();
400
+ for (const candidate of intents) {
401
+ if (!isBusinessIntent(candidate)) {
402
+ throw new Error(`language('${domain}') accepts finished intent(...).atomically() declarations only`);
403
+ }
404
+ if (seen.has(candidate.id)) {
405
+ throw new Error(`language('${domain}') publishes intent '${candidate.id}' more than once`);
406
+ }
407
+ seen.add(candidate.id);
408
+ }
409
+ const workspaceSharing = Object.freeze([...(options.workspaceSharing ?? [])]);
410
+ for (const policy of workspaceSharing) {
411
+ for (const initiator of policy.initiatedBy) {
412
+ if (!intents.some((candidate) => candidate.id === initiator.id)) {
413
+ throw new Error(
414
+ `language('${domain}') workspaceSharing(${policy.orders.id}) initiator '${initiator.id}' is not a public business action`,
415
+ );
416
+ }
417
+ }
418
+ }
419
+ return {
420
+ [LANGUAGE_BRAND]: true,
421
+ domain,
422
+ intents: Object.freeze([...intents]),
423
+ workspaceSharing,
424
+ };
425
+ }
426
+
427
+ export function isApplicationLanguage(value: unknown): value is ApplicationLanguage {
428
+ return (
429
+ value !== null &&
430
+ typeof value === "object" &&
431
+ (value as Record<string, unknown>)[LANGUAGE_BRAND] === true
432
+ );
433
+ }
434
+
435
+ export function applicationLanguageOf(
436
+ modules: readonly Record<string, unknown>[],
437
+ domain: string,
438
+ ): ApplicationLanguage | undefined {
439
+ const found: ApplicationLanguage[] = [];
440
+ for (const mod of modules) {
441
+ for (const value of Object.values(mod)) {
442
+ if (isApplicationLanguage(value)) found.push(value);
443
+ }
444
+ }
445
+ const unique = [...new Set(found)];
446
+ if (unique.length > 1) {
447
+ throw new Error(`domain '${domain}' exports more than one application language`);
448
+ }
449
+ const language = unique[0];
450
+ if (language !== undefined && language.domain !== domain) {
451
+ throw new Error(
452
+ `domain '${domain}' exports language('${language.domain}') — the language name must match its domain`,
453
+ );
454
+ }
455
+ return language;
456
+ }
457
+
458
+ /**
459
+ * Close the executable command surface around a declared application language.
460
+ * The intent plans are routable; the directives they compose are private plan
461
+ * fragments and cannot be offered separately.
462
+ */
463
+ export function executableDirectivesForLanguage(
464
+ language: ApplicationLanguage,
465
+ discovered: readonly Directive<any>[],
466
+ domain: string,
467
+ ): Directive<any>[] {
468
+ const publicById = new Map(language.intents.map((candidate) => [candidate.id, candidate]));
469
+ const privateIds = new Set(
470
+ language.intents.flatMap((candidate) => candidate.steps.map((step) => step.directive.id)),
471
+ );
472
+
473
+ for (const candidate of language.intents) {
474
+ if (privateIds.has(candidate.id)) {
475
+ throw new Error(
476
+ `domain '${domain}' publishes intent '${candidate.id}' with the same name as one of its private directives`,
477
+ );
478
+ }
479
+ }
480
+ for (const candidate of discovered) {
481
+ const published = publicById.get(candidate.id);
482
+ const sameLowering =
483
+ published !== undefined &&
484
+ isBusinessIntent(candidate) &&
485
+ candidate.aggregateId === published.aggregateId &&
486
+ candidate.marker === published.marker &&
487
+ candidate.steps.length === published.steps.length &&
488
+ candidate.steps.every(
489
+ (step, index) => step.directive.id === published.steps[index]?.directive.id,
490
+ );
491
+ if (published !== undefined && candidate !== published && !sameLowering) {
492
+ throw new Error(
493
+ `domain '${domain}' intent '${candidate.id}' collides with a different directive — rename one of them`,
494
+ );
495
+ }
496
+ if (isBusinessIntent(candidate) && published === undefined) {
497
+ throw new Error(
498
+ `domain '${domain}' exports intent '${candidate.id}' outside its declared language — add it to language('${domain}', [...]) or remove it`,
499
+ );
500
+ }
501
+ }
502
+
503
+ const remaining = discovered.filter(
504
+ (candidate) => !privateIds.has(candidate.id) && !isBusinessIntent(candidate),
505
+ );
506
+ return [...remaining, ...language.intents];
507
+ }
508
+
509
+ /**
510
+ * Refuse a half-migrated source module. Once a domain declares its language,
511
+ * every authored directive export must either be a published intent or one of
512
+ * that intent's private effects. Compiler-generated framework directives are
513
+ * added later and are deliberately outside this source-level completeness gate.
514
+ */
515
+ export function assertApplicationLanguageCoversAuthoredDirectives(
516
+ language: ApplicationLanguage,
517
+ authored: readonly Directive<any>[],
518
+ domain: string,
519
+ ): void {
520
+ const publicIds = new Set(language.intents.map((candidate) => candidate.id));
521
+ const privateIds = new Set(
522
+ language.intents.flatMap((candidate) => candidate.steps.map((step) => step.directive.id)),
523
+ );
524
+ const sharingReceiptIds = new Set(
525
+ language.workspaceSharing.flatMap((policy) =>
526
+ [...policy.delivered, ...policy.refused].map((receipt) => receipt.records.id)
527
+ ),
528
+ );
529
+ const uncovered = authored
530
+ .filter(
531
+ (directive) =>
532
+ directive.businessBirthInternal !== true &&
533
+ !publicIds.has(directive.id) &&
534
+ !privateIds.has(directive.id) &&
535
+ !sharingReceiptIds.has(directive.id),
536
+ )
537
+ .map((directive) => directive.id)
538
+ .sort();
539
+ if (uncovered.length > 0) {
540
+ throw new Error(
541
+ `domain '${domain}' declares language('${domain}', [...]) but still exports routable directive ` +
542
+ `${uncovered.map((id) => `'${id}'`).join(", ")} outside it — publish business intents for ` +
543
+ `those actions or make their effects private`,
544
+ );
545
+ }
546
+ }
547
+
548
+ /** Carry a private directive's invariant onto every public intent that uses it. */
549
+ export function remapWorkspaceInvariantsForLanguage(
550
+ invariants: readonly WorkspaceInvariantDecl[],
551
+ language: ApplicationLanguage,
552
+ ): WorkspaceInvariantDecl[] {
553
+ const out: WorkspaceInvariantDecl[] = [];
554
+ for (const invariant of invariants) {
555
+ const uses = language.intents.flatMap((candidate) =>
556
+ candidate.steps
557
+ .filter((step) => step.directive.id === invariant.on)
558
+ .map((step) => ({ intent: candidate, step })),
559
+ );
560
+ if (uses.length === 0) {
561
+ out.push(invariant);
562
+ continue;
563
+ }
564
+ for (const use of uses) {
565
+ const mapFacts = (facts: Record<string, unknown>): Record<string, unknown> => {
566
+ const mapped = use.step.payload(facts);
567
+ const parsed = use.step.directive.payloadSchema.safeParse(mapped);
568
+ // The flow/security compiler probes invariant read-sets with empty
569
+ // placeholder facts. That probe must remain total even when the real
570
+ // business payload requires non-empty values. Runtime intent planning
571
+ // still validates every mapped consequence before it can commit.
572
+ return (parsed.success ? parsed.data : mapped) as Record<string, unknown>;
573
+ };
574
+ out.push({
575
+ ...invariant,
576
+ id: `${use.intent.id}__${invariant.id}`,
577
+ on: use.intent.id,
578
+ reads: (ctx) => invariant.reads({ ...ctx, intent: mapFacts(ctx.intent) }),
579
+ assert: (snapshots, ctx) => invariant.assert(
580
+ snapshots,
581
+ ctx === undefined ? undefined : { ...ctx, intent: mapFacts(ctx.intent) },
582
+ ),
583
+ });
584
+ }
585
+ }
586
+ return out;
587
+ }