@githolon/dsl 0.101.6 → 0.102.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dart/CHANGELOG.md +13 -0
- package/dart/lib/nomos_client.dart +3 -52
- package/dart/lib/src/dev_cloud.dart +5 -0
- package/dart/lib/src/home.dart +10 -15
- package/dart/lib/src/nms_conformance_runtime.dart +12 -0
- package/dart/lib/src/nms_generated_runtime.dart +29 -0
- package/dart/lib/src/nms_test_environment.dart +8 -0
- package/dart/lib/src/node_runner_transport.dart +56 -18
- package/dart/lib/src/provider.dart +5 -1
- package/dart/lib/src/record_origin.dart +55 -0
- package/dart/lib/src/runner/node-entry.mjs +9105 -0
- package/dart/lib/src/runner/offline-install.mjs +4729 -0
- package/dart/lib/src/session_bound.dart +23 -10
- package/dart/lib/src/source_trust.dart +51 -0
- package/dart/lib/src/test_harness.dart +490 -58
- package/dart/lib/src/webview_bridge.dart +167 -58
- package/dart/lib/src/workspace_name.dart +7 -1
- package/dart/pubspec.lock +3 -3
- package/dart/pubspec.yaml +4 -2
- package/dart/test/acquire_session_credentials_test.dart +34 -17
- package/dart/test/acquire_session_test.dart +109 -24
- package/dart/test/adv_realm_interference_test.dart +227 -99
- package/dart/test/bundle_version_pairing_test.dart +1 -1
- package/dart/test/cloud_bound_topology_test.dart +1 -1
- package/dart/test/cluster_attested_test.dart +52 -35
- package/dart/test/cluster_births_test.dart +5 -2
- package/dart/test/cluster_device_keyed_acceptance_test.dart +5 -2
- package/dart/test/cluster_local_autosync_test.dart +5 -2
- package/dart/test/cluster_scale_test.dart +5 -2
- package/dart/test/compat_test.dart +1 -1
- package/dart/test/dlq_session_surface_test.dart +2 -1
- package/dart/test/errors_test.dart +1 -1
- package/dart/test/first_app_gauntlet_test.dart +78 -51
- package/dart/test/four_layer_nested_topology_test.dart +1 -1
- package/dart/test/harness_phase_timeout_test.dart +1 -1
- package/dart/test/home_parity_test.dart +9 -5
- package/dart/test/local_sync_inert_test.dart +22 -14
- package/dart/test/node_runner_transport_test.dart +9 -5
- package/dart/test/null_slot_test.dart +1 -1
- package/dart/test/parent_signer_recovery_test.dart +5 -2
- package/dart/test/schema_validation_test.dart +1 -1
- package/dart/test/sealing_client_test.dart +2 -1
- package/dart/test/session_bound_test.dart +92 -119
- package/dart/test/stale_bundle_test.dart +1 -1
- package/dart/test/test_harness_test.dart +77 -22
- package/dart/test/vo_read_typing_test.dart +32 -14
- package/dart/test/watch_after_offer_stall_test.dart +2 -1
- package/dart/test/webview_bridge_test.dart +17 -4
- package/dart/test/wire_test.dart +1 -1
- package/dart_types/CHANGELOG.md +8 -0
- package/dart_types/lib/nomos_types.dart +7 -6
- package/dart_types/lib/src/activity.dart +23 -13
- package/dart_types/lib/src/foreign_ref.dart +61 -112
- package/dart_types/lib/src/invitation.dart +96 -0
- package/dart_types/lib/src/nomos_ref.dart +97 -59
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +125 -19
- package/package.json +7 -3
- package/runtime/generated-application-host.js +8851 -0
- package/src/activity.ts +2 -2
- package/src/aggregate.ts +24 -1
- package/src/application_composition.ts +282 -0
- package/src/application_topology.ts +151 -0
- package/src/attested_core.ts +2 -2
- package/src/authoring_context.ts +9 -0
- package/src/birth_meter_ensure.ts +1 -1
- package/src/births.ts +53 -5
- package/src/build_package.ts +79 -71
- package/src/business_birth.ts +93 -10
- package/src/clear_directive.ts +3 -1
- package/src/codegen_dart.ts +369 -420
- package/src/codegen_dart_application.ts +1508 -0
- package/src/codegen_dart_client.ts +156 -95
- package/src/codegen_dart_scenario.ts +299 -0
- package/src/codegen_openapi.ts +8 -3
- package/src/codegen_proof.ts +1113 -1137
- package/src/codegen_provider_dart.ts +12 -1
- package/src/codegen_ts.ts +1176 -496
- package/src/compat_package_main.ts +22 -10
- package/src/compile_package_main.ts +1536 -337
- package/src/contract.ts +10 -4
- package/src/dart_barrel_guard.ts +5 -1
- package/src/describe_model.ts +13 -5
- package/src/directive.ts +244 -5
- package/src/engine_entry.ts +56 -13
- package/src/engine_javascript.ts +31 -0
- package/src/evolve_preview.ts +3 -12
- package/src/framework/application_home.ts +24 -10
- package/src/framework/capability.ts +15 -1
- package/src/framework/domain_lifecycle.ts +8 -0
- package/src/framework/framework_aggregates.ts +2 -2
- package/src/framework/genesis.ts +11 -1
- package/src/framework/home.ts +178 -75
- package/src/framework/impure_capability.ts +24 -7
- package/src/framework/relations.ts +13 -5
- package/src/framework/workspaces.ts +13 -12
- package/src/generated_client_names.ts +42 -3
- package/src/idof_lint.ts +65 -13
- package/src/index.ts +46 -12
- package/src/intent.ts +587 -0
- package/src/law_check.ts +64 -16
- package/src/precondition.ts +16 -4
- package/src/read_projection_namespace.ts +2 -7
- package/src/recipient.ts +33 -0
- package/src/relation_schema.ts +93 -0
- package/src/secondary_ensure.ts +11 -2
- package/src/shape_compat.ts +11 -1
- package/src/usd.ts +30 -8
- package/src/usd_layers.ts +447 -246
- package/src/usd_state.ts +1 -1
- package/src/value_object.ts +49 -13
- package/src/wire_encode.ts +124 -26
- package/src/workspace_sharing.ts +269 -0
- package/src/zod_introspection.ts +84 -0
- package/dart/example/nomos_secure_key_store.dart +0 -82
- package/dart/example/nomos_webview_transport.dart +0 -63
- package/dart/lib/nomos_headless.dart +0 -50
- package/dart/lib/testing.dart +0 -14
- package/dart/test/co2_product_gauntlet_test.dart +0 -244
- package/src/framework/identity.ts +0 -537
|
@@ -59,7 +59,6 @@ import {
|
|
|
59
59
|
generateDartActivityRegistry,
|
|
60
60
|
collectActivityDescriptors,
|
|
61
61
|
collectPayloadIdOfTargets,
|
|
62
|
-
typedIdClassForAggregateType,
|
|
63
62
|
type DartImport,
|
|
64
63
|
type DartRefImport,
|
|
65
64
|
type DomainModule,
|
|
@@ -67,14 +66,28 @@ import {
|
|
|
67
66
|
} from "./codegen_dart.js";
|
|
68
67
|
import type { ContractHandle } from "./contract.js";
|
|
69
68
|
import { generateDartBridgeClient } from "./codegen_dart_client.js";
|
|
69
|
+
import {
|
|
70
|
+
dartApplicationClassName,
|
|
71
|
+
dartApplicationPublicClassNames,
|
|
72
|
+
generateDartApplication,
|
|
73
|
+
} from "./codegen_dart_application.js";
|
|
74
|
+
import {
|
|
75
|
+
dartScenarioGivenClassName,
|
|
76
|
+
dartScenarioClassName,
|
|
77
|
+
dartTestPersonClassName,
|
|
78
|
+
generateDartScenario,
|
|
79
|
+
} from "./codegen_dart_scenario.js";
|
|
70
80
|
import { buildNomosDescribeModel } from "./describe_model.js";
|
|
71
81
|
import {
|
|
82
|
+
FRAMEWORK_AGGREGATE_IDS,
|
|
83
|
+
FRAMEWORK_DIRECTIVE_IDS,
|
|
72
84
|
isFrameworkAggregate,
|
|
73
85
|
isFrameworkDirective,
|
|
74
86
|
isFrameworkQuery,
|
|
75
87
|
} from "./framework/framework_aggregates.js";
|
|
76
88
|
import { RelationTuple, relationTuplesByObject } from "./framework/relations.js";
|
|
77
89
|
import { isNamespacedAggregate } from "./read_projection_namespace.js";
|
|
90
|
+
import { collectPayloadValueObjectDescriptors } from "./value_object.js";
|
|
78
91
|
import { finishCount, type AnyCount } from "./count.js";
|
|
79
92
|
import type { QueryDecl } from "./query.js";
|
|
80
93
|
import type { SpatialDecl } from "./spatial.js";
|
|
@@ -88,7 +101,6 @@ import {
|
|
|
88
101
|
composeDomainModule,
|
|
89
102
|
directivesOf,
|
|
90
103
|
emitUsdJsonForModules,
|
|
91
|
-
packageUsda,
|
|
92
104
|
sha256HexUtf8,
|
|
93
105
|
type Mod,
|
|
94
106
|
} from "./build_package.js";
|
|
@@ -99,6 +111,10 @@ import { expandBusinessBirthExports } from "./business_birth.js";
|
|
|
99
111
|
import { withOwnedCustodyExports } from "./framework/owned_custody.js";
|
|
100
112
|
import { expandRoleExports, rolesOf } from "./role.js";
|
|
101
113
|
import { collectEngineRouting } from "./engine_entry.js";
|
|
114
|
+
import {
|
|
115
|
+
canonicalizeEngineJavascript,
|
|
116
|
+
canonicalizeGeneratedClientJavascript,
|
|
117
|
+
} from "./engine_javascript.js";
|
|
102
118
|
import {
|
|
103
119
|
discover,
|
|
104
120
|
isCountDecl,
|
|
@@ -113,12 +129,21 @@ import { canonicalTaxonomyFragment, validateWorkspaceTaxonomyAndRoutes } from ".
|
|
|
113
129
|
import { shardingLawModule, type ShardingLawSpec } from "./workspace_sharding.js";
|
|
114
130
|
import { finishExtremum, type AnyExtremum } from "./extremum.js";
|
|
115
131
|
import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
|
|
116
|
-
import {
|
|
132
|
+
import {
|
|
133
|
+
discoverRelationTypeDeclarations,
|
|
134
|
+
mergeRelationTypeDeclarations,
|
|
135
|
+
type RelationTypeDecl,
|
|
136
|
+
} from "./relation_schema.js";
|
|
117
137
|
import {
|
|
118
138
|
emitLayeredDomain,
|
|
119
139
|
flattenLayeredUsd,
|
|
120
140
|
layeredRootUsda,
|
|
141
|
+
nativePlanPackageUsda,
|
|
142
|
+
parseUsdaDocument,
|
|
143
|
+
usdaLayerText,
|
|
121
144
|
type LayeredModuleInput,
|
|
145
|
+
type UsdPlanAsset,
|
|
146
|
+
type UsdPlanRoute,
|
|
122
147
|
} from "./usd_layers.js";
|
|
123
148
|
import { domainManifest, canonicalJson } from "./manifest.js";
|
|
124
149
|
import type { WireSchema } from "./wire.js";
|
|
@@ -130,7 +155,7 @@ import {
|
|
|
130
155
|
import { emitUsd, type UsdLayer } from "./usd.js";
|
|
131
156
|
import { emitLayeredRelationUsda, type RelationLayerInput } from "./relation_composition.js";
|
|
132
157
|
import { assertNoDartBarrelCollisions, type BarrelExportedFile } from "./dart_barrel_guard.js";
|
|
133
|
-
import { generateTsClient, tsClientFactoryName, recursiveLazyTypes } from "./codegen_ts.js";
|
|
158
|
+
import { generateTsClient, tsApplicationName, tsClientFactoryName, recursiveLazyTypes } from "./codegen_ts.js";
|
|
134
159
|
import { generateOpenApiFragment, mergeOpenApiFragments } from "./codegen_openapi.js";
|
|
135
160
|
import { domainInterface, type DomainInterface } from "./shape_compat.js";
|
|
136
161
|
import { generateTsProof, proofLegsIndex } from "./codegen_proof.js";
|
|
@@ -138,6 +163,14 @@ import { checkDomainModules, formatFindings, hasRefusal } from "./law_check.js";
|
|
|
138
163
|
import type { NomosCompat } from "./compat_manifest.js";
|
|
139
164
|
import { requiredKernelFeatures, type FeatureProbeModule } from "./kernel_capabilities.js";
|
|
140
165
|
import { APPLICATION_HOME_DOMAIN } from "./framework/application_home_domain.js";
|
|
166
|
+
import { resolveApplicationTopology } from "./application_topology.js";
|
|
167
|
+
import { isBusinessIntent } from "./intent.js";
|
|
168
|
+
import {
|
|
169
|
+
compileApplicationComposition,
|
|
170
|
+
isApplicationCompositionConfig,
|
|
171
|
+
packageAssetUsda,
|
|
172
|
+
type PackageRootPart,
|
|
173
|
+
} from "./application_composition.js";
|
|
141
174
|
|
|
142
175
|
const DSL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
143
176
|
|
|
@@ -188,6 +221,8 @@ type DartSupportImport = "vendored" | "package";
|
|
|
188
221
|
interface PackageConfig {
|
|
189
222
|
readonly name: string;
|
|
190
223
|
readonly domains: readonly DomainConfig[];
|
|
224
|
+
/** The one OpenUSD model root this independently published package exports. */
|
|
225
|
+
readonly rootPart?: PackageRootPart;
|
|
191
226
|
/** reportId → "modulePath#exportName" (the export is `(actor) => Report`). */
|
|
192
227
|
readonly reports?: Record<string, string>;
|
|
193
228
|
readonly outDir?: string;
|
|
@@ -195,7 +230,7 @@ interface PackageConfig {
|
|
|
195
230
|
* Typed DART frontend (the Flutter sibling of the generated TS client; #codegen_dart).
|
|
196
231
|
* `false`/absent = skip (default). `true` = emit into `<outDir>/dart/`;
|
|
197
232
|
* `{ out: "relative/dir" }` = emit there (resolved against the config dir).
|
|
198
|
-
* `{ support: "package" }` keeps
|
|
233
|
+
* `{ support: "package" }` keeps the framework-internal runtime import for generated Flutter applications that
|
|
199
234
|
* already depend on `nomos_client`/`nomos_flutter`. The default `"vendored"` mode preserves the old
|
|
200
235
|
* self-contained directory.
|
|
201
236
|
* Per composed domain one `<name>.dart` (payload classes + read models + typed read
|
|
@@ -224,7 +259,7 @@ interface PackageConfig {
|
|
|
224
259
|
* pure `nomos_types` core — frontend-safe: no bridge, no sessions, no lifecycle.
|
|
225
260
|
* • `client` — the effectful runtime clients (`<name>.client.dart`, offer methods over
|
|
226
261
|
* NomosBridge). Depends on the types package + `nomos_client`.
|
|
227
|
-
* • `test` — optional;
|
|
262
|
+
* • `test` — optional; generated business scenarios for widget/integration tests.
|
|
228
263
|
* The layering line: UI code depends on `types`; composition/runtime code on `client`;
|
|
229
264
|
* generated TYPES may flow to the UI, generated CLIENTS may not.
|
|
230
265
|
* Each package's `pubspec.yaml` is scaffolded ONLY IF MISSING (tenant-owned versioning);
|
|
@@ -271,26 +306,8 @@ interface PackageConfig {
|
|
|
271
306
|
readonly names?: Readonly<Record<string, string>>;
|
|
272
307
|
};
|
|
273
308
|
/**
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
* authored MODULE — and `build/<name>.layered.usda`, a root whose subLayers stack
|
|
277
|
-
* them (USD strongest-first == Nomos module order reversed). The flattened
|
|
278
|
-
* `.package.usda` emission and the domainHash DO NOT MOVE; the compile asserts
|
|
279
|
-
* fail-closed that the layer stack flattens BYTE-IDENTICALLY to the canonical
|
|
280
|
-
* composed IR (the monoid homomorphism — see `usd_layers.ts` /
|
|
281
|
-
* `docs/usd_layered_composition.md`).
|
|
282
|
-
*/
|
|
283
|
-
readonly layered?: boolean;
|
|
284
|
-
/**
|
|
285
|
-
* THE COMPACT PACKAGE ERA (opt-in; also `--compact` / NOMOS_COMPACT=1). ONE batched,
|
|
286
|
-
* NAMED domainHash move that shrinks the package ~4x (guestbook 1.48MB → ~0.35MB):
|
|
287
|
-
* • minified engine lump (esbuild minify — string literals, and so refusal codes, unchanged);
|
|
288
|
-
* • zod locales stripped to `en` only (the locale barrel is 35% of every lump and is
|
|
289
|
-
* namespace-re-exported so tree-shaking can't drop it; `en` keeps error messages
|
|
290
|
-
* byte-identical to today's default);
|
|
291
|
-
* • base64 blob fields (`*B64`, 1.33x text) instead of hex (2x).
|
|
292
|
-
* REQUIRES the whole consuming stack (cloud kernel + clients) at ≥ the release that shipped
|
|
293
|
-
* the decode arms. Default stays off until a future major; hex packages parse forever.
|
|
309
|
+
* Minify each content-addressed execution plan asset. Native USD law remains readable and
|
|
310
|
+
* unchanged; this only changes the implementation asset bytes and therefore the package hash.
|
|
294
311
|
*/
|
|
295
312
|
readonly compact?: boolean;
|
|
296
313
|
}
|
|
@@ -300,6 +317,67 @@ function fail(msg: string): never {
|
|
|
300
317
|
process.exit(1);
|
|
301
318
|
}
|
|
302
319
|
|
|
320
|
+
/** Locate stock OpenUSD's usdcat without making it a runtime dependency. */
|
|
321
|
+
function resolveUsdcat(): string | undefined {
|
|
322
|
+
const candidates = [process.env.NOMOS_USDCAT, "/usr/bin/usdcat", "usdcat"]
|
|
323
|
+
.filter((candidate): candidate is string => typeof candidate === "string" && candidate.length > 0);
|
|
324
|
+
for (const candidate of [...new Set(candidates)]) {
|
|
325
|
+
try {
|
|
326
|
+
execFileSync(candidate, ["--help"], { stdio: "ignore" });
|
|
327
|
+
return candidate;
|
|
328
|
+
} catch {
|
|
329
|
+
// Try the next stock-tool location.
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return undefined;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Name the first structural disagreement between two canonical law values. */
|
|
336
|
+
function firstLawDifference(expected: unknown, actual: unknown, at = "$law"): string | undefined {
|
|
337
|
+
if (Object.is(expected, actual)) return undefined;
|
|
338
|
+
if (Array.isArray(expected) || Array.isArray(actual)) {
|
|
339
|
+
if (!Array.isArray(expected) || !Array.isArray(actual)) {
|
|
340
|
+
return `${at}: expected ${Array.isArray(expected) ? "an array" : typeof expected}, got ${Array.isArray(actual) ? "an array" : typeof actual}`;
|
|
341
|
+
}
|
|
342
|
+
if (expected.length !== actual.length) {
|
|
343
|
+
const ids = (items: unknown[]): string[] => items.flatMap((item) =>
|
|
344
|
+
item !== null && typeof item === "object" && typeof (item as { id?: unknown }).id === "string"
|
|
345
|
+
? [(item as { id: string }).id]
|
|
346
|
+
: []);
|
|
347
|
+
const expectedIds = ids(expected);
|
|
348
|
+
const actualIds = ids(actual);
|
|
349
|
+
const missing = expectedIds.filter((id) => !actualIds.includes(id));
|
|
350
|
+
const extra = actualIds.filter((id) => !expectedIds.includes(id));
|
|
351
|
+
return `${at}.length: expected ${expected.length}, got ${actual.length}` +
|
|
352
|
+
`${missing.length > 0 ? `; missing [${missing.join(", ")}]` : ""}` +
|
|
353
|
+
`${extra.length > 0 ? `; extra [${extra.join(", ")}]` : ""}`;
|
|
354
|
+
}
|
|
355
|
+
for (let index = 0; index < expected.length; index++) {
|
|
356
|
+
const difference = firstLawDifference(expected[index], actual[index], `${at}[${index}]`);
|
|
357
|
+
if (difference !== undefined) return difference;
|
|
358
|
+
}
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
if (expected !== null && actual !== null && typeof expected === "object" && typeof actual === "object") {
|
|
362
|
+
const expectedObject = expected as Record<string, unknown>;
|
|
363
|
+
const actualObject = actual as Record<string, unknown>;
|
|
364
|
+
if (typeof expectedObject.path === "string" && expectedObject.path === actualObject.path) {
|
|
365
|
+
at = `${at}{path=${expectedObject.path}}`;
|
|
366
|
+
}
|
|
367
|
+
const expectedKeys = Object.keys(expectedObject).sort();
|
|
368
|
+
const actualKeys = Object.keys(actualObject).sort();
|
|
369
|
+
if (JSON.stringify(expectedKeys) !== JSON.stringify(actualKeys)) {
|
|
370
|
+
return `${at}: expected keys [${expectedKeys.join(", ")}], got [${actualKeys.join(", ")}]`;
|
|
371
|
+
}
|
|
372
|
+
for (const key of expectedKeys) {
|
|
373
|
+
const difference = firstLawDifference(expectedObject[key], actualObject[key], `${at}.${key}`);
|
|
374
|
+
if (difference !== undefined) return difference;
|
|
375
|
+
}
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
return `${at}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`;
|
|
379
|
+
}
|
|
380
|
+
|
|
303
381
|
// ── decl auto-discovery (by SHAPE, mirroring the engine entry's duck-types) ───────
|
|
304
382
|
// The query/workspace-invariant/spatial/sum/count predicates + `discover` live in the SHARED
|
|
305
383
|
// bundle-safe `decl_shapes.ts` (imported above) — the lump's self-describe verb discovers by the
|
|
@@ -432,7 +510,7 @@ function resolveNamed<T>(merged: Mod, names: readonly string[] | undefined, what
|
|
|
432
510
|
* Vendored output rewrites it to the relative barrel so the directory is
|
|
433
511
|
* self-contained (no pub path-dependency on the framework package).
|
|
434
512
|
*/
|
|
435
|
-
const DART_PACKAGE_IMPORT = `import 'package:nomos_client/
|
|
513
|
+
const DART_PACKAGE_IMPORT = `import 'package:nomos_client/src/nms_generated_runtime.dart';`;
|
|
436
514
|
const DART_RELATIVE_IMPORT = `import 'nomos_client.dart';`;
|
|
437
515
|
|
|
438
516
|
/**
|
|
@@ -482,10 +560,50 @@ const DART_SUPPORT_CORE_SRC = [
|
|
|
482
560
|
// `package:nomos_types/...` imports are rewritten to the local vendored shim (see emitDart).
|
|
483
561
|
// (`home.dart` is NOT vendored — the home-onboarding runtime drags `package:crypto`; nothing the
|
|
484
562
|
// codegen emits references it.)
|
|
485
|
-
const DART_SUPPORT_RUNTIME_SRC = [
|
|
563
|
+
const DART_SUPPORT_RUNTIME_SRC = [
|
|
564
|
+
"provider.dart",
|
|
565
|
+
"webview_bridge.dart",
|
|
566
|
+
"session_bound.dart",
|
|
567
|
+
"record_origin.dart",
|
|
568
|
+
] as const;
|
|
486
569
|
const DART_SUPPORT_SRC = [...DART_SUPPORT_CORE_SRC, ...DART_SUPPORT_RUNTIME_SRC] as const;
|
|
487
570
|
/** The local shim replacing `package:nomos_types/nomos_types.dart` inside vendored runtime files. */
|
|
488
571
|
const VENDORED_TYPES_SHIM = "nomos_types.dart";
|
|
572
|
+
const VENDORED_META_DEP = "^1.12.0";
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Vendored runtime files carry package:meta's @internal boundary. Keep the
|
|
576
|
+
* containing Dart package honest by declaring that implementation dependency
|
|
577
|
+
* instead of stripping the annotation from generated copies.
|
|
578
|
+
*/
|
|
579
|
+
function ensureVendoredMetaDependency(dartOut: string): void {
|
|
580
|
+
let dir = path.resolve(dartOut);
|
|
581
|
+
let pubspec: string | undefined;
|
|
582
|
+
for (let hops = 0; hops < 8; hops++) {
|
|
583
|
+
const candidate = path.join(dir, "pubspec.yaml");
|
|
584
|
+
if (existsSync(candidate)) {
|
|
585
|
+
pubspec = candidate;
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
const parent = path.dirname(dir);
|
|
589
|
+
if (parent === dir) break;
|
|
590
|
+
dir = parent;
|
|
591
|
+
}
|
|
592
|
+
if (pubspec === undefined) return;
|
|
593
|
+
const source = readFileSync(pubspec, "utf8");
|
|
594
|
+
if (/^ meta:\s*/m.test(source)) return;
|
|
595
|
+
if (!/^dependencies:\s*$/m.test(source)) {
|
|
596
|
+
fail("generated Dart vendored support needs a dependencies: block in " + pubspec);
|
|
597
|
+
}
|
|
598
|
+
writeFileSync(
|
|
599
|
+
pubspec,
|
|
600
|
+
source.replace(
|
|
601
|
+
/^dependencies:\s*$/m,
|
|
602
|
+
"dependencies:\n meta: " + VENDORED_META_DEP,
|
|
603
|
+
),
|
|
604
|
+
"utf8",
|
|
605
|
+
);
|
|
606
|
+
}
|
|
489
607
|
|
|
490
608
|
/** The generated-owned subdirectory, relative to `dartOut` (the pub package's `lib/`). WIPED and
|
|
491
609
|
* rewritten on every compile — nothing hand-written may live in it. The stable facade (the package
|
|
@@ -510,14 +628,6 @@ function collectPackageContracts(modules: readonly DomainModule[]): ContractHand
|
|
|
510
628
|
return [...byId.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
511
629
|
}
|
|
512
630
|
|
|
513
|
-
/** Every aggregate's generated typed-id class name across the package — the collision guard for the
|
|
514
|
-
* contract typed-id emission (a contract named after a local aggregate must not re-declare `<X>Id`). */
|
|
515
|
-
function packageAggregateIdClassNames(modules: readonly DomainModule[]): Set<string> {
|
|
516
|
-
const out = new Set<string>();
|
|
517
|
-
for (const m of modules) for (const a of m.aggregates) out.add(typedIdClassForAggregateType(a.id));
|
|
518
|
-
return out;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
631
|
/**
|
|
522
632
|
* CROSS-DOMAIN REF TRAVERSAL map (dart_codegen_parity.md §2a follow-on): every aggregate id in the package
|
|
523
633
|
* → its owning domain (`domain` for the generated client class name; `name` for the sibling
|
|
@@ -527,11 +637,19 @@ function packageAggregateIdClassNames(modules: readonly DomainModule[]): Set<str
|
|
|
527
637
|
*/
|
|
528
638
|
function buildPackageAggregateDomains(
|
|
529
639
|
modules: readonly DomainModule[],
|
|
530
|
-
): Record<string, { domain: string; name: string }> {
|
|
531
|
-
const map: Record<
|
|
640
|
+
): Record<string, { domain: string; name: string; namespaced: boolean }> {
|
|
641
|
+
const map: Record<
|
|
642
|
+
string,
|
|
643
|
+
{ domain: string; name: string; namespaced: boolean }
|
|
644
|
+
> = {};
|
|
532
645
|
for (const m of modules) {
|
|
533
|
-
const
|
|
534
|
-
|
|
646
|
+
for (const agg of m.aggregates) {
|
|
647
|
+
map[agg.id] = {
|
|
648
|
+
domain: m.domain ?? m.name,
|
|
649
|
+
name: m.name,
|
|
650
|
+
namespaced: isNamespacedAggregate(agg),
|
|
651
|
+
};
|
|
652
|
+
}
|
|
535
653
|
}
|
|
536
654
|
return map;
|
|
537
655
|
}
|
|
@@ -565,7 +683,7 @@ function guardNoConcreteImportCycle(
|
|
|
565
683
|
fail(
|
|
566
684
|
`domain contracts: concrete cross-domain import cycle between '${from}' and '${to}' — their ` +
|
|
567
685
|
`generated Dart files would import each other (a t.foreignRef in each targets a concrete aggregate ` +
|
|
568
|
-
`owned by the other). Break it with a
|
|
686
|
+
`owned by the other). Break it with a record contract: declare recordContract(...) for the shared shape, ` +
|
|
569
687
|
`.implements(...) it on the owning aggregate, and point the foreignRef at the CONTRACT (it resolves ` +
|
|
570
688
|
`through the shared contracts.dart, keeping the domain graph acyclic).`,
|
|
571
689
|
);
|
|
@@ -574,12 +692,77 @@ function guardNoConcreteImportCycle(
|
|
|
574
692
|
}
|
|
575
693
|
}
|
|
576
694
|
|
|
695
|
+
/**
|
|
696
|
+
* Close the duplicate direct-birth presentation lane after the whole package has been composed.
|
|
697
|
+
*
|
|
698
|
+
* A `birth(A)` declaration is reusable law. When another domain publishes
|
|
699
|
+
* `intent(...).births(thatDeclaration)`, however, the wider intent is the ONE tenant action: exposing
|
|
700
|
+
* the declaration's direct `nomosBirthA` route as well would let callers skip the intent's other
|
|
701
|
+
* consequences. `business_birth.ts` can suppress that route by object identity when module evaluation
|
|
702
|
+
* happens in the convenient order, but package domains are deliberately lazy and their order must not
|
|
703
|
+
* change the public API. Resolve the ownership semantically across the completed package instead.
|
|
704
|
+
*
|
|
705
|
+
* This is presentation-only. The direct directive remains installed law for genesis/replay; generated
|
|
706
|
+
* clients, public OpenAPI and proofs see it as an implementation detail. Authority is unaffected.
|
|
707
|
+
*/
|
|
708
|
+
function closeComposedBusinessBirthPresentation(
|
|
709
|
+
modules: readonly DomainModule[],
|
|
710
|
+
): DomainModule[] {
|
|
711
|
+
type BirthMetadata = {
|
|
712
|
+
aggregateId?: unknown;
|
|
713
|
+
rootId?: unknown;
|
|
714
|
+
namespace?: unknown;
|
|
715
|
+
keyField?: unknown;
|
|
716
|
+
};
|
|
717
|
+
type BirthDirective = DomainModule["directives"][number] & {
|
|
718
|
+
businessBirth?: BirthMetadata;
|
|
719
|
+
businessBirthInternal?: true;
|
|
720
|
+
};
|
|
721
|
+
const keyOf = (directive: BirthDirective): string | undefined => {
|
|
722
|
+
const birth = directive.businessBirth;
|
|
723
|
+
if (
|
|
724
|
+
birth === undefined ||
|
|
725
|
+
typeof birth.aggregateId !== "string" ||
|
|
726
|
+
typeof birth.rootId !== "string" ||
|
|
727
|
+
typeof birth.namespace !== "string" ||
|
|
728
|
+
typeof birth.keyField !== "string"
|
|
729
|
+
) return undefined;
|
|
730
|
+
return [birth.aggregateId, birth.rootId, birth.namespace, birth.keyField].join("\u0000");
|
|
731
|
+
};
|
|
732
|
+
|
|
733
|
+
const composedBirths = new Set<string>();
|
|
734
|
+
for (const mod of modules) {
|
|
735
|
+
for (const directive of mod.directives as BirthDirective[]) {
|
|
736
|
+
if (!isBusinessIntent(directive)) continue;
|
|
737
|
+
const key = keyOf(directive);
|
|
738
|
+
if (key !== undefined) composedBirths.add(key);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (composedBirths.size === 0) return [...modules];
|
|
742
|
+
|
|
743
|
+
const hideDirectBirth = (directive: BirthDirective): BirthDirective => {
|
|
744
|
+
if (isBusinessIntent(directive)) return directive;
|
|
745
|
+
const key = keyOf(directive);
|
|
746
|
+
return key !== undefined && composedBirths.has(key)
|
|
747
|
+
? { ...directive, businessBirthInternal: true }
|
|
748
|
+
: directive;
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
return modules.map((mod) => ({
|
|
752
|
+
...mod,
|
|
753
|
+
directives: (mod.directives as BirthDirective[]).map(hideDirectBirth),
|
|
754
|
+
...(mod.applicationIntents === undefined
|
|
755
|
+
? {}
|
|
756
|
+
: { applicationIntents: (mod.applicationIntents as BirthDirective[]).map(hideDirectBirth) }),
|
|
757
|
+
}));
|
|
758
|
+
}
|
|
759
|
+
|
|
577
760
|
/** FRAMEWORK-SHARED symbol dedup, shared by the flat emit and the layered-packages emit (Dart-codegen
|
|
578
761
|
* hygiene): framework-owned symbols fold into MANY domains, so their generated Dart would be DEFINED
|
|
579
762
|
* once per domain — a package barrel re-exporting ≥2 domains then COLLIDES. THREE kinds are deduped,
|
|
580
763
|
* each identified by the framework BRAND (object identity, so a tenant symbol that merely shares a
|
|
581
764
|
* name is never misclassified) and emitted EXACTLY ONCE into `nomos_framework.dart`:
|
|
582
|
-
* • AGGREGATES — read-model class
|
|
765
|
+
* • AGGREGATES — projection/read-model class and by-id accessors;
|
|
583
766
|
* • DIRECTIVES — the generated payload class (the domain still OFFERS them per-domain);
|
|
584
767
|
* • QUERIES — the top-level `watch<Q>`/`read<Q>` accessors.
|
|
585
768
|
* Plus VALUE OBJECTS used by ≥2 domains (same collision rule). Per-domain files SKIP each and
|
|
@@ -593,6 +776,40 @@ interface DartSharedSymbols {
|
|
|
593
776
|
/** The synthetic module whose emit IS `nomos_framework.dart` — undefined when nothing is shared. */
|
|
594
777
|
readonly frameworkMod: DomainModule | undefined;
|
|
595
778
|
}
|
|
779
|
+
|
|
780
|
+
/** Every named value object whose Dart type this domain needs, including payload-only VOs pulled
|
|
781
|
+
* across a domain boundary by a composed business birth. `mod.valueObjects` only sees aggregate
|
|
782
|
+
* fields; without the payload half a parent `births(childBirth)` re-emits the child's VO locally and
|
|
783
|
+
* the flat package barrel becomes ambiguous. Count once per domain, not once per use. */
|
|
784
|
+
function dartValueObjectsForModule(
|
|
785
|
+
mod: DomainModule,
|
|
786
|
+
): NonNullable<DomainModule["valueObjects"]> {
|
|
787
|
+
const codegenDirectives = (mod.applicationIntents ?? mod.directives)
|
|
788
|
+
.filter((d) => (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true)
|
|
789
|
+
.map((d) => {
|
|
790
|
+
const birth = (d as {
|
|
791
|
+
businessBirth?: { publicPayloadSchema?: unknown };
|
|
792
|
+
}).businessBirth;
|
|
793
|
+
return birth?.publicPayloadSchema === undefined
|
|
794
|
+
? d
|
|
795
|
+
: { ...d, payloadSchema: birth.publicPayloadSchema };
|
|
796
|
+
});
|
|
797
|
+
const byName = new Map<string, NonNullable<DomainModule["valueObjects"]>[number]>();
|
|
798
|
+
for (const descriptor of [
|
|
799
|
+
...(mod.valueObjects ?? []),
|
|
800
|
+
...collectPayloadValueObjectDescriptors(codegenDirectives),
|
|
801
|
+
]) {
|
|
802
|
+
const prior = byName.get(descriptor.name);
|
|
803
|
+
if (prior !== undefined && JSON.stringify(prior) !== JSON.stringify(descriptor)) {
|
|
804
|
+
throw new Error(
|
|
805
|
+
`value object '${descriptor.name}' has DIVERGENT aggregate/payload shapes in domain '${mod.domain ?? mod.name}'`,
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
byName.set(descriptor.name, descriptor);
|
|
809
|
+
}
|
|
810
|
+
return [...byName.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
811
|
+
}
|
|
812
|
+
|
|
596
813
|
function collectDartSharedSymbols(
|
|
597
814
|
modules: readonly DomainModule[],
|
|
598
815
|
packageContracts: readonly ContractHandle[] = [],
|
|
@@ -616,13 +833,13 @@ function collectDartSharedSymbols(
|
|
|
616
833
|
const voDomainsByName = new Map<string, number>();
|
|
617
834
|
const voDescriptorByName = new Map<string, NonNullable<DomainModule["valueObjects"]>[number]>();
|
|
618
835
|
for (const mod of modules) {
|
|
619
|
-
for (const d of mod
|
|
836
|
+
for (const d of dartValueObjectsForModule(mod)) {
|
|
620
837
|
voDomainsByName.set(d.name, (voDomainsByName.get(d.name) ?? 0) + 1);
|
|
621
838
|
voDescriptorByName.set(d.name, d);
|
|
622
839
|
}
|
|
623
840
|
}
|
|
624
841
|
// CONTRACT-USED VOs ARE LAYER-ZERO (co2 #117): a VO referenced by a contract FIELD (e.g. a
|
|
625
|
-
// `ReferenceCode.field()` inside `
|
|
842
|
+
// `ReferenceCode.field()` inside `recordContract(...)`) is rendered inside `contracts.dart` — the shared layer
|
|
626
843
|
// every domain imports and which may import NO domain file (the acyclic rule). So its class must live in
|
|
627
844
|
// `nomos_framework.dart` REGARDLESS of how many domains use it (a single-domain VO would otherwise land in
|
|
628
845
|
// that domain's file, unreachable from contracts.dart). Transitive deps included (a nested VO renders too).
|
|
@@ -694,7 +911,7 @@ function domainDedupOpts(mod: DomainModule, shared: DartSharedSymbols): {
|
|
|
694
911
|
(modForDart.queries ?? []).filter((q) => shared.frameworkQueryIds.has(q.id)).map((q) => q.id),
|
|
695
912
|
);
|
|
696
913
|
const skipValueObjectNames = new Set(
|
|
697
|
-
(modForDart
|
|
914
|
+
dartValueObjectsForModule(modForDart).filter((d) => shared.sharedVoNames.has(d.name)).map((d) => d.name),
|
|
698
915
|
);
|
|
699
916
|
const foldsFramework =
|
|
700
917
|
skipAggregateIds.size > 0 || skipDirectiveIds.size > 0 || skipQueryIds.size > 0 || skipValueObjectNames.size > 0;
|
|
@@ -799,6 +1016,7 @@ function emitDart(
|
|
|
799
1016
|
// 1. optional vendored framework support (barrel + src/), copied byte-for-byte except
|
|
800
1017
|
// the barrel's schema_validation export (see DART_SUPPORT_SRC).
|
|
801
1018
|
if (supportImport === "vendored") {
|
|
1019
|
+
ensureVendoredMetaDependency(dartOut);
|
|
802
1020
|
mkdirSync(path.join(genDir, "src"), { recursive: true });
|
|
803
1021
|
// Core files come from the pure `nomos_types` package, runtime files from `nomos_client`
|
|
804
1022
|
// (the layered split). Runtime files import the core via `package:nomos_types/...` — the
|
|
@@ -807,10 +1025,15 @@ function emitDart(
|
|
|
807
1025
|
const clientDir = path.join(DSL_DIR, "dart", "lib", "src");
|
|
808
1026
|
for (const f of DART_SUPPORT_CORE_SRC) copyFileSync(path.join(coreDir, f), path.join(genDir, "src", f));
|
|
809
1027
|
for (const f of DART_SUPPORT_RUNTIME_SRC) {
|
|
810
|
-
const src = readFileSync(path.join(clientDir, f), "utf8")
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1028
|
+
const src = readFileSync(path.join(clientDir, f), "utf8")
|
|
1029
|
+
.replace(
|
|
1030
|
+
/import 'package:nomos_types\/nomos_types\.dart'/g,
|
|
1031
|
+
`import '${VENDORED_TYPES_SHIM}'`,
|
|
1032
|
+
)
|
|
1033
|
+
.replace(
|
|
1034
|
+
/import 'package:nomos_types\/src\/(?:nomos_ref|foreign_ref)\.dart'/g,
|
|
1035
|
+
`import '${VENDORED_TYPES_SHIM}'`,
|
|
1036
|
+
);
|
|
814
1037
|
writeFileSync(path.join(genDir, "src", f), src, "utf8");
|
|
815
1038
|
}
|
|
816
1039
|
// The local shim the rewritten runtime imports resolve through (the vendored twin of the
|
|
@@ -848,7 +1071,7 @@ function emitDart(
|
|
|
848
1071
|
// readSignerDelegationByUser, …` band-aid). THREE kinds are deduped, each identified by the
|
|
849
1072
|
// framework BRAND (object identity, so a tenant symbol that merely shares a name is never
|
|
850
1073
|
// misclassified) and emitted EXACTLY ONCE into `nomos_framework.dart`:
|
|
851
|
-
// • AGGREGATES — read-model class
|
|
1074
|
+
// • AGGREGATES — projection/read-model class and by-id accessors (`RelationTuple` auto-added
|
|
852
1075
|
// to every relation-using domain, `SignerDelegation`/`KeyScope`/… re-exported verbatim);
|
|
853
1076
|
// • DIRECTIVES — the generated payload class (relate/signer/enroll family). The domain still
|
|
854
1077
|
// OFFERS them (client offer methods stay per-domain); only the payload TYPE moves to the shared file;
|
|
@@ -891,7 +1114,6 @@ function emitDart(
|
|
|
891
1114
|
if (packageContracts.length > 0) {
|
|
892
1115
|
writeGenerated(CONTRACTS_FILE, generateDartContracts(packageContracts, {
|
|
893
1116
|
...(frameworkImportUri !== undefined ? { frameworkImportUri } : {}),
|
|
894
|
-
aggregateIdClassNames: packageAggregateIdClassNames(modules),
|
|
895
1117
|
}));
|
|
896
1118
|
}
|
|
897
1119
|
// 2a-ter. the shared ACCOUNTABLE ACTIVITY + DESCRIPTION REGISTRY (`activity_registry.dart`) — emitted
|
|
@@ -951,7 +1173,7 @@ function emitDart(
|
|
|
951
1173
|
}
|
|
952
1174
|
}
|
|
953
1175
|
// AUTO cross-domain `idOf(Agg)` imports (dart_codegen_parity.md P2): a payload field typed with a
|
|
954
|
-
// SIBLING
|
|
1176
|
+
// SIBLING aggregate's generated type needs that domain's generated file, exactly as a
|
|
955
1177
|
// foreignRef target does. Same-domain idOf targets declare their id class locally — skip them.
|
|
956
1178
|
for (const target of collectPayloadIdOfTargets(modForDart.directives)) {
|
|
957
1179
|
const targetFile = aggIdToDartFile.get(target);
|
|
@@ -967,6 +1189,7 @@ function emitDart(
|
|
|
967
1189
|
const modForDartWithImports = mergedDartImports.length > 0 ? { ...modForDart, dartImports: mergedDartImports } : modForDart;
|
|
968
1190
|
const src = generateDartDomain(modForDartWithImports, {
|
|
969
1191
|
...skips,
|
|
1192
|
+
packageAggregateDomains,
|
|
970
1193
|
...(domainFrameworkImport !== undefined ? { frameworkImportUri: domainFrameworkImport } : {}),
|
|
971
1194
|
});
|
|
972
1195
|
writeGenerated(mod.name, src);
|
|
@@ -980,11 +1203,25 @@ function emitDart(
|
|
|
980
1203
|
emitActivity: true,
|
|
981
1204
|
activityImportUri,
|
|
982
1205
|
describeModel,
|
|
1206
|
+
});
|
|
1207
|
+
const applicationSrc = generateDartApplication(modForDart, {
|
|
1208
|
+
supportImport,
|
|
983
1209
|
packageAggregateDomains,
|
|
984
1210
|
});
|
|
985
|
-
const
|
|
986
|
-
|
|
1211
|
+
const clientFile = `${mod.name}.client.dart`;
|
|
1212
|
+
const applicationFile = `${mod.name}.application.dart`;
|
|
1213
|
+
const encapsulated = encapsulateDartClient(
|
|
1214
|
+
clientSrc,
|
|
1215
|
+
applicationSrc,
|
|
1216
|
+
clientFile,
|
|
1217
|
+
applicationFile,
|
|
1218
|
+
);
|
|
1219
|
+
const cp = path.join(genDir, clientFile);
|
|
1220
|
+
writeFileSync(cp, encapsulated.clientPart, "utf8");
|
|
987
1221
|
written.push(cp);
|
|
1222
|
+
const ap = path.join(genDir, `${mod.name}.application.dart`);
|
|
1223
|
+
writeFileSync(ap, encapsulated.applicationLibrary, "utf8");
|
|
1224
|
+
written.push(ap);
|
|
988
1225
|
}
|
|
989
1226
|
|
|
990
1227
|
// 3. THE PACKAGE BARREL — the stable public facade (`dartOut/<barrelName>.dart`), generated-owned
|
|
@@ -1024,15 +1261,24 @@ function emitDart(
|
|
|
1024
1261
|
.map((f) => `${genRel}/${f}.dart`);
|
|
1025
1262
|
})(),
|
|
1026
1263
|
),
|
|
1264
|
+
// The generated clients import their runtime support directly. Keeping it out of the public barrel is
|
|
1265
|
+
// the surface firewall: a tenant import receives business types and application APIs, never raw runtime.
|
|
1027
1266
|
supportImport === "vendored"
|
|
1028
|
-
? `export '${genRel}/
|
|
1029
|
-
: `export 'package:
|
|
1030
|
-
...(
|
|
1031
|
-
|
|
1032
|
-
|
|
1267
|
+
? `export '${genRel}/src/${VENDORED_TYPES_SHIM}' show NomosRef;`
|
|
1268
|
+
: `export 'package:nomos_types/nomos_types.dart' show NomosRef;`,
|
|
1269
|
+
...(contractsImportUri !== undefined
|
|
1270
|
+
? tenantDartExport(`${genRel}/${CONTRACTS_FILE}.dart`, sourceByFile.get(CONTRACTS_FILE) ?? "")
|
|
1271
|
+
: []),
|
|
1272
|
+
...(activityImportUri !== undefined
|
|
1273
|
+
? tenantDartExport(
|
|
1274
|
+
`${genRel}/${ACTIVITY_FILE}.dart`,
|
|
1275
|
+
sourceByFile.get(ACTIVITY_FILE) ?? "",
|
|
1276
|
+
["renderActivity", "activityNarrative"],
|
|
1277
|
+
)
|
|
1278
|
+
: []),
|
|
1033
1279
|
...modules.flatMap((m) => [
|
|
1034
|
-
|
|
1035
|
-
`export '${genRel}/${m.name}.
|
|
1280
|
+
...tenantDartExport(`${genRel}/${m.name}.dart`, sourceByFile.get(m.name) ?? ""),
|
|
1281
|
+
`export '${genRel}/${m.name}.application.dart' show ${dartApplicationPublicClassNames(m).join(", ")};`,
|
|
1036
1282
|
]),
|
|
1037
1283
|
...dartAliasLines(
|
|
1038
1284
|
names,
|
|
@@ -1052,17 +1298,23 @@ function emitDart(
|
|
|
1052
1298
|
// ── the LAYERED-PACKAGES Dart emit (dart.packages: blast-radius isolation) ────────
|
|
1053
1299
|
|
|
1054
1300
|
/** The pure-core support import generated TYPES files carry in layered-packages mode. */
|
|
1055
|
-
const DART_TYPES_IMPORT =
|
|
1301
|
+
const DART_TYPES_IMPORT = [
|
|
1302
|
+
`import 'package:nomos_types/nomos_types.dart';`,
|
|
1303
|
+
`import 'package:nomos_types/src/nomos_ref.dart' show NmsInternalNomosRefs;`,
|
|
1304
|
+
`import 'package:nomos_types/src/foreign_ref.dart' show NmsInternalForeignRefs;`,
|
|
1305
|
+
`import 'package:nomos_types/src/invitation.dart' show NmsInternalInvitations, NmsInternalRecipients;`,
|
|
1306
|
+
].join("\n");
|
|
1056
1307
|
/** Version constraints the scaffolded pubspecs pin — move together with the Dart packages' releases.
|
|
1057
1308
|
* Caret on 0.x is EXACT-MINOR (`^0.45.0` = `<0.46.0`), so these must track the CURRENT release, not
|
|
1058
1309
|
* just the floor generated code needs — a lagging pin forces tenants into dependency_overrides. */
|
|
1059
1310
|
// 0.3.0: SurfacedValue<T>/SurfacedObservation/PendingConflictRow — generated Surfaced-field typing +
|
|
1060
1311
|
// the pendingConflicts read rows.
|
|
1061
|
-
const NOMOS_TYPES_DEP = "^0.
|
|
1312
|
+
const NOMOS_TYPES_DEP = "^0.8.0";
|
|
1062
1313
|
// 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
|
|
1063
1314
|
// DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
|
|
1064
1315
|
// REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
|
|
1065
|
-
const NOMOS_CLIENT_DEP = "^0.
|
|
1316
|
+
const NOMOS_CLIENT_DEP = "^0.64.0";
|
|
1317
|
+
const NOMOS_FLUTTER_DEP = "^0.63.0";
|
|
1066
1318
|
// DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
|
|
1067
1319
|
// and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
|
|
1068
1320
|
// land back in dependency_overrides (the exact co2 report this closes, third occurrence).
|
|
@@ -1078,13 +1330,327 @@ const aliasImportLines = (names: Readonly<Record<string, string>> | undefined, u
|
|
|
1078
1330
|
* The alias validator and the cursed-name report both read this surface. */
|
|
1079
1331
|
function declaredDartTypeNames(src: string): Set<string> {
|
|
1080
1332
|
const names = new Set<string>();
|
|
1081
|
-
const re = /^(?:abstract\s+|final\s+|sealed\s+)*(?:class|enum|mixin|extension type)\s+([A-Za-z][A-Za-z0-9_]*)/gm;
|
|
1333
|
+
const re = /^(?:abstract\s+|base\s+|final\s+|sealed\s+|interface\s+|mixin\s+)*(?:class|enum|mixin|extension type)\s+(?:const\s+)?([A-Za-z][A-Za-z0-9_]*)/gm;
|
|
1082
1334
|
for (let m = re.exec(src); m !== null; m = re.exec(src)) {
|
|
1083
1335
|
if (!m[1]!.startsWith("_")) names.add(m[1]!);
|
|
1084
1336
|
}
|
|
1085
1337
|
return names;
|
|
1086
1338
|
}
|
|
1087
1339
|
|
|
1340
|
+
/** Public declarations safe for the generated application barrel. Runtime/framework names stay in the
|
|
1341
|
+
* implementation packages even when a generated source file imports or declares them. */
|
|
1342
|
+
function tenantDartTypeNames(src: string): string[] {
|
|
1343
|
+
return [...declaredDartTypeNames(src)]
|
|
1344
|
+
// Framework-owned source files are excluded at the file boundary. Do not blanket-hide the `Nomos…`
|
|
1345
|
+
// product vocabulary generated into business files: describe/activity types such as NomosModel and
|
|
1346
|
+
// NomosDescription are required by the generated application. Only the parallel workspace-less ID
|
|
1347
|
+
// wrappers are forbidden here.
|
|
1348
|
+
.sort();
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/** Named Dart extensions are part of the tenant API too. A `show` barrel that exports the
|
|
1352
|
+
* read-model class but omits its generated conflict extension makes documented getters disappear
|
|
1353
|
+
* at the canonical package import, forcing tenants to deep-import generated files. */
|
|
1354
|
+
function tenantDartExtensionNames(src: string): string[] {
|
|
1355
|
+
const names = new Set<string>();
|
|
1356
|
+
const re = /^extension\s+([A-Za-z][A-Za-z0-9_]*)\s+(?:<[^\n>]+>\s+)?on\s+/gm;
|
|
1357
|
+
for (let match = re.exec(src); match !== null; match = re.exec(src)) {
|
|
1358
|
+
if (!match[1]!.startsWith("_")) names.add(match[1]!);
|
|
1359
|
+
}
|
|
1360
|
+
return [...names].sort();
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function tenantDartExportNames(src: string): string[] {
|
|
1364
|
+
return [...new Set([...tenantDartTypeNames(src), ...tenantDartExtensionNames(src)])].sort();
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Turn the generated bridge client into a Dart `part` of the business application library.
|
|
1369
|
+
*
|
|
1370
|
+
* `lib/src` is only a convention: a tenant can deep-import a normal Dart library. A part file is a
|
|
1371
|
+
* language boundary — it cannot be imported on its own — and its underscore-prefixed declarations are
|
|
1372
|
+
* private to the generated application library. This makes the raw bind/open client mechanically
|
|
1373
|
+
* unavailable while leaving the business application as the sole exported entry point.
|
|
1374
|
+
*/
|
|
1375
|
+
function encapsulateDartClient(
|
|
1376
|
+
clientSrc: string,
|
|
1377
|
+
applicationSrc: string,
|
|
1378
|
+
clientFile: string,
|
|
1379
|
+
applicationFile: string,
|
|
1380
|
+
): { readonly clientPart: string; readonly applicationLibrary: string } {
|
|
1381
|
+
const directive = /^(import|export)\s+'([^']+)'(?:\s+[^;]+)?;\s*$/;
|
|
1382
|
+
const clientImportUri = clientFile.replace(/\.dart$/, "");
|
|
1383
|
+
const split = (src: string) => {
|
|
1384
|
+
const directives: string[] = [];
|
|
1385
|
+
const body: string[] = [];
|
|
1386
|
+
const ignores: string[] = [];
|
|
1387
|
+
for (const line of src.split("\n")) {
|
|
1388
|
+
if (line.startsWith("// ignore_for_file:")) {
|
|
1389
|
+
ignores.push(line);
|
|
1390
|
+
} else if (directive.test(line)) {
|
|
1391
|
+
directives.push(line);
|
|
1392
|
+
} else {
|
|
1393
|
+
body.push(line);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return { directives, body, ignores };
|
|
1397
|
+
};
|
|
1398
|
+
const client = split(clientSrc);
|
|
1399
|
+
const application = split(applicationSrc);
|
|
1400
|
+
|
|
1401
|
+
// Every implementation-side declared type and named extension becomes genuinely library-private.
|
|
1402
|
+
const implementationNames = new Set(declaredDartTypeNames(clientSrc));
|
|
1403
|
+
for (const match of clientSrc.matchAll(/^extension\s+([A-Za-z][A-Za-z0-9_]*)\s+/gm)) {
|
|
1404
|
+
if (match[1] !== undefined && !match[1].startsWith("_")) implementationNames.add(match[1]);
|
|
1405
|
+
}
|
|
1406
|
+
for (const match of clientSrc.matchAll(/^const\s+(?:[A-Za-z][A-Za-z0-9_<>, ?]*\s+)?([a-z][A-Za-z0-9_]*)\s*=/gm)) {
|
|
1407
|
+
if (match[1] !== undefined && !match[1].startsWith("_")) implementationNames.add(match[1]);
|
|
1408
|
+
}
|
|
1409
|
+
const privateNames = new Map(
|
|
1410
|
+
[...implementationNames].map((name) => [name, `_Nms${name}`] as const),
|
|
1411
|
+
);
|
|
1412
|
+
const applicationNames = new Set(declaredDartTypeNames(applicationSrc));
|
|
1413
|
+
const privatize = (src: string, preserve: ReadonlySet<string> = new Set()): string => {
|
|
1414
|
+
let out = src;
|
|
1415
|
+
for (const [name, privateName] of privateNames) {
|
|
1416
|
+
if (preserve.has(name)) continue;
|
|
1417
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1418
|
+
out = out.replace(new RegExp(`\\b${escaped}\\b`, "g"), privateName);
|
|
1419
|
+
}
|
|
1420
|
+
return out;
|
|
1421
|
+
};
|
|
1422
|
+
|
|
1423
|
+
// Merge imports/exports into the application library. Prefer an unrestricted import over a `show`
|
|
1424
|
+
// import of the same URI because the client implementation necessarily uses the fuller surface.
|
|
1425
|
+
const imports = new Map<string, string>();
|
|
1426
|
+
const exports = new Map<string, string>();
|
|
1427
|
+
for (const line of [...client.directives, ...application.directives]) {
|
|
1428
|
+
const match = directive.exec(line);
|
|
1429
|
+
if (match === null) continue;
|
|
1430
|
+
const [, kind, uri] = match;
|
|
1431
|
+
if (kind === "import" && (uri === clientFile || uri === clientImportUri)) continue;
|
|
1432
|
+
// Preserve business-type re-exports so one application import carries its
|
|
1433
|
+
// payloads and read models. Framework/runtime exports belonged to the retired
|
|
1434
|
+
// bridge library and must not enter tenant autocomplete.
|
|
1435
|
+
if (
|
|
1436
|
+
kind === "export" &&
|
|
1437
|
+
client.directives.includes(line) &&
|
|
1438
|
+
/(?:^|\/)(?:nomos_framework|nomos_client)\.dart$/.test(uri!)
|
|
1439
|
+
) continue;
|
|
1440
|
+
const target = kind === "import" ? imports : exports;
|
|
1441
|
+
const prior = target.get(uri!);
|
|
1442
|
+
if (prior === undefined || (/\sshow\s/.test(prior) && !/\sshow\s/.test(line))) {
|
|
1443
|
+
target.set(uri!, line);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
return {
|
|
1448
|
+
clientPart: [
|
|
1449
|
+
"// AUTO-GENERATED by nomos-compile — private implementation part; importing this file is invalid.",
|
|
1450
|
+
...client.ignores,
|
|
1451
|
+
"// ignore_for_file: unused_element, unused_element_parameter",
|
|
1452
|
+
`part of '${applicationFile}';`,
|
|
1453
|
+
"",
|
|
1454
|
+
privatize(client.body.join("\n")).trimStart(),
|
|
1455
|
+
"",
|
|
1456
|
+
].join("\n"),
|
|
1457
|
+
applicationLibrary: [
|
|
1458
|
+
"// AUTO-GENERATED by nomos-compile — the sole importable application library.",
|
|
1459
|
+
"// ignore_for_file: unused_import, unused_field, invalid_use_of_internal_member",
|
|
1460
|
+
...imports.values(),
|
|
1461
|
+
...exports.values(),
|
|
1462
|
+
`part '${clientFile}';`,
|
|
1463
|
+
"",
|
|
1464
|
+
privatize(application.body.join("\n"), applicationNames).trimStart(),
|
|
1465
|
+
"",
|
|
1466
|
+
].join("\n"),
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
const dartApplicationMember = (name: string): string => {
|
|
1471
|
+
const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
1472
|
+
const joined = words.length === 0
|
|
1473
|
+
? "domain"
|
|
1474
|
+
: words[0]!.toLowerCase() + words.slice(1).map((word) => word[0]!.toUpperCase() + word.slice(1)).join("");
|
|
1475
|
+
return /^(?:assert|break|case|catch|class|const|continue|default|do|else|enum|extends|false|final|for|if|in|is|new|null|rethrow|return|super|switch|this|throw|true|try|var|void|while|with|dynamic)$/.test(joined)
|
|
1476
|
+
? `${joined}_`
|
|
1477
|
+
: joined;
|
|
1478
|
+
};
|
|
1479
|
+
|
|
1480
|
+
function generatedPackageApplication(
|
|
1481
|
+
packageName: string,
|
|
1482
|
+
domains: readonly {
|
|
1483
|
+
readonly domain: string;
|
|
1484
|
+
readonly file: string;
|
|
1485
|
+
readonly invitationPolicies: readonly {
|
|
1486
|
+
readonly domainHash: string;
|
|
1487
|
+
readonly query: string;
|
|
1488
|
+
readonly redeem: string;
|
|
1489
|
+
readonly personField: string;
|
|
1490
|
+
readonly roleField: string;
|
|
1491
|
+
readonly scopeField: string;
|
|
1492
|
+
}[];
|
|
1493
|
+
}[],
|
|
1494
|
+
homeDomain: string,
|
|
1495
|
+
rootDomains: readonly string[],
|
|
1496
|
+
): string {
|
|
1497
|
+
const application = dartApplicationClassName(packageName);
|
|
1498
|
+
const root = domains.find(({ domain }) => domain === homeDomain);
|
|
1499
|
+
if (root === undefined) {
|
|
1500
|
+
throw new Error(`generated Dart package application cannot find its home domain '${homeDomain}'`);
|
|
1501
|
+
}
|
|
1502
|
+
const members = domains
|
|
1503
|
+
.filter(({ domain }) => rootDomains.includes(domain))
|
|
1504
|
+
.map(({ domain, file, invitationPolicies }, index) => ({
|
|
1505
|
+
domain,
|
|
1506
|
+
file,
|
|
1507
|
+
prefix: `_nmsDomain${index}`,
|
|
1508
|
+
member: dartApplicationMember(domain),
|
|
1509
|
+
application: dartApplicationClassName(domain),
|
|
1510
|
+
invitationPolicies,
|
|
1511
|
+
}));
|
|
1512
|
+
if (members.length !== rootDomains.length) {
|
|
1513
|
+
throw new Error("generated Dart package application could not bind every co-resident root domain");
|
|
1514
|
+
}
|
|
1515
|
+
const invitationPolicies = members.flatMap(({ domain, invitationPolicies }) =>
|
|
1516
|
+
invitationPolicies.map((policy) => ({ domain, ...policy })));
|
|
1517
|
+
const hasInvitations = invitationPolicies.length > 0;
|
|
1518
|
+
const duplicate = members.find((candidate, index) =>
|
|
1519
|
+
members.findIndex((other) => other.member === candidate.member) !== index);
|
|
1520
|
+
if (duplicate !== undefined) {
|
|
1521
|
+
throw new Error(
|
|
1522
|
+
`generated Dart package application member '${duplicate.member}' is ambiguous; rename one colliding domain`,
|
|
1523
|
+
);
|
|
1524
|
+
}
|
|
1525
|
+
const frameworkMembers = new Map<string, string>([
|
|
1526
|
+
["nomosWidget", "the generated Nomos Flutter bootstrap"],
|
|
1527
|
+
["currentUser", "the generated signed-in application participant"],
|
|
1528
|
+
["currentStatus", "the generated application status"],
|
|
1529
|
+
["watchStatus", "the generated application status watch"],
|
|
1530
|
+
["recipient", "the generated typed recipient boundary"],
|
|
1531
|
+
["createInvitation", "the generated invitation capability"],
|
|
1532
|
+
["redeemInvitation", "the generated invitation redemption"],
|
|
1533
|
+
]);
|
|
1534
|
+
const frameworkCollision = members.find(({ member }) => frameworkMembers.has(member));
|
|
1535
|
+
if (frameworkCollision !== undefined) {
|
|
1536
|
+
throw new Error(
|
|
1537
|
+
`Sorry mate — domain '${frameworkCollision.domain}' claims generated Dart application member ` +
|
|
1538
|
+
`'${frameworkCollision.member}', which is already owned by ${frameworkMembers.get(frameworkCollision.member)}. ` +
|
|
1539
|
+
`Nomos will not silently rename or reroute it; rename the tenant domain and compile again.`,
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
return [
|
|
1543
|
+
`// AUTO-GENERATED by nomos-compile — the one tenant application for "${packageName}".`,
|
|
1544
|
+
`// Domain clients remain generated implementation details behind this stateful business surface.`,
|
|
1545
|
+
`// ignore_for_file: invalid_use_of_internal_member`,
|
|
1546
|
+
`import 'package:flutter/widgets.dart';`,
|
|
1547
|
+
`import 'package:nomos_flutter/src/application_status.dart' show ApplicationSignIn, ApplicationStatus;`,
|
|
1548
|
+
`import 'package:nomos_client/src/nms_generated_runtime.dart' show NmsInternalUsers, NomosSession, User${hasInvitations ? ", Invitation, Recipient" : ""};`,
|
|
1549
|
+
`import 'package:nomos_flutter/src/generated_application_host.dart' show NmsInternalApplicationHost, NmsInternalApplicationUserBinding${hasInvitations ? ", NmsInternalInvitationPolicy" : ""};`,
|
|
1550
|
+
...members.map(({ file, prefix }) =>
|
|
1551
|
+
`import '${file}.application.dart' as ${prefix};`),
|
|
1552
|
+
``,
|
|
1553
|
+
`/// The one canonical stateful application for ${packageName}.`,
|
|
1554
|
+
`///`,
|
|
1555
|
+
`/// Keep business UI and services on this object. Nomos owns identity, persistence,`,
|
|
1556
|
+
`/// synchronization, workspace navigation, access delivery and recovery underneath it;`,
|
|
1557
|
+
`/// do not build a second runtime host, repository, session, or application-state mirror.`,
|
|
1558
|
+
`///`,
|
|
1559
|
+
`/// Canonical bootstrap:`,
|
|
1560
|
+
"/// ```dart",
|
|
1561
|
+
`/// return ${application}.nomosWidget(`,
|
|
1562
|
+
`/// cloud: cloud,`,
|
|
1563
|
+
`/// application: '${packageName}',`,
|
|
1564
|
+
`/// signIn: signIn,`,
|
|
1565
|
+
`/// builder: (context, application) => MyBusinessUi(application: application),`,
|
|
1566
|
+
`/// );`,
|
|
1567
|
+
"/// ```",
|
|
1568
|
+
`/// Pass that same [${application}] down. It is the application state.`,
|
|
1569
|
+
`final class ${application} {`,
|
|
1570
|
+
` final NomosSession _nmsSession;`,
|
|
1571
|
+
...(hasInvitations ? [` final NmsInternalApplicationUserBinding _nmsUser;`] : []),
|
|
1572
|
+
` final User currentUser;`,
|
|
1573
|
+
...members.flatMap(({ domain, member, application: child, prefix }) => [
|
|
1574
|
+
` /// Business actions, records and watches declared by ${domain}.`,
|
|
1575
|
+
` final ${prefix}.${child} ${member};`,
|
|
1576
|
+
]),
|
|
1577
|
+
` const ${application}._({required NomosSession nmsSession, ${hasInvitations ? "required NmsInternalApplicationUserBinding nmsUser, " : ""}required this.currentUser, ${members.map(({ member }) => `required this.${member}`).join(", ")}}) : _nmsSession = nmsSession${hasInvitations ? ", _nmsUser = nmsUser" : ""};`,
|
|
1578
|
+
``,
|
|
1579
|
+
` /// Current durability and convergence across this business application.`,
|
|
1580
|
+
` ApplicationStatus get currentStatus => NmsInternalApplicationHost.currentStatus(_nmsSession);`,
|
|
1581
|
+
``,
|
|
1582
|
+
` /// React whenever this application saves, receives work, goes offline or needs attention.`,
|
|
1583
|
+
` Stream<ApplicationStatus> watchStatus() => NmsInternalApplicationHost.watchStatus(_nmsSession);`,
|
|
1584
|
+
...(hasInvitations
|
|
1585
|
+
? [
|
|
1586
|
+
``,
|
|
1587
|
+
` /// Turn an identity-provider uid into a typed invitation recipient.`,
|
|
1588
|
+
` Recipient<User> recipient(String uid) => NmsInternalApplicationHost.recipient<User>(uid);`,
|
|
1589
|
+
``,
|
|
1590
|
+
` /// Create an invitation; transport its link however your product chooses.`,
|
|
1591
|
+
` Future<Invitation<User>> createInvitation() => NmsInternalApplicationHost.createInvitation<User>(_nmsSession);`,
|
|
1592
|
+
``,
|
|
1593
|
+
` /// Redeem a transported invitation as [currentUser].`,
|
|
1594
|
+
` Future<void> redeemInvitation(String link) => NmsInternalApplicationHost.redeemInvitation<User>(`,
|
|
1595
|
+
` _nmsSession,`,
|
|
1596
|
+
` link: link,`,
|
|
1597
|
+
` identity: _nmsUser.nmsInternalActor,`,
|
|
1598
|
+
` policies: [`,
|
|
1599
|
+
...invitationPolicies.flatMap((policy) => [
|
|
1600
|
+
` NmsInternalInvitationPolicy(`,
|
|
1601
|
+
` domain: ${JSON.stringify(policy.domain)},`,
|
|
1602
|
+
` domainHash: ${JSON.stringify(policy.domainHash)},`,
|
|
1603
|
+
` query: ${JSON.stringify(policy.query)},`,
|
|
1604
|
+
` redeem: ${JSON.stringify(policy.redeem)},`,
|
|
1605
|
+
` personField: ${JSON.stringify(policy.personField)},`,
|
|
1606
|
+
` roleField: ${JSON.stringify(policy.roleField)},`,
|
|
1607
|
+
` scopeField: ${JSON.stringify(policy.scopeField)},`,
|
|
1608
|
+
` ),`,
|
|
1609
|
+
]),
|
|
1610
|
+
` ],`,
|
|
1611
|
+
` );`,
|
|
1612
|
+
]
|
|
1613
|
+
: []),
|
|
1614
|
+
``,
|
|
1615
|
+
` /// Mount the one application at the UI boundary and pass [application] down.`,
|
|
1616
|
+
` ///`,
|
|
1617
|
+
` /// This is the only application bootstrap tenant code should own.`,
|
|
1618
|
+
` ///`,
|
|
1619
|
+
` /// The object supplied to [builder] is the application state. Keep it as the only`,
|
|
1620
|
+
` /// business-state dependency; do not recreate its records in providers, repositories or caches.`,
|
|
1621
|
+
` static Widget nomosWidget({`,
|
|
1622
|
+
` Key? key,`,
|
|
1623
|
+
` required String cloud,`,
|
|
1624
|
+
` required String application,`,
|
|
1625
|
+
` required ApplicationSignIn signIn,`,
|
|
1626
|
+
` required Widget Function(BuildContext context, ${application} application) builder,`,
|
|
1627
|
+
` Widget Function(BuildContext context)? loadingBuilder,`,
|
|
1628
|
+
` Widget Function(BuildContext context, Object error, VoidCallback retry)? errorBuilder,`,
|
|
1629
|
+
` }) => NmsInternalApplicationHost.home<${application}>(`,
|
|
1630
|
+
` key: key, cloud: cloud, application: application, signIn: signIn,`,
|
|
1631
|
+
` bind: nomosInternalBind${application}, builder: builder,`,
|
|
1632
|
+
` loadingBuilder: loadingBuilder, errorBuilder: errorBuilder,`,
|
|
1633
|
+
` );`,
|
|
1634
|
+
`}`,
|
|
1635
|
+
``,
|
|
1636
|
+
`Future<${application}> nomosInternalBind${application}(`,
|
|
1637
|
+
` NomosSession session, NmsInternalApplicationUserBinding user,`,
|
|
1638
|
+
`) async => ${application}._(`,
|
|
1639
|
+
` nmsSession: session,`,
|
|
1640
|
+
...(hasInvitations ? [` nmsUser: user,`] : []),
|
|
1641
|
+
` currentUser: NmsInternalUsers.create(identityId: user.nmsInternalIdentityId, identityAddress: user.nmsInternalIdentityAddress),`,
|
|
1642
|
+
...members.map(({ member, application: child, prefix }) =>
|
|
1643
|
+
` ${member}: await ${prefix}.nomosInternalBind${child}(session, user),`),
|
|
1644
|
+
`);`,
|
|
1645
|
+
``,
|
|
1646
|
+
].join("\n");
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function tenantDartExport(uri: string, src: string, publicFunctions: readonly string[] = []): string[] {
|
|
1650
|
+
const names = [...tenantDartExportNames(src), ...publicFunctions].sort();
|
|
1651
|
+
return names.length === 0 ? [] : [`export '${uri}' show ${names.join(", ")};`];
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1088
1654
|
/** `typedef <alias> = <generated>;` lines for the barrel, validated FAIL-CLOSED against the emitted
|
|
1089
1655
|
* types surface: every target must exist, no alias may collide with a generated name or another alias. */
|
|
1090
1656
|
function dartAliasLines(names: Readonly<Record<string, string>> | undefined, declared: ReadonlySet<string>): string[] {
|
|
@@ -1153,6 +1719,7 @@ function emitDartPackages(
|
|
|
1153
1719
|
names: Readonly<Record<string, string>> | undefined,
|
|
1154
1720
|
compat?: NomosCompat,
|
|
1155
1721
|
describePackageName = packages.client,
|
|
1722
|
+
deployJsonPath?: string,
|
|
1156
1723
|
): { written: string[]; cursed: string[] } {
|
|
1157
1724
|
for (const [role, name] of Object.entries(packages)) {
|
|
1158
1725
|
if (typeof name !== "string" || !isPubPackageName(name)) {
|
|
@@ -1206,6 +1773,9 @@ function emitDartPackages(
|
|
|
1206
1773
|
// banning the Dart SDK. Allow every dart:* import; keep the cross-package allowlist otherwise.
|
|
1207
1774
|
!m[1]!.startsWith("dart:") &&
|
|
1208
1775
|
m[1] !== "package:nomos_types/nomos_types.dart" &&
|
|
1776
|
+
m[1] !== "package:nomos_types/src/nomos_ref.dart" &&
|
|
1777
|
+
m[1] !== "package:nomos_types/src/foreign_ref.dart" &&
|
|
1778
|
+
m[1] !== "package:nomos_types/src/invitation.dart" &&
|
|
1209
1779
|
m[1] !== `${FRAMEWORK_FILE}.dart` &&
|
|
1210
1780
|
!siblingTypeFiles.has(m[1]!)
|
|
1211
1781
|
) {
|
|
@@ -1228,7 +1798,6 @@ function emitDartPackages(
|
|
|
1228
1798
|
if (packageContracts.length > 0) {
|
|
1229
1799
|
writeTypes(CONTRACTS_FILE, generateDartContracts(packageContracts, {
|
|
1230
1800
|
...(shared.frameworkMod !== undefined ? { frameworkImportUri: `${FRAMEWORK_FILE}.dart` } : {}),
|
|
1231
|
-
aggregateIdClassNames: packageAggregateIdClassNames(modules),
|
|
1232
1801
|
}));
|
|
1233
1802
|
}
|
|
1234
1803
|
// ACTIVITY + DESCRIPTION REGISTRY (layered): universal, including packages with empty history.
|
|
@@ -1258,7 +1827,7 @@ function emitDartPackages(
|
|
|
1258
1827
|
}
|
|
1259
1828
|
}
|
|
1260
1829
|
// AUTO cross-domain `idOf(Agg)` imports (dart_codegen_parity.md P2): a payload field typed with a
|
|
1261
|
-
// SIBLING
|
|
1830
|
+
// SIBLING aggregate's generated type needs that domain's TYPES file (a permitted sibling import).
|
|
1262
1831
|
for (const target of collectPayloadIdOfTargets(dd.modForDart.directives)) {
|
|
1263
1832
|
const tf = aggIdToTypeFile.get(target);
|
|
1264
1833
|
if (tf !== undefined && tf !== dd.modForDart.name) frImports.add(`${tf}.dart`);
|
|
@@ -1272,6 +1841,7 @@ function emitDartPackages(
|
|
|
1272
1841
|
mod.name,
|
|
1273
1842
|
generateDartDomain(modForTypes, {
|
|
1274
1843
|
...dd.skips,
|
|
1844
|
+
packageAggregateDomains: buildPackageAggregateDomains(modules),
|
|
1275
1845
|
...(dd.foldsFramework ? { frameworkImportUri: `${FRAMEWORK_FILE}.dart` } : {}),
|
|
1276
1846
|
}),
|
|
1277
1847
|
);
|
|
@@ -1288,6 +1858,14 @@ function emitDartPackages(
|
|
|
1288
1858
|
// but a file no alias references needs no import (the `export` re-exports it). Importing every file
|
|
1289
1859
|
// produced unused_import warnings in the tenant's barrel (co2 feedback).
|
|
1290
1860
|
const aliasTargets = new Set(Object.keys(names ?? {}));
|
|
1861
|
+
const frameworkAliasTargets = [...aliasTargets].filter((name) =>
|
|
1862
|
+
(declaredByFile.get(FRAMEWORK_FILE) ?? new Set<string>()).has(name));
|
|
1863
|
+
if (frameworkAliasTargets.length > 0) {
|
|
1864
|
+
fail(
|
|
1865
|
+
`dart.names cannot publish Nomos implementation types (${frameworkAliasTargets.sort().join(", ")}) — ` +
|
|
1866
|
+
`name business concepts declared by the tenant domains instead`,
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1291
1869
|
const aliasImportUris = [...typesSources.keys()]
|
|
1292
1870
|
.filter((f) => [...(declaredByFile.get(f) ?? [])].some((n) => aliasTargets.has(n)))
|
|
1293
1871
|
.map((f) => `${genRel}/${f}.dart`);
|
|
@@ -1308,6 +1886,37 @@ function emitDartPackages(
|
|
|
1308
1886
|
const typesExportKeys = explicitBarrel
|
|
1309
1887
|
? [...typesSources.keys()].filter((f) => !domainFileBases.has(f))
|
|
1310
1888
|
: [...typesSources.keys()];
|
|
1889
|
+
// Framework-shared types exist only so generated implementation parts can
|
|
1890
|
+
// compile. They are not tenant vocabulary and must never enter either
|
|
1891
|
+
// public package barrel (SignerDelegation/EnrollDelegationPayload and the
|
|
1892
|
+
// like otherwise become tempting autocomplete footguns).
|
|
1893
|
+
const tenantTypesExportKeys = typesExportKeys.filter((f) => f !== FRAMEWORK_FILE);
|
|
1894
|
+
const frameworkSource = typesSources.get(FRAMEWORK_FILE) ?? "";
|
|
1895
|
+
// Shared business value objects may be emitted into the private framework
|
|
1896
|
+
// implementation file because several domains reuse them. Promote those
|
|
1897
|
+
// shared values and their transitive variant members, but mechanically
|
|
1898
|
+
// exclude every framework aggregate/directive family and Nomos-prefixed type.
|
|
1899
|
+
const sharedBusinessTypeNames = tenantDartTypeNames(frameworkSource).filter(
|
|
1900
|
+
(name) =>
|
|
1901
|
+
!name.startsWith("Nomos") &&
|
|
1902
|
+
![...FRAMEWORK_AGGREGATE_IDS].some((id) => name.startsWith(id)) &&
|
|
1903
|
+
![...FRAMEWORK_DIRECTIVE_IDS].some((id) => name === `${id[0]!.toUpperCase()}${id.slice(1)}Payload`),
|
|
1904
|
+
);
|
|
1905
|
+
const businessSharedFile = "business_shared.dart";
|
|
1906
|
+
const businessSharedPath = path.join(typesGen, businessSharedFile);
|
|
1907
|
+
writeFileSync(
|
|
1908
|
+
businessSharedPath,
|
|
1909
|
+
[
|
|
1910
|
+
"// GENERATED by nomos-compile — shared business values and activity; no runtime machinery.",
|
|
1911
|
+
`export 'package:nomos_types/nomos_types.dart' show NomosRef, Identity, User, Recipient, Invitation, ActivityEvent, ActivityRef, ActivitySeverity, SurfacedObservation, SurfacedValue, SurfacedResolved, SurfacedPending, SurfacedEvolutionRequired;`,
|
|
1912
|
+
...(sharedBusinessTypeNames.length === 0
|
|
1913
|
+
? []
|
|
1914
|
+
: [`export '${FRAMEWORK_FILE}.dart' show ${sharedBusinessTypeNames.join(", ")};`]),
|
|
1915
|
+
"",
|
|
1916
|
+
].join("\n"),
|
|
1917
|
+
"utf8",
|
|
1918
|
+
);
|
|
1919
|
+
written.push(businessSharedPath);
|
|
1311
1920
|
const explicitBarrelNote = explicitBarrel
|
|
1312
1921
|
? [
|
|
1313
1922
|
`// barrelExports: 'explicit' — per-domain aggregate/read-model classes are NOT flat-exported here.`,
|
|
@@ -1319,7 +1928,7 @@ function emitDartPackages(
|
|
|
1319
1928
|
// under `barrelExports: 'explicit'` per-domain files are withheld, so they cannot collide there.
|
|
1320
1929
|
try {
|
|
1321
1930
|
assertNoDartBarrelCollisions(
|
|
1322
|
-
|
|
1931
|
+
tenantTypesExportKeys.map((f): BarrelExportedFile => ({ label: f, src: typesSources.get(f) ?? "" })),
|
|
1323
1932
|
);
|
|
1324
1933
|
} catch (e) {
|
|
1325
1934
|
fail((e as Error).message);
|
|
@@ -1332,8 +1941,13 @@ function emitDartPackages(
|
|
|
1332
1941
|
"",
|
|
1333
1942
|
...explicitBarrelNote,
|
|
1334
1943
|
...aliasImportLines(barrelNames, barrelAliasImportUris),
|
|
1335
|
-
`export '
|
|
1336
|
-
...
|
|
1944
|
+
`export '${genRel}/${businessSharedFile}';`,
|
|
1945
|
+
...tenantTypesExportKeys.flatMap((f) =>
|
|
1946
|
+
tenantDartExport(
|
|
1947
|
+
`${genRel}/${f}.dart`,
|
|
1948
|
+
typesSources.get(f) ?? "",
|
|
1949
|
+
f === ACTIVITY_FILE ? ["renderActivity", "activityNarrative"] : [],
|
|
1950
|
+
)),
|
|
1337
1951
|
...dartAliasLines(barrelNames, declared),
|
|
1338
1952
|
"",
|
|
1339
1953
|
];
|
|
@@ -1358,8 +1972,17 @@ function emitDartPackages(
|
|
|
1358
1972
|
// ── the CLIENT package — effectful runtime, depends on types + nomos_client ──
|
|
1359
1973
|
const clientGen = freshGenDir(packages.client);
|
|
1360
1974
|
const typesImportUri = `package:${packages.types}/${packages.types}.dart`;
|
|
1975
|
+
const implementationTypeImports = explicitBarrel
|
|
1976
|
+
? modules.map((m) => `package:${packages.types}/${genRel}/${m.name}.dart`)
|
|
1977
|
+
: [];
|
|
1978
|
+
const frameworkImplementationImport = typesSources.has(FRAMEWORK_FILE)
|
|
1979
|
+
? `package:${packages.types}/${genRel}/${FRAMEWORK_FILE}.dart`
|
|
1980
|
+
: undefined;
|
|
1981
|
+
const frameworkTypeNames = frameworkImplementationImport === undefined
|
|
1982
|
+
? []
|
|
1983
|
+
: [...declaredDartTypeNames(typesSources.get(FRAMEWORK_FILE) ?? "")];
|
|
1361
1984
|
for (const { mod, dd } of perDomain) {
|
|
1362
|
-
const
|
|
1985
|
+
const makeClient = (imports: readonly string[]) => generateDartBridgeClient(dd.modForDart, {
|
|
1363
1986
|
domainHash,
|
|
1364
1987
|
interface: packageInterface[dd.modForDart.domain ?? dd.modForDart.name],
|
|
1365
1988
|
...(compat !== undefined ? { compat } : {}),
|
|
@@ -1367,27 +1990,121 @@ function emitDartPackages(
|
|
|
1367
1990
|
typesImportUri,
|
|
1368
1991
|
emitActivity: true,
|
|
1369
1992
|
describeModel,
|
|
1370
|
-
|
|
1993
|
+
// Cross-domain record traversal lives on the application facade. Keeping
|
|
1994
|
+
// the package map out of the private client prevents sibling client imports.
|
|
1371
1995
|
// Under 'explicit' the types barrel withholds per-domain classes — import every domain's TYPES
|
|
1372
1996
|
// file directly so the client's read-model/payload/ref/typed-id references resolve. (The no-dup-
|
|
1373
1997
|
// symbol guarantee across domain files means an over-broad set never introduces ambiguity.)
|
|
1374
|
-
...(
|
|
1375
|
-
? { explicitTypeImports:
|
|
1998
|
+
...(imports.length > 0
|
|
1999
|
+
? { explicitTypeImports: imports }
|
|
1376
2000
|
: {}),
|
|
1377
2001
|
});
|
|
1378
|
-
const
|
|
1379
|
-
|
|
2002
|
+
const makeApplication = (imports: readonly string[]) => generateDartApplication(dd.modForDart, {
|
|
2003
|
+
supportImport: "package",
|
|
2004
|
+
typesImportUri,
|
|
2005
|
+
packageAggregateDomains: buildPackageAggregateDomains(modules),
|
|
2006
|
+
flutterHost: true,
|
|
2007
|
+
tenantRoot: perDomain.length === 1,
|
|
2008
|
+
...(imports.length > 0
|
|
2009
|
+
? { explicitTypeImports: imports }
|
|
2010
|
+
: {}),
|
|
2011
|
+
});
|
|
2012
|
+
let clientSrc = makeClient(implementationTypeImports);
|
|
2013
|
+
let applicationSrc = makeApplication(implementationTypeImports);
|
|
2014
|
+
// Import the private framework types file only into implementation libraries
|
|
2015
|
+
// that actually name one of its declarations. Ignore the embedded describe
|
|
2016
|
+
// JSON while scanning so framework words in documentation do not manufacture
|
|
2017
|
+
// an otherwise-unused import.
|
|
2018
|
+
const implementationSource = `${clientSrc}\n${applicationSrc}`.replace(/r'''[\s\S]*?'''/g, "");
|
|
2019
|
+
const needsFrameworkTypes = frameworkImplementationImport !== undefined &&
|
|
2020
|
+
frameworkTypeNames.some((name) => new RegExp(`\\b${name}\\b`).test(implementationSource));
|
|
2021
|
+
if (needsFrameworkTypes) {
|
|
2022
|
+
const imports = [...implementationTypeImports, frameworkImplementationImport];
|
|
2023
|
+
clientSrc = makeClient(imports);
|
|
2024
|
+
applicationSrc = makeApplication(imports);
|
|
2025
|
+
}
|
|
2026
|
+
const clientFile = `${mod.name}.client.dart`;
|
|
2027
|
+
const applicationFile = `${mod.name}.application.dart`;
|
|
2028
|
+
const encapsulated = encapsulateDartClient(
|
|
2029
|
+
clientSrc,
|
|
2030
|
+
applicationSrc,
|
|
2031
|
+
clientFile,
|
|
2032
|
+
applicationFile,
|
|
2033
|
+
);
|
|
2034
|
+
const cp = path.join(clientGen, clientFile);
|
|
2035
|
+
writeFileSync(cp, encapsulated.clientPart, "utf8");
|
|
1380
2036
|
written.push(cp);
|
|
2037
|
+
const ap = path.join(clientGen, applicationFile);
|
|
2038
|
+
writeFileSync(ap, encapsulated.applicationLibrary, "utf8");
|
|
2039
|
+
written.push(ap);
|
|
2040
|
+
}
|
|
2041
|
+
const packageApplicationFile = `${describePackageName}.package.application`;
|
|
2042
|
+
if (perDomain.length > 1) {
|
|
2043
|
+
const topology = resolveApplicationTopology(modules);
|
|
2044
|
+
const packageApplicationPath = path.join(clientGen, `${packageApplicationFile}.dart`);
|
|
2045
|
+
writeFileSync(
|
|
2046
|
+
packageApplicationPath,
|
|
2047
|
+
generatedPackageApplication(
|
|
2048
|
+
describePackageName,
|
|
2049
|
+
perDomain.map(({ mod, dd }) => ({
|
|
2050
|
+
domain: dd.modForDart.domain ?? dd.modForDart.name,
|
|
2051
|
+
file: mod.name,
|
|
2052
|
+
invitationPolicies: (dd.modForDart.applicationWorkspaceSharing ?? []).map((policy) => ({
|
|
2053
|
+
domainHash,
|
|
2054
|
+
query: policy.invitationLaw.pendingByRecipient.id,
|
|
2055
|
+
redeem: policy.invitationLaw.redeem.id,
|
|
2056
|
+
personField: policy.person,
|
|
2057
|
+
roleField: policy.role,
|
|
2058
|
+
scopeField: policy.scope,
|
|
2059
|
+
})),
|
|
2060
|
+
})),
|
|
2061
|
+
topology.homeDomain,
|
|
2062
|
+
topology.rootDomains,
|
|
2063
|
+
),
|
|
2064
|
+
"utf8",
|
|
2065
|
+
);
|
|
2066
|
+
written.push(packageApplicationPath);
|
|
1381
2067
|
}
|
|
1382
2068
|
const clientBarrel = [
|
|
1383
2069
|
"// GENERATED by nomos-compile — the CLIENT package's public entrypoint. DO NOT EDIT.",
|
|
1384
|
-
|
|
1385
|
-
|
|
2070
|
+
`/// The generated business application for ${describePackageName}.`,
|
|
2071
|
+
`///`,
|
|
2072
|
+
`/// Import this library, mount [${dartApplicationClassName(describePackageName)}.nomosWidget] once, and pass the`,
|
|
2073
|
+
`/// supplied [${dartApplicationClassName(describePackageName)}] through the UI. Its typed actions, records,`,
|
|
2074
|
+
`/// watches, references and status are the canonical application state. Do not recreate them`,
|
|
2075
|
+
`/// in tenant repositories, providers, sessions, clients or caches.`,
|
|
2076
|
+
`///`,
|
|
2077
|
+
`/// Nomos privately owns persistence, offline work, synchronization, identity, access,`,
|
|
2078
|
+
`/// provenance, conflicts, schema evolution and recovery. Framework implementation details`,
|
|
2079
|
+
`/// are deliberately not exported here.`,
|
|
1386
2080
|
"library;",
|
|
1387
2081
|
"",
|
|
1388
|
-
`export 'package
|
|
1389
|
-
|
|
1390
|
-
|
|
2082
|
+
`export 'package:${packages.types}/${genRel}/${businessSharedFile}';`,
|
|
2083
|
+
...tenantTypesExportKeys
|
|
2084
|
+
.map((file) => {
|
|
2085
|
+
const names = tenantDartExportNames(typesSources.get(file) ?? "");
|
|
2086
|
+
return names.length === 0
|
|
2087
|
+
? null
|
|
2088
|
+
: `export 'package:${packages.types}/${genRel}/${file}.dart' show ${names.join(", ")};`;
|
|
2089
|
+
})
|
|
2090
|
+
.filter((line): line is string => line !== null),
|
|
2091
|
+
...(barrelNames !== undefined && Object.keys(barrelNames).length > 0
|
|
2092
|
+
? [`export 'package:${packages.types}/${packages.types}.dart' show ${Object.values(barrelNames).sort().join(", ")};`]
|
|
2093
|
+
: []),
|
|
2094
|
+
...(perDomain.length > 1
|
|
2095
|
+
? [
|
|
2096
|
+
`export 'package:nomos_flutter/src/application_status.dart' show ApplicationSignIn, ApplicationStatus, ApplicationStatusPhase;`,
|
|
2097
|
+
`export '${genRel}/${packageApplicationFile}.dart' show ${dartApplicationClassName(describePackageName)};`,
|
|
2098
|
+
...perDomain.map(({ mod, dd }) => {
|
|
2099
|
+
const packageClass = dartApplicationClassName(describePackageName);
|
|
2100
|
+
const publicNames = dartApplicationPublicClassNames(dd.modForDart).filter(
|
|
2101
|
+
(name) => name !== packageClass,
|
|
2102
|
+
);
|
|
2103
|
+
return `export '${genRel}/${mod.name}.application.dart' show ${publicNames.join(", ")};`;
|
|
2104
|
+
}),
|
|
2105
|
+
]
|
|
2106
|
+
: perDomain.map(({ mod, dd }) =>
|
|
2107
|
+
`export '${genRel}/${mod.name}.application.dart' show ${dartApplicationPublicClassNames(dd.modForDart, { tenantRoot: true }).join(", ")};`)),
|
|
1391
2108
|
"",
|
|
1392
2109
|
];
|
|
1393
2110
|
const clientBarrelPath = path.join(pkgDir(packages.client), "lib", `${packages.client}.dart`);
|
|
@@ -1397,31 +2114,82 @@ function emitDartPackages(
|
|
|
1397
2114
|
pkgDir(packages.client),
|
|
1398
2115
|
[
|
|
1399
2116
|
`name: ${packages.client}`,
|
|
1400
|
-
`description: Generated Nomos
|
|
2117
|
+
`description: Generated Nomos business application APIs.`,
|
|
1401
2118
|
`version: 0.1.0`,
|
|
1402
2119
|
`publish_to: none # tenant-owned: remove to publish`,
|
|
1403
2120
|
`environment:`,
|
|
1404
2121
|
` sdk: ^3.5.0`,
|
|
2122
|
+
` flutter: ">=3.10.0"`,
|
|
1405
2123
|
`dependencies:`,
|
|
2124
|
+
` flutter:`,
|
|
2125
|
+
` sdk: flutter`,
|
|
1406
2126
|
` nomos_client: ${NOMOS_CLIENT_DEP}`,
|
|
2127
|
+
` nomos_flutter: ${NOMOS_FLUTTER_DEP}`,
|
|
1407
2128
|
` ${packages.types}:`,
|
|
1408
2129
|
` path: ../${packages.types} # co-emitted sibling; replace with a hosted constraint when publishing`,
|
|
1409
2130
|
``,
|
|
1410
2131
|
].join("\n"),
|
|
1411
2132
|
);
|
|
1412
2133
|
|
|
1413
|
-
// ── the TEST package (optional) —
|
|
2134
|
+
// ── the TEST package (optional) — generated business scenarios, test-only dependency ──
|
|
1414
2135
|
if (packages.test !== undefined) {
|
|
1415
2136
|
const testGen = freshGenDir(packages.test);
|
|
1416
|
-
|
|
2137
|
+
if (deployJsonPath === undefined) fail("dart.packages.test requires the compiled application package");
|
|
2138
|
+
const embeddedDeployName = `${describePackageName}.deploy.json`;
|
|
2139
|
+
const embeddedDeployPath = path.join(testGen, embeddedDeployName);
|
|
2140
|
+
copyFileSync(deployJsonPath, embeddedDeployPath);
|
|
2141
|
+
written.push(embeddedDeployPath);
|
|
2142
|
+
const scenarioSurfaces = perDomain.length > 1
|
|
2143
|
+
? (() => {
|
|
2144
|
+
const topology = resolveApplicationTopology(modules);
|
|
2145
|
+
const home = perDomain.find(({ dd }) =>
|
|
2146
|
+
(dd.modForDart.domain ?? dd.modForDart.name) === topology.homeDomain,
|
|
2147
|
+
);
|
|
2148
|
+
if (home === undefined) {
|
|
2149
|
+
fail(`generated Dart scenario cannot find its home domain '${topology.homeDomain}'`);
|
|
2150
|
+
}
|
|
2151
|
+
return [{
|
|
2152
|
+
mod: home.mod,
|
|
2153
|
+
dd: home.dd,
|
|
2154
|
+
file: describePackageName,
|
|
2155
|
+
surfaceName: describePackageName,
|
|
2156
|
+
applicationFile: packageApplicationFile,
|
|
2157
|
+
}];
|
|
2158
|
+
})()
|
|
2159
|
+
: perDomain.map(({ mod, dd }) => ({
|
|
2160
|
+
mod,
|
|
2161
|
+
dd,
|
|
2162
|
+
file: mod.name,
|
|
2163
|
+
surfaceName: dd.modForDart.domain ?? dd.modForDart.name,
|
|
2164
|
+
applicationFile: `${mod.name}.application`,
|
|
2165
|
+
}));
|
|
2166
|
+
for (const { mod, dd, file, surfaceName, applicationFile } of scenarioSurfaces) {
|
|
2167
|
+
const scenarioPath = path.join(testGen, `${file}.scenario.dart`);
|
|
2168
|
+
writeFileSync(
|
|
2169
|
+
scenarioPath,
|
|
2170
|
+
generateDartScenario(
|
|
2171
|
+
dd.modForDart,
|
|
2172
|
+
packages.client,
|
|
2173
|
+
packages.test,
|
|
2174
|
+
embeddedDeployName,
|
|
2175
|
+
{
|
|
2176
|
+
surfaceName,
|
|
2177
|
+
applicationFile,
|
|
2178
|
+
applicationModules: perDomain.map(({ dd: candidate }) => candidate.modForDart),
|
|
2179
|
+
},
|
|
2180
|
+
),
|
|
2181
|
+
"utf8",
|
|
2182
|
+
);
|
|
2183
|
+
written.push(scenarioPath);
|
|
2184
|
+
}
|
|
1417
2185
|
const testBarrel = [
|
|
1418
2186
|
"// GENERATED by nomos-compile — the TEST package's public entrypoint. DO NOT EDIT.",
|
|
1419
|
-
"//
|
|
1420
|
-
"// full client layer, for widget/integration tests. Depend on this ONLY from dev_dependencies.",
|
|
2187
|
+
"// Business scenarios drive the exact generated production application; framework machinery stays private.",
|
|
1421
2188
|
"library;",
|
|
1422
2189
|
"",
|
|
1423
|
-
`export 'package:nomos_client/testing.dart';`,
|
|
1424
2190
|
`export 'package:${packages.client}/${packages.client}.dart';`,
|
|
2191
|
+
...scenarioSurfaces.map(({ file, surfaceName }) =>
|
|
2192
|
+
`export '${genRel}/${file}.scenario.dart' show ${dartScenarioClassName(surfaceName)}, ${dartTestPersonClassName(surfaceName)}, ${dartScenarioGivenClassName(surfaceName)};`),
|
|
1425
2193
|
"",
|
|
1426
2194
|
];
|
|
1427
2195
|
const testBarrelPath = path.join(pkgDir(packages.test), "lib", `${packages.test}.dart`);
|
|
@@ -1431,15 +2199,22 @@ function emitDartPackages(
|
|
|
1431
2199
|
pkgDir(packages.test),
|
|
1432
2200
|
[
|
|
1433
2201
|
`name: ${packages.test}`,
|
|
1434
|
-
`description: Generated
|
|
2202
|
+
`description: Generated business application scenarios. dev_dependencies only.`,
|
|
1435
2203
|
`version: 0.1.0`,
|
|
1436
2204
|
`publish_to: none # tenant-owned: remove to publish`,
|
|
1437
2205
|
`environment:`,
|
|
1438
2206
|
` sdk: ^3.5.0`,
|
|
1439
2207
|
`dependencies:`,
|
|
2208
|
+
` flutter:`,
|
|
2209
|
+
` sdk: flutter`,
|
|
1440
2210
|
` nomos_client: ${NOMOS_CLIENT_DEP}`,
|
|
1441
2211
|
` ${packages.client}:`,
|
|
1442
2212
|
` path: ../${packages.client} # co-emitted sibling; replace with a hosted constraint when publishing`,
|
|
2213
|
+
`dev_dependencies:`,
|
|
2214
|
+
` test: ^1.25.0`,
|
|
2215
|
+
`flutter:`,
|
|
2216
|
+
` assets:`,
|
|
2217
|
+
` - lib/${genRel}/${embeddedDeployName}`,
|
|
1443
2218
|
``,
|
|
1444
2219
|
].join("\n"),
|
|
1445
2220
|
);
|
|
@@ -1519,7 +2294,15 @@ function emitDartPackages(
|
|
|
1519
2294
|
|
|
1520
2295
|
async function main(): Promise<void> {
|
|
1521
2296
|
const argv = process.argv.slice(2);
|
|
1522
|
-
const
|
|
2297
|
+
const supportedFlags = new Set(["--require-usdcat", "--compact", "--analyze"]);
|
|
2298
|
+
for (const flag of argv.filter((arg) => arg.startsWith("--"))) {
|
|
2299
|
+
if (supportedFlags.has(flag)) continue;
|
|
2300
|
+
if (flag === "--layered") {
|
|
2301
|
+
fail("--layered no longer exists: every package is a USD layer stack and every compile proves its flattened law");
|
|
2302
|
+
}
|
|
2303
|
+
fail(`unknown compile flag '${flag}'`);
|
|
2304
|
+
}
|
|
2305
|
+
const requireUsdcat = argv.includes("--require-usdcat") || process.env.NOMOS_REQUIRE_USDCAT === "1";
|
|
1523
2306
|
const compactFlag = argv.includes("--compact") || process.env["NOMOS_COMPACT"] === "1";
|
|
1524
2307
|
// §"Shipped (1–4)" follow-up: `githolon compile --analyze` (or NOMOS_ANALYZE=1) runs
|
|
1525
2308
|
// `dart analyze` on the generated Dart package after emission and fails the compile on red.
|
|
@@ -1549,11 +2332,29 @@ async function main(): Promise<void> {
|
|
|
1549
2332
|
}
|
|
1550
2333
|
} catch { /* no dsl resolvable from the law dir (vendored/workspace setups) — nothing to compare */ }
|
|
1551
2334
|
|
|
1552
|
-
const cfgModule = (await import(pathToFileURL(configPath).href)) as { default?:
|
|
1553
|
-
const
|
|
2335
|
+
const cfgModule = (await import(pathToFileURL(configPath).href)) as { default?: unknown };
|
|
2336
|
+
const configValue = cfgModule.default;
|
|
2337
|
+
if (isApplicationCompositionConfig(configValue)) {
|
|
2338
|
+
try {
|
|
2339
|
+
const emitted = compileApplicationComposition(configValue, configPath, { requireUsdcat });
|
|
2340
|
+
console.log([
|
|
2341
|
+
`${configValue.application} — compiled Nomos application composition`,
|
|
2342
|
+
`stage: ${emitted.outputPath}`,
|
|
2343
|
+
`assets: ${emitted.referencedAssets.length} local content-addressed reference(s)`,
|
|
2344
|
+
`runtime configuration: USD (the authoring .mjs does not travel)`,
|
|
2345
|
+
].join("\n"));
|
|
2346
|
+
return;
|
|
2347
|
+
} catch (error) {
|
|
2348
|
+
fail(`application composition refused: ${(error as Error).message}`);
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
const cfg = configValue as PackageConfig | undefined;
|
|
1554
2352
|
if (!cfg || typeof cfg.name !== "string" || !Array.isArray(cfg.domains) || cfg.domains.length === 0) {
|
|
1555
2353
|
fail(`config must default-export { name, domains: [{ key, modules }, ...] }`);
|
|
1556
2354
|
}
|
|
2355
|
+
if (Object.prototype.hasOwnProperty.call(cfg, "layered")) {
|
|
2356
|
+
fail("nomos.package.mjs no longer accepts 'layered': every package is layered, with no alternate compile lane");
|
|
2357
|
+
}
|
|
1557
2358
|
// ANY readable name works ("tool-library", "tool_library", "Tool Library"):
|
|
1558
2359
|
// artifact FILENAMES keep it verbatim; generated SYMBOLS normalize it
|
|
1559
2360
|
// (toolLibraryClient, TOOL_LIBRARY_DOMAIN_HASH — see codegen_ts.ts).
|
|
@@ -1561,22 +2362,31 @@ async function main(): Promise<void> {
|
|
|
1561
2362
|
fail(`package name '${cfg.name}' is not a safe basename (letters, digits, spaces, '.', '-', '_')`);
|
|
1562
2363
|
}
|
|
1563
2364
|
|
|
1564
|
-
//
|
|
1565
|
-
//
|
|
1566
|
-
//
|
|
1567
|
-
//
|
|
1568
|
-
//
|
|
2365
|
+
// EVERY GENERATED APPLICATION IS A COMPLETE PRODUCT ENTRYPOINT. The compiler privately appends the
|
|
2366
|
+
// authority bootstrap law that turns verified credential evidence into durable device authorship. Tenant
|
|
2367
|
+
// config never names this domain and presentation filters it back out: a package cannot emit the generated
|
|
2368
|
+
// application facade while omitting the private machinery needed to open it. The deliberately obscure key
|
|
2369
|
+
// reduces collision likelihood, but collision handling is still fail-closed: never reinterpret a tenant's
|
|
2370
|
+
// same-named concept as framework machinery.
|
|
1569
2371
|
if (cfg.domains.some((d) => d.key === APPLICATION_HOME_DOMAIN)) {
|
|
1570
2372
|
fail(
|
|
1571
|
-
`domain key '${APPLICATION_HOME_DOMAIN}' is reserved by Nomos for
|
|
2373
|
+
`domain key '${APPLICATION_HOME_DOMAIN}' is reserved by Nomos for private application authority law; ` +
|
|
1572
2374
|
`rename the tenant domain (Nomos will not silently overwrite or reroute it)`,
|
|
1573
2375
|
);
|
|
1574
2376
|
}
|
|
1575
|
-
const domainsToCompile: DomainConfig[] = [
|
|
1576
|
-
|
|
2377
|
+
const domainsToCompile: DomainConfig[] = [
|
|
2378
|
+
...cfg.domains,
|
|
2379
|
+
{
|
|
2380
|
+
key: APPLICATION_HOME_DOMAIN,
|
|
2381
|
+
modules: [path.join(DSL_DIR, "src", "framework", "application_home.ts")],
|
|
2382
|
+
},
|
|
2383
|
+
];
|
|
1577
2384
|
|
|
1578
2385
|
const outDir = path.resolve(cfgDir, cfg.outDir ?? "build");
|
|
1579
2386
|
mkdirSync(outDir, { recursive: true });
|
|
2387
|
+
// The package-wide executable lane no longer exists. Remove its ignored build artifact on every
|
|
2388
|
+
// native compile so a developer cannot accidentally inspect or execute stale bytes as canonical law.
|
|
2389
|
+
rmSync(path.join(outDir, ".nomos_lump.js"), { force: true });
|
|
1580
2390
|
|
|
1581
2391
|
// ── import + compose each domain ──
|
|
1582
2392
|
// LAZY PER-DOMAIN BOOT (#34): the generated entry registers each domain's modules
|
|
@@ -1585,12 +2395,10 @@ async function main(): Promise<void> {
|
|
|
1585
2395
|
// per-plan sandbox boots ONLY the domain the dispatched intent touches. The
|
|
1586
2396
|
// routing table for type/relation-keyed dispatches is computed below by the
|
|
1587
2397
|
// SAME scans the engine registries run (`collectEngineRouting` — one machinery).
|
|
1588
|
-
//
|
|
1589
|
-
//
|
|
1590
|
-
const
|
|
1591
|
-
const
|
|
1592
|
-
const entryDomains: string[] = [];
|
|
1593
|
-
const entryMintInstalls: string[] = []; // route-tagged in-plan mints (taxonomy packages only)
|
|
2398
|
+
// Every executable is an independently addressable lazy plan asset. There is no package-wide
|
|
2399
|
+
// eager/monolithic execution mode: keeping that alternative would let it become canonical again.
|
|
2400
|
+
const entryDomainSourceByKey = new Map<string, string>();
|
|
2401
|
+
const entryMintInstallByKey = new Map<string, string>();
|
|
1594
2402
|
const routingInput: Record<string, Mod[]> = {};
|
|
1595
2403
|
const domainModules: DomainModule[] = [];
|
|
1596
2404
|
// THE SELF-DESCRIBE DECLS (G1): per domain key, the FINISHED plain read/maintenance decls the
|
|
@@ -1599,9 +2407,7 @@ async function main(): Promise<void> {
|
|
|
1599
2407
|
// pass finished decls through), serialized into the generated entry as `openapiDecls`, so the
|
|
1600
2408
|
// kernel-run describe reproduces THIS compile's `<name>.openapi.json` byte-for-byte.
|
|
1601
2409
|
const openapiDeclsByDomain: Record<string, unknown> = {};
|
|
1602
|
-
const layered = layeredFlag || cfg.layered === true;
|
|
1603
2410
|
const layeredDomains: { key: string; inputs: LayeredModuleInput[] }[] = [];
|
|
1604
|
-
let importSeq = 0;
|
|
1605
2411
|
/**
|
|
1606
2412
|
* THE CAPABILITY LOWERING (capability_marketplace.md §4 — the deploy-warning
|
|
1607
2413
|
* prerequisite + the executor's task→capability→domain map). Rides the
|
|
@@ -1633,29 +2439,10 @@ async function main(): Promise<void> {
|
|
|
1633
2439
|
if (!existsSync(abs)) fail(`domain '${d.key}': module not found: ${abs}`);
|
|
1634
2440
|
const m = (await import(pathToFileURL(abs).href)) as Mod;
|
|
1635
2441
|
mods.push(m);
|
|
1636
|
-
|
|
1637
|
-
const v = `m${importSeq++}`;
|
|
1638
|
-
sourceExprs.push(v);
|
|
1639
|
-
entryImports.push(`import * as ${v} from ${JSON.stringify(abs)};`);
|
|
1640
|
-
} else {
|
|
1641
|
-
sourceExprs.push(`() => require(${JSON.stringify(abs)})`);
|
|
1642
|
-
}
|
|
2442
|
+
sourceExprs.push(`() => require(${JSON.stringify(abs)})`);
|
|
1643
2443
|
}
|
|
1644
2444
|
let merged: Mod = {};
|
|
1645
2445
|
for (const m of mods) merged = { ...merged, ...m };
|
|
1646
|
-
const declaresBusinessBirth = Object.values(merged).some(
|
|
1647
|
-
(value) =>
|
|
1648
|
-
value !== null &&
|
|
1649
|
-
typeof value === "object" &&
|
|
1650
|
-
(value as { __isBusinessBirth?: unknown }).__isBusinessBirth === true,
|
|
1651
|
-
);
|
|
1652
|
-
if (declaresBusinessBirth && !applicationHomeInjected) {
|
|
1653
|
-
applicationHomeInjected = true;
|
|
1654
|
-
domainsToCompile.push({
|
|
1655
|
-
key: APPLICATION_HOME_DOMAIN,
|
|
1656
|
-
modules: [path.join(DSL_DIR, "src", "framework", "application_home.ts")],
|
|
1657
|
-
});
|
|
1658
|
-
}
|
|
1659
2446
|
merged = expandBusinessBirthExports(merged, d.key);
|
|
1660
2447
|
merged = withOwnedCustodyExports(merged);
|
|
1661
2448
|
// `capability()` bundles expand into discoverable entries (the CLI plane's
|
|
@@ -1755,29 +2542,18 @@ async function main(): Promise<void> {
|
|
|
1755
2542
|
shardingMins = shardingExtrema("min");
|
|
1756
2543
|
shardingMaxes = shardingExtrema("max");
|
|
1757
2544
|
const specJson = JSON.stringify(spec);
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
entryImports.push(
|
|
1761
|
-
`import { shardingLawModule as __nomosShardingLaw, installRouteTaggedMint as __nomosInstallMint } from "@githolon/dsl/workspace-sharding";`,
|
|
1762
|
-
);
|
|
1763
|
-
}
|
|
1764
|
-
sourceExprs.push(`__nomosShardingLaw(${specJson})`);
|
|
1765
|
-
} else {
|
|
1766
|
-
shardingLazyExpr = `() => (require("@githolon/dsl/workspace-sharding") as any).shardingLawModule(${specJson})`;
|
|
1767
|
-
sourceExprs.push(shardingLazyExpr);
|
|
1768
|
-
}
|
|
2545
|
+
shardingLazyExpr = `() => (require("@githolon/dsl/workspace-sharding") as any).shardingLawModule(${specJson})`;
|
|
2546
|
+
sourceExprs.push(shardingLazyExpr);
|
|
1769
2547
|
// ROUTE-TAGGED IN-PLAN MINTS (§4, slice 3): installed AFTER registerEngine at
|
|
1770
2548
|
// the lump's top level — pre-freeze, taxonomy packages only (a taxonomy-free
|
|
1771
2549
|
// lump never imports the module: the slice-1 hash-stability law).
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
? `__nomosInstallMint(${specJson});`
|
|
1775
|
-
: `(require("@githolon/dsl/workspace-sharding") as any).installRouteTaggedMint(${specJson});`,
|
|
1776
|
-
);
|
|
2550
|
+
const mintInstall = `(require("@githolon/dsl/workspace-sharding") as any).installRouteTaggedMint(${specJson});`;
|
|
2551
|
+
entryMintInstallByKey.set(d.key, mintInstall);
|
|
1777
2552
|
}
|
|
1778
2553
|
}
|
|
1779
2554
|
|
|
1780
|
-
|
|
2555
|
+
const domainSource = ` ${JSON.stringify(d.key)}: [${sourceExprs.join(", ")}],`;
|
|
2556
|
+
entryDomainSourceByKey.set(d.key, domainSource);
|
|
1781
2557
|
routingInput[d.key] = mods;
|
|
1782
2558
|
// ── THE AUX INVARIANT-ROUTING KEY (#47, sharding slice 7 — the invariant-gate
|
|
1783
2559
|
// flame pass): in a LAZY lump, the derived sharding law registers a SECOND
|
|
@@ -1791,14 +2567,15 @@ async function main(): Promise<void> {
|
|
|
1791
2567
|
// canonical manifests are untouched (identity hashes do not move); eager
|
|
1792
2568
|
// lumps are untouched (no routing table — the pre-#34 equivalence shape);
|
|
1793
2569
|
// taxonomy-free packages emit NOTHING here (the slice-1 byte-stability law).
|
|
1794
|
-
if (shardingModForDomain !== undefined && shardingLazyExpr !== undefined
|
|
2570
|
+
if (shardingModForDomain !== undefined && shardingLazyExpr !== undefined) {
|
|
1795
2571
|
const auxKey = `${d.key}/nomos-sharding`;
|
|
1796
2572
|
if (domainsToCompile.some((x) => x.key === auxKey) || routingInput[auxKey] !== undefined) {
|
|
1797
2573
|
fail(
|
|
1798
2574
|
`domain key '${auxKey}' is reserved for the derived sharding law's invariant routing — rename the domain`,
|
|
1799
2575
|
);
|
|
1800
2576
|
}
|
|
1801
|
-
|
|
2577
|
+
const auxSource = ` ${JSON.stringify(auxKey)}: [${shardingLazyExpr}],`;
|
|
2578
|
+
entryDomainSourceByKey.set(auxKey, auxSource);
|
|
1802
2579
|
routingInput[d.key] = mods.filter((m) => m !== shardingModForDomain);
|
|
1803
2580
|
routingInput[auxKey] = [shardingModForDomain];
|
|
1804
2581
|
}
|
|
@@ -1833,7 +2610,7 @@ async function main(): Promise<void> {
|
|
|
1833
2610
|
const usesRelations =
|
|
1834
2611
|
dirsAll.some((dir) => (dir as { requiresRelationGate?: unknown }).requiresRelationGate !== undefined) ||
|
|
1835
2612
|
dirsAll.some((dir) => (dir as { aggregateId?: string }).aggregateId === RelationTuple.id) ||
|
|
1836
|
-
|
|
2613
|
+
discoverRelationTypeDeclarations(merged).length > 0;
|
|
1837
2614
|
if (usesRelations && !queries.some((q) => q.id === relationTuplesByObject.id)) {
|
|
1838
2615
|
queries.push(relationTuplesByObject);
|
|
1839
2616
|
}
|
|
@@ -1870,11 +2647,11 @@ async function main(): Promise<void> {
|
|
|
1870
2647
|
...directivesOf(merged).flatMap((dir) => synthesizeBirthQuotaInvariants(dir)),
|
|
1871
2648
|
];
|
|
1872
2649
|
// Native Zanzibar relation schema (P1) — auto-discovered by brand, like workspaceInvariants.
|
|
1873
|
-
const
|
|
1874
|
-
...
|
|
2650
|
+
const relationTypeFragments = [
|
|
2651
|
+
...discoverRelationTypeDeclarations(merged),
|
|
1875
2652
|
...resolveNamed<RelationTypeDecl>(merged, d.relationTypes, "relationTypes"),
|
|
1876
2653
|
];
|
|
1877
|
-
|
|
2654
|
+
const relationTypes = mergeRelationTypeDeclarations(relationTypeFragments);
|
|
1878
2655
|
// Sums are AUTO-DISCOVERED like queries/counts (#57 — an exported SumDecl used to be
|
|
1879
2656
|
// SILENTLY DROPPED unless config-named; the config lane stays as the escape hatch).
|
|
1880
2657
|
// Reference-dedupe: a sum that is BOTH exported and config-named is the same object.
|
|
@@ -1942,7 +2719,9 @@ async function main(): Promise<void> {
|
|
|
1942
2719
|
...(maxes.length > 0 ? { maxes } : {}),
|
|
1943
2720
|
...(workspaceTypes.length > 0 ? { workspaceTypes } : {}),
|
|
1944
2721
|
...(workspaceInvariants.length > 0 ? { workspaceInvariants } : {}),
|
|
1945
|
-
|
|
2722
|
+
// Relation declarations are already present in `modules` (including config-named exports), and
|
|
2723
|
+
// composeDomainModule performs the authoritative discovery + fragment union after expanding role
|
|
2724
|
+
// sugar. Re-threading the compiler's independently expanded copy here would invent a second author.
|
|
1946
2725
|
...(spatials.length > 0 ? { spatials } : {}),
|
|
1947
2726
|
...(d.dartImports !== undefined && d.dartImports.length > 0
|
|
1948
2727
|
? { dartImports: d.dartImports }
|
|
@@ -2033,17 +2812,20 @@ async function main(): Promise<void> {
|
|
|
2033
2812
|
}
|
|
2034
2813
|
}
|
|
2035
2814
|
|
|
2036
|
-
// ──
|
|
2815
|
+
// ── ONE USD LAYER PER MODULE (Φ applied per module, not once over the
|
|
2037
2816
|
// composed fold) — the layered emission `usd_layers.ts` proves flattens back
|
|
2038
2817
|
// to the EXACT canonical IR (asserted below, fail-closed). Per-module decls
|
|
2039
2818
|
// are discovered the same way as above, on each module's OWN exports; config
|
|
2040
2819
|
// export-names resolve on whichever module actually exports them; config
|
|
2041
|
-
// `extraAggregates` ride the FIRST (weakest) layer, prelude-style.
|
|
2042
|
-
//
|
|
2043
|
-
|
|
2820
|
+
// `extraAggregates` ride the FIRST (weakest) layer, prelude-style. Compiler-
|
|
2821
|
+
// discovered reads are seated beside the aggregate schema they read.
|
|
2822
|
+
{
|
|
2044
2823
|
const usedStems = new Set<string>();
|
|
2045
|
-
|
|
2046
|
-
|
|
2824
|
+
let inputs: LayeredModuleInput[] = mods.map((m, i) => {
|
|
2825
|
+
const authoredPath = d.modules[i];
|
|
2826
|
+
let stem = authoredPath === undefined
|
|
2827
|
+
? `nomos-derived-${i + 1}`
|
|
2828
|
+
: path.basename(authoredPath).replace(/\.[^.]*$/, "");
|
|
2047
2829
|
for (let n = 2; usedStems.has(stem); n++) stem = `${stem}-${n}`;
|
|
2048
2830
|
usedStems.add(stem);
|
|
2049
2831
|
|
|
@@ -2061,20 +2843,90 @@ async function main(): Promise<void> {
|
|
|
2061
2843
|
...discover<CombinedDecl>(m, isCombinedDecl),
|
|
2062
2844
|
...resolveNamed<CombinedDecl>(m, presentNames(d.combineds), "combineds"),
|
|
2063
2845
|
];
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2846
|
+
// Lower the module through the SAME compiler-owned expansion seam as the
|
|
2847
|
+
// canonical fold. A hand-built `{aggregates,directives}` approximation silently
|
|
2848
|
+
// dropped relation, invariant, attestation, flow and projection opinions, so the
|
|
2849
|
+
// pretty USD stack was not the law. Single-module composition retains those
|
|
2850
|
+
// generated opinions in the layer where their authored cause lives.
|
|
2851
|
+
const module = composeDomainModule({
|
|
2068
2852
|
name: d.name ?? d.key,
|
|
2069
2853
|
domain: d.key,
|
|
2070
|
-
|
|
2071
|
-
|
|
2854
|
+
modules: [m],
|
|
2855
|
+
...(i === 0 && extraAggregates.length > 0 ? { extraAggregates } : {}),
|
|
2856
|
+
// Compiler-synthesized/domain-wide declarations have no honest authored
|
|
2857
|
+
// module owner. Seat them once in the weakest prelude layer so USD and
|
|
2858
|
+
// Nomos both see one contribution instead of duplicating them per source file.
|
|
2859
|
+
...(i === 0 && (composed.workspaceInvariants?.length ?? 0) > 0
|
|
2860
|
+
? { workspaceInvariants: composed.workspaceInvariants }
|
|
2861
|
+
: {}),
|
|
2862
|
+
...(i === 0 && workspaceTypes.length > 0 ? { workspaceTypes } : {}),
|
|
2863
|
+
...(i === 0 && impureCapabilities.length > 0 ? { impureCapabilities } : {}),
|
|
2072
2864
|
...(layerQueries.length > 0 ? { queries: layerQueries } : {}),
|
|
2073
2865
|
...(layerDeriveds.length > 0 ? { deriveds: layerDeriveds } : {}),
|
|
2074
2866
|
...(layerCombineds.length > 0 ? { combineds: layerCombineds } : {}),
|
|
2075
|
-
};
|
|
2867
|
+
});
|
|
2076
2868
|
return { name: stem, module };
|
|
2077
2869
|
});
|
|
2870
|
+
if (inputs[0] !== undefined) {
|
|
2871
|
+
// Work on shallow module copies: package-owned declarations usually belong
|
|
2872
|
+
// in the weakest prelude, but a read recipe belongs beside the aggregate
|
|
2873
|
+
// schema it reads. Putting a compiler-added query in an unrelated prelude
|
|
2874
|
+
// makes that individual USD layer ill-typed even though the whole stack has
|
|
2875
|
+
// the target in a stronger layer.
|
|
2876
|
+
inputs = inputs.map((input) => ({
|
|
2877
|
+
...input,
|
|
2878
|
+
module: { ...input.module } as DomainModule,
|
|
2879
|
+
}));
|
|
2880
|
+
// Config-named and compiler-derived reads are package opinions rather than
|
|
2881
|
+
// exports of one source file. Seat only declarations missing from every
|
|
2882
|
+
// authored layer, once. Reads sit beside their returned aggregate; all other
|
|
2883
|
+
// domain-wide declarations sit in the weakest prelude.
|
|
2884
|
+
for (const field of [
|
|
2885
|
+
"queries",
|
|
2886
|
+
"counts",
|
|
2887
|
+
"spatials",
|
|
2888
|
+
"sums",
|
|
2889
|
+
"mins",
|
|
2890
|
+
"maxes",
|
|
2891
|
+
"deriveds",
|
|
2892
|
+
"combineds",
|
|
2893
|
+
"workspaceTypes",
|
|
2894
|
+
"workspaceInvariants",
|
|
2895
|
+
"orderedReads",
|
|
2896
|
+
"exists_",
|
|
2897
|
+
] as const) {
|
|
2898
|
+
const canonical = (composed as unknown as Record<string, unknown>)[field];
|
|
2899
|
+
if (!Array.isArray(canonical)) continue;
|
|
2900
|
+
const present = new Set(inputs.flatMap((input) => {
|
|
2901
|
+
const values = (input.module as unknown as Record<string, unknown>)[field];
|
|
2902
|
+
return Array.isArray(values)
|
|
2903
|
+
? values.flatMap((value) => value !== null && typeof value === "object" && typeof (value as { id?: unknown }).id === "string"
|
|
2904
|
+
? [(value as { id: string }).id]
|
|
2905
|
+
: [])
|
|
2906
|
+
: [];
|
|
2907
|
+
}));
|
|
2908
|
+
const missing = canonical.filter((value) =>
|
|
2909
|
+
value !== null && typeof value === "object" && typeof (value as { id?: unknown }).id === "string"
|
|
2910
|
+
? !present.has((value as { id: string }).id)
|
|
2911
|
+
: true);
|
|
2912
|
+
for (const value of missing) {
|
|
2913
|
+
const declarationTarget = value !== null && typeof value === "object"
|
|
2914
|
+
? field === "queries"
|
|
2915
|
+
? (value as { returns?: unknown }).returns
|
|
2916
|
+
: (["counts", "spatials", "sums", "mins", "maxes", "deriveds", "combineds"] as const).includes(field as never)
|
|
2917
|
+
? (value as { of?: unknown }).of
|
|
2918
|
+
: undefined
|
|
2919
|
+
: undefined;
|
|
2920
|
+
const ownerIndex = typeof declarationTarget === "string"
|
|
2921
|
+
? inputs.findIndex((input) => input.module.aggregates.some((aggregate) => aggregate.id === declarationTarget))
|
|
2922
|
+
: -1;
|
|
2923
|
+
const targetIndex = ownerIndex >= 0 ? ownerIndex : 0;
|
|
2924
|
+
const target = inputs[targetIndex]!.module as DomainModule & Record<string, unknown>;
|
|
2925
|
+
const existing = target[field];
|
|
2926
|
+
target[field] = [...(Array.isArray(existing) ? existing : []), value];
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2078
2930
|
layeredDomains.push({ key: d.key, inputs });
|
|
2079
2931
|
}
|
|
2080
2932
|
}
|
|
@@ -2082,7 +2934,7 @@ async function main(): Promise<void> {
|
|
|
2082
2934
|
// Presentation is a separate concern from executable law. The private application/home domain remains
|
|
2083
2935
|
// in `domainModules` for hashing, routing, self-description, replay and static checking, but generated
|
|
2084
2936
|
// tenant clients/proofs/interfaces contain business vocabulary only.
|
|
2085
|
-
const presentationDomainModules = domainModules.filter(
|
|
2937
|
+
const presentationDomainModules = closeComposedBusinessBirthPresentation(domainModules).filter(
|
|
2086
2938
|
(mod) => (mod.domain ?? mod.name) !== APPLICATION_HOME_DOMAIN,
|
|
2087
2939
|
);
|
|
2088
2940
|
|
|
@@ -2248,16 +3100,10 @@ async function main(): Promise<void> {
|
|
|
2248
3100
|
if (!rel || !exportName) fail(`report '${reportId}' must be "modulePath#exportName"; got '${ref}'`);
|
|
2249
3101
|
const abs = path.resolve(cfgDir, rel);
|
|
2250
3102
|
if (!existsSync(abs)) fail(`report '${reportId}': module not found: ${abs}`);
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
} else {
|
|
2256
|
-
// A lazy report factory: the module's `__esm` init runs on first render call.
|
|
2257
|
-
entryReports.push(
|
|
2258
|
-
` ${JSON.stringify(reportId)}: (actor: string) => (require(${JSON.stringify(abs)}) as any).${exportName}(actor),`,
|
|
2259
|
-
);
|
|
2260
|
-
}
|
|
3103
|
+
// A lazy report factory: the module's init runs only when this report asset is selected.
|
|
3104
|
+
entryReports.push(
|
|
3105
|
+
` ${JSON.stringify(reportId)}: (actor: string) => (require(${JSON.stringify(abs)}) as any).${exportName}(actor),`,
|
|
3106
|
+
);
|
|
2261
3107
|
}
|
|
2262
3108
|
|
|
2263
3109
|
// ── 1. generate + bundle the engine entry ──
|
|
@@ -2281,7 +3127,7 @@ async function main(): Promise<void> {
|
|
|
2281
3127
|
// entry's top level — pre-freeze, exactly where the eager lump ran them.
|
|
2282
3128
|
const zodSeedImports: string[] = [];
|
|
2283
3129
|
const zodSeedNames: string[] = [];
|
|
2284
|
-
|
|
3130
|
+
{
|
|
2285
3131
|
const zodPkgJson = [cfgDir, DSL_DIR]
|
|
2286
3132
|
.map((dir) => {
|
|
2287
3133
|
try {
|
|
@@ -2308,43 +3154,6 @@ async function main(): Promise<void> {
|
|
|
2308
3154
|
zodSeedNames.push(v);
|
|
2309
3155
|
}
|
|
2310
3156
|
}
|
|
2311
|
-
const entrySource = [
|
|
2312
|
-
`// AUTO-GENERATED by nomos-compile from ${path.basename(configPath)} — do not edit.`,
|
|
2313
|
-
...(eagerLump
|
|
2314
|
-
? []
|
|
2315
|
-
: [
|
|
2316
|
-
`// LAZY PER-DOMAIN BOOT (#34): each domain registers as require-thunks; esbuild`,
|
|
2317
|
-
`// defers every module's init to its first dispatch touch inside the fresh sandbox.`,
|
|
2318
|
-
`declare function require(id: string): Record<string, unknown>;`,
|
|
2319
|
-
...zodSeedImports,
|
|
2320
|
-
]),
|
|
2321
|
-
...entryImports,
|
|
2322
|
-
`import { registerEngine } from "@githolon/dsl/engine-entry";`,
|
|
2323
|
-
``,
|
|
2324
|
-
`registerEngine({`,
|
|
2325
|
-
` domains: {`,
|
|
2326
|
-
...entryDomains,
|
|
2327
|
-
` },`,
|
|
2328
|
-
...(entryReports.length > 0 ? [` reports: {`, ...entryReports, ` },`] : []),
|
|
2329
|
-
...(eagerLump ? [] : [` routing: ${JSON.stringify(routing)},`]),
|
|
2330
|
-
...(zodSeedNames.length > 0 ? [` seeds: [${zodSeedNames.join(", ")}],`] : []),
|
|
2331
|
-
// THE SELF-DESCRIBE INPUTS (G1): the finished read/maintenance decls + the package title, baked
|
|
2332
|
-
// into the lump so `globalThis.describe` reproduces this compile's `<name>.openapi.json` over the
|
|
2333
|
-
// FOLDED LAW — the kernel serves the spec, the host stages nothing. Omit-when-empty (decl-free
|
|
2334
|
-
// packages: offers still derive from directives; the title default is the workspace-level title).
|
|
2335
|
-
...(Object.keys(openapiDeclsByDomain).length > 0
|
|
2336
|
-
? [` openapiDecls: ${JSON.stringify(openapiDeclsByDomain)},`]
|
|
2337
|
-
: []),
|
|
2338
|
-
` packageTitle: ${JSON.stringify(`${cfg.name} (nomos package)`)},`,
|
|
2339
|
-
`});`,
|
|
2340
|
-
// ROUTE-TAGGED IN-PLAN MINTS (sharding §4, slice 3): wraps globalThis.plan +
|
|
2341
|
-
// globalThis.nomos.mint at top-level eval — pre-freeze, after registerEngine's
|
|
2342
|
-
// own assignments. Empty (and therefore byte-absent) for taxonomy-free packages.
|
|
2343
|
-
...entryMintInstalls,
|
|
2344
|
-
``,
|
|
2345
|
-
].join("\n");
|
|
2346
|
-
const entryPath = path.join(outDir, ".nomos_entry.ts");
|
|
2347
|
-
writeFileSync(entryPath, entrySource, "utf8");
|
|
2348
3157
|
|
|
2349
3158
|
// Resolve esbuild through Node's resolver (caller's copy first, else the DSL's
|
|
2350
3159
|
// own dependency) — NOT node_modules/.bin paths, which hoisted/workspace
|
|
@@ -2367,64 +3176,95 @@ async function main(): Promise<void> {
|
|
|
2367
3176
|
const esbuildBin = path.join(path.dirname(esbuildPkgJson), esbuildBinRel ?? "bin/esbuild");
|
|
2368
3177
|
const esbuildMod = (await import(pathToFileURL(path.join(path.dirname(esbuildPkgJson), "lib", "main.js")).href)) as typeof import("esbuild");
|
|
2369
3178
|
|
|
2370
|
-
const canonicalizeEngineJavascript = (source: string): string => {
|
|
2371
|
-
// esbuild includes module labels derived from the package's install topology in
|
|
2372
|
-
// non-minified bundles. The same law therefore used to hash differently when the
|
|
2373
|
-
// DSL was reached through this monorepo (../../dsl/src/...) and through a packed
|
|
2374
|
-
// dependency (node_modules/@githolon/dsl/src/...). Canonicalise only esbuild's
|
|
2375
|
-
// generated comment/function labels: tenant strings and executable imports are
|
|
2376
|
-
// deliberately outside these narrow replacements.
|
|
2377
|
-
const body = source
|
|
2378
|
-
.replace(/^\s*"use strict";\s*$/gm, "")
|
|
2379
|
-
.replace(/^(\s*\/\/ )(?:(?:\.\.\/)+dsl\/src\/|node_modules\/@githolon\/dsl\/src\/)/gm, "$1@githolon/dsl/src/")
|
|
2380
|
-
.replace(/"(?:(?:\.\.\/)+dsl\/src\/|node_modules\/@githolon\/dsl\/src\/)([^"]+)"\(\) \{/g, '"@githolon/dsl/src/$1"() {')
|
|
2381
|
-
.replace(/^(\s*\/\/ )(?:(?:\.\.\/)+node_modules\/zod\/|node_modules\/zod\/)/gm, "$1zod/")
|
|
2382
|
-
.replace(/"(?:(?:\.\.\/)+node_modules\/zod\/|node_modules\/zod\/)([^"]+)"\(\) \{/g, '"zod/$1"() {')
|
|
2383
|
-
.replace(/\n[ \t]*\n/g, "\n")
|
|
2384
|
-
.trimEnd();
|
|
2385
|
-
return `"use strict";\n${body}\n`;
|
|
2386
|
-
};
|
|
2387
|
-
|
|
2388
3179
|
const compact = compactFlag || cfg.compact === true;
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
3180
|
+
|
|
3181
|
+
// ── CONTENT-ADDRESSED PLAN ASSETS ─────────────────────────────────────────────
|
|
3182
|
+
// The ledger package no longer executes the package-wide compatibility lump above. Build the
|
|
3183
|
+
// smallest domain set each dispatch class needs, index it by a typed selector, and let the kernel
|
|
3184
|
+
// cache/pool the selected plan by its own content hash. Identical domain sets compile once and are
|
|
3185
|
+
// shared by every selector that needs them.
|
|
3186
|
+
const planAssets: UsdPlanAsset[] = [];
|
|
3187
|
+
const planRoutes: UsdPlanRoute[] = [];
|
|
3188
|
+
const planBySignature = new Map<string, UsdPlanAsset>();
|
|
3189
|
+
let planSequence = 0;
|
|
3190
|
+
const orderedDomainKeys = [...entryDomainSourceByKey.keys()];
|
|
3191
|
+
const planEntrySource = (domainKeys: readonly string[], includeReports: boolean): string => {
|
|
3192
|
+
const wanted = new Set(domainKeys);
|
|
3193
|
+
const decls = Object.fromEntries(Object.entries(openapiDeclsByDomain).filter(([key]) => wanted.has(key)));
|
|
3194
|
+
return [
|
|
3195
|
+
`// AUTO-GENERATED Nomos plan asset from ${path.basename(configPath)} — do not edit.`,
|
|
3196
|
+
`declare function require(id: string): Record<string, unknown>;`,
|
|
3197
|
+
...zodSeedImports,
|
|
3198
|
+
`import { registerEngine } from "@githolon/dsl/engine-entry";`,
|
|
3199
|
+
``,
|
|
3200
|
+
`registerEngine({`,
|
|
3201
|
+
` domains: {`,
|
|
3202
|
+
...domainKeys.map((key) => {
|
|
3203
|
+
const source = entryDomainSourceByKey.get(key);
|
|
3204
|
+
if (source === undefined) throw new Error(`missing generated entry source for '${key}'`);
|
|
3205
|
+
return source;
|
|
3206
|
+
}),
|
|
3207
|
+
` },`,
|
|
3208
|
+
...(includeReports && entryReports.length > 0 ? [` reports: {`, ...entryReports, ` },`] : []),
|
|
3209
|
+
` routing: ${JSON.stringify(routing)},`,
|
|
3210
|
+
...(zodSeedNames.length > 0 ? [` seeds: [${zodSeedNames.join(", ")}],`] : []),
|
|
3211
|
+
...(Object.keys(decls).length > 0 ? [` openapiDecls: ${JSON.stringify(decls)},`] : []),
|
|
3212
|
+
` packageTitle: ${JSON.stringify(`${cfg.name} (nomos package)`)},`,
|
|
3213
|
+
`});`,
|
|
3214
|
+
...domainKeys.flatMap((key) => {
|
|
3215
|
+
const install = entryMintInstallByKey.get(key);
|
|
3216
|
+
return install === undefined ? [] : [install];
|
|
3217
|
+
}),
|
|
3218
|
+
``,
|
|
3219
|
+
].join("\n");
|
|
3220
|
+
};
|
|
3221
|
+
const compilePlan = (domainKeys: readonly string[], includeReports: boolean): UsdPlanAsset => {
|
|
3222
|
+
const ordered = orderedDomainKeys.filter((key) => domainKeys.includes(key));
|
|
3223
|
+
const signature = JSON.stringify({ domains: ordered, reports: includeReports });
|
|
3224
|
+
const hit = planBySignature.get(signature);
|
|
3225
|
+
if (hit !== undefined) return hit;
|
|
3226
|
+
const sourcePath = path.join(outDir, `.nomos_plan_${planSequence++}.ts`);
|
|
3227
|
+
writeFileSync(sourcePath, planEntrySource(ordered, includeReports), "utf8");
|
|
3228
|
+
const args = [
|
|
3229
|
+
sourcePath,
|
|
3230
|
+
"--bundle", "--format=iife", "--target=es2020", "--platform=neutral", "--charset=utf8",
|
|
3231
|
+
"--define:__nmsRuntimeLaw=true",
|
|
3232
|
+
...(compact ? ["--minify"] : []),
|
|
3233
|
+
];
|
|
3234
|
+
let bundled = execFileSync(esbuildBin, args, {
|
|
3235
|
+
encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], cwd: cfgDir, maxBuffer: 256 * 1024 * 1024,
|
|
3236
|
+
}).trimEnd() + "\n";
|
|
3237
|
+
bundled = canonicalizeEngineJavascript(bundled);
|
|
3238
|
+
if (!/globalThis\.plan\s*=/.test(bundled) && !/globalThis\["plan"\]\s*=/.test(bundled)) {
|
|
3239
|
+
fail(`generated plan asset does not assign globalThis.plan — refusing to package`);
|
|
3240
|
+
}
|
|
3241
|
+
const asset = { sha256: sha256HexUtf8(bundled), javascript: bundled };
|
|
3242
|
+
planBySignature.set(signature, asset);
|
|
3243
|
+
planAssets.push(asset);
|
|
3244
|
+
return asset;
|
|
3245
|
+
};
|
|
3246
|
+
const routePlan = (selector: string, domainKeys: readonly string[], includeReports = false): void => {
|
|
3247
|
+
const asset = compilePlan(domainKeys, includeReports);
|
|
3248
|
+
planRoutes.push({ selector, planSha256: asset.sha256 });
|
|
3249
|
+
};
|
|
3250
|
+
for (const key of orderedDomainKeys) routePlan(`plan:domain:${key}`, [key]);
|
|
3251
|
+
routePlan("describe", orderedDomainKeys);
|
|
3252
|
+
for (const reportId of Object.keys(cfg.reports ?? {})) routePlan(`report:report:${reportId}`, [], true);
|
|
3253
|
+
const routeTable = (
|
|
3254
|
+
prefix: string,
|
|
3255
|
+
table: Readonly<Record<string, readonly string[]>> | undefined,
|
|
3256
|
+
): void => {
|
|
3257
|
+
for (const [id, domains] of Object.entries(table ?? {})) routePlan(`${prefix}${id}`, domains);
|
|
3258
|
+
};
|
|
3259
|
+
routeTable("report:derive:", routing.derivedOf);
|
|
3260
|
+
routeTable("report:combine:", routing.combinedOf);
|
|
3261
|
+
routeTable("report:aggregateInvariant:", routing.aggregateInvariantOf);
|
|
3262
|
+
routeTable("report:invariant:", routing.relationOf);
|
|
3263
|
+
routeTable("report:workspaceInvariantReads:", routing.workspaceInvariantOf);
|
|
3264
|
+
routeTable("report:workspaceInvariant:", routing.workspaceInvariantOf);
|
|
3265
|
+
const workspaceInvariantDomains = [...new Set(Object.values(routing.workspaceInvariantOf ?? {}).flat())];
|
|
3266
|
+
if (workspaceInvariantDomains.length > 0) {
|
|
3267
|
+
routePlan("report:workspaceInvariantList", workspaceInvariantDomains);
|
|
2428
3268
|
}
|
|
2429
3269
|
|
|
2430
3270
|
// ── 1a. THE CAPTURED-READ GATE (#57, lifted-when-declared since #58) ──
|
|
@@ -2441,14 +3281,18 @@ async function main(): Promise<void> {
|
|
|
2441
3281
|
// Slice B: a FOREIGN (attested) read declaration ALSO opts the law into the read lane —
|
|
2442
3282
|
// `read(q, args, {from})` is served from the verified injected envelope, so a law whose
|
|
2443
3283
|
// only reads are foreign is just as declared as a local-read law.
|
|
2444
|
-
|
|
3284
|
+
// Framework-private authority law has its own declared read. It must not globally bless an undeclared
|
|
3285
|
+
// tenant `read()` merely because both happen to share one package lump. Only presented business law can
|
|
3286
|
+
// satisfy the tenant captured-read declaration gate; the private law uses `readFrameworkDeclared` and
|
|
3287
|
+
// deliberately does not carry the tenant marker.
|
|
3288
|
+
const declaresCapturedReads = presentationDomainModules.some((m) =>
|
|
2445
3289
|
m.directives.some(
|
|
2446
3290
|
(d) =>
|
|
2447
3291
|
((d as { declaredQueryReads?: unknown[] }).declaredQueryReads ?? []).length > 0 ||
|
|
2448
3292
|
((d as { declaredForeignReads?: unknown[] }).declaredForeignReads ?? []).length > 0,
|
|
2449
3293
|
),
|
|
2450
3294
|
);
|
|
2451
|
-
if (javascript.includes(CAPTURED_READ_LUMP_MARKER) && !declaresCapturedReads) {
|
|
3295
|
+
if (planAssets.some((asset) => asset.javascript.includes(CAPTURED_READ_LUMP_MARKER)) && !declaresCapturedReads) {
|
|
2452
3296
|
fail(
|
|
2453
3297
|
`this law references read() — the CAPTURED-READ lane — but NO directive declares a ` +
|
|
2454
3298
|
`captured-read query, so the sealed engine would provide no nomos.read and every ` +
|
|
@@ -2480,62 +3324,67 @@ async function main(): Promise<void> {
|
|
|
2480
3324
|
}
|
|
2481
3325
|
}
|
|
2482
3326
|
|
|
2483
|
-
// ── 1b. THE FROZEN-SANDBOX BOOT PROBE (
|
|
2484
|
-
// The sealed engine freezes globalThis after
|
|
3327
|
+
// ── 1b. THE FROZEN-SANDBOX BOOT PROBE (fail-closed) ──
|
|
3328
|
+
// The sealed engine freezes globalThis after a plan asset's top level; a lazy
|
|
2485
3329
|
// domain's module init runs LATER, inside the dispatch. Any module that writes
|
|
2486
3330
|
// an ambient global at init (the zod slots are pre-seeded above; this guards
|
|
2487
3331
|
// everything else) would deterministically throw at FIRST DISPATCH in
|
|
2488
3332
|
// production. Reproduce the sandbox order here — eval the real bundle in a
|
|
2489
3333
|
// fresh frozen context, then force every domain — and refuse to package on any
|
|
2490
3334
|
// failure that is not the expected unknown-directive refusal.
|
|
2491
|
-
|
|
3335
|
+
{
|
|
2492
3336
|
// A CHILD process: the probe must freeze a REAL global object (a `vm` context
|
|
2493
3337
|
// global refuses Object.freeze), and the lump must run STRICT (the engine
|
|
2494
3338
|
// evals it with the STRICT flag — sloppy mode would silently no-op the very
|
|
2495
3339
|
// frozen-global writes the probe exists to catch).
|
|
2496
|
-
const lumpPath = path.join(outDir, ".nomos_lump.js");
|
|
2497
|
-
writeFileSync(lumpPath, javascript, "utf8");
|
|
2498
3340
|
const probePath = path.join(outDir, ".nomos_boot_probe.cjs");
|
|
2499
3341
|
writeFileSync(
|
|
2500
3342
|
probePath,
|
|
2501
3343
|
[
|
|
2502
3344
|
`// AUTO-GENERATED by nomos-compile — the frozen-sandbox lazy-boot probe.`,
|
|
2503
3345
|
`const fs = require("node:fs");`,
|
|
2504
|
-
`const
|
|
2505
|
-
`const
|
|
2506
|
-
`(0, eval)('"use strict";\\n' +
|
|
3346
|
+
`const plan = fs.readFileSync(process.argv[2], "utf8");`,
|
|
3347
|
+
`const key = process.argv[3];`,
|
|
3348
|
+
`(0, eval)('"use strict";\\n' + plan); // indirect eval → real global scope, strict like the engine`,
|
|
2507
3349
|
`Object.freeze(globalThis); // the sealed sandbox freezes before dispatch`,
|
|
2508
|
-
`
|
|
2509
|
-
`
|
|
2510
|
-
`
|
|
2511
|
-
`
|
|
2512
|
-
` if (!/no directive registered/.test(msg)) { console.error(key + "\\u0000" + msg); process.exit(3); }`,
|
|
2513
|
-
`}`,
|
|
3350
|
+
`let msg;`,
|
|
3351
|
+
`try { globalThis.plan({ intent: { domain: key, directiveId: "__nomos_boot_probe__" } }); msg = "unexpected-success"; }`,
|
|
3352
|
+
`catch (e) { msg = String((e && e.message) || e); }`,
|
|
3353
|
+
`if (!/no directive registered/.test(msg)) { console.error(key + "\\u0000" + msg); process.exit(3); }`,
|
|
2514
3354
|
``,
|
|
2515
3355
|
].join("\n"),
|
|
2516
3356
|
"utf8",
|
|
2517
3357
|
);
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
3358
|
+
for (const route of planRoutes.filter((candidate) => candidate.selector.startsWith("plan:domain:"))) {
|
|
3359
|
+
const domainKey = route.selector.slice("plan:domain:".length);
|
|
3360
|
+
const asset = planAssets.find((candidate) => candidate.sha256 === route.planSha256)!;
|
|
3361
|
+
const planPath = path.join(outDir, `.nomos_boot_${planSequence++}.js`);
|
|
3362
|
+
writeFileSync(planPath, asset.javascript, "utf8");
|
|
3363
|
+
try {
|
|
3364
|
+
execFileSync(process.execPath, [probePath, planPath, domainKey], {
|
|
3365
|
+
encoding: "utf8", stdio: ["ignore", "ignore", "pipe"],
|
|
3366
|
+
});
|
|
3367
|
+
} catch (e) {
|
|
3368
|
+
const stderr = String((e as { stderr?: string }).stderr ?? "").trim();
|
|
3369
|
+
const [, msg = stderr] = stderr.includes("\u0000") ? stderr.split("\u0000") : ["?", stderr];
|
|
3370
|
+
fail(
|
|
3371
|
+
`domain '${domainKey}' failed its frozen-sandbox plan probe — a module initializer ` +
|
|
3372
|
+
`touches ambient globals after the freeze (the engine would refuse its first dispatch): ${msg}`,
|
|
3373
|
+
);
|
|
3374
|
+
}
|
|
2531
3375
|
}
|
|
2532
3376
|
}
|
|
2533
3377
|
|
|
2534
3378
|
// ── 2. USD IR + the package envelope ──
|
|
2535
3379
|
const usdJson = emitUsdJsonForModules(domainModules);
|
|
2536
|
-
const text =
|
|
3380
|
+
const text = nativePlanPackageUsda(
|
|
3381
|
+
cfg.name,
|
|
3382
|
+
JSON.parse(usdJson),
|
|
3383
|
+
domainModules,
|
|
3384
|
+
planAssets,
|
|
3385
|
+
planRoutes,
|
|
3386
|
+
);
|
|
2537
3387
|
const pkgPath = path.join(outDir, `${cfg.name}.package.usda`);
|
|
2538
|
-
writeFileSync(pkgPath, text, "utf8");
|
|
2539
3388
|
const domainHash = sha256HexUtf8(text);
|
|
2540
3389
|
|
|
2541
3390
|
// ── 2a. the REAL layered RELATION `.usda` set — an additive, deterministic visualization +
|
|
@@ -2563,22 +3412,54 @@ async function main(): Promise<void> {
|
|
|
2563
3412
|
}
|
|
2564
3413
|
}
|
|
2565
3414
|
|
|
2566
|
-
// ── 2b.
|
|
2567
|
-
//
|
|
2568
|
-
// homomorphism is ASSERTED fail-closed on every
|
|
2569
|
-
// per-module layer stack must
|
|
2570
|
-
//
|
|
3415
|
+
// ── 2b. REQUIRED LAYERED emission — the executable envelope above retains the
|
|
3416
|
+
// canonical resolved law, while these are the authored/composable source layers. The
|
|
3417
|
+
// homomorphism is ASSERTED fail-closed on every compile: the
|
|
3418
|
+
// per-module layer stack must resolve to the same canonical law embedded
|
|
3419
|
+
// in the package, or compilation refuses.
|
|
2571
3420
|
let layeredRootPath: string | undefined;
|
|
2572
3421
|
let layeredFileCount = 0;
|
|
2573
|
-
|
|
3422
|
+
let usdcatFlattenVerified = false;
|
|
3423
|
+
let composedUsdIdentity: string | undefined;
|
|
3424
|
+
{
|
|
2574
3425
|
const safe = (s: string) => s.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
2575
3426
|
const layersDir = path.join(outDir, "layers");
|
|
2576
3427
|
mkdirSync(layersDir, { recursive: true });
|
|
2577
3428
|
const allLayers: UsdLayer[] = [];
|
|
2578
3429
|
const relPaths: string[] = [];
|
|
2579
3430
|
const pendingWrites: { path: string; text: string }[] = [];
|
|
3431
|
+
const canonicalLayers = new Map(
|
|
3432
|
+
(JSON.parse(usdJson) as { layers: UsdLayer[] }).layers.map((layer) => [layer.path, layer] as const),
|
|
3433
|
+
);
|
|
2580
3434
|
for (const ld of layeredDomains) {
|
|
2581
|
-
|
|
3435
|
+
const authored = emitLayeredDomain(ld.key, ld.inputs, { applyBusinessCorrectionLayer: false });
|
|
3436
|
+
const canonicalLayer = canonicalLayers.get(`/Nomos/${ld.key}`);
|
|
3437
|
+
// Workspace invariants include compiler-owned consequences of the complete
|
|
3438
|
+
// application language (birth quota, receiving-group policy, recovery). They
|
|
3439
|
+
// are not owned by one source file, so seat the canonical declaration once in
|
|
3440
|
+
// the weakest package prelude instead of losing it or duplicating it. Contract
|
|
3441
|
+
// implementation links are likewise a cross-module fact: the contract may be
|
|
3442
|
+
// declared in one layer and implemented by an aggregate in another.
|
|
3443
|
+
const emittedLayers = authored.map((emitted, index) => {
|
|
3444
|
+
if (index !== 0 || canonicalLayer === undefined) return emitted;
|
|
3445
|
+
const needsPackagePrelude = canonicalLayer.nomosInvariantGate !== undefined || canonicalLayer.nomosContracts !== undefined;
|
|
3446
|
+
if (!needsPackagePrelude) return emitted;
|
|
3447
|
+
const layer: UsdLayer = {
|
|
3448
|
+
...emitted.layer,
|
|
3449
|
+
...(canonicalLayer.nomosInvariantGate !== undefined
|
|
3450
|
+
? { nomosInvariantGate: canonicalLayer.nomosInvariantGate }
|
|
3451
|
+
: {}),
|
|
3452
|
+
...(canonicalLayer.nomosContracts !== undefined
|
|
3453
|
+
? { nomosContracts: canonicalLayer.nomosContracts }
|
|
3454
|
+
: {}),
|
|
3455
|
+
};
|
|
3456
|
+
return {
|
|
3457
|
+
...emitted,
|
|
3458
|
+
layer,
|
|
3459
|
+
usda: usdaLayerText(ld.key, emitted.name, layer, ld.inputs[index]!.module, new Set()),
|
|
3460
|
+
};
|
|
3461
|
+
});
|
|
3462
|
+
for (const emitted of emittedLayers) {
|
|
2582
3463
|
const base = `${safe(ld.key)}--${safe(emitted.name)}`;
|
|
2583
3464
|
pendingWrites.push({ path: path.join(layersDir, `${base}.usda`), text: emitted.usda });
|
|
2584
3465
|
pendingWrites.push({
|
|
@@ -2590,20 +3471,89 @@ async function main(): Promise<void> {
|
|
|
2590
3471
|
}
|
|
2591
3472
|
}
|
|
2592
3473
|
const flat = flattenLayeredUsd(allLayers);
|
|
2593
|
-
|
|
2594
|
-
|
|
3474
|
+
composedUsdIdentity = sha256HexUtf8(canonicalJson(flat));
|
|
3475
|
+
const canonicalLaw = JSON.parse(usdJson);
|
|
3476
|
+
if (canonicalJson(flat) !== canonicalJson(canonicalLaw)) {
|
|
3477
|
+
const difference = firstLawDifference(canonicalLaw, flat) ?? "canonical law differs";
|
|
2595
3478
|
fail(
|
|
2596
3479
|
`layered emission does not flatten to the canonical composed IR — the module fold and ` +
|
|
2597
3480
|
`the layer-stack fold DISAGREE for this package (usually a re-declared directive ` +
|
|
2598
3481
|
`silently dropping an omit-when-empty reads/emits boundary, or a re-declared ` +
|
|
2599
3482
|
`aggregate dropping a field: USD shows the weaker opinion through; the module fold ` +
|
|
2600
|
-
`does not). Refusing to emit the
|
|
3483
|
+
`does not). First difference: ${difference}. Refusing to emit the package.`,
|
|
2601
3484
|
);
|
|
2602
3485
|
}
|
|
2603
3486
|
for (const w of pendingWrites) writeFileSync(w.path, w.text, "utf8");
|
|
2604
3487
|
layeredRootPath = path.join(outDir, `${cfg.name}.layered.usda`);
|
|
2605
3488
|
writeFileSync(layeredRootPath, layeredRootUsda(relPaths, flat), "utf8");
|
|
2606
3489
|
layeredFileCount = relPaths.length;
|
|
3490
|
+
|
|
3491
|
+
const usdcat = resolveUsdcat();
|
|
3492
|
+
if (usdcat === undefined) {
|
|
3493
|
+
if (requireUsdcat) {
|
|
3494
|
+
fail(
|
|
3495
|
+
"stock OpenUSD `usdcat` is required for this compile but was not found. " +
|
|
3496
|
+
"Install OpenUSD or set NOMOS_USDCAT to the executable path.",
|
|
3497
|
+
);
|
|
3498
|
+
}
|
|
3499
|
+
} else {
|
|
3500
|
+
let flattenedText: string;
|
|
3501
|
+
try {
|
|
3502
|
+
flattenedText = execFileSync(usdcat, ["--flatten", layeredRootPath], {
|
|
3503
|
+
encoding: "utf8",
|
|
3504
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3505
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
3506
|
+
});
|
|
3507
|
+
} catch (error) {
|
|
3508
|
+
const stderr = String((error as { stderr?: string }).stderr ?? "").trim();
|
|
3509
|
+
fail(`stock OpenUSD refused the generated layer stack: ${stderr || String(error)}`);
|
|
3510
|
+
}
|
|
3511
|
+
const openUsdLaw = parseUsdaDocument(flattenedText);
|
|
3512
|
+
if (canonicalJson(openUsdLaw) !== canonicalJson(canonicalLaw)) {
|
|
3513
|
+
const difference = firstLawDifference(canonicalLaw, openUsdLaw) ?? "canonical law differs";
|
|
3514
|
+
fail(
|
|
3515
|
+
`stock OpenUSD and Nomos resolve different effective law. First difference: ${difference}. ` +
|
|
3516
|
+
`Refusing to emit the package.`,
|
|
3517
|
+
);
|
|
3518
|
+
}
|
|
3519
|
+
usdcatFlattenVerified = true;
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
// The executable package is the COMMITTED result of composition. Do not leave
|
|
3523
|
+
// a newly-written package behind when either Nomos or stock OpenUSD rejects its
|
|
3524
|
+
// source stack; rejected layer artifacts remain useful diagnostics, not runnable law.
|
|
3525
|
+
writeFileSync(pkgPath, text, "utf8");
|
|
3526
|
+
|
|
3527
|
+
// ── 2c. THE PUBLISHED OPENUSD PACKAGE ASSET ──
|
|
3528
|
+
// `rootPart` is the explicit package/application boundary from the OpenUSD composition ADR.
|
|
3529
|
+
// The asset references the REAL authored layer stack. It deliberately contains neither the
|
|
3530
|
+
// legacy executable lump nor a hidden JSON copy of the law: this is the source of truth the
|
|
3531
|
+
// application composer consumes, and the shape the ledger/runtime migration targets.
|
|
3532
|
+
let packageAssetPath: string | undefined;
|
|
3533
|
+
if (cfg.rootPart !== undefined) {
|
|
3534
|
+
try {
|
|
3535
|
+
const packageAsset = packageAssetUsda({
|
|
3536
|
+
packageName: cfg.name,
|
|
3537
|
+
packageIdentity: composedUsdIdentity!,
|
|
3538
|
+
domains: presentationDomainModules.map((module) => module.domain ?? module.name),
|
|
3539
|
+
root: cfg.rootPart,
|
|
3540
|
+
lawAsset: `./${cfg.name}.layered.usda`,
|
|
3541
|
+
});
|
|
3542
|
+
packageAssetPath = path.join(outDir, `${cfg.name}.asset.usda`);
|
|
3543
|
+
const candidateAssetPath = path.join(outDir, `.${cfg.name}.asset.candidate.usda`);
|
|
3544
|
+
writeFileSync(candidateAssetPath, packageAsset, "utf8");
|
|
3545
|
+
const usdcat = resolveUsdcat();
|
|
3546
|
+
if (usdcat !== undefined) {
|
|
3547
|
+
execFileSync(usdcat, ["--loadOnly", candidateAssetPath], {
|
|
3548
|
+
encoding: "utf8",
|
|
3549
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
3550
|
+
});
|
|
3551
|
+
}
|
|
3552
|
+
renameSync(candidateAssetPath, packageAssetPath);
|
|
3553
|
+
} catch (error) {
|
|
3554
|
+
rmSync(path.join(outDir, `.${cfg.name}.asset.candidate.usda`), { force: true });
|
|
3555
|
+
fail(`published OpenUSD package asset refused: ${(error as Error).message}`);
|
|
3556
|
+
}
|
|
2607
3557
|
}
|
|
2608
3558
|
|
|
2609
3559
|
// ── 3. the IDENTITY emit (for the summary's per-domain identityHash) ──
|
|
@@ -2667,7 +3617,7 @@ async function main(): Promise<void> {
|
|
|
2667
3617
|
// (currentLaw); the host ingests it and builds its router from it — no domain/directive name in host
|
|
2668
3618
|
// source. Self-describing for the whole web ecosystem (Swagger/codegen/Hono/agent tool-calling).
|
|
2669
3619
|
const publicOpenapi = mergeOpenApiFragments(
|
|
2670
|
-
|
|
3620
|
+
presentationDomainModules.map((m) => generateOpenApiFragment(m, { lawHash: domainHash })),
|
|
2671
3621
|
{ title: `${cfg.name} (nomos package)`, lawHash: domainHash },
|
|
2672
3622
|
);
|
|
2673
3623
|
const runtimeOpenapi = mergeOpenApiFragments(
|
|
@@ -2743,6 +3693,10 @@ async function main(): Promise<void> {
|
|
|
2743
3693
|
}
|
|
2744
3694
|
} catch (e) {
|
|
2745
3695
|
proofSkip = (e as Error).message;
|
|
3696
|
+
// A skipped generation must not leave yesterday's client story in build/. Otherwise editors and CI
|
|
3697
|
+
// typecheck a stale proof which no longer describes the compiled law — worse than having no proof.
|
|
3698
|
+
rmSync(path.join(outDir, `${cfg.name}.proof.mts`), { force: true });
|
|
3699
|
+
rmSync(path.join(outDir, `${cfg.name}.proof-legs.json`), { force: true });
|
|
2746
3700
|
}
|
|
2747
3701
|
// BIRTHS DX: a `.births()` directive that can't be (or can only shakily be) proven says WHY here —
|
|
2748
3702
|
// never a silent skip, never an opaque runtime `born=[]` first contact. When the .mts generation
|
|
@@ -2767,7 +3721,10 @@ async function main(): Promise<void> {
|
|
|
2767
3721
|
` queries: ${d.queries.join(", ") || "—"} counts: ${d.counts.join(", ") || "—"} sums: ${d.sums.join(", ") || "—"}`,
|
|
2768
3722
|
]),
|
|
2769
3723
|
`deploy: POST ${cfg.name}.deploy.json to /v2/workspaces/<ws>/domains (or: githolon deploy <ws>)`,
|
|
2770
|
-
|
|
3724
|
+
packageAssetPath !== undefined
|
|
3725
|
+
? `package asset: ${path.basename(packageAssetPath)} — the real USD package root consumed by application composition`
|
|
3726
|
+
: `package asset: NOT EMITTED — declare rootPart: { name, kind: "component" | "assembly" }`,
|
|
3727
|
+
`typed application: ${cfg.name}.client.mjs — open${cfg.name.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part) => part[0]!.toUpperCase() + part.slice(1)).join("")}Application({ cloud, signIn })`,
|
|
2771
3728
|
proofPath !== undefined
|
|
2772
3729
|
? `generated proof: ${cfg.name}.proof.mts — run: githolon proof (live; ends ALL GREEN or names the jam)`
|
|
2773
3730
|
: `generated proof: SKIPPED — ${proofSkip}`,
|
|
@@ -2777,14 +3734,257 @@ async function main(): Promise<void> {
|
|
|
2777
3734
|
|
|
2778
3735
|
// ── 5. the typed TS client (#10 — the web sibling of codegen_dart), with the
|
|
2779
3736
|
// deployed law's content hash baked in (emitted AFTER the package is hashed) ──
|
|
2780
|
-
const clientPath = path.join(outDir, `${cfg.name}.client.
|
|
2781
|
-
const
|
|
3737
|
+
const clientPath = path.join(outDir, `${cfg.name}.client.mjs`);
|
|
3738
|
+
const clientDeclarationPath = path.join(outDir, `${cfg.name}.client.d.mts`);
|
|
3739
|
+
const clientSource = generateTsClient(presentationDomainModules, {
|
|
3740
|
+
packageName: cfg.name,
|
|
3741
|
+
domainHash,
|
|
3742
|
+
interface: packageInterface,
|
|
3743
|
+
compat: nomosCompat,
|
|
3744
|
+
homeDomain: resolveApplicationTopology(presentationDomainModules).homeDomain,
|
|
3745
|
+
});
|
|
3746
|
+
const runtimePath = path.join(DSL_DIR, "runtime", "generated-application-host.js");
|
|
3747
|
+
if (!existsSync(runtimePath)) {
|
|
3748
|
+
fail("the compiler-private generated application runtime is missing — reinstall @githolon/dsl");
|
|
3749
|
+
}
|
|
3750
|
+
const temporaryClientPath = path.join(outDir, `.${cfg.name}.client.source.ts`);
|
|
3751
|
+
const temporaryRuntimeTypesPath = path.join(outDir, `.${cfg.name}.application-runtime.d.ts`);
|
|
3752
|
+
const temporaryDeclarationPath = temporaryClientPath.replace(/\.ts$/, ".d.ts");
|
|
3753
|
+
writeFileSync(temporaryClientPath, clientSource, "utf8");
|
|
3754
|
+
writeFileSync(temporaryRuntimeTypesPath, [
|
|
3755
|
+
`export declare function nomosInternalOpenGeneratedApplication<T extends object>(`,
|
|
3756
|
+
` options: { cloud: string; application: string; signIn: (request?: { readonly deviceChallenge?: string }) => Promise<string> },`,
|
|
3757
|
+
` bind: (holon: unknown, user: { readonly identityId: string; readonly identityAddress: string }) => T,`,
|
|
3758
|
+
`): Promise<T & {`,
|
|
3759
|
+
` currentStatus(): Promise<any>;`,
|
|
3760
|
+
` watchStatus(listener: (status: any) => void): () => void;`,
|
|
3761
|
+
`}>;`,
|
|
3762
|
+
`export declare function nomosInternalOpenGeneratedChildApplication<T extends object>(`,
|
|
3763
|
+
` parent: unknown,`,
|
|
3764
|
+
` workspace: string,`,
|
|
3765
|
+
` bind: (holon: unknown) => T,`,
|
|
3766
|
+
`): Promise<T>;`,
|
|
3767
|
+
`export declare function nomosInternalCreateInvitation(`,
|
|
3768
|
+
` holon: unknown,`,
|
|
3769
|
+
`): Promise<{ readonly address: string; readonly token: string }>;`,
|
|
3770
|
+
`export declare function nomosInternalRedeemInvitation(`,
|
|
3771
|
+
` holon: unknown,`,
|
|
3772
|
+
` link: string,`,
|
|
3773
|
+
` policies: readonly { readonly domain: string; readonly domainHash: string; readonly query: string; readonly redeem: string; readonly personField: string; readonly roleField: string; readonly scopeField: string }[],`,
|
|
3774
|
+
`): Promise<void>;`,
|
|
3775
|
+
``,
|
|
3776
|
+
].join("\n"), "utf8");
|
|
2782
3777
|
try {
|
|
2783
|
-
|
|
3778
|
+
const typescriptEntry = createRequire(import.meta.url).resolve("typescript/lib/typescript.js");
|
|
3779
|
+
const ts = (await import(pathToFileURL(typescriptEntry).href)) as typeof import("typescript");
|
|
3780
|
+
const program = ts.createProgram(
|
|
3781
|
+
[temporaryClientPath, temporaryRuntimeTypesPath],
|
|
3782
|
+
{
|
|
3783
|
+
target: ts.ScriptTarget.ES2022,
|
|
3784
|
+
module: ts.ModuleKind.NodeNext,
|
|
3785
|
+
moduleResolution: ts.ModuleResolutionKind.NodeNext,
|
|
3786
|
+
declaration: true,
|
|
3787
|
+
emitDeclarationOnly: true,
|
|
3788
|
+
noEmitOnError: true,
|
|
3789
|
+
strict: true,
|
|
3790
|
+
skipLibCheck: true,
|
|
3791
|
+
allowImportingTsExtensions: true,
|
|
3792
|
+
baseUrl: outDir,
|
|
3793
|
+
paths: {
|
|
3794
|
+
"@githolon/client/_generated_application_host_7c1f5e": [temporaryRuntimeTypesPath],
|
|
3795
|
+
},
|
|
3796
|
+
outDir,
|
|
3797
|
+
},
|
|
3798
|
+
);
|
|
3799
|
+
const emit = program.emit();
|
|
3800
|
+
const diagnostics = [...ts.getPreEmitDiagnostics(program), ...emit.diagnostics];
|
|
3801
|
+
if (diagnostics.length > 0 || emit.emitSkipped || !existsSync(temporaryDeclarationPath)) {
|
|
3802
|
+
const detail = diagnostics.map((diagnostic) => {
|
|
3803
|
+
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
|
|
3804
|
+
if (diagnostic.file === undefined || diagnostic.start === undefined) return message;
|
|
3805
|
+
const point = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
|
|
3806
|
+
return `${path.basename(diagnostic.file.fileName)}:${point.line + 1}:${point.character + 1}: ${message}`;
|
|
3807
|
+
}).join("\n");
|
|
3808
|
+
fail(`generated TypeScript application declarations are invalid — this is a Nomos codegen bug:\n${detail}`);
|
|
3809
|
+
}
|
|
3810
|
+
const sourceFile = program.getSourceFile(temporaryClientPath);
|
|
3811
|
+
const applicationBusinessName = `_${tsApplicationName(cfg.name)}Business`;
|
|
3812
|
+
const businessAlias = sourceFile?.statements.find(
|
|
3813
|
+
(statement): statement is import("typescript").TypeAliasDeclaration =>
|
|
3814
|
+
ts.isTypeAliasDeclaration(statement) && statement.name.text === applicationBusinessName,
|
|
3815
|
+
);
|
|
3816
|
+
if (sourceFile === undefined || businessAlias === undefined) {
|
|
3817
|
+
fail(`generated TypeScript application has no ${applicationBusinessName} type`);
|
|
3818
|
+
}
|
|
3819
|
+
const checker = program.getTypeChecker();
|
|
3820
|
+
const structuralAliases = sourceFile.statements.filter(
|
|
3821
|
+
(statement): statement is import("typescript").TypeAliasDeclaration =>
|
|
3822
|
+
ts.isTypeAliasDeclaration(statement) &&
|
|
3823
|
+
(statement.name.text === applicationBusinessName ||
|
|
3824
|
+
(statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true &&
|
|
3825
|
+
statement.getText(sourceFile).includes("_NomosPublicDomain<"))),
|
|
3826
|
+
);
|
|
3827
|
+
const expansions = new Map<string, { exported: boolean; body: string }>();
|
|
3828
|
+
for (const alias of structuralAliases) {
|
|
3829
|
+
const aliasType = checker.getTypeFromTypeNode(alias.type);
|
|
3830
|
+
const publicProperties = checker.getPropertiesOfType(aliasType);
|
|
3831
|
+
if (alias.name.text === applicationBusinessName && publicProperties.length === 0) {
|
|
3832
|
+
fail(`generated TypeScript application has no public business members`);
|
|
3833
|
+
}
|
|
3834
|
+
// `typeToString(theWholeAlias)` is not a public-boundary operation. For a large intersection TypeScript
|
|
3835
|
+
// deliberately preserves `_NomosPublicDomain<{ domain; domainHash; interface; ... }>` rather than
|
|
3836
|
+
// reducing Omit, which makes every private property textually reachable from the declaration pruner.
|
|
3837
|
+
// Ask the checker for the alias's ACTUAL properties first, then render each public member independently.
|
|
3838
|
+
// This applies equally to the root application and every child application returned by a business birth.
|
|
3839
|
+
const body = `{\n${publicProperties.map((property) => {
|
|
3840
|
+
const propertyType = checker.getTypeOfSymbolAtLocation(property, alias);
|
|
3841
|
+
const rendered = checker.typeToString(
|
|
3842
|
+
propertyType,
|
|
3843
|
+
alias,
|
|
3844
|
+
ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.InTypeAlias,
|
|
3845
|
+
);
|
|
3846
|
+
const optional = (property.flags & ts.SymbolFlags.Optional) !== 0 ? "?" : "";
|
|
3847
|
+
return ` readonly ${JSON.stringify(property.getName())}${optional}: ${rendered};`;
|
|
3848
|
+
}).join("\n")}\n}`;
|
|
3849
|
+
expansions.set(alias.name.text, {
|
|
3850
|
+
exported: alias.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true,
|
|
3851
|
+
body,
|
|
3852
|
+
});
|
|
3853
|
+
}
|
|
3854
|
+
if (!expansions.has(applicationBusinessName)) {
|
|
3855
|
+
fail(`generated TypeScript application has no public business members`);
|
|
3856
|
+
}
|
|
3857
|
+
let declarationSource = readFileSync(temporaryDeclarationPath, "utf8");
|
|
3858
|
+
const emittedDeclaration = ts.createSourceFile(
|
|
3859
|
+
temporaryDeclarationPath,
|
|
3860
|
+
declarationSource,
|
|
3861
|
+
ts.ScriptTarget.Latest,
|
|
3862
|
+
true,
|
|
3863
|
+
ts.ScriptKind.TS,
|
|
3864
|
+
);
|
|
3865
|
+
// A type alias may contain hundreds of semicolon-delimited object members. The previous regexp stopped
|
|
3866
|
+
// at the first one, so a real multi-domain application silently retained ReturnType<privateFactory>
|
|
3867
|
+
// dependencies and dragged raw domain hashes, binders and compatibility manifests into the tenant .d.mts.
|
|
3868
|
+
// Replace complete declaration nodes from the end of the file backward; the reachability pass below can
|
|
3869
|
+
// then discard private factories and every framework-only type they were keeping alive.
|
|
3870
|
+
const structuralDeclarations = emittedDeclaration.statements
|
|
3871
|
+
.filter((statement): statement is import("typescript").TypeAliasDeclaration =>
|
|
3872
|
+
ts.isTypeAliasDeclaration(statement) && expansions.has(statement.name.text))
|
|
3873
|
+
.sort((left, right) => right.getStart(emittedDeclaration) - left.getStart(emittedDeclaration));
|
|
3874
|
+
if (!structuralDeclarations.some((statement) => statement.name.text === applicationBusinessName)) {
|
|
3875
|
+
fail(`generated TypeScript declaration has no ${applicationBusinessName} type`);
|
|
3876
|
+
}
|
|
3877
|
+
for (const declaration of structuralDeclarations) {
|
|
3878
|
+
const expansion = expansions.get(declaration.name.text)!;
|
|
3879
|
+
declarationSource =
|
|
3880
|
+
declarationSource.slice(0, declaration.getStart(emittedDeclaration)) +
|
|
3881
|
+
`${expansion.exported ? "export " : ""}type ${declaration.name.text} = ${expansion.body};` +
|
|
3882
|
+
declarationSource.slice(declaration.end);
|
|
3883
|
+
}
|
|
3884
|
+
|
|
3885
|
+
// Declaration emit follows private aliases that appear in exported type expressions. Replace the one
|
|
3886
|
+
// application alias with its already-filtered structural business type, then retain only declarations
|
|
3887
|
+
// reachable from real exports. The resulting .d.mts cannot teach or autocomplete NomosHolon, raw offers,
|
|
3888
|
+
// binders or compatibility machinery because none are part of the public graph. Record coordinates remain
|
|
3889
|
+
// deliberately reachable through NomosRef<T>: addresses are public and portable; authority stays private.
|
|
3890
|
+
const declarationFile = ts.createSourceFile(
|
|
3891
|
+
clientDeclarationPath,
|
|
3892
|
+
declarationSource,
|
|
3893
|
+
ts.ScriptTarget.Latest,
|
|
3894
|
+
true,
|
|
3895
|
+
ts.ScriptKind.TS,
|
|
3896
|
+
);
|
|
3897
|
+
const namesOf = (statement: import("typescript").Statement): readonly string[] => {
|
|
3898
|
+
if (
|
|
3899
|
+
ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) ||
|
|
3900
|
+
ts.isClassDeclaration(statement) || ts.isEnumDeclaration(statement) ||
|
|
3901
|
+
ts.isFunctionDeclaration(statement)
|
|
3902
|
+
) return statement.name === undefined ? [] : [statement.name.text];
|
|
3903
|
+
if (ts.isVariableStatement(statement)) {
|
|
3904
|
+
return statement.declarationList.declarations.flatMap((declaration) =>
|
|
3905
|
+
ts.isIdentifier(declaration.name) ? [declaration.name.text] : []
|
|
3906
|
+
);
|
|
3907
|
+
}
|
|
3908
|
+
return [];
|
|
3909
|
+
};
|
|
3910
|
+
const exported = (statement: import("typescript").Statement): boolean =>
|
|
3911
|
+
ts.isExportDeclaration(statement) || ts.isExportAssignment(statement) ||
|
|
3912
|
+
(ts.canHaveModifiers(statement) &&
|
|
3913
|
+
(ts.getModifiers(statement)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false));
|
|
3914
|
+
const declarations = new Map<string, number>();
|
|
3915
|
+
declarationFile.statements.forEach((statement, index) => {
|
|
3916
|
+
for (const name of namesOf(statement)) declarations.set(name, index);
|
|
3917
|
+
});
|
|
3918
|
+
const keep = new Set<number>();
|
|
3919
|
+
const queue: number[] = [];
|
|
3920
|
+
declarationFile.statements.forEach((statement, index) => {
|
|
3921
|
+
if (exported(statement)) { keep.add(index); queue.push(index); }
|
|
3922
|
+
});
|
|
3923
|
+
while (queue.length > 0) {
|
|
3924
|
+
const index = queue.pop()!;
|
|
3925
|
+
const visit = (node: import("typescript").Node): void => {
|
|
3926
|
+
if (ts.isIdentifier(node)) {
|
|
3927
|
+
const dependency = declarations.get(node.text);
|
|
3928
|
+
if (dependency !== undefined && !keep.has(dependency)) {
|
|
3929
|
+
keep.add(dependency);
|
|
3930
|
+
queue.push(dependency);
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
ts.forEachChild(node, visit);
|
|
3934
|
+
};
|
|
3935
|
+
visit(declarationFile.statements[index]!);
|
|
3936
|
+
}
|
|
3937
|
+
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, removeComments: false });
|
|
3938
|
+
declarationSource = [
|
|
3939
|
+
"// GENERATED by nomos-compile — the only TypeScript tenant surface. DO NOT EDIT.",
|
|
3940
|
+
...declarationFile.statements
|
|
3941
|
+
.filter((_statement, index) => keep.has(index))
|
|
3942
|
+
.map((statement) => printer.printNode(ts.EmitHint.Unspecified, statement, declarationFile)),
|
|
3943
|
+
"",
|
|
3944
|
+
].join("\n");
|
|
3945
|
+
writeFileSync(temporaryDeclarationPath, declarationSource, "utf8");
|
|
3946
|
+
renameSync(temporaryDeclarationPath, clientDeclarationPath);
|
|
3947
|
+
await esbuildMod.build({
|
|
3948
|
+
stdin: {
|
|
3949
|
+
contents: clientSource,
|
|
3950
|
+
loader: "ts",
|
|
3951
|
+
resolveDir: outDir,
|
|
3952
|
+
sourcefile: `${cfg.name}.client.ts`,
|
|
3953
|
+
},
|
|
3954
|
+
outfile: clientPath,
|
|
3955
|
+
bundle: true,
|
|
3956
|
+
packages: "bundle",
|
|
3957
|
+
platform: "node",
|
|
3958
|
+
format: "esm",
|
|
3959
|
+
target: "node20",
|
|
3960
|
+
legalComments: "none",
|
|
3961
|
+
logLevel: "silent",
|
|
3962
|
+
plugins: [{
|
|
3963
|
+
name: "nomos-generated-application-runtime",
|
|
3964
|
+
setup(build) {
|
|
3965
|
+
build.onResolve(
|
|
3966
|
+
{ filter: /^@githolon\/client\/_generated_application_host_7c1f5e$/ },
|
|
3967
|
+
() => ({ path: runtimePath }),
|
|
3968
|
+
);
|
|
3969
|
+
},
|
|
3970
|
+
}],
|
|
3971
|
+
});
|
|
3972
|
+
const bundled = canonicalizeGeneratedClientJavascript(readFileSync(clientPath, "utf8"));
|
|
3973
|
+
writeFileSync(clientPath, bundled, "utf8");
|
|
3974
|
+
if (
|
|
3975
|
+
/^import\s+.*["']@githolon\/client/m.test(bundled) ||
|
|
3976
|
+
/export\s*\{[^}]*nomosInternalOpenGeneratedApplication/s.test(bundled)
|
|
3977
|
+
) {
|
|
3978
|
+
fail("generated TypeScript application did not seal its private runtime binding");
|
|
3979
|
+
}
|
|
2784
3980
|
} catch (e) {
|
|
2785
|
-
fail(`generated TypeScript
|
|
3981
|
+
fail(`generated TypeScript application could not be sealed — this is a Nomos codegen bug: ${(e as Error).message}`);
|
|
3982
|
+
} finally {
|
|
3983
|
+
rmSync(temporaryClientPath, { force: true });
|
|
3984
|
+
rmSync(temporaryRuntimeTypesPath, { force: true });
|
|
3985
|
+
rmSync(temporaryDeclarationPath, { force: true });
|
|
3986
|
+
rmSync(path.join(outDir, `${cfg.name}.client.ts`), { force: true });
|
|
2786
3987
|
}
|
|
2787
|
-
writeFileSync(clientPath, clientSource, "utf8");
|
|
2788
3988
|
|
|
2789
3989
|
// ── 6. the typed Dart frontend (opt-in; the Flutter sibling of step 5, emitted
|
|
2790
3990
|
// after the package hash like the TS client) ──
|
|
@@ -2830,7 +4030,10 @@ async function main(): Promise<void> {
|
|
|
2830
4030
|
// COMPILE ATOMICITY (co2): the Dart emit is transactional (beginAtomicGenDir) — a mid-emit fail()/throw
|
|
2831
4031
|
// restores the prior good packages instead of leaving them wiped/partial. Finalized at process exit.
|
|
2832
4032
|
if (dartPackages !== undefined) {
|
|
2833
|
-
const r = emitDartPackages(
|
|
4033
|
+
const r = emitDartPackages(
|
|
4034
|
+
presentationDomainModules, dartOut, domainHash, packageInterface, dartPackages,
|
|
4035
|
+
dartNames, nomosCompat, cfg.name, deployPath,
|
|
4036
|
+
);
|
|
2834
4037
|
dartFiles = r.written;
|
|
2835
4038
|
dartCursedHints = r.cursed;
|
|
2836
4039
|
} else {
|
|
@@ -2889,8 +4092,9 @@ async function main(): Promise<void> {
|
|
|
2889
4092
|
if (selfDir.includes(`${path.sep}node_modules${path.sep}`)) return undefined;
|
|
2890
4093
|
const client = path.resolve(selfDir, "..", "dart");
|
|
2891
4094
|
const types = path.resolve(selfDir, "..", "dart_types");
|
|
4095
|
+
const flutter = path.resolve(selfDir, "..", "..", "nomos_flutter");
|
|
2892
4096
|
if (existsSync(path.join(client, "pubspec.yaml")) && existsSync(path.join(types, "pubspec.yaml"))) {
|
|
2893
|
-
return { client, types };
|
|
4097
|
+
return { client, types, ...(existsSync(path.join(flutter, "pubspec.yaml")) ? { flutter } : {}) };
|
|
2894
4098
|
}
|
|
2895
4099
|
return undefined;
|
|
2896
4100
|
})();
|
|
@@ -2903,6 +4107,7 @@ async function main(): Promise<void> {
|
|
|
2903
4107
|
const lines = ["dependency_overrides:"];
|
|
2904
4108
|
const usesClient = /(^|\n)\s+nomos_client\s*:/.test(spec);
|
|
2905
4109
|
const usesTypes = /(^|\n)\s+nomos_types\s*:/.test(spec);
|
|
4110
|
+
const usesFlutter = /(^|\n)\s+nomos_flutter\s*:/.test(spec);
|
|
2906
4111
|
if (usesClient) lines.push(` nomos_client:`, ` path: ${localDart.client}`);
|
|
2907
4112
|
// A generated CLIENT or TEST package may only mention nomos_client directly, but that local
|
|
2908
4113
|
// package depends on the simultaneously-developed nomos_types. Pub overrides do not propagate
|
|
@@ -2911,6 +4116,11 @@ async function main(): Promise<void> {
|
|
|
2911
4116
|
// analyzer ever sees the code — exactly the package-skew ceremony this local dogfood gate exists
|
|
2912
4117
|
// to absorb for tenant developers.
|
|
2913
4118
|
if (usesTypes || usesClient) lines.push(` nomos_types:`, ` path: ${localDart.types}`);
|
|
4119
|
+
// The generated client application imports nomos_flutter, and a TEST package reaches that
|
|
4120
|
+
// application transitively through its generated client sibling. Overrides do not propagate,
|
|
4121
|
+
// so every local nomos_client solve root must also seat the matching local Flutter host even
|
|
4122
|
+
// when its own pubspec does not name nomos_flutter directly.
|
|
4123
|
+
if ((usesFlutter || usesClient) && localDart.flutter) lines.push(` nomos_flutter:`, ` path: ${localDart.flutter}`);
|
|
2914
4124
|
if (lines.length > 1) writeFileSync(path.join(pkg, "pubspec_overrides.yaml"), lines.join("\n") + "\n", "utf8");
|
|
2915
4125
|
}
|
|
2916
4126
|
}
|
|
@@ -2962,7 +4172,12 @@ async function main(): Promise<void> {
|
|
|
2962
4172
|
console.log(` domainHash ${domainHash}`);
|
|
2963
4173
|
if (layeredRootPath !== undefined) {
|
|
2964
4174
|
console.log(
|
|
2965
|
-
` layered ${rel(layeredRootPath)} (${layeredFileCount} module layer(s) in build/layers/ — flatten
|
|
4175
|
+
` layered ${rel(layeredRootPath)} (${layeredFileCount} module layer(s) in build/layers/ — Nomos flatten resolves the canonical law)`,
|
|
4176
|
+
);
|
|
4177
|
+
console.log(
|
|
4178
|
+
usdcatFlattenVerified
|
|
4179
|
+
? ` usdcat ✓ stock OpenUSD --flatten resolves the same canonical law`
|
|
4180
|
+
: ` usdcat ⚠ unavailable — set NOMOS_REQUIRE_USDCAT=1 in release CI to make this proof mandatory`,
|
|
2966
4181
|
);
|
|
2967
4182
|
}
|
|
2968
4183
|
{
|
|
@@ -2974,9 +4189,6 @@ async function main(): Promise<void> {
|
|
|
2974
4189
|
// from a plan's deterministic secondary `ensure(...)` — the id-mint-gate DX made visible (the author
|
|
2975
4190
|
// wrote `ensure(B, …)`; the compiler declared `Ensure → B` so the deterministic id admits).
|
|
2976
4191
|
for (const m of presentationDomainModules) {
|
|
2977
|
-
for (const ty of m.autoDeclaredEnsures ?? []) {
|
|
2978
|
-
console.log(` ensure auto-declared Ensure→${ty} in '${m.domain ?? m.name}' from a plan's secondary ensure(...) (id-mint gate opened)`);
|
|
2979
|
-
}
|
|
2980
4192
|
}
|
|
2981
4193
|
const presentedIdentityNames = new Set(presentationDomainModules.map((m) => m.domain ?? m.name));
|
|
2982
4194
|
const presentedHashes = Object.entries(identity.hashes).filter(([domain]) => presentedIdentityNames.has(domain));
|
|
@@ -3072,19 +4284,6 @@ async function main(): Promise<void> {
|
|
|
3072
4284
|
}
|
|
3073
4285
|
}
|
|
3074
4286
|
console.log(` deploy ${rel(deployPath)}`);
|
|
3075
|
-
if (compact) {
|
|
3076
|
-
// COMPACT PACKAGES ride a base64 blob (nomos:usdJsonB64) — a peer needs the b64 decode arm
|
|
3077
|
-
// (kernel ≥ the 2026-07-04 release; every current cloud/browser/node runtime has it). A NATIVE
|
|
3078
|
-
// (iOS/macOS AOT) client pins its OWN kernel binary: if that xcframework predates the arm, install
|
|
3079
|
-
// fails with `parse embedded USDA IR: domain package missing USDA field nomos:usdJsonHex`. Flag it
|
|
3080
|
-
// so the tenant isn't surprised (the requested "flag compact when targeting native").
|
|
3081
|
-
console.log(``);
|
|
3082
|
-
console.log(` ⚠ compact package (base64 blob): requires a kernel with the b64 decode arm (≥ 2026-07-04).`);
|
|
3083
|
-
console.log(` Current cloud/browser/node runtimes have it. A NATIVE client (nomos_flutter iOS/macOS)`);
|
|
3084
|
-
console.log(` pins its OWN kernel — refetch the current NomosKernel.xcframework (rm ios/Frameworks/`);
|
|
3085
|
-
console.log(` NomosKernel.xcframework && pod install), or set { compact: false } for a hex package that`);
|
|
3086
|
-
console.log(` parses on every kernel (larger deploy body; may need the signed-upload deploy lane).`);
|
|
3087
|
-
}
|
|
3088
4287
|
console.log(``);
|
|
3089
4288
|
console.log(`your business path:`);
|
|
3090
4289
|
if (proofPath !== undefined) {
|