@githolon/dsl 0.105.13 → 0.105.15

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.
@@ -27,6 +27,7 @@ import { createHash } from "node:crypto";
27
27
  import { z } from "zod";
28
28
 
29
29
  import type { AggregateHandle } from "./aggregate.js";
30
+ import type { Field } from "./fields.js";
30
31
  import type { Directive } from "./directive.js";
31
32
  import type { DomainModule, PermissionVocabulary, DartImport } from "./codegen_dart.js";
32
33
  import type { AnyCount } from "./count.js";
@@ -83,6 +84,11 @@ import {
83
84
  applyConflictPolicyResolvers,
84
85
  conflictPoliciesOf,
85
86
  } from "./conflicts.js";
87
+ import {
88
+ applyFieldEvolutionsToAggregates,
89
+ fieldEvolutionsOf,
90
+ retirementsOf,
91
+ } from "./evolution.js";
86
92
  import {
87
93
  expandBusinessBirthExports,
88
94
  isBusinessBirthDirective,
@@ -200,6 +206,8 @@ export interface DomainModuleSpec {
200
206
  export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
201
207
  const domain = spec.domain ?? spec.name;
202
208
  const conflictPolicies = conflictPoliciesOf(spec.modules, domain);
209
+ const fieldEvolutions = fieldEvolutionsOf(spec.modules);
210
+ const retirements = retirementsOf(spec.modules, domain);
203
211
  const applicationLanguage = applicationLanguageOf(spec.modules, domain);
204
212
  let merged: Mod = {};
205
213
  for (const m of spec.modules) merged = { ...merged, ...m };
@@ -520,11 +528,34 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
520
528
  directives.push(dir);
521
529
  autoDeclaredEnsures.push(ty);
522
530
  }
523
- const aggregates = applyConflictPoliciesToAggregates(
524
- [...byId.values()],
525
- conflictPolicies,
531
+ const aggregates = applyFieldEvolutionsToAggregates(
532
+ applyConflictPoliciesToAggregates(
533
+ [...byId.values()],
534
+ conflictPolicies,
535
+ domain,
536
+ ),
537
+ fieldEvolutions,
526
538
  domain,
527
539
  );
540
+ const currentAggregates = new Map(aggregates.map((aggregate) => [aggregate.id, aggregate]));
541
+ for (const retired of retirements) {
542
+ const current = currentAggregates.get(retired.aggregate);
543
+ if (retired.kind === "aggregate" && current !== undefined) {
544
+ throw new Error(
545
+ `domain '${domain}' declares '${retired.aggregate}' both current and retired`,
546
+ );
547
+ }
548
+ if (
549
+ retired.kind === "field" &&
550
+ current !== undefined &&
551
+ retired.field !== undefined &&
552
+ (current.fields as Record<string, Field>)[retired.field] !== undefined
553
+ ) {
554
+ throw new Error(
555
+ `domain '${domain}' declares '${retired.aggregate}.${retired.field}' both current and retired`,
556
+ );
557
+ }
558
+ }
528
559
  const directiveIds = new Set(directives.map((d) => d.id));
529
560
  for (const task of declaredCapabilities) {
530
561
  for (const directive of [task.obligation, task.complete, task.fail, task.block, task.deadLetter]) {
@@ -855,6 +886,9 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
855
886
  // hash-bearing manifest; payload-field tracing also drives generated typed references.
856
887
  // Omit-when-empty because ordinary domains need no resolver metadata.
857
888
  ...(surfacedResolvers.length > 0 ? { surfacedResolvers } : {}),
889
+ // Explicit authored tombstones are law. The compiler identity registry may remember
890
+ // every disappearance, but only these declarations permit an activation to retire it.
891
+ ...(retirements.length > 0 ? { retirements } : {}),
858
892
  ...(queries.length > 0 ? { queries } : {}),
859
893
  ...(counts.length > 0 ? { counts } : {}),
860
894
  ...(spatials.length > 0 ? { spatials } : {}),
@@ -17,6 +17,8 @@ import { pathToFileURL } from "node:url";
17
17
  import { composeDomainModule, type Mod } from "./build_package.js";
18
18
  import type { DomainModule } from "./codegen_dart.js";
19
19
  import { bindEnumTypeExports } from "./enum_type.js";
20
+ import { discoverEvolutionModulePaths, isEvolutionModulePath } from "./evolution_files.js";
21
+ import { assertEvolutionModuleExports } from "./evolution.js";
20
22
  import { checkDomainModules, formatFindings, hasRefusal, type Finding } from "./law_check.js";
21
23
  import { LawSourceMap } from "./law_source_map.js";
22
24
  import {
@@ -105,9 +107,18 @@ export async function loadComposedModulesWithSources(
105
107
  const files: string[] = [];
106
108
  for (const rel of d.modules) {
107
109
  const abs = path.resolve(cfgDir, rel);
110
+ if (isEvolutionModulePath(abs)) {
111
+ fail(
112
+ `domain '${d.key}': ${rel} is an evolution concern; move it under ` +
113
+ `evolutions/${d.key}/ and remove it from feature modules so it cannot enter QuickJS`,
114
+ );
115
+ }
108
116
  if (!existsSync(abs)) fail(`domain '${d.key}': module not found: ${abs}`);
109
117
  files.push(abs);
110
118
  }
119
+ for (const abs of discoverEvolutionModulePaths(cfgDir, d.key)) {
120
+ if (!files.includes(abs)) files.push(abs);
121
+ }
111
122
  try {
112
123
  assertPublicPackageImports(files);
113
124
  } catch (error) {
@@ -116,6 +127,7 @@ export async function loadComposedModulesWithSources(
116
127
  for (const abs of files) {
117
128
  // Cache-bust so a re-import (the LSP re-check after an edit) re-evaluates the edited module.
118
129
  const mod = (await import(`${pathToFileURL(abs).href}?t=${Date.now()}`)) as Mod;
130
+ if (isEvolutionModulePath(abs)) assertEvolutionModuleExports(mod, abs);
119
131
  bindEnumTypeExports(mod, abs);
120
132
  mods.push(mod);
121
133
  }
@@ -201,7 +213,6 @@ async function main(): Promise<void> {
201
213
  const findings: Finding[] = [...dependencyFindings, ...checkDomainModules(modules)];
202
214
 
203
215
  // ── EVOLVE PREVIEW (--against): diff the current build against a prior law + pre-fire the gate. ──
204
- let evolveDispositions: Record<string, unknown> | undefined;
205
216
  if (againstIdx >= 0 || againstArg !== undefined) {
206
217
  if (againstArg === undefined || againstArg.startsWith("--")) {
207
218
  fail(`--against needs a path: --against <${NOMOS_IDENTITY_FILENAME} | package dir>`);
@@ -218,7 +229,6 @@ async function main(): Promise<void> {
218
229
  const current = currentEvolveLaw(modules, lock);
219
230
  const diff = evolveDiff(prior.law, current);
220
231
  findings.push(...diff.findings);
221
- if (Object.keys(diff.dispositions).length > 0) evolveDispositions = diff.dispositions;
222
232
  if (!asJson) console.log(`githolon check: evolve preview against ${prior.source}`);
223
233
  }
224
234
 
@@ -255,14 +265,6 @@ async function main(): Promise<void> {
255
265
  }
256
266
  if (findings.length === 0) console.log(" all clear — no static twin flagged anything");
257
267
 
258
- // The consolidated, pasteable dispositions the destructive evolves demand (per domain).
259
- if (evolveDispositions !== undefined) {
260
- console.log(
261
- "\ngitholon check: to admit the destructive evolve, include `dispositions` in the installation offer:",
262
- );
263
- console.log(JSON.stringify(evolveDispositions, null, 2));
264
- }
265
-
266
268
  process.exit(hasRefusal(findings) ? 1 : 0);
267
269
  }
268
270
 
@@ -2742,14 +2742,14 @@ export function generateDartActivityRegistry(entries: readonly PackageActivityEn
2742
2742
  ` final String summary;`,
2743
2743
  ` final NomosModelReview review;`,
2744
2744
  ` final Future<NomosModelAdoption> Function()? _stage;`,
2745
- ` final Future<NomosModelAdoption> Function(String, {Map<String, Object?>? dispositions})? _accept;`,
2745
+ ` final Future<NomosModelAdoption> Function(String)? _accept;`,
2746
2746
  ` const NomosModelAdoption.adopted({required this.summary, required this.review})`,
2747
2747
  ` : status = NomosModelAdoptionStatus.adopted, _stage = null, _accept = null;`,
2748
2748
  ` const NomosModelAdoption.blocked({required this.summary, required this.review})`,
2749
2749
  ` : status = NomosModelAdoptionStatus.blocked, _stage = null, _accept = null;`,
2750
2750
  ` NomosModelAdoption.needsStaging({required this.summary, required this.review, required Future<NomosModelAdoption> Function() stage})`,
2751
2751
  ` : status = NomosModelAdoptionStatus.needsStaging, _stage = stage, _accept = null;`,
2752
- ` NomosModelAdoption.needsDecision({required this.summary, required this.review, required Future<NomosModelAdoption> Function(String, {Map<String, Object?>? dispositions}) accept})`,
2752
+ ` NomosModelAdoption.needsDecision({required this.summary, required this.review, required Future<NomosModelAdoption> Function(String) accept})`,
2753
2753
  ` : status = NomosModelAdoptionStatus.needsDecision, _stage = null, _accept = accept;`,
2754
2754
  ` Future<NomosModelAdoption> stage() {`,
2755
2755
  ` final stageCandidate = _stage;`,
@@ -2758,14 +2758,14 @@ export function generateDartActivityRegistry(entries: readonly PackageActivityEn
2758
2758
  ` }`,
2759
2759
  ` return stageCandidate();`,
2760
2760
  ` }`,
2761
- ` Future<NomosModelAdoption> accept(String because, {Map<String, Object?>? dispositions}) {`,
2761
+ ` Future<NomosModelAdoption> accept(String because) {`,
2762
2762
  ` final reason = because.trim();`,
2763
2763
  ` final acceptDecision = _accept;`,
2764
2764
  ` if (status != NomosModelAdoptionStatus.needsDecision || acceptDecision == null) {`,
2765
2765
  ` throw StateError('This model change is not waiting for a business decision.');`,
2766
2766
  ` }`,
2767
2767
  ` if (reason.isEmpty) throw ArgumentError('accept(reason) needs the business reason for changing prior outcomes');`,
2768
- ` return acceptDecision(reason, dispositions: dispositions);`,
2768
+ ` return acceptDecision(reason);`,
2769
2769
  ` }`,
2770
2770
  `}`,
2771
2771
  ``,
@@ -4788,6 +4788,9 @@ export interface DomainModule {
4788
4788
  * Absent when the domain declares no Surfaced field (byte-identical — the era rule).
4789
4789
  */
4790
4790
  surfacedResolvers?: import("./surfaced_lint.js").SurfacedResolverBinding[];
4791
+ /** Explicit tombstones authored in separate evolution files. These compile into
4792
+ * candidate USDA and are never exposed through generated tenant clients. */
4793
+ retirements?: import("./evolution.js").Retirement[];
4791
4794
  /**
4792
4795
  * STABLE-ID CONTINUITY (#58 — names are labels, identity is minted). When present
4793
4796
  * (attached by `nomos-compile` from the PRIOR build's identity manifests), the
@@ -1673,11 +1673,11 @@ export function generateDartBridgeClient(
1673
1673
  ` sameOutcome: projection?['equal'] as bool?, differences: differences);`,
1674
1674
  ` }`,
1675
1675
  ``,
1676
- ` Future<NomosModelAdoption> _adoptGeneratedModel([String? because, Map<String, Object?>? dispositions]) async {`,
1676
+ ` Future<NomosModelAdoption> _adoptGeneratedModel([String? because]) async {`,
1677
1677
  ` final raw = await bridge.nomosFrameworkAdoptGeneratedModel(`,
1678
1678
  ` domain: ${dartStr(
1679
1679
  domain
1680
- )}, candidateHash: domainHash, because: because, dispositions: dispositions);`,
1680
+ )}, candidateHash: domainHash, because: because);`,
1681
1681
  ` final reportRaw = raw['report'];`,
1682
1682
  ` final report = reportRaw is Map ? reportRaw.cast<String, Object?>() : null;`,
1683
1683
  ` final review = _nomosModelReview(report);`,
@@ -1703,20 +1703,9 @@ export function generateDartBridgeClient(
1703
1703
  ` if (stage == 'acceptance') {`,
1704
1704
  ` return NomosModelAdoption.needsDecision(`,
1705
1705
  ` summary: 'This model would change prior business outcomes. Review the differences and record why that is intended.',`,
1706
- ` review: review, accept: (reason, {dispositions}) => _adoptGeneratedModel(reason, dispositions));`,
1706
+ ` review: review, accept: (reason) => _adoptGeneratedModel(reason));`,
1707
1707
  ` }`,
1708
1708
  ` final evolve = (report?['evolve'] as Map?)?.cast<String, Object?>();`,
1709
- ` // A field/aggregate the candidate genuinely removed is a #58 disposition decision, not a`,
1710
- ` // dead end: retry with the SAME accept/dispositions seam so the tenant's own policy can`,
1711
- ` // supply dispositions.retired. Every prior stage (promoted/staging-required/acceptance) is`,
1712
- ` // already handled above; 'identity' (needs sign-in) is the one remaining stage this must NOT`,
1713
- ` // paper over, so it is the sole exclusion rather than requiring an exact 'preflight' match or`,
1714
- ` // exact boolean report fields, which may not round-trip identically across every bridge.`,
1715
- ` if (stage != 'identity') {`,
1716
- ` return NomosModelAdoption.needsDecision(`,
1717
- ` summary: 'This model cannot yet interpret every existing value without a declared disposition. Review the differences and record why that is intended.',`,
1718
- ` review: review, accept: (reason, {dispositions}) => _adoptGeneratedModel(reason, dispositions));`,
1719
- ` }`,
1720
1709
  ` final rawError = raw['error']?.toString().trim();`,
1721
1710
  ` final summary = rawError != null && rawError.isNotEmpty`,
1722
1711
  ` ? rawError`,
@@ -2436,8 +2425,8 @@ export function generateDartBridgeClient(
2436
2425
  ` : null,`,
2437
2426
  ]),
2438
2427
  ` accept: adoption.status == NomosModelAdoptionStatus.needsDecision`,
2439
- ` ? (reason, {dispositions}) async {`,
2440
- ` final accepted = await adoption.accept(reason, dispositions: dispositions);`,
2428
+ ` ? (reason) async {`,
2429
+ ` final accepted = await adoption.accept(reason);`,
2441
2430
  ` // A retry that still isn't adopted (a transient business-outcome/history race,`,
2442
2431
  ` // e.g. concurrent writes landing between the bare preflight and this accept) must`,
2443
2432
  ` // NOT be swallowed as a terminal StateError: re-throw the SAME typed exception so`,
package/src/codegen_ts.ts CHANGED
@@ -751,6 +751,9 @@ export interface TsClientOptions {
751
751
  readonly packageName: string;
752
752
  /** Content identity of the built typed USDA closure (`.law.usdz`). */
753
753
  readonly domainHash: string;
754
+ /** Sibling lossless USDZ transport carried by the generated client when its signed-in workspace
755
+ * still serves the predecessor model. The archive is never canonical state and is not embedded. */
756
+ readonly lawAssetName?: string;
754
757
  /** The package's STRUCTURAL INTERFACE (#72) — directives (payload shapes) + aggregates
755
758
  * (read shapes, sid-keyed), keyed by domain. Baked in so `connect()` can negotiate
756
759
  * shape-compatibility against the workspace's installed offer — NOT by hash. */
@@ -819,6 +822,7 @@ function emitTsActivityBlock(entries: readonly PackageActivityEntry[], describeM
819
822
  `}`,
820
823
  `type NomosModelAdoption =`,
821
824
  ` | { status: "adopted"; summary: string; review: NomosModelReview }`,
825
+ ` | { status: "needs-staging"; summary: string; review: NomosModelReview; stage(): Promise<NomosModelAdoption> }`,
822
826
  ` | { status: "needs-decision"; summary: string; review: NomosModelReview; accept(because: string): Promise<NomosModelAdoption> }`,
823
827
  ` | { status: "blocked"; summary: string; error: string; review: NomosModelReview };`,
824
828
  `type NomosModelRestoration =`,
@@ -868,7 +872,7 @@ function emitTsActivityBlock(entries: readonly PackageActivityEntry[], describeM
868
872
  ` };`,
869
873
  `}`,
870
874
  `async function _nomosAdoptModel(`,
871
- ` holon: NomosHolon, domain: string, candidateHash: string, because?: string,`,
875
+ ` holon: NomosHolon, domain: string, candidateHash: string, lawAsset?: URL, because?: string,`,
872
876
  `): Promise<NomosModelAdoption> {`,
873
877
  ` if (!holon.__nomosAdoptModel) {`,
874
878
  ` const error = "This device needs the current Nomos client before it can adopt a model.";`,
@@ -879,6 +883,26 @@ function emitTsActivityBlock(entries: readonly PackageActivityEntry[], describeM
879
883
  ` if (raw.stage === "promoted" && raw.ok) {`,
880
884
  ` return { status: "adopted", summary: "Nomos proved this model against current history and made it active.", review };`,
881
885
  ` }`,
886
+ ` if (raw.stage === "staging-required" && lawAsset !== undefined && holon.__nomosStageGeneratedModel !== undefined) {`,
887
+ ` return {`,
888
+ ` status: "needs-staging",`,
889
+ ` summary: "This workspace is running the previous model. Nomos can stage this generated USDA candidate through its signed author gate.",`,
890
+ ` review,`,
891
+ ` stage: async () => {`,
892
+ ` // Keep Node's file loader behind a runtime-only boundary: browser-generated clients`,
893
+ ` // must typecheck without @types/node, while the local CLI can still stage its USDA asset.`,
894
+ ` const dynamicImport = Function("specifier", "return import(specifier)") as (specifier: string) => Promise<{ readFile(source: URL): Promise<Uint8Array> }>;`,
895
+ ` const { readFile } = await dynamicImport("node:fs/promises");`,
896
+ ` const lawUsdz = new Uint8Array(await readFile(lawAsset));`,
897
+ ` const staged = await holon.__nomosStageGeneratedModel!(domain, candidateHash, lawUsdz);`,
898
+ ` if (staged.ok !== true) {`,
899
+ ` const error = staged.error ?? "The workspace refused the generated USDA candidate.";`,
900
+ ` return { status: "blocked", summary: error, error, review };`,
901
+ ` }`,
902
+ ` return _nomosAdoptModel(holon, domain, candidateHash, lawAsset);`,
903
+ ` },`,
904
+ ` };`,
905
+ ` }`,
882
906
  ` if (raw.stage === "acceptance") {`,
883
907
  ` return {`,
884
908
  ` status: "needs-decision",`,
@@ -887,7 +911,7 @@ function emitTsActivityBlock(entries: readonly PackageActivityEntry[], describeM
887
911
  ` accept: async (reason: string) => {`,
888
912
  ` const stated = reason.trim();`,
889
913
  ` if (!stated) throw new Error("accept(reason) needs the business reason for changing prior outcomes");`,
890
- ` return _nomosAdoptModel(holon, domain, candidateHash, stated);`,
914
+ ` return _nomosAdoptModel(holon, domain, candidateHash, lawAsset, stated);`,
891
915
  ` },`,
892
916
  ` };`,
893
917
  ` }`,
@@ -1807,6 +1831,8 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1807
1831
  ` /** FRAMEWORK-INTERNAL exact-history adoption seam. Generated clients expose only`,
1808
1832
  ` * \`model.adopt()\` and a chained \`decision.accept(reason)\`. */`,
1809
1833
  ` __nomosAdoptModel?(domain: string, candidateHash: string, opts?: { because?: string }): Promise<__NomosModelAdoptionWire>;`,
1834
+ ` /** FRAMEWORK-INTERNAL signed lifecycle offer used by generated model staging. */`,
1835
+ ` __nomosStageGeneratedModel?(domain: string, candidateHash: string, lawUsdz: Uint8Array): Promise<{ ok?: boolean; error?: string }>;`,
1810
1836
  ` /** FRAMEWORK-INTERNAL exact-history restoration seam. Generated clients expose only`,
1811
1837
  ` * \`model.restorePrevious()\` and a chained \`decision.accept(reason)\`. */`,
1812
1838
  ` __nomosRestorePreviousModel?(domain: string, currentHash: string, opts?: { because?: string }): Promise<__NomosModelRestorationWire>;`,
@@ -3844,7 +3870,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
3844
3870
  ` /** Adopt THIS generated model. Nomos proves it against current history; a deliberate`,
3845
3871
  ` * difference returns a decision whose accept(reason) re-proves and records the reason. */`,
3846
3872
  ` model: {`,
3847
- ` adopt: (): Promise<NomosModelAdoption> => _nomosAdoptModel(holon, ${JSON.stringify(domain)}, domainHash),`,
3873
+ ` adopt: (): Promise<NomosModelAdoption> => _nomosAdoptModel(holon, ${JSON.stringify(domain)}, domainHash${opts.lawAssetName === undefined ? "" : `, new URL(${JSON.stringify(`./${opts.lawAssetName}`)}, import.meta.url)`}),`,
3848
3874
  ` restorePrevious: (): Promise<NomosModelRestoration> => _nomosRestorePreviousModel(holon, ${JSON.stringify(domain)}, domainHash),`,
3849
3875
  ` },`,
3850
3876
  ...(clientInterface !== undefined
@@ -3971,6 +3997,8 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
3971
3997
  ` readonly signIn: (request?: { readonly deviceChallenge?: string }) => Promise<string>;`,
3972
3998
  ` /** Typed timings for developer tooling; Nomos routing, credentials and Git custody remain private. */`,
3973
3999
  ` readonly onPerformance?: (sample: ${applicationName}PerformanceSample) => void;`,
4000
+ ` /** Supply the accountable business decision when this client intentionally changes the deployed model. */`,
4001
+ ` readonly approveModel?: (decision: { readonly summary: string; readonly review: NomosModelReview }) => Promise<{ readonly because: string }>;`,
3974
4002
  `}`,
3975
4003
  `function _nomosPublicDomain<T extends Record<string, unknown>>(domain: T): _NomosPublicDomain<T> {`,
3976
4004
  ` const application: Record<string, unknown> = {};`,
@@ -4106,7 +4134,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
4106
4134
  ` * or parallel cache around it. Nomos owns offline custody and synchronization. */`,
4107
4135
  `export function ${applicationOpener}(options: ${applicationName}OpenOptions): Promise<${applicationName}> {`,
4108
4136
  ` return nomosInternalOpenGeneratedApplication(`,
4109
- ` { cloud: options.cloud, application: options.application, signIn: options.signIn, onPerformance: options.onPerformance, lawHash: ${hashConst}, readyCommands: [${rootBirths.map((birth) => `{ domain: ${JSON.stringify(birth.sourceDomain)}, packageHash: ${hashConst}, directiveId: ${JSON.stringify(birth.directiveId)} }`).join(", ")}] },`,
4137
+ ` { cloud: options.cloud, application: options.application, signIn: options.signIn, onPerformance: options.onPerformance, approveModel: options.approveModel, lawHash: ${hashConst}, readyCommands: [${rootBirths.map((birth) => `{ domain: ${JSON.stringify(birth.sourceDomain)}, packageHash: ${hashConst}, directiveId: ${JSON.stringify(birth.directiveId)} }`).join(", ")}] },`,
4110
4138
  ` (holon, user) => _nomosBind${applicationName}(holon as NomosHolon, user),`,
4111
4139
  ` );`,
4112
4140
  `}`,
@@ -64,6 +64,8 @@ import {
64
64
  } from "./codegen_dart.js";
65
65
  import type { ContractHandle } from "./contract.js";
66
66
  import { bindEnumTypeExports } from "./enum_type.js";
67
+ import { discoverEvolutionModulePaths, isEvolutionModulePath } from "./evolution_files.js";
68
+ import { assertEvolutionModuleExports } from "./evolution.js";
67
69
  import { generateDartBridgeClient } from "./codegen_dart_client.js";
68
70
  import {
69
71
  dartApplicationClassName,
@@ -154,6 +156,7 @@ import {
154
156
  serializeIdentityUsda,
155
157
  } from "./identity_usda.js";
156
158
  import { reconcileStableIdsWithIdentity } from "./stable_id_identity_bridge.js";
159
+ import { LAW_FRAMEWORK_PARENT } from "./law_identity.js";
157
160
  import { discoverLawGraph, type LawGraph } from "./law_graph.js";
158
161
  import { emitFrameworkLawUsda, emitSparseLawGraphUsda } from "./law_graph_usda.js";
159
162
  import { assertNoDartBarrelCollisions, type BarrelExportedFile } from "./dart_barrel_guard.js";
@@ -163,6 +166,7 @@ import { domainInterface, type DomainInterface } from "./shape_compat.js";
163
166
  import { generateTsProof, proofLegsIndex } from "./codegen_proof.js";
164
167
  import { usdzPack } from "./usdz.js";
165
168
  import { checkDomainModules, formatFindings, hasRefusal } from "./law_check.js";
169
+ import { currentEvolveLaw, evolveDiff, evolveLawFromLockfile } from "./evolve_preview.js";
166
170
  import type { NomosCompat } from "./compat_manifest.js";
167
171
  import { requiredKernelFeatures, type FeatureProbeModule } from "./kernel_capabilities.js";
168
172
  import { APPLICATION_HOME_DOMAIN } from "./framework/application_home_domain.js";
@@ -1660,8 +1664,8 @@ const NOMOS_TYPES_DEP = "^0.8.7";
1660
1664
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
1661
1665
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
1662
1666
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
1663
- const NOMOS_CLIENT_DEP = "^0.64.28";
1664
- const NOMOS_FLUTTER_DEP = "^0.63.51";
1667
+ const NOMOS_CLIENT_DEP = "^0.64.31";
1668
+ const NOMOS_FLUTTER_DEP = "^0.63.54";
1665
1669
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
1666
1670
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
1667
1671
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -3137,6 +3141,7 @@ async function main(): Promise<void> {
3137
3141
  fail(`each domain needs { key, modules: [path, ...] }; got ${JSON.stringify(d)}`);
3138
3142
  }
3139
3143
  const mods: Mod[] = [];
3144
+ const evolutionMods: Mod[] = [];
3140
3145
  const sourceExprs: string[] = [];
3141
3146
  const directOfferExports = new Map<string, { modulePath: string; exportName: string; aggregateId: string }>();
3142
3147
  const directWorkspaceInvariantExports = new Map<string, {
@@ -3145,6 +3150,12 @@ async function main(): Promise<void> {
3145
3150
  }>();
3146
3151
  for (const rel of d.modules) {
3147
3152
  const abs = path.resolve(cfgDir, rel);
3153
+ if (isEvolutionModulePath(abs)) {
3154
+ fail(
3155
+ `domain '${d.key}': ${rel} is an evolution concern; move it under ` +
3156
+ `evolutions/${d.key}/ and remove it from feature modules so it cannot enter QuickJS`,
3157
+ );
3158
+ }
3148
3159
  if (!existsSync(abs)) fail(`domain '${d.key}': module not found: ${abs}`);
3149
3160
  const m = (await import(pathToFileURL(abs).href)) as Mod;
3150
3161
  bindEnumTypeExports(m, abs);
@@ -3182,6 +3193,16 @@ async function main(): Promise<void> {
3182
3193
  }
3183
3194
  }
3184
3195
  }
3196
+ const configuredPaths = new Set(d.modules.map((rel) => path.resolve(cfgDir, rel)));
3197
+ for (const abs of discoverEvolutionModulePaths(cfgDir, d.key)) {
3198
+ // Explicitly listed files remain supported during migration, but the same
3199
+ // module must not be composed twice when it is already in the convention.
3200
+ if (configuredPaths.has(abs)) continue;
3201
+ const evolutionModule = (await import(pathToFileURL(abs).href)) as Mod;
3202
+ assertEvolutionModuleExports(evolutionModule, abs);
3203
+ bindEnumTypeExports(evolutionModule, abs);
3204
+ evolutionMods.push(evolutionModule);
3205
+ }
3185
3206
  if (mods.some((mod) => Object.prototype.hasOwnProperty.call(mod, BUSINESS_BIRTH_POLICY_DOMAINS_EXPORT))) {
3186
3207
  fail(
3187
3208
  `domain '${d.key}' exports reserved compiler binding '${BUSINESS_BIRTH_POLICY_DOMAINS_EXPORT}'; ` +
@@ -3502,7 +3523,7 @@ async function main(): Promise<void> {
3502
3523
  composeDomainModule({
3503
3524
  name: d.name ?? d.key,
3504
3525
  domain: d.key,
3505
- modules: mods,
3526
+ modules: [...mods, ...evolutionMods],
3506
3527
  ...(extraAggregates.length > 0 ? { extraAggregates } : {}),
3507
3528
  ...(queries.length > 0 ? { queries } : {}),
3508
3529
  ...(counts.length > 0 ? { counts } : {}),
@@ -3748,7 +3769,8 @@ async function main(): Promise<void> {
3748
3769
  // exactly ONE disappeared + ONE appeared name of the same kind/shape is an inferred
3749
3770
  // RENAME (sid carried, lineage recorded, printed below); anything else mints fresh
3750
3771
  // at first appearance (a leftover disappearance is THE EVOLVE GATE's typed question
3751
- // at deploy — answered in the upgrade intent's disposition). The resolved continuity
3772
+ // at compile — answered by a typed declaration in evolutions/<domain>/*.evolution.ts).
3773
+ // The resolved continuity
3752
3774
  // is attached to the module so the canonical manifest + USD-IR carry it.
3753
3775
  const lockPath = path.join(cfgDir, NOMOS_IDENTITY_FILENAME);
3754
3776
  const priorLock: CompilerIdentityModel | undefined = existsSync(lockPath)
@@ -3891,6 +3913,63 @@ async function main(): Promise<void> {
3891
3913
  schemas: Object.fromEntries(currentAggs.map((a) => [a.id, a.schema])),
3892
3914
  };
3893
3915
  }
3916
+
3917
+ // ── ORDINARY-COMPILE EVOLUTION GATE ──────────────────────────────────────────────
3918
+ // Agents reliably run `compile`; they do not reliably discover an optional migration command.
3919
+ // Compare the candidate against the committed identity frontier here, after stable-id
3920
+ // reconciliation (so inferred renames have their true identity) but before writing either the
3921
+ // lock or the custody package. The kernel independently repeats the same admission decision at
3922
+ // deployment; this is its fast, local static twin, not a replacement for runtime authority.
3923
+ //
3924
+ // The identity file is project-wide while a package may own only some of its domains. Scope the
3925
+ // prior side to the modules in this compile so compiling one sibling never reads another sibling's
3926
+ // absence as a domain retirement.
3927
+ if (priorLock !== undefined) {
3928
+ const priorDomainsForPackage: CompilerIdentityModel["domains"] = {};
3929
+ const priorIdentity = identityRegistryFromModel(priorLock);
3930
+ const frameworkAggregateSids = new Set(
3931
+ Object.values(priorIdentity.entities)
3932
+ .filter((entity) => entity.kind === "aggregate" && entity.parent === LAW_FRAMEWORK_PARENT)
3933
+ .map((entity) => entity.sid),
3934
+ );
3935
+ for (const mod of domainModules) {
3936
+ const domainKey = mod.domain ?? mod.name;
3937
+ const locked = priorLock.domains[mod.name] ?? priorLock.domains[domainKey];
3938
+ if (locked !== undefined) {
3939
+ // Framework aggregates are shared implementation law, reconciled under @framework rather
3940
+ // than owned by this tenant domain. A package may compose a different framework subset as
3941
+ // its topology changes; never instruct a tenant to author a retirement for Nomos internals.
3942
+ const localAggregates = Object.fromEntries(
3943
+ Object.entries(locked.stableIds.aggregates)
3944
+ .filter(([, aggregate]) => !frameworkAggregateSids.has(aggregate.sid)),
3945
+ );
3946
+ priorDomainsForPackage[domainKey] = {
3947
+ stableIds: { ...locked.stableIds, aggregates: localAggregates },
3948
+ ...(locked.schemas === undefined
3949
+ ? {}
3950
+ : {
3951
+ schemas: Object.fromEntries(
3952
+ Object.entries(locked.schemas).filter(([label]) => label in localAggregates),
3953
+ ),
3954
+ }),
3955
+ };
3956
+ }
3957
+ }
3958
+ const priorLaw = evolveLawFromLockfile({
3959
+ v: 1,
3960
+ domains: priorDomainsForPackage,
3961
+ ...(priorLock.identity === undefined ? {} : { identity: priorLock.identity }),
3962
+ });
3963
+ const candidateLaw = currentEvolveLaw(domainModules, nextLock);
3964
+ const evolutionFindings = evolveDiff(priorLaw, candidateLaw).findings;
3965
+ const evolutionRefusals = evolutionFindings.filter((finding) => finding.level === "refuse");
3966
+ if (evolutionRefusals.length > 0) {
3967
+ console.error("nomos-compile: candidate law evolution refused before custody emission:");
3968
+ for (const line of formatFindings(evolutionRefusals)) console.error(` ✗ ${line}`);
3969
+ process.exitCode = 1;
3970
+ return;
3971
+ }
3972
+ }
3894
3973
  // Persist the typed identity layer (commit it with the law: it is how a rename keeps its
3895
3974
  // identity across clean clones). Existing project-domain order is retained, so once sibling
3896
3975
  // packages are registered, recompiling them in any order rewrites the same bytes without a
@@ -5214,8 +5293,11 @@ async function main(): Promise<void> {
5214
5293
  : `package asset: NOT EMITTED — declare rootPart: { name, kind: "component" | "assembly" }`,
5215
5294
  `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 })`,
5216
5295
  proofPath !== undefined
5217
- ? `generated proof: ${cfg.name}.proof.mts — run: githolon proof (live; ends ALL GREEN or names the jam)`
5296
+ ? `generated proof: ${cfg.name}.proof.mts — fresh: githolon proof; existing custody: githolon proof --upgrade`
5218
5297
  : `generated proof: SKIPPED — ${proofSkip}`,
5298
+ ...(priorLock === undefined
5299
+ ? []
5300
+ : [`upgrade proof: githolon proof --upgrade — exact existing history, disposable promotion, cold restart`]),
5219
5301
  ``,
5220
5302
  ].join("\n");
5221
5303
  writeFileSync(path.join(outDir, `${cfg.name}.summary.txt`), summaryTxt, "utf8");
@@ -5258,6 +5340,7 @@ async function main(): Promise<void> {
5258
5340
  const clientOptions = {
5259
5341
  packageName: cfg.name,
5260
5342
  domainHash,
5343
+ lawAssetName: `${cfg.name}.law.usdz`,
5261
5344
  interface: packageInterface,
5262
5345
  compat: nomosCompat,
5263
5346
  homeDomain: resolveApplicationTopology(presentationDomainModules).homeDomain,
@@ -5271,6 +5354,11 @@ async function main(): Promise<void> {
5271
5354
  if (!existsSync(runtimePath)) {
5272
5355
  fail("the compiler-private generated application runtime is missing — reinstall @githolon/dsl");
5273
5356
  }
5357
+ // Validate the generated ESM clients as ESM even when the tenant project has no package.json yet.
5358
+ // NodeNext classifies a plain `.ts` file from the nearest package boundary, so a freshly scaffolded
5359
+ // or fixture-only project would otherwise be treated as CommonJS and reject the intentional
5360
+ // `import.meta.url` used to locate its adjacent law asset. The compiler options below describe the
5361
+ // generated `.mjs` runtime contract directly instead of inheriting unrelated tenant metadata.
5274
5362
  const temporaryClientPath = path.join(outDir, `.${cfg.name}.client.source.ts`);
5275
5363
  const temporaryHeadlessClientPath = path.join(outDir, `.${cfg.name}.headless.source.ts`);
5276
5364
  const temporaryRuntimeTypesPath = path.join(outDir, `.${cfg.name}.application-runtime.d.ts`);
@@ -5278,7 +5366,7 @@ async function main(): Promise<void> {
5278
5366
  const temporaryHeadlessDeclarationPath = temporaryHeadlessClientPath.replace(/\.ts$/, ".d.ts");
5279
5367
  const runtimeTypesSource = [
5280
5368
  `export declare function nomosInternalOpenGeneratedApplication<T extends object>(`,
5281
- ` options: { cloud: string; application: string; signIn: (request?: { readonly deviceChallenge?: string }) => Promise<string>; onPerformance?: (sample: { readonly operation: "open" | "offer" | "birth" | "read" | "sync" | "maintenance"; readonly name: string; readonly durationMs: number; readonly domain?: string; readonly directive?: string }) => void; lawHash: string; readyCommands?: readonly { readonly domain: string; readonly packageHash: string; readonly directiveId: string }[] },`,
5369
+ ` options: { cloud: string; application: string; signIn: (request?: { readonly deviceChallenge?: string }) => Promise<string>; onPerformance?: (sample: { readonly operation: "open" | "offer" | "birth" | "read" | "sync" | "maintenance"; readonly name: string; readonly durationMs: number; readonly domain?: string; readonly directive?: string }) => void; approveModel?: (decision: { readonly summary: string; readonly review: { checkedActions: number; candidateActions: number; sameOutcome: boolean | null; differences: { kind: "rules" | "outcome" | "evidence"; area?: string; action?: string; summary: string }[] } }) => Promise<{ readonly because: string }>; lawHash: string; readyCommands?: readonly { readonly domain: string; readonly packageHash: string; readonly directiveId: string }[] },`,
5282
5370
  ` bind: (holon: unknown, user: { readonly identityId: string; readonly identityAddress: string }) => T,`,
5283
5371
  `): Promise<T & {`,
5284
5372
  ` currentStatus(): Promise<any>;`,
@@ -5357,8 +5445,8 @@ async function main(): Promise<void> {
5357
5445
  [temporaryClientPath, temporaryHeadlessClientPath, temporaryRuntimeTypesPath],
5358
5446
  {
5359
5447
  target: ts.ScriptTarget.ES2022,
5360
- module: ts.ModuleKind.NodeNext,
5361
- moduleResolution: ts.ModuleResolutionKind.NodeNext,
5448
+ module: ts.ModuleKind.ESNext,
5449
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
5362
5450
  declaration: true,
5363
5451
  emitDeclarationOnly: true,
5364
5452
  noEmitOnError: true,
@@ -6042,12 +6130,15 @@ async function main(): Promise<void> {
6042
6130
  console.log(` deploy githolon deploy <workspace> --file ${rel(lawUsdzPath)}`);
6043
6131
  console.log(``);
6044
6132
  console.log(`your business path:`);
6133
+ if (priorLock !== undefined) {
6134
+ console.log(` npx githolon proof --upgrade # candidate -> exact existing history -> disposable promotion -> cold restart`);
6135
+ }
6045
6136
  if (proofPath !== undefined) {
6046
6137
  console.log(` npx githolon proof # generated birth -> verify -> durable restore -> business read-back`);
6047
6138
  console.log(` npx githolon dev # rerun that proof on every save`);
6048
6139
  console.log(` npx githolon proof --live # the same business proof against Nomos Cloud`);
6049
6140
  if (proofDomains.length > 1) {
6050
- console.log(` this package proves ${proofDomains.length} domains (${proofDomains.join(", ")}) — default '${proofDomains[0]}'; pick another: npx githolon proof --domain <key>`);
6141
+ console.log(` this package proves all ${proofDomains.length} business areas together (${proofDomains.join(", ")})`);
6051
6142
  }
6052
6143
  } else {
6053
6144
  console.log(` add a business birth declaration to generate an end-to-end proof`);