@githolon/dsl 0.102.4 → 0.103.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/compile_package.mjs +43 -2
  2. package/compile_proof.d.mts +47 -0
  3. package/compile_proof.mjs +343 -0
  4. package/dart/CHANGELOG.md +7 -0
  5. package/dart/lib/src/dev_cloud.dart +8 -8
  6. package/dart/lib/src/nms_generated_runtime.dart +2 -0
  7. package/dart/lib/src/runner/node-entry.mjs +5504 -1236
  8. package/dart/lib/src/runner/offline-install.mjs +4578 -528
  9. package/dart/lib/src/test_harness.dart +206 -152
  10. package/dart/lib/src/webview_bridge.dart +196 -89
  11. package/dart/pubspec.lock +1 -1
  12. package/dart/pubspec.yaml +2 -2
  13. package/dart/test/acquire_session_credentials_test.dart +4 -3
  14. package/dart/test/adv_realm_interference_test.dart +7 -6
  15. package/dart/test/cloud_bound_topology_test.dart +3 -3
  16. package/dart/test/cluster_births_test.dart +4 -3
  17. package/dart/test/cluster_device_keyed_acceptance_test.dart +10 -16
  18. package/dart/test/cluster_local_autosync_test.dart +3 -2
  19. package/dart/test/cluster_scale_test.dart +3 -2
  20. package/dart/test/dlq_session_surface_test.dart +7 -6
  21. package/dart/test/first_app_gauntlet_test.dart +14 -21
  22. package/dart/test/fixtures/wedge/fake-install.mjs +1 -1
  23. package/dart/test/four_layer_nested_topology_test.dart +4 -4
  24. package/dart/test/harness_phase_timeout_test.dart +2 -2
  25. package/dart/test/local_sync_inert_test.dart +1 -1
  26. package/dart/test/parent_signer_recovery_test.dart +2 -2
  27. package/dart/test/sealing_client_test.dart +4 -3
  28. package/dart/test/session_bound_test.dart +8 -7
  29. package/dart/test/test_harness_test.dart +13 -12
  30. package/dart/test/vo_read_typing_test.dart +4 -3
  31. package/dart/test/watch_after_offer_stall_test.dart +4 -3
  32. package/dart/test/webview_bridge_test.dart +102 -5
  33. package/dart_types/CHANGELOG.md +5 -0
  34. package/dart_types/lib/nomos_types.dart +9 -0
  35. package/dart_types/lib/src/nomos_ref.dart +20 -0
  36. package/dart_types/lib/src/optional_ref.dart +85 -0
  37. package/dart_types/lib/src/sha256_digest.dart +41 -0
  38. package/dart_types/lib/src/versioned_nomos_ref.dart +81 -0
  39. package/dart_types/pubspec.yaml +1 -1
  40. package/dart_types/test/optional_ref_test.dart +44 -0
  41. package/dart_types/test/sha256_digest_test.dart +18 -0
  42. package/package.json +9 -5
  43. package/runtime/generated-application-host.js +5365 -1105
  44. package/src/aggregate.ts +18 -0
  45. package/src/application_composition.ts +6 -32
  46. package/src/attested_core.ts +6 -3
  47. package/src/birth_meter.ts +1 -0
  48. package/src/birth_meter_ensure.ts +28 -2
  49. package/src/births.ts +1 -1
  50. package/src/build_package.ts +162 -538
  51. package/src/business_birth.ts +19 -3
  52. package/src/canonical_json.ts +21 -0
  53. package/src/check_package_main.ts +76 -35
  54. package/src/clear_directive.ts +19 -8
  55. package/src/codegen_dart.ts +305 -22
  56. package/src/codegen_dart_application.ts +844 -292
  57. package/src/codegen_dart_client.ts +1084 -407
  58. package/src/codegen_dart_scenario.ts +12 -12
  59. package/src/codegen_proof.ts +169 -39
  60. package/src/codegen_provider_dart.ts +17 -1
  61. package/src/codegen_ts.ts +298 -106
  62. package/src/compat_manifest.ts +2 -2
  63. package/src/compat_package_main.ts +8 -12
  64. package/src/compile_package_main.ts +1866 -597
  65. package/src/compose.ts +1 -1
  66. package/src/directive.ts +208 -15
  67. package/src/engine_entry.ts +165 -111
  68. package/src/evolve_preview.ts +43 -81
  69. package/src/extremum.ts +1 -1
  70. package/src/fields.ts +183 -4
  71. package/src/flow_gate.ts +75 -2
  72. package/src/framework/application_home.ts +2 -0
  73. package/src/framework/auth_provider_law.ts +2 -2
  74. package/src/framework/birth_meter.ts +7 -19
  75. package/src/framework/bootstrap.ts +0 -2
  76. package/src/framework/business_corrections.ts +9 -5
  77. package/src/framework/domain_lifecycle.ts +15 -29
  78. package/src/framework/encryption_scope.ts +2 -1
  79. package/src/framework/framework_aggregates.ts +2 -0
  80. package/src/framework/genesis.ts +16 -14
  81. package/src/framework/home.ts +20 -14
  82. package/src/framework/owned_custody.ts +9 -3
  83. package/src/framework/platform_admin.ts +3 -3
  84. package/src/framework/relations.ts +21 -13
  85. package/src/framework/signing_identity.ts +45 -40
  86. package/src/framework/workspace_invariant.ts +245 -0
  87. package/src/framework/workspaces.ts +23 -29
  88. package/src/index.ts +36 -16
  89. package/src/intent.ts +178 -6
  90. package/src/law_check.ts +32 -11
  91. package/src/law_graph.ts +2902 -0
  92. package/src/law_graph_usda.ts +1799 -0
  93. package/src/law_identity.ts +435 -0
  94. package/src/law_value_type.ts +230 -0
  95. package/src/lsp_server.ts +61 -31
  96. package/src/ops.ts +7 -0
  97. package/src/package_config.ts +141 -0
  98. package/src/package_dependencies.ts +108 -0
  99. package/src/package_dependency_usage.ts +150 -0
  100. package/src/package_source_imports.ts +71 -0
  101. package/src/plan_source_slice.ts +335 -0
  102. package/src/point_guard.ts +192 -0
  103. package/src/precondition.ts +44 -9
  104. package/src/read_projection_namespace.ts +1 -1
  105. package/src/runtime_zod.ts +44 -0
  106. package/src/secondary_ensure.ts +67 -14
  107. package/src/shape_compat.ts +1 -1
  108. package/src/stable_id_identity_bridge.ts +149 -0
  109. package/src/stable_id_lockfile.ts +278 -0
  110. package/src/stable_ids.ts +7 -2
  111. package/src/stable_ids_for_module.ts +348 -0
  112. package/src/stable_ids_types.ts +1 -1
  113. package/src/sum.ts +1 -1
  114. package/src/usd_state.ts +1 -1
  115. package/src/usda_relationship_integrity.ts +66 -0
  116. package/src/usda_text.ts +18 -0
  117. package/src/usdz.ts +78 -0
  118. package/src/value_object.ts +95 -0
  119. package/src/wire.ts +19 -0
  120. package/src/wire_encode.ts +22 -0
  121. package/src/workspace_sharding.ts +52 -36
  122. package/src/workspace_sharing.ts +28 -4
  123. package/src/zod_introspection.ts +1 -1
  124. package/dart/test/cluster_attested_test.dart +0 -201
  125. package/src/compile_engine.ts +0 -595
  126. package/src/manifest.ts +0 -1476
  127. package/src/read_projection_composition.ts +0 -117
  128. package/src/read_projection_manifest.ts +0 -115
  129. package/src/relation_composition.ts +0 -286
  130. package/src/usd.ts +0 -1967
  131. package/src/usd_layers.ts +0 -1776
@@ -8,7 +8,7 @@
8
8
  /**
9
9
  * THE GENERIC PACKAGE BUILD SURFACE — `@githolon/dsl/build-package` (#M4).
10
10
  *
11
- * BUILD-TIME ONLY (imports `node:crypto` via `./manifest.js`): reached via the
11
+ * BUILD-TIME ONLY (imports `node:crypto` for derived artifact digests): reached via the
12
12
  * `@githolon/dsl/build-package` subpath, NEVER the runtime barrel — the engine bundle
13
13
  * must stay node-builtin-free (esbuild `--platform=neutral`).
14
14
  *
@@ -19,37 +19,26 @@
19
19
  * 15-positional-parameter `module(...)` helper (dedupe by wire id, the
20
20
  * impure-capability directive check, default read-models, and the
21
21
  * HASH-LOAD-BEARING omit-when-empty key discipline).
22
- * * `buildReadManifest(modules)` — the read-projection descriptor (aggregateFieldKinds +
23
- * query/count/spatial/derived/combined routing). Field KINDS, not drivers. NO LONGER WRITTEN to
24
- * `build/` (the old `domain_manifests.json` sidecar was a local diagnostic the contract ignored); the
25
- * read routing lives in the package OpenAPI now. Kept as a function for callers that want the value.
26
- * * `buildIdentity(modules)` — the per-domain IDENTITY emit + admits_domain_hash registry (#193/#136),
27
- * hoisted from `emit_identity.ts` (palette-gap domains recorded-and-excluded). The admission identity
28
- * is carried in the package USD IR + the committed `nomos.stable-ids.json` lockfile; the old
29
- * `domain_identity_*.json` sidecars (and `writeIdentity`) are REMOVED. The per-domain hash is surfaced
30
- * in the compile summary.
31
- * * `emitUsdJsonForModules(modules)` — the one-line USD-IR emit every
32
- * `emit_*_usd_json.ts` wrapped (`emitUsd` over `/Nomos/<domain>` layers).
22
+ * Projection routes and admission identity are deliberately absent from this surface: both now
23
+ * come from the typed USDA law graph. The retired JSON read/identity manifest builders were
24
+ * removed so tenant tooling cannot accidentally create a second law representation.
33
25
  */
34
26
  import { createHash } from "node:crypto";
35
- import { mkdirSync, writeFileSync } from "node:fs";
36
- import { join } from "node:path";
37
-
38
- import { z } from "zod";
39
27
 
40
28
  import type { AggregateHandle } from "./aggregate.js";
41
29
  import type { Directive } from "./directive.js";
42
30
  import type { DomainModule, PermissionVocabulary, DartImport } from "./codegen_dart.js";
43
- import { finishCount, type AnyCount, type CountDecl } from "./count.js";
31
+ import type { AnyCount } from "./count.js";
44
32
  import type { QueryDecl } from "./query.js";
45
33
  import type { SpatialDecl } from "./spatial.js";
46
- import { finishSum, type AnySum } from "./sum.js";
47
- import { finishExtremum, type AnyExtremum } from "./extremum.js";
34
+ import type { AnySum } from "./sum.js";
35
+ import type { AnyExtremum } from "./extremum.js";
48
36
  import type { DerivedDecl } from "./derived.js";
49
37
  import type { CombinedDecl } from "./combined.js";
50
38
  import type { ImpureCapabilityDecl } from "./codegen_provider_dart.js";
51
39
  import type { WorkspaceTypeDecl } from "./workspace_type.js";
52
40
  import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
41
+ import { withWorkspaceSharingInvitationExports } from "./workspace_sharing.js";
53
42
  import {
54
43
  applicationLanguageOf,
55
44
  assertApplicationLanguageCoversAuthoredDirectives,
@@ -67,6 +56,7 @@ import { withClearDirectives } from "./clear_directive.js";
67
56
  import { expandRoleExports, rolesOf } from "./role.js";
68
57
  import {
69
58
  withAttestedReadExports,
59
+ NomosSourceFrontier,
70
60
  isTrustSourceDecl,
71
61
  isTrustPolicyDecl,
72
62
  type TrustSourceDecl,
@@ -77,15 +67,14 @@ import {
77
67
  mergeRelationTypeDeclarations,
78
68
  type RelationTypeDecl,
79
69
  } from "./relation_schema.js";
80
- import { domainHash, emitManifestBytes } from "./manifest.js";
81
70
  import { collectValueObjectDescriptors } from "./value_object.js";
82
- import { emitUsd } from "./usd.js";
83
71
  import { expandCapabilityExports } from "./capability_exports.js";
84
72
  import { RelationTuple, relationTuplesByObject } from "./framework/relations.js";
85
73
  import { signerDelegationByUser } from "./framework/signing_identity.js";
86
74
  import { AuthProvider, setAuthProvider, authProvider } from "./framework/workspaces.js";
87
75
  import { parentAttestedBirthMarker } from "./births.js";
88
76
  import { discoverUnsanctionedEnsures, sanctioningEnsureDirective } from "./secondary_ensure.js";
77
+ import { probeEnsuredTypes, probePlanOps } from "./codegen_proof.js";
89
78
  import { assertIdOfLint } from "./idof_lint.js";
90
79
  import { assertSurfacedResolvers } from "./surfaced_lint.js";
91
80
  import {
@@ -191,6 +180,12 @@ export interface DomainModuleSpec {
191
180
  readonly dartImports?: readonly DartImport[];
192
181
  readonly dartRefImports?: DomainModule["dartRefImports"];
193
182
  readonly permissionVocabulary?: PermissionVocabulary;
183
+ /**
184
+ * Layer-emission only: the caller is composing one authored file from a domain that has already
185
+ * passed the Surfaced resolver lint as a complete multi-file domain. Cross-file aggregate and
186
+ * resolver declarations are intentionally incomplete in an individual OpenUSD layer.
187
+ */
188
+ readonly deferCrossModuleSurfacedLint?: boolean;
194
189
  }
195
190
 
196
191
  /**
@@ -212,6 +207,7 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
212
207
  directivesOf(merged),
213
208
  domain,
214
209
  );
210
+ merged = withWorkspaceSharingInvitationExports(merged, applicationLanguage.workspaceSharing);
215
211
  }
216
212
  merged = expandBusinessBirthExports(merged, spec.domain ?? spec.name);
217
213
  merged = withOwnedCustodyExports(merged);
@@ -245,6 +241,31 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
245
241
  byId.set(policy.invitationLaw.claim.id, policy.invitationLaw.claim);
246
242
  }
247
243
  const discoveredDirectives = directivesOf(merged);
244
+ // A directive builder already possessed the typed aggregate handle. Retain that compiler link so
245
+ // re-exporting a lawful directive is self-contained: its local target schema joins the domain without
246
+ // a second magic re-export. A cross-workspace birth remains a dependency edge, not a copied child schema.
247
+ for (const directive of discoveredDirectives) {
248
+ if (directive.birthsChild !== true && directive.aggregate !== undefined) {
249
+ const prior = byId.get(directive.aggregate.id);
250
+ byId.set(
251
+ directive.aggregate.id,
252
+ (prior as { businessBirthInternal?: unknown } | undefined)?.businessBirthInternal === true
253
+ ? { ...directive.aggregate, businessBirthInternal: true } as AggregateHandle
254
+ : directive.aggregate,
255
+ );
256
+ }
257
+ for (const output of directive.secondaryOutputs ?? []) {
258
+ if (output.aggregate !== undefined) {
259
+ const prior = byId.get(output.aggregate.id);
260
+ byId.set(
261
+ output.aggregate.id,
262
+ (prior as { businessBirthInternal?: unknown } | undefined)?.businessBirthInternal === true
263
+ ? { ...output.aggregate, businessBirthInternal: true } as AggregateHandle
264
+ : output.aggregate,
265
+ );
266
+ }
267
+ }
268
+ }
248
269
  const directives = applyConflictPolicyResolvers(
249
270
  applicationLanguage === undefined
250
271
  ? discoveredDirectives
@@ -286,6 +307,114 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
286
307
  const declaresOwnedRead = [...byId.values()].some(
287
308
  (a) => (a as { audience?: { kind?: unknown } }).audience?.kind === "owned",
288
309
  );
310
+ // BUSINESS-BIRTH INITIALISER EFFECT SIGNATURE: `birth(A, { initialise })`
311
+ // lowers to a private `nomosSeedA` directive. The tenant writes the
312
+ // initializer, but never receives that generated directive and therefore
313
+ // cannot honestly append `.alsoEnsures(B)` to it. Capture the initializer's
314
+ // observed typed effects into the compiler-owned seed before applying the
315
+ // same closed-effect check used for ordinary, tenant-authored directives.
316
+ for (let index = 0; index < directives.length; index += 1) {
317
+ const candidate = directives[index]!;
318
+ if (candidate.businessBirthSeed !== true) continue;
319
+ const where = `${spec.domain ?? spec.name}/${candidate.id}`;
320
+ const probed = probePlanOps(candidate, where);
321
+ if (probed === undefined) {
322
+ throw new Error(
323
+ `[business birth effects] ${where}: Nomos could not analyse the generated birth initializer. ` +
324
+ "Supply a lawful .example(...) for any cross-field payload constraints; the child seed " +
325
+ "will not be emitted with an incomplete effect signature.",
326
+ );
327
+ }
328
+ const declared = new Set<string>([
329
+ `${candidate.marker}:${candidate.aggregateId}`,
330
+ ...(candidate.secondaryOutputs ?? []).map(
331
+ (output) => `${output.marker}:${output.aggregateId}`,
332
+ ),
333
+ ]);
334
+ const additions: NonNullable<Directive<unknown>["secondaryOutputs"]>[number][] = [];
335
+ for (const op of probed.ops) {
336
+ const marker = op.marker;
337
+ const aggregateId = op.aggregateType;
338
+ if (marker === undefined || aggregateId === undefined) continue;
339
+ const key = `${marker}:${aggregateId}`;
340
+ if (declared.has(key)) continue;
341
+ const aggregate = byId.get(aggregateId);
342
+ if (aggregate === undefined) {
343
+ throw new Error(
344
+ `[business birth effects] ${where}: the initializer writes ${marker} -> ${aggregateId}, ` +
345
+ "but that aggregate is not part of the domain. Export its aggregate declaration so the " +
346
+ "generated child law can carry its schema.",
347
+ );
348
+ }
349
+ declared.add(key);
350
+ additions.push({ marker, aggregateId, aggregate });
351
+ }
352
+ if (additions.length > 0) {
353
+ directives[index] = {
354
+ ...candidate,
355
+ secondaryOutputs: [...(candidate.secondaryOutputs ?? []), ...additions],
356
+ };
357
+ }
358
+ }
359
+ // OWNED CREATE/ENSURE EFFECT SIGNATURE: `create(Owned)` / `ensure(Owned, id)` deliberately seats the
360
+ // verified creator on the framework RelationTuple in the SAME plan. That consequence is Nomos-generated,
361
+ // so the tenant must not know to spell `.alsoEnsures(RelationTuple)` themselves; make the transformed law
362
+ // state the hidden effect honestly before the generic effect checker probes it.
363
+ for (let index = 0; index < directives.length; index += 1) {
364
+ const candidate = directives[index]!;
365
+ const target = byId.get(candidate.aggregateId);
366
+ const ownsTarget = target?.audience?.kind === "owned";
367
+ const birthsOwnedRecord = candidate.marker === "creates" || candidate.marker === "ensures";
368
+ const alreadyDeclared = (candidate.secondaryOutputs ?? []).some(
369
+ (output) => output.marker === "ensures" && output.aggregateId === RelationTuple.id,
370
+ );
371
+ if (ownsTarget && birthsOwnedRecord && !alreadyDeclared) {
372
+ directives[index] = {
373
+ ...candidate,
374
+ secondaryOutputs: [
375
+ ...(candidate.secondaryOutputs ?? []),
376
+ { marker: "ensures", aggregateId: RelationTuple.id, aggregate: RelationTuple },
377
+ ],
378
+ };
379
+ }
380
+ }
381
+ // `writeTuple(...)` and attested foreign reads have deterministic framework bookkeeping
382
+ // effects visible in the plan but intentionally hidden from tenant business types. Infer
383
+ // only those framework effects into the signature before the closed-effect checker runs.
384
+ // Arbitrary tenant aggregates remain explicit `.alsoEnsures(...)` declarations.
385
+ const inferredFrameworkEnsures = new Map<string, AggregateHandle>([
386
+ [RelationTuple.id, RelationTuple as AggregateHandle],
387
+ [NomosSourceFrontier.id, NomosSourceFrontier as AggregateHandle],
388
+ ]);
389
+ for (let index = 0; index < directives.length; index += 1) {
390
+ const candidate = directives[index]!;
391
+ if (candidate.birthsChild === true || candidate.manifestOnly === true) continue;
392
+ const probedEnsures = probeEnsuredTypes(
393
+ candidate,
394
+ `${spec.domain ?? spec.name}/${candidate.id}`,
395
+ );
396
+ const additions: AggregateHandle[] = [];
397
+ for (const aggregateId of new Set(probedEnsures)) {
398
+ const aggregate = inferredFrameworkEnsures.get(aggregateId);
399
+ if (aggregate === undefined) continue;
400
+ if ((candidate.secondaryOutputs ?? []).some(
401
+ (output) => output.marker === "ensures" && output.aggregateId === aggregate.id,
402
+ )) continue;
403
+ additions.push(aggregate);
404
+ }
405
+ if (additions.length === 0) continue;
406
+ directives[index] = {
407
+ ...candidate,
408
+ secondaryOutputs: [
409
+ ...(candidate.secondaryOutputs ?? []),
410
+ ...additions.map((aggregate) => ({
411
+ marker: "ensures" as const,
412
+ aggregateId: aggregate.id,
413
+ aggregate,
414
+ })),
415
+ ],
416
+ };
417
+ }
289
418
  if (declaresPrivateRead) {
290
419
  if (!byId.has(AuthProvider.id)) {
291
420
  byId.set(AuthProvider.id, declaresOwnedRead
@@ -490,17 +619,17 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
490
619
  ? { ...relationTuplesByObject, businessBirthInternal: true } as QueryDecl
491
620
  : relationTuplesByObject);
492
621
  }
493
- // FIRST-CLASS VALUE OBJECTS (value_object.ts): the deduped, sorted descriptors of every
494
- // named VO the domain's aggregates carry (transitive nested VOs included) — the input to
495
- // the additive `nomosValueObjects` USD key. Omit-when-empty (era rule): a VO-free domain's
496
- // module — and thus its USD/package bytes — is byte-identical to before VOs existed.
497
- const valueObjects = collectValueObjectDescriptors(aggregates);
498
622
  // DOMAIN CONTRACTS (domain_contracts.md): the public DTO shapes this domain declares, discovered by
499
623
  // shape across the merged module list. Deduped by id (later-wins), sorted for byte-determinism. Omit-
500
624
  // when-empty (era rule): a contract-free domain's module/USD/package bytes are byte-identical.
501
625
  const contractById = new Map<string, import("./contract.js").ContractHandle>();
502
626
  for (const c of contractsOf(merged)) contractById.set(c.id, c);
503
627
  const contracts = [...contractById.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
628
+ // FIRST-CLASS VALUE OBJECTS (value_object.ts): close over both stored aggregates and public
629
+ // contracts. A contract may be the only local owner of a named value object (especially a
630
+ // cross-domain publication DTO); collecting aggregates alone left a dangling type reference in
631
+ // the canonical law graph. Transitive nested VOs remain included by the descriptor collector.
632
+ const valueObjects = collectValueObjectDescriptors([...aggregates, ...contracts]);
504
633
  // SPATIAL × VO PAIRING (fail at build, not at fold): a spatial index over a VO-typed field
505
634
  // must index a GEO value object (an RFC 7946 geometry — the R*Tree's bbox source). A
506
635
  // structural VO (e.g. a PostalAddress) has no geometry to index. Non-VO fields (a
@@ -619,11 +748,13 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
619
748
  // chained conflict-reference codegen consumes AND the canonical manifest records as the
620
749
  // hash-bearing conflictResolution contract. A Surfaced-free domain runs zero checks and is
621
750
  // byte-identical; a resolver policy deliberately becomes part of business law.
622
- const surfacedResolvers = assertSurfacedResolvers(
623
- aggregates,
624
- directives,
625
- spec.domain ?? spec.name,
626
- );
751
+ const surfacedResolvers = spec.deferCrossModuleSurfacedLint
752
+ ? []
753
+ : assertSurfacedResolvers(
754
+ aggregates,
755
+ directives,
756
+ spec.domain ?? spec.name,
757
+ );
627
758
  const publicApplicationIntents = applicationLanguage === undefined
628
759
  ? undefined
629
760
  : [
@@ -696,513 +827,6 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
696
827
  };
697
828
  }
698
829
 
699
- // ─────────────────────────────────────────────────────────────────────────────────
700
- // buildReadManifest — domain_manifests.json (hoisted from emit_manifests.ts)
701
- // ─────────────────────────────────────────────────────────────────────────────────
702
-
703
- /** One field's read-projection descriptor: the leaf kind (+ a map's value kind). */
704
- export interface FieldKindDescriptor {
705
- kind: string;
706
- mapValueKind?: string;
707
- }
708
- export type AggregateSchema = Record<string, FieldKindDescriptor>;
709
- export type AggregateFieldKinds = Record<string, AggregateSchema>;
710
-
711
- export interface QueryKeyField {
712
- field: string;
713
- kind: string;
714
- }
715
- export interface QueryDescriptor {
716
- id: string;
717
- key: QueryKeyField[];
718
- returns: string;
719
- }
720
- export interface CountDescriptor {
721
- id: string;
722
- of: string;
723
- by: string | null;
724
- /** The canonical predicate (`WirePred` in the Rust parser — `{kind:"eq"|"ne"|"and"|"or",…}`).
725
- * OMITTED entirely when the count declares none: predicate-free read manifests stay
726
- * byte-identical (the Rust side reads it `#[serde(rename = "where", default)]`). */
727
- where?: unknown;
728
- }
729
- export interface SpatialDescriptor {
730
- id: string;
731
- of: string;
732
- on: string;
733
- }
734
- export interface ProjectionReturnDescriptor {
735
- type: string;
736
- nullable: boolean;
737
- jsonSchema: unknown;
738
- }
739
- export interface DerivedDescriptor {
740
- id: string;
741
- of: string;
742
- returns: ProjectionReturnDescriptor;
743
- }
744
- export interface CombinedDescriptor {
745
- id: string;
746
- of: string;
747
- refField: string;
748
- reads: string;
749
- returns: ProjectionReturnDescriptor;
750
- }
751
-
752
- /** One maintained-sum descriptor (the projection's `WireSum` shape — note `field`). */
753
- export interface SumDescriptor {
754
- id: string;
755
- of: string;
756
- field: string;
757
- by: string | null;
758
- /** The canonical predicate — same omit-when-absent discipline as {@link CountDescriptor}. */
759
- where?: unknown;
760
- }
761
-
762
- /** One min/max read-manifest descriptor — the Rust `WireExtremum` shape (#47): identical
763
- * to {@link SumDescriptor}; the KIND is implicit in which array it rides (`mins` vs `maxes`). */
764
- export interface ExtremumDescriptor {
765
- id: string;
766
- of: string;
767
- field: string;
768
- by: string | null;
769
- where?: unknown;
770
- }
771
-
772
- /** The combined read-manifest artifact (the EXACT wire shape the Rust read engine parses). */
773
- export interface ReadManifest {
774
- aggregateFieldKinds: AggregateFieldKinds;
775
- queries: QueryDescriptor[];
776
- counts: CountDescriptor[];
777
- spatials: SpatialDescriptor[];
778
- deriveds: DerivedDescriptor[];
779
- combineds: CombinedDescriptor[];
780
- /** OMITTED when no domain declares a sum (read manifests predating sums stay byte-identical). */
781
- sums?: SumDescriptor[];
782
- /** OMITTED when no domain declares a min/max (extremum-free read manifests stay byte-identical). */
783
- mins?: ExtremumDescriptor[];
784
- maxes?: ExtremumDescriptor[];
785
- }
786
-
787
- type ZodLike = {
788
- _def?: { type?: string; innerType?: ZodLike };
789
- def?: { type?: string; innerType?: ZodLike };
790
- };
791
-
792
- function zodDef(zt: ZodLike): NonNullable<ZodLike["_def"]> {
793
- return zt._def ?? zt.def ?? {};
794
- }
795
-
796
- function zodTypeName(zt: ZodLike): string {
797
- return zodDef(zt).type ?? "unknown";
798
- }
799
-
800
- function describeProjectionReturn(schema: z.ZodTypeAny): ProjectionReturnDescriptor {
801
- let cur = schema as unknown as ZodLike;
802
- let nullable = false;
803
- while (
804
- zodTypeName(cur) === "optional" ||
805
- zodTypeName(cur) === "default" ||
806
- zodTypeName(cur) === "nullable"
807
- ) {
808
- nullable = true;
809
- const inner = zodDef(cur).innerType;
810
- if (inner === undefined) break;
811
- cur = inner;
812
- }
813
- return {
814
- type: zodTypeName(cur),
815
- nullable,
816
- jsonSchema: z.toJSONSchema(schema),
817
- };
818
- }
819
-
820
- /** Capture one aggregate's field KINDS straight off the DSL `Field` objects. */
821
- function schemaOf(agg: AggregateHandle): AggregateSchema {
822
- const fields: AggregateSchema = {};
823
- // STORED fields only — virtual `t.hasMany` inverses are partitioned into `agg.hasMany`.
824
- for (const [name, field] of Object.entries(agg.fields)) {
825
- const descriptor: FieldKindDescriptor = { kind: field.kind };
826
- if (field.mapValueKind !== undefined) descriptor.mapValueKind = field.mapValueKind;
827
- fields[name] = descriptor;
828
- }
829
- return fields;
830
- }
831
-
832
- /** Stable JSON for an aggregate schema, to detect a genuine cross-domain divergence. */
833
- function stable(schema: AggregateSchema): string {
834
- const sorted: AggregateSchema = {};
835
- for (const k of Object.keys(schema).sort()) sorted[k] = schema[k]!;
836
- return JSON.stringify(sorted);
837
- }
838
-
839
- /**
840
- * Resolve the routing KIND of one query KEY FIELD against the returns-type's field
841
- * schema. NO FALLBACK: an undeclared key field is a declaration bug — THROW.
842
- */
843
- function keyFieldKind(
844
- byType: AggregateFieldKinds,
845
- queryId: string,
846
- returns: string,
847
- keyField: string,
848
- ): string {
849
- const schema = byType[returns];
850
- if (schema === undefined) {
851
- throw new Error(
852
- `build-package: query '${queryId}' returns '${returns}', which has no field ` +
853
- `schema — the returns handle must be an aggregate emitted into the manifest.`,
854
- );
855
- }
856
- // A dotted key indexes a sub-value of a top-level JSON leaf; kind off the leading field.
857
- const lookup = keyField.includes(".") ? keyField.split(".")[0]! : keyField;
858
- const descriptor = schema[lookup];
859
- if (descriptor === undefined) {
860
- throw new Error(
861
- `build-package: query '${queryId}' is keyed on '${keyField}' (field '${lookup}'), ` +
862
- `which '${returns}' does not declare — a query cannot index an undefined field.`,
863
- );
864
- }
865
- return descriptor.kind;
866
- }
867
-
868
- function describeQuery(byType: AggregateFieldKinds, q: QueryDecl): QueryDescriptor {
869
- return {
870
- id: q.id,
871
- key: q.key.map((field) => ({
872
- field,
873
- kind: keyFieldKind(byType, q.id, q.returns, field),
874
- })),
875
- returns: q.returns,
876
- };
877
- }
878
-
879
- function describeCount(byType: AggregateFieldKinds, c: CountDecl): CountDescriptor {
880
- if (byType[c.of] === undefined) {
881
- throw new Error(
882
- `build-package: count '${c.id}' tallies '${c.of}', which has no field schema — ` +
883
- `the of-type must be an aggregate emitted into the manifest.`,
884
- );
885
- }
886
- // `where` rides to the projection (WireCount parses it) — OMITTED when absent, so
887
- // predicate-free read manifests keep their exact pre-slice-4 bytes.
888
- return { id: c.id, of: c.of, by: c.by ?? null, ...(c.where !== undefined ? { where: c.where } : {}) };
889
- }
890
-
891
- function describeSpatial(byType: AggregateFieldKinds, s: SpatialDecl): SpatialDescriptor {
892
- const schema = byType[s.of];
893
- if (schema === undefined) {
894
- throw new Error(
895
- `build-package: spatial '${s.id}' covers '${s.of}', which has no field schema — ` +
896
- `the of-type must be an aggregate emitted into the manifest.`,
897
- );
898
- }
899
- if (schema[s.on] === undefined) {
900
- throw new Error(
901
- `build-package: spatial '${s.id}' indexes geometry field '${s.on}' on '${s.of}', ` +
902
- `which '${s.of}' does not declare — the geometry field must exist (no fallback).`,
903
- );
904
- }
905
- return { id: s.id, of: s.of, on: s.on };
906
- }
907
-
908
- function describeDerived(byType: AggregateFieldKinds, d: DerivedDecl): DerivedDescriptor {
909
- if (byType[d.of] === undefined) {
910
- throw new Error(
911
- `build-package: derived '${d.id}' derives from '${d.of}', which has no field ` +
912
- `schema — the of-type must be an aggregate emitted into the manifest.`,
913
- );
914
- }
915
- return { id: d.id, of: d.of, returns: describeProjectionReturn(d.returns) };
916
- }
917
-
918
- function describeCombined(byType: AggregateFieldKinds, c: CombinedDecl): CombinedDescriptor {
919
- if (byType[c.of] === undefined) {
920
- throw new Error(
921
- `build-package: combined '${c.id}' lives on '${c.of}', which has no field ` +
922
- `schema — the of-type must be an aggregate emitted into the manifest.`,
923
- );
924
- }
925
- if (byType[c.reads] === undefined) {
926
- throw new Error(
927
- `build-package: combined '${c.id}' reads '${c.reads}', which has no field ` +
928
- `schema — the queried type must be an aggregate emitted into the manifest.`,
929
- );
930
- }
931
- return {
932
- id: c.id,
933
- of: c.of,
934
- refField: c.refField,
935
- reads: c.reads,
936
- returns: describeProjectionReturn(c.returns),
937
- };
938
- }
939
-
940
- /**
941
- * Build the single combined READ manifest for the modules (`domain_manifests.json`).
942
- * Hoisted VERBATIM-in-behaviour from `emit_manifests.ts` `buildManifests` — sorted
943
- * keys/arrays for byte-stability, divergence detection across domains, NO-FALLBACK
944
- * throws on undeclared of-types (the Rust parse would reject them anyway, loudly,
945
- * and break ALL reads for the workspace — fail at build instead).
946
- */
947
- export function buildReadManifest(modules: readonly DomainModule[]): ReadManifest {
948
- const byType: AggregateFieldKinds = {};
949
- for (const mod of modules) {
950
- for (const agg of mod.aggregates as AggregateHandle[]) {
951
- const schema = schemaOf(agg);
952
- const existing = byType[agg.id];
953
- if (existing !== undefined && stable(existing) !== stable(schema)) {
954
- throw new Error(
955
- `build-package: aggregate '${agg.id}' has DIVERGENT field schemas across ` +
956
- `domains — the read projection cannot have two shapes for one wire type. ` +
957
- `Reconcile the aggregate definition.`,
958
- );
959
- }
960
- byType[agg.id] = schema;
961
- }
962
- }
963
- const sorted: AggregateFieldKinds = {};
964
- for (const k of Object.keys(byType).sort()) sorted[k] = byType[k]!;
965
-
966
- const byId = new Map<string, QueryDescriptor>();
967
- for (const mod of modules) {
968
- for (const q of mod.queries ?? []) {
969
- const descriptor = describeQuery(sorted, q);
970
- const existing = byId.get(q.id);
971
- if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(descriptor)) {
972
- throw new Error(
973
- `build-package: query id '${q.id}' is declared with DIVERGENT routing across ` +
974
- `domains — one id cannot map to two routes. Reconcile the query declaration.`,
975
- );
976
- }
977
- byId.set(q.id, descriptor);
978
- }
979
- }
980
- const queries = [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
981
-
982
- const countsById = new Map<string, CountDescriptor>();
983
- for (const mod of modules) {
984
- for (const raw of mod.counts ?? []) {
985
- const c = finishCount(raw);
986
- const descriptor = describeCount(sorted, c);
987
- const existing = countsById.get(c.id);
988
- if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(descriptor)) {
989
- throw new Error(
990
- `build-package: count id '${c.id}' is declared with DIVERGENT maintenance across ` +
991
- `domains — one id cannot map to two counters. Reconcile the count declaration.`,
992
- );
993
- }
994
- countsById.set(c.id, descriptor);
995
- }
996
- }
997
- const counts = [...countsById.values()].sort((a, b) => a.id.localeCompare(b.id));
998
-
999
- // Maintained SUMS (sharding slice 3 routes the derived `nomosEstateSummary` through
1000
- // here; tenant sums ride the same lane). The projection's `WireSum` names the summed
1001
- // field `field`. Omitted entirely when none — sum-free read manifests stay byte-identical.
1002
- const sumsById = new Map<string, SumDescriptor>();
1003
- for (const mod of modules) {
1004
- for (const raw of mod.sums ?? []) {
1005
- const s = finishSum(raw);
1006
- if (sorted[s.of] === undefined) {
1007
- throw new Error(
1008
- `build-package: sum '${s.id}' tallies '${s.of}', which has no field schema — ` +
1009
- `the of-type must be an aggregate emitted into the manifest.`,
1010
- );
1011
- }
1012
- const descriptor: SumDescriptor = { id: s.id, of: s.of, field: s.sumField, by: s.by ?? null, ...(s.where !== undefined ? { where: s.where } : {}) };
1013
- const existing = sumsById.get(s.id);
1014
- if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(descriptor)) {
1015
- throw new Error(
1016
- `build-package: sum id '${s.id}' is declared with DIVERGENT maintenance across ` +
1017
- `domains — one id cannot map to two totals. Reconcile the sum declaration.`,
1018
- );
1019
- }
1020
- sumsById.set(s.id, descriptor);
1021
- }
1022
- }
1023
- const sums = [...sumsById.values()].sort((a, b) => a.id.localeCompare(b.id));
1024
-
1025
- // Maintained MIN/MAX reads (#47 — the extremum RPC's manifest half): the SAME lane
1026
- // as sums, two arrays (the kind is implicit in which array a descriptor rides —
1027
- // the Rust `WireExtremum` has parsed them since the read-engine slice). Omitted
1028
- // entirely when none — extremum-free read manifests stay byte-identical.
1029
- const lowerExtrema = (key: "mins" | "maxes"): ExtremumDescriptor[] => {
1030
- const byId = new Map<string, ExtremumDescriptor>();
1031
- for (const mod of modules) {
1032
- for (const raw of mod[key] ?? []) {
1033
- const e = finishExtremum(raw);
1034
- if (sorted[e.of] === undefined) {
1035
- throw new Error(
1036
- `build-package: ${key === "mins" ? "min" : "max"} '${e.id}' reads '${e.of}', which has no field schema — ` +
1037
- `the of-type must be an aggregate emitted into the manifest.`,
1038
- );
1039
- }
1040
- const descriptor: ExtremumDescriptor = { id: e.id, of: e.of, field: e.valueField, by: e.by ?? null, ...(e.where !== undefined ? { where: e.where } : {}) };
1041
- const existing = byId.get(e.id);
1042
- if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(descriptor)) {
1043
- throw new Error(
1044
- `build-package: ${key === "mins" ? "min" : "max"} id '${e.id}' is declared with DIVERGENT ` +
1045
- `maintenance across domains — one id cannot map to two extrema. Reconcile the declaration.`,
1046
- );
1047
- }
1048
- byId.set(e.id, descriptor);
1049
- }
1050
- }
1051
- return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
1052
- };
1053
- const mins = lowerExtrema("mins");
1054
- const maxes = lowerExtrema("maxes");
1055
-
1056
- const spatialsById = new Map<string, SpatialDescriptor>();
1057
- for (const mod of modules) {
1058
- for (const s of mod.spatials ?? []) {
1059
- const descriptor = describeSpatial(sorted, s);
1060
- const existing = spatialsById.get(s.id);
1061
- if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(descriptor)) {
1062
- throw new Error(
1063
- `build-package: spatial id '${s.id}' is declared with DIVERGENT maintenance across ` +
1064
- `domains — one id cannot map to two indexes. Reconcile the spatial declaration.`,
1065
- );
1066
- }
1067
- spatialsById.set(s.id, descriptor);
1068
- }
1069
- }
1070
- const spatials = [...spatialsById.values()].sort((a, b) => a.id.localeCompare(b.id));
1071
-
1072
- const derivedsByNamespaceAndId = new Map<string, DerivedDescriptor>();
1073
- for (const mod of modules) {
1074
- for (const d of mod.deriveds ?? []) {
1075
- const descriptor = describeDerived(sorted, d);
1076
- const key = `${descriptor.of}\u0000${descriptor.id}`;
1077
- const existing = derivedsByNamespaceAndId.get(key);
1078
- if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(descriptor)) {
1079
- throw new Error(
1080
- `build-package: derived field '${descriptor.of}.${descriptor.id}' is declared ` +
1081
- `with DIVERGENT projection across domains. Reconcile the declaration.`,
1082
- );
1083
- }
1084
- derivedsByNamespaceAndId.set(key, descriptor);
1085
- }
1086
- }
1087
- const deriveds = [...derivedsByNamespaceAndId.values()].sort(
1088
- (a, b) => a.of.localeCompare(b.of) || a.id.localeCompare(b.id),
1089
- );
1090
-
1091
- const combinedsByNamespaceAndId = new Map<string, CombinedDescriptor>();
1092
- for (const mod of modules) {
1093
- for (const c of mod.combineds ?? []) {
1094
- const descriptor = describeCombined(sorted, c);
1095
- const key = `${descriptor.of}\u0000${descriptor.id}`;
1096
- const existing = combinedsByNamespaceAndId.get(key);
1097
- if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(descriptor)) {
1098
- throw new Error(
1099
- `build-package: combined field '${descriptor.of}.${descriptor.id}' is declared ` +
1100
- `with DIVERGENT projection across domains. Reconcile the declaration.`,
1101
- );
1102
- }
1103
- combinedsByNamespaceAndId.set(key, descriptor);
1104
- }
1105
- }
1106
- const combineds = [...combinedsByNamespaceAndId.values()].sort(
1107
- (a, b) => a.of.localeCompare(b.of) || a.id.localeCompare(b.id),
1108
- );
1109
-
1110
- return {
1111
- aggregateFieldKinds: sorted,
1112
- queries,
1113
- counts,
1114
- spatials,
1115
- deriveds,
1116
- combineds,
1117
- ...(sums.length > 0 ? { sums } : {}),
1118
- ...(mins.length > 0 ? { mins } : {}),
1119
- ...(maxes.length > 0 ? { maxes } : {}),
1120
- };
1121
- }
1122
-
1123
- // ─────────────────────────────────────────────────────────────────────────────────
1124
- // buildIdentity / writeIdentity — the IDENTITY artifacts (hoisted from emit_identity.ts)
1125
- // ─────────────────────────────────────────────────────────────────────────────────
1126
-
1127
- /** One domain that could not produce a canonical identity, with the reason recorded. */
1128
- export interface ExcludedDomain {
1129
- readonly domain: string;
1130
- readonly reason: string;
1131
- }
1132
-
1133
- /** The result of emitting the identity manifest over a set of domain modules. */
1134
- export interface IdentityEmit {
1135
- /** `{domain: canonicalManifestString}` — the gate's NOMOS_DOMAIN_MANIFEST shape. */
1136
- readonly manifests: Record<string, string>;
1137
- /** `{domain: sha256(canonicalManifest)}` — the certified hash registry. */
1138
- readonly hashes: Record<string, string>;
1139
- /** Domains OMITTED because they have no canonical identity (palette gap), recorded. */
1140
- readonly excluded: ExcludedDomain[];
1141
- }
1142
-
1143
- /**
1144
- * Build the identity manifest + hashes. A domain whose canonical manifest THROWS (a
1145
- * palette-only driver the kernel cannot lower) is RECORDED in `excluded` and omitted —
1146
- * never fabricated. The cross-language anchor (`sha256(bytes) === domainHash`) is
1147
- * asserted for every EMITTED domain; an anchor failure is a hard error.
1148
- */
1149
- export function buildIdentity(modules: readonly DomainModule[]): IdentityEmit {
1150
- const manifests: Record<string, string> = {};
1151
- const hashes: Record<string, string> = {};
1152
- const excluded: ExcludedDomain[] = [];
1153
-
1154
- for (const mod of [...modules].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
1155
- let bytes: Buffer;
1156
- let hash: string;
1157
- try {
1158
- bytes = emitManifestBytes(mod);
1159
- hash = domainHash(mod);
1160
- } catch (e) {
1161
- // No canonical identity under the current kernel palette — record, do not fabricate.
1162
- excluded.push({ domain: mod.name, reason: (e as Error).message });
1163
- continue;
1164
- }
1165
- const fileHash = createHash("sha256").update(bytes).digest("hex");
1166
- if (fileHash !== hash) {
1167
- throw new Error(
1168
- `build-package: ${mod.name} manifest-byte sha256 ${fileHash} != domainHash ${hash} — ` +
1169
- `the identity anchor is broken (the gate would reject these bytes).`,
1170
- );
1171
- }
1172
- manifests[mod.name] = bytes.toString("utf8");
1173
- hashes[mod.name] = hash;
1174
- }
1175
-
1176
- if (Object.keys(manifests).length === 0) {
1177
- throw new Error(
1178
- "build-package: produced NO domain identities — refusing to emit an empty registry.",
1179
- );
1180
- }
1181
- return { manifests, hashes, excluded };
1182
- }
1183
-
1184
- // writeIdentity (the domain_identity_*.json sidecars) — REMOVED. The admission identity is carried in the
1185
- // package USD IR + the committed nomos.stable-ids.json lockfile; the deploy/runtime contract never read the
1186
- // sidecars (they were local diagnostics that only confused /build consumers). `buildIdentity` stays — its
1187
- // per-domain hash is surfaced in the compile summary.
1188
-
1189
- // ─────────────────────────────────────────────────────────────────────────────────
1190
- // The canonical USD-IR emit
1191
- // ─────────────────────────────────────────────────────────────────────────────────
1192
-
1193
- /**
1194
- * The USD-IR JSON for a package's modules: `emitUsd` over `/Nomos/<domain>` layers,
1195
- * `JSON.stringify`'d EXACTLY like every `emit_*_usd_json.ts` (single line, insertion
1196
- * order — `emitUsd`'s sorted layers/prims are what make it reproducible; do NOT
1197
- * canonicalize or pretty-print, the package hash rides on these bytes).
1198
- */
1199
- export function emitUsdJsonForModules(modules: readonly DomainModule[]): string {
1200
- const doc = emitUsd(
1201
- modules.map((mod) => ({ path: `/Nomos/${mod.domain ?? mod.name}`, module: mod })),
1202
- );
1203
- return JSON.stringify(doc);
1204
- }
1205
-
1206
830
  /** sha256 hex over a UTF-8 string — the deploy identity (`policy:{hash}`). */
1207
831
  export function sha256HexUtf8(text: string): string {
1208
832
  return createHash("sha256").update(Buffer.from(text, "utf8")).digest("hex");