@githolon/dsl 0.102.4 → 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/compile_package_main.ts +9 -8
- package/src/evolve_preview.ts +14 -12
- package/src/lsp_server.ts +3 -2
- package/src/stable_id_lockfile.ts +144 -0
- package/src/stable_ids.ts +6 -1
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
|
}
|
|
@@ -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/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];
|