@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.102.4",
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 as StableIdLockfile),
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")) as StableIdLockfile;
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: StableIdLock | undefined = existsSync(lockPath)
3062
- ? (JSON.parse(readFileSync(lockPath, "utf8")) as StableIdLock)
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: StableIdLock = { v: 1, domains: { ...(priorLock?.domains ?? {}) } };
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, JSON.stringify(nextLock, null, 2) + "\n", "utf8");
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
@@ -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 lockfile schemas (interface has none).
258
- if (locked?.schemas !== undefined) {
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 schema = locked.schemas[agg.label];
261
- if (schema === undefined) continue;
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 driver = schema[f.label];
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 as StableIdLockfile);
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")) as StableIdLockfile;
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: { priorSchema?: WireSchema } | undefined,
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];