@githolon/dsl 0.105.5 → 0.105.6

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/src/lsp_server.ts CHANGED
@@ -20,7 +20,7 @@
20
20
  * Multi-root: each document resolves to its NEAREST `nomos.project.ts` (walk up), so a repo with
21
21
  * several packages diagnoses each against its own config.
22
22
  */
23
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
23
+ import { existsSync, readFileSync, statSync } from "node:fs";
24
24
  import path from "node:path";
25
25
  import { pathToFileURL, fileURLToPath } from "node:url";
26
26
  import { bindEnumTypeExports } from "./enum_type.js";
@@ -45,12 +45,14 @@ import { LawSourceMap } from "./law_source_map.js";
45
45
  import {
46
46
  currentEvolveLaw,
47
47
  evolveDiff,
48
- evolveLawFromInterface,
49
48
  evolveLawFromLockfile,
50
49
  type EvolveLaw,
51
- type StableIdLockfile,
50
+ type CompilerIdentityModel,
52
51
  } from "./evolve_preview.js";
53
- import { parseStableIdLockfile } from "./stable_id_lockfile.js";
52
+ import {
53
+ NOMOS_IDENTITY_FILENAME,
54
+ parseIdentityUsda,
55
+ } from "./identity_usda.js";
54
56
  import {
55
57
  validateNomosPackageConfig,
56
58
  type NormalizedNomosPackageConfig,
@@ -194,54 +196,29 @@ async function composePackage(
194
196
  return { modules: composed, domainFiles, dependencyFindings };
195
197
  }
196
198
 
197
- /** Load prior evolve diagnostics from an interface sidecar/build dir/lockfile. */
199
+ /** Load prior evolve diagnostics from the one committed typed identity layer. */
198
200
  function loadPriorEvolveLaw(arg: string, cfgDir: string): EvolveLaw | undefined {
199
201
  const abs = path.isAbsolute(arg) ? arg : path.resolve(cfgDir, arg);
200
202
  if (!existsSync(abs)) return undefined;
201
203
  let file = abs;
202
204
  if (statSync(abs).isDirectory()) {
203
- const buildDir = path.join(abs, "build");
204
- const scan = (d: string): string | undefined => {
205
- if (!existsSync(d)) return undefined;
206
- const hit = readdirSync(d).find((f) => f.endsWith(".interface.json"));
207
- return hit !== undefined ? path.join(d, hit) : undefined;
208
- };
209
- const iface = scan(abs) ?? scan(buildDir);
210
- const lock = path.join(abs, "nomos.stable-ids.json");
211
- if (iface !== undefined) file = iface;
212
- else if (existsSync(lock)) file = lock;
205
+ const identity = path.join(abs, NOMOS_IDENTITY_FILENAME);
206
+ if (existsSync(identity)) file = identity;
213
207
  else return undefined;
214
208
  }
215
- let parsed: unknown;
216
209
  try {
217
- parsed = JSON.parse(readFileSync(file, "utf8"));
210
+ return evolveLawFromLockfile(parseIdentityUsda(readFileSync(file, "utf8")));
218
211
  } catch {
219
212
  return undefined;
220
213
  }
221
- if (path.basename(file) === "nomos.stable-ids.json") {
222
- return evolveLawFromLockfile(parseStableIdLockfile(parsed));
223
- }
224
- const lockFile = [
225
- path.join(path.dirname(file), "nomos.stable-ids.json"),
226
- path.join(path.dirname(path.dirname(file)), "nomos.stable-ids.json"),
227
- ].find((candidate) => existsSync(candidate));
228
- let lock: StableIdLockfile | undefined;
229
- if (lockFile !== undefined) {
230
- try {
231
- lock = parseStableIdLockfile(JSON.parse(readFileSync(lockFile, "utf8")));
232
- } catch {
233
- return undefined;
234
- }
235
- }
236
- return evolveLawFromInterface(parsed as Parameters<typeof evolveLawFromInterface>[0], lock);
237
214
  }
238
215
 
239
- /** Read the committed `nomos.stable-ids.json` lockfile beside the config (the CURRENT identity). */
240
- function loadCwdLockfile(cfgDir: string): StableIdLockfile | undefined {
241
- const lockPath = path.join(cfgDir, "nomos.stable-ids.json");
216
+ /** Read the committed typed identity layer beside the config (the CURRENT identity). */
217
+ function loadCwdLockfile(cfgDir: string): CompilerIdentityModel | undefined {
218
+ const lockPath = path.join(cfgDir, NOMOS_IDENTITY_FILENAME);
242
219
  if (!existsSync(lockPath)) return undefined;
243
220
  try {
244
- return parseStableIdLockfile(JSON.parse(readFileSync(lockPath, "utf8")));
221
+ return parseIdentityUsda(readFileSync(lockPath, "utf8"));
245
222
  } catch {
246
223
  return undefined;
247
224
  }
package/src/stable_ids.ts CHANGED
@@ -33,7 +33,7 @@
33
33
  * package therefore publishes one identical field-contract identity for that shared row.
34
34
  *
35
35
  * CONTINUITY (across compiles): `nomos-compile` reads the PRIOR build's identity
36
- * manifests / the committed `nomos.stable-ids.json` lockfile and derives a continuity
36
+ * manifests / the committed `nomos.identity.usda` lockfile and derives a continuity
37
37
  * table per domain — match by name first; then the UNAMBIGUOUS-PAIR rule: exactly ONE
38
38
  * disappeared name + exactly ONE same-shaped appeared name is an inferred RENAME (the
39
39
  * sid is carried, the old label recorded in `was`, and the compile summary prints the
@@ -1,278 +0,0 @@
1
- import type { StableAggregateId, StableFieldId, StableIds } from "./stable_ids_types.js";
2
- import type { WireSchema } from "./wire.js";
3
- import { mintGlobalAggregateSid } from "./stable_ids.js";
4
- import {
5
- LAW_FRAMEWORK_PARENT,
6
- emptyLawIdentityRegistry,
7
- lawEntityKey,
8
- validateLawIdentityRegistry,
9
- type LawIdentityEntity,
10
- type LawIdentityRegistry,
11
- } from "./law_identity.js";
12
-
13
- export interface StableIdLockDomain {
14
- stableIds: StableIds;
15
- schemas?: Record<string, WireSchema>;
16
- }
17
-
18
- /** The materialized lock shape consumed inside the compiler and authoring tools. */
19
- export interface StableIdLockfile {
20
- v: 1;
21
- domains: Record<string, StableIdLockDomain>;
22
- /** W4 compiler identity custody. Semantics remain exclusively in USDA. */
23
- identity?: LawIdentityRegistry;
24
- }
25
-
26
- type CompactStableFieldId = Omit<StableFieldId, "contract" | "shape"> & {
27
- contract?: string | number;
28
- };
29
-
30
- interface CompactStableAggregateId extends Omit<StableAggregateId, "fields"> {
31
- fields: Record<string, CompactStableFieldId>;
32
- }
33
-
34
- interface CompactStableIds extends Omit<StableIds, "aggregates"> {
35
- aggregates: Record<string, CompactStableAggregateId>;
36
- }
37
-
38
- interface CompactStableIdLockfile {
39
- v: 2;
40
- /** [contract hash, canonical contract descriptor], addressed by field.contract. */
41
- contracts: readonly (readonly [string, unknown])[];
42
- domains: Record<string, { stableIds: CompactStableIds }>;
43
- }
44
-
45
- interface IdentityStableIdLockfile {
46
- v: 3;
47
- identity: LawIdentityRegistry;
48
- /** Temporary W4 compatibility evidence; deleted once evolution traverses prior USDA. */
49
- contracts: readonly (readonly [string, unknown])[];
50
- /** Temporary aggregate/field compatibility view consumed by the v2 compiler path. */
51
- domains: Record<string, { stableIds: CompactStableIds }>;
52
- }
53
-
54
- function isRecord(value: unknown): value is Record<string, unknown> {
55
- return typeof value === "object" && value !== null && !Array.isArray(value);
56
- }
57
-
58
- function sortedJson(value: unknown): unknown {
59
- if (Array.isArray(value)) return value.map(sortedJson);
60
- if (!isRecord(value)) return value;
61
- return Object.fromEntries(
62
- Object.keys(value)
63
- .sort()
64
- .map((key) => [key, sortedJson(value[key])]),
65
- );
66
- }
67
-
68
- /**
69
- * Read both the original verbose lock and the compact v2 wire shape. Callers always
70
- * receive the same fully materialized v1 object, so continuity and evolution logic
71
- * never needs to know how the durable file was encoded.
72
- */
73
- export function parseStableIdLockfile(value: unknown): StableIdLockfile {
74
- if (!isRecord(value)) throw new Error("stable-id lock must be a JSON object");
75
- if (value.v === 1) return value as unknown as StableIdLockfile;
76
- if (
77
- (value.v !== 2 && value.v !== 3) ||
78
- !Array.isArray(value.contracts) ||
79
- !isRecord(value.domains)
80
- ) {
81
- throw new Error("unsupported stable-id lock encoding");
82
- }
83
-
84
- let identity: LawIdentityRegistry | undefined;
85
- if (value.v === 3) {
86
- if (!isRecord(value.identity)) throw new Error("stable-id lock v3 identity is malformed");
87
- identity = value.identity as unknown as LawIdentityRegistry;
88
- validateLawIdentityRegistry(identity);
89
- }
90
-
91
- const compact = value as unknown as CompactStableIdLockfile | IdentityStableIdLockfile;
92
- const contracts = compact.contracts.map((entry, index) => {
93
- if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string") {
94
- throw new Error(`stable-id lock contract ${index} is malformed`);
95
- }
96
- return entry;
97
- });
98
- const domains: StableIdLockfile["domains"] = {};
99
-
100
- for (const [domainName, domain] of Object.entries(compact.domains)) {
101
- const aggregates: StableIds["aggregates"] = {};
102
- for (const [aggregateName, aggregate] of Object.entries(domain.stableIds.aggregates ?? {})) {
103
- const fields: StableAggregateId["fields"] = {};
104
- for (const [fieldName, field] of Object.entries(aggregate.fields ?? {})) {
105
- const contractRef = field.contract;
106
- const { contract: _contractRef, ...fieldWithoutContract } = field;
107
- if (typeof contractRef === "number") {
108
- const contract = contracts[contractRef];
109
- if (contract === undefined) {
110
- throw new Error(
111
- `stable-id lock field ${domainName}.${aggregateName}.${fieldName} references missing contract ${contractRef}`,
112
- );
113
- }
114
- fields[fieldName] = {
115
- ...fieldWithoutContract,
116
- contract: contract[0],
117
- shape: contract[1],
118
- };
119
- } else if (typeof contractRef === "string") {
120
- fields[fieldName] = { ...fieldWithoutContract, contract: contractRef };
121
- } else {
122
- fields[fieldName] = fieldWithoutContract;
123
- }
124
- }
125
- aggregates[aggregateName] = { ...aggregate, fields };
126
- }
127
- domains[domainName] = {
128
- stableIds: { ...domain.stableIds, aggregates },
129
- };
130
- }
131
-
132
- return { v: 1, domains, ...(identity === undefined ? {} : { identity }) };
133
- }
134
-
135
- /**
136
- * Encode the committed lock without repeating identical contract descriptors or
137
- * aggregate driver schemas. Fields already retain their canonical merge driver;
138
- * the contract table retains the full descriptor needed to prove safe widening.
139
- */
140
- export function serializeStableIdLockfile(lock: StableIdLockfile): string {
141
- if (lock.identity !== undefined) validateLawIdentityRegistry(lock.identity);
142
- const contracts: [string, unknown][] = [];
143
- const contractIndexes = new Map<string, number>();
144
- const domains: CompactStableIdLockfile["domains"] = {};
145
-
146
- for (const [domainName, domain] of Object.entries(lock.domains)) {
147
- const aggregates: CompactStableIds["aggregates"] = {};
148
- for (const [aggregateName, aggregate] of Object.entries(domain.stableIds.aggregates)) {
149
- const fields: CompactStableAggregateId["fields"] = {};
150
- for (const [fieldName, field] of Object.entries(aggregate.fields)) {
151
- const { shape, ...withoutShape } = field;
152
- if (field.contract !== undefined && shape !== undefined) {
153
- let index = contractIndexes.get(field.contract);
154
- if (index === undefined) {
155
- index = contracts.length;
156
- contracts.push([field.contract, shape]);
157
- contractIndexes.set(field.contract, index);
158
- }
159
- fields[fieldName] = { ...withoutShape, contract: index };
160
- } else {
161
- fields[fieldName] = withoutShape;
162
- }
163
- }
164
- aggregates[aggregateName] = { ...aggregate, fields };
165
- }
166
- domains[domainName] = {
167
- stableIds: { ...domain.stableIds, aggregates },
168
- };
169
- }
170
-
171
- // This is a machine-owned lockfile. Compact JSON is intentional: unlike generated
172
- // law and clients, it must be committed so identity survives a clean clone.
173
- const encoded =
174
- lock.identity === undefined
175
- ? ({ v: 2, contracts, domains } satisfies CompactStableIdLockfile)
176
- : ({ v: 3, identity: lock.identity, contracts, domains } satisfies IdentityStableIdLockfile);
177
- return JSON.stringify(sortedJson(encoded)) + "\n";
178
- }
179
-
180
- /**
181
- * One-time W4 import of the existing domain/aggregate/field SIDs. The legacy view has
182
- * already forgotten removals, so only currently present entities can be imported;
183
- * from this point onward the returned registry retains every retirement.
184
- */
185
- export function identityRegistryFromStableIdLockfile(
186
- lock: StableIdLockfile | undefined,
187
- ): LawIdentityRegistry {
188
- if (lock?.identity !== undefined) {
189
- validateLawIdentityRegistry(lock.identity);
190
- return lock.identity;
191
- }
192
- if (lock === undefined) return emptyLawIdentityRegistry();
193
-
194
- const entities: Record<string, LawIdentityEntity> = {};
195
- const aggregateOccurrences = new Map<string, number>();
196
- for (const domain of Object.values(lock.domains)) {
197
- for (const aggregate of Object.values(domain.stableIds.aggregates)) {
198
- aggregateOccurrences.set(aggregate.sid, (aggregateOccurrences.get(aggregate.sid) ?? 0) + 1);
199
- }
200
- }
201
-
202
- for (const [domainName, domain] of Object.entries(lock.domains).sort(([a], [b]) =>
203
- compareText(a, b),
204
- )) {
205
- const domainKey = importIdentityEntity(entities, {
206
- kind: "domain",
207
- sid: domain.stableIds.domain,
208
- label: domainName,
209
- generation: 0,
210
- status: "active",
211
- });
212
- for (const [aggregateName, aggregate] of Object.entries(domain.stableIds.aggregates).sort(
213
- ([a], [b]) => compareText(a, b),
214
- )) {
215
- const parent =
216
- (aggregateOccurrences.get(aggregate.sid) ?? 0) > 1 ||
217
- aggregate.sid === mintGlobalAggregateSid(aggregateName)
218
- ? LAW_FRAMEWORK_PARENT
219
- : domainKey;
220
- const aggregateKey = importIdentityEntity(entities, {
221
- kind: "aggregate",
222
- sid: aggregate.sid,
223
- parent,
224
- label: aggregateName,
225
- ...(aggregate.was === undefined ? {} : { priorLabels: aggregate.was }),
226
- generation: 0,
227
- status: "active",
228
- });
229
- for (const [fieldName, field] of Object.entries(aggregate.fields).sort(([a], [b]) =>
230
- compareText(a, b),
231
- )) {
232
- importIdentityEntity(entities, {
233
- kind: "field",
234
- sid: field.sid,
235
- parent: aggregateKey,
236
- label: fieldName,
237
- ...(field.was === undefined ? {} : { priorLabels: field.was }),
238
- generation: 0,
239
- status: "active",
240
- });
241
- }
242
- }
243
- }
244
-
245
- const registry: LawIdentityRegistry = {
246
- v: 1,
247
- entities: Object.fromEntries(Object.entries(entities).sort(([a], [b]) => compareText(a, b))),
248
- };
249
- validateLawIdentityRegistry(registry);
250
- return registry;
251
- }
252
-
253
- function importIdentityEntity(
254
- entities: Record<string, LawIdentityEntity>,
255
- entity: LawIdentityEntity,
256
- ): string {
257
- const key = lawEntityKey(entity.kind, entity.sid);
258
- const prior = entities[key];
259
- if (prior === undefined) {
260
- entities[key] = entity;
261
- return key;
262
- }
263
- if (
264
- prior.kind !== entity.kind ||
265
- prior.parent !== entity.parent ||
266
- prior.label !== entity.label ||
267
- JSON.stringify(prior.priorLabels ?? []) !== JSON.stringify(entity.priorLabels ?? [])
268
- ) {
269
- throw new Error(
270
- `NMS-ID-002 legacy SID ${key} aliases incompatible ${prior.kind} '${prior.label}' and ${entity.kind} '${entity.label}'`,
271
- );
272
- }
273
- return key;
274
- }
275
-
276
- function compareText(a: string, b: string): number {
277
- return a < b ? -1 : a > b ? 1 : 0;
278
- }