@githolon/dsl 0.103.12 → 0.103.13

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 (43) hide show
  1. package/dart/CHANGELOG.md +5 -0
  2. package/dart/lib/src/runner/node-entry.mjs +234 -170
  3. package/dart/lib/src/runner/offline-install.mjs +87 -112
  4. package/dart/lib/src/webview_bridge.dart +11 -11
  5. package/dart/pubspec.yaml +1 -1
  6. package/dart/test/webview_bridge_test.dart +10 -7
  7. package/dart_types/CHANGELOG.md +1 -1
  8. package/dart_types/lib/nomos_types.dart +4 -4
  9. package/dart_types/lib/src/versioned_nomos_ref.dart +13 -11
  10. package/package.json +1 -1
  11. package/runtime/generated-application-host.js +424 -237
  12. package/src/aggregate.ts +3 -1
  13. package/src/build_package.ts +77 -6
  14. package/src/check_package_main.ts +39 -16
  15. package/src/codegen_dart.ts +185 -48
  16. package/src/codegen_dart_application.ts +82 -16
  17. package/src/codegen_dart_client.ts +38 -10
  18. package/src/codegen_dot.ts +3 -3
  19. package/src/codegen_proof.ts +24 -2
  20. package/src/codegen_ts.ts +32 -19
  21. package/src/compile_package_main.ts +68 -11
  22. package/src/derived.ts +17 -0
  23. package/src/directive.ts +65 -0
  24. package/src/engine_entry.ts +22 -2
  25. package/src/enum_type.ts +71 -0
  26. package/src/fields.ts +135 -5
  27. package/src/flow_gate.ts +2 -1
  28. package/src/framework/workspaces.ts +73 -0
  29. package/src/idof_lint.ts +5 -1
  30. package/src/index.ts +20 -0
  31. package/src/kernel_capabilities.ts +12 -1
  32. package/src/law_check.ts +107 -28
  33. package/src/law_graph.ts +151 -20
  34. package/src/law_graph_usda.ts +23 -0
  35. package/src/law_value_type.ts +10 -3
  36. package/src/lsp_server.ts +26 -16
  37. package/src/package_config.ts +112 -9
  38. package/src/package_feature_topology.ts +174 -0
  39. package/src/plan_source_slice.ts +6 -0
  40. package/src/report.ts +6 -0
  41. package/src/stable_ids_for_module.ts +1 -0
  42. package/src/value_object.ts +9 -2
  43. package/src/workspace_sharding.ts +4 -4
@@ -63,6 +63,7 @@ import {
63
63
  type PermissionVocabulary,
64
64
  } from "./codegen_dart.js";
65
65
  import type { ContractHandle } from "./contract.js";
66
+ import { bindEnumTypeExports } from "./enum_type.js";
66
67
  import { generateDartBridgeClient } from "./codegen_dart_client.js";
67
68
  import {
68
69
  dartApplicationClassName,
@@ -179,10 +180,11 @@ import {
179
180
  import {
180
181
  validateNomosPackageConfig,
181
182
  type NomosPackageConfig,
182
- type NomosPackageDomainConfig,
183
+ type NormalizedNomosPackageDomainConfig,
183
184
  } from "./package_config.js";
184
185
  import { assertUsdaRelationshipIntegrity } from "./usda_relationship_integrity.js";
185
186
  import { assertPublicPackageImports } from "./package_source_imports.js";
187
+ import { assertPackageFeatureTopology } from "./package_feature_topology.js";
186
188
  import { packageDependencyUsageFindings } from "./package_dependency_usage.js";
187
189
  import {
188
190
  PlanSourceSliceProgram,
@@ -224,7 +226,7 @@ const BUSINESS_BIRTH_FRAMEWORK_PLAN_SOURCES: Readonly<Record<string, PlanSourceS
224
226
  };
225
227
  const BUSINESS_BIRTH_FRAMEWORK_PLAN_DOMAIN = "__nomos_birth_genesis_plan_v1";
226
228
 
227
- interface DomainConfig extends NomosPackageDomainConfig {
229
+ interface DomainConfig extends NormalizedNomosPackageDomainConfig {
228
230
  readonly key: string;
229
231
  readonly name?: string;
230
232
  readonly modules: readonly string[];
@@ -821,7 +823,7 @@ const DART_SUPPORT_CORE_SRC = [
821
823
  "foreign_ref.dart",
822
824
  "activity.dart",
823
825
  "nomos_ref.dart",
824
- // Immutable publication pin + stable series and the required freshness sum
826
+ // Immutable publication pin + stable series and the required version-status sum
825
827
  // type. Pure data; the generated application owns the effectful check.
826
828
  "versioned_nomos_ref.dart",
827
829
  // Optional first-class references are explicit required choices in generated
@@ -1583,8 +1585,8 @@ function emitDart(
1583
1585
  // The generated clients import their runtime support directly. Keeping it out of the public barrel is
1584
1586
  // the surface firewall: a tenant import receives business types and application APIs, never raw runtime.
1585
1587
  supportImport === "vendored"
1586
- ? `export '${genRel}/src/${VENDORED_TYPES_SHIM}' show NomosRef, NomosOptionalRef, VersionedNomosRef, ReferenceFreshness, ReferenceCurrent, ReferenceStale, ReferenceFreshnessUnavailable${sha256DigestExport}${gitObjectIdExport};`
1587
- : `export 'package:nomos_types/nomos_types.dart' show NomosRef, NomosOptionalRef, VersionedNomosRef, ReferenceFreshness, ReferenceCurrent, ReferenceStale, ReferenceFreshnessUnavailable${sha256DigestExport}${gitObjectIdExport};`,
1588
+ ? `export '${genRel}/src/${VENDORED_TYPES_SHIM}' show NomosRef, NomosOptionalRef, VersionedNomosRef, ReferenceVersionStatus, ReferenceVersionCurrent, ReferenceVersionOutdated, ReferenceVersionUnavailable${sha256DigestExport}${gitObjectIdExport};`
1589
+ : `export 'package:nomos_types/nomos_types.dart' show NomosRef, NomosOptionalRef, VersionedNomosRef, ReferenceVersionStatus, ReferenceVersionCurrent, ReferenceVersionOutdated, ReferenceVersionUnavailable${sha256DigestExport}${gitObjectIdExport};`,
1588
1590
  ...(contractsImportUri !== undefined
1589
1591
  ? tenantDartExport(`${genRel}/${CONTRACTS_FILE}.dart`, sourceByFile.get(CONTRACTS_FILE) ?? "")
1590
1592
  : []),
@@ -1650,8 +1652,8 @@ const NOMOS_TYPES_DEP = "^0.8.4";
1650
1652
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
1651
1653
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
1652
1654
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
1653
- const NOMOS_CLIENT_DEP = "^0.64.13";
1654
- const NOMOS_FLUTTER_DEP = "^0.63.16";
1655
+ const NOMOS_CLIENT_DEP = "^0.64.14";
1656
+ const NOMOS_FLUTTER_DEP = "^0.63.17";
1655
1657
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
1656
1658
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
1657
1659
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -2028,9 +2030,22 @@ function dartAliasLines(names: Readonly<Record<string, string>> | undefined, dec
2028
2030
  ];
2029
2031
  for (const [generated, alias] of Object.entries(names).sort(([a], [b]) => (a < b ? -1 : 1))) {
2030
2032
  if (!declared.has(generated)) {
2033
+ const semanticTail = generated.match(/[A-Z][a-z0-9]+(?:[A-Z][a-z0-9]+)?$/u)?.[0];
2034
+ const nearby = [...declared]
2035
+ .filter((name) =>
2036
+ name.includes("AssetFieldRequirement") ||
2037
+ (generated.endsWith("FieldType") && name.endsWith("FieldType")) ||
2038
+ (semanticTail !== undefined && name.endsWith(semanticTail)))
2039
+ .sort((left, right) => {
2040
+ const leftExactKind = generated.endsWith("FieldType") && left.endsWith("FieldType") ? 0 : 1;
2041
+ const rightExactKind = generated.endsWith("FieldType") && right.endsWith("FieldType") ? 0 : 1;
2042
+ return leftExactKind - rightExactKind || left.localeCompare(right);
2043
+ })
2044
+ .slice(0, 20);
2031
2045
  fail(
2032
2046
  `dart.names: '${generated}' is not a generated public type in this package's TYPES surface — ` +
2033
- `check the spelling against the emitted src/generated/*.dart (aliases cover types, not client classes)`,
2047
+ `check the spelling against the emitted src/generated/*.dart (aliases cover types, not client classes)` +
2048
+ (nearby.length === 0 ? "" : `; nearby generated types: ${nearby.join(", ")}`),
2034
2049
  );
2035
2050
  }
2036
2051
  if (!/^[A-Z][A-Za-z0-9_]*$/.test(alias)) {
@@ -2294,7 +2309,18 @@ function emitDartPackages(
2294
2309
  ...(dd.modForDart.dartImports ?? []),
2295
2310
  ...[...frImports].filter((uri) => !(dd.modForDart.dartImports ?? []).some((i) => i.uri === uri)).sort().map((uri) => ({ uri })),
2296
2311
  ];
2297
- const modForTypes = merged.length > 0 ? { ...dd.modForDart, dartImports: merged } : dd.modForDart;
2312
+ const typedDomain = merged.length > 0
2313
+ ? { ...dd.modForDart, dartImports: merged }
2314
+ : dd.modForDart;
2315
+ // A capability declaration is canonical law, but its provider dispatcher is
2316
+ // effectful runtime machinery (TaskProvider, HostClock, NomosIntent). Layered
2317
+ // packages deliberately keep the types package pure, so never inject that
2318
+ // adapter into a frontend data library merely because the law can emit an
2319
+ // obligation. Provider SDKs belong to an explicitly selected provider target;
2320
+ // flat/legacy generation retains its existing opt-in behavior.
2321
+ const modForTypes = (typedDomain.impureCapabilities?.length ?? 0) === 0
2322
+ ? typedDomain
2323
+ : { ...typedDomain, impureCapabilities: [] };
2298
2324
  writeTypes(
2299
2325
  mod.name,
2300
2326
  generateDartDomain(modForTypes, {
@@ -2372,7 +2398,7 @@ function emitDartPackages(
2372
2398
  businessSharedPath,
2373
2399
  [
2374
2400
  "// GENERATED by nomos-compile — shared business values and activity; no runtime machinery.",
2375
- `export 'package:nomos_types/nomos_types.dart' show NomosRef, NomosOptionalRef, VersionedNomosRef, ReferenceFreshness, ReferenceCurrent, ReferenceStale, ReferenceFreshnessUnavailable${sha256DigestExport}${gitObjectIdExport}, Identity, User, Recipient, Invitation, ActivityEvent, ActivityRef, ActivitySeverity, SurfacedObservation, SurfacedValue, SurfacedResolved, SurfacedPending, SurfacedEvolutionRequired;`,
2401
+ `export 'package:nomos_types/nomos_types.dart' show NomosRef, NomosOptionalRef, VersionedNomosRef, ReferenceVersionStatus, ReferenceVersionCurrent, ReferenceVersionOutdated, ReferenceVersionUnavailable${sha256DigestExport}${gitObjectIdExport}, Identity, User, Recipient, Invitation, ActivityEvent, ActivityRef, ActivitySeverity, SurfacedObservation, SurfacedValue, SurfacedResolved, SurfacedPending, SurfacedEvolutionRequired;`,
2376
2402
  ...(sharedBusinessTypeNames.length === 0
2377
2403
  ? []
2378
2404
  : [`export '${FRAMEWORK_FILE}.dart' show ${sharedBusinessTypeNames.join(", ")};`]),
@@ -2870,13 +2896,31 @@ async function main(): Promise<void> {
2870
2896
  if (Object.prototype.hasOwnProperty.call(cfg, "layered")) {
2871
2897
  fail("nomos.project.ts no longer accepts 'layered': every law closure is layered, with no alternate compile lane");
2872
2898
  }
2899
+ let sourceTopologyDiagnostics: ReturnType<typeof assertPackageFeatureTopology> = [];
2873
2900
  try {
2901
+ sourceTopologyDiagnostics = assertPackageFeatureTopology(cfg.domains, cfgDir);
2874
2902
  assertPublicPackageImports(
2875
2903
  cfg.domains.flatMap((domain) => domain.modules.map((module) => path.resolve(cfgDir, module))),
2876
2904
  );
2877
2905
  } catch (error) {
2878
2906
  fail(`package import boundary refused: ${(error as Error).message}`);
2879
2907
  }
2908
+ if (sourceTopologyDiagnostics.length > 0) {
2909
+ const heading =
2910
+ `nomos-compile: ${sourceTopologyDiagnostics.length} unacknowledged source-shape warning(s)`;
2911
+ const lines = sourceTopologyDiagnostics.map((diagnostic) =>
2912
+ ` ⚠ [${diagnostic.rule}] ${diagnostic.domain}/${diagnostic.feature} ` +
2913
+ `${diagnostic.source}: ${diagnostic.message} — fix: ${diagnostic.fix}`
2914
+ );
2915
+ if (preflightFlag) {
2916
+ console.error(`${heading}; preflight requires an explicit fix or per-file acknowledgement`);
2917
+ for (const line of lines) console.error(line);
2918
+ process.exitCode = 1;
2919
+ return;
2920
+ }
2921
+ console.warn(heading);
2922
+ for (const line of lines) console.warn(line);
2923
+ }
2880
2924
  // EVERY PUBLISHED APPLICATION IS A COMPLETE PRODUCT ENTRYPOINT. `rootPart` is the explicit OpenUSD
2881
2925
  // application/package boundary: only that independently published root privately receives the authority
2882
2926
  // bootstrap law which turns verified credential evidence into durable device authorship. Ordinary law
@@ -2920,6 +2964,7 @@ async function main(): Promise<void> {
2920
2964
  const abs = path.resolve(cfgDir, rel);
2921
2965
  if (!existsSync(abs)) fail(`domain '${domain.key}': module not found: ${abs}`);
2922
2966
  const imported = await import(pathToFileURL(abs).href) as Mod;
2967
+ bindEnumTypeExports(imported, abs);
2923
2968
  authored = { ...authored, ...imported };
2924
2969
  for (const [exportName, value] of Object.entries(imported)) {
2925
2970
  if (value !== cfg.application?.homeGenesis || !isApplicationHomeGenesis(value)) continue;
@@ -2949,6 +2994,10 @@ async function main(): Promise<void> {
2949
2994
  ? []
2950
2995
  : [{
2951
2996
  key: APPLICATION_HOME_DOMAIN,
2997
+ features: [{
2998
+ key: "application-home",
2999
+ modules: [path.join(DSL_DIR, "src", "framework", "application_home.ts")],
3000
+ }],
2952
3001
  modules: [path.join(DSL_DIR, "src", "framework", "application_home.ts")],
2953
3002
  }]),
2954
3003
  ];
@@ -3088,6 +3137,7 @@ async function main(): Promise<void> {
3088
3137
  const abs = path.resolve(cfgDir, rel);
3089
3138
  if (!existsSync(abs)) fail(`domain '${d.key}': module not found: ${abs}`);
3090
3139
  const m = (await import(pathToFileURL(abs).href)) as Mod;
3140
+ bindEnumTypeExports(m, abs);
3091
3141
  mods.push(m);
3092
3142
  sourceExprs.push(`() => require(${JSON.stringify(abs)})`);
3093
3143
  for (const [exportName, value] of Object.entries(m)) {
@@ -4055,9 +4105,15 @@ async function main(): Promise<void> {
4055
4105
  planInputDigestCache.set(absolute, digest);
4056
4106
  return digest;
4057
4107
  };
4108
+ const planSourceSliceImplementation = fileURLToPath(new URL("./plan_source_slice.ts", import.meta.url));
4058
4109
  const planCompilerIdentity = planBytesDigest(Buffer.concat([
4059
4110
  Buffer.from("nomos.plan-compiler.v1\0", "utf8"),
4060
4111
  readFileSync(fileURLToPath(import.meta.url)),
4112
+ // The slicer decides which bindings and imports enter every cached plan. A slicer fix without
4113
+ // this input otherwise replays bytecode produced by the old dependency closure indefinitely.
4114
+ ...(existsSync(planSourceSliceImplementation)
4115
+ ? [Buffer.from("\0plan-source-slice\0", "utf8"), readFileSync(planSourceSliceImplementation)]
4116
+ : []),
4061
4117
  Buffer.from(`\0esbuild:${esbuildMod.version}\0`, "utf8"),
4062
4118
  ]));
4063
4119
  let planSequence = 0;
@@ -5039,6 +5095,7 @@ async function main(): Promise<void> {
5039
5095
  const requiresKernel = ((): string[] => {
5040
5096
  const probes: FeatureProbeModule[] = domainModules.map((m) => ({
5041
5097
  ...(m.queries ? { queries: m.queries } : {}),
5098
+ ...(m.deriveds ? { deriveds: m.deriveds } : {}),
5042
5099
  ...(m.surfacedResolvers ? { surfacedResolvers: m.surfacedResolvers } : {}),
5043
5100
  ...(m.aggregates ? { aggregates: m.aggregates } : {}),
5044
5101
  }));
@@ -5177,7 +5234,7 @@ async function main(): Promise<void> {
5177
5234
  const temporaryDeclarationPath = temporaryClientPath.replace(/\.ts$/, ".d.ts");
5178
5235
  const runtimeTypesSource = [
5179
5236
  `export declare function nomosInternalOpenGeneratedApplication<T extends object>(`,
5180
- ` options: { cloud: string; application: string; signIn: (request?: { readonly deviceChallenge?: string }) => Promise<string>; onPerformance?: (sample: { readonly operation: "open" | "offer" | "birth" | "read" | "sync" | "maintenance"; readonly name: string; readonly durationMs: number; readonly domain?: string; readonly directive?: string }) => void; lawHash: string },`,
5237
+ ` options: { cloud: string; application: string; signIn: (request?: { readonly deviceChallenge?: string }) => Promise<string>; onPerformance?: (sample: { readonly operation: "open" | "offer" | "birth" | "read" | "sync" | "maintenance"; readonly name: string; readonly durationMs: number; readonly domain?: string; readonly directive?: string }) => void; lawHash: string; readyCommands?: readonly { readonly domain: string; readonly packageHash: string; readonly directiveId: string }[] },`,
5181
5238
  ` bind: (holon: unknown, user: { readonly identityId: string; readonly identityAddress: string }) => T,`,
5182
5239
  `): Promise<T & {`,
5183
5240
  ` currentStatus(): Promise<any>;`,
package/src/derived.ts CHANGED
@@ -33,6 +33,7 @@
33
33
  */
34
34
  import type { z } from "zod";
35
35
  import type { AggregateHandle } from "./aggregate.js";
36
+ import type { TemporalEnumTransition } from "./fields.js";
36
37
 
37
38
  /**
38
39
  * The pure derive function: maps ONE aggregate's folded fields (a plain JSON object — the
@@ -59,6 +60,22 @@ export interface DerivedDecl {
59
60
  readonly fn: DeriveFn;
60
61
  }
61
62
 
63
+ /** A clock-aware enum projection compiled entirely as typed data and evaluated natively by the
64
+ * kernel. It deliberately has no function body: source, missing state and monotone thresholds are
65
+ * the complete executable algebra. Instances are synthesized from `t.timestamp().temporalEnum(...)`. */
66
+ export interface TemporalEnumDecl {
67
+ readonly kind: "temporalEnum";
68
+ readonly id: string;
69
+ readonly of: string;
70
+ readonly sourceField: string;
71
+ readonly missing: string;
72
+ readonly initial: string;
73
+ readonly transitions: readonly TemporalEnumTransition[];
74
+ readonly returns: z.ZodTypeAny;
75
+ }
76
+
77
+ export type AnyDerivedDecl = DerivedDecl | TemporalEnumDecl;
78
+
62
79
  /**
63
80
  * The {@link DerivedOf} BUILDER: it has named its `of`-type, so `.as(...)` (which fixes
64
81
  * the pure fn and yields the finished {@link DerivedDecl}) is available. This is the ONLY
package/src/directive.ts CHANGED
@@ -24,6 +24,7 @@ import type { Ports } from "./ctx.js";
24
24
  import type { RelationDecl } from "./relation.js";
25
25
  import type { QueryDecl } from "./query.js";
26
26
  import { normalizeActivity, type ActivityInput, type ActivityDescriptor } from "./activity.js";
27
+ import { idOfMetaOf } from "./value_object.js";
27
28
  import { BIRTH_METER_QUERIES } from "./birth_meter.js";
28
29
  import { zodDef, zodKind, zodObjectShape } from "./zod_introspection.js";
29
30
  import {
@@ -50,6 +51,16 @@ import {
50
51
 
51
52
  export type ReferentialMarker = "creates" | "mutates" | "ensures" | "archives";
52
53
 
54
+ /**
55
+ * The existing record selected by a mutation/archive. This is application-shape
56
+ * metadata, not law: custody still carries the ordinary typed payload field,
57
+ * while generated clients expose the reference separately from business facts.
58
+ */
59
+ export interface DirectiveTargetBinding {
60
+ readonly field: string;
61
+ readonly aggregateId: string;
62
+ }
63
+
53
64
  /**
54
65
  * A DECLARATIVE WRITE-PATH PRECONDITION (the precondition-primitives arc). One descriptor per
55
66
  * `.requiresExisting` / `.creates(Agg).payload(...).withId(idFn)` create-once / `.where` marker on a directive. Each
@@ -217,6 +228,8 @@ export interface Directive<P = unknown> {
217
228
  readonly manifestOnly?: true;
218
229
  /** Typed declaration target retained for compiler composition; omitted by every wire/law serializer. */
219
230
  readonly aggregate?: AggregateHandle;
231
+ /** Generated-API target binding. Omitted from canonical law/wire output. */
232
+ readonly targetBinding?: DirectiveTargetBinding;
220
233
  /**
221
234
  * Business-result cardinality for the primary marker target. The default is
222
235
  * exactly one; `.affectsMany()` and `.affectsOptional()` opt into the other
@@ -795,6 +808,7 @@ class DirectivePlanStep<Id extends string, P> {
795
808
  private readonly payloadSchema: z.ZodType<P>,
796
809
  private readonly createOnceIdField?: (payload: unknown) => string,
797
810
  private readonly preconditions: PreconditionDescriptor[] = [],
811
+ private readonly targetField?: string,
798
812
  ) {}
799
813
 
800
814
  /** Non-destructive copy carrying one more precondition (and/or the create-once id field). */
@@ -809,6 +823,38 @@ class DirectivePlanStep<Id extends string, P> {
809
823
  this.payloadSchema,
810
824
  createOnceIdField ?? this.createOnceIdField,
811
825
  desc !== undefined ? [...this.preconditions, desc] : this.preconditions,
826
+ this.targetField,
827
+ );
828
+ }
829
+
830
+ /**
831
+ * Select the existing aggregate reference targeted by this mutation/archive.
832
+ * Normally unnecessary: a single top-level `idOf(Target)` field is inferred.
833
+ * Use this only when the payload carries more than one reference to the same
834
+ * aggregate and the compiler cannot choose without domain knowledge.
835
+ */
836
+ target<K extends Extract<keyof P, string>>(field: K): DirectivePlanStep<Id, P> {
837
+ if (this.marker !== "mutates" && this.marker !== "archives") {
838
+ throw new Error(
839
+ `directive('${this.id}').target(...) is only valid on .mutates(...) or .archives(...)`,
840
+ );
841
+ }
842
+ const shape = zodObjectShape(this.payloadSchema);
843
+ const schema = shape[field];
844
+ const actual = schema === undefined ? undefined : idOfMetaOf(schema);
845
+ if (actual !== this.aggregate.id) {
846
+ throw new Error(
847
+ `directive('${this.id}').target('${field}') must name an idOf(${shortName(this.aggregate.id)}) payload field`,
848
+ );
849
+ }
850
+ return new DirectivePlanStep(
851
+ this.id,
852
+ this.marker,
853
+ this.aggregate,
854
+ this.payloadSchema,
855
+ this.createOnceIdField,
856
+ this.preconditions,
857
+ field,
812
858
  );
813
859
  }
814
860
 
@@ -968,10 +1014,28 @@ class DirectivePlanStep<Id extends string, P> {
968
1014
  ...this.preconditions.slice(0, -1),
969
1015
  { ...last, declassify: trimmed },
970
1016
  ],
1017
+ this.targetField,
971
1018
  );
972
1019
  }
973
1020
 
974
1021
  plan(fn: (payload: P, ctx: Ports) => PlannedOp[]): RequirableDirective<P> {
1022
+ let targetBinding: DirectiveTargetBinding | undefined;
1023
+ if (this.marker === "mutates" || this.marker === "archives") {
1024
+ const shape = zodObjectShape(this.payloadSchema);
1025
+ const candidates = Object.entries(shape)
1026
+ .filter(([, schema]) => idOfMetaOf(schema) === this.aggregate.id)
1027
+ .map(([field]) => field);
1028
+ const field = this.targetField ?? (candidates.length === 1 ? candidates[0] : undefined);
1029
+ if (this.targetField === undefined && candidates.length > 1) {
1030
+ throw new Error(
1031
+ `directive('${this.id}') has several idOf(${shortName(this.aggregate.id)}) payload fields; ` +
1032
+ `select the mutation target with .target('field') before .plan(...)`,
1033
+ );
1034
+ }
1035
+ if (field !== undefined) {
1036
+ targetBinding = { field, aggregateId: this.aggregate.id };
1037
+ }
1038
+ }
975
1039
  return makeRequirable({
976
1040
  id: this.id,
977
1041
  marker: this.marker,
@@ -979,6 +1043,7 @@ class DirectivePlanStep<Id extends string, P> {
979
1043
  aggregate: this.aggregate,
980
1044
  payloadSchema: this.payloadSchema,
981
1045
  plan: fn,
1046
+ ...(targetBinding === undefined ? {} : { targetBinding }),
982
1047
  declaredReads: [],
983
1048
  declaredQueryReads: [],
984
1049
  declaredEmits: {},
@@ -52,7 +52,7 @@
52
52
  * tenant package for the long-form rationale of each branch.
53
53
  */
54
54
  import { executeDirectiveToIntent } from "./wire_encode.js";
55
- import { expandCapabilityExports } from "./capability_exports.js";
55
+ import { expandCapabilityExports, isCapabilityDecl } from "./capability_exports.js";
56
56
  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";
@@ -193,7 +193,8 @@ function aggregatesOf(mod: DomainModuleExports): Map<string, AggregateHandle> {
193
193
  if (
194
194
  v &&
195
195
  typeof v === "object" &&
196
- (v as { __isAggregateHandle?: boolean }).__isAggregateHandle === true
196
+ (v as { __isAggregateHandle?: boolean }).__isAggregateHandle === true &&
197
+ (v as { __isForwardRef?: boolean }).__isForwardRef !== true
197
198
  ) {
198
199
  const h = v as AggregateHandle;
199
200
  out.set(h.id, h);
@@ -332,6 +333,25 @@ function buildDomainSlice(mod: DomainModuleExports, domain: string): DomainSlice
332
333
  const executableDirectives = applicationLanguage === undefined
333
334
  ? discoveredDirectives
334
335
  : executableDirectivesForLanguage(applicationLanguage, discoveredDirectives, domain);
336
+ // Closed application languages hide their private consequence fragments from
337
+ // generated tenant clients. Capability lifecycle plans are nevertheless
338
+ // kernel-executable law: an atomic public action can create an obligation and
339
+ // a provider must later author its typed receipt. Mirror package composition
340
+ // by retaining the exact framework-derived lifecycle here as well.
341
+ for (const value of Object.values(mod)) {
342
+ if (!isCapabilityDecl(value)) continue;
343
+ for (const directive of [
344
+ value.obligation,
345
+ value.complete,
346
+ value.fail,
347
+ value.block,
348
+ value.deadLetter,
349
+ ] as Directive<unknown>[]) {
350
+ if (!executableDirectives.some((candidate) => candidate.id === directive.id)) {
351
+ executableDirectives.push(directive);
352
+ }
353
+ }
354
+ }
335
355
  const dirs = new Map(executableDirectives.map((directive) => [directive.id, directive]));
336
356
  // A directive already owns the typed aggregate handle it targets. Retain that
337
357
  // link unconditionally in the EXECUTABLE slice instead of requiring a second
@@ -0,0 +1,71 @@
1
+ import { z } from "zod";
2
+
3
+ const NOMOS_ENUM_TYPE_NAME = Symbol.for("nomos.enumType.name");
4
+
5
+ export interface EnumType<E extends readonly [string, ...string[]]> {
6
+ readonly __isEnumType: true;
7
+ readonly values: E;
8
+ readonly zod: z.ZodType<E[number]>;
9
+ }
10
+
11
+ /**
12
+ * One scalar vocabulary reused across fields, payloads and value objects.
13
+ *
14
+ * Its canonical name is the exported TypeScript binding. Authors do not repeat that binding as a
15
+ * string: `export const CustomFieldType = enumType([...])` is the declaration and the compiler
16
+ * binds `CustomFieldType` while loading the configured source module.
17
+ */
18
+ export function enumType<const E extends readonly [string, ...string[]]>(
19
+ values: E,
20
+ ): EnumType<E> {
21
+ const schema = z.enum(values) as unknown as z.ZodType<E[number]>;
22
+ return Object.freeze({ __isEnumType: true as const, values, zod: schema });
23
+ }
24
+
25
+ export function enumTypeNameOf(schema: z.ZodTypeAny): string | undefined {
26
+ const name = (schema as unknown as Record<symbol, unknown>)[NOMOS_ENUM_TYPE_NAME];
27
+ return typeof name === "string" ? name : undefined;
28
+ }
29
+
30
+ export function isEnumType(value: unknown): value is EnumType<readonly [string, ...string[]]> {
31
+ return value !== null && typeof value === "object" &&
32
+ (value as { __isEnumType?: unknown }).__isEnumType === true;
33
+ }
34
+
35
+ /** Compiler/LSP seam: bind semantic identity from the language's own exported symbol table. */
36
+ export function bindEnumTypeExport(
37
+ value: unknown,
38
+ exportName: string,
39
+ source = "configured law module",
40
+ ): void {
41
+ if (!isEnumType(value)) return;
42
+ if (!/^[A-Z][A-Za-z0-9]*$/u.test(exportName)) {
43
+ throw new Error(
44
+ `${source} exports enumType(...) as '${exportName}'; semantic type exports must be UpperCamelCase`,
45
+ );
46
+ }
47
+ const existing = enumTypeNameOf(value.zod);
48
+ if (existing !== undefined && existing !== exportName) {
49
+ throw new Error(
50
+ `${source} exports one enumType(...) as both '${existing}' and '${exportName}'; ` +
51
+ "a semantic type has one canonical exported identity",
52
+ );
53
+ }
54
+ if (existing === undefined) {
55
+ Object.defineProperty(value.zod, NOMOS_ENUM_TYPE_NAME, {
56
+ value: exportName,
57
+ enumerable: false,
58
+ configurable: false,
59
+ writable: false,
60
+ });
61
+ }
62
+ }
63
+
64
+ export function bindEnumTypeExports(
65
+ exports: Record<string, unknown>,
66
+ source = "configured law module",
67
+ ): void {
68
+ for (const [exportName, value] of Object.entries(exports)) {
69
+ bindEnumTypeExport(value, exportName, source);
70
+ }
71
+ }