@githolon/dsl 0.119.13 → 0.119.29

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.
@@ -9,7 +9,7 @@ description: >-
9
9
  Pure kernel wire/data types for Nomos Dart frontends — no transport, no bridge,
10
10
  no runtime. Typed protobuf and USDA boundary values plus the offer codec,
11
11
  reactive read engine, schema validation, and data-only error/trace types.
12
- version: 0.15.3
12
+ version: 0.16.0
13
13
 
14
14
  environment:
15
15
  sdk: ^3.5.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.119.13",
3
+ "version": "0.119.29",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
@@ -82,16 +82,16 @@
82
82
  "test": "vitest run"
83
83
  },
84
84
  "dependencies": {
85
- "@githolon/runtime": "^0.119.13",
86
- "@bufbuild/protobuf": "^2.13.0",
87
- "@bufbuild/protoc-gen-es": "^2.13.0",
85
+ "@githolon/runtime": "0.119.29",
86
+ "@bufbuild/protobuf": "2.13.0",
87
+ "@bufbuild/protoc-gen-es": "2.13.0",
88
88
  "esbuild": "0.24.2",
89
89
  "fflate": "0.8.2",
90
- "tsx": "^4.19.2",
91
- "typescript": "^5.6.3",
90
+ "tsx": "4.22.4",
91
+ "typescript": "5.9.3",
92
92
  "vscode-languageserver": "^9.0.1",
93
93
  "vscode-languageserver-textdocument": "^1.0.12",
94
- "zod": "^4.4.3"
94
+ "zod": "4.4.3"
95
95
  },
96
96
  "devDependencies": {
97
97
  "@types/node": "^25.9.2",
@@ -1,13 +1,15 @@
1
- // NOMOS — Structural interning of anonymous value types: one semantic shape, one canonical node.
1
+ // NOMOS — Structural interning: one semantic shape AND wire contract, one canonical node.
2
2
  //
3
3
  // Anonymous value types are structural values, not independently lived declarations. The graph
4
4
  // deliberately retains their source owner for diagnostics, but USDA must not turn that provenance
5
5
  // into thousands of duplicate runtime prims. This computes a semantic key which excludes owner
6
- // identity, then points every equivalent type graph — and each of its member fields and union
7
- // variants — at one canonical node. Declared types remain untouched. Recursive graphs keep their
6
+ // identity, retaining member wire allocations, then points equivalent type graphs and their member
7
+ // fields and union variants at one canonical node. Declared types remain untouched. Recursive graphs keep their
8
8
  // owner-scoped identity for now: aliasing a cycle needs an explicit recursion-isomorphism proof,
9
9
  // whereas every acyclic case below is exact by construction.
10
10
  //
11
+ // Discovery may ignore wire allocations to find candidates for a first allocation (CAP-983).
12
+ // Final sharing always includes them: equal shapes with distinct released keys remain separate.
11
13
  // ONE interning, TWO consumers. The USDA emitter (law_graph_usda.ts) redirects duplicate prims to
12
14
  // the canonical prim; the graph discovery (law_graph.ts) ledgers a nested payload field's wire key
13
15
  // against the canonical member, because that is the key installed law serves and generated clients
@@ -23,6 +25,8 @@ export interface AnonymousValueInterningInput {
23
25
  readonly types: readonly LawAnonymousValueTypeNode[];
24
26
  readonly fields: readonly LawAnonymousValueFieldNode[];
25
27
  readonly variants: readonly LawAnonymousValueVariantNode[];
28
+ /** Discovery only: find shape equivalence before retaining transport allocations. */
29
+ readonly ignoreWireAllocations?: boolean;
26
30
  /** Whether this type is emitted as its own `Types/t_<sid>` prim (wrappers and framework scalars are not). */
27
31
  readonly ownsPrim: (type: LawAnonymousValueTypeNode) => boolean;
28
32
  }
@@ -108,11 +112,13 @@ export function internAnonymousValues(input: AnonymousValueInterningInput): Anon
108
112
  break;
109
113
  }
110
114
  case "record": {
111
- const members: Array<readonly [string, boolean, string]> = [];
115
+ const members: Array<readonly [string, boolean, string, (string | undefined)?, (readonly string[])?]> = [];
112
116
  for (const field of fieldsByOwner.get(id) ?? []) {
113
117
  const child = childKey(field.value.id);
114
118
  if (child === undefined) break;
115
- members.push([field.name, field.required, child]);
119
+ members.push(input.ignoreWireAllocations
120
+ ? [field.name, field.required, child]
121
+ : [field.name, field.required, child, field.entity.wireKey, [...(field.entity.wireAliases ?? [])].sort()]);
116
122
  }
117
123
  if (members.length === (fieldsByOwner.get(id)?.length ?? 0)) {
118
124
  members.sort(([left], [right]) => left.localeCompare(right));
@@ -122,7 +128,7 @@ export function internAnonymousValues(input: AnonymousValueInterningInput): Anon
122
128
  break;
123
129
  }
124
130
  case "sum": {
125
- const members: Array<readonly [string, string]> = [];
131
+ const members: Array<readonly [string, string, (string | undefined)?]> = [];
126
132
  for (const variant of variantsByOwner.get(id) ?? []) {
127
133
  const child = childKey(variant.value.id);
128
134
  if (child === undefined) break;
@@ -130,7 +136,8 @@ export function internAnonymousValues(input: AnonymousValueInterningInput): Anon
130
136
  // with the same carried value shapes but different arm labels cannot
131
137
  // share one USDA prim: authoring reconstructs the nominal oneof from
132
138
  // these labels before it decodes the generated Protobuf call.
133
- members.push([variant.label ?? variant.entity.label, child]);
139
+ members.push(input.ignoreWireAllocations ? [variant.label ?? variant.entity.label, child]
140
+ : [variant.label ?? variant.entity.label, child, variant.entity.wireKey]);
134
141
  }
135
142
  if (members.length === (variantsByOwner.get(id)?.length ?? 0)) {
136
143
  members.sort(([leftLabel, leftChild], [rightLabel, rightChild]) =>
@@ -23,6 +23,7 @@ import { readInspectionUnits } from "./release_inspection.js";
23
23
  import { lawUnitSetIdentityWith } from "./law_unit_set_identity.js";
24
24
  import {
25
25
  LAW_FRAMEWORK_PARENT,
26
+ createLawIdentityReconciler,
26
27
  lawEntityKey,
27
28
  validateLawIdentityRegistry,
28
29
  type LawEntityKind,
@@ -48,6 +49,7 @@ import {
48
49
  } from "./law_layer_prims.js";
49
50
  import {
50
51
  BASELINE_NO_RELEASE,
52
+ NOMOS_BASELINE_FORMAT,
51
53
  serializeBaselineUsda,
52
54
  baselineSha256,
53
55
  type BaselineSource,
@@ -284,13 +286,14 @@ function scanWireAllocations(
284
286
  intentPrim: UsdaPrim,
285
287
  prims: ReadonlyMap<string, UsdaPrim>,
286
288
  out: BaselineWireAllocation[],
289
+ valueRoot?: { owner: string; declaredTypes: ReadonlySet<string> },
287
290
  ): void {
288
291
  const intentSid = sidOf(intentPrim);
289
292
  if (intentSid === undefined) return;
290
- const intent = lawEntityKey("intent", intentSid);
293
+ const intent = valueRoot?.owner ?? lawEntityKey("intent", intentSid);
291
294
  const seen = new Set<string>();
292
295
  const allocate = (field: UsdaPrim, path: string): void => {
293
- const canonical = usdaString(field, "wireKey");
296
+ const canonical = usdaString(field, "wireKey") ?? (field.name.startsWith("v_") ? sidOf(field) : undefined);
294
297
  if (canonical === undefined) return;
295
298
  const aliases = usdaStringList(field, "wireAliases") ?? [];
296
299
  const coordinate = `${intent}\u0000${path}`;
@@ -299,7 +302,7 @@ function scanWireAllocations(
299
302
  out.push({ intent, field: path, protobuf: { canonical, ...(aliases.length > 0 ? { aliases } : {}) } });
300
303
  };
301
304
  const walkType = (typePath: string | undefined, path: string, visiting: ReadonlySet<string>): void => {
302
- if (typePath === undefined || visiting.has(typePath)) return;
305
+ if (typePath === undefined || visiting.has(typePath) || valueRoot?.declaredTypes.has(typePath)) return;
303
306
  const type = prims.get(typePath);
304
307
  if (type === undefined) return;
305
308
  const next = new Set(visiting).add(typePath);
@@ -317,7 +320,9 @@ function scanWireAllocations(
317
320
  const variants = usdaChild(type, "Variants")?.children ?? [];
318
321
  variants.forEach((variant, index) => {
319
322
  const label = usdaString(variant, "label") ?? String(index);
320
- walkType(variant.rels.get("type")?.[0], unionVariantPath(path, label), next);
323
+ const arm = unionVariantPath(path, label);
324
+ allocate(variant, arm);
325
+ walkType(variant.rels.get("type")?.[0], arm, next);
321
326
  });
322
327
  break;
323
328
  }
@@ -335,6 +340,25 @@ function scanWireAllocations(
335
340
  allocate(field, path);
336
341
  walkType(field.rels.get("type")?.[0], path, visiting);
337
342
  };
343
+ if (valueRoot !== undefined) {
344
+ if (valueRoot.owner.startsWith("f_")) walkType(intentPrim.rels.get("type")?.[0], "$", new Set());
345
+ else if (valueRoot.owner.startsWith("q_")) {
346
+ walkType(intentPrim.rels.get("returns")?.[0], "$returns", new Set());
347
+ const contract = prims.get(intentPrim.rels.get("contract")?.[0] ?? "");
348
+ for (const parameter of (contract === undefined ? [] : usdaChild(contract, "Parameters")?.children ?? [])) {
349
+ const name = usdaString(parameter, "name");
350
+ if (name !== undefined) walkType(parameter.rels.get("type")?.[0], nestedMemberPath("$parameters", name), new Set());
351
+ }
352
+ }
353
+ else for (const variant of usdaChild(intentPrim, "Variants")?.children ?? []) {
354
+ const label = usdaString(variant, "label");
355
+ if (label === undefined) throw new Error("baseline export: a value variant lacks its discriminator");
356
+ const arm = unionVariantPath("$", label);
357
+ allocate(variant, arm);
358
+ walkType(variant.rels.get("type")?.[0], arm, new Set());
359
+ }
360
+ return;
361
+ }
338
362
  for (const relation of ["accepts", "publicAccepts"]) {
339
363
  const contract = prims.get(intentPrim.rels.get(relation)?.[0] ?? "");
340
364
  if (contract === undefined) continue;
@@ -404,11 +428,45 @@ export function exportBaselineFromUnits(input: BaselineExportInput): BaselineExp
404
428
  }
405
429
  entities[key] = entity;
406
430
  }
407
- const identity: LawIdentityRegistry = {
431
+ let identity: LawIdentityRegistry = {
408
432
  v: 1,
409
433
  entities: Object.fromEntries(Object.entries(entities).sort(([l], [r]) => (l < r ? -1 : l > r ? 1 : 0))),
410
434
  };
411
435
  validateLawIdentityRegistry(identity);
436
+ // Recover read-value continuity from the bytes actually served, including pre-CAP-983 releases.
437
+ // Named declarations own their nested allocations. Anonymous representative IDs are not owners.
438
+ const declaredTypes = new Set([...prims].filter(([, prim]) => {
439
+ const sid = sidOf(prim);
440
+ return sid !== undefined && identity.entities[`t_${sid}`]?.kind === "type";
441
+ }).map(([path]) => path));
442
+ const valueAllocations: BaselineWireAllocation[] = [];
443
+ for (const [, prim] of prims) {
444
+ const sid = sidOf(prim);
445
+ if (sid === undefined) continue;
446
+ const owner = prim.name.startsWith("f_") ? `f_${sid}` : prim.name.startsWith("t_") ? `t_${sid}` : prim.name.startsWith("q_") ? `q_${sid}` : undefined;
447
+ if (owner === undefined || identity.entities[owner] === undefined) continue;
448
+ scanWireAllocations(prim, prims, valueAllocations, { owner, declaredTypes });
449
+ }
450
+ // Repeated framework declarations may expose the same coordinate in several layers.
451
+ // A baseline must report disagreement, not let preferred-first reconciliation hide it.
452
+ const releasedByCoordinate = new Map<string, BaselineWireAllocation>();
453
+ for (const allocation of valueAllocations) {
454
+ const coordinate = `${allocation.intent}\0${allocation.field}`;
455
+ const prior = releasedByCoordinate.get(coordinate);
456
+ if (prior !== undefined && prior.protobuf.canonical !== allocation.protobuf.canonical) {
457
+ throw new Error(`baseline export: conflicting released wire allocations for ${allocation.intent}.${allocation.field}: ${prior.protobuf.canonical}, ${allocation.protobuf.canonical}`);
458
+ }
459
+ const aliases = [...new Set([...(prior?.protobuf.aliases ?? []), ...(allocation.protobuf.aliases ?? [])])].sort();
460
+ releasedByCoordinate.set(coordinate, { ...allocation, protobuf: { canonical: allocation.protobuf.canonical,
461
+ ...(aliases.length === 0 ? {} : { aliases }),
462
+ } });
463
+ }
464
+ const reconciler = createLawIdentityReconciler(identity);
465
+ for (const allocation of releasedByCoordinate.values()) reconciler.reconcile({
466
+ kind: "value-field", parent: allocation.intent, labels: [allocation.field],
467
+ preferredOperationFieldWire: { [allocation.field]: { protobuf: allocation.protobuf } }, retireAbsent: false,
468
+ });
469
+ identity = reconciler.finish();
412
470
 
413
471
  const baselineBody: Omit<ReleasedBaseline, "format"> = {
414
472
  source: input.source,
@@ -424,7 +482,7 @@ export function exportBaselineFromUnits(input: BaselineExportInput): BaselineExp
424
482
  };
425
483
  const text = serializeBaselineUsda(baselineBody);
426
484
  return {
427
- baseline: { format: "nomos.baseline.v1", ...baselineBody },
485
+ baseline: { format: NOMOS_BASELINE_FORMAT, ...baselineBody },
428
486
  text,
429
487
  sha256: baselineSha256(text),
430
488
  nonLedgerPrims: scanned.nonLedger.value,
@@ -160,6 +160,15 @@ export function importBaseline(
160
160
  retirementDivergence.push(`${key} (${coordinateOf(releasedEntity)}): retired by the release, active in the ledger`);
161
161
  continue;
162
162
  }
163
+ if (releasedEntity.kind === "value-field") {
164
+ const expected = releasedEntity.operationFieldWire?.protobuf;
165
+ const actual = local.operationFieldWire?.protobuf;
166
+ if (expected === undefined || actual?.canonical !== expected.canonical ||
167
+ (expected.aliases ?? []).some(alias => !actual.aliases?.includes(alias))) {
168
+ throw new BaselineRefusal("baseline-wire-divergence", `nested value ${coordinateOf(releasedEntity)} disagrees with released wire custody`,
169
+ [`released ${expected?.canonical ?? "missing"}, ledger ${actual?.canonical ?? "missing"}`]);
170
+ }
171
+ }
163
172
  if (local.label === releasedEntity.label || local.priorLabels?.includes(releasedEntity.label) === true) {
164
173
  verified += 1;
165
174
  continue;
@@ -288,6 +297,14 @@ export function assertBaselineRetained(final: LawIdentityRegistry, baseline: Rel
288
297
  continue;
289
298
  }
290
299
  if (local.status !== "active") continue; // retired by a declared candidate-law retirement; the evolution gate judged it
300
+ if (releasedEntity.kind === "value-field") {
301
+ const expected = releasedEntity.operationFieldWire?.protobuf;
302
+ const actual = local.operationFieldWire?.protobuf;
303
+ if (expected === undefined || actual?.canonical !== expected.canonical ||
304
+ (expected.aliases ?? []).some(alias => !actual.aliases?.includes(alias))) {
305
+ details.push(`${coordinateOf(releasedEntity)}: released wire allocation changed`);
306
+ }
307
+ }
291
308
  const lineage = [releasedEntity.label, ...(releasedEntity.priorLabels ?? [])];
292
309
  const occupant = activeAt(final.entities, releasedEntity.kind, local.parent, lineage);
293
310
  if (occupant !== undefined && occupant[0] !== key) {
@@ -26,7 +26,7 @@ import {
26
26
  } from "./identity_usda.js";
27
27
  import type { LawOperationFieldWire } from "./law_identity.js";
28
28
 
29
- export const NOMOS_BASELINE_FORMAT = "nomos.baseline.v1";
29
+ export const NOMOS_BASELINE_FORMAT = "nomos.baseline.v2";
30
30
  /** The pinned baseline's conventional filename, beside `nomos.project.ts`. */
31
31
  export const NOMOS_BASELINE_FILENAME = "nomos.baseline.usda";
32
32
  /** The baseline's own "no release" marker, matching release_publication.ts `NO_PREDECESSOR`. */
@@ -142,7 +142,7 @@ function attr(prim: IdentityPrim, name: string): string {
142
142
  /** Read a baseline back. Refuses anything that is not this exact format. */
143
143
  export function parseBaselineUsda(text: string): ReleasedBaseline {
144
144
  if (!isBaselineUsda(text)) {
145
- throw new Error(`${NOMOS_BASELINE_FILENAME} is not ${NOMOS_BASELINE_FORMAT} — refusing to guess a baseline encoding`);
145
+ throw new Error(`${NOMOS_BASELINE_FILENAME} is not ${NOMOS_BASELINE_FORMAT} — re-export from the same released units to recover nested read wire allocations (CAP-983)`);
146
146
  }
147
147
  const roots = parseIdentityPrims(text, NOMOS_BASELINE_FILENAME);
148
148
  const root = roots.find((prim) => prim.name === "NomosBaseline");
@@ -97,7 +97,9 @@ export const TS_RESULT_USDA_PRELUDE = [
97
97
  `const _nmsUsdaJsonValue = (prim: _NmsUsdaPrim): unknown => {`,
98
98
  ` if (prim.typeName === "NomosNullValue") return null; if (prim.typeName === "NomosBoolValue") return _nmsUsdaBoolValue(prim);`,
99
99
  ` if (prim.typeName === "NomosIntValue" || prim.typeName === "NomosNumberValue") return _nmsUsdaNumberValue(prim); if (prim.typeName === "NomosStringValue" || prim.typeName === "NomosTokenValue") return _nmsUsdaStringValue(prim);`,
100
- ` if (prim.typeName === "NomosJsonValue") { const value = prim.children.find((child) => child.name === "Value"); if (!value) throw new Error("NomosJsonValue has no Value"); return _nmsUsdaJsonValue(value); }`,
100
+ // Nominal records (including nomosRef) wrap their JSON in Value, just like
101
+ // NomosJsonValue. Keep parity with the kernel and Dart's result decoder.
102
+ ` if (prim.typeName === "NomosJsonValue" || prim.typeName === "NomosRecordValue") { const value = prim.children.find((child) => child.name === "Value"); if (!value) throw new Error(prim.typeName + " has no Value"); return _nmsUsdaJsonValue(value); }`,
101
103
  ` if (prim.typeName === "NomosJsonArray") return prim.children.map(_nmsUsdaJsonValue);`,
102
104
  ` if (prim.typeName === "NomosJsonObject") { const out: Record<string, unknown> = {}; for (const member of prim.children) { const key = _nmsUsdaProperty(member, "nomos:key"); const value = member.children.find((child) => child.name === "Value"); if (typeof key !== "string" || !value) throw new Error("invalid NomosJsonMember"); out[key] = _nmsUsdaJsonValue(value); } return out; }`,
103
105
  ` throw new Error("unsupported nominal JSON prim " + prim.typeName);`,
@@ -1,3 +1,5 @@
1
+ import { assertCanonicalCompilerToolchain } from "./compiler_toolchain.js";
2
+ import { dartAnalysisRoots } from "./dart_analysis_roots.js";
1
3
  import { collectPackageSources, findBirthKeyWrites, formatBirthKeyWriteWarnings } from "./birth_key_rule.js";
2
4
  import type { Directive } from "./directive.js";
3
5
  import { usdaString } from "./usda_text.js";
@@ -2587,12 +2589,12 @@ const dartTypesImportFor = (generatedSource: string): string => {
2587
2589
  * just the floor generated code needs — a lagging pin forces tenants into dependency_overrides. */
2588
2590
  // 0.3.0: SurfacedValue<T>/SurfacedObservation/PendingConflictRow — generated Surfaced-field typing +
2589
2591
  // the pendingConflicts read rows.
2590
- const NOMOS_TYPES_DEP = "^0.15.3";
2592
+ const NOMOS_TYPES_DEP = "^0.16.0";
2591
2593
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
2592
2594
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
2593
2595
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
2594
- const NOMOS_CLIENT_DEP = "^0.70.13";
2595
- const NOMOS_FLUTTER_DEP = "^0.69.3";
2596
+ const NOMOS_CLIENT_DEP = "^0.70.28";
2597
+ const NOMOS_FLUTTER_DEP = "^0.69.19";
2596
2598
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in the Dart and Flutter
2597
2599
  // package pubspecs — a release without this bump fails the suite, so tenants never
2598
2600
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -4247,6 +4249,7 @@ async function assertOneDslInstance(lawDir: string): Promise<void> {
4247
4249
  }
4248
4250
 
4249
4251
  async function main(): Promise<void> {
4252
+ assertCanonicalCompilerToolchain(DSL_DIR);
4250
4253
  const argv = process.argv.slice(2);
4251
4254
  const supportedFlags = new Set([
4252
4255
  "--require-usdcat",
@@ -9156,9 +9159,9 @@ async function main(): Promise<void> {
9156
9159
  // the analyzer reports unresolved `package:` imports as errors and the gate can't tell a codegen bug
9157
9160
  // from an un-got package (the co2 "analyze skipped → false success" trap).
9158
9161
  const dOut: string = dartOut;
9159
- // Analyze exactly THE FILES THIS COMPILE EMITTED (`dartFiles`) — never the whole dir, which would
9160
- // sweep in the transactional `.nomos-bak` backup and any stale legacy-layout leftovers and judge the
9161
- // compile on files it did not write. First `dart pub get` each emitted PACKAGE (a dir up-chain with a
9162
+ // Analyze exactly THE FILES THIS COMPILE EMITTED (`dartFiles`). Compact directory roots only when
9163
+ // their Dart coverage is identical; never sweep in transactional `.nomos-bak` backups, tenant source
9164
+ // or stale legacy-layout leftovers. First `dart pub get` each emitted PACKAGE (a dir up-chain with a
9162
9165
  // pubspec) so fixed `package:` imports resolve.
9163
9166
  const emittedDart = dartFiles.filter((f) => f.endsWith(".dart"));
9164
9167
  const pkgDirOf = (file: string): string | undefined => {
@@ -9462,7 +9465,7 @@ async function main(): Promise<void> {
9462
9465
  if (!dartAnalysisCached) {
9463
9466
  execFileSync(
9464
9467
  "dart",
9465
- ["analyze", "--no-fatal-warnings", ...inPackage],
9468
+ ["analyze", "--no-fatal-warnings", ...dartAnalysisRoots(inPackage, pkgDirs)],
9466
9469
  { stdio: "pipe", encoding: "utf8" }
9467
9470
  );
9468
9471
  mkdirSync(proofDir, { recursive: true });
@@ -0,0 +1,35 @@
1
+ import { compilerPackageManifest } from "../compile_proof.mjs";
2
+ import { readFileSync } from "node:fs";
3
+ import { createRequire } from "node:module";
4
+ import { join } from "node:path";
5
+
6
+ /** These compiler dependencies emit or contribute bytes to immutable law units.
7
+ * Package-manager overrides must fail rather than silently author a different law.
8
+ * Tenant dependencies remain authored inputs; inspect the compiler's resolution. */
9
+ export function assertCanonicalCompilerToolchain(dslDir: string): void {
10
+ const manifest = join(dslDir, "package.json");
11
+ const dependencies = JSON.parse(readFileSync(manifest, "utf8")).dependencies as Record<string, string>;
12
+ const compiler = createRequire(manifest);
13
+ const check = (name: string, expected: string | undefined, from: NodeJS.Require): string => {
14
+ const prefix = `NMS-TOOLCHAIN-001: non-canonical compiler toolchain: ${name}`;
15
+ if (expected === undefined || !/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(expected)) {
16
+ throw new Error(`${prefix} must have an exact compiler dependency pin (got ${expected}).`);
17
+ }
18
+ let file: string;
19
+ try { file = compilerPackageManifest(name, from); }
20
+ catch { throw new Error(`${prefix} is missing; reinstall @githolon/dsl with its declared dependencies.`); }
21
+ const actual = JSON.parse(readFileSync(file, "utf8")).version;
22
+ if (actual !== expected) {
23
+ throw new Error(`${prefix} resolved ${actual}, expected ${expected}. Remove compiler dependency overrides and reinstall @githolon/dsl; a toolchain upgrade requires a new compiler release.`);
24
+ }
25
+ return file;
26
+ };
27
+ for (const name of ["@githolon/runtime", "@bufbuild/protobuf", "esbuild", "fflate", "tsx", "typescript", "zod"]) {
28
+ check(name, dependencies[name], compiler);
29
+ }
30
+ const version = dependencies["@bufbuild/protoc-gen-es"];
31
+ const generator = createRequire(check("@bufbuild/protoc-gen-es", version, compiler));
32
+ const plugin = createRequire(check("@bufbuild/protoplugin", version, generator));
33
+ check("@bufbuild/protobuf", dependencies["@bufbuild/protobuf"], generator);
34
+ check("@bufbuild/protobuf", dependencies["@bufbuild/protobuf"], plugin);
35
+ }
@@ -0,0 +1,71 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /** Compact analyzer roots without widening the set of generated Dart files judged.
5
+ * Each file root creates filesystem watches in the Dart analysis server. A layered
6
+ * package can have dozens of roots for the same directory; on macOS their FSEvents
7
+ * teardown can dominate analysis. A directory is equivalent only when every Dart
8
+ * file beneath it was emitted by this compile. Backups, tenant code and symlinks
9
+ * prevent compaction across that boundary.
10
+ */
11
+ export function dartAnalysisRoots(
12
+ files: readonly string[],
13
+ packageRoots: readonly string[],
14
+ ): string[] {
15
+ const owned = new Set(files.map((file) => path.resolve(file)));
16
+ const packages = packageRoots.map((root) => path.resolve(root)).sort((a, b) => b.length - a.length);
17
+ const directories = new Set<string>();
18
+ const explicit = new Set<string>();
19
+ const incompressible = new Set<string>();
20
+ const options = new Map<string, boolean>();
21
+ const hasAnalysisOptions = (directory: string): boolean => {
22
+ const held = options.get(directory);
23
+ if (held !== undefined) return held;
24
+ const parent = path.dirname(directory);
25
+ const found = existsSync(path.join(directory, "analysis_options.yaml")) ||
26
+ (parent !== directory && hasAnalysisOptions(parent));
27
+ options.set(directory, found);
28
+ return found;
29
+ };
30
+ for (const file of owned) {
31
+ const pkg = packages.find((root) => file.startsWith(`${root}${path.sep}`));
32
+ const relative = pkg === undefined ? undefined : path.relative(pkg, file).split(path.sep);
33
+ // Directory roots can apply analyzer exclusions differently from explicit files.
34
+ // Keep the original invocation for inherited/custom options and hidden/build paths.
35
+ if (hasAnalysisOptions(path.dirname(file)) || relative?.some((part) => part.startsWith(".") || part === "build")) {
36
+ incompressible.add(file);
37
+ }
38
+ // Never analyze the package root itself: only the emitted source subtrees.
39
+ if (pkg === undefined || relative!.length === 1) explicit.add(file);
40
+ else directories.add(path.join(pkg, relative![0]!));
41
+ }
42
+ const seen = new Set<string>();
43
+ const visit = (directory: string): { roots: string[]; foreign: boolean } => {
44
+ const roots: string[] = [];
45
+ let foreign = false;
46
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
47
+ const file = path.join(directory, entry.name);
48
+ if (entry.isSymbolicLink()) {
49
+ // Do not infer ownership of anything the analyzer may reach through a link.
50
+ foreign = true;
51
+ } else if (entry.isDirectory()) {
52
+ const child = visit(file);
53
+ roots.push(...child.roots);
54
+ foreign ||= child.foreign;
55
+ } else if (entry.isFile() && entry.name.endsWith(".dart")) {
56
+ if (owned.has(file)) {
57
+ seen.add(file);
58
+ roots.push(file);
59
+ foreign ||= incompressible.has(file);
60
+ } else foreign = true;
61
+ }
62
+ }
63
+ return { roots: roots.length > 0 && !foreign ? [directory] : roots, foreign };
64
+ };
65
+ for (const directory of directories) {
66
+ for (const root of visit(directory).roots) explicit.add(root);
67
+ }
68
+ // A missing file must remain an analyzer input, never silently lose coverage.
69
+ for (const file of owned) if (!seen.has(file)) explicit.add(file);
70
+ return [...explicit].sort();
71
+ }
package/src/intent.ts CHANGED
@@ -490,7 +490,9 @@ const dedupeBy = <T>(values: readonly T[], key: (value: T) => string): T[] => {
490
490
  const seen = new Set<string>();
491
491
  for (const value of values) {
492
492
  const k = key(value);
493
- if (seen.add(k)) out.push(value);
493
+ if (seen.has(k)) continue;
494
+ seen.add(k);
495
+ out.push(value);
494
496
  }
495
497
  return out;
496
498
  };
package/src/law_graph.ts CHANGED
@@ -33,7 +33,7 @@ import {
33
33
  import type { StableFieldEvolution, StableIds } from "./stable_ids_types.js";
34
34
  import { isBusinessIntent, publicOfferRoute } from "./intent.js";
35
35
  import { listElementPath, mapValuePath, nestedMemberPath, unionVariantPath } from "./wire_paths.js";
36
- import { anonymousTypeOwnsPrim, internAnonymousValues } from "./anonymous_value_interning.js";
36
+ import { retainValueWireAllocations } from "./value_wire_continuity.js";
37
37
  import {
38
38
  lowerLawValueType,
39
39
  type LawConstant,
@@ -1302,7 +1302,7 @@ export function discoverLawGraph(
1302
1302
  // (wire_paths.ts) in the intent's operation-field scope: the first allocation is still the
1303
1303
  // structural sid (unchanged law bytes for every existing package), and from then on the
1304
1304
  // ledger keeps it through named and structural generations.
1305
- const nestedByPath = new Map<string, LawAnonymousValueFieldNode>();
1305
+ const nestedByPath = new Map<string, LawAnonymousValueFieldNode | LawAnonymousValueVariantNode>();
1306
1306
  const pathsByNode = new Map<string, string[]>();
1307
1307
  const walkNested = (valueRef: LawValueTypeRef, path: string, visiting: ReadonlySet<string>): void => {
1308
1308
  if (visiting.has(valueRef.id)) return;
@@ -1331,7 +1331,9 @@ export function discoverLawGraph(
1331
1331
  valueNode.expression.variants.forEach((variantRef, index) => {
1332
1332
  const variant = provisionalById.get(variantRef.id);
1333
1333
  if (variant?.kind !== "structural" || variant.structuralKind !== "value-variant") return;
1334
- walkNested(variant.value, unionVariantPath(path, variant.label ?? String(index)), next);
1334
+ const arm = unionVariantPath(path, variant.label ?? String(index));
1335
+ nestedByPath.set(arm, variant);
1336
+ walkNested(variant.value, arm, next);
1335
1337
  });
1336
1338
  break;
1337
1339
  case "nullable":
@@ -2624,77 +2626,12 @@ export function discoverLawGraph(
2624
2626
  },
2625
2627
  }),
2626
2628
  };
2627
- // NESTED WIRE CONTINUITY, PHASE 2 (CAP-660). Every nested payload field is served on the wire key of
2628
- // the canonical interned member (the prim law_graph_usda.ts emits; the key codegen_call_proto.ts
2629
- // encodes). Its first allocation is exactly that member's structural sid — the key every existing
2630
- // package already serves, so law bytes do not move — and from then on the ledger keeps it under
2631
- // the field's path through named and structural generations. Paths that end up sharing one
2632
- // canonical member must agree on one allocation: two released keys for one served prim is an
2633
- // explicit evolution decision, never a compile's silent pick.
2634
- {
2635
- const valueNodes = [...anonymousValueNodes.values()];
2636
- const interning = internAnonymousValues({
2637
- types: valueNodes.filter((node): node is LawAnonymousValueTypeNode => node.structuralKind === "value-type"),
2638
- fields: valueNodes.filter((node): node is LawAnonymousValueFieldNode => node.structuralKind === "value-field"),
2639
- variants: valueNodes.filter((node): node is LawAnonymousValueVariantNode => node.structuralKind === "value-variant"),
2640
- ownsPrim: anonymousTypeOwnsPrim,
2641
- });
2642
- const canonicalFieldOf = (fieldId: string): string => interning.canonicalFieldIdById.get(fieldId) ?? fieldId;
2643
- const allocationByCanonical = new Map<string, { canonical: string; aliases: Set<string>; owners: string[] }>();
2644
- for (const [intentKey, paths] of nestedPathsByIntent) {
2645
- if (paths.size === 0) continue;
2646
- const intentLabel = registry.entities[intentKey]?.label ?? intentKey;
2647
- const preferredOperationFieldWire: Record<string, LawOperationFieldWire> = {};
2648
- for (const [path, fieldId] of paths) {
2649
- const canonical = anonymousValueNodes.get(canonicalFieldOf(fieldId));
2650
- if (canonical === undefined) fail(`directive '${intentLabel}': nested field '${path}' has no canonical value node`);
2651
- preferredOperationFieldWire[path] = { protobuf: { canonical: canonical.entity.sid } };
2652
- }
2653
- const ledger = identities.reconcile({
2654
- kind: "operation-field",
2655
- parent: intentKey,
2656
- labels: [...paths.keys()],
2657
- preferredOperationFieldWire,
2658
- retireAbsent: false,
2659
- });
2660
- registry = ledger.registry;
2661
- for (const [path, fieldId] of paths) {
2662
- const entity = registry.entities[ledger.resolved[path] ?? ""];
2663
- if (entity?.kind !== "operation-field" || entity.status !== "active" || entity.operationFieldWire === undefined) {
2664
- fail(`directive '${intentLabel}' has no active wire allocation for nested field '${path}'`);
2665
- }
2666
- const wire = entity.operationFieldWire.protobuf;
2667
- const canonicalId = canonicalFieldOf(fieldId);
2668
- const owner = `${intentLabel}.${path}`;
2669
- const prior = allocationByCanonical.get(canonicalId);
2670
- if (prior === undefined) {
2671
- allocationByCanonical.set(canonicalId, { canonical: wire.canonical, aliases: new Set(wire.aliases ?? []), owners: [owner] });
2672
- continue;
2673
- }
2674
- if (prior.canonical !== wire.canonical) {
2675
- fail(
2676
- `NMS-WIRE-001 nested fields ${[...prior.owners, owner].map((name) => `'${name}'`).join(" and ")} share one structural value ` +
2677
- `but hold different wire allocations (${prior.canonical}, ${wire.canonical}); ` +
2678
- "declare an evolution plan for the reallocation instead of letting a compile choose",
2679
- );
2680
- }
2681
- for (const alias of wire.aliases ?? []) prior.aliases.add(alias);
2682
- prior.owners.push(owner);
2683
- }
2684
- }
2685
- // Every node of an allocated equivalence class — the canonical member the law emits and each
2686
- // duplicate a generated client reads — carries the one allocation.
2687
- for (const node of valueNodes) {
2688
- if (node.structuralKind !== "value-field") continue;
2689
- const allocation = allocationByCanonical.get(canonicalFieldOf(node.entity.id));
2690
- if (allocation === undefined) continue;
2691
- const aliases = [...allocation.aliases].sort(compareText);
2692
- anonymousValueNodes.set(node.entity.id, {
2693
- ...node,
2694
- entity: { ...node.entity, wireKey: allocation.canonical, ...(aliases.length === 0 ? {} : { wireAliases: aliases }) },
2695
- });
2696
- }
2697
- }
2629
+ const retainedWire = retainValueWireAllocations({
2630
+ nodes: [...anonymousValueNodes.values()], fields: fieldNodes, types: typeNodes, reads: readContractNodes,
2631
+ operations: nestedPathsByIntent, identities, registry,
2632
+ });
2633
+ registry = retainedWire.registry;
2634
+ for (const node of retainedWire.nodes) anonymousValueNodes.set(node.entity.id, node);
2698
2635
  const nodes: DeclaredLawNode[] = [
2699
2636
  domainNode,
2700
2637
  ...aggregateNodes,
@@ -423,7 +423,7 @@ export function compileSparseLawGraphUsda(
423
423
  for (const type of anonymousTypes) resolveAnonymousTypePath(type.entity.id);
424
424
 
425
425
  // Anonymous value types are structural values, not independently lived declarations: one semantic
426
- // shape, one canonical prim (anonymous_value_interning.ts — the same interning the graph discovery
426
+ // shape and wire contract, one canonical prim (anonymous_value_interning.ts — the same interning the graph discovery
427
427
  // ledgers nested wire keys against). Declared types remain untouched.
428
428
  const interning = internAnonymousValues({
429
429
  types: anonymousTypes,
@@ -463,7 +463,7 @@ export function compileSparseLawGraphUsda(
463
463
  }
464
464
  for (const variant of anonymousVariants) {
465
465
  const owner = pathById.get(variant.owner.id);
466
- if (owner !== undefined) pathById.set(variant.entity.id, `${owner}/Variants/v_${variant.entity.sid}`);
466
+ if (owner !== undefined) pathById.set(variant.entity.id, `${owner}/Variants/v_${variant.entity.wireKey ?? variant.entity.sid}`);
467
467
  }
468
468
 
469
469
  // When two record/sum types share one canonical prim, their nested structural references must
@@ -1489,7 +1489,7 @@ function emitType(
1489
1489
  for (const [index, variant] of owned.entries()) {
1490
1490
  if (index > 0) body.push("");
1491
1491
  body.push(
1492
- `${pad} def Scope "v_${variant.entity.sid}"`,
1492
+ `${pad} def Scope "v_${variant.entity.wireKey ?? variant.entity.sid}"`,
1493
1493
  `${pad} {`,
1494
1494
  `${pad} custom uniform string nomos:label = ${quote(variant.label ?? variant.entity.label)}`,
1495
1495
  `${pad} custom rel nomos:type = <${requiredPath(variant.value.id, paths)}>`,
@@ -1637,7 +1637,7 @@ function emitAnonymousType(
1637
1637
  for (const [index, variant] of owned.entries()) {
1638
1638
  if (index > 0) body.push("");
1639
1639
  body.push(
1640
- `${pad} def Scope "v_${variant.entity.sid}"`,
1640
+ `${pad} def Scope "v_${variant.entity.wireKey ?? variant.entity.sid}"`,
1641
1641
  `${pad} {`,
1642
1642
  `${pad} custom uniform string nomos:label = ${quote(variant.label ?? variant.entity.label)}`,
1643
1643
  `${pad} custom rel nomos:type = <${requiredPath(variant.value.id, paths)}>`,