@githolon/dsl 0.102.3 → 0.102.5
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/package.json +1 -1
- package/src/check_package_main.ts +3 -2
- package/src/codegen_dart.ts +2 -2
- package/src/codegen_dart_application.ts +90 -30
- package/src/codegen_ts.ts +1 -1
- package/src/compile_package_main.ts +9 -8
- package/src/evolve_preview.ts +14 -12
- package/src/fields.ts +8 -1
- package/src/lsp_server.ts +3 -2
- package/src/stable_id_lockfile.ts +144 -0
- package/src/stable_ids.ts +6 -1
- package/src/value_object.ts +8 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.102.
|
|
3
|
+
"version": "0.102.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type EvolveLaw,
|
|
27
27
|
type StableIdLockfile,
|
|
28
28
|
} from "./evolve_preview.js";
|
|
29
|
+
import { parseStableIdLockfile } from "./stable_id_lockfile.js";
|
|
29
30
|
|
|
30
31
|
interface DomainConfig {
|
|
31
32
|
key: string;
|
|
@@ -125,7 +126,7 @@ function loadPriorEvolveLaw(arg: string): { law: EvolveLaw; source: string; note
|
|
|
125
126
|
const p = parsed as Record<string, unknown>;
|
|
126
127
|
if (p.domains !== undefined && p.interface === undefined && p.packageUsda === undefined) {
|
|
127
128
|
return {
|
|
128
|
-
law: evolveLawFromLockfile(parsed
|
|
129
|
+
law: evolveLawFromLockfile(parseStableIdLockfile(parsed)),
|
|
129
130
|
source: file,
|
|
130
131
|
note: `prior is a lockfile — aggregate/field identity is diffed, but directive payload changes cannot be (deploy.json carries the interface).`,
|
|
131
132
|
};
|
|
@@ -149,7 +150,7 @@ function loadCwdLockfile(configArg: string): StableIdLockfile | undefined {
|
|
|
149
150
|
const lockPath = path.join(cfgDir, "nomos.stable-ids.json");
|
|
150
151
|
if (!existsSync(lockPath)) return undefined;
|
|
151
152
|
try {
|
|
152
|
-
return JSON.parse(readFileSync(lockPath, "utf8"))
|
|
153
|
+
return parseStableIdLockfile(JSON.parse(readFileSync(lockPath, "utf8")));
|
|
153
154
|
} catch {
|
|
154
155
|
return undefined;
|
|
155
156
|
}
|
package/src/codegen_dart.ts
CHANGED
|
@@ -1707,7 +1707,7 @@ function dartTypeOfVoField(f: ValueObjectFieldDescriptor, voName: string, fieldN
|
|
|
1707
1707
|
switch (f.type) {
|
|
1708
1708
|
case "string":
|
|
1709
1709
|
return f.idOf !== undefined
|
|
1710
|
-
? `NomosRef<${concreteClassName(f.idOf)}>`
|
|
1710
|
+
? `NomosRef<${f.idOfContract === true ? contractTypeName(f.idOf) : concreteClassName(f.idOf)}>`
|
|
1711
1711
|
: f.brand ?? "String"; // a brandedId(...) → the generated extension-type wrapper
|
|
1712
1712
|
case "number":
|
|
1713
1713
|
// a UNIT-tagged number is the generated zero-cost extension type; `.int()` is Dart int.
|
|
@@ -1742,7 +1742,7 @@ function voFromExpr(
|
|
|
1742
1742
|
switch (f.type) {
|
|
1743
1743
|
case "string":
|
|
1744
1744
|
if (f.idOf !== undefined) {
|
|
1745
|
-
const target = concreteClassName(f.idOf);
|
|
1745
|
+
const target = f.idOfContract === true ? contractTypeName(f.idOf) : concreteClassName(f.idOf);
|
|
1746
1746
|
return `NmsInternalNomosRefs.create<${target}>(workspace: '', type: '${f.idOf}', id: ${expr} as String)`;
|
|
1747
1747
|
}
|
|
1748
1748
|
return f.brand !== undefined ? `${f.brand}(${expr} as String)` : `${expr} as String`;
|
|
@@ -721,6 +721,11 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
721
721
|
const targetFile = owner?.name ?? mod.name;
|
|
722
722
|
const targetApplication = dartApplicationClassName(targetDomain);
|
|
723
723
|
const opener = `nomosInternalOpen${targetApplication}`;
|
|
724
|
+
const rootAggregate = aggregates.get(birth.aggregateId);
|
|
725
|
+
const rootType = concreteClassName(birth.aggregateId);
|
|
726
|
+
const rootCollection = member(plural(rootAggregate === undefined
|
|
727
|
+
? birth.aggregateId
|
|
728
|
+
: concreteClassName(rootAggregate.id)));
|
|
724
729
|
if (targetFile !== mod.name) {
|
|
725
730
|
addSibling(`${targetFile}.application.dart`, targetApplication);
|
|
726
731
|
addSibling(`${targetFile}.application.dart`, opener);
|
|
@@ -735,7 +740,9 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
735
740
|
` /// or call a lower-level client.`,
|
|
736
741
|
` Future<${targetApplication}> ${method}(${dartActionInputClassName(directive)} facts) async {`,
|
|
737
742
|
` final reference = await _client.${method}(facts);`,
|
|
738
|
-
|
|
743
|
+
opts.supportImport === "package"
|
|
744
|
+
? ` return ${rootCollection}._nmsOpenReference(reference);`
|
|
745
|
+
: ` return ${opener}(_client.bridge, reference, _client.actor);`,
|
|
739
746
|
` }`,
|
|
740
747
|
``,
|
|
741
748
|
);
|
|
@@ -744,31 +751,47 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
744
751
|
` /// Give one ${birth.aggregateId} its own durable life.`,
|
|
745
752
|
` Future<${targetApplication}> birth(Create${cap(camel(birth.aggregateId))}Input facts) async {`,
|
|
746
753
|
` final reference = await _client.birth(facts);`,
|
|
747
|
-
|
|
754
|
+
opts.supportImport === "package"
|
|
755
|
+
? ` return _nmsOpenReference(reference);`
|
|
756
|
+
: ` return ${opener}(_client.bridge, reference, _client.actor);`,
|
|
748
757
|
` }`,
|
|
749
758
|
``,
|
|
750
759
|
]);
|
|
751
760
|
}
|
|
752
761
|
if (opts.supportImport === "package") {
|
|
753
762
|
usesWorkspaceName = true;
|
|
754
|
-
const rootAggregate = aggregates.get(birth.aggregateId);
|
|
755
763
|
const rootModel = rootAggregate === undefined
|
|
756
764
|
? `${concreteClassName(birth.aggregateId)}ReadModel`
|
|
757
765
|
: readModelClassName(rootAggregate.id);
|
|
758
|
-
const rootType = concreteClassName(birth.aggregateId);
|
|
759
|
-
const rootCollection = member(plural(rootAggregate === undefined
|
|
760
|
-
? birth.aggregateId
|
|
761
|
-
: concreteClassName(rootAggregate.id)));
|
|
762
766
|
addCollectionMember(birth.aggregateId, "open", [
|
|
767
|
+
` final Map<String, Future<${targetApplication}>> _nmsApplications = {};`,
|
|
768
|
+
``,
|
|
769
|
+
` Future<${targetApplication}> _nmsOpenReference(NomosRef<${rootType}> reference) {`,
|
|
770
|
+
` final workspace = NmsInternalNomosRefs.workspace(reference);`,
|
|
771
|
+
` final existing = _nmsApplications[workspace];`,
|
|
772
|
+
` if (existing != null) return existing;`,
|
|
773
|
+
` late final Future<${targetApplication}> opened;`,
|
|
774
|
+
` opened = (() async {`,
|
|
775
|
+
` try {`,
|
|
776
|
+
` return await ${opener}(_client.bridge, reference, _client.actor);`,
|
|
777
|
+
` } catch (_) {`,
|
|
778
|
+
` if (identical(_nmsApplications[workspace], opened)) {`,
|
|
779
|
+
` _nmsApplications.remove(workspace);`,
|
|
780
|
+
` }`,
|
|
781
|
+
` rethrow;`,
|
|
782
|
+
` }`,
|
|
783
|
+
` })();`,
|
|
784
|
+
` _nmsApplications[workspace] = opened;`,
|
|
785
|
+
` return opened;`,
|
|
786
|
+
` }`,
|
|
787
|
+
``,
|
|
763
788
|
` /// Open the ${birth.aggregateId} whose ${birth.keyField} is [key].`,
|
|
764
|
-
` Future<${targetApplication}> open(String key) =>
|
|
765
|
-
` _client.bridge,`,
|
|
789
|
+
` Future<${targetApplication}> open(String key) => _nmsOpenReference(`,
|
|
766
790
|
` NmsInternalNomosRefs.create<${rootType}>(`,
|
|
767
791
|
` workspace: deriveWorkspaceName(${JSON.stringify(birth.namespace)}, key),`,
|
|
768
792
|
` type: ${JSON.stringify(birth.aggregateId)},`,
|
|
769
793
|
` id: ${JSON.stringify(birth.rootId)},`,
|
|
770
794
|
` ),`,
|
|
771
|
-
` _client.actor,`,
|
|
772
795
|
` );`,
|
|
773
796
|
``,
|
|
774
797
|
]);
|
|
@@ -790,14 +813,9 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
790
813
|
` if (raw is! Map) continue;`,
|
|
791
814
|
` final child = (raw['child'] ?? '').toString();`,
|
|
792
815
|
` if (!child.startsWith(${JSON.stringify(`${birth.namespace}-`)})) continue;`,
|
|
793
|
-
` final
|
|
794
|
-
`
|
|
795
|
-
`
|
|
796
|
-
` _client.actor,`,
|
|
797
|
-
` );`,
|
|
798
|
-
` final record = await application.${rootCollection}.find(`,
|
|
799
|
-
` NmsInternalNomosRefs.create<${rootType}>(workspace: child, type: ${JSON.stringify(birth.aggregateId)}, id: ${JSON.stringify(birth.rootId)}),`,
|
|
800
|
-
` );`,
|
|
816
|
+
` final reference = NmsInternalNomosRefs.create<${rootType}>(workspace: child, type: ${JSON.stringify(birth.aggregateId)}, id: ${JSON.stringify(birth.rootId)});`,
|
|
817
|
+
` final application = await _nmsOpenReference(reference);`,
|
|
818
|
+
` final record = await application.${rootCollection}.find(reference);`,
|
|
801
819
|
` if (record != null) records.add(record);`,
|
|
802
820
|
` }`,
|
|
803
821
|
` return records;`,
|
|
@@ -833,14 +851,9 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
833
851
|
` object != 'workspace:self' ||`,
|
|
834
852
|
` prunedAt.isNotEmpty ||`,
|
|
835
853
|
` !seen.add(workspace)) continue;`,
|
|
836
|
-
` final
|
|
837
|
-
`
|
|
838
|
-
`
|
|
839
|
-
` _client.actor,`,
|
|
840
|
-
` );`,
|
|
841
|
-
` final record = await application.${rootCollection}.find(`,
|
|
842
|
-
` NmsInternalNomosRefs.create<${rootType}>(workspace: workspace, type: ${JSON.stringify(birth.aggregateId)}, id: ${JSON.stringify(birth.rootId)}),`,
|
|
843
|
-
` );`,
|
|
854
|
+
` final reference = NmsInternalNomosRefs.create<${rootType}>(workspace: workspace, type: ${JSON.stringify(birth.aggregateId)}, id: ${JSON.stringify(birth.rootId)});`,
|
|
855
|
+
` final application = await _nmsOpenReference(reference);`,
|
|
856
|
+
` final record = await application.${rootCollection}.find(reference);`,
|
|
844
857
|
` if (record != null) records.add(record);`,
|
|
845
858
|
` }`,
|
|
846
859
|
` return records;`,
|
|
@@ -855,10 +868,8 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
855
868
|
` /// Continue working in the business application that produced [record].`,
|
|
856
869
|
` Future<${targetApplication}> applicationOf(${rootModel} record) {`,
|
|
857
870
|
` final origin = NmsInternalRecordOrigins.require(record);`,
|
|
858
|
-
` return
|
|
859
|
-
` _client.bridge,`,
|
|
871
|
+
` return _nmsOpenReference(`,
|
|
860
872
|
` NmsInternalNomosRefs.create<${rootType}>(workspace: origin.workspace, type: ${JSON.stringify(birth.aggregateId)}, id: origin.id),`,
|
|
861
|
-
` _client.actor,`,
|
|
862
873
|
` );`,
|
|
863
874
|
` }`,
|
|
864
875
|
``,
|
|
@@ -1028,6 +1039,55 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
1028
1039
|
(candidate as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
|
|
1029
1040
|
)) {
|
|
1030
1041
|
const sourceModel = readModelClassName(aggregate.id);
|
|
1042
|
+
for (const contractId of aggregate.implementsContracts ?? []) {
|
|
1043
|
+
const contract = contractTypeName(contractId);
|
|
1044
|
+
const method = member(`as${contract}`);
|
|
1045
|
+
addCollectionMember(aggregate.id, method, [
|
|
1046
|
+
` /// Use [record] wherever another domain declares [${contract}].`,
|
|
1047
|
+
` ///`,
|
|
1048
|
+
` /// Nomos preserves the record's workspace-qualified identity while exposing only`,
|
|
1049
|
+
` /// the contract that this aggregate explicitly implements. This does not grant access.`,
|
|
1050
|
+
` NomosRef<${contract}> ${method}(${sourceModel} record) {`,
|
|
1051
|
+
` final reference = record.ref;`,
|
|
1052
|
+
` _nmsRequireReference(reference, ${JSON.stringify(aggregate.id)}, '');`,
|
|
1053
|
+
` return NmsInternalNomosRefs.create<${contract}>(`,
|
|
1054
|
+
` workspace: reference.workspace,`,
|
|
1055
|
+
` type: ${JSON.stringify(contractId)},`,
|
|
1056
|
+
` id: reference.id,`,
|
|
1057
|
+
` );`,
|
|
1058
|
+
` }`,
|
|
1059
|
+
``,
|
|
1060
|
+
]);
|
|
1061
|
+
const resolveMethod = member(`resolve${contract}`);
|
|
1062
|
+
addCollectionMember(aggregate.id, resolveMethod, [
|
|
1063
|
+
` /// Resolve [reference] as the concrete ${sourceModel} that implements [${contract}].`,
|
|
1064
|
+
` ///`,
|
|
1065
|
+
` /// Use this after following a generated cross-application reference and opening`,
|
|
1066
|
+
` /// its business application. Nomos checks the declared contract and access on read.`,
|
|
1067
|
+
` Future<${sourceModel}?> ${resolveMethod}(ForeignRef<${contract}> reference) {`,
|
|
1068
|
+
` _nmsRequireReference(reference.ref, ${JSON.stringify(contractId)}, '');`,
|
|
1069
|
+
` return find(NmsInternalNomosRefs.create<${concreteClassName(aggregate.id)}>(`,
|
|
1070
|
+
` workspace: reference.workspace,`,
|
|
1071
|
+
` type: ${JSON.stringify(aggregate.id)},`,
|
|
1072
|
+
` id: reference.id,`,
|
|
1073
|
+
` ));`,
|
|
1074
|
+
` }`,
|
|
1075
|
+
``,
|
|
1076
|
+
]);
|
|
1077
|
+
const watchMethod = member(`watch${contract}`);
|
|
1078
|
+
addCollectionMember(aggregate.id, watchMethod, [
|
|
1079
|
+
` /// React when the concrete record behind [reference] changes.`,
|
|
1080
|
+
` Stream<${sourceModel}?> ${watchMethod}(ForeignRef<${contract}> reference) {`,
|
|
1081
|
+
` _nmsRequireReference(reference.ref, ${JSON.stringify(contractId)}, '');`,
|
|
1082
|
+
` return watchOne(NmsInternalNomosRefs.create<${concreteClassName(aggregate.id)}>(`,
|
|
1083
|
+
` workspace: reference.workspace,`,
|
|
1084
|
+
` type: ${JSON.stringify(aggregate.id)},`,
|
|
1085
|
+
` id: reference.id,`,
|
|
1086
|
+
` ));`,
|
|
1087
|
+
` }`,
|
|
1088
|
+
``,
|
|
1089
|
+
]);
|
|
1090
|
+
}
|
|
1031
1091
|
const sameDomainRefs = Object.entries(aggregate.fields).filter(([, field]) =>
|
|
1032
1092
|
field.kind === "ref" &&
|
|
1033
1093
|
field.foreignRef === undefined &&
|
|
@@ -1301,7 +1361,7 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
|
|
|
1301
1361
|
`/// a repository or parallel application state.`,
|
|
1302
1362
|
`final class ${collection} {`,
|
|
1303
1363
|
` final ${client} _client;`,
|
|
1304
|
-
`
|
|
1364
|
+
` ${collection}._(this._client);`,
|
|
1305
1365
|
``,
|
|
1306
1366
|
...members,
|
|
1307
1367
|
`}`,
|
package/src/codegen_ts.ts
CHANGED
|
@@ -598,7 +598,7 @@ function tsTypeOfVoField(f: ValueObjectFieldDescriptor): string {
|
|
|
598
598
|
switch (f.type) {
|
|
599
599
|
case "string":
|
|
600
600
|
return f.idOf !== undefined
|
|
601
|
-
? `NomosRef<${f.idOf}>`
|
|
601
|
+
? `NomosRef<${f.idOfContract === true ? contractTypeName(f.idOf) : f.idOf}>`
|
|
602
602
|
: f.brand ?? "string"; // a brandedId(...) → the emitted branded alias
|
|
603
603
|
case "number":
|
|
604
604
|
return "number"; // `.int()`/units are Dart-side nominal products; TS keeps one number
|
|
@@ -152,6 +152,11 @@ import {
|
|
|
152
152
|
type InferredRename,
|
|
153
153
|
type StableIds,
|
|
154
154
|
} from "./stable_ids.js";
|
|
155
|
+
import {
|
|
156
|
+
parseStableIdLockfile,
|
|
157
|
+
serializeStableIdLockfile,
|
|
158
|
+
type StableIdLockfile,
|
|
159
|
+
} from "./stable_id_lockfile.js";
|
|
155
160
|
import { emitUsd, type UsdLayer } from "./usd.js";
|
|
156
161
|
import { emitLayeredRelationUsda, type RelationLayerInput } from "./relation_composition.js";
|
|
157
162
|
import { assertNoDartBarrelCollisions, type BarrelExportedFile } from "./dart_barrel_guard.js";
|
|
@@ -3053,20 +3058,16 @@ async function main(): Promise<void> {
|
|
|
3053
3058
|
// at first appearance (a leftover disappearance is THE EVOLVE GATE's typed question
|
|
3054
3059
|
// at deploy — answered in the upgrade intent's disposition). The resolved continuity
|
|
3055
3060
|
// is attached to the module so the canonical manifest + USD-IR carry it.
|
|
3056
|
-
interface StableIdLock {
|
|
3057
|
-
v: 1;
|
|
3058
|
-
domains: Record<string, { stableIds: StableIds; schemas: Record<string, WireSchema> }>;
|
|
3059
|
-
}
|
|
3060
3061
|
const lockPath = path.join(cfgDir, "nomos.stable-ids.json");
|
|
3061
|
-
const priorLock:
|
|
3062
|
-
? (JSON.parse(readFileSync(lockPath, "utf8"))
|
|
3062
|
+
const priorLock: StableIdLockfile | undefined = existsSync(lockPath)
|
|
3063
|
+
? parseStableIdLockfile(JSON.parse(readFileSync(lockPath, "utf8")))
|
|
3063
3064
|
: undefined;
|
|
3064
3065
|
const inferredRenames: { domain: string; renames: InferredRename[] }[] = [];
|
|
3065
3066
|
// The lock is project-wide, not package-local. Several package configs commonly live beside one
|
|
3066
3067
|
// application stage; compiling one must not erase the stable identity of domains owned by its siblings.
|
|
3067
3068
|
// Current package domains are replaced below, while absent domains remain only as continuity metadata
|
|
3068
3069
|
// and never leak into this package's interface, law, clients or deployment.
|
|
3069
|
-
const nextLock:
|
|
3070
|
+
const nextLock: StableIdLockfile = { v: 1, domains: { ...(priorLock?.domains ?? {}) } };
|
|
3070
3071
|
for (const mod of domainModules) {
|
|
3071
3072
|
const identityName = mod.name;
|
|
3072
3073
|
// The CURRENT canonical aggregate schemas (a pre-continuity manifest pass —
|
|
@@ -3165,7 +3166,7 @@ async function main(): Promise<void> {
|
|
|
3165
3166
|
// identity across clean clones). Existing project-domain order is retained, so once sibling
|
|
3166
3167
|
// packages are registered, recompiling them in any order rewrites the same bytes without a
|
|
3167
3168
|
// giant formatting-only diff in an established lock.
|
|
3168
|
-
writeFileSync(lockPath,
|
|
3169
|
+
writeFileSync(lockPath, serializeStableIdLockfile(nextLock), "utf8");
|
|
3169
3170
|
|
|
3170
3171
|
// ── THE STRUCTURAL INTERFACE (#72) — directives (payload shapes) + aggregates (read
|
|
3171
3172
|
// shapes, sid-keyed), keyed by DOMAIN. The SAME object is (a) baked into the generated
|
package/src/evolve_preview.ts
CHANGED
|
@@ -34,6 +34,8 @@ import type { Finding } from "./law_check.js";
|
|
|
34
34
|
import type { DirectivePayloadShape, DomainInterface } from "./shape_compat.js";
|
|
35
35
|
import { domainInterface } from "./shape_compat.js";
|
|
36
36
|
import type { StableIds } from "./stable_ids_types.js";
|
|
37
|
+
import type { StableIdLockfile } from "./stable_id_lockfile.js";
|
|
38
|
+
export type { StableIdLockfile } from "./stable_id_lockfile.js";
|
|
37
39
|
import type { DomainModule } from "./codegen_dart.js";
|
|
38
40
|
import type { WireSchema } from "./wire.js";
|
|
39
41
|
import { parseUsdaDocument } from "./usd_layers.js";
|
|
@@ -92,7 +94,7 @@ function aggregatesFromStableIds(
|
|
|
92
94
|
for (const [aggLabel, a] of Object.entries(stableIds.aggregates)) {
|
|
93
95
|
const fields: EvolveAggregate["fields"] = {};
|
|
94
96
|
for (const [fLabel, f] of Object.entries(a.fields ?? {})) {
|
|
95
|
-
const driver = schemas?.[aggLabel]?.[fLabel];
|
|
97
|
+
const driver = f.driver ?? schemas?.[aggLabel]?.[fLabel];
|
|
96
98
|
fields[f.sid] = {
|
|
97
99
|
sid: f.sid,
|
|
98
100
|
label: fLabel,
|
|
@@ -130,12 +132,6 @@ function evolveDomainFromInterface(iface: DomainInterface): EvolveDomain {
|
|
|
130
132
|
};
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
/** The committed `nomos.stable-ids.json` lockfile shape (compile_package_main writes it). */
|
|
134
|
-
export interface StableIdLockfile {
|
|
135
|
-
v: 1;
|
|
136
|
-
domains: Record<string, { stableIds: StableIds; schemas?: Record<string, WireSchema> }>;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
135
|
/** A deploy.json body (the subset this preview reads): the per-domain interface + the package
|
|
140
136
|
* bytes (for the embedded stable-ids/driver schemas when no lockfile rides alongside). */
|
|
141
137
|
export interface DeployJson {
|
|
@@ -254,13 +250,19 @@ export function currentEvolveLaw(
|
|
|
254
250
|
const stableIds = locked?.stableIds;
|
|
255
251
|
const iface = domainInterface(mod, stableIds);
|
|
256
252
|
const dom = evolveDomainFromInterface(iface);
|
|
257
|
-
// Fold drivers from the
|
|
258
|
-
|
|
253
|
+
// Fold drivers from the stable fields. Verbose v1 locks may also carry the
|
|
254
|
+
// older aggregate-schema copy; compact v2 locks deliberately omit it.
|
|
255
|
+
if (stableIds !== undefined) {
|
|
259
256
|
for (const agg of Object.values(dom.aggregates)) {
|
|
260
|
-
const
|
|
261
|
-
|
|
257
|
+
const stableAggregate = Object.values(stableIds.aggregates).find(
|
|
258
|
+
(candidate) => candidate.sid === agg.sid,
|
|
259
|
+
);
|
|
260
|
+
const schema = locked?.schemas?.[agg.label];
|
|
262
261
|
for (const f of Object.values(agg.fields)) {
|
|
263
|
-
const
|
|
262
|
+
const stableField = Object.values(stableAggregate?.fields ?? {}).find(
|
|
263
|
+
(candidate) => candidate.sid === f.sid,
|
|
264
|
+
);
|
|
265
|
+
const driver = stableField?.driver ?? schema?.[f.label];
|
|
264
266
|
if (driver !== undefined) (f as { driver?: string }).driver = JSON.stringify(driver);
|
|
265
267
|
}
|
|
266
268
|
}
|
package/src/fields.ts
CHANGED
|
@@ -171,7 +171,14 @@ export type ValueObjectFieldDescriptor =
|
|
|
171
171
|
/** A plain string, OR a BRANDED nominal string when `brand` is present (a `brandedId(...)`
|
|
172
172
|
* — the generated TS branded alias / Dart extension-type wrapper's name). Wire-identical
|
|
173
173
|
* to a plain string; the brand is a compile-time nominal tag only. */
|
|
174
|
-
| {
|
|
174
|
+
| {
|
|
175
|
+
readonly type: "string";
|
|
176
|
+
readonly brand?: string;
|
|
177
|
+
readonly idOf?: string;
|
|
178
|
+
/** Present only for an `idOf(recordContract(...))` target. Aggregate is the default. */
|
|
179
|
+
readonly idOfContract?: true;
|
|
180
|
+
readonly optional?: true;
|
|
181
|
+
}
|
|
175
182
|
/** `int: true` when the zod carried `.int()` (Dart `int`, TS `number`); `unit` when the field
|
|
176
183
|
* was declared through `unit(...)` — the generated Dart extension-type wrapper's name. */
|
|
177
184
|
| { readonly type: "number"; readonly int?: true; readonly unit?: string; readonly optional?: true }
|
package/src/lsp_server.ts
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
type EvolveLaw,
|
|
50
50
|
type StableIdLockfile,
|
|
51
51
|
} from "./evolve_preview.js";
|
|
52
|
+
import { parseStableIdLockfile } from "./stable_id_lockfile.js";
|
|
52
53
|
|
|
53
54
|
interface DomainConfig {
|
|
54
55
|
key: string;
|
|
@@ -192,7 +193,7 @@ function loadPriorEvolveLaw(arg: string, cfgDir: string): EvolveLaw | undefined
|
|
|
192
193
|
}
|
|
193
194
|
const p = parsed as Record<string, unknown>;
|
|
194
195
|
if (p.domains !== undefined && p.interface === undefined && p.packageUsda === undefined) {
|
|
195
|
-
return evolveLawFromLockfile(parsed
|
|
196
|
+
return evolveLawFromLockfile(parseStableIdLockfile(parsed));
|
|
196
197
|
}
|
|
197
198
|
return evolveLawFromDeployJson(parsed as never);
|
|
198
199
|
}
|
|
@@ -202,7 +203,7 @@ function loadCwdLockfile(cfgDir: string): StableIdLockfile | undefined {
|
|
|
202
203
|
const lockPath = path.join(cfgDir, "nomos.stable-ids.json");
|
|
203
204
|
if (!existsSync(lockPath)) return undefined;
|
|
204
205
|
try {
|
|
205
|
-
return JSON.parse(readFileSync(lockPath, "utf8"))
|
|
206
|
+
return parseStableIdLockfile(JSON.parse(readFileSync(lockPath, "utf8")));
|
|
206
207
|
} catch {
|
|
207
208
|
return undefined;
|
|
208
209
|
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { StableAggregateId, StableFieldId, StableIds } from "./stable_ids_types.js";
|
|
2
|
+
import type { WireSchema } from "./wire.js";
|
|
3
|
+
|
|
4
|
+
export interface StableIdLockDomain {
|
|
5
|
+
stableIds: StableIds;
|
|
6
|
+
schemas?: Record<string, WireSchema>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The materialized lock shape consumed inside the compiler and authoring tools. */
|
|
10
|
+
export interface StableIdLockfile {
|
|
11
|
+
v: 1;
|
|
12
|
+
domains: Record<string, StableIdLockDomain>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type CompactStableFieldId = Omit<StableFieldId, "contract" | "shape"> & {
|
|
16
|
+
contract?: string | number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
interface CompactStableAggregateId extends Omit<StableAggregateId, "fields"> {
|
|
20
|
+
fields: Record<string, CompactStableFieldId>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface CompactStableIds extends Omit<StableIds, "aggregates"> {
|
|
24
|
+
aggregates: Record<string, CompactStableAggregateId>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface CompactStableIdLockfile {
|
|
28
|
+
v: 2;
|
|
29
|
+
/** [contract hash, canonical contract descriptor], addressed by field.contract. */
|
|
30
|
+
contracts: readonly (readonly [string, unknown])[];
|
|
31
|
+
domains: Record<string, { stableIds: CompactStableIds }>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
35
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sortedJson(value: unknown): unknown {
|
|
39
|
+
if (Array.isArray(value)) return value.map(sortedJson);
|
|
40
|
+
if (!isRecord(value)) return value;
|
|
41
|
+
return Object.fromEntries(
|
|
42
|
+
Object.keys(value)
|
|
43
|
+
.sort()
|
|
44
|
+
.map((key) => [key, sortedJson(value[key])]),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Read both the original verbose lock and the compact v2 wire shape. Callers always
|
|
50
|
+
* receive the same fully materialized v1 object, so continuity and evolution logic
|
|
51
|
+
* never needs to know how the durable file was encoded.
|
|
52
|
+
*/
|
|
53
|
+
export function parseStableIdLockfile(value: unknown): StableIdLockfile {
|
|
54
|
+
if (!isRecord(value)) throw new Error("stable-id lock must be a JSON object");
|
|
55
|
+
if (value.v === 1) return value as unknown as StableIdLockfile;
|
|
56
|
+
if (value.v !== 2 || !Array.isArray(value.contracts) || !isRecord(value.domains)) {
|
|
57
|
+
throw new Error("unsupported stable-id lock encoding");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const compact = value as unknown as CompactStableIdLockfile;
|
|
61
|
+
const contracts = compact.contracts.map((entry, index) => {
|
|
62
|
+
if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string") {
|
|
63
|
+
throw new Error(`stable-id lock contract ${index} is malformed`);
|
|
64
|
+
}
|
|
65
|
+
return entry;
|
|
66
|
+
});
|
|
67
|
+
const domains: StableIdLockfile["domains"] = {};
|
|
68
|
+
|
|
69
|
+
for (const [domainName, domain] of Object.entries(compact.domains)) {
|
|
70
|
+
const aggregates: StableIds["aggregates"] = {};
|
|
71
|
+
for (const [aggregateName, aggregate] of Object.entries(domain.stableIds.aggregates ?? {})) {
|
|
72
|
+
const fields: StableAggregateId["fields"] = {};
|
|
73
|
+
for (const [fieldName, field] of Object.entries(aggregate.fields ?? {})) {
|
|
74
|
+
const contractRef = field.contract;
|
|
75
|
+
const { contract: _contractRef, ...fieldWithoutContract } = field;
|
|
76
|
+
if (typeof contractRef === "number") {
|
|
77
|
+
const contract = contracts[contractRef];
|
|
78
|
+
if (contract === undefined) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`stable-id lock field ${domainName}.${aggregateName}.${fieldName} references missing contract ${contractRef}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
fields[fieldName] = {
|
|
84
|
+
...fieldWithoutContract,
|
|
85
|
+
contract: contract[0],
|
|
86
|
+
shape: contract[1],
|
|
87
|
+
};
|
|
88
|
+
} else if (typeof contractRef === "string") {
|
|
89
|
+
fields[fieldName] = { ...fieldWithoutContract, contract: contractRef };
|
|
90
|
+
} else {
|
|
91
|
+
fields[fieldName] = fieldWithoutContract;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
aggregates[aggregateName] = { ...aggregate, fields };
|
|
95
|
+
}
|
|
96
|
+
domains[domainName] = {
|
|
97
|
+
stableIds: { ...domain.stableIds, aggregates },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { v: 1, domains };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Encode the committed lock without repeating identical contract descriptors or
|
|
106
|
+
* aggregate driver schemas. Fields already retain their canonical merge driver;
|
|
107
|
+
* the contract table retains the full descriptor needed to prove safe widening.
|
|
108
|
+
*/
|
|
109
|
+
export function serializeStableIdLockfile(lock: StableIdLockfile): string {
|
|
110
|
+
const contracts: [string, unknown][] = [];
|
|
111
|
+
const contractIndexes = new Map<string, number>();
|
|
112
|
+
const domains: CompactStableIdLockfile["domains"] = {};
|
|
113
|
+
|
|
114
|
+
for (const [domainName, domain] of Object.entries(lock.domains)) {
|
|
115
|
+
const aggregates: CompactStableIds["aggregates"] = {};
|
|
116
|
+
for (const [aggregateName, aggregate] of Object.entries(domain.stableIds.aggregates)) {
|
|
117
|
+
const fields: CompactStableAggregateId["fields"] = {};
|
|
118
|
+
for (const [fieldName, field] of Object.entries(aggregate.fields)) {
|
|
119
|
+
const { shape, ...withoutShape } = field;
|
|
120
|
+
if (field.contract !== undefined && shape !== undefined) {
|
|
121
|
+
let index = contractIndexes.get(field.contract);
|
|
122
|
+
if (index === undefined) {
|
|
123
|
+
index = contracts.length;
|
|
124
|
+
contracts.push([field.contract, shape]);
|
|
125
|
+
contractIndexes.set(field.contract, index);
|
|
126
|
+
}
|
|
127
|
+
fields[fieldName] = { ...withoutShape, contract: index };
|
|
128
|
+
} else {
|
|
129
|
+
fields[fieldName] = withoutShape;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
aggregates[aggregateName] = { ...aggregate, fields };
|
|
133
|
+
}
|
|
134
|
+
domains[domainName] = {
|
|
135
|
+
stableIds: { ...domain.stableIds, aggregates },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// This is a machine-owned lockfile. Compact JSON is intentional: unlike generated
|
|
140
|
+
// law and clients, it must be committed so identity survives a clean clone.
|
|
141
|
+
return JSON.stringify(
|
|
142
|
+
sortedJson({ v: 2, contracts, domains } satisfies CompactStableIdLockfile),
|
|
143
|
+
) + "\n";
|
|
144
|
+
}
|
package/src/stable_ids.ts
CHANGED
|
@@ -233,9 +233,14 @@ export function deriveStableIdContinuity(
|
|
|
233
233
|
* drivers). Returns `undefined` (pair allowed on name-cardinality alone) when no
|
|
234
234
|
* snapshot is available. */
|
|
235
235
|
function priorFieldDriver(
|
|
236
|
-
resolved: {
|
|
236
|
+
resolved: {
|
|
237
|
+
priorFields?: Record<string, StableFieldId>;
|
|
238
|
+
priorSchema?: WireSchema;
|
|
239
|
+
} | undefined,
|
|
237
240
|
oldField: string,
|
|
238
241
|
): string | undefined {
|
|
242
|
+
const retained = resolved?.priorFields?.[oldField]?.driver;
|
|
243
|
+
if (retained !== undefined) return JSON.stringify(retained);
|
|
239
244
|
const schema = resolved?.priorSchema;
|
|
240
245
|
if (schema === undefined) return undefined;
|
|
241
246
|
const d = schema[oldField];
|
package/src/value_object.ts
CHANGED
|
@@ -396,12 +396,17 @@ function describeZod(
|
|
|
396
396
|
switch (zodKind(schema)) {
|
|
397
397
|
case "string": {
|
|
398
398
|
const brandedBrand = (schema as unknown as Record<symbol, unknown>)[NOMOS_BRANDED_ID_BRAND];
|
|
399
|
-
const
|
|
399
|
+
const idOfTarget = idOfMetaOf(schema);
|
|
400
400
|
// idOf(Agg) REFERENCES the aggregate's canonical `NomosRef<Agg>`. Carry `idOf` in the stable
|
|
401
401
|
// descriptor so every emitter can recover the aggregate type while the wire remains a string.
|
|
402
402
|
// A standalone `brandedId("X")` remains an unrelated nominal string and declares its own wrapper.
|
|
403
|
-
if (
|
|
404
|
-
return {
|
|
403
|
+
if (idOfTarget !== undefined) {
|
|
404
|
+
return {
|
|
405
|
+
type: "string",
|
|
406
|
+
brand: `${idOfTarget}Id`,
|
|
407
|
+
idOf: idOfTarget,
|
|
408
|
+
...(idOfTargetKindOf(schema) === "contract" ? { idOfContract: true as const } : {}),
|
|
409
|
+
};
|
|
405
410
|
}
|
|
406
411
|
return typeof brandedBrand === "string" ? { type: "string", brand: brandedBrand } : { type: "string" };
|
|
407
412
|
}
|