@githolon/dsl 0.102.5 → 0.103.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/compile_package.mjs +43 -2
- package/compile_proof.d.mts +47 -0
- package/compile_proof.mjs +343 -0
- package/dart/CHANGELOG.md +14 -0
- package/dart/lib/src/dev_cloud.dart +8 -8
- package/dart/lib/src/nms_generated_runtime.dart +2 -0
- package/dart/lib/src/runner/node-entry.mjs +5539 -1228
- package/dart/lib/src/runner/offline-install.mjs +4578 -528
- package/dart/lib/src/test_harness.dart +206 -152
- package/dart/lib/src/webview_bridge.dart +239 -102
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/acquire_session_credentials_test.dart +4 -3
- package/dart/test/acquire_session_test.dart +9 -0
- package/dart/test/adv_realm_interference_test.dart +7 -6
- package/dart/test/cloud_bound_topology_test.dart +3 -3
- package/dart/test/cluster_births_test.dart +4 -3
- package/dart/test/cluster_device_keyed_acceptance_test.dart +10 -16
- package/dart/test/cluster_local_autosync_test.dart +3 -2
- package/dart/test/cluster_scale_test.dart +3 -2
- package/dart/test/dlq_session_surface_test.dart +7 -6
- package/dart/test/first_app_gauntlet_test.dart +14 -21
- package/dart/test/fixtures/wedge/fake-install.mjs +1 -1
- package/dart/test/four_layer_nested_topology_test.dart +4 -4
- package/dart/test/harness_phase_timeout_test.dart +2 -2
- package/dart/test/local_sync_inert_test.dart +1 -1
- package/dart/test/parent_signer_recovery_test.dart +2 -2
- package/dart/test/sealing_client_test.dart +4 -3
- package/dart/test/session_bound_test.dart +8 -7
- package/dart/test/test_harness_test.dart +13 -12
- package/dart/test/vo_read_typing_test.dart +4 -3
- package/dart/test/watch_after_offer_stall_test.dart +4 -3
- package/dart/test/webview_bridge_test.dart +102 -5
- package/dart_types/CHANGELOG.md +5 -0
- package/dart_types/lib/nomos_types.dart +9 -0
- package/dart_types/lib/src/nomos_ref.dart +20 -0
- package/dart_types/lib/src/optional_ref.dart +85 -0
- package/dart_types/lib/src/sha256_digest.dart +41 -0
- package/dart_types/lib/src/versioned_nomos_ref.dart +81 -0
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/optional_ref_test.dart +44 -0
- package/dart_types/test/sha256_digest_test.dart +18 -0
- package/package.json +9 -5
- package/runtime/generated-application-host.js +5365 -1105
- package/src/aggregate.ts +18 -0
- package/src/application_composition.ts +6 -32
- package/src/attested_core.ts +6 -3
- package/src/birth_meter.ts +1 -0
- package/src/birth_meter_ensure.ts +28 -2
- package/src/births.ts +1 -1
- package/src/build_package.ts +162 -538
- package/src/business_birth.ts +19 -3
- package/src/canonical_json.ts +21 -0
- package/src/check_package_main.ts +73 -33
- package/src/clear_directive.ts +19 -8
- package/src/codegen_dart.ts +305 -22
- package/src/codegen_dart_application.ts +862 -292
- package/src/codegen_dart_client.ts +1084 -407
- package/src/codegen_dart_scenario.ts +12 -12
- package/src/codegen_proof.ts +169 -39
- package/src/codegen_provider_dart.ts +17 -1
- package/src/codegen_ts.ts +298 -106
- package/src/compat_manifest.ts +2 -2
- package/src/compat_package_main.ts +8 -12
- package/src/compile_package_main.ts +1860 -592
- package/src/compose.ts +1 -1
- package/src/directive.ts +208 -15
- package/src/engine_entry.ts +165 -111
- package/src/evolve_preview.ts +29 -69
- package/src/extremum.ts +1 -1
- package/src/fields.ts +183 -4
- package/src/flow_gate.ts +75 -2
- package/src/framework/application_home.ts +2 -0
- package/src/framework/auth_provider_law.ts +2 -2
- package/src/framework/birth_meter.ts +7 -19
- package/src/framework/bootstrap.ts +0 -2
- package/src/framework/business_corrections.ts +9 -5
- package/src/framework/domain_lifecycle.ts +15 -29
- package/src/framework/encryption_scope.ts +2 -1
- package/src/framework/framework_aggregates.ts +2 -0
- package/src/framework/genesis.ts +16 -14
- package/src/framework/home.ts +20 -14
- package/src/framework/owned_custody.ts +9 -3
- package/src/framework/platform_admin.ts +3 -3
- package/src/framework/relations.ts +21 -13
- package/src/framework/signing_identity.ts +45 -40
- package/src/framework/workspace_invariant.ts +245 -0
- package/src/framework/workspaces.ts +23 -29
- package/src/index.ts +36 -16
- package/src/intent.ts +178 -6
- package/src/law_check.ts +32 -11
- package/src/law_graph.ts +2902 -0
- package/src/law_graph_usda.ts +1799 -0
- package/src/law_identity.ts +435 -0
- package/src/law_value_type.ts +230 -0
- package/src/lsp_server.ts +58 -29
- package/src/ops.ts +7 -0
- package/src/package_config.ts +141 -0
- package/src/package_dependencies.ts +108 -0
- package/src/package_dependency_usage.ts +150 -0
- package/src/package_source_imports.ts +71 -0
- package/src/plan_source_slice.ts +335 -0
- package/src/point_guard.ts +192 -0
- package/src/precondition.ts +44 -9
- package/src/read_projection_namespace.ts +1 -1
- package/src/runtime_zod.ts +44 -0
- package/src/secondary_ensure.ts +67 -14
- package/src/shape_compat.ts +1 -1
- package/src/stable_id_identity_bridge.ts +149 -0
- package/src/stable_id_lockfile.ts +140 -6
- package/src/stable_ids.ts +1 -1
- package/src/stable_ids_for_module.ts +348 -0
- package/src/stable_ids_types.ts +1 -1
- package/src/sum.ts +1 -1
- package/src/usd_state.ts +1 -1
- package/src/usda_relationship_integrity.ts +66 -0
- package/src/usda_text.ts +18 -0
- package/src/usdz.ts +78 -0
- package/src/value_object.ts +95 -0
- package/src/wire.ts +19 -0
- package/src/wire_encode.ts +22 -0
- package/src/workspace_sharding.ts +52 -36
- package/src/workspace_sharing.ts +28 -4
- package/src/zod_introspection.ts +1 -1
- package/dart/test/cluster_attested_test.dart +0 -201
- package/src/compile_engine.ts +0 -595
- package/src/manifest.ts +0 -1476
- package/src/read_projection_composition.ts +0 -117
- package/src/read_projection_manifest.ts +0 -115
- package/src/relation_composition.ts +0 -286
- package/src/usd.ts +0 -1967
- 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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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<{
|
|
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
|
|
129
|
-
|
|
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 {
|
|
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
|
-
|
|
167
|
-
|
|
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
|
|
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(".
|
|
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
|
|
199
|
+
const iface = scan(abs) ?? scan(buildDir);
|
|
183
200
|
const lock = path.join(abs, "nomos.stable-ids.json");
|
|
184
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
+
}
|