@githolon/dsl 0.102.5 → 0.103.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/compile_package.mjs +43 -2
  2. package/compile_proof.d.mts +47 -0
  3. package/compile_proof.mjs +343 -0
  4. package/dart/CHANGELOG.md +7 -0
  5. package/dart/lib/src/dev_cloud.dart +8 -8
  6. package/dart/lib/src/nms_generated_runtime.dart +2 -0
  7. package/dart/lib/src/runner/node-entry.mjs +5504 -1236
  8. package/dart/lib/src/runner/offline-install.mjs +4578 -528
  9. package/dart/lib/src/test_harness.dart +206 -152
  10. package/dart/lib/src/webview_bridge.dart +196 -89
  11. package/dart/pubspec.lock +1 -1
  12. package/dart/pubspec.yaml +2 -2
  13. package/dart/test/acquire_session_credentials_test.dart +4 -3
  14. package/dart/test/adv_realm_interference_test.dart +7 -6
  15. package/dart/test/cloud_bound_topology_test.dart +3 -3
  16. package/dart/test/cluster_births_test.dart +4 -3
  17. package/dart/test/cluster_device_keyed_acceptance_test.dart +10 -16
  18. package/dart/test/cluster_local_autosync_test.dart +3 -2
  19. package/dart/test/cluster_scale_test.dart +3 -2
  20. package/dart/test/dlq_session_surface_test.dart +7 -6
  21. package/dart/test/first_app_gauntlet_test.dart +14 -21
  22. package/dart/test/fixtures/wedge/fake-install.mjs +1 -1
  23. package/dart/test/four_layer_nested_topology_test.dart +4 -4
  24. package/dart/test/harness_phase_timeout_test.dart +2 -2
  25. package/dart/test/local_sync_inert_test.dart +1 -1
  26. package/dart/test/parent_signer_recovery_test.dart +2 -2
  27. package/dart/test/sealing_client_test.dart +4 -3
  28. package/dart/test/session_bound_test.dart +8 -7
  29. package/dart/test/test_harness_test.dart +13 -12
  30. package/dart/test/vo_read_typing_test.dart +4 -3
  31. package/dart/test/watch_after_offer_stall_test.dart +4 -3
  32. package/dart/test/webview_bridge_test.dart +102 -5
  33. package/dart_types/CHANGELOG.md +5 -0
  34. package/dart_types/lib/nomos_types.dart +9 -0
  35. package/dart_types/lib/src/nomos_ref.dart +20 -0
  36. package/dart_types/lib/src/optional_ref.dart +85 -0
  37. package/dart_types/lib/src/sha256_digest.dart +41 -0
  38. package/dart_types/lib/src/versioned_nomos_ref.dart +81 -0
  39. package/dart_types/pubspec.yaml +1 -1
  40. package/dart_types/test/optional_ref_test.dart +44 -0
  41. package/dart_types/test/sha256_digest_test.dart +18 -0
  42. package/package.json +9 -5
  43. package/runtime/generated-application-host.js +5365 -1105
  44. package/src/aggregate.ts +18 -0
  45. package/src/application_composition.ts +6 -32
  46. package/src/attested_core.ts +6 -3
  47. package/src/birth_meter.ts +1 -0
  48. package/src/birth_meter_ensure.ts +28 -2
  49. package/src/births.ts +1 -1
  50. package/src/build_package.ts +162 -538
  51. package/src/business_birth.ts +19 -3
  52. package/src/canonical_json.ts +21 -0
  53. package/src/check_package_main.ts +73 -33
  54. package/src/clear_directive.ts +19 -8
  55. package/src/codegen_dart.ts +305 -22
  56. package/src/codegen_dart_application.ts +844 -292
  57. package/src/codegen_dart_client.ts +1084 -407
  58. package/src/codegen_dart_scenario.ts +12 -12
  59. package/src/codegen_proof.ts +169 -39
  60. package/src/codegen_provider_dart.ts +17 -1
  61. package/src/codegen_ts.ts +298 -106
  62. package/src/compat_manifest.ts +2 -2
  63. package/src/compat_package_main.ts +8 -12
  64. package/src/compile_package_main.ts +1858 -590
  65. package/src/compose.ts +1 -1
  66. package/src/directive.ts +208 -15
  67. package/src/engine_entry.ts +165 -111
  68. package/src/evolve_preview.ts +29 -69
  69. package/src/extremum.ts +1 -1
  70. package/src/fields.ts +183 -4
  71. package/src/flow_gate.ts +75 -2
  72. package/src/framework/application_home.ts +2 -0
  73. package/src/framework/auth_provider_law.ts +2 -2
  74. package/src/framework/birth_meter.ts +7 -19
  75. package/src/framework/bootstrap.ts +0 -2
  76. package/src/framework/business_corrections.ts +9 -5
  77. package/src/framework/domain_lifecycle.ts +15 -29
  78. package/src/framework/encryption_scope.ts +2 -1
  79. package/src/framework/framework_aggregates.ts +2 -0
  80. package/src/framework/genesis.ts +16 -14
  81. package/src/framework/home.ts +20 -14
  82. package/src/framework/owned_custody.ts +9 -3
  83. package/src/framework/platform_admin.ts +3 -3
  84. package/src/framework/relations.ts +21 -13
  85. package/src/framework/signing_identity.ts +45 -40
  86. package/src/framework/workspace_invariant.ts +245 -0
  87. package/src/framework/workspaces.ts +23 -29
  88. package/src/index.ts +36 -16
  89. package/src/intent.ts +178 -6
  90. package/src/law_check.ts +32 -11
  91. package/src/law_graph.ts +2902 -0
  92. package/src/law_graph_usda.ts +1799 -0
  93. package/src/law_identity.ts +435 -0
  94. package/src/law_value_type.ts +230 -0
  95. package/src/lsp_server.ts +58 -29
  96. package/src/ops.ts +7 -0
  97. package/src/package_config.ts +141 -0
  98. package/src/package_dependencies.ts +108 -0
  99. package/src/package_dependency_usage.ts +150 -0
  100. package/src/package_source_imports.ts +71 -0
  101. package/src/plan_source_slice.ts +335 -0
  102. package/src/point_guard.ts +192 -0
  103. package/src/precondition.ts +44 -9
  104. package/src/read_projection_namespace.ts +1 -1
  105. package/src/runtime_zod.ts +44 -0
  106. package/src/secondary_ensure.ts +67 -14
  107. package/src/shape_compat.ts +1 -1
  108. package/src/stable_id_identity_bridge.ts +149 -0
  109. package/src/stable_id_lockfile.ts +140 -6
  110. package/src/stable_ids.ts +1 -1
  111. package/src/stable_ids_for_module.ts +348 -0
  112. package/src/stable_ids_types.ts +1 -1
  113. package/src/sum.ts +1 -1
  114. package/src/usd_state.ts +1 -1
  115. package/src/usda_relationship_integrity.ts +66 -0
  116. package/src/usda_text.ts +18 -0
  117. package/src/usdz.ts +78 -0
  118. package/src/value_object.ts +95 -0
  119. package/src/wire.ts +19 -0
  120. package/src/wire_encode.ts +22 -0
  121. package/src/workspace_sharding.ts +52 -36
  122. package/src/workspace_sharing.ts +28 -4
  123. package/src/zod_introspection.ts +1 -1
  124. package/dart/test/cluster_attested_test.dart +0 -201
  125. package/src/compile_engine.ts +0 -595
  126. package/src/manifest.ts +0 -1476
  127. package/src/read_projection_composition.ts +0 -117
  128. package/src/read_projection_manifest.ts +0 -115
  129. package/src/relation_composition.ts +0 -286
  130. package/src/usd.ts +0 -1967
  131. package/src/usd_layers.ts +0 -1776
package/src/lsp_server.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  /**
7
7
  * `githolon lsp` — THE AUTHOR-TIME GATE AS A LANGUAGE SERVER (architecture/author_time_gate.md,
8
8
  * slice 3). A thin `vscode-languageserver` (stdio) wrapper over the SAME static-twin machinery
9
- * `githolon check` runs: on open/save/change of a `.ts` under a `nomos.package.mjs`'s domain graph,
9
+ * `githolon check` runs: on open/save/change of a `.ts` under a `nomos.project.ts`'s domain graph,
10
10
  * it composes the package IN-PROCESS (cache-busted TS re-import under tsx — no per-keystroke process
11
11
  * spawn), runs {@link checkDomainModules}, maps findings to source ranges ({@link LawSourceMap}), and
12
12
  * publishes diagnostics (refuse→Error, warn→Warning, note→Information).
@@ -17,7 +17,7 @@
17
17
  * tsx). Composition is memoized per config keyed by the module files' mtimes so an UNCHANGED package
18
18
  * re-checks without re-importing (the cheap warm win).
19
19
  *
20
- * Multi-root: each document resolves to its NEAREST `nomos.package.mjs` (walk up), so a repo with
20
+ * Multi-root: each document resolves to its NEAREST `nomos.project.ts` (walk up), so a repo with
21
21
  * several packages diagnoses each against its own config.
22
22
  */
23
23
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
@@ -44,21 +44,26 @@ import { LawSourceMap } from "./law_source_map.js";
44
44
  import {
45
45
  currentEvolveLaw,
46
46
  evolveDiff,
47
- evolveLawFromDeployJson,
47
+ evolveLawFromInterface,
48
48
  evolveLawFromLockfile,
49
49
  type EvolveLaw,
50
50
  type StableIdLockfile,
51
51
  } from "./evolve_preview.js";
52
52
  import { parseStableIdLockfile } from "./stable_id_lockfile.js";
53
+ import {
54
+ validateNomosPackageConfig,
55
+ type NomosPackageConfig,
56
+ } from "./package_config.js";
57
+ import { assertPublicPackageImports } from "./package_source_imports.js";
58
+ import { packageDependencyUsageFindings } from "./package_dependency_usage.js";
53
59
 
54
60
  interface DomainConfig {
55
61
  key: string;
56
62
  name?: string;
57
63
  modules: string[];
58
64
  }
59
- interface PackageConfig {
60
- name: string;
61
- domains: DomainConfig[];
65
+ interface PackageConfig extends NomosPackageConfig {
66
+ readonly domains: readonly DomainConfig[];
62
67
  }
63
68
 
64
69
  /** A finding enriched with the resolved source file (absolute) it belongs to. */
@@ -81,11 +86,11 @@ export interface CheckPassResult {
81
86
  readonly configPath: string;
82
87
  }
83
88
 
84
- /** Walk up from `startDir` to find the nearest `nomos.package.mjs`; `undefined` at the filesystem root. */
89
+ /** Walk up from `startDir` to find the nearest `nomos.project.ts`; `undefined` at the filesystem root. */
85
90
  export function findNearestConfig(startDir: string): string | undefined {
86
91
  let dir = startDir;
87
92
  for (;;) {
88
- const candidate = path.join(dir, "nomos.package.mjs");
93
+ const candidate = path.join(dir, "nomos.project.ts");
89
94
  if (existsSync(candidate)) return candidate;
90
95
  const parent = path.dirname(dir);
91
96
  if (parent === dir) return undefined;
@@ -98,6 +103,7 @@ interface ComposedCache {
98
103
  readonly mtimeKey: string;
99
104
  readonly modules: DomainModule[];
100
105
  readonly domainFiles: Map<string, string[]>;
106
+ readonly dependencyFindings: readonly Finding[];
101
107
  }
102
108
  const composedCache = new Map<string, ComposedCache>();
103
109
 
@@ -120,23 +126,22 @@ function mtimeKey(files: readonly string[]): string {
120
126
  */
121
127
  async function composePackage(
122
128
  configPath: string,
123
- ): Promise<{ modules: DomainModule[]; domainFiles: Map<string, string[]> }> {
129
+ ): Promise<{
130
+ modules: DomainModule[];
131
+ domainFiles: Map<string, string[]>;
132
+ dependencyFindings: readonly Finding[];
133
+ }> {
124
134
  const cfgDir = path.dirname(configPath);
125
135
  const cfgModule = (await import(`${pathToFileURL(configPath).href}?t=${Date.now()}`)) as {
126
136
  default?: PackageConfig;
127
137
  };
128
- const cfg = cfgModule.default;
129
- if (!cfg || typeof cfg.name !== "string" || !Array.isArray(cfg.domains) || cfg.domains.length === 0) {
130
- throw new Error("config must default-export { name, domains: [{ key, modules }, ...] }");
131
- }
138
+ const validated = validateNomosPackageConfig(cfgModule.default);
139
+ const cfg = validated.config as PackageConfig;
132
140
 
133
141
  // Resolve every module file first so we can compute the mtime cache key before importing.
134
142
  const perDomainFiles: { key: string; name: string | undefined; files: string[] }[] = [];
135
143
  const allFiles: string[] = [];
136
144
  for (const d of cfg.domains) {
137
- if (typeof d.key !== "string" || !Array.isArray(d.modules) || d.modules.length === 0) {
138
- throw new Error(`each domain needs { key, modules: [path, ...] }; got ${JSON.stringify(d)}`);
139
- }
140
145
  const files: string[] = [];
141
146
  for (const rel of d.modules) {
142
147
  const abs = path.resolve(cfgDir, rel);
@@ -150,9 +155,15 @@ async function composePackage(
150
155
  const key = `${statSync(configPath).mtimeMs}::${mtimeKey(allFiles)}`;
151
156
  const cached = composedCache.get(configPath);
152
157
  if (cached !== undefined && cached.mtimeKey === key) {
153
- return { modules: cached.modules, domainFiles: cached.domainFiles };
158
+ return {
159
+ modules: cached.modules,
160
+ domainFiles: cached.domainFiles,
161
+ dependencyFindings: cached.dependencyFindings,
162
+ };
154
163
  }
155
164
 
165
+ assertPublicPackageImports(allFiles);
166
+
156
167
  const composed: DomainModule[] = [];
157
168
  const domainFiles = new Map<string, string[]>();
158
169
  for (const d of perDomainFiles) {
@@ -163,11 +174,17 @@ async function composePackage(
163
174
  domainFiles.set(d.key, d.files);
164
175
  composed.push(composeDomainModule({ name: d.name ?? d.key, domain: d.key, modules: mods }));
165
176
  }
166
- composedCache.set(configPath, { mtimeKey: key, modules: composed, domainFiles });
167
- return { modules: composed, domainFiles };
177
+ const dependencyFindings = packageDependencyUsageFindings({
178
+ packageName: cfg.name,
179
+ exportedDomains: cfg.domains.map((domain) => domain.key),
180
+ runtimeDependencies: validated.runtimeDependencies,
181
+ modules: composed,
182
+ });
183
+ composedCache.set(configPath, { mtimeKey: key, modules: composed, domainFiles, dependencyFindings });
184
+ return { modules: composed, domainFiles, dependencyFindings };
168
185
  }
169
186
 
170
- /** Load the `--against` prior evolve law from a deploy.json / build dir / lockfile (LSP variant — throws). */
187
+ /** Load prior evolve diagnostics from an interface sidecar/build dir/lockfile. */
171
188
  function loadPriorEvolveLaw(arg: string, cfgDir: string): EvolveLaw | undefined {
172
189
  const abs = path.isAbsolute(arg) ? arg : path.resolve(cfgDir, arg);
173
190
  if (!existsSync(abs)) return undefined;
@@ -176,12 +193,12 @@ function loadPriorEvolveLaw(arg: string, cfgDir: string): EvolveLaw | undefined
176
193
  const buildDir = path.join(abs, "build");
177
194
  const scan = (d: string): string | undefined => {
178
195
  if (!existsSync(d)) return undefined;
179
- const hit = readdirSync(d).find((f) => f.endsWith(".deploy.json"));
196
+ const hit = readdirSync(d).find((f) => f.endsWith(".interface.json"));
180
197
  return hit !== undefined ? path.join(d, hit) : undefined;
181
198
  };
182
- const deploy = scan(abs) ?? scan(buildDir);
199
+ const iface = scan(abs) ?? scan(buildDir);
183
200
  const lock = path.join(abs, "nomos.stable-ids.json");
184
- if (deploy !== undefined) file = deploy;
201
+ if (iface !== undefined) file = iface;
185
202
  else if (existsSync(lock)) file = lock;
186
203
  else return undefined;
187
204
  }
@@ -191,11 +208,22 @@ function loadPriorEvolveLaw(arg: string, cfgDir: string): EvolveLaw | undefined
191
208
  } catch {
192
209
  return undefined;
193
210
  }
194
- const p = parsed as Record<string, unknown>;
195
- if (p.domains !== undefined && p.interface === undefined && p.packageUsda === undefined) {
211
+ if (path.basename(file) === "nomos.stable-ids.json") {
196
212
  return evolveLawFromLockfile(parseStableIdLockfile(parsed));
197
213
  }
198
- return evolveLawFromDeployJson(parsed as never);
214
+ const lockFile = [
215
+ path.join(path.dirname(file), "nomos.stable-ids.json"),
216
+ path.join(path.dirname(path.dirname(file)), "nomos.stable-ids.json"),
217
+ ].find((candidate) => existsSync(candidate));
218
+ let lock: StableIdLockfile | undefined;
219
+ if (lockFile !== undefined) {
220
+ try {
221
+ lock = parseStableIdLockfile(JSON.parse(readFileSync(lockFile, "utf8")));
222
+ } catch {
223
+ return undefined;
224
+ }
225
+ }
226
+ return evolveLawFromInterface(parsed as Parameters<typeof evolveLawFromInterface>[0], lock);
199
227
  }
200
228
 
201
229
  /** Read the committed `nomos.stable-ids.json` lockfile beside the config (the CURRENT identity). */
@@ -218,13 +246,14 @@ export async function runCheckPass(configPath: string, against?: string): Promis
218
246
  const cfgDir = path.dirname(configPath);
219
247
  let modules: DomainModule[];
220
248
  let domainFiles: Map<string, string[]>;
249
+ let dependencyFindings: readonly Finding[];
221
250
  try {
222
- ({ modules, domainFiles } = await composePackage(configPath));
251
+ ({ modules, domainFiles, dependencyFindings } = await composePackage(configPath));
223
252
  } catch (e) {
224
253
  return { located: [], error: (e as Error).message, files: [], configPath };
225
254
  }
226
255
 
227
- const findings: Finding[] = [...checkDomainModules(modules)];
256
+ const findings: Finding[] = [...dependencyFindings, ...checkDomainModules(modules)];
228
257
 
229
258
  if (against !== undefined && against.trim().length > 0) {
230
259
  try {
@@ -337,7 +366,7 @@ export function startServer(): void {
337
366
  } catch {
338
367
  return;
339
368
  }
340
- if (!fsPath.endsWith(".ts")) return;
369
+ if (!/\.(?:ts|mts|js|mjs)$/.test(fsPath)) return;
341
370
  const configPath = findNearestConfig(path.dirname(fsPath));
342
371
  if (configPath === undefined) return;
343
372
 
package/src/ops.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import type { AggregateHandle, BoundAggregate } from "./aggregate.js";
19
19
  import type { Field, FieldValue } from "./fields.js";
20
+ import type { WireValue } from "./wire.js";
20
21
  import {
21
22
  endorsementOf,
22
23
  type AttributionPrincipal,
@@ -58,6 +59,12 @@ export interface FieldOp {
58
59
  readonly value?: unknown;
59
60
  readonly items?: readonly string[];
60
61
  readonly entryKey?: string;
62
+ /**
63
+ * Framework-only typed kernel value. This bypasses generic field-kind lowering for a value whose
64
+ * runtime algebra is stricter than its tenant-facing projection field. The encoder validates the
65
+ * target aggregate and field before accepting it; tenant code cannot use this as an untyped escape.
66
+ */
67
+ readonly kernelValue?: WireValue;
61
68
  /**
62
69
  * THE PRINCIPAL-FLOW ENDORSEMENT (security_model.md — the endorse model): present iff this op's
63
70
  * value came from a PrincipalEvidence conversion. The raw principal is materialised into `value`;
@@ -0,0 +1,141 @@
1
+ // NOMOS — one package contract for authoring, checking, compilation and editor diagnostics.
2
+
3
+ import {
4
+ normalizeRuntimePackageDependencies,
5
+ type RuntimePackageDependency,
6
+ } from "./package_dependencies.js";
7
+
8
+ /** The source modules which compose one domain exported by a Nomos package. */
9
+ export interface NomosPackageDomainConfig {
10
+ readonly key: string;
11
+ readonly name?: string;
12
+ readonly modules: readonly string[];
13
+ }
14
+
15
+ /**
16
+ * The common, law-bearing part of `nomos.project.ts`.
17
+ *
18
+ * Compiler-only presentation/code-generation options extend this shape, but these fields are
19
+ * deliberately owned here so compile, check and LSP cannot disagree about what a package is.
20
+ */
21
+ export interface NomosPackageConfig {
22
+ readonly name: string;
23
+ readonly domains: readonly NomosPackageDomainConfig[];
24
+ /** Separately installed law required at runtime; USDA composition arcs do not belong here. */
25
+ readonly dependencies?: readonly RuntimePackageDependency[];
26
+ }
27
+
28
+ type PackageDomainKey<Config extends NomosPackageConfig> =
29
+ Config["domains"][number]["key"] & string;
30
+
31
+ /**
32
+ * The importable, typed contract returned by {@link definePackage}.
33
+ *
34
+ * A dependent law imports this definition and asks it for the domains it actually needs:
35
+ *
36
+ * ```ts
37
+ * import referenceData from "@co2/reference-data/nomos.project.ts";
38
+ *
39
+ * export default definePackage({
40
+ * name: "estate",
41
+ * dependencies: [referenceData.requiresDomains("standards")],
42
+ * domains: [/* ... *\/],
43
+ * });
44
+ * ```
45
+ *
46
+ * Package and domain names therefore come from the dependency's declaration rather than being
47
+ * retyped as unrelated strings. The returned value remains the ordinary logical dependency which
48
+ * the compiler writes into typed USDA; it carries no version choice or host authority.
49
+ */
50
+ export type NomosPackageDefinition<Config extends NomosPackageConfig> = Config & {
51
+ readonly requiresDomains: <
52
+ const First extends PackageDomainKey<Config>,
53
+ const Rest extends readonly PackageDomainKey<Config>[],
54
+ >(
55
+ first: First,
56
+ ...rest: Rest
57
+ ) => {
58
+ readonly package: Config["name"];
59
+ readonly domains: readonly [First, ...Rest];
60
+ };
61
+ };
62
+
63
+ /** Validate the common package contract and return its canonical runtime requirements. */
64
+ export function validateNomosPackageConfig(value: unknown): {
65
+ readonly config: NomosPackageConfig;
66
+ readonly runtimeDependencies: readonly RuntimePackageDependency[];
67
+ } {
68
+ if (value === null || typeof value !== "object") {
69
+ throw new Error("config must default-export a Nomos package object");
70
+ }
71
+ const config = value as Partial<NomosPackageConfig>;
72
+ if (typeof config.name !== "string" || config.name.length === 0) {
73
+ throw new Error("config needs a non-empty string name");
74
+ }
75
+ if (!Array.isArray(config.domains) || config.domains.length === 0) {
76
+ throw new Error("config needs domains: [{ key, modules }, ...]");
77
+ }
78
+ const domainKeys = new Set<string>();
79
+ for (const domain of config.domains) {
80
+ if (
81
+ domain === null
82
+ || typeof domain !== "object"
83
+ || typeof domain.key !== "string"
84
+ || domain.key.length === 0
85
+ || !Array.isArray(domain.modules)
86
+ || domain.modules.length === 0
87
+ || domain.modules.some((module: unknown) => typeof module !== "string" || module.length === 0)
88
+ ) {
89
+ throw new Error(`each domain needs { key, modules: [path, ...] }; got ${JSON.stringify(domain)}`);
90
+ }
91
+ if (!domainKeys.add(domain.key)) {
92
+ throw new Error(`package '${config.name}' repeats domain key '${domain.key}'`);
93
+ }
94
+ }
95
+ if (config.dependencies !== undefined && !Array.isArray(config.dependencies)) {
96
+ throw new Error("package dependencies must be an array");
97
+ }
98
+ const runtimeDependencies = normalizeRuntimePackageDependencies(
99
+ config.name,
100
+ config.dependencies ?? [],
101
+ { requiresNomos: true },
102
+ );
103
+ return { config: config as NomosPackageConfig, runtimeDependencies };
104
+ }
105
+
106
+ /**
107
+ * Typed public package-authoring entrypoint. It is intentionally identity-preserving: the compiler
108
+ * still consumes the ordinary object, while TypeScript/JavaScript editors expose the package and
109
+ * dependency contract at the declaration site.
110
+ */
111
+ export function definePackage<const Config extends NomosPackageConfig>(
112
+ config: Config,
113
+ ): NomosPackageDefinition<Config> {
114
+ validateNomosPackageConfig(config);
115
+ const exportedDomains = new Set(config.domains.map((domain) => domain.key));
116
+ Object.defineProperty(config, "requiresDomains", {
117
+ enumerable: false,
118
+ configurable: false,
119
+ writable: false,
120
+ value: (first: string, ...rest: readonly string[]): RuntimePackageDependency => {
121
+ const dependency = { package: config.name, domains: [first, ...rest] } as const;
122
+ // Keep the imported contract honest even when called from plain JavaScript without TS. Most
123
+ // tenant configs are `.mjs`, so the generic's editor error is assistance rather than the gate.
124
+ // A misspelled or removed export must refuse while loading the config, not survive until a
125
+ // workspace install asks the kernel to resolve an impossible logical dependency.
126
+ normalizeRuntimePackageDependencies("dependent-package", [dependency], {
127
+ requiresNomos: false,
128
+ });
129
+ for (const domain of dependency.domains) {
130
+ if (!exportedDomains.has(domain)) {
131
+ throw new Error(
132
+ `package '${config.name}' does not export domain '${domain}'; available domains: ` +
133
+ `${[...exportedDomains].sort().join(", ") || "(none)"}`,
134
+ );
135
+ }
136
+ }
137
+ return dependency;
138
+ },
139
+ });
140
+ return config as NomosPackageDefinition<Config>;
141
+ }
@@ -0,0 +1,108 @@
1
+ // NOMOS — runtime package requirements are canonical typed USDA law, not ambient host setup.
2
+
3
+ import { usdaString, usdaStringArray } from "./usda_text.js";
4
+
5
+ /**
6
+ * A separately installed package whose law must be Active for this package to execute.
7
+ *
8
+ * This is deliberately different from an OpenUSD composition dependency: a sublayer or
9
+ * reference is already part of this package's authenticated closure. Runtime requirements
10
+ * name law which retains its own lifecycle and must therefore be resolved by the author gate.
11
+ */
12
+ export interface RuntimePackageDependency {
13
+ readonly package: string;
14
+ readonly domains: readonly string[];
15
+ }
16
+
17
+ const safeLogicalName = (value: string, what: string): void => {
18
+ if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]*$/.test(value)) {
19
+ throw new Error(`${what} '${value}' is not a safe Nomos logical name`);
20
+ }
21
+ };
22
+
23
+ /** Validate, deduplicate and canonically order the requirements that enter package identity. */
24
+ export function normalizeRuntimePackageDependencies(
25
+ packageName: string,
26
+ authored: readonly RuntimePackageDependency[],
27
+ options: { readonly requiresNomos: boolean },
28
+ ): readonly RuntimePackageDependency[] {
29
+ safeLogicalName(packageName, "package name");
30
+ const byPackage = new Map<string, Set<string>>();
31
+ const add = (dependency: RuntimePackageDependency, source: string): void => {
32
+ safeLogicalName(dependency.package, `${source} dependency package`);
33
+ if (dependency.package === packageName) {
34
+ throw new Error(`package '${packageName}' cannot declare itself as a runtime dependency`);
35
+ }
36
+ if (dependency.domains.length === 0) {
37
+ throw new Error(`runtime dependency '${dependency.package}' must require at least one domain`);
38
+ }
39
+ const domains = byPackage.get(dependency.package) ?? new Set<string>();
40
+ byPackage.set(dependency.package, domains);
41
+ for (const domain of dependency.domains) {
42
+ safeLogicalName(domain, `domain required from '${dependency.package}'`);
43
+ domains.add(domain);
44
+ }
45
+ };
46
+
47
+ for (const dependency of authored) add(dependency, "authored");
48
+ if (options.requiresNomos && packageName !== "nomos") {
49
+ add({ package: "nomos", domains: ["nomos"] }, "compiler-owned");
50
+ }
51
+ return [...byPackage]
52
+ .sort(([left], [right]) => left.localeCompare(right))
53
+ .map(([dependencyPackage, domains]) => ({
54
+ package: dependencyPackage,
55
+ domains: [...domains].sort(),
56
+ }));
57
+ }
58
+
59
+ /** Emit the package contract into the same composed stage the kernel executes. */
60
+ export function packageContractUsda(input: {
61
+ readonly packageName: string;
62
+ readonly exportedDomains: readonly string[];
63
+ readonly runtimeDependencies: readonly RuntimePackageDependency[];
64
+ }): string {
65
+ const dependencies = [...input.runtimeDependencies];
66
+ const dependencyPrimName = (index: number): string => `d_${String(index).padStart(6, "0")}`;
67
+ const targets = dependencies.map(
68
+ (_dependency, index) => `</Nomos/Package/RuntimeDependencies/${dependencyPrimName(index)}>`,
69
+ );
70
+ return [
71
+ "#usda 1.0",
72
+ "(",
73
+ " customLayerData = {",
74
+ ' string "nomos:format" = "nomos.law-layer.v3"',
75
+ ' string "nomos:logicalPath" = "package"',
76
+ " }",
77
+ ")",
78
+ "",
79
+ 'over "Nomos"',
80
+ "{",
81
+ ' def NomosPackage "Package"',
82
+ " {",
83
+ ` custom uniform string nomos:name = ${usdaString(input.packageName)}`,
84
+ ` custom uniform string[] nomos:domains = ${usdaStringArray([...input.exportedDomains].sort())}`,
85
+ ...(targets.length > 0
86
+ ? [
87
+ " custom rel nomos:runtimeDependencies = [",
88
+ ...targets.map((target, index) => ` ${target}${index + 1 === targets.length ? "" : ","}`),
89
+ " ]",
90
+ ]
91
+ : [" custom rel nomos:runtimeDependencies = []"]),
92
+ "",
93
+ ' def Scope "RuntimeDependencies"',
94
+ " {",
95
+ ...dependencies.flatMap((dependency, index) => [
96
+ ` def NomosRuntimePackageDependency ${usdaString(dependencyPrimName(index))}`,
97
+ " {",
98
+ ` custom uniform string nomos:package = ${usdaString(dependency.package)}`,
99
+ ` custom uniform string[] nomos:domains = ${usdaStringArray(dependency.domains)}`,
100
+ " }",
101
+ "",
102
+ ]),
103
+ " }",
104
+ " }",
105
+ "}",
106
+ "",
107
+ ].join("\n");
108
+ }
@@ -0,0 +1,150 @@
1
+ // NOMOS — cross-package law use must be declared before an offer can reach the kernel.
2
+
3
+ import type { DomainModule } from "./codegen_dart.js";
4
+ import type { Field } from "./fields.js";
5
+ import type { Finding } from "./law_check.js";
6
+ import type { RuntimePackageDependency } from "./package_dependencies.js";
7
+
8
+ interface ReferenceUse {
9
+ readonly targetId: string;
10
+ readonly targetDomain: string | undefined;
11
+ readonly description: string;
12
+ }
13
+
14
+ /**
15
+ * Pre-fire the runtime dependency refusal from the composed typed law.
16
+ *
17
+ * A field may refer to an entity exported by this package without a runtime dependency. If the
18
+ * target lives in separately installed law, the field must name its owning domain and exactly one
19
+ * declared package dependency must export that domain. This is deliberately a semantic check over
20
+ * the composed DSL objects, not an import-string heuristic: filesystem imports do not establish
21
+ * runtime law availability or authority.
22
+ */
23
+ export function packageDependencyUsageFindings(input: {
24
+ readonly packageName: string;
25
+ readonly exportedDomains: readonly string[];
26
+ readonly runtimeDependencies: readonly RuntimePackageDependency[];
27
+ readonly modules: readonly DomainModule[];
28
+ }): readonly Finding[] {
29
+ const exportedDomains = new Set(input.exportedDomains);
30
+ const entityOwners = new Map<string, Set<string>>();
31
+ const addOwner = (id: string, domain: string): void => {
32
+ const owners = entityOwners.get(id) ?? new Set<string>();
33
+ owners.add(domain);
34
+ entityOwners.set(id, owners);
35
+ };
36
+ for (const module of input.modules) {
37
+ const domain = module.domain ?? module.name;
38
+ if (!exportedDomains.has(domain)) continue;
39
+ for (const aggregate of module.aggregates) addOwner(aggregate.id, domain);
40
+ for (const contract of module.contracts ?? []) addOwner(contract.id, domain);
41
+ }
42
+
43
+ const packagesByDomain = new Map<string, Set<string>>();
44
+ for (const dependency of input.runtimeDependencies) {
45
+ for (const domain of dependency.domains) {
46
+ const packages = packagesByDomain.get(domain) ?? new Set<string>();
47
+ packages.add(dependency.package);
48
+ packagesByDomain.set(domain, packages);
49
+ }
50
+ }
51
+
52
+ const findings: Finding[] = [];
53
+ const refuse = (
54
+ domain: string,
55
+ aggregateId: string,
56
+ what: string,
57
+ fix: string,
58
+ ): void => {
59
+ findings.push({ domain, directiveId: aggregateId, level: "refuse", what, fix });
60
+ };
61
+
62
+ for (const module of input.modules) {
63
+ const domain = module.domain ?? module.name;
64
+ if (!exportedDomains.has(domain)) continue;
65
+ for (const aggregate of module.aggregates) {
66
+ for (const [fieldName, field] of Object.entries(aggregate.fields as Record<string, Field>)) {
67
+ const uses: ReferenceUse[] = [];
68
+ if (field.foreignRef !== undefined) {
69
+ uses.push({
70
+ targetId: field.foreignRef.targetAggregateId,
71
+ targetDomain: field.foreignRef.targetDomain,
72
+ description: "foreign reference",
73
+ });
74
+ }
75
+ if (field.versionedForeignRef !== undefined) {
76
+ uses.push({
77
+ targetId: field.versionedForeignRef.targetAggregateId,
78
+ targetDomain: field.versionedForeignRef.targetDomain,
79
+ description: "versioned foreign reference target",
80
+ });
81
+ uses.push({
82
+ targetId: field.versionedForeignRef.seriesAggregateId,
83
+ targetDomain: field.versionedForeignRef.targetDomain,
84
+ description: "versioned foreign reference series",
85
+ });
86
+ }
87
+
88
+ for (const use of uses) {
89
+ const localOwners = [...(entityOwners.get(use.targetId) ?? [])].sort();
90
+ if (use.targetDomain === undefined) {
91
+ if (localOwners.length === 1) continue;
92
+ if (localOwners.length > 1) {
93
+ refuse(
94
+ domain,
95
+ aggregate.id,
96
+ `${aggregate.id}.${fieldName} has an ambiguous ${use.description} '${use.targetId}'; ` +
97
+ `that name is exported by local domains ${localOwners.map((owner) => `'${owner}'`).join(", ")}`,
98
+ `Set domain: "<owning-domain>" on the field so generated clients and law composition use one typed target.`,
99
+ );
100
+ continue;
101
+ }
102
+ refuse(
103
+ domain,
104
+ aggregate.id,
105
+ `${aggregate.id}.${fieldName} uses ${use.description} '${use.targetId}' outside package ` +
106
+ `'${input.packageName}' but does not name the target domain`,
107
+ `Set domain: "<exported-domain>" on the field and declare the provider with ` +
108
+ `dependencies: [providerPackage.requiresDomains("<exported-domain>")].`,
109
+ );
110
+ continue;
111
+ }
112
+
113
+ if (exportedDomains.has(use.targetDomain)) {
114
+ if (localOwners.includes(use.targetDomain)) continue;
115
+ refuse(
116
+ domain,
117
+ aggregate.id,
118
+ `${aggregate.id}.${fieldName} says ${use.description} '${use.targetId}' belongs to local domain ` +
119
+ `'${use.targetDomain}', but that domain exports no aggregate or contract with that id`,
120
+ `Point the field at the public contract exported by '${use.targetDomain}', or correct its domain.`,
121
+ );
122
+ continue;
123
+ }
124
+
125
+ const providers = [...(packagesByDomain.get(use.targetDomain) ?? [])].sort();
126
+ if (providers.length === 1) continue;
127
+ if (providers.length === 0) {
128
+ refuse(
129
+ domain,
130
+ aggregate.id,
131
+ `${aggregate.id}.${fieldName} uses ${use.description} '${use.targetId}' from runtime domain ` +
132
+ `'${use.targetDomain}', but package '${input.packageName}' declares no dependency exporting it`,
133
+ `Import the provider's public package contract and add ` +
134
+ `dependencies: [providerPackage.requiresDomains("${use.targetDomain}")].`,
135
+ );
136
+ continue;
137
+ }
138
+ refuse(
139
+ domain,
140
+ aggregate.id,
141
+ `${aggregate.id}.${fieldName} targets runtime domain '${use.targetDomain}', but it is exported by ` +
142
+ `multiple declared packages (${providers.map((provider) => `'${provider}'`).join(", ")})`,
143
+ `Declare only the package that owns '${use.targetDomain}', or namespace the domain so the dependency is unambiguous.`,
144
+ );
145
+ }
146
+ }
147
+ }
148
+ }
149
+ return findings;
150
+ }