@wairon/cli 5.1.1-dev.112 → 5.1.1-dev.113

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/dist/index.js CHANGED
@@ -3115,8 +3115,8 @@ var require_dist = __commonJS({
3115
3115
  walkPackDir(root, path52.join(current2, entry.name), out);
3116
3116
  } else if (entry.isFile()) {
3117
3117
  const absolute = path52.join(current2, entry.name);
3118
- const relative25 = path52.relative(root, absolute).split(path52.sep).join("/");
3119
- out.push({ path: relative25, contents: fs33.readFileSync(absolute) });
3118
+ const relative27 = path52.relative(root, absolute).split(path52.sep).join("/");
3119
+ out.push({ path: relative27, contents: fs33.readFileSync(absolute) });
3120
3120
  }
3121
3121
  }
3122
3122
  }
@@ -3640,6 +3640,7 @@ __export(src_exports, {
3640
3640
  createEmptyTopologyConfig: () => createEmptyTopologyConfig,
3641
3641
  createMember: () => createMember,
3642
3642
  createProjectConfig: () => createProjectConfig,
3643
+ crossProjectWriteRefusal: () => crossProjectWriteRefusal,
3643
3644
  currentRootBinding: () => currentRootBinding,
3644
3645
  declareExternal: () => declareExternal,
3645
3646
  declareMember: () => declareMember,
@@ -3809,6 +3810,7 @@ __export(src_exports, {
3809
3810
  moveMember: () => moveMember,
3810
3811
  moveMethods: () => moveMethods,
3811
3812
  moveMountToMembers: () => moveMountToMembers,
3813
+ moveSpec: () => moveSpec,
3812
3814
  nameKey: () => nameKey,
3813
3815
  namesAnInteger: () => namesAnInteger,
3814
3816
  narrativeStepForeignFields: () => narrativeStepForeignFields,
@@ -3838,6 +3840,7 @@ __export(src_exports, {
3838
3840
  projectIdentity: () => projectIdentity,
3839
3841
  promoteMember: () => promoteMember,
3840
3842
  provisionProject: () => provisionProject,
3843
+ publishedUsesOf: () => publishedUsesOf,
3841
3844
  qualifiedTypeId: () => qualifiedTypeId,
3842
3845
  rangeProblem: () => rangeProblem,
3843
3846
  readCodeAnnotation: () => readCodeAnnotation,
@@ -3867,6 +3870,7 @@ __export(src_exports, {
3867
3870
  renameId: () => renameProjectId,
3868
3871
  renameMethod: () => renameMethod,
3869
3872
  renameParam: () => renameParam,
3873
+ renameSpecId: () => renameSpecId,
3870
3874
  renameType: () => renameType,
3871
3875
  renderDiagram: () => renderDiagram2,
3872
3876
  repairForeignStepFields: () => repairForeignStepFields,
@@ -3896,6 +3900,7 @@ __export(src_exports, {
3896
3900
  retiredReachFacts: () => retiredReachFacts,
3897
3901
  returnTypeOf: () => returnTypeOf,
3898
3902
  rewriteReferences: () => rewriteReferences2,
3903
+ routerLinkage: () => routerLinkage,
3899
3904
  runWithHostedLookup: () => runWithHostedLookup,
3900
3905
  runWithProjectBinding: () => runWithProjectBinding,
3901
3906
  runWithProjectRoot: () => runWithProjectRoot,
@@ -3916,6 +3921,7 @@ __export(src_exports, {
3916
3921
  settledSpecPaths: () => settledSpecPaths,
3917
3922
  signatureFacts: () => signatureFacts,
3918
3923
  signatureTypeRefs: () => signatureTypeRefs,
3924
+ specIndexMethodReferences: () => specIndexMethodReferences,
3919
3925
  specIndexReferencesTo: () => specIndexReferencesTo,
3920
3926
  specIndexRetiredBy: () => specIndexRetiredBy,
3921
3927
  specIndexSubtreeOf: () => specIndexSubtreeOf,
@@ -4022,22 +4028,22 @@ function sampleOf(file) {
4022
4028
  if (fd !== void 0) fs.closeSync(fd);
4023
4029
  }
4024
4030
  }
4025
- function attributeMatches(pattern, relative25) {
4031
+ function attributeMatches(pattern, relative27) {
4026
4032
  const glob = pattern.replace(/^\//, "");
4027
- const target = glob.includes("/") ? relative25 : path.posix.basename(relative25);
4033
+ const target = glob.includes("/") ? relative27 : path.posix.basename(relative27);
4028
4034
  const source = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0000/g, "(?:.*/)?");
4029
4035
  return new RegExp(`^${source}$`).test(target);
4030
4036
  }
4031
4037
  function attributeEol(attributesFile, file) {
4032
4038
  const text3 = readFileOrNull(attributesFile);
4033
4039
  if (text3 === null) return null;
4034
- const relative25 = path.relative(path.dirname(attributesFile), file).split(path.sep).join("/");
4040
+ const relative27 = path.relative(path.dirname(attributesFile), file).split(path.sep).join("/");
4035
4041
  let eol = null;
4036
4042
  for (const raw of text3.split(/\r?\n/)) {
4037
4043
  const line = raw.trim();
4038
4044
  if (line === "" || line.startsWith("#")) continue;
4039
4045
  const [pattern, ...attrs] = line.split(/\s+/);
4040
- if (!attributeMatches(pattern, relative25)) continue;
4046
+ if (!attributeMatches(pattern, relative27)) continue;
4041
4047
  for (const attr of attrs) {
4042
4048
  if (attr === "eol=crlf") eol = "\r\n";
4043
4049
  else if (attr === "eol=lf") eol = "\n";
@@ -4318,7 +4324,7 @@ function defaultTargetConfig(type) {
4318
4324
  enabled: true
4319
4325
  };
4320
4326
  }
4321
- var WAIRON_VERSION = "5.1.1-dev.112";
4327
+ var WAIRON_VERSION = "5.1.1-dev.113";
4322
4328
  var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
4323
4329
  var ARCHITECT_AGENT_ID = "agent-architect";
4324
4330
  var ARCHITECT_TEMPLATE_ID = "architect";
@@ -7489,9 +7495,9 @@ var RawParser = class {
7489
7495
  }
7490
7496
  name += this.ident();
7491
7497
  while (this.isP("::") || this.isP(".")) {
7492
- const sep17 = this.peek().v;
7498
+ const sep18 = this.peek().v;
7493
7499
  this.at++;
7494
- name += sep17 + this.ident();
7500
+ name += sep18 + this.ident();
7495
7501
  }
7496
7502
  if (!this.isP("<")) return { k: "name", name };
7497
7503
  this.at++;
@@ -8690,7 +8696,10 @@ function conceptNoun(component) {
8690
8696
  }
8691
8697
  var HttpMethodSchema = import_zod5.z.enum(["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]);
8692
8698
  var EndpointSchema = import_zod5.z.discriminatedUnion("transport", [
8693
- import_zod5.z.object({ transport: import_zod5.z.literal("HTTP"), method: HttpMethodSchema, path: import_zod5.z.string() }),
8699
+ // `status`: the success code the operation answers with when it is not the
8700
+ // verb's convention (202 Accepted, a 3xx redirect, a 201 for a workflow verb
8701
+ // that files something) — the design's own word, which the OpenAPI export uses.
8702
+ import_zod5.z.object({ transport: import_zod5.z.literal("HTTP"), method: HttpMethodSchema, path: import_zod5.z.string(), status: import_zod5.z.number().int().min(200).max(399).optional() }),
8694
8703
  import_zod5.z.object({ transport: import_zod5.z.literal("gRPC"), service: import_zod5.z.string(), method: import_zod5.z.string() }),
8695
8704
  import_zod5.z.object({ transport: import_zod5.z.literal("GraphQL"), operation: import_zod5.z.enum(["query", "mutation", "subscription"]), field: import_zod5.z.string() }),
8696
8705
  import_zod5.z.object({ transport: import_zod5.z.literal("MessageBus"), topic: import_zod5.z.string(), event: import_zod5.z.string(), queue: import_zod5.z.string().optional(), direction: import_zod5.z.enum(["subscribe", "publish"]).default("subscribe") }),
@@ -10033,6 +10042,49 @@ function specIndexReferencesTo(index, targets, system) {
10033
10042
  }
10034
10043
  return out;
10035
10044
  }
10045
+ function specIndexMethodReferences(index, component, methods) {
10046
+ const named2 = new Set(methods);
10047
+ const contracts = index.interfaces.filter((i) => i.component === component);
10048
+ const target = (method2) => {
10049
+ const holder = contracts.find((i) => i.methods.some((m) => m.name === method2));
10050
+ return holder ? { kind: "interface", id: holder.id } : { kind: "component", id: component };
10051
+ };
10052
+ const hits = (comp, method2) => comp === component && method2 !== void 0 && named2.has(method2);
10053
+ const out = [];
10054
+ for (const sub of index.subsystems) {
10055
+ for (const entry of sub.lifecycle ?? []) {
10056
+ if (hits(entry.component, entry.method)) out.push({ kind: "subsystem", id: sub.id, position: "lifecycle", target: target(entry.method) });
10057
+ }
10058
+ }
10059
+ for (const comp of index.components) {
10060
+ for (const binding of comp.dispatch ?? []) {
10061
+ if (hits(binding.component, binding.method)) out.push({ kind: "component", id: comp.id, position: "dispatch", target: target(binding.method) });
10062
+ }
10063
+ }
10064
+ for (const intf of index.interfaces) {
10065
+ for (const method2 of intf.methods) {
10066
+ const source = method2.signatureFrom;
10067
+ const cut = source?.lastIndexOf(".") ?? -1;
10068
+ if (source === void 0 || cut <= 0) continue;
10069
+ const name = source.slice(cut + 1);
10070
+ if (hits(source.slice(0, cut), name)) out.push({ kind: "interface", id: intf.id, position: `methods.${method2.name}.signatureFrom`, target: target(name) });
10071
+ }
10072
+ }
10073
+ for (const impl of index.implementations) {
10074
+ for (const method2 of impl.methods) {
10075
+ for (const step of method2.narrative ?? []) {
10076
+ if (hits(step.targetComponent, step.targetMethod)) {
10077
+ out.push({ kind: "implementation", id: impl.id, position: `methods.${method2.name}.narrative.step ${step.stepNumber}`, target: target(step.targetMethod) });
10078
+ }
10079
+ }
10080
+ for (const entry of method2.calls ?? []) {
10081
+ const call = parseDeclaredCall(entry);
10082
+ if (call && hits(call.compId, call.methodName)) out.push({ kind: "implementation", id: impl.id, position: `methods.${method2.name}.calls`, target: target(call.methodName) });
10083
+ }
10084
+ }
10085
+ }
10086
+ return out;
10087
+ }
10036
10088
 
10037
10089
  // src/utils/canonical-json.ts
10038
10090
  function canonicalize(value) {
@@ -10184,6 +10236,10 @@ function parseLockRecord(text3, file) {
10184
10236
  } catch (e) {
10185
10237
  throw new LockRecordUnreadableError(file, `it is not valid JSON (${e.message})`);
10186
10238
  }
10239
+ const duplicate = firstDuplicateKey(body);
10240
+ if (duplicate !== null) {
10241
+ throw new LockRecordUnreadableError(file, `it names the key "${duplicate}" twice (a conflict resolved by hand leaves this), so which of the two is the record cannot be told`);
10242
+ }
10187
10243
  if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
10188
10244
  const what = raw === null ? "null" : Array.isArray(raw) ? "an array" : `a ${typeof raw}`;
10189
10245
  throw new LockRecordUnreadableError(file, `it holds ${what}, not a lock record object`);
@@ -10204,6 +10260,37 @@ function parseLockRecord(text3, file) {
10204
10260
  }
10205
10261
  return normalizeRecord(record);
10206
10262
  }
10263
+ function firstDuplicateKey(text3) {
10264
+ const stack = [];
10265
+ const pathOf = (frame, key2) => frame && frame.path ? `${frame.path}.${key2}` : key2;
10266
+ for (let i = 0; i < text3.length; i++) {
10267
+ const c = text3[i];
10268
+ if (c === '"') {
10269
+ let j = i + 1;
10270
+ while (j < text3.length && text3[j] !== '"') j += text3[j] === "\\" ? 2 : 1;
10271
+ const top = stack[stack.length - 1];
10272
+ if (top && top.keys && top.expectKey) {
10273
+ const key2 = JSON.parse(text3.slice(i, j + 1));
10274
+ if (top.keys.has(key2)) return pathOf(top, key2);
10275
+ top.keys.add(key2);
10276
+ top.lastKey = key2;
10277
+ top.expectKey = false;
10278
+ }
10279
+ i = j;
10280
+ } else if (c === "{" || c === "[") {
10281
+ const top = stack[stack.length - 1];
10282
+ const segment = top ? top.keys ? top.lastKey ?? "" : "[]" : "";
10283
+ const path52 = top && segment ? pathOf(top, segment) : top?.path ?? "";
10284
+ stack.push({ keys: c === "{" ? /* @__PURE__ */ new Set() : null, expectKey: c === "{", path: path52 });
10285
+ } else if (c === "}" || c === "]") {
10286
+ stack.pop();
10287
+ } else if (c === ",") {
10288
+ const top = stack[stack.length - 1];
10289
+ if (top && top.keys) top.expectKey = true;
10290
+ }
10291
+ }
10292
+ return null;
10293
+ }
10207
10294
  function displayPath2(file) {
10208
10295
  const rel2 = path9.relative(process.cwd(), file);
10209
10296
  const shown2 = rel2 && !rel2.startsWith("..") && !path9.isAbsolute(rel2) ? rel2 : file;
@@ -10859,6 +10946,12 @@ var EXTENSION_LANGUAGE = {
10859
10946
  ".kt": "kotlin",
10860
10947
  ".swift": "swift"
10861
10948
  };
10949
+ function routerLinkage(router) {
10950
+ const hash = router.lastIndexOf("#");
10951
+ if (hash > 0) return { file: router.slice(0, hash), ...hash < router.length - 1 ? { name: router.slice(hash + 1) } : {} };
10952
+ if (router.includes("/") || EXTENSION_LANGUAGE[path11.extname(router).toLowerCase()] !== void 0) return { file: router };
10953
+ return { name: router };
10954
+ }
10862
10955
  function languageOfSourcePath(sourcePath) {
10863
10956
  return EXTENSION_LANGUAGE[path11.extname(sourcePath).toLowerCase()];
10864
10957
  }
@@ -11109,6 +11202,12 @@ function fieldRenamesSince(pinned, live, publicName, member, pinnedDigest) {
11109
11202
  const back = { ...live, types: live.types.map((d) => undone.get(d.id) ?? d) };
11110
11203
  return { renames, renameOnly: memberDigest(back, publicName, member) === pinnedDigest };
11111
11204
  }
11205
+ function shownEndpoint(endpoint) {
11206
+ if (!endpoint) return "none";
11207
+ if (endpoint.transport === "HTTP") return `${endpoint.method} ${endpoint.path}${endpoint.status !== void 0 ? ` (answers ${endpoint.status})` : ""}`;
11208
+ const { transport, ...address } = endpoint;
11209
+ return `${String(transport)} ${Object.entries(address).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${String(v)}`).join(" ")}`;
11210
+ }
11112
11211
  function shownFact(value) {
11113
11212
  return value === void 0 || value === null || value === "" ? "none" : typeof value === "string" ? value : canonicalize(value);
11114
11213
  }
@@ -11278,6 +11377,7 @@ function surfaceChanges(newer, older) {
11278
11377
  }
11279
11378
  const oldMethods = new Map(before.methods.map((m) => [m.name, m]));
11280
11379
  const usedOld = /* @__PURE__ */ new Set();
11380
+ const embeddedBy = /* @__PURE__ */ new Map();
11281
11381
  entry.methods.forEach((method2, methodIndex) => {
11282
11382
  let was = oldMethods.get(method2.name);
11283
11383
  if (!was) {
@@ -11301,7 +11401,10 @@ function surfaceChanges(newer, older) {
11301
11401
  } else if (a !== b) {
11302
11402
  const exported = new Set((newer.exportedTypes ?? []).map((t) => t.type));
11303
11403
  const embedded = renamesIn(closureIds(newer, method2).filter((id) => !exported.has(id)), notes);
11304
- if (embedded.length > 0) out.push({ kind: "changed", name: entry.id, member: method2.name, detail: `signature reads the same, but it names renamed types: ${embedded.join("; ")}` });
11404
+ if (embedded.length > 0) {
11405
+ const key2 = embedded.join("; ");
11406
+ embeddedBy.set(key2, [...embeddedBy.get(key2) ?? [], method2.name]);
11407
+ }
11305
11408
  }
11306
11409
  for (const r of paramRenames(was, method2)) {
11307
11410
  out.push({ kind: "renamed", name: entry.id, member: method2.name, from: r.from, detail: `parameter "${r.from}" renamed to "${r.to}"` });
@@ -11309,7 +11412,13 @@ function surfaceChanges(newer, older) {
11309
11412
  if (factValue(was.effect) !== factValue(method2.effect)) {
11310
11413
  out.push({ kind: "changed", name: entry.id, member: method2.name, detail: `effect ${shownEffect(was.effect)} \u2192 ${shownEffect(method2.effect)}` });
11311
11414
  }
11415
+ if (factValue(was.endpoint) !== factValue(method2.endpoint)) {
11416
+ out.push({ kind: "changed", name: entry.id, member: method2.name, detail: `endpoint ${shownEndpoint(was.endpoint)} \u2192 ${shownEndpoint(method2.endpoint)}` });
11417
+ }
11312
11418
  });
11419
+ for (const [renames, methods] of embeddedBy) {
11420
+ out.push(methods.length === 1 ? { kind: "changed", name: entry.id, member: methods[0], detail: `signature reads the same, but it names renamed types: ${renames}` } : { kind: "changed", name: entry.id, detail: `${methods.length} methods (${methods.join(", ")}): signatures read the same, but they name renamed types: ${renames}` });
11421
+ }
11313
11422
  for (const m of before.methods) {
11314
11423
  if (!usedOld.has(m.name)) out.push({ kind: "removed", name: entry.id, member: m.name, detail: `method removed: ${shownSignature(m)}` });
11315
11424
  }
@@ -11354,7 +11463,14 @@ function surfaceChanges(newer, older) {
11354
11463
  for (const t of older.exportedTypes ?? []) {
11355
11464
  if (!(newer.exportedTypes ?? []).some((x) => x.id === t.id) && !consumed.has(t.id)) out.push({ kind: "removed", name: t.id, detail: "type removed" });
11356
11465
  }
11357
- return out.sort((x, y) => x.name === y.name ? (x.member ?? "") < (y.member ?? "") ? -1 : (x.member ?? "") > (y.member ?? "") ? 1 : 0 : x.name < y.name ? -1 : 1);
11466
+ const seen = /* @__PURE__ */ new Set();
11467
+ const unique2 = out.filter((c) => {
11468
+ const key2 = JSON.stringify([c.kind, c.name, c.member ?? "", c.from ?? "", c.detail]);
11469
+ if (seen.has(key2)) return false;
11470
+ seen.add(key2);
11471
+ return true;
11472
+ });
11473
+ return unique2.sort((x, y) => x.name === y.name ? (x.member ?? "") < (y.member ?? "") ? -1 : (x.member ?? "") > (y.member ?? "") ? 1 : 0 : x.name < y.name ? -1 : 1);
11358
11474
  }
11359
11475
 
11360
11476
  // src/models/exports.ts
@@ -11392,6 +11508,14 @@ function narrowedToUses(consumer, published) {
11392
11508
  }
11393
11509
  }
11394
11510
  }
11511
+ if (use.kind === "component") {
11512
+ for (const name of names) {
11513
+ for (const u of consumer.uses ?? []) {
11514
+ if (u.publicName !== name || u.members.length > 0) continue;
11515
+ add2({ publicName: name, kind: u.kind, members: [...use.members].sort(), ...u.specs ? { specs: [...u.specs] } : {} });
11516
+ }
11517
+ }
11518
+ }
11395
11519
  for (const former of use.formerNames ?? []) {
11396
11520
  if ((consumer.broken ?? []).includes(former) && !merged.has(former)) add2({ publicName: former, kind: use.kind, members: use.kind === "type" ? ["type"] : [] });
11397
11521
  }
@@ -12317,12 +12441,15 @@ function generateExcalidrawScene(model) {
12317
12441
  // src/core/canvas.ts
12318
12442
  function memberProjectNodes() {
12319
12443
  const family = graph();
12444
+ const top = family.nodes.find((n) => n.namespace === "")?.directory;
12445
+ const sibling = (dir) => top !== void 0 && path13.relative(top, dir).startsWith("..");
12320
12446
  const contained = family.nodes.filter((n) => n.namespace !== "").map((n) => ({
12321
12447
  id: n.namespace,
12322
12448
  name: n.mountAlias ?? n.namespace,
12323
12449
  description: n.memberDescription ?? n.name ?? "",
12324
12450
  trustedLinks: [],
12325
- project: true
12451
+ project: true,
12452
+ ...sibling(n.directory) ? { storage: storageBadge("path", n.directory, void 0) } : {}
12326
12453
  }));
12327
12454
  const rootExternals = family.nodes.find((n) => n.namespace === "")?.externals ?? [];
12328
12455
  const referenced = rootExternals.filter((e) => e.role === "member").map((e) => ({ id: e.alias, name: e.alias, description: "", trustedLinks: [], project: true, storage: storageBadge(e.sourceKind === "family" ? "contained" : e.sourceKind, e.directory, e.commit) }));
@@ -17225,40 +17352,64 @@ function idHit(name, ids) {
17225
17352
  const local = name.split(/::|\./).pop().toLowerCase().replace(/[^a-z0-9_-]/g, "");
17226
17353
  return [...ids].find((id) => id.toLowerCase() === local);
17227
17354
  }
17228
- function schemaScope(ownDefs, externals, reached, alias) {
17355
+ function componentKey(name) {
17356
+ return name.replace(/::/g, ".").replace(/[^A-Za-z0-9._-]/g, "_");
17357
+ }
17358
+ function projectIdentity2(surface) {
17359
+ return surface.projectId ?? surface.projectName;
17360
+ }
17361
+ function schemaScope(ownDefs, externals, registry, alias) {
17229
17362
  const ownIds = ownDefs.map((t) => t.id);
17230
17363
  const externalId = (pinned, name) => {
17231
17364
  const exported = (pinned.exportedTypes ?? []).find((t) => t.id === name || t.id.toLowerCase() === name.toLowerCase());
17232
- return exported && pinned.types.some((t) => t.id === exported.type) ? exported.type : idHit(name, pinned.types.map((t) => t.id));
17365
+ if (exported && pinned.types.some((t) => t.id === exported.type)) return exported.type;
17366
+ if (pinned.types.some((t) => t.id === name)) return name;
17367
+ return idHit(name, pinned.types.map((t) => t.id));
17368
+ };
17369
+ const aliasOf = (identity2) => [...externals].find(([, s]) => projectIdentity2(s) === identity2)?.[0] ?? identity2;
17370
+ const record = (identity2, id, preferred, reached) => {
17371
+ const identityKey = `${identity2}\0${id}`;
17372
+ const held = registry.keyOf.get(identityKey);
17373
+ if (held !== void 0) return held;
17374
+ let key2 = componentKey(preferred);
17375
+ for (let n = 2; registry.reached.has(key2); n++) key2 = componentKey(`${preferred}_${n}`);
17376
+ registry.keyOf.set(identityKey, key2);
17377
+ registry.reached.set(key2, reached);
17378
+ return key2;
17233
17379
  };
17234
17380
  const external = (from, name) => {
17235
17381
  const pinned = externals.get(from);
17236
17382
  const id = pinned ? externalId(pinned, name) : void 0;
17237
17383
  if (!pinned || id === void 0) return void 0;
17238
- const key2 = `${from}.${id}`;
17239
- if (!reached.has(key2)) reached.set(key2, { alias: from, def: pinned.types.find((t) => t.id === id) });
17240
- return key2;
17384
+ const identity2 = projectIdentity2(pinned);
17385
+ const keyAlias = aliasOf(identity2);
17386
+ const surface = externals.get(keyAlias) ?? pinned;
17387
+ const def = surface.types.find((t) => t.id === id) ?? pinned.types.find((t) => t.id === id);
17388
+ return record(identity2, id, `${keyAlias}.${id}`, { alias: externals.has(keyAlias) ? keyAlias : from, def });
17241
17389
  };
17390
+ const own2 = (id) => record("", id, id, { def: ownDefs.find((t) => t.id === id) });
17242
17391
  return {
17243
17392
  componentOf(name) {
17244
- const sep17 = name.indexOf("::");
17245
- if (sep17 > 0 && externals.has(name.slice(0, sep17))) return external(name.slice(0, sep17), name.slice(sep17 + 2));
17393
+ const sep18 = name.indexOf("::");
17394
+ if (sep18 > 0 && externals.has(name.slice(0, sep18))) return external(name.slice(0, sep18), name.slice(sep18 + 2));
17395
+ if (alias === void 0 && ownIds.includes(name)) return own2(name);
17246
17396
  if (alias !== void 0) return external(alias, name);
17247
- return idHit(name, ownIds);
17397
+ const hit = idHit(name, ownIds);
17398
+ return hit !== void 0 ? own2(hit) : void 0;
17248
17399
  },
17249
17400
  isObject(typeRef2) {
17250
17401
  const expr = expressionOf(typeRef2);
17251
17402
  if (!expr || expr.form !== "named" && expr.form !== "applied") return false;
17252
17403
  const name = expr.name ?? "";
17253
- const sep17 = name.indexOf("::");
17254
- const from = sep17 > 0 && externals.has(name.slice(0, sep17)) ? name.slice(0, sep17) : alias;
17404
+ const sep18 = name.indexOf("::");
17405
+ const from = sep18 > 0 && externals.has(name.slice(0, sep18)) ? name.slice(0, sep18) : alias;
17255
17406
  const pinned = from !== void 0 ? externals.get(from) : void 0;
17256
- const local = sep17 > 0 ? name.slice(sep17 + 2) : name;
17257
- const def = pinned ? pinned.types.find((t) => t.id === externalId(pinned, local)) : ownDefs.find((t) => t.id === idHit(name, ownIds));
17407
+ const local = sep18 > 0 && from !== alias ? name.slice(sep18 + 2) : name;
17408
+ const def = pinned ? pinned.types.find((t) => t.id === externalId(pinned, local)) : ownDefs.find((t) => t.id === (ownIds.includes(name) ? name : idHit(name, ownIds)));
17258
17409
  return !!def && def.kind !== "enum" && def.kind !== "signature" && def.holds === void 0;
17259
17410
  },
17260
17411
  within(next) {
17261
- return schemaScope(ownDefs, externals, reached, next);
17412
+ return schemaScope(ownDefs, externals, registry, next);
17262
17413
  }
17263
17414
  };
17264
17415
  }
@@ -17317,14 +17468,30 @@ function creates(method2) {
17317
17468
  if (method2.effect === "none" || method2.effect === "read") return false;
17318
17469
  return CREATING_NAME.test(method2.name);
17319
17470
  }
17471
+ var STATUS_TEXT = { "200": "Success", "201": "Created", "202": "Accepted", "203": "Success", "204": "Success", "206": "Partial content" };
17472
+ function conventionalStatus(method2, httpVerb) {
17473
+ const returns = method2.returns ?? "";
17474
+ if (!returns || returnsNothing(returns)) return 204;
17475
+ return httpVerb === "post" && creates(method2) ? 201 : 200;
17476
+ }
17477
+ function admitsNoValue(typeRef2) {
17478
+ return expressionOf(typeRef2)?.form === "optional";
17479
+ }
17480
+ function described(schema, description) {
17481
+ return description && schema.description === void 0 ? { ...schema, description } : schema;
17482
+ }
17320
17483
  function responsesFor(method2, httpVerb, scope) {
17321
17484
  const returns = method2.returns ?? "";
17322
17485
  const nothing = !returns || returnsNothing(returns);
17323
- const code = nothing ? "204" : httpVerb === "post" && creates(method2) ? "201" : "200";
17486
+ const stated = method2.endpoint?.transport === "HTTP" ? method2.endpoint.status : void 0;
17487
+ const code = String(stated ?? conventionalStatus(method2, httpVerb));
17488
+ const redirect = /^3/.test(code);
17489
+ const bodyless = nothing || redirect || code === "204";
17324
17490
  const responses = {
17325
17491
  [code]: {
17326
- description: nothing ? "Success, with no content" : `Success: ${returns}`,
17327
- ...nothing ? {} : { content: { "application/json": { schema: schemaFor(returns, scope) } } }
17492
+ description: redirect ? `Redirect${returns && !nothing ? `: the Location header is the ${returns} the method answers` : ""}` : nothing ? `${STATUS_TEXT[code] ?? "Success"}, with no content` : `${STATUS_TEXT[code] ?? "Success"}: ${returns}`,
17493
+ ...redirect ? { headers: { Location: { description: "Where the client is sent", schema: { type: "string" } } } } : {},
17494
+ ...bodyless ? {} : { content: { "application/json": { schema: schemaFor(returns, scope) } } }
17328
17495
  }
17329
17496
  };
17330
17497
  const failure = failureOf(returns);
@@ -17340,14 +17507,14 @@ function enumDescription(values) {
17340
17507
  return ["Values:", ...values.map((v) => `- \`${v.name}\`${v.description ? `: ${v.description}` : ""}`)].join("\n");
17341
17508
  }
17342
17509
  function enumValuesFrom(schema) {
17343
- const described = /* @__PURE__ */ new Map();
17510
+ const described2 = /* @__PURE__ */ new Map();
17344
17511
  if (typeof schema.description === "string") {
17345
17512
  for (const line of schema.description.split("\n")) {
17346
17513
  const m = /^- `([^`]+)`: (.+)$/.exec(line.trim());
17347
- if (m) described.set(m[1], m[2]);
17514
+ if (m) described2.set(m[1], m[2]);
17348
17515
  }
17349
17516
  }
17350
- return schema.enum.filter((v) => typeof v === "string" && v.length > 0).map((name) => ({ name, ...described.has(name) ? { description: described.get(name) } : {} }));
17517
+ return schema.enum.filter((v) => typeof v === "string" && v.length > 0).map((name) => ({ name, ...described2.has(name) ? { description: described2.get(name) } : {} }));
17351
17518
  }
17352
17519
  function openApiPath(basePath, endpointPath) {
17353
17520
  const join31 = (a, b) => `${a.replace(/\/+$/, "")}/${b.replace(/^\/+/, "")}`;
@@ -17404,7 +17571,7 @@ function operationFor(method2, scope, entry) {
17404
17571
  const parameter = (p, where) => ({
17405
17572
  name: p.name,
17406
17573
  in: where,
17407
- required: where === "path" ? true : !p.optional,
17574
+ required: where === "path" ? true : !p.optional && !admitsNoValue(p.type),
17408
17575
  ...p.description ? { description: p.description } : {},
17409
17576
  schema: schemaFor(p.type, scope)
17410
17577
  });
@@ -17413,7 +17580,7 @@ function operationFor(method2, scope, entry) {
17413
17580
  const bodyParam = rest.length === 1 ? rest[0] : void 0;
17414
17581
  if (bodyVerbs.has(httpVerb) && bodyParam && scope.isObject(bodyParam.type)) {
17415
17582
  op.requestBody = {
17416
- required: !bodyParam.optional,
17583
+ required: !bodyParam.optional && !admitsNoValue(bodyParam.type),
17417
17584
  ...bodyParam.description ? { description: bodyParam.description } : {},
17418
17585
  content: { "application/json": { schema: schemaFor(bodyParam.type, scope) } }
17419
17586
  };
@@ -17425,8 +17592,8 @@ function operationFor(method2, scope, entry) {
17425
17592
  "application/json": {
17426
17593
  schema: {
17427
17594
  type: "object",
17428
- properties: Object.fromEntries(rest.map((p) => [p.name, schemaFor(p.type, scope)])),
17429
- required: rest.filter((p) => !p.optional).map((p) => p.name)
17595
+ properties: Object.fromEntries(rest.map((p) => [p.name, described(schemaFor(p.type, scope), p.description)])),
17596
+ required: rest.filter((p) => !p.optional && !admitsNoValue(p.type)).map((p) => p.name)
17430
17597
  }
17431
17598
  }
17432
17599
  }
@@ -17448,8 +17615,9 @@ function typeComponent(t, scope) {
17448
17615
  return {
17449
17616
  type: "object",
17450
17617
  title: t.name,
17451
- properties: Object.fromEntries(t.fields.map((f) => [f.name, schemaFor(f.type, scope)])),
17452
- required: t.fields.filter((f) => !f.optional).map((f) => f.name)
17618
+ // A field's description travels on its property; a `T?` field may be left out, like an optional one.
17619
+ properties: Object.fromEntries(t.fields.map((f) => [f.name, described(schemaFor(f.type, scope), f.description)])),
17620
+ required: t.fields.filter((f) => !f.optional && !admitsNoValue(f.type)).map((f) => f.name)
17453
17621
  };
17454
17622
  }
17455
17623
  var SIGNATURE_EXTENSION = "x-wairon-signature";
@@ -17554,8 +17722,8 @@ var OpenApiRouteCollisionError = class extends Error {
17554
17722
  };
17555
17723
  function renderDoc(snapshot, entries, context, opts = {}) {
17556
17724
  const { schemes, securityByEntry } = buildSecurity(entries);
17557
- const reached = /* @__PURE__ */ new Map();
17558
- const scope = schemaScope(snapshot.types, context.externals ?? /* @__PURE__ */ new Map(), reached);
17725
+ const registry = { reached: /* @__PURE__ */ new Map(), keyOf: /* @__PURE__ */ new Map() };
17726
+ const scope = schemaScope(snapshot.types, context.externals ?? /* @__PURE__ */ new Map(), registry);
17559
17727
  const paths = {};
17560
17728
  const taken = /* @__PURE__ */ new Map();
17561
17729
  for (const entry of entries) {
@@ -17577,15 +17745,12 @@ function renderDoc(snapshot, entries, context, opts = {}) {
17577
17745
  }
17578
17746
  }
17579
17747
  const schemas = {};
17580
- for (const t of snapshot.types) schemas[t.id] = typeComponent(t, scope);
17581
- const rendered = /* @__PURE__ */ new Set();
17582
17748
  for (let more = true; more; ) {
17583
17749
  more = false;
17584
- for (const [key2, { alias, def }] of [...reached]) {
17585
- if (rendered.has(key2)) continue;
17586
- rendered.add(key2);
17750
+ for (const [key2, { alias, def }] of [...registry.reached]) {
17751
+ if (key2 in schemas) continue;
17587
17752
  more = true;
17588
- schemas[key2] = typeComponent(def, scope.within(alias));
17753
+ schemas[key2] = typeComponent(def, alias !== void 0 ? scope.within(alias) : scope);
17589
17754
  }
17590
17755
  }
17591
17756
  const components = {};
@@ -17699,6 +17864,13 @@ function describedWith(description, note) {
17699
17864
  const text3 = [description, note].filter((x) => !!x).join(" ");
17700
17865
  return text3 ? { description: text3 } : {};
17701
17866
  }
17867
+ function leftOut(required, type) {
17868
+ return required || type.endsWith("?") ? {} : { optional: true };
17869
+ }
17870
+ function statedStatus(code, method2, verb) {
17871
+ if (code === void 0 || !/^[23]\d\d$/.test(code)) return {};
17872
+ return Number(code) === conventionalStatus(method2, verb) ? {} : { status: Number(code) };
17873
+ }
17702
17874
  function authFromSecurityScheme(scheme) {
17703
17875
  const desc = typeof scheme.description === "string" ? { description: scheme.description } : {};
17704
17876
  if (scheme.type === "apiKey") {
@@ -17757,10 +17929,11 @@ function fromOpenApi(document, projectName) {
17757
17929
  for (const p of op.parameters ?? []) {
17758
17930
  if (typeof p.name !== "string") continue;
17759
17931
  const schema = p.schema;
17932
+ const type = typeRefFromSchema(schema);
17760
17933
  params.push({
17761
17934
  name: p.name,
17762
- type: typeRefFromSchema(schema),
17763
- ...p.required === true ? {} : { optional: true },
17935
+ type,
17936
+ ...leftOut(p.required === true, type),
17764
17937
  ...describedWith(typeof p.description === "string" ? p.description : void 0, inlineEnumNote(schema))
17765
17938
  });
17766
17939
  }
@@ -17779,10 +17952,11 @@ function fromOpenApi(document, projectName) {
17779
17952
  const required = new Set(bodySchema.required ?? []);
17780
17953
  if (Object.keys(props).length) {
17781
17954
  for (const [pname, pschema] of Object.entries(props)) {
17955
+ const type = typeRefFromSchema(pschema);
17782
17956
  params.push({
17783
17957
  name: pname,
17784
- type: typeRefFromSchema(pschema),
17785
- ...required.has(pname) ? {} : { optional: true },
17958
+ type,
17959
+ ...leftOut(required.has(pname), type),
17786
17960
  ...describedWith(typeof pschema.description === "string" ? pschema.description : void 0, inlineEnumNote(pschema))
17787
17961
  });
17788
17962
  }
@@ -17792,7 +17966,7 @@ function fromOpenApi(document, projectName) {
17792
17966
  }
17793
17967
  const responses = op.responses ?? {};
17794
17968
  const jsonSchema = (r) => r?.content?.["application/json"]?.schema;
17795
- const successCode = Object.keys(responses).filter((c) => /^2\d\d$/.test(c)).sort()[0];
17969
+ const successCode = Object.keys(responses).filter((c) => /^2\d\d$/.test(c)).sort()[0] ?? Object.keys(responses).filter((c) => /^3\d\d$/.test(c)).sort()[0];
17796
17970
  const responseSchema = successCode !== void 0 ? jsonSchema(responses[successCode]) : void 0;
17797
17971
  const failureCode = ["default", ...Object.keys(responses).filter((c) => /^[45](\d\d|XX)$/i.test(c)).sort()].find((c) => jsonSchema(responses[c]) !== void 0);
17798
17972
  const success = responseSchema ? typeRefFromSchema(responseSchema) : "void";
@@ -17809,7 +17983,7 @@ function fromOpenApi(document, projectName) {
17809
17983
  signature: `${name}(${params.map((p) => `${p.name}: ${p.type}`).join(", ")}): ${returns}`,
17810
17984
  returns,
17811
17985
  params,
17812
- endpoint: { transport: "HTTP", method: verb.toUpperCase(), path: rawPath },
17986
+ endpoint: { transport: "HTTP", method: verb.toUpperCase(), path: rawPath, ...statedStatus(successCode, { name, returns, ...effect ? { effect } : {} }, verb) },
17813
17987
  ...guarantees.length ? { guarantees } : {},
17814
17988
  ...effect ? { effect } : {},
17815
17989
  ...ext ? { ext } : {}
@@ -17838,11 +18012,15 @@ function fromOpenApi(document, projectName) {
17838
18012
  id,
17839
18013
  name: typeof schema.title === "string" ? schema.title : id,
17840
18014
  kind: "value-object",
17841
- fields: Object.entries(props).map(([fname, fschema]) => ({
17842
- name: fname,
17843
- type: typeRefFromSchema(fschema),
17844
- ...required.has(fname) ? {} : { optional: true }
17845
- }))
18015
+ fields: Object.entries(props).map(([fname, fschema]) => {
18016
+ const type = typeRefFromSchema(fschema);
18017
+ return {
18018
+ name: fname,
18019
+ type,
18020
+ ...leftOut(required.has(fname), type),
18021
+ ...typeof fschema.description === "string" ? { description: fschema.description } : {}
18022
+ };
18023
+ })
17846
18024
  });
17847
18025
  }
17848
18026
  const securitySchemes = parsed.components?.securitySchemes ?? {};
@@ -18596,7 +18774,7 @@ function listPinnedExternals() {
18596
18774
  const config = loadProjectConfig();
18597
18775
  declared = config ? [
18598
18776
  ...declaredExternals(config),
18599
- ...declaredMembers(config).filter((m) => !m.problem && m.storage !== "contained").map((m) => ({ alias: m.alias, project: m.alias }))
18777
+ ...declaredMembers(config).filter((m) => !m.problem && m.storage !== "contained" && m.storage !== "path").map((m) => ({ alias: m.alias, project: m.alias }))
18600
18778
  ] : [];
18601
18779
  } catch {
18602
18780
  return [];
@@ -18715,7 +18893,7 @@ function foreignSurfaces() {
18715
18893
  }
18716
18894
  const root = getProjectRoot();
18717
18895
  for (const m of members) {
18718
- if (m.problem || m.storage !== "contained" || m.path === void 0 || out.has(m.alias)) continue;
18896
+ if (m.problem || m.storage !== "contained" && m.storage !== "path" || m.path === void 0 || out.has(m.alias)) continue;
18719
18897
  try {
18720
18898
  const surface = runWithProjectRoot(path15.resolve(root, m.path), () => projectOwnSurface("project"));
18721
18899
  out.set(m.alias, surface);
@@ -18730,7 +18908,7 @@ function withForeignTypes(snapshot, foreign) {
18730
18908
  const have = new Set(types.map((t) => t.id));
18731
18909
  const queue = [];
18732
18910
  const scan2 = (text3) => {
18733
- for (const token of (text3 ?? "").match(/[A-Za-z0-9_-]+::[A-Za-z0-9_-]+/g) ?? []) queue.push(token);
18911
+ for (const token of (text3 ?? "").match(/[A-Za-z0-9_-]+(?:::[A-Za-z0-9_-]+)+/g) ?? []) queue.push(token);
18734
18912
  };
18735
18913
  for (const e of snapshot.interfaces) for (const m of e.methods) {
18736
18914
  for (const p of m.params ?? []) scan2(p.type);
@@ -18749,12 +18927,14 @@ function withForeignTypes(snapshot, foreign) {
18749
18927
  const name = ref.slice(cut + 2);
18750
18928
  const pin2 = foreign.get(alias);
18751
18929
  if (!pin2) continue;
18752
- const defId = (pin2.exportedTypes ?? []).find((t) => t.id === name)?.type ?? name;
18930
+ const segment = (id) => id.includes("::") ? id.slice(id.lastIndexOf("::") + 2) : id;
18931
+ const shared = (id) => pin2.types.filter((d) => segment(d.id) === segment(id)).length > 1;
18932
+ const local = new Map(pin2.types.map((d) => [d.id, `${alias}::${(pin2.exportedTypes ?? []).find((t) => t.type === d.id)?.id ?? (shared(d.id) ? d.id : segment(d.id))}`]));
18933
+ const defId = (pin2.exportedTypes ?? []).find((t) => t.id === name)?.type ?? (pin2.types.some((d) => d.id === name) ? name : [...local].find(([, spelled]) => spelled === ref)?.[0]);
18753
18934
  const def = pin2.types.find((d) => d.id === defId);
18754
18935
  if (!def) continue;
18755
- const local = new Map(pin2.types.map((d) => [d.id, `${alias}::${(pin2.exportedTypes ?? []).find((t) => t.type === d.id)?.id ?? d.id}`]));
18756
18936
  local.set(def.id, ref);
18757
- const respell = (text3) => text3?.replace(/[A-Za-z0-9_][A-Za-z0-9_-]*/g, (token) => local.get(token) ?? token);
18937
+ const respell = (text3) => text3?.replace(/[A-Za-z0-9_][A-Za-z0-9_-]*(?:::[A-Za-z0-9_][A-Za-z0-9_-]*)*/g, (token) => local.get(token) ?? token);
18758
18938
  const expanded = {
18759
18939
  ...def,
18760
18940
  id: ref,
@@ -19016,6 +19196,10 @@ function pinExternals(aliases) {
19016
19196
  const declared = bindings.map((b) => b.external.alias);
19017
19197
  const named2 = aliases && aliases.length > 0 ? aliases : void 0;
19018
19198
  const unknown = (named2 ?? []).filter((a) => !declared.includes(a));
19199
+ const live = unknown.filter((a) => declaredMembers(loadProjectConfig() ?? {}).some((m) => m.alias === a && !m.problem && (m.storage === "contained" || m.storage === "path")));
19200
+ if (live.length > 0 && live.length === unknown.length) {
19201
+ throw new WaironError(`${live.map((a) => `"${a}"`).join(", ")} ${live.length === 1 ? "is a member" : "are members"} of this project, read live where ${live.length === 1 ? "it is" : "they are"} stored (inside it, or a \`../\` sibling checkout) and composed in the family run like any contained member \u2014 a member is never pinned. Pin only an external (\`externals add\`), or a git or hosted member. Nothing was written.`);
19202
+ }
19019
19203
  if (unknown.length) throw new UnknownExternalAliasError(unknown, declared);
19020
19204
  const kept = named2 ? bindings.filter((b) => named2.includes(b.external.alias)) : bindings;
19021
19205
  const lock = externalsRepository.readLock() ?? { externals: {} };
@@ -19323,7 +19507,23 @@ function listExternals() {
19323
19507
  return [...declared, ...orphans];
19324
19508
  }
19325
19509
  function listConsumers2(search) {
19326
- return listConsumers(search);
19510
+ const consumers = listConsumers(search);
19511
+ if (consumers.length === 0) return consumers;
19512
+ let own2;
19513
+ try {
19514
+ own2 = projectOwnSurface("project");
19515
+ } catch {
19516
+ return consumers;
19517
+ }
19518
+ const held = new Map(own2.interfaces.map((e) => [e.id, new Set(e.methods.map((m) => m.name))]));
19519
+ return consumers.map((c) => {
19520
+ const gone = (c.uses ?? []).flatMap((u) => {
19521
+ const methods = held.get(u.publicName);
19522
+ return methods ? u.members.filter((m) => m !== "type" && !m.startsWith("capability:") && !methods.has(m)).map((m) => `${u.publicName}.${m}`) : [];
19523
+ });
19524
+ if (gone.length === 0) return c;
19525
+ return { ...c, broken: [.../* @__PURE__ */ new Set([...c.broken ?? [], ...gone])].sort() };
19526
+ });
19327
19527
  }
19328
19528
  function diff(against) {
19329
19529
  const saved = against !== void 0 && fs9.existsSync(against) && fs9.statSync(against).isFile();
@@ -19819,6 +20019,7 @@ function readMembers(body, mode) {
19819
20019
  if (seg.startsWith("[") || seg.startsWith("(") || seg.startsWith("<") || seg.startsWith("...") || /^new\b/.test(seg)) continue;
19820
20020
  const name = IDENT.exec(seg)?.[0];
19821
20021
  if (name === void 0 || name === "constructor") continue;
20022
+ if (mode !== "type" && name.startsWith("_")) continue;
19822
20023
  let i = name.length;
19823
20024
  while (seg[i] === "?" || seg[i] === "!") i++;
19824
20025
  if (accessor) {
@@ -19881,7 +20082,7 @@ function tagBefore(clean, docs, start) {
19881
20082
  function lineAt(clean, index) {
19882
20083
  return clean.slice(0, index).split("\n").length;
19883
20084
  }
19884
- function commonJsDeclaration(value, base) {
20085
+ function commonJsDeclaration(value, base, clean = "", docs = []) {
19885
20086
  const t = value.trim();
19886
20087
  if (t.startsWith("{")) return { ...base, kind: "object", members: readMembers(t.slice(1, closeOf(t, 0) - 1), "object") };
19887
20088
  const params = functionValueParams(t);
@@ -19891,25 +20092,68 @@ function commonJsDeclaration(value, base) {
19891
20092
  const open = cls[0].length - 1;
19892
20093
  return { ...base, kind: "class", members: readMembers(t.slice(open + 1, closeOf(t, open) - 1), "class") };
19893
20094
  }
20095
+ const ident = /^([A-Za-z_$][\w$]*)\s*$/.exec(t);
20096
+ if (ident && clean) {
20097
+ const local = localDeclaration(clean, docs, ident[1]);
20098
+ const tag = base.tag ?? local?.tag;
20099
+ if (local) return { ...local, name: base.name, ...tag ? { tag } : {} };
20100
+ }
20101
+ return null;
20102
+ }
20103
+ function localDeclaration(clean, docs, name) {
20104
+ const escaped = name.replace(/\$/g, "\\$");
20105
+ const re = new RegExp(`(?<![\\w$.])(?:declare\\s+)?(?:(?:abstract\\s+)?(class)|(?:async\\s+)?(function)\\s*\\*?|(interface)|(?:const|let|var))\\s+${escaped}(?![\\w$])`, "g");
20106
+ for (const match2 of clean.matchAll(re)) {
20107
+ const start = match2.index ?? 0;
20108
+ const tag = tagBefore(clean, docs, start);
20109
+ const base = { name, line: lineAt(clean, start), ...tag ? { tag } : {} };
20110
+ let i = start + match2[0].length;
20111
+ if (match2[1] || match2[3]) {
20112
+ const open = clean.indexOf("{", i);
20113
+ if (open < 0) continue;
20114
+ return { ...base, kind: match2[1] ? "class" : "interface", members: readMembers(clean.slice(open + 1, closeOf(clean, open) - 1), match2[1] ? "class" : "type") };
20115
+ }
20116
+ if (match2[2]) {
20117
+ i = skipTypeParams(clean, i);
20118
+ i = skipSpace(clean, i);
20119
+ return { ...base, kind: "function", params: clean[i] === "(" ? paramNames(clean.slice(i + 1, closeOf(clean, i) - 1)) : [], members: [] };
20120
+ }
20121
+ const eq = clean.indexOf("=", i);
20122
+ if (eq < 0) continue;
20123
+ const decl = commonJsDeclaration(valueAt2(clean, eq + 1), base);
20124
+ if (decl) return decl;
20125
+ }
19894
20126
  return null;
19895
20127
  }
19896
20128
  var CJS_NAMED_RE = /(?<![\w$.])(?:module\.)?exports\.([A-Za-z_$][\w$]*)\s*=(?!=)/g;
19897
- var CJS_WHOLE_RE = /(?<![\w$.])module\.exports\s*=(?!=)/g;
20129
+ var CJS_WHOLE_RE = /(?<![\w$.])(?:module\.exports|export)\s*=(?![=>])/g;
19898
20130
  function valueAt2(clean, from) {
19899
20131
  return splitTop(clean.slice(from), ";")[0] ?? "";
19900
20132
  }
20133
+ function wholeExportName(value) {
20134
+ const named2 = /^(?:class|(?:async\s+)?function\s*\*?)\s+([A-Za-z_$][\w$]*)/.exec(value)?.[1] ?? /^([A-Za-z_$][\w$]*)\s*$/.exec(value)?.[1];
20135
+ return named2 === "require" ? void 0 : named2;
20136
+ }
19901
20137
  function readCommonJs(clean, docs) {
19902
20138
  const out = [];
19903
20139
  for (const match2 of clean.matchAll(CJS_NAMED_RE)) {
19904
20140
  const start = match2.index ?? 0;
19905
20141
  const tag = tagBefore(clean, docs, start);
19906
- const decl = commonJsDeclaration(valueAt2(clean, start + match2[0].length), { name: match2[1], line: lineAt(clean, start), ...tag ? { tag } : {} });
20142
+ const decl = commonJsDeclaration(valueAt2(clean, start + match2[0].length), { name: match2[1], line: lineAt(clean, start), ...tag ? { tag } : {} }, clean, docs);
19907
20143
  if (decl) out.push(decl);
19908
20144
  }
19909
20145
  for (const match2 of clean.matchAll(CJS_WHOLE_RE)) {
19910
20146
  const start = match2.index ?? 0;
19911
20147
  const open = skipSpace(clean, start + match2[0].length);
19912
- if (clean[open] !== "{") continue;
20148
+ const wholeTag = tagBefore(clean, docs, start);
20149
+ if (clean[open] !== "{") {
20150
+ const value = valueAt2(clean, open).trim();
20151
+ const named2 = wholeExportName(value);
20152
+ if (named2 === void 0) continue;
20153
+ const decl = commonJsDeclaration(value, { name: named2, line: lineAt(clean, start), ...wholeTag ? { tag: wholeTag } : {} }, clean, docs);
20154
+ if (decl) out.push(decl);
20155
+ continue;
20156
+ }
19913
20157
  const body = clean.slice(open + 1, closeOf(clean, open) - 1);
19914
20158
  let offset = open + 1;
19915
20159
  for (const raw of splitTop(body, ",")) {
@@ -19918,31 +20162,60 @@ function readCommonJs(clean, docs) {
19918
20162
  if (found >= 0) offset = found + raw.length;
19919
20163
  const seg = raw.replace(/^async\s+/, "");
19920
20164
  const name = IDENT.exec(seg)?.[0];
19921
- if (name === void 0) continue;
19922
- const tag = tagBefore(clean, docs, at);
20165
+ if (name === void 0 || name.startsWith("_")) continue;
20166
+ const own2 = tagBefore(clean, docs, at);
20167
+ const tag = own2 ?? wholeTag;
19923
20168
  const base = { name, line: lineAt(clean, at), ...tag ? { tag } : {} };
19924
20169
  const rest = seg.slice(name.length).trimStart();
19925
20170
  if (rest.startsWith("(")) {
19926
20171
  out.push({ ...base, kind: "function", params: paramNames(rest.slice(1, closeOf(rest, 0) - 1)), members: [] });
19927
20172
  } else if (rest.startsWith(":")) {
19928
- const decl = commonJsDeclaration(rest.slice(1), base);
20173
+ const decl = commonJsDeclaration(rest.slice(1), base, clean, docs);
19929
20174
  if (decl) out.push(decl);
20175
+ } else if (rest === "") {
20176
+ const local = localDeclaration(clean, docs, name);
20177
+ const localTag = own2 ?? local?.tag ?? wholeTag;
20178
+ if (local) out.push({ ...local, ...localTag ? { tag: localTag } : {} });
19930
20179
  }
19931
20180
  }
19932
20181
  }
19933
20182
  return out;
19934
20183
  }
20184
+ function readTypedefs(text3, clean) {
20185
+ const out = [];
20186
+ for (const comment of text3.matchAll(/\/\*\*[\s\S]*?\*\//g)) {
20187
+ const body = comment[0];
20188
+ const typedef = /@typedef\s+(?:\{[^}]*\}\s*)?([A-Za-z_$][\w$]*)/.exec(body);
20189
+ if (!typedef) continue;
20190
+ const members = [];
20191
+ for (const prop of body.matchAll(/@prop(?:erty)?\s+(?:\{([^}]*)\}\s*)?\[?([A-Za-z_$][\w$]*)/g)) {
20192
+ members.push(prop[1] ? { name: prop[2], type: prop[1].trim() } : { name: prop[2] });
20193
+ }
20194
+ const tag = TAG_RE.exec(body);
20195
+ out.push({
20196
+ name: typedef[1],
20197
+ kind: members.length > 0 ? "type" : "alias",
20198
+ members,
20199
+ line: lineAt(clean, (comment.index ?? 0) + body.indexOf("@typedef")),
20200
+ ...tag ? { tag: `${tag[1]}::${tag[2]}` } : {}
20201
+ });
20202
+ }
20203
+ return out;
20204
+ }
19935
20205
  function readUnreadForms(text3) {
19936
- const { clean } = scan(text3);
20206
+ const { clean, docs } = scan(text3);
19937
20207
  const forms = /* @__PURE__ */ new Set();
19938
20208
  const shown2 = (value) => /^require\s*\(/.test(value) ? "require(\u2026)" : `${IDENT.exec(value)?.[0] ?? "\u2026"}${/^[A-Za-z_$][\w$.]*\s*\(/.test(value) ? "(\u2026)" : ""}`;
19939
20209
  for (const match2 of clean.matchAll(CJS_WHOLE_RE)) {
19940
20210
  const value = valueAt2(clean, (match2.index ?? 0) + match2[0].length).trim();
19941
- if (!value.startsWith("{")) forms.add(`module.exports = ${shown2(value)}`);
20211
+ if (value.startsWith("{")) continue;
20212
+ const named2 = wholeExportName(value);
20213
+ if (named2 !== void 0 && commonJsDeclaration(value, { name: named2, line: 0 }, clean, docs) !== null) continue;
20214
+ forms.add(`${match2[0].startsWith("export") ? "export" : "module.exports"} = ${shown2(value)}`);
19942
20215
  }
19943
20216
  for (const match2 of clean.matchAll(CJS_NAMED_RE)) {
19944
20217
  const value = valueAt2(clean, (match2.index ?? 0) + match2[0].length).trim();
19945
- if (commonJsDeclaration(value, { name: match2[1], line: 0 }) === null) forms.add(`exports.${match2[1]} = ${shown2(value)}`);
20218
+ if (commonJsDeclaration(value, { name: match2[1], line: 0 }, clean, docs) === null) forms.add(`exports.${match2[1]} = ${shown2(value)}`);
19946
20219
  }
19947
20220
  return [...forms];
19948
20221
  }
@@ -19984,7 +20257,7 @@ function readDeclarations(text3) {
19984
20257
  }
19985
20258
  }
19986
20259
  const seen = new Set(out.map((d) => d.name));
19987
- for (const decl of readCommonJs(clean, docs)) {
20260
+ for (const decl of [...readCommonJs(clean, docs), ...readTypedefs(text3, clean)]) {
19988
20261
  if (!seen.has(decl.name)) out.push(decl);
19989
20262
  seen.add(decl.name);
19990
20263
  }
@@ -20115,6 +20388,13 @@ function releaseStampAgainst(record, running) {
20115
20388
  if (isNewerVersion(running, stamp)) return "newer";
20116
20389
  return isNewerVersion(stamp, running) ? "older" : "same";
20117
20390
  }
20391
+ function releaseStampProblem(record, running) {
20392
+ const reading = releaseStampAgainst(record, running);
20393
+ if (reading === "older" || reading === "same") return null;
20394
+ const stamp = record.validatorVersion;
20395
+ if (reading === "newer") return `its release stamp says wairon ${String(stamp)} took it, a newer release than this one (${running})`;
20396
+ return typeof stamp === "string" && stamp !== "" ? `its release stamp "${stamp}" is no wairon version` : "it carries no release stamp";
20397
+ }
20118
20398
  function stampRecordsGateParts(stamp) {
20119
20399
  return stamp === GATE_PARTS_SINCE || isNewerVersion(GATE_PARTS_SINCE, stamp);
20120
20400
  }
@@ -20872,6 +21152,18 @@ function specsUsing(family, alias, publicName) {
20872
21152
  const specs = family.authoredReferences.filter((r) => (family.owners.get(r.specId) ?? "") === "" && (r.authored.startsWith(`${alias}::`) || (r.importedVia ?? "").split(", ").includes(alias)) && (publicName === void 0 || (r.publicName ?? r.authored.split("::")[r.form === "import" ? 0 : 1]) === publicName)).map((r) => r.specId);
20873
21153
  return [...new Set(specs)].sort();
20874
21154
  }
21155
+ function bindingModulesOf(specs) {
21156
+ if (specs.length === 0) return [];
21157
+ const named2 = new Set(specs);
21158
+ const componentOf = new Map(loadInterfaceSpecs().map((i) => [i.id, i.component]));
21159
+ const out = /* @__PURE__ */ new Set();
21160
+ for (const impl of loadImplementationSpecs()) {
21161
+ if (impl.id.includes("::") || !impl.bindings?.length) continue;
21162
+ if (!named2.has(impl.id) && !named2.has(impl.contract) && !named2.has(componentOf.get(impl.contract) ?? "")) continue;
21163
+ for (const b of impl.bindings) out.add(b.replace(/\\/g, "/"));
21164
+ }
21165
+ return [...out].sort();
21166
+ }
20875
21167
  function movedUse(use) {
20876
21168
  return `${useName(use)} ${howMoved(use).replace(/^was renamed/, "renamed").replace(/^is gone/, "gone")}`;
20877
21169
  }
@@ -20892,10 +21184,11 @@ function adviseOn(status, family, config) {
20892
21184
  const broken = status.uses.filter(isBroken);
20893
21185
  const users = [...new Set(broken.flatMap((u) => specsUsing(family, status.alias, u.publicName)))].sort();
20894
21186
  const renamed2 = broken.some((u) => u.state === "renamed" || u.detail?.includes("follow the rename"));
21187
+ const bindings = bindingModulesOf(users);
20895
21188
  return advisory(
20896
21189
  config,
20897
21190
  "EXTERNAL_LIVE_INCOMPATIBLE",
20898
- `${who} moved in its live producer since it was pinned: ${broken.map(movedUse).join("; ")}. Used by ${users.length ? users.map((s) => `"${s}"`).join(", ") : "this project's references"}. Adapt the uses${renamed2 ? " (follow the rename)" : ""}, then re-pin with ${repin(status.alias)}. Advisory: the pin still gates.`
21191
+ `${who} moved in its live producer since it was pinned: ${broken.map(movedUse).join("; ")}. Used by ${users.length ? users.map((s) => `"${s}"`).join(", ") : "this project's references"}. ` + (bindings.length ? `Binding module(s) declared there: ${bindings.map((b) => `"${b}"`).join(", ")} \u2014 compared with the pin, not the live producer, so ${bindings.length === 1 ? "it draws" : "they draw"} BINDING_DRIFT once the re-pin lands; follow the move there too. ` : "") + `Adapt the uses${renamed2 ? " (follow the rename)" : ""}, then re-pin with ${repin(status.alias)}. Advisory: the pin still gates.`
20899
21192
  );
20900
21193
  }
20901
21194
  case "drifted":
@@ -21146,10 +21439,13 @@ function approvalAt(node, ceiling) {
21146
21439
  if (!record) return base;
21147
21440
  const same = (id) => !!id && record.stateId.algorithm === id.algorithm && record.stateId.digest === id.digest;
21148
21441
  const matches = same(current2) || same(current2.asRecorded);
21149
- const partsIgnored = gatePartsProblem(record, current2) ?? void 0;
21442
+ const partsIgnored = gatePartsProblem(record, current2) ?? (matches && record.gateParts === void 0 && current2.parts ? `the record carries none, though every record wairon ${GATE_PARTS_SINCE} and later writes carries them` : void 0);
21150
21443
  const parts = usableGateParts(record, current2);
21151
21444
  const moved = matches || !parts ? null : movedGateParts({ gateParts: parts }, current2.parts);
21152
21445
  const release = matches ? void 0 : releaseVerdict(record, current2);
21446
+ const stampClause = releaseStampProblem(record, WAIRON_VERSION);
21447
+ const stampNamed = release?.reason !== void 0 && release.reason.includes(String(record.validatorVersion ?? "no release stamp"));
21448
+ const stampProblem = stampClause !== null && !stampNamed ? stampClause : void 0;
21153
21449
  const owed = owedBy(record);
21154
21450
  const covered = (matches || release?.carried === true) && owed === void 0;
21155
21451
  return {
@@ -21160,7 +21456,8 @@ function approvalAt(node, ceiling) {
21160
21456
  ...release ? { release } : {},
21161
21457
  ...moved && moved.length > 0 && !release ? { inputsMoved: moved } : {},
21162
21458
  ...owed !== void 0 ? { owed } : {},
21163
- ...partsIgnored !== void 0 ? { partsIgnored } : {}
21459
+ ...partsIgnored !== void 0 ? { partsIgnored } : {},
21460
+ ...stampProblem !== void 0 ? { stampProblem } : {}
21164
21461
  };
21165
21462
  });
21166
21463
  }
@@ -21251,8 +21548,23 @@ function owedBy(record) {
21251
21548
  for (const [alias, uses] of Object.entries(unrecorded())) {
21252
21549
  reasons.push(`external "${alias}" is used beyond its pin: ${uses.join(", ")} (used now, but not in the lock)`);
21253
21550
  }
21551
+ const unloadable = unloadablePacks();
21552
+ if (unloadable.length > 0) reasons.push(`extension pack(s) selected in .wai/project.yaml cannot load \u2014 ${unloadable.join("; ")}`);
21254
21553
  return reasons.length > 0 ? reasons.join("; ") : void 0;
21255
21554
  }
21555
+ function unloadablePacks() {
21556
+ let loaded;
21557
+ try {
21558
+ loaded = loadProjectExtensions();
21559
+ } catch {
21560
+ return [];
21561
+ }
21562
+ return [
21563
+ ...loaded.errors,
21564
+ // A selection that resolved with a warning (unpinned, unfetchable origin) still loaded.
21565
+ ...(loaded.selectionFailures ?? []).filter((f) => f.severity === "error").map((f) => f.message)
21566
+ ];
21567
+ }
21256
21568
  function pinOf(parent, alias, subject) {
21257
21569
  if (!parent) return "unpinned";
21258
21570
  const recorded = parent.members ? ownHas(parent.members, alias) ? ownGet(parent.members, alias).subject ?? "never" : void 0 : ownGet(parent.children, alias);
@@ -23425,9 +23737,9 @@ function draftContext(ctx, interfaceId, component) {
23425
23737
  const intf = ctx.interfaceMap.get(interfaceId);
23426
23738
  return ctx.isComponentDraft(component) || intf?.status === "draft" || intf?.status === "design";
23427
23739
  }
23428
- function projectOf(componentKey) {
23429
- const at = componentKey.lastIndexOf("::");
23430
- return at < 0 ? "" : componentKey.slice(0, at);
23740
+ function projectOf(componentKey2) {
23741
+ const at = componentKey2.lastIndexOf("::");
23742
+ return at < 0 ? "" : componentKey2.slice(0, at);
23431
23743
  }
23432
23744
  function targetComponent(target) {
23433
23745
  return target.slice(0, target.lastIndexOf("."));
@@ -28604,7 +28916,7 @@ function writeCandidates(ctx) {
28604
28916
  }
28605
28917
  }
28606
28918
  };
28607
- const receiving = (call.receivers ?? []).flatMap((r) => realization.componentsAt(r.path));
28919
+ const receiving = (call.receivers ?? []).flatMap((r) => realization.componentsAt(r.path)).filter((c) => c.id !== caller.id);
28608
28920
  if (receiving.length > 0) {
28609
28921
  for (const callee of receiving) take(callee);
28610
28922
  return out;
@@ -28711,12 +29023,13 @@ function describeMiss(m) {
28711
29023
  var callConformanceRule = {
28712
29024
  name: "call-conformance",
28713
29025
  judges: "code",
28714
- description: "Code\u2194spec Level 3: the CLAIMED call \u2194 realized call relation, judged both ways against the method's own source file (its sourcePath, else the implementation's), at exact analysis grade only. A method claims a call two ways and they face one check: a narrative `call` step, and an entry of the declared `calls` a narrative-less method reaches its collaborators through. They assert the same thing, and the step number is no part of the claim \u2014 this check never reads order, so it is only how a finding points at one. Forward: every claim must be realized by a call whose callee RESOLVES TO one of the target method's own source files \u2014 the target's contract name or a per-method `symbol` override, closed transitively over the named helpers the realized function calls, and resolved against every file the call CAN have reached: a `this.<field>.<method>()` receiver followed through the field's DECLARED TYPE, a `new Class(...).<method>()` receiver through the module its CLASS NAME came from, a plain `<name>.<method>()` receiver through the type the file ANNOTATES that name with \u2014 a parameter's, or an annotated variable's, which is how a module that wires its collaborators as closures writes every one of its calls \u2014 a `fn(...).<method>()` receiver through the type fn RETURNS where the code settles it, and a `this.<method>()` receiver through the class whose own method it sits in, and a receiver CHAIN (`this.<field>.<property>.<method>()`, `<name>.<property>.<method>()`) link by link through each link's declared type \u2014 a declared type read through a member-preserving utility (Pick, Omit, Partial, Required, Readonly) and one alias hop; a package specifier naming a package of this repository resolves like a relative one, and an export alias (`export { a as b }`, an adapter object property `{ b: a }`) forwards by identity like a re-export. A matching call whose origin a pure model cannot resolve is reported apart as CALL_ORIGIN_UNRESOLVED, which NAMES the shape it could not follow and asks for nothing \u2014 a coverage hole in the reader, reported for the reason CONFORMANCE_DEGRADED is: a silently degraded gate is worse than a degraded one. Where that claim's target has no code yet \u2014 no file any of its implementations names exists, the normal state after the first component of a service is implemented from its brief through a port to a collaborator still planned \u2014 the call is planned rather than unresolved (CALL_TARGET_PLANNED, a notice, as SOURCE_FILE_PLANNED is), and it is judged like any other claim once the target's realization begins. A finding names a landing only from the PROVEN tier so that widening what a call reached can accept a claim but never accuse one, and a target that names no file of its own falls back to name membership. Where a type checker read the file, a claim is also realized by any call or reference in the bodies the function reaches \u2014 and in the unowned code they call \u2014 that the checker lands on the target: its file under a name it is realized by, a class realizing the port the call was typed by, or what the target forwards to by identity \u2014 whatever the receiver's spelling (a type-only or `export *` barrel, a port declared in a shared module, a dependency bag typed inline); a claim it does not land keeps the shape tier's answer, since the checker's landing on a declaration says what a receiver is TYPED as, never where the call went instead. N:1 identity \u2014 a caller that IS the target \u2014 is judged on the body, the same anchor in the same file, never on a shared name: a Portal method forwarding to a same-named Orchestrator method elsewhere must make that call. Converse: a call that resolves to a modelled method of ANOTHER component in the SAME file crosses a component boundary while looking local, so the method must claim it \u2014 in a step or in `calls` (UNDECLARED_COLOCATED_CALL) \u2014 judged on the proven tier alone, and a same-file private helper is no modelled method and is never reported; a function forwarding by identity to a modelled method written in another file IS that method, so what it calls is that method's subject. And a call \u2014 or a reference the code takes as a value \u2014 that the TypeScript type checker resolves into ANOTHER file, onto a write- or lifecycle-effect contract method of another component, or, from a Portal method whose body is its own, onto a verb of an Orchestrator, Supervisor or Actor whose effect is a write \u2014 declared (write, lifecycle or io), or, where its contract declares none, read off the verb's own narrative transitively: a call step or declared call reaching a method that changes state makes it a write, only reads or none a read, and a verb that settles nothing (no narrative and no declared calls, a dispatch step, a target outside this tree, a non-workflow method declaring no effect) is reported as an unnarrated call whose effect is undeclared, never as a write (a Portal changes state only by dispatching to such a verb, so its narrative is the list of those it may reach), that the method claims nowhere is a mutation the design says the method never makes (UNDECLARED_WRITE_CALL): narrate it, declare it in `calls`, or remove it \u2014 judged in the bodies the method's function reaches and in the UNOWNED code they call (a function in a file no component realizes, or one that is none of its file's components' modelled methods, read as if inlined at the call site, transitively to a bounded depth, the finding naming the hops \u2014 but a member of the CLASS a workflow component (Orchestrator, Supervisor, Actor) is realized by, one of the classes holding its modelled methods, is that component's code even where its contract does not declare it, so the walk stops there: a claim the function answers by calling such a member in place of the claimed method is CALL_STEP_UNREALIZED naming the method the code calls instead, the shape a rename made in the code alone leaves, and a Portal's call to one no missed claim names is an unnarrated call to a method whose effect no narrative settles, never read through its body as the Portal's own write), never for a Portal's call into a data component (PORTAL_WRITE_SHORTCUT_IN_CODE's subject), and only where a type checker was loaded. A call there the checker cannot follow \u2014 its receiver typed any or unknown, cast to either or to an index signature, typed by an index signature naming no member of that name, or its member picked by a computed key \u2014 under the name of such a write the method claims nowhere (where the checker recorded the receiver's original type \u2014 every cast, non-null mark and parenthesis taken off, an `await (x)` written outside an async function read as the x it means, nullability set aside so an optional field is the class it holds, a name bound once by a const read through its initializer, and an interface or shape no class realizes landing on the file declaring it \u2014 a write of the component that type lands on, any of its writes when a computed key names none; else a write of that name in the method's own subsystem or one its component depends into) fails closed as CALL_ORIGIN_UNRESOLVED: neither proven an unnarrated write nor cleared. A member picked by a key that names no single member, on a receiver that was one of this project's classes before any cast, is such a call however the receiver or the key is cast and whether it is invoked on the spot or taken as a value and invoked later (`const f = store[k]; f.call(store, \u2026)`); a key the checker types as one literal names its member by that literal's value, never by the variable holding it. A method that claims nothing at all is judged in neither direction: what it leaves unsaid is UNUSED_*'s subject. Order, arguments and conditions stay unverified, dispatch steps (runtime-table routed) are no claim about a call site, a malformed declared reference is MALFORMED_DECLARED_CALL's finding and names no target here, the conformance dial (off) skips, and weaker analysis grades never guess.",
29026
+ description: "Code\u2194spec Level 3: the CLAIMED call \u2194 realized call relation, judged both ways against the method's own source file (its sourcePath, else the implementation's), at exact analysis grade only. A method claims a call two ways and they face one check: a narrative `call` step, and an entry of the declared `calls` a narrative-less method reaches its collaborators through. They assert the same thing, and the step number is no part of the claim \u2014 this check never reads order, so it is only how a finding points at one. Forward: every claim must be realized by a call whose callee RESOLVES TO one of the target method's own source files \u2014 the target's contract name or a per-method `symbol` override, closed transitively over the named helpers the realized function calls, and resolved against every file the call CAN have reached: a `this.<field>.<method>()` receiver followed through the field's DECLARED TYPE, a `new Class(...).<method>()` receiver through the module its CLASS NAME came from, a plain `<name>.<method>()` receiver through the type the file ANNOTATES that name with \u2014 a parameter's, or an annotated variable's, which is how a module that wires its collaborators as closures writes every one of its calls \u2014 a `fn(...).<method>()` receiver through the type fn RETURNS where the code settles it, and a `this.<method>()` receiver through the class whose own method it sits in, and a receiver CHAIN (`this.<field>.<property>.<method>()`, `<name>.<property>.<method>()`) link by link through each link's declared type \u2014 a declared type read through a member-preserving utility (Pick, Omit, Partial, Required, Readonly) and one alias hop; a package specifier naming a package of this repository resolves like a relative one, and an export alias (`export { a as b }`, an adapter object property `{ b: a }`) forwards by identity like a re-export. A matching call whose origin a pure model cannot resolve is reported apart as CALL_ORIGIN_UNRESOLVED, which NAMES the shape it could not follow and asks for nothing \u2014 a coverage hole in the reader, reported for the reason CONFORMANCE_DEGRADED is: a silently degraded gate is worse than a degraded one. Where that claim's target has no code yet \u2014 no file any of its implementations names exists, the normal state after the first component of a service is implemented from its brief through a port to a collaborator still planned \u2014 the call is planned rather than unresolved (CALL_TARGET_PLANNED, a notice, as SOURCE_FILE_PLANNED is), and it is judged like any other claim once the target's realization begins. A finding names a landing only from the PROVEN tier so that widening what a call reached can accept a claim but never accuse one, and a target that names no file of its own falls back to name membership. Where a type checker read the file, a claim is also realized by any call or reference in the bodies the function reaches \u2014 and in the unowned code they call \u2014 that the checker lands on the target: its file under a name it is realized by, a class realizing the port the call was typed by, or what the target forwards to by identity \u2014 whatever the receiver's spelling (a type-only or `export *` barrel, a port declared in a shared module, a dependency bag typed inline); a claim it does not land keeps the shape tier's answer, since the checker's landing on a declaration says what a receiver is TYPED as, never where the call went instead. N:1 identity \u2014 a caller that IS the target \u2014 is judged on the body, the same anchor in the same file, never on a shared name: a Portal method forwarding to a same-named Orchestrator method elsewhere must make that call. Converse: a call that resolves to a modelled method of ANOTHER component in the SAME file crosses a component boundary while looking local, so the method must claim it \u2014 in a step or in `calls` (UNDECLARED_COLOCATED_CALL) \u2014 judged on the proven tier alone, and a same-file private helper is no modelled method and is never reported; a function forwarding by identity to a modelled method written in another file IS that method, so what it calls is that method's subject. And a call \u2014 or a reference the code takes as a value \u2014 that the TypeScript type checker resolves into ANOTHER file, onto a write- or lifecycle-effect contract method of another component, or, from a Portal method whose body is its own, onto a verb of an Orchestrator, Supervisor or Actor whose effect is a write \u2014 declared (write, lifecycle or io), or, where its contract declares none, read off the verb's own narrative transitively: a call step or declared call reaching a method that changes state makes it a write, only reads or none a read, and a verb that settles nothing (no narrative and no declared calls, a dispatch step, a target outside this tree, a non-workflow method declaring no effect) is reported as an unnarrated call whose effect is undeclared, never as a write (a Portal changes state only by dispatching to such a verb, so its narrative is the list of those it may reach), that the method claims nowhere is a mutation the design says the method never makes (UNDECLARED_WRITE_CALL): narrate it, declare it in `calls`, or remove it \u2014 judged in the bodies the method's function reaches and in the UNOWNED code they call (a function in a file no component realizes, or one that is none of its file's components' modelled methods, read as if inlined at the call site, transitively to a bounded depth, the finding naming the hops \u2014 but a member of the CLASS a workflow component (Orchestrator, Supervisor, Actor) is realized by, one of the classes holding its modelled methods, is that component's code even where its contract does not declare it, so the walk stops there: a claim the function answers by calling such a member in place of the claimed method is CALL_STEP_UNREALIZED naming the method the code calls instead, the shape a rename made in the code alone leaves, and a Portal's call to one no missed claim names is an unnarrated call to a method whose effect no narrative settles, never read through its body as the Portal's own write), never for a Portal's call into a data component (PORTAL_WRITE_SHORTCUT_IN_CODE's subject), and only where a type checker was loaded. A call there the checker cannot follow, or a member read by name off such a receiver and taken as a value (`const f = (store as any).save; f(x)`) \u2014 its receiver typed any or unknown however it came to be (a cast, an untyped or any-typed parameter of a closure, a function result typed any), cast to either or to an index signature, typed by an index signature naming no member of that name, or its member picked by a computed key \u2014 under the name of such a write the method claims nowhere (where the checker recorded the receiver's original type \u2014 every cast, non-null mark and parenthesis taken off, an `await (x)` written outside an async function read as the x it means, nullability set aside so an optional field is the class it holds, a name bound once by a const read through its initializer, and an interface or shape no class realizes landing on the file declaring it \u2014 a write of the component that type lands on, any of its writes when a computed key names none; else a write of that name in the method's own subsystem or one its component depends into \u2014 the design's write set, which a data component whose code is still planned has too; a receiver landing only on the caller's own file, a port it declares for itself, leaves it to that name as well) fails closed as CALL_ORIGIN_UNRESOLVED: neither proven an unnarrated write nor cleared. A member picked by a key that names no single member, on a receiver that was one of this project's classes before any cast, is such a call however the receiver or the key is cast and whether it is invoked on the spot or taken as a value and invoked later (`const f = store[k]; f.call(store, \u2026)`); a key the checker types as one literal names its member by that literal's value, never by the variable holding it. A method that claims nothing at all is judged in neither direction: what it leaves unsaid is UNUSED_*'s subject. Order, arguments and conditions stay unverified, dispatch steps (runtime-table routed) are no claim about a call site, a malformed declared reference is MALFORMED_DECLARED_CALL's finding and names no target here, the conformance dial set off skips the forward direction \u2014 whether the claims are found in the code \u2014 but never the converse, so off is no way past an unnarrated write or a call the analysis cannot follow; a claim whose target's code is in ANOTHER language than the caller's file (every file the target's implementations name is in a language a TypeScript call graph does not read, or, naming none, the member or project it belongs to declares such a target language) is not judged by this call graph at all, and the language boundary is said once per project and language (CALL_ACROSS_LANGUAGE, a notice) \u2014 never as an unrealized call no code could ever clear, since what crosses that boundary is the binding; and weaker analysis grades never guess.",
28715
29027
  codes: [
28716
29028
  { code: "CALL_STEP_UNREALIZED", defaultSeverity: "warning", summary: "Narrative call step or declared call realized by no call that resolves to the target method's own source file \u2014 the call is absent, or it lands in another module", carryable: true },
28717
29029
  { code: "CALL_ORIGIN_UNRESOLVED", defaultSeverity: "warning", summary: "Narrative call step or declared call whose target name IS called, but only from call sites written in a shape this analysis cannot resolve to a file \u2014 the claim was not checked, and is neither proven realized nor accused; or, the fail-closed half of UNDECLARED_WRITE_CALL, a call through a receiver the analysis cannot follow (typed any or unknown, cast to either or to an index signature, a computed key) under the name of a write the method claims nowhere. Where a type checker read the file, whatever the checker resolves; without one, what resolves is a name bound to a module of THIS project: a bare or namespaced call through such an import binding, a this.<field> receiver whose declared type names one, a receiver chain (this.<field>.<property>, <name>.<property>) each of whose links the code declares a type for, a new Class(\u2026) receiver whose class does, a receiver name the file annotates with such a type, a fn(\u2026) receiver whose return the code settles, and this inside a class's own method \u2014 a declared type read through Pick/Omit/Partial/Required/Readonly and one alias hop. What does not: a third-party package import, a value the module assembled, and a receiver the file declares nothing for", carryable: true },
28718
29030
  { code: "UNDECLARED_COLOCATED_CALL", defaultSeverity: "warning", summary: "The realized function calls a modelled method of another component living in the same source file, and neither a narrative step nor a declared call names it", carryable: true },
28719
29031
  { code: "UNDECLARED_WRITE_CALL", defaultSeverity: "warning", summary: "The realized function, or unowned code it calls, calls or takes as a value a write- or lifecycle-effect method of another component in another file \u2014 or, from a Portal, a workflow verb whose effect, declared or read off its own narrative, is a write, or that nothing settles (said as an unnarrated call whose effect is undeclared), or a method of a workflow component's class its contract does not declare \u2014 as the type checker resolves it, and neither a narrative step nor a declared call names it \u2014 an unnarrated mutation", carryable: true },
29032
+ { code: "CALL_ACROSS_LANGUAGE", defaultSeverity: "notice", summary: "Claimed calls from TypeScript or JavaScript code reach a component whose code is in another language (a member or project written in Rust, say): the call graph cannot resolve into it, so the claims are not judged by it, and the boundary is said once per project and language" },
28720
29033
  { code: "CALL_TARGET_PLANNED", defaultSeverity: "notice", summary: "A narrative call step or declared call written by name through a receiver nothing realizes yet, whose target component has no code yet (no file its implementations name exists) \u2014 planned, not unresolved; judged like any other claim once the target's realization begins" }
28721
29034
  ],
28722
29035
  check(ctx) {
@@ -28815,10 +29128,26 @@ var callConformanceRule = {
28815
29128
  for (const claim of claimsOf(entry.method)) refs.add(`${claim.component}.${claim.method}`);
28816
29129
  claimsByBody.set(key2, refs);
28817
29130
  }
29131
+ const script = (language) => language === "typescript" || language === "javascript";
29132
+ const foreignLanguageOf = (callerFile, target, componentId) => {
29133
+ if (!script(languageOfSourcePath(callerFile))) return void 0;
29134
+ const sep18 = componentId.indexOf("::");
29135
+ const project3 = sep18 > 0 ? componentId.slice(0, sep18) : "this project";
29136
+ if (target.files.size > 0) {
29137
+ const languages = new Set([...target.files].map((f) => languageOfSourcePath(f)));
29138
+ if ([...languages].some((l) => l === void 0 || script(l))) return void 0;
29139
+ return { project: project3, language: [...languages].sort().join(" and ") };
29140
+ }
29141
+ if (sep18 <= 0) return void 0;
29142
+ const node = (ctx.projectFamily?.nodes ?? []).find((n) => n.namespace === project3 || n.mountAlias === project3 || n.id === project3);
29143
+ const declared = node?.targetLanguage?.toLowerCase();
29144
+ if (!node || !declared || /typescript|javascript/.test(declared)) return void 0;
29145
+ return { project: project3, language: node.targetLanguage };
29146
+ };
29147
+ const boundaries = /* @__PURE__ */ new Map();
28818
29148
  for (const entry of methods) {
28819
29149
  const { implementation: impl, method: implMethod, component, sourceFile: file } = entry;
28820
- const tier = implMethod.conformance ?? impl.conformance ?? defaultConformanceTier(component);
28821
- if (tier === "off") continue;
29150
+ const off = (implMethod.conformance ?? impl.conformance ?? defaultConformanceTier(component)) === "off";
28822
29151
  if (!implMethod.narrative.length && !implMethod.calls?.length) continue;
28823
29152
  if (!file) continue;
28824
29153
  const facts = code.factsAt(file);
@@ -28848,10 +29177,18 @@ var callConformanceRule = {
28848
29177
  const missed = [];
28849
29178
  const plannedClaims = [];
28850
29179
  const hinted = /* @__PURE__ */ new Set();
28851
- for (const claim of claimsOf(implMethod)) {
29180
+ for (const claim of off ? [] : claimsOf(implMethod)) {
28852
29181
  if (!ctx.componentMap.has(claim.component)) continue;
28853
29182
  if (isRemoteLink(component, ctx.componentMap.get(claim.component))) continue;
28854
29183
  const target = resolveCallTarget(ctx, claim.component, claim.method);
29184
+ const foreign = foreignLanguageOf(file, target, claim.component);
29185
+ if (foreign) {
29186
+ const key2 = `${foreign.project}|${foreign.language}`;
29187
+ const boundary = boundaries.get(key2) ?? { ...foreign, from: languageOfSourcePath(file) ?? "unknown", anchor: impl.id, draft: entry.draftContext, units: [] };
29188
+ boundary.units.push(`${impl.id}.${implMethod.name} \u2192 ${claim.component}.${claim.method}`);
29189
+ boundaries.set(key2, boundary);
29190
+ continue;
29191
+ }
28855
29192
  if (target.bodies.has(bodyKey(here, fnSymbol, implMethod.exportedVia))) continue;
28856
29193
  const sharedBody = targetForwards(code, target);
28857
29194
  if (code.forwardsOf(file, fnSymbol, implMethod.exportedVia).some((p) => p.file !== here && sharedBody.has(`${p.file}#${p.name}`))) continue;
@@ -29032,7 +29369,7 @@ var callConformanceRule = {
29032
29369
  if (call.unresolved) {
29033
29370
  for (const ref of unresolvedCandidates(call, component)) {
29034
29371
  if (!unclaimed(ref) || unresolvedWrites.has(ref)) continue;
29035
- unresolvedWrites.set(ref, `\`${call.written}(\u2026)\`${throughText(through)}`);
29372
+ unresolvedWrites.set(ref, `${call.reference ? `\`${call.written}\` (taken as a value)` : `\`${call.written}(\u2026)\``}${throughText(through)}`);
29036
29373
  }
29037
29374
  }
29038
29375
  }
@@ -29071,6 +29408,18 @@ var callConformanceRule = {
29071
29408
  }
29072
29409
  }
29073
29410
  }
29411
+ for (const boundary of boundaries.values()) {
29412
+ const where = boundary.project === "this project" ? "components of this project" : `the project "${boundary.project}"`;
29413
+ ctx.addIssue(
29414
+ "notice",
29415
+ "CALL_ACROSS_LANGUAGE",
29416
+ `${boundary.units.length} claimed call(s) from ${boundary.from} code reach ${where}, whose code is ${boundary.language} \u2014 ${boundary.units.join("; ")}. A ${boundary.from} call graph never resolves into a ${boundary.language} file, so these claims are not judged by it, and calling them unrealized would be a finding no code could ever clear. What crosses the boundary is the binding: declare the module that carries it in the implementation's \`bindings\`, which is compared with what the other side exports.`,
29417
+ boundary.anchor,
29418
+ boundary.draft,
29419
+ void 0,
29420
+ { at: boundary.project, covers: boundary.units }
29421
+ );
29422
+ }
29074
29423
  }
29075
29424
  };
29076
29425
 
@@ -29136,13 +29485,14 @@ var UNOWNED_IMPORT_DEPTH = 8;
29136
29485
  var dependencyConformanceRule = {
29137
29486
  name: "dependency-conformance",
29138
29487
  judges: "code",
29139
- description: "Code\u2194spec Level 2: runtime import edges between component-mapped source files (a component maps to every file its implementations and their methods name) must be justified by declared relations \u2014 a direct dependsOn/owns pair, a shared component, membership in a depended-on pattern, or (across subsystems) a declared edge to the target subsystem's published surface (UNDECLARED_DEPENDENCY). A type-only import alone never accuses \u2014 type coupling is allowed \u2014 but a CALL of a modelled contract method through a collaborator whose declared type comes from another component's file is runtime collaboration whatever the import's form, and the same justification is owed (UNDECLARED_DEPENDENCY on that edge). Conversely, a declared dependsOn/owns edge between components realized in different files should be visible as an import between any file of the source and any file of the target, or \u2014 where the type checker read a file of the source \u2014 as a call or reference the checker lands in a file of the target, through every barrel, port declaration, dependency bag and unowned helper (UNREALIZED_DEPENDENCY \u2014 DI indirection can defeat the import reading, hence warning); a type-only import realizes it, since `import type` is what dependency injection writes down. An Adapter's edge to a Portal reached over an out-of-process transport (HTTP, gRPC, a bus, a CLI\u2026) is the link the design models: the Adapter's transport client realizes it, never an import, so it is never UNREALIZED_DEPENDENCY \u2014 and nothing asks for an import across that boundary, which would couple two deployables' builds. Two twins of design rules are judged on the same code: a Portal file calling a write- or lifecycle-effect contract method of a Repository, Index, Store or Registry is the persistence shortcut the design rule PORTAL_WRITE_SHORTCUT refuses, unless the Portal's own narrative already claims that call (the design rule's finding then) (PORTAL_WRITE_SHORTCUT_IN_CODE). A Portal's calls are resolved by the TypeScript type checker: every call written in a file realizing only Portals \u2014 in a file shared with other components, every call in a body the Portal's own methods reach \u2014 and every reference there to a function or method the code takes as a value (passed as an argument, assigned, returned, bound, handed to Reflect.apply), together with every call the UNOWNED code they reach makes (a function in a file no component realizes, or one that is none of its file's components' modelled methods, read as if inlined at the call site, transitively to a bounded depth, the finding naming the hops), lands on the declaration its signature resolves to, whatever the receiver's spelling, and a declaration in an interface or type shape, a port the Portal declares for itself included, lands on each class of the project that realizes it (the classes whose implements clause names it when any does, else every class the checker finds assignable to it); such a landing on a component the file neither imports at runtime nor is justified to reach is UNDECLARED_DEPENDENCY on that edge. Where no type checker could be loaded, the receiver is followed through the shape facts instead: a non-null assertion, parentheses, `satisfies`, a cast to a named type and a local alias or destructuring of a field exactly as through the field itself. A call in a Portal's own code the analysis cannot resolve at all \u2014 its receiver typed any or unknown, cast to either or to an index signature, its member picked by a computed key, or typed nothing \u2014 under the name of a write- or lifecycle-effect method of a data component, or reading a member by a key that names no single member off a receiver that was a data component before any cast \u2014 invoked on the spot or taken as a value, however the receiver or the key is cast (where the checker recorded the receiver's ORIGINAL type, before any cast \u2014 an `await (x)` written outside an async function read as the x it means, nullability set aside, a name bound once by a const read through its initializer, an interface no class realizes landing on the file declaring it \u2014 a write of the data component that type lands on \u2014 any of its writes when a computed key names none; else one in the Portal's own subsystem or one it depends into), which the Portal does not already claim, fails closed: it is neither proven a shortcut nor cleared, and it is a warning, so a CI gate never passes what it could not read (PORTAL_CALL_UNRESOLVED). So does a call landing only on a PORT no written class realizes \u2014 a member of an interface or shape declared where no component's contract answers for it \u2014 under the name of such a write the Portal claims nowhere and no narrative of the Portal names on any component: the design says what that port will be before the code realizing it is written, and the finding says when the data component has no code yet. And an import, runtime or type-only, from ANOTHER project's source \u2014 a folder holding its own .wai/project.yaml that is not this project's root \u2014 that the type checker resolves to nothing, the module gone or the name no longer exported, is CROSS_PROJECT_IMPORT_UNRESOLVED: another project's code moves on its own release, and a type-only import of it is erased at run time, so only a compiler would notice. And a file importing a technology's package \u2014 itself, or through the UNOWNED modules it imports (a module no component realizes, read as if inlined in its importer, transitively to a bounded depth, the finding naming the hops; a module a component realizes is judged as its own) \u2014 while no component it realizes is that technology's home is the leak TECH_LEAKAGE refuses in the design (TECH_LEAKAGE_IN_CODE): a technology's packages are its declared tokens (its name, or its `matches`), the curated built-in table of the common packages its name is known by (postgres: pg, postgres, @neondatabase/serverless, @vercel/postgres; mysql: mysql2, mysql; redis: redis, ioredis; mongodb: mongodb, mongoose; sqlite: better-sqlite3, sqlite3; kafka: kafkajs; rabbitmq: amqplib \u2014 an HTTP client is never a technology leak), and the packages a loaded pack contributes for it, each compared exactly with an import's package name. A package of the built-in table or of a loaded pack whose technology NO implementation binds is reported too, since the design never says where that technology lives: bind it on the data-layer component that owns the vendor call. Only exact-grade analyzed files participate; chained subprojects validate standalone.",
29488
+ description: "Code\u2194spec Level 2: runtime import edges between component-mapped source files (a component maps to every file its implementations and their methods name) must be justified by declared relations \u2014 a direct dependsOn/owns pair, a shared component, membership in a depended-on pattern, or (across subsystems) a declared edge to the target subsystem's published surface (UNDECLARED_DEPENDENCY). A type-only import alone never accuses \u2014 type coupling is allowed \u2014 but a CALL of a modelled contract method through a collaborator whose declared type comes from another component's file is runtime collaboration whatever the import's form, and the same justification is owed (UNDECLARED_DEPENDENCY on that edge). Conversely, a declared dependsOn/owns edge between components realized in different files should be visible as an import between any file of the source and any file of the target, or \u2014 where the type checker read a file of the source \u2014 as a call or reference the checker lands in a file of the target, through every barrel, port declaration, dependency bag and unowned helper (UNREALIZED_DEPENDENCY \u2014 DI indirection can defeat the import reading, hence warning); a type-only import realizes it, since `import type` is what dependency injection writes down. An Adapter's edge to a Portal reached over an out-of-process transport (HTTP, gRPC, a bus, a CLI\u2026) is the link the design models: the Adapter's transport client realizes it, never an import, so it is never UNREALIZED_DEPENDENCY \u2014 and nothing asks for an import across that boundary, which would couple two deployables' builds. Two twins of design rules are judged on the same code: a Portal file calling a write- or lifecycle-effect contract method of a Repository, Index, Store or Registry is the persistence shortcut the design rule PORTAL_WRITE_SHORTCUT refuses, unless the Portal's own narrative already claims that call (the design rule's finding then) (PORTAL_WRITE_SHORTCUT_IN_CODE). A Portal's calls are resolved by the TypeScript type checker: every call written in a file realizing only Portals \u2014 in a file shared with other components, every call in a body the Portal's own methods reach \u2014 and every reference there to a function or method the code takes as a value (passed as an argument, assigned, returned, bound, handed to Reflect.apply), together with every call the UNOWNED code they reach makes (a function in a file no component realizes, or one that is none of its file's components' modelled methods, read as if inlined at the call site, transitively to a bounded depth, the finding naming the hops), lands on the declaration its signature resolves to, whatever the receiver's spelling, and a declaration in an interface or type shape, a port the Portal declares for itself included, lands on each class of the project that realizes it (the classes whose implements clause names it when any does, else every class the checker finds assignable to it); such a landing on a component the file neither imports at runtime nor is justified to reach is UNDECLARED_DEPENDENCY on that edge. Where no type checker could be loaded, the receiver is followed through the shape facts instead: a non-null assertion, parentheses, `satisfies`, a cast to a named type and a local alias or destructuring of a field exactly as through the field itself. A call in a Portal's own code the analysis cannot resolve at all \u2014 its receiver typed any or unknown however it came to be (a cast, an untyped or any-typed parameter of a closure, a local typed any, a function result typed any), cast to either or to an index signature, its member picked by a computed key, or typed nothing \u2014 under the name of a write- or lifecycle-effect method of a data component, or reading a member by a key that names no single member off a receiver that was a data component before any cast \u2014 invoked on the spot or taken as a value, however the receiver or the key is cast (where the checker recorded the receiver's ORIGINAL type, before any cast \u2014 an `await (x)` written outside an async function read as the x it means, nullability set aside, a name bound once by a const read through its initializer, an interface no class realizes landing on the file declaring it \u2014 a write of the data component that type lands on \u2014 any of its writes when a computed key names none; else one in the Portal's own subsystem or one it depends into \u2014 the design's write set, which a data component whose code is still planned has too, and what a receiver landing only on the Portal's own file, a port it declares for itself, is judged by), which the Portal does not already claim, fails closed \u2014 one finding per site and per function it is written in, so one shortcut never hides another of the same name: it is neither proven a shortcut nor cleared, and it is a warning, so a CI gate never passes what it could not read (PORTAL_CALL_UNRESOLVED). So does a call landing only on a PORT no written class realizes \u2014 a member of an interface or shape declared where no component's contract answers for it \u2014 under the name of such a write the Portal claims nowhere and no narrative of the Portal names on any component: the design says what that port will be before the code realizing it is written, and the finding says when the data component has no code yet. And an import, runtime or type-only, from ANOTHER project's source \u2014 a folder holding its own .wai/project.yaml that is not this project's root \u2014 that the type checker resolves to nothing, the module gone or the name no longer exported, is CROSS_PROJECT_IMPORT_UNRESOLVED: another project's code moves on its own release, and a type-only import of it is erased at run time, so only a compiler would notice. One that DOES resolve is judged like any other edge, because it is one: a name it imports that the other project does not publish \u2014 not in its L0 export table as a member exports it now, nor in the pinned snapshot of the external it is (an exported type by its id or code name, an exported component by its contract methods and their code symbols; a project that is neither publishes nothing to this one) \u2014 is code reaching into that project's internals past its surface, which no pin and no binding compares (CROSS_PROJECT_SOURCE_IMPORT, a warning; the finding says when the module it lands in realizes a data component of that project, since a write through it is the persistence shortcut past the other project's surface). A binding module an implementation declares is the sanctioned place for exactly that reach, compared by binding conformance, so an import written in one is never reported here. And a file importing a technology's package \u2014 itself, or through the UNOWNED modules it imports (a module no component realizes, read as if inlined in its importer, transitively to a bounded depth, the finding naming the hops; a module a component realizes is judged as its own) \u2014 while no component it realizes is that technology's home is the leak TECH_LEAKAGE refuses in the design (TECH_LEAKAGE_IN_CODE): a technology's packages are its declared tokens (its name, or its `matches`), the curated built-in table of the common packages its name is known by (postgres: pg, postgres, @neondatabase/serverless, @vercel/postgres; mysql: mysql2, mysql; redis: redis, ioredis; mongodb: mongodb, mongoose; sqlite: better-sqlite3, sqlite3; kafka: kafkajs; rabbitmq: amqplib \u2014 an HTTP client is never a technology leak), and the packages a loaded pack contributes for it, each compared exactly with an import's package name. A package of the built-in table or of a loaded pack whose technology NO implementation binds is reported too, since the design never says where that technology lives: bind it on the data-layer component that owns the vendor call. Only exact-grade analyzed files participate; chained subprojects validate standalone.",
29140
29489
  codes: [
29141
29490
  { code: "UNDECLARED_DEPENDENCY", defaultSeverity: "warning", summary: "A runtime import, a call through a type-only-imported collaborator, or a Portal's call the type checker resolves into another component's file, between component-mapped files has no declared dependsOn/owns (or published-surface) justification" },
29142
29491
  { code: "PORTAL_WRITE_SHORTCUT_IN_CODE", defaultSeverity: "error", summary: "A Portal's code calls, or takes as a value, a write- or lifecycle-effect method of a data component (Repository, Index, Store, Registry) \u2014 directly or through unowned code it calls \u2014 the code twin of PORTAL_WRITE_SHORTCUT: mutations route through an Orchestrator" },
29143
29492
  { code: "TECH_LEAKAGE_IN_CODE", defaultSeverity: "warning", summary: "A source file imports a technology's package although no component it realizes binds that technology \u2014 the code twin of TECH_LEAKAGE" },
29144
29493
  { code: "UNREALIZED_DEPENDENCY", defaultSeverity: "warning", summary: "A declared dependsOn/owns edge between components in different files is realized by no import between any of their files, nor by any call the type checker lands across it" },
29145
29494
  { code: "PORTAL_CALL_UNRESOLVED", defaultSeverity: "warning", summary: "A call in a Portal's own code, under the name of a data component's write- or lifecycle-effect method \u2014 or with its member picked by a computed key on a receiver that was a data component before a cast \u2014 goes through a receiver the analysis cannot resolve (typed any or unknown, cast to either or to an index signature, or typed nothing), or lands only on a port no written class realizes \u2014 neither proven a shortcut nor cleared, so it fails closed until the receiver carries its type" },
29495
+ { code: "CROSS_PROJECT_SOURCE_IMPORT", defaultSeverity: "warning", summary: "A source file imports a name from ANOTHER project's source that the project does not export \u2014 code reaching into its internals past its surface, which no pin and no binding compares (a write into its Store through it included); a declared binding module is the sanctioned place for that reach", carryable: true },
29146
29496
  { code: "CROSS_PROJECT_IMPORT_UNRESOLVED", defaultSeverity: "warning", summary: "A source file imports from ANOTHER project's source \u2014 a folder holding its own .wai/project.yaml \u2014 a module that is gone or a name it no longer exports: a type-only import is erased at run time, so only a compiler would notice, and the gate reads the file with one", carryable: true }
29147
29497
  ],
29148
29498
  check(ctx) {
@@ -29437,8 +29787,8 @@ var dependencyConformanceRule = {
29437
29787
  const near = /* @__PURE__ */ new Set([portal.subsystem, ...portal.dependsOn.map((d) => ctx.componentMap.get(d)?.subsystem)]);
29438
29788
  const candidates = (call.call ? candidatesOf(call.call, portal, (callee) => DATA_COMPONENTS2.has(callee.componentType)) : (dataWrites.get(call.name) ?? []).filter((w) => near.has(ctx.componentMap.get(w.component)?.subsystem))).filter((w) => !claims(portal.id, w.component, w.method));
29439
29789
  const at = `${fromPath} -> ${call.name || candidates.map((w) => `${w.component}.${w.method}`).join(", ")}`;
29440
- if (candidates.length > 0 && !reportedUnresolved.has(at)) {
29441
- reportedUnresolved.add(at);
29790
+ if (candidates.length > 0 && !reportedUnresolved.has(`${at}|${call.where}`)) {
29791
+ reportedUnresolved.add(`${at}|${call.where}`);
29442
29792
  ctx.addIssue(
29443
29793
  "warning",
29444
29794
  "PORTAL_CALL_UNRESOLVED",
@@ -29610,6 +29960,7 @@ var dependencyConformanceRule = {
29610
29960
  }
29611
29961
  for (const facts of ctx.codeModel.files) {
29612
29962
  for (const broken of facts.crossProjectImports ?? []) {
29963
+ if (broken.resolved) continue;
29613
29964
  const file = pathKey(facts.path);
29614
29965
  const what = broken.name !== void 0 ? `"${broken.name}" from "${broken.specifier}"` : `"${broken.specifier}"`;
29615
29966
  ctx.addIssue(
@@ -29623,8 +29974,97 @@ var dependencyConformanceRule = {
29623
29974
  );
29624
29975
  }
29625
29976
  }
29977
+ const bindingFiles = new Set(ctx.implementations.flatMap((impl) => impl.bindings ?? []).map(pathKey));
29978
+ const surfaces = /* @__PURE__ */ new Map();
29979
+ const surfaceOf = (project3) => {
29980
+ if (surfaces.has(project3)) return surfaces.get(project3);
29981
+ const answer = projectSurface(ctx, project3);
29982
+ surfaces.set(project3, answer);
29983
+ return answer;
29984
+ };
29985
+ for (const facts of ctx.codeModel.files) {
29986
+ const file = pathKey(facts.path);
29987
+ if (bindingFiles.has(file)) continue;
29988
+ const reaches = (facts.crossProjectImports ?? []).filter((fact) => fact.resolved);
29989
+ const bySpecifier = /* @__PURE__ */ new Map();
29990
+ for (const reach3 of reaches) bySpecifier.set(reach3.specifier, [...bySpecifier.get(reach3.specifier) ?? [], reach3]);
29991
+ for (const [specifier, list] of bySpecifier) {
29992
+ const first = list[0];
29993
+ const surface = surfaceOf(first.project);
29994
+ const internal = list.filter((reach3) => !(surface && reach3.name !== void 0 && reach3.name !== "*" && surface.names.has(nameKey2(reach3.name))));
29995
+ if (internal.length === 0) continue;
29996
+ const names = internal.map((reach3) => reach3.name === void 0 ? "the module itself" : reach3.name === "*" ? "the whole module" : `"${reach3.name}"`);
29997
+ const owner = first.module !== void 0 ? surface?.data.get(first.module) : void 0;
29998
+ const writes = owner !== void 0 ? ` "${first.module}" realizes ${owner} of that project: a write through it is the persistence shortcut past the other project's surface, and at least as serious as an undeclared dependency.` : "";
29999
+ const label7 = surface?.label ?? `"${first.project}"`;
30000
+ ctx.addIssue(
30001
+ "warning",
30002
+ "CROSS_PROJECT_SOURCE_IMPORT",
30003
+ `"${file}" imports ${names.join(", ")} from "${specifier}" (line ${first.line}), the SOURCE of another project, ${label7}, which ${surface ? "does not export that name" : "exports nothing this project can reach"} \u2014 code reaching into that project's internals past its surface, which no pin and no binding compares.${writes} Reach what the project exports (its L0 export table: declare it there, then use it through the member alias or a pinned external), or carry the reach in a binding module the implementation declares (\`bindings\`), which is compared with what the project exports.`,
30004
+ realization.implementationsAt(file)[0]?.id,
30005
+ draftAt(file),
30006
+ void 0,
30007
+ { at: `${file} -> ${specifier}`, covers: internal.map((reach3) => reach3.name ?? "<module>") }
30008
+ );
30009
+ }
30010
+ }
29626
30011
  }
29627
30012
  };
30013
+ var nameKey2 = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
30014
+ function projectSurface(ctx, project3) {
30015
+ const root = import_path.posix.normalize(ctx.codeModel.projectRoot.replace(/\\/g, "/"));
30016
+ const directory = import_path.posix.normalize(import_path.posix.join(root, project3));
30017
+ const same = (dir) => !!dir && import_path.posix.normalize(dir.replace(/\\/g, "/")).toLowerCase() === directory.toLowerCase();
30018
+ const names = /* @__PURE__ */ new Set();
30019
+ const data = /* @__PURE__ */ new Map();
30020
+ const DATA = /* @__PURE__ */ new Set(["Repository", "Index", "Store", "Registry"]);
30021
+ const family = ctx.projectFamily?.nodes ?? [];
30022
+ const member = family.find((node) => node.namespace !== "" && same(node.directory));
30023
+ if (member) {
30024
+ const ns = member.namespace;
30025
+ const table = (ctx.exportTables ?? []).find((t) => t.level === "project" && t.owner === ns);
30026
+ const local = (id) => id.slice(id.lastIndexOf(":") + 1).replace(/^.*\./, "");
30027
+ for (const entry of table?.entries ?? []) {
30028
+ names.add(nameKey2(entry.publicName));
30029
+ if (entry.kind === "type" && entry.typeDef) {
30030
+ const type = ctx.types.find((t) => t.id === entry.typeDef || local(t.id) === local(entry.typeDef));
30031
+ if (type) {
30032
+ names.add(nameKey2(type.name));
30033
+ names.add(nameKey2(type.symbol ?? type.name));
30034
+ names.add(nameKey2(local(type.id)));
30035
+ }
30036
+ } else if (entry.component) {
30037
+ for (const contract of ctx.interfacesByComponent.get(entry.component) ?? []) {
30038
+ for (const method2 of contract.methods) names.add(nameKey2(method2.name));
30039
+ }
30040
+ for (const impl of ctx.implementations) {
30041
+ if (ctx.interfaceMap.get(impl.contract)?.component !== entry.component) continue;
30042
+ for (const method2 of impl.methods) if (method2.symbol) names.add(nameKey2(method2.symbol));
30043
+ }
30044
+ }
30045
+ }
30046
+ for (const impl of ctx.implementations) {
30047
+ const component = ctx.componentMap.get(ctx.interfaceMap.get(impl.contract)?.component ?? "");
30048
+ if (!component || !component.id.startsWith(`${ns}::`) || !DATA.has(component.componentType)) continue;
30049
+ for (const file of implementationSourceFiles(impl)) data.set(pathKey(file), `${component.id} (a ${component.componentType})`);
30050
+ }
30051
+ return { label: `the member "${member.mountAlias ?? ns}"`, names, data };
30052
+ }
30053
+ const bound2 = family.find((node) => node.namespace === "");
30054
+ const external = bound2?.externals.find((e) => same(e.directory));
30055
+ const pin2 = external ? ctx.pinnedExternals.find((p) => p.alias === external.alias) : void 0;
30056
+ if (!external || !pin2?.snapshot) return null;
30057
+ for (const type of pin2.snapshot.types) {
30058
+ names.add(nameKey2(type.id));
30059
+ names.add(nameKey2(type.name));
30060
+ }
30061
+ for (const exported of pin2.snapshot.exportedTypes ?? []) names.add(nameKey2(exported.id));
30062
+ for (const entry of pin2.snapshot.interfaces) {
30063
+ names.add(nameKey2(entry.id));
30064
+ for (const method2 of entry.methods) names.add(nameKey2(method2.name));
30065
+ }
30066
+ return { label: `the external "${external.alias}"`, names, data };
30067
+ }
29628
30068
 
29629
30069
  // src/core/rules/conformance/portal-imports.ts
29630
30070
  var portalImportsRule = {
@@ -30201,8 +30641,17 @@ function bare(name) {
30201
30641
  return name.replace(/^_+/, "") || name;
30202
30642
  }
30203
30643
  var HANDLE_NAMES = /* @__PURE__ */ new Set(["req", "request", "res", "response", "reply", "url", "ctx", "context", "next", "event", "socket"]);
30644
+ var TRANSPORT_HANDLES = /* @__PURE__ */ new Set(["req", "res", "ctx", "request", "response", "next", "reply"]);
30645
+ var REQUEST_HANDLES = /* @__PURE__ */ new Set(["req", "request", "ctx", "context", "event"]);
30204
30646
  var PRIMITIVE_KINDS = /* @__PURE__ */ new Set(["string", "number", "boolean"]);
30205
30647
  var aKind = (kind) => kind === "object" ? "an object" : `a ${kind}`;
30648
+ var fieldKey = (name) => name.toLowerCase().replace(/[_-]/g, "");
30649
+ function structural(fields, realized) {
30650
+ if (!fields || fields.length === 0 || realized.kind !== "object" || !realized.fields || realized.fields.length === 0) return void 0;
30651
+ const has = new Set(realized.fields.map(fieldKey));
30652
+ const present = fields.filter((f) => has.has(fieldKey(f))).length;
30653
+ return present === 0 ? "none" : present === fields.length ? "all" : "some";
30654
+ }
30206
30655
  var PAIRING_BUDGET = 5e3;
30207
30656
  function* subsets(n, k) {
30208
30657
  const pick = [];
@@ -30242,28 +30691,37 @@ function pairUp(declared, realized, score) {
30242
30691
  }
30243
30692
  return align(best);
30244
30693
  }
30245
- function judge2(declared, realized, injected, typeAgrees, kindOf4) {
30246
- let wiring = 0;
30247
- while (wiring < realized.length && realized[wiring].name !== void 0 && injected.has(realized[wiring].name)) wiring++;
30248
- let taken = realized.map((param, at) => ({ param, at })).slice(wiring);
30694
+ function judge2(declared, realized, injected, typeAgrees, expected) {
30249
30695
  const declaredNames = new Set(declared.map((param) => bare(param.name)));
30696
+ const isInjected = (param) => param.name !== void 0 && injected.has(bare(param.name));
30697
+ let lead = 0;
30698
+ while (lead < realized.length && isInjected(realized[lead])) lead++;
30699
+ let end = realized.length;
30700
+ while (end > lead && isInjected(realized[end - 1]) && !declaredNames.has(bare(realized[end - 1].name))) end--;
30701
+ let taken = realized.map((param, at) => ({ param, at })).slice(lead, end);
30250
30702
  taken = taken.filter(({ param }) => !(param.name !== void 0 && param.name.startsWith("_") && param.unused === true && !declaredNames.has(bare(param.name))));
30251
30703
  const score = (d, r) => {
30252
30704
  let total = 0;
30253
30705
  if (r.name !== void 0 && bare(r.name) === bare(d.name)) total += 4;
30254
30706
  if (typeAgrees(d, r)) total += 2;
30255
- const want = kindOf4(d);
30256
- if (want && r.type && r.kind) total += want === r.kind ? 1 : -2;
30707
+ const want = expected(d);
30708
+ if (want?.kinds && r.type && r.kind) total += want.kinds.has(r.kind) ? 1 : -2;
30709
+ const shape = structural(want?.fields, r);
30710
+ if (shape === "all") total += 2;
30711
+ else if (shape === "none") total -= 2;
30257
30712
  return total;
30258
30713
  };
30259
30714
  const pairs = pairUp(declared, taken.map((entry) => entry.param), score);
30715
+ const wiringParams = [...realized.slice(0, lead), ...realized.slice(end)];
30260
30716
  const found = {
30261
30717
  unrealized: /* @__PURE__ */ new Map(),
30262
30718
  undeclared: /* @__PURE__ */ new Map(),
30263
30719
  renamed: /* @__PURE__ */ new Map(),
30264
30720
  optionality: /* @__PURE__ */ new Map(),
30265
30721
  transport: /* @__PURE__ */ new Map(),
30266
- wiring: realized.slice(0, wiring).map((param) => param.name)
30722
+ wiring: realized.slice(0, lead).map((param) => param.name),
30723
+ trailing: realized.slice(end).map((param) => param.name),
30724
+ carried: /* @__PURE__ */ new Map()
30267
30725
  };
30268
30726
  const pairedDeclared = new Set(pairs.map(([d]) => d));
30269
30727
  const pairedTaken = new Set(pairs.map(([, r]) => r));
@@ -30272,21 +30730,34 @@ function judge2(declared, realized, injected, typeAgrees, kindOf4) {
30272
30730
  const param = declared[d];
30273
30731
  const { param: at, at: position2 } = taken[r];
30274
30732
  const sameName = at.name !== void 0 && bare(at.name) === bare(param.name);
30275
- const want = kindOf4(param);
30276
- if (!sameName && want && at.type && at.kind && want !== at.kind && !typeAgrees(param, at)) {
30733
+ const want = expected(param);
30734
+ const agrees = typeAgrees(param, at);
30735
+ const shape = structural(want?.fields, at);
30736
+ const handleName = at.name !== void 0 && TRANSPORT_HANDLES.has(bare(at.name).toLowerCase()) && !TRANSPORT_HANDLES.has(bare(param.name).toLowerCase());
30737
+ let instead;
30738
+ if (!agrees) {
30739
+ if (!sameName && want?.kinds && at.type && at.kind && !want.kinds.has(at.kind)) instead = aKind(at.kind);
30740
+ else if (at.transport && want?.fields) instead = "a transport handle";
30741
+ else if (shape === "none") instead = `an object sharing none of its fields (${(want?.fields ?? []).join(", ")})`;
30742
+ else if (handleName && (!at.kind || at.transport)) instead = "a transport handle";
30743
+ }
30744
+ if (instead !== void 0) {
30277
30745
  substituted.add(r);
30746
+ const declares2 = want?.told ?? "an argument of its own";
30278
30747
  found.unrealized.set(param.name, {
30279
30748
  unit: param.name,
30280
- told: `"${param.name}" (the code takes ${at.name ? `"${at.name}"` : `parameter #${position2 + 1}`}, ${aKind(at.kind)}, where the contract declares ${aKind(want)})`
30749
+ told: `"${param.name}" (the code takes ${at.name ? `"${at.name}"` : `parameter #${position2 + 1}`}, ${instead}, where the contract declares ${declares2})`
30281
30750
  });
30282
30751
  const unit = unitOf2(at, position2);
30283
- found.undeclared.set(unit, { unit, told: `"${unit}" (${aKind(at.kind)} in the place of the contract's "${param.name}", ${aKind(want)})` });
30752
+ found.undeclared.set(unit, { unit, told: `"${unit}" (${instead} in the place of the contract's "${param.name}", ${declares2})` });
30284
30753
  continue;
30285
30754
  }
30286
- if (at.name !== void 0 && !sameName && (typeAgrees(param, at) || !!want && !!at.type && want === at.kind && PRIMITIVE_KINDS.has(want))) {
30755
+ const sameKind = !!want?.kinds && !!at.type && !!at.kind && want.kinds.has(at.kind) && (PRIMITIVE_KINDS.has(at.kind) || !!want.loose);
30756
+ if (at.name !== void 0 && !sameName && (agrees || sameKind || shape === "all")) {
30757
+ const former = (param.previousNames ?? []).some((name) => bare(name) === bare(at.name));
30287
30758
  found.renamed.set(`${param.name}\u2192${at.name}`, {
30288
30759
  unit: param.name,
30289
- told: `"${param.name}" (the code calls it "${at.name}")`
30760
+ told: `"${param.name}" (the code calls it "${at.name}"${former ? ", the name the contract retired" : ""})`
30290
30761
  });
30291
30762
  }
30292
30763
  if (!!param.optional !== at.optional) {
@@ -30296,8 +30767,14 @@ function judge2(declared, realized, injected, typeAgrees, kindOf4) {
30296
30767
  });
30297
30768
  }
30298
30769
  }
30770
+ const requestReads = new Set(wiringParams.filter((param) => param.name !== void 0 && REQUEST_HANDLES.has(bare(param.name).toLowerCase())).flatMap((param) => (param.reads ?? []).map(bare)));
30299
30771
  declared.forEach((param, d) => {
30300
- if (!pairedDeclared.has(d)) found.unrealized.set(param.name, { unit: param.name, told: `"${param.name}"` });
30772
+ if (pairedDeclared.has(d)) return;
30773
+ if (requestReads.has(bare(param.name))) {
30774
+ found.carried.set(param.name, { unit: param.name, told: param.name });
30775
+ return;
30776
+ }
30777
+ found.unrealized.set(param.name, { unit: param.name, told: `"${param.name}"` });
30301
30778
  });
30302
30779
  const firstPaired = Math.min(...pairs.filter(([, r]) => !substituted.has(r)).map(([, r]) => r), taken.length);
30303
30780
  taken.forEach(({ param, at }, r) => {
@@ -30358,7 +30835,7 @@ function typeKind(type) {
30358
30835
  var paramConformanceRule = {
30359
30836
  name: "param-conformance",
30360
30837
  judges: "code",
30361
- description: "Code-to-contract for the SIGNATURE, the last of the three readings a spec-driven gate never made: a contract declares `params`, and nothing ever compared them to the parameters of the function that realizes the method. A contract could promise an argument the code does not take, take one the contract never mentions \u2014 including a secret \u2014 or name the same argument two different things, and the brief handed to an implementer would carry the contract's version. What a realization takes BEFORE the contract's own parameters is wiring, declared on the implementation as `injectedParams` rather than inferred, because an inferred prefix cannot be told from a renamed first argument; only a leading run of the names it declares is dropped. A parameter whose name starts with an underscore means UNUSED, and it is set aside only where the analysis proves it is \u2014 named, with no identifier of that name in the body or in another parameter's default value and no read of `arguments`, as the request and URL a transport hands a handler that never reads them: a used `_secret` is judged like any parameter, and a name compared for a rename is read without its leading underscores. The rest are PAIRED in order, since a caller passes arguments in order: among the order-keeping pairings, the one where the most names agree, then the most declared types, then the most kinds of value, and on a tie the one aligned against the TAIL of the realization's list \u2014 so an inserted first argument is the one named undeclared, never the declared one it pushed along. The declared type is what tells a rename from a substitution. Types agree when the code's annotation, read through the dialect of the language the file was analyzed as (type_dialect.agrees), is the contract's canonical type \u2014 so `string[]` agrees with `list<string>`, TypeScript's `number` with int and float alike, and an annotation the dialect cannot read agrees with nothing; a named type agrees through its code-level name, an EXTERNAL one (`alias::name`) through the name its pinned snapshot gives it, else its public name's last segment in the code's type casing. Where a type checker read the file, each parameter the code annotates also carries the KIND of value it takes (string, number, boolean, list, object or function; a platform class the checker's fixed library leaves out \u2014 a URL, a Request, node:http's IncomingMessage \u2014 is an object), compared with the kind the contract's type is (a primitive's, a list, a map or set as an object, an enum as a string, a named scalar as what it holds, a record type as an object, a signature as a function; a date, bytes and anything unsettled compare with nothing). A paired parameter whose name differs and whose type agrees \u2014 or whose kind is the same primitive \u2014 is a rename; one whose name differs and whose kind of value differs is a SUBSTITUTION, a different argument in the declared one's place, so the declared parameter is unrealized and the code's undeclared: `getRoute(params: Record<string, string>)` realizing `getRoute(id: string)` is never a silent pairing. Where neither side settles a type, nothing is said about the name. A parameter standing ahead of every kept pair under a name transports give their handles (req, request, res, response, reply, url, ctx, context, next, event, socket) is how a transport handle arrives, and the finding names the one green path for it: the handle in `injectedParams`, the contract's own parameters after it in order, the router unpacking the request into them. A name with several bodies is judged on what they all agree on, and a method the named file only CALLS is left to `methodRealization`, which already reports that the body is not here.",
30838
+ description: "Code-to-contract for the SIGNATURE, the last of the three readings a spec-driven gate never made: a contract declares `params`, and nothing ever compared them to the parameters of the function that realizes the method. A contract could promise an argument the code does not take, take one the contract never mentions \u2014 including a secret \u2014 or name the same argument two different things, and the brief handed to an implementer would carry the contract's version. What a realization takes besides the contract's own parameters, at either end of its list, is wiring, declared on the implementation as `injectedParams` rather than inferred, because an inferred prefix cannot be told from a renamed first argument; a leading run and a trailing run of the names it declares are dropped, each name matching with or without its leading underscore, and a trailing one only where the contract declares no parameter of that name. A parameter whose name starts with an underscore means UNUSED, and it is set aside only where the analysis proves it is \u2014 named, with no identifier of that name in the body or in another parameter's default value and no read of `arguments`, as the request and URL a transport hands a handler that never reads them: a used `_secret` is judged like any parameter, and a name compared for a rename is read without its leading underscores. The rest are PAIRED in order, since a caller passes arguments in order: among the order-keeping pairings, the one where the most names agree, then the most declared types, then the most kinds of value, and on a tie the one aligned against the TAIL of the realization's list \u2014 so an inserted first argument is the one named undeclared, never the declared one it pushed along. The declared type is what tells a rename from a substitution. Types agree when the code's annotation, read through the dialect of the language the file was analyzed as (type_dialect.agrees), is the contract's canonical type \u2014 so `string[]` agrees with `list<string>`, TypeScript's `number` with int and float alike, and an annotation the dialect cannot read agrees with nothing; a named type agrees through its code-level name, an EXTERNAL one (`alias::name`) through the name its pinned snapshot gives it, else its public name's last segment in the code's type casing. Where a type checker read the file, each parameter the code annotates also carries the KIND of value it takes (string, number, boolean, list, object or function; a platform class the checker's fixed library leaves out \u2014 a URL, a Request, node:http's IncomingMessage \u2014 is an object), compared with the kind the contract's type is (a primitive's, a list, a map or set as an object, an enum as a string, a named scalar as what it holds, a record type as an object, a signature as a function; a date or a datetime as a string or an object, a duration as a number or a string, bytes as an object, a list or a string; anything unsettled compares with nothing). A paired parameter whose name differs and whose type agrees \u2014 or whose kind is the same primitive, or one of the kinds a date or bytes is written as \u2014 is a rename, said with the contract's former name when the code still uses it; one whose name differs and whose kind of value differs is a SUBSTITUTION, a different argument in the declared one's place, so the declared parameter is unrealized and the code's undeclared: `getRoute(params: Record<string, string>)` realizing `getRoute(id: string)` is never a silent pairing. Where the contract's type is a record, an object is compared STRUCTURALLY, by the property names the checker gives its type: one holding every field of the record is a rename, one sharing none of them a substitution whatever either side calls it, and a platform transport class (node:http's IncomingMessage, a fetch Request or Response, a URL, a socket) where a record is declared is a substitution too \u2014 so `planRoute(req: IncomingMessage)` and `planRoute(session: { user: string })` realizing `planRoute(request: plan_request)` are never silent pairings. Where the code settles no kind \u2014 unannotated JavaScript, a parameter typed any \u2014 a paired parameter named as a transport handle (req, res, ctx, request, response, next, reply) standing where the contract declares an argument of its own is a substitution: `cancelOrder(req, res)` realizing `cancelOrder(orderId, customerId)` is a handler's shape, not the contract's. Where neither side settles a type and no handle name speaks, nothing is said about the name. A parameter standing ahead of every kept pair under a name transports give their handles (req, request, res, response, reply, url, ctx, context, next, event, socket) is how a transport handle arrives, and the finding names the one green path for it: the handle in `injectedParams`, the contract's own parameters after it in order, the router unpacking the request into them \u2014 or, where the framework hands the function the handles alone, the contract's parameters read off the injected request by their own names: a contract parameter the code does not take but reads off an injected request handle (req, request, ctx, context, event) by its own name \u2014 `req.params.code`, `req.query['limit']`, a destructuring of such a read \u2014 is realized through that handle. An injected name none of the implementation's realizing functions read in this run takes is stale linkage (UNUSED_INJECTED_PARAM): it would wave through the next parameter of that name unread. A name with several bodies is judged on what they all agree on, and a method the named file only CALLS is left to `methodRealization`, which already reports that the body is not here; a method whose conformance dial is off (its own, else its implementation's) is not judged.",
30362
30839
  codes: [
30363
30840
  {
30364
30841
  code: "UNREALIZED_PARAM",
@@ -30380,6 +30857,12 @@ var paramConformanceRule = {
30380
30857
  summary: "A contract parameter and the one realizing it agree on position and type but not on name \u2014 the contract, the ERD and every brief say one word while the code says another",
30381
30858
  carryable: true
30382
30859
  },
30860
+ {
30861
+ code: "UNUSED_INJECTED_PARAM",
30862
+ defaultSeverity: "warning",
30863
+ summary: "An implementation declares an injected parameter that none of its realizing functions takes \u2014 stale linkage that would wave through the next parameter of that name, wherever it appears",
30864
+ carryable: true
30865
+ },
30383
30866
  {
30384
30867
  code: "PARAM_OPTIONALITY",
30385
30868
  defaultSeverity: "warning",
@@ -30426,11 +30909,34 @@ var paramConformanceRule = {
30426
30909
  }
30427
30910
  return dialect.agrees(realized.type, stated.expression, names);
30428
30911
  };
30429
- const kindOf4 = (declared) => expressionKind(parseTypePosition(declared.type, "param", !!declared.optional).expression, (name) => typeKind(typeNamed.get(name)));
30430
- for (const { implementation, method: method2, sourceFile, draftContext: draftContext4 } of ctx.implementationMethods()) {
30912
+ const expected = (declared) => {
30913
+ let expression = parseTypePosition(declared.type, "param", !!declared.optional).expression;
30914
+ while (expression && (expression.form === "optional" || expression.form === "result")) expression = expression.args[0];
30915
+ if (!expression) return void 0;
30916
+ if (expression.form === "primitive") {
30917
+ const loose = {
30918
+ date: ["string", "object"],
30919
+ datetime: ["string", "object"],
30920
+ duration: ["number", "string"],
30921
+ bytes: ["object", "list", "string"]
30922
+ };
30923
+ if (expression.name && loose[expression.name]) return { kinds: new Set(loose[expression.name]), loose: true, told: `a ${expression.name}` };
30924
+ }
30925
+ const kind = expressionKind(expression, (name) => typeKind(typeNamed.get(name)));
30926
+ if (expression.form === "named" && expression.name) {
30927
+ const type = typeNamed.get(expression.name);
30928
+ const fields = (type?.fields ?? []).map((field) => field && typeof field === "object" ? field.name : void 0).filter((name) => typeof name === "string");
30929
+ if (fields.length > 0 && kind === "object") return { kinds: /* @__PURE__ */ new Set(["object"]), fields, told: `the record "${expression.name}"` };
30930
+ }
30931
+ return kind ? { kinds: /* @__PURE__ */ new Set([kind]), told: aKind(kind) } : void 0;
30932
+ };
30933
+ const takenBy = /* @__PURE__ */ new Map();
30934
+ for (const { implementation, method: method2, component, sourceFile, draftContext: draftContext4 } of ctx.implementationMethods()) {
30935
+ const tier = method2.conformance ?? implementation.conformance ?? defaultConformanceTier(component);
30936
+ if (tier === "off") continue;
30431
30937
  const contract = ctx.interfaceMap.get(implementation.contract);
30938
+ if (!sourceFile) continue;
30432
30939
  const declared = contract?.methods.find((m) => m.name === method2.name)?.params ?? [];
30433
- if (declared.length === 0 || !sourceFile) continue;
30434
30940
  const file = pathKey(sourceFile);
30435
30941
  const facts = code.factsAt(file);
30436
30942
  if (!facts || facts.status !== "analyzed" || facts.analysisGrade !== "exact") continue;
@@ -30439,14 +30945,19 @@ var paramConformanceRule = {
30439
30945
  if (!signatures || !Object.prototype.hasOwnProperty.call(signatures, symbol)) continue;
30440
30946
  const candidates = publishedBodies(signatures[symbol], facts.functionBodies?.[symbol], new Set(facts.exportedNames), new Set(Object.keys(facts.typeShapes ?? {})));
30441
30947
  if (candidates.length === 0) continue;
30442
- const injected = new Set(implementation.injectedParams ?? []);
30948
+ const taken = takenBy.get(implementation.id) ?? /* @__PURE__ */ new Set();
30949
+ for (const candidate of candidates) for (const param of candidate) if (param.name !== void 0) taken.add(bare(param.name));
30950
+ takenBy.set(implementation.id, taken);
30951
+ if (declared.length === 0) continue;
30952
+ const injected = new Set((implementation.injectedParams ?? []).map(bare));
30443
30953
  const typeAgrees = typeAgreesIn(dialectOf(facts));
30444
- const judgements = candidates.map((realized) => judge2(declared, realized, injected, typeAgrees, kindOf4));
30954
+ const judgements = candidates.map((realized) => judge2(declared, realized, injected, typeAgrees, expected));
30445
30955
  const subject = candidates.length > 1 ? `every function called "${symbol}" in "${file}"` : `the function "${symbol}" in "${file}"`;
30446
30956
  const opening = subject.charAt(0).toUpperCase() + subject.slice(1);
30447
30957
  const transport = agreed(judgements, "transport").map((found) => found.told);
30448
30958
  const wiring = judgements[0].wiring;
30449
- const greenPath = transport.length > 0 ? ` If ${transport.map((name) => `"${name}"`).join(" and ")} ${transport.length === 1 ? "is a handle" : "are handles"} whatever serves this method hands it (a request, a URL, a response) rather than an argument of the contract's caller, the one green path is: name ${transport.length === 1 ? "it" : "them"} in the implementation's \`injectedParams\` \u2014 [${[...wiring, ...transport].join(", ")}] \u2014 and take the contract's own parameters after ${transport.length === 1 ? "it" : "them"}, in order: ${symbol}(${[...wiring, ...transport, ...declared.map((param) => `${param.name}${param.optional ? "?" : ""}`)].join(", ")}), the router unpacking the path, query and body into them. A handle the function never reads may carry a leading underscore instead.` : "";
30959
+ const trailing = judgements[0].trailing;
30960
+ const greenPath = transport.length > 0 ? ` If ${transport.map((name) => `"${name}"`).join(" and ")} ${transport.length === 1 ? "is a handle" : "are handles"} whatever serves this method hands it (a request, a URL, a response) rather than an argument of the contract's caller, the one green path is: name ${transport.length === 1 ? "it" : "them"} in the implementation's \`injectedParams\` \u2014 [${[...wiring, ...transport].join(", ")}] \u2014 and take the contract's own parameters after ${transport.length === 1 ? "it" : "them"}, in order: ${symbol}(${[...wiring, ...transport, ...declared.map((param) => `${param.name}${param.optional ? "?" : ""}`), ...trailing].join(", ")}), the router unpacking the path, query and body into them \u2014 or, where the framework hands the function the handles alone, read each of the contract's parameters off the injected request by its own name (\`${transport[0]}.params.${declared[0]?.name ?? "id"}\`), which realizes it through the handle. An injected name matches with or without its leading underscore, and a handle the function never reads may carry one instead.` : "";
30450
30961
  const unrealized = agreed(judgements, "unrealized");
30451
30962
  if (unrealized.length > 0) {
30452
30963
  ctx.addIssue(
@@ -30464,7 +30975,7 @@ var paramConformanceRule = {
30464
30975
  ctx.addIssue(
30465
30976
  "warning",
30466
30977
  "UNDECLARED_PARAM",
30467
- `${opening} realizing method "${method2.name}" of contract "${implementation.contract}" takes ${undeclared.length} parameter(s) the contract does not declare and no declared injection accounts for \u2014 ${undeclared.map((found) => found.told).join(", ")}. An argument a caller must supply that the design never mentions is how a credential ends up in a signature nobody has read against its contract. Declare it on the contract, name it in the implementation's \`injectedParams\` when whatever wires this component up supplies it (a LEADING run only), or take it out of the signature.` + greenPath,
30978
+ `${opening} realizing method "${method2.name}" of contract "${implementation.contract}" takes ${undeclared.length} parameter(s) the contract does not declare and no declared injection accounts for \u2014 ${undeclared.map((found) => found.told).join(", ")}. An argument a caller must supply that the design never mentions is how a credential ends up in a signature nobody has read against its contract. Declare it on the contract, name it in the implementation's \`injectedParams\` when whatever wires this component up supplies it (a leading or trailing run, with or without its underscore), or take it out of the signature.` + greenPath,
30468
30979
  implementation.id,
30469
30980
  draftContext4,
30470
30981
  void 0,
@@ -30496,6 +31007,22 @@ var paramConformanceRule = {
30496
31007
  );
30497
31008
  }
30498
31009
  }
31010
+ for (const implementation of ctx.implementations) {
31011
+ const injected = implementation.injectedParams ?? [];
31012
+ const taken = takenBy.get(implementation.id);
31013
+ if (injected.length === 0 || !taken) continue;
31014
+ const stale = injected.filter((name) => !taken.has(bare(name)));
31015
+ if (stale.length === 0) continue;
31016
+ ctx.addIssue(
31017
+ "warning",
31018
+ "UNUSED_INJECTED_PARAM",
31019
+ `Implementation "${implementation.id}" declares ${stale.length} injected parameter(s) none of its realizing functions takes \u2014 ${stale.map((name) => `"${name}"`).join(", ")}. An injection is wiring the parameter check sets aside wherever it stands at either end of a signature, so one that no function takes waves through the next parameter of that name unread \u2014 a credential included. Drop it from injectedParams, or take it in the handler that is handed it.`,
31020
+ implementation.id,
31021
+ ctx.isImplementationDraft(implementation),
31022
+ void 0,
31023
+ { at: "injectedParams", covers: stale }
31024
+ );
31025
+ }
30499
31026
  }
30500
31027
  };
30501
31028
 
@@ -30503,7 +31030,7 @@ var paramConformanceRule = {
30503
31030
  var asyncConformanceRule = {
30504
31031
  name: "async-conformance",
30505
31032
  judges: "code",
30506
- description: "Code-to-contract for ASYNC: a contract method whose returns is `async T` is realized by a function that completes later, and one whose returns is not, by one that completes now \u2014 every caller in every language with async/await is shaped by the difference, so a contract that hides it misleads every implementer and translation built from it. Compared with source_file_facts.asyncFunctions for the method's realizing function (under its `symbol`), at exact grade only; where a name has several bodies only what they all agree on is reported, as param-conformance does. A method whose file only calls the function is methodRealization's finding.",
31033
+ description: "Code-to-contract for ASYNC: a contract method whose returns is `async T` is realized by a function that completes later, and one whose returns is not, by one that completes now \u2014 every caller in every language with async/await is shaped by the difference, so a contract that hides it misleads every implementer and translation built from it. Compared with source_file_facts.asyncFunctions for the method's realizing function (under its `symbol`), at exact grade only; where a name has several bodies only what they all agree on is reported, as param-conformance does. A method whose file only calls the function is methodRealization's finding, and a method whose conformance dial is off (its own, else its implementation's) is not judged.",
30507
31034
  codes: [
30508
31035
  {
30509
31036
  code: "ASYNC_MISMATCH",
@@ -30514,7 +31041,8 @@ var asyncConformanceRule = {
30514
31041
  ],
30515
31042
  check(ctx) {
30516
31043
  const code = ctx.codeIndex();
30517
- for (const { implementation, method: method2, sourceFile, draftContext: draftContext4 } of ctx.implementationMethods()) {
31044
+ for (const { implementation, method: method2, component, sourceFile, draftContext: draftContext4 } of ctx.implementationMethods()) {
31045
+ if ((method2.conformance ?? implementation.conformance ?? defaultConformanceTier(component)) === "off") continue;
30518
31046
  const contractMethod2 = ctx.interfaceMap.get(implementation.contract)?.methods.find((m) => m.name === method2.name);
30519
31047
  if (!contractMethod2 || typeof contractMethod2.returns !== "string" || !sourceFile) continue;
30520
31048
  const file = pathKey(sourceFile);
@@ -30599,19 +31127,27 @@ function memberSurface(ctx, node, alias) {
30599
31127
  for (const f of type.fields ?? []) pending3.push(...extractTypeIdentifiers(f.type));
30600
31128
  }
30601
31129
  }
30602
- const seen = new Set(types.map((t) => nameKey(t.def.id)));
31130
+ const seen = new Set(types.map((t) => `${t.alias}::${nameKey(t.def.id)}`));
30603
31131
  while (pending3.length > 0) {
30604
31132
  const ref = pending3.pop();
30605
31133
  const type = memberType(ref);
30606
31134
  if (!type) continue;
30607
31135
  const id = lastSegment4(type.id.replace(/::/g, "."));
30608
- if (seen.has(nameKey(id))) continue;
30609
- seen.add(nameKey(id));
30610
- types.push({ alias, def: asDef(type, id) });
31136
+ const owner = type.id.includes("::") ? type.id.slice(0, type.id.indexOf("::")) : ns;
31137
+ const ownerAlias = owner === ns ? alias : ownerAliasOf(ctx, owner);
31138
+ const key2 = `${ownerAlias}::${nameKey(id)}`;
31139
+ if (seen.has(key2)) continue;
31140
+ seen.add(key2);
31141
+ types.push({ alias: ownerAlias, def: asDef(type, id) });
30611
31142
  for (const f of type.fields ?? []) pending3.push(...extractTypeIdentifiers(f.type));
30612
31143
  }
30613
31144
  return { entries, types };
30614
31145
  }
31146
+ function ownerAliasOf(ctx, namespace) {
31147
+ const node = (ctx.projectFamily?.nodes ?? []).find((n) => n.namespace === namespace);
31148
+ const declared = [...boundRoot(ctx)?.aliases.entries() ?? []].find(([, key2]) => key2 === namespace)?.[0];
31149
+ return node?.mountAlias ?? declared ?? node?.id ?? namespace;
31150
+ }
30615
31151
  function stem(name) {
30616
31152
  return nameKey(name).replace(/(?:api|port|client)$/, "") || nameKey(name);
30617
31153
  }
@@ -30726,10 +31262,12 @@ function matchOf(decl, entries, types, tagAlias) {
30726
31262
  const name = rawName.split(".")[0];
30727
31263
  const entry = entries.find((e) => e.alias === alias && (nameKey(e.entry.id) === nameKey(name) || nameKey(e.entry.name) === nameKey(name)));
30728
31264
  if (entry) return { entry };
30729
- const type = types.find((t) => t.alias === alias && (nameKey(t.def.id) === nameKey(name) || nameKey(t.def.name) === nameKey(name)));
30730
- if (type) return { type };
31265
+ const byId = types.find((t) => t.alias === alias && nameKey(t.def.id) === nameKey(name));
31266
+ if (byId) return { type: byId };
30731
31267
  const renamed2 = types.find((t) => t.alias === alias && (t.def.formerly ?? []).some((f) => nameKey(f) === nameKey(name)));
30732
31268
  if (renamed2) return { renamedType: renamed2 };
31269
+ const byName2 = types.find((t) => t.alias === alias && nameKey(t.def.name) === nameKey(name));
31270
+ if (byName2) return { type: byName2 };
30733
31271
  }
30734
31272
  const methods = decl.members.filter((m) => m.params !== void 0);
30735
31273
  const byName = stem(decl.name);
@@ -30774,7 +31312,8 @@ function declarationDrift(decl, entries, types, tagAlias) {
30774
31312
  const type = match2.type ?? match2.renamedType;
30775
31313
  if (type) {
30776
31314
  const renamedFrom = formerType(decl.name, [type]) ?? (match2.renamedType ? type : void 0);
30777
- const named2 = renamedFrom ? [`the type "${decl.name}" was renamed to "${type.def.name}" (${type.alias}::${type.def.id}) \u2014 follow the rename`] : [];
31315
+ const now = nameKey(type.def.name) === nameKey(decl.name) ? type.def.id : type.def.name;
31316
+ const named2 = renamedFrom ? [`the type "${decl.name}" was renamed to "${now}" (${type.alias}::${type.def.id}) \u2014 follow the rename`] : [];
30778
31317
  const drift = decl.kind === "alias" || decl.kind === "enum" ? named2 : [...named2, ...fieldDrift(decl.members, type, types)];
30779
31318
  return { against: `${type.alias}::${type.def.id}`, drift };
30780
31319
  }
@@ -30831,7 +31370,7 @@ function unreadReason(module2, reach2) {
30831
31370
  var bindingModulesRule = {
30832
31371
  name: "binding-modules",
30833
31372
  judges: "code",
30834
- description: "Code-to-pin for a consumer's hand-written binding module (implementation_spec.bindings): the one file where the consumer's code spells another project's names is compared with what the projects the implementation's component reaches publish, so a re-pin (or a member's edit) that renamed, removed or reshaped a name the binding still declares is reported instead of surviving tsc and the tests on the binding's own stale declarations. A component reaches a project through every `alias::name` its specs write: a dependsOn, owns, narrative or declared call, an implements, and every named type its contract's params and returns write, directly or through the bound project's own types they name (so a binding to a types-only contracts library is reached by the types a contract names). A pinned external is compared with its pinned snapshot. A contained MEMBER project (an alias the bound root declares under `members` \u2014 named by that alias or by the member's project id \u2014 read live and never pinned) is compared the same way against what its L0 export table exports now: each exported contract's methods with their params and rename traces, each exported type's fields with theirs, so a parameter, field or method rename in a member reaches a member consumer at validate time, with the rename to follow. Each binding declaration is matched to a reached interface or type by the `alias::name` its doc comment names, else by name (nameKey of the interface's id or name, a type's id or name, with an Api/Port/Client suffix ignored), else \u2014 for an interface with methods \u2014 by the reached interface most of its member names belong to (a trace in `formerly` counts); an exported function is matched by name against the methods of every reached interface, or, when its doc comment names `alias::name` (or `alias::name.method`), against that interface alone. A matched member that is a former name is reported with the rename to follow; a member of a matched interface it does not hold, a function its tagged interface no longer holds, and a function or member a pinned entry's `retired` list names (a method the producer removed since an earlier pin) are reported removed; a parameter list whose arity or names differ from the method's (a param's previousNames giving the rename) is reported; a type declaration is compared field by field \u2014 a field the matched type does not declare (a field's `formerly` giving the rename), a field it declares that the binding lacks, a declaration still named by the type's former name, and a field whose declared type names a type the producer renamed are each reported with the new name. Names compare by nameKey, so snake_case and camelCase spellings of one name agree. A declaration nothing matches is the binding's own plumbing and is never judged. A binding that cannot be read, is not TypeScript or JavaScript, declares nothing the reader compares (naming each export form it met but cannot see into, such as a CommonJS `module.exports = require(\u2026)`), or whose component reaches neither a pinned external nor a member project is reported once as unread \u2014 a member is named as a member (one that exports nothing is told to export there), and pinning is suggested only for a declared external, never for a member \u2014 a notice, so a planned binding passes a design-only gate.",
31373
+ description: "Code-to-pin for a consumer's hand-written binding module (implementation_spec.bindings): the one file where the consumer's code spells another project's names is compared with what the projects the implementation's component reaches publish, so a re-pin (or a member's edit) that renamed, removed or reshaped a name the binding still declares is reported instead of surviving tsc and the tests on the binding's own stale declarations. A component reaches a project through every `alias::name` its specs write: a dependsOn, owns, narrative or declared call, an implements, and every named type its contract's params and returns write, directly or through the bound project's own types they name (so a binding to a types-only contracts library is reached by the types a contract names). A pinned external is compared with its pinned snapshot. A contained MEMBER project (an alias the bound root declares under `members` \u2014 named by that alias or by the member's project id \u2014 read live and never pinned) is compared the same way against what its L0 export table exports now: each exported contract's methods with their params and rename traces, each exported type's fields with theirs (a type its exports name that another project owns is that project's, compared and named under the alias the bound root reaches it by, never the member's), so a parameter, field or method rename in a member reaches a member consumer at validate time, with the rename to follow. Each binding declaration is matched to a reached interface or type by the `alias::name` its doc comment names (a public name or type id, never a display name: a tag naming a type's former id is that type under its retired name), else by name (nameKey of the interface's id or name, a type's id or name, with an Api/Port/Client suffix ignored), else \u2014 for an interface with methods \u2014 by the reached interface most of its member names belong to (a trace in `formerly` counts); an exported function is matched by name against the methods of every reached interface, or, when its doc comment names `alias::name` (or `alias::name.method`), against that interface alone. A matched member that is a former name is reported with the rename to follow; a member of a matched interface it does not hold, a function its tagged interface no longer holds, and a function or member a pinned entry's `retired` list names (a method the producer removed since an earlier pin) are reported removed; a parameter list whose arity or names differ from the method's (a param's previousNames giving the rename) is reported; a type declaration is compared field by field \u2014 a field the matched type does not declare (a field's `formerly` giving the rename), a field it declares that the binding lacks, a declaration still named by the type's former name, and a field whose declared type names a type the producer renamed are each reported with the new name. Names compare by nameKey, so snake_case and camelCase spellings of one name agree. A declaration nothing matches is the binding's own plumbing and is never judged. A binding that cannot be read, is not TypeScript or JavaScript, declares nothing the reader compares (naming each export form it met but cannot see into, such as a CommonJS `module.exports = require(\u2026)`), or whose component reaches neither a pinned external nor a member project is reported once as unread \u2014 a member is named as a member (one that exports nothing is told to export there), and pinning is suggested only for a declared external, never for a member \u2014 a notice, so a planned binding passes a design-only gate.",
30835
31374
  codes: [
30836
31375
  { code: "BINDING_DRIFT", defaultSeverity: "warning", summary: "A binding module declares a producer name its pin (or a member project's live export) renamed (with the rename to follow) or removed, a parameter list that differs from the method's, or a type whose name or fields differ from the producer's type \u2014 code calling into another project through a shape the producer no longer has" },
30837
31376
  { code: "BINDING_UNREAD", defaultSeverity: "notice", summary: "A binding module an implementation names could not be compared: it is not on disk yet, escapes the root, cannot be read, is in a language the reader does not read, declares nothing the reader compares (naming the export forms it cannot see into), or its component reaches neither a pinned external nor a member project that exports something" }
@@ -30867,7 +31406,7 @@ var bindingModulesRule = {
30867
31406
  ...reach2.pins.flatMap((p) => p.snapshot.types.map((def) => ({ alias: p.alias, def }))),
30868
31407
  ...reach2.members.flatMap((m) => m.surface.types)
30869
31408
  ];
30870
- const live = new Set(reach2.members.map((m) => m.alias));
31409
+ const live = new Set(reach2.members.flatMap((m) => [m.alias, ...m.surface.types.map((t) => t.alias)]));
30871
31410
  const tagAlias = (alias) => {
30872
31411
  if (reach2.pins.some((p) => p.alias === alias)) return alias;
30873
31412
  return reach2.members.find((m) => m.alias === alias || m.node.namespace === alias || m.node.id === alias)?.alias ?? alias;
@@ -30912,7 +31451,7 @@ function answers(route2, endpoint) {
30912
31451
  var routeCoverageRule = {
30913
31452
  name: "route-coverage",
30914
31453
  judges: "code",
30915
- description: "Code-to-contract for the ROUTES: does every route a router actually answers have a contract endpoint, and does every contract endpoint have a route that answers it? A portal's endpoints are what its contract promises; the router is what the code serves; nothing compared the two, so a route with no contract (including a write) could run for months without a single rule noticing. The Portal's own implementation names its router entry (`router`, code linkage), and the routes are read out of that function and the functions of its file it calls by name. Two idioms are read: guards (a method comparison with comparisons on the path's split segments), where a prefix the router strips before splitting the path is folded in front; and a route table the router names (an array of objects pairing a method with a `/a/:b` template or an anchored regular expression, or an object keyed `VERB /path`), read only when every entry settles. Every compared value is a literal or one the code's constants, concatenations and template literals settle \u2014 through the type checker, any module's constant and any expression it types as one string literal \u2014 and never a guess. A route that states the whole path is read under the Portal's basePath, which the contract's endpoint paths are written beneath; a leading path segment a guard router never checks is completed from the first segments of the Portal's own HTTP endpoint paths, the segments whatever serves the Portal routes on. Which process serves which Portal is implementation, so no listener is consulted. A router that yields no route in either idiom is reported as unread, never passed: a check that cannot see a router must say so rather than stay quiet.",
31454
+ description: "Code-to-contract for the ROUTES: does every route a router actually answers have a contract endpoint, and does every contract endpoint have a route that answers it? A portal's endpoints are what its contract promises; the router is what the code serves; nothing compared the two, so a route with no contract (including a write) could run for months without a single rule noticing. The Portal's own implementation names its router (`router`, code linkage): an entry its own files export, an entry or route table another module exports (`<module>#<name>`, a central routes.ts), or a module alone, every route-bearing export of which is read as one router; the routes are read out of that entry and the functions of its file it calls by name. A router several Portals name is read once against them all: a route is declared when any of them declares it, and an endpoint is unrouted per Portal. Two idioms are read: guards (a method comparison with comparisons on the path's split segments), where a prefix the router strips before splitting the path is folded in front; and a route table \u2014 a const the router names, one it returns or states in place unnamed, or the table the linkage names itself \u2014 (an array of objects pairing a method with a `/a/:b` template or an anchored regular expression, or an object keyed `VERB /path`, a key written as a template literal or a concatenation over constants settling like any other value), read only when every entry settles. A table is written under the ONE prefix its router strips off the path before matching it (`path.slice(BASE.length)`, `.replace(BASE, '')`), which is folded in front of every entry that does not already state it, so a stripped prefix that is not the Portal's basePath is read as the different URL it is, and the finding names both. Every compared value is a literal or one the code's constants, concatenations and template literals settle \u2014 through the type checker, any module's constant and any expression it types as one string literal \u2014 and never a guess. A route that states the whole path is read under the Portal's basePath, which the contract's endpoint paths are written beneath; a leading path segment a guard router never checks is completed from the first segments of the Portal's own HTTP endpoint paths, the segments whatever serves the Portal routes on. Which process serves which Portal is implementation, so no listener is consulted. A router that yields no route in either idiom is reported as unread, never passed: a check that cannot see a router must say so rather than stay quiet.",
30916
31455
  codes: [
30917
31456
  {
30918
31457
  code: "UNDECLARED_ROUTE",
@@ -30938,40 +31477,42 @@ var routeCoverageRule = {
30938
31477
  check(ctx) {
30939
31478
  const code = ctx.codeIndex();
30940
31479
  const realization = ctx.realizationIndex();
31480
+ const groups = /* @__PURE__ */ new Map();
30941
31481
  for (const portal of ctx.components) {
30942
31482
  if (portal.componentType !== "Portal") continue;
30943
31483
  for (const impl of realization.implementationsOf(portal.id)) {
30944
31484
  const via = impl.router;
30945
31485
  if (!via) continue;
30946
- const anchor = impl.id;
30947
- const holders = realization.filesOf(portal.id).map(pathKey).filter((file) => {
31486
+ const linkage = routerLinkage(via);
31487
+ const files = linkage.file ? [pathKey(linkage.file)] : realization.filesOf(portal.id).map(pathKey);
31488
+ const exact = files.filter((file) => {
30948
31489
  const facts = code.factsAt(file);
30949
- return !!facts && facts.status === "analyzed" && facts.analysisGrade === "exact" && !!facts.functionParams && Object.prototype.hasOwnProperty.call(facts.functionParams, via);
31490
+ return !!facts && facts.status === "analyzed" && facts.analysisGrade === "exact";
30950
31491
  });
31492
+ const holds = (file, name) => {
31493
+ const facts = code.factsAt(file);
31494
+ return !!facts.functionParams && Object.prototype.hasOwnProperty.call(facts.functionParams, name) || !!facts.functionRoutes && Object.prototype.hasOwnProperty.call(facts.functionRoutes, name);
31495
+ };
31496
+ const holders = linkage.name !== void 0 ? exact.filter((file) => holds(file, linkage.name)) : exact;
30951
31497
  if (holders.length === 0) continue;
31498
+ const prefixes = /* @__PURE__ */ new Set();
30952
31499
  const read2 = holders.flatMap((file) => {
30953
- const routes = code.factsAt(file).functionRoutes;
31500
+ const facts = code.factsAt(file);
31501
+ const routes = facts.functionRoutes;
30954
31502
  if (!routes) return [];
30955
- const names = /* @__PURE__ */ new Set([via]);
30956
- for (const site of closedCallSites(code, file, via) ?? []) {
30957
- if (!site.member && pathKey(site.from ?? file) === file) names.add(site.name);
31503
+ const entries = linkage.name !== void 0 ? [linkage.name] : facts.exportedNames.filter((name) => Object.prototype.hasOwnProperty.call(routes, name));
31504
+ const names = new Set(entries);
31505
+ for (const entry of entries) {
31506
+ for (const site of closedCallSites(code, file, entry) ?? []) {
31507
+ if (!site.member && pathKey(site.from ?? file) === file) names.add(site.name);
31508
+ }
31509
+ }
31510
+ for (const name of names) {
31511
+ const stripped = facts.routePrefixes?.[name];
31512
+ if (stripped !== void 0 && Object.prototype.hasOwnProperty.call(facts.routePrefixes, name)) prefixes.add(stripped);
30958
31513
  }
30959
31514
  return [...names].flatMap((name) => Object.prototype.hasOwnProperty.call(routes, name) ? routes[name] : []);
30960
31515
  });
30961
- const draftContext4 = ctx.isComponentDraft(portal.id) || ctx.isImplementationDraft(impl);
30962
- const where = holders.map((file) => `"${file}"`).join(", ");
30963
- if (read2.length === 0) {
30964
- ctx.addIssue(
30965
- "warning",
30966
- "UNREADABLE_ROUTER",
30967
- `Portal "${portal.id}" names its router "${via}" (in ${where}), but no branch of "${via}" reads as a route \u2014 so none of its routes were checked against the contract at all. Only two idioms are read, in the entry and in the functions of its file it calls by name: an \`if\` whose conditions, together with those of every enclosing \`if\`, compare \`<request>.method\` with a string and \`parts[i]\` / \`parts.length\` with values the code spells out or its constants settle; and a route table it names \u2014 an array of objects pairing a method with a path pattern (a \`/a/:b\` template or an anchored regular expression), or an object keyed \`VERB /path\` \u2014 every entry of which settles. Silence here would read as a clean router; it is only one this analysis cannot see. Write the router in one of those idioms, or carry this finding with the reason it cannot be.`,
30968
- anchor,
30969
- draftContext4,
30970
- void 0,
30971
- { at: via }
30972
- );
30973
- continue;
30974
- }
30975
31516
  const endpoints = ctx.interfaceMethodsOf(portal.id).flatMap((method2) => {
30976
31517
  const endpoint = method2.endpoint;
30977
31518
  if (endpoint?.transport !== "HTTP") return [];
@@ -30982,32 +31523,83 @@ var routeCoverageRule = {
30982
31523
  }];
30983
31524
  });
30984
31525
  const heads = [...new Set(endpoints.map((endpoint) => endpoint.segments[0]).filter((head2) => !!head2 && head2 !== "*"))].sort();
30985
- const base = (portal.basePath ?? "").split("/").filter(Boolean);
30986
- const served = read2.map((route2) => completed(route2, heads, base));
30987
- const undeclared = [...new Set(served.filter((variants) => !variants.some((route2) => endpoints.some((endpoint) => answers(route2, endpoint)))).flatMap((variants) => variants.map((route2) => route2.key)))].sort();
30988
- if (undeclared.length > 0) {
30989
- ctx.addIssue(
30990
- "warning",
30991
- "UNDECLARED_ROUTE",
30992
- `Portal "${portal.id}"'s router "${via}" (${where}) answers ${undeclared.length} route(s) no contract endpoint of "${portal.id}" declares \u2014 ${undeclared.map((key2) => `"${key2}"`).join(", ")}. A surface the code serves and the design never promised is how a write runs with no contract: no brief, no auth review, no rule reading it. Declare each as a contract method with its endpoint, or take the route out of the router.`,
30993
- anchor,
30994
- draftContext4,
30995
- void 0,
30996
- { at: via, covers: undeclared }
30997
- );
30998
- }
30999
- const unrouted = [...new Set(endpoints.filter((endpoint) => !served.some((variants) => variants.some((route2) => answers(route2, endpoint)))).map((endpoint) => endpoint.key))].sort();
31000
- if (unrouted.length > 0) {
31526
+ const router = {
31527
+ portal,
31528
+ impl,
31529
+ via,
31530
+ holders,
31531
+ read: read2,
31532
+ endpoints,
31533
+ heads,
31534
+ base: (portal.basePath ?? "").split("/").filter(Boolean),
31535
+ draftContext: ctx.isComponentDraft(portal.id) || ctx.isImplementationDraft(impl),
31536
+ ...prefixes.size === 1 ? { prefix: [...prefixes][0] } : {}
31537
+ };
31538
+ const key2 = linkage.file ? `${pathKey(linkage.file)}#${linkage.name ?? "*"}` : `${impl.id}#${via}`;
31539
+ groups.set(key2, [...groups.get(key2) ?? [], router]);
31540
+ }
31541
+ }
31542
+ for (const routers of groups.values()) {
31543
+ const [first] = routers;
31544
+ const where = first.holders.map((file) => `"${file}"`).join(", ");
31545
+ const via = first.via;
31546
+ if (first.read.length === 0) {
31547
+ for (const router of routers) {
31001
31548
  ctx.addIssue(
31002
31549
  "warning",
31003
- "UNROUTED_ENDPOINT",
31004
- `Portal "${portal.id}" binds ${unrouted.length} HTTP endpoint(s) that no route of its router "${via}" (${where}) answers \u2014 ${unrouted.map((key2) => `"${key2}"`).join(", ")}. The contract promises a route no request can reach. Route it in the router, or drop the endpoint from the contract.`,
31005
- anchor,
31006
- draftContext4,
31550
+ "UNREADABLE_ROUTER",
31551
+ `Portal "${router.portal.id}" names its router "${via}" (in ${where}), but no branch of "${via}" reads as a route \u2014 so none of its routes were checked against the contract at all. Only two idioms are read, in the entry and in the functions of its file it calls by name: an \`if\` whose conditions, together with those of every enclosing \`if\`, compare \`<request>.method\` with a string and \`parts[i]\` / \`parts.length\` with values the code spells out or its constants settle; and a route table \u2014 an array of objects pairing a method with a path pattern (a \`/a/:b\` template or an anchored regular expression), or an object keyed \`VERB /path\` (a key may be a template literal over constants), every entry of which settles \u2014 whether a const names it, the function returns it in place, or the router linkage names the table itself (\`<module>#<table>\`, or a module whose exports are its tables). Silence here would read as a clean router; it is only one this analysis cannot see. Write the router in one of those idioms, or carry this finding with the reason it cannot be.`,
31552
+ router.impl.id,
31553
+ router.draftContext,
31007
31554
  void 0,
31008
- { at: via, covers: unrouted }
31555
+ { at: via }
31009
31556
  );
31010
31557
  }
31558
+ continue;
31559
+ }
31560
+ const prefixSegments = first.prefix !== void 0 ? first.prefix.split("/").filter(Boolean) : [];
31561
+ const routes = first.read.map((route2) => {
31562
+ if (!route2.table || prefixSegments.length === 0) return route2;
31563
+ const already = prefixSegments.every((segment, index) => route2.segments[index] === segment);
31564
+ return already ? route2 : { ...route2, segments: [...prefixSegments, ...route2.segments], fullPath: true };
31565
+ });
31566
+ const servedBy = (router) => routes.map((route2) => completed(route2, router.heads, router.base));
31567
+ const strip = (router) => {
31568
+ if (first.prefix === void 0 || !first.read.some((route2) => route2.table)) return "";
31569
+ const basePath = `/${router.base.join("/")}`;
31570
+ if (router.base.length === prefixSegments.length && router.base.every((segment, index) => segment === prefixSegments[index])) return "";
31571
+ return ` The router strips "${first.prefix}" off the path before reading its table, where the Portal's basePath is "${basePath}" \u2014 so every table route is served under "${first.prefix}", which is not where the contract puts it.`;
31572
+ };
31573
+ const portals = routers.map((router) => `"${router.portal.id}"`).join(", ");
31574
+ const declaredBySome = (index) => routers.some((router) => {
31575
+ const variants = servedBy(router)[index];
31576
+ return variants.some((route2) => router.endpoints.some((endpoint) => answers(route2, endpoint)));
31577
+ });
31578
+ const undeclared = [...new Set(routes.map((_, index) => index).filter((index) => !declaredBySome(index)).flatMap((index) => servedBy(first)[index].map((route2) => route2.key)))].sort();
31579
+ if (undeclared.length > 0) {
31580
+ ctx.addIssue(
31581
+ "warning",
31582
+ "UNDECLARED_ROUTE",
31583
+ `Portal "${first.portal.id}"'s router "${via}" (${where}) answers ${undeclared.length} route(s) no contract endpoint of ${routers.length > 1 ? `the Portals reading it (${portals})` : `"${first.portal.id}"`} declares \u2014 ${undeclared.map((key2) => `"${key2}"`).join(", ")}. A surface the code serves and the design never promised is how a write runs with no contract: no brief, no auth review, no rule reading it. Declare each as a contract method with its endpoint, or take the route out of the router.${strip(first)}`,
31584
+ first.impl.id,
31585
+ first.draftContext,
31586
+ void 0,
31587
+ { at: via, covers: undeclared }
31588
+ );
31589
+ }
31590
+ for (const router of routers) {
31591
+ const served = servedBy(router);
31592
+ const unrouted = [...new Set(router.endpoints.filter((endpoint) => !served.some((variants) => variants.some((route2) => answers(route2, endpoint)))).map((endpoint) => endpoint.key))].sort();
31593
+ if (unrouted.length === 0) continue;
31594
+ ctx.addIssue(
31595
+ "warning",
31596
+ "UNROUTED_ENDPOINT",
31597
+ `Portal "${router.portal.id}" binds ${unrouted.length} HTTP endpoint(s) that no route of its router "${via}" (${where}) answers \u2014 ${unrouted.map((key2) => `"${key2}"`).join(", ")}. The contract promises a route no request can reach. Route it in the router, or drop the endpoint from the contract.${strip(router)}`,
31598
+ router.impl.id,
31599
+ router.draftContext,
31600
+ void 0,
31601
+ { at: via, covers: unrouted }
31602
+ );
31011
31603
  }
31012
31604
  }
31013
31605
  }
@@ -31017,7 +31609,7 @@ var routeCoverageRule = {
31017
31609
  var unclaimedSourceRule = {
31018
31610
  name: "unclaimed-source",
31019
31611
  judges: "code",
31020
- description: "Code\u2194spec Level 1, asked the other way round: every source file under the project's declared source roots must be named by some spec \u2014 an implementation's sourcePath, a method's, a simPath, a binding module an implementation declares in `bindings`, or a type's \u2014 or be carried in the frozen `rules.conformance.unclaimed` list. A pure re-export barrel declares nothing of its own and only republishes other modules, so it has no code to claim and is exempt (exact grade only: a weaker grade cannot tell a barrel from a file it failed to parse). Declaring no source roots leaves the check silent, which is what makes it opt-in; the unclaimed list is a one-way debt register, so an entry the walk no longer finds unclaimed must be deleted and the list can only shrink.",
31612
+ description: "Code\u2194spec Level 1, asked the other way round: every source file under the project's declared source roots must be named by some spec \u2014 an implementation's sourcePath, a method's, a simPath, a binding module an implementation declares in `bindings`, the module a Portal's router linkage names (`<module>#<name>`, or the module alone), or a type's \u2014 or be carried in the frozen `rules.conformance.unclaimed` list. A pure re-export barrel declares nothing of its own and only republishes other modules, so it has no code to claim and is exempt (exact grade only: a weaker grade cannot tell a barrel from a file it failed to parse). Declaring no source roots leaves the check silent, which is what makes it opt-in; the unclaimed list is a one-way debt register, so an entry the walk no longer finds unclaimed must be deleted and the list can only shrink.",
31021
31613
  codes: [
31022
31614
  { code: "UNCLAIMED_SOURCE_FILE", defaultSeverity: "warning", summary: "A source file under a declared source root that no spec names and the frozen unclaimed list does not carry \u2014 code nobody designed" },
31023
31615
  { code: "STALE_UNCLAIMED_ENTRY", defaultSeverity: "warning", summary: "An entry of the frozen unclaimed list names a file that is now claimed, or that the source-root walk no longer finds \u2014 delete it, the list only shrinks" }
@@ -31031,6 +31623,8 @@ var unclaimedSourceRule = {
31031
31623
  for (const file of implementationSourceFiles(impl)) claimed.add(pathKey(file));
31032
31624
  if (impl.simPath) claimed.add(pathKey(impl.simPath));
31033
31625
  for (const file of impl.bindings ?? []) claimed.add(pathKey(file));
31626
+ const routerFile = impl.router ? routerLinkage(impl.router).file : void 0;
31627
+ if (routerFile) claimed.add(pathKey(routerFile));
31034
31628
  }
31035
31629
  for (const type of ctx.types) {
31036
31630
  for (const file of typeSourceFiles(type)) claimed.add(pathKey(file));
@@ -31066,7 +31660,7 @@ var NOTHING_PROMISED = /* @__PURE__ */ new Set();
31066
31660
  var exportConformanceRule = {
31067
31661
  name: "export-conformance",
31068
31662
  judges: "code",
31069
- description: "Code-to-contract for the SURFACE: does this file publish anything the components it realizes never promised, that another component then takes? The rest of the conformance set reads contract-to-code (it asks whether the code holds what a spec claims), so a file could export whatever it liked under a component's name and nothing looked. That is one half of the same hole as a contract promising a parameter it never passes. A method's `exportedVia` names the export a consumer imports to REACH it: the value that composes it, which `symbol` cannot name because `symbol` names the function inside. A Portal implementation's `router` names the entry whatever serves the Portal calls to hand it its requests. Both are declared publication, and a declared handle the file does not actually export is itself a finding, so naming one can never become a free-text suppression. Both questions are asked only of a file read at exact grade, and a taker counts only when its own import specifier resolves to the file it is accused of taking from: below that nothing separates an export from a mention, or a shared word from a shared module. A taker inside the owns fence is no crossing: a pattern constructs the blocks it owns, so the owner's file, or another member of the same pattern, importing a member's class is the owns relation made real.",
31663
+ description: "Code-to-contract for the SURFACE: does this file publish anything the components it realizes never promised, that another component then takes? The rest of the conformance set reads contract-to-code (it asks whether the code holds what a spec claims), so a file could export whatever it liked under a component's name and nothing looked. That is one half of the same hole as a contract promising a parameter it never passes. A method's `exportedVia` names the export a consumer imports to REACH it: the value that composes it, which `symbol` cannot name because `symbol` names the function inside. A Portal implementation's `router` names the entry whatever serves the Portal calls to hand it its requests \u2014 in the Portal's own files, or, written `<module>#<name>`, in the module it names, which must export that name (a module named alone must be on disk). Both are declared publication, and a declared handle the file does not actually export is itself a finding, so naming one can never become a free-text suppression. Both questions are asked only of a file read at exact grade, and a taker counts only when its own import specifier resolves to the file it is accused of taking from: below that nothing separates an export from a mention, or a shared word from a shared module. A taker inside the owns fence is no crossing: a pattern constructs the blocks it owns, so the owner's file, or another member of the same pattern, importing a member's class is the owns relation made real.",
31070
31664
  codes: [
31071
31665
  {
31072
31666
  code: "UNDECLARED_EXPORT",
@@ -31132,6 +31726,22 @@ var exportConformanceRule = {
31132
31726
  const contract = ctx.interfaceMap.get(implementation.contract);
31133
31727
  const portal = contract ? ctx.componentMap.get(contract.component) : void 0;
31134
31728
  if (!portal || portal.componentType !== "Portal" || ctx.isInChainedSubproject(portal.subsystem)) continue;
31729
+ const linkage = routerLinkage(handle);
31730
+ if (linkage.file) {
31731
+ const file = pathKey(linkage.file);
31732
+ const facts = code.factsAt(file);
31733
+ const missing = !facts || facts.status === "missing";
31734
+ const exactHere = !!facts && facts.status === "analyzed" && facts.analysisGrade === "exact";
31735
+ if (!missing && (!exactHere || linkage.name === void 0 || facts.exportedNames.includes(linkage.name))) continue;
31736
+ ctx.addIssue(
31737
+ "warning",
31738
+ "UNREALIZED_EXPORT_HANDLE",
31739
+ `Portal "${portal.id}" names its router "${handle}" (implementation "${implementation.id}"), but ` + (missing ? `"${file}" is not on disk.` : `"${file}" exports no "${linkage.name}".`) + " A `router` names the entry or the route table whatever serves the Portal hands its requests to, so one nobody can import promises a route nobody can reach, and it allows nothing either. Name an export the module actually has, or drop `router` if the Portal is served method by method.",
31740
+ implementation.id,
31741
+ ctx.isImplementationDraft(implementation) || ctx.isComponentDraft(portal.id)
31742
+ );
31743
+ continue;
31744
+ }
31135
31745
  const files = realization.filesOf(portal.id).map(pathKey);
31136
31746
  const exact = files.filter((file) => {
31137
31747
  const facts = code.factsAt(file);
@@ -31388,14 +31998,14 @@ var methodCohesionRule = {
31388
31998
  if (calledBy.size < 2) continue;
31389
31999
  const groups = groupsSharingCollaborators(calledBy).filter((g) => g.length >= 2);
31390
32000
  if (groups.length < 2) continue;
31391
- const described = groups.map((g) => {
32001
+ const described2 = groups.map((g) => {
31392
32002
  const collaborators = [...new Set(g.flatMap((m) => [...calledBy.get(m)]))].sort();
31393
32003
  return `${g.join(", ")} (calling ${collaborators.join(", ")})`;
31394
32004
  }).join("; ");
31395
32005
  ctx.addIssue(
31396
32006
  "warning",
31397
32007
  "INCOHESIVE_METHODS",
31398
- `Orchestrator "${component.id}" holds ${groups.length} groups of methods that share no called component: ${described}. Split it along those groups, move the logic out until every method is a single hand-off (a pure forwarder is exempt), or acknowledge a deliberate facade with a reasoned lint.allow for INCOHESIVE_METHODS.`,
32008
+ `Orchestrator "${component.id}" holds ${groups.length} groups of methods that share no called component: ${described2}. Split it along those groups, move the logic out until every method is a single hand-off (a pure forwarder is exempt), or acknowledge a deliberate facade with a reasoned lint.allow for INCOHESIVE_METHODS.`,
31399
32009
  component.id,
31400
32010
  ctx.isComponentDraft(component.id)
31401
32011
  );
@@ -32963,6 +33573,7 @@ function compilerIdentity(projectRoot) {
32963
33573
  }
32964
33574
  var HTTP_VERBS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
32965
33575
  var METHOD_KEYS = /* @__PURE__ */ new Set(["method", "verb", "httpMethod"]);
33576
+ var ROUTER_TEXT = /[Mm][Ee][Tt][Hh][Oo][Dd]|[Vv][Ee][Rr][Bb]|\b(?:GET|POST|PUT|PATCH|DELETE)\b/;
32966
33577
  var PATH_KEYS = /* @__PURE__ */ new Set(["path", "pattern", "route", "url", "template"]);
32967
33578
  var SETTLE_DEPTH = 8;
32968
33579
  var LITERAL_SEGMENT = /^[\w.~@!$&'+,;=%-]+$/;
@@ -33142,7 +33753,7 @@ function tableEntry(ts, entry, initializerOf, literalOf) {
33142
33753
  }
33143
33754
  const verb = keyedVerb ?? (verbs.length === 1 ? verbs[0] : void 0);
33144
33755
  const route2 = keyedPath ?? (paths.length === 1 ? paths[0] : void 0);
33145
- return verb && route2 ? { verb, segments: route2.segments, exactLength: route2.exactLength, fullPath: true } : void 0;
33756
+ return verb && route2 ? { verb, segments: route2.segments, exactLength: route2.exactLength, fullPath: true, table: true } : void 0;
33146
33757
  }
33147
33758
  function tableRoutes(ts, value, initializerOf, literalOf) {
33148
33759
  const at = bareExpression(ts, value);
@@ -33159,16 +33770,38 @@ function tableRoutes(ts, value, initializerOf, literalOf) {
33159
33770
  }
33160
33771
  if (ts.isObjectLiteralExpression(at)) {
33161
33772
  for (const property of at.properties) {
33162
- const key2 = property.name ? propertyKeyText(ts, property.name) : void 0;
33773
+ const key2 = !property.name ? void 0 : ts.isComputedPropertyName(property.name) ? settleString(ts, property.name.expression, initializerOf, literalOf) : propertyKeyText(ts, property.name);
33163
33774
  const match2 = key2 ? /^([A-Za-z]+)\s+(\/\S*)$/.exec(key2) : null;
33164
33775
  if (!match2 || !HTTP_VERBS.has(match2[1].toUpperCase())) return [];
33165
33776
  const segments = templateSegments(match2[2]);
33166
33777
  if (!segments) return [];
33167
- out.push({ verb: match2[1].toUpperCase(), segments, exactLength: true, fullPath: true });
33778
+ out.push({ verb: match2[1].toUpperCase(), segments, exactLength: true, fullPath: true, table: true });
33168
33779
  }
33169
33780
  }
33170
33781
  return out;
33171
33782
  }
33783
+ function strippedAnywhere(ts, body, initializerOf, literalOf) {
33784
+ const UNSETTLED = "\0";
33785
+ const prefixes = /* @__PURE__ */ new Set();
33786
+ const strip = (node) => {
33787
+ if (namedFunctionNameOf(ts, node) !== void 0 && node !== body) return;
33788
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
33789
+ const name = node.expression.name.text;
33790
+ const [first, second] = node.arguments;
33791
+ if ((name === "slice" || name === "substring" || name === "substr") && node.arguments.length === 1 && first && ts.isPropertyAccessExpression(first) && first.name.text === "length") {
33792
+ prefixes.add(settleString(ts, first.expression, initializerOf, literalOf) ?? UNSETTLED);
33793
+ } else if (name === "replace" && first && second && !ts.isRegularExpressionLiteral(first) && settleString(ts, second, initializerOf, literalOf) === "") {
33794
+ prefixes.add(settleString(ts, first, initializerOf, literalOf) ?? UNSETTLED);
33795
+ }
33796
+ }
33797
+ ts.forEachChild(node, strip);
33798
+ };
33799
+ strip(body);
33800
+ if (prefixes.size !== 1) return void 0;
33801
+ const [prefix] = [...prefixes];
33802
+ if (!prefix.startsWith("/") || prefix.length < 2) return void 0;
33803
+ return prefix.split("/").filter(Boolean).every((segment) => LITERAL_SEGMENT.test(segment)) ? prefix : void 0;
33804
+ }
33172
33805
  function strippedPrefix(ts, body, initializerOf, literalOf) {
33173
33806
  const sources = [];
33174
33807
  const find = (node) => {
@@ -33279,6 +33912,16 @@ function readRoutes(ts, fn, initializerOf, literalOf) {
33279
33912
  ts.forEachChild(node, names);
33280
33913
  };
33281
33914
  names(fn.body);
33915
+ const inPlace = (value) => {
33916
+ for (const route2 of tableRoutes(ts, value, initializerOf, literalOf)) routes.set(routeKey2(route2), route2);
33917
+ };
33918
+ if (!ts.isBlock(fn.body)) inPlace(fn.body);
33919
+ const returns = (node) => {
33920
+ if (namedFunctionNameOf(ts, node) !== void 0) return;
33921
+ if (ts.isReturnStatement(node) && node.expression) inPlace(node.expression);
33922
+ ts.forEachChild(node, returns);
33923
+ };
33924
+ returns(fn.body);
33282
33925
  return routes;
33283
33926
  }
33284
33927
  function walkExact(ts, sourceText, fileName) {
@@ -33577,6 +34220,47 @@ function walkExact(ts, sourceText, fileName) {
33577
34220
  const container = containerOf(owner);
33578
34221
  return container !== void 0 ? { container } : nested;
33579
34222
  };
34223
+ const readsOff = (body, name) => {
34224
+ const out = /* @__PURE__ */ new Set();
34225
+ const visit2 = (node) => {
34226
+ if (out.size >= 64) return;
34227
+ if (ts.isIdentifier(node) && node.text === name) {
34228
+ const parent = node.parent;
34229
+ const memberName = !!parent && (ts.isPropertyAccessExpression(parent) && parent.name === node || ts.isPropertyAssignment(parent) && parent.name === node);
34230
+ if (!memberName) {
34231
+ let at = node;
34232
+ for (; ; ) {
34233
+ const up = at.parent;
34234
+ if (!up) break;
34235
+ if (ts.isParenthesizedExpression(up) || ts.isNonNullExpression(up) || ts.isAsExpression(up)) {
34236
+ at = up;
34237
+ continue;
34238
+ }
34239
+ if (ts.isPropertyAccessExpression(up) && up.expression === at) {
34240
+ out.add(up.name.text);
34241
+ at = up;
34242
+ continue;
34243
+ }
34244
+ if (ts.isElementAccessExpression(up) && up.expression === at && ts.isStringLiteralLike(up.argumentExpression)) {
34245
+ out.add(up.argumentExpression.text);
34246
+ at = up;
34247
+ continue;
34248
+ }
34249
+ if (ts.isVariableDeclaration(up) && up.initializer === at && ts.isObjectBindingPattern(up.name)) {
34250
+ for (const element of up.name.elements) {
34251
+ const key2 = element.propertyName ?? element.name;
34252
+ if (ts.isIdentifier(key2) || ts.isStringLiteral(key2)) out.add(key2.text);
34253
+ }
34254
+ }
34255
+ break;
34256
+ }
34257
+ }
34258
+ }
34259
+ ts.forEachChild(node, visit2);
34260
+ };
34261
+ visit2(body);
34262
+ return [...out].sort();
34263
+ };
33580
34264
  const declaredParameters = (fn) => {
33581
34265
  const parameters = fn.parameters ?? [];
33582
34266
  const used = /* @__PURE__ */ new Set();
@@ -33598,6 +34282,14 @@ function walkExact(ts, sourceText, fileName) {
33598
34282
  if (ts.isIdentifier(parameter.name)) {
33599
34283
  fact.name = parameter.name.text;
33600
34284
  if (!used.has(fact.name) && !used.has("arguments")) fact.unused = true;
34285
+ else if (body) {
34286
+ const read2 = readsOff(body, fact.name);
34287
+ if (read2.length > 0) fact.reads = read2;
34288
+ }
34289
+ }
34290
+ if (parameter.type && ts.isTypeReferenceNode(parameter.type)) {
34291
+ const named2 = ts.isIdentifier(parameter.type.typeName) ? parameter.type.typeName.text : parameter.type.typeName.right.text;
34292
+ if (TRANSPORT_TYPES.has(named2)) fact.transport = true;
33601
34293
  }
33602
34294
  if (parameter.type) fact.type = parameter.type.getText(sf).replace(/\s+/g, " ").trim();
33603
34295
  return fact;
@@ -33875,14 +34567,24 @@ function walkExact(ts, sourceText, fileName) {
33875
34567
  ts.forEachChild(node, visit);
33876
34568
  };
33877
34569
  visit(sf);
33878
- const fileValues = fileInitializer(ts, topLevelConstants(ts, sf));
34570
+ const constants = topLevelConstants(ts, sf);
34571
+ const fileValues = fileInitializer(ts, constants);
34572
+ const routePrefixes = /* @__PURE__ */ new Map();
33879
34573
  for (const { name, node } of routeBodies) {
34574
+ const body = node.body;
34575
+ const prefix = body ? strippedAnywhere(ts, body, fileValues, void 0) : void 0;
34576
+ if (prefix !== void 0) routePrefixes.set(name, prefix);
33880
34577
  const routes = readRoutes(ts, node, fileValues);
33881
34578
  if (routes.size === 0) continue;
33882
34579
  const known = functionRoutes.get(name) ?? /* @__PURE__ */ new Map();
33883
34580
  for (const [key2, route2] of routes) known.set(key2, route2);
33884
34581
  functionRoutes.set(name, known);
33885
34582
  }
34583
+ for (const [name, initializer] of constants) {
34584
+ if (functionRoutes.has(name)) continue;
34585
+ const table = tableRoutes(ts, initializer, fileValues, void 0);
34586
+ if (table.length > 0) functionRoutes.set(name, new Map(table.map((route2) => [routeKey2(route2), route2])));
34587
+ }
33886
34588
  const composedMembers = (expression, seen) => {
33887
34589
  const extensions = [];
33888
34590
  const compose3 = (members) => {
@@ -33941,7 +34643,7 @@ function walkExact(ts, sourceText, fileName) {
33941
34643
  importBindings.set(name, fact);
33942
34644
  }
33943
34645
  for (const alias of propertyAliases) if (exported.has(alias.container)) exportAliases.push(alias);
33944
- return { declared, anchors, exported, imports, reexports, starExports, namedReexports, exportAliases, returnTypes, complexity, calls, bodies, importBindings, typeOnlyBindings, fieldTypes, localTypes, memberTypes, aliasTargets: aliasTargets2, implementsClauses, typeShapes, functionParams, paramSites, functionRoutes, asyncFunctions, enumValues, aliasTypes, mutableBindings, reexportOnly };
34646
+ return { declared, anchors, exported, imports, reexports, starExports, namedReexports, exportAliases, returnTypes, complexity, calls, bodies, importBindings, typeOnlyBindings, fieldTypes, localTypes, memberTypes, aliasTargets: aliasTargets2, implementsClauses, typeShapes, functionParams, paramSites, functionRoutes, routePrefixes, asyncFunctions, enumValues, aliasTypes, mutableBindings, reexportOnly };
33945
34647
  }
33946
34648
  var NO_PACKAGES = /* @__PURE__ */ new Map();
33947
34649
  function resolveModule(fromFile, specifier, packages) {
@@ -34276,7 +34978,7 @@ function checkerOptions(ts, projectRoot) {
34276
34978
  }
34277
34979
  function resolveCalls(ts, files, projectRoot) {
34278
34980
  const out = /* @__PURE__ */ new Map();
34279
- if (files.size === 0) return { calls: out, imports: /* @__PURE__ */ new Map(), kinds: /* @__PURE__ */ new Map(), routes: /* @__PURE__ */ new Map(), crossProjectImports: /* @__PURE__ */ new Map() };
34981
+ if (files.size === 0) return { calls: out, imports: /* @__PURE__ */ new Map(), kinds: /* @__PURE__ */ new Map(), routes: /* @__PURE__ */ new Map(), prefixes: /* @__PURE__ */ new Map(), crossProjectImports: /* @__PURE__ */ new Map() };
34280
34982
  const options = checkerOptions(ts, projectRoot);
34281
34983
  const settings = JSON.stringify(options);
34282
34984
  const host = ts.createCompilerHost(options, true);
@@ -34296,7 +34998,7 @@ function resolveCalls(ts, files, projectRoot) {
34296
34998
  try {
34297
34999
  program = ts.createProgram({ rootNames: [...files.keys()], options, host });
34298
35000
  } catch {
34299
- return { calls: out, imports: /* @__PURE__ */ new Map(), kinds: /* @__PURE__ */ new Map(), routes: /* @__PURE__ */ new Map(), crossProjectImports: /* @__PURE__ */ new Map() };
35001
+ return { calls: out, imports: /* @__PURE__ */ new Map(), kinds: /* @__PURE__ */ new Map(), routes: /* @__PURE__ */ new Map(), prefixes: /* @__PURE__ */ new Map(), crossProjectImports: /* @__PURE__ */ new Map() };
34300
35002
  }
34301
35003
  const checker = program.getTypeChecker();
34302
35004
  const assignable = checker.isTypeAssignableTo;
@@ -34691,6 +35393,26 @@ function resolveCalls(ts, files, projectRoot) {
34691
35393
  if (where.container !== void 0) fact.enclosingContainer = where.container;
34692
35394
  return fact;
34693
35395
  };
35396
+ const opaqueReferenceOf = (node) => {
35397
+ if (!ts.isPropertyAccessExpression(node) || !isValuePosition(node)) return void 0;
35398
+ const symbol = symbolOf(node.name);
35399
+ if (symbol?.declarations?.length) return void 0;
35400
+ const receiver = node.expression;
35401
+ if (!(isOpaque(checker.getTypeAtLocation(receiver)) || rootOpaque(receiver))) return void 0;
35402
+ const fact = {
35403
+ name: node.name.text,
35404
+ written: node.getText().replace(/\s+/g, " ").slice(0, 80),
35405
+ targets: [],
35406
+ reference: true,
35407
+ unresolved: true
35408
+ };
35409
+ const receivers = receiversOf(receiver, node.name.text);
35410
+ if (receivers.length > 0) fact.receivers = receivers;
35411
+ const where = enclosingOf(node);
35412
+ if (where.name !== void 0) fact.enclosing = where.name;
35413
+ if (where.container !== void 0) fact.enclosingContainer = where.container;
35414
+ return fact;
35415
+ };
34694
35416
  const resolveFile = (sf) => {
34695
35417
  const calls = [];
34696
35418
  const visit = (node) => {
@@ -34699,7 +35421,7 @@ function resolveCalls(ts, files, projectRoot) {
34699
35421
  const fact = resolveOne2(node);
34700
35422
  if (fact) calls.push(fact);
34701
35423
  } else if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node) || ts.isIdentifier(node)) {
34702
- const fact = referenceOf(node) ?? (ts.isElementAccessExpression(node) ? computedAccessOf(node) : void 0);
35424
+ const fact = referenceOf(node) ?? (ts.isElementAccessExpression(node) ? computedAccessOf(node) : opaqueReferenceOf(node));
34703
35425
  if (fact) calls.push(fact);
34704
35426
  }
34705
35427
  } catch {
@@ -34768,6 +35490,7 @@ function resolveCalls(ts, files, projectRoot) {
34768
35490
  }
34769
35491
  const kinds = /* @__PURE__ */ new Map();
34770
35492
  const routes = /* @__PURE__ */ new Map();
35493
+ const prefixes = /* @__PURE__ */ new Map();
34771
35494
  const crossProjectImports = /* @__PURE__ */ new Map();
34772
35495
  const literalOf = (node) => {
34773
35496
  const type = checker.getTypeAtLocation(node);
@@ -34798,24 +35521,42 @@ function resolveCalls(ts, files, projectRoot) {
34798
35521
  if (!sf) continue;
34799
35522
  const fileKinds = [];
34800
35523
  const fileRoutes = /* @__PURE__ */ new Map();
35524
+ const filePrefixes = /* @__PURE__ */ new Map();
34801
35525
  const visit = (node) => {
34802
35526
  const name = namedFunctionNameOf(ts, node);
34803
35527
  const body = node.body;
34804
35528
  if (name !== void 0 && body) {
34805
35529
  fileKinds.push((node.parameters ?? []).map((parameter) => {
34806
35530
  try {
34807
- return kindOf4(checker.getTypeAtLocation(parameter)) ?? platformKind(parameter.type);
35531
+ const type = checker.getTypeAtLocation(parameter);
35532
+ const kind = kindOf4(type) ?? platformKind(parameter.type);
35533
+ const reading = {};
35534
+ if (kind) reading.kind = kind;
35535
+ if (kind === "object") {
35536
+ const props = checker.getPropertiesOfType(checker.getNonNullableType(type)).map((p) => p.getName());
35537
+ if (props.length > 0) reading.fields = props.slice(0, 200);
35538
+ }
35539
+ return reading;
34808
35540
  } catch {
34809
- return void 0;
35541
+ return {};
34810
35542
  }
34811
35543
  }));
34812
- if (/method|verb/i.test(body.getText(sf))) try {
35544
+ if (ROUTER_TEXT.test(body.getText(sf))) try {
34813
35545
  const read2 = readRoutes(ts, node, initializerOf, literalOf);
34814
35546
  if (read2.size > 0) {
34815
35547
  const known = fileRoutes.get(name) ?? /* @__PURE__ */ new Map();
34816
35548
  for (const [routeKeyText, route2] of read2) known.set(routeKeyText, route2);
34817
35549
  fileRoutes.set(name, known);
34818
35550
  }
35551
+ const prefix = strippedAnywhere(ts, body, initializerOf, literalOf);
35552
+ if (prefix !== void 0) filePrefixes.set(name, prefix);
35553
+ } catch {
35554
+ }
35555
+ }
35556
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && node.parent && ts.isVariableDeclarationList(node.parent) && node.parent.parent && ts.isVariableStatement(node.parent.parent) && node.parent.parent.parent === sf) {
35557
+ try {
35558
+ const table = tableRoutes(ts, node.initializer, initializerOf, literalOf);
35559
+ if (table.length > 0) fileRoutes.set(node.name.text, new Map(table.map((route2) => [routeKey2(route2), route2])));
34819
35560
  } catch {
34820
35561
  }
34821
35562
  }
@@ -34824,6 +35565,7 @@ function resolveCalls(ts, files, projectRoot) {
34824
35565
  visit(sf);
34825
35566
  kinds.set(key2, fileKinds);
34826
35567
  routes.set(key2, new Map([...fileRoutes].map(([name, read2]) => [name, [...read2.values()]])));
35568
+ if (filePrefixes.size > 0) prefixes.set(key2, filePrefixes);
34827
35569
  const broken = [];
34828
35570
  for (const statement of sf.statements) {
34829
35571
  if (!ts.isImportDeclaration(statement) || !ts.isStringLiteralLike(statement.moduleSpecifier)) continue;
@@ -34854,19 +35596,28 @@ function resolveCalls(ts, files, projectRoot) {
34854
35596
  return false;
34855
35597
  }
34856
35598
  };
34857
- if (clause?.name && unresolvedName(clause.name)) {
34858
- broken.push({ specifier, name: "default", project: project3, line, typeOnly: clause.isTypeOnly });
35599
+ const module2 = pathKey(path20.relative(home, path20.resolve(resolved)));
35600
+ const reached = (name, typeOnly) => {
35601
+ broken.push({ specifier, ...name !== void 0 ? { name } : {}, project: project3, line, typeOnly, resolved: true, module: module2 });
35602
+ };
35603
+ if (clause?.name) {
35604
+ if (unresolvedName(clause.name)) broken.push({ specifier, name: "default", project: project3, line, typeOnly: clause.isTypeOnly });
35605
+ else reached(clause.name.text, clause.isTypeOnly);
34859
35606
  }
34860
35607
  if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
34861
35608
  for (const element of clause.namedBindings.elements) {
34862
- if (!unresolvedName(element.name)) continue;
34863
- broken.push({ specifier, name: (element.propertyName ?? element.name).text, project: project3, line, typeOnly: clause.isTypeOnly || element.isTypeOnly });
35609
+ const name = (element.propertyName ?? element.name).text;
35610
+ if (unresolvedName(element.name)) broken.push({ specifier, name, project: project3, line, typeOnly: clause.isTypeOnly || element.isTypeOnly });
35611
+ else reached(name, clause.isTypeOnly || element.isTypeOnly);
34864
35612
  }
35613
+ } else if (clause?.namedBindings) {
35614
+ reached("*", clause.isTypeOnly);
34865
35615
  }
35616
+ if (!clause) reached(void 0, false);
34866
35617
  }
34867
35618
  if (broken.length > 0) crossProjectImports.set(key2, broken);
34868
35619
  }
34869
- return { calls: out, imports, kinds, routes, crossProjectImports };
35620
+ return { calls: out, imports, kinds, routes, prefixes, crossProjectImports };
34870
35621
  function platformKind(annotation) {
34871
35622
  if (!annotation || !ts.isTypeReferenceNode(annotation)) return void 0;
34872
35623
  const name = ts.isIdentifier(annotation.typeName) ? annotation.typeName.text : annotation.typeName.right.text;
@@ -34915,6 +35666,22 @@ var PLATFORM_OBJECT_TYPES = /* @__PURE__ */ new Set([
34915
35666
  "Socket",
34916
35667
  "Buffer"
34917
35668
  ]);
35669
+ var TRANSPORT_TYPES = /* @__PURE__ */ new Set([
35670
+ "URL",
35671
+ "URLSearchParams",
35672
+ "Request",
35673
+ "Response",
35674
+ "Headers",
35675
+ "IncomingMessage",
35676
+ "ServerResponse",
35677
+ "Http2ServerRequest",
35678
+ "Http2ServerResponse",
35679
+ "Socket",
35680
+ "FastifyRequest",
35681
+ "FastifyReply",
35682
+ "NextRequest",
35683
+ "NextResponse"
35684
+ ]);
34918
35685
  function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], exclude = []) {
34919
35686
  const files = [];
34920
35687
  const seen = /* @__PURE__ */ new Set();
@@ -34930,6 +35697,8 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
34930
35697
  for (const impl of implementations) {
34931
35698
  declaredPaths.push(...implementationSourceFiles(impl));
34932
35699
  if (impl.simPath) declaredPaths.push(impl.simPath);
35700
+ const routerFile = impl.router ? routerLinkage(impl.router).file : void 0;
35701
+ if (routerFile) declaredPaths.push(routerFile);
34933
35702
  }
34934
35703
  for (const type of types) declaredPaths.push(...typeSourceFiles(type));
34935
35704
  declaredPaths.push(...rootFiles);
@@ -34999,6 +35768,7 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
34999
35768
  typeShapes: Object.fromEntries(facts.typeShapes),
35000
35769
  functionParams: Object.fromEntries(facts.functionParams),
35001
35770
  functionRoutes: Object.fromEntries([...facts.functionRoutes].map(([k, v]) => [k, [...v.values()]])),
35771
+ ...facts.routePrefixes.size > 0 ? { routePrefixes: Object.fromEntries(facts.routePrefixes) } : {},
35002
35772
  asyncFunctions: [...facts.asyncFunctions],
35003
35773
  enumValues: Object.fromEntries(facts.enumValues),
35004
35774
  aliasTypes: Object.fromEntries(facts.aliasTypes),
@@ -35045,13 +35815,16 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
35045
35815
  sites.forEach((site, index) => {
35046
35816
  if (settled[index].length !== site.params.length) return;
35047
35817
  site.params.forEach((param, at) => {
35048
- const kind = settled[index][at];
35049
- if (kind) param.kind = kind;
35818
+ const reading2 = settled[index][at];
35819
+ if (reading2.kind) param.kind = reading2.kind;
35820
+ if (reading2.fields) param.fields = reading2.fields;
35050
35821
  });
35051
35822
  });
35052
35823
  }
35053
35824
  const read2 = reading.routes.get(facts.path);
35054
35825
  if (read2 && read2.size > 0) facts.functionRoutes = { ...facts.functionRoutes ?? {}, ...Object.fromEntries(read2) };
35826
+ const stripped = reading.prefixes.get(facts.path);
35827
+ if (stripped && stripped.size > 0) facts.routePrefixes = { ...facts.routePrefixes ?? {}, ...Object.fromEntries(stripped) };
35055
35828
  const broken = reading.crossProjectImports.get(facts.path);
35056
35829
  if (broken) facts.crossProjectImports = broken;
35057
35830
  }
@@ -35983,7 +36756,12 @@ var INTRODUCED_CODES = /* @__PURE__ */ new Set([
35983
36756
  "ARCHITECTURE_VIOLATION_QUERY_DEP",
35984
36757
  "ENDPOINT_ROUTE_DUPLICATE"
35985
36758
  ]);
35986
- function introducedFindings(kind, candidate, opts = {}) {
36759
+ var BOUNDARY_CODES = /* @__PURE__ */ new Set([
36760
+ "CROSS_SUBSYSTEM_NON_ADAPTER",
36761
+ "CROSS_SUBSYSTEM_PRIVATE_ACCESS",
36762
+ "CROSS_SUBSYSTEM_TARGET_NON_PORTAL"
36763
+ ]);
36764
+ function introducedFindings(kind, candidate, opts = {}, others = []) {
35987
36765
  if (kind !== "component" && kind !== "interface") return { errors: [], warnings: [], notices: [] };
35988
36766
  const index = scanAllSpecs({ memberDepth: 0 });
35989
36767
  registerBuiltinRules();
@@ -36010,18 +36788,28 @@ function introducedFindings(kind, candidate, opts = {}) {
36010
36788
  });
36011
36789
  entrypointDepsRule.check(ctx);
36012
36790
  dataBlockDepsRule.check(ctx);
36791
+ subsystemBoundaryDepsRule.check(ctx);
36013
36792
  portalsRule.check(ctx);
36014
- return issues.filter((i) => INTRODUCED_CODES.has(i.code));
36793
+ return issues.filter((i) => INTRODUCED_CODES.has(i.code) || BOUNDARY_CODES.has(i.code));
36015
36794
  };
36795
+ const changed = kind === "component" ? [candidate, ...others] : others;
36796
+ const afterComponents = changed.reduce((list, spec) => swapped(list, spec), index.components);
36016
36797
  const before = run3(index.components, index.interfaces);
36017
- const after = kind === "component" ? run3(swapped(index.components, candidate), index.interfaces) : run3(index.components, swapped(index.interfaces, candidate));
36798
+ const after = kind === "component" ? run3(afterComponents, index.interfaces) : run3(afterComponents, swapped(index.interfaces, candidate));
36018
36799
  const key2 = (i) => `${i.code}|${i.specId ?? ""}|${i.message}`;
36019
36800
  const held = new Set(before.map(key2));
36020
36801
  const added = after.filter((i) => !held.has(key2(i)));
36802
+ const fixedEdges = afterComponents.flatMap((comp) => (comp.dependsOn ?? []).flatMap((ref) => {
36803
+ const target = afterComponents.find((c) => c.id === ref);
36804
+ return target && target.subsystem !== comp.subsystem && target.componentType !== "Portal" ? [{ from: comp.id, to: target.id }] : [];
36805
+ }));
36806
+ const fixed = (i) => !BOUNDARY_CODES.has(i.code) || fixedEdges.some((e) => e.from === i.specId && i.message.includes(`"${e.to}"`));
36807
+ const curable = added.filter((i) => !fixed(i));
36808
+ const decided = added.filter(fixed);
36021
36809
  return {
36022
- errors: added.filter((i) => i.severity === "error"),
36023
- warnings: added.filter((i) => i.severity === "warning"),
36024
- notices: added.filter((i) => i.severity === "notice")
36810
+ errors: decided.filter((i) => i.severity === "error"),
36811
+ warnings: [...decided.filter((i) => i.severity === "warning"), ...curable.filter((i) => i.severity !== "notice")],
36812
+ notices: [...decided, ...curable].filter((i) => i.severity === "notice")
36025
36813
  };
36026
36814
  }
36027
36815
 
@@ -37203,8 +37991,8 @@ function generateSequenceDiagram(componentId, methodName, options) {
37203
37991
  const steps = [...methodImpl.narrative].sort((a, b) => a.stepNumber - b.stepNumber);
37204
37992
  for (const step of steps) {
37205
37993
  for (const par of parallelArms) {
37206
- const sep17 = par.sepByStep.get(step.stepNumber);
37207
- if (sep17 !== void 0) lines.push(` and ${escapeLabel(sep17)}`);
37994
+ const sep18 = par.sepByStep.get(step.stepNumber);
37995
+ if (sep18 !== void 0) lines.push(` and ${escapeLabel(sep18)}`);
37208
37996
  }
37209
37997
  switch (step.type) {
37210
37998
  case "local":
@@ -37459,6 +38247,7 @@ function bindReferencedMembers(node, root) {
37459
38247
  const out = [];
37460
38248
  for (const member of declaredMembers(root.config ?? {})) {
37461
38249
  if (member.problem || member.storage === "contained" || root.parts.some((p) => p.alias === member.alias)) continue;
38250
+ if (member.storage === "path" && !root.referenced.some((r) => r.alias === member.alias)) continue;
37462
38251
  const base = { alias: member.alias, project: member.alias, audience: "instance", role: "member" };
37463
38252
  if (member.storage === "hosted") {
37464
38253
  out.push({ ...base, sourceKind: "hosted", hosted: member.source.hosted });
@@ -38552,6 +39341,9 @@ function headAcross(rootDir, dir) {
38552
39341
  if (ours !== null && key2(ours) === key2(theirs)) return void 0;
38553
39342
  return head(dir) ?? void 0;
38554
39343
  }
39344
+ function followsLive(decl) {
39345
+ return decl.storage === "contained" || decl.storage === "path";
39346
+ }
38555
39347
  var PART_FORBIDDEN_FIELDS = ["members", "externals", "rules", "extensions", "composition", "targets", "network"];
38556
39348
  function whereToEdit(decl, dir) {
38557
39349
  if (decl.storage === "git") {
@@ -39525,11 +40317,11 @@ function rewriteSpecRefs(raw, remap) {
39525
40317
  changed = true;
39526
40318
  }
39527
40319
  };
39528
- const rewritePair = (holder, componentKey, methodKey2) => {
39529
- const component = holder?.[componentKey];
40320
+ const rewritePair = (holder, componentKey2, methodKey2) => {
40321
+ const component = holder?.[componentKey2];
39530
40322
  const method2 = holder?.[methodKey2];
39531
40323
  rewriteMethod(holder, methodKey2, component);
39532
- rewrite(holder, componentKey, "component", typeof method2 === "string" ? method2 : void 0);
40324
+ rewrite(holder, componentKey2, "component", typeof method2 === "string" ? method2 : void 0);
39533
40325
  };
39534
40326
  const entries = (list) => Array.isArray(list) ? list : [];
39535
40327
  const rewriteEach = (list, position) => {
@@ -39976,7 +40768,7 @@ function carriesEditMarker(item) {
39976
40768
  const o = item;
39977
40769
  return o.action !== void 0 || o.remove !== void 0;
39978
40770
  }
39979
- function ineffectiveDeltaPaths(delta, merged, stored) {
40771
+ function ineffectiveDeltaPaths(delta, merged, stored, respellings) {
39980
40772
  const out = [];
39981
40773
  const isPlain = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
39982
40774
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
@@ -39995,8 +40787,9 @@ function ineffectiveDeltaPaths(delta, merged, stored) {
39995
40787
  if (plain.length > 0) {
39996
40788
  const text3 = (v) => v && typeof v === "object" ? String(v.description ?? JSON.stringify(v)) : String(v);
39997
40789
  const holds = (list, x) => Array.isArray(list) && list.some((y) => text3(y) === String(x));
40790
+ const order2 = (list) => Array.isArray(list) ? list.map(text3) : [];
39998
40791
  if (plain.some((x) => !holds(m, x))) say(path52, "the level does not keep this value, so the write dropped it");
39999
- else if (plain.every((x) => holds(s, x)) && d.every((x) => plain.includes(x))) say(path52, "the stored spec already held this value");
40792
+ else if (plain.every((x) => holds(s, x)) && d.every((x) => plain.includes(x)) && same(order2(m), order2(s))) say(path52, "the stored spec already held this value");
40000
40793
  if (d.every((x) => plain.includes(x) || carriesEditMarker(x))) return;
40001
40794
  }
40002
40795
  for (const element of d) {
@@ -40026,8 +40819,9 @@ function ineffectiveDeltaPaths(delta, merged, stored) {
40026
40819
  walk2(d, m, s, path52, field);
40027
40820
  return;
40028
40821
  }
40029
- if (!same(m, d)) say(path52, "the write did not store it \u2014 the level's schema does not keep this value");
40030
- else if (same(s, d)) say(path52, "the stored spec already held this value");
40822
+ const respelled2 = typeof d === "string" && typeof m === "string" && (respellings ?? []).some((r) => r.written === d && r.stored === m);
40823
+ if (!same(m, d) && !respelled2) say(path52, "the write did not store it \u2014 the level's schema does not keep this value");
40824
+ else if (same(s, d) || respelled2 && same(s, m)) say(path52, "the stored spec already held this value");
40031
40825
  };
40032
40826
  function walk2(d, m, s, path52, container) {
40033
40827
  for (const [key2, value] of Object.entries(d)) {
@@ -40234,7 +41028,8 @@ var SpecWorkspace = class {
40234
41028
  for (const decl of raw.members) this.readMemberKind(raw, decl);
40235
41029
  if (depth >= maxDepth) return;
40236
41030
  for (const decl of raw.members) {
40237
- if (decl.kind === "part" || decl.storage !== "contained") continue;
41031
+ if (decl.kind === "part" || !followsLive(decl)) continue;
41032
+ if (raw.record.referenced.some((r) => r.alias === decl.alias)) continue;
40238
41033
  const childDir = this.followableMember(raw, decl, ancestors);
40239
41034
  if (!childDir) continue;
40240
41035
  const before = raws.length;
@@ -40258,7 +41053,7 @@ var SpecWorkspace = class {
40258
41053
  return null;
40259
41054
  }
40260
41055
  const childDir = path23.resolve(raw.dir, decl.path);
40261
- if (projectPathEscapesRoot(raw.dir, decl.path, childDir)) {
41056
+ if (decl.storage !== "path" && projectPathEscapesRoot(raw.dir, decl.path, childDir)) {
40262
41057
  this.loaderIssues.push({
40263
41058
  severity: "error",
40264
41059
  code: "PROJECTPATH_ESCAPE",
@@ -40349,7 +41144,7 @@ var SpecWorkspace = class {
40349
41144
  this.memberProblem(raw, "kind-mismatch", decl.alias, `the member "${decl.alias}" is asserted \`as: ${asserted}\`, but its content makes it a ${kind}: ${kind === "project" ? "its tree declares an id, holds an L0 or carries a lock \u2014 drop the `as`, or remove that content with `wairon member demote`" : "its tree declares no id, holds no L0 and carries no lock \u2014 drop the `as`, or create the project content with `wairon member promote`"}`);
40350
41145
  }
40351
41146
  if (kind === "part") this.readPart(raw, decl, located2.dir, located2.commit);
40352
- else if (decl.storage !== "contained") this.recordReferenced(raw, decl, located2.dir, located2.commit);
41147
+ else if (!followsLive(decl)) this.recordReferenced(raw, decl, located2.dir, located2.commit);
40353
41148
  }
40354
41149
  /**
40355
41150
  * A referenced project member (stage 8): where its root is — the sibling
@@ -40854,9 +41649,9 @@ var SpecWorkspace = class {
40854
41649
  return { root: root.dir, project: root.project, subsystemId };
40855
41650
  }
40856
41651
  /** The local id of a component's subsystem, by the component's key; null when no such component is stored. */
40857
- componentSubsystem(componentKey) {
40858
- if (!componentKey) return null;
40859
- const subsystem = this.loadComponentSpec(componentKey)?.subsystem;
41652
+ componentSubsystem(componentKey2) {
41653
+ if (!componentKey2) return null;
41654
+ const subsystem = this.loadComponentSpec(componentKey2)?.subsystem;
40860
41655
  return subsystem ? splitNamespace(subsystem).localId : null;
40861
41656
  }
40862
41657
  /**
@@ -41332,12 +42127,12 @@ var SpecWorkspace = class {
41332
42127
  return this.relativizeSpec("interface", stored);
41333
42128
  }
41334
42129
  prepareImplementationForWrite(spec, carry = true) {
41335
- const relative25 = this.relativizeSpec("implementation", spec, carry);
42130
+ const relative27 = this.relativizeSpec("implementation", spec, carry);
41336
42131
  const member = this.getSubprojectPrefix(spec.id);
41337
42132
  const memberRoot = member ? this.resolveSubprojectForNamespace(member) : null;
41338
- if (memberRoot) return childRelativeFilePaths(relative25, this.rootDir, memberRoot);
42133
+ if (memberRoot) return childRelativeFilePaths(relative27, this.rootDir, memberRoot);
41339
42134
  const partDir = this.partHolding(this.getImplementationPath(spec.id, spec.contract))?.part.directory;
41340
- return partDir ? partRelativeFilePaths(relative25, this.rootDir, partDir) : relative25;
42135
+ return partDir ? partRelativeFilePaths(relative27, this.rootDir, partDir) : relative27;
41341
42136
  }
41342
42137
  /**
41343
42138
  * A type written in its stored form — every type position canonical (one
@@ -41347,9 +42142,9 @@ var SpecWorkspace = class {
41347
42142
  prepareTypeForWrite(spec) {
41348
42143
  const canonical3 = typeCanonicalTypes(spec).spec;
41349
42144
  const stored = { ...canonical3, methods: (canonical3.methods ?? []).map((m) => storedTypeMethod(m)) };
41350
- const relative25 = this.relativizeSpec("type", stored);
42145
+ const relative27 = this.relativizeSpec("type", stored);
41351
42146
  const partDir = this.partHolding(this.getTypePath(spec.id, spec.subsystem, spec.group))?.part.directory;
41352
- return partDir ? partRelativeTypePaths(relative25, this.rootDir, partDir) : relative25;
42147
+ return partDir ? partRelativeTypePaths(relative27, this.rootDir, partDir) : relative27;
41353
42148
  }
41354
42149
  prepareGroupForWrite(spec) {
41355
42150
  return this.relativizeSpec("group", spec);
@@ -41940,7 +42735,8 @@ var SpecWorkspace = class {
41940
42735
  const rawRewritten = refKind === "component" ? withUniqueTargets(rewritten) : rewritten;
41941
42736
  this.carryDisabled = true;
41942
42737
  try {
41943
- if (JSON.stringify(this.prepareForKind(refKind, rawRewritten)) === asStored) return false;
42738
+ const prepared = this.prepareForKind(refKind, rawRewritten);
42739
+ if (JSON.stringify(prepared) === asStored && !this.storedTextsStale(refKind, id, prepared)) return false;
41944
42740
  this.saveOfKind(refKind, rawRewritten, { preserveUpdatedAt: true });
41945
42741
  } finally {
41946
42742
  this.carryDisabled = false;
@@ -41985,10 +42781,25 @@ var SpecWorkspace = class {
41985
42781
  );
41986
42782
  }
41987
42783
  if (JSON.stringify(document) === JSON.stringify(stored)) return false;
42784
+ if (kind === "interface" || kind === "type") deriveMergedTexts(kind, document);
41988
42785
  this.writeHome(file, document, kind);
41989
42786
  registryInvalidateCache();
41990
42787
  return true;
41991
42788
  }
42789
+ /**
42790
+ * Whether the stored file of one interface or type holds a method text its
42791
+ * prepared form re-derives differently: the text a raw rewrite of its params
42792
+ * left behind.
42793
+ */
42794
+ storedTextsStale(kind, id, prepared) {
42795
+ if (kind !== "interface" && kind !== "type") return false;
42796
+ const file = this.storedFileOf(kind, id);
42797
+ if (!file || !pathExists(file)) return false;
42798
+ const stored = readSpecFile(file);
42799
+ const storedText = new Map((stored?.methods ?? []).filter((m) => m && typeof m === "object").map((m) => [m.name, m.signature]));
42800
+ const methods = prepared.methods ?? [];
42801
+ return methods.some((m) => m.signature !== void 0 && storedText.has(m.name) && storedText.get(m.name) !== m.signature);
42802
+ }
41992
42803
  /** The file holding one stored spec, by its in-memory key from the bound root; a system by its project's key. */
41993
42804
  storedFileOf(kind, id) {
41994
42805
  const index = this.scanAll();
@@ -42339,8 +43150,8 @@ var SpecWorkspace = class {
42339
43150
  const renamedBy = {
42340
43151
  component: "rename it with sdd_rename_component, which moves its contract and implementation with it and rewrites every reference",
42341
43152
  type: "rename it with sdd_rename_type, which rewrites every reference",
42342
- interface: "a contract's id follows its component's: sdd_rename_component renames both; no tool renames a contract alone yet",
42343
- implementation: "an implementation's id follows its component's: sdd_rename_component renames both; no tool renames an implementation alone yet",
43153
+ interface: "rename it with sdd_rename_spec (kind interface), which rewrites every implementation's contract and every export entry naming it",
43154
+ implementation: "rename it with sdd_rename_spec (kind implementation), which moves its file and keys its findings to the new id",
42344
43155
  subsystem: "no tool renames a subsystem yet"
42345
43156
  };
42346
43157
  if (kind !== "system" && changes("id")) {
@@ -42351,8 +43162,8 @@ var SpecWorkspace = class {
42351
43162
  }
42352
43163
  if ((kind === "component" || kind === "type") && changes("subsystem")) {
42353
43164
  const owner = typeof stored.subsystem === "string" ? stored.subsystem : void 0;
42354
- if (owner !== void 0 && this.loadSubsystemSpec(owner) !== null) {
42355
- return `"subsystem" is where the ${kind} lives, not a field a delta edits \u2014 no tool moves a ${kind} to another subsystem with its references and published entries yet (owed), and a delta would move the file and leave them behind.`;
43165
+ if (owner !== void 0 && this.loadSubsystemSpec(owner) !== null || kind === "type" && owner === void 0) {
43166
+ return `"subsystem" is where the ${kind} lives, not a field a delta edits \u2014 a delta would rewrite the field and leave the file, its references and its published entries behind; move it with sdd_move_spec (kind ${kind}), which moves ${kind === "component" ? "the component with the members it owns, its contracts and implementations, and re-homes its export entries and lifecycle entrypoints" : "the type's file, its export entries and every reference qualified by its subsystem"}.`;
42356
43167
  }
42357
43168
  }
42358
43169
  if (kind === "interface" && changes("component")) {
@@ -42363,6 +43174,42 @@ var SpecWorkspace = class {
42363
43174
  }
42364
43175
  return void 0;
42365
43176
  }
43177
+ /**
43178
+ * core_orchestrator.crossProjectWriteRefusal — the sentence refusing a write
43179
+ * any of whose targets lives in another project, or undefined when every one
43180
+ * is the bound project's own. A target lives elsewhere when its key carries a
43181
+ * member project's prefix (a stored spec read through this tree, or an
43182
+ * `alias::name` the bound alias table binds to a member), or when its first
43183
+ * segment is an external's alias. A legacy L1 mount keeps its old cross-tree
43184
+ * writes. Reads only.
43185
+ */
43186
+ crossProjectWriteRefusal(targets, tool) {
43187
+ const verb = tool.replace(/^sdd_/, "").split("_")[0] || "write";
43188
+ for (const target of targets) {
43189
+ if (target.kind === "system" || typeof target.id !== "string" || !target.id.includes("::")) continue;
43190
+ const writable = ["subsystem", "component", "interface", "implementation", "type"].includes(target.kind);
43191
+ const stored = writable ? this.load(target.kind, target.id)?.id : void 0;
43192
+ const key2 = stored ?? this.bindReference("", target.id);
43193
+ const alias = target.id.slice(0, target.id.indexOf("::"));
43194
+ const aliased = this.cachedRawRoots[0]?.record.aliases.get(alias);
43195
+ const member = this.getSubprojectPrefix(key2) ?? this.getSubprojectPrefix(target.id) ?? (aliased !== void 0 && this.rootCovering(aliased) !== null ? aliased : null);
43196
+ if (member !== null) {
43197
+ const root = this.rootCovering(member);
43198
+ if (root?.legacyMount) continue;
43199
+ const qualified = stored ?? target.id;
43200
+ const local = qualified.includes("::") ? qualified.slice(qualified.lastIndexOf("::") + 2) : qualified;
43201
+ const dir = this.resolveSubprojectForNamespace(member);
43202
+ const folder = dir ? path23.relative(this.rootDir, dir).split(path23.sep).join("/") || "." : void 0;
43203
+ return `chained-spec: "${qualified}" lives in another project; ${verb} it from that project's own root. It belongs to the member project "${member}"${folder ? ` at ${folder}` : ""}: open a session in that folder (its own guide and .mcp.json \u2014 run \`wairon generate\` and \`wairon mcp install --backend claude\` there first if it has none) and call ${tool} ${target.kind} ${local} there. Nothing was written.`;
43204
+ }
43205
+ const externals = this.cachedRawRoots[0]?.record.config?.externals ?? {};
43206
+ if (Object.prototype.hasOwnProperty.call(externals, alias)) {
43207
+ const producer = externals[alias]?.project ?? alias;
43208
+ return `chained-spec: "${target.id}" names a spec of "${producer}", a project this one declares as the external "${alias}": a project's specs are written from its own root, and this project only reads them through its pin. Open a session in that project's folder and call ${tool} there. Nothing was written.`;
43209
+ }
43210
+ }
43211
+ return void 0;
43212
+ }
42366
43213
  updateSpec(kind, id, delta, hooks, dryRun = false) {
42367
43214
  const notices = [];
42368
43215
  const result = storedFormOf(kind, this.load(kind, id));
@@ -42370,16 +43217,8 @@ var SpecWorkspace = class {
42370
43217
  throw new Error(`Spec of kind "${kind}" with ID "${id}" does not exist. Define it first.`);
42371
43218
  }
42372
43219
  if (kind !== "system") {
42373
- const stored = String(result.id ?? id);
42374
- const member = this.getSubprojectPrefix(stored) ?? this.getSubprojectPrefix(id);
42375
- if (member !== null && this.rootCovering(member)?.legacyMount === null) {
42376
- const local = stored.startsWith(`${member}::`) ? stored.slice(member.length + 2) : stored;
42377
- const dir = this.resolveSubprojectForNamespace(member);
42378
- const folder = dir ? path23.relative(this.rootDir, dir).split(path23.sep).join("/") || "." : void 0;
42379
- throw new Error(
42380
- `chained-spec: "${stored}" lives in another project; update it from that project's own root. It belongs to the member project "${member}"${folder ? ` at ${folder}` : ""}: open a session in that folder (its own guide and .mcp.json \u2014 run \`wairon generate\` and \`wairon mcp install --backend claude\` there first if it has none) and call sdd_update_spec ${kind} ${local} there. Nothing was written.`
42381
- );
42382
- }
43220
+ const refusal2 = this.crossProjectWriteRefusal([{ kind, id: String(result.id ?? id) }], "sdd_update_spec");
43221
+ if (refusal2 !== void 0) throw new Error(refusal2);
42383
43222
  }
42384
43223
  if (kind === "system" && id && id !== "system" && id !== result.name) {
42385
43224
  throw new Error(
@@ -42984,7 +43823,8 @@ var SpecWorkspace = class {
42984
43823
  const ineffective = ineffectiveDeltaPaths(
42985
43824
  { ...qualifiedDelta, ...unsetFields.length ? { unset: unsetFields } : {} },
42986
43825
  canonicalMerged,
42987
- canonicalStored
43826
+ canonicalStored,
43827
+ respellings
42988
43828
  );
42989
43829
  if (changes.length === 0) {
42990
43830
  return {
@@ -43946,6 +44786,9 @@ function normalizeReferences(kind, id) {
43946
44786
  function rewriteReferences(kind, id, edits) {
43947
44787
  return current().respellReferences(kind, id, edits);
43948
44788
  }
44789
+ function crossProjectWriteRefusal(targets, tool) {
44790
+ return current().crossProjectWriteRefusal(targets, tool);
44791
+ }
43949
44792
  function assertSpecsInReach(targets, what, projectRung = false) {
43950
44793
  if (!getWriteReach()) return;
43951
44794
  const ws = current();
@@ -44253,8 +45096,8 @@ function approvedRevision(against) {
44253
45096
  if (!commit2) {
44254
45097
  throw new Error(against !== void 0 ? `"${against}" names no commit of the repository at ${repo}` : "no approval of this project was ever committed (.wai/lock.json is in no commit) \u2014 lock and commit first, or name a revision (`--against <ref>`)");
44255
45098
  }
44256
- const relative25 = path25.relative(repo, bound2);
44257
- const directory = fetch(repo, commit2, relative25 === "" ? void 0 : relative25);
45099
+ const relative27 = path25.relative(repo, bound2);
45100
+ const directory = fetch(repo, commit2, relative27 === "" ? void 0 : relative27);
44258
45101
  return { directory, commit: commit2, label: against !== void 0 ? `${against} (${commit2.slice(0, 12)})` : `the last approval, committed at ${commit2.slice(0, 12)}` };
44259
45102
  }
44260
45103
 
@@ -45099,13 +45942,20 @@ function promoteMember(alias, id) {
45099
45942
  declareCrossingEntries(inside, outside, parentId || parentName, result);
45100
45943
  recordCrossingTopics(inside, outside, parentId || parentName, result);
45101
45944
  if (crossing.out.length > 0 || qualified.out.length > 0 || [...climbs.values()].some((t) => t.startsWith(`${parentId}::`))) runWithProjectRoot(dir, () => projectConfigRepository.declareExternal(parentId, {}));
45945
+ declareSiblings(family, inside, dir, alias, result);
45102
45946
  for (const s of inside.filter((x) => x.kind === "subsystem")) restateReExports(s.id, alias);
45103
45947
  exportCrossings([...crossing.out, ...qualified.out], dir, alias, parentId, "out", result);
45104
45948
  exportCrossings([...crossing.in, ...qualified.in], dir, alias, parentId, "in", result);
45105
45949
  exportReExported(inside, dir, alias, result);
45106
45950
  invalidateSpecCache();
45107
- for (const spec of parentSide) if (spec.kind !== "system") normalizeReferences(spec.kind, spec.id);
45108
45951
  const memberKey = graph().nodes.find((n) => n.parent === "" && n.mountAlias === alias)?.namespace;
45952
+ rederiveSignatures(new Set(result.respelled.map((r) => r.specId)), result.signaturesRederived);
45953
+ runWithProjectRoot(dir, () => {
45954
+ invalidateSpecCache();
45955
+ rederiveSignatures(new Set(result.memberRespelled.map((r) => localIn(r.specId, memberKey ?? alias))), result.signaturesRederived);
45956
+ });
45957
+ invalidateSpecCache();
45958
+ for (const spec of parentSide) if (spec.kind !== "system") normalizeReferences(spec.kind, spec.id);
45109
45959
  if (memberKey !== void 0) {
45110
45960
  for (const spec of memberSide) if (spec.kind !== "system") normalizeReferences(spec.kind, `${memberKey}::${spec.id}`);
45111
45961
  } else {
@@ -45122,14 +45972,52 @@ function promoteMember(alias, id) {
45122
45972
  }
45123
45973
  invalidateSpecCache();
45124
45974
  rebaseEscapingPaths(dir, getProjectRoot(), result);
45125
- rederiveSignatures(new Set(result.respelled.map((r) => r.specId)), result.signaturesRederived);
45126
- runWithProjectRoot(dir, () => {
45127
- invalidateSpecCache();
45128
- rederiveSignatures(new Set(result.memberRespelled.map((r) => localIn(r.specId, memberKey ?? alias))), result.signaturesRederived);
45129
- });
45130
- invalidateSpecCache();
45131
45975
  return result;
45132
45976
  }
45977
+ function declareSiblings(family, inside, dir, alias, result) {
45978
+ const root = getProjectRoot();
45979
+ const parent = projectConfigRepository.load() ?? {};
45980
+ const ids = new Set(inside.map((s) => s.id));
45981
+ const named2 = /* @__PURE__ */ new Map();
45982
+ for (const ref of family.authoredReferences) {
45983
+ if (!ids.has(ref.specId) || (family.owners.get(ref.specId) ?? "") !== "") continue;
45984
+ const via = ref.form === "alias" ? ref.authored.split("::")[0] : ref.importedVia;
45985
+ if (via === void 0 || via === alias || via.includes(",")) continue;
45986
+ const names = named2.get(via) ?? /* @__PURE__ */ new Set();
45987
+ if (ref.form !== "alias") names.add(ref.authored);
45988
+ named2.set(via, names);
45989
+ }
45990
+ const own2 = runWithProjectRoot(dir, () => projectConfigRepository.load());
45991
+ for (const [sibling, imported] of named2) {
45992
+ if (ownGet(own2?.externals, sibling) !== void 0 || ownGet(own2?.members, sibling) !== void 0) continue;
45993
+ const member = declaredMembers(parent).find((m) => m.alias === sibling && !m.problem);
45994
+ const external = ownGet(parent.externals, sibling);
45995
+ let declaration2;
45996
+ if (member) {
45997
+ const at = member.source.path !== void 0 ? path27.resolve(root, member.source.path) : void 0;
45998
+ const id = at !== void 0 ? configAt2(at) : null;
45999
+ const project3 = id ? effectiveProjectId(id) ?? void 0 : void 0;
46000
+ declaration2 = {
46001
+ ...project3 !== void 0 && project3 !== sibling ? { project: project3 } : {},
46002
+ source: at !== void 0 ? { path: toPosixPath(path27.relative(dir, at)) || "." } : member.source
46003
+ };
46004
+ } else if (external !== void 0) {
46005
+ const source = externalSourceOf(external);
46006
+ declaration2 = {
46007
+ ...external.project !== void 0 ? { project: external.project } : {},
46008
+ ...source?.path !== void 0 && !path27.isAbsolute(source.path) ? { source: { path: toPosixPath(path27.relative(dir, path27.resolve(root, source.path))) } } : source ? { source } : {}
46009
+ };
46010
+ } else continue;
46011
+ const use = [...imported].filter((n) => !n.includes("::")).sort();
46012
+ runWithProjectRoot(dir, () => {
46013
+ projectConfigRepository.declareExternal(sibling, declaration2);
46014
+ if (use.length > 0) projectConfigRepository.importNames(sibling, use);
46015
+ });
46016
+ const written = typeof declaration2.source === "string" ? void 0 : declaration2.source;
46017
+ const where = typeof declaration2.source === "string" ? declaration2.source : written?.path ?? written?.git ?? written?.hosted ?? "";
46018
+ (result.externalsDeclared ??= []).push(`${sibling}${declaration2.project !== void 0 ? ` (project ${declaration2.project})` : ""}${where ? ` at ${where}` : ""}${use.length > 0 ? `, use ${use.join(", ")}` : ""}`);
46019
+ }
46020
+ }
45133
46021
  function rebaseEscapingPaths(memberDir, familyRoot, result) {
45134
46022
  const shownMember = toPosixPath(path27.relative(familyRoot, memberDir)) || ".";
45135
46023
  for (const file of specFilesUnder(aiPathsAt(memberDir).specsDir())) {
@@ -45598,13 +46486,63 @@ var MEMBER_WAI_GROUPS = /* @__PURE__ */ new Map([
45598
46486
  ["generated", "derived outputs"],
45599
46487
  ["rules", "derived outputs"],
45600
46488
  ["templates", "derived outputs"],
45601
- ["topology.yaml", "derived outputs"]
46489
+ ["topology.yaml", "derived outputs"],
46490
+ ["mcp", "derived outputs"],
46491
+ ["phased_design.md", "design workbook"],
46492
+ ["git.json", "host and producer bindings"],
46493
+ ["remote.json", "host and producer bindings"],
46494
+ ["producers.json", "host and producer bindings"],
46495
+ ["variants", "placed in this project"],
46496
+ ["packs", "placed in this project"]
45602
46497
  ]);
46498
+ var PLACED_WAI_ENTRIES = /* @__PURE__ */ new Set(["variants", "packs"]);
46499
+ function placedWaiFiles(memberDir, homeRoot) {
46500
+ const wai = path27.join(memberDir, ".wai");
46501
+ const home = path27.join(homeRoot, ".wai");
46502
+ return [...PLACED_WAI_ENTRIES].filter((entry) => fs19.existsSync(path27.join(wai, entry))).flatMap((entry) => listFilesRecursive(path27.join(wai, entry), "")).map((from) => {
46503
+ const rel2 = toPosixPath(path27.relative(wai, from));
46504
+ return { rel: rel2, from, to: path27.join(home, rel2) };
46505
+ });
46506
+ }
46507
+ function waiRefusals(memberDir, homeRoot) {
46508
+ const out = [];
46509
+ const wai = path27.join(memberDir, ".wai");
46510
+ for (const entry of fs19.existsSync(wai) ? fs19.readdirSync(wai) : []) {
46511
+ if (entry === "specs" || entry === "transactions" || MEMBER_WAI_GROUPS.has(entry)) continue;
46512
+ out.push(`its .wai holds "${entry}", which this write does not recognise \u2014 move or delete it by hand`);
46513
+ }
46514
+ for (const f of placedWaiFiles(memberDir, homeRoot)) {
46515
+ if (fs19.existsSync(f.to) && !fs19.readFileSync(f.to).equals(fs19.readFileSync(f.from))) {
46516
+ out.push(`its .wai/${f.rel} differs from this project's file of that name \u2014 reconcile the two by hand first`);
46517
+ }
46518
+ }
46519
+ return out;
46520
+ }
46521
+ function placeWaiEntries(memberDir, homeRoot, result) {
46522
+ for (const f of placedWaiFiles(memberDir, homeRoot)) {
46523
+ if (fs19.existsSync(f.to)) continue;
46524
+ fs19.mkdirSync(path27.dirname(f.to), { recursive: true });
46525
+ fs19.copyFileSync(f.from, f.to);
46526
+ result.placed.push(`.wai/${f.rel} \u2192 this project's .wai/${f.rel}`);
46527
+ }
46528
+ const workbook = path27.join(memberDir, ".wai", "phased_design.md");
46529
+ if (!fs19.existsSync(workbook)) return;
46530
+ const named2 = (dir, text3) => {
46531
+ const name = configAt2(dir)?.name;
46532
+ return (name ? text3.split(name).join("<project>") : text3).replace(/\r\n/g, "\n");
46533
+ };
46534
+ const homeWorkbook = path27.join(homeRoot, ".wai", "phased_design.md");
46535
+ const theirs = named2(memberDir, fs19.readFileSync(workbook, "utf8"));
46536
+ const ours = fs19.existsSync(homeWorkbook) ? named2(homeRoot, fs19.readFileSync(homeWorkbook, "utf8")) : null;
46537
+ if (ours !== theirs) {
46538
+ result.notCarried.push(".wai/phased_design.md: the member's design workbook differs from this project's \u2014 it is deleted with the boundary (the design itself is its specs, which move); copy what you want to keep from it first");
46539
+ }
46540
+ }
45603
46541
  function internalizeMember(alias, destination) {
45604
46542
  const family = graph();
45605
46543
  const part = family.nodes.find((n) => n.namespace === "")?.parts.find((p) => p.alias === alias);
45606
46544
  if (part) return internalizePart(alias, part);
45607
- if (family.nodes.find((n) => n.namespace === "")?.externals.some((e) => e.role === "member" && e.sourceKind === "path" && e.alias === alias)) {
46545
+ if (storedAsSibling(alias)) {
45608
46546
  const demoted = demoteMember(alias, destination);
45609
46547
  const nowPart = graph().nodes.find((n) => n.namespace === "")?.parts.find((p) => p.alias === alias);
45610
46548
  if (!nowPart) throw new WaironError(`cannot internalize "${alias}": demoted, it does not read as a part of this project.`);
@@ -45630,8 +46568,10 @@ function internalizeMember(alias, destination) {
45630
46568
  carryDeclarations(scan2, result);
45631
46569
  carryPacks(scan2, destination, result);
45632
46570
  projectConfigRepository.removeMember(alias);
46571
+ placeWaiEntries(scan2.memberDir, getProjectRoot(), result);
45633
46572
  result.deleted = deleteMemberWai(scan2.memberDir);
45634
46573
  invalidateSpecCache();
46574
+ rederiveSignatures(new Set(rewritten.map((s) => s.id)), result.signaturesRederived ??= []);
45635
46575
  for (const spec of rewritten) if (spec.kind !== "system") normalizeReferences(spec.kind, spec.id);
45636
46576
  invalidateSpecCache();
45637
46577
  return result;
@@ -45706,11 +46646,7 @@ function internalizeRefusals(scan2, destination) {
45706
46646
  const held = exported.get(name);
45707
46647
  if (held !== void 0 && held !== scan2.alias) out.push(`the export "${name}" is a public name this project already exports for ${held}`);
45708
46648
  }
45709
- const wai = path27.join(scan2.memberDir, ".wai");
45710
- for (const entry of fs19.existsSync(wai) ? fs19.readdirSync(wai) : []) {
45711
- if (entry === "specs" || entry === "transactions" || MEMBER_WAI_GROUPS.has(entry)) continue;
45712
- out.push(`its .wai holds "${entry}", which this write does not recognise \u2014 move or delete it by hand`);
45713
- }
46649
+ out.push(...waiRefusals(scan2.memberDir, getProjectRoot()));
45714
46650
  return out;
45715
46651
  }
45716
46652
  function resolveSubsystemExportsOfProject() {
@@ -45929,11 +46865,14 @@ function deleteMemberWai(memberDir) {
45929
46865
  if (fs19.readdirSync(wai).length === 0) fs19.rmdirSync(wai);
45930
46866
  return deleted.sort();
45931
46867
  }
46868
+ function storedAsSibling(alias) {
46869
+ return declaredMembers(projectConfigRepository.load() ?? {}).find((m) => m.alias === alias)?.storage === "path";
46870
+ }
45932
46871
  function memberToDemote(family, alias) {
45933
46872
  const bound2 = family.nodes.find((n) => n.namespace === "");
45934
46873
  if (bound2?.parts.some((p) => p.alias === alias)) throw new WaironError(`demote refused: "${alias}" is already a part of this project.`);
45935
46874
  const node = family.nodes.find((n) => n.parent === "" && n.mountAlias === alias);
45936
- if (node) return { dir: node.directory, contained: true, key: node.namespace };
46875
+ if (node) return { dir: node.directory, contained: !storedAsSibling(alias), key: node.namespace };
45937
46876
  const referenced = bound2?.externals.find((e) => e.role === "member" && e.alias === alias);
45938
46877
  if (referenced?.sourceKind === "git") {
45939
46878
  throw new WaironError(`demote refused: the member "${alias}" is fetched from git \u2014 its files are the fetch cache's. Demote it from a checkout of its repository.`);
@@ -45970,11 +46909,7 @@ function demoteRefusals(scan2, destination, memberIds, family) {
45970
46909
  const held = exported.get(name);
45971
46910
  if (held !== void 0 && held !== scan2.alias) out.push(`the export "${name}" is a public name this project already exports for ${held}`);
45972
46911
  }
45973
- const wai = path27.join(scan2.memberDir, ".wai");
45974
- for (const entry of fs19.existsSync(wai) ? fs19.readdirSync(wai) : []) {
45975
- if (entry === "specs" || entry === "transactions" || MEMBER_WAI_GROUPS.has(entry)) continue;
45976
- out.push(`its .wai holds "${entry}", which this write does not recognise \u2014 move or delete it by hand`);
45977
- }
46912
+ out.push(...waiRefusals(scan2.memberDir, getProjectRoot()));
45978
46913
  return out;
45979
46914
  }
45980
46915
  function demoteMember(alias, destination) {
@@ -46026,6 +46961,7 @@ function demoteMember(alias, destination) {
46026
46961
  patchKeepingStamp(s.file, (raw) => raw.parentSystem === parentName ? false : (raw.parentSystem = parentName, true));
46027
46962
  }
46028
46963
  }
46964
+ placeWaiEntries(dir, root, result);
46029
46965
  result.deleted = deleteProjectContent(dir);
46030
46966
  if (!contained && parentId !== void 0) {
46031
46967
  runWithProjectRoot(dir, () => projectConfigRepository.setPartOf({ project: parentId, path: toPosixPath(path27.relative(dir, root)) || "." }));
@@ -46040,9 +46976,9 @@ function demoteMember(alias, destination) {
46040
46976
  carryPacks(scan2, destination, result);
46041
46977
  retireBoundaryExports(family, crossed, key2);
46042
46978
  invalidateSpecCache();
46979
+ rederiveSignatures(new Set([...parentSide, ...memberSide].map((s) => s.id)), result.signaturesRederived ??= []);
46043
46980
  for (const spec of [...parentSide, ...memberSide]) if (spec.kind !== "system") normalizeReferences(spec.kind, spec.id);
46044
46981
  invalidateSpecCache();
46045
- rederiveSignatures(new Set([...parentSide, ...memberSide].map((s) => s.id)), result.signaturesRederived ??= []);
46046
46982
  return result;
46047
46983
  }
46048
46984
  function respellQualifiedBack(parentSide, memberSide, aliases, tables, crossed) {
@@ -46400,17 +47336,43 @@ function moveRenamedSpecs(componentId, newId, component, movingInterface, moving
46400
47336
  saveInterfaceSpec({ ...movedInterface, id: interfaceId, component: newId, previousIds: traced(movedInterface.previousIds, movedInterface.id) });
46401
47337
  }
46402
47338
  if (movedImplementation) {
46403
- const oldFile = getImplementationPath(movedImplementation.id);
46404
- if (path27.resolve(getImplementationPath(implementationId, movedImplementation.contract)) === path27.resolve(oldFile)) {
46405
- fs19.unlinkSync(oldFile);
47339
+ const oldFile2 = getImplementationPath(movedImplementation.id);
47340
+ if (path27.resolve(getImplementationPath(implementationId, movedImplementation.contract)) === path27.resolve(oldFile2)) {
47341
+ fs19.unlinkSync(oldFile2);
46406
47342
  }
46407
47343
  saveImplementationSpec({ ...movedImplementation, id: implementationId, previousIds: traced(movedImplementation.previousIds, movedImplementation.id) });
46408
47344
  }
47345
+ invalidateSpecCache();
47346
+ const oldFile = path27.resolve(getComponentPath(componentId));
47347
+ const oldDir = path27.basename(oldFile) === ".index.yaml" ? path27.dirname(oldFile) : null;
47348
+ const moved = /* @__PURE__ */ new Set([interfaceId, implementationId, ...movingInterface ? [movingInterface.id] : [], ...movingImplementation ? [movingImplementation.id] : []]);
47349
+ const staying = loadInterfaceSpecs().filter((i) => i.component === newId && !moved.has(i.id));
47350
+ const contracts = /* @__PURE__ */ new Set([...staying.map((i) => i.id), interfaceId]);
47351
+ const stayingImplementations = loadImplementationSpecs().filter((impl) => contracts.has(impl.contract) && !moved.has(impl.id));
47352
+ if (oldDir !== null) {
47353
+ for (const contract of staying) relocateSpec("interface", contract, getInterfacePath(contract.id), oldDir);
47354
+ for (const impl of stayingImplementations) relocateSpec("implementation", impl, getImplementationPath(impl.id), oldDir);
47355
+ }
46409
47356
  removeSpecFile2(getComponentPath(componentId), componentId);
46410
47357
  if (movingInterface) removeSpecFile2(getInterfacePath(movingInterface.id), movingInterface.id);
46411
47358
  if (movingImplementation) removeSpecFile2(getImplementationPath(movingImplementation.id), movingImplementation.id);
46412
47359
  invalidateSpecCache();
46413
47360
  }
47361
+ function relocateSpec(kind, spec, file, fromDir) {
47362
+ if (!fs19.existsSync(file) || path27.dirname(path27.resolve(file)) !== fromDir) return;
47363
+ const backup = fs19.readFileSync(file);
47364
+ fs19.unlinkSync(file);
47365
+ invalidateSpecCache();
47366
+ try {
47367
+ if (kind === "interface") saveInterfaceSpec(spec);
47368
+ else saveImplementationSpec(spec);
47369
+ } catch (e) {
47370
+ fs19.writeFileSync(file, backup);
47371
+ invalidateSpecCache();
47372
+ throw e;
47373
+ }
47374
+ invalidateSpecCache();
47375
+ }
46414
47376
  function traced(trace, former) {
46415
47377
  const out = [...trace ?? []];
46416
47378
  if (!out.includes(former)) out.push(former);
@@ -46472,6 +47434,281 @@ function removeSpecFile2(file, id) {
46472
47434
  fs19.rmdirSync(dir);
46473
47435
  }
46474
47436
  }
47437
+ function renameSpecId(kind, id, newId, dryRun) {
47438
+ if (kind !== "interface" && kind !== "implementation") {
47439
+ const owner = kind === "component" ? "sdd_rename_component" : kind === "type" ? "sdd_rename_type" : "no tool yet";
47440
+ throw new WaironError(`invalid-kind: sdd_rename_spec renames a contract (kind interface) or an implementation on its own; a ${kind} is renamed with ${owner}.`);
47441
+ }
47442
+ const spec = kind === "interface" ? loadInterfaceSpec(id) : loadImplementationSpec(id);
47443
+ if (!spec) throw new WaironError(`spec-missing: no ${kind} has the id "${id}".`);
47444
+ if (id.includes("::")) {
47445
+ throw new WaironError(`chained-spec: "${id}" lives in another project; rename it from that project's own root. ${memberRootWay(id, `sdd_rename_spec ${kind} ${id.slice(id.lastIndexOf("::") + 2)} <new id>`)}`);
47446
+ }
47447
+ if (!SpecIdSchema.safeParse(newId).success || kind === "interface" && !/^i[a-z0-9-_]+$/.test(newId)) {
47448
+ throw new WaironError(`invalid-id: "${newId}" is not a lowercase identifier with no namespace separator${kind === "interface" ? ' beginning with "i", as a contract id does' : ""}.`);
47449
+ }
47450
+ if (newId === id) throw new WaironError(`name-unchanged: ${kind} "${id}" already has that id \u2014 nothing to do.`);
47451
+ const taken = kind === "interface" ? loadInterfaceSpecs().some((i) => i.id === newId) : loadImplementationSpecs().some((i) => i.id === newId);
47452
+ if (taken) throw new WaironError(`id-taken: a ${kind} already has the id "${newId}".`);
47453
+ refuseRetiredIds([{ kind, id: newId }]);
47454
+ const rename = { specs: [{ from: id, to: newId }] };
47455
+ const registerEdits = projectConfigRepository.rekeyCarried(rename, true);
47456
+ const specsDir = aiPathsAt(getProjectRoot()).specsDir();
47457
+ const remap = (ref, position) => kind === "interface" && position === "interface" && ref === id ? newId : ref;
47458
+ const derivesName = (entry) => kind === "interface" && entry.interface === id;
47459
+ const others = (specs) => [...new Set(specs.filter((s) => !(s.kind === kind && s.id === id)).map((s) => s.id))];
47460
+ const keptDry = keepPublicNames(specsDir, derivesName, true);
47461
+ const wouldRewrite = [...keptDry.specs, ...rewriteRefFields(specsDir, remap, void 0, true), ...rekeyLintAllows(specsDir, rename, true)];
47462
+ assertSpecsInReach([{ kind, id }, ...wouldRewrite], `renaming ${kind} "${id}"`, registerEdits.length > 0);
47463
+ const renamed2 = [{ kind, from: id, to: newId }];
47464
+ if (dryRun) return { kind, renamed: renamed2, rewritten: others(wouldRewrite), carried: registerEdits, keptPublicNames: keptDry.names, dryRun: true };
47465
+ const kept = keepPublicNames(specsDir, derivesName);
47466
+ const rewritten = others([...kept.specs, ...rewriteRefFields(specsDir, remap), ...rekeyLintAllows(specsDir, rename)]);
47467
+ invalidateSpecCache();
47468
+ const oldFile = kind === "interface" ? getInterfacePath(id) : getImplementationPath(id);
47469
+ if (kind === "interface") {
47470
+ const current2 = loadInterfaceSpec(id) ?? spec;
47471
+ const next = { ...current2, id: newId, previousIds: traced(current2.previousIds, id) };
47472
+ if (path27.resolve(getInterfacePath(newId, current2.component)) === path27.resolve(oldFile)) fs19.unlinkSync(oldFile);
47473
+ saveInterfaceSpec(next);
47474
+ } else {
47475
+ const current2 = loadImplementationSpec(id) ?? spec;
47476
+ const next = { ...current2, id: newId, previousIds: traced(current2.previousIds, id) };
47477
+ if (path27.resolve(getImplementationPath(newId, current2.contract)) === path27.resolve(oldFile)) fs19.unlinkSync(oldFile);
47478
+ saveImplementationSpec(next);
47479
+ }
47480
+ invalidateSpecCache();
47481
+ removeSpecFile2(oldFile, id);
47482
+ const carried = projectConfigRepository.rekeyCarried(rename);
47483
+ invalidateSpecCache();
47484
+ return { kind, renamed: renamed2, rewritten, carried, keptPublicNames: kept.names };
47485
+ }
47486
+ function moveSpec(kind, id, subsystem, dryRun) {
47487
+ if (kind !== "component" && kind !== "type") {
47488
+ throw new WaironError(`invalid-kind: sdd_move_spec moves a component or a type to another subsystem; a ${kind} ${kind === "interface" || kind === "implementation" ? "follows its component (sdd_move_methods moves behaviour between components)" : "does not move"}.`);
47489
+ }
47490
+ const subsystems = loadSubsystemSpecs().filter((s) => !s.id.includes("::"));
47491
+ const target = subsystem === void 0 || subsystem === "" ? void 0 : subsystem;
47492
+ if (target !== void 0 && !subsystems.some((s) => s.id === target)) {
47493
+ throw new WaironError(`subsystem-missing: this project has no subsystem "${target}"${target.includes("::") ? " \u2014 a spec never moves into another project" : ""}. Known subsystems: ${subsystems.map((s) => s.id).join(", ") || "none"}.`);
47494
+ }
47495
+ return kind === "component" ? moveComponentSpec(id, target, subsystems, dryRun === true) : moveTypeSpec(id, target, subsystems, dryRun === true);
47496
+ }
47497
+ function moveComponentSpec(id, target, subsystems, dryRun) {
47498
+ const components = loadComponentSpecs();
47499
+ const component = components.find((c) => c.id === id);
47500
+ if (!component) throw new WaironError(`spec-missing: no component has the id "${id}".`);
47501
+ if (id.includes("::")) {
47502
+ throw new WaironError(`chained-spec: "${id}" lives in another project; move it from that project's own root. ${memberRootWay(id, `sdd_move_spec component ${id.slice(id.lastIndexOf("::") + 2)} <subsystem>`)}`);
47503
+ }
47504
+ if (target === void 0) throw new WaironError(`subsystem-missing: name the subsystem component "${id}" moves to \u2014 a component always lives in a subsystem.`);
47505
+ if (component.subsystem === target) throw new WaironError(`same-subsystem: component "${id}" already lives in "${target}" \u2014 nothing to move.`);
47506
+ const owner = components.find((c) => (c.owns ?? []).includes(id));
47507
+ if (owner) {
47508
+ throw new WaironError(`owned: "${id}" is a member owned by "${owner.id}", and a pattern's members live with it \u2014 move "${owner.id}", which takes "${id}" along.`);
47509
+ }
47510
+ const movingIds = [];
47511
+ const visit = (cid) => {
47512
+ if (movingIds.includes(cid)) return;
47513
+ movingIds.push(cid);
47514
+ for (const member of components.find((c) => c.id === cid)?.owns ?? []) visit(member);
47515
+ };
47516
+ visit(id);
47517
+ const contracts = loadInterfaceSpecs().filter((i) => movingIds.includes(i.component));
47518
+ const implementations = loadImplementationSpecs().filter((impl) => contracts.some((i) => i.id === impl.contract));
47519
+ const moved = [
47520
+ ...movingIds.map((cid) => ({ kind: "component", id: cid })),
47521
+ ...contracts.map((i) => ({ kind: "interface", id: i.id })),
47522
+ ...implementations.map((impl) => ({ kind: "implementation", id: impl.id }))
47523
+ ];
47524
+ const from = component.subsystem;
47525
+ const fromSpec = subsystems.find((s) => s.id === from);
47526
+ const toSpec = subsystems.find((s) => s.id === target);
47527
+ const names = (ref) => ref !== void 0 && movingIds.includes(ref);
47528
+ const exportsMoving = (fromSpec?.publicInterfaces ?? []).filter((e) => names(e.component));
47529
+ const lifecycleMoving = (fromSpec?.lifecycle ?? []).filter((e) => names(e.component));
47530
+ const system = loadSystemSpec();
47531
+ const l0 = system?.publicInterfaces ?? [];
47532
+ const repointed = l0.filter((e) => e.from === from && names(e.component));
47533
+ const whole = l0.filter((e) => e.from === from && e.component === void 0 && e.typeDef === void 0 && e.interface === void 0);
47534
+ const added = whole.length > 0 ? exportsMoving.map((e) => ({
47535
+ from: target,
47536
+ component: e.component,
47537
+ ...e.interface ? { interface: e.interface } : {},
47538
+ ...e.as ? { as: e.as } : {},
47539
+ ...whole[0].audience ? { audience: whole[0].audience } : {}
47540
+ })) : [];
47541
+ const rewritten = [
47542
+ ...exportsMoving.length + lifecycleMoving.length > 0 ? [from, target] : [],
47543
+ ...repointed.length + added.length > 0 ? ["system"] : []
47544
+ ];
47545
+ assertSpecsInReach([...moved, ...rewritten.map((s) => s === "system" ? { kind: "system", id: "system" } : { kind: "subsystem", id: s })], `moving component "${id}"`);
47546
+ const report3 = { kind: "component", id, from, to: target, moved, rewritten: [...new Set(rewritten)], notices: [] };
47547
+ if (dryRun) return { ...report3, dryRun: true };
47548
+ if (exportsMoving.length + lifecycleMoving.length > 0 && fromSpec) {
47549
+ saveSpec("subsystem", {
47550
+ ...fromSpec,
47551
+ publicInterfaces: (fromSpec.publicInterfaces ?? []).filter((e) => !exportsMoving.includes(e)),
47552
+ ...fromSpec.lifecycle ? { lifecycle: fromSpec.lifecycle.filter((e) => !lifecycleMoving.includes(e)) } : {}
47553
+ });
47554
+ saveSpec("subsystem", {
47555
+ ...toSpec,
47556
+ publicInterfaces: [...toSpec.publicInterfaces ?? [], ...exportsMoving],
47557
+ ...lifecycleMoving.length > 0 || toSpec.lifecycle ? { lifecycle: [...toSpec.lifecycle ?? [], ...lifecycleMoving] } : {}
47558
+ });
47559
+ }
47560
+ if (system && repointed.length + added.length > 0) {
47561
+ saveSystemSpec({ ...system, publicInterfaces: [...l0.map((e) => repointed.includes(e) ? { ...e, from: target } : e), ...added] });
47562
+ }
47563
+ invalidateSpecCache();
47564
+ for (const cid of movingIds) {
47565
+ const current2 = loadComponentSpec(cid);
47566
+ if (current2) saveComponentSpec({ ...current2, subsystem: target });
47567
+ invalidateSpecCache();
47568
+ }
47569
+ invalidateSpecCache();
47570
+ return report3;
47571
+ }
47572
+ function moveTypeSpec(id, target, subsystems, dryRun) {
47573
+ const types = loadTypeSpecs();
47574
+ const cut = id.lastIndexOf("::");
47575
+ const qualifier = cut >= 0 ? id.slice(0, cut) : void 0;
47576
+ if (qualifier !== void 0 && !subsystems.some((s) => s.id === qualifier)) {
47577
+ throw new WaironError(`chained-spec: "${id}" lives in another project; move it from that project's own root. ${memberRootWay(id, `sdd_move_spec type ${id.slice(cut + 2)} <subsystem>`)}`);
47578
+ }
47579
+ const bare2 = cut >= 0 ? id.slice(cut + 2) : id;
47580
+ const matching = types.filter((t) => t.id === bare2 && (qualifier === void 0 || t.subsystem === qualifier));
47581
+ if (matching.length === 0) throw new WaironError(`spec-missing: no type has the id "${id}" (a type is addressed by its bare id, or as <subsystem>::<id>).`);
47582
+ if (matching.length > 1) throw new WaironError(`ambiguous: ${matching.length} types have the id "${bare2}" \u2014 address it as <subsystem>::${bare2} (${matching.map((t) => qualifiedTypeId(t)).join(", ")}).`);
47583
+ const type = matching[0];
47584
+ const from = type.subsystem ?? "";
47585
+ if ((target ?? "") === from) throw new WaironError(`same-subsystem: type "${id}" already lives ${from ? `in "${from}"` : "at system level"} \u2014 nothing to move.`);
47586
+ if (types.some((t) => t !== type && t.id === bare2 && (t.subsystem ?? "") === (target ?? ""))) {
47587
+ throw new WaironError(`id-taken: ${target ? `subsystem "${target}"` : "the system level"} already holds a type with the id "${bare2}".`);
47588
+ }
47589
+ const fromSpec = subsystems.find((s) => s.id === from);
47590
+ const names = (ref) => ref !== void 0 && (ref === bare2 || ref === `${from}::${bare2}`);
47591
+ const exportsMoving = (fromSpec?.publicInterfaces ?? []).filter((e) => e.component === void 0 && names(e.typeDef));
47592
+ const system = loadSystemSpec();
47593
+ const l0 = system?.publicInterfaces ?? [];
47594
+ const repointed = l0.filter((e) => from !== "" && e.from === from && names(e.typeDef));
47595
+ if (target === void 0 && exportsMoving.length + repointed.length > 0) {
47596
+ throw new WaironError(`exported: subsystem "${from}" exports type "${bare2}" (${exportsMoving.length + repointed.length} entr${exportsMoving.length + repointed.length === 1 ? "y" : "ies"}), and a system-level type is published by the L0 alone \u2014 move its export entries to the L0 first, or move the type to another subsystem.`);
47597
+ }
47598
+ const oldToken = from ? `${from}::${bare2}` : void 0;
47599
+ const newToken = target ? `${target}::${bare2}` : bare2;
47600
+ const specsDir = aiPathsAt(getProjectRoot()).specsDir();
47601
+ const rewriteAll2 = (write6) => {
47602
+ if (oldToken === void 0) return [];
47603
+ const out = [];
47604
+ for (const file of listFilesRecursive(specsDir, ".yaml")) {
47605
+ let raw;
47606
+ try {
47607
+ raw = readYamlFile(file);
47608
+ } catch {
47609
+ continue;
47610
+ }
47611
+ const kind = raw && typeof raw === "object" ? specKind(raw) : void 0;
47612
+ if (!kind || !respellQualifiedType(raw, oldToken, newToken)) continue;
47613
+ if (write6) writeYamlFile(file, raw);
47614
+ out.push({ kind, id: kind === "system" ? "system" : String(raw.id) });
47615
+ }
47616
+ return out;
47617
+ };
47618
+ const would = rewriteAll2(false);
47619
+ const rewritten = [.../* @__PURE__ */ new Set([
47620
+ ...would.filter((s) => !(s.kind === "type" && s.id === bare2)).map((s) => s.id),
47621
+ ...exportsMoving.length > 0 && target !== void 0 ? [from, target] : [],
47622
+ ...repointed.length > 0 ? ["system"] : []
47623
+ ])];
47624
+ assertSpecsInReach([{ kind: "type", id: qualifiedTypeId(type) }, ...would], `moving type "${id}"`);
47625
+ const report3 = { kind: "type", id: bare2, from, to: target ?? "", moved: [{ kind: "type", id: qualifiedTypeId(type) }], rewritten, notices: [] };
47626
+ if (dryRun) return { ...report3, dryRun: true };
47627
+ rewriteAll2(true);
47628
+ invalidateSpecCache();
47629
+ if (exportsMoving.length > 0 && fromSpec && target !== void 0) {
47630
+ const stated = loadSubsystemSpecs().find((s) => s.id === from) ?? fromSpec;
47631
+ saveSpec("subsystem", { ...stated, publicInterfaces: (stated.publicInterfaces ?? []).filter((e) => !(e.component === void 0 && names(e.typeDef))) });
47632
+ invalidateSpecCache();
47633
+ const toSpec = loadSubsystemSpecs().find((s) => s.id === target);
47634
+ saveSpec("subsystem", { ...toSpec, publicInterfaces: [...toSpec.publicInterfaces ?? [], ...exportsMoving.map((e) => ({ ...e, typeDef: bare2 }))] });
47635
+ }
47636
+ if (system && repointed.length > 0) {
47637
+ const stated = loadSystemSpec() ?? system;
47638
+ const current3 = stated.publicInterfaces ?? [];
47639
+ saveSystemSpec({ ...stated, publicInterfaces: current3.map((e) => e.from === from && names(e.typeDef) ? { ...e, from: target } : e) });
47640
+ }
47641
+ invalidateSpecCache();
47642
+ const current2 = loadTypeSpecs().find((t) => t.id === bare2 && (t.subsystem ?? "") === from) ?? type;
47643
+ const oldFile = typeFileOf(bare2, current2);
47644
+ const backup = fs19.existsSync(oldFile) ? fs19.readFileSync(oldFile) : null;
47645
+ if (backup) fs19.unlinkSync(oldFile);
47646
+ invalidateSpecCache();
47647
+ try {
47648
+ const { subsystem: _old, ...rest } = current2;
47649
+ saveSpec("type", target !== void 0 ? { ...rest, subsystem: target } : rest);
47650
+ } catch (e) {
47651
+ if (backup) fs19.writeFileSync(oldFile, backup);
47652
+ invalidateSpecCache();
47653
+ throw e;
47654
+ }
47655
+ invalidateSpecCache();
47656
+ return report3;
47657
+ }
47658
+ var TYPE_POSITION_KEYS = /* @__PURE__ */ new Set(["type", "returns", "signature", "signatureFrom", "typeDef", "references", "linkedEntity", "holds"]);
47659
+ function respellQualifiedType(raw, oldToken, newToken) {
47660
+ let changed = false;
47661
+ const escaped = oldToken.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47662
+ const walk2 = (node) => {
47663
+ if (Array.isArray(node)) {
47664
+ node.forEach(walk2);
47665
+ return;
47666
+ }
47667
+ if (!node || typeof node !== "object") return;
47668
+ for (const [key2, value] of Object.entries(node)) {
47669
+ if (typeof value === "string" && TYPE_POSITION_KEYS.has(key2)) {
47670
+ const next = value.replace(new RegExp(`(?<![A-Za-z0-9_:-])${escaped}(?![A-Za-z0-9_-])`, "g"), newToken);
47671
+ if (next !== value) {
47672
+ node[key2] = next;
47673
+ changed = true;
47674
+ }
47675
+ } else if (value && typeof value === "object" && key2 !== "previousIds" && key2 !== "previousNames") {
47676
+ walk2(value);
47677
+ }
47678
+ }
47679
+ };
47680
+ walk2(raw);
47681
+ return changed;
47682
+ }
47683
+ function publishedUsesOf(removed, methods) {
47684
+ const contracts = loadInterfaceSpecs();
47685
+ const uses = [];
47686
+ for (const ref of removed) {
47687
+ if (ref.kind === "component") uses.push(...publishingUses(ref.id, contracts.filter((i) => i.component === ref.id), () => true));
47688
+ if (ref.kind === "interface") {
47689
+ const contract = contracts.find((i) => i.id === ref.id);
47690
+ if (contract) uses.push(...publishingUses(contract.component, [contract], () => true));
47691
+ }
47692
+ }
47693
+ const types = loadTypeSpecs();
47694
+ for (const ref of removed.filter((r) => r.kind === "type")) {
47695
+ const type = types.find((t) => t.id === ref.id || qualifiedTypeId(t) === ref.id);
47696
+ if (type) uses.push(...publishedCarriers(type));
47697
+ }
47698
+ for (const entry of methods) {
47699
+ const cut = entry.lastIndexOf(".");
47700
+ const contract = cut > 0 ? contracts.find((i) => i.id === entry.slice(0, cut)) : void 0;
47701
+ if (!contract) continue;
47702
+ const name = entry.slice(cut + 1);
47703
+ const source = `${contract.component}.${name}`;
47704
+ const followers = contracts.filter((i) => i !== contract && i.methods.some((m) => followsSource(m.signatureFrom, source, i.component)));
47705
+ uses.push(
47706
+ ...publishingUses(contract.component, [contract], (m) => m.name === name),
47707
+ ...followers.flatMap((i) => publishingUses(i.component, [i], (m) => followsSource(m.signatureFrom, source, i.component)))
47708
+ );
47709
+ }
47710
+ return mergeUses(uses);
47711
+ }
46475
47712
  var METHOD_CASINGS = {
46476
47713
  camelCase: /^[a-z][a-zA-Z0-9]*$/,
46477
47714
  PascalCase: /^[A-Z][a-zA-Z0-9]*$/,
@@ -46816,12 +48053,14 @@ function renameType(typeId, newId, dryRun) {
46816
48053
  `renaming type "${typeId}"`,
46817
48054
  Array.isArray(registerEdits) && registerEdits.length > 0
46818
48055
  );
48056
+ const publishedIn = publishedCarriers(type);
46819
48057
  if (dryRun) {
46820
48058
  return {
46821
48059
  from: qualifiedTypeId(type),
46822
48060
  to: qualifiedTypeId({ id: newId, subsystem: type.subsystem }),
46823
48061
  rewritten: [...new Set(wouldRewrite.filter((spec) => !(spec.kind === "type" && spec.id === oldId)).map((spec) => spec.id))],
46824
48062
  keptPublicNames: keptDry.names,
48063
+ publishedIn,
46825
48064
  carried: Array.isArray(registerEdits) ? registerEdits : [],
46826
48065
  dryRun: true
46827
48066
  };
@@ -46843,6 +48082,7 @@ function renameType(typeId, newId, dryRun) {
46843
48082
  to: qualifiedTypeId({ id: newId, subsystem: type.subsystem }),
46844
48083
  rewritten,
46845
48084
  keptPublicNames: kept.names,
48085
+ publishedIn,
46846
48086
  carried
46847
48087
  };
46848
48088
  }
@@ -48111,7 +49351,7 @@ ${typeMapping.map((line) => `- ${line}`).join("\n")}
48111
49351
  const externals = externalsUsedBy(record);
48112
49352
  let readPaths = record.readPaths;
48113
49353
  if (externals.length > 0) {
48114
- const bindings = bindingModulesOf(record);
49354
+ const bindings = bindingModulesOf2(record);
48115
49355
  const bindingNote = bindings.length > 0 ? `
48116
49356
  The binding modules these implementations name are the one place the code spells the producer's names \u2014 keep them exactly as the pin has them (\`validate\` compares them with it and reports a stale name, parameter or field as BINDING_DRIFT, with the rename to follow): ${bindings.map((b) => `\`${b}\``).join(", ")}.
48117
49357
  ` : "\nWhen the code reaches a producer through a hand-written binding module (a typed binding to a native library, a client stub), name it on the implementation as `bindings` \u2014 code linkage, no re-lock \u2014 so `validate` compares it with the pin.\n";
@@ -48229,11 +49469,11 @@ function memberHop(agentId) {
48229
49469
  const alias = agentId.slice(0, at);
48230
49470
  const member = graph().nodes.find((n) => n.parent === "" && n.mountAlias === alias);
48231
49471
  if (!member) return null;
48232
- const relative25 = path31.relative(getProjectRoot(), member.directory).split(path31.sep).join("/");
48233
- return { alias, rest: agentId.slice(at + 2), directory: member.directory, relative: relative25 };
49472
+ const relative27 = path31.relative(getProjectRoot(), member.directory).split(path31.sep).join("/");
49473
+ return { alias, rest: agentId.slice(at + 2), directory: member.directory, relative: relative27 };
48234
49474
  }
48235
- function throughMount(brief, alias, relative25) {
48236
- const under = (p) => path31.posix.join(relative25, p);
49475
+ function throughMount(brief, alias, relative27) {
49476
+ const under = (p) => path31.posix.join(relative27, p);
48237
49477
  return {
48238
49478
  ...brief,
48239
49479
  agentId: `${alias}::${brief.agentId}`,
@@ -48241,7 +49481,7 @@ function throughMount(brief, alias, relative25) {
48241
49481
  ...brief.codeFence ? { codeFence: brief.codeFence.map(under) } : {},
48242
49482
  ...brief.sharedPaths ? { sharedPaths: brief.sharedPaths.map(under) } : {},
48243
49483
  ...brief.readPaths ? { readPaths: brief.readPaths.map(under) } : {},
48244
- root: brief.root ? under(brief.root) : relative25
49484
+ root: brief.root ? under(brief.root) : relative27
48245
49485
  };
48246
49486
  }
48247
49487
  function loadRegistry() {
@@ -48402,7 +49642,7 @@ function externalsUsedBy(record) {
48402
49642
  }
48403
49643
  return out;
48404
49644
  }
48405
- function bindingModulesOf(record) {
49645
+ function bindingModulesOf2(record) {
48406
49646
  const ids = new Set(implementedComponents(record).map((c) => c.id));
48407
49647
  const contracts = new Set(loadInterfaceSpecs().filter((i) => ids.has(i.component)).map((i) => i.id));
48408
49648
  return [...new Set(loadImplementationSpecs().filter((impl) => contracts.has(impl.contract)).flatMap((impl) => impl.bindings ?? []))];
@@ -48685,19 +49925,19 @@ function commitStagedTree(stagingDir, destDir, roots, backupExisting) {
48685
49925
  const backupDir = path36.join(root, BACKUP_DIR, timestampSlug());
48686
49926
  let backedUp = false;
48687
49927
  for (const rootRel of roots) {
48688
- const relative25 = rootRel === "." ? "" : rootRel;
48689
- const stagedSpecDir = path36.join(stagingDir, relative25, ".wai");
49928
+ const relative27 = rootRel === "." ? "" : rootRel;
49929
+ const stagedSpecDir = path36.join(stagingDir, relative27, ".wai");
48690
49930
  if (!fs23.existsSync(stagedSpecDir)) continue;
48691
- const liveSpecDir = liveSpecDirFor(path36.join(root, relative25));
49931
+ const liveSpecDir = liveSpecDirFor(path36.join(root, relative27));
48692
49932
  if (backupExisting && liveSpecDir && fs23.existsSync(liveSpecDir)) {
48693
- const backupTarget = path36.join(backupDir, relative25, path36.basename(liveSpecDir));
49933
+ const backupTarget = path36.join(backupDir, relative27, path36.basename(liveSpecDir));
48694
49934
  fs23.mkdirSync(path36.dirname(backupTarget), { recursive: true });
48695
49935
  fs23.renameSync(liveSpecDir, backupTarget);
48696
49936
  backedUp = true;
48697
49937
  } else if (liveSpecDir && fs23.existsSync(liveSpecDir)) {
48698
49938
  fs23.rmSync(liveSpecDir, { recursive: true, force: true });
48699
49939
  }
48700
- const target = path36.join(root, relative25, ".wai");
49940
+ const target = path36.join(root, relative27, ".wai");
48701
49941
  fs23.mkdirSync(path36.dirname(target), { recursive: true });
48702
49942
  if (fs23.existsSync(target)) fs23.rmSync(target, { recursive: true, force: true });
48703
49943
  fs23.renameSync(stagedSpecDir, target);
@@ -49260,7 +50500,7 @@ function retireSpecialists(apply3) {
49260
50500
  if (dependency2 && DATA_FACADES.has(dependency2.componentType)) return "facade";
49261
50501
  return "neither";
49262
50502
  };
49263
- const described = (id) => {
50503
+ const described2 = (id) => {
49264
50504
  const dependency2 = byId.get(id);
49265
50505
  const fit = fitOf(id);
49266
50506
  let what;
@@ -49273,14 +50513,14 @@ function retireSpecialists(apply3) {
49273
50513
  const reasonFor = (specialist, decision, settled2) => {
49274
50514
  const dependencies = specialist.dependsOn;
49275
50515
  if (decision === "pure") {
49276
- return dependencies.length === 0 ? "no dependencies" : `depends only on pure logic: ${dependencies.map(described).join(", ")}`;
50516
+ return dependencies.length === 0 ? "no dependencies" : `depends only on pure logic: ${dependencies.map(described2).join(", ")}`;
49277
50517
  }
49278
50518
  if (decision === "read") {
49279
50519
  const deciding2 = dependencies.filter((id) => fitOf(id) !== "pure");
49280
- return `${settled2 ? "in or behind a dependency cycle; " : ""}depends on read logic or a data facade: ${deciding2.map(described).join(", ")}`;
50520
+ return `${settled2 ? "in or behind a dependency cycle; " : ""}depends on read logic or a data facade: ${deciding2.map(described2).join(", ")}`;
49281
50521
  }
49282
50522
  const deciding = dependencies.filter((id) => fitOf(id) === "neither");
49283
- return `depends on ${deciding.map(described).join(", ")}, which neither pure nor read logic may depend on`;
50523
+ return `depends on ${deciding.map(described2).join(", ")}, which neither pure nor read logic may depend on`;
49284
50524
  };
49285
50525
  let classifiedAny;
49286
50526
  do {
@@ -49976,12 +51216,12 @@ Approved: ${stamp} \u2014 this lock predates per-spec approval, so drift is only
49976
51216
  const carry = isContentReadingRecord(approval) && own2?.state === "approved" ? `
49977
51217
  ${REEXPRESS_HINT}` : "";
49978
51218
  if (diffSize(diff3) === 0) {
49979
- const owes = own2?.state === "drifted" && !own2.upgraded ? own2.owed !== void 0 ? ` But the approval does not cover this project: ${own2.owed}. \`wairon lock-check\` fails until it is cleared${/never pinned|beyond its pin/.test(own2.owed) ? " \u2014 pin first (`wairon externals pin`), then `wairon lock`" : " \u2014 `wairon lock` re-approves the unchanged design"}.` : own2.release && !own2.release.carried ? own2.release.reason ? ` But ${own2.release.reason}` : ` But it was approved under wairon ${own2.release.from}, and the new release (${own2.release.to}) finds ${own2.release.count ?? 0} issue(s) in the approved design: ${(own2.release.findings ?? []).join("; ")}. Resolve them, then \`wairon lock\`.` : pinDrift ? "" : own2.inputsMoved && own2.inputsMoved.length > 0 ? ` But what it was approved under did change: ${describeGateParts(own2.inputsMoved)}. \`wairon lock\` re-approves the unchanged design under it.` : " But what it was approved under did change: an input the gate identity covers \u2014 the doctrine, the network declaration, a consumed contract's pin, `composition` or a member's approval. `wairon lock` re-approves the unchanged design." : "";
51219
+ const owes = own2?.state === "drifted" && !own2.upgraded ? own2.owed !== void 0 ? ` But the approval does not cover this project: ${own2.owed}. \`wairon lock-check\` fails until it is cleared${/never pinned|beyond its pin/.test(own2.owed) ? " \u2014 pin first (`wairon externals pin`), then `wairon lock`" : /extension pack\(s\) selected/.test(own2.owed) ? " \u2014 make the pack load (install or vendor it, or drop the selection), then `wairon lock`" : " \u2014 `wairon lock` re-approves the unchanged design"}.` : own2.release && !own2.release.carried ? own2.release.reason ? ` But ${own2.release.reason}` : ` But it was approved under wairon ${own2.release.from}, and the new release (${own2.release.to}) finds ${own2.release.count ?? 0} issue(s) in the approved design: ${(own2.release.findings ?? []).join("; ")}. Resolve them, then \`wairon lock\`.` : pinDrift ? "" : own2.inputsMoved && own2.inputsMoved.length > 0 ? ` But what it was approved under did change: ${describeGateParts(own2.inputsMoved)}. \`wairon lock\` re-approves the unchanged design under it.` : " But what it was approved under did change: an input the gate identity covers \u2014 the doctrine, the network declaration, a consumed contract's pin, `composition` or a member's approval. `wairon lock` re-approves the unchanged design." : "";
49980
51220
  const released = own2?.release?.carried ? ` Approved under wairon ${own2.release.from}, re-validated under ${own2.release.to}: still approved (\`wairon lock\` refreshes the record's release stamp without a re-approval).` : "";
49981
51221
  const ignored = own2?.partsIgnored !== void 0 ? ` Notice: the record's gate parts are ignored \u2014 ${own2.partsIgnored}; \`wairon lock\` rewrites them.` : "";
49982
51222
  return {
49983
51223
  text: `
49984
- Approved: ${stamp} \u2014 no spec has changed since.${released}${owes}${ignored}${childNote}${carry}
51224
+ Approved: ${stamp} \u2014 no spec has changed since.${released}${owes}${ignored}${stampNotice(own2)}${childNote}${carry}
49985
51225
  `,
49986
51226
  drifted: pinDrift || own2?.state === "drifted"
49987
51227
  };
@@ -49990,7 +51230,7 @@ Approved: ${stamp} \u2014 no spec has changed since.${released}${owes}${ignored}
49990
51230
  if (diff3.changed.length) parts.push(`${diff3.changed.length} changed`);
49991
51231
  if (diff3.added.length) parts.push(`${diff3.added.length} added`);
49992
51232
  if (diff3.removed.length) parts.push(`${diff3.removed.length} removed`);
49993
- const named2 = [...diff3.changed, ...diff3.added, ...diff3.removed].slice(0, 5);
51233
+ const named2 = [...diff3.changed, ...diff3.added, ...diff3.removed].slice(0, VERDICT_LIST_LIMIT);
49994
51234
  const rest = diffSize(diff3) - named2.length;
49995
51235
  const n = diffSize(diff3);
49996
51236
  const noun = `${n} spec${n === 1 ? "" : "s"}`;
@@ -49999,8 +51239,9 @@ Approved: ${stamp} \u2014 no spec has changed since.${released}${owes}${ignored}
49999
51239
  text: `
50000
51240
  ${headline} \u2014 approved ${stamp}:
50001
51241
  ` + named2.map((p) => ` ${p}`).join("\n") + (rest > 0 ? `
50002
- \u2026 and ${rest} more` : "") + (own2?.owed !== void 0 ? `
50003
- The approval also owes: ${own2.owed}.` : "") + childNote + carry + "\n",
51242
+ \u2026 and ${rest} more \u2014 \`wairon status --all\` lists every one` : "") + (own2?.owed !== void 0 ? `
51243
+ The approval also owes: ${own2.owed}.` : "") + (own2?.stampProblem !== void 0 ? `
51244
+ ${stampNotice(own2).trim()}` : "") + childNote + carry + "\n",
50004
51245
  drifted: true,
50005
51246
  moved: [...diff3.changed.map((p) => `~ ${p}`), ...diff3.added.map((p) => `+ ${p}`), ...diff3.removed.map((p) => `- ${p}`)]
50006
51247
  };
@@ -50008,6 +51249,10 @@ The approval also owes: ${own2.owed}.` : "") + childNote + carry + "\n",
50008
51249
  return quiet;
50009
51250
  }
50010
51251
  }
51252
+ var VERDICT_LIST_LIMIT = 40;
51253
+ function stampNotice(own2) {
51254
+ return own2?.stampProblem !== void 0 ? ` Notice: ${own2.stampProblem} \u2014 the identity alone decided this, and \`wairon lock\` replaces the stamp with this release.` : "";
51255
+ }
50011
51256
  function isContentReadingRecord(record) {
50012
51257
  return record.specsReading !== "design";
50013
51258
  }
@@ -50119,13 +51364,13 @@ This project uses **wairon**. System specs live under \`.wai/specs/\` (L0 System
50119
51364
 
50120
51365
  ### How you operate
50121
51366
  - **To design/modify specs**: Use **\`sdd-architect\`** skill (in \`.claude/skills/\` or \`.gemini/skills/\`).
50122
- - **Manage specs via MCP tools only** (namespaced if needed; do not edit specs manually): author with \`sdd_initialize_system\`, \`sdd_add_subsystem\`, \`sdd_add_component\`, \`sdd_define_interface\`, \`sdd_set_endpoints\`, \`sdd_write_narrative\`, \`sdd_add_type\`, \`sdd_set_public_interfaces\`, \`sdd_update_spec\` (a granular delta, with \`dryRun\`) and \`sdd_delete_spec\`; rename and move with traces with \`sdd_rename_component\`, \`sdd_rename_method\` (with \`dryRun\`: it names the consumers a renamed verb breaks), \`sdd_rename_param\`, \`sdd_rename_type\`, \`sdd_rename_field\` and \`sdd_move_methods\`; read with \`sdd_get_status\`, \`sdd_get_spec\`, \`sdd_validate_tree\`, \`sdd_get_agent_brief\`, \`sdd_get_network_flows\` and \`sdd_explain_flow\` (the network), \`sdd_list_consumers\` and \`sdd_surface_diff\` (who uses this project's exports, method by method, and what its public surface changed since the last approval), \`sdd_get_externals_status\` and \`sdd_pack_impact\`; the network with \`sdd_set_network\`; externals with \`sdd_add_external\`, \`sdd_update_external\`, \`sdd_pin_externals\` and \`sdd_remove_external\`; the family's shape with the member tools below. The legacy topology tools \`getAgent\`, \`getProjectConfig\`, \`listAgents\`, \`listDomains\` and \`validateTopology\` read the generated agent registry only.
51367
+ - **Manage specs via MCP tools only** (namespaced if needed; do not edit specs manually): author with \`sdd_initialize_system\`, \`sdd_add_subsystem\`, \`sdd_add_component\`, \`sdd_define_interface\`, \`sdd_set_endpoints\`, \`sdd_write_narrative\`, \`sdd_add_type\`, \`sdd_set_public_interfaces\`, \`sdd_update_spec\` (a granular delta, with \`dryRun\`) and \`sdd_delete_spec\`; rename and move with traces with \`sdd_rename_component\`, \`sdd_rename_method\` (with \`dryRun\`: it names the consumers a renamed verb breaks), \`sdd_rename_param\`, \`sdd_rename_type\`, \`sdd_rename_field\`, \`sdd_rename_spec\` (a contract or an implementation on its own) and \`sdd_move_methods\`, and move a component or a type to another subsystem with \`sdd_move_spec\` (every one with \`dryRun\`; \`sdd_delete_spec\` and a contract method removed through \`sdd_update_spec\` name the consumers they break, as the renames do); read with \`sdd_get_status\`, \`sdd_get_spec\`, \`sdd_validate_tree\`, \`sdd_get_agent_brief\`, \`sdd_get_network_flows\` and \`sdd_explain_flow\` (the network), \`sdd_list_consumers\` and \`sdd_surface_diff\` (who uses this project's exports, method by method, and what its public surface changed since the last approval), \`sdd_get_externals_status\` and \`sdd_pack_impact\`; the network with \`sdd_set_network\`; externals with \`sdd_add_external\`, \`sdd_update_external\`, \`sdd_pin_externals\` and \`sdd_remove_external\`; the family's shape with the member tools below. The legacy topology tools \`getAgent\`, \`getProjectConfig\`, \`listAgents\`, \`listDomains\` and \`validateTopology\` read the generated agent registry only.
50123
51368
  - **Growing a system \u2014 subsystems \u2192 parts \u2192 projects**: boundaries are earned. Start with subsystems in one folder. A piece that needs its own folder or repository but stays the same system is a **part**: declared in \`.wai/project.yaml\` \`members\` by one location key (\`scheduler: services/scheduler\`, \`admin: ../admin\`, \`payments: git@host:acme/payments.git#<commit>\`; long form \`{ source, as, description }\`), its subsystems this project's own \u2014 local ids, this project's lock. \`sdd_add_member\` creates a part by default (\`as: project\` for a project) and \`sdd_externalize_subsystem\` moves a subsystem into one. A **project** is an independent boundary \u2014 its own id, exports, lock \u2014 reached as \`alias::name\`; make one only when it needs its own team, release, approval or public surface: \`sdd_promote_member\` (\`sdd_demote_member\` back). What a member is follows from its content (an id, an L0 or a lock make a project).
50124
51369
  - **Members & cross-project references**: relocate a member with \`sdd_move_member\`. Every other change of the family's shape is a **family migration**, each with \`dryRun\`: promote/demote, make an existing project a member with \`sdd_attach_member\`, take one out and back with \`sdd_detach_member\` / \`sdd_adopt_member\`, rename a project's id with \`sdd_rename_project\` or an alias with \`sdd_rename_member_alias\`, move a subsystem into a part with \`sdd_externalize_subsystem\` and fold a member back in with \`sdd_internalize_member\`. Run it with \`dryRun: true\` first and show the plan (the human's CLI calls the same plan \`--report\`); applied, it writes every project it touches or none, and never locks \u2014 it names the projects to re-lock. A project member is never a subsystem of its parent: it has its own \`.wai/\` tree and is designed from its own root \u2014 its specs, its L0 export table and its \`project.yaml\` are written by a session opened in that member's folder (its own guide and \`.mcp.json\`), and the tools here refuse such a write naming that folder.
50125
51370
  - **\`alias::name\`**: An id without \`::\` is local to the project that writes it. Anything another project provides is referenced as \`alias::name\` \u2014 the alias is one of your members or declared \`externals\`, the name a public name in that project's L0 export table. A reference to something it does not export is reported (\`EXTERNAL_NOT_EXPORTED\`).
50126
51371
  - **Deprecated forms** (they still resolve for one release, are reported, and \`wairon doctor --fix\` rewrites them): a leading \`::\` (\`::shared::error-type\`), \`super::\` (\`super::sibling_comp\`), member paths (\`billing::invoice::invoice_portal\`), and an L1 subsystem carrying \`projectPath\` (\`DEPRECATED_MOUNT_FORM\`).
50127
51372
  - **Do not run the \`wairon\` CLI**: Use \`sdd_validate_tree\` and \`sdd_get_status\` instead of CLI commands.
50128
- - **Handoff to implementation**: Once design is complete and validates cleanly, tell the human: *"The specs are complete and validate. Please run \`wairon lock\` to approve them, and commit \`.wai/lock.json\`."* The lock records the approval; it does not rewrite spec files or their \`status\`. No session restart is needed after the lock \u2014 delegate implementation right away via the \`sdd-delegate\` skill.
51373
+ - **Handoff to implementation**: Once design is complete and validates cleanly, tell the human: *"The specs are complete and validate. Please run \`wairon lock\` to approve them, and commit \`.wai/lock.json\`."* The lock records the approval; it does not rewrite spec files or their \`status\`. When a change to an approved design stays inside one subsystem, the human may re-approve just that subsystem with \`wairon lock --subsystem <id>\` (refused as a first approval, when anything outside the subsystem moved, and for a member project's subsystem, which is locked at the member's own root). In a family, members lock first at their own roots, then the parent pins them. No session restart is needed after the lock \u2014 delegate implementation right away via the \`sdd-delegate\` skill.
50129
51374
  - **Approval, not status**: a design is ready to implement when it is APPROVED \u2014 \`sdd_get_status\` reports the approval state (approved, or which specs changed since), and \`wairon lock-check\` gives the same verdict in CI. A spec's \`status\` (draft/design/complete) is authoring readiness only; never wait for it to become \`complete\`. It is left out of the approval, so promoting a status never reopens an approved design.
50130
51375
  - **Prose is design; linkage is not**: an L4/L5 prose change \u2014 an implementation's or a method's description, intent or narrative step text \u2014 IS a design change: it re-opens the approval, so \`wairon lock\` is owed before code is implemented against it (an implement step asked for in the same turn as a prose edit is refused until the human re-locks; sequence spec turn \u2192 lock \u2192 code turn). Only code linkage is outside it.
50131
51376
  - **Code linkage is not approval**: the lock approves the DESIGN. Where it is realized \u2014 \`sourcePath\`, \`symbol\`, \`exportedVia\`, \`simPath\`, \`injectedParams\`, conformance tiers, timestamps \u2014 is outside the approved digests, so setting or changing it never asks for a re-lock. Declare each implementation's planned \`sourcePath\` at design time: a named file not written yet is \`SOURCE_FILE_PLANNED\` (a notice), at method level as at implementation level; once the component's realization begins (any file it names exists), a contract method naming no file is \`METHOD_SOURCE_PATH_MISSING\` (warning) and one its existing file does not hold is \`UNREALIZED_METHOD\`. \`rules.conformance.requireCode: true\` makes the planned and unlinked notices errors. Briefs fence planned files, marked \`(planned \u2014 create it)\`.
@@ -50732,7 +51977,9 @@ function getStatusReport(options = {}, decor) {
50732
51977
  members.forEach((member, j) => {
50733
51978
  const isLast = ownSubs.length + j === total - 1;
50734
51979
  const form = member.mountForm === "mount" ? mark.draft(" [mount form]") : "";
50735
- const label7 = `[Project] ${member.mountAlias ?? member.namespace} (${member.id ?? "no id"})`;
51980
+ const top = family.nodes.find((n) => n.namespace === "")?.directory;
51981
+ const sibling = top !== void 0 && path39.relative(top, member.directory).startsWith("..") ? ` \xB7 ${storageOf("path", member.directory, void 0)}` : "";
51982
+ const label7 = `[Project] ${member.mountAlias ?? member.namespace} (${member.id ?? "no id"})${sibling}`;
50736
51983
  const approval = approvalTag(options.approvals?.find((a) => a.key === member.namespace));
50737
51984
  output += `${mark.structure(indent + (isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "))}${mark.layer("project", label7)}${form}${approval}
50738
51985
  `;
@@ -50764,7 +52011,8 @@ function getStatusReport(options = {}, decor) {
50764
52011
  } else if (own2) {
50765
52012
  const release = own2.release ? own2.release.carried ? ` (approved under wairon ${own2.release.from}, re-validated under ${own2.release.to}: still approved)` : own2.release.reason ? ` (${own2.release.reason})` : ` (approved under wairon ${own2.release.from}; the new release finds ${own2.release.count ?? 0} issue(s) in the approved design)` : "";
50766
52013
  const ignored = own2.partsIgnored !== void 0 ? ` (notice: the record's gate parts are ignored \u2014 ${own2.partsIgnored}; \`wairon lock\` rewrites them)` : "";
50767
- output += `${mark.layer("system", "Approval:")} this project is ${OWN_APPROVAL_WORDS[own2.state]}${release}${own2.upgraded ? " (locked under an earlier gate identity \u2014 re-lock once)" : ""}${ignored}
52014
+ const stamp = own2.stampProblem !== void 0 ? ` (notice: ${own2.stampProblem}; \`wairon lock\` replaces the stamp)` : "";
52015
+ output += `${mark.layer("system", "Approval:")} this project is ${OWN_APPROVAL_WORDS[own2.state]}${release}${own2.upgraded ? " (locked under an earlier gate identity \u2014 re-lock once)" : ""}${ignored}${stamp}
50768
52016
  `;
50769
52017
  }
50770
52018
  return { text: output, failed: false };
@@ -51585,9 +52833,14 @@ function canonical(value) {
51585
52833
  }
51586
52834
  function writeSpec(restatement) {
51587
52835
  const bound2 = candidateOptions();
52836
+ const id = restatement.kind === "system" ? "system" : restatement.spec.id;
51588
52837
  const parentRef = restatementParent(restatement);
52838
+ const foreign = crossProjectWriteRefusal(
52839
+ [{ kind: restatement.kind, id }, ...parentRef && typeof parentRef.id === "string" ? [{ kind: parentRef.kind, id: parentRef.id }] : []],
52840
+ CREATE_TOOL[restatement.kind]
52841
+ );
52842
+ if (foreign !== void 0) throw new Error(foreign);
51589
52843
  const parent = parentRef ? loadSpec(parentRef.kind, parentRef.id) : null;
51590
- const id = restatement.kind === "system" ? "system" : restatement.spec.id;
51591
52844
  const existing = loadSpec(restatement.kind, id);
51592
52845
  if (!existing && TRACED_KINDS.has(restatement.kind)) {
51593
52846
  const spec = restatement.spec;
@@ -51603,21 +52856,83 @@ function writeSpec(restatement) {
51603
52856
  if (application.refusal !== void 0) throw new Error(application.refusal);
51604
52857
  const names = introducedNameProblems(restatement.kind, application.spec, existing);
51605
52858
  if (names.length) throw new Error(nameRefusal(restatement.kind, id, names));
52859
+ refuseMountForm(restatement.kind, application.spec);
51606
52860
  const gateNotices = restatement.kind === "component" ? judgeComponent(application.spec, bound2) : [];
51607
52861
  gateNotices.push(...judgeInTree(restatement.kind, application.spec, bound2));
51608
- const persisted = persistCandidate(restatement.kind, application.spec);
52862
+ let cascade = [];
52863
+ if (restatement.kind === "interface" && existing) {
52864
+ const dropped = removedByName(existing.methods, application.spec.methods);
52865
+ if (dropped.length > 0) {
52866
+ const removal = methodRemoval(scanAllSpecs({ memberDepth: 0 }), id, application.spec.component, dropped);
52867
+ if (removal.references.length > 0) throw new Error(methodReferencedRefusal(id, removal.referenced, removal.references));
52868
+ cascade = removal.cascade;
52869
+ }
52870
+ }
51609
52871
  const testsToRevisit = testsInvalidatedBy(restatement.kind, id, application.changedMethods, [existing, application.spec], bound2);
52872
+ const cascaded = cascade.map((c) => `${c.implementation}.${c.method}`);
52873
+ const name = application.spec.name;
52874
+ const status = application.status ? { status: application.status } : {};
52875
+ if (restatement.dryRun) {
52876
+ return {
52877
+ kind: restatement.kind,
52878
+ id,
52879
+ name,
52880
+ replacedExisting: application.replacedExisting,
52881
+ ...status,
52882
+ notices: [...gateNotices, ...application.notices, ...cascadeNotices(cascade, true)],
52883
+ respellings: application.respellings,
52884
+ testsToRevisit,
52885
+ ...cascaded.length > 0 ? { cascaded } : {},
52886
+ dryRun: true
52887
+ };
52888
+ }
52889
+ const persisted = persistCandidate(restatement.kind, application.spec);
52890
+ for (const [implementation, methods] of groupCascade(cascade)) {
52891
+ updateSpec("implementation", implementation, { methods: methods.map((m) => ({ name: m, action: "delete" })) });
52892
+ }
51610
52893
  return {
51611
52894
  kind: restatement.kind,
51612
52895
  id,
51613
- name: application.spec.name,
52896
+ name,
51614
52897
  replacedExisting: application.replacedExisting,
51615
- ...application.status ? { status: application.status } : {},
51616
- notices: [...gateNotices, ...persisted.notices, ...application.notices],
52898
+ ...status,
52899
+ notices: [...gateNotices, ...persisted.notices, ...application.notices, ...cascadeNotices(cascade, false)],
51617
52900
  respellings: application.respellings,
51618
- testsToRevisit
52901
+ testsToRevisit,
52902
+ ...cascaded.length > 0 ? { cascaded } : {}
51619
52903
  };
51620
52904
  }
52905
+ var CREATE_TOOL = {
52906
+ system: "sdd_initialize_system",
52907
+ subsystem: "sdd_add_subsystem",
52908
+ component: "sdd_add_component",
52909
+ interface: "sdd_define_interface",
52910
+ implementation: "sdd_write_narrative",
52911
+ type: "sdd_add_type"
52912
+ };
52913
+ function methodRemoval(index, contract, component, removed) {
52914
+ const realizing = index.implementations.filter((impl) => impl.contract === contract);
52915
+ const stillDeclared = (m) => index.interfaces.some((i) => i.component === component && i.id !== contract && i.methods.some((x) => x.name === m));
52916
+ const referenced = removed.filter((m) => !stillDeclared(m));
52917
+ const inside = (r) => r.kind === "implementation" && realizing.some((impl) => impl.id === r.id) && removed.some((m) => r.position === `methods.${m}` || r.position.startsWith(`methods.${m}.`));
52918
+ const references = referenced.length === 0 ? [] : specIndexMethodReferences(index, component, referenced).filter((r) => !inside(r));
52919
+ const cascade = realizing.flatMap((impl) => removed.filter((m) => impl.methods.some((x) => x.name === m)).map((m) => ({ implementation: impl.id, method: m })));
52920
+ return { referenced, references, cascade };
52921
+ }
52922
+ function methodReferencedRefusal(contract, removed, references) {
52923
+ return `Refusing to remove ${removed.length === 1 ? "method" : "methods"} ${removed.map((m) => `"${m}"`).join(", ")} from contract "${contract}": specs that stay still hold ${references.length === 1 ? "a reference" : `${references.length} references`} to ${removed.length === 1 ? "it" : "them"} \u2014 the invalid method references validate would report:
52924
+ ${references.map((r) => `- ${r.kind} "${r.id}" (${r.position}) -> ${r.target.kind} "${r.target.id}"`).join("\n")}
52925
+ Edit those specs first \u2014 retarget or remove each call. Nothing was written.`;
52926
+ }
52927
+ function groupCascade(cascade) {
52928
+ const out = /* @__PURE__ */ new Map();
52929
+ for (const c of cascade) out.set(c.implementation, [...out.get(c.implementation) ?? [], c.method]);
52930
+ return out;
52931
+ }
52932
+ function cascadeNotices(cascade, dryRun) {
52933
+ if (cascade.length === 0) return [];
52934
+ return [`Cascaded: ${dryRun ? "would remove" : "removed"} the implementation ${cascade.length === 1 ? "entry" : "entries"} ${cascade.map((c) => `"${c.implementation}.${c.method}"`).join(", ")} with ${cascade.length === 1 ? "its" : "their"} contract method \u2014 a narrative realizes its contract method and means nothing without it.`];
52935
+ }
51621
52936
  function judgeComponent(candidate, options) {
51622
52937
  const verdict = validateComponentCandidate(candidate, options);
51623
52938
  if (verdict.errors.length) throw new Error(formatCandidateRefusal(verdict));
@@ -51636,6 +52951,10 @@ Nothing was written. Route the dependency the way the message names, or bind the
51636
52951
  return noticesFrom(verdict);
51637
52952
  }
51638
52953
  function persistCandidate(kind, spec) {
52954
+ refuseMountForm(kind, spec);
52955
+ return { notices: saveSpec(kind, spec) };
52956
+ }
52957
+ function refuseMountForm(kind, spec) {
51639
52958
  const projectPath = kind === "subsystem" ? spec.projectPath : void 0;
51640
52959
  if (projectPath !== void 0 && projectPath.trim() !== "") {
51641
52960
  const id = spec.id;
@@ -51643,7 +52962,6 @@ function persistCandidate(kind, spec) {
51643
52962
  `mount form refused: subsystem "${id}" names a projectPath ("${projectPath}"), which is the legacy L1 mount form \u2014 wairon never writes it. A member project is declared in project.yaml \`members\`: use sdd_add_member (or \`wairon member add ${id} ${projectPath}\`), which scaffolds the member project and declares it.`
51644
52963
  );
51645
52964
  }
51646
- return { notices: saveSpec(kind, spec) };
51647
52965
  }
51648
52966
  function testsInvalidatedBy(kind, id, methods, specs, options, written = []) {
51649
52967
  const testRoots = options.rules?.conformance?.testRoots ?? [];
@@ -51671,25 +52989,51 @@ function searchedMethods(kind, id, names, specs, written) {
51671
52989
  });
51672
52990
  }
51673
52991
  function deleteSpec2(kind, id, dryRun, force) {
51674
- if (kind === "system" || kind !== "type" && id.includes("::")) {
52992
+ const foreign = crossProjectWriteRefusal([{ kind, id }], "sdd_delete_spec");
52993
+ if (foreign !== void 0) throw new Error(foreign);
52994
+ if (kind === "system") {
52995
+ throw new Error("Refusing to delete the L0 System spec: it is the root every other spec hangs from, and is never deleted. Nothing was deleted.");
52996
+ }
52997
+ if (kind !== "type" && id.includes("::")) {
51675
52998
  const deleted = dryRun ? loadSpec(kind, id) !== null : deleteSpec(kind, id);
51676
- return { kind, id, deleted: deleted && !dryRun, dryRun: dryRun === true, removed: deleted ? [{ kind, id }] : [], references: [], testsToRevisit: [] };
52999
+ if (!deleted && loadSpec(kind, id) === null) throw new Error(`spec-missing: no ${kind} has the id "${id}" \u2014 nothing was deleted.`);
53000
+ return { kind, id, deleted: deleted && !dryRun, dryRun: dryRun === true, removed: [{ kind, id }], references: [], testsToRevisit: [], published: [] };
51677
53001
  }
51678
53002
  const bound2 = candidateOptions();
51679
53003
  const index = scanAllSpecs({ memberDepth: 0 });
51680
53004
  const system = loadSpec("system", "system");
51681
53005
  const removed = specIndexSubtreeOf(index, kind, id);
51682
- if (removed.length === 0) return { kind, id, deleted: false, dryRun: dryRun === true, removed: [], references: [], testsToRevisit: [] };
53006
+ if (removed.length === 0) throw new Error(missingRefusal(kind, id, index));
51683
53007
  const references = specIndexReferencesTo(index, removed, system);
53008
+ const published = publishedUsesOf(removed, []);
51684
53009
  const testsToRevisit = removed.flatMap((ref) => {
51685
53010
  const spec = specOf(index, ref);
51686
53011
  const methods = (spec?.methods ?? []).map((m) => m.name);
51687
53012
  return ref.kind === "interface" || ref.kind === "implementation" || ref.kind === "type" ? testsInvalidatedBy(ref.kind, ref.id, methods, [spec], bound2) : [];
51688
53013
  });
51689
- if (dryRun) return { kind, id, deleted: false, dryRun: true, removed, references, testsToRevisit };
53014
+ if (dryRun) return { kind, id, deleted: false, dryRun: true, removed, references, testsToRevisit, published };
51690
53015
  if (references.length > 0 && !force) throw new Error(referencedRefusal(kind, id, references));
51691
53016
  for (const ref of removed) deleteSpec(ref.kind, ref.id);
51692
- return { kind, id, deleted: true, dryRun: false, removed, references, testsToRevisit };
53017
+ return { kind, id, deleted: true, dryRun: false, removed, references, testsToRevisit, published };
53018
+ }
53019
+ function missingRefusal(kind, id, index) {
53020
+ const pools = {
53021
+ subsystem: index.subsystems,
53022
+ component: index.components,
53023
+ interface: index.interfaces,
53024
+ implementation: index.implementations,
53025
+ type: index.types
53026
+ };
53027
+ const hints = [];
53028
+ const elsewhere = Object.keys(pools).filter((k) => k !== kind && pools[k].some((s) => s.id === id));
53029
+ if (elsewhere.length > 0) hints.push(`"${id}" is ${elsewhere.map((k) => `a ${k}`).join(" and ")} \u2014 pass kind "${elsewhere[0]}"`);
53030
+ const dot = id.lastIndexOf(".");
53031
+ if (kind === "type" && dot > 0 && index.types.some((t) => t.id === id.slice(dot + 1))) {
53032
+ hints.push(`a type is addressed by its bare id, or as <subsystem>::<id> \u2014 "${id.slice(dot + 1)}" or "${id.slice(0, dot)}::${id.slice(dot + 1)}"`);
53033
+ }
53034
+ const known = pools[kind].map((s) => kind === "type" ? qualifiedTypeId(s) : s.id).sort();
53035
+ const listed = known.length === 0 ? `This project has no ${kind} at all.` : `Its ${kind}s: ${known.slice(0, 12).join(", ")}${known.length > 12 ? `, \u2026 (${known.length} in all)` : ""}.`;
53036
+ return `spec-missing: no ${kind} has the id "${id}" \u2014 nothing was deleted.${hints.length > 0 ? ` ${hints.join("; ")}.` : ""} ${listed}`;
51693
53037
  }
51694
53038
  function specOf(index, ref) {
51695
53039
  const pool = ref.kind === "subsystem" ? index.subsystems : ref.kind === "component" ? index.components : ref.kind === "interface" ? index.interfaces : ref.kind === "implementation" ? index.implementations : ref.kind === "type" ? index.types : [];
@@ -51702,10 +53046,29 @@ Edit those specs first, or pass force: true to delete anyway and leave them dang
51702
53046
  }
51703
53047
  function updateSpecGated(kind, id, delta, dryRun) {
51704
53048
  const bound2 = candidateOptions();
53049
+ const foreign = crossProjectWriteRefusal([{ kind, id }], "sdd_update_spec");
53050
+ if (foreign !== void 0) throw new Error(foreign);
51705
53051
  const testRoots = bound2.rules?.conformance?.testRoots ?? [];
51706
53052
  const stored = loadSpec(kind, id);
53053
+ const removedMethods = kind === "interface" ? methodsDeltaRemoves(stored, delta) : [];
53054
+ let cascade = [];
53055
+ let published = [];
53056
+ if (removedMethods.length > 0) {
53057
+ const removal = methodRemoval(scanAllSpecs({ memberDepth: 0 }), id, stored.component, removedMethods);
53058
+ if (removal.references.length > 0) throw new Error(methodReferencedRefusal(id, removal.referenced, removal.references));
53059
+ cascade = removal.cascade;
53060
+ published = publishedUsesOf([], removal.referenced.map((m) => `${id}.${m}`));
53061
+ }
51707
53062
  const gate = componentCandidateGate(bound2, stored?.subsystem, stored);
51708
53063
  const report3 = updateSpec(kind, id, delta, gate, dryRun);
53064
+ if (cascade.length > 0 && report3.changes.length > 0) {
53065
+ for (const [implementation, methods] of groupCascade(cascade)) {
53066
+ updateSpec("implementation", implementation, { methods: methods.map((m) => ({ name: m, action: "delete" })) }, void 0, dryRun);
53067
+ }
53068
+ report3.cascaded = cascade.map((c) => `${c.implementation}.${c.method}`);
53069
+ report3.notices.push(...cascadeNotices(cascade, dryRun === true));
53070
+ }
53071
+ if (published.length > 0 && report3.changes.length > 0) report3.published = published;
51709
53072
  if (kind === "component" && typeof delta?.transport === "string" && report3.changes.some((c) => c.path === "transport")) {
51710
53073
  report3.notices.push(...adapterTransportImpact(id, delta.transport));
51711
53074
  }
@@ -51720,6 +53083,14 @@ function updateSpecGated(kind, id, delta, dryRun) {
51720
53083
  }
51721
53084
  return report3;
51722
53085
  }
53086
+ function methodsDeltaRemoves(stored, delta) {
53087
+ const held = (stored?.methods ?? []).map((m) => m.name);
53088
+ if (held.length === 0 || !delta || typeof delta !== "object") return [];
53089
+ const methods = delta.methods;
53090
+ if (Array.isArray(methods) && methods.length === 0 || Array.isArray(delta.unset) && delta.unset.includes("methods")) return held;
53091
+ if (!Array.isArray(methods)) return [];
53092
+ return methods.filter((m) => m && typeof m === "object" && (m.action === "delete" || m.remove === true) && held.includes(m.name)).map((m) => String(m.name));
53093
+ }
51723
53094
  function adapterTransportImpact(portal, transport) {
51724
53095
  const components = loadComponentSpecs();
51725
53096
  if (components.find((c) => c.id === portal)?.componentType !== "Portal") return [];
@@ -51765,8 +53136,34 @@ function judgeMoveHome(component, source, options) {
51765
53136
  return { codes: [...new Set(codes)], verdict, ceiling };
51766
53137
  }
51767
53138
  function moveMethods2(from, to, methods, dryRun) {
53139
+ const foreign = crossProjectWriteRefusal([{ kind: "component", id: from }, { kind: "component", id: to }], "sdd_move_methods");
53140
+ if (foreign !== void 0) throw new Error(foreign);
51768
53141
  return moveMethods(from, to, methods, methodMoveGate(from), dryRun);
51769
53142
  }
53143
+ function moveSpec2(kind, id, subsystem, dryRun) {
53144
+ const foreign = crossProjectWriteRefusal(
53145
+ [{ kind, id }, ...subsystem ? [{ kind: "subsystem", id: subsystem }] : []],
53146
+ "sdd_move_spec"
53147
+ );
53148
+ if (foreign !== void 0) throw new Error(foreign);
53149
+ if (kind !== "component") return moveSpec(kind, id, subsystem, dryRun);
53150
+ const bound2 = candidateOptions();
53151
+ const plan6 = moveSpec(kind, id, subsystem, true);
53152
+ const moving = plan6.moved.filter((ref) => ref.kind === "component").map((ref) => loadSpec("component", ref.id)).filter((spec) => spec !== null).map((spec) => ({ ...spec, subsystem: plan6.to }));
53153
+ const [first, ...rest] = moving;
53154
+ const verdict = first ? introducedFindings("component", first, bound2, rest) : { errors: [], warnings: [], notices: [] };
53155
+ if (verdict.errors.length > 0) {
53156
+ throw new Error(
53157
+ `Refused: moving component "${id}" to subsystem "${plan6.to}" would introduce ${verdict.errors.length === 1 ? "an error" : `${verdict.errors.length} errors`} validate reports \u2014 a forbidden edge between two blocks, or an edge into another subsystem whose target is not a Portal:
53158
+ ${verdict.errors.map((e) => `- ${e.code}: ${e.message}`).join("\n")}
53159
+
53160
+ Nothing was written. Route each such dependency through a client Adapter calling the other subsystem's Portal, or move its collaborators with it.`
53161
+ );
53162
+ }
53163
+ const notices = noticesFrom(verdict);
53164
+ const report3 = dryRun ? plan6 : moveSpec(kind, id, subsystem, false);
53165
+ return { ...report3, notices: [...report3.notices, ...notices] };
53166
+ }
51770
53167
 
51771
53168
  // src/mcp/build.ts
51772
53169
  var fs26 = __toESM(require("fs"));
@@ -53659,17 +55056,24 @@ function boundNode(family, bound2) {
53659
55056
  function nodeAt(family, dir) {
53660
55057
  return family.nodes.find((n) => path45.resolve(n.directory) === path45.resolve(dir));
53661
55058
  }
55059
+ function memberLocation(boundDir, written) {
55060
+ const location = parseMemberSource(written);
55061
+ if (location.storage === "path" && location.problem === void 0) {
55062
+ const sibling = path45.resolve(boundDir, written);
55063
+ if (path45.relative(sibling, boundDir).startsWith("..")) return { dir: sibling };
55064
+ return { refused: `the path "${written}" names a directory holding {bound} itself (${boundDir}), never a member of it` };
55065
+ }
55066
+ const dir = resolve36(boundDir, written);
55067
+ if (dir !== null) return { dir };
55068
+ return { refused: `the path "${written}" does not resolve strictly within {bound} (${boundDir}), and is not a sibling written with a leading \`../\`${location.problem ? ` (${location.problem})` : ""}` };
55069
+ }
53662
55070
  function planAttach(family, bound2, request) {
53663
55071
  const plan6 = emptyPlan(family, request);
53664
55072
  const node = boundNode(family, bound2);
53665
55073
  const alias = request.alias ?? "";
53666
- const location = parseMemberSource(request.path ?? "");
53667
- const sibling = location.storage === "path" && location.problem === void 0 ? path45.resolve(node.directory, request.path ?? "") : null;
53668
- const holdsBound = sibling !== null && !path45.relative(sibling, node.directory).startsWith("..");
53669
- const dir = sibling !== null && !holdsBound ? sibling : resolve36(node.directory, request.path ?? "");
53670
- if (dir === null) {
53671
- refuse(plan6, "not-contained", bound2, holdsBound ? `the path "${request.path ?? ""}" names a directory holding ${label4(bound2)} itself (${node.directory}), never a member of it` : `the path "${request.path ?? ""}" does not resolve strictly within ${label4(bound2)} (${node.directory}), and is not a sibling written with a leading \`../\`${location.problem ? ` (${location.problem})` : ""}`);
53672
- }
55074
+ const located2 = memberLocation(node.directory, request.path ?? "");
55075
+ const dir = "dir" in located2 ? located2.dir : null;
55076
+ if ("refused" in located2) refuse(plan6, "not-contained", bound2, located2.refused.replace("{bound}", label4(bound2)));
53673
55077
  const config = configAt3(node.directory) ?? {};
53674
55078
  const held = ownGet(config.members, alias);
53675
55079
  const heldPath = held !== void 0 ? memberLocationOf(held) : void 0;
@@ -53830,9 +55234,12 @@ function planAdopt(family, bound2, request) {
53830
55234
  if (!external) refuse(plan6, "not-an-external", bound2, `${label4(bound2)} declares no external "${alias}"`);
53831
55235
  else if (request.relocation) return planRelocatedAdopt(plan6, family, bound2, alias, external, request.relocation);
53832
55236
  else {
53833
- dir = resolve36(boundDir, externalSourceOf2(external)?.path ?? "");
53834
- if (dir === null) refuse(plan6, "not-contained", bound2, `the external "${alias}" ${externalSourceOf2(external)?.path ? `is found at "${externalSourceOf2(external)?.path}", which is not strictly within ${label4(bound2)}` : "names no source.path"}`);
53835
- else if (nodeAt(family, dir)) refuse(plan6, "already-member", nodeAt(family, dir).namespace, `${dir} is already the family project ${label4(nodeAt(family, dir).namespace)}`);
55237
+ const written = externalSourceOf2(external)?.path;
55238
+ const located2 = written !== void 0 ? memberLocation(boundDir, written) : null;
55239
+ dir = located2 !== null && "dir" in located2 ? located2.dir : null;
55240
+ if (located2 === null) refuse(plan6, "not-contained", bound2, `the external "${alias}" names no source.path`);
55241
+ else if ("refused" in located2) refuse(plan6, "not-contained", bound2, `the external "${alias}" is found at "${written}": ${located2.refused.replace("{bound}", label4(bound2))}`);
55242
+ else if (nodeAt(family, located2.dir)) refuse(plan6, "already-member", nodeAt(family, located2.dir).namespace, `${located2.dir} is already the family project ${label4(nodeAt(family, located2.dir).namespace)}`);
53836
55243
  }
53837
55244
  if (plan6.refusals.length > 0) return plan6;
53838
55245
  return adoptEdits(plan6, family, bound2, alias, external, dir);
@@ -54259,8 +55666,7 @@ function planInternalize(family, bound2, request) {
54259
55666
  const alias = request.alias ?? "";
54260
55667
  const part = familyNode(family, bound2)?.parts.find((p) => p.alias === alias);
54261
55668
  if (part) return planPartInternalize(plan6, family, bound2, alias, part);
54262
- const sibling = familyNode(family, bound2)?.externals.find((e) => e.role === "member" && e.sourceKind === "path" && e.alias === alias);
54263
- if (sibling) return planSiblingInternalize(plan6, family, bound2, request);
55669
+ if (storedAsSibling2(family, bound2, alias)) return planSiblingInternalize(plan6, family, bound2, request);
54264
55670
  const member = memberUnder2(family, bound2, alias, plan6);
54265
55671
  if (!member) return plan6;
54266
55672
  const consumers = consumersOf(family, bound2, member);
@@ -54388,12 +55794,19 @@ function internalizeEdits(plan6, family, bound2, member, alias, consumers, desti
54388
55794
  );
54389
55795
  for (const c of repinned) {
54390
55796
  edit3(plan6, c.node.namespace, "pin", `pin ${c.alias} taken again against ${boundNode2.id ?? "the top project"}`, { root: c.node.directory, call: "pinExternals", args: [[c.alias]] });
55797
+ loopClosed(plan6, family, bound2, member, c);
54391
55798
  }
54392
55799
  if (carried.length > 0) edit3(plan6, bound2, "pin", `pin ${carried.join(", ")} (externals carried from ${alias})`, { root: boundNode2.directory, call: "pinExternals", args: [carried] });
54393
55800
  plan6.edits.push({ project: member.namespace, kind: "delete", detail: `${alias}'s lock, pins and derived outputs have no home: deleted, each listed` });
54394
55801
  plan6.notes.push(`Consumers outside the family that name "${member.id ?? alias}" by path are not found: each must repoint its own external at ${boundNode2.id ?? "this project"}.`);
54395
55802
  if (member.id !== void 0 && approvalRecord(member.directory)) plan6.notes.push(`${label6(member.namespace)}'s approval ends with it: its lock is deleted, and ${label6(bound2)} is re-locked instead.`);
54396
55803
  }
55804
+ function loopClosed(plan6, family, bound2, member, c) {
55805
+ const uses = family.references.filter((r) => (r.consumer === bound2 || r.consumer === member.namespace) && r.producer === c.node.namespace);
55806
+ if (uses.length === 0) return;
55807
+ const named2 = [...new Set(uses.map((r) => `"${r.authored}"`))].slice(0, 3).join(", ");
55808
+ plan6.notes.push(`${label6(c.node.namespace)} is re-pointed at ${label6(bound2)}, which uses it (${named2}${uses.length > 3 ? ", \u2026" : ""}): the two projects depend on each other in a loop after the internalize \u2014 PROJECT_DEPENDENCY_CYCLE. To keep them apart, move what ${label6(c.node.namespace)} uses of "${member.mountAlias ?? member.namespace}" into ${label6(c.node.namespace)} itself, or demote ${label6(c.node.namespace)} into a part of ${label6(bound2)} first.`);
55809
+ }
54397
55810
  function respellConsumer(plan6, family, c, own2) {
54398
55811
  const seen = /* @__PURE__ */ new Set();
54399
55812
  for (const ref of family.authoredReferences) {
@@ -54531,6 +55944,11 @@ function plan3(family, bound2, request) {
54531
55944
  throw new Error(`boundary_migration plans no "${request.verb}": internalize | externalize | promote | demote`);
54532
55945
  }
54533
55946
  }
55947
+ function storedAsSibling2(family, bound2, alias) {
55948
+ const held = ownGet(configAt5(familyNode(family, bound2).directory)?.members, alias);
55949
+ const location = held === void 0 ? void 0 : memberLocationOf(held);
55950
+ return location !== void 0 && parseMemberSource(location).storage === "path";
55951
+ }
54534
55952
  function insideFamily(plan6, dir) {
54535
55953
  const rel2 = path47.relative(plan6.familyRoot, dir);
54536
55954
  return rel2 !== "" && !rel2.startsWith("..") && !path47.isAbsolute(rel2);
@@ -54595,7 +56013,6 @@ function planPromote(family, bound2, request) {
54595
56013
  { root: node.directory, call: "promoteMember", args: [alias, newId] }
54596
56014
  );
54597
56015
  edit3(plan6, newId, "pin", `pin the new project's external for ${node.id ?? "this project"} (when its specs reference it)`, { root: part.directory, call: "pinExternals", args: [[]] });
54598
- if (part.storage !== "contained") edit3(plan6, bound2, "pin", `pin ${alias} (a project stored outside ${label6(bound2)} is judged against its pin)`, { root: node.directory, call: "pinExternals", args: [[alias]] });
54599
56016
  repinIntactConsumers2(plan6, family, bound2, [part.directory]);
54600
56017
  plan6.notes.push(`"${newId}" has no lock yet: lock it at its own root (\`wairon lock\`) once it validates.`);
54601
56018
  return plan6;
@@ -54617,7 +56034,7 @@ function planDemote(family, bound2, request) {
54617
56034
  if (referenced?.sourceKind === "git") refuse3(plan6, "read-only", bound2, `the member "${alias}" is fetched from git: demote it in a checkout of its repository`);
54618
56035
  if (referenced?.sourceKind === "hosted") refuse3(plan6, "read-only", bound2, `the member "${alias}" is a hosted record: a part lives in its parent's tree`);
54619
56036
  const dir = member?.directory ?? referenced?.directory;
54620
- if (!member && referenced?.sourceKind === "path" && dir !== void 0) siblingRefused(plan6, bound2, alias, dir);
56037
+ if (dir !== void 0 && (storedAsSibling2(family, bound2, alias) || referenced?.sourceKind === "path")) siblingRefused(plan6, bound2, alias, dir);
54621
56038
  if (member) {
54622
56039
  const users = /* @__PURE__ */ new Map();
54623
56040
  for (const ref of family.references) {
@@ -54656,7 +56073,7 @@ function write5(plan6, rehearsal) {
54656
56073
  const [kind, id] = spec.split("\n");
54657
56074
  runWithProjectRoot(rehearsalRoot(rehearsal, plan6.familyRoot), () => rewriteReferences2(kind, id, edits));
54658
56075
  }
54659
- for (const e of planned.filter((x) => x.write.call === "internalizeMember")) writeOne3(rehearsal, e.write);
56076
+ for (const e of planned.filter((x) => x.write.call === "internalizeMember")) recordFolded(plan6, e.project, writeOne3(rehearsal, e.write));
54660
56077
  for (const e of planned.filter((x) => ["importNames", "unpin", "removeExternal"].includes(x.write.call))) writeOne3(rehearsal, e.write);
54661
56078
  for (const e of planned.filter((x) => x.write.call === "repointExternal")) writeOne3(rehearsal, e.write);
54662
56079
  for (const e of planned.filter((x) => x.write.call === "pinExternals")) writeOne3(rehearsal, e.write);
@@ -54675,7 +56092,7 @@ function write5(plan6, rehearsal) {
54675
56092
  for (const e of planned.filter((x) => x.write.call === "demoteMember")) {
54676
56093
  const demoted = writeOne3(rehearsal, e.write);
54677
56094
  for (const line of demoted?.consumersRestored ?? []) plan6.edits.push({ project: e.project, kind: "reference", detail: `consumers across the old boundary restored: ${line}` });
54678
- for (const line of demoted?.signaturesRederived ?? []) plan6.edits.push({ project: e.project, kind: "reference", detail: `signature text re-derived \u2014 ${line}` });
56095
+ recordFolded(plan6, e.project, demoted);
54679
56096
  }
54680
56097
  for (const e of planned.filter((x) => x.write.call === "unpin")) writeOne3(rehearsal, e.write);
54681
56098
  }
@@ -54724,6 +56141,7 @@ function recordCrossing(plan6, bound2, member, result) {
54724
56141
  }
54725
56142
  for (const line of crossed.exported) plan6.edits.push({ project: bound2, kind: "export", detail: `export ${line}` });
54726
56143
  for (const line of crossed.imported) plan6.edits.push({ project: bound2, kind: "external", detail: `import: ${line}` });
56144
+ for (const line of crossed.externalsDeclared ?? []) plan6.edits.push({ project: member, kind: "external", detail: `externals: ${line} \u2014 a project its moved specs name` });
54727
56145
  for (const line of crossed.consumersDropped) plan6.edits.push({ project: bound2, kind: "reference", detail: `consumers across the boundary to ${member} dropped: ${line}` });
54728
56146
  for (const line of crossed.pathsRebased ?? []) plan6.edits.push({ project: member, kind: "reference", detail: `path re-expressed under the new root \u2014 ${line}` });
54729
56147
  for (const line of crossed.signaturesRederived ?? []) plan6.edits.push({ project: bound2, kind: "reference", detail: `signature text re-derived \u2014 ${line}` });
@@ -54734,6 +56152,12 @@ function recordCrossing(plan6, bound2, member, result) {
54734
56152
  for (const line of topics) plan6.notes.push(` ${line}`);
54735
56153
  }
54736
56154
  }
56155
+ function recordFolded(plan6, project3, result) {
56156
+ const folded = result;
56157
+ if (!folded) return;
56158
+ for (const line of folded.signaturesRederived ?? []) plan6.edits.push({ project: project3, kind: "reference", detail: `signature text re-derived \u2014 ${line}` });
56159
+ for (const line of folded.notCarried) if (!plan6.notes.includes(`not carried: ${line}`)) plan6.notes.push(`not carried: ${line}`);
56160
+ }
54737
56161
  function writeOne3(rehearsal, w) {
54738
56162
  const args = w.args;
54739
56163
  return runWithProjectRoot(rehearsalRoot(rehearsal, w.root), () => {
@@ -55407,10 +56831,14 @@ function newFindings(plan6, rehearsal, scope, changes) {
55407
56831
  if (owners.length === 0) return [];
55408
56832
  const run3 = (copy) => {
55409
56833
  try {
55410
- return owners.flatMap((owner) => runWithProjectRoot(rehearsalRoot(copy, owner), () => {
55411
- const codeCodes = new Set(listRules2().filter((r) => r.judges === "code").flatMap((r) => r.codes.map((c) => c.code)));
55412
- return validateFamily({ memberDepth: 0 }).issues.filter((i) => (i.severity === "error" || i.severity === "warning") && !codeCodes.has(i.code)).map((issue2) => ({ owner, issue: issue2 }));
55413
- }));
56834
+ const shape = familyShape(plan6, copy);
56835
+ return [
56836
+ ...owners.filter((owner) => shape.roots.has(path49.resolve(rehearsalRoot(copy, owner)))).flatMap((owner) => runWithProjectRoot(rehearsalRoot(copy, owner), () => {
56837
+ const codeCodes = new Set(listRules2().filter((r) => r.judges === "code").flatMap((r) => r.codes.map((c) => c.code)));
56838
+ return validateFamily({ memberDepth: 0 }).issues.filter((i) => (i.severity === "error" || i.severity === "warning") && !codeCodes.has(i.code)).map((issue2) => ({ owner, issue: issue2 }));
56839
+ })),
56840
+ ...shape.loops.map((loop) => ({ owner: `loop ${loop.where}`, issue: loop.issue }))
56841
+ ];
55414
56842
  } catch {
55415
56843
  return null;
55416
56844
  }
@@ -55448,6 +56876,28 @@ function newFindings(plan6, rehearsal, scope, changes) {
55448
56876
  }
55449
56877
  return notes;
55450
56878
  }
56879
+ function familyShape(plan6, copy) {
56880
+ const top = rehearsalRoot(copy, plan6.familyRoot);
56881
+ const family = runWithProjectRoot(top, () => graph());
56882
+ const roots = new Set(family.nodes.map((n) => path49.resolve(n.directory)));
56883
+ const label7 = (key2) => {
56884
+ const node = family.nodes.find((n) => n.namespace === key2);
56885
+ return `"${node?.id ?? node?.name ?? key2}"${key2 === "" ? "" : ` (member "${node?.mountAlias ?? key2}")`}`;
56886
+ };
56887
+ const loops = dependencyCycles(family).map((loop) => {
56888
+ const where = [...new Set(loop)].map((k) => posixPath(path49.relative(top, family.nodes.find((n) => n.namespace === k)?.directory ?? top))).sort().join(",");
56889
+ const issue2 = {
56890
+ severity: "warning",
56891
+ code: "PROJECT_DEPENDENCY_CYCLE",
56892
+ message: `Projects ${loop.map(label7).join(" -> ")} depend on each other in a loop, so neither can be approved or released before the other`
56893
+ };
56894
+ return { where, issue: issue2 };
56895
+ });
56896
+ return { roots, loops };
56897
+ }
56898
+ function posixPath(p) {
56899
+ return p.split(path49.sep).join("/");
56900
+ }
55451
56901
  function movedOf(plan6) {
55452
56902
  const relocation = plan6.request.relocation;
55453
56903
  const move2 = plan6.edits.find((e) => e.write?.call === "move");
@@ -55511,9 +56961,7 @@ function partDirectories(plan6, family) {
55511
56961
  return rel2 !== "" && !rel2.startsWith("..") && !path49.isAbsolute(rel2);
55512
56962
  };
55513
56963
  const subject = plan6.request.alias;
55514
- const parts = family.nodes.flatMap((n) => n.parts.filter((p) => p.storage !== "git" && p.directory !== void 0 && (inside(p.directory) || p.alias === subject)).map((p) => p.directory));
55515
- const siblings = family.nodes.flatMap((n) => n.externals.filter((e) => e.role === "member" && e.sourceKind === "path" && e.directory !== void 0 && e.alias === subject).map((e) => e.directory));
55516
- return [...parts, ...siblings];
56964
+ return family.nodes.flatMap((n) => n.parts.filter((p) => p.storage !== "git" && p.directory !== void 0 && (inside(p.directory) || p.alias === subject)).map((p) => p.directory));
55517
56965
  }
55518
56966
  function broughtIn(plan6, family) {
55519
56967
  const known = new Set(family.nodes.map((n) => path49.resolve(n.directory)));
@@ -55673,11 +57121,11 @@ function statusFamilyContext() {
55673
57121
  const root = getProjectRoot();
55674
57122
  const members = listDirectChainedSubprojects(root);
55675
57123
  if (members.length > 0) {
55676
- const described = members.map((m) => {
57124
+ const described2 = members.map((m) => {
55677
57125
  const rel2 = path51.relative(root, m.dir).split(path51.sep).join("/") || ".";
55678
57126
  return `${m.alias} (${rel2}${m.form === "mount" ? ", legacy L1 mount" : ""})`;
55679
57127
  });
55680
- lines.push(`Family: member projects declared here \u2014 ${described.join(", ")}.`);
57128
+ lines.push(`Family: member projects declared here \u2014 ${described2.join(", ")}.`);
55681
57129
  }
55682
57130
  } catch {
55683
57131
  }
@@ -55959,9 +57407,24 @@ function renderChangeReport(report3) {
55959
57407
  lines.push("", `DROPPED FROM THE STORED FILE (${report3.dryRun ? "would be " : ""}removed \u2014 the schema does not know them):`, ...report3.strippedKeys.map((k) => `- ${k}`));
55960
57408
  }
55961
57409
  lines.push(...renderRespellings(report3.respellings));
57410
+ lines.push(...renderBreaks(report3.published ?? [], report3.breaks ?? [], report3.dryRun));
55962
57411
  lines.push(...renderTestsToRevisit(report3.testsToRevisit));
55963
57412
  return lines.join("\n");
55964
57413
  }
57414
+ function renderBreaks(published, breaks, dryRun) {
57415
+ if (published.length === 0) return [];
57416
+ const lines = ["", `PUBLISHED as: ${published.map((u) => `${u.publicName}${u.members.length ? ` (${u.members.join(", ")})` : ""}`).join("; ")} \u2014 a consumer of these ${dryRun ? "would lose" : "lost"} what goes.`];
57417
+ if (breaks.length === 0) {
57418
+ lines.push("No consumer in reach uses it (the family, and the searched folders when `search` names any).");
57419
+ return lines;
57420
+ }
57421
+ lines.push(`BREAKS ${breaks.length} consumer(s), whose specs still reach it:`);
57422
+ for (const c of breaks) {
57423
+ const uses = (c.uses ?? []).map((u) => `${u.publicName}${u.members.length ? `.${u.members.join("/")}` : ""}${u.specs?.length ? ` in ${u.specs.join(", ")}` : ""}`);
57424
+ lines.push(`- ${c.project} (${c.section}.${c.alias})${c.found === "search" ? ` [${c.directory}]` : ""}${uses.length ? `: ${uses.join("; ")}` : ""}`);
57425
+ }
57426
+ return lines;
57427
+ }
55965
57428
  function renderDeletion(deletion) {
55966
57429
  const others = deletion.removed.filter((r) => !(r.kind === deletion.kind && r.id === deletion.id));
55967
57430
  const lines = [deletion.dryRun ? `DRY RUN \u2014 nothing was deleted. Deleting ${deletion.kind} "${deletion.id}" would remove ${deletion.removed.length} spec${deletion.removed.length === 1 ? "" : "s"}.` : `Successfully deleted ${deletion.kind} spec "${deletion.id}"${others.length ? ` and ${others.length} spec${others.length === 1 ? "" : "s"} it contained` : ""}.`];
@@ -55970,6 +57433,7 @@ function renderDeletion(deletion) {
55970
57433
  lines.push("", deletion.dryRun ? "STILL REFERENCED \u2014 the deletion would be refused unless forced (force: true leaves these dangling):" : "LEFT DANGLING (forced) \u2014 validate reports each:");
55971
57434
  lines.push(...deletion.references.map((r) => `- ${r.kind} "${r.id}" (${r.position}) -> ${r.target.kind} "${r.target.id}"`));
55972
57435
  }
57436
+ lines.push(...renderBreaks(deletion.published, deletion.breaks ?? [], deletion.dryRun));
55973
57437
  lines.push(...renderTestsToRevisit(deletion.testsToRevisit));
55974
57438
  return lines.join("\n");
55975
57439
  }
@@ -56020,6 +57484,25 @@ function renderRespellings(entries) {
56020
57484
  if (!entries.length) return [];
56021
57485
  return ["", "RESPELLED (stored canonical):", ...entries.map((r) => `- ${r.path}: "${r.written}" \u2192 "${r.stored}"`)];
56022
57486
  }
57487
+ var exportUseOutput = import_zod13.z.object({
57488
+ publicName: import_zod13.z.string(),
57489
+ kind: import_zod13.z.string(),
57490
+ members: import_zod13.z.array(import_zod13.z.string()).describe("The contract methods (or `type`) a consumer reaches on it that the removal takes away."),
57491
+ specs: import_zod13.z.array(import_zod13.z.string()).optional(),
57492
+ formerNames: import_zod13.z.array(import_zod13.z.string()).optional(),
57493
+ formerMembers: import_zod13.z.record(import_zod13.z.array(import_zod13.z.string())).optional()
57494
+ });
57495
+ var breakingConsumerOutput = import_zod13.z.object({
57496
+ project: import_zod13.z.string(),
57497
+ key: import_zod13.z.string(),
57498
+ directory: import_zod13.z.string(),
57499
+ alias: import_zod13.z.string(),
57500
+ section: import_zod13.z.enum(["externals", "members"]),
57501
+ names: import_zod13.z.array(import_zod13.z.string()),
57502
+ broken: import_zod13.z.array(import_zod13.z.string()).optional(),
57503
+ found: import_zod13.z.enum(["search"]).optional(),
57504
+ uses: import_zod13.z.array(exportUseOutput).optional()
57505
+ });
56023
57506
  var specWriteReceiptOutput = {
56024
57507
  kind: import_zod13.z.enum(SPEC_KINDS).describe("The spec kind written."),
56025
57508
  id: import_zod13.z.string().describe('The id the spec is stored under; "system" for the L0 singleton.'),
@@ -56037,6 +57520,10 @@ var specWriteReceiptOutput = {
56037
57520
  "Each type position the write normalised: what the input wrote against the canonical spelling stored. Aliases are accepted, never refused, and this list is how an author learns the canonical form. Empty when every position was already canonical."
56038
57521
  ),
56039
57522
  testsToRevisit: testsToRevisitOutput,
57523
+ dryRun: import_zod13.z.boolean().optional().describe("Present, true, on a dry run: every judgement ran and nothing was written."),
57524
+ cascaded: import_zod13.z.array(import_zod13.z.string()).optional().describe(
57525
+ "Each implementation method entry a contract restatement removed with a method it dropped, as `<implementation>.<method>`."
57526
+ ),
56040
57527
  ...staleServerOutput
56041
57528
  };
56042
57529
  function writeReceipt(sentence, receipt) {
@@ -56058,6 +57545,12 @@ var specDeletionOutput = {
56058
57545
  removed: import_zod13.z.array(import_zod13.z.object({ kind: import_zod13.z.string(), id: import_zod13.z.string() })).describe("Every spec the deletion removes (or would), innermost first and the addressed spec last."),
56059
57546
  references: import_zod13.z.array(import_zod13.z.object({ kind: import_zod13.z.string(), id: import_zod13.z.string(), position: import_zod13.z.string(), target: import_zod13.z.object({ kind: import_zod13.z.string(), id: import_zod13.z.string() }) })).describe("Every reference a spec outside the removed set holds to it: why a deletion is refused, or what a forced one left dangling."),
56060
57547
  testsToRevisit: testsToRevisitOutput,
57548
+ published: import_zod13.z.array(exportUseOutput).describe(
57549
+ "Every public name of this project a consumer reaches what the deletion removes through, read before anything goes. Empty when nothing removed is published."
57550
+ ),
57551
+ breaks: import_zod13.z.array(breakingConsumerOutput).optional().describe(
57552
+ "The consumers in reach (family members included, and checkouts in the searched folders) whose specs reach a published name on a member the deletion takes \u2014 the projects it breaks. Absent when none breaks."
57553
+ ),
56061
57554
  ...staleServerOutput
56062
57555
  };
56063
57556
  var specChangeOutput = {
@@ -56090,6 +57583,15 @@ var specChangeReportOutput = {
56090
57583
  strippedKeys: import_zod13.z.array(import_zod13.z.string()).optional().describe(
56091
57584
  "Keys the stored file carries that the level's schema does not know, each as `path: value`, which this write drops (or, on a dry run, would drop). Absent when there are none."
56092
57585
  ),
57586
+ cascaded: import_zod13.z.array(import_zod13.z.string()).optional().describe(
57587
+ "Each implementation method entry removed with a contract method the delta removed, as `<implementation>.<method>` (on a dry run, would be)."
57588
+ ),
57589
+ published: import_zod13.z.array(exportUseOutput).optional().describe(
57590
+ "Every public name a consumer reaches a removed contract method through. Absent when no removed method is published."
57591
+ ),
57592
+ breaks: import_zod13.z.array(breakingConsumerOutput).optional().describe(
57593
+ "The consumers in reach whose specs call a removed method on a published name \u2014 the projects the removal breaks."
57594
+ ),
56093
57595
  ...staleServerOutput
56094
57596
  };
56095
57597
  function statusNetworkLine() {
@@ -56454,6 +57956,8 @@ var SPEC_WRITE_TOOLS = /* @__PURE__ */ new Set([
56454
57956
  "sdd_rename_type",
56455
57957
  "sdd_rename_field",
56456
57958
  "sdd_rename_param",
57959
+ "sdd_rename_spec",
57960
+ "sdd_move_spec",
56457
57961
  "sdd_add_component",
56458
57962
  "sdd_define_interface",
56459
57963
  "sdd_set_endpoints",
@@ -56464,6 +57968,11 @@ var SPEC_WRITE_TOOLS = /* @__PURE__ */ new Set([
56464
57968
  "sdd_move_methods"
56465
57969
  ]);
56466
57970
  var listChangedEmitters = /* @__PURE__ */ new WeakMap();
57971
+ function withArgumentNames(description, inputSchema) {
57972
+ const names = Object.entries(inputSchema).map(([name, schema]) => schema.isOptional() ? `${name}?` : name);
57973
+ if (names.length === 0) return description;
57974
+ return `${description.trimEnd()} Arguments: ${names.join(", ")}.`;
57975
+ }
56467
57976
  function reg(server, name, config, cb) {
56468
57977
  const guarded2 = (args) => {
56469
57978
  const freshness = assessBuildFreshness(serverBuildStamps.get(server) ?? null);
@@ -56475,7 +57984,7 @@ function reg(server, name, config, cb) {
56475
57984
  if (result.isError !== true && SPEC_WRITE_TOOLS.has(name)) listChangedEmitters.get(server)?.();
56476
57985
  return result;
56477
57986
  };
56478
- const strictConfig = config.inputSchema ? { ...config, inputSchema: import_zod13.z.object(config.inputSchema).strict() } : config;
57987
+ const strictConfig = config.inputSchema ? { ...config, description: withArgumentNames(config.description, config.inputSchema), inputSchema: import_zod13.z.object(config.inputSchema).strict() } : config;
56479
57988
  server.registerTool(name, strictConfig, guarded2);
56480
57989
  }
56481
57990
  var statusInput = import_zod13.z.enum(STATUS_VALUES).optional().describe(
@@ -56933,7 +58442,7 @@ ${renderChangeReport(report3)}`,
56933
58442
  server,
56934
58443
  "sdd_adopt_member",
56935
58444
  {
56936
- description: "Make the bound project's external a member \u2014 sdd_detach_member's inverse. Locally the external is found by a path inside the bound project and adopted where it is; on a hosted instance it names a project by source.hosted and `path` says where to adopt it (the hosting server relocates it there). All-or-nothing; dryRun answers the plan and writes nothing. Never locks.",
58445
+ description: "Make the bound project's external a member \u2014 sdd_detach_member's inverse. Locally the external is found by a path inside the bound project or a `../` sibling checkout, and adopted where it is; on a hosted instance it names a project by source.hosted and `path` says where to adopt it (the hosting server relocates it there). All-or-nothing; dryRun answers the plan and writes nothing. Never locks.",
56937
58446
  inputSchema: {
56938
58447
  alias: import_zod13.z.string().describe("The external's alias"),
56939
58448
  path: import_zod13.z.string().optional().describe("Hosted: the member path to adopt a source.hosted external at, relative to the bound project; a source.path external is adopted where it is"),
@@ -56946,7 +58455,7 @@ ${renderChangeReport(report3)}`,
56946
58455
  server,
56947
58456
  "sdd_rename_component",
56948
58457
  {
56949
- description: "Rename a component and rewrite every reference to it in the bound tree: dependsOn, owns and dispatch entries, lifecycle entrypoints, published interfaces at L1 and L0, narrative call/dispatch/register targets, an interface's component, an implementation's contract and an entity's componentClass. The interface named i<id> and the implementation named <id>_impl move to the new ids and files; interfaces and implementations named otherwise keep their ids. Findings keyed on the component follow it too: every lint allow naming it on an edge site or in a covered unit, and every entry of the debt register (.wai/project.yaml rules.conformance.carried) anchored on a renamed spec or naming the component in its site or units \u2014 rewritten in place, every comment and all other formatting of the register kept, so unchanged debt never reads as paid and new at once. A register that cannot be rewritten that precisely is refused before the first write. Display names are left as they are. Refuses, writing nothing: a component that does not exist (component-missing), one inside a chained subproject (chained-component \u2014 rename it from that project's own root), a new id that is not a lowercase identifier without a namespace separator (invalid-id), and a new id \u2014 or i<newId> or <newId>_impl \u2014 already in use (id-taken). With dryRun it answers what it would move, rewrite, keep and rekey, and writes nothing. Returns the specs moved, each with its old and new id, the ids of the other specs rewritten (lint allows included), and `carried`: each register edit \u2014 the entry by code, spec and site as it stood, the key changed (spec, at or covers), from and to.",
58458
+ description: "Rename a component and rewrite every reference to it in the bound tree: dependsOn, owns and dispatch entries, lifecycle entrypoints, published interfaces at L1 and L0, narrative call/dispatch/register targets, an interface's component, an implementation's contract and an entity's componentClass. The interface named i<id> and the implementation named <id>_impl move to the new ids and files; interfaces and implementations named otherwise keep their ids, and their files follow the component's folder (rename one of those on its own with sdd_rename_spec). Findings keyed on the component follow it too: every lint allow naming it on an edge site or in a covered unit, and every entry of the debt register (.wai/project.yaml rules.conformance.carried) anchored on a renamed spec or naming the component in its site or units \u2014 rewritten in place, every comment and all other formatting of the register kept, so unchanged debt never reads as paid and new at once. A register that cannot be rewritten that precisely is refused before the first write. Display names are left as they are. Refuses, writing nothing: a component that does not exist (component-missing), one inside a chained subproject (chained-component \u2014 rename it from that project's own root), a new id that is not a lowercase identifier without a namespace separator (invalid-id), and a new id \u2014 or i<newId> or <newId>_impl \u2014 already in use (id-taken). With dryRun it answers what it would move, rewrite, keep and rekey, and writes nothing. Returns the specs moved, each with its old and new id, the ids of the other specs rewritten (lint allows included), and `carried`: each register edit \u2014 the entry by code, spec and site as it stood, the key changed (spec, at or covers), from and to.",
56950
58459
  inputSchema: {
56951
58460
  id: import_zod13.z.string().describe("The component to rename (namespaced if needed)"),
56952
58461
  newId: import_zod13.z.string().describe("Its new id: a lowercase identifier with no namespace separator"),
@@ -56980,7 +58489,8 @@ ${renderChangeReport(report3)}`,
56980
58489
  const root = getProjectRoot();
56981
58490
  const consumers = listConsumers3(search?.map((d) => path51.resolve(root, d)));
56982
58491
  const report3 = renameMethod(qualifiedComponentId(id), method2, newName, pinSymbol, dryRun);
56983
- const breaks = report3.publishedIn.length > 0 ? consumers.filter((c) => consumerReaches(c, report3.publishedIn, method2)).map((c) => narrowedToMember(c, report3.publishedIn, method2)) : [];
58492
+ const published = report3.publishedIn.map((publicName) => ({ publicName, kind: "component", members: [method2] }));
58493
+ const breaks = consumers.map((c) => narrowedToUses(c, published)).filter((c) => c !== null);
56984
58494
  return json({ ...report3, ...breaks.length > 0 ? { breaks } : {} });
56985
58495
  } catch (e) {
56986
58496
  return errText(String(e));
@@ -56991,18 +58501,22 @@ ${renderChangeReport(report3)}`,
56991
58501
  server,
56992
58502
  "sdd_rename_type",
56993
58503
  {
56994
- description: "Rename a type and rewrite every reference to it in the bound tree: every type position naming it (contract and type-method params and returns, fields, signature-type params and returns), a signatureFrom naming it as a signature type, and an L1 or L0 export entry's typeDef. Only the named members of a type expression that name this type are respelled, in the style they were written (`Invoice` \u2192 `Bill`, `invoice` \u2192 `bill`); the rest of the text stays as written. The type moves to the new id and file under the same owner, and its old id joins its rename trace (previousIds) so a consumer of the design export reads the rename as a rename; an export entry that derived its public name from the old id gets `as: <that name>` first, so the published name and every consumer's pin stay. Rename traces are never rewritten. Findings keyed on the type follow it: lint allows naming it, and every debt-register entry anchored on it, rewritten in place (a register that cannot be rewritten that precisely is refused before the first write). The display name is left as it is. Refuses, writing nothing: a type that does not exist (type-missing), one in another project (chained-type), a new id that is not a lowercase identifier (invalid-id), and a new id another type of the same owner holds (id-taken) or retired (id-retired \u2014 unsetting the holder's previousIds releases it). With dryRun it answers what it would rewrite, keep and rekey, and writes nothing. Returns from and to, the specs rewritten, keptPublicNames and `carried`: each register edit.",
58504
+ description: "Rename a type and rewrite every reference to it in the bound tree: every type position naming it (contract and type-method params and returns, fields, signature-type params and returns), a signatureFrom naming it as a signature type, and an L1 or L0 export entry's typeDef. Only the named members of a type expression that name this type are respelled, in the style they were written (`Invoice` \u2192 `Bill`, `invoice` \u2192 `bill`); the rest of the text stays as written. The type moves to the new id and file under the same owner, and its old id joins its rename trace (previousIds) so a consumer of the design export reads the rename as a rename; an export entry that derived its public name from the old id gets `as: <that name>` first, so the published name and every consumer's pin stay. Rename traces are never rewritten. Findings keyed on the type follow it: lint allows naming it, and every debt-register entry anchored on it, rewritten in place (a register that cannot be rewritten that precisely is refused before the first write). The display name is left as it is. Refuses, writing nothing: a type that does not exist (type-missing), one in another project (chained-type), a new id that is not a lowercase identifier (invalid-id), and a new id another type of the same owner holds (id-taken) or retired (id-retired \u2014 unsetting the holder's previousIds releases it). With dryRun it answers what it would rewrite, keep and rekey, and writes nothing. Returns from and to, the specs rewritten, keptPublicNames, `carried`: each register edit, `publishedIn` \u2014 every public name a consumer reaches the type through (an exported type that is or embeds it, member `type`; an exported contract whose methods name one in their signature closure, those methods) \u2014 and `breaks`: the consumers in reach (family members included, and checkouts in the `search` folders) whose specs reach one of those, a contract they depend on as a whole included, narrowed to what carries the type \u2014 as sdd_rename_field and sdd_rename_param name theirs.",
56995
58505
  inputSchema: {
56996
58506
  id: import_zod13.z.string().describe("The type to rename (qualified by its owning subsystem if needed, `subsystem::id`)"),
56997
58507
  newId: import_zod13.z.string().describe("Its new id: a lowercase identifier with no namespace separator"),
56998
- dryRun: import_zod13.z.boolean().optional().describe("Answer what the rename would do \u2014 refused exactly as the real rename would be \u2014 and write nothing")
58508
+ dryRun: import_zod13.z.boolean().optional().describe("Answer what the rename would do \u2014 refused exactly as the real rename would be \u2014 and write nothing"),
58509
+ search: import_zod13.z.array(import_zod13.z.string()).optional().describe("Folders to scan for consumer checkouts outside the family (e.g. the folder holding sibling checkouts), relative to the bound project root or absolute")
56999
58510
  }
57000
58511
  },
57001
- ({ id, newId, dryRun }) => {
58512
+ ({ id, newId, dryRun, search }) => {
57002
58513
  try {
57003
58514
  const bare2 = id.startsWith("::") ? id.slice(2) : id;
57004
58515
  const resolved = loadTypeSpec(bare2) ? bare2 : id;
57005
- return json(renameType(resolved, newId, dryRun));
58516
+ const consumers = listConsumers3(search?.map((d) => path51.resolve(getProjectRoot(), d)));
58517
+ const report3 = renameType(resolved, newId, dryRun);
58518
+ const breaks = consumers.map((c) => narrowedToUses(c, report3.publishedIn)).filter((c) => c !== null);
58519
+ return json({ ...report3, ...breaks.length > 0 ? { breaks } : {} });
57006
58520
  } catch (e) {
57007
58521
  return errText(String(e));
57008
58522
  }
@@ -57083,6 +58597,46 @@ ${renderChangeReport(report3)}`,
57083
58597
  }
57084
58598
  }
57085
58599
  );
58600
+ reg(
58601
+ server,
58602
+ "sdd_rename_spec",
58603
+ {
58604
+ description: "Rename ONE contract (kind interface) or ONE implementation on its own \u2014 the ids sdd_rename_component keeps when they are not derived from the component's (i<id>, <id>_impl) \u2014 and rewrite every reference to it in the bound tree: every implementation's contract and every L1 and L0 export entry's interface (an entry whose public name derived from the old id gets `as: <that name>` first, so the published surface and every consumer's pin stay), lint allows and debt-register entries keyed on it. The spec moves to its new id and file beside its owner, its old id in its rename trace (previousIds). Refuses, writing nothing: another kind (invalid-kind, naming the tool that renames it), a spec that does not exist (spec-missing) or lives in another project (chained-spec), a new id outside the grammar \u2014 a contract id begins with i \u2014 (invalid-id), and one taken or retired (id-taken, id-retired). With dryRun it answers what it would move, rewrite and keep, and writes nothing.",
58605
+ inputSchema: {
58606
+ kind: import_zod13.z.enum(["interface", "implementation"]).describe("interface (a contract) or implementation"),
58607
+ id: import_zod13.z.string().describe("The spec to rename"),
58608
+ newId: import_zod13.z.string().describe("Its new id: a lowercase identifier with no namespace separator (a contract's begins with i)"),
58609
+ dryRun: import_zod13.z.boolean().optional().describe("Answer what the rename would do \u2014 refused exactly as the real rename would be \u2014 and write nothing")
58610
+ }
58611
+ },
58612
+ ({ kind, id, newId, dryRun }) => {
58613
+ try {
58614
+ return json(renameSpecId(kind, id, newId, dryRun));
58615
+ } catch (e) {
58616
+ return errText(String(e));
58617
+ }
58618
+ }
58619
+ );
58620
+ reg(
58621
+ server,
58622
+ "sdd_move_spec",
58623
+ {
58624
+ description: "Move a component or a type to another subsystem of the bound project with everything that follows it. A component takes the members it owns, its contracts and their implementations (their files move to the new subsystem's folder in the nested layout); ids never change, so references by id stay, and what names the OLD subsystem follows: its export entries and lifecycle entrypoints for the moved components, and an L0 re-export from it. A type takes its file, its L1 export entries and every reference qualified by the old subsystem (`old::type` becomes `new::type`); omit subsystem to make a type system-level. Gated like every write: a move that would create a forbidden doctrine edge, or a cross-subsystem edge into a non-Portal, is refused with the rule's own words, and a curable boundary finding (a direct cross-subsystem edge into a Portal, which a client Adapter or a trustedLink would license) is named among the notices. Refuses, writing nothing: a kind other than component or type, a spec that does not exist or lives in another project, a subsystem the bound tree does not have or the one it already lives in, a component a pattern owns (move its owner), a type id the target already holds. With dryRun it answers the plan and the notices, and writes nothing.",
58625
+ inputSchema: {
58626
+ kind: import_zod13.z.enum(["component", "type"]).describe("component or type"),
58627
+ id: import_zod13.z.string().describe("The spec to move (a type by its bare id, or as <subsystem>::<id>)"),
58628
+ subsystem: import_zod13.z.string().optional().describe("The subsystem it moves to; omitted for a type made system-level"),
58629
+ dryRun: import_zod13.z.boolean().optional().describe("Answer what the move would do \u2014 refused exactly as the move would be \u2014 and write nothing")
58630
+ }
58631
+ },
58632
+ ({ kind, id, subsystem, dryRun }) => {
58633
+ try {
58634
+ return json(moveSpec2(kind, id, subsystem, dryRun));
58635
+ } catch (e) {
58636
+ return errText(String(e));
58637
+ }
58638
+ }
58639
+ );
57086
58640
  const componentInput = {
57087
58641
  id: import_zod13.z.string().describe("Lowercase identifier for the component"),
57088
58642
  name: import_zod13.z.string().describe("Human-readable display name"),
@@ -57254,7 +58808,7 @@ ${renderChangeReport(report3)}`,
57254
58808
  server,
57255
58809
  "sdd_set_endpoints",
57256
58810
  {
57257
- description: "Bind concrete wire endpoints to existing L3 interface methods, for a Portal whose transport requires them (every transport but InProcess and Custom). Pick the Portal's own `transport` and fill that transport's address fields. A binding for an InProcess Portal is refused: its verbs are its contract methods. Run after sdd_define_interface. Written through the authoring seam as one gated delta, and answered with the change report \u2014 every binding set or changed, or that nothing changed \u2014 as structured content beside the sentence.",
58811
+ description: "Bind concrete wire endpoints to existing L3 interface methods, for a Portal whose transport requires them (every transport but InProcess and Custom). Pick the Portal's own `transport` and fill that transport's address fields \u2014 for HTTP the verb, the path and, optionally, the success `status` (a 2xx or 3xx code: 202 Accepted, a redirect, a 201 for a workflow verb) the operation answers with, which the OpenAPI export uses in place of its convention. A binding for an InProcess Portal is refused: its verbs are its contract methods. Run after sdd_define_interface. Written through the authoring seam as one gated delta, and answered with the change report \u2014 every binding set or changed, or that nothing changed \u2014 as structured content beside the sentence.",
57258
58812
  inputSchema: {
57259
58813
  interface: import_zod13.z.string().describe('The L3 interface ID (e.g. "ibilling-gateway")'),
57260
58814
  endpoints: import_zod13.z.array(import_zod13.z.object({
@@ -57262,6 +58816,7 @@ ${renderChangeReport(report3)}`,
57262
58816
  transport: import_zod13.z.enum(["HTTP", "HTTP_API", "gRPC", "GraphQL", "MessageBus", "NamedPipe", "IPC", "CLI", "JSONRPC", "Custom"]).describe("Wire protocol; must be the Portal's own transport. The retired spelling HTTP_API is still read as HTTP for one release; any other transport is refused, naming the Portal's"),
57263
58817
  httpMethod: import_zod13.z.enum(["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]).optional().describe("HTTP: verb"),
57264
58818
  path: import_zod13.z.string().optional().describe('HTTP: route path, e.g. "/v1/checkout"'),
58819
+ status: import_zod13.z.number().int().min(200).max(399).optional().describe("HTTP: the success status the operation answers with when it is not the verb's convention \u2014 202 Accepted, a 3xx redirect, a 201 for a workflow verb that files something. The OpenAPI export uses it; omitted, the export answers 204 for a method returning nothing, 201 for a POST that creates, else 200"),
57265
58820
  service: import_zod13.z.string().optional().describe("gRPC: service name"),
57266
58821
  rpcMethod: import_zod13.z.string().optional().describe("gRPC: rpc method name"),
57267
58822
  operation: import_zod13.z.enum(["query", "mutation", "subscription"]).optional().describe("GraphQL: operation kind"),
@@ -57275,11 +58830,12 @@ ${renderChangeReport(report3)}`,
57275
58830
  command: import_zod13.z.string().optional().describe("CLI: command/subcommand"),
57276
58831
  jsonRpcMethod: import_zod13.z.string().optional().describe("JSONRPC: the JSON-RPC method name"),
57277
58832
  address: import_zod13.z.string().optional().describe("Custom: free-form address")
57278
- }).strict()).describe("One binding per method")
58833
+ }).strict()).describe("One binding per method"),
58834
+ dryRun: import_zod13.z.boolean().optional().describe("Answer the change report the binding would give \u2014 the gate (a route another method binds is refused) and the writer's parse included \u2014 and write nothing")
57279
58835
  },
57280
58836
  outputSchema: specChangeReportOutput
57281
58837
  },
57282
- ({ interface: interfaceId, endpoints }) => {
58838
+ ({ interface: interfaceId, endpoints, dryRun }) => {
57283
58839
  try {
57284
58840
  const intf = loadInterfaceSpec(interfaceId);
57285
58841
  if (!intf) return errText(`Interface "${interfaceId}" does not exist.`);
@@ -57291,7 +58847,7 @@ ${renderChangeReport(report3)}`,
57291
58847
  const buildRaw = (e, transport) => {
57292
58848
  switch (transport) {
57293
58849
  case "HTTP":
57294
- return { transport: "HTTP", method: e.httpMethod, path: e.path };
58850
+ return { transport: "HTTP", method: e.httpMethod, path: e.path, ...e.status !== void 0 ? { status: e.status } : {} };
57295
58851
  case "gRPC":
57296
58852
  return { transport: "gRPC", service: e.service, method: e.rpcMethod };
57297
58853
  case "GraphQL":
@@ -57329,9 +58885,9 @@ ${renderChangeReport(report3)}`,
57329
58885
  methods.push({ name: e.method, endpoint: parsed.data });
57330
58886
  bound2.push(`${e.method}\u2192${transport}`);
57331
58887
  }
57332
- const report3 = updateSpecGated("interface", interfaceId, { methods });
58888
+ const report3 = updateSpecGated("interface", interfaceId, { methods }, dryRun);
57333
58889
  return structured(
57334
- `Bound ${bound2.length} endpoint(s) on "${interfaceId}": ${bound2.join(", ")}.
58890
+ `${dryRun ? "Would bind" : "Bound"} ${bound2.length} endpoint(s) on "${interfaceId}": ${bound2.join(", ")}.
57335
58891
  ${renderChangeReport(report3)}`,
57336
58892
  report3
57337
58893
  );
@@ -57401,7 +58957,7 @@ ${renderChangeReport(report3)}`,
57401
58957
  bindings: import_zod13.z.array(import_zod13.z.string()).optional().describe("Optional code linkage: the hand-written binding modules (project-relative; N:1 sharing allowed) through which this realization reaches another project's code \u2014 a typed binding to a native library, a client stub. Outside the approval, like sourcePath. Validate compares each with the pinned snapshots of the externals this component reaches (function and method names, parameter names and arity, type field names) and reports a name the pin renamed or no longer exports (BINDING_DRIFT)"),
57402
58958
  router: import_zod13.z.string().min(1).optional().describe("Portal-only code linkage: the router entry this Portal's own file exports, through which whatever process serves the Portal hands it its requests (replaces the retired listener mount's `via`). Route coverage reads the routes out of it and export conformance holds the file to it. Outside the approval, like sourcePath"),
57403
58959
  technologies: import_zod13.z.array(import_zod13.z.union([import_zod13.z.string(), import_zod13.z.object({ name: import_zod13.z.string(), matches: import_zod13.z.array(import_zod13.z.string()).min(1).describe("The tokens the leakage and contract checks match INSTEAD of the name \u2014 for a technology whose name is also an ordinary word of the tree (the yaml package, the YAML format)") }).strict()])).optional().describe(`External technologies this implementation binds to (e.g. ["mysql"], or [{ "name": "yaml", "matches": ["yaml package", "parseDocument"] }] when the bare name would match a common word) \u2014 declares this component's ownership tree as the technology's home; references outside it are flagged (TECH_LEAKAGE) and contract identifiers must stay intent-language. Only for Adapter/Store/Registry/Index components, and an Observer bound to a messaging technology.`),
57404
- injectedParams: import_zod13.z.array(import_zod13.z.string()).optional().describe("The parameter names this realization takes BEFORE the ones its contract declares \u2014 a config object, a data root, the transport handles a portal is handed. Supplied by whatever wires the component up, never by the caller the contract describes, which is why they belong here and not to the contract: another realization may hold them as fields instead. Declared rather than guessed, because a leading parameter the contract does not name cannot be told from one it named under a different name (`seed(config)` realized as `bootstrapInstance(cfg)`); a leading parameter this list does not name is reported (UNDECLARED_PARAM). Only a LEADING run is dropped \u2014 one of these names appearing after the contract's own parameters is an argument in the middle of the caller's list, not wiring"),
58960
+ injectedParams: import_zod13.z.array(import_zod13.z.string()).optional().describe("The parameter names this realization takes BEFORE the ones its contract declares \u2014 a config object, a data root, the transport handles a portal is handed. Supplied by whatever wires the component up, never by the caller the contract describes, which is why they belong here and not to the contract: another realization may hold them as fields instead. Declared rather than guessed, because a leading parameter the contract does not name cannot be told from one it named under a different name (`seed(config)` realized as `bootstrapInstance(cfg)`); a leading parameter this list does not name is reported (UNDECLARED_PARAM). A name matches with or without a leading `_` (`ctx` names `_ctx`). A LEADING run of these names is dropped, and so is a TRAILING run of names the contract does not declare (a handler's `res` after the contract's own parameters); one appearing in the middle of the contract's parameters is an argument of the caller's list, not wiring"),
57405
58961
  detail: detailEnum.optional().describe("Spec-level narrative detail default for all methods"),
57406
58962
  conformance: conformanceEnum.optional().describe("Spec-level conformance tier default: declared | anchored | off (omitted = stereotype default: Portal \u2192 anchored, else declared)"),
57407
58963
  methods: import_zod13.z.array(import_zod13.z.object(implMethodShape).strict()).optional().describe("Method implementations containing L5 narratives"),
@@ -57412,7 +58968,7 @@ ${renderChangeReport(report3)}`,
57412
58968
  server,
57413
58969
  "sdd_write_narrative",
57414
58970
  {
57415
- description: "Write L4 Concrete Implementation spec containing L5 method narratives. Narratives are a FLAT ordered step list; flow steps (branch/switch/loop/try/parallel/jump/return/throw) jump by step number \u2014 blocks are just skipped regions. Steps may declare a `label` anchor, and every jump field has a *Label twin (toLabel, onTrueLabel, endLabel, \u2026) resolved to step numbers at write time \u2014 prefer labels over hand-counted numbers; an unresolvable label rejects the write. Detail dial per method: full (narrative required) | calls-only (call choreography suffices) | intent (prose instead of steps); omitted = stereotype default (Portal/Observer/Adapter: calls-only, Store/Index/Registry: intent, else full). A method whose narrative shows no steps declares the calls it makes in `calls` (one \"<component>.<method>\" each): the reachability walk takes exactly those edges and no others, so an intent-level method that reaches a collaborator must name it or that collaborator is reported unused. Conformance dial per method or spec: declared | anchored | off \u2014 how strictly structural conformance requires contract methods to be realized in their source file (omitted = Portal: anchored, else declared). A method whose body lives in its own file names it in the method's sourcePath; the implementation's sourcePath is the default for every method that names none. Parameters the realization takes BEFORE its contract's own \u2014 a config object, a data root, a portal's transport handles \u2014 are wiring, and are declared once for the spec in injectedParams; a leading parameter it does not name is reported against the contract (UNDECLARED_PARAM). Re-authoring an existing id REPLACES the method list: a method left out of the input is REMOVED together with its narrative (and reported); spec-level lint/ext are carried forward, and the stored status is kept unless this input states a higher one. The answer carries a write receipt as structured content beside the sentence \u2014 the status written, whether a spec already held the id, and the notices a restatement raised, each as its own entry.",
58971
+ description: "Write L4 Concrete Implementation spec containing L5 method narratives. Narratives are a FLAT ordered step list; flow steps (branch/switch/loop/try/parallel/jump/return/throw) jump by step number \u2014 blocks are just skipped regions. Steps may declare a `label` anchor, and every jump field has a *Label twin (toLabel, onTrueLabel, endLabel, \u2026) resolved to step numbers at write time \u2014 prefer labels over hand-counted numbers; an unresolvable label rejects the write. Detail dial per method: full (narrative required) | calls-only (call choreography suffices) | intent (prose instead of steps); omitted = stereotype default (Portal/Observer/Adapter: calls-only, Store/Index/Registry: intent, else full). A method whose narrative shows no steps declares the calls it makes in `calls` (one \"<component>.<method>\" each): the reachability walk takes exactly those edges and no others, so an intent-level method that reaches a collaborator must name it or that collaborator is reported unused. Conformance dial per method or spec: declared | anchored | off \u2014 how strictly structural conformance requires contract methods to be realized in their source file (omitted = Portal: anchored, else declared). A method whose body lives in its own file names it in the method's sourcePath; the implementation's sourcePath is the default for every method that names none. Parameters the realization takes BEFORE its contract's own \u2014 a config object, a data root, a portal's transport handles \u2014 or after them (a handler's response handle), are wiring, and are declared once for the spec in injectedParams (matched with or without a leading `_`); a leading or trailing parameter it does not name is reported against the contract (UNDECLARED_PARAM). Re-authoring an existing id REPLACES the method list: a method left out of the input is REMOVED together with its narrative (and reported); spec-level lint/ext are carried forward, and the stored status is kept unless this input states a higher one. The answer carries a write receipt as structured content beside the sentence \u2014 the status written, whether a spec already held the id, and the notices a restatement raised, each as its own entry.",
57416
58972
  inputSchema: implInput,
57417
58973
  outputSchema: specWriteReceiptOutput
57418
58974
  },
@@ -57500,10 +59056,10 @@ ${renderChangeReport(report3)}`,
57500
59056
  "sdd_add_type",
57501
59057
  {
57502
59058
  description: "Define a type: an entity or value-object (the data components operate on), a signature \u2014 a named function type \u2014 or an enum: a closed, ordered set of named `values` ({name, description?}, unique by name ignoring case and separators; the name is also the value as data carries it), with optional pure methods and nothing else (no fields, params or returns). A value-object may instead be a NAMED SCALAR: it `holds` one primitive in place of fields (a newtype or type alias in every language). Every type position (a field's type, a method's params and returns, a signature's params and returns, a named scalar's holds) is written in the neutral type grammar; aliases are respelled and listed in the receipt. Entities are owned by a subsystem; shared value objects and signatures omit subsystem (system-level). Fields are data; methods are PURE intrinsic behaviour only \u2014 anything needing a collaborator belongs on a component, taking the entity as an argument \u2014 and a type method may carry structured params, which then derive its signature text as a contract method's do. A signature carries top-level params and returns and nothing else (no fields, methods or invariants \u2014 SIGNATURE_TYPE_MEMBERS); a contract method takes it by naming it as its signatureFrom, and a param may be typed by one (a callback). A type may also CLAIM code: sourcePath names the file holding its declaration (and each method may name its own), symbol binds the code-level name when it differs \u2014 the file must then resolve and the declaration must be anchored in it. An owning subsystem the tree does not have is refused, writing nothing, exactly as a component under an unknown subsystem is. Re-defining an existing id REPLACES fields/methods/invariants and restates sourcePath/symbol (an omitted list or path is CLEARED, and a dropped member is reported); lint/ext are carried forward. The answer carries a write receipt as structured content beside the sentence \u2014 whether a spec already held the id, and the notices a restatement raised, each as its own entry. A type carries no lifecycle status, so the receipt states none.",
57503
- inputSchema: typeInput,
59059
+ inputSchema: { ...typeInput, dryRun: import_zod13.z.boolean().optional().describe("Answer the receipt the write would give \u2014 every judgement run, a refusal exactly as the write would give it \u2014 and write nothing") },
57504
59060
  outputSchema: specWriteReceiptOutput
57505
59061
  },
57506
- ({ kind, id, name, description, subsystem, group, fields, methods, componentClass, invariants, database, table, linkedEntity, sourcePath, symbol, params, returns, values, holds }) => {
59062
+ ({ kind, id, name, description, subsystem, group, fields, methods, componentClass, invariants, database, table, linkedEntity, sourcePath, symbol, params, returns, values, holds, dryRun }) => {
57507
59063
  try {
57508
59064
  const spec = {
57509
59065
  kind,
@@ -57533,9 +59089,9 @@ ${renderChangeReport(report3)}`,
57533
59089
  ...values ? { values } : {},
57534
59090
  ...holds ? { holds } : {}
57535
59091
  };
57536
- const receipt = writeSpec({ kind: "type", spec, fields: typeInputFields });
59092
+ const receipt = writeSpec({ kind: "type", spec, fields: typeInputFields, ...dryRun ? { dryRun: true } : {} });
57537
59093
  return writeReceipt(
57538
- `Successfully ${receipt.replacedExisting ? "re-authored" : "defined"} ${kind} type "${name}" (${id}).`,
59094
+ `${dryRun ? "DRY RUN \u2014 nothing was written. Would have " : "Successfully "}${receipt.replacedExisting ? "re-authored" : "defined"} ${kind} type "${name}" (${id}).`,
57539
59095
  receipt
57540
59096
  );
57541
59097
  } catch (e) {
@@ -57693,20 +59249,23 @@ ${renderChangeReport(report3)}`,
57693
59249
  server,
57694
59250
  "sdd_delete_spec",
57695
59251
  {
57696
- description: "Delete a spec and everything it contains \u2014 a contract with its implementations, a component with its contracts and owned members, a subsystem with its components and types \u2014 pruning the folders left empty. Refused, naming every reference, while a spec that stays still references what would go (the invalid references the deletion would introduce); force: true deletes anyway and leaves them for validate to report. dryRun answers the same plan and removes nothing. The structured answer lists every spec removed, every reference, and the tests that encode a removed method (when the project declares test roots).",
59252
+ description: "Delete a spec and everything it contains \u2014 a contract with its implementations, a component with its contracts and owned members, a subsystem with its components and types \u2014 pruning the folders left empty. Refused, naming every reference, while a spec that stays still references what would go (the invalid references the deletion would introduce); force: true deletes anyway and leaves them for validate to report. Refused outright, the dry run included: a spec of another project (a member's spec read through this tree, `alias::name`, is deleted from that member's own root \u2014 the refusal names its folder), an id no spec of the kind holds (the refusal names what it might have meant: another kind holding it, the `<subsystem>::<id>` form of a type, the ids the kind holds), and the L0. dryRun answers the same plan and removes nothing. Like a rename, it reports `published` \u2014 every public name of this project a consumer reaches what goes through \u2014 and `breaks`: the consumers in reach (family members included, and checkouts in the `search` folders) whose specs reach one of those. The structured answer lists every spec removed, every reference, the published names, the consumers it breaks, and the tests that encode a removed method (when the project declares test roots).",
57697
59253
  inputSchema: {
57698
59254
  kind: import_zod13.z.enum(["subsystem", "component", "interface", "implementation", "type"]).describe("The kind of spec to delete"),
57699
59255
  id: import_zod13.z.string().describe("The ID of the spec to delete"),
57700
59256
  dryRun: import_zod13.z.boolean().optional().describe("Answer with the plan \u2014 every spec that would be removed, every reference that would be left \u2014 and remove nothing"),
57701
- force: import_zod13.z.boolean().optional().describe("Delete even though specs that stay still reference what goes, leaving those references for validate to report. Without it such a deletion is refused")
59257
+ force: import_zod13.z.boolean().optional().describe("Delete even though specs that stay still reference what goes, leaving those references for validate to report. Without it such a deletion is refused"),
59258
+ search: import_zod13.z.array(import_zod13.z.string()).optional().describe("Folders to scan for consumer checkouts outside the family (e.g. the folder holding sibling checkouts), relative to the bound project root or absolute")
57702
59259
  },
57703
59260
  outputSchema: specDeletionOutput
57704
59261
  },
57705
- ({ kind, id, dryRun, force }) => {
59262
+ ({ kind, id, dryRun, force, search }) => {
57706
59263
  try {
59264
+ const consumers = listConsumers3(search?.map((d) => path51.resolve(getProjectRoot(), d)));
57707
59265
  const deletion = deleteSpec2(kind, id, dryRun, force);
57708
- if (deletion.removed.length === 0) return errText(`Spec of kind "${kind}" with ID "${id}" could not be deleted (file may not exist).`);
57709
- return structured(renderDeletion(deletion), deletion);
59266
+ const breaks = consumers.map((c) => narrowedToUses(c, deletion.published)).filter((c) => c !== null);
59267
+ const answer = breaks.length > 0 ? { ...deletion, breaks } : deletion;
59268
+ return structured(renderDeletion(answer), answer);
57710
59269
  } catch (e) {
57711
59270
  return errText(String(e));
57712
59271
  }
@@ -57721,13 +59280,18 @@ ${renderChangeReport(report3)}`,
57721
59280
  kind: import_zod13.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).describe("The spec kind to update (system = the singleton L0 \u2014 vision, boundaries, globalRequirements, databases, and publicInterfaces: the project export table, each entry a re-export {from, component, interface, typeDef, as, name, type, details, audience: project|department|instance|partner|external} \u2014 from alone re-exports everything a subsystem exports, and a legacy {id, subsystem} reads as {as, from})"),
57722
59281
  id: import_zod13.z.string().describe("The ID of the spec to update (namespaced if needed)"),
57723
59282
  delta: import_zod13.z.record(import_zod13.z.any()).describe(`The fields to merge. LISTS UPSERT by identity at every depth: naming one element leaves the others, and a named element merges field by field \u2014 a field left out, or null, keeps its stored value (a method named without "narrative" keeps its steps). Identity is name or id by default; dispatch by capability, lifecycle by phase+component+method, emits/subscribesTo by topic+event, trustedLinks by subsystem, publicInterfaces by component+interface (a re-export by from+item+interface+as), lint.allow by code+at, findings by code, boundaries by name, globalRequirements by description, cases by value, catches by error. Plain-value lists (owns, dependsOn, guarantees, technologies, requirements written as text) append what they lack and keep what they hold. Restating a list with exactly its stored elements in another order REORDERS it. Remove an element with "action": "delete" (or "remove": true) beside its identity, a plain value as {"value": \u2026, "action": "delete"}; [] clears a list; {"unset": ["basePath"]} removes an optional field (null means no change). NARRATIVE steps are addressed by stepNumber, applied in ascending order against the numbering earlier entries of the same delta left: "action": "insert" shifts later steps up, "delete" shifts them down \u2014 refused when a jump still targets the step, when a restated label or description does not match it, or for a loop/try/parallel header whose body stands. Stored jumps relocate; a jump the delta writes names the final numbering; "captureJumps": true on an insert lands entry jumps on the new step. Every jump field has a *Label twin (toLabel, onFalseLabel, endLabel, a case or catch "label") resolved after the merge. Retyping a step rebuilds it for its new type and names the fields dropped. lint: {"allow": [{code, at, covers, reason}]} silences a warning or notice on this spec: "at" is the site the finding reports and is required when it reports one, "covers" lists an aggregating finding's units; errors never silence. Local ids are qualified as the loader qualifies them. A key that is not a field at its depth is named back under NO EFFECT: read that list.`),
57724
- dryRun: import_zod13.z.boolean().optional().describe("Ask what this delta WOULD do: the whole write runs \u2014 the gate and the writer's parse included \u2014 and answers the report it would produce, marked DRY RUN, with not one byte written.")
59283
+ dryRun: import_zod13.z.boolean().optional().describe("Ask what this delta WOULD do: the whole write runs \u2014 the gate and the writer's parse included \u2014 and answers the report it would produce, marked DRY RUN, with not one byte written."),
59284
+ search: import_zod13.z.array(import_zod13.z.string()).optional().describe("Folders to scan for consumer checkouts outside the family, read when the delta removes contract methods: the removal names the consumers it breaks, as a rename does")
57725
59285
  },
57726
59286
  outputSchema: specChangeReportOutput
57727
59287
  },
57728
- ({ kind, id, delta, dryRun }) => {
59288
+ ({ kind, id, delta, dryRun, search }) => {
57729
59289
  try {
59290
+ const removes = kind === "interface" && delta && typeof delta === "object" && (Array.isArray(delta.methods) && (delta.methods.length === 0 || delta.methods.some((m) => m?.action === "delete" || m?.remove === true)) || Array.isArray(delta.unset) && delta.unset.includes("methods"));
59291
+ const consumers = removes ? listConsumers3(search?.map((d) => path51.resolve(getProjectRoot(), d))) : [];
57730
59292
  const report3 = updateSpecGated(kind, id, delta, dryRun);
59293
+ const breaks = report3.published ? consumers.map((c) => narrowedToUses(c, report3.published)).filter((c) => c !== null) : [];
59294
+ if (breaks.length > 0) report3.breaks = breaks;
57731
59295
  return structured(renderChangeReport(report3), report3);
57732
59296
  } catch (e) {
57733
59297
  return errText(String(e));
@@ -58289,6 +59853,7 @@ function loadProjectConfig3() {
58289
59853
  createEmptyTopologyConfig,
58290
59854
  createMember,
58291
59855
  createProjectConfig,
59856
+ crossProjectWriteRefusal,
58292
59857
  currentRootBinding,
58293
59858
  declareExternal,
58294
59859
  declareMember,
@@ -58458,6 +60023,7 @@ function loadProjectConfig3() {
58458
60023
  moveMember,
58459
60024
  moveMethods,
58460
60025
  moveMountToMembers,
60026
+ moveSpec,
58461
60027
  nameKey,
58462
60028
  namesAnInteger,
58463
60029
  narrativeStepForeignFields,
@@ -58487,6 +60053,7 @@ function loadProjectConfig3() {
58487
60053
  projectIdentity,
58488
60054
  promoteMember,
58489
60055
  provisionProject,
60056
+ publishedUsesOf,
58490
60057
  qualifiedTypeId,
58491
60058
  rangeProblem,
58492
60059
  readCodeAnnotation,
@@ -58516,6 +60083,7 @@ function loadProjectConfig3() {
58516
60083
  renameId,
58517
60084
  renameMethod,
58518
60085
  renameParam,
60086
+ renameSpecId,
58519
60087
  renameType,
58520
60088
  renderDiagram,
58521
60089
  repairForeignStepFields,
@@ -58545,6 +60113,7 @@ function loadProjectConfig3() {
58545
60113
  retiredReachFacts,
58546
60114
  returnTypeOf,
58547
60115
  rewriteReferences,
60116
+ routerLinkage,
58548
60117
  runWithHostedLookup,
58549
60118
  runWithProjectBinding,
58550
60119
  runWithProjectRoot,
@@ -58565,6 +60134,7 @@ function loadProjectConfig3() {
58565
60134
  settledSpecPaths,
58566
60135
  signatureFacts,
58567
60136
  signatureTypeRefs,
60137
+ specIndexMethodReferences,
58568
60138
  specIndexReferencesTo,
58569
60139
  specIndexRetiredBy,
58570
60140
  specIndexSubtreeOf,