@githolon/dsl 0.105.12 → 0.105.14
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 +14 -0
- package/dart/lib/src/runner/node-entry.mjs +22 -10
- package/dart/lib/src/runner/offline-install.mjs +8 -9
- package/dart/lib/src/session_bound.dart +7 -7
- package/dart/lib/src/webview_bridge.dart +4 -5
- package/dart/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/runtime/generated-application-host.js +51 -18
- package/src/build_package.ts +37 -3
- package/src/check_package_main.ts +12 -10
- package/src/codegen_dart.ts +7 -4
- package/src/codegen_dart_client.ts +5 -16
- package/src/codegen_proof.ts +40 -2
- package/src/codegen_ts.ts +32 -4
- package/src/compile_package_main.ts +100 -9
- package/src/evolution.ts +313 -0
- package/src/evolution_files.ts +31 -0
- package/src/evolve_preview.ts +64 -55
- package/src/index.ts +10 -0
- package/src/law_graph.ts +58 -1
- package/src/law_graph_usda.ts +26 -0
- package/src/stable_ids_for_module.ts +1 -1
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.
|
|
1664
|
-
const NOMOS_FLUTTER_DEP = "^0.63.
|
|
1667
|
+
const NOMOS_CLIENT_DEP = "^0.64.29";
|
|
1668
|
+
const NOMOS_FLUTTER_DEP = "^0.63.52";
|
|
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
|
|
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 —
|
|
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.
|
|
5361
|
-
moduleResolution: ts.ModuleResolutionKind.
|
|
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}
|
|
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`);
|
package/src/evolution.ts
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// NOMOS — business model evolution as a separate, composable concern.
|
|
2
|
+
//
|
|
3
|
+
// Current aggregate files describe what the business means now. An evolution file
|
|
4
|
+
// relates an earlier lawful field contract to that current meaning. The compiler
|
|
5
|
+
// discovers these typed declarations under evolutions/<domain>/**/*.evolution.ts,
|
|
6
|
+
// proves them, and lowers them into the same canonical USDA law. They are never host
|
|
7
|
+
// configuration and never deployment-time stable-id strings.
|
|
8
|
+
|
|
9
|
+
import type { AggregateHandle, StoredFields } from "./aggregate.js";
|
|
10
|
+
import type {
|
|
11
|
+
Field,
|
|
12
|
+
FieldEvolutionDeclaration,
|
|
13
|
+
FieldValue,
|
|
14
|
+
} from "./fields.js";
|
|
15
|
+
|
|
16
|
+
const EVOLUTION_BRAND = "__nomos_field_evolution_v1" as const;
|
|
17
|
+
const FORMER_AGGREGATE_BRAND = "__nomos_former_aggregate_v1" as const;
|
|
18
|
+
const FORMER_FIELD_BRAND = "__nomos_former_field_v1" as const;
|
|
19
|
+
const RETIREMENT_BRAND = "__nomos_retirement_v1" as const;
|
|
20
|
+
|
|
21
|
+
export interface FieldEvolution<
|
|
22
|
+
A extends AggregateHandle = AggregateHandle,
|
|
23
|
+
> {
|
|
24
|
+
readonly [EVOLUTION_BRAND]: true;
|
|
25
|
+
readonly aggregate: A;
|
|
26
|
+
readonly field: string;
|
|
27
|
+
readonly declaration: FieldEvolutionDeclaration;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FieldEvolutionBuilder<T> {
|
|
31
|
+
/**
|
|
32
|
+
* Relate one earlier lawful field contract to the current field. With no
|
|
33
|
+
* projection, Nomos proves identity widening. A projection is admitted only
|
|
34
|
+
* for a finite source language and is exhaustively compiled to a case table.
|
|
35
|
+
*/
|
|
36
|
+
from<From>(
|
|
37
|
+
source: Field<From>,
|
|
38
|
+
project?: (value: From) => T,
|
|
39
|
+
): FieldEvolution;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FormerField<AggregateId extends string = string, Name extends string = string> {
|
|
43
|
+
readonly [FORMER_FIELD_BRAND]: true;
|
|
44
|
+
readonly aggregate: AggregateId;
|
|
45
|
+
readonly name: Name;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface FormerAggregate<
|
|
49
|
+
Id extends string = string,
|
|
50
|
+
FieldName extends string = string,
|
|
51
|
+
> {
|
|
52
|
+
readonly [FORMER_AGGREGATE_BRAND]: true;
|
|
53
|
+
readonly id: Id;
|
|
54
|
+
readonly fields: Readonly<Record<FieldName, FormerField<Id, FieldName>>>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface Retirement {
|
|
58
|
+
readonly [RETIREMENT_BRAND]: true;
|
|
59
|
+
readonly kind: "aggregate" | "field";
|
|
60
|
+
readonly aggregate: string;
|
|
61
|
+
readonly field?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Name a declaration from the immediately preceding model without putting it
|
|
66
|
+
* back into the current business model. The names occur once at this historical
|
|
67
|
+
* declaration site; every retirement elsewhere refers to the returned handles.
|
|
68
|
+
*/
|
|
69
|
+
export function formerAggregate<
|
|
70
|
+
const Id extends string,
|
|
71
|
+
const FieldName extends string,
|
|
72
|
+
>(
|
|
73
|
+
id: Id,
|
|
74
|
+
fieldNames: readonly FieldName[] = [],
|
|
75
|
+
): FormerAggregate<Id, FieldName> {
|
|
76
|
+
if (!id.trim()) throw new Error("formerAggregate(id) needs the prior aggregate's non-empty name");
|
|
77
|
+
const fields: Record<string, FormerField<Id, FieldName>> = {};
|
|
78
|
+
for (const name of fieldNames) {
|
|
79
|
+
if (!name.trim()) throw new Error(`formerAggregate(${id}) has an empty prior field name`);
|
|
80
|
+
if (fields[name] !== undefined) throw new Error(`formerAggregate(${id}) repeats prior field '${name}'`);
|
|
81
|
+
fields[name] = Object.freeze({
|
|
82
|
+
[FORMER_FIELD_BRAND]: true as const,
|
|
83
|
+
aggregate: id,
|
|
84
|
+
name,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
[FORMER_AGGREGATE_BRAND]: true as const,
|
|
89
|
+
id,
|
|
90
|
+
fields: Object.freeze(fields) as Readonly<Record<FieldName, FormerField<Id, FieldName>>>,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Declare one intentional tombstone in a separate evolution file. */
|
|
95
|
+
export function retirement(
|
|
96
|
+
entity: FormerAggregate | FormerField,
|
|
97
|
+
): Retirement {
|
|
98
|
+
if (entity === null || typeof entity !== "object") {
|
|
99
|
+
throw new Error("retirement(entity) needs a handle from formerAggregate(...)");
|
|
100
|
+
}
|
|
101
|
+
if ((entity as unknown as Record<string, unknown>)[FORMER_AGGREGATE_BRAND] === true) {
|
|
102
|
+
const aggregate = entity as FormerAggregate;
|
|
103
|
+
return Object.freeze({
|
|
104
|
+
[RETIREMENT_BRAND]: true as const,
|
|
105
|
+
kind: "aggregate" as const,
|
|
106
|
+
aggregate: aggregate.id,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if ((entity as unknown as Record<string, unknown>)[FORMER_FIELD_BRAND] === true) {
|
|
110
|
+
const field = entity as FormerField;
|
|
111
|
+
return Object.freeze({
|
|
112
|
+
[RETIREMENT_BRAND]: true as const,
|
|
113
|
+
kind: "field" as const,
|
|
114
|
+
aggregate: field.aggregate,
|
|
115
|
+
field: field.name,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
throw new Error("retirement(entity) needs a handle from formerAggregate(...)");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Begin a separately authored evolution for one current business field.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* // evolutions/publication-reference.evolution.ts
|
|
127
|
+
* export const publicationReference = evolution(Snapshot, "publicationId")
|
|
128
|
+
* .from(legacyPublicationId);
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export function evolution<
|
|
132
|
+
const Id extends string,
|
|
133
|
+
F extends Record<string, Field>,
|
|
134
|
+
K extends keyof StoredFields<F> & string,
|
|
135
|
+
>(
|
|
136
|
+
aggregate: AggregateHandle<Id, F>,
|
|
137
|
+
field: K,
|
|
138
|
+
): FieldEvolutionBuilder<FieldValue<StoredFields<F>[K]>> {
|
|
139
|
+
const target = (aggregate.fields as Record<string, Field>)[field];
|
|
140
|
+
if (target === undefined) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`evolution(${aggregate.id}, ${JSON.stringify(field)}): '${field}' is not a stored field of ${aggregate.id}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
from<From>(source: Field<From>, project?: (value: From) => FieldValue<StoredFields<F>[K]>): FieldEvolution {
|
|
147
|
+
// Reuse the field declaration's single proof implementation. The returned
|
|
148
|
+
// field is only a temporary immutable value from which we take the one new
|
|
149
|
+
// edge; the current aggregate declaration is never mutated.
|
|
150
|
+
const evolved = target.evolvesFrom(
|
|
151
|
+
source,
|
|
152
|
+
project as ((value: From) => unknown) | undefined,
|
|
153
|
+
);
|
|
154
|
+
const declarations = evolved.evolutions ?? [];
|
|
155
|
+
const declaration = declarations[declarations.length - 1];
|
|
156
|
+
if (declaration === undefined) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`evolution(${aggregate.id}, ${JSON.stringify(field)}): failed to construct an evolution edge`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return Object.freeze({
|
|
162
|
+
[EVOLUTION_BRAND]: true as const,
|
|
163
|
+
aggregate,
|
|
164
|
+
field,
|
|
165
|
+
declaration,
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function isFieldEvolution(value: unknown): value is FieldEvolution {
|
|
172
|
+
return (
|
|
173
|
+
value !== null &&
|
|
174
|
+
typeof value === "object" &&
|
|
175
|
+
(value as Record<string, unknown>)[EVOLUTION_BRAND] === true
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function isRetirement(value: unknown): value is Retirement {
|
|
180
|
+
return (
|
|
181
|
+
value !== null &&
|
|
182
|
+
typeof value === "object" &&
|
|
183
|
+
(value as Record<string, unknown>)[RETIREMENT_BRAND] === true
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Evolution files are compiler-only by construction; executable/business exports would be law
|
|
188
|
+
* that is absent from QuickJS. Refuse that split-brain shape at the file boundary. */
|
|
189
|
+
export function assertEvolutionModuleExports(
|
|
190
|
+
module: Readonly<Record<string, unknown>>,
|
|
191
|
+
file: string,
|
|
192
|
+
): void {
|
|
193
|
+
for (const [name, value] of Object.entries(module)) {
|
|
194
|
+
if (isFieldEvolution(value) || isRetirement(value)) continue;
|
|
195
|
+
throw new Error(
|
|
196
|
+
`${file}: evolution file export '${name}' is not evolution(...) or retirement(...). ` +
|
|
197
|
+
"Keep current aggregates, directives and helpers in business modules; evolution files " +
|
|
198
|
+
"are compiler-only historical law.",
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Collect evolution edges without allowing re-exports to duplicate one object. */
|
|
204
|
+
export function fieldEvolutionsOf(
|
|
205
|
+
modules: readonly Record<string, unknown>[],
|
|
206
|
+
): FieldEvolution[] {
|
|
207
|
+
const seen = new Set<FieldEvolution>();
|
|
208
|
+
const out: FieldEvolution[] = [];
|
|
209
|
+
for (const module of modules) {
|
|
210
|
+
for (const value of Object.values(module)) {
|
|
211
|
+
if (!isFieldEvolution(value) || seen.has(value)) continue;
|
|
212
|
+
seen.add(value);
|
|
213
|
+
out.push(value);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Collect explicit tombstones; two files may not retire the same identity twice. */
|
|
220
|
+
export function retirementsOf(
|
|
221
|
+
modules: readonly Record<string, unknown>[],
|
|
222
|
+
domain: string,
|
|
223
|
+
): Retirement[] {
|
|
224
|
+
const out: Retirement[] = [];
|
|
225
|
+
const keys = new Set<string>();
|
|
226
|
+
for (const module of modules) {
|
|
227
|
+
for (const value of Object.values(module)) {
|
|
228
|
+
if (!isRetirement(value)) continue;
|
|
229
|
+
const key = value.kind === "aggregate"
|
|
230
|
+
? `aggregate:${value.aggregate}`
|
|
231
|
+
: `field:${value.aggregate}.${value.field ?? ""}`;
|
|
232
|
+
if (!keys.add(key)) {
|
|
233
|
+
throw new Error(`domain '${domain}' retires '${key}' more than once`);
|
|
234
|
+
}
|
|
235
|
+
out.push(value);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function cloneFieldWithEvolution(
|
|
242
|
+
field: Field,
|
|
243
|
+
declaration: FieldEvolutionDeclaration,
|
|
244
|
+
): Field {
|
|
245
|
+
const clone = Object.defineProperties(
|
|
246
|
+
{},
|
|
247
|
+
Object.getOwnPropertyDescriptors(field),
|
|
248
|
+
) as Field;
|
|
249
|
+
Object.defineProperty(clone, "evolutions", {
|
|
250
|
+
value: Object.freeze([...(field.evolutions ?? []), declaration]),
|
|
251
|
+
enumerable: true,
|
|
252
|
+
configurable: true,
|
|
253
|
+
writable: false,
|
|
254
|
+
});
|
|
255
|
+
return clone;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Apply separately authored edges to the aggregate handles consumed by law emission. */
|
|
259
|
+
export function applyFieldEvolutionsToAggregates(
|
|
260
|
+
aggregates: readonly AggregateHandle[],
|
|
261
|
+
evolutions: readonly FieldEvolution[],
|
|
262
|
+
domain: string,
|
|
263
|
+
): AggregateHandle[] {
|
|
264
|
+
const byAggregate = new Map(aggregates.map((aggregate) => [aggregate.id, aggregate]));
|
|
265
|
+
const byTarget = new Map<string, FieldEvolution[]>();
|
|
266
|
+
for (const declaration of evolutions) {
|
|
267
|
+
const aggregate = byAggregate.get(declaration.aggregate.id);
|
|
268
|
+
if (aggregate === undefined) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`domain '${domain}' exports an evolution for '${declaration.aggregate.id}.${declaration.field}' ` +
|
|
271
|
+
"but does not export the current aggregate itself",
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
if ((aggregate.fields as Record<string, Field>)[declaration.field] === undefined) {
|
|
275
|
+
throw new Error(
|
|
276
|
+
`domain '${domain}' exports an evolution for missing current field ` +
|
|
277
|
+
`'${declaration.aggregate.id}.${declaration.field}'`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const key = `${declaration.aggregate.id}\u0000${declaration.field}`;
|
|
281
|
+
const current = byTarget.get(key) ?? [];
|
|
282
|
+
current.push(declaration);
|
|
283
|
+
byTarget.set(key, current);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return aggregates.map((aggregate) => {
|
|
287
|
+
const targets = [...byTarget]
|
|
288
|
+
.filter(([key]) => key.startsWith(`${aggregate.id}\u0000`))
|
|
289
|
+
.flatMap(([, declarations]) => declarations);
|
|
290
|
+
if (targets.length === 0) return aggregate;
|
|
291
|
+
const fields = Object.defineProperties(
|
|
292
|
+
{},
|
|
293
|
+
Object.getOwnPropertyDescriptors(aggregate.fields),
|
|
294
|
+
) as Record<string, Field>;
|
|
295
|
+
for (const target of targets) {
|
|
296
|
+
fields[target.field] = cloneFieldWithEvolution(
|
|
297
|
+
fields[target.field]!,
|
|
298
|
+
target.declaration,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
const clone = Object.defineProperties(
|
|
302
|
+
{},
|
|
303
|
+
Object.getOwnPropertyDescriptors(aggregate),
|
|
304
|
+
) as AggregateHandle;
|
|
305
|
+
Object.defineProperty(clone, "fields", {
|
|
306
|
+
value: fields,
|
|
307
|
+
enumerable: true,
|
|
308
|
+
configurable: true,
|
|
309
|
+
writable: false,
|
|
310
|
+
});
|
|
311
|
+
return clone;
|
|
312
|
+
});
|
|
313
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const EVOLUTION_MODULE_SUFFIX = /\.evolution\.(?:ts|tsx|mts|mjs|js)$/;
|
|
5
|
+
|
|
6
|
+
export function isEvolutionModulePath(file: string): boolean {
|
|
7
|
+
return EVOLUTION_MODULE_SUFFIX.test(file);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Discover compiler-only evolution concerns by convention. Both `compile` and
|
|
12
|
+
* `check` use this function so local assurance cannot inspect a different law
|
|
13
|
+
* closure from the one that will be deployed.
|
|
14
|
+
*/
|
|
15
|
+
export function discoverEvolutionModulePaths(
|
|
16
|
+
configDirectory: string,
|
|
17
|
+
domainKey: string,
|
|
18
|
+
): string[] {
|
|
19
|
+
const root = path.join(configDirectory, "evolutions", domainKey);
|
|
20
|
+
if (!existsSync(root)) return [];
|
|
21
|
+
const out: string[] = [];
|
|
22
|
+
const visit = (directory: string): void => {
|
|
23
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
24
|
+
const absolute = path.join(directory, entry.name);
|
|
25
|
+
if (entry.isDirectory()) visit(absolute);
|
|
26
|
+
else if (entry.isFile() && isEvolutionModulePath(entry.name)) out.push(absolute);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
visit(root);
|
|
30
|
+
return out.sort((left, right) => left.localeCompare(right));
|
|
31
|
+
}
|