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

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/cli/index.js CHANGED
@@ -65,7 +65,7 @@ var init_defaults = __esm({
65
65
  copilot: ".github/prompts",
66
66
  codex: ".codex/agents"
67
67
  };
68
- WAIRON_VERSION = "5.1.1-dev.113";
68
+ WAIRON_VERSION = "5.1.1-dev.114";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  SUPPORTED_ALIASES = ["wai"];
71
71
  SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
@@ -705,6 +705,13 @@ function lastCommitOf(directory, pathspec) {
705
705
  return null;
706
706
  }
707
707
  }
708
+ function commitIntroducing(directory, pathspec, text3) {
709
+ try {
710
+ return git(["log", "--reverse", "--format=%H", `-S${text3}`, "--", pathspec], directory).split(/\r?\n/).find((l) => l.trim() !== "")?.trim() ?? null;
711
+ } catch {
712
+ return null;
713
+ }
714
+ }
708
715
  function commitOf(directory, ref) {
709
716
  try {
710
717
  return git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], directory).trim() || null;
@@ -6584,6 +6591,97 @@ function removeSpecFile(specPath, specsRoot) {
6584
6591
  pruneEmptyDirs(path10.dirname(specPath), path10.resolve(specsRoot));
6585
6592
  return true;
6586
6593
  }
6594
+ function sleepSync(ms) {
6595
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
6596
+ }
6597
+ function processAlive(pid) {
6598
+ if (!Number.isInteger(pid) || pid <= 0) return false;
6599
+ try {
6600
+ process.kill(pid, 0);
6601
+ return true;
6602
+ } catch (e) {
6603
+ return e.code === "EPERM";
6604
+ }
6605
+ }
6606
+ function lockHolder(file) {
6607
+ try {
6608
+ const [pid, at] = fs6.readFileSync(file, "utf8").split("\n");
6609
+ if (!/^\d+$/.test(pid ?? "") || !/^\d+$/.test(at ?? "")) return null;
6610
+ return { pid: Number(pid), at: Number(at) };
6611
+ } catch {
6612
+ return null;
6613
+ }
6614
+ }
6615
+ function lockTree(root) {
6616
+ const dir = path10.join(root, ".wai");
6617
+ if (!fs6.existsSync(dir)) return;
6618
+ const file = path10.join(dir, LOCK_FILE);
6619
+ const held = lockHolds.get(file) ?? 0;
6620
+ if (held > 0) {
6621
+ lockHolds.set(file, held + 1);
6622
+ return;
6623
+ }
6624
+ const deadline = Date.now() + LOCK_WAIT_MS;
6625
+ while (!tryCreateLock(file)) {
6626
+ if (breakStaleLock(file)) continue;
6627
+ if (Date.now() > deadline) {
6628
+ throw new Error(
6629
+ `spec-tree-locked: another session (process ${lockHolder(file)?.pid ?? "unknown"}) has been writing this project's specs for over ${LOCK_WAIT_MS / 1e3}s (${file}). Nothing was written. Try again once it finishes; a lock left by a process that is gone is broken automatically.`
6630
+ );
6631
+ }
6632
+ sleepSync(15 + Math.floor(Math.random() * 20));
6633
+ }
6634
+ lockHolds.set(file, 1);
6635
+ }
6636
+ function lockAge(file) {
6637
+ try {
6638
+ return Date.now() - fs6.statSync(file).mtimeMs;
6639
+ } catch {
6640
+ return 0;
6641
+ }
6642
+ }
6643
+ function tryCreateLock(file) {
6644
+ let fd;
6645
+ try {
6646
+ fd = fs6.openSync(file, "wx");
6647
+ } catch (e) {
6648
+ if (e.code === "EEXIST") return false;
6649
+ throw e;
6650
+ }
6651
+ try {
6652
+ fs6.writeSync(fd, `${process.pid}
6653
+ ${Date.now()}
6654
+ `);
6655
+ } finally {
6656
+ fs6.closeSync(fd);
6657
+ }
6658
+ return true;
6659
+ }
6660
+ function breakStaleLock(file) {
6661
+ const holder = lockHolder(file);
6662
+ const stale = holder === null ? lockAge(file) > LOCK_STALE_MS : holder.pid === process.pid || !processAlive(holder.pid) || Date.now() - holder.at > LOCK_STALE_MS;
6663
+ if (!stale) return false;
6664
+ try {
6665
+ fs6.unlinkSync(file);
6666
+ } catch {
6667
+ }
6668
+ return true;
6669
+ }
6670
+ function unlockTree(root) {
6671
+ const file = path10.join(root, ".wai", LOCK_FILE);
6672
+ const held = lockHolds.get(file) ?? 0;
6673
+ if (held <= 0) return;
6674
+ if (held > 1) {
6675
+ lockHolds.set(file, held - 1);
6676
+ return;
6677
+ }
6678
+ lockHolds.delete(file);
6679
+ if (lockHolder(file)?.pid !== process.pid) return;
6680
+ try {
6681
+ fs6.unlinkSync(file);
6682
+ } catch {
6683
+ }
6684
+ }
6587
6685
  function pruneEmptyDirs(dir, specsRoot) {
6588
6686
  let at = dir;
6589
6687
  while (at !== specsRoot && at.startsWith(specsRoot)) {
@@ -6592,7 +6690,7 @@ function pruneEmptyDirs(dir, specsRoot) {
6592
6690
  at = path10.dirname(at);
6593
6691
  }
6594
6692
  }
6595
- var fs6, path10, SPEC_FILE_EXTENSION;
6693
+ var fs6, path10, SPEC_FILE_EXTENSION, LOCK_FILE, LOCK_STALE_MS, LOCK_WAIT_MS, lockHolds;
6596
6694
  var init_spec_files = __esm({
6597
6695
  "src/core/spec-files.ts"() {
6598
6696
  "use strict";
@@ -6601,6 +6699,10 @@ var init_spec_files = __esm({
6601
6699
  init_fs();
6602
6700
  init_yaml();
6603
6701
  SPEC_FILE_EXTENSION = ".yaml";
6702
+ LOCK_FILE = ".spec-write.lock";
6703
+ LOCK_STALE_MS = 6e4;
6704
+ LOCK_WAIT_MS = 3e4;
6705
+ lockHolds = /* @__PURE__ */ new Map();
6604
6706
  }
6605
6707
  });
6606
6708
 
@@ -6908,10 +7010,10 @@ function stepGraph(method2) {
6908
7010
  const s = byNum.get(n);
6909
7011
  if (s.type !== "parallel" || s.endStep === void 0 || !s.branches?.length) continue;
6910
7012
  const entries = s.branches.map((b) => b.step).sort((a, b) => a - b);
6911
- const join68 = fallNext(s.endStep);
7013
+ const join69 = fallNext(s.endStep);
6912
7014
  for (let i = 0; i < entries.length; i++) {
6913
7015
  const armEnd = i + 1 < entries.length ? prevOf(entries[i + 1]) : s.endStep;
6914
- if (armEnd !== void 0 && armEnd >= entries[i]) armEndJoin.set(armEnd, join68);
7016
+ if (armEnd !== void 0 && armEnd >= entries[i]) armEndJoin.set(armEnd, join69);
6915
7017
  }
6916
7018
  }
6917
7019
  const successorsOf = (n) => {
@@ -7297,7 +7399,13 @@ function closureTypeOf(snapshot, identifier) {
7297
7399
  const ref = local.join("::");
7298
7400
  const matches = snapshot.types.filter((def) => matchTypeRef(ref, def.id));
7299
7401
  if (matches.length === 1) return matches[0];
7300
- return matches.find((def) => nameKey(def.id) === nameKey(ref));
7402
+ const exact = matches.find((def) => nameKey(def.id) === nameKey(ref));
7403
+ if (exact || matches.length > 0) return exact;
7404
+ if (local.length === 2 && !identifier.includes("::")) {
7405
+ const unqualified = snapshot.types.filter((def) => !def.id.includes("::") && !def.id.includes(".") && nameKey(def.id) === nameKey(local[1]));
7406
+ if (unqualified.length === 1) return unqualified[0];
7407
+ }
7408
+ return void 0;
7301
7409
  }
7302
7410
  function renamedRefs(expr, rename) {
7303
7411
  const args = expr.args.map((a) => renamedRefs(a, rename));
@@ -7398,6 +7506,9 @@ function carriedFactChanges(pinned, live) {
7398
7506
  const nowMethod = liveMethods.get(method2.name);
7399
7507
  if (!nowMethod) continue;
7400
7508
  differs(`effect of ${entry.id}.${method2.name}`, method2.effect, nowMethod.effect);
7509
+ if (method2.endpoint && factValue(method2.endpoint) !== factValue(nowMethod.endpoint)) {
7510
+ changes.push(`endpoint of ${entry.id}.${method2.name} (${shownEndpoint(method2.endpoint)} \u2192 ${shownEndpoint(nowMethod.endpoint)})`);
7511
+ }
7401
7512
  differs(`rename trace of ${entry.id}.${method2.name}`, method2.formerly, nowMethod.formerly);
7402
7513
  for (const r of paramRenames(method2, nowMethod)) {
7403
7514
  changes.push(`parameter "${r.from}" of ${entry.id}.${method2.name} (renamed to "${r.to}")`);
@@ -7442,7 +7553,8 @@ function memberDigest(snapshot, publicName, member) {
7442
7553
  return sha256(canonicalize({ method: methodShape(snapshot, method2), closure: closureShapes(snapshot, methodTypeRefs(method2)) }));
7443
7554
  }
7444
7555
  function shownSignature(method2) {
7445
- return method2.signature || `${method2.name}(${(method2.params ?? []).map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`).join(", ")}): ${method2.returns}`;
7556
+ const derived = `${method2.name}(${(method2.params ?? []).map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`).join(", ")}): ${method2.returns}`;
7557
+ return (method2.params?.length ? derived : method2.signature) || derived;
7446
7558
  }
7447
7559
  function entryFacts(before, after) {
7448
7560
  return [
@@ -7550,8 +7662,45 @@ function closureRenames(newer, older) {
7550
7662
  function renamesIn(ids, notes) {
7551
7663
  return ids.flatMap((id) => notes.get(id) ?? []);
7552
7664
  }
7665
+ function carriedThroughOwnRecords(snapshot, method2, exported) {
7666
+ const byId = new Map(snapshot.types.map((def) => [def.id, def]));
7667
+ const carried = /* @__PURE__ */ new Set();
7668
+ const expanded = /* @__PURE__ */ new Set();
7669
+ const queue = methodTypeRefs(method2).flatMap((expr) => extractTypeIdentifiers(canonicalTypeRef(snapshot, expr))).filter((id) => byId.has(id) && !exported.has(id));
7670
+ while (queue.length) {
7671
+ const id = queue.shift();
7672
+ if (expanded.has(id)) continue;
7673
+ expanded.add(id);
7674
+ for (const expr of typeDefExprs(byId.get(id))) {
7675
+ for (const child of extractTypeIdentifiers(canonicalTypeRef(snapshot, expr))) {
7676
+ if (!byId.has(child)) continue;
7677
+ carried.add(child);
7678
+ if (!exported.has(child)) queue.push(child);
7679
+ }
7680
+ }
7681
+ }
7682
+ return carried;
7683
+ }
7684
+ function tracedRenamesBehind(older, newer, was, now) {
7685
+ const written = new Set(methodTypeRefs(was).flatMap((expr) => expr.match(TYPE_IDENTIFIER) ?? []).map((token) => token.replace(/^::/, "")));
7686
+ const olderDefs = new Map(older.types.map((d) => [d.id, d]));
7687
+ const out = [];
7688
+ for (const id of closureIds(newer, now)) {
7689
+ const def = newer.types.find((d) => d.id === id);
7690
+ const from = (def.formerly ?? []).find((f) => written.has(f) && !written.has(def.id));
7691
+ if (from === void 0) continue;
7692
+ out.push(`type "${from}" renamed to "${def.id}"`);
7693
+ const olderDef = olderDefs.get(from);
7694
+ const renamed2 = olderDef ? fieldRenames(olderDef, def) : def.fields.flatMap((field) => (field.formerly ?? []).filter((f) => !def.fields.some((x) => x.name === f)).slice(-1).map((f) => ({ from: f, to: field.name })));
7695
+ for (const r of renamed2) out.push(`field "${def.id}.${r.from}" renamed to "${r.to}"`);
7696
+ }
7697
+ return out;
7698
+ }
7553
7699
  function signatureChange(older, newer, was, now, undoneNow = now) {
7554
- if (shownSignature(was) !== shownSignature(underName(undoneNow, was.name))) return `signature ${shownSignature(was)} \u2192 ${shownSignature(now)}`;
7700
+ if (shownSignature(was) !== shownSignature(underName(undoneNow, was.name))) {
7701
+ const behind = tracedRenamesBehind(older, newer, was, now);
7702
+ return `signature ${shownSignature(was)} \u2192 ${shownSignature(now)}${behind.length ? ` (${behind.join("; ")})` : ""}`;
7703
+ }
7555
7704
  const oldDefs = new Map(older.types.map((d) => [d.id, d]));
7556
7705
  const newDefs = new Map(newer.types.map((d) => [d.id, d]));
7557
7706
  const ids = [.../* @__PURE__ */ new Set([...closureIds(older, was), ...closureIds(newer, now)])].sort();
@@ -7561,7 +7710,8 @@ function signatureChange(older, newer, was, now, undoneNow = now) {
7561
7710
  return !a || !b || canonicalize(typeShape(older, a)) !== canonicalize(typeShape(newer, b));
7562
7711
  });
7563
7712
  const exported = new Set([...newer.exportedTypes ?? [], ...older.exportedTypes ?? []].map((t) => t.type));
7564
- const unlisted = moved.filter((id) => !exported.has(id));
7713
+ const carried = carriedThroughOwnRecords(newer, now, exported);
7714
+ const unlisted = moved.filter((id) => !exported.has(id) || carried.has(id));
7565
7715
  if (moved.length > 0 && unlisted.length === 0) return null;
7566
7716
  const named2 = (unlisted.length > 0 ? unlisted : moved).map((id) => `"${id}"`).join(", ");
7567
7717
  return named2 ? `signature reads the same, but a type it names changed shape: ${named2}` : "signature reads the same, but a type it names changed shape";
@@ -7690,7 +7840,8 @@ function surfaceChanges(newer, older) {
7690
7840
  if (changed !== null) out.push({ kind: "changed", name: entry.id, member: method2.name, detail: changed });
7691
7841
  } else if (a !== b) {
7692
7842
  const exported = new Set((newer.exportedTypes ?? []).map((t) => t.type));
7693
- const embedded = renamesIn(closureIds(newer, method2).filter((id) => !exported.has(id)), notes);
7843
+ const carried = carriedThroughOwnRecords(newer, method2, exported);
7844
+ const embedded = renamesIn(closureIds(newer, method2).filter((id) => !exported.has(id) || carried.has(id)), notes);
7694
7845
  if (embedded.length > 0) {
7695
7846
  const key2 = embedded.join("; ");
7696
7847
  embeddedBy.set(key2, [...embeddedBy.get(key2) ?? [], method2.name]);
@@ -13794,7 +13945,8 @@ function schemaScope(ownDefs, externals, registry, alias) {
13794
13945
  return hit !== void 0 ? own2(hit) : void 0;
13795
13946
  },
13796
13947
  isObject(typeRef2) {
13797
- const expr = expressionOf(typeRef2);
13948
+ const read2 = expressionOf(typeRef2);
13949
+ const expr = read2?.form === "optional" ? read2.args[0] : read2;
13798
13950
  if (!expr || expr.form !== "named" && expr.form !== "applied") return false;
13799
13951
  const name = expr.name ?? "";
13800
13952
  const sep21 = name.indexOf("::");
@@ -13911,10 +14063,10 @@ function enumValuesFrom(schema) {
13911
14063
  return schema.enum.filter((v) => typeof v === "string" && v.length > 0).map((name) => ({ name, ...described2.has(name) ? { description: described2.get(name) } : {} }));
13912
14064
  }
13913
14065
  function openApiPath(basePath, endpointPath2) {
13914
- const join68 = (a, b) => `${a.replace(/\/+$/, "")}/${b.replace(/^\/+/, "")}`;
14066
+ const join69 = (a, b) => `${a.replace(/\/+$/, "")}/${b.replace(/^\/+/, "")}`;
13915
14067
  const base = basePath && basePath.trim() && basePath.trim() !== "/" ? basePath.trim() : "";
13916
14068
  const rootedBase = base.startsWith("/") || !base ? base : `/${base}`;
13917
- const raw = base ? endpointPath2.replace(/\/+$/, "") === "" ? rootedBase : join68(rootedBase, endpointPath2) : endpointPath2;
14069
+ const raw = base ? endpointPath2.replace(/\/+$/, "") === "" ? rootedBase : join69(rootedBase, endpointPath2) : endpointPath2;
13918
14070
  const rooted = raw.startsWith("/") ? raw : `/${raw}`;
13919
14071
  return rooted.replace(/\/:([A-Za-z_][A-Za-z0-9_]*)/g, "/{$1}");
13920
14072
  }
@@ -13966,7 +14118,7 @@ function operationFor(method2, scope, entry) {
13966
14118
  const bodyParam = rest.length === 1 ? rest[0] : void 0;
13967
14119
  if (bodyVerbs.has(httpVerb) && bodyParam && scope.isObject(bodyParam.type)) {
13968
14120
  op.requestBody = {
13969
- required: !bodyParam.optional && !admitsNoValue(bodyParam.type),
14121
+ required: !bodyParam.optional,
13970
14122
  ...bodyParam.description ? { description: bodyParam.description } : {},
13971
14123
  content: { "application/json": { schema: schemaFor(bodyParam.type, scope) } }
13972
14124
  };
@@ -15084,7 +15236,10 @@ function canonicalReferences(snapshot) {
15084
15236
  ...entry,
15085
15237
  methods: entry.methods.map((m) => ({
15086
15238
  ...m,
15087
- signature: canon(m.signature),
15239
+ // Rebuilt from the structured params when it has them: a parameter's
15240
+ // NAME is never a type position, so one spelled like a type
15241
+ // (`order_id: order_id`) keeps its name.
15242
+ signature: m.params?.length ? `${m.name}(${m.params.map((p) => `${p.name}${p.optional ? "?" : ""}: ${canon(p.type)}`).join(", ")}): ${canon(m.returns)}` : canon(m.signature),
15088
15243
  returns: canon(m.returns),
15089
15244
  ...m.params ? { params: m.params.map((p) => ({ ...p, type: canon(p.type) })) } : {}
15090
15245
  }))
@@ -15242,14 +15397,20 @@ function foreignSurfaces() {
15242
15397
  }
15243
15398
  const root = getProjectRoot();
15244
15399
  for (const m of members) {
15245
- if (m.problem || m.storage !== "contained" && m.storage !== "path" || m.path === void 0 || out.has(m.alias)) continue;
15400
+ const location = m.path ?? m.source.path;
15401
+ if (m.problem || m.storage !== "contained" && m.storage !== "path" || location === void 0 || out.has(m.alias)) continue;
15246
15402
  try {
15247
- const surface = runWithProjectRoot(path15.resolve(root, m.path), () => projectOwnSurface("project"));
15403
+ const surface = runWithProjectRoot(path15.resolve(root, location), () => projectOwnSurface("project"));
15248
15404
  out.set(m.alias, surface);
15249
15405
  if (surface.projectId && !out.has(surface.projectId)) out.set(surface.projectId, surface);
15250
15406
  } catch {
15251
15407
  }
15252
15408
  }
15409
+ try {
15410
+ const own2 = boundProjectId();
15411
+ if (own2 !== void 0 && !out.has(own2)) out.set(own2, projectOwnSurface("project"));
15412
+ } catch {
15413
+ }
15253
15414
  return out;
15254
15415
  }
15255
15416
  function withForeignTypes(snapshot, foreign) {
@@ -15488,6 +15649,11 @@ function narrowerExports(external, missing, read2) {
15488
15649
  }
15489
15650
  return [...out].map(([name, audience]) => ({ name, audience }));
15490
15651
  }
15652
+ function withMoved(detail, moved) {
15653
+ if (moved.length === 0) return detail;
15654
+ const said = `moved since the last pin: ${moved.join(", ")}`;
15655
+ return { detail: detail.detail ? `${said}; ${detail.detail}` : said };
15656
+ }
15491
15657
  function pinBinding(binding, lock) {
15492
15658
  const { external } = binding;
15493
15659
  const unexported = binding.usage?.unexported ?? [];
@@ -15504,6 +15670,7 @@ function pinBinding(binding, lock) {
15504
15670
  if (pinned) snapshot = withRetired(snapshot, pinned);
15505
15671
  const snapshotChanged = !pinned || contentDigest(pinned) !== digest3 || pinnedContentKey(pinned) !== pinnedContentKey(snapshot);
15506
15672
  if (snapshotChanged) externalsRepository.saveSnapshot(external.alias, snapshot);
15673
+ const moved = pinned && snapshotChanged ? carriedFactChanges(pinned, snapshot) : [];
15507
15674
  const { used, missing, uncarried } = usedDigests(binding.usage, snapshot, external.alias);
15508
15675
  const narrower = missing.length > 0 && external.audience !== "project" ? narrowerExports(external, missing, snapshot) : [];
15509
15676
  const entry = {
@@ -15525,7 +15692,7 @@ function pinBinding(binding, lock) {
15525
15692
  digest: digest3,
15526
15693
  usedNames: Object.keys(used).length,
15527
15694
  unexported: [...unexported, ...uncarried],
15528
- ...pinDetail(missing, Object.keys(used).length, external.alias, narrower, external.audience)
15695
+ ...withMoved(pinDetail(missing, Object.keys(used).length, external.alias, narrower, external.audience), moved)
15529
15696
  },
15530
15697
  changed: entryChanged
15531
15698
  };
@@ -15708,6 +15875,20 @@ function changedDetail(pinned, live, publicName, member, digest3) {
15708
15875
  if (renames.length === 0) return {};
15709
15876
  return { detail: `${renames.join("; ")}${renameOnly ? " (its shape is otherwise unchanged) \u2014 follow the rename" : " (and its shape changed beyond that)"}` };
15710
15877
  }
15878
+ function routeOf(endpoint) {
15879
+ if (!endpoint) return "none";
15880
+ if (endpoint.transport === "HTTP") return `${endpoint.method} ${endpoint.path}${endpoint.status !== void 0 ? ` (answers ${endpoint.status})` : ""}`;
15881
+ const { transport, ...address } = endpoint;
15882
+ return `${String(transport)} ${Object.entries(address).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${String(v)}`).join(" ")}`;
15883
+ }
15884
+ function endpointMove(pinned, live, publicName, member) {
15885
+ if (!pinned || member === "type" || member.startsWith("capability:")) return null;
15886
+ const was = pinned.interfaces.find((e) => e.id === publicName)?.methods.find((m) => m.name === member)?.endpoint;
15887
+ if (!was) return null;
15888
+ const now = live.interfaces.find((e) => e.id === publicName)?.methods.find((m) => m.name === member)?.endpoint;
15889
+ if (canonicalize(was) === canonicalize(now ?? null)) return null;
15890
+ return now ? { removed: false, detail: `endpoint moved: ${routeOf(was)} \u2192 ${routeOf(now)} \u2014 its signature is unchanged, but a client that spells the route (a hand-written HTTP client, a binding) must follow` } : { removed: true, detail: `endpoint ${routeOf(was)} removed \u2014 the verb is bound to no route any more, so a client calling it over the wire breaks` };
15891
+ }
15711
15892
  function compareUses(entry, usage, live, pinned = null) {
15712
15893
  const uses = [];
15713
15894
  const locked = entry?.used ?? {};
@@ -15717,11 +15898,21 @@ function compareUses(entry, usage, live, pinned = null) {
15717
15898
  uses.push(liveHas(publicName) ? { publicName, state: "unchanged" } : goneUse(live, publicName, void 0, void 0));
15718
15899
  continue;
15719
15900
  }
15720
- for (const [member, digest3] of Object.entries(members)) {
15901
+ for (const [member, locked2] of Object.entries(members)) {
15902
+ const digest3 = (pinned ? memberDigest(pinned, publicName, member) : null) ?? locked2;
15721
15903
  const now = memberDigest(live, publicName, member);
15722
- if (now === null) uses.push(goneUse(live, publicName, member, digest3, pinned));
15723
- else if (now === digest3) uses.push({ publicName, member, state: "unchanged" });
15724
- else uses.push({ publicName, member, state: "changed", ...changedDetail(pinned, live, publicName, member, digest3) });
15904
+ if (now === null) {
15905
+ uses.push(goneUse(live, publicName, member, digest3, pinned));
15906
+ continue;
15907
+ }
15908
+ const route2 = endpointMove(pinned, live, publicName, member);
15909
+ if (now === digest3) {
15910
+ uses.push(route2 === null ? { publicName, member, state: "unchanged" } : { publicName, member, state: route2.removed ? "changed" : "unchanged", detail: route2.detail });
15911
+ continue;
15912
+ }
15913
+ const changed = changedDetail(pinned, live, publicName, member, digest3);
15914
+ const detail = [changed.detail, route2?.detail].filter((d) => !!d).join("; ");
15915
+ uses.push({ publicName, member, state: "changed", ...detail ? { detail } : {} });
15725
15916
  }
15726
15917
  }
15727
15918
  const unlocked = (publicName, member) => {
@@ -15798,7 +15989,7 @@ function externalStatus(binding, lock) {
15798
15989
  const snapshot = entry !== void 0 ? externalsRepository.readSnapshot(external.alias) : null;
15799
15990
  const uses = compareUses(entry, binding.usage, live, snapshot);
15800
15991
  const staleFacts = snapshot ? carriedFactChanges(snapshot, live) : [];
15801
- const drifted = entry !== void 0 ? contentDigest(live) !== entry.digest || staleFacts.length > 0 : void 0;
15992
+ const drifted = entry !== void 0 ? contentDigest(live) !== (snapshot ? contentDigest(snapshot) : entry.digest) || staleFacts.length > 0 : void 0;
15802
15993
  return {
15803
15994
  ...base,
15804
15995
  reachable: true,
@@ -15808,6 +15999,48 @@ function externalStatus(binding, lock) {
15808
15999
  uses
15809
16000
  };
15810
16001
  }
16002
+ function projectedAt(directory) {
16003
+ try {
16004
+ return runWithProjectRoot(directory, () => projectOwnSurface("project"));
16005
+ } catch {
16006
+ return null;
16007
+ }
16008
+ }
16009
+ function memberApprovedSurfaces() {
16010
+ const out = {};
16011
+ for (const member of memberRevisions()) {
16012
+ if (!member.revision) continue;
16013
+ const approved = projectedAt(member.revision.directory);
16014
+ if (approved) out[member.alias] = approved;
16015
+ }
16016
+ return out;
16017
+ }
16018
+ function memberStatuses() {
16019
+ const out = [];
16020
+ for (const member of memberRevisions()) {
16021
+ if (!member.revision) continue;
16022
+ const live = projectedAt(member.root);
16023
+ const approved = projectedAt(member.revision.directory);
16024
+ if (!live || !approved) continue;
16025
+ const { used } = usedDigests(member.usage, approved, member.alias);
16026
+ const entry = { project: member.project, snapshot: "", digest: contentDigest(approved), used };
16027
+ const uses = compareUses(entry, void 0, live, approved).filter((u) => u.state !== "unlocked" && u.state !== "unavailable");
16028
+ const staleFacts = carriedFactChanges(approved, live);
16029
+ out.push({
16030
+ alias: member.alias,
16031
+ project: member.project,
16032
+ sourceKind: "family",
16033
+ pinned: false,
16034
+ reachable: true,
16035
+ stale: uses.some((u) => u.state === "changed" || u.state === "removed" || u.state === "renamed"),
16036
+ drifted: contentDigest(live) !== contentDigest(approved) || staleFacts.length > 0,
16037
+ ...staleFacts.length ? { staleFacts } : {},
16038
+ uses,
16039
+ detail: `a live member, compared with ${member.revision.label}`
16040
+ });
16041
+ }
16042
+ return out;
16043
+ }
15811
16044
  function hostedOnly(external) {
15812
16045
  return external.hosted !== void 0 && external.sourceKind === "unresolved" && getHostedLookup() === null;
15813
16046
  }
@@ -16155,11 +16388,11 @@ function declare(request) {
16155
16388
  } catch (e) {
16156
16389
  bindProblem = e instanceof Error ? e.message : String(e);
16157
16390
  }
16158
- const answered = binding ? readProducer(binding) : { why: bindProblem };
16159
- const producer = "read" in answered ? answered.read : null;
16391
+ const answered2 = binding ? readProducer(binding) : { why: bindProblem };
16392
+ const producer = "read" in answered2 ? answered2.read : null;
16160
16393
  const contradicted = contradiction(request, binding, producer);
16161
16394
  if (contradicted) return refused(alias, contradicted);
16162
- const why2 = "why" in answered ? answered.why : "unknown";
16395
+ const why2 = "why" in answered2 ? answered2.why : "unknown";
16163
16396
  if (request.dryRun) {
16164
16397
  return {
16165
16398
  alias,
@@ -16233,10 +16466,10 @@ function updateUse(request) {
16233
16466
  const named2 = appended.filter((u) => u !== "*");
16234
16467
  if (named2.length > 0) {
16235
16468
  const binding = resolveDeclared(true).find((b) => b.external.alias === alias);
16236
- const answered = binding ? readProducer(binding) : null;
16237
- if (answered && "read" in answered) {
16238
- const missing = named2.filter((u) => !answered.read.exported.includes(u));
16239
- if (missing.length) return useRefused(alias, current2, missing.map((u) => unexportedReason(u, answered.read)).join("; "));
16469
+ const answered2 = binding ? readProducer(binding) : null;
16470
+ if (answered2 && "read" in answered2) {
16471
+ const missing = named2.filter((u) => !answered2.read.exported.includes(u));
16472
+ if (missing.length) return useRefused(alias, current2, missing.map((u) => unexportedReason(u, answered2.read)).join("; "));
16240
16473
  }
16241
16474
  }
16242
16475
  const same = next.length === current2.length && next.every((u, i) => u === current2[i]);
@@ -16304,6 +16537,12 @@ function listExternals2() {
16304
16537
  function listPinnedExternals2() {
16305
16538
  return listPinnedExternals();
16306
16539
  }
16540
+ function memberApprovedSurfaces2() {
16541
+ return memberApprovedSurfaces();
16542
+ }
16543
+ function memberStatuses2() {
16544
+ return memberStatuses();
16545
+ }
16307
16546
  function unrecordedUses2() {
16308
16547
  return unrecordedUses();
16309
16548
  }
@@ -17684,12 +17923,11 @@ function adviseOn(status3, family, config) {
17684
17923
  `${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(status3.alias)}. Advisory: the pin still gates.`
17685
17924
  );
17686
17925
  }
17687
- case "drifted":
17688
- return advisory(
17689
- config,
17690
- "EXTERNAL_DRIFTED",
17691
- `The live producer of ${who} moved since it was pinned, but nothing this project uses changed.${staleTail(status3)} Re-pin when convenient (${repin(status3.alias)}).`
17692
- );
17926
+ case "drifted": {
17927
+ const routes = status3.uses.filter((u) => u.state === "unchanged" && u.detail?.startsWith("endpoint moved: "));
17928
+ const lead = routes.length ? `The live producer of ${who} moved since it was pinned: the route of ${routes.map((u) => `${useName(u)} (${u.detail.slice("endpoint moved: ".length).split(" \u2014 ")[0]})`).join(", ")} moved. The signatures this project uses are unchanged, but a client that spells the route (a hand-written HTTP client, a binding) must follow.` : `The live producer of ${who} moved since it was pinned, but nothing this project uses changed.`;
17929
+ return advisory(config, "EXTERNAL_DRIFTED", `${lead}${staleTail(status3)} Re-pin when convenient (${repin(status3.alias)}).`);
17930
+ }
17693
17931
  case "unavailable": {
17694
17932
  const beyond = status3.pinnedDigest !== void 0 && status3.reachable && !status3.outOfReach ? status3.uses.filter((u) => u.state === "unlocked") : [];
17695
17933
  if (beyond.length > 0 && status3.uses.every((u) => u.state !== "unavailable")) {
@@ -18134,7 +18372,7 @@ function failureAt(ref, failures) {
18134
18372
  const last = ref.resolved.split("::").pop() ?? "";
18135
18373
  return failures.find((i) => i.resolution.callSite === site && (i.resolution.canonicalTarget === ref.authored || (i.resolution.canonicalTarget ?? "").endsWith(`::${last}`)));
18136
18374
  }
18137
- function containedStatus(child, refs, failures) {
18375
+ function containedStatus(child, refs, failures, approved) {
18138
18376
  const base = { alias: child.mountAlias, project: child.id ?? child.namespace, sourceKind: "family", pinned: false };
18139
18377
  const unreachable = outsideReach(child);
18140
18378
  if (unreachable) {
@@ -18144,21 +18382,42 @@ function containedStatus(child, refs, failures) {
18144
18382
  const uses = [];
18145
18383
  for (const ref of refs) {
18146
18384
  const failed = failureAt(ref, failures);
18147
- const use = failed ? { publicName: referenceUse(ref), state: "removed", code: failed.code, detail: failed.resolution.reason } : { publicName: referenceUse(ref), state: "unchanged" };
18385
+ if (!failed) {
18386
+ const compared = (approved?.uses ?? []).filter((u) => u.publicName === referenceUse(ref));
18387
+ for (const use2 of compared.length ? compared : [{ publicName: referenceUse(ref), state: "unchanged" }]) {
18388
+ if (!uses.some((u) => u.publicName === use2.publicName && u.member === use2.member && u.state === use2.state)) uses.push(use2);
18389
+ }
18390
+ continue;
18391
+ }
18392
+ const use = { publicName: referenceUse(ref), state: "removed", code: failed.code, detail: failed.resolution.reason };
18148
18393
  if (!uses.some((u) => u.publicName === use.publicName && u.state === use.state)) uses.push(use);
18149
18394
  }
18150
- return { ...base, reachable: true, stale: uses.some((u) => u.state === "removed"), uses, detail: "a contained member, read live \u2014 nothing is pinned" };
18395
+ return {
18396
+ ...base,
18397
+ reachable: true,
18398
+ stale: uses.some((u) => u.state === "removed" || u.state === "changed" || u.state === "renamed"),
18399
+ ...approved?.drifted ? { drifted: true } : {},
18400
+ ...approved?.staleFacts?.length ? { staleFacts: approved.staleFacts } : {},
18401
+ uses,
18402
+ detail: approved ? `a live member, nothing pinned \u2014 ${approved.detail ?? "compared with its approval"}` : "a contained member, read live \u2014 nothing is pinned"
18403
+ };
18151
18404
  }
18152
- function containedRelations(answered) {
18405
+ function containedRelations(answered2) {
18153
18406
  const own2 = graph();
18154
- const consumed = own2.nodes.filter((n) => n.parent === "" && n.mountAlias !== void 0 && !answered.some((s) => s.alias === n.mountAlias)).map((child) => ({
18407
+ const consumed = own2.nodes.filter((n) => n.parent === "" && n.mountAlias !== void 0 && !answered2.some((s) => s.alias === n.mountAlias)).map((child) => ({
18155
18408
  child,
18156
18409
  refs: own2.authoredReferences.filter((r) => (own2.owners.get(r.specId) ?? "") === "" && r.producer === child.namespace && r.binding !== "local")
18157
18410
  })).filter((c) => c.refs.length > 0);
18158
18411
  if (consumed.length === 0) return [];
18159
18412
  const config = configOrNull();
18160
18413
  const failures = validateProject({ rules: config?.rules, projectType: config?.projectType }).issues.filter((i) => i.resolution !== void 0 && i.resolution.outcome !== "resolved");
18161
- return consumed.map(({ child, refs }) => containedStatus(child, refs, failures));
18414
+ let approved = [];
18415
+ try {
18416
+ approved = memberStatuses2();
18417
+ } catch {
18418
+ approved = [];
18419
+ }
18420
+ return consumed.map(({ child, refs }) => containedStatus(child, refs, failures, approved.find((s) => s.alias === child.mountAlias)));
18162
18421
  }
18163
18422
  function relationsAt(key2, dir, reach2) {
18164
18423
  const bound2 = (fn) => reach2 === null ? runWithProjectRoot(dir, fn) : runWithProjectBinding(dir, reach2, fn);
@@ -22789,20 +23048,61 @@ function duplicateRoutes(ctx) {
22789
23048
  }
22790
23049
  }
22791
23050
  }
22792
- var portalsRule;
23051
+ function answered(returns) {
23052
+ if (!returns) return null;
23053
+ const expr = parseTypeExpression(returns.trim(), "returns").expression;
23054
+ const awaited = expr?.form === "async" ? expr.args[0] : expr;
23055
+ return (awaited?.form === "result" ? awaited.args[0] : awaited) ?? null;
23056
+ }
23057
+ function isLocation(ctx, expr) {
23058
+ const inner2 = expr.form === "optional" ? expr.args[0] : expr;
23059
+ if (inner2.form === "primitive") return inner2.name === "string";
23060
+ if (inner2.form !== "named" || !inner2.name) return false;
23061
+ const local = inner2.name.slice(inner2.name.lastIndexOf("::") + (inner2.name.includes("::") ? 2 : 0)).split(".").pop() ?? inner2.name;
23062
+ const def = ctx.types.find((t) => t.id === inner2.name || t.id === local || t.id.endsWith(`::${local}`));
23063
+ return def?.holds?.trim() === "string";
23064
+ }
23065
+ function statusMismatches(ctx) {
23066
+ for (const comp of ctx.components) {
23067
+ if (comp.componentType !== "Portal" || comp.transport !== "HTTP") continue;
23068
+ const isDraftCtx = ctx.isComponentDraft(comp.id);
23069
+ for (const intf of ctx.interfaces.filter((i) => i.component === comp.id)) {
23070
+ const isIntfDraft = intf.status === "draft" || intf.status === "design";
23071
+ for (const m of intf.methods) {
23072
+ const endpoint = m.endpoint;
23073
+ if (endpoint?.transport !== "HTTP" || endpoint.status === void 0) continue;
23074
+ const status3 = endpoint.status;
23075
+ const answer = answered(m.returns);
23076
+ const value = answer !== null && !(answer.form === "primitive" && answer.name === "void");
23077
+ const problem = !STANDARD_STATUSES.has(status3) ? `${status3} is no standard success or redirect code (200-208, 226, 300-308), so no client library names it` : (status3 === 204 || status3 === 304) && value ? `${status3} carries no body, yet the method answers "${m.returns}" \u2014 the response would drop it` : REDIRECTS.has(status3) && value && answer !== null && !isLocation(ctx, answer) ? `${status3} is a redirect, answered with a Location header and no body, yet the method answers "${m.returns}" \u2014 a redirecting method answers the location itself (a string), so the response would drop it` : null;
23078
+ if (problem === null) continue;
23079
+ ctx.addIssue(
23080
+ "warning",
23081
+ "ENDPOINT_STATUS_MISMATCH",
23082
+ `Method "${m.name}" on interface "${intf.id}" binds ${endpoint.method ?? "HTTP"} ${endpoint.path ?? ""} with status ${status3}: ${problem}. State the status the method's answer allows, or change what it answers.`,
23083
+ intf.id,
23084
+ isDraftCtx || isIntfDraft
23085
+ );
23086
+ }
23087
+ }
23088
+ }
23089
+ }
23090
+ var portalsRule, STANDARD_STATUSES, REDIRECTS;
22793
23091
  var init_portal_endpoints = __esm({
22794
23092
  "src/core/rules/doctrine/portal-endpoints.ts"() {
22795
23093
  "use strict";
22796
23094
  init_models();
23095
+ init_type_grammar();
22797
23096
  portalsRule = {
22798
23097
  name: "portal-endpoints",
22799
23098
  judges: "design",
22800
- description: "A Portal binds every interface method to a concrete endpoint of its own transport, when that transport requires one (transport.requiresEndpoint): every transport but InProcess, whose verbs are the contract methods themselves, and Custom, whose address is free-form. An HTTP endpoint's path placeholders (`{name}`, or a `:name` segment) bind the method's parameters, so each must be closed, appear once and name a parameter of the method or a field of a parameter's type (a method that states no structured params is not judged on names). One HTTP route binds one verb: two methods bound to the same HTTP method and path \u2014 the Portal's basePath joined in, placeholders compared by position whatever their name or spelling (`{id}` and `:code` are the same segment) \u2014 within one Portal, or across Portals that declare the same basePath, are a duplicate route: no router can dispatch both and an OpenAPI document holds one operation per path and verb.",
23099
+ description: "A Portal binds every interface method to a concrete endpoint of its own transport, when that transport requires one (transport.requiresEndpoint): every transport but InProcess, whose verbs are the contract methods themselves, and Custom, whose address is free-form. An HTTP endpoint's path placeholders (`{name}`, or a `:name` segment) bind the method's parameters, so each must be closed, appear once and name a parameter of the method or a field of a parameter's type (a method that states no structured params is not judged on names). An HTTP endpoint's stated `status` is the success code its callers and its OpenAPI document read, so it must be a standard success or redirect code (200-208, 226, 300-308) and must agree with what the method answers: a 204 or a 304 carries no body, so a method that answers a value cannot state it, and a redirect sends the caller on with a Location header and no body, so the value a redirecting method answers is that location \u2014 a string (or a named type holding one), never a record. One HTTP route binds one verb: two methods bound to the same HTTP method and path \u2014 the Portal's basePath joined in, placeholders compared by position whatever their name or spelling (`{id}` and `:code` are the same segment) \u2014 within one Portal, or across Portals that declare the same basePath, are a duplicate route: no router can dispatch both and an OpenAPI document holds one operation per path and verb.",
22801
23100
  codes: [
22802
23101
  { code: "MISSING_ENDPOINT", defaultSeverity: "error", summary: "Portal method without a wire endpoint binding, on a transport that requires one" },
22803
23102
  { code: "ENDPOINT_TRANSPORT_MISMATCH", defaultSeverity: "error", summary: "Endpoint transport does not match the Portal's transport" },
22804
23103
  { code: "ENDPOINT_PATH_PLACEHOLDER", defaultSeverity: "error", summary: "An HTTP endpoint path holds an unclosed placeholder, one placeholder twice, or a placeholder that names no parameter of its method" },
22805
- { code: "ENDPOINT_ROUTE_DUPLICATE", defaultSeverity: "error", summary: "Two methods bound to the same HTTP method and path (placeholders compared by position) within one Portal, or across Portals declaring the same basePath" }
23104
+ { code: "ENDPOINT_ROUTE_DUPLICATE", defaultSeverity: "error", summary: "Two methods bound to the same HTTP method and path (placeholders compared by position) within one Portal, or across Portals declaring the same basePath" },
23105
+ { code: "ENDPOINT_STATUS_MISMATCH", defaultSeverity: "warning", summary: "An HTTP endpoint states a status that is no standard success or redirect code, or one that contradicts what its method answers: a 204 or 304 for a method answering a value, or a redirect for a method answering anything but the location (a string)" }
22806
23106
  ],
22807
23107
  check(ctx) {
22808
23108
  for (const comp of ctx.components) {
@@ -22846,9 +23146,12 @@ var init_portal_endpoints = __esm({
22846
23146
  }
22847
23147
  }
22848
23148
  }
23149
+ statusMismatches(ctx);
22849
23150
  duplicateRoutes(ctx);
22850
23151
  }
22851
23152
  };
23153
+ STANDARD_STATUSES = /* @__PURE__ */ new Set([200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308]);
23154
+ REDIRECTS = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
22852
23155
  }
22853
23156
  });
22854
23157
 
@@ -25912,10 +26215,11 @@ var init_method_realization = __esm({
25912
26215
  methodRealizationRule = {
25913
26216
  name: "method-realization",
25914
26217
  judges: "code",
25915
- description: "Code\u2194spec Level 1: every L3 contract method must be realized in its own source file (the method's sourcePath, else the implementation's) at its conformance tier \u2014 declared | anchored | off; Portals default to anchored, everything else to declared, and a per-method `symbol` maps an intent-language name onto the code name. A method realized by a DECLARATION owes a function BODY as well: at exact grade one must be reachable under the symbol, here or through the imports and republications this file forwards it by, so a signature, an ambient or interface declaration or a plain value binding stops reading as an implementation (METHOD_BODY_NOT_FOUND). Findings carry the analysis grade (exact AST | pattern table | generic scan) so weaker analysis is visible. Methods whose file escapes the root, is missing or could not be analyzed are left to source-file-linkage, and implementations under chained subsystems (projectPath) validate standalone in their own project run.",
26218
+ description: "Code\u2194spec Level 1: every L3 contract method must be realized in its own source file (the method's sourcePath, else the implementation's) at its conformance tier \u2014 declared | anchored | off; Portals default to anchored, everything else to declared, and a per-method `symbol` maps an intent-language name onto the code name. A method realized by a DECLARATION owes a function BODY as well: at exact grade one must be reachable under the symbol, here or through the imports and republications this file forwards it by, so a signature, an ambient or interface declaration or a plain value binding stops reading as an implementation (METHOD_BODY_NOT_FOUND). Findings carry the analysis grade (exact AST | pattern table | generic scan) so weaker analysis is visible. A dial turned off is said once per implementation as a notice (REALIZATION_UNCHECKED) naming the methods it covers: the dial is code linkage, outside the approval, so without it one line in an implementation spec would switch the realization checks off \u2014 method, parameters, async, whether narrated calls are found \u2014 with no surface saying so; the doctrine checks still run. Methods whose file escapes the root, is missing or could not be analyzed are left to source-file-linkage, and implementations under chained subsystems (projectPath) validate standalone in their own project run.",
25916
26219
  codes: [
25917
26220
  { code: "UNREALIZED_METHOD", defaultSeverity: "warning", summary: "An L3 contract method has no anchor in its own source file (the method's sourcePath, else the implementation's) at the required conformance tier" },
25918
- { code: "METHOD_BODY_NOT_FOUND", defaultSeverity: "warning", summary: "The method symbol IS declared in its own source file, but the file holds no function-like body under it \u2014 a signature, an ambient or interface declaration, a value binding, an imported or re-exported name", carryable: true }
26221
+ { code: "METHOD_BODY_NOT_FOUND", defaultSeverity: "warning", summary: "The method symbol IS declared in its own source file, but the file holds no function-like body under it \u2014 a signature, an ambient or interface declaration, a value binding, an imported or re-exported name", carryable: true },
26222
+ { code: "REALIZATION_UNCHECKED", defaultSeverity: "notice", summary: "An implementation's conformance dial is off, on itself or on some of its methods, so whether those methods are realized in its code \u2014 present, taking the contract's parameters, async as declared, making the calls their narratives claim \u2014 is not checked" }
25919
26223
  ],
25920
26224
  check(ctx) {
25921
26225
  const code = ctx.codeIndex();
@@ -25927,6 +26231,19 @@ var init_method_realization = __esm({
25927
26231
  if (ctx.isInChainedSubproject(component.subsystem)) continue;
25928
26232
  const draft = ctx.isImplementationDraft(impl);
25929
26233
  const specTier = impl.conformance ?? defaultConformanceTier(component);
26234
+ const offMethods = contract.methods.filter((method2) => (impl.methods.find((m) => m.name === method2.name)?.conformance ?? specTier) === "off").map((method2) => method2.name);
26235
+ if (specTier === "off" || offMethods.length > 0) {
26236
+ const covers = specTier === "off" && offMethods.length === contract.methods.length ? `on the implementation itself \u2014 every method of contract "${impl.contract}"` : `on ${offMethods.length} method(s) of contract "${impl.contract}" \u2014 ${offMethods.map((name) => `"${name}"`).join(", ")}`;
26237
+ ctx.addIssue(
26238
+ "notice",
26239
+ "REALIZATION_UNCHECKED",
26240
+ `Realization not checked: conformance off on "${impl.id}", ${covers}. Whether those methods are in the code, take the contract's parameters, are async as declared and make the calls their narratives claim is not compared; the doctrine checks (an unnarrated write, a call the analysis cannot follow, a Portal write shortcut, route coverage) still run. The dial is code linkage, outside the approval \u2014 this notice is where it shows. It is meant for generated or vendored code; drop it (sdd_update_spec unset conformance) to have the code compared again.`,
26241
+ impl.id,
26242
+ draft,
26243
+ void 0,
26244
+ { at: "conformance" }
26245
+ );
26246
+ }
25930
26247
  for (const method2 of contract.methods) {
25931
26248
  const methodImpl = impl.methods.find((m) => m.name === method2.name);
25932
26249
  const tier = methodImpl?.conformance ?? specTier;
@@ -27988,7 +28305,7 @@ function pairUp(declared, realized, score) {
27988
28305
  }
27989
28306
  return align(best);
27990
28307
  }
27991
- function judge2(declared, realized, injected, typeAgrees, expected) {
28308
+ function judge2(declared, realized, injected, typeAgrees, expected, typescript) {
27992
28309
  const declaredNames = new Set(declared.map((param) => bare(param.name)));
27993
28310
  const isInjected = (param) => param.name !== void 0 && injected.has(bare(param.name));
27994
28311
  let lead = 0;
@@ -27997,6 +28314,25 @@ function judge2(declared, realized, injected, typeAgrees, expected) {
27997
28314
  while (end > lead && isInjected(realized[end - 1]) && !declaredNames.has(bare(realized[end - 1].name))) end--;
27998
28315
  let taken = realized.map((param, at) => ({ param, at })).slice(lead, end);
27999
28316
  taken = taken.filter(({ param }) => !(param.name !== void 0 && param.name.startsWith("_") && param.unused === true && !declaredNames.has(bare(param.name))));
28317
+ const writtenAt = new Map(taken.map(({ param, at }) => [unitOf2(param, at), at]));
28318
+ let bundle;
28319
+ for (const entry of taken) {
28320
+ const fields = entry.param.fields;
28321
+ if (entry.param.kind !== "object" || !fields || fields.length === 0) continue;
28322
+ if (declared.some((d) => typeAgrees(d, entry.param) || structural(expected(d)?.fields, entry.param) === "all")) continue;
28323
+ const keys = new Set(fields.map(fieldKey));
28324
+ const takenElsewhere = new Set(taken.filter((other) => other !== entry && other.param.name !== void 0).map((other) => fieldKey(bare(other.param.name))));
28325
+ const covers = new Set(declared.flatMap((d, index) => {
28326
+ const key2 = fieldKey(bare(d.name));
28327
+ return keys.has(key2) && !takenElsewhere.has(key2) ? [index] : [];
28328
+ }));
28329
+ if (covers.size >= 2) {
28330
+ bundle = { entry, covers };
28331
+ break;
28332
+ }
28333
+ }
28334
+ if (bundle) taken = taken.filter((entry) => entry !== bundle?.entry);
28335
+ const pairable = declared.flatMap((param, index) => bundle?.covers.has(index) ? [] : [{ param, index }]);
28000
28336
  const score = (d, r) => {
28001
28337
  let total = 0;
28002
28338
  if (r.name !== void 0 && bare(r.name) === bare(d.name)) total += 4;
@@ -28008,7 +28344,7 @@ function judge2(declared, realized, injected, typeAgrees, expected) {
28008
28344
  else if (shape === "none") total -= 2;
28009
28345
  return total;
28010
28346
  };
28011
- const pairs = pairUp(declared, taken.map((entry) => entry.param), score);
28347
+ const pairs = pairUp(pairable.map((entry) => entry.param), taken.map((entry) => entry.param), score).map(([d, r]) => [pairable[d].index, r]);
28012
28348
  const wiringParams = [...realized.slice(0, lead), ...realized.slice(end)];
28013
28349
  const found = {
28014
28350
  unrealized: /* @__PURE__ */ new Map(),
@@ -28018,38 +28354,73 @@ function judge2(declared, realized, injected, typeAgrees, expected) {
28018
28354
  transport: /* @__PURE__ */ new Map(),
28019
28355
  wiring: realized.slice(0, lead).map((param) => param.name),
28020
28356
  trailing: realized.slice(end).map((param) => param.name),
28021
- carried: /* @__PURE__ */ new Map()
28357
+ carried: /* @__PURE__ */ new Map(),
28358
+ mismatched: /* @__PURE__ */ new Map()
28022
28359
  };
28360
+ if (bundle) {
28361
+ const { param: object, at } = bundle.entry;
28362
+ const bundled = declared.filter((_, index) => bundle?.covers.has(index)).map((param) => param.name);
28363
+ const label8 = object.name ? `"${object.name}"` : `parameter #${at + 1}`;
28364
+ for (const name of bundled) {
28365
+ found.unrealized.set(name, { unit: name, told: `"${name}" (the code takes it inside the object ${label8}, where the contract declares it as a parameter of its own)` });
28366
+ }
28367
+ const unit = unitOf2(object, at);
28368
+ found.undeclared.set(unit, { unit, told: `"${unit}" (an object bundling the contract's own parameters ${bundled.join(", ")})` });
28369
+ }
28023
28370
  const pairedDeclared = new Set(pairs.map(([d]) => d));
28024
28371
  const pairedTaken = new Set(pairs.map(([, r]) => r));
28025
28372
  const substituted = /* @__PURE__ */ new Set();
28026
28373
  for (const [d, r] of pairs) {
28027
28374
  const param = declared[d];
28028
- const { param: at, at: position2 } = taken[r];
28375
+ const { param: at, at: position } = taken[r];
28029
28376
  const sameName = at.name !== void 0 && bare(at.name) === bare(param.name);
28030
28377
  const want = expected(param);
28031
28378
  const agrees = typeAgrees(param, at);
28032
28379
  const shape = structural(want?.fields, at);
28033
28380
  const handleName = at.name !== void 0 && TRANSPORT_HANDLES.has(bare(at.name).toLowerCase()) && !TRANSPORT_HANDLES.has(bare(param.name).toLowerCase());
28381
+ const sameKind = !!want?.kinds && !!at.type && !!at.kind && want.kinds.has(at.kind) && (PRIMITIVE_KINDS.has(at.kind) || !!want.loose);
28034
28382
  let instead;
28035
28383
  if (!agrees) {
28036
28384
  if (!sameName && want?.kinds && at.type && at.kind && !want.kinds.has(at.kind)) instead = aKind(at.kind);
28037
28385
  else if (at.transport && want?.fields) instead = "a transport handle";
28038
28386
  else if (shape === "none") instead = `an object sharing none of its fields (${(want?.fields ?? []).join(", ")})`;
28039
- else if (handleName && (!at.kind || at.transport)) instead = "a transport handle";
28387
+ else if (handleName && shape !== "all" && !sameKind) instead = "a transport handle";
28040
28388
  }
28041
28389
  if (instead !== void 0) {
28042
28390
  substituted.add(r);
28043
28391
  const declares2 = want?.told ?? "an argument of its own";
28044
28392
  found.unrealized.set(param.name, {
28045
28393
  unit: param.name,
28046
- told: `"${param.name}" (the code takes ${at.name ? `"${at.name}"` : `parameter #${position2 + 1}`}, ${instead}, where the contract declares ${declares2})`
28394
+ told: `"${param.name}" (the code takes ${at.name ? `"${at.name}"` : `parameter #${position + 1}`}, ${instead}, where the contract declares ${declares2})`
28047
28395
  });
28048
- const unit = unitOf2(at, position2);
28396
+ const unit = unitOf2(at, position);
28049
28397
  found.undeclared.set(unit, { unit, told: `"${unit}" (${instead} in the place of the contract's "${param.name}", ${declares2})` });
28050
28398
  continue;
28051
28399
  }
28052
- const sameKind = !!want?.kinds && !!at.type && !!at.kind && want.kinds.has(at.kind) && (PRIMITIVE_KINDS.has(at.kind) || !!want.loose);
28400
+ if (!agrees && want?.fields) {
28401
+ const opaque = at.type !== void 0 && at.opaque === true || at.type === void 0 && typescript;
28402
+ let shapeTold;
28403
+ if (opaque) {
28404
+ shapeTold = `${at.type !== void 0 ? `typed "${at.type}"` : "with no annotation"}, a type that admits any value and names none of its fields (${want.fields.join(", ")})`;
28405
+ } else if (at.kind === "object" && at.fields && at.fields.length > 0) {
28406
+ const has = new Set(at.fields.map(fieldKey));
28407
+ const declaredKeys = new Set(want.fields.map(fieldKey));
28408
+ const missing = want.fields.filter((field) => !has.has(fieldKey(field)));
28409
+ const added = at.fields.filter((field) => !declaredKeys.has(fieldKey(field)));
28410
+ if (missing.length > 0 && missing.length < want.fields.length || missing.length === 0 && added.length > 0) {
28411
+ shapeTold = `an object ${[
28412
+ ...missing.length > 0 ? [`missing ${missing.map((field) => `"${field}"`).join(", ")}`] : [],
28413
+ ...added.length > 0 ? [`adding ${added.map((field) => `"${field}"`).join(", ")}`] : []
28414
+ ].join(" and ")}`;
28415
+ }
28416
+ }
28417
+ if (shapeTold !== void 0) {
28418
+ found.mismatched.set(param.name, {
28419
+ unit: param.name,
28420
+ told: `"${param.name}" (the code takes ${at.name ? `"${at.name}"` : `parameter #${position + 1}`} ${shapeTold}, where the contract declares ${want.told})`
28421
+ });
28422
+ }
28423
+ }
28053
28424
  if (at.name !== void 0 && !sameName && (agrees || sameKind || shape === "all")) {
28054
28425
  const former = (param.previousNames ?? []).some((name) => bare(name) === bare(at.name));
28055
28426
  found.renamed.set(`${param.name}\u2192${at.name}`, {
@@ -28065,9 +28436,12 @@ function judge2(declared, realized, injected, typeAgrees, expected) {
28065
28436
  }
28066
28437
  }
28067
28438
  const requestReads = new Set(wiringParams.filter((param) => param.name !== void 0 && REQUEST_HANDLES.has(bare(param.name).toLowerCase())).flatMap((param) => (param.reads ?? []).map(bare)));
28439
+ const left = declared.filter((param, d) => !pairedDeclared.has(d) && !bundle?.covers.has(d) && !requestReads.has(bare(param.name)));
28440
+ const records = left.filter((param) => (expected(param)?.fields?.length ?? 0) > 0);
28441
+ const bodyRecord = requestReads.has("body") && records.length === 1 ? records[0].name : void 0;
28068
28442
  declared.forEach((param, d) => {
28069
- if (pairedDeclared.has(d)) return;
28070
- if (requestReads.has(bare(param.name))) {
28443
+ if (pairedDeclared.has(d) || bundle?.covers.has(d)) return;
28444
+ if (requestReads.has(bare(param.name)) || param.name === bodyRecord) {
28071
28445
  found.carried.set(param.name, { unit: param.name, told: param.name });
28072
28446
  return;
28073
28447
  }
@@ -28084,8 +28458,7 @@ function judge2(declared, realized, injected, typeAgrees, expected) {
28084
28458
  if (pairedTaken.has(r) && !substituted.has(r)) return;
28085
28459
  if (HANDLE_NAMES.has(bare(param.name).toLowerCase())) found.transport.set(param.name, { unit: unitOf2(param, at), told: param.name });
28086
28460
  });
28087
- const position = new Map(taken.map(({ param, at }) => [unitOf2(param, at), at]));
28088
- found.undeclared = new Map([...found.undeclared].sort(([a], [b]) => (position.get(a) ?? 0) - (position.get(b) ?? 0)));
28461
+ found.undeclared = new Map([...found.undeclared].sort(([a], [b]) => (writtenAt.get(a) ?? 0) - (writtenAt.get(b) ?? 0)));
28089
28462
  return found;
28090
28463
  }
28091
28464
  function agreed(judgements, reading) {
@@ -28136,7 +28509,7 @@ var init_param_conformance = __esm({
28136
28509
  init_models();
28137
28510
  HANDLE_NAMES = /* @__PURE__ */ new Set(["req", "request", "res", "response", "reply", "url", "ctx", "context", "next", "event", "socket"]);
28138
28511
  TRANSPORT_HANDLES = /* @__PURE__ */ new Set(["req", "res", "ctx", "request", "response", "next", "reply"]);
28139
- REQUEST_HANDLES = /* @__PURE__ */ new Set(["req", "request", "ctx", "context", "event"]);
28512
+ REQUEST_HANDLES = /* @__PURE__ */ new Set(["req", "request", "ctx", "context", "event", "url"]);
28140
28513
  PRIMITIVE_KINDS = /* @__PURE__ */ new Set(["string", "number", "boolean"]);
28141
28514
  aKind = (kind) => kind === "object" ? "an object" : `a ${kind}`;
28142
28515
  fieldKey = (name) => name.toLowerCase().replace(/[_-]/g, "");
@@ -28144,7 +28517,7 @@ var init_param_conformance = __esm({
28144
28517
  paramConformanceRule = {
28145
28518
  name: "param-conformance",
28146
28519
  judges: "code",
28147
- 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.",
28520
+ 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. An OBJECT whose properties name two or more of the contract's own parameters BUNDLES them and is set apart first: each parameter it bundles is unrealized (the code takes it inside the object) and the object undeclared, so `cancelOrder(input: { orderId, customerId })` realizing `cancelOrder(orderId, customerId)` names both, never one half by position. 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, a MEMBER's through the name its own type spec gives it, read live from the family with its kind and fields \u2014 so where a parameter's type lives never decides whether it is compared \u2014 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 data 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. 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 unless its type agrees with the declared one or holds every field of the declared record \u2014 however the handle is typed, a hand-written `type Req = { params: \u2026 }` included: `cancelOrder(req, res)` realizing `cancelOrder(orderId, customerId)` is a handler's shape, not the contract's. Where the contract's type is a record, a paired parameter whose type cannot hold it fails closed, as an opaque receiver does (PARAM_TYPE_MISMATCH): an OPAQUE type \u2014 any, unknown, object, {}, an index signature alone (`Record<string, unknown>`), or no annotation at all in a TypeScript file \u2014 names no field the analysis could compare, and an object sharing some of the record's fields but not all of them, or adding fields the record does not declare, is another shape; the finding names the fields missing and the fields added, so `planRoute(request: any)` and `planRoute(request: { stops: unknown[]; secret: string })` realizing `planRoute(request: plan_request)` are never silent pairings. 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, url) by its own name is realized through that handle \u2014 `req.params.code`, `req.query.limit`, `req.body.code`, `url.searchParams.get('limit')`, a destructuring at any depth, a local the handle's value flows into (`const url = new URL(req.url, base)`), and what a helper the handle is passed to reads off it or its result yields, where the helper is written in the same file or in a file no implementation claims (`const { orderId } = pathParams(req)`) \u2014 and the one RECORD parameter left unpaired is realized by the request's body read whole (`req.body`). An injected name none of the implementation's realizing functions read in this run takes is said as a NOTICE (UNUSED_INJECTED_PARAM): an injection is code linkage outside the approval, and one nothing takes hides nothing, since no function takes a parameter it could set aside \u2014 so it never fails a gate. 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.",
28148
28521
  codes: [
28149
28522
  {
28150
28523
  code: "UNREALIZED_PARAM",
@@ -28168,8 +28541,14 @@ var init_param_conformance = __esm({
28168
28541
  },
28169
28542
  {
28170
28543
  code: "UNUSED_INJECTED_PARAM",
28544
+ defaultSeverity: "notice",
28545
+ summary: "An implementation declares an injected parameter that none of its realizing functions takes \u2014 stale code linkage, said so it can be dropped; nothing takes it, so it hides nothing and never fails a gate",
28546
+ carryable: true
28547
+ },
28548
+ {
28549
+ code: "PARAM_TYPE_MISMATCH",
28171
28550
  defaultSeverity: "warning",
28172
- 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",
28551
+ summary: "A contract parameter declared as a record is realized by a parameter whose type cannot hold it \u2014 opaque (any, unknown, an index signature) or an object missing the record's fields or adding its own \u2014 so what a caller hands over is never compared with what the design declares",
28173
28552
  carryable: true
28174
28553
  },
28175
28554
  {
@@ -28207,7 +28586,28 @@ var init_param_conformance = __esm({
28207
28586
  typeNamed.set(`${pin2.alias}::${exported.id}`, type);
28208
28587
  }
28209
28588
  }
28210
- const externalCodeName = (ref) => (ref.split("::").pop() ?? ref).split(/[_\-\s]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
28589
+ const family = ctx.projectFamily?.nodes ?? [];
28590
+ const root = family.find((node) => node.namespace === "");
28591
+ const memberTypes = /* @__PURE__ */ new Map();
28592
+ const lastOf = (id) => id.split(/::|\./).pop() ?? id;
28593
+ const memberType = (ref) => {
28594
+ const cut = ref.indexOf("::");
28595
+ if (cut <= 0) return void 0;
28596
+ if (memberTypes.has(ref)) return memberTypes.get(ref) ?? void 0;
28597
+ const segment = ref.slice(0, cut);
28598
+ const local = lastOf(ref.slice(cut + 2));
28599
+ const key2 = root?.aliases.get(segment) ?? family.find((node2) => node2.aliases.has(segment))?.aliases.get(segment) ?? segment;
28600
+ const node = family.find((n) => n.namespace !== "" && (n.namespace === key2 || n.mountAlias === segment || n.id === segment));
28601
+ const found = node ? ctx.types.find((t) => t.id === `${node.namespace}::${local}`) ?? ctx.types.find((t) => t.id.startsWith(`${node.namespace}::`) && lastOf(t.id) === local) : void 0;
28602
+ memberTypes.set(ref, found ?? null);
28603
+ return found;
28604
+ };
28605
+ const typeOf = (name) => typeNamed.get(name) ?? memberType(name);
28606
+ const externalCodeName = (ref) => {
28607
+ const member = memberType(ref);
28608
+ if (member) return member.symbol ?? member.name;
28609
+ return (ref.split("::").pop() ?? ref).split(/[_\-\s]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
28610
+ };
28211
28611
  const typeAgreesIn = (dialect) => (declared, realized) => {
28212
28612
  if (!realized.type || !dialect) return false;
28213
28613
  const stated = parseTypePosition(declared.type, "param", !!declared.optional);
@@ -28231,9 +28631,9 @@ var init_param_conformance = __esm({
28231
28631
  };
28232
28632
  if (expression.name && loose[expression.name]) return { kinds: new Set(loose[expression.name]), loose: true, told: `a ${expression.name}` };
28233
28633
  }
28234
- const kind = expressionKind(expression, (name) => typeKind(typeNamed.get(name)));
28634
+ const kind = expressionKind(expression, (name) => typeKind(typeOf(name)));
28235
28635
  if (expression.form === "named" && expression.name) {
28236
- const type = typeNamed.get(expression.name);
28636
+ const type = typeOf(expression.name);
28237
28637
  const fields = (type?.fields ?? []).map((field) => field && typeof field === "object" ? field.name : void 0).filter((name) => typeof name === "string");
28238
28638
  if (fields.length > 0 && kind === "object") return { kinds: /* @__PURE__ */ new Set(["object"]), fields, told: `the record "${expression.name}"` };
28239
28639
  }
@@ -28260,7 +28660,8 @@ var init_param_conformance = __esm({
28260
28660
  if (declared.length === 0) continue;
28261
28661
  const injected = new Set((implementation.injectedParams ?? []).map(bare));
28262
28662
  const typeAgrees = typeAgreesIn(dialectOf(facts));
28263
- const judgements = candidates.map((realized) => judge2(declared, realized, injected, typeAgrees, expected));
28663
+ const typescript = facts.language === "typescript";
28664
+ const judgements = candidates.map((realized) => judge2(declared, realized, injected, typeAgrees, expected, typescript));
28264
28665
  const subject = candidates.length > 1 ? `every function called "${symbol}" in "${file}"` : `the function "${symbol}" in "${file}"`;
28265
28666
  const opening = subject.charAt(0).toUpperCase() + subject.slice(1);
28266
28667
  const transport = agreed(judgements, "transport").map((found) => found.told);
@@ -28291,6 +28692,18 @@ var init_param_conformance = __esm({
28291
28692
  { at: method2.name, covers: undeclared.map((found) => found.unit) }
28292
28693
  );
28293
28694
  }
28695
+ const mismatched = agreed(judgements, "mismatched");
28696
+ if (mismatched.length > 0) {
28697
+ ctx.addIssue(
28698
+ "warning",
28699
+ "PARAM_TYPE_MISMATCH",
28700
+ `Method "${method2.name}" of contract "${implementation.contract}" declares ${mismatched.length} record parameter(s) ${subject} takes under a type that cannot hold the record \u2014 ${mismatched.map((found) => found.told).join("; ")}. Whatever a caller hands over there is never compared with what the design declares \u2014 an opaque type names no field to compare, and an object missing the record's fields or adding its own is another shape, the one a credential rides in unread \u2014 so the check fails closed on it, as it does on an opaque receiver. Type the parameter as the record (its code name), or declare on the contract the shape the code really takes.`,
28701
+ implementation.id,
28702
+ draftContext4,
28703
+ void 0,
28704
+ { at: method2.name, covers: mismatched.map((found) => found.unit) }
28705
+ );
28706
+ }
28294
28707
  const renamed2 = agreed(judgements, "renamed");
28295
28708
  if (renamed2.length > 0) {
28296
28709
  ctx.addIssue(
@@ -28323,9 +28736,9 @@ var init_param_conformance = __esm({
28323
28736
  const stale = injected.filter((name) => !taken.has(bare(name)));
28324
28737
  if (stale.length === 0) continue;
28325
28738
  ctx.addIssue(
28326
- "warning",
28739
+ "notice",
28327
28740
  "UNUSED_INJECTED_PARAM",
28328
- `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.`,
28741
+ `Implementation "${implementation.id}" declares ${stale.length} injected parameter(s) none of its realizing functions takes \u2014 ${stale.map((name) => `"${name}"`).join(", ")}. An injection is code linkage, outside the approval, and one no function takes hides nothing: there is no parameter of that name for it to set aside, so it never fails a gate. It is stale linkage all the same \u2014 drop it from injectedParams (sdd_update_spec, no re-lock), or take it in the handler that is handed it.`,
28329
28742
  implementation.id,
28330
28743
  ctx.isImplementationDraft(implementation),
28331
28744
  void 0,
@@ -28435,7 +28848,14 @@ function memberSurface(ctx, node, alias) {
28435
28848
  ...m,
28436
28849
  formerly: (m.previousNames ?? []).map(lastSegment4)
28437
28850
  }));
28438
- entries.push({ alias, entry: { id: e.publicName, name: e.publicName, component: e.component, methods } });
28851
+ const approved = ctx.memberApprovedSurfaces?.[alias]?.interfaces.find((x) => x.id === e.publicName);
28852
+ const kept = new Set(methods.flatMap((m) => [m.name, ...m.formerly ?? []]).map(nameKey));
28853
+ const retired = (approved?.methods ?? []).map((m) => m.name).filter((n) => !kept.has(nameKey(n)));
28854
+ entries.push({
28855
+ alias,
28856
+ entry: { id: e.publicName, name: e.publicName, component: e.component, methods, ...retired.length ? { retired } : {} },
28857
+ ...retired.length ? { approved: true } : {}
28858
+ });
28439
28859
  for (const m of methods) pending4.push(...methodTypeRefs(m));
28440
28860
  } else if (e.kind === "type" && e.typeDef) {
28441
28861
  const type = memberType(e.typeDef);
@@ -28530,7 +28950,7 @@ function methodDrift(where, params, entry, reportMissing, method2) {
28530
28950
  if (current2) return paramDrift(where, params, current2);
28531
28951
  const renamed2 = entry.entry.methods.find((m) => (m.formerly ?? []).some((f) => nameKey(f) === key2));
28532
28952
  if (renamed2) return [`"${method2 ?? where}" was renamed to "${renamed2.name}" in ${entry.alias}::${entry.entry.id} \u2014 follow the rename`];
28533
- if (retiredIn(entry, key2)) return [`"${method2 ?? where}" was removed from ${entry.alias}::${entry.entry.id} (an earlier pin held it) \u2014 drop it from the binding, and the calls that use it`];
28953
+ if (retiredIn(entry, key2)) return [`"${method2 ?? where}" was removed from ${entry.alias}::${entry.entry.id} (${entry.approved ? "the member's approved design held it" : "an earlier pin held it"}) \u2014 drop it from the binding, and the calls that use it`];
28534
28954
  return reportMissing ? [`"${method2 ?? where}" is not exported by ${entry.alias}::${entry.entry.id} (it holds ${entry.entry.methods.map((m) => `"${m.name}"`).join(", ") || "no methods"})`] : [];
28535
28955
  }
28536
28956
  function formerType(name, types) {
@@ -28644,6 +29064,16 @@ function reachOf(ctx, segments) {
28644
29064
  const members = [];
28645
29065
  const unpinned2 = [];
28646
29066
  const externals = new Set((boundRoot(ctx)?.imports ?? []).filter((i) => i.section === "externals").map((i) => i.alias));
29067
+ const ownId = boundRoot(ctx)?.id;
29068
+ if (ownId !== void 0 && segments.has(ownId)) {
29069
+ segments = new Set(segments);
29070
+ for (const table of (ctx.exportTables ?? []).filter((x) => x.level === "project" && !(ctx.projectFamily?.nodes ?? []).some((n) => n.namespace !== "" && n.namespace === x.owner))) {
29071
+ for (const e of table.entries) {
29072
+ const target = e.typeDef ?? e.component ?? "";
29073
+ if (target.includes("::")) segments.add(target.slice(0, target.indexOf("::")));
29074
+ }
29075
+ }
29076
+ }
28647
29077
  for (const segment of segments) {
28648
29078
  if (pins.some((p) => p.alias === segment)) continue;
28649
29079
  const node = memberNode(ctx, segment);
@@ -28782,7 +29212,7 @@ var init_route_coverage = __esm({
28782
29212
  routeCoverageRule = {
28783
29213
  name: "route-coverage",
28784
29214
  judges: "code",
28785
- 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.",
29215
+ 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 \u2014 the same entry or table in the same file, however each linkage spells it \u2014 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; an array may spread other tables and an object other objects, `[...ingestRoutes, ...statsRoutes]`, each followed through the type checker to the table it names in any module), 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. Route coverage is never silently off: a router that yields no route in either idiom, or a linkage naming a declaration that holds neither a function body nor a route table this analysis reads, is reported as unread; and a Portal binding HTTP endpoints whose implementation names no router while its code is read at exact grade is said as a notice (ROUTER_UNDECLARED) naming `router:` and the route-bearing names its files hold \u2014 a check that cannot see a router must say so rather than stay quiet.",
28786
29216
  codes: [
28787
29217
  {
28788
29218
  code: "UNDECLARED_ROUTE",
@@ -28803,17 +29233,50 @@ var init_route_coverage = __esm({
28803
29233
  defaultSeverity: "warning",
28804
29234
  summary: "A Portal's router entry yields no route this analysis can read, so its routes were not checked against the contract at all",
28805
29235
  carryable: true
29236
+ },
29237
+ {
29238
+ code: "ROUTER_UNDECLARED",
29239
+ defaultSeverity: "notice",
29240
+ summary: "A Portal binds HTTP endpoints but its implementation names no router (`router:`), so none of the routes its code serves were checked against its contract"
28806
29241
  }
28807
29242
  ],
28808
29243
  check(ctx) {
28809
29244
  const code = ctx.codeIndex();
28810
29245
  const realization = ctx.realizationIndex();
28811
29246
  const groups = /* @__PURE__ */ new Map();
29247
+ const httpEndpoints = (portal) => ctx.interfaceMethodsOf(portal.id).flatMap((method2) => {
29248
+ const endpoint = method2.endpoint;
29249
+ if (endpoint?.transport !== "HTTP") return [];
29250
+ return [{
29251
+ verb: endpoint.method.toUpperCase(),
29252
+ segments: endpoint.path.split("/").filter(Boolean).map(normalizeSegment),
29253
+ key: `${endpoint.method.toUpperCase()} ${endpoint.path}`
29254
+ }];
29255
+ });
29256
+ const exactAt = (file) => {
29257
+ const facts = code.factsAt(file);
29258
+ return !!facts && facts.status === "analyzed" && facts.analysisGrade === "exact";
29259
+ };
28812
29260
  for (const portal of ctx.components) {
28813
29261
  if (portal.componentType !== "Portal") continue;
28814
29262
  for (const impl of realization.implementationsOf(portal.id)) {
28815
29263
  const via = impl.router;
28816
- if (!via) continue;
29264
+ if (!via) {
29265
+ const endpoints2 = httpEndpoints(portal);
29266
+ const files2 = realization.filesOf(portal.id).map(pathKey).filter(exactAt);
29267
+ if (endpoints2.length === 0 || files2.length === 0) continue;
29268
+ const held = [...new Set(files2.flatMap((file) => Object.entries(code.factsAt(file)?.functionRoutes ?? {}).filter(([, routes]) => routes.length > 0).map(([name]) => `${name} (${file})`)))].sort();
29269
+ ctx.addIssue(
29270
+ "notice",
29271
+ "ROUTER_UNDECLARED",
29272
+ `Portal "${portal.id}" binds ${endpoints2.length} HTTP endpoint(s), but its implementation "${impl.id}" names no router (\`router:\`), so no route its code serves was checked against its contract \u2014 not a route the design never promised, not an endpoint no route answers. Name the router in the implementation's \`router:\` (code linkage, no re-lock): an entry its own files export, \`<module>#<name>\` for an entry or a route table another module holds, or a module alone. ` + (held.length > 0 ? `Its files hold what reads as a router or a route table: ${held.join(", ")}.` : "Its files hold no function or table that reads as a router \u2014 when another module routes this Portal, name that one."),
29273
+ impl.id,
29274
+ ctx.isComponentDraft(portal.id) || ctx.isImplementationDraft(impl),
29275
+ void 0,
29276
+ { at: "router" }
29277
+ );
29278
+ continue;
29279
+ }
28817
29280
  const linkage = routerLinkage(via);
28818
29281
  const files = linkage.file ? [pathKey(linkage.file)] : realization.filesOf(portal.id).map(pathKey);
28819
29282
  const exact = files.filter((file) => {
@@ -28825,7 +29288,22 @@ var init_route_coverage = __esm({
28825
29288
  return !!facts.functionParams && Object.prototype.hasOwnProperty.call(facts.functionParams, name) || !!facts.functionRoutes && Object.prototype.hasOwnProperty.call(facts.functionRoutes, name);
28826
29289
  };
28827
29290
  const holders = linkage.name !== void 0 ? exact.filter((file) => holds(file, linkage.name)) : exact;
28828
- if (holders.length === 0) continue;
29291
+ if (holders.length === 0) {
29292
+ const name = linkage.name;
29293
+ const declaring = exact.filter((file) => code.declarationsAt(file).has(name));
29294
+ if (declaring.length > 0) {
29295
+ ctx.addIssue(
29296
+ "warning",
29297
+ "UNREADABLE_ROUTER",
29298
+ `Portal "${portal.id}" names its router "${via}" (in ${declaring.map((file) => `"${file}"`).join(", ")}), but "${name}" there holds neither a function body nor a route table this analysis reads \u2014 so none of its routes were checked against the contract at all. A table must settle in every entry (a spread included: each spread must name a table that settles), and a router must be a function the file declares. Write it so, or carry this finding with the reason it cannot be.`,
29299
+ impl.id,
29300
+ ctx.isComponentDraft(portal.id) || ctx.isImplementationDraft(impl),
29301
+ void 0,
29302
+ { at: via }
29303
+ );
29304
+ }
29305
+ continue;
29306
+ }
28829
29307
  const prefixes = /* @__PURE__ */ new Set();
28830
29308
  const read2 = holders.flatMap((file) => {
28831
29309
  const facts = code.factsAt(file);
@@ -28844,15 +29322,7 @@ var init_route_coverage = __esm({
28844
29322
  }
28845
29323
  return [...names].flatMap((name) => Object.prototype.hasOwnProperty.call(routes, name) ? routes[name] : []);
28846
29324
  });
28847
- const endpoints = ctx.interfaceMethodsOf(portal.id).flatMap((method2) => {
28848
- const endpoint = method2.endpoint;
28849
- if (endpoint?.transport !== "HTTP") return [];
28850
- return [{
28851
- verb: endpoint.method.toUpperCase(),
28852
- segments: endpoint.path.split("/").filter(Boolean).map(normalizeSegment),
28853
- key: `${endpoint.method.toUpperCase()} ${endpoint.path}`
28854
- }];
28855
- });
29325
+ const endpoints = httpEndpoints(portal);
28856
29326
  const heads = [...new Set(endpoints.map((endpoint) => endpoint.segments[0]).filter((head2) => !!head2 && head2 !== "*"))].sort();
28857
29327
  const router = {
28858
29328
  portal,
@@ -28866,7 +29336,7 @@ var init_route_coverage = __esm({
28866
29336
  draftContext: ctx.isComponentDraft(portal.id) || ctx.isImplementationDraft(impl),
28867
29337
  ...prefixes.size === 1 ? { prefix: [...prefixes][0] } : {}
28868
29338
  };
28869
- const key2 = linkage.file ? `${pathKey(linkage.file)}#${linkage.name ?? "*"}` : `${impl.id}#${via}`;
29339
+ const key2 = `${[...holders].sort().join("|")}#${linkage.name ?? "*"}`;
28870
29340
  groups.set(key2, [...groups.get(key2) ?? [], router]);
28871
29341
  }
28872
29342
  }
@@ -29570,15 +30040,31 @@ var init_technology_binding = __esm({
29570
30040
  technologyBindingRule = {
29571
30041
  name: "technology-binding",
29572
30042
  judges: "design",
29573
- description: "Only the stereotypes that sit at a technology seam (Adapter/Store/Registry/Index, and Observer \u2014 the edge block that subscribes to a messaging technology) should bind a technology directly: an L4 that declares `technologies` on logic has no swap seam. No hardcoded vendor lists \u2014 only declared tokens are policed, so the rule never fires on a tree that doesn't opt in.",
30043
+ description: "Only the stereotypes that sit at a technology seam (Adapter/Store/Registry/Index, Observer \u2014 the edge block that subscribes to a messaging technology \u2014 and a Portal, the transport seam, which binds the framework that serves its endpoints) should bind a technology directly: an L4 that declares `technologies` on logic has no swap seam. A Portal binding a technology an Adapter, Store, Registry, Index or Observer of the tree also binds is reaching past its own seam into theirs, and is reported. No hardcoded vendor lists \u2014 only declared tokens are policed, so the rule never fires on a tree that doesn't opt in.",
29574
30044
  codes: [
29575
- { code: "TECH_ON_LOGIC_COMPONENT", defaultSeverity: "warning", summary: "Technology bound by a non-data-layer stereotype" }
30045
+ { code: "TECH_ON_LOGIC_COMPONENT", defaultSeverity: "warning", summary: "Technology bound by a stereotype that sits at no technology seam, or a Portal binding a technology a data-layer stereotype binds" }
29576
30046
  ],
29577
30047
  check(ctx) {
30048
+ const componentOf = (contract) => {
30049
+ const intf = ctx.interfaceMap.get(contract);
30050
+ return intf ? ctx.componentMap.get(intf.component) : void 0;
30051
+ };
30052
+ const dataLayer = new Set(ctx.implementations.filter((impl) => DATA_LAYER.has(componentOf(impl.contract)?.componentType ?? "")).flatMap((impl) => (impl.technologies ?? []).map((t) => technologyName(t).toLowerCase())));
29578
30053
  for (const impl of ctx.implementations) {
29579
- const intf = ctx.interfaceMap.get(impl.contract);
29580
- const comp = intf ? ctx.componentMap.get(intf.component) : void 0;
30054
+ const comp = componentOf(impl.contract);
29581
30055
  if (!impl.technologies?.length || !comp || DATA_LAYER.has(comp.componentType)) continue;
30056
+ if (comp.componentType === "Portal") {
30057
+ const reached = impl.technologies.map(technologyName).filter((name) => dataLayer.has(name.toLowerCase()));
30058
+ if (reached.length === 0) continue;
30059
+ ctx.addIssue(
30060
+ "warning",
30061
+ "TECH_ON_LOGIC_COMPONENT",
30062
+ `Implementation "${impl.id}" binds ${reached.join(", ")} on Portal "${comp.id}", a technology a data-layer component of this tree binds too. A Portal is the transport seam: it binds the framework that serves its endpoints, and reaches the data layer only through the components that bind it \u2014 drop ${reached.length === 1 ? "it" : "them"} from this implementation's technologies.`,
30063
+ impl.id,
30064
+ ctx.isImplementationDraft(impl)
30065
+ );
30066
+ continue;
30067
+ }
29582
30068
  ctx.addIssue(
29583
30069
  "warning",
29584
30070
  "TECH_ON_LOGIC_COMPONENT",
@@ -31197,11 +31683,26 @@ function tableEntry(ts, entry, initializerOf, literalOf) {
31197
31683
  const route2 = keyedPath ?? (paths.length === 1 ? paths[0] : void 0);
31198
31684
  return verb && route2 ? { verb, segments: route2.segments, exactLength: route2.exactLength, fullPath: true, table: true } : void 0;
31199
31685
  }
31200
- function tableRoutes(ts, value, initializerOf, literalOf) {
31686
+ function tableRoutes(ts, value, initializerOf, literalOf, depth = 0) {
31687
+ if (depth > SETTLE_DEPTH) return [];
31201
31688
  const at = bareExpression(ts, value);
31202
31689
  const out = [];
31690
+ const spread = (expression) => {
31691
+ const named2 = bareExpression(ts, expression);
31692
+ if (!ts.isIdentifier(named2) && !ts.isPropertyAccessExpression(named2)) return void 0;
31693
+ const initializer = initializerOf(named2);
31694
+ if (!initializer) return void 0;
31695
+ const inner2 = tableRoutes(ts, initializer, initializerOf, literalOf, depth + 1);
31696
+ return inner2.length > 0 ? inner2 : void 0;
31697
+ };
31203
31698
  if (ts.isArrayLiteralExpression(at)) {
31204
31699
  for (const element of at.elements) {
31700
+ if (ts.isSpreadElement(element)) {
31701
+ const routes = spread(element.expression);
31702
+ if (!routes) return [];
31703
+ out.push(...routes);
31704
+ continue;
31705
+ }
31205
31706
  const entry = bareExpression(ts, element);
31206
31707
  if (!ts.isObjectLiteralExpression(entry)) return [];
31207
31708
  const route2 = tableEntry(ts, entry, initializerOf, literalOf);
@@ -31212,6 +31713,12 @@ function tableRoutes(ts, value, initializerOf, literalOf) {
31212
31713
  }
31213
31714
  if (ts.isObjectLiteralExpression(at)) {
31214
31715
  for (const property of at.properties) {
31716
+ if (ts.isSpreadAssignment(property)) {
31717
+ const routes = spread(property.expression);
31718
+ if (!routes) return [];
31719
+ out.push(...routes);
31720
+ continue;
31721
+ }
31215
31722
  const key2 = !property.name ? void 0 : ts.isComputedPropertyName(property.name) ? settleString(ts, property.name.expression, initializerOf, literalOf) : propertyKeyText(ts, property.name);
31216
31723
  const match2 = key2 ? /^([A-Za-z]+)\s+(\/\S*)$/.exec(key2) : null;
31217
31724
  if (!match2 || !HTTP_VERBS.has(match2[1].toUpperCase())) return [];
@@ -31311,7 +31818,7 @@ function readRoutes(ts, fn, initializerOf, literalOf) {
31311
31818
  return void 0;
31312
31819
  };
31313
31820
  const prefix = strippedPrefix(ts, fn.body, initializerOf, literalOf);
31314
- const routeOf = (conjuncts) => {
31821
+ const routeOf2 = (conjuncts) => {
31315
31822
  let verb;
31316
31823
  let length;
31317
31824
  const fixed = /* @__PURE__ */ new Map();
@@ -31332,7 +31839,7 @@ function readRoutes(ts, fn, initializerOf, literalOf) {
31332
31839
  if (namedFunctionNameOf(ts, node) !== void 0) return;
31333
31840
  if (ts.isIfStatement(node)) {
31334
31841
  const guarded2 = [...enclosing2, ...conjunctsOf(node.expression)];
31335
- const route2 = routeOf(guarded2);
31842
+ const route2 = routeOf2(guarded2);
31336
31843
  if (route2) routes.set(routeKey2(route2), route2);
31337
31844
  walk2(node.thenStatement, guarded2);
31338
31845
  if (node.elseStatement) walk2(node.elseStatement, enclosing2);
@@ -31366,6 +31873,137 @@ function readRoutes(ts, fn, initializerOf, literalOf) {
31366
31873
  returns(fn.body);
31367
31874
  return routes;
31368
31875
  }
31876
+ function bindingKeys(ts, pattern, out) {
31877
+ if (!ts.isObjectBindingPattern(pattern)) return;
31878
+ for (const element of pattern.elements) {
31879
+ const key2 = element.propertyName ?? element.name;
31880
+ if (ts.isIdentifier(key2) || ts.isStringLiteral(key2)) out.add(key2.text);
31881
+ if (!ts.isIdentifier(element.name)) bindingKeys(ts, element.name, out);
31882
+ }
31883
+ }
31884
+ function calleeName(ts, expression) {
31885
+ if (ts.isIdentifier(expression)) return expression.text;
31886
+ if (ts.isPropertyAccessExpression(expression)) return expression.name.text;
31887
+ return void 0;
31888
+ }
31889
+ function helperParam(ts, fn, index) {
31890
+ const body = fn.body;
31891
+ const parameter = (fn.parameters ?? [])[index];
31892
+ if (!body || !parameter || parameter.dotDotDotToken) return void 0;
31893
+ if (ts.isIdentifier(parameter.name)) return { body, name: parameter.name.text };
31894
+ const keys = /* @__PURE__ */ new Set();
31895
+ bindingKeys(ts, parameter.name, keys);
31896
+ return keys.size > 0 ? { body, keys: [...keys] } : void 0;
31897
+ }
31898
+ function handleReads(ts, body, name, helperOf, out = /* @__PURE__ */ new Set(), depth = 0, seen = /* @__PURE__ */ new Set()) {
31899
+ const visitKey = `${body.getSourceFile().fileName}:${body.pos}:${body.end}:${name}`;
31900
+ if (seen.has(visitKey)) return out;
31901
+ seen.add(visitKey);
31902
+ const deeper = (scope, local) => {
31903
+ if (depth < READS_DEPTH) handleReads(ts, scope, local, helperOf, out, depth + 1, seen);
31904
+ };
31905
+ const follow = (node) => {
31906
+ let at = node;
31907
+ for (; ; ) {
31908
+ const up = at.parent;
31909
+ if (!up || out.size >= READS_CAP) return;
31910
+ if (ts.isParenthesizedExpression(up) || ts.isNonNullExpression(up) || ts.isAsExpression(up) || ts.isTypeAssertionExpression(up) || ts.isAwaitExpression(up) || typeof ts.isSatisfiesExpression === "function" && ts.isSatisfiesExpression(up)) {
31911
+ at = up;
31912
+ continue;
31913
+ }
31914
+ if (ts.isBinaryExpression(up) && (up.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken || up.operatorToken.kind === ts.SyntaxKind.BarBarToken)) {
31915
+ at = up;
31916
+ continue;
31917
+ }
31918
+ if (ts.isPropertyAccessExpression(up) && up.expression === at) {
31919
+ out.add(up.name.text);
31920
+ at = up;
31921
+ continue;
31922
+ }
31923
+ if (ts.isElementAccessExpression(up) && up.expression === at) {
31924
+ if (!ts.isStringLiteralLike(up.argumentExpression)) return;
31925
+ out.add(up.argumentExpression.text);
31926
+ at = up;
31927
+ continue;
31928
+ }
31929
+ if (ts.isCallExpression(up) && up.expression === at) {
31930
+ const [first] = up.arguments;
31931
+ if (first && ts.isStringLiteralLike(first)) {
31932
+ out.add(first.text);
31933
+ return;
31934
+ }
31935
+ if (up.arguments.length > 0) return;
31936
+ at = up;
31937
+ continue;
31938
+ }
31939
+ if ((ts.isCallExpression(up) || ts.isNewExpression(up)) && up.expression !== at) {
31940
+ const args = up.arguments ?? [];
31941
+ const index = args.indexOf(at);
31942
+ if (index < 0) return;
31943
+ const helper = ts.isCallExpression(up) && depth < READS_DEPTH ? helperOf?.(up, index) : void 0;
31944
+ if (helper) {
31945
+ for (const key2 of helper.keys ?? []) out.add(key2);
31946
+ if (helper.name) deeper(helper.body, helper.name);
31947
+ at = up;
31948
+ continue;
31949
+ }
31950
+ const callee = calleeName(ts, up.expression);
31951
+ if (callee !== void 0 && FLOW_CALLS.has(callee)) {
31952
+ at = up;
31953
+ continue;
31954
+ }
31955
+ return;
31956
+ }
31957
+ if (ts.isVariableDeclaration(up) && up.initializer === at) {
31958
+ if (ts.isIdentifier(up.name)) {
31959
+ if (up.name.text !== name) deeper(body, up.name.text);
31960
+ } else {
31961
+ bindingKeys(ts, up.name, out);
31962
+ }
31963
+ }
31964
+ return;
31965
+ }
31966
+ };
31967
+ const visit = (node) => {
31968
+ if (out.size >= READS_CAP) return;
31969
+ if (ts.isIdentifier(node) && node.text === name) {
31970
+ const parent = node.parent;
31971
+ const declares2 = !!parent && (ts.isPropertyAccessExpression(parent) && parent.name === node || ts.isPropertyAssignment(parent) && parent.name === node || (ts.isVariableDeclaration(parent) || ts.isParameter(parent) || ts.isBindingElement(parent)) && parent.name === node || ts.isBindingElement(parent) && parent.propertyName === node);
31972
+ if (!declares2) follow(node);
31973
+ }
31974
+ ts.forEachChild(node, visit);
31975
+ };
31976
+ visit(body);
31977
+ return out;
31978
+ }
31979
+ function sameFileHelpers(ts, sf) {
31980
+ const functions = /* @__PURE__ */ new Map();
31981
+ for (const statement of sf.statements) {
31982
+ if (ts.isFunctionDeclaration(statement) && statement.name && statement.body) functions.set(statement.name.text, statement);
31983
+ if (ts.isVariableStatement(statement)) {
31984
+ for (const declaration2 of statement.declarationList.declarations) {
31985
+ const initializer = declaration2.initializer && bareExpression(ts, declaration2.initializer);
31986
+ if (ts.isIdentifier(declaration2.name) && initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer))) {
31987
+ functions.set(declaration2.name.text, initializer);
31988
+ }
31989
+ }
31990
+ }
31991
+ }
31992
+ return (call, index) => {
31993
+ const callee = call.expression;
31994
+ if (ts.isIdentifier(callee)) {
31995
+ const fn = functions.get(callee.text);
31996
+ return fn ? helperParam(ts, fn, index) : void 0;
31997
+ }
31998
+ if (ts.isPropertyAccessExpression(callee) && callee.expression.kind === ts.SyntaxKind.ThisKeyword) {
31999
+ let owner = call.parent;
32000
+ while (owner && !ts.isClassLike(owner)) owner = owner.parent;
32001
+ const member = owner && ts.isClassLike(owner) ? owner.members.find((m) => ts.isMethodDeclaration(m) && m.name && propertyKeyText(ts, m.name) === callee.name.text) : void 0;
32002
+ return member ? helperParam(ts, member, index) : void 0;
32003
+ }
32004
+ return void 0;
32005
+ };
32006
+ }
31369
32007
  function walkExact(ts, sourceText, fileName) {
31370
32008
  const sf = ts.createSourceFile(
31371
32009
  fileName,
@@ -31662,47 +32300,8 @@ function walkExact(ts, sourceText, fileName) {
31662
32300
  const container = containerOf(owner);
31663
32301
  return container !== void 0 ? { container } : nested;
31664
32302
  };
31665
- const readsOff = (body, name) => {
31666
- const out = /* @__PURE__ */ new Set();
31667
- const visit2 = (node) => {
31668
- if (out.size >= 64) return;
31669
- if (ts.isIdentifier(node) && node.text === name) {
31670
- const parent = node.parent;
31671
- const memberName = !!parent && (ts.isPropertyAccessExpression(parent) && parent.name === node || ts.isPropertyAssignment(parent) && parent.name === node);
31672
- if (!memberName) {
31673
- let at = node;
31674
- for (; ; ) {
31675
- const up = at.parent;
31676
- if (!up) break;
31677
- if (ts.isParenthesizedExpression(up) || ts.isNonNullExpression(up) || ts.isAsExpression(up)) {
31678
- at = up;
31679
- continue;
31680
- }
31681
- if (ts.isPropertyAccessExpression(up) && up.expression === at) {
31682
- out.add(up.name.text);
31683
- at = up;
31684
- continue;
31685
- }
31686
- if (ts.isElementAccessExpression(up) && up.expression === at && ts.isStringLiteralLike(up.argumentExpression)) {
31687
- out.add(up.argumentExpression.text);
31688
- at = up;
31689
- continue;
31690
- }
31691
- if (ts.isVariableDeclaration(up) && up.initializer === at && ts.isObjectBindingPattern(up.name)) {
31692
- for (const element of up.name.elements) {
31693
- const key2 = element.propertyName ?? element.name;
31694
- if (ts.isIdentifier(key2) || ts.isStringLiteral(key2)) out.add(key2.text);
31695
- }
31696
- }
31697
- break;
31698
- }
31699
- }
31700
- }
31701
- ts.forEachChild(node, visit2);
31702
- };
31703
- visit2(body);
31704
- return [...out].sort();
31705
- };
32303
+ const helpersHere = sameFileHelpers(ts, sf);
32304
+ const readsOff = (body, name) => [...handleReads(ts, body, name, helpersHere)].sort();
31706
32305
  const declaredParameters = (fn) => {
31707
32306
  const parameters = fn.parameters ?? [];
31708
32307
  const used = /* @__PURE__ */ new Set();
@@ -32412,7 +33011,7 @@ function checkerOptions(ts, projectRoot2) {
32412
33011
  maxNodeModuleJsDepth: 0
32413
33012
  };
32414
33013
  }
32415
- function resolveCalls(ts, files, projectRoot2) {
33014
+ function resolveCalls(ts, files, projectRoot2, claimed = /* @__PURE__ */ new Set()) {
32416
33015
  const out = /* @__PURE__ */ new Map();
32417
33016
  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() };
32418
33017
  const options = checkerOptions(ts, projectRoot2);
@@ -32969,9 +33568,14 @@ function resolveCalls(ts, files, projectRoot2) {
32969
33568
  const reading = {};
32970
33569
  if (kind) reading.kind = kind;
32971
33570
  if (kind === "object") {
32972
- const props = checker.getPropertiesOfType(checker.getNonNullableType(type)).map((p) => p.getName());
33571
+ const props = checker.getPropertiesOfType(checker.getNonNullableType(type)).filter((p) => (p.flags & ts.SymbolFlags.Method) === 0).map((p) => p.getName());
32973
33572
  if (props.length > 0) reading.fields = props.slice(0, 200);
32974
33573
  }
33574
+ if (opaqueType(type)) reading.opaque = true;
33575
+ if (ts.isIdentifier(parameter.name) && REQUEST_HANDLE_NAMES.has(parameter.name.text.replace(/^_+/, "").toLowerCase())) {
33576
+ const read2 = handleReads(ts, body, parameter.name.text, checkerHelpers);
33577
+ if (read2.size > 0) reading.reads = [...read2].sort();
33578
+ }
32975
33579
  return reading;
32976
33580
  } catch {
32977
33581
  return {};
@@ -32996,6 +33600,16 @@ function resolveCalls(ts, files, projectRoot2) {
32996
33600
  } catch {
32997
33601
  }
32998
33602
  }
33603
+ if (ts.isImportSpecifier(node) && !node.isTypeOnly && !fileRoutes.has(node.name.text)) {
33604
+ try {
33605
+ const declaration2 = symbolOf(node.name)?.valueDeclaration;
33606
+ if (declaration2 && ts.isVariableDeclaration(declaration2) && declaration2.initializer && (ts.getCombinedNodeFlags(declaration2) & ts.NodeFlags.Const) !== 0) {
33607
+ const table = tableRoutes(ts, declaration2.initializer, initializerOf, literalOf);
33608
+ if (table.length > 0) fileRoutes.set(node.name.text, new Map(table.map((route2) => [routeKey2(route2), route2])));
33609
+ }
33610
+ } catch {
33611
+ }
33612
+ }
32999
33613
  ts.forEachChild(node, visit);
33000
33614
  };
33001
33615
  visit(sf);
@@ -33054,6 +33668,29 @@ function resolveCalls(ts, files, projectRoot2) {
33054
33668
  if (broken.length > 0) crossProjectImports.set(key2, broken);
33055
33669
  }
33056
33670
  return { calls: out, imports, kinds, routes, prefixes, crossProjectImports };
33671
+ function opaqueType(type) {
33672
+ const at = checker.getNonNullableType(type);
33673
+ if (at.intrinsicName === "error") return false;
33674
+ if ((at.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.NonPrimitive)) !== 0) return true;
33675
+ if ((at.flags & ts.TypeFlags.Object) === 0 || at.getProperties().length > 0) return false;
33676
+ if (at.getCallSignatures().length > 0 || at.getConstructSignatures().length > 0) return false;
33677
+ const lists = checker;
33678
+ return !(lists.isArrayType?.(at) || lists.isTupleType?.(at));
33679
+ }
33680
+ function checkerHelpers(call, index) {
33681
+ let declaration2;
33682
+ try {
33683
+ declaration2 = checker.getResolvedSignature(call)?.getDeclaration();
33684
+ } catch {
33685
+ return void 0;
33686
+ }
33687
+ if (!declaration2 || !declaration2.body) return void 0;
33688
+ const home = declaration2.getSourceFile();
33689
+ if (home.isDeclarationFile || !ownKey(home)) return void 0;
33690
+ const absolute = path20.resolve(home.fileName);
33691
+ if (absolute !== path20.resolve(call.getSourceFile().fileName) && claimed.has(absolute)) return void 0;
33692
+ return helperParam(ts, declaration2, index);
33693
+ }
33057
33694
  function platformKind(annotation) {
33058
33695
  if (!annotation || !ts.isTypeReferenceNode(annotation)) return void 0;
33059
33696
  const name = ts.isIdentifier(annotation.typeName) ? annotation.typeName.text : annotation.typeName.right.text;
@@ -33096,8 +33733,10 @@ function buildCodeModel(implementations, types, projectRoot2, sourceRoots = [],
33096
33733
  const packages = {};
33097
33734
  for (const [specifier, absolute] of localPackages) packages[specifier] = pathKey(path20.relative(projectRoot2, absolute));
33098
33735
  const declaredPaths = [];
33736
+ const claimedFiles = /* @__PURE__ */ new Set();
33099
33737
  for (const impl of implementations) {
33100
33738
  declaredPaths.push(...implementationSourceFiles(impl));
33739
+ for (const file of implementationSourceFiles(impl)) claimedFiles.add(path20.resolve(projectRoot2, file));
33101
33740
  if (impl.simPath) declaredPaths.push(impl.simPath);
33102
33741
  const routerFile = impl.router ? routerLinkage(impl.router).file : void 0;
33103
33742
  if (routerFile) declaredPaths.push(routerFile);
@@ -33197,7 +33836,7 @@ function buildCodeModel(implementations, types, projectRoot2, sourceRoots = [],
33197
33836
  let imports = /* @__PURE__ */ new Map();
33198
33837
  let reading;
33199
33838
  try {
33200
- reading = resolveCalls(ts, checkerRoots, projectRoot2);
33839
+ reading = resolveCalls(ts, checkerRoots, projectRoot2, claimedFiles);
33201
33840
  ({ calls: resolved, imports } = reading);
33202
33841
  } catch {
33203
33842
  resolved = /* @__PURE__ */ new Map();
@@ -33220,6 +33859,8 @@ function buildCodeModel(implementations, types, projectRoot2, sourceRoots = [],
33220
33859
  const reading2 = settled[index][at];
33221
33860
  if (reading2.kind) param.kind = reading2.kind;
33222
33861
  if (reading2.fields) param.fields = reading2.fields;
33862
+ if (reading2.opaque) param.opaque = true;
33863
+ if (reading2.reads) param.reads = [.../* @__PURE__ */ new Set([...param.reads ?? [], ...reading2.reads])].sort();
33223
33864
  });
33224
33865
  });
33225
33866
  }
@@ -33373,7 +34014,7 @@ function findTestsReferencing(methods, projectRoot2, testRoots) {
33373
34014
  }
33374
34015
  return found;
33375
34016
  }
33376
- var fs15, path20, import_module2, C_FAMILY_COMMENTS, NAMED_IMPORT_BINDINGS_RE, JS_PATTERNS, LANGUAGE_PATTERNS, PATTERN_ANALYSIS_MAX_BYTES, IDENTIFIER_RE, STRING_RE, tsLoads, HTTP_VERBS, METHOD_KEYS, ROUTER_TEXT, PATH_KEYS, SETTLE_DEPTH, LITERAL_SEGMENT, NO_PACKAGES, BUILD_EXTENSIONS, SOURCE_EXTENSIONS, ENTRY_CONDITIONS, parsedSources, PARSED_SOURCES_MAX, PLATFORM_OBJECT_TYPES, TRANSPORT_TYPES, IMPORT_FROM_RE, DYNAMIC_IMPORT_RE, IDENTIFIER_ONLY_RE, NAMED_REEXPORT_RE, STAR_REEXPORT_RE;
34017
+ var fs15, path20, import_module2, C_FAMILY_COMMENTS, NAMED_IMPORT_BINDINGS_RE, JS_PATTERNS, LANGUAGE_PATTERNS, PATTERN_ANALYSIS_MAX_BYTES, IDENTIFIER_RE, STRING_RE, tsLoads, HTTP_VERBS, METHOD_KEYS, ROUTER_TEXT, PATH_KEYS, SETTLE_DEPTH, LITERAL_SEGMENT, READS_CAP, READS_DEPTH, FLOW_CALLS, REQUEST_HANDLE_NAMES, NO_PACKAGES, BUILD_EXTENSIONS, SOURCE_EXTENSIONS, ENTRY_CONDITIONS, parsedSources, PARSED_SOURCES_MAX, PLATFORM_OBJECT_TYPES, TRANSPORT_TYPES, IMPORT_FROM_RE, DYNAMIC_IMPORT_RE, IDENTIFIER_ONLY_RE, NAMED_REEXPORT_RE, STAR_REEXPORT_RE;
33377
34018
  var init_source_analysis = __esm({
33378
34019
  "src/core/source-analysis.ts"() {
33379
34020
  "use strict";
@@ -33487,6 +34128,10 @@ var init_source_analysis = __esm({
33487
34128
  PATH_KEYS = /* @__PURE__ */ new Set(["path", "pattern", "route", "url", "template"]);
33488
34129
  SETTLE_DEPTH = 8;
33489
34130
  LITERAL_SEGMENT = /^[\w.~@!$&'+,;=%-]+$/;
34131
+ READS_CAP = 64;
34132
+ READS_DEPTH = 3;
34133
+ FLOW_CALLS = /* @__PURE__ */ new Set(["URL", "URLSearchParams", "parse"]);
34134
+ REQUEST_HANDLE_NAMES = /* @__PURE__ */ new Set(["req", "request", "ctx", "context", "event", "url"]);
33490
34135
  NO_PACKAGES = /* @__PURE__ */ new Map();
33491
34136
  BUILD_EXTENSIONS = [".d.mts", ".d.cts", ".d.ts", ".mjs", ".cjs", ".jsx", ".js"];
33492
34137
  SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
@@ -34158,6 +34803,7 @@ function buildRuleContext(opts) {
34158
34803
  ...opts.typeSpellingFacts ? { typeSpellingFacts: opts.typeSpellingFacts } : {},
34159
34804
  pinnedExternals,
34160
34805
  ...opts.bindingModules ? { bindingModules: opts.bindingModules } : {},
34806
+ ...opts.memberApprovedSurfaces ? { memberApprovedSurfaces: opts.memberApprovedSurfaces } : {},
34161
34807
  codeModel: opts.codeModel ?? emptyCodeModel(),
34162
34808
  roundTripIssues: opts.roundTripIssues,
34163
34809
  lintAllows: lintAllows2,
@@ -34817,6 +35463,7 @@ function runOwnersGate(rulesOrOptions, projectType = "backend", reachOnly = fals
34817
35463
  const parts = new Set(graph().nodes.find((n) => n.namespace === "")?.parts.map((p) => p.alias) ?? []);
34818
35464
  const pinnedExternals = listPinnedExternals2().filter((p) => !parts.has(p.alias));
34819
35465
  const bindingModules = reachOnly ? [] : readBindingModules(implementations.filter((impl) => !impl.id.includes("::")).flatMap((impl) => impl.bindings ?? []), getProjectRoot());
35466
+ const approvedMemberSurfaces = bindingModules.length > 0 && declaredMembers(boundConfig ?? {}).length > 0 ? memberApprovedSurfacesOrNone() : void 0;
34820
35467
  const conformance = rules?.conformance;
34821
35468
  const codeModel = reachOnly ? buildCodeModel([], [], getProjectRoot(), [], []) : buildCodeModel(implementations, types, getProjectRoot(), conformance?.sourceRoots ?? [], conformance?.exclude ?? []);
34822
35469
  const statusBearing = treatAllAsComplete ? [...subsystems, ...components, ...interfaces, ...implementations] : settledStatusBearing({ subsystems, components, interfaces, implementations });
@@ -34903,6 +35550,7 @@ function runOwnersGate(rulesOrOptions, projectType = "backend", reachOnly = fals
34903
35550
  typeSpellingFacts: typeSpellings,
34904
35551
  pinnedExternals,
34905
35552
  bindingModules,
35553
+ ...approvedMemberSurfaces ? { memberApprovedSurfaces: approvedMemberSurfaces } : {},
34906
35554
  // By-name selections only: a legacy path ref pins nothing to check. A dry
34907
35555
  // run supplies its candidate's; otherwise the stored ones.
34908
35556
  packSelections: packSelections ?? projectPackSelections(),
@@ -35242,6 +35890,13 @@ function familyApprovals(depth) {
35242
35890
  function familyRelations2() {
35243
35891
  return familyRelations();
35244
35892
  }
35893
+ function memberApprovedSurfacesOrNone() {
35894
+ try {
35895
+ return memberApprovedSurfaces2();
35896
+ } catch {
35897
+ return void 0;
35898
+ }
35899
+ }
35245
35900
  function unpinnedExternals() {
35246
35901
  return unpinned();
35247
35902
  }
@@ -37400,6 +38055,7 @@ function respellStoredReferences(kind, doc, map) {
37400
38055
  break;
37401
38056
  case "interface":
37402
38057
  at(doc, "component", "contract");
38058
+ at(doc, "implements", "implements");
37403
38059
  typedSlots("interface", doc, map);
37404
38060
  break;
37405
38061
  case "implementation":
@@ -37902,6 +38558,55 @@ function parseOrThrow(schema, value, kind, id) {
37902
38558
  }
37903
38559
  return res.data;
37904
38560
  }
38561
+ function respelledField(kind, position) {
38562
+ switch (position) {
38563
+ case "narrative":
38564
+ case "calls":
38565
+ case "auth":
38566
+ return "methods";
38567
+ case "type":
38568
+ return kind === "type" ? "fields" : "methods";
38569
+ case "contract":
38570
+ return kind === "interface" ? "component" : "contract";
38571
+ default:
38572
+ return position;
38573
+ }
38574
+ }
38575
+ function omittedListNotices(kind, stored, delta) {
38576
+ const out = [];
38577
+ const fields = kind === "type" ? ["methods", "fields"] : kind === "interface" || kind === "implementation" ? ["methods"] : [];
38578
+ for (const field of fields) {
38579
+ const given = delta[field];
38580
+ if (!Array.isArray(given) || given.length === 0) continue;
38581
+ const named2 = new Set(given.map((e) => e && typeof e === "object" ? e.name : void 0).filter((n) => typeof n === "string"));
38582
+ const kept = (Array.isArray(stored[field]) ? stored[field] : []).map((e) => e?.name).filter((n) => typeof n === "string" && !named2.has(n));
38583
+ if (kept.length === 0) continue;
38584
+ out.push(
38585
+ `${field}: the delta left out ${kept.map((n) => `"${n}"`).join(", ")}, and a list delta upserts \u2014 what it leaves out is kept, never removed. To remove one, name it with a delete marker: {"${field}": [{"name": "${kept[0]}", "action": "delete"}]}.`
38586
+ );
38587
+ }
38588
+ return out;
38589
+ }
38590
+ function pathSafeIdProblem(id) {
38591
+ if (typeof id !== "string" || id === "") return "it is empty";
38592
+ const segments = id.split("::");
38593
+ const named2 = id.startsWith("::") ? segments.slice(1) : segments;
38594
+ const bad = named2.find((s) => !PATH_SAFE_SEGMENT.test(s));
38595
+ if (bad === void 0) return null;
38596
+ if (/[\\/]/.test(bad)) return `"${bad}" holds a path separator`;
38597
+ if (bad === "") return "it has an empty segment";
38598
+ if (/^\.+$/.test(bad) || bad.includes(".")) return `"${bad}" holds a dot`;
38599
+ return `"${bad}" holds a character outside letters, digits, "-" and "_"`;
38600
+ }
38601
+ function assertPathSafe(id, what) {
38602
+ const problem = pathSafeIdProblem(id);
38603
+ if (problem !== null) {
38604
+ throw new Error(`not-an-id: the ${what} id "${id}" cannot name a spec \u2014 ${problem}. An id becomes a folder or a file name, so it is a plain id (letters, digits, "-" and "_", qualified by "::"), never a path. Nothing was written.`);
38605
+ }
38606
+ }
38607
+ function storedUnder(storedId, askedId) {
38608
+ return typeof storedId !== "string" || storedId === askedId.split("::").pop();
38609
+ }
37905
38610
  function computeSpecTreeSignature(dirs, files = []) {
37906
38611
  const parts = [];
37907
38612
  for (const f of files) {
@@ -38665,6 +39370,23 @@ function registryInvalidateCache() {
38665
39370
  function invalidateSpecCache() {
38666
39371
  registryInvalidateCache();
38667
39372
  }
39373
+ function registryLockSpecTree() {
39374
+ lockTree(getProjectRoot());
39375
+ for (const ws of workspaces.values()) ws.expireFreshness();
39376
+ }
39377
+ function lockTree2() {
39378
+ registryLockSpecTree();
39379
+ }
39380
+ function pinnedTypeDefinition(snapshot, name) {
39381
+ const exported = (snapshot.exportedTypes ?? []).find((t) => t.id === name);
39382
+ return exported ? snapshot.types.find((t) => t.id === exported.type) : void 0;
39383
+ }
39384
+ function readPinnedSpec(kind, id) {
39385
+ return current().readPinnedSpec(kind, id);
39386
+ }
39387
+ function unlockTree2() {
39388
+ unlockTree(getProjectRoot());
39389
+ }
38668
39390
  function getLoaderIssues() {
38669
39391
  return current().loaderIssues;
38670
39392
  }
@@ -38928,7 +39650,7 @@ function assertSpecsInReach(targets, what, projectRung = false) {
38928
39650
  function moveMethods(from, to, methods, hooks, dryRun) {
38929
39651
  return current().moveMethods(from, to, methods, hooks, dryRun);
38930
39652
  }
38931
- var fs16, path23, import_zod11, crypto5, PartReadOnly, SubsystemWriteDenied, TOLERATED_KEYS, UNKNOWN_KEY_HINTS, PART_FORBIDDEN_FIELDS, TYPE_EXPRESSION_PATHS, IMPORTABLE_POSITIONS, SIGNATURE_TTL_MS, COMPONENT_AUTH_SOURCE, STEP_JUMP_FIELDS, STEP_JUMP_LISTS, DELTA_VERBS, JUMP_LABEL_TWINS, LABEL_TWIN_CONTAINERS, SpecWorkspace, DEFAULT_DEPENDENCY_LIMIT, PROSE_FIELDS, DELTA_JUMP_PINS, workspaces, lastServedBinding;
39653
+ var fs16, path23, import_zod11, crypto5, PartReadOnly, SubsystemWriteDenied, TOLERATED_KEYS, UNKNOWN_KEY_HINTS, PART_FORBIDDEN_FIELDS, TYPE_EXPRESSION_PATHS, IMPORTABLE_POSITIONS, SIGNATURE_TTL_MS, REPLACED_LINKAGE_LISTS, PATH_SAFE_SEGMENT, COMPONENT_AUTH_SOURCE, STEP_JUMP_FIELDS, STEP_JUMP_LISTS, DELTA_VERBS, JUMP_LABEL_TWINS, LABEL_TWIN_CONTAINERS, SpecWorkspace, DEFAULT_DEPENDENCY_LIMIT, PROSE_FIELDS, DELTA_JUMP_PINS, workspaces, lastServedBinding;
38932
39654
  var init_specs2 = __esm({
38933
39655
  "src/core/specs.ts"() {
38934
39656
  "use strict";
@@ -38986,6 +39708,8 @@ var init_specs2 = __esm({
38986
39708
  };
38987
39709
  IMPORTABLE_POSITIONS = /* @__PURE__ */ new Set(["dependsOn", "dispatch", "mounts", "narrative"]);
38988
39710
  SIGNATURE_TTL_MS = 2e3;
39711
+ REPLACED_LINKAGE_LISTS = /* @__PURE__ */ new Set(["injectedParams", "bindings"]);
39712
+ PATH_SAFE_SEGMENT = /^[A-Za-z0-9_-]+$/;
38989
39713
  COMPONENT_AUTH_SOURCE = "component:";
38990
39714
  STEP_JUMP_FIELDS = ["onTrueStep", "onFalseStep", "defaultStep", "endStep", "finallyStep", "toStep"];
38991
39715
  STEP_JUMP_LISTS = [["cases", "value"], ["catches", "error"], ["branches", "name"]];
@@ -39952,10 +40676,117 @@ var init_specs2 = __esm({
39952
40676
  const mapped = mapSpecReferences(kind, spec, (position, value) => position === "signatureFrom" && !components.has(value) ? value : this.writeReference(from, spec.id, position, value, carrying));
39953
40677
  return { ...mapped, id: localOf(from, spec.id).split("::").pop() };
39954
40678
  }
40679
+ /**
40680
+ * The keys an `alias::name` read lands on when no spec holds the id as
40681
+ * written: the id bound through the bound project's alias table as a write
40682
+ * binds it (a public name to the member's key), and the member's own key for
40683
+ * a name it does not export — so a read resolves the form the guide
40684
+ * prescribes exactly as a write resolves it to refuse. None for a bare id.
40685
+ */
40686
+ aliasedReadKeys(id) {
40687
+ if (!id.includes("::") || pathSafeIdProblem(id) !== null) return [];
40688
+ const out = [];
40689
+ try {
40690
+ const bound2 = this.bindReference("", id);
40691
+ if (bound2 !== id) out.push(bound2);
40692
+ } catch {
40693
+ }
40694
+ const at = id.indexOf("::");
40695
+ const namespace = this.cachedRawRoots[0]?.record.aliases.get(id.slice(0, at));
40696
+ if (namespace !== void 0 && namespace !== "" && this.rootCovering(namespace) !== null) out.push(`${namespace}::${id.slice(at + 2)}`);
40697
+ return [...new Set(out)];
40698
+ }
40699
+ /**
40700
+ * The references a delta writes in ANOTHER TEXT than the stored spec holds
40701
+ * at the same position while both bind to one target (core_orchestrator
40702
+ * updateSpec step 14): the stored file's authored texts read per position,
40703
+ * the delta's read the same way, and a delta text the position does not hold
40704
+ * whose binding equals a stored text's binding is a respelling of it.
40705
+ */
40706
+ referenceRespellingsOf(kind, id, delta) {
40707
+ if (kind === "system") return [];
40708
+ const file = this.storedFileOf(kind, id);
40709
+ if (!file || !pathExists(file)) return [];
40710
+ let stored;
40711
+ try {
40712
+ stored = JSON.parse(JSON.stringify(readSpecFile(file)));
40713
+ } catch {
40714
+ return [];
40715
+ }
40716
+ const held = /* @__PURE__ */ new Map();
40717
+ respellStoredReferences(kind, stored, (position, value) => {
40718
+ held.set(position, (held.get(position) ?? /* @__PURE__ */ new Set()).add(value));
40719
+ return value;
40720
+ });
40721
+ const written = [];
40722
+ respellStoredReferences(kind, JSON.parse(JSON.stringify(delta)), (position, value) => {
40723
+ written.push({ position, value });
40724
+ return value;
40725
+ });
40726
+ const prefix = this.writePrefixFor(id);
40727
+ const bind = (value) => {
40728
+ try {
40729
+ return this.bindReference(prefix, value);
40730
+ } catch {
40731
+ return value;
40732
+ }
40733
+ };
40734
+ const out = [];
40735
+ for (const w of written) {
40736
+ const texts = held.get(w.position);
40737
+ if (!texts || texts.has(w.value)) continue;
40738
+ const target = bind(w.value);
40739
+ const from = [...texts].find((s) => s !== w.value && bind(s) === target);
40740
+ if (from !== void 0 && !out.some((o) => o.position === w.position && o.from === from)) out.push({ position: w.position, from, to: w.value });
40741
+ }
40742
+ return out;
40743
+ }
40744
+ /**
40745
+ * spec_loader.readPinnedSpec — an EXTERNAL's spec read by `alias::name` from
40746
+ * the pinned snapshot the bound project holds of it: the contract entry (a
40747
+ * component or an interface asked for, or no kind) or the exported type (a
40748
+ * type). Null when the alias is no declared external (a member's alias is
40749
+ * read under its key instead), its pin is absent or unreadable, or the pin
40750
+ * holds no such public name. Read-only, never cached.
40751
+ */
40752
+ readPinnedSpec(kind, id) {
40753
+ const at = id.indexOf("::");
40754
+ const pin2 = at > 0 ? this.pinnedSnapshotOf(id.slice(0, at)) : null;
40755
+ if (!pin2) return null;
40756
+ const name = id.slice(at + 2);
40757
+ const entry = kind === "type" ? void 0 : pin2.snapshot.interfaces.find((e) => e.id === name);
40758
+ if (entry) return { ...pin2.base, kind: "interface", id: name, spec: entry };
40759
+ const definition = kind === void 0 || kind === "type" ? pinnedTypeDefinition(pin2.snapshot, name) : void 0;
40760
+ return definition ? { ...pin2.base, kind: "type", id: name, spec: definition } : null;
40761
+ }
40762
+ /** The pinned snapshot of the bound project's external `alias` — none for a member's alias, an undeclared one, or an absent or unreadable pin. */
40763
+ pinnedSnapshotOf(alias) {
40764
+ this.scanAll();
40765
+ const root = this.cachedRawRoots[0];
40766
+ const config = root?.record.config;
40767
+ if (!root || !config || root.record.aliases.has(alias)) return null;
40768
+ const declared = declaredExternals(config).find((d) => d.alias === alias && !d.problem);
40769
+ const file = path23.join(aiPathsAt(root.dir).root(), "externals", `${alias}.yaml`);
40770
+ if (!declared || !pathExists(file)) return null;
40771
+ try {
40772
+ const snapshot = SurfaceSnapshotSchema.parse(readSpecFile(file));
40773
+ const rel2 = path23.relative(root.dir, file).split(path23.sep).join("/");
40774
+ return { snapshot, base: { alias, project: declared.project, file: rel2, ...snapshot.stateId !== void 0 ? { stateId: snapshot.stateId } : {} } };
40775
+ } catch {
40776
+ return null;
40777
+ }
40778
+ }
39955
40779
  // -------------------------------------------------------------------------
39956
40780
  // Path builders
40781
+ //
40782
+ // Every id a path is built from must be path-safe FIRST: a subsystem, a
40783
+ // component, a contract, an implementation or a type id becomes a folder or
40784
+ // a file name, and one carrying a separator or a `..` segment would build a
40785
+ // path out of the tree — into another project's specs. Refused here, before
40786
+ // any path exists, whichever write asked.
39957
40787
  // -------------------------------------------------------------------------
39958
40788
  getSubsystemPath(id) {
40789
+ assertPathSafe(id, "subsystem");
39959
40790
  const index = this.scanAll();
39960
40791
  if (index.paths.subsystem[id]) {
39961
40792
  return index.paths.subsystem[id];
@@ -39980,6 +40811,8 @@ var init_specs2 = __esm({
39980
40811
  return path23.join(this.paths.specsDir(), id, ".index.yaml");
39981
40812
  }
39982
40813
  getComponentPath(id, subsystemId) {
40814
+ assertPathSafe(id, "component");
40815
+ if (subsystemId !== void 0) assertPathSafe(subsystemId, "subsystem");
39983
40816
  const index = this.scanAll();
39984
40817
  if (index.paths.component[id]) {
39985
40818
  return index.paths.component[id];
@@ -40014,6 +40847,8 @@ var init_specs2 = __esm({
40014
40847
  return path23.join(this.paths.specsDir(), targetSubsystem, id, ".index.yaml");
40015
40848
  }
40016
40849
  getInterfacePath(id, componentId) {
40850
+ assertPathSafe(id, "interface");
40851
+ if (componentId !== void 0) assertPathSafe(componentId, "component");
40017
40852
  const index = this.scanAll();
40018
40853
  if (index.paths.interface[id]) {
40019
40854
  return index.paths.interface[id];
@@ -40049,6 +40884,8 @@ var init_specs2 = __esm({
40049
40884
  return path23.join(this.paths.specsDir(), "default", targetComponent2, ".interface.yaml");
40050
40885
  }
40051
40886
  getImplementationPath(id, contractId) {
40887
+ assertPathSafe(id, "implementation");
40888
+ if (contractId !== void 0) assertPathSafe(contractId, "interface");
40052
40889
  const index = this.scanAll();
40053
40890
  if (index.paths.implementation[id]) {
40054
40891
  return index.paths.implementation[id];
@@ -40084,6 +40921,9 @@ var init_specs2 = __esm({
40084
40921
  return path23.join(this.paths.specsDir(), "default", targetContract, ".implementation.yaml");
40085
40922
  }
40086
40923
  getTypePath(id, subsystemId, group) {
40924
+ assertPathSafe(id, "type");
40925
+ if (subsystemId !== void 0 && subsystemId !== "") assertPathSafe(subsystemId, "subsystem");
40926
+ if (group !== void 0 && group !== "") assertPathSafe(group, "group");
40087
40927
  const index = this.scanAll();
40088
40928
  if (index.paths.type[id]) return index.paths.type[id];
40089
40929
  if (id.includes("::")) {
@@ -40226,11 +41066,15 @@ var init_specs2 = __esm({
40226
41066
  const index = this.scanAll();
40227
41067
  const cached = index.subsystems.find((s) => s.id === id);
40228
41068
  if (cached) return cached;
41069
+ const aliased = this.aliasedReadKeys(id).map((key2) => index.subsystems.find((s) => s.id === key2)).find((s) => s !== void 0);
41070
+ if (aliased) return aliased;
41071
+ if (pathSafeIdProblem(id) !== null) return null;
40229
41072
  const p = this.getSubsystemPath(id);
40230
41073
  if (!pathExists(p)) return null;
40231
41074
  try {
40232
41075
  const raw = readSpecFile(p);
40233
- return SubsystemSpecSchema.parse(raw);
41076
+ const parsed = SubsystemSpecSchema.parse(raw);
41077
+ return storedUnder(parsed.id, id) ? parsed : null;
40234
41078
  } catch (e) {
40235
41079
  this.loaderIssues.push({
40236
41080
  severity: "error",
@@ -40398,10 +41242,14 @@ var init_specs2 = __esm({
40398
41242
  const index = this.scanAll();
40399
41243
  const spec = index.components.find((c) => c.id === id);
40400
41244
  if (spec) return spec;
41245
+ const aliased = this.aliasedReadKeys(id).map((key2) => index.components.find((c) => c.id === key2)).find((c) => c !== void 0);
41246
+ if (aliased) return aliased;
41247
+ if (pathSafeIdProblem(id) !== null) return null;
40401
41248
  const p = this.getComponentPath(id);
40402
41249
  if (!pathExists(p)) return null;
40403
41250
  try {
40404
41251
  const raw = readSpecFile(p);
41252
+ if (!storedUnder(raw.id, id)) return null;
40405
41253
  readRetiredReachForms("component", raw);
40406
41254
  return attachRetiredMounts(ComponentSpecSchema.parse(raw), raw);
40407
41255
  } catch (e) {
@@ -40501,10 +41349,14 @@ var init_specs2 = __esm({
40501
41349
  const index = this.scanAll();
40502
41350
  const spec = index.interfaces.find((i) => i.id === id);
40503
41351
  if (spec) return spec;
41352
+ const aliased = this.aliasedReadKeys(id).map((key2) => index.interfaces.find((i) => i.id === key2)).find((i) => i !== void 0);
41353
+ if (aliased) return aliased;
41354
+ if (pathSafeIdProblem(id) !== null) return null;
40504
41355
  const p = this.getInterfacePath(id);
40505
41356
  if (!pathExists(p)) return null;
40506
41357
  try {
40507
41358
  const raw = readSpecFile(p);
41359
+ if (!storedUnder(raw.id, id)) return null;
40508
41360
  const owner = typeof raw.component === "string" ? index.components.find((c) => c.id === raw.component) : void 0;
40509
41361
  readRetiredReachForms("interface", raw, owner?.componentType === "Portal");
40510
41362
  return resolveTree([interfaceCanonicalTypes(InterfaceSpecSchema.parse(raw)).spec], index.components, index.types).interfaces[0];
@@ -40600,10 +41452,14 @@ var init_specs2 = __esm({
40600
41452
  const index = this.scanAll();
40601
41453
  const spec = index.implementations.find((impl) => impl.id === id);
40602
41454
  if (spec) return spec;
41455
+ const aliased = this.aliasedReadKeys(id).map((key2) => index.implementations.find((impl) => impl.id === key2)).find((impl) => impl !== void 0);
41456
+ if (aliased) return aliased;
41457
+ if (pathSafeIdProblem(id) !== null) return null;
40603
41458
  const p = this.getImplementationPath(id);
40604
41459
  if (!pathExists(p)) return null;
40605
41460
  try {
40606
41461
  const raw = readSpecFile(p);
41462
+ if (!storedUnder(raw.id, id)) return null;
40607
41463
  return ImplementationSpecSchema.parse(raw);
40608
41464
  } catch (e) {
40609
41465
  this.loaderIssues.push({
@@ -40653,7 +41509,8 @@ var init_specs2 = __esm({
40653
41509
  return this.scanAll().types;
40654
41510
  }
40655
41511
  loadTypeSpec(id) {
40656
- return this.scanAll().types.find((t) => t.id === id) ?? null;
41512
+ const types = this.scanAll().types;
41513
+ return types.find((t) => t.id === id) ?? this.aliasedReadKeys(id).map((key2) => types.find((t) => t.id === key2)).find((t) => t !== void 0) ?? null;
40657
41514
  }
40658
41515
  /**
40659
41516
  * Returns non-fatal placement notices (empty when there is nothing to
@@ -41866,6 +42723,8 @@ var init_specs2 = __esm({
41866
42723
  res.dispatch = inDeltaOrder(key2, existing.dispatch, value, mergeKeyedArray(existing.dispatch, value, (b) => String(b?.capability)));
41867
42724
  } else if (key2 === "lifecycle" && Array.isArray(value) && Array.isArray(existing.lifecycle)) {
41868
42725
  res.lifecycle = inDeltaOrder(key2, existing.lifecycle, value, mergeKeyedArray(existing.lifecycle, value, (le) => `${le?.phase} ${le?.component} ${le?.method}`));
42726
+ } else if (kind === "implementation" && delta2 === qualifiedDelta && REPLACED_LINKAGE_LISTS.has(key2) && Array.isArray(value) && !value.some(isValueMarker)) {
42727
+ res[key2] = value;
41869
42728
  } else if (Array.isArray(value) && isPlainValueList(key2, existing[key2], value)) {
41870
42729
  res[key2] = inDeltaOrder(key2, existing[key2] ?? [], value, mergePlainValues(key2, existing[key2] ?? [], value));
41871
42730
  } else if (Array.isArray(value) && value.length > 0 && Array.isArray(existing[key2]) && isIdentifiedArray(key2, existing[key2], value)) {
@@ -41966,12 +42825,14 @@ var init_specs2 = __esm({
41966
42825
  const canonicalStored = canonical3(storedBefore);
41967
42826
  const canonicalMerged = canonical3(mergedResult);
41968
42827
  const changes = specChanges(canonicalStored, canonicalMerged);
42828
+ const referenceRespellings = this.referenceRespellingsOf(kind, id, mergeableDelta);
42829
+ for (const r of referenceRespellings) changes.push({ path: respelledField(kind, r.position), change: "set", before: r.from, after: r.to });
41969
42830
  const ineffective = ineffectiveDeltaPaths(
41970
42831
  { ...qualifiedDelta, ...unsetFields.length ? { unset: unsetFields } : {} },
41971
42832
  canonicalMerged,
41972
42833
  canonicalStored,
41973
42834
  respellings
41974
- );
42835
+ ).filter((line2) => !referenceRespellings.some((r) => line2.startsWith(respelledField(kind, r.position)) && line2.includes("already held")));
41975
42836
  if (changes.length === 0) {
41976
42837
  return {
41977
42838
  kind,
@@ -41980,7 +42841,7 @@ var init_specs2 = __esm({
41980
42841
  dryRun,
41981
42842
  changes,
41982
42843
  ineffective,
41983
- notices,
42844
+ notices: [...notices, ...omittedListNotices(kind, storedBefore, mergeableDelta)],
41984
42845
  respellings: [],
41985
42846
  testsToRevisit: [],
41986
42847
  summary: `No change to ${kind} "${id}" \u2014 the delta matches what is stored, so nothing ${dryRun ? "would be" : "was"} written.`
@@ -42010,6 +42871,9 @@ var init_specs2 = __esm({
42010
42871
  allowStatusDemotion: Object.prototype.hasOwnProperty.call(delta, "status")
42011
42872
  };
42012
42873
  notices.push(...this.saveOfKind(kind, mergedResult, opts));
42874
+ if (referenceRespellings.length > 0) {
42875
+ this.respellReferences(kind, id, referenceRespellings.map((r) => ({ kind, specId: id, position: r.position, from: r.from, to: r.to })));
42876
+ }
42013
42877
  return {
42014
42878
  kind,
42015
42879
  id,
@@ -42736,9 +43600,48 @@ function listConsumers(search) {
42736
43600
  const bound2 = path25.resolve(getProjectRoot());
42737
43601
  const { top } = climb(bound2);
42738
43602
  const family = familyConsumers(bound2, top);
42739
- const seen = new Set(family.map((c) => dirKey(c.directory)));
42740
- const found = searchedConsumers(bound2, search ?? []).filter((c) => !seen.has(dirKey(c.directory)));
42741
- return [...family, ...found];
43603
+ const seen = new Set([bound2, top, ...family.map((c) => c.directory)].map(dirKey));
43604
+ const parents = composingParents(bound2, top, seen);
43605
+ for (const c of parents) seen.add(dirKey(c.directory));
43606
+ const found = searchedConsumers(bound2, search ?? [], seen).filter((c) => !seen.has(dirKey(c.directory)));
43607
+ return [...family, ...parents, ...found];
43608
+ }
43609
+ function composesByPath(config, root, bound2) {
43610
+ return declaredMembers(config).some((m) => !m.problem && m.source.path !== void 0 && m.source.path.trim() !== "" && dirKey(path25.resolve(root, m.source.path)) === dirKey(bound2));
43611
+ }
43612
+ function composingParents(bound2, top, seen) {
43613
+ if (getHostedLookup() !== null) return [];
43614
+ const reach2 = getRequestParentReach();
43615
+ if (reach2 && !reach2.parentReach) return [];
43616
+ const folders = [...new Set([path25.dirname(top), path25.dirname(bound2)].map((d) => path25.resolve(d)))];
43617
+ const out = [];
43618
+ for (const root of [...new Set(folders.flatMap(enclosedRoots).map((r) => path25.resolve(r)))]) {
43619
+ if (seen.has(dirKey(root)) || out.some((c) => dirKey(c.directory) === dirKey(root))) continue;
43620
+ const config = membersConfigAt(root);
43621
+ if (!config || !composesByPath(config, root, bound2)) continue;
43622
+ out.push(...familyConsumers(bound2, root).filter((c) => !seen.has(dirKey(c.directory))));
43623
+ }
43624
+ return out;
43625
+ }
43626
+ function enclosedRoots(folder) {
43627
+ let entries;
43628
+ try {
43629
+ entries = fs17.readdirSync(folder, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && d.name !== "node_modules");
43630
+ } catch {
43631
+ return [];
43632
+ }
43633
+ if (entries.length > ENCLOSING_FOLDER_LIMIT) return [];
43634
+ return entries.map((d) => path25.join(folder, d.name)).filter((dir) => fs17.existsSync(path25.join(dir, ".wai", "project.yaml")));
43635
+ }
43636
+ function membersConfigAt(root) {
43637
+ try {
43638
+ const text3 = fs17.readFileSync(path25.join(root, ".wai", "project.yaml"), "utf8");
43639
+ if (!/^members\s*:/m.test(text3)) return null;
43640
+ const parsed = parseYaml(text3);
43641
+ return parsed && typeof parsed === "object" ? parsed : null;
43642
+ } catch {
43643
+ return null;
43644
+ }
42742
43645
  }
42743
43646
  function familyConsumers(bound2, top) {
42744
43647
  return runWithProjectRoot(top, () => {
@@ -42781,7 +43684,7 @@ function projectRootsUnder(folder) {
42781
43684
  if (isRoot(at)) return [at];
42782
43685
  return fs17.readdirSync(at, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && d.name !== "node_modules").map((d) => path25.join(at, d.name)).filter(isRoot);
42783
43686
  }
42784
- function searchedConsumers(bound2, search) {
43687
+ function searchedConsumers(bound2, search, seen = /* @__PURE__ */ new Set()) {
42785
43688
  if (search.length === 0) return [];
42786
43689
  if (getHostedLookup() !== null) throw new Error("searching folders for consumers is a local read: a hosted request reads no folder outside its project");
42787
43690
  const producerId = runWithProjectRoot(bound2, () => {
@@ -42791,6 +43694,13 @@ function searchedConsumers(bound2, search) {
42791
43694
  const roots = [...new Set(search.flatMap(projectRootsUnder).map((r) => path25.resolve(r)))].filter((r) => dirKey(r) !== dirKey(bound2));
42792
43695
  const out = [];
42793
43696
  for (const root of roots) {
43697
+ if (seen.has(dirKey(root))) continue;
43698
+ const members = membersConfigAt(root);
43699
+ const composing = members !== null && composesByPath(members, root, bound2);
43700
+ if (composing) {
43701
+ out.push(...familyConsumers(bound2, root).filter((c) => !seen.has(dirKey(c.directory))).map((c) => ({ ...c, found: "search" })));
43702
+ continue;
43703
+ }
42794
43704
  const answer = runWithProjectRoot(root, () => {
42795
43705
  let config;
42796
43706
  try {
@@ -42818,6 +43728,47 @@ function searchedConsumers(bound2, search) {
42818
43728
  }
42819
43729
  return out.sort((a, b) => a.directory < b.directory ? -1 : a.directory > b.directory ? 1 : 0);
42820
43730
  }
43731
+ function recordedMemberSubjects(root) {
43732
+ try {
43733
+ const record2 = JSON.parse(fs17.readFileSync(path25.join(root, ".wai", "lock.json"), "utf8"));
43734
+ return new Map(Object.entries(record2.members ?? {}).filter(([, m]) => typeof m?.subject === "string").map(([alias, m]) => [alias, m.subject]));
43735
+ } catch {
43736
+ return /* @__PURE__ */ new Map();
43737
+ }
43738
+ }
43739
+ function memberRevisions() {
43740
+ if (getHostedLookup() !== null) return [];
43741
+ const bound2 = path25.resolve(getProjectRoot());
43742
+ const family = graph();
43743
+ const own2 = consumerNode(family, bound2);
43744
+ if (!own2) return [];
43745
+ const subjects = recordedMemberSubjects(bound2);
43746
+ const out = [];
43747
+ for (const node of family.nodes) {
43748
+ if (node.parent !== own2.namespace || node === own2 || node.mountAlias === void 0) continue;
43749
+ const alias = node.mountAlias;
43750
+ const usage = exportUsage(own2.namespace, node.namespace);
43751
+ const entry = { alias, project: node.id ?? node.namespace, root: node.directory, usage };
43752
+ const repo = repositoryRoot(node.directory);
43753
+ if (repo) {
43754
+ const lockPath3 = path25.join(".wai", "lock.json");
43755
+ const subject = subjects.get(alias);
43756
+ const digest3 = subject?.slice(subject.lastIndexOf(":") + 1);
43757
+ const recorded = digest3 ? commitIntroducing(node.directory, lockPath3, digest3) : null;
43758
+ const commit3 = recorded ?? lastCommitOf(node.directory, lockPath3);
43759
+ if (commit3) {
43760
+ try {
43761
+ const relative35 = path25.relative(repo, node.directory);
43762
+ const directory = fetch2(repo, commit3, relative35 === "" ? void 0 : relative35);
43763
+ entry.revision = { directory, commit: commit3, label: recorded ? `the approval this project's lock records for it (${commit3.slice(0, 12)})` : `its own last committed approval (${commit3.slice(0, 12)})` };
43764
+ } catch {
43765
+ }
43766
+ }
43767
+ }
43768
+ out.push(entry);
43769
+ }
43770
+ return out;
43771
+ }
42821
43772
  function approvedRevision(against) {
42822
43773
  const bound2 = path25.resolve(getProjectRoot());
42823
43774
  const repo = repositoryRoot(bound2);
@@ -42830,18 +43781,20 @@ function approvedRevision(against) {
42830
43781
  const directory = fetch2(repo, commit3, relative35 === "" ? void 0 : relative35);
42831
43782
  return { directory, commit: commit3, label: against !== void 0 ? `${against} (${commit3.slice(0, 12)})` : `the last approval, committed at ${commit3.slice(0, 12)}` };
42832
43783
  }
42833
- var fs17, path25, NOT_COMPARED_OFFLINE, repoKey;
43784
+ var fs17, path25, NOT_COMPARED_OFFLINE, repoKey, ENCLOSING_FOLDER_LIMIT;
42834
43785
  var init_external_producers = __esm({
42835
43786
  "src/core/external-producers.ts"() {
42836
43787
  "use strict";
42837
43788
  fs17 = __toESM(require("fs"));
42838
43789
  path25 = __toESM(require("path"));
43790
+ init_yaml();
42839
43791
  init_fs();
42840
43792
  init_models();
42841
43793
  init_specs2();
42842
43794
  init_git_source();
42843
43795
  NOT_COMPARED_OFFLINE = "not compared offline \u2014 `wairon externals status` fetches it";
42844
43796
  repoKey = (dir) => process.platform === "win32" ? path25.resolve(dir).toLowerCase() : path25.resolve(dir);
43797
+ ENCLOSING_FOLDER_LIMIT = 256;
42845
43798
  }
42846
43799
  });
42847
43800
 
@@ -44435,6 +45388,19 @@ function moveMemberSpecs(scan2, remap) {
44435
45388
  });
44436
45389
  }
44437
45390
  const rewritten = rewriteRefFields(parentSpecsDir, remap);
45391
+ for (const file of listFilesRecursive(parentSpecsDir, ".yaml")) {
45392
+ let raw;
45393
+ try {
45394
+ raw = readYamlFile(file);
45395
+ } catch {
45396
+ continue;
45397
+ }
45398
+ const kind = raw && typeof raw === "object" ? specKind(raw) : void 0;
45399
+ if (!kind || !respellTypeTokens(raw, remap)) continue;
45400
+ writeYamlFile(file, raw);
45401
+ const id = kind === "system" ? "system" : String(raw.id);
45402
+ if (!rewritten.some((s) => s.kind === kind && s.id === id)) rewritten.push({ kind, id });
45403
+ }
44438
45404
  invalidateSpecCache();
44439
45405
  return rewritten;
44440
45406
  }
@@ -44983,6 +45949,8 @@ function patchSubsystemIndex(indexPath, mutate) {
44983
45949
  writeYamlFile(indexPath, raw);
44984
45950
  }
44985
45951
  function renameComponent(componentId, newId, dryRun) {
45952
+ const foreign = foreignIdRefusal(componentId, "chained-component", "rename it", `sdd_rename_component ${componentId.slice(componentId.lastIndexOf("::") + 2)} <new id>`);
45953
+ if (foreign !== null) throw new WaironError(foreign);
44986
45954
  const component = loadComponentSpec(componentId);
44987
45955
  if (!component) {
44988
45956
  throw new WaironError(`component-missing: no component has the id "${componentId}".`);
@@ -45169,6 +46137,8 @@ function renameSpecId(kind, id, newId, dryRun) {
45169
46137
  const owner = kind === "component" ? "sdd_rename_component" : kind === "type" ? "sdd_rename_type" : "no tool yet";
45170
46138
  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}.`);
45171
46139
  }
46140
+ const foreign = foreignIdRefusal(id, "chained-spec", "rename it", `sdd_rename_spec ${kind} ${id.slice(id.lastIndexOf("::") + 2)} <new id>`);
46141
+ if (foreign !== null) throw new WaironError(foreign);
45172
46142
  const spec = kind === "interface" ? loadInterfaceSpec(id) : loadImplementationSpec(id);
45173
46143
  if (!spec) throw new WaironError(`spec-missing: no ${kind} has the id "${id}".`);
45174
46144
  if (id.includes("::")) {
@@ -45325,11 +46295,11 @@ function moveTypeSpec(id, target, subsystems, dryRun) {
45325
46295
  if (target === void 0 && exportsMoving.length + repointed.length > 0) {
45326
46296
  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.`);
45327
46297
  }
45328
- const oldToken = from ? `${from}::${bare2}` : void 0;
45329
- const newToken = target ? `${target}::${bare2}` : bare2;
46298
+ const ownId = effectiveProjectId(projectConfigRepository.load() ?? { name: loadSystemSpec()?.name ?? "" });
46299
+ const moveToken = movedTypeToken(from, bare2, target, ownId);
45330
46300
  const specsDir = aiPathsAt(getProjectRoot()).specsDir();
45331
46301
  const rewriteAll2 = (write6) => {
45332
- if (oldToken === void 0) return [];
46302
+ if (from === "") return [];
45333
46303
  const out = [];
45334
46304
  for (const file of listFilesRecursive(specsDir, ".yaml")) {
45335
46305
  let raw;
@@ -45339,7 +46309,7 @@ function moveTypeSpec(id, target, subsystems, dryRun) {
45339
46309
  continue;
45340
46310
  }
45341
46311
  const kind = raw && typeof raw === "object" ? specKind(raw) : void 0;
45342
- if (!kind || !respellQualifiedType(raw, oldToken, newToken)) continue;
46312
+ if (!kind || !respellTypeTokens(raw, moveToken)) continue;
45343
46313
  if (write6) writeYamlFile(file, raw);
45344
46314
  out.push({ kind, id: kind === "system" ? "system" : String(raw.id) });
45345
46315
  }
@@ -45385,22 +46355,32 @@ function moveTypeSpec(id, target, subsystems, dryRun) {
45385
46355
  invalidateSpecCache();
45386
46356
  return report4;
45387
46357
  }
45388
- function respellQualifiedType(raw, oldToken, newToken) {
46358
+ function respellTypeTokens(raw, map) {
45389
46359
  let changed = false;
45390
- const escaped = oldToken.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
46360
+ const respell = (value) => value.replace(QUALIFIED_TYPE_TOKEN, (_m, lead, token) => `${lead}${map(token)}`);
45391
46361
  const walk2 = (node) => {
45392
46362
  if (Array.isArray(node)) {
45393
46363
  node.forEach(walk2);
45394
46364
  return;
45395
46365
  }
45396
46366
  if (!node || typeof node !== "object") return;
45397
- for (const [key2, value] of Object.entries(node)) {
46367
+ const holder = node;
46368
+ for (const [key2, value] of Object.entries(holder)) {
45398
46369
  if (typeof value === "string" && TYPE_POSITION_KEYS.has(key2)) {
45399
- const next = value.replace(new RegExp(`(?<![A-Za-z0-9_:-])${escaped}(?![A-Za-z0-9_-])`, "g"), newToken);
46370
+ const next = respell(value);
45400
46371
  if (next !== value) {
45401
- node[key2] = next;
46372
+ holder[key2] = next;
45402
46373
  changed = true;
45403
46374
  }
46375
+ } else if (key2 === "assertsInvariants" && Array.isArray(value)) {
46376
+ value.forEach((ref, i) => {
46377
+ if (typeof ref !== "string") return;
46378
+ const next = respell(ref);
46379
+ if (next !== ref) {
46380
+ value[i] = next;
46381
+ changed = true;
46382
+ }
46383
+ });
45404
46384
  } else if (value && typeof value === "object" && key2 !== "previousIds" && key2 !== "previousNames") {
45405
46385
  walk2(value);
45406
46386
  }
@@ -45409,6 +46389,23 @@ function respellQualifiedType(raw, oldToken, newToken) {
45409
46389
  walk2(raw);
45410
46390
  return changed;
45411
46391
  }
46392
+ function movedTypeToken(from, bare2, target, ownId) {
46393
+ return (token) => {
46394
+ const lead = token.startsWith("::") ? "::" : "";
46395
+ const parts = token.slice(lead.length).split(/(::|\.)/);
46396
+ const segment = (i) => parts[2 * i];
46397
+ const count = (parts.length + 1) / 2;
46398
+ for (let i = 0; i + 1 < count && i <= 1; i++) {
46399
+ if (nameKey(segment(i)) !== nameKey(from) || nameKey(segment(i + 1)) !== nameKey(bare2)) continue;
46400
+ if (i === 1 && (lead !== "" || ownId === null || nameKey(segment(0)) !== nameKey(ownId))) continue;
46401
+ const next = [...parts];
46402
+ if (target !== void 0) next[2 * i] = target;
46403
+ else next.splice(2 * i, 2);
46404
+ return `${lead}${next.join("")}`;
46405
+ }
46406
+ return token;
46407
+ };
46408
+ }
45412
46409
  function publishedUsesOf(removed, methods) {
45413
46410
  const contracts = loadInterfaceSpecs();
45414
46411
  const uses = [];
@@ -45458,7 +46455,16 @@ function languageOf(subsystem) {
45458
46455
  function memberNameRefusal(name, kind, subsystem, casing) {
45459
46456
  return identifierProblem(name, kind) ?? reservedWordProblem(name, kind, languageOf(subsystem), casing);
45460
46457
  }
46458
+ function shouldPinSymbol(implementation, methodName, pinSymbol) {
46459
+ const realization = implementation.methods.find((m) => m.name === methodName);
46460
+ if (!realization || realization.symbol !== void 0 || pinSymbol === false) return false;
46461
+ if (pinSymbol === true) return true;
46462
+ const file = realization.sourcePath ?? implementation.sourcePath;
46463
+ return file !== void 0 && file !== "" && fs19.existsSync(path27.resolve(getProjectRoot(), file));
46464
+ }
45461
46465
  function renameMethod(componentId, methodName, newName, pinSymbol, dryRun) {
46466
+ const foreign = foreignIdRefusal(componentId, "chained-component", "rename its method", `sdd_rename_method ${componentId.slice(componentId.lastIndexOf("::") + 2)} <method> <new name>`);
46467
+ if (foreign !== null) throw new WaironError(foreign);
45462
46468
  const component = loadComponentSpec(componentId);
45463
46469
  if (!component) {
45464
46470
  throw new WaironError(`component-missing: no component has the id "${componentId}".`);
@@ -45519,7 +46525,7 @@ function renameMethod(componentId, methodName, newName, pinSymbol, dryRun) {
45519
46525
  );
45520
46526
  if (dryRun) {
45521
46527
  const realizing = implementations.filter((impl) => movingContracts.has(impl.contract) && impl.methods.some((m) => m.name === methodName));
45522
- const wouldPin = pinSymbol !== false && realizing.some((impl) => impl.methods.find((m) => m.name === methodName)?.symbol === void 0);
46528
+ const wouldPin = realizing.some((impl) => shouldPinSymbol(impl, methodName, pinSymbol));
45523
46529
  const wouldRewrite = [...rewriteRefFields(reachDir, methodRemap, void 0, true), ...rekeyLintAllows(reachDir, rename, true)];
45524
46530
  return {
45525
46531
  component: componentId,
@@ -45551,7 +46557,7 @@ function renameMethod(componentId, methodName, newName, pinSymbol, dryRun) {
45551
46557
  if (!movingContracts.has(implementation.contract)) continue;
45552
46558
  const realization = implementation.methods.find((m) => m.name === methodName);
45553
46559
  if (!realization) continue;
45554
- const pin2 = realization.symbol === void 0 && pinSymbol !== false;
46560
+ const pin2 = shouldPinSymbol(implementation, methodName, pinSymbol);
45555
46561
  if (pin2) pinnedSymbol = methodName;
45556
46562
  saveImplementationSpec({
45557
46563
  ...implementation,
@@ -45645,15 +46651,29 @@ function respellLike(written, oldId, newId) {
45645
46651
  return /^[A-Z]/.test(written) ? pascal(newId) : newId;
45646
46652
  }
45647
46653
  function memberRootWay(qualifiedId2, call) {
46654
+ return projectRootWay(qualifiedId2, call) ?? `It belongs to the member project "${qualifiedId2.slice(0, qualifiedId2.indexOf("::"))}": 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 ${call} there.`;
46655
+ }
46656
+ function projectRootWay(qualifiedId2, call) {
45648
46657
  const alias = qualifiedId2.slice(0, qualifiedId2.indexOf("::"));
45649
- let folder;
45650
46658
  try {
45651
46659
  const node = graph().nodes.find((n) => n.namespace === alias || n.parent === "" && n.mountAlias === alias);
45652
- if (node) folder = path27.relative(getProjectRoot(), node.directory).split(path27.sep).join("/") || ".";
46660
+ if (node) {
46661
+ const folder = path27.relative(getProjectRoot(), node.directory).split(path27.sep).join("/") || ".";
46662
+ return `It belongs to the member project "${node.id ?? alias}" 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 ${call} there.`;
46663
+ }
45653
46664
  } catch {
45654
46665
  }
45655
- const where = folder ? `the member project "${alias}" at ${folder}` : `the member project "${alias}"`;
45656
- return `It belongs to ${where}: 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 ${call} there.`;
46666
+ const external = ownGet(projectConfigRepository.load()?.externals, alias);
46667
+ if (external !== void 0) {
46668
+ const producer = external?.project ?? alias;
46669
+ return `"${alias}" is the external "${producer}", a project this one only reads through its pin: a project's specs are written from its own root \u2014 open a session in that project's folder and call ${call} there.`;
46670
+ }
46671
+ return null;
46672
+ }
46673
+ function foreignIdRefusal(id, code, doing, call) {
46674
+ if (!id.includes("::")) return null;
46675
+ const way = projectRootWay(id, call);
46676
+ return way === null ? null : `${code}: "${id}" lives in another project; ${doing} from that project's own root. ${way}`;
45657
46677
  }
45658
46678
  function renameType(typeId, newId, dryRun) {
45659
46679
  const all = loadTypeSpecs();
@@ -45929,6 +46949,8 @@ function publishedCarriers(type) {
45929
46949
  return mergeUses([...out.values()]);
45930
46950
  }
45931
46951
  function renameParam(componentId, methodName, param, newName, dryRun) {
46952
+ const foreign = foreignIdRefusal(componentId, "chained-component", "rename its parameter", `sdd_rename_param ${componentId.slice(componentId.lastIndexOf("::") + 2)} ${methodName} ${param} <new name>`);
46953
+ if (foreign !== null) throw new WaironError(foreign);
45932
46954
  const component = loadComponentSpec(componentId);
45933
46955
  if (!component) throw new WaironError(`component-missing: no component has the id "${componentId}".`);
45934
46956
  if (componentId.includes("::")) {
@@ -46115,7 +47137,7 @@ function normalizeReferences2(kind, id) {
46115
47137
  function externalSourceOf(declaration2) {
46116
47138
  return readExternalSource(declaration2?.source).source;
46117
47139
  }
46118
- var fs19, path27, FETCHABLE_RE, MOVED_REF_POSITIONS, INTO_MEMBER_POSITIONS, MEMBER_WAI_GROUPS, PLACED_WAI_ENTRIES, publicNameOf, TYPE_POSITION_KEYS, METHOD_CASINGS, IDENTIFIER, PROSE_FIELDS2;
47140
+ var fs19, path27, FETCHABLE_RE, MOVED_REF_POSITIONS, INTO_MEMBER_POSITIONS, MEMBER_WAI_GROUPS, PLACED_WAI_ENTRIES, publicNameOf, TYPE_POSITION_KEYS, QUALIFIED_TYPE_TOKEN, METHOD_CASINGS, IDENTIFIER, PROSE_FIELDS2;
46119
47141
  var init_provision = __esm({
46120
47142
  "src/core/provision.ts"() {
46121
47143
  "use strict";
@@ -46163,6 +47185,7 @@ var init_provision = __esm({
46163
47185
  PLACED_WAI_ENTRIES = /* @__PURE__ */ new Set(["variants", "packs"]);
46164
47186
  publicNameOf = (e) => e.as ?? e.component ?? e.typeDef;
46165
47187
  TYPE_POSITION_KEYS = /* @__PURE__ */ new Set(["type", "returns", "signature", "signatureFrom", "typeDef", "references", "linkedEntity", "holds"]);
47188
+ QUALIFIED_TYPE_TOKEN = /(^|[^A-Za-z0-9_:.-])((?:::)?[A-Za-z0-9_][A-Za-z0-9_-]*(?:(?:::|\.)[A-Za-z0-9_][A-Za-z0-9_-]*)+)/g;
46166
47189
  METHOD_CASINGS = {
46167
47190
  camelCase: /^[a-z][a-zA-Z0-9]*$/,
46168
47191
  PascalCase: /^[A-Z][a-zA-Z0-9]*$/,
@@ -46324,8 +47347,8 @@ function walkForPackages(projectRoot2, currentDir, depth, results) {
46324
47347
  }
46325
47348
  }
46326
47349
  function pathToId(relPath) {
46327
- const basename21 = path28.basename(relPath);
46328
- return basename21.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
47350
+ const basename22 = path28.basename(relPath);
47351
+ return basename22.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
46329
47352
  }
46330
47353
  function pathToName(relPath) {
46331
47354
  const id = pathToId(relPath);
@@ -47024,7 +48047,10 @@ function resolveLayer(delegate, implementers = false) {
47024
48047
  if (owners.size === 1 && owners.has(comp.id)) add2(file);
47025
48048
  }
47026
48049
  for (const type of types.filter((t) => t.componentClass === comp.id && t.subsystem === comp.subsystem)) {
47027
- for (const file of typeSourceFiles(type)) add2(file);
48050
+ for (const file of typeSourceFiles(type)) {
48051
+ const owners = claims.get(file);
48052
+ if (!owners || [...owners].every((o) => o === comp.id)) add2(file);
48053
+ }
47028
48054
  }
47029
48055
  const namesAny = compImpls.some((impl) => codeLocationsOf(impl).length > 0);
47030
48056
  if (ownedPaths.length === 0 && !namesAny) {
@@ -47065,6 +48091,14 @@ function resolveLayer(delegate, implementers = false) {
47065
48091
  updatedAt: comp.updatedAt
47066
48092
  });
47067
48093
  }
48094
+ const implementerRecords = agents.filter((a) => a.template === "implementer");
48095
+ const fencedBy = /* @__PURE__ */ new Map();
48096
+ for (const r of implementerRecords) for (const p of r.ownedPaths) fencedBy.set(p, (fencedBy.get(p) ?? 0) + 1);
48097
+ for (const r of implementerRecords) {
48098
+ if (!r.ownedPaths.some((p) => (fencedBy.get(p) ?? 0) > 1)) continue;
48099
+ r.ownedPaths = r.ownedPaths.filter((p) => (fencedBy.get(p) ?? 0) <= 1);
48100
+ r.writePaths = r.ownedPaths;
48101
+ }
47068
48102
  }
47069
48103
  const now = (/* @__PURE__ */ new Date()).toISOString();
47070
48104
  for (const dom of loadConfig().domains) {
@@ -47123,7 +48157,7 @@ function composeAgentBrief(agentId) {
47123
48157
  ${codeFence.map(shown2).join("\n")}
47124
48158
  ` : sharedPaths.length > 0 && namesAnyCode(record2) ? "Every file this agent's specs name is named by another component's specs too, so none is this agent's alone: they are listed as shared below.\n" : "No spec names a code location yet, so no code location is declared. The spawning session should declare the planned `sourcePath` on the implementation now \u2014 it is code linkage, not part of the approval, so declaring it costs no re-lock \u2014 and that file is then this agent's fence.\n";
47125
48159
  const sharedSection = sharedPaths.length > 0 ? `
47126
- Shared, owned by no single agent \u2014 create or extend these only for what this component needs (an import, a wiring line, a module setting), and name each one you touch in your report. A shared type file is created at its planned home exactly as its spec declares it, never redeclared in your own file:
48160
+ Shared, owned by no single agent \u2014 create or extend these only for what this component needs (an import, a wiring line, a module setting), and name each one you touch in your report. A shared type file is created at its planned home exactly as its spec declares it, never redeclared in your own file. A module root listed here (\`mod.rs\`, \`index.ts\`, \`__init__.py\`) may be another agent's file: add only the line that declares or re-exports your module:
47127
48161
 
47128
48162
  ${sharedPaths.map(shown2).join("\n")}
47129
48163
  ` : "";
@@ -47133,6 +48167,9 @@ ${sharedPaths.map(shown2).join("\n")}
47133
48167
  ## Code write fence
47134
48168
 
47135
48169
  ${ownSection}${sharedSection}${rule}`;
48170
+ instructions = `${instructions.trimEnd()}
48171
+
48172
+ ${linkageSection(record2)}`;
47136
48173
  }
47137
48174
  const guidance = loadAgentOverride(agentId);
47138
48175
  if (guidance !== null) {
@@ -47159,9 +48196,15 @@ ${typeMapping.map((line2) => `- ${line2}`).join("\n")}
47159
48196
  let readPaths = record2.readPaths;
47160
48197
  if (externals.length > 0) {
47161
48198
  const bindings = bindingModulesOf2(record2);
48199
+ const membersOnly = externals.every((e) => e.member);
48200
+ const pinsOnly = externals.every((e) => !e.member);
48201
+ const source = membersOnly ? "the member's live L0 export table names them now" : pinsOnly ? "the pin has them" : "the pin has them (for a member, as its live L0 export table names them now)";
48202
+ const comparedWith = membersOnly ? "that table" : pinsOnly ? "the pin" : "the pin or the table";
47162
48203
  const bindingNote = bindings.length > 0 ? `
47163
- 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(", ")}.
47164
- ` : "\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";
48204
+ The binding modules these implementations name are the one place the code spells the producer's names \u2014 keep them exactly as ${source} (\`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(", ")}.
48205
+ ` : `
48206
+ When 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 ${comparedWith}.
48207
+ `;
47165
48208
  instructions = `${instructions.trimEnd()}
47166
48209
 
47167
48210
  ## Externals used
@@ -47303,6 +48346,23 @@ function loadRegistry() {
47303
48346
  function isSubsystemOwner(record2) {
47304
48347
  return record2.template === "domain-owner" && record2.creationReason.startsWith("Automatically inferred from L1");
47305
48348
  }
48349
+ function linkageSection(record2) {
48350
+ const comps = scopeComponents(record2);
48351
+ const ids = new Set(comps.map((c) => c.id));
48352
+ const contractOwner = new Map(loadInterfaceSpecs().map((i) => [i.id, i.component]));
48353
+ const impls = loadImplementationSpecs().filter((impl) => ids.has(contractOwner.get(impl.contract) ?? ""));
48354
+ const parts = [];
48355
+ if (comps.some((c) => c.componentType === "Portal")) {
48356
+ parts.push("## Handler shape\n\nEach verb's function takes the contract's OWN parameters, in order, under the contract's names. What its framework hands it besides those \u2014 a request, a response, a context, `next` \u2014 is wiring: name it in the implementation's `injectedParams` (it matches at the start or the end of the list, with or without a leading `_`). Two honest shapes: the router unpacks the path, query and body and calls the verb with the contract's parameters; or the function gets the request alone and reads each contract parameter off it by its own name (`req.params.<name>`, `req.query.<name>`, `req.body.<name>`, `url.searchParams.get('<name>')`). A `(req, res)` handler that reads nothing by the contract's names is reported as substitutions. Set the implementation's `router` to the entry its file exports, or to a central route table (`src/routes.ts#ROUTES`). `conformance: off` is only for generated or vendored code: it switches the realization checks off, never the doctrine. The full rules: `sdd-implement`, **Handler shape** and **Routers**.\n");
48357
+ }
48358
+ const declared = impls.filter((impl) => (impl.injectedParams ?? []).length > 0);
48359
+ const listed = declared.length > 0 ? ` Declared now: ${declared.map((impl) => `\`${impl.id}\`: [${(impl.injectedParams ?? []).join(", ")}]`).join("; ")}. These may be guesses written before any code existed: keep the ones your code really takes and remove the rest with \`sdd_update_spec\` (\`{"injectedParams": [{"value": "<name>", "action": "delete"}]}\`, or \`[]\` to clear) \u2014 an injection no function takes is UNUSED_INJECTED_PARAM.` : "";
48360
+ parts.push(`## Code linkage you own
48361
+
48362
+ \`injectedParams\` are code linkage, not design: declare them only when your code takes a parameter its framework or wiring imposes beside the contract's own (a request handle, a context), with \`sdd_write_narrative\` (\`injectedParams\`) or \`sdd_update_spec\` \u2014 no re-lock. A dependency held as a field or passed to a constructor is not one.${listed}
48363
+ `);
48364
+ return parts.join("\n");
48365
+ }
47306
48366
  function namesAnyCode(record2) {
47307
48367
  const ids = new Set(scopeComponents(record2).map((c) => c.id));
47308
48368
  const contracts = new Set(loadInterfaceSpecs().filter((i) => ids.has(i.component)).map((i) => i.id));
@@ -47336,15 +48396,18 @@ function sharedFilesOf(record2, fence) {
47336
48396
  const parts = folder === "." ? [] : folder.split("/");
47337
48397
  for (let i = 1; i <= parts.length; i++) chain.add(parts.slice(0, i).join("/"));
47338
48398
  }
47339
- const join68 = (dir, file) => dir === "." ? file : `${dir}/${file}`;
48399
+ const join69 = (dir, file) => dir === "." ? file : `${dir}/${file}`;
47340
48400
  for (const dir of [...chain].sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b))) {
47341
- for (const file of SHARED_SETUP_FILES) if (fs22.existsSync(path31.join(root, dir, file))) add2(join68(dir, file));
47342
- }
47343
- for (const dir of ownFolders) {
47344
- for (const file of SHARED_ROOT_FILES) if (fs22.existsSync(path31.join(root, dir, file))) add2(join68(dir, file));
48401
+ for (const file of SHARED_SETUP_FILES) if (fs22.existsSync(path31.join(root, dir, file))) add2(join69(dir, file));
47345
48402
  }
47346
48403
  const named2 = new Set(claims.keys());
47347
48404
  for (const type of types) for (const file of typeSourceFiles(type)) named2.add(file);
48405
+ for (const dir of ownFolders) {
48406
+ for (const file of SHARED_ROOT_FILES) {
48407
+ const rootFile = join69(dir, file);
48408
+ if (named2.has(rootFile) || fs22.existsSync(path31.join(root, dir, file))) add2(rootFile);
48409
+ }
48410
+ }
47348
48411
  let siblings = 0;
47349
48412
  for (const dir of ownFolders) {
47350
48413
  let entries = [];
@@ -47354,7 +48417,7 @@ function sharedFilesOf(record2, fence) {
47354
48417
  continue;
47355
48418
  }
47356
48419
  for (const entry of entries.filter((e) => e.isFile()).sort((a, b) => a.name.localeCompare(b.name))) {
47357
- const file = join68(dir, entry.name);
48420
+ const file = join69(dir, entry.name);
47358
48421
  if (siblings >= MAX_UNNAMED_SIBLINGS) break;
47359
48422
  if (!SOURCE_EXTENSIONS2.has(path31.extname(entry.name)) || named2.has(file) || owned.has(file) || out.includes(file)) continue;
47360
48423
  if (/\.(test|spec)\.[a-z]+$/.test(entry.name)) continue;
@@ -47416,7 +48479,9 @@ function externalsUsedBy(record2) {
47416
48479
  const node = externals.has(alias) ? void 0 : memberOf(alias);
47417
48480
  if (node) {
47418
48481
  const system = path31.relative(root, path31.join(node.directory, ".wai", "specs", ".index.yaml")).replace(/\\/g, "/");
47419
- out.push({ alias, names: [...names].sort(), pin: system, pinned: true, bindings: /* @__PURE__ */ new Map(), member: { id: node.id ?? node.namespace, system } });
48482
+ const publicName = memberPublicNames(path31.join(node.directory, ".wai", "specs", ".index.yaml"));
48483
+ const spelled = [...new Set([...names].map((n) => publicName.get(n) ?? n))].sort();
48484
+ out.push({ alias, names: spelled, pin: system, pinned: true, bindings: /* @__PURE__ */ new Map(), member: { id: node.id ?? node.namespace, system } });
47420
48485
  continue;
47421
48486
  }
47422
48487
  const pin2 = `.wai/externals/${alias}.yaml`;
@@ -47434,6 +48499,19 @@ function externalsUsedBy(record2) {
47434
48499
  }
47435
48500
  return out;
47436
48501
  }
48502
+ function memberPublicNames(systemSpec) {
48503
+ const out = /* @__PURE__ */ new Map();
48504
+ try {
48505
+ const doc = readYamlFile(systemSpec);
48506
+ for (const entry of doc?.publicInterfaces ?? []) {
48507
+ const exported = entry.as ?? entry.name;
48508
+ const internal = entry.component ?? entry.typeDef;
48509
+ if (internal && exported && !out.has(internal)) out.set(internal, exported);
48510
+ }
48511
+ } catch {
48512
+ }
48513
+ return out;
48514
+ }
47437
48515
  function bindingModulesOf2(record2) {
47438
48516
  const ids = new Set(implementedComponents(record2).map((c) => c.id));
47439
48517
  const contracts = new Set(loadInterfaceSpecs().filter((i) => ids.has(i.component)).map((i) => i.id));
@@ -52802,7 +53880,7 @@ function registerProjectServer(projectRoot2) {
52802
53880
  writeFile(file, JSON.stringify(settings, null, 2) + "\n");
52803
53881
  return file;
52804
53882
  }
52805
- var fs24, os6, path38, GUIDE_MARKER_START, GUIDE_MARKER_END, GLOBAL_GUIDE_BODY, LOCAL_GUIDE_BODY, ROOT_MARKER_START, ROOT_MARKER_END, CLAUDE_IMPORT, CLAUDE_POINTER_TEXT, LEGACY_POINTERS, GUIDE_TARGETS;
53883
+ var fs24, os6, path38, GUIDE_MARKER_START, GUIDE_MARKER_END, HUMAN_COMMANDS, LINKAGE_FACTS, GLOBAL_GUIDE_BODY, LOCAL_GUIDE_BODY, ROOT_MARKER_START, ROOT_MARKER_END, CLAUDE_IMPORT, CLAUDE_POINTER_TEXT, LEGACY_POINTERS, GUIDE_TARGETS;
52806
53884
  var init_ai_guide = __esm({
52807
53885
  "src/utils/ai-guide.ts"() {
52808
53886
  "use strict";
@@ -52813,6 +53891,19 @@ var init_ai_guide = __esm({
52813
53891
  init_fs();
52814
53892
  GUIDE_MARKER_START = "<!-- wairon-guide-start -->";
52815
53893
  GUIDE_MARKER_END = "<!-- wairon-guide-end -->";
53894
+ HUMAN_COMMANDS = `### What the human runs \u2014 recommend these, never run them
53895
+ The \`wairon\` CLI is the human developer's tool: you never run it, but when the human asks for something only it does, tell them the exact command:
53896
+ - \`wairon lock\` \u2014 approve the design (then commit \`.wai/lock.json\`); \`wairon lock-check\` is the CI merge gate on that approval.
53897
+ - \`wairon validate --ci\` \u2014 the CI gate. It FAILS on any error and on any warning, except the draft-related ones (a \`DRAFT_*\` warning, or an \`UNUSED_COMPONENT\` whose component is itself still draft or design status); notices never fail it, nor do the advisory live-externals findings. Say so plainly \u2014 no run is needed to know it.
53898
+ - \`wairon surface export --format openapi --portal <portal-id> --out <file>\` \u2014 a Portal's OpenAPI document (one per Portal); \`wairon surface diff\` \u2014 the public-surface changelog since the last approval; \`wairon export\` \u2014 the whole resolved design as one JSON document.
53899
+ - \`wairon externals pin <alias>\` \u2014 re-pin an external once its uses are adapted; \`wairon externals status\` \u2014 the live compatibility gate (exit 1 incompatible, 2 not compared).
53900
+ - \`wairon network declare\` \u2014 declare this project's network boundary (your tool for it is \`sdd_set_network\`).
53901
+ - \`wairon member add | attach | detach | adopt | promote | demote | internalize | move | rename-alias | update\` \u2014 the human's twins of the member tools; their \`--report\` is your \`dryRun\`.
53902
+ - \`wairon doctor --fix\` \u2014 rewrite deprecated forms; \`wairon generate\` \u2014 refresh the generated guides, skills and context.
53903
+ - \`wairon agent customize <id>\` \u2014 scaffold \`.wai/agents/<id>.md\`, guidance folded into every brief of that agent; \`wairon agent brief <id>\` \u2014 print a live brief.
53904
+ - \`wairon status\` \u2014 readiness and approval; \`wairon diagram\` \u2014 architecture diagrams; \`wairon network flows\` \u2014 the allowed-flows matrix.`;
53905
+ LINKAGE_FACTS = `- **\`injectedParams\` are set when the code exists, by the implementer**: never declare them at design time. They name a parameter a framework imposes on written code (a request handle, a context) beside the contract's own; the implementer declares them with \`sdd_write_narrative\` (\`injectedParams\`) or \`sdd_update_spec\` once its code takes one, and removes a design-time guess its code does not take (\`UNUSED_INJECTED_PARAM\`) the same way \u2014 code linkage, no re-lock.
53906
+ - **Plain JavaScript is checked too**: a JSDoc \`@typedef\` with \`@property\` lines declares a plain-JS shape, and in a binding module its field names are compared with the producer's type like a TypeScript interface's \u2014 add one rather than telling the human a \`.js\`/\`.cjs\` binding's fields cannot be checked.`;
52816
53907
  GLOBAL_GUIDE_BODY = `## wairon \u2014 Spec-Driven Development (optional)
52817
53908
 
52818
53909
  If \`.wai/specs/\` exists, the wairon SDD workflow is active; otherwise ignore it. wairon does not orchestrate sessions \u2014 it equips yours.
@@ -52829,7 +53920,12 @@ If \`.wai/specs/\` exists, the wairon SDD workflow is active; otherwise ignore i
52829
53920
  6. **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 waits for the human's re-lock; sequence spec turn \u2192 lock \u2192 code turn). Only code linkage is outside it.
52830
53921
  7. **Consistency**: Code must match L3 interfaces and L5 narratives exactly. If the spec is wrong, stop and update the spec.
52831
53922
  8. **Members & References**: A project may declare **members** in its \`.wai/project.yaml\` \`members\` (create one with \`sdd_add_member\`). A **part** (the default) stores some of this project's subsystems in another folder or repository: local ids, this project's lock. A **project** member is an independent boundary with its own spec tree and lock, designed from its own root. Reference what another project exports as \`alias::name\` (the alias is a member or a declared external, the name a public name of its L0 export table); an id without \`::\` is local. A leading \`::\`, \`super::\`, member paths and an L1 subsystem carrying \`projectPath\` are deprecated: they still resolve for one release, are reported, and \`wairon doctor --fix\` rewrites them.
52832
- 9. **Reachability**: every Portal verb is reached by a modelled caller or declared an entry (\`invokedBy: { kind: entry }\`) for real callers outside the design \u2014 never an entry invented to silence a finding.`;
53923
+ 9. **Reachability**: every Portal verb is reached by a modelled caller or declared an entry (\`invokedBy: { kind: entry }\`) for real callers outside the design \u2014 never an entry invented to silence a finding.
53924
+
53925
+ ### Code linkage facts
53926
+ ${LINKAGE_FACTS}
53927
+
53928
+ ${HUMAN_COMMANDS}`;
52833
53929
  LOCAL_GUIDE_BODY = `## Wairon \u2014 Spec-Driven Development (you are operating inside it)
52834
53930
 
52835
53931
  This project uses **wairon**. System specs live under \`.wai/specs/\` (L0 System \u2192 L1 Subsystem \u2192 L2 Component \u2192 L3 Interface \u2192 L4 Implementation \u2192 Narrative); agent topology and code are derived from it.
@@ -52845,14 +53941,17 @@ This project uses **wairon**. System specs live under \`.wai/specs/\` (L0 System
52845
53941
  - **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.
52846
53942
  - **\`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\`).
52847
53943
  - **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\`).
52848
- - **Do not run the \`wairon\` CLI**: Use \`sdd_validate_tree\` and \`sdd_get_status\` instead of CLI commands.
53944
+ - **Do not run the \`wairon\` CLI**: Use \`sdd_validate_tree\` and \`sdd_get_status\` instead of CLI commands \u2014 and name the human's command when they ask for what only it does (see *What the human runs* below).
52849
53945
  - **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.
52850
53946
  - **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.
52851
53947
  - **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.
52852
53948
  - **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)\`.
53949
+ ${LINKAGE_FACTS}
52853
53950
  - **Externals \u2014 the pin gates, live drift is visible**: declare a project this one consumes with \`sdd_add_external\` (alias, and a source \`../sibling\`, \`hosted:<id>\`, \`<git url>\` or \`<git url>#<commit>\`) \u2014 never by hand-editing \`.wai/project.yaml\`; it is checked against the producer and pinned. Change its \`use\` imports with \`sdd_update_external\` and remove it (declaration and pin together) with \`sdd_remove_external\`. A name the producer exports to a narrower audience (\`project\` < \`department\` < \`instance\` < \`partner\` < \`external\`) than this project is read at is refused, naming both. The owner's gate judges every external against its pin. \`sdd_validate_tree\` and \`sdd_get_status\` also compare each external with its LIVE producer, offline, as ADVISORY findings (\`advisory: true\`): \`EXTERNAL_LIVE_INCOMPATIBLE\` (a used member changed, was renamed \u2014 the new name is given \u2014 or is gone), \`EXTERNAL_DRIFTED\`, \`EXTERNAL_LIVE_UNCOMPARED\`. They never make the tree invalid or fail \`--ci\`; the fix is to adapt the uses, then ask the human to re-pin (\`wairon externals pin <alias>\`). A git producer is compared live only by \`wairon externals status\` \u2014 the opt-in live CI gate (exit 1 incompatible, 2 not compared, 0 otherwise).
52854
53951
  - **To implement code**: Delegate via the \`sdd-delegate\` skill: fetch the component's live brief with the \`sdd_get_agent_brief\` MCP tool (or the \`wairon-agent://\` resource) and spawn a subagent from it. Briefs are composed per call from the current spec tree, so they are always current \u2014 never wait for a restart. Implementations must match L3 interfaces and L5 narratives exactly. Generated agent files under \`.claude/agents/\` are an optional materialized view of the same topology \u2014 the live briefs are canonical.
52855
53952
 
53953
+ ${HUMAN_COMMANDS}
53954
+
52856
53955
  ### Rules (enforced by \`sdd_validate_tree\`)
52857
53956
  1. **Design before code**: Complete spec and pass validator before writing source code.
52858
53957
  2. **Human-in-the-loop**: Ask user approval for each spec layer before proceeding.
@@ -53323,6 +54422,15 @@ function getStatusReport(options = {}, decor) {
53323
54422
  const referencedKeys2 = new Set(family.nodes.flatMap((n) => n.externals.filter((e) => e.role === "member").map((e) => n.namespace === "" ? e.alias : `${n.namespace}::${e.alias}`)));
53324
54423
  for (const absent of (options.approvals ?? []).filter((a) => a.key !== "" && a.as !== "part" && !referencedKeys2.has(a.key) && !family.nodes.some((n) => n.namespace === a.key))) {
53325
54424
  output += `${mark.structure(" ")}${mark.missing(`[Project] ${absent.alias ?? absent.key} (no project on disk)`)}${approvalTag(absent)}
54425
+ `;
54426
+ }
54427
+ const dialledOff = implementations.flatMap((impl) => {
54428
+ if (impl.conformance === "off") return [`${impl.id} (every method)`];
54429
+ const off = impl.methods.filter((m) => m.conformance === "off").map((m) => m.name);
54430
+ return off.length > 0 ? [`${impl.id} (${off.join(", ")})`] : [];
54431
+ });
54432
+ if (dialledOff.length > 0) {
54433
+ output += `${mark.layer("system", "Conformance off:")} ${dialledOff.join(", ")} \u2014 realization not checked (method, parameters, async, narrated calls); the doctrine checks still run
53326
54434
  `;
53327
54435
  }
53328
54436
  const own2 = options.approvals?.find((a) => a.key === "");
@@ -53393,6 +54501,7 @@ var init_core = __esm({
53393
54501
  init_external_producers();
53394
54502
  init_external_producers();
53395
54503
  init_external_producers();
54504
+ init_external_producers();
53396
54505
  init_part_context();
53397
54506
  init_provision();
53398
54507
  init_statehash();
@@ -53548,7 +54657,14 @@ brief with \`sdd_get_agent_brief\`, and spawn a scoped subagent from it \u2014 i
53548
54657
  its \`sharedPaths\` as the files it may touch only for its own needs. Briefs are composed
53549
54658
  from the current spec tree on every call, so a re-lock never requires a session
53550
54659
  restart \u2014 fetch fresh per delegation. \`wairon-skill://sdd-delegate\` carries the
53551
- full flow.`;
54660
+ full flow.
54661
+
54662
+ ## Facts to state, not guess
54663
+
54664
+ \`injectedParams\` are never set at design time: the implementer sets them once its
54665
+ code takes one. The human runs the CLI: \`wairon lock\`; \`wairon validate --ci\`
54666
+ (fails on errors and non-draft warnings, never on notices); \`wairon surface export
54667
+ --format openapi --portal <id>\` for a Portal's OpenAPI.`;
53552
54668
  }
53553
54669
  function buildServerInstructions() {
53554
54670
  const profile = governingProfile();
@@ -54245,6 +55361,13 @@ function writeSpec(restatement) {
54245
55361
  CREATE_TOOL[restatement.kind]
54246
55362
  );
54247
55363
  if (foreign !== void 0) throw new Error(foreign);
55364
+ const pathNames = pathBoundNames(restatement, id, parentRef);
55365
+ const notAnId = pathNames.find((n) => idPathProblem(n.value) !== null);
55366
+ if (notAnId) {
55367
+ throw new Error(
55368
+ `not-an-id: the ${notAnId.what} "${notAnId.value}" cannot name a spec \u2014 ${idPathProblem(notAnId.value)}. A spec id, and the subsystem, component or contract a spec is written under, become folder and file names: each is a plain id (letters, digits, "-" and "_", qualified by "::"), never a path. Nothing was written.`
55369
+ );
55370
+ }
54248
55371
  const parent = parentRef ? loadSpec(parentRef.kind, parentRef.id) : null;
54249
55372
  const existing = loadSpec(restatement.kind, id);
54250
55373
  if (!existing && TRACED_KINDS.has(restatement.kind)) {
@@ -54307,6 +55430,23 @@ function writeSpec(restatement) {
54307
55430
  ...cascaded.length > 0 ? { cascaded } : {}
54308
55431
  };
54309
55432
  }
55433
+ function idPathProblem(value) {
55434
+ const segments = value.split("::");
55435
+ const named2 = value.startsWith("::") ? segments.slice(1) : segments;
55436
+ const bad = named2.find((s) => !ID_SEGMENT.test(s));
55437
+ if (bad === void 0) return null;
55438
+ if (/[\\/]/.test(bad)) return `"${bad}" holds a path separator`;
55439
+ if (bad.includes(".")) return `"${bad}" holds a dot`;
55440
+ return bad === "" ? "it has an empty segment" : `"${bad}" holds a character outside letters, digits, "-" and "_"`;
55441
+ }
55442
+ function pathBoundNames(restatement, id, parentRef) {
55443
+ const out = [];
55444
+ if (restatement.kind !== "system" && typeof id === "string") out.push({ what: `${restatement.kind} id`, value: id });
55445
+ if (parentRef && typeof parentRef.id === "string" && parentRef.kind !== "system") out.push({ what: `${parentRef.kind} it is written under`, value: parentRef.id });
55446
+ const group = restatement.spec.group;
55447
+ if (restatement.kind === "type" && typeof group === "string" && group !== "") out.push({ what: "type group", value: group });
55448
+ return out;
55449
+ }
54310
55450
  function methodRemoval(index, contract, component, removed) {
54311
55451
  const realizing = index.implementations.filter((impl) => impl.contract === contract);
54312
55452
  const stillDeclared = (m) => index.interfaces.some((i) => i.component === component && i.id !== contract && i.methods.some((x) => x.name === m));
@@ -54441,9 +55581,9 @@ function referencedRefusal(kind, id, references) {
54441
55581
  ${references.map((r) => `- ${r.kind} "${r.id}" (${r.position}) -> ${r.target.kind} "${r.target.id}"`).join("\n")}
54442
55582
  Edit those specs first, or pass force: true to delete anyway and leave them dangling. Nothing was deleted; dryRun lists the whole plan.`;
54443
55583
  }
54444
- function updateSpecGated(kind, id, delta, dryRun) {
55584
+ function updateSpecGated(kind, id, delta, dryRun, tool = "sdd_update_spec") {
54445
55585
  const bound2 = candidateOptions();
54446
- const foreign = crossProjectWriteRefusal([{ kind, id }], "sdd_update_spec");
55586
+ const foreign = crossProjectWriteRefusal([{ kind, id }], tool);
54447
55587
  if (foreign !== void 0) throw new Error(foreign);
54448
55588
  const testRoots = bound2.rules?.conformance?.testRoots ?? [];
54449
55589
  const stored = loadSpec(kind, id);
@@ -54537,31 +55677,70 @@ function moveMethods2(from, to, methods, dryRun) {
54537
55677
  if (foreign !== void 0) throw new Error(foreign);
54538
55678
  return moveMethods(from, to, methods, methodMoveGate(from), dryRun);
54539
55679
  }
54540
- function moveSpec2(kind, id, subsystem, dryRun) {
55680
+ function moveSpec2(kind, id, subsystem, dryRun, together) {
55681
+ const ids = [.../* @__PURE__ */ new Set([id, ...together ?? []])];
54541
55682
  const foreign = crossProjectWriteRefusal(
54542
- [{ kind, id }, ...subsystem ? [{ kind: "subsystem", id: subsystem }] : []],
55683
+ [...ids.map((sid) => ({ kind, id: sid })), ...subsystem ? [{ kind: "subsystem", id: subsystem }] : []],
54543
55684
  "sdd_move_spec"
54544
55685
  );
54545
55686
  if (foreign !== void 0) throw new Error(foreign);
54546
- if (kind !== "component") return moveSpec(kind, id, subsystem, dryRun);
55687
+ if (kind !== "component") {
55688
+ const plans2 = ids.map((sid) => moveSpec(kind, sid, subsystem, true));
55689
+ if (dryRun) return foldMoves(plans2, true);
55690
+ return foldMoves(ids.map((sid) => moveSpec(kind, sid, subsystem, false)), false);
55691
+ }
54547
55692
  const bound2 = candidateOptions();
54548
- const plan7 = moveSpec(kind, id, subsystem, true);
55693
+ const owners = ids.filter((sid) => !ids.some((other) => other !== sid && ownsTransitively(other, sid)));
55694
+ const plans = owners.map((sid) => moveSpec(kind, sid, subsystem, true));
55695
+ const plan7 = foldMoves(plans, true);
54549
55696
  const moving = plan7.moved.filter((ref) => ref.kind === "component").map((ref) => loadSpec("component", ref.id)).filter((spec) => spec !== null).map((spec) => ({ ...spec, subsystem: plan7.to }));
54550
55697
  const [first, ...rest] = moving;
54551
55698
  const verdict = first ? introducedFindings("component", first, bound2, rest) : { errors: [], warnings: [], notices: [] };
54552
55699
  if (verdict.errors.length > 0) {
55700
+ const left = collaboratorsLeftBehind(moving.map((c) => c.id), plans.map((p) => p.from));
55701
+ const named2 = ids.length > 1 ? `components ${ids.map((sid) => `"${sid}"`).join(", ")}` : `component "${id}"`;
54553
55702
  throw new Error(
54554
- `Refused: moving component "${id}" to subsystem "${plan7.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:
55703
+ `Refused: moving ${named2} to subsystem "${plan7.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:
54555
55704
  ${verdict.errors.map((e) => `- ${e.code}: ${e.message}`).join("\n")}
54556
55705
 
54557
- Nothing was written. Route each such dependency through a client Adapter calling the other subsystem's Portal, or move its collaborators with it.`
55706
+ Nothing was written. Route each such dependency through a client Adapter calling the other subsystem's Portal, or move its collaborators with it` + (left.length > 0 ? ` in the same call \u2014 together: [${[...ids.filter((sid) => sid !== id), ...left].map((sid) => `"${sid}"`).join(", ")}] moves ${left.map((sid) => `"${sid}"`).join(", ")} along and judges the whole set as one move (dryRun first shows the plan).` : ".")
54558
55707
  );
54559
55708
  }
54560
55709
  const notices = noticesFrom(verdict);
54561
- const report4 = dryRun ? plan7 : moveSpec(kind, id, subsystem, false);
55710
+ const report4 = dryRun ? plan7 : foldMoves(owners.map((sid) => moveSpec(kind, sid, subsystem, false)), false);
54562
55711
  return { ...report4, notices: [...report4.notices, ...notices] };
54563
55712
  }
54564
- var STORE_MANAGED_FIELDS, ALWAYS_CARRIED_FIELDS, TRACED_KINDS, STATUS_ORDER, STATUSED_KINDS, MISSING_PARENT, REWRITE_LABEL, SPEC_SCHEMA, METHOD_SCHEMA, CREATE_TOOL;
55713
+ function ownsTransitively(owner, member) {
55714
+ const seen = /* @__PURE__ */ new Set();
55715
+ const visit = (cid) => {
55716
+ if (seen.has(cid)) return false;
55717
+ seen.add(cid);
55718
+ const owns = loadSpec("component", cid)?.owns ?? [];
55719
+ return owns.includes(member) || owns.some(visit);
55720
+ };
55721
+ return visit(owner);
55722
+ }
55723
+ function collaboratorsLeftBehind(moved, from) {
55724
+ const movedSet = new Set(moved);
55725
+ const sources = new Set(from);
55726
+ const all = scanAllSpecs({ memberDepth: 0 }).components;
55727
+ const components = all.filter((c) => sources.has(c.subsystem) && !movedSet.has(c.id));
55728
+ const movedSpecs = all.filter((c) => movedSet.has(c.id));
55729
+ const linked = components.filter((c) => (c.dependsOn ?? []).some((d) => movedSet.has(d)) || movedSpecs.some((m) => (m.dependsOn ?? []).includes(c.id)));
55730
+ const owned = new Set(components.flatMap((c) => c.owns ?? []));
55731
+ return [...new Set(linked.map((c) => owned.has(c.id) ? components.find((o) => (o.owns ?? []).includes(c.id))?.id ?? c.id : c.id))].sort();
55732
+ }
55733
+ function foldMoves(moves, dryRun) {
55734
+ const [first] = moves;
55735
+ return {
55736
+ ...first,
55737
+ moved: moves.flatMap((m) => m.moved),
55738
+ rewritten: [...new Set(moves.flatMap((m) => m.rewritten))],
55739
+ notices: moves.flatMap((m) => m.notices),
55740
+ ...dryRun ? { dryRun: true } : {}
55741
+ };
55742
+ }
55743
+ var STORE_MANAGED_FIELDS, ALWAYS_CARRIED_FIELDS, TRACED_KINDS, STATUS_ORDER, STATUSED_KINDS, MISSING_PARENT, REWRITE_LABEL, SPEC_SCHEMA, METHOD_SCHEMA, ID_SEGMENT, CREATE_TOOL;
54565
55744
  var init_authoring = __esm({
54566
55745
  "src/core/authoring.ts"() {
54567
55746
  "use strict";
@@ -54608,6 +55787,7 @@ var init_authoring = __esm({
54608
55787
  implementation: MethodImplementationSchema,
54609
55788
  type: TypeMethodSchema
54610
55789
  };
55790
+ ID_SEGMENT = /^[A-Za-z0-9_-]+$/;
54611
55791
  CREATE_TOOL = {
54612
55792
  system: "sdd_initialize_system",
54613
55793
  subsystem: "sdd_add_subsystem",
@@ -54754,8 +55934,8 @@ function workloadOf(where, value) {
54754
55934
  };
54755
55935
  }
54756
55936
  function parseYaml2(file) {
54757
- const lines = new import_yaml15.LineCounter();
54758
- const doc = (0, import_yaml15.parseDocument)(readText(file), { lineCounter: lines });
55937
+ const lines = new import_yaml16.LineCounter();
55938
+ const doc = (0, import_yaml16.parseDocument)(readText(file), { lineCounter: lines });
54759
55939
  if (doc.errors.length > 0) {
54760
55940
  const e = doc.errors[0];
54761
55941
  throw new NetworkInputError(`${file}:${lines.linePos(e.pos[0]).line}: ${e.message.split("\n")[0]}`);
@@ -54861,13 +56041,13 @@ function readObservedFlows(path99) {
54861
56041
  const text3 = readText(path99).replace(/^/, "");
54862
56042
  return /\.json$/i.test(path99) ? observedFromJson(path99, text3) : observedFromCsv(path99, text3);
54863
56043
  }
54864
- var fs28, nodePath, import_yaml15, NetworkInputError, OBSERVED_FIELDS;
56044
+ var fs28, nodePath, import_yaml16, NetworkInputError, OBSERVED_FIELDS;
54865
56045
  var init_files = __esm({
54866
56046
  "src/network/adapters/files.ts"() {
54867
56047
  "use strict";
54868
56048
  fs28 = __toESM(require("fs"));
54869
56049
  nodePath = __toESM(require("path"));
54870
- import_yaml15 = require("yaml");
56050
+ import_yaml16 = require("yaml");
54871
56051
  init_errors();
54872
56052
  NetworkInputError = class extends WaironError {
54873
56053
  constructor(message) {
@@ -55196,12 +56376,12 @@ function encodeFlows(flows3, format, rootProject) {
55196
56376
  return { format: "markdown", content: markdownOf(flows3), unbound: [], ...gate };
55197
56377
  }
55198
56378
  }
55199
- function bindingKeys(party) {
56379
+ function bindingKeys2(party) {
55200
56380
  if (party.scope !== void 0) return [workload(party)];
55201
56381
  return [party.component, party.subsystem, party.project, workload(party)].filter((k) => k !== void 0 && k !== "");
55202
56382
  }
55203
56383
  function resolve36(party, bindings) {
55204
- const keys = bindingKeys(party);
56384
+ const keys = bindingKeys2(party);
55205
56385
  const bound2 = keys.find((k) => Object.prototype.hasOwnProperty.call(bindings.workloads, k));
55206
56386
  return bound2 !== void 0 ? { name: bound2, binding: bindings.workloads[bound2] } : { name: workload(party) };
55207
56387
  }
@@ -57233,7 +58413,10 @@ function planAttach(family, bound2, request) {
57233
58413
  const config = configAt3(node.directory) ?? {};
57234
58414
  const held = ownGet(config.members, alias);
57235
58415
  const heldPath = held !== void 0 ? memberLocationOf(held) : void 0;
57236
- if (dir !== null && heldPath !== void 0 && path47.resolve(node.directory, heldPath) === dir) return plan7;
58416
+ if (dir !== null && heldPath !== void 0 && path47.resolve(node.directory, heldPath) === dir) {
58417
+ plan7.notes.push(`"${alias}" is already attached: ${label5(bound2)}'s members already declare ${alias} \u2192 ${heldPath}. Nothing to do.`);
58418
+ return plan7;
58419
+ }
57237
58420
  if (!EXTERNAL_ALIAS_RE.test(alias)) refuse(plan7, "alias-invalid", bound2, `"${alias}" is no alias: ${aliasGrammarProblem(alias)}`);
57238
58421
  else if (ownGet(config.members, alias) !== void 0 || ownGet(config.externals, alias) !== void 0) refuse(plan7, "alias-taken", bound2, `${label5(bound2)} already declares "${alias}"`);
57239
58422
  if (dir !== null) {
@@ -59836,14 +61019,43 @@ function withArgumentNames(description, inputSchema) {
59836
61019
  if (names.length === 0) return description;
59837
61020
  return `${description.trimEnd()} Arguments: ${names.join(", ")}.`;
59838
61021
  }
61022
+ function sameFolder(a, b) {
61023
+ const norm = (p) => {
61024
+ const resolved = path53.resolve(p);
61025
+ return process.platform === "win32" || process.platform === "darwin" ? resolved.toLowerCase() : resolved;
61026
+ };
61027
+ return norm(a) === norm(b);
61028
+ }
61029
+ function initializeFromElsewhereRefusal() {
61030
+ const existing = loadSystemSpec();
61031
+ if (sessionFolder === null || existing === null) return null;
61032
+ const root = getProjectRoot();
61033
+ if (sameFolder(sessionFolder, root)) return null;
61034
+ const rel2 = path53.relative(root, sessionFolder).split(path53.sep).join("/");
61035
+ const inside = rel2 !== "" && !rel2.startsWith("..") && !path53.isAbsolute(rel2);
61036
+ const alias = path53.basename(sessionFolder).toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "member";
61037
+ return `Refused: this session was opened in ${sessionFolder}, which is not the root of the project it is bound to \u2014 ${root}, whose L0 "${existing.name}" already exists. sdd_initialize_system here would re-author THAT project's L0 (its name, vision, boundaries, requirements and targetLanguage). Nothing was written. ` + (inside ? `To make this folder a project of its own, as \`wairon init\` there says: from the root run \`wairon member add ${alias} ${rel2} --project\`, then open a session in this folder (its own guide and .mcp.json) and design it from there. ` : "") + `To re-author ${existing.name}'s L0 itself, open the session at ${root}.`;
61038
+ }
61039
+ function underTreeLock(handler) {
61040
+ try {
61041
+ lockTree2();
61042
+ } catch (e) {
61043
+ return errText(e instanceof Error ? e.message : String(e));
61044
+ }
61045
+ try {
61046
+ return handler();
61047
+ } finally {
61048
+ unlockTree2();
61049
+ }
61050
+ }
59839
61051
  function reg(server, name, config, cb) {
59840
61052
  const guarded2 = (args) => {
59841
61053
  const freshness = assessBuildFreshness(serverBuildStamps.get(server) ?? null);
59842
61054
  if (freshness.writesRefused && SPEC_WRITE_TOOLS.has(name)) {
59843
61055
  return errText(`${name} refused \u2014 ${freshness.reason}`);
59844
61056
  }
59845
- const answered = cb(args);
59846
- const result = freshness.state === "fresh" ? answered : markStale(answered, freshness, name === "sdd_get_status" ? "lead" : "trail");
61057
+ const answered2 = SPEC_WRITE_TOOLS.has(name) ? underTreeLock(() => cb(args)) : cb(args);
61058
+ const result = freshness.state === "fresh" ? answered2 : markStale(answered2, freshness, name === "sdd_get_status" ? "lead" : "trail");
59847
61059
  if (result.isError !== true && SPEC_WRITE_TOOLS.has(name)) listChangedEmitters.get(server)?.();
59848
61060
  return result;
59849
61061
  };
@@ -60100,12 +61312,14 @@ function createBareMcpServer(options = {}) {
60100
61312
  server,
60101
61313
  "sdd_initialize_system",
60102
61314
  {
60103
- description: "Initialize the L0 System Specification (system.yaml). Re-running it on an existing system RE-AUTHORS it: the fields above are replaced, and everything this tool cannot express (databases, the project gateway publicInterfaces, diagram defaults) is carried forward. The answer carries a write receipt as structured content beside the sentence \u2014 what was written, and whether a system spec already existed \u2014 so a caller never has to read English to find out.",
61315
+ description: "Initialize the L0 System Specification (system.yaml). Re-running it on an existing system RE-AUTHORS it: the fields above are replaced, and everything this tool cannot express (databases, the project gateway publicInterfaces, diagram defaults) is carried forward. Refused, writing nothing, when the session was opened in a folder that is not the bound project's root (it bound upward to the project above it) and that project already has an L0: the refusal names the root and the next step \u2014 `wairon member add <alias> <path> --project` from the root makes the folder a project of its own. The answer carries a write receipt as structured content beside the sentence \u2014 what was written, and whether a system spec already existed \u2014 so a caller never has to read English to find out.",
60104
61316
  inputSchema: systemInput,
60105
61317
  outputSchema: specWriteReceiptOutput
60106
61318
  },
60107
61319
  ({ name, vision, boundaries, globalRequirements, targetLanguage }) => {
60108
61320
  try {
61321
+ const elsewhere = initializeFromElsewhereRefusal();
61322
+ if (elsewhere !== null) return errText(elsewhere);
60109
61323
  const restatement = {
60110
61324
  kind: "system",
60111
61325
  spec: {
@@ -60211,7 +61425,7 @@ function createBareMcpServer(options = {}) {
60211
61425
  if (!sub) return errText(`Subsystem "${subsystem}" does not exist.`);
60212
61426
  const delta = publicInterfacesReplacement(subsystem, sub.publicInterfaces ?? [], publicInterfaces);
60213
61427
  if (typeof delta === "string") return errText(delta);
60214
- const report4 = updateSpecGated("subsystem", subsystem, delta);
61428
+ const report4 = updateSpecGated("subsystem", subsystem, delta, void 0, "sdd_set_public_interfaces");
60215
61429
  return structured(
60216
61430
  `Updated public interfaces for subsystem "${subsystem}" (${publicInterfaces.length} ${publicInterfaces.length === 1 ? "entry" : "entries"}).
60217
61431
  ${renderChangeReport(report4)}`,
@@ -60237,9 +61451,15 @@ ${renderChangeReport(report4)}`,
60237
61451
  },
60238
61452
  ({ alias, source, description, as }) => {
60239
61453
  try {
61454
+ const root = getProjectRoot();
61455
+ const dir = typeof source === "string" && !/^[a-z][a-z0-9+.-]*:|^git@|#/i.test(source.trim()) ? path53.resolve(root, source.trim()) : null;
61456
+ const relToRoot = dir === null ? null : path53.relative(root, dir);
61457
+ const outside = dir !== null && relToRoot !== null && (relToRoot.startsWith("..") || path53.isAbsolute(relToRoot));
61458
+ const createdOutside = outside && !fs35.existsSync(dir);
60240
61459
  const creation = createMember(alias, source, description, as);
60241
61460
  const next = creation.as === "project" ? `${describeMemberPacks(creation)} Design it from its own root and export what others consume from its L0; reference it here as ${alias}::<name>.` : ` Its subsystems are this project's own: write them by their local ids. sdd_promote_member makes it a project when it needs its own team, release, approval or public surface.`;
60242
- return structured(`Added the ${creation.as} "${alias}" at ${source}${creation.commit ? ` (pinned at ${creation.commit})` : ""}, declared in project.yaml \`members\`.${next}`, creation);
61461
+ const created = createdOutside ? `Created a new folder OUTSIDE this project, at ${dir} (beside ${root}). ` : "";
61462
+ return structured(`${created}Added the ${creation.as} "${alias}" at ${source}${creation.commit ? ` (pinned at ${creation.commit})` : ""}, declared in project.yaml \`members\`.${next}`, creation);
60243
61463
  } catch (e) {
60244
61464
  return errText(String(e));
60245
61465
  }
@@ -60448,12 +61668,12 @@ ${renderChangeReport(report4)}`,
60448
61668
  server,
60449
61669
  "sdd_rename_method",
60450
61670
  {
60451
- description: "Rename a contract method and retarget every reference to it in the bound tree. The method moves on every interface of the component that declares it \u2014 its name, and the name inside its signature \u2014 and on the implementations of those contracts, carrying narrative, sourcePath, symbol, detail, intent and findings unchanged; an implementation that declared no symbol is pinned to the old name unless pinSymbol is false, so the function it already binds to keeps binding. Narrative call, register and dispatch steps naming this component and method, dispatch-table bindings and lifecycle entrypoints are retargeted, and so are the findings keyed on it: a lint allow at the method on a spec it moved in or covering `<component>.<method>`, and every debt-register entry (.wai/project.yaml rules.conformance.carried) keyed the same way \u2014 rewritten in place with every comment and all other formatting kept (a register that cannot be rewritten that precisely is refused before the first write). Prose is never rewritten and a gRPC endpoint binding keeps its wire method \u2014 renaming a contract method must not silently rename an RPC; both are reported as mentions. Refuses, writing nothing: a component that does not exist (component-missing), one inside a chained subproject (chained-component \u2014 rename its method from that project's own root), a new name that is not an identifier in the method casing of the tree \u2014 the configured rules.naming.methods, else the convention of its targetLanguage: snake_case for Rust or Python, camelCase for TypeScript; for a component one of whose contracts implements another project's extension point, any identifier, since the producer names those methods (invalid-name), a method the component does not declare (method-missing), and a name a moving contract already declares (name-taken). Returns the specs the method moved in, the specs retargeted (lint allows included), the specs whose prose still names it, the pinned symbol when one was set, and `carried`: each register edit.",
61671
+ description: "Rename a contract method and retarget every reference to it in the bound tree. The method moves on every interface of the component that declares it \u2014 its name, and the name inside its signature \u2014 and on the implementations of those contracts, carrying narrative, sourcePath, symbol, detail, intent and findings unchanged; an implementation that declared no symbol is pinned to the old name when its code exists (the file realizing the method is on disk), so the function it already binds to keeps binding \u2014 with no code written yet nothing is pinned unless pinSymbol is true, and pinSymbol false never pins. Narrative call, register and dispatch steps naming this component and method, dispatch-table bindings and lifecycle entrypoints are retargeted, and so are the findings keyed on it: a lint allow at the method on a spec it moved in or covering `<component>.<method>`, and every debt-register entry (.wai/project.yaml rules.conformance.carried) keyed the same way \u2014 rewritten in place with every comment and all other formatting kept (a register that cannot be rewritten that precisely is refused before the first write). Prose is never rewritten and a gRPC endpoint binding keeps its wire method \u2014 renaming a contract method must not silently rename an RPC; both are reported as mentions. Refuses, writing nothing: a component that does not exist (component-missing), one inside a chained subproject (chained-component \u2014 rename its method from that project's own root), a new name that is not an identifier in the method casing of the tree \u2014 the configured rules.naming.methods, else the convention of its targetLanguage: snake_case for Rust or Python, camelCase for TypeScript; for a component one of whose contracts implements another project's extension point, any identifier, since the producer names those methods (invalid-name), a method the component does not declare (method-missing), and a name a moving contract already declares (name-taken). Returns the specs the method moved in, the specs retargeted (lint allows included), the specs whose prose still names it, the pinned symbol when one was set, and `carried`: each register edit.",
60452
61672
  inputSchema: {
60453
61673
  id: import_zod13.z.string().describe("The component whose method is renamed (namespaced if needed)"),
60454
61674
  method: import_zod13.z.string().describe("The method name as it stands"),
60455
61675
  newName: import_zod13.z.string().describe("Its new name: an identifier in the method casing of the tree (snake_case in a Rust or Python tree, camelCase in a TypeScript one)"),
60456
- pinSymbol: import_zod13.z.boolean().optional().describe("Whether an implementation that declares no symbol is pinned to the old name so its function still binds; true when omitted"),
61676
+ pinSymbol: import_zod13.z.boolean().optional().describe("Whether an implementation that declares no symbol is pinned to the old name so its function still binds: omitted, only when the file realizing the method exists; true always pins, false never does"),
60457
61677
  dryRun: import_zod13.z.boolean().optional().describe("Answer what the rename would move, retarget and break (the consumers that call the method through an export), and write nothing"),
60458
61678
  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")
60459
61679
  }
@@ -60595,17 +61815,18 @@ ${renderChangeReport(report4)}`,
60595
61815
  server,
60596
61816
  "sdd_move_spec",
60597
61817
  {
60598
- 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.",
61818
+ 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, in either spelling and wherever it stands in a type expression (`old::type` becomes `new::type`, `list<old.type>` becomes `list<new.type>`); omit subsystem to make a type system-level. Several specs of the same kind move as ONE move with `together` \u2014 a connected cluster (a subsystem split) whose edges no single move could keep inside one subsystem: every one is planned, the components are judged together in their new subsystem, and all are written or none. 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 \u2014 naming the collaborators left behind as the `together` list that takes them along \u2014 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.",
60599
61819
  inputSchema: {
60600
61820
  kind: import_zod13.z.enum(["component", "type"]).describe("component or type"),
60601
61821
  id: import_zod13.z.string().describe("The spec to move (a type by its bare id, or as <subsystem>::<id>)"),
60602
61822
  subsystem: import_zod13.z.string().optional().describe("The subsystem it moves to; omitted for a type made system-level"),
60603
- 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")
61823
+ 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"),
61824
+ together: import_zod13.z.array(import_zod13.z.string()).optional().describe("More specs of the same kind that move with it to the same subsystem, as one move judged together \u2014 the collaborators a single move's refusal names")
60604
61825
  }
60605
61826
  },
60606
- ({ kind, id, subsystem, dryRun }) => {
61827
+ ({ kind, id, subsystem, dryRun, together }) => {
60607
61828
  try {
60608
- return json(moveSpec2(kind, id, subsystem, dryRun));
61829
+ return json(moveSpec2(kind, id, subsystem, dryRun, together));
60609
61830
  } catch (e) {
60610
61831
  return errText(String(e));
60611
61832
  }
@@ -60859,7 +62080,7 @@ ${renderChangeReport(report4)}`,
60859
62080
  methods.push({ name: e.method, endpoint: parsed.data });
60860
62081
  bound2.push(`${e.method}\u2192${transport}`);
60861
62082
  }
60862
- const report4 = updateSpecGated("interface", interfaceId, { methods }, dryRun);
62083
+ const report4 = updateSpecGated("interface", interfaceId, { methods }, dryRun, "sdd_set_endpoints");
60863
62084
  return structured(
60864
62085
  `${dryRun ? "Would bind" : "Bound"} ${bound2.length} endpoint(s) on "${interfaceId}": ${bound2.join(", ")}.
60865
62086
  ${renderChangeReport(report4)}`,
@@ -60931,7 +62152,7 @@ ${renderChangeReport(report4)}`,
60931
62152
  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)"),
60932
62153
  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"),
60933
62154
  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.`),
60934
- 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"),
62155
+ injectedParams: import_zod13.z.array(import_zod13.z.string()).optional().describe("Code linkage, set by the implementer once the code exists \u2014 never at design time: leave it out while designing, and remove a name the code does not take. 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"),
60935
62156
  detail: detailEnum.optional().describe("Spec-level narrative detail default for all methods"),
60936
62157
  conformance: conformanceEnum.optional().describe("Spec-level conformance tier default: declared | anchored | off (omitted = stereotype default: Portal \u2192 anchored, else declared)"),
60937
62158
  methods: import_zod13.z.array(import_zod13.z.object(implMethodShape).strict()).optional().describe("Method implementations containing L5 narratives"),
@@ -60942,7 +62163,7 @@ ${renderChangeReport(report4)}`,
60942
62163
  server,
60943
62164
  "sdd_write_narrative",
60944
62165
  {
60945
- 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.",
62166
+ 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). injectedParams are code linkage, set when the code exists: do NOT declare them while designing \u2014 the implementer declares them once its code takes such a parameter, and removes a design-time guess its code does not take (UNUSED_INJECTED_PARAM); neither asks for a re-lock. 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.",
60946
62167
  inputSchema: implInput,
60947
62168
  outputSchema: specWriteReceiptOutput
60948
62169
  },
@@ -61126,7 +62347,7 @@ ${renderChangeReport(report4)}`,
61126
62347
  server,
61127
62348
  "sdd_get_spec",
61128
62349
  {
61129
- description: `Get/read the parsed JSON contents of a specific spec from the spec tree. Returns structural contents without file system path searching. Pass "methods" to read only the named methods of a contract, an implementation or a type \u2014 a 45-method spec fetched whole to look at one of them is the read side of the same waste a restatement is on the write side; the answer then carries a "partialResult" marker naming what was left out, and must never be re-authored from. For a variant-tagged COMPONENT the result also carries a derived, read-only "variantGuidance" (the variant's base, its implementation guidance, and the same-variant sibling components to implement alike) \u2014 it is resolved from the variant registry, not part of the spec, so never write it back. A contract's methods come back in their STORED form \u2014 a method that takes its signature from a source carries its signatureFrom, not the params the loader resolves into it \u2014 so the answer can be re-authored from as it is; the resolved params, returns and text of each such method come back as a derived, read-only "resolvedSignatures" marker. The structured content carries the same answer with the derived markers KEPT SEPARATE from the stored spec ({kind, id, spec, partialResult?, variantGuidance?, resolvedSignatures?}), so nothing derived can be mistaken for something stored; the text block folds them in as it always has.`,
62350
+ description: `Get/read the parsed JSON contents of a specific spec from the spec tree. Returns structural contents without file system path searching. An \`alias::name\` id resolves through the bound project's alias table exactly as a write resolves it: a member's spec reads under its key, and an EXTERNAL's spec reads from the pinned snapshot this project holds of it \u2014 the pinned contract entry or exported type as the spec, a read-only "pinnedSnapshot" marker beside it (it is written from the producer's own root). Pass "methods" to read only the named methods of a contract, an implementation or a type \u2014 a 45-method spec fetched whole to look at one of them is the read side of the same waste a restatement is on the write side; the answer then carries a "partialResult" marker naming what was left out, and must never be re-authored from. For a variant-tagged COMPONENT the result also carries a derived, read-only "variantGuidance" (the variant's base, its implementation guidance, and the same-variant sibling components to implement alike) \u2014 it is resolved from the variant registry, not part of the spec, so never write it back. A contract's methods come back in their STORED form \u2014 a method that takes its signature from a source carries its signatureFrom, not the params the loader resolves into it \u2014 so the answer can be re-authored from as it is; the resolved params, returns and text of each such method come back as a derived, read-only "resolvedSignatures" marker. The structured content carries the same answer with the derived markers KEPT SEPARATE from the stored spec ({kind, id, spec, partialResult?, variantGuidance?, resolvedSignatures?}), so nothing derived can be mistaken for something stored; the text block folds them in as it always has.`,
61130
62351
  inputSchema: {
61131
62352
  kind: import_zod13.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).optional().describe("The kind of specification. Omit it to infer the kind from the id: the read is refused, naming the candidate kinds, when the id names specs at more than one level (an interface and an implementation sharing an id, say), or at none"),
61132
62353
  id: import_zod13.z.string().describe('The identifier of the spec to fetch (the L0 system spec is a singleton \u2014 pass the system name or "system")'),
@@ -61136,11 +62357,34 @@ ${renderChangeReport(report4)}`,
61136
62357
  },
61137
62358
  ({ kind: askedKind, id, methods }) => {
61138
62359
  try {
62360
+ const pinnedAnswer = (asked) => {
62361
+ if (!id.includes("::") || methods !== void 0) return null;
62362
+ const read2 = readPinnedSpec(asked, id);
62363
+ if (!read2) return null;
62364
+ const marker = {
62365
+ alias: read2.alias,
62366
+ project: read2.project,
62367
+ file: read2.file,
62368
+ ...read2.stateId !== void 0 ? { stateId: read2.stateId } : {},
62369
+ readOnly: true,
62370
+ note: `"${read2.alias}" is an external of this project: this is its pinned ${read2.kind === "type" ? "type" : "contract entry"} "${read2.id}" as ${read2.file} records it \u2014 written from ${read2.project}'s own root, never here.`
62371
+ };
62372
+ return structured(JSON.stringify({ ...read2.spec, pinnedSnapshot: marker }, null, 2), {
62373
+ kind: read2.kind === "type" ? "type" : asked ?? "interface",
62374
+ id,
62375
+ spec: read2.spec,
62376
+ pinnedSnapshot: marker
62377
+ });
62378
+ };
61139
62379
  let kind;
61140
62380
  if (askedKind) {
61141
62381
  kind = askedKind;
61142
62382
  } else {
61143
62383
  const holders = specKindsHolding(id);
62384
+ if (holders.length === 0) {
62385
+ const pinned = pinnedAnswer(void 0);
62386
+ if (pinned) return pinned;
62387
+ }
61144
62388
  if (holders.length !== 1) {
61145
62389
  return errText(holders.length === 0 ? `No spec of any kind has the ID "${id}".` : `The ID "${id}" names specs of more than one kind (${holders.join(", ")}). Pass "kind" to choose one.`);
61146
62390
  }
@@ -61168,7 +62412,7 @@ ${renderChangeReport(report4)}`,
61168
62412
  result = loadTypeSpec(id);
61169
62413
  break;
61170
62414
  }
61171
- if (!result) return errText(`Spec of kind "${kind}" with ID "${id}" does not exist.`);
62415
+ if (!result) return pinnedAnswer(kind) ?? errText(`Spec of kind "${kind}" with ID "${id}" does not exist.`);
61172
62416
  let resolvedSignatures = [];
61173
62417
  if (kind === "interface") {
61174
62418
  const answer = storedContractAnswer(result);
@@ -61671,6 +62915,13 @@ async function scopeToClientWorkspace(server) {
61671
62915
  } catch {
61672
62916
  return;
61673
62917
  }
62918
+ for (const r of roots) {
62919
+ try {
62920
+ sessionFolder = path53.resolve(r.uri.startsWith("file:") ? (0, import_url.fileURLToPath)(r.uri) : r.uri);
62921
+ break;
62922
+ } catch {
62923
+ }
62924
+ }
61674
62925
  for (const r of roots) {
61675
62926
  let dir = null;
61676
62927
  try {
@@ -61691,6 +62942,8 @@ async function scopeToClientWorkspace(server) {
61691
62942
  }
61692
62943
  }
61693
62944
  async function startMcpServer() {
62945
+ const pinned = process.env["WAIRON_PROJECT_DIR"];
62946
+ sessionFolder = path53.resolve(pinned && fs35.existsSync(path53.join(pinned, ".wai")) ? pinned : process.cwd());
61694
62947
  const server = createMcpServer();
61695
62948
  const transport = new import_stdio.StdioServerTransport();
61696
62949
  await server.connect(transport);
@@ -61702,7 +62955,7 @@ async function startMcpServer() {
61702
62955
  } catch {
61703
62956
  }
61704
62957
  }
61705
- var import_mcp, import_stdio, import_zod13, import_types17, fs35, path53, import_url, crypto10, TYPE_REF_GRAMMAR, staleServerOutput, externalPinsOutput, externalAdditionOutput, externalRemovalOutput, externalConsumersOutput, surfaceDiffOutput, externalUseChangeOutput, flowPartyOutput, networkFlowOutput, networkFlowsOutput, flowExplanationOutput, networkDeclarationOutput, externalStatusesOutput, SPEC_KINDS, STATUS_VALUES, testsToRevisitOutput, typeRespellingOutput, exportUseOutput, breakingConsumerOutput, specWriteReceiptOutput, specDeletionOutput, specChangeOutput, specChangeReportOutput, validationIssueOutput, memberCreationOutput, doctrineChangeOutput, impactTotalsOutput, packImpactOutput, validateTreeOutput, getSpecOutput, FINGERPRINT_VERSION, runningFingerprint, RECONNECT, assessedAsChanged, serverBuildStamps, SERVER_BUILD_STAMP, SPEC_WRITE_TOOLS, listChangedEmitters, statusInput, SKILL_RESOURCE_MIME, AGENT_BRIEF_SCHEME;
62958
+ var import_mcp, import_stdio, import_zod13, import_types17, fs35, path53, import_url, crypto10, TYPE_REF_GRAMMAR, staleServerOutput, externalPinsOutput, externalAdditionOutput, externalRemovalOutput, externalConsumersOutput, surfaceDiffOutput, externalUseChangeOutput, flowPartyOutput, networkFlowOutput, networkFlowsOutput, flowExplanationOutput, networkDeclarationOutput, externalStatusesOutput, SPEC_KINDS, STATUS_VALUES, testsToRevisitOutput, typeRespellingOutput, exportUseOutput, breakingConsumerOutput, specWriteReceiptOutput, specDeletionOutput, specChangeOutput, specChangeReportOutput, validationIssueOutput, memberCreationOutput, doctrineChangeOutput, impactTotalsOutput, packImpactOutput, validateTreeOutput, getSpecOutput, FINGERPRINT_VERSION, runningFingerprint, RECONNECT, assessedAsChanged, serverBuildStamps, SERVER_BUILD_STAMP, SPEC_WRITE_TOOLS, listChangedEmitters, sessionFolder, statusInput, SKILL_RESOURCE_MIME, AGENT_BRIEF_SCHEME;
61706
62959
  var init_server = __esm({
61707
62960
  "src/mcp/server.ts"() {
61708
62961
  "use strict";
@@ -62112,6 +63365,16 @@ var init_server = __esm({
62112
63365
  }).optional().describe(
62113
63366
  "Present only when `methods` filtered the read. A filtered spec is a partial one: sdd_define_interface and sdd_write_narrative REPLACE the method list, so re-authoring from it would delete every method left out."
62114
63367
  ),
63368
+ pinnedSnapshot: import_zod13.z.object({
63369
+ alias: import_zod13.z.string().describe("The external's alias the read went through."),
63370
+ project: import_zod13.z.string().describe("The producer project the alias names."),
63371
+ file: import_zod13.z.string().describe("The pinned snapshot file the entry was read from, project-relative."),
63372
+ stateId: import_zod13.z.string().optional().describe("The producer's StateId the pin was taken at, when recorded."),
63373
+ readOnly: import_zod13.z.literal(true).describe("Always true: a pinned entry is the pin's record, written from the producer's own root."),
63374
+ note: import_zod13.z.string().describe("One sentence saying where the entry comes from and where it is written.")
63375
+ }).optional().describe(
63376
+ "Present only when the id named an EXTERNAL through its alias (`alias::name`): `spec` is then the entry the pinned snapshot records \u2014 read-only, never a spec of this tree, never written back."
63377
+ ),
62115
63378
  variantGuidance: import_zod13.z.object({
62116
63379
  variant: import_zod13.z.string().describe("The variant id the component declares."),
62117
63380
  base: import_zod13.z.string().describe("The core stereotype it specializes."),
@@ -62174,6 +63437,7 @@ var init_server = __esm({
62174
63437
  "sdd_move_methods"
62175
63438
  ]);
62176
63439
  listChangedEmitters = /* @__PURE__ */ new WeakMap();
63440
+ sessionFolder = null;
62177
63441
  statusInput = import_zod13.z.enum(STATUS_VALUES).optional().describe(
62178
63442
  "The spec's lifecycle status. Omitted means draft for a NEW spec and the status already stored for a re-authoring, so a restatement never reopens a frozen spec. State it to author straight at design or complete instead of promoting afterwards. A status that would LOWER the stored one is refused \u2014 reopening a spec for revision is sdd_update_spec's job, which sets the demotion deliberately."
62179
63443
  );
@@ -84191,6 +85455,32 @@ function exportGateErrors() {
84191
85455
  return [];
84192
85456
  }
84193
85457
  }
85458
+ function unresolvedAdvice(unresolved) {
85459
+ let config = null;
85460
+ try {
85461
+ config = loadProjectConfig();
85462
+ } catch {
85463
+ config = null;
85464
+ }
85465
+ const members = new Set(Object.keys(config?.members ?? {}));
85466
+ const externals = new Set(Object.keys(config?.externals ?? {}));
85467
+ const own2 = new Set([config?.id, config?.name].filter((n) => !!n));
85468
+ const by = /* @__PURE__ */ new Map();
85469
+ for (const name of unresolved) {
85470
+ const alias = name.includes("::") ? name.slice(0, name.indexOf("::")) : "";
85471
+ by.set(alias, [...by.get(alias) ?? [], name]);
85472
+ }
85473
+ const said = [];
85474
+ for (const [alias, names] of by) {
85475
+ const list2 = names.join(", ");
85476
+ if (externals.has(alias)) said.push(`${list2}: pin the external "${alias}" (\`wairon externals pin ${alias}\`), or export the type from its L0`);
85477
+ else if (members.has(alias)) said.push(`${list2}: the member "${alias}" is read live and never pinned \u2014 export the type from its L0, or, when the name is one it retired, follow the rename at the project that writes it`);
85478
+ else if (own2.has(alias)) said.push(`${list2}: this project's own L0 export table names no such type \u2014 export it there, or point the reference at the project that holds it`);
85479
+ else if (alias === "") said.push(`${list2}: no type of this project's own has that name`);
85480
+ else said.push(`${list2}: "${alias}" is neither a member nor an external this project declares`);
85481
+ }
85482
+ return said.join("; ");
85483
+ }
84194
85484
  function printSurfaceDiff(diff4) {
84195
85485
  logger.info(`Public surface of "${diff4.project}" against ${diff4.against}:`);
84196
85486
  if (!diff4.changes.length) {
@@ -84245,7 +85535,7 @@ async function runSurface(action, options = {}) {
84245
85535
  }
84246
85536
  const unresolved = result.unresolvedTypes ?? [];
84247
85537
  if (unresolved.length > 0) {
84248
- status2("warn", `The document has no schema for ${unresolved.length} type(s) it names: ${unresolved.join(", ")}. Each reads "Unresolved type" where a client generator expects a schema: pin the external it comes from (\`wairon externals pin\`), or export the type from the member's or external's L0.`);
85538
+ status2("warn", `The document has no schema for ${unresolved.length} type(s) it names: ${unresolved.join(", ")}. Each reads "Unresolved type" where a client generator expects a schema: ${unresolvedAdvice(unresolved)}.`);
84249
85539
  }
84250
85540
  if (gateErrors.length > 0) {
84251
85541
  const codes = [...new Set(gateErrors)].map((c) => `${c}${gateErrors.filter((x) => x === c).length > 1 ? ` \xD7${gateErrors.filter((x) => x === c).length}` : ""}`).join(", ");