@githolon/dsl 0.102.1 → 0.102.2

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.
@@ -262,8 +262,9 @@ interface PackageConfig {
262
262
  * • `test` — optional; generated business scenarios for widget/integration tests.
263
263
  * The layering line: UI code depends on `types`; composition/runtime code on `client`;
264
264
  * generated TYPES may flow to the UI, generated CLIENTS may not.
265
- * Each package's `pubspec.yaml` is scaffolded ONLY IF MISSING (tenant-owned versioning);
266
- * barrels + `lib/src/generated/` are generated-owned and rewritten every compile.
265
+ * Each package's `pubspec.yaml` keeps tenant-owned versioning/publishing metadata and custom
266
+ * dependencies; Nomos-owned framework dependency floors are reconciled on every compile.
267
+ * Barrels + `lib/src/generated/` are generated-owned and rewritten every compile.
267
268
  * Requires `support: "package"` (vendored mode contradicts pub-package layering).
268
269
  */
269
270
  readonly packages?: {
@@ -1298,12 +1299,27 @@ function emitDart(
1298
1299
  // ── the LAYERED-PACKAGES Dart emit (dart.packages: blast-radius isolation) ────────
1299
1300
 
1300
1301
  /** The pure-core support import generated TYPES files carry in layered-packages mode. */
1301
- const DART_TYPES_IMPORT = [
1302
- `import 'package:nomos_types/nomos_types.dart';`,
1303
- `import 'package:nomos_types/src/nomos_ref.dart' show NmsInternalNomosRefs;`,
1304
- `import 'package:nomos_types/src/foreign_ref.dart' show NmsInternalForeignRefs;`,
1305
- `import 'package:nomos_types/src/invitation.dart' show NmsInternalInvitations, NmsInternalRecipients;`,
1306
- ].join("\n");
1302
+ const dartTypesImportFor = (generatedSource: string): string => {
1303
+ const lines = [
1304
+ `import 'package:nomos_types/nomos_types.dart';`,
1305
+ ...(generatedSource.includes("NmsInternalNomosRefs.")
1306
+ ? [`import 'package:nomos_types/src/nomos_ref.dart' show NmsInternalNomosRefs;`]
1307
+ : []),
1308
+ ...(generatedSource.includes("NmsInternalForeignRefs.")
1309
+ ? [`import 'package:nomos_types/src/foreign_ref.dart' show NmsInternalForeignRefs;`]
1310
+ : []),
1311
+ ];
1312
+ const invitationHelpers = [
1313
+ ...(generatedSource.includes("NmsInternalInvitations.") ? ["NmsInternalInvitations"] : []),
1314
+ ...(generatedSource.includes("NmsInternalRecipients.") ? ["NmsInternalRecipients"] : []),
1315
+ ];
1316
+ if (invitationHelpers.length > 0) {
1317
+ lines.push(
1318
+ `import 'package:nomos_types/src/invitation.dart' show ${invitationHelpers.join(", ")};`,
1319
+ );
1320
+ }
1321
+ return lines.join("\n");
1322
+ };
1307
1323
  /** Version constraints the scaffolded pubspecs pin — move together with the Dart packages' releases.
1308
1324
  * Caret on 0.x is EXACT-MINOR (`^0.45.0` = `<0.46.0`), so these must track the CURRENT release, not
1309
1325
  * just the floor generated code needs — a lagging pin forces tenants into dependency_overrides. */
@@ -1313,8 +1329,8 @@ const NOMOS_TYPES_DEP = "^0.8.0";
1313
1329
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
1314
1330
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
1315
1331
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
1316
- const NOMOS_CLIENT_DEP = "^0.64.0";
1317
- const NOMOS_FLUTTER_DEP = "^0.63.0";
1332
+ const NOMOS_CLIENT_DEP = "^0.64.1";
1333
+ const NOMOS_FLUTTER_DEP = "^0.63.1";
1318
1334
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
1319
1335
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
1320
1336
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -1690,13 +1706,59 @@ function cursedNameHints(declared: ReadonlySet<string>, names: Readonly<Record<s
1690
1706
  .sort((a, b) => b.length - a.length);
1691
1707
  }
1692
1708
 
1693
- /** Scaffold a pubspec ONLY IF MISSING — the tenant owns versioning/publishing; regeneration must
1694
- * never clobber their bumps. Returns true when written. */
1695
- function scaffoldPubspec(pkgDir: string, yaml: string): boolean {
1709
+ /** Scaffold a pubspec when missing, otherwise reconcile only the dependencies owned by Nomos.
1710
+ * Tenant versioning, publishing metadata and application dependencies remain untouched, while a
1711
+ * regenerated client can never silently retain an obsolete framework floor. */
1712
+ function reconcilePubspec(
1713
+ pkgDir: string,
1714
+ yaml: string,
1715
+ managedDependencies: Readonly<Record<string, string>> = {},
1716
+ ): boolean {
1696
1717
  const p = path.join(pkgDir, "pubspec.yaml");
1697
- if (existsSync(p)) return false;
1698
- mkdirSync(pkgDir, { recursive: true });
1699
- writeFileSync(p, yaml, "utf8");
1718
+ if (!existsSync(p)) {
1719
+ mkdirSync(pkgDir, { recursive: true });
1720
+ writeFileSync(p, yaml, "utf8");
1721
+ return true;
1722
+ }
1723
+ if (Object.keys(managedDependencies).length === 0) return false;
1724
+
1725
+ const source = readFileSync(p, "utf8");
1726
+ const trailingNewline = source.endsWith("\n");
1727
+ const lines = source.split(/\r?\n/);
1728
+ if (trailingNewline) lines.pop();
1729
+ let dependenciesStart = lines.findIndex((line) => /^dependencies:\s*(?:#.*)?$/.test(line));
1730
+ if (dependenciesStart < 0) {
1731
+ const before = lines.findIndex((line) => /^dev_dependencies:\s*(?:#.*)?$/.test(line));
1732
+ dependenciesStart = before < 0 ? lines.length : before;
1733
+ lines.splice(dependenciesStart, 0, "dependencies:");
1734
+ }
1735
+ let dependenciesEnd = dependenciesStart + 1;
1736
+ while (dependenciesEnd < lines.length) {
1737
+ const line = lines[dependenciesEnd]!;
1738
+ if (line.trim() !== "" && !/^\s/.test(line) && !/^\s*#/.test(line)) break;
1739
+ dependenciesEnd += 1;
1740
+ }
1741
+
1742
+ for (const [name, constraint] of Object.entries(managedDependencies)) {
1743
+ const prefix = ` ${name}:`;
1744
+ const existing = lines.findIndex(
1745
+ (line, index) => index > dependenciesStart && index < dependenciesEnd && line.startsWith(prefix),
1746
+ );
1747
+ const replacement = ` ${name}: ${constraint}`;
1748
+ if (existing < 0) {
1749
+ lines.splice(dependenciesEnd, 0, replacement);
1750
+ dependenciesEnd += 1;
1751
+ continue;
1752
+ }
1753
+ let nestedEnd = existing + 1;
1754
+ while (nestedEnd < dependenciesEnd && /^(?: {4,}|\t)/.test(lines[nestedEnd]!)) nestedEnd += 1;
1755
+ lines.splice(existing, nestedEnd - existing, replacement);
1756
+ dependenciesEnd -= nestedEnd - existing - 1;
1757
+ }
1758
+
1759
+ const next = `${lines.join("\n")}${trailingNewline ? "\n" : ""}`;
1760
+ if (next === source) return false;
1761
+ writeFileSync(p, next, "utf8");
1700
1762
  return true;
1701
1763
  }
1702
1764
 
@@ -1705,7 +1767,8 @@ function scaffoldPubspec(pkgDir: string, yaml: string): boolean {
1705
1767
  * `<types>/` (pure data, depends only on `nomos_types`), `<client>/` (effectful runtime clients,
1706
1768
  * depends on the types package + `nomos_client`), and optionally `<test>/` (the headless-harness
1707
1769
  * surface). Generated code lives under each package's `lib/src/generated/` (wiped per compile)
1708
- * behind a generated-owned barrel `lib/<pkg>.dart`; pubspecs are scaffolded only if missing.
1770
+ * behind a generated-owned barrel `lib/<pkg>.dart`; pubspec framework floors are compiler-owned while
1771
+ * tenant versioning, publishing metadata and application dependencies survive regeneration.
1709
1772
  * TYPES-FILE PURITY is enforced fail-closed: a generated types file may import ONLY the
1710
1773
  * `nomos_types` barrel and the relative `nomos_framework.dart` — any runtime import is a compile
1711
1774
  * failure here, not a review comment downstream.
@@ -1760,7 +1823,7 @@ function emitDartPackages(
1760
1823
  if (!src.includes(DART_PACKAGE_IMPORT)) {
1761
1824
  fail(`generated Dart for '${fileBase}' does not carry the expected support import — codegen_dart header drifted`);
1762
1825
  }
1763
- const pure = src.replace(DART_PACKAGE_IMPORT, DART_TYPES_IMPORT);
1826
+ const pure = src.replace(DART_PACKAGE_IMPORT, dartTypesImportFor(src));
1764
1827
  // PURITY, fail-closed: only the nomos_types barrel, the framework file, and SIBLING generated type files
1765
1828
  // (cross-domain foreignRef targets) may be imported. Runtime/bridge surfaces (nomos_client) stay out.
1766
1829
  for (const line of pure.split("\n")) {
@@ -1954,7 +2017,7 @@ function emitDartPackages(
1954
2017
  const typesBarrelPath = path.join(pkgDir(packages.types), "lib", `${packages.types}.dart`);
1955
2018
  writeFileSync(typesBarrelPath, typesBarrel.join("\n"), "utf8");
1956
2019
  written.push(typesBarrelPath);
1957
- scaffoldPubspec(
2020
+ reconcilePubspec(
1958
2021
  pkgDir(packages.types),
1959
2022
  [
1960
2023
  `name: ${packages.types}`,
@@ -1967,6 +2030,7 @@ function emitDartPackages(
1967
2030
  ` nomos_types: ${NOMOS_TYPES_DEP}`,
1968
2031
  ``,
1969
2032
  ].join("\n"),
2033
+ { nomos_types: NOMOS_TYPES_DEP },
1970
2034
  );
1971
2035
 
1972
2036
  // ── the CLIENT package — effectful runtime, depends on types + nomos_client ──
@@ -2110,7 +2174,7 @@ function emitDartPackages(
2110
2174
  const clientBarrelPath = path.join(pkgDir(packages.client), "lib", `${packages.client}.dart`);
2111
2175
  writeFileSync(clientBarrelPath, clientBarrel.join("\n"), "utf8");
2112
2176
  written.push(clientBarrelPath);
2113
- scaffoldPubspec(
2177
+ reconcilePubspec(
2114
2178
  pkgDir(packages.client),
2115
2179
  [
2116
2180
  `name: ${packages.client}`,
@@ -2129,6 +2193,7 @@ function emitDartPackages(
2129
2193
  ` path: ../${packages.types} # co-emitted sibling; replace with a hosted constraint when publishing`,
2130
2194
  ``,
2131
2195
  ].join("\n"),
2196
+ { nomos_client: NOMOS_CLIENT_DEP, nomos_flutter: NOMOS_FLUTTER_DEP },
2132
2197
  );
2133
2198
 
2134
2199
  // ── the TEST package (optional) — generated business scenarios, test-only dependency ──
@@ -2195,7 +2260,7 @@ function emitDartPackages(
2195
2260
  const testBarrelPath = path.join(pkgDir(packages.test), "lib", `${packages.test}.dart`);
2196
2261
  writeFileSync(testBarrelPath, testBarrel.join("\n"), "utf8");
2197
2262
  written.push(testBarrelPath);
2198
- scaffoldPubspec(
2263
+ reconcilePubspec(
2199
2264
  pkgDir(packages.test),
2200
2265
  [
2201
2266
  `name: ${packages.test}`,
@@ -2217,6 +2282,7 @@ function emitDartPackages(
2217
2282
  ` - lib/${genRel}/${embeddedDeployName}`,
2218
2283
  ``,
2219
2284
  ].join("\n"),
2285
+ { nomos_client: NOMOS_CLIENT_DEP },
2220
2286
  );
2221
2287
  }
2222
2288
 
@@ -2270,7 +2336,7 @@ function emitDartPackages(
2270
2336
  const compatBarrelPath = path.join(pkgDir(compatName), "lib", `${compatName}.dart`);
2271
2337
  writeFileSync(compatBarrelPath, compatBarrel.join("\n"), "utf8");
2272
2338
  written.push(compatBarrelPath);
2273
- scaffoldPubspec(
2339
+ reconcilePubspec(
2274
2340
  pkgDir(compatName),
2275
2341
  [
2276
2342
  `name: ${compatName}`,
@@ -2341,6 +2407,8 @@ async function main(): Promise<void> {
2341
2407
  `${configValue.application} — compiled Nomos application composition`,
2342
2408
  `stage: ${emitted.outputPath}`,
2343
2409
  `assets: ${emitted.referencedAssets.length} local content-addressed reference(s)`,
2410
+ `deployment: ${emitted.deploymentPackages.filter((p) => p.placement === "reference").length} serving reference package(s), ` +
2411
+ `${emitted.deploymentPackages.filter((p) => p.placement === "payload").length} deferred payload package(s) — derived from the stage`,
2344
2412
  `runtime configuration: USD (the authoring .mjs does not travel)`,
2345
2413
  ].join("\n"));
2346
2414
  return;
@@ -2897,6 +2965,44 @@ async function main(): Promise<void> {
2897
2965
  ] as const) {
2898
2966
  const canonical = (composed as unknown as Record<string, unknown>)[field];
2899
2967
  if (!Array.isArray(canonical)) continue;
2968
+ // A package-level language may be declared in a different source module from the
2969
+ // aggregate its generated read indexes (workspaceSharing is the canonical case).
2970
+ // Single-module composition then discovers the read in the language module even though
2971
+ // that layer cannot type it. Move every targeted declaration beside its aggregate schema
2972
+ // before filling genuinely missing package opinions. This changes no composed law: one
2973
+ // declaration id still contributes once; it merely gives every authored USD layer a
2974
+ // self-contained, valid read projection.
2975
+ for (const value of canonical) {
2976
+ if (value === null || typeof value !== "object") continue;
2977
+ const id = (value as { id?: unknown }).id;
2978
+ if (typeof id !== "string") continue;
2979
+ const declarationTarget = field === "queries"
2980
+ ? (value as { returns?: unknown }).returns
2981
+ : (["counts", "spatials", "sums", "mins", "maxes", "deriveds", "combineds"] as const)
2982
+ .includes(field as never)
2983
+ ? (value as { of?: unknown }).of
2984
+ : undefined;
2985
+ if (typeof declarationTarget !== "string") continue;
2986
+ const ownerIndex = inputs.findIndex((input) =>
2987
+ input.module.aggregates.some((aggregate) => aggregate.id === declarationTarget));
2988
+ if (ownerIndex < 0) continue;
2989
+ for (const [inputIndex, input] of inputs.entries()) {
2990
+ if (inputIndex === ownerIndex) continue;
2991
+ const module = input.module as DomainModule & Record<string, unknown>;
2992
+ const existing = module[field];
2993
+ if (!Array.isArray(existing)) continue;
2994
+ (module as Record<string, unknown>)[field] = existing.filter((candidate) =>
2995
+ candidate === null || typeof candidate !== "object" ||
2996
+ (candidate as { id?: unknown }).id !== id);
2997
+ }
2998
+ const owner = inputs[ownerIndex]!.module as DomainModule & Record<string, unknown>;
2999
+ const ownerValues = Array.isArray(owner[field]) ? owner[field] as unknown[] : [];
3000
+ if (!ownerValues.some((candidate) =>
3001
+ candidate !== null && typeof candidate === "object" &&
3002
+ (candidate as { id?: unknown }).id === id)) {
3003
+ (owner as Record<string, unknown>)[field] = [...ownerValues, value];
3004
+ }
3005
+ }
2900
3006
  const present = new Set(inputs.flatMap((input) => {
2901
3007
  const values = (input.module as unknown as Record<string, unknown>)[field];
2902
3008
  return Array.isArray(values)
@@ -2956,7 +3062,11 @@ async function main(): Promise<void> {
2956
3062
  ? (JSON.parse(readFileSync(lockPath, "utf8")) as StableIdLock)
2957
3063
  : undefined;
2958
3064
  const inferredRenames: { domain: string; renames: InferredRename[] }[] = [];
2959
- const nextLock: StableIdLock = { v: 1, domains: {} };
3065
+ // The lock is project-wide, not package-local. Several package configs commonly live beside one
3066
+ // application stage; compiling one must not erase the stable identity of domains owned by its siblings.
3067
+ // Current package domains are replaced below, while absent domains remain only as continuity metadata
3068
+ // and never leak into this package's interface, law, clients or deployment.
3069
+ const nextLock: StableIdLock = { v: 1, domains: { ...(priorLock?.domains ?? {}) } };
2960
3070
  for (const mod of domainModules) {
2961
3071
  const identityName = mod.name;
2962
3072
  // The CURRENT canonical aggregate schemas (a pre-continuity manifest pass —
@@ -3052,7 +3162,9 @@ async function main(): Promise<void> {
3052
3162
  };
3053
3163
  }
3054
3164
  // Persist the lockfile (commit it with the law: it is how a rename keeps its
3055
- // identity across clean clones). Deterministic — re-compiles rewrite the same bytes.
3165
+ // identity across clean clones). Existing project-domain order is retained, so once sibling
3166
+ // packages are registered, recompiling them in any order rewrites the same bytes without a
3167
+ // giant formatting-only diff in an established lock.
3056
3168
  writeFileSync(lockPath, JSON.stringify(nextLock, null, 2) + "\n", "utf8");
3057
3169
 
3058
3170
  // ── THE STRUCTURAL INTERFACE (#72) — directives (payload shapes) + aggregates (read
@@ -3434,24 +3546,21 @@ async function main(): Promise<void> {
3434
3546
  for (const ld of layeredDomains) {
3435
3547
  const authored = emitLayeredDomain(ld.key, ld.inputs, { applyBusinessCorrectionLayer: false });
3436
3548
  const canonicalLayer = canonicalLayers.get(`/Nomos/${ld.key}`);
3437
- // Workspace invariants include compiler-owned consequences of the complete
3438
- // application language (birth quota, receiving-group policy, recovery). They
3439
- // are not owned by one source file, so seat the canonical declaration once in
3440
- // the weakest package prelude instead of losing it or duplicating it. Contract
3441
- // implementation links are likewise a cross-module fact: the contract may be
3442
- // declared in one layer and implemented by an aggregate in another.
3443
- const emittedLayers = authored.map((emitted, index) => {
3444
- if (index !== 0 || canonicalLayer === undefined) return emitted;
3445
- const needsPackagePrelude = canonicalLayer.nomosInvariantGate !== undefined || canonicalLayer.nomosContracts !== undefined;
3446
- if (!needsPackagePrelude) return emitted;
3549
+ // Authored layers carry structural declarations (aggregate/directive/query prims).
3550
+ // Domain-level `nomos:*` values are the compiler's closure over the complete package
3551
+ // (relation namespaces, read projections, invariant gates, contracts and language
3552
+ // consequences), so placing partial versions on source layers lets weaker generated
3553
+ // facts show through. Strip those derived attributes from source layers and append one
3554
+ // explicit strongest closure layer. This is ordinary OpenUSD composition: source
3555
+ // opinions remain inspectable and the compiler-owned overlay is the one canonical law.
3556
+ const structuralLayers = authored.map((emitted, index) => {
3557
+ const { path: layerPath, prims, queries, deriveds, combineds } = emitted.layer;
3447
3558
  const layer: UsdLayer = {
3448
- ...emitted.layer,
3449
- ...(canonicalLayer.nomosInvariantGate !== undefined
3450
- ? { nomosInvariantGate: canonicalLayer.nomosInvariantGate }
3451
- : {}),
3452
- ...(canonicalLayer.nomosContracts !== undefined
3453
- ? { nomosContracts: canonicalLayer.nomosContracts }
3454
- : {}),
3559
+ path: layerPath,
3560
+ prims,
3561
+ ...(queries !== undefined ? { queries } : {}),
3562
+ ...(deriveds !== undefined ? { deriveds } : {}),
3563
+ ...(combineds !== undefined ? { combineds } : {}),
3455
3564
  };
3456
3565
  return {
3457
3566
  ...emitted,
@@ -3459,6 +3568,20 @@ async function main(): Promise<void> {
3459
3568
  usda: usdaLayerText(ld.key, emitted.name, layer, ld.inputs[index]!.module, new Set()),
3460
3569
  };
3461
3570
  });
3571
+ const closureLayers = canonicalLayer === undefined
3572
+ ? []
3573
+ : (() => {
3574
+ const layer: UsdLayer = canonicalLayer;
3575
+ const name = "__nomos_package_closure";
3576
+ const canonicalModule = domainModules.find((module) =>
3577
+ (module.domain ?? module.name) === ld.key);
3578
+ return [{
3579
+ name,
3580
+ layer,
3581
+ usda: usdaLayerText(ld.key, name, layer, canonicalModule, new Set()),
3582
+ }];
3583
+ })();
3584
+ const emittedLayers = [...structuralLayers, ...closureLayers];
3462
3585
  for (const emitted of emittedLayers) {
3463
3586
  const base = `${safe(ld.key)}--${safe(emitted.name)}`;
3464
3587
  pendingWrites.push({ path: path.join(layersDir, `${base}.usda`), text: emitted.usda });
@@ -3485,7 +3608,7 @@ async function main(): Promise<void> {
3485
3608
  }
3486
3609
  for (const w of pendingWrites) writeFileSync(w.path, w.text, "utf8");
3487
3610
  layeredRootPath = path.join(outDir, `${cfg.name}.layered.usda`);
3488
- writeFileSync(layeredRootPath, layeredRootUsda(relPaths, flat), "utf8");
3611
+ writeFileSync(layeredRootPath, layeredRootUsda(relPaths), "utf8");
3489
3612
  layeredFileCount = relPaths.length;
3490
3613
 
3491
3614
  const usdcat = resolveUsdcat();
@@ -3634,6 +3757,9 @@ async function main(): Promise<void> {
3634
3757
  // agent who simply cats the build output.
3635
3758
  const summary = {
3636
3759
  package: cfg.name,
3760
+ // Binds the deploy body to the published package asset consumed by an application stage. This is the
3761
+ // content identity of the composed source layers; domainHash below is the runtime package-law hash.
3762
+ sourceIdentity: composedUsdIdentity!,
3637
3763
  domainHash,
3638
3764
  domains: presentationDomainModules.map((m) => ({
3639
3765
  domain: m.domain ?? m.name,
@@ -81,7 +81,11 @@ export const ReceivingGroup = aggregate("NomosInternalReceivingGroup7c1f5e", {
81
81
  workspaceName: t.string().merge(Lww),
82
82
  /** ISO-8601, caller-stamped (determinism — no clock in the plan). */
83
83
  bornAt: t.string().merge(Lww),
84
- }, { cap: 1 }).public();
84
+ // The group's genesis seats its controller in OWNED_READ_RELATION below. Declaring that same capability
85
+ // here makes the law's coarse pack gate recognise the seat, so a newly enrolled device can clone its own
86
+ // receiving group before any tenant records or tenant-authored grants exist. This remains ordinary,
87
+ // replayable Zanzibar state: revocation removes the seat; there is no kernel-level founder bypass.
88
+ }, { cap: 1 }).readableBy(OWNED_READ_RELATION);
85
89
 
86
90
  /**
87
91
  * The framework-owned device lifecycle policy for one receiving group. This is deliberately data, not a
@@ -177,6 +181,7 @@ export const seedReceivingGroup = directive("nomosInternalSeedReceivingGroup7c1f
177
181
  export const receivingGroupNameBindsPrincipal = workspaceInvariant("ReceivingGroupNameBindsPrincipal")
178
182
  .on("nomosInternalSeedReceivingGroup7c1f5e")
179
183
  .reads(() => [refAs("group", "NomosInternalReceivingGroup7c1f5e", RECEIVING_GROUP_ID)])
184
+ .declassify("the receiving-group address is a public hash of its stable principal; refusing a mismatch reveals no private field")
180
185
  .assert(({ group }) => {
181
186
  const g = (group ?? {}) as { principal?: string; workspaceName?: string };
182
187
  const expected = RECEIVING_GROUP_NAME_PREFIX
@@ -196,6 +201,7 @@ const receivingGroupPolicyRefs = () => [
196
201
  export const receivingGroupRejectsKeylessAdditionalDevice = workspaceInvariant("ReceivingGroupRejectsKeylessAdditionalDevice")
197
202
  .on("enrollFirstDevice")
198
203
  .reads(receivingGroupPolicyRefs)
204
+ .declassify("device enrollment deliberately reveals that this receiving group has already completed first-device bootstrap")
199
205
  .assert(({ group }) => typeof group?.principal !== "string" || group.principal.length === 0
200
206
  ? { accept: true }
201
207
  : { reject: "receiving-group-additional-device-requires-current-device-or-guardian" });
package/src/law_check.ts CHANGED
@@ -409,6 +409,14 @@ function unreadPayloadFindings(domain: string, dir: Directive<unknown>, probe: P
409
409
  // the gate object from a named payload id field (per-record authz). Excluding it keeps a real
410
410
  // gate-consumed field from being flagged dead (the todo-pro `shareTodo.listId` case).
411
411
  const gateConsumed = new Set<string>();
412
+ // Compiler-owned law may deliberately consume proof material only in an invariant. The
413
+ // plan must not persist a signature merely to satisfy this lint, and the tenant must not
414
+ // receive a warning about private security machinery. This is internal lowering metadata,
415
+ // not an authored escape hatch.
416
+ for (const field of (dir as { nomosInvariantConsumedPayloadFields?: readonly string[] })
417
+ .nomosInvariantConsumedPayloadFields ?? []) {
418
+ gateConsumed.add(field);
419
+ }
412
420
  const objectFromField = (dir as { requiresRelationGate?: { objectFrom?: { idField?: string } } })
413
421
  .requiresRelationGate?.objectFrom?.idField;
414
422
  if (typeof objectFromField === "string") gateConsumed.add(objectFromField);
package/src/usd_layers.ts CHANGED
@@ -83,6 +83,11 @@ export const NOMOS_USD_LAYERED_PACKAGE_FORMAT = "nomos.usd-layered-package.v1";
83
83
  /** The self-contained ledger/runtime stage: native law plus indexed executable plan assets. */
84
84
  export const NOMOS_USD_PLAN_PACKAGE_FORMAT = "nomos.openusd-plan-package.v2";
85
85
 
86
+ /** Append generated lines without turning a large law into function arguments. */
87
+ function appendLines(target: string[], lines: readonly string[]): void {
88
+ for (const line of lines) target.push(line);
89
+ }
90
+
86
91
  // ── small codecs ───────────────────────────────────────────────────────────────────
87
92
 
88
93
  /** Escape a string for a double-quoted usda string literal. */
@@ -201,7 +206,8 @@ export function nativeValueLines(name: string, value: unknown, indent: string):
201
206
  const position = `i${String(index).padStart(6, "0")}`;
202
207
  const identity = key === undefined ? position : `${position}:k${keyHex(key)}`;
203
208
  if (child !== null && typeof child === "object") {
204
- lines.push("", ...emitContainer(identity.replace(/:/g, "_"), child, `${at} `));
209
+ lines.push("");
210
+ appendLines(lines, emitContainer(identity.replace(/:/g, "_"), child, `${at} `));
205
211
  } else {
206
212
  lines.push(scalarLine(child, identity, `${at} `));
207
213
  }
@@ -210,11 +216,9 @@ export function nativeValueLines(name: string, value: unknown, indent: string):
210
216
  return lines;
211
217
  };
212
218
  const rootName = `__nms_${primName(name)}_${hash}`;
213
- return [
214
- `${indent}custom string nomos:value:${name} = ${usdaString(hash)}`,
215
- "",
216
- ...emitContainer(rootName, value, indent),
217
- ];
219
+ const lines = [`${indent}custom string nomos:value:${name} = ${usdaString(hash)}`, ""];
220
+ appendLines(lines, emitContainer(rootName, value, indent));
221
+ return lines;
218
222
  }
219
223
 
220
224
  /** Field→driver map with keys sorted (the canonical schema shape `manifest.ts` emits). */
@@ -243,7 +247,11 @@ function domainLayerAttrLines(domain: string, layer: UsdLayer, indent: string):
243
247
  ["eraCertGate", layer.nomosEraCertGate], ["valueObjects", layer.nomosValueObjects],
244
248
  ["contracts", layer.nomosContracts], ["foreignRefs", layer.nomosForeignRefs], ["flowGate", layer.nomosFlowGate],
245
249
  ];
246
- for (const [name, value] of values) if (value !== undefined) attrs.push("", ...nativeValueLines(name, value, indent));
250
+ for (const [name, value] of values) {
251
+ if (value === undefined) continue;
252
+ attrs.push("");
253
+ appendLines(attrs, nativeValueLines(name, value, indent));
254
+ }
247
255
  return attrs;
248
256
  }
249
257
 
@@ -379,7 +387,7 @@ export function usdaLayerText(
379
387
  for (const [field, driver] of Object.entries(prim.schema)) {
380
388
  body.push(`${ind3}custom string nomos:driver:${fieldToken(field)} = ${usdaString(driverToken(driver))}`);
381
389
  }
382
- body.push(...relationLines(domainScope, aggById.get(prim.type), ind3));
390
+ appendLines(body, relationLines(domainScope, aggById.get(prim.type), ind3));
383
391
  } else {
384
392
  body.push(
385
393
  `${ind3}custom string nomos:kind = "directive"`,
@@ -392,13 +400,16 @@ export function usdaLayerText(
392
400
  body.push(`${ind3}custom string[] nomos:reads = ${usdaStringArray(prim.reads)}`);
393
401
  }
394
402
  if (prim.emits !== undefined) {
395
- body.push("", ...nativeValueLines("emits", prim.emits, ind3));
403
+ body.push("");
404
+ appendLines(body, nativeValueLines("emits", prim.emits, ind3));
396
405
  }
397
406
  if (prim.secondaryOutputs !== undefined) {
398
- body.push("", ...nativeValueLines("secondaryOutputs", prim.secondaryOutputs, ind3));
407
+ body.push("");
408
+ appendLines(body, nativeValueLines("secondaryOutputs", prim.secondaryOutputs, ind3));
399
409
  }
400
410
  if (prim.payloadJsonSchema !== undefined) {
401
- body.push("", ...nativeValueLines("payloadJsonSchema", prim.payloadJsonSchema, ind3));
411
+ body.push("");
412
+ appendLines(body, nativeValueLines("payloadJsonSchema", prim.payloadJsonSchema, ind3));
402
413
  }
403
414
  }
404
415
  body.push(`${ind2}}`, ``);
@@ -424,7 +435,9 @@ export function usdaLayerText(
424
435
  `${ind3}custom string nomos:id = ${usdaString(d.id)}`,
425
436
  `${ind3}custom string nomos:of = ${usdaString(d.of)}`,
426
437
  ``,
427
- ...nativeValueLines("returns", d.returns, ind3),
438
+ );
439
+ appendLines(body, nativeValueLines("returns", d.returns, ind3));
440
+ body.push(
428
441
  `${ind2}}`,
429
442
  ``,
430
443
  );
@@ -439,7 +452,9 @@ export function usdaLayerText(
439
452
  `${ind3}custom string nomos:refField = ${usdaString(c.refField)}`,
440
453
  `${ind3}custom string nomos:readsFrom = ${usdaString(c.reads)}`,
441
454
  ``,
442
- ...nativeValueLines("returns", c.returns, ind3),
455
+ );
456
+ appendLines(body, nativeValueLines("returns", c.returns, ind3));
457
+ body.push(
443
458
  `${ind2}}`,
444
459
  ``,
445
460
  );
@@ -447,7 +462,7 @@ export function usdaLayerText(
447
462
  // Drop the trailing blank line inside the scope.
448
463
  if (body[body.length - 1] === "") body.pop();
449
464
 
450
- return [
465
+ const lines = [
451
466
  `#usda 1.0`,
452
467
  `(`,
453
468
  ` customLayerData = {`,
@@ -461,14 +476,17 @@ export function usdaLayerText(
461
476
  `{`,
462
477
  ` def Scope ${usdaString(primName(domain))}`,
463
478
  ` {`,
464
- // Stable ids + holon-owned manifests: native layer-level law values.
465
- ...domainLayerAttrLines(domain, layer, " "),
466
- ``,
467
- ...body,
479
+ ];
480
+ // Stable ids + holon-owned manifests: native layer-level law values.
481
+ appendLines(lines, domainLayerAttrLines(domain, layer, " "));
482
+ lines.push(``);
483
+ appendLines(lines, body);
484
+ lines.push(
468
485
  ` }`,
469
486
  `}`,
470
487
  ``,
471
- ].join("\n");
488
+ );
489
+ return lines.join("\n");
472
490
  }
473
491
 
474
492
  /**
@@ -482,26 +500,18 @@ export function layeredRootUsda(
482
500
  composed?: UsdDocument,
483
501
  ): string {
484
502
  const strongestFirst = [...sublayerRelPathsWeakToStrong].reverse();
485
- const composedDomainOpinions =
486
- composed === undefined || composed.layers.length === 0
487
- ? []
488
- : [
489
- ``,
490
- `over "Nomos"`,
491
- `{`,
492
- ...composed.layers.flatMap((layer) => {
493
- const domain = layer.path.slice(layer.path.lastIndexOf("/") + 1);
494
- return [
495
- ` over ${usdaString(primName(domain))}`,
496
- ` {`,
497
- ...domainLayerAttrLines(domain, layer, " "),
498
- ` }`,
499
- ];
500
- }),
501
- `}`,
502
- ``,
503
- ];
504
- return [
503
+ const composedDomainOpinions: string[] = [];
504
+ if (composed !== undefined && composed.layers.length > 0) {
505
+ composedDomainOpinions.push(``, `over "Nomos"`, `{`);
506
+ for (const layer of composed.layers) {
507
+ const domain = layer.path.slice(layer.path.lastIndexOf("/") + 1);
508
+ composedDomainOpinions.push(` over ${usdaString(primName(domain))}`, ` {`);
509
+ appendLines(composedDomainOpinions, domainLayerAttrLines(domain, layer, " "));
510
+ composedDomainOpinions.push(` }`);
511
+ }
512
+ composedDomainOpinions.push(`}`, ``);
513
+ }
514
+ const lines = [
505
515
  `#usda 1.0`,
506
516
  `(`,
507
517
  ` doc = """Nomos layered domain package. subLayers are STRONGEST-FIRST (USD layer-stack`,
@@ -512,14 +522,17 @@ export function layeredRootUsda(
512
522
  ` string "nomos:format" = ${usdaString(NOMOS_USD_LAYERED_PACKAGE_FORMAT)}`,
513
523
  ` }`,
514
524
  ` subLayers = [`,
515
- ...strongestFirst.map(
516
- (p, i) => ` @${p}@${i < strongestFirst.length - 1 ? "," : ""}`,
517
- ),
525
+ ];
526
+ strongestFirst.forEach((p, index) => {
527
+ lines.push(` @${p}@${index < strongestFirst.length - 1 ? "," : ""}`);
528
+ });
529
+ lines.push(
518
530
  ` ]`,
519
531
  `)`,
520
- ...composedDomainOpinions,
521
- ``,
522
- ].join("\n");
532
+ );
533
+ appendLines(lines, composedDomainOpinions);
534
+ lines.push(``);
535
+ return lines.join("\n");
523
536
  }
524
537
 
525
538
  export interface UsdPlanAsset {
@@ -579,7 +592,8 @@ export function nativePlanPackageUsda(
579
592
  if (opened && balance === 0) { end = index; break; }
580
593
  }
581
594
  if (end < 0) throw new Error(`usd-layers: native domain '${domain}' has unbalanced scopes`);
582
- domainBlocks.push(...lines.slice(start, end + 1), "");
595
+ appendLines(domainBlocks, lines.slice(start, end + 1));
596
+ domainBlocks.push("");
583
597
  }
584
598
  if (domainBlocks[domainBlocks.length - 1] === "") domainBlocks.pop();
585
599
 
@@ -617,7 +631,7 @@ export function nativePlanPackageUsda(
617
631
  if (runtime[runtime.length - 1] === "") runtime.pop();
618
632
  runtime.push(` }`, `}`);
619
633
 
620
- return [
634
+ const lines = [
621
635
  `#usda 1.0`,
622
636
  `(`,
623
637
  ` customLayerData = {`,
@@ -627,12 +641,15 @@ export function nativePlanPackageUsda(
627
641
  ``,
628
642
  `def Scope "Nomos"`,
629
643
  `{`,
630
- ...domainBlocks,
644
+ ];
645
+ appendLines(lines, domainBlocks);
646
+ lines.push(
631
647
  `}`,
632
648
  ``,
633
- ...runtime,
634
- ``,
635
- ].join("\n");
649
+ );
650
+ appendLines(lines, runtime);
651
+ lines.push(``);
652
+ return lines.join("\n");
636
653
  }
637
654
 
638
655
  // ── the ordered opinion-map fold (flatten) ───────────────────────────────────────────
@@ -166,8 +166,8 @@ export function workspaceSharing(options: WorkspaceSharingOptions): WorkspaceSha
166
166
  businessBirthInternal: true as const,
167
167
  };
168
168
  const b64 = z.string().regex(/^[A-Za-z0-9+/]+={0,2}$/, "expected base64");
169
- const redeem = nomosImplementationDetail(
170
- directive(redeemId)
169
+ const redeem = nomosImplementationDetail({
170
+ ...directive(redeemId)
171
171
  .mutates(options.orders)
172
172
  .payload(z.object({
173
173
  orderId: z.string().min(1),
@@ -199,7 +199,9 @@ export function workspaceSharing(options: WorkspaceSharingOptions): WorkspaceSha
199
199
  })
200
200
  .alsoEnsures(RelationTuple)
201
201
  .alsoEnsures(NomosInvitationClaim),
202
- );
202
+ // Possession proof is verified by NomosInvitationGate and intentionally never persisted.
203
+ nomosInvariantConsumedPayloadFields: ["invitationSignature"] as const,
204
+ });
203
205
  const gate = workspaceInvariant(`NomosInvitationGate:${options.orders.id}`)
204
206
  .on(redeemId)
205
207
  .reads(({ intent }) => [