@githolon/dsl 0.105.4 → 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.
@@ -10,7 +10,7 @@
10
10
  * discovery), runs {@link checkDomainModules}, prints the unified report, and exits
11
11
  * non-zero on any REFUSE-level finding. `compile` runs the same twins implicitly.
12
12
  */
13
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
13
+ import { existsSync, readFileSync, statSync } from "node:fs";
14
14
  import path from "node:path";
15
15
  import { pathToFileURL } from "node:url";
16
16
 
@@ -22,12 +22,14 @@ import { LawSourceMap } from "./law_source_map.js";
22
22
  import {
23
23
  currentEvolveLaw,
24
24
  evolveDiff,
25
- evolveLawFromInterface,
26
25
  evolveLawFromLockfile,
27
26
  type EvolveLaw,
28
- type StableIdLockfile,
27
+ type CompilerIdentityModel,
29
28
  } from "./evolve_preview.js";
30
- import { parseStableIdLockfile } from "./stable_id_lockfile.js";
29
+ import {
30
+ NOMOS_IDENTITY_FILENAME,
31
+ parseIdentityUsda,
32
+ } from "./identity_usda.js";
31
33
  import {
32
34
  validateNomosPackageConfig,
33
35
  type NormalizedNomosPackageConfig,
@@ -136,77 +138,46 @@ export async function loadComposedModulesWithSources(
136
138
  }
137
139
 
138
140
  /**
139
- * Load the PRIOR law surface for `--against <path>` (the evolve preview's baseline). The supported
140
- * prior is a diagnostic `build/<name>.interface.json`, a BUILD DIR / package dir containing one, or
141
- * the committed `nomos.stable-ids.json` LOCKFILE (aggregate identity only — no directive payloads).
142
- * A live `https://…` prior is NOT supported (the installed package's stable-ids aren't retrievable
143
- * from GET /domains); point `--against` at the deployed source's interface and lock sidecars instead.
141
+ * Load the PRIOR identity surface for `--against <path>` (the evolve preview's baseline). The one
142
+ * supported artifact is the committed `nomos.identity.usda` layer, directly or via its package dir.
143
+ * There is no interface/JSON sidecar lane: prior executable-law inspection belongs to the holon.
144
144
  */
145
145
  function loadPriorEvolveLaw(arg: string): { law: EvolveLaw; source: string; note?: string } {
146
146
  if (/^https?:\/\//.test(arg)) {
147
147
  fail(
148
148
  `--against does not support a live URL (${arg}) — the installed package's #58 stable-ids are ` +
149
- `not retrievable from GET /domains. Point --against at the deployed source's build/<name>.interface.json.`,
149
+ `not retrievable from GET /domains. Point --against at the deployed source's ${NOMOS_IDENTITY_FILENAME}.`,
150
150
  );
151
151
  }
152
152
  const abs = path.resolve(process.cwd(), arg);
153
153
  if (!existsSync(abs)) fail(`--against: prior not found: ${abs}`);
154
- // Resolve a directory to its diagnostic interface or stable-id lockfile.
154
+ // Resolve a directory to its one committed identity layer.
155
155
  let file = abs;
156
156
  if (statSync(abs).isDirectory()) {
157
- const iface = findInterfaceJson(abs);
158
- const lock = path.join(abs, "nomos.stable-ids.json");
159
- if (iface !== undefined) file = iface;
160
- else if (existsSync(lock)) file = lock;
161
- else fail(`--against: no *.interface.json or nomos.stable-ids.json under ${abs}`);
157
+ const identity = path.join(abs, NOMOS_IDENTITY_FILENAME);
158
+ if (existsSync(identity)) file = identity;
159
+ else fail(`--against: no ${NOMOS_IDENTITY_FILENAME} under ${abs}`);
162
160
  }
163
- let parsed: unknown;
161
+ let lock: CompilerIdentityModel;
164
162
  try {
165
- parsed = JSON.parse(readFileSync(file, "utf8"));
163
+ lock = parseIdentityUsda(readFileSync(file, "utf8"));
166
164
  } catch (e) {
167
165
  fail(`--against: could not parse ${file}: ${(e as Error).message}`);
168
166
  }
169
- if (path.basename(file) === "nomos.stable-ids.json") {
170
- return {
171
- law: evolveLawFromLockfile(parseStableIdLockfile(parsed)),
172
- source: file,
173
- note: `prior is a lockfile — aggregate/field identity is diffed, but directive payload changes require the sibling interface sidecar.`,
174
- };
175
- }
176
- const lockCandidates = [
177
- path.join(path.dirname(file), "nomos.stable-ids.json"),
178
- path.join(path.dirname(path.dirname(file)), "nomos.stable-ids.json"),
179
- ];
180
- const lockFile = lockCandidates.find((candidate) => existsSync(candidate));
181
- const lock = lockFile === undefined
182
- ? undefined
183
- : parseStableIdLockfile(JSON.parse(readFileSync(lockFile, "utf8")));
184
167
  return {
185
- law: evolveLawFromInterface(parsed as Parameters<typeof evolveLawFromInterface>[0], lock),
186
- source: lockFile === undefined ? file : `${file} + ${lockFile}`,
187
- ...(lockFile === undefined
188
- ? { note: `prior has no stable-id lock beside it — directive shapes are checked, but driver and contract evolution evidence is incomplete.` }
189
- : {}),
190
- };
191
- }
192
-
193
- /** Find a compiler diagnostic `*.interface.json` directly or one level down in `build/`. */
194
- function findInterfaceJson(dir: string): string | undefined {
195
- const scan = (d: string): string | undefined => {
196
- if (!existsSync(d)) return undefined;
197
- const hit = readdirSync(d).find((f) => f.endsWith(".interface.json"));
198
- return hit !== undefined ? path.join(d, hit) : undefined;
168
+ law: evolveLawFromLockfile(lock),
169
+ source: file,
170
+ note: `prior identity is diffed by stable id; directive payload compatibility is negotiated from installed USDA by the holon, never a sidecar.`,
199
171
  };
200
- return scan(dir) ?? scan(path.join(dir, "build"));
201
172
  }
202
173
 
203
- /** Read the committed `nomos.stable-ids.json` lockfile beside the config (the CURRENT identity). */
204
- function loadCwdLockfile(configArg: string): StableIdLockfile | undefined {
174
+ /** Read the committed typed identity layer beside the config (the CURRENT identity). */
175
+ function loadCwdLockfile(configArg: string): CompilerIdentityModel | undefined {
205
176
  const cfgDir = path.dirname(path.resolve(process.cwd(), configArg));
206
- const lockPath = path.join(cfgDir, "nomos.stable-ids.json");
177
+ const lockPath = path.join(cfgDir, NOMOS_IDENTITY_FILENAME);
207
178
  if (!existsSync(lockPath)) return undefined;
208
179
  try {
209
- return parseStableIdLockfile(JSON.parse(readFileSync(lockPath, "utf8")));
180
+ return parseIdentityUsda(readFileSync(lockPath, "utf8"));
210
181
  } catch {
211
182
  return undefined;
212
183
  }
@@ -233,12 +204,12 @@ async function main(): Promise<void> {
233
204
  let evolveDispositions: Record<string, unknown> | undefined;
234
205
  if (againstIdx >= 0 || againstArg !== undefined) {
235
206
  if (againstArg === undefined || againstArg.startsWith("--")) {
236
- fail(`--against needs a path: --against <build/<name>.interface.json | build dir | nomos.stable-ids.json>`);
207
+ fail(`--against needs a path: --against <${NOMOS_IDENTITY_FILENAME} | package dir>`);
237
208
  }
238
209
  const lock = loadCwdLockfile(configArg);
239
210
  if (lock === undefined && !asJson) {
240
211
  console.log(
241
- "githolon check: --against — no nomos.stable-ids.json in the config dir; the current law's stable ids " +
212
+ `githolon check: --against — no ${NOMOS_IDENTITY_FILENAME} in the config dir; the current law's stable ids ` +
242
213
  "fall back to NAMES (a rename would read as removal+add). Run `githolon compile` first for a sid-accurate preview.",
243
214
  );
244
215
  }
package/src/codegen_ts.ts CHANGED
@@ -4078,6 +4078,13 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
4078
4078
  ` (holon, user) => _nomosBind${applicationName}(holon as NomosHolon, user),`,
4079
4079
  ` );`,
4080
4080
  `}`,
4081
+ ...(clientInterface === undefined
4082
+ ? []
4083
+ : [
4084
+ `// Compiler tooling compares the requirement carried by the client that will actually ship.`,
4085
+ `// A global symbol keeps this off tenant autocomplete while avoiding a duplicate sidecar.`,
4086
+ `Object.defineProperty(${applicationOpener}, Symbol.for("nomos.generated-interface"), { value: ${interfaceConstName(opts.packageName)}, enumerable: false });`,
4087
+ ]),
4081
4088
  ``,
4082
4089
  );
4083
4090
  }
@@ -5,17 +5,17 @@
5
5
  /**
6
6
  * `githolon compat <target> [--config …] [--domain …] [--json]` — THE CI GATE (task #31, the compat superpower).
7
7
  *
8
- * Compares the CURRENT build's structural interface (`build/<name>.interface.json`, the client's
9
- * REQUIREMENT) against a TARGET's installed law (the workspace OFFER), PER MEMBER, via the #72
8
+ * Compares the CURRENT generated client's carried structural REQUIREMENT against a TARGET's
9
+ * installed law (the workspace OFFER), PER MEMBER, via the #72
10
10
  * subtype relation (`memberVerdicts`). Exits NON-ZERO on any incompatible member — so a client/law
11
11
  * mismatch is caught in CI, before it ships. The target is:
12
- * • an `.interface.json` (or a build/package DIR containing one) — the diagnostic offer directly;
12
+ * • a generated `.client.mjs` (or a build/package DIR containing one);
13
13
  * • a LIVE workspace URL (`https://host/v2/workspaces/<ws>` or `https://host --workspace <ws>`) —
14
14
  * connect()s and reads the on-ledger offer (`holon.interfaceOffer()`).
15
15
  *
16
16
  * Human report by default; `--json` emits the machine surface (the per-member array + verdict).
17
17
  */
18
- import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
18
+ import { existsSync, readdirSync, statSync } from "node:fs";
19
19
  import path from "node:path";
20
20
 
21
21
  import { memberVerdicts, type DomainInterface, type MemberVerdict } from "./shape_compat.js";
@@ -41,7 +41,7 @@ function mergeInterfaces(byDomain: InterfaceMap | undefined): DomainInterface {
41
41
  return out;
42
42
  }
43
43
 
44
- /** The package name from `nomos.project.ts` (only field we need — to locate `build/<name>.interface.json`). */
44
+ /** The package name from `nomos.project.ts` (only field we need — to locate its generated client). */
45
45
  async function loadPackageName(configArg: string): Promise<{ name: string; dir: string }> {
46
46
  const configPath = path.resolve(process.cwd(), configArg);
47
47
  if (!existsSync(configPath)) fail(`config not found: ${configPath} (pass the package config, or run from the package dir)`);
@@ -51,45 +51,51 @@ async function loadPackageName(configArg: string): Promise<{ name: string; dir:
51
51
  return { name: cfg.name, dir: path.dirname(configPath) };
52
52
  }
53
53
 
54
- /** The CURRENT build's interface — `build/<name>.interface.json`. Requires a prior `githolon compile`. */
55
- function loadCurrentInterface(pkgDir: string, name: string): InterfaceMap {
56
- const p = path.join(pkgDir, "build", `${name}.interface.json`);
54
+ const GENERATED_INTERFACE = Symbol.for("nomos.generated-interface");
55
+
56
+ async function loadClientRequirement(file: string): Promise<InterfaceMap> {
57
+ const { pathToFileURL } = await import("node:url");
58
+ let generated: Record<string, unknown>;
59
+ try {
60
+ generated = await import(`${pathToFileURL(file).href}?compat=${Date.now()}`) as Record<string, unknown>;
61
+ } catch (error) {
62
+ fail(`could not load generated client ${file}: ${(error as Error).message}`);
63
+ }
64
+ for (const value of Object.values(generated)) {
65
+ if (typeof value !== "function") continue;
66
+ const requirement = (value as unknown as Record<PropertyKey, unknown>)[GENERATED_INTERFACE];
67
+ if (requirement !== null && typeof requirement === "object") return requirement as InterfaceMap;
68
+ }
69
+ fail(`generated client ${file} carries no Nomos structural requirement; recompile it with the current @githolon/dsl`);
70
+ }
71
+
72
+ /** The CURRENT requirement carried by the generated client that will ship. */
73
+ async function loadCurrentInterface(pkgDir: string, name: string): Promise<InterfaceMap> {
74
+ const p = path.join(pkgDir, "build", `${name}.client.mjs`);
57
75
  if (!existsSync(p)) {
58
76
  fail(
59
- `no current interface at ${p} — run \`githolon compile\` first so the build carries the structural interface this checks.`,
77
+ `no current generated client at ${p} — run \`githolon compile\` first.`,
60
78
  );
61
79
  }
62
- try {
63
- return JSON.parse(readFileSync(p, "utf8")) as InterfaceMap;
64
- } catch (e) {
65
- fail(`could not parse ${p}: ${(e as Error).message}`);
66
- }
80
+ return loadClientRequirement(p);
67
81
  }
68
82
 
69
- /** Read a TARGET diagnostic offer from an interface.json / directory. */
70
- function loadFileOffer(arg: string): { offer: InterfaceMap; source: string } {
83
+ /** Read a TARGET requirement from the generated client it will ship. */
84
+ async function loadFileOffer(arg: string): Promise<{ offer: InterfaceMap; source: string }> {
71
85
  const abs = path.resolve(process.cwd(), arg);
72
86
  if (!existsSync(abs)) fail(`target not found: ${abs}`);
73
87
  let file = abs;
74
88
  if (statSync(abs).isDirectory()) {
75
89
  const scan = (d: string): string | undefined => {
76
90
  if (!existsSync(d)) return undefined;
77
- const hit = readdirSync(d).find((f) => f.endsWith(".interface.json"));
91
+ const hit = readdirSync(d).find((f) => f.endsWith(".client.mjs"));
78
92
  return hit !== undefined ? path.join(d, hit) : undefined;
79
93
  };
80
94
  const found = scan(abs) ?? scan(path.join(abs, "build"));
81
- if (found === undefined) fail(`no *.interface.json under ${abs}`);
95
+ if (found === undefined) fail(`no *.client.mjs under ${abs}`);
82
96
  file = found;
83
97
  }
84
- let parsed: unknown;
85
- try {
86
- parsed = JSON.parse(readFileSync(file, "utf8"));
87
- } catch (e) {
88
- fail(`could not parse ${file}: ${(e as Error).message}`);
89
- }
90
- const offer = parsed as InterfaceMap;
91
- if (offer === null || typeof offer !== "object") fail(`target ${file} carries no structural interface`);
92
- return { offer, source: file };
98
+ return { offer: await loadClientRequirement(file), source: file };
93
99
  }
94
100
 
95
101
  /** Read a LIVE workspace's on-ledger offer through the compiler-private runtime bundled with the DSL. */
@@ -155,10 +161,10 @@ async function main(): Promise<void> {
155
161
  // The positional target = the first non-flag arg that isn't a flag VALUE.
156
162
  const flagsWithVals = new Set(["--config", "--workspace", "--domain"]);
157
163
  const target = argv.find((a, i) => !a.startsWith("--") && !(i > 0 && flagsWithVals.has(argv[i - 1]!)));
158
- if (!target) fail(`usage: githolon compat <workspace-url | interface.json | dir> [--config <cfg>] [--workspace <ws>] [--domain <name,...>] [--json]`);
164
+ if (!target) fail(`usage: githolon compat <workspace-url | client.mjs | dir> [--config <cfg>] [--workspace <ws>] [--domain <name,...>] [--json]`);
159
165
 
160
166
  const { name, dir } = await loadPackageName(configArg);
161
- const allCurrent = loadCurrentInterface(dir, name);
167
+ const allCurrent = await loadCurrentInterface(dir, name);
162
168
  const requestedDomains = domainFlag?.split(",").map((domain) => domain.trim()).filter(Boolean) ?? [];
163
169
  for (const domain of requestedDomains) {
164
170
  if (allCurrent[domain] === undefined) fail(`current package '${name}' has no domain '${domain}'`);
@@ -169,7 +175,7 @@ async function main(): Promise<void> {
169
175
 
170
176
  const { offer, source } = /^https?:\/\//.test(target)
171
177
  ? await loadLiveOffer(target, workspaceFlag)
172
- : loadFileOffer(target);
178
+ : await loadFileOffer(target);
173
179
 
174
180
  // The subtype check is PER MEMBER over the merged interfaces (directives keyed by id, aggregates by label).
175
181
  const verdicts = memberVerdicts(mergeInterfaces(current), mergeInterfaces(offer));
@@ -145,11 +145,14 @@ import {
145
145
  type StableIds,
146
146
  } from "./stable_ids.js";
147
147
  import {
148
- identityRegistryFromStableIdLockfile,
149
- parseStableIdLockfile,
150
- serializeStableIdLockfile,
151
- type StableIdLockfile,
152
- } from "./stable_id_lockfile.js";
148
+ identityRegistryFromModel,
149
+ type CompilerIdentityModel,
150
+ } from "./identity_model.js";
151
+ import {
152
+ NOMOS_IDENTITY_FILENAME,
153
+ parseIdentityUsda,
154
+ serializeIdentityUsda,
155
+ } from "./identity_usda.js";
153
156
  import { reconcileStableIdsWithIdentity } from "./stable_id_identity_bridge.js";
154
157
  import { discoverLawGraph, type LawGraph } from "./law_graph.js";
155
158
  import { emitFrameworkLawUsda, emitSparseLawGraphUsda } from "./law_graph_usda.js";
@@ -657,7 +660,7 @@ function firstLawDifference(expected: unknown, actual: unknown, at = "$law"): st
657
660
  // ── decl auto-discovery (by SHAPE, mirroring the engine entry's duck-types) ───────
658
661
  // The query/workspace-invariant/spatial/sum/count predicates + `discover` live in the SHARED
659
662
  // bundle-safe `decl_shapes.ts` (imported above) — the lump's self-describe verb discovers by the
660
- // EXACT same shape so the kernel-served OpenAPI matches the compiled `<name>.openapi.json`.
663
+ // EXACT same shape so the kernel-served OpenAPI is a deterministic projection of installed law.
661
664
 
662
665
  function isDerivedDecl(v: unknown): v is DerivedDecl {
663
666
  return (
@@ -836,6 +839,10 @@ const DART_SUPPORT_CORE_SRC = [
836
839
  // The custody-lifecycle types (NomosCustodyBreak/NomosCustodyStatus/CustodyBreakPolicy) — the generated
837
840
  // client + the vendored bridge reference them through the barrel, so they must vendor. Pure data.
838
841
  "custody.dart",
842
+ // THE LOCAL-WORK-REJECTED LIFECYCLE (seated_barrier_and_seal_obligation.md Part 4):
843
+ // NomosLocalWorkRejectedEvent/NomosRejectedIntent — the vendored bridge's localWorkRejectedEvents
844
+ // stream types ride through the barrel, so they must vendor. Pure data.
845
+ "local_work_rejected.dart",
839
846
  // SURFACED MERGE CONFLICTS: SurfacedValue<T>/SurfacedObservation<T> — a generated read model with a
840
847
  // `.merge(Surfaced(...))` field references them through the barrel, so they must vendor. Pure data.
841
848
  "surfaced.dart",
@@ -1586,8 +1593,8 @@ function emitDart(
1586
1593
  // The generated clients import their runtime support directly. Keeping it out of the public barrel is
1587
1594
  // the surface firewall: a tenant import receives business types and application APIs, never raw runtime.
1588
1595
  supportImport === "vendored"
1589
- ? `export '${genRel}/src/${VENDORED_TYPES_SHIM}' show NomosRef, NomosOptionalRef, VersionedNomosRef, ReferenceVersionStatus, ReferenceVersionCurrent, ReferenceVersionOutdated, ReferenceVersionUnavailable${sha256DigestExport}${gitObjectIdExport};`
1590
- : `export 'package:nomos_types/nomos_types.dart' show NomosRef, NomosOptionalRef, VersionedNomosRef, ReferenceVersionStatus, ReferenceVersionCurrent, ReferenceVersionOutdated, ReferenceVersionUnavailable${sha256DigestExport}${gitObjectIdExport};`,
1596
+ ? `export '${genRel}/src/${VENDORED_TYPES_SHIM}' show NomosRef, NomosOptionalRef, NomosSlot, VersionedNomosRef, ReferenceVersionStatus, ReferenceVersionCurrent, ReferenceVersionOutdated, ReferenceVersionUnavailable${sha256DigestExport}${gitObjectIdExport};`
1597
+ : `export 'package:nomos_types/nomos_types.dart' show NomosRef, NomosOptionalRef, NomosSlot, VersionedNomosRef, ReferenceVersionStatus, ReferenceVersionCurrent, ReferenceVersionOutdated, ReferenceVersionUnavailable${sha256DigestExport}${gitObjectIdExport};`,
1591
1598
  ...(contractsImportUri !== undefined
1592
1599
  ? tenantDartExport(`${genRel}/${CONTRACTS_FILE}.dart`, sourceByFile.get(CONTRACTS_FILE) ?? "")
1593
1600
  : []),
@@ -1649,12 +1656,12 @@ const dartTypesImportFor = (generatedSource: string): string => {
1649
1656
  * just the floor generated code needs — a lagging pin forces tenants into dependency_overrides. */
1650
1657
  // 0.3.0: SurfacedValue<T>/SurfacedObservation/PendingConflictRow — generated Surfaced-field typing +
1651
1658
  // the pendingConflicts read rows.
1652
- const NOMOS_TYPES_DEP = "^0.8.5";
1659
+ const NOMOS_TYPES_DEP = "^0.8.7";
1653
1660
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
1654
1661
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
1655
1662
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
1656
- const NOMOS_CLIENT_DEP = "^0.64.19";
1657
- const NOMOS_FLUTTER_DEP = "^0.63.30";
1663
+ const NOMOS_CLIENT_DEP = "^0.64.23";
1664
+ const NOMOS_FLUTTER_DEP = "^0.63.44";
1658
1665
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
1659
1666
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
1660
1667
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -2399,7 +2406,7 @@ function emitDartPackages(
2399
2406
  businessSharedPath,
2400
2407
  [
2401
2408
  "// GENERATED by nomos-compile — shared business values and activity; no runtime machinery.",
2402
- `export 'package:nomos_types/nomos_types.dart' show NomosRef, NomosOptionalRef, VersionedNomosRef, ReferenceVersionStatus, ReferenceVersionCurrent, ReferenceVersionOutdated, ReferenceVersionUnavailable${sha256DigestExport}${gitObjectIdExport}, Identity, User, Recipient, Invitation, ActivityEvent, ActivityRef, ActivitySeverity, SurfacedObservation, SurfacedValue, SurfacedResolved, SurfacedPending, SurfacedEvolutionRequired;`,
2409
+ `export 'package:nomos_types/nomos_types.dart' show NomosRef, NomosOptionalRef, NomosSlot, VersionedNomosRef, ReferenceVersionStatus, ReferenceVersionCurrent, ReferenceVersionOutdated, ReferenceVersionUnavailable${sha256DigestExport}${gitObjectIdExport}, Identity, User, Recipient, Invitation, ActivityEvent, ActivityRef, ActivitySeverity, SurfacedObservation, SurfacedValue, SurfacedResolved, SurfacedPending, SurfacedEvolutionRequired;`,
2403
2410
  ...(sharedBusinessTypeNames.length === 0
2404
2411
  ? []
2405
2412
  : [`export '${FRAMEWORK_FILE}.dart' show ${sharedBusinessTypeNames.join(", ")};`]),
@@ -3017,6 +3024,8 @@ async function main(): Promise<void> {
3017
3024
  `${cfg.name}.layered.usda`,
3018
3025
  `${cfg.name}.law-closure.json`,
3019
3026
  `${cfg.name}.deploy.json`,
3027
+ `${cfg.name}.interface.json`,
3028
+ `${cfg.name}.openapi.json`,
3020
3029
  ]) rmSync(path.join(outDir, retired), { force: true });
3021
3030
  for (const retiredDir of ["layers", "v3-layers", `${cfg.name}.relation-layers`]) {
3022
3031
  rmSync(path.join(outDir, retiredDir), { recursive: true, force: true });
@@ -3065,7 +3074,7 @@ async function main(): Promise<void> {
3065
3074
  // lump's `globalThis.describe` needs that it cannot rediscover by shape alone (mins/maxes are
3066
3075
  // config-named; sharding sums are framework-appended). Plain JSON (finishCount/Sum/Extremum
3067
3076
  // pass finished decls through), serialized into the generated entry as `openapiDecls`, so the
3068
- // kernel-run describe reproduces THIS compile's `<name>.openapi.json` byte-for-byte.
3077
+ // kernel-run describe reproduces this compile's public description deterministically.
3069
3078
  const openapiDeclsByDomain: Record<string, unknown> = {};
3070
3079
  // A tenant module may import declarations from another configured law module (business-birth
3071
3080
  // composition is the common case). Ordinary ES evaluation then initializes that entire closure
@@ -3734,25 +3743,25 @@ async function main(): Promise<void> {
3734
3743
 
3735
3744
  // ── STABLE-ID CONTINUITY (#58 — names are labels, identity is minted) ──
3736
3745
  // Derive each composed domain's stable ids from the PRIOR compile: the committed
3737
- // `nomos.stable-ids.json` lockfile beside the config (the durable source — survives
3746
+ // `nomos.identity.usda` layer beside the config (the durable source — survives
3738
3747
  // a clean clone), else the previous build's identity manifests. Match by name;
3739
3748
  // exactly ONE disappeared + ONE appeared name of the same kind/shape is an inferred
3740
3749
  // RENAME (sid carried, lineage recorded, printed below); anything else mints fresh
3741
3750
  // at first appearance (a leftover disappearance is THE EVOLVE GATE's typed question
3742
3751
  // at deploy — answered in the upgrade intent's disposition). The resolved continuity
3743
3752
  // is attached to the module so the canonical manifest + USD-IR carry it.
3744
- const lockPath = path.join(cfgDir, "nomos.stable-ids.json");
3745
- const priorLock: StableIdLockfile | undefined = existsSync(lockPath)
3746
- ? parseStableIdLockfile(JSON.parse(readFileSync(lockPath, "utf8")))
3753
+ const lockPath = path.join(cfgDir, NOMOS_IDENTITY_FILENAME);
3754
+ const priorLock: CompilerIdentityModel | undefined = existsSync(lockPath)
3755
+ ? parseIdentityUsda(readFileSync(lockPath, "utf8"))
3747
3756
  : undefined;
3748
3757
  const inferredRenames: { domain: string; renames: InferredRename[] }[] = [];
3749
- let nextIdentity = identityRegistryFromStableIdLockfile(priorLock);
3758
+ let nextIdentity = identityRegistryFromModel(priorLock);
3750
3759
  const typedLawGraphs = new Map<string, LawGraph>();
3751
3760
  // The lock is project-wide, not package-local. Several package configs commonly live beside one
3752
3761
  // application stage; compiling one must not erase the stable identity of domains owned by its siblings.
3753
3762
  // Current package domains are replaced below, while absent domains remain only as continuity metadata
3754
3763
  // and never leak into this package's interface, law, clients or deployment.
3755
- const nextLock: StableIdLockfile = {
3764
+ const nextLock: CompilerIdentityModel = {
3756
3765
  v: 1,
3757
3766
  domains: { ...(priorLock?.domains ?? {}) },
3758
3767
  identity: nextIdentity,
@@ -3786,7 +3795,7 @@ async function main(): Promise<void> {
3786
3795
  // it carries no stable ids either.
3787
3796
  continue;
3788
3797
  }
3789
- // Continuity source: the committed nomos.stable-ids.json lockfile (the canonical identity ledger).
3798
+ // Continuity source: the committed nomos.identity.usda layer (the canonical identity ledger).
3790
3799
  // (The old "else the prior build's domain_identity_manifests.json" fallback is gone with that sidecar.)
3791
3800
  let prior: StableIds | undefined;
3792
3801
  let priorSchemas: Record<string, WireSchema> | undefined;
@@ -3882,11 +3891,11 @@ async function main(): Promise<void> {
3882
3891
  schemas: Object.fromEntries(currentAggs.map((a) => [a.id, a.schema])),
3883
3892
  };
3884
3893
  }
3885
- // Persist the lockfile (commit it with the law: it is how a rename keeps its
3894
+ // Persist the typed identity layer (commit it with the law: it is how a rename keeps its
3886
3895
  // identity across clean clones). Existing project-domain order is retained, so once sibling
3887
3896
  // packages are registered, recompiling them in any order rewrites the same bytes without a
3888
3897
  // giant formatting-only diff in an established lock.
3889
- writeFileSync(lockPath, serializeStableIdLockfile(nextLock), "utf8");
3898
+ writeFileSync(lockPath, serializeIdentityUsda(nextLock), "utf8");
3890
3899
 
3891
3900
  // ── THE STRUCTURAL INTERFACE (#72) — directives (payload shapes) + aggregates (read
3892
3901
  // shapes, sid-keyed), keyed by DOMAIN. The SAME object is (a) baked into the generated
@@ -3934,11 +3943,12 @@ async function main(): Promise<void> {
3934
3943
  }
3935
3944
  : di;
3936
3945
  }
3937
- writeFileSync(
3938
- path.join(outDir, `${cfg.name}.interface.json`),
3939
- JSON.stringify(packageInterface, null, 2) + "\n",
3940
- "utf8",
3941
- );
3946
+ // NO INTERFACE SIDECAR. The structural interface is an in-memory codegen model only: it is baked
3947
+ // INTO the generated clients where compiler tooling can inspect the requirement that will ship.
3948
+ // Emitting it as JSON created a second representation of law that DRIFTED: the prior sidecar
3949
+ // omitted `principal` and `bornAt`, two fields the USDA declares
3950
+ // `nomos:required = true`, so anything reading it saw a contract the kernel would refuse.
3951
+ // One fact, one representation: the typed USDA closure is the contract.
3942
3952
 
3943
3953
  // ── RECURSIVE-PAYLOAD NOTES (z.lazy) — a self-referencing tree/JSON payload field is FULLY
3944
3954
  // supported: each codegen layer mints a named recursive type for it (TS `type <Name>`, Dart
@@ -5111,16 +5121,11 @@ async function main(): Promise<void> {
5111
5121
  requiresKernel,
5112
5122
  };
5113
5123
 
5114
- // ── 3b. public OpenAPI diagnostic/codegen projection. This JSON document is not law, install input,
5115
- // a runtime route table, or host authority. Kernel execution and current-law description derive from
5116
- // the installed typed USDA stage; this projection exists only for Swagger, client generators and agent
5117
- // tooling that consume OpenAPI.
5118
- const publicOpenapi = mergeOpenApiFragments(
5119
- presentationDomainModules.map((m) => generateOpenApiFragment(m, { lawHash: domainHash })),
5120
- { title: `${cfg.name} (nomos package)`, lawHash: domainHash },
5121
- );
5122
- const openapiPath = path.join(outDir, `${cfg.name}.openapi.json`);
5123
- writeFileSync(openapiPath, JSON.stringify(publicOpenapi, null, 2) + "\n", "utf8");
5124
+ // ── 3b. NO OPENAPI SIDECAR. OpenAPI remains the sanctioned standard for describing this package's
5125
+ // API — but it is SERVED, not stored: the kernel derives it from the installed typed USDA IR
5126
+ // (`GET /v2/workspaces/<ws>/openapi.json`), so the description cannot drift from the law it
5127
+ // describes. Writing a build-time copy created exactly the drift the representation rule forbids
5128
+ // — a derived second copy a reader could believe. Ask the holon that runs the law.
5124
5129
 
5125
5130
  // ── 4. human build summary (diagnostic only; never deployment input) ──
5126
5131
  const summary = {
@@ -5882,7 +5887,7 @@ async function main(): Promise<void> {
5882
5887
  // STABLE IDS (#58): the minted/carried identity surface + every INFERRED rename —
5883
5888
  // the silent lane made visible (if an inference is wrong — you meant remove+add —
5884
5889
  // split the edit into two compiles, or answer the evolve gate with a disposition).
5885
- console.log(` stable-ids ${rel(lockPath)} (committed lockfile — identity across renames)`);
5890
+ console.log(` identity ${rel(lockPath)} (committed typed USDA — identity across renames and retirements)`);
5886
5891
  for (const { domain, renames } of inferredRenames) {
5887
5892
  for (const r of renames) {
5888
5893
  console.log(
@@ -5899,7 +5904,7 @@ async function main(): Promise<void> {
5899
5904
  }
5900
5905
  console.log(` client ${rel(clientPath)} (typed TS client — payloads, read models, query accessors)`);
5901
5906
  for (const note of recursiveNotes) console.log(` note ${note}`);
5902
- console.log(` openapi ${rel(openapiPath)} (${Object.keys(publicOpenapi.paths).length} operation(s) — public diagnostic sidecar; runtime self-description comes from installed USDA)`);
5907
+ console.log(` openapi served by the holon from installed USDA (GET /v2/workspaces/<ws>/openapi.json) — no build-time copy to drift`);
5903
5908
  if (proofPath !== undefined) {
5904
5909
  console.log(` proof ${rel(proofPath)} (a runnable e2e GENERATED from your law — run: githolon proof)`);
5905
5910
  } else {
@@ -10,7 +10,7 @@
10
10
  * Why shared: the kernel-served OpenAPI (G1 — openapi residue removal) is produced by the lump
11
11
  * running `generateOpenApiFragment` over the SAME modules it dispatches; if the lump discovered
12
12
  * queries/counts/sums/spatials by a DIFFERENT shape than the compiler did when it wrote
13
- * `<name>.openapi.json`, the kernel-served spec would drift from the compiled one. One predicate
13
+ * the kernel-served OpenAPI, runtime description would drift from installed law. One predicate
14
14
  * set ⇒ one shape ⇒ byte-identity.
15
15
  *
16
16
  * ENGINE-BUNDLE-SAFE: imports NO node builtin (only DSL types) — it bundles under esbuild
@@ -175,7 +175,7 @@ export interface EngineEntryConfig {
175
175
  readonly openapiDocument?: OpenApiDoc;
176
176
  /**
177
177
  * The package's OpenAPI document title (the compiler's `"<name> (nomos package)"`). Baked into
178
- * the lump so `describe` reproduces the compiler's `<name>.openapi.json` byte-for-byte without
178
+ * the lump so `describe` reproduces the compiler's public description byte-for-byte without
179
179
  * the kernel knowing the package name. The kernel MAY override it (the workspace-level title).
180
180
  */
181
181
  readonly packageTitle?: string;
@@ -1075,7 +1075,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
1075
1075
  * dispatches. The kernel selects the content-addressed describe plan from the native USD
1076
1076
  * package in custody, so the workspace's API surface is a pure,
1077
1077
  * deterministic function of installed law — NOT a host-staged blob. Its public `paths` reproduce
1078
- * the `<name>.openapi.json` the compiler wrote; describe also carries a private, law-derived
1078
+ * the public description the compiler derived; describe also carries a private, law-derived
1079
1079
  * framework routing manifest which the host consumes internally and never publishes.
1080
1080
  * `opts.lawHash`/`opts.title` are injected by the kernel at describe-time (the version stamp +
1081
1081
  * package title), matching the compiler's `domainHash`/`"<name> (nomos package)"`.
@@ -26,7 +26,7 @@
26
26
  * law get typed 422s until regenerated (directives are not sid-tracked).
27
27
  *
28
28
  * The preview NEVER needs to emit a `rebinds` fragment: a rename is detected by a MATCHED sid
29
- * (the compiler carries it via the nomos.stable-ids.json lockfile), so it is already a silent
29
+ * (the compiler carries it via the nomos.identity.usda lockfile), so it is already a silent
30
30
  * admit — rebinds is only for the hand-repair path where the sid was lost, which this preview,
31
31
  * reading the carried sids, never produces.
32
32
  */
@@ -34,8 +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
+ import type { CompilerIdentityModel } from "./identity_model.js";
38
+ export type { CompilerIdentityModel } from "./identity_model.js";
39
39
  import type { DomainModule } from "./codegen_dart.js";
40
40
  import type { WireSchema } from "./wire.js";
41
41
 
@@ -137,7 +137,7 @@ function evolveDomainFromInterface(iface: DomainInterface): EvolveDomain {
137
137
  * by the kernel; `.law.usdz` is only its lossless one-file transport envelope. */
138
138
  export function evolveLawFromInterface(
139
139
  ifaces: Record<string, DomainInterface>,
140
- lock?: StableIdLockfile,
140
+ lock?: CompilerIdentityModel,
141
141
  ): EvolveLaw {
142
142
  const law: EvolveLaw = {};
143
143
  for (const [key, iface] of Object.entries(ifaces)) {
@@ -180,10 +180,10 @@ export function evolveLawFromInterface(
180
180
  return law;
181
181
  }
182
182
 
183
- /** Build an EvolveLaw from a `nomos.stable-ids.json` lockfile — aggregate identity + drivers, but
183
+ /** Build an EvolveLaw from a `nomos.identity.usda` lockfile — aggregate identity + drivers, but
184
184
  * NO directive payloads (a lockfile carries no interface). Directive-level warnings are then
185
185
  * unavailable; the caller notes it. */
186
- export function evolveLawFromLockfile(lock: StableIdLockfile): EvolveLaw {
186
+ export function evolveLawFromLockfile(lock: CompilerIdentityModel): EvolveLaw {
187
187
  const law: EvolveLaw = {};
188
188
  for (const [key, d] of Object.entries(lock.domains ?? {})) {
189
189
  law[key] = {
@@ -201,7 +201,7 @@ export function evolveLawFromLockfile(lock: StableIdLockfile): EvolveLaw {
201
201
  * pre-#58 fallback) — a rename then reads as removal+add; the caller warns to compile first. */
202
202
  export function currentEvolveLaw(
203
203
  modules: readonly DomainModule[],
204
- lock?: StableIdLockfile,
204
+ lock?: CompilerIdentityModel,
205
205
  ): EvolveLaw {
206
206
  const law: EvolveLaw = {};
207
207
  for (const mod of modules) {