@githolon/dsl 0.61.0 → 0.61.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.61.0",
3
+ "version": "0.61.2",
4
4
  "type": "module",
5
5
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -116,6 +116,41 @@ const SUBJECT_CANDIDATES: readonly string[] = [`user:${ZERO_UUID}`, `key:${HEX64
116
116
  * schema and return the first that validates. If none fits an exotic constraint, fall back to the
117
117
  * readable placeholder (behaviour unchanged from before — that directive is proven by hand).
118
118
  */
119
+ /** BIRTHS ISOLATION (multi-`.births()` parent): a parent with SEVERAL birth legs (co2:
120
+ * birthCatalogueWorkspace + birthEstateWorkspace) synthesizes the SAME readable `sample-<field>` payloads,
121
+ * so each leg's plan DERIVES THE SAME child workspace name and the second birth collides on an
122
+ * already-born name. Give each leg a distinct child by suffixing its `sample-`-prefixed string values with
123
+ * a per-leg discriminator — but ONLY when the suffixed payload STILL satisfies the directive's own schema
124
+ * (a constrained field is left untouched, never broken). A single-birth parent is unaffected (no suffix). */
125
+ function uniquifyBirthPayload(
126
+ payload: Record<string, unknown>,
127
+ disc: string,
128
+ schema: z.ZodTypeAny | undefined,
129
+ ): Record<string, unknown> {
130
+ const suffixed: Record<string, unknown> = {};
131
+ let changed = false;
132
+ for (const [k, v] of Object.entries(payload)) {
133
+ if (typeof v === "string" && v.startsWith("sample-")) { suffixed[k] = `${v}-${disc}`; changed = true; }
134
+ else suffixed[k] = v;
135
+ }
136
+ if (!changed) return payload;
137
+ // Re-validate PER FIELD (the birthPayload has its framework/law-hash fields deleted — they are injected at
138
+ // runtime — so a whole-payload parse would spuriously fail on the missing required fields). For a
139
+ // ZodObject we check each suffixed value against ITS OWN field schema; a field whose schema rejects the
140
+ // suffix (a length/regex bound) is reverted to its original value (never break a constrained field).
141
+ const shape =
142
+ schema !== undefined && typeof (schema as { shape?: unknown }).shape === "object"
143
+ ? ((schema as unknown as { shape: Record<string, z.ZodTypeAny> }).shape)
144
+ : undefined;
145
+ for (const [k, v] of Object.entries(suffixed)) {
146
+ const orig = payload[k];
147
+ if (v === orig) continue;
148
+ const fieldSchema = shape?.[k];
149
+ if (fieldSchema !== undefined && !fieldSchema.safeParse(v).success) suffixed[k] = orig; // revert the constrained field
150
+ }
151
+ return suffixed;
152
+ }
153
+
119
154
  function sampleString(zt: z.ZodTypeAny, field: string): string {
120
155
  const readable = `sample-${field}`;
121
156
  if (zt.safeParse(readable).success) return readable;
@@ -2071,8 +2106,20 @@ export function legsForDomain(mod: DomainModule, modules: readonly DomainModule[
2071
2106
  // The child-birth legs (one per `.births()` directive) — discovered by running each plan. A parent
2072
2107
  // with SEVERAL births (co2: birthCatalogueWorkspace + birthEstateWorkspace) keeps EVERY leg; keeping
2073
2108
  // only the first left the other children compile-valid-only (unprovable through their birth).
2074
- const childBirths = allBirths.filter((b) => b.parentDomain === domain).map((b) => {
2109
+ const parentBirths = allBirths.filter((b) => b.parentDomain === domain);
2110
+ const childBirths = parentBirths.map((b) => {
2075
2111
  const withParent = attachParentSetup(b, allBirths);
2112
+ // BIRTHS ISOLATION: with >1 birth leg, uniquify each leg's synthesized child so the legs don't collide
2113
+ // on one generated child workspace name (the discriminator = the birth directive id, unique per leg).
2114
+ if (parentBirths.length > 1) {
2115
+ const birthDir = (mod.directives as Directive<unknown>[]).find((d) => d?.id === b.birthDirectiveId);
2116
+ const schema = birthDir?.payloadSchema as z.ZodTypeAny | undefined;
2117
+ const disc = b.birthDirectiveId;
2118
+ const uniq = uniquifyBirthPayload(withParent.birthPayload, disc, schema);
2119
+ if (uniq !== withParent.birthPayload) {
2120
+ return { ...withParent, birthPayload: uniq };
2121
+ }
2122
+ }
2076
2123
  // A GATED birth with no parent-birth authority path: seat its relation via the SAME granter/seed
2077
2124
  // discovery the relation-bootstrap leg uses (the law's own authority path, e.g. co2's
2078
2125
  // seedPlatformFounder → seedPlatformAdmin → grantCo2EstateCreator → birthEstateWorkspace).
@@ -2293,6 +2340,65 @@ function v2CatchFinally(): string[] {
2293
2340
  * lawHash are injected from the deployed law (the child install resolves bytes by hash from the parent's
2294
2341
  * custody; a `sample-*` hash would fail). The dispatch rides the gateway offer lane (authenticated principal).
2295
2342
  */
2343
+ /**
2344
+ * THE LIVE AUTHORITY-CHAIN SEATING for a GATED `.births()` leg (the co2 case). A birth gated by
2345
+ * `.requires(R)` needs its actor to HOLD R before the birth admits — and R may itself be reachable only
2346
+ * through a RECURSIVE chain of grants (co2: seedPlatformFounder → seedPlatformAdmin [grants co2Admin] →
2347
+ * grantCo2EstateCreator [grants co2EstateCreator] → birthEstateWorkspace). This walks the discovered
2348
+ * `relationSetup` chain and emits each step as a cloud dispatch under the SAME `birthActor`, seating every
2349
+ * relation ON that actor (subject/founder/grantedBy fields overwritten to it) — so one actor plays founder,
2350
+ * admin, and creator, ending holding R. Recurses granter-authority-first (deepest prerequisite runs first).
2351
+ * `actorVar`/`wsVar` are the runtime var names the birth leg already binds.
2352
+ */
2353
+ function liveAuthorityChainLines(setup: RelationBootstrapLeg, actorVar: string, wsVar: string): string[] {
2354
+ const lines: string[] = [];
2355
+ // 1. the granter's OWN authority first (recursive — the deepest prerequisite is seated before its dependents).
2356
+ if (setup.granterAuthority !== undefined) {
2357
+ lines.push(...liveAuthorityChainLines(setup.granterAuthority, actorVar, wsVar));
2358
+ }
2359
+ // A runtime dispatch: build the payload literal, overwrite subject/founder/grantedBy/routed fields to the
2360
+ // actor (seat the relation ON it), stamp the auto-stamped fields, POST to the parent ws as the actor.
2361
+ const dispatch = (
2362
+ directiveId: string,
2363
+ payload: Record<string, unknown>,
2364
+ routed: readonly string[],
2365
+ autoStamped: readonly string[],
2366
+ label: string,
2367
+ ): string[] => {
2368
+ const overwrite = new Set<string>(routed);
2369
+ const entries = Object.entries(payload).map(([k, v]) =>
2370
+ overwrite.has(k) || /^(subject|principal|grantedBy|grantee|member|user|owner|founder)$/i.test(k)
2371
+ ? `${JSON.stringify(k)}: ${actorVar}`
2372
+ : `${JSON.stringify(k)}: ${JSON.stringify(v)}`,
2373
+ );
2374
+ return [
2375
+ ` {`,
2376
+ ` const cp: Record<string, unknown> = { ${entries.join(", ")} };`,
2377
+ ...autoStamped.map((f) => ` cp[${JSON.stringify(f)}] = new Date().toISOString();`),
2378
+ ` const cr = await fetch(\`\${CLOUD}/v2/workspaces/\${${wsVar}}/${directiveId}\`, { method: "POST", headers: { "content-type": "application/json", "x-nomos-principal": ${actorVar} }, body: JSON.stringify(cp) });`,
2379
+ ` const cd = await cr.json();`,
2380
+ ` if (!cd.ok) fail(\`authority chain (${label}) ${directiveId} refused: \${JSON.stringify(cd)}\`);`,
2381
+ ` ok(\`authority chain — ${directiveId} seated (as \${${actorVar}})\`);`,
2382
+ ` }`,
2383
+ ];
2384
+ };
2385
+ // 2. this leg's prerequisite seeds (as the actor = founder).
2386
+ for (const seed of setup.prerequisiteSeeds ?? []) {
2387
+ lines.push(...dispatch(seed.directiveId, seed.payload, [], seed.autoStamped, "prerequisite"));
2388
+ }
2389
+ // 3. the grant that seats this leg's relation ON the actor (subject = actor).
2390
+ lines.push(
2391
+ ...dispatch(
2392
+ setup.grantDirectiveId,
2393
+ setup.grantPayload,
2394
+ [...setup.grantSubjectFields, ...setup.grantGrantedByFields],
2395
+ setup.grantAutoStamped,
2396
+ "grant",
2397
+ ),
2398
+ );
2399
+ return lines;
2400
+ }
2401
+
2296
2402
  function liveChildBirthLegLines(cb: ChildBirthLeg, opts: TsProofOptions, n: () => number): string[] {
2297
2403
  const hashConst = tsHashConstName(opts.packageName);
2298
2404
  // The birth payload literal: the synthesized values + the REAL hashes onto the discovered fields + the
@@ -2356,6 +2462,14 @@ function liveChildBirthLegLines(cb: ChildBirthLeg, opts: TsProofOptions, n: () =
2356
2462
  : []),
2357
2463
  ` birthPayload[${JSON.stringify(cb.lawHashField ?? "lawHash")}] = ${hashConst};`,
2358
2464
  ...cb.birthAutoStamped.map((f) => ` birthPayload[${JSON.stringify(f)}] = new Date().toISOString();`),
2465
+ // GATED BIRTH: seat the FULL authority chain on birthActor before the birth (co2: seedPlatformFounder →
2466
+ // seedPlatformAdmin → grantCo2EstateCreator → the birth). Without this the .requires(R) birth is refused.
2467
+ ...(cb.relationSetup !== undefined
2468
+ ? [
2469
+ ` // seat the .requires("${cb.birthRequiresRelation}") authority chain on birthActor before the gated birth:`,
2470
+ ...liveAuthorityChainLines(cb.relationSetup, "birthActor", "birthParentWs"),
2471
+ ]
2472
+ : []),
2359
2473
  ` const br = await fetch(\`\${CLOUD}/v2/workspaces/\${birthParentWs}/${cb.birthDirectiveId}\`, { method: "POST", headers: { "content-type": "application/json", "x-nomos-principal": birthActor }, body: JSON.stringify(birthPayload) });`,
2360
2474
  ` const bd = await br.json();`,
2361
2475
  ` if (!bd.ok) fail(\`child birth offer ${cb.parentDomain}/${cb.birthDirectiveId} refused: \${JSON.stringify(bd)}\`);`,
@@ -97,11 +97,29 @@ function loadFileOffer(arg: string): { offer: InterfaceMap; source: string } {
97
97
  }
98
98
 
99
99
  /** Read a LIVE workspace's on-ledger offer via connect() (`@githolon/client`). */
100
+ /**
101
+ * Parse a live compat target URL into its cloud BASE and workspace. The base is everything BEFORE
102
+ * `/v2/workspaces/` — NOT just the origin — so a gateway mounted under a PATH PREFIX
103
+ * (`https://host/nomos/cloud/v2/workspaces/<ws>`) keeps that prefix. Otherwise the client would fetch the
104
+ * runtime at the wrong base (`https://host/v1/runtime/holon.wasm` 404s instead of
105
+ * `https://host/nomos/cloud/v1/runtime/holon.wasm`). Split on the marker so an arbitrary-depth prefix
106
+ * survives; without the marker the whole URL (a `--workspace`-paired base) is the cloud.
107
+ */
108
+ export function parseLiveTarget(
109
+ url: string,
110
+ workspaceFlag?: string,
111
+ ): { cloud: string; workspace: string | undefined } {
112
+ const marker = "/v2/workspaces/";
113
+ const at = url.indexOf(marker);
114
+ const cloud = (at >= 0 ? url.slice(0, at) : url.replace(/[?#].*$/, "")).replace(/\/+$/, "");
115
+ const wsFromUrl = at >= 0 ? url.slice(at + marker.length).split(/[/?#]/)[0] : undefined;
116
+ const workspace = workspaceFlag ?? (wsFromUrl !== undefined && wsFromUrl !== "" ? wsFromUrl : undefined);
117
+ return { cloud, workspace };
118
+ }
119
+
100
120
  async function loadLiveOffer(url: string, workspaceFlag?: string): Promise<{ offer: InterfaceMap; source: string }> {
101
- const m = /^(https?:\/\/[^/]+)(?:\/v2\/workspaces\/([^/?#]+))?/.exec(url);
102
- if (!m) fail(`could not parse the live target URL: ${url}`);
103
- const cloud = m[1]!;
104
- const workspace = workspaceFlag ?? m[2];
121
+ if (!/^https?:\/\//.test(url)) fail(`could not parse the live target URL: ${url}`);
122
+ const { cloud, workspace } = parseLiveTarget(url, workspaceFlag);
105
123
  if (!workspace) fail(`live target needs a workspace — use https://host/v2/workspaces/<ws> or add --workspace <ws>`);
106
124
  let connect: (opts: { cloud: string; workspace: string }) => Promise<{ interfaceOffer(): Promise<InterfaceMap | null> }>;
107
125
  try {
@@ -184,4 +202,9 @@ async function main(): Promise<void> {
184
202
  process.exit(incompatible.length > 0 ? 1 : 0);
185
203
  }
186
204
 
187
- void main();
205
+ // Run ONLY as the entry point (the compat launcher), never on import (the test imports parseLiveTarget).
206
+ {
207
+ const { pathToFileURL } = await import("node:url");
208
+ const entry = process.argv[1];
209
+ if (entry !== undefined && import.meta.url === pathToFileURL(entry).href) void main();
210
+ }