@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/index.js CHANGED
@@ -3059,7 +3059,7 @@ var require_dist = __commonJS({
3059
3059
  function slug(name) {
3060
3060
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3061
3061
  }
3062
- var fs33 = __toESM2(require("fs"));
3062
+ var fs34 = __toESM2(require("fs"));
3063
3063
  var path52 = __toESM2(require("path"));
3064
3064
  var import_fflate = require_node();
3065
3065
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".hg", ".svn"]);
@@ -3099,8 +3099,8 @@ var require_dist = __commonJS({
3099
3099
  function writeTree(destDir, files) {
3100
3100
  for (const file of files) {
3101
3101
  const absolute = path52.join(destDir, file.path);
3102
- fs33.mkdirSync(path52.dirname(absolute), { recursive: true });
3103
- fs33.writeFileSync(absolute, file.contents);
3102
+ fs34.mkdirSync(path52.dirname(absolute), { recursive: true });
3103
+ fs34.writeFileSync(absolute, file.contents);
3104
3104
  }
3105
3105
  }
3106
3106
  function classifyKind(name, symlinks) {
@@ -3109,14 +3109,14 @@ var require_dist = __commonJS({
3109
3109
  return "file";
3110
3110
  }
3111
3111
  function walkPackDir(root, current2, out) {
3112
- for (const entry of fs33.readdirSync(current2, { withFileTypes: true })) {
3112
+ for (const entry of fs34.readdirSync(current2, { withFileTypes: true })) {
3113
3113
  if (entry.isDirectory()) {
3114
3114
  if (SKIP_DIRS.has(entry.name)) continue;
3115
3115
  walkPackDir(root, path52.join(current2, entry.name), out);
3116
3116
  } else if (entry.isFile()) {
3117
3117
  const absolute = path52.join(current2, entry.name);
3118
3118
  const relative27 = path52.relative(root, absolute).split(path52.sep).join("/");
3119
- out.push({ path: relative27, contents: fs33.readFileSync(absolute) });
3119
+ out.push({ path: relative27, contents: fs34.readFileSync(absolute) });
3120
3120
  }
3121
3121
  }
3122
3122
  }
@@ -3792,6 +3792,7 @@ __export(src_exports, {
3792
3792
  localApprover: () => localApprover,
3793
3793
  localGuideFilePath: () => localGuideFilePath,
3794
3794
  localTypesOf: () => localTypesOf,
3795
+ lockTree: () => lockTree2,
3795
3796
  logger: () => logger,
3796
3797
  makeScopeFilter: () => makeScopeFilter,
3797
3798
  markSelectionsBundled: () => markSelectionsBundled,
@@ -3801,6 +3802,7 @@ __export(src_exports, {
3801
3802
  memberDeclarationOf: () => memberDeclarationOf,
3802
3803
  memberDigest: () => memberDigest,
3803
3804
  memberLocationOf: () => memberLocationOf,
3805
+ memberRevisions: () => memberRevisions,
3804
3806
  memberTypesOf: () => memberTypesOf,
3805
3807
  methodCasingFor: () => methodCasingFor,
3806
3808
  methodGenericParameters: () => methodGenericParameters,
@@ -3849,6 +3851,7 @@ __export(src_exports, {
3849
3851
  readJsonFile: () => readJsonFile,
3850
3852
  readLockRecord: () => readLockRecord,
3851
3853
  readLockState: () => readLockState,
3854
+ readPinnedSpec: () => readPinnedSpec,
3852
3855
  readRetiredReachForms: () => readRetiredReachForms,
3853
3856
  readStampVersion: () => readStampVersion,
3854
3857
  readYamlFile: () => readYamlFile,
@@ -3957,6 +3960,7 @@ __export(src_exports, {
3957
3960
  typeSourceFiles: () => typeSourceFiles,
3958
3961
  typeSpellingFacts: () => typeSpellingFacts,
3959
3962
  uninstallPack: () => uninstallPack,
3963
+ unlockTree: () => unlockTree2,
3960
3964
  updateMember: () => updateMember,
3961
3965
  updateSpec: () => updateSpec,
3962
3966
  upsertPackSelection: () => upsertPackSelection,
@@ -4324,7 +4328,7 @@ function defaultTargetConfig(type) {
4324
4328
  enabled: true
4325
4329
  };
4326
4330
  }
4327
- var WAIRON_VERSION = "5.1.1-dev.113";
4331
+ var WAIRON_VERSION = "5.1.1-dev.114";
4328
4332
  var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
4329
4333
  var ARCHITECT_AGENT_ID = "agent-architect";
4330
4334
  var ARCHITECT_TEMPLATE_ID = "architect";
@@ -4570,6 +4574,13 @@ function lastCommitOf(directory, pathspec) {
4570
4574
  return null;
4571
4575
  }
4572
4576
  }
4577
+ function commitIntroducing(directory, pathspec, text3) {
4578
+ try {
4579
+ return git(["log", "--reverse", "--format=%H", `-S${text3}`, "--", pathspec], directory).split(/\r?\n/).find((l) => l.trim() !== "")?.trim() ?? null;
4580
+ } catch {
4581
+ return null;
4582
+ }
4583
+ }
4573
4584
  function commitOf(directory, ref) {
4574
4585
  try {
4575
4586
  return git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], directory).trim() || null;
@@ -10344,6 +10355,101 @@ function removeSpecFile(specPath, specsRoot) {
10344
10355
  pruneEmptyDirs(path10.dirname(specPath), path10.resolve(specsRoot));
10345
10356
  return true;
10346
10357
  }
10358
+ var LOCK_FILE = ".spec-write.lock";
10359
+ var LOCK_STALE_MS = 6e4;
10360
+ var LOCK_WAIT_MS = 3e4;
10361
+ var lockHolds = /* @__PURE__ */ new Map();
10362
+ function sleepSync(ms) {
10363
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
10364
+ }
10365
+ function processAlive(pid) {
10366
+ if (!Number.isInteger(pid) || pid <= 0) return false;
10367
+ try {
10368
+ process.kill(pid, 0);
10369
+ return true;
10370
+ } catch (e) {
10371
+ return e.code === "EPERM";
10372
+ }
10373
+ }
10374
+ function lockHolder(file) {
10375
+ try {
10376
+ const [pid, at] = fs6.readFileSync(file, "utf8").split("\n");
10377
+ if (!/^\d+$/.test(pid ?? "") || !/^\d+$/.test(at ?? "")) return null;
10378
+ return { pid: Number(pid), at: Number(at) };
10379
+ } catch {
10380
+ return null;
10381
+ }
10382
+ }
10383
+ function lockTree(root) {
10384
+ const dir = path10.join(root, ".wai");
10385
+ if (!fs6.existsSync(dir)) return;
10386
+ const file = path10.join(dir, LOCK_FILE);
10387
+ const held = lockHolds.get(file) ?? 0;
10388
+ if (held > 0) {
10389
+ lockHolds.set(file, held + 1);
10390
+ return;
10391
+ }
10392
+ const deadline = Date.now() + LOCK_WAIT_MS;
10393
+ while (!tryCreateLock(file)) {
10394
+ if (breakStaleLock(file)) continue;
10395
+ if (Date.now() > deadline) {
10396
+ throw new Error(
10397
+ `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.`
10398
+ );
10399
+ }
10400
+ sleepSync(15 + Math.floor(Math.random() * 20));
10401
+ }
10402
+ lockHolds.set(file, 1);
10403
+ }
10404
+ function lockAge(file) {
10405
+ try {
10406
+ return Date.now() - fs6.statSync(file).mtimeMs;
10407
+ } catch {
10408
+ return 0;
10409
+ }
10410
+ }
10411
+ function tryCreateLock(file) {
10412
+ let fd;
10413
+ try {
10414
+ fd = fs6.openSync(file, "wx");
10415
+ } catch (e) {
10416
+ if (e.code === "EEXIST") return false;
10417
+ throw e;
10418
+ }
10419
+ try {
10420
+ fs6.writeSync(fd, `${process.pid}
10421
+ ${Date.now()}
10422
+ `);
10423
+ } finally {
10424
+ fs6.closeSync(fd);
10425
+ }
10426
+ return true;
10427
+ }
10428
+ function breakStaleLock(file) {
10429
+ const holder = lockHolder(file);
10430
+ const stale = holder === null ? lockAge(file) > LOCK_STALE_MS : holder.pid === process.pid || !processAlive(holder.pid) || Date.now() - holder.at > LOCK_STALE_MS;
10431
+ if (!stale) return false;
10432
+ try {
10433
+ fs6.unlinkSync(file);
10434
+ } catch {
10435
+ }
10436
+ return true;
10437
+ }
10438
+ function unlockTree(root) {
10439
+ const file = path10.join(root, ".wai", LOCK_FILE);
10440
+ const held = lockHolds.get(file) ?? 0;
10441
+ if (held <= 0) return;
10442
+ if (held > 1) {
10443
+ lockHolds.set(file, held - 1);
10444
+ return;
10445
+ }
10446
+ lockHolds.delete(file);
10447
+ if (lockHolder(file)?.pid !== process.pid) return;
10448
+ try {
10449
+ fs6.unlinkSync(file);
10450
+ } catch {
10451
+ }
10452
+ }
10347
10453
  function pruneEmptyDirs(dir, specsRoot) {
10348
10454
  let at = dir;
10349
10455
  while (at !== specsRoot && at.startsWith(specsRoot)) {
@@ -10627,10 +10733,10 @@ function stepGraph(method2) {
10627
10733
  const s = byNum.get(n);
10628
10734
  if (s.type !== "parallel" || s.endStep === void 0 || !s.branches?.length) continue;
10629
10735
  const entries = s.branches.map((b) => b.step).sort((a, b) => a - b);
10630
- const join31 = fallNext(s.endStep);
10736
+ const join32 = fallNext(s.endStep);
10631
10737
  for (let i = 0; i < entries.length; i++) {
10632
10738
  const armEnd = i + 1 < entries.length ? prevOf(entries[i + 1]) : s.endStep;
10633
- if (armEnd !== void 0 && armEnd >= entries[i]) armEndJoin.set(armEnd, join31);
10739
+ if (armEnd !== void 0 && armEnd >= entries[i]) armEndJoin.set(armEnd, join32);
10634
10740
  }
10635
10741
  }
10636
10742
  const successorsOf = (n) => {
@@ -11007,7 +11113,13 @@ function closureTypeOf(snapshot, identifier) {
11007
11113
  const ref = local.join("::");
11008
11114
  const matches = snapshot.types.filter((def) => matchTypeRef(ref, def.id));
11009
11115
  if (matches.length === 1) return matches[0];
11010
- return matches.find((def) => nameKey(def.id) === nameKey(ref));
11116
+ const exact = matches.find((def) => nameKey(def.id) === nameKey(ref));
11117
+ if (exact || matches.length > 0) return exact;
11118
+ if (local.length === 2 && !identifier.includes("::")) {
11119
+ const unqualified = snapshot.types.filter((def) => !def.id.includes("::") && !def.id.includes(".") && nameKey(def.id) === nameKey(local[1]));
11120
+ if (unqualified.length === 1) return unqualified[0];
11121
+ }
11122
+ return void 0;
11011
11123
  }
11012
11124
  function renamedRefs(expr, rename) {
11013
11125
  const args = expr.args.map((a) => renamedRefs(a, rename));
@@ -11108,6 +11220,9 @@ function carriedFactChanges(pinned, live) {
11108
11220
  const nowMethod = liveMethods.get(method2.name);
11109
11221
  if (!nowMethod) continue;
11110
11222
  differs(`effect of ${entry.id}.${method2.name}`, method2.effect, nowMethod.effect);
11223
+ if (method2.endpoint && factValue(method2.endpoint) !== factValue(nowMethod.endpoint)) {
11224
+ changes.push(`endpoint of ${entry.id}.${method2.name} (${shownEndpoint(method2.endpoint)} \u2192 ${shownEndpoint(nowMethod.endpoint)})`);
11225
+ }
11111
11226
  differs(`rename trace of ${entry.id}.${method2.name}`, method2.formerly, nowMethod.formerly);
11112
11227
  for (const r of paramRenames(method2, nowMethod)) {
11113
11228
  changes.push(`parameter "${r.from}" of ${entry.id}.${method2.name} (renamed to "${r.to}")`);
@@ -11152,7 +11267,8 @@ function memberDigest(snapshot, publicName, member) {
11152
11267
  return sha256(canonicalize({ method: methodShape(snapshot, method2), closure: closureShapes(snapshot, methodTypeRefs(method2)) }));
11153
11268
  }
11154
11269
  function shownSignature(method2) {
11155
- return method2.signature || `${method2.name}(${(method2.params ?? []).map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`).join(", ")}): ${method2.returns}`;
11270
+ const derived = `${method2.name}(${(method2.params ?? []).map((p) => `${p.name}${p.optional ? "?" : ""}: ${p.type}`).join(", ")}): ${method2.returns}`;
11271
+ return (method2.params?.length ? derived : method2.signature) || derived;
11156
11272
  }
11157
11273
  function entryFacts(before, after) {
11158
11274
  return [
@@ -11260,8 +11376,45 @@ function closureRenames(newer, older) {
11260
11376
  function renamesIn(ids, notes) {
11261
11377
  return ids.flatMap((id) => notes.get(id) ?? []);
11262
11378
  }
11379
+ function carriedThroughOwnRecords(snapshot, method2, exported) {
11380
+ const byId = new Map(snapshot.types.map((def) => [def.id, def]));
11381
+ const carried = /* @__PURE__ */ new Set();
11382
+ const expanded = /* @__PURE__ */ new Set();
11383
+ const queue = methodTypeRefs(method2).flatMap((expr) => extractTypeIdentifiers(canonicalTypeRef(snapshot, expr))).filter((id) => byId.has(id) && !exported.has(id));
11384
+ while (queue.length) {
11385
+ const id = queue.shift();
11386
+ if (expanded.has(id)) continue;
11387
+ expanded.add(id);
11388
+ for (const expr of typeDefExprs(byId.get(id))) {
11389
+ for (const child of extractTypeIdentifiers(canonicalTypeRef(snapshot, expr))) {
11390
+ if (!byId.has(child)) continue;
11391
+ carried.add(child);
11392
+ if (!exported.has(child)) queue.push(child);
11393
+ }
11394
+ }
11395
+ }
11396
+ return carried;
11397
+ }
11398
+ function tracedRenamesBehind(older, newer, was, now) {
11399
+ const written = new Set(methodTypeRefs(was).flatMap((expr) => expr.match(TYPE_IDENTIFIER) ?? []).map((token) => token.replace(/^::/, "")));
11400
+ const olderDefs = new Map(older.types.map((d) => [d.id, d]));
11401
+ const out = [];
11402
+ for (const id of closureIds(newer, now)) {
11403
+ const def = newer.types.find((d) => d.id === id);
11404
+ const from = (def.formerly ?? []).find((f) => written.has(f) && !written.has(def.id));
11405
+ if (from === void 0) continue;
11406
+ out.push(`type "${from}" renamed to "${def.id}"`);
11407
+ const olderDef = olderDefs.get(from);
11408
+ 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 })));
11409
+ for (const r of renamed2) out.push(`field "${def.id}.${r.from}" renamed to "${r.to}"`);
11410
+ }
11411
+ return out;
11412
+ }
11263
11413
  function signatureChange(older, newer, was, now, undoneNow = now) {
11264
- if (shownSignature(was) !== shownSignature(underName(undoneNow, was.name))) return `signature ${shownSignature(was)} \u2192 ${shownSignature(now)}`;
11414
+ if (shownSignature(was) !== shownSignature(underName(undoneNow, was.name))) {
11415
+ const behind = tracedRenamesBehind(older, newer, was, now);
11416
+ return `signature ${shownSignature(was)} \u2192 ${shownSignature(now)}${behind.length ? ` (${behind.join("; ")})` : ""}`;
11417
+ }
11265
11418
  const oldDefs = new Map(older.types.map((d) => [d.id, d]));
11266
11419
  const newDefs = new Map(newer.types.map((d) => [d.id, d]));
11267
11420
  const ids = [.../* @__PURE__ */ new Set([...closureIds(older, was), ...closureIds(newer, now)])].sort();
@@ -11271,7 +11424,8 @@ function signatureChange(older, newer, was, now, undoneNow = now) {
11271
11424
  return !a || !b || canonicalize(typeShape(older, a)) !== canonicalize(typeShape(newer, b));
11272
11425
  });
11273
11426
  const exported = new Set([...newer.exportedTypes ?? [], ...older.exportedTypes ?? []].map((t) => t.type));
11274
- const unlisted = moved.filter((id) => !exported.has(id));
11427
+ const carried = carriedThroughOwnRecords(newer, now, exported);
11428
+ const unlisted = moved.filter((id) => !exported.has(id) || carried.has(id));
11275
11429
  if (moved.length > 0 && unlisted.length === 0) return null;
11276
11430
  const named2 = (unlisted.length > 0 ? unlisted : moved).map((id) => `"${id}"`).join(", ");
11277
11431
  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";
@@ -11400,7 +11554,8 @@ function surfaceChanges(newer, older) {
11400
11554
  if (changed !== null) out.push({ kind: "changed", name: entry.id, member: method2.name, detail: changed });
11401
11555
  } else if (a !== b) {
11402
11556
  const exported = new Set((newer.exportedTypes ?? []).map((t) => t.type));
11403
- const embedded = renamesIn(closureIds(newer, method2).filter((id) => !exported.has(id)), notes);
11557
+ const carried = carriedThroughOwnRecords(newer, method2, exported);
11558
+ const embedded = renamesIn(closureIds(newer, method2).filter((id) => !exported.has(id) || carried.has(id)), notes);
11404
11559
  if (embedded.length > 0) {
11405
11560
  const key2 = embedded.join("; ");
11406
11561
  embeddedBy.set(key2, [...embeddedBy.get(key2) ?? [], method2.name]);
@@ -17398,7 +17553,8 @@ function schemaScope(ownDefs, externals, registry, alias) {
17398
17553
  return hit !== void 0 ? own2(hit) : void 0;
17399
17554
  },
17400
17555
  isObject(typeRef2) {
17401
- const expr = expressionOf(typeRef2);
17556
+ const read2 = expressionOf(typeRef2);
17557
+ const expr = read2?.form === "optional" ? read2.args[0] : read2;
17402
17558
  if (!expr || expr.form !== "named" && expr.form !== "applied") return false;
17403
17559
  const name = expr.name ?? "";
17404
17560
  const sep18 = name.indexOf("::");
@@ -17517,10 +17673,10 @@ function enumValuesFrom(schema) {
17517
17673
  return schema.enum.filter((v) => typeof v === "string" && v.length > 0).map((name) => ({ name, ...described2.has(name) ? { description: described2.get(name) } : {} }));
17518
17674
  }
17519
17675
  function openApiPath(basePath, endpointPath) {
17520
- const join31 = (a, b) => `${a.replace(/\/+$/, "")}/${b.replace(/^\/+/, "")}`;
17676
+ const join32 = (a, b) => `${a.replace(/\/+$/, "")}/${b.replace(/^\/+/, "")}`;
17521
17677
  const base = basePath && basePath.trim() && basePath.trim() !== "/" ? basePath.trim() : "";
17522
17678
  const rootedBase = base.startsWith("/") || !base ? base : `/${base}`;
17523
- const raw = base ? endpointPath.replace(/\/+$/, "") === "" ? rootedBase : join31(rootedBase, endpointPath) : endpointPath;
17679
+ const raw = base ? endpointPath.replace(/\/+$/, "") === "" ? rootedBase : join32(rootedBase, endpointPath) : endpointPath;
17524
17680
  const rooted = raw.startsWith("/") ? raw : `/${raw}`;
17525
17681
  return rooted.replace(/\/:([A-Za-z_][A-Za-z0-9_]*)/g, "/{$1}");
17526
17682
  }
@@ -17580,7 +17736,7 @@ function operationFor(method2, scope, entry) {
17580
17736
  const bodyParam = rest.length === 1 ? rest[0] : void 0;
17581
17737
  if (bodyVerbs.has(httpVerb) && bodyParam && scope.isObject(bodyParam.type)) {
17582
17738
  op.requestBody = {
17583
- required: !bodyParam.optional && !admitsNoValue(bodyParam.type),
17739
+ required: !bodyParam.optional,
17584
17740
  ...bodyParam.description ? { description: bodyParam.description } : {},
17585
17741
  content: { "application/json": { schema: schemaFor(bodyParam.type, scope) } }
17586
17742
  };
@@ -18657,7 +18813,10 @@ function canonicalReferences(snapshot) {
18657
18813
  ...entry,
18658
18814
  methods: entry.methods.map((m) => ({
18659
18815
  ...m,
18660
- signature: canon(m.signature),
18816
+ // Rebuilt from the structured params when it has them: a parameter's
18817
+ // NAME is never a type position, so one spelled like a type
18818
+ // (`order_id: order_id`) keeps its name.
18819
+ signature: m.params?.length ? `${m.name}(${m.params.map((p) => `${p.name}${p.optional ? "?" : ""}: ${canon(p.type)}`).join(", ")}): ${canon(m.returns)}` : canon(m.signature),
18661
18820
  returns: canon(m.returns),
18662
18821
  ...m.params ? { params: m.params.map((p) => ({ ...p, type: canon(p.type) })) } : {}
18663
18822
  }))
@@ -18893,14 +19052,20 @@ function foreignSurfaces() {
18893
19052
  }
18894
19053
  const root = getProjectRoot();
18895
19054
  for (const m of members) {
18896
- if (m.problem || m.storage !== "contained" && m.storage !== "path" || m.path === void 0 || out.has(m.alias)) continue;
19055
+ const location = m.path ?? m.source.path;
19056
+ if (m.problem || m.storage !== "contained" && m.storage !== "path" || location === void 0 || out.has(m.alias)) continue;
18897
19057
  try {
18898
- const surface = runWithProjectRoot(path15.resolve(root, m.path), () => projectOwnSurface("project"));
19058
+ const surface = runWithProjectRoot(path15.resolve(root, location), () => projectOwnSurface("project"));
18899
19059
  out.set(m.alias, surface);
18900
19060
  if (surface.projectId && !out.has(surface.projectId)) out.set(surface.projectId, surface);
18901
19061
  } catch {
18902
19062
  }
18903
19063
  }
19064
+ try {
19065
+ const own2 = boundProjectId();
19066
+ if (own2 !== void 0 && !out.has(own2)) out.set(own2, projectOwnSurface("project"));
19067
+ } catch {
19068
+ }
18904
19069
  return out;
18905
19070
  }
18906
19071
  function withForeignTypes(snapshot, foreign) {
@@ -19148,6 +19313,11 @@ function narrowerExports(external, missing, read2) {
19148
19313
  }
19149
19314
  return [...out].map(([name, audience]) => ({ name, audience }));
19150
19315
  }
19316
+ function withMoved(detail, moved) {
19317
+ if (moved.length === 0) return detail;
19318
+ const said = `moved since the last pin: ${moved.join(", ")}`;
19319
+ return { detail: detail.detail ? `${said}; ${detail.detail}` : said };
19320
+ }
19151
19321
  function pinBinding(binding, lock) {
19152
19322
  const { external } = binding;
19153
19323
  const unexported = binding.usage?.unexported ?? [];
@@ -19164,6 +19334,7 @@ function pinBinding(binding, lock) {
19164
19334
  if (pinned) snapshot = withRetired(snapshot, pinned);
19165
19335
  const snapshotChanged = !pinned || contentDigest(pinned) !== digest3 || pinnedContentKey(pinned) !== pinnedContentKey(snapshot);
19166
19336
  if (snapshotChanged) externalsRepository.saveSnapshot(external.alias, snapshot);
19337
+ const moved = pinned && snapshotChanged ? carriedFactChanges(pinned, snapshot) : [];
19167
19338
  const { used, missing, uncarried } = usedDigests(binding.usage, snapshot, external.alias);
19168
19339
  const narrower = missing.length > 0 && external.audience !== "project" ? narrowerExports(external, missing, snapshot) : [];
19169
19340
  const entry = {
@@ -19185,7 +19356,7 @@ function pinBinding(binding, lock) {
19185
19356
  digest: digest3,
19186
19357
  usedNames: Object.keys(used).length,
19187
19358
  unexported: [...unexported, ...uncarried],
19188
- ...pinDetail(missing, Object.keys(used).length, external.alias, narrower, external.audience)
19359
+ ...withMoved(pinDetail(missing, Object.keys(used).length, external.alias, narrower, external.audience), moved)
19189
19360
  },
19190
19361
  changed: entryChanged
19191
19362
  };
@@ -19368,6 +19539,20 @@ function changedDetail(pinned, live, publicName, member, digest3) {
19368
19539
  if (renames.length === 0) return {};
19369
19540
  return { detail: `${renames.join("; ")}${renameOnly ? " (its shape is otherwise unchanged) \u2014 follow the rename" : " (and its shape changed beyond that)"}` };
19370
19541
  }
19542
+ function routeOf(endpoint) {
19543
+ if (!endpoint) return "none";
19544
+ if (endpoint.transport === "HTTP") return `${endpoint.method} ${endpoint.path}${endpoint.status !== void 0 ? ` (answers ${endpoint.status})` : ""}`;
19545
+ const { transport, ...address } = endpoint;
19546
+ return `${String(transport)} ${Object.entries(address).filter(([, v]) => v !== void 0).map(([k, v]) => `${k}=${String(v)}`).join(" ")}`;
19547
+ }
19548
+ function endpointMove(pinned, live, publicName, member) {
19549
+ if (!pinned || member === "type" || member.startsWith("capability:")) return null;
19550
+ const was = pinned.interfaces.find((e) => e.id === publicName)?.methods.find((m) => m.name === member)?.endpoint;
19551
+ if (!was) return null;
19552
+ const now = live.interfaces.find((e) => e.id === publicName)?.methods.find((m) => m.name === member)?.endpoint;
19553
+ if (canonicalize(was) === canonicalize(now ?? null)) return null;
19554
+ 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` };
19555
+ }
19371
19556
  function compareUses(entry, usage, live, pinned = null) {
19372
19557
  const uses = [];
19373
19558
  const locked = entry?.used ?? {};
@@ -19377,11 +19562,21 @@ function compareUses(entry, usage, live, pinned = null) {
19377
19562
  uses.push(liveHas(publicName) ? { publicName, state: "unchanged" } : goneUse(live, publicName, void 0, void 0));
19378
19563
  continue;
19379
19564
  }
19380
- for (const [member, digest3] of Object.entries(members)) {
19565
+ for (const [member, locked2] of Object.entries(members)) {
19566
+ const digest3 = (pinned ? memberDigest(pinned, publicName, member) : null) ?? locked2;
19381
19567
  const now = memberDigest(live, publicName, member);
19382
- if (now === null) uses.push(goneUse(live, publicName, member, digest3, pinned));
19383
- else if (now === digest3) uses.push({ publicName, member, state: "unchanged" });
19384
- else uses.push({ publicName, member, state: "changed", ...changedDetail(pinned, live, publicName, member, digest3) });
19568
+ if (now === null) {
19569
+ uses.push(goneUse(live, publicName, member, digest3, pinned));
19570
+ continue;
19571
+ }
19572
+ const route2 = endpointMove(pinned, live, publicName, member);
19573
+ if (now === digest3) {
19574
+ uses.push(route2 === null ? { publicName, member, state: "unchanged" } : { publicName, member, state: route2.removed ? "changed" : "unchanged", detail: route2.detail });
19575
+ continue;
19576
+ }
19577
+ const changed = changedDetail(pinned, live, publicName, member, digest3);
19578
+ const detail = [changed.detail, route2?.detail].filter((d) => !!d).join("; ");
19579
+ uses.push({ publicName, member, state: "changed", ...detail ? { detail } : {} });
19385
19580
  }
19386
19581
  }
19387
19582
  const unlocked = (publicName, member) => {
@@ -19458,7 +19653,7 @@ function externalStatus(binding, lock) {
19458
19653
  const snapshot = entry !== void 0 ? externalsRepository.readSnapshot(external.alias) : null;
19459
19654
  const uses = compareUses(entry, binding.usage, live, snapshot);
19460
19655
  const staleFacts = snapshot ? carriedFactChanges(snapshot, live) : [];
19461
- const drifted = entry !== void 0 ? contentDigest(live) !== entry.digest || staleFacts.length > 0 : void 0;
19656
+ const drifted = entry !== void 0 ? contentDigest(live) !== (snapshot ? contentDigest(snapshot) : entry.digest) || staleFacts.length > 0 : void 0;
19462
19657
  return {
19463
19658
  ...base,
19464
19659
  reachable: true,
@@ -19468,6 +19663,48 @@ function externalStatus(binding, lock) {
19468
19663
  uses
19469
19664
  };
19470
19665
  }
19666
+ function projectedAt(directory) {
19667
+ try {
19668
+ return runWithProjectRoot(directory, () => projectOwnSurface("project"));
19669
+ } catch {
19670
+ return null;
19671
+ }
19672
+ }
19673
+ function memberApprovedSurfaces() {
19674
+ const out = {};
19675
+ for (const member of memberRevisions()) {
19676
+ if (!member.revision) continue;
19677
+ const approved = projectedAt(member.revision.directory);
19678
+ if (approved) out[member.alias] = approved;
19679
+ }
19680
+ return out;
19681
+ }
19682
+ function memberStatuses() {
19683
+ const out = [];
19684
+ for (const member of memberRevisions()) {
19685
+ if (!member.revision) continue;
19686
+ const live = projectedAt(member.root);
19687
+ const approved = projectedAt(member.revision.directory);
19688
+ if (!live || !approved) continue;
19689
+ const { used } = usedDigests(member.usage, approved, member.alias);
19690
+ const entry = { project: member.project, snapshot: "", digest: contentDigest(approved), used };
19691
+ const uses = compareUses(entry, void 0, live, approved).filter((u) => u.state !== "unlocked" && u.state !== "unavailable");
19692
+ const staleFacts = carriedFactChanges(approved, live);
19693
+ out.push({
19694
+ alias: member.alias,
19695
+ project: member.project,
19696
+ sourceKind: "family",
19697
+ pinned: false,
19698
+ reachable: true,
19699
+ stale: uses.some((u) => u.state === "changed" || u.state === "removed" || u.state === "renamed"),
19700
+ drifted: contentDigest(live) !== contentDigest(approved) || staleFacts.length > 0,
19701
+ ...staleFacts.length ? { staleFacts } : {},
19702
+ uses,
19703
+ detail: `a live member, compared with ${member.revision.label}`
19704
+ });
19705
+ }
19706
+ return out;
19707
+ }
19471
19708
  function hostedOnly(external) {
19472
19709
  return external.hosted !== void 0 && external.sourceKind === "unresolved" && getHostedLookup() === null;
19473
19710
  }
@@ -19715,11 +19952,11 @@ function declare(request) {
19715
19952
  } catch (e) {
19716
19953
  bindProblem = e instanceof Error ? e.message : String(e);
19717
19954
  }
19718
- const answered = binding ? readProducer(binding) : { why: bindProblem };
19719
- const producer = "read" in answered ? answered.read : null;
19955
+ const answered2 = binding ? readProducer(binding) : { why: bindProblem };
19956
+ const producer = "read" in answered2 ? answered2.read : null;
19720
19957
  const contradicted = contradiction(request, binding, producer);
19721
19958
  if (contradicted) return refused(alias, contradicted);
19722
- const why2 = "why" in answered ? answered.why : "unknown";
19959
+ const why2 = "why" in answered2 ? answered2.why : "unknown";
19723
19960
  if (request.dryRun) {
19724
19961
  return {
19725
19962
  alias,
@@ -19793,10 +20030,10 @@ function updateUse(request) {
19793
20030
  const named2 = appended.filter((u) => u !== "*");
19794
20031
  if (named2.length > 0) {
19795
20032
  const binding = resolveDeclared(true).find((b) => b.external.alias === alias);
19796
- const answered = binding ? readProducer(binding) : null;
19797
- if (answered && "read" in answered) {
19798
- const missing = named2.filter((u) => !answered.read.exported.includes(u));
19799
- if (missing.length) return useRefused(alias, current2, missing.map((u) => unexportedReason(u, answered.read)).join("; "));
20033
+ const answered2 = binding ? readProducer(binding) : null;
20034
+ if (answered2 && "read" in answered2) {
20035
+ const missing = named2.filter((u) => !answered2.read.exported.includes(u));
20036
+ if (missing.length) return useRefused(alias, current2, missing.map((u) => unexportedReason(u, answered2.read)).join("; "));
19800
20037
  }
19801
20038
  }
19802
20039
  const same = next.length === current2.length && next.every((u, i) => u === current2[i]);
@@ -19849,6 +20086,12 @@ function listExternals2() {
19849
20086
  function listPinnedExternals2() {
19850
20087
  return listPinnedExternals();
19851
20088
  }
20089
+ function memberApprovedSurfaces2() {
20090
+ return memberApprovedSurfaces();
20091
+ }
20092
+ function memberStatuses2() {
20093
+ return memberStatuses();
20094
+ }
19852
20095
  function unrecordedUses2() {
19853
20096
  return unrecordedUses();
19854
20097
  }
@@ -21191,12 +21434,11 @@ function adviseOn(status, family, config) {
21191
21434
  `${who} moved in its live producer since it was pinned: ${broken.map(movedUse).join("; ")}. Used by ${users.length ? users.map((s) => `"${s}"`).join(", ") : "this project's references"}. ` + (bindings.length ? `Binding module(s) declared there: ${bindings.map((b) => `"${b}"`).join(", ")} \u2014 compared with the pin, not the live producer, so ${bindings.length === 1 ? "it draws" : "they draw"} BINDING_DRIFT once the re-pin lands; follow the move there too. ` : "") + `Adapt the uses${renamed2 ? " (follow the rename)" : ""}, then re-pin with ${repin(status.alias)}. Advisory: the pin still gates.`
21192
21435
  );
21193
21436
  }
21194
- case "drifted":
21195
- return advisory(
21196
- config,
21197
- "EXTERNAL_DRIFTED",
21198
- `The live producer of ${who} moved since it was pinned, but nothing this project uses changed.${staleTail(status)} Re-pin when convenient (${repin(status.alias)}).`
21199
- );
21437
+ case "drifted": {
21438
+ const routes = status.uses.filter((u) => u.state === "unchanged" && u.detail?.startsWith("endpoint moved: "));
21439
+ 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.`;
21440
+ return advisory(config, "EXTERNAL_DRIFTED", `${lead}${staleTail(status)} Re-pin when convenient (${repin(status.alias)}).`);
21441
+ }
21200
21442
  case "unavailable": {
21201
21443
  const beyond = status.pinnedDigest !== void 0 && status.reachable && !status.outOfReach ? status.uses.filter((u) => u.state === "unlocked") : [];
21202
21444
  if (beyond.length > 0 && status.uses.every((u) => u.state !== "unavailable")) {
@@ -21643,7 +21885,7 @@ function failureAt(ref, failures) {
21643
21885
  const last = ref.resolved.split("::").pop() ?? "";
21644
21886
  return failures.find((i) => i.resolution.callSite === site && (i.resolution.canonicalTarget === ref.authored || (i.resolution.canonicalTarget ?? "").endsWith(`::${last}`)));
21645
21887
  }
21646
- function containedStatus(child, refs, failures) {
21888
+ function containedStatus(child, refs, failures, approved) {
21647
21889
  const base = { alias: child.mountAlias, project: child.id ?? child.namespace, sourceKind: "family", pinned: false };
21648
21890
  const unreachable = outsideReach(child);
21649
21891
  if (unreachable) {
@@ -21653,21 +21895,42 @@ function containedStatus(child, refs, failures) {
21653
21895
  const uses = [];
21654
21896
  for (const ref of refs) {
21655
21897
  const failed = failureAt(ref, failures);
21656
- const use = failed ? { publicName: referenceUse(ref), state: "removed", code: failed.code, detail: failed.resolution.reason } : { publicName: referenceUse(ref), state: "unchanged" };
21898
+ if (!failed) {
21899
+ const compared = (approved?.uses ?? []).filter((u) => u.publicName === referenceUse(ref));
21900
+ for (const use2 of compared.length ? compared : [{ publicName: referenceUse(ref), state: "unchanged" }]) {
21901
+ if (!uses.some((u) => u.publicName === use2.publicName && u.member === use2.member && u.state === use2.state)) uses.push(use2);
21902
+ }
21903
+ continue;
21904
+ }
21905
+ const use = { publicName: referenceUse(ref), state: "removed", code: failed.code, detail: failed.resolution.reason };
21657
21906
  if (!uses.some((u) => u.publicName === use.publicName && u.state === use.state)) uses.push(use);
21658
21907
  }
21659
- return { ...base, reachable: true, stale: uses.some((u) => u.state === "removed"), uses, detail: "a contained member, read live \u2014 nothing is pinned" };
21908
+ return {
21909
+ ...base,
21910
+ reachable: true,
21911
+ stale: uses.some((u) => u.state === "removed" || u.state === "changed" || u.state === "renamed"),
21912
+ ...approved?.drifted ? { drifted: true } : {},
21913
+ ...approved?.staleFacts?.length ? { staleFacts: approved.staleFacts } : {},
21914
+ uses,
21915
+ detail: approved ? `a live member, nothing pinned \u2014 ${approved.detail ?? "compared with its approval"}` : "a contained member, read live \u2014 nothing is pinned"
21916
+ };
21660
21917
  }
21661
- function containedRelations(answered) {
21918
+ function containedRelations(answered2) {
21662
21919
  const own2 = graph();
21663
- const consumed = own2.nodes.filter((n) => n.parent === "" && n.mountAlias !== void 0 && !answered.some((s) => s.alias === n.mountAlias)).map((child) => ({
21920
+ const consumed = own2.nodes.filter((n) => n.parent === "" && n.mountAlias !== void 0 && !answered2.some((s) => s.alias === n.mountAlias)).map((child) => ({
21664
21921
  child,
21665
21922
  refs: own2.authoredReferences.filter((r) => (own2.owners.get(r.specId) ?? "") === "" && r.producer === child.namespace && r.binding !== "local")
21666
21923
  })).filter((c) => c.refs.length > 0);
21667
21924
  if (consumed.length === 0) return [];
21668
21925
  const config = configOrNull();
21669
21926
  const failures = validateProject({ rules: config?.rules, projectType: config?.projectType }).issues.filter((i) => i.resolution !== void 0 && i.resolution.outcome !== "resolved");
21670
- return consumed.map(({ child, refs }) => containedStatus(child, refs, failures));
21927
+ let approved = [];
21928
+ try {
21929
+ approved = memberStatuses2();
21930
+ } catch {
21931
+ approved = [];
21932
+ }
21933
+ return consumed.map(({ child, refs }) => containedStatus(child, refs, failures, approved.find((s) => s.alias === child.mountAlias)));
21671
21934
  }
21672
21935
  function relationsAt(key2, dir, reach2) {
21673
21936
  const bound2 = (fn) => reach2 === null ? runWithProjectRoot(dir, fn) : runWithProjectBinding(dir, reach2, fn);
@@ -25877,12 +26140,13 @@ function placeholderProblems(path52, bindable, params) {
25877
26140
  var portalsRule = {
25878
26141
  name: "portal-endpoints",
25879
26142
  judges: "design",
25880
- 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.",
26143
+ 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.",
25881
26144
  codes: [
25882
26145
  { code: "MISSING_ENDPOINT", defaultSeverity: "error", summary: "Portal method without a wire endpoint binding, on a transport that requires one" },
25883
26146
  { code: "ENDPOINT_TRANSPORT_MISMATCH", defaultSeverity: "error", summary: "Endpoint transport does not match the Portal's transport" },
25884
26147
  { 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" },
25885
- { 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" }
26148
+ { 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" },
26149
+ { 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)" }
25886
26150
  ],
25887
26151
  check(ctx) {
25888
26152
  for (const comp of ctx.components) {
@@ -25926,6 +26190,7 @@ var portalsRule = {
25926
26190
  }
25927
26191
  }
25928
26192
  }
26193
+ statusMismatches(ctx);
25929
26194
  duplicateRoutes(ctx);
25930
26195
  }
25931
26196
  };
@@ -25971,6 +26236,47 @@ function duplicateRoutes(ctx) {
25971
26236
  }
25972
26237
  }
25973
26238
  }
26239
+ var STANDARD_STATUSES = /* @__PURE__ */ new Set([200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308]);
26240
+ var REDIRECTS = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
26241
+ function answered(returns) {
26242
+ if (!returns) return null;
26243
+ const expr = parseTypeExpression(returns.trim(), "returns").expression;
26244
+ const awaited = expr?.form === "async" ? expr.args[0] : expr;
26245
+ return (awaited?.form === "result" ? awaited.args[0] : awaited) ?? null;
26246
+ }
26247
+ function isLocation(ctx, expr) {
26248
+ const inner2 = expr.form === "optional" ? expr.args[0] : expr;
26249
+ if (inner2.form === "primitive") return inner2.name === "string";
26250
+ if (inner2.form !== "named" || !inner2.name) return false;
26251
+ const local = inner2.name.slice(inner2.name.lastIndexOf("::") + (inner2.name.includes("::") ? 2 : 0)).split(".").pop() ?? inner2.name;
26252
+ const def = ctx.types.find((t) => t.id === inner2.name || t.id === local || t.id.endsWith(`::${local}`));
26253
+ return def?.holds?.trim() === "string";
26254
+ }
26255
+ function statusMismatches(ctx) {
26256
+ for (const comp of ctx.components) {
26257
+ if (comp.componentType !== "Portal" || comp.transport !== "HTTP") continue;
26258
+ const isDraftCtx = ctx.isComponentDraft(comp.id);
26259
+ for (const intf of ctx.interfaces.filter((i) => i.component === comp.id)) {
26260
+ const isIntfDraft = intf.status === "draft" || intf.status === "design";
26261
+ for (const m of intf.methods) {
26262
+ const endpoint = m.endpoint;
26263
+ if (endpoint?.transport !== "HTTP" || endpoint.status === void 0) continue;
26264
+ const status = endpoint.status;
26265
+ const answer = answered(m.returns);
26266
+ const value = answer !== null && !(answer.form === "primitive" && answer.name === "void");
26267
+ const problem = !STANDARD_STATUSES.has(status) ? `${status} is no standard success or redirect code (200-208, 226, 300-308), so no client library names it` : (status === 204 || status === 304) && value ? `${status} carries no body, yet the method answers "${m.returns}" \u2014 the response would drop it` : REDIRECTS.has(status) && value && answer !== null && !isLocation(ctx, answer) ? `${status} 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;
26268
+ if (problem === null) continue;
26269
+ ctx.addIssue(
26270
+ "warning",
26271
+ "ENDPOINT_STATUS_MISMATCH",
26272
+ `Method "${m.name}" on interface "${intf.id}" binds ${endpoint.method ?? "HTTP"} ${endpoint.path ?? ""} with status ${status}: ${problem}. State the status the method's answer allows, or change what it answers.`,
26273
+ intf.id,
26274
+ isDraftCtx || isIntfDraft
26275
+ );
26276
+ }
26277
+ }
26278
+ }
26279
+ }
25974
26280
 
25975
26281
  // src/core/rules/doctrine/non-portal-endpoints.ts
25976
26282
  var nonPortalEndpointsRule = {
@@ -28696,10 +29002,11 @@ function bodyReachable(code, file, symbol) {
28696
29002
  var methodRealizationRule = {
28697
29003
  name: "method-realization",
28698
29004
  judges: "code",
28699
- 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.",
29005
+ 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.",
28700
29006
  codes: [
28701
29007
  { 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" },
28702
- { 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 }
29008
+ { 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 },
29009
+ { 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" }
28703
29010
  ],
28704
29011
  check(ctx) {
28705
29012
  const code = ctx.codeIndex();
@@ -28711,6 +29018,19 @@ var methodRealizationRule = {
28711
29018
  if (ctx.isInChainedSubproject(component.subsystem)) continue;
28712
29019
  const draft = ctx.isImplementationDraft(impl);
28713
29020
  const specTier = impl.conformance ?? defaultConformanceTier(component);
29021
+ const offMethods = contract.methods.filter((method2) => (impl.methods.find((m) => m.name === method2.name)?.conformance ?? specTier) === "off").map((method2) => method2.name);
29022
+ if (specTier === "off" || offMethods.length > 0) {
29023
+ 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(", ")}`;
29024
+ ctx.addIssue(
29025
+ "notice",
29026
+ "REALIZATION_UNCHECKED",
29027
+ `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.`,
29028
+ impl.id,
29029
+ draft,
29030
+ void 0,
29031
+ { at: "conformance" }
29032
+ );
29033
+ }
28714
29034
  for (const method2 of contract.methods) {
28715
29035
  const methodImpl = impl.methods.find((m) => m.name === method2.name);
28716
29036
  const tier = methodImpl?.conformance ?? specTier;
@@ -30642,7 +30962,7 @@ function bare(name) {
30642
30962
  }
30643
30963
  var HANDLE_NAMES = /* @__PURE__ */ new Set(["req", "request", "res", "response", "reply", "url", "ctx", "context", "next", "event", "socket"]);
30644
30964
  var TRANSPORT_HANDLES = /* @__PURE__ */ new Set(["req", "res", "ctx", "request", "response", "next", "reply"]);
30645
- var REQUEST_HANDLES = /* @__PURE__ */ new Set(["req", "request", "ctx", "context", "event"]);
30965
+ var REQUEST_HANDLES = /* @__PURE__ */ new Set(["req", "request", "ctx", "context", "event", "url"]);
30646
30966
  var PRIMITIVE_KINDS = /* @__PURE__ */ new Set(["string", "number", "boolean"]);
30647
30967
  var aKind = (kind) => kind === "object" ? "an object" : `a ${kind}`;
30648
30968
  var fieldKey = (name) => name.toLowerCase().replace(/[_-]/g, "");
@@ -30691,7 +31011,7 @@ function pairUp(declared, realized, score) {
30691
31011
  }
30692
31012
  return align(best);
30693
31013
  }
30694
- function judge2(declared, realized, injected, typeAgrees, expected) {
31014
+ function judge2(declared, realized, injected, typeAgrees, expected, typescript) {
30695
31015
  const declaredNames = new Set(declared.map((param) => bare(param.name)));
30696
31016
  const isInjected = (param) => param.name !== void 0 && injected.has(bare(param.name));
30697
31017
  let lead = 0;
@@ -30700,6 +31020,25 @@ function judge2(declared, realized, injected, typeAgrees, expected) {
30700
31020
  while (end > lead && isInjected(realized[end - 1]) && !declaredNames.has(bare(realized[end - 1].name))) end--;
30701
31021
  let taken = realized.map((param, at) => ({ param, at })).slice(lead, end);
30702
31022
  taken = taken.filter(({ param }) => !(param.name !== void 0 && param.name.startsWith("_") && param.unused === true && !declaredNames.has(bare(param.name))));
31023
+ const writtenAt = new Map(taken.map(({ param, at }) => [unitOf2(param, at), at]));
31024
+ let bundle;
31025
+ for (const entry of taken) {
31026
+ const fields = entry.param.fields;
31027
+ if (entry.param.kind !== "object" || !fields || fields.length === 0) continue;
31028
+ if (declared.some((d) => typeAgrees(d, entry.param) || structural(expected(d)?.fields, entry.param) === "all")) continue;
31029
+ const keys = new Set(fields.map(fieldKey));
31030
+ const takenElsewhere = new Set(taken.filter((other) => other !== entry && other.param.name !== void 0).map((other) => fieldKey(bare(other.param.name))));
31031
+ const covers = new Set(declared.flatMap((d, index) => {
31032
+ const key2 = fieldKey(bare(d.name));
31033
+ return keys.has(key2) && !takenElsewhere.has(key2) ? [index] : [];
31034
+ }));
31035
+ if (covers.size >= 2) {
31036
+ bundle = { entry, covers };
31037
+ break;
31038
+ }
31039
+ }
31040
+ if (bundle) taken = taken.filter((entry) => entry !== bundle?.entry);
31041
+ const pairable = declared.flatMap((param, index) => bundle?.covers.has(index) ? [] : [{ param, index }]);
30703
31042
  const score = (d, r) => {
30704
31043
  let total = 0;
30705
31044
  if (r.name !== void 0 && bare(r.name) === bare(d.name)) total += 4;
@@ -30711,7 +31050,7 @@ function judge2(declared, realized, injected, typeAgrees, expected) {
30711
31050
  else if (shape === "none") total -= 2;
30712
31051
  return total;
30713
31052
  };
30714
- const pairs = pairUp(declared, taken.map((entry) => entry.param), score);
31053
+ const pairs = pairUp(pairable.map((entry) => entry.param), taken.map((entry) => entry.param), score).map(([d, r]) => [pairable[d].index, r]);
30715
31054
  const wiringParams = [...realized.slice(0, lead), ...realized.slice(end)];
30716
31055
  const found = {
30717
31056
  unrealized: /* @__PURE__ */ new Map(),
@@ -30721,38 +31060,73 @@ function judge2(declared, realized, injected, typeAgrees, expected) {
30721
31060
  transport: /* @__PURE__ */ new Map(),
30722
31061
  wiring: realized.slice(0, lead).map((param) => param.name),
30723
31062
  trailing: realized.slice(end).map((param) => param.name),
30724
- carried: /* @__PURE__ */ new Map()
31063
+ carried: /* @__PURE__ */ new Map(),
31064
+ mismatched: /* @__PURE__ */ new Map()
30725
31065
  };
31066
+ if (bundle) {
31067
+ const { param: object, at } = bundle.entry;
31068
+ const bundled = declared.filter((_, index) => bundle?.covers.has(index)).map((param) => param.name);
31069
+ const label7 = object.name ? `"${object.name}"` : `parameter #${at + 1}`;
31070
+ for (const name of bundled) {
31071
+ found.unrealized.set(name, { unit: name, told: `"${name}" (the code takes it inside the object ${label7}, where the contract declares it as a parameter of its own)` });
31072
+ }
31073
+ const unit = unitOf2(object, at);
31074
+ found.undeclared.set(unit, { unit, told: `"${unit}" (an object bundling the contract's own parameters ${bundled.join(", ")})` });
31075
+ }
30726
31076
  const pairedDeclared = new Set(pairs.map(([d]) => d));
30727
31077
  const pairedTaken = new Set(pairs.map(([, r]) => r));
30728
31078
  const substituted = /* @__PURE__ */ new Set();
30729
31079
  for (const [d, r] of pairs) {
30730
31080
  const param = declared[d];
30731
- const { param: at, at: position2 } = taken[r];
31081
+ const { param: at, at: position } = taken[r];
30732
31082
  const sameName = at.name !== void 0 && bare(at.name) === bare(param.name);
30733
31083
  const want = expected(param);
30734
31084
  const agrees = typeAgrees(param, at);
30735
31085
  const shape = structural(want?.fields, at);
30736
31086
  const handleName = at.name !== void 0 && TRANSPORT_HANDLES.has(bare(at.name).toLowerCase()) && !TRANSPORT_HANDLES.has(bare(param.name).toLowerCase());
31087
+ const sameKind = !!want?.kinds && !!at.type && !!at.kind && want.kinds.has(at.kind) && (PRIMITIVE_KINDS.has(at.kind) || !!want.loose);
30737
31088
  let instead;
30738
31089
  if (!agrees) {
30739
31090
  if (!sameName && want?.kinds && at.type && at.kind && !want.kinds.has(at.kind)) instead = aKind(at.kind);
30740
31091
  else if (at.transport && want?.fields) instead = "a transport handle";
30741
31092
  else if (shape === "none") instead = `an object sharing none of its fields (${(want?.fields ?? []).join(", ")})`;
30742
- else if (handleName && (!at.kind || at.transport)) instead = "a transport handle";
31093
+ else if (handleName && shape !== "all" && !sameKind) instead = "a transport handle";
30743
31094
  }
30744
31095
  if (instead !== void 0) {
30745
31096
  substituted.add(r);
30746
31097
  const declares2 = want?.told ?? "an argument of its own";
30747
31098
  found.unrealized.set(param.name, {
30748
31099
  unit: param.name,
30749
- told: `"${param.name}" (the code takes ${at.name ? `"${at.name}"` : `parameter #${position2 + 1}`}, ${instead}, where the contract declares ${declares2})`
31100
+ told: `"${param.name}" (the code takes ${at.name ? `"${at.name}"` : `parameter #${position + 1}`}, ${instead}, where the contract declares ${declares2})`
30750
31101
  });
30751
- const unit = unitOf2(at, position2);
31102
+ const unit = unitOf2(at, position);
30752
31103
  found.undeclared.set(unit, { unit, told: `"${unit}" (${instead} in the place of the contract's "${param.name}", ${declares2})` });
30753
31104
  continue;
30754
31105
  }
30755
- const sameKind = !!want?.kinds && !!at.type && !!at.kind && want.kinds.has(at.kind) && (PRIMITIVE_KINDS.has(at.kind) || !!want.loose);
31106
+ if (!agrees && want?.fields) {
31107
+ const opaque = at.type !== void 0 && at.opaque === true || at.type === void 0 && typescript;
31108
+ let shapeTold;
31109
+ if (opaque) {
31110
+ 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(", ")})`;
31111
+ } else if (at.kind === "object" && at.fields && at.fields.length > 0) {
31112
+ const has = new Set(at.fields.map(fieldKey));
31113
+ const declaredKeys = new Set(want.fields.map(fieldKey));
31114
+ const missing = want.fields.filter((field) => !has.has(fieldKey(field)));
31115
+ const added = at.fields.filter((field) => !declaredKeys.has(fieldKey(field)));
31116
+ if (missing.length > 0 && missing.length < want.fields.length || missing.length === 0 && added.length > 0) {
31117
+ shapeTold = `an object ${[
31118
+ ...missing.length > 0 ? [`missing ${missing.map((field) => `"${field}"`).join(", ")}`] : [],
31119
+ ...added.length > 0 ? [`adding ${added.map((field) => `"${field}"`).join(", ")}`] : []
31120
+ ].join(" and ")}`;
31121
+ }
31122
+ }
31123
+ if (shapeTold !== void 0) {
31124
+ found.mismatched.set(param.name, {
31125
+ unit: param.name,
31126
+ told: `"${param.name}" (the code takes ${at.name ? `"${at.name}"` : `parameter #${position + 1}`} ${shapeTold}, where the contract declares ${want.told})`
31127
+ });
31128
+ }
31129
+ }
30756
31130
  if (at.name !== void 0 && !sameName && (agrees || sameKind || shape === "all")) {
30757
31131
  const former = (param.previousNames ?? []).some((name) => bare(name) === bare(at.name));
30758
31132
  found.renamed.set(`${param.name}\u2192${at.name}`, {
@@ -30768,9 +31142,12 @@ function judge2(declared, realized, injected, typeAgrees, expected) {
30768
31142
  }
30769
31143
  }
30770
31144
  const requestReads = new Set(wiringParams.filter((param) => param.name !== void 0 && REQUEST_HANDLES.has(bare(param.name).toLowerCase())).flatMap((param) => (param.reads ?? []).map(bare)));
31145
+ const left = declared.filter((param, d) => !pairedDeclared.has(d) && !bundle?.covers.has(d) && !requestReads.has(bare(param.name)));
31146
+ const records = left.filter((param) => (expected(param)?.fields?.length ?? 0) > 0);
31147
+ const bodyRecord = requestReads.has("body") && records.length === 1 ? records[0].name : void 0;
30771
31148
  declared.forEach((param, d) => {
30772
- if (pairedDeclared.has(d)) return;
30773
- if (requestReads.has(bare(param.name))) {
31149
+ if (pairedDeclared.has(d) || bundle?.covers.has(d)) return;
31150
+ if (requestReads.has(bare(param.name)) || param.name === bodyRecord) {
30774
31151
  found.carried.set(param.name, { unit: param.name, told: param.name });
30775
31152
  return;
30776
31153
  }
@@ -30787,8 +31164,7 @@ function judge2(declared, realized, injected, typeAgrees, expected) {
30787
31164
  if (pairedTaken.has(r) && !substituted.has(r)) return;
30788
31165
  if (HANDLE_NAMES.has(bare(param.name).toLowerCase())) found.transport.set(param.name, { unit: unitOf2(param, at), told: param.name });
30789
31166
  });
30790
- const position = new Map(taken.map(({ param, at }) => [unitOf2(param, at), at]));
30791
- found.undeclared = new Map([...found.undeclared].sort(([a], [b]) => (position.get(a) ?? 0) - (position.get(b) ?? 0)));
31167
+ found.undeclared = new Map([...found.undeclared].sort(([a], [b]) => (writtenAt.get(a) ?? 0) - (writtenAt.get(b) ?? 0)));
30792
31168
  return found;
30793
31169
  }
30794
31170
  function agreed(judgements, reading) {
@@ -30835,7 +31211,7 @@ function typeKind(type) {
30835
31211
  var paramConformanceRule = {
30836
31212
  name: "param-conformance",
30837
31213
  judges: "code",
30838
- description: "Code-to-contract for the SIGNATURE, the last of the three readings a spec-driven gate never made: a contract declares `params`, and nothing ever compared them to the parameters of the function that realizes the method. A contract could promise an argument the code does not take, take one the contract never mentions \u2014 including a secret \u2014 or name the same argument two different things, and the brief handed to an implementer would carry the contract's version. What a realization takes besides the contract's own parameters, at either end of its list, is wiring, declared on the implementation as `injectedParams` rather than inferred, because an inferred prefix cannot be told from a renamed first argument; a leading run and a trailing run of the names it declares are dropped, each name matching with or without its leading underscore, and a trailing one only where the contract declares no parameter of that name. A parameter whose name starts with an underscore means UNUSED, and it is set aside only where the analysis proves it is \u2014 named, with no identifier of that name in the body or in another parameter's default value and no read of `arguments`, as the request and URL a transport hands a handler that never reads them: a used `_secret` is judged like any parameter, and a name compared for a rename is read without its leading underscores. The rest are PAIRED in order, since a caller passes arguments in order: among the order-keeping pairings, the one where the most names agree, then the most declared types, then the most kinds of value, and on a tie the one aligned against the TAIL of the realization's list \u2014 so an inserted first argument is the one named undeclared, never the declared one it pushed along. The declared type is what tells a rename from a substitution. Types agree when the code's annotation, read through the dialect of the language the file was analyzed as (type_dialect.agrees), is the contract's canonical type \u2014 so `string[]` agrees with `list<string>`, TypeScript's `number` with int and float alike, and an annotation the dialect cannot read agrees with nothing; a named type agrees through its code-level name, an EXTERNAL one (`alias::name`) through the name its pinned snapshot gives it, else its public name's last segment in the code's type casing. Where a type checker read the file, each parameter the code annotates also carries the KIND of value it takes (string, number, boolean, list, object or function; a platform class the checker's fixed library leaves out \u2014 a URL, a Request, node:http's IncomingMessage \u2014 is an object), compared with the kind the contract's type is (a primitive's, a list, a map or set as an object, an enum as a string, a named scalar as what it holds, a record type as an object, a signature as a function; a date or a datetime as a string or an object, a duration as a number or a string, bytes as an object, a list or a string; anything unsettled compares with nothing). A paired parameter whose name differs and whose type agrees \u2014 or whose kind is the same primitive, or one of the kinds a date or bytes is written as \u2014 is a rename, said with the contract's former name when the code still uses it; one whose name differs and whose kind of value differs is a SUBSTITUTION, a different argument in the declared one's place, so the declared parameter is unrealized and the code's undeclared: `getRoute(params: Record<string, string>)` realizing `getRoute(id: string)` is never a silent pairing. Where the contract's type is a record, an object is compared STRUCTURALLY, by the property names the checker gives its type: one holding every field of the record is a rename, one sharing none of them a substitution whatever either side calls it, and a platform transport class (node:http's IncomingMessage, a fetch Request or Response, a URL, a socket) where a record is declared is a substitution too \u2014 so `planRoute(req: IncomingMessage)` and `planRoute(session: { user: string })` realizing `planRoute(request: plan_request)` are never silent pairings. Where the code settles no kind \u2014 unannotated JavaScript, a parameter typed any \u2014 a paired parameter named as a transport handle (req, res, ctx, request, response, next, reply) standing where the contract declares an argument of its own is a substitution: `cancelOrder(req, res)` realizing `cancelOrder(orderId, customerId)` is a handler's shape, not the contract's. Where neither side settles a type and no handle name speaks, nothing is said about the name. A parameter standing ahead of every kept pair under a name transports give their handles (req, request, res, response, reply, url, ctx, context, next, event, socket) is how a transport handle arrives, and the finding names the one green path for it: the handle in `injectedParams`, the contract's own parameters after it in order, the router unpacking the request into them \u2014 or, where the framework hands the function the handles alone, the contract's parameters read off the injected request by their own names: a contract parameter the code does not take but reads off an injected request handle (req, request, ctx, context, event) by its own name \u2014 `req.params.code`, `req.query['limit']`, a destructuring of such a read \u2014 is realized through that handle. An injected name none of the implementation's realizing functions read in this run takes is stale linkage (UNUSED_INJECTED_PARAM): it would wave through the next parameter of that name unread. A name with several bodies is judged on what they all agree on, and a method the named file only CALLS is left to `methodRealization`, which already reports that the body is not here; a method whose conformance dial is off (its own, else its implementation's) is not judged.",
31214
+ 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.",
30839
31215
  codes: [
30840
31216
  {
30841
31217
  code: "UNREALIZED_PARAM",
@@ -30859,8 +31235,14 @@ var paramConformanceRule = {
30859
31235
  },
30860
31236
  {
30861
31237
  code: "UNUSED_INJECTED_PARAM",
31238
+ defaultSeverity: "notice",
31239
+ 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",
31240
+ carryable: true
31241
+ },
31242
+ {
31243
+ code: "PARAM_TYPE_MISMATCH",
30862
31244
  defaultSeverity: "warning",
30863
- summary: "An implementation declares an injected parameter that none of its realizing functions takes \u2014 stale linkage that would wave through the next parameter of that name, wherever it appears",
31245
+ 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",
30864
31246
  carryable: true
30865
31247
  },
30866
31248
  {
@@ -30898,7 +31280,28 @@ var paramConformanceRule = {
30898
31280
  typeNamed.set(`${pin2.alias}::${exported.id}`, type);
30899
31281
  }
30900
31282
  }
30901
- const externalCodeName = (ref) => (ref.split("::").pop() ?? ref).split(/[_\-\s]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
31283
+ const family = ctx.projectFamily?.nodes ?? [];
31284
+ const root = family.find((node) => node.namespace === "");
31285
+ const memberTypes = /* @__PURE__ */ new Map();
31286
+ const lastOf = (id) => id.split(/::|\./).pop() ?? id;
31287
+ const memberType = (ref) => {
31288
+ const cut = ref.indexOf("::");
31289
+ if (cut <= 0) return void 0;
31290
+ if (memberTypes.has(ref)) return memberTypes.get(ref) ?? void 0;
31291
+ const segment = ref.slice(0, cut);
31292
+ const local = lastOf(ref.slice(cut + 2));
31293
+ const key2 = root?.aliases.get(segment) ?? family.find((node2) => node2.aliases.has(segment))?.aliases.get(segment) ?? segment;
31294
+ const node = family.find((n) => n.namespace !== "" && (n.namespace === key2 || n.mountAlias === segment || n.id === segment));
31295
+ 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;
31296
+ memberTypes.set(ref, found ?? null);
31297
+ return found;
31298
+ };
31299
+ const typeOf = (name) => typeNamed.get(name) ?? memberType(name);
31300
+ const externalCodeName = (ref) => {
31301
+ const member = memberType(ref);
31302
+ if (member) return member.symbol ?? member.name;
31303
+ return (ref.split("::").pop() ?? ref).split(/[_\-\s]+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
31304
+ };
30902
31305
  const typeAgreesIn = (dialect) => (declared, realized) => {
30903
31306
  if (!realized.type || !dialect) return false;
30904
31307
  const stated = parseTypePosition(declared.type, "param", !!declared.optional);
@@ -30922,9 +31325,9 @@ var paramConformanceRule = {
30922
31325
  };
30923
31326
  if (expression.name && loose[expression.name]) return { kinds: new Set(loose[expression.name]), loose: true, told: `a ${expression.name}` };
30924
31327
  }
30925
- const kind = expressionKind(expression, (name) => typeKind(typeNamed.get(name)));
31328
+ const kind = expressionKind(expression, (name) => typeKind(typeOf(name)));
30926
31329
  if (expression.form === "named" && expression.name) {
30927
- const type = typeNamed.get(expression.name);
31330
+ const type = typeOf(expression.name);
30928
31331
  const fields = (type?.fields ?? []).map((field) => field && typeof field === "object" ? field.name : void 0).filter((name) => typeof name === "string");
30929
31332
  if (fields.length > 0 && kind === "object") return { kinds: /* @__PURE__ */ new Set(["object"]), fields, told: `the record "${expression.name}"` };
30930
31333
  }
@@ -30951,7 +31354,8 @@ var paramConformanceRule = {
30951
31354
  if (declared.length === 0) continue;
30952
31355
  const injected = new Set((implementation.injectedParams ?? []).map(bare));
30953
31356
  const typeAgrees = typeAgreesIn(dialectOf(facts));
30954
- const judgements = candidates.map((realized) => judge2(declared, realized, injected, typeAgrees, expected));
31357
+ const typescript = facts.language === "typescript";
31358
+ const judgements = candidates.map((realized) => judge2(declared, realized, injected, typeAgrees, expected, typescript));
30955
31359
  const subject = candidates.length > 1 ? `every function called "${symbol}" in "${file}"` : `the function "${symbol}" in "${file}"`;
30956
31360
  const opening = subject.charAt(0).toUpperCase() + subject.slice(1);
30957
31361
  const transport = agreed(judgements, "transport").map((found) => found.told);
@@ -30982,6 +31386,18 @@ var paramConformanceRule = {
30982
31386
  { at: method2.name, covers: undeclared.map((found) => found.unit) }
30983
31387
  );
30984
31388
  }
31389
+ const mismatched = agreed(judgements, "mismatched");
31390
+ if (mismatched.length > 0) {
31391
+ ctx.addIssue(
31392
+ "warning",
31393
+ "PARAM_TYPE_MISMATCH",
31394
+ `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.`,
31395
+ implementation.id,
31396
+ draftContext4,
31397
+ void 0,
31398
+ { at: method2.name, covers: mismatched.map((found) => found.unit) }
31399
+ );
31400
+ }
30985
31401
  const renamed2 = agreed(judgements, "renamed");
30986
31402
  if (renamed2.length > 0) {
30987
31403
  ctx.addIssue(
@@ -31014,9 +31430,9 @@ var paramConformanceRule = {
31014
31430
  const stale = injected.filter((name) => !taken.has(bare(name)));
31015
31431
  if (stale.length === 0) continue;
31016
31432
  ctx.addIssue(
31017
- "warning",
31433
+ "notice",
31018
31434
  "UNUSED_INJECTED_PARAM",
31019
- `Implementation "${implementation.id}" declares ${stale.length} injected parameter(s) none of its realizing functions takes \u2014 ${stale.map((name) => `"${name}"`).join(", ")}. An injection is wiring the parameter check sets aside wherever it stands at either end of a signature, so one that no function takes waves through the next parameter of that name unread \u2014 a credential included. Drop it from injectedParams, or take it in the handler that is handed it.`,
31435
+ `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.`,
31020
31436
  implementation.id,
31021
31437
  ctx.isImplementationDraft(implementation),
31022
31438
  void 0,
@@ -31118,7 +31534,14 @@ function memberSurface(ctx, node, alias) {
31118
31534
  ...m,
31119
31535
  formerly: (m.previousNames ?? []).map(lastSegment4)
31120
31536
  }));
31121
- entries.push({ alias, entry: { id: e.publicName, name: e.publicName, component: e.component, methods } });
31537
+ const approved = ctx.memberApprovedSurfaces?.[alias]?.interfaces.find((x) => x.id === e.publicName);
31538
+ const kept = new Set(methods.flatMap((m) => [m.name, ...m.formerly ?? []]).map(nameKey));
31539
+ const retired = (approved?.methods ?? []).map((m) => m.name).filter((n) => !kept.has(nameKey(n)));
31540
+ entries.push({
31541
+ alias,
31542
+ entry: { id: e.publicName, name: e.publicName, component: e.component, methods, ...retired.length ? { retired } : {} },
31543
+ ...retired.length ? { approved: true } : {}
31544
+ });
31122
31545
  for (const m of methods) pending3.push(...methodTypeRefs(m));
31123
31546
  } else if (e.kind === "type" && e.typeDef) {
31124
31547
  const type = memberType(e.typeDef);
@@ -31213,7 +31636,7 @@ function methodDrift(where, params, entry, reportMissing, method2) {
31213
31636
  if (current2) return paramDrift(where, params, current2);
31214
31637
  const renamed2 = entry.entry.methods.find((m) => (m.formerly ?? []).some((f) => nameKey(f) === key2));
31215
31638
  if (renamed2) return [`"${method2 ?? where}" was renamed to "${renamed2.name}" in ${entry.alias}::${entry.entry.id} \u2014 follow the rename`];
31216
- 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`];
31639
+ 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`];
31217
31640
  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"})`] : [];
31218
31641
  }
31219
31642
  function formerType(name, types) {
@@ -31327,6 +31750,16 @@ function reachOf(ctx, segments) {
31327
31750
  const members = [];
31328
31751
  const unpinned2 = [];
31329
31752
  const externals = new Set((boundRoot(ctx)?.imports ?? []).filter((i) => i.section === "externals").map((i) => i.alias));
31753
+ const ownId = boundRoot(ctx)?.id;
31754
+ if (ownId !== void 0 && segments.has(ownId)) {
31755
+ segments = new Set(segments);
31756
+ for (const table of (ctx.exportTables ?? []).filter((x) => x.level === "project" && !(ctx.projectFamily?.nodes ?? []).some((n) => n.namespace !== "" && n.namespace === x.owner))) {
31757
+ for (const e of table.entries) {
31758
+ const target = e.typeDef ?? e.component ?? "";
31759
+ if (target.includes("::")) segments.add(target.slice(0, target.indexOf("::")));
31760
+ }
31761
+ }
31762
+ }
31330
31763
  for (const segment of segments) {
31331
31764
  if (pins.some((p) => p.alias === segment)) continue;
31332
31765
  const node = memberNode(ctx, segment);
@@ -31451,7 +31884,7 @@ function answers(route2, endpoint) {
31451
31884
  var routeCoverageRule = {
31452
31885
  name: "route-coverage",
31453
31886
  judges: "code",
31454
- description: "Code-to-contract for the ROUTES: does every route a router actually answers have a contract endpoint, and does every contract endpoint have a route that answers it? A portal's endpoints are what its contract promises; the router is what the code serves; nothing compared the two, so a route with no contract (including a write) could run for months without a single rule noticing. The Portal's own implementation names its router (`router`, code linkage): an entry its own files export, an entry or route table another module exports (`<module>#<name>`, a central routes.ts), or a module alone, every route-bearing export of which is read as one router; the routes are read out of that entry and the functions of its file it calls by name. A router several Portals name is read once against them all: a route is declared when any of them declares it, and an endpoint is unrouted per Portal. Two idioms are read: guards (a method comparison with comparisons on the path's split segments), where a prefix the router strips before splitting the path is folded in front; and a route table \u2014 a const the router names, one it returns or states in place unnamed, or the table the linkage names itself \u2014 (an array of objects pairing a method with a `/a/:b` template or an anchored regular expression, or an object keyed `VERB /path`, a key written as a template literal or a concatenation over constants settling like any other value), read only when every entry settles. A table is written under the ONE prefix its router strips off the path before matching it (`path.slice(BASE.length)`, `.replace(BASE, '')`), which is folded in front of every entry that does not already state it, so a stripped prefix that is not the Portal's basePath is read as the different URL it is, and the finding names both. Every compared value is a literal or one the code's constants, concatenations and template literals settle \u2014 through the type checker, any module's constant and any expression it types as one string literal \u2014 and never a guess. A route that states the whole path is read under the Portal's basePath, which the contract's endpoint paths are written beneath; a leading path segment a guard router never checks is completed from the first segments of the Portal's own HTTP endpoint paths, the segments whatever serves the Portal routes on. Which process serves which Portal is implementation, so no listener is consulted. A router that yields no route in either idiom is reported as unread, never passed: a check that cannot see a router must say so rather than stay quiet.",
31887
+ 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.",
31455
31888
  codes: [
31456
31889
  {
31457
31890
  code: "UNDECLARED_ROUTE",
@@ -31472,17 +31905,50 @@ var routeCoverageRule = {
31472
31905
  defaultSeverity: "warning",
31473
31906
  summary: "A Portal's router entry yields no route this analysis can read, so its routes were not checked against the contract at all",
31474
31907
  carryable: true
31908
+ },
31909
+ {
31910
+ code: "ROUTER_UNDECLARED",
31911
+ defaultSeverity: "notice",
31912
+ 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"
31475
31913
  }
31476
31914
  ],
31477
31915
  check(ctx) {
31478
31916
  const code = ctx.codeIndex();
31479
31917
  const realization = ctx.realizationIndex();
31480
31918
  const groups = /* @__PURE__ */ new Map();
31919
+ const httpEndpoints = (portal) => ctx.interfaceMethodsOf(portal.id).flatMap((method2) => {
31920
+ const endpoint = method2.endpoint;
31921
+ if (endpoint?.transport !== "HTTP") return [];
31922
+ return [{
31923
+ verb: endpoint.method.toUpperCase(),
31924
+ segments: endpoint.path.split("/").filter(Boolean).map(normalizeSegment),
31925
+ key: `${endpoint.method.toUpperCase()} ${endpoint.path}`
31926
+ }];
31927
+ });
31928
+ const exactAt = (file) => {
31929
+ const facts = code.factsAt(file);
31930
+ return !!facts && facts.status === "analyzed" && facts.analysisGrade === "exact";
31931
+ };
31481
31932
  for (const portal of ctx.components) {
31482
31933
  if (portal.componentType !== "Portal") continue;
31483
31934
  for (const impl of realization.implementationsOf(portal.id)) {
31484
31935
  const via = impl.router;
31485
- if (!via) continue;
31936
+ if (!via) {
31937
+ const endpoints2 = httpEndpoints(portal);
31938
+ const files2 = realization.filesOf(portal.id).map(pathKey).filter(exactAt);
31939
+ if (endpoints2.length === 0 || files2.length === 0) continue;
31940
+ const held = [...new Set(files2.flatMap((file) => Object.entries(code.factsAt(file)?.functionRoutes ?? {}).filter(([, routes]) => routes.length > 0).map(([name]) => `${name} (${file})`)))].sort();
31941
+ ctx.addIssue(
31942
+ "notice",
31943
+ "ROUTER_UNDECLARED",
31944
+ `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."),
31945
+ impl.id,
31946
+ ctx.isComponentDraft(portal.id) || ctx.isImplementationDraft(impl),
31947
+ void 0,
31948
+ { at: "router" }
31949
+ );
31950
+ continue;
31951
+ }
31486
31952
  const linkage = routerLinkage(via);
31487
31953
  const files = linkage.file ? [pathKey(linkage.file)] : realization.filesOf(portal.id).map(pathKey);
31488
31954
  const exact = files.filter((file) => {
@@ -31494,7 +31960,22 @@ var routeCoverageRule = {
31494
31960
  return !!facts.functionParams && Object.prototype.hasOwnProperty.call(facts.functionParams, name) || !!facts.functionRoutes && Object.prototype.hasOwnProperty.call(facts.functionRoutes, name);
31495
31961
  };
31496
31962
  const holders = linkage.name !== void 0 ? exact.filter((file) => holds(file, linkage.name)) : exact;
31497
- if (holders.length === 0) continue;
31963
+ if (holders.length === 0) {
31964
+ const name = linkage.name;
31965
+ const declaring = exact.filter((file) => code.declarationsAt(file).has(name));
31966
+ if (declaring.length > 0) {
31967
+ ctx.addIssue(
31968
+ "warning",
31969
+ "UNREADABLE_ROUTER",
31970
+ `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.`,
31971
+ impl.id,
31972
+ ctx.isComponentDraft(portal.id) || ctx.isImplementationDraft(impl),
31973
+ void 0,
31974
+ { at: via }
31975
+ );
31976
+ }
31977
+ continue;
31978
+ }
31498
31979
  const prefixes = /* @__PURE__ */ new Set();
31499
31980
  const read2 = holders.flatMap((file) => {
31500
31981
  const facts = code.factsAt(file);
@@ -31513,15 +31994,7 @@ var routeCoverageRule = {
31513
31994
  }
31514
31995
  return [...names].flatMap((name) => Object.prototype.hasOwnProperty.call(routes, name) ? routes[name] : []);
31515
31996
  });
31516
- const endpoints = ctx.interfaceMethodsOf(portal.id).flatMap((method2) => {
31517
- const endpoint = method2.endpoint;
31518
- if (endpoint?.transport !== "HTTP") return [];
31519
- return [{
31520
- verb: endpoint.method.toUpperCase(),
31521
- segments: endpoint.path.split("/").filter(Boolean).map(normalizeSegment),
31522
- key: `${endpoint.method.toUpperCase()} ${endpoint.path}`
31523
- }];
31524
- });
31997
+ const endpoints = httpEndpoints(portal);
31525
31998
  const heads = [...new Set(endpoints.map((endpoint) => endpoint.segments[0]).filter((head2) => !!head2 && head2 !== "*"))].sort();
31526
31999
  const router = {
31527
32000
  portal,
@@ -31535,7 +32008,7 @@ var routeCoverageRule = {
31535
32008
  draftContext: ctx.isComponentDraft(portal.id) || ctx.isImplementationDraft(impl),
31536
32009
  ...prefixes.size === 1 ? { prefix: [...prefixes][0] } : {}
31537
32010
  };
31538
- const key2 = linkage.file ? `${pathKey(linkage.file)}#${linkage.name ?? "*"}` : `${impl.id}#${via}`;
32011
+ const key2 = `${[...holders].sort().join("|")}#${linkage.name ?? "*"}`;
31539
32012
  groups.set(key2, [...groups.get(key2) ?? [], router]);
31540
32013
  }
31541
32014
  }
@@ -32191,15 +32664,31 @@ var DATA_LAYER = /* @__PURE__ */ new Set(["Adapter", "Store", "Registry", "Index
32191
32664
  var technologyBindingRule = {
32192
32665
  name: "technology-binding",
32193
32666
  judges: "design",
32194
- 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.",
32667
+ 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.",
32195
32668
  codes: [
32196
- { code: "TECH_ON_LOGIC_COMPONENT", defaultSeverity: "warning", summary: "Technology bound by a non-data-layer stereotype" }
32669
+ { 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" }
32197
32670
  ],
32198
32671
  check(ctx) {
32672
+ const componentOf = (contract) => {
32673
+ const intf = ctx.interfaceMap.get(contract);
32674
+ return intf ? ctx.componentMap.get(intf.component) : void 0;
32675
+ };
32676
+ const dataLayer = new Set(ctx.implementations.filter((impl) => DATA_LAYER.has(componentOf(impl.contract)?.componentType ?? "")).flatMap((impl) => (impl.technologies ?? []).map((t) => technologyName(t).toLowerCase())));
32199
32677
  for (const impl of ctx.implementations) {
32200
- const intf = ctx.interfaceMap.get(impl.contract);
32201
- const comp = intf ? ctx.componentMap.get(intf.component) : void 0;
32678
+ const comp = componentOf(impl.contract);
32202
32679
  if (!impl.technologies?.length || !comp || DATA_LAYER.has(comp.componentType)) continue;
32680
+ if (comp.componentType === "Portal") {
32681
+ const reached = impl.technologies.map(technologyName).filter((name) => dataLayer.has(name.toLowerCase()));
32682
+ if (reached.length === 0) continue;
32683
+ ctx.addIssue(
32684
+ "warning",
32685
+ "TECH_ON_LOGIC_COMPONENT",
32686
+ `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.`,
32687
+ impl.id,
32688
+ ctx.isImplementationDraft(impl)
32689
+ );
32690
+ continue;
32691
+ }
32203
32692
  ctx.addIssue(
32204
32693
  "warning",
32205
32694
  "TECH_ON_LOGIC_COMPONENT",
@@ -33755,11 +34244,26 @@ function tableEntry(ts, entry, initializerOf, literalOf) {
33755
34244
  const route2 = keyedPath ?? (paths.length === 1 ? paths[0] : void 0);
33756
34245
  return verb && route2 ? { verb, segments: route2.segments, exactLength: route2.exactLength, fullPath: true, table: true } : void 0;
33757
34246
  }
33758
- function tableRoutes(ts, value, initializerOf, literalOf) {
34247
+ function tableRoutes(ts, value, initializerOf, literalOf, depth = 0) {
34248
+ if (depth > SETTLE_DEPTH) return [];
33759
34249
  const at = bareExpression(ts, value);
33760
34250
  const out = [];
34251
+ const spread = (expression) => {
34252
+ const named2 = bareExpression(ts, expression);
34253
+ if (!ts.isIdentifier(named2) && !ts.isPropertyAccessExpression(named2)) return void 0;
34254
+ const initializer = initializerOf(named2);
34255
+ if (!initializer) return void 0;
34256
+ const inner2 = tableRoutes(ts, initializer, initializerOf, literalOf, depth + 1);
34257
+ return inner2.length > 0 ? inner2 : void 0;
34258
+ };
33761
34259
  if (ts.isArrayLiteralExpression(at)) {
33762
34260
  for (const element of at.elements) {
34261
+ if (ts.isSpreadElement(element)) {
34262
+ const routes = spread(element.expression);
34263
+ if (!routes) return [];
34264
+ out.push(...routes);
34265
+ continue;
34266
+ }
33763
34267
  const entry = bareExpression(ts, element);
33764
34268
  if (!ts.isObjectLiteralExpression(entry)) return [];
33765
34269
  const route2 = tableEntry(ts, entry, initializerOf, literalOf);
@@ -33770,6 +34274,12 @@ function tableRoutes(ts, value, initializerOf, literalOf) {
33770
34274
  }
33771
34275
  if (ts.isObjectLiteralExpression(at)) {
33772
34276
  for (const property of at.properties) {
34277
+ if (ts.isSpreadAssignment(property)) {
34278
+ const routes = spread(property.expression);
34279
+ if (!routes) return [];
34280
+ out.push(...routes);
34281
+ continue;
34282
+ }
33773
34283
  const key2 = !property.name ? void 0 : ts.isComputedPropertyName(property.name) ? settleString(ts, property.name.expression, initializerOf, literalOf) : propertyKeyText(ts, property.name);
33774
34284
  const match2 = key2 ? /^([A-Za-z]+)\s+(\/\S*)$/.exec(key2) : null;
33775
34285
  if (!match2 || !HTTP_VERBS.has(match2[1].toUpperCase())) return [];
@@ -33869,7 +34379,7 @@ function readRoutes(ts, fn, initializerOf, literalOf) {
33869
34379
  return void 0;
33870
34380
  };
33871
34381
  const prefix = strippedPrefix(ts, fn.body, initializerOf, literalOf);
33872
- const routeOf = (conjuncts) => {
34382
+ const routeOf2 = (conjuncts) => {
33873
34383
  let verb;
33874
34384
  let length;
33875
34385
  const fixed = /* @__PURE__ */ new Map();
@@ -33890,7 +34400,7 @@ function readRoutes(ts, fn, initializerOf, literalOf) {
33890
34400
  if (namedFunctionNameOf(ts, node) !== void 0) return;
33891
34401
  if (ts.isIfStatement(node)) {
33892
34402
  const guarded2 = [...enclosing2, ...conjunctsOf(node.expression)];
33893
- const route2 = routeOf(guarded2);
34403
+ const route2 = routeOf2(guarded2);
33894
34404
  if (route2) routes.set(routeKey2(route2), route2);
33895
34405
  walk2(node.thenStatement, guarded2);
33896
34406
  if (node.elseStatement) walk2(node.elseStatement, enclosing2);
@@ -33924,6 +34434,141 @@ function readRoutes(ts, fn, initializerOf, literalOf) {
33924
34434
  returns(fn.body);
33925
34435
  return routes;
33926
34436
  }
34437
+ var READS_CAP = 64;
34438
+ var READS_DEPTH = 3;
34439
+ var FLOW_CALLS = /* @__PURE__ */ new Set(["URL", "URLSearchParams", "parse"]);
34440
+ function bindingKeys(ts, pattern, out) {
34441
+ if (!ts.isObjectBindingPattern(pattern)) return;
34442
+ for (const element of pattern.elements) {
34443
+ const key2 = element.propertyName ?? element.name;
34444
+ if (ts.isIdentifier(key2) || ts.isStringLiteral(key2)) out.add(key2.text);
34445
+ if (!ts.isIdentifier(element.name)) bindingKeys(ts, element.name, out);
34446
+ }
34447
+ }
34448
+ function calleeName(ts, expression) {
34449
+ if (ts.isIdentifier(expression)) return expression.text;
34450
+ if (ts.isPropertyAccessExpression(expression)) return expression.name.text;
34451
+ return void 0;
34452
+ }
34453
+ function helperParam(ts, fn, index) {
34454
+ const body = fn.body;
34455
+ const parameter = (fn.parameters ?? [])[index];
34456
+ if (!body || !parameter || parameter.dotDotDotToken) return void 0;
34457
+ if (ts.isIdentifier(parameter.name)) return { body, name: parameter.name.text };
34458
+ const keys = /* @__PURE__ */ new Set();
34459
+ bindingKeys(ts, parameter.name, keys);
34460
+ return keys.size > 0 ? { body, keys: [...keys] } : void 0;
34461
+ }
34462
+ function handleReads(ts, body, name, helperOf, out = /* @__PURE__ */ new Set(), depth = 0, seen = /* @__PURE__ */ new Set()) {
34463
+ const visitKey = `${body.getSourceFile().fileName}:${body.pos}:${body.end}:${name}`;
34464
+ if (seen.has(visitKey)) return out;
34465
+ seen.add(visitKey);
34466
+ const deeper = (scope, local) => {
34467
+ if (depth < READS_DEPTH) handleReads(ts, scope, local, helperOf, out, depth + 1, seen);
34468
+ };
34469
+ const follow = (node) => {
34470
+ let at = node;
34471
+ for (; ; ) {
34472
+ const up = at.parent;
34473
+ if (!up || out.size >= READS_CAP) return;
34474
+ if (ts.isParenthesizedExpression(up) || ts.isNonNullExpression(up) || ts.isAsExpression(up) || ts.isTypeAssertionExpression(up) || ts.isAwaitExpression(up) || typeof ts.isSatisfiesExpression === "function" && ts.isSatisfiesExpression(up)) {
34475
+ at = up;
34476
+ continue;
34477
+ }
34478
+ if (ts.isBinaryExpression(up) && (up.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken || up.operatorToken.kind === ts.SyntaxKind.BarBarToken)) {
34479
+ at = up;
34480
+ continue;
34481
+ }
34482
+ if (ts.isPropertyAccessExpression(up) && up.expression === at) {
34483
+ out.add(up.name.text);
34484
+ at = up;
34485
+ continue;
34486
+ }
34487
+ if (ts.isElementAccessExpression(up) && up.expression === at) {
34488
+ if (!ts.isStringLiteralLike(up.argumentExpression)) return;
34489
+ out.add(up.argumentExpression.text);
34490
+ at = up;
34491
+ continue;
34492
+ }
34493
+ if (ts.isCallExpression(up) && up.expression === at) {
34494
+ const [first] = up.arguments;
34495
+ if (first && ts.isStringLiteralLike(first)) {
34496
+ out.add(first.text);
34497
+ return;
34498
+ }
34499
+ if (up.arguments.length > 0) return;
34500
+ at = up;
34501
+ continue;
34502
+ }
34503
+ if ((ts.isCallExpression(up) || ts.isNewExpression(up)) && up.expression !== at) {
34504
+ const args = up.arguments ?? [];
34505
+ const index = args.indexOf(at);
34506
+ if (index < 0) return;
34507
+ const helper = ts.isCallExpression(up) && depth < READS_DEPTH ? helperOf?.(up, index) : void 0;
34508
+ if (helper) {
34509
+ for (const key2 of helper.keys ?? []) out.add(key2);
34510
+ if (helper.name) deeper(helper.body, helper.name);
34511
+ at = up;
34512
+ continue;
34513
+ }
34514
+ const callee = calleeName(ts, up.expression);
34515
+ if (callee !== void 0 && FLOW_CALLS.has(callee)) {
34516
+ at = up;
34517
+ continue;
34518
+ }
34519
+ return;
34520
+ }
34521
+ if (ts.isVariableDeclaration(up) && up.initializer === at) {
34522
+ if (ts.isIdentifier(up.name)) {
34523
+ if (up.name.text !== name) deeper(body, up.name.text);
34524
+ } else {
34525
+ bindingKeys(ts, up.name, out);
34526
+ }
34527
+ }
34528
+ return;
34529
+ }
34530
+ };
34531
+ const visit = (node) => {
34532
+ if (out.size >= READS_CAP) return;
34533
+ if (ts.isIdentifier(node) && node.text === name) {
34534
+ const parent = node.parent;
34535
+ 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);
34536
+ if (!declares2) follow(node);
34537
+ }
34538
+ ts.forEachChild(node, visit);
34539
+ };
34540
+ visit(body);
34541
+ return out;
34542
+ }
34543
+ function sameFileHelpers(ts, sf) {
34544
+ const functions = /* @__PURE__ */ new Map();
34545
+ for (const statement of sf.statements) {
34546
+ if (ts.isFunctionDeclaration(statement) && statement.name && statement.body) functions.set(statement.name.text, statement);
34547
+ if (ts.isVariableStatement(statement)) {
34548
+ for (const declaration2 of statement.declarationList.declarations) {
34549
+ const initializer = declaration2.initializer && bareExpression(ts, declaration2.initializer);
34550
+ if (ts.isIdentifier(declaration2.name) && initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer))) {
34551
+ functions.set(declaration2.name.text, initializer);
34552
+ }
34553
+ }
34554
+ }
34555
+ }
34556
+ return (call, index) => {
34557
+ const callee = call.expression;
34558
+ if (ts.isIdentifier(callee)) {
34559
+ const fn = functions.get(callee.text);
34560
+ return fn ? helperParam(ts, fn, index) : void 0;
34561
+ }
34562
+ if (ts.isPropertyAccessExpression(callee) && callee.expression.kind === ts.SyntaxKind.ThisKeyword) {
34563
+ let owner = call.parent;
34564
+ while (owner && !ts.isClassLike(owner)) owner = owner.parent;
34565
+ const member = owner && ts.isClassLike(owner) ? owner.members.find((m) => ts.isMethodDeclaration(m) && m.name && propertyKeyText(ts, m.name) === callee.name.text) : void 0;
34566
+ return member ? helperParam(ts, member, index) : void 0;
34567
+ }
34568
+ return void 0;
34569
+ };
34570
+ }
34571
+ var REQUEST_HANDLE_NAMES = /* @__PURE__ */ new Set(["req", "request", "ctx", "context", "event", "url"]);
33927
34572
  function walkExact(ts, sourceText, fileName) {
33928
34573
  const sf = ts.createSourceFile(
33929
34574
  fileName,
@@ -34220,47 +34865,8 @@ function walkExact(ts, sourceText, fileName) {
34220
34865
  const container = containerOf(owner);
34221
34866
  return container !== void 0 ? { container } : nested;
34222
34867
  };
34223
- const readsOff = (body, name) => {
34224
- const out = /* @__PURE__ */ new Set();
34225
- const visit2 = (node) => {
34226
- if (out.size >= 64) return;
34227
- if (ts.isIdentifier(node) && node.text === name) {
34228
- const parent = node.parent;
34229
- const memberName = !!parent && (ts.isPropertyAccessExpression(parent) && parent.name === node || ts.isPropertyAssignment(parent) && parent.name === node);
34230
- if (!memberName) {
34231
- let at = node;
34232
- for (; ; ) {
34233
- const up = at.parent;
34234
- if (!up) break;
34235
- if (ts.isParenthesizedExpression(up) || ts.isNonNullExpression(up) || ts.isAsExpression(up)) {
34236
- at = up;
34237
- continue;
34238
- }
34239
- if (ts.isPropertyAccessExpression(up) && up.expression === at) {
34240
- out.add(up.name.text);
34241
- at = up;
34242
- continue;
34243
- }
34244
- if (ts.isElementAccessExpression(up) && up.expression === at && ts.isStringLiteralLike(up.argumentExpression)) {
34245
- out.add(up.argumentExpression.text);
34246
- at = up;
34247
- continue;
34248
- }
34249
- if (ts.isVariableDeclaration(up) && up.initializer === at && ts.isObjectBindingPattern(up.name)) {
34250
- for (const element of up.name.elements) {
34251
- const key2 = element.propertyName ?? element.name;
34252
- if (ts.isIdentifier(key2) || ts.isStringLiteral(key2)) out.add(key2.text);
34253
- }
34254
- }
34255
- break;
34256
- }
34257
- }
34258
- }
34259
- ts.forEachChild(node, visit2);
34260
- };
34261
- visit2(body);
34262
- return [...out].sort();
34263
- };
34868
+ const helpersHere = sameFileHelpers(ts, sf);
34869
+ const readsOff = (body, name) => [...handleReads(ts, body, name, helpersHere)].sort();
34264
34870
  const declaredParameters = (fn) => {
34265
34871
  const parameters = fn.parameters ?? [];
34266
34872
  const used = /* @__PURE__ */ new Set();
@@ -34976,7 +35582,7 @@ function checkerOptions(ts, projectRoot) {
34976
35582
  maxNodeModuleJsDepth: 0
34977
35583
  };
34978
35584
  }
34979
- function resolveCalls(ts, files, projectRoot) {
35585
+ function resolveCalls(ts, files, projectRoot, claimed = /* @__PURE__ */ new Set()) {
34980
35586
  const out = /* @__PURE__ */ new Map();
34981
35587
  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() };
34982
35588
  const options = checkerOptions(ts, projectRoot);
@@ -35533,9 +36139,14 @@ function resolveCalls(ts, files, projectRoot) {
35533
36139
  const reading = {};
35534
36140
  if (kind) reading.kind = kind;
35535
36141
  if (kind === "object") {
35536
- const props = checker.getPropertiesOfType(checker.getNonNullableType(type)).map((p) => p.getName());
36142
+ const props = checker.getPropertiesOfType(checker.getNonNullableType(type)).filter((p) => (p.flags & ts.SymbolFlags.Method) === 0).map((p) => p.getName());
35537
36143
  if (props.length > 0) reading.fields = props.slice(0, 200);
35538
36144
  }
36145
+ if (opaqueType(type)) reading.opaque = true;
36146
+ if (ts.isIdentifier(parameter.name) && REQUEST_HANDLE_NAMES.has(parameter.name.text.replace(/^_+/, "").toLowerCase())) {
36147
+ const read2 = handleReads(ts, body, parameter.name.text, checkerHelpers);
36148
+ if (read2.size > 0) reading.reads = [...read2].sort();
36149
+ }
35539
36150
  return reading;
35540
36151
  } catch {
35541
36152
  return {};
@@ -35560,6 +36171,16 @@ function resolveCalls(ts, files, projectRoot) {
35560
36171
  } catch {
35561
36172
  }
35562
36173
  }
36174
+ if (ts.isImportSpecifier(node) && !node.isTypeOnly && !fileRoutes.has(node.name.text)) {
36175
+ try {
36176
+ const declaration2 = symbolOf(node.name)?.valueDeclaration;
36177
+ if (declaration2 && ts.isVariableDeclaration(declaration2) && declaration2.initializer && (ts.getCombinedNodeFlags(declaration2) & ts.NodeFlags.Const) !== 0) {
36178
+ const table = tableRoutes(ts, declaration2.initializer, initializerOf, literalOf);
36179
+ if (table.length > 0) fileRoutes.set(node.name.text, new Map(table.map((route2) => [routeKey2(route2), route2])));
36180
+ }
36181
+ } catch {
36182
+ }
36183
+ }
35563
36184
  ts.forEachChild(node, visit);
35564
36185
  };
35565
36186
  visit(sf);
@@ -35618,6 +36239,29 @@ function resolveCalls(ts, files, projectRoot) {
35618
36239
  if (broken.length > 0) crossProjectImports.set(key2, broken);
35619
36240
  }
35620
36241
  return { calls: out, imports, kinds, routes, prefixes, crossProjectImports };
36242
+ function opaqueType(type) {
36243
+ const at = checker.getNonNullableType(type);
36244
+ if (at.intrinsicName === "error") return false;
36245
+ if ((at.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.NonPrimitive)) !== 0) return true;
36246
+ if ((at.flags & ts.TypeFlags.Object) === 0 || at.getProperties().length > 0) return false;
36247
+ if (at.getCallSignatures().length > 0 || at.getConstructSignatures().length > 0) return false;
36248
+ const lists = checker;
36249
+ return !(lists.isArrayType?.(at) || lists.isTupleType?.(at));
36250
+ }
36251
+ function checkerHelpers(call, index) {
36252
+ let declaration2;
36253
+ try {
36254
+ declaration2 = checker.getResolvedSignature(call)?.getDeclaration();
36255
+ } catch {
36256
+ return void 0;
36257
+ }
36258
+ if (!declaration2 || !declaration2.body) return void 0;
36259
+ const home = declaration2.getSourceFile();
36260
+ if (home.isDeclarationFile || !ownKey(home)) return void 0;
36261
+ const absolute = path20.resolve(home.fileName);
36262
+ if (absolute !== path20.resolve(call.getSourceFile().fileName) && claimed.has(absolute)) return void 0;
36263
+ return helperParam(ts, declaration2, index);
36264
+ }
35621
36265
  function platformKind(annotation) {
35622
36266
  if (!annotation || !ts.isTypeReferenceNode(annotation)) return void 0;
35623
36267
  const name = ts.isIdentifier(annotation.typeName) ? annotation.typeName.text : annotation.typeName.right.text;
@@ -35694,8 +36338,10 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
35694
36338
  const packages = {};
35695
36339
  for (const [specifier, absolute] of localPackages) packages[specifier] = pathKey(path20.relative(projectRoot, absolute));
35696
36340
  const declaredPaths = [];
36341
+ const claimedFiles = /* @__PURE__ */ new Set();
35697
36342
  for (const impl of implementations) {
35698
36343
  declaredPaths.push(...implementationSourceFiles(impl));
36344
+ for (const file of implementationSourceFiles(impl)) claimedFiles.add(path20.resolve(projectRoot, file));
35699
36345
  if (impl.simPath) declaredPaths.push(impl.simPath);
35700
36346
  const routerFile = impl.router ? routerLinkage(impl.router).file : void 0;
35701
36347
  if (routerFile) declaredPaths.push(routerFile);
@@ -35795,7 +36441,7 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
35795
36441
  let imports = /* @__PURE__ */ new Map();
35796
36442
  let reading;
35797
36443
  try {
35798
- reading = resolveCalls(ts, checkerRoots, projectRoot);
36444
+ reading = resolveCalls(ts, checkerRoots, projectRoot, claimedFiles);
35799
36445
  ({ calls: resolved, imports } = reading);
35800
36446
  } catch {
35801
36447
  resolved = /* @__PURE__ */ new Map();
@@ -35818,6 +36464,8 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
35818
36464
  const reading2 = settled[index][at];
35819
36465
  if (reading2.kind) param.kind = reading2.kind;
35820
36466
  if (reading2.fields) param.fields = reading2.fields;
36467
+ if (reading2.opaque) param.opaque = true;
36468
+ if (reading2.reads) param.reads = [.../* @__PURE__ */ new Set([...param.reads ?? [], ...reading2.reads])].sort();
35821
36469
  });
35822
36470
  });
35823
36471
  }
@@ -36684,6 +37332,7 @@ function buildRuleContext(opts) {
36684
37332
  ...opts.typeSpellingFacts ? { typeSpellingFacts: opts.typeSpellingFacts } : {},
36685
37333
  pinnedExternals,
36686
37334
  ...opts.bindingModules ? { bindingModules: opts.bindingModules } : {},
37335
+ ...opts.memberApprovedSurfaces ? { memberApprovedSurfaces: opts.memberApprovedSurfaces } : {},
36687
37336
  codeModel: opts.codeModel ?? emptyCodeModel(),
36688
37337
  roundTripIssues: opts.roundTripIssues,
36689
37338
  lintAllows: lintAllows2,
@@ -37214,6 +37863,7 @@ function runOwnersGate(rulesOrOptions, projectType = "backend", reachOnly = fals
37214
37863
  const parts = new Set(graph().nodes.find((n) => n.namespace === "")?.parts.map((p) => p.alias) ?? []);
37215
37864
  const pinnedExternals = listPinnedExternals2().filter((p) => !parts.has(p.alias));
37216
37865
  const bindingModules = reachOnly ? [] : readBindingModules(implementations.filter((impl) => !impl.id.includes("::")).flatMap((impl) => impl.bindings ?? []), getProjectRoot());
37866
+ const approvedMemberSurfaces = bindingModules.length > 0 && declaredMembers(boundConfig ?? {}).length > 0 ? memberApprovedSurfacesOrNone() : void 0;
37217
37867
  const conformance = rules?.conformance;
37218
37868
  const codeModel = reachOnly ? buildCodeModel([], [], getProjectRoot(), [], []) : buildCodeModel(implementations, types, getProjectRoot(), conformance?.sourceRoots ?? [], conformance?.exclude ?? []);
37219
37869
  const statusBearing = treatAllAsComplete ? [...subsystems, ...components, ...interfaces, ...implementations] : settledStatusBearing({ subsystems, components, interfaces, implementations });
@@ -37300,6 +37950,7 @@ function runOwnersGate(rulesOrOptions, projectType = "backend", reachOnly = fals
37300
37950
  typeSpellingFacts: typeSpellings,
37301
37951
  pinnedExternals,
37302
37952
  bindingModules,
37953
+ ...approvedMemberSurfaces ? { memberApprovedSurfaces: approvedMemberSurfaces } : {},
37303
37954
  // By-name selections only: a legacy path ref pins nothing to check. A dry
37304
37955
  // run supplies its candidate's; otherwise the stored ones.
37305
37956
  packSelections: packSelections ?? projectPackSelections(),
@@ -37642,6 +38293,13 @@ function familyApprovals(depth) {
37642
38293
  function familyRelations2() {
37643
38294
  return familyRelations();
37644
38295
  }
38296
+ function memberApprovedSurfacesOrNone() {
38297
+ try {
38298
+ return memberApprovedSurfaces2();
38299
+ } catch {
38300
+ return void 0;
38301
+ }
38302
+ }
37645
38303
  function implementsProblem2(ref) {
37646
38304
  return implementsProblem(ref);
37647
38305
  }
@@ -39755,6 +40413,7 @@ function respellStoredReferences(kind, doc, map) {
39755
40413
  break;
39756
40414
  case "interface":
39757
40415
  at(doc, "component", "contract");
40416
+ at(doc, "implements", "implements");
39758
40417
  typedSlots("interface", doc, map);
39759
40418
  break;
39760
40419
  case "implementation":
@@ -40258,6 +40917,57 @@ function parseOrThrow(schema, value, kind, id) {
40258
40917
  return res.data;
40259
40918
  }
40260
40919
  var SIGNATURE_TTL_MS = 2e3;
40920
+ var REPLACED_LINKAGE_LISTS = /* @__PURE__ */ new Set(["injectedParams", "bindings"]);
40921
+ function respelledField(kind, position) {
40922
+ switch (position) {
40923
+ case "narrative":
40924
+ case "calls":
40925
+ case "auth":
40926
+ return "methods";
40927
+ case "type":
40928
+ return kind === "type" ? "fields" : "methods";
40929
+ case "contract":
40930
+ return kind === "interface" ? "component" : "contract";
40931
+ default:
40932
+ return position;
40933
+ }
40934
+ }
40935
+ function omittedListNotices(kind, stored, delta) {
40936
+ const out = [];
40937
+ const fields = kind === "type" ? ["methods", "fields"] : kind === "interface" || kind === "implementation" ? ["methods"] : [];
40938
+ for (const field of fields) {
40939
+ const given = delta[field];
40940
+ if (!Array.isArray(given) || given.length === 0) continue;
40941
+ const named2 = new Set(given.map((e) => e && typeof e === "object" ? e.name : void 0).filter((n) => typeof n === "string"));
40942
+ const kept = (Array.isArray(stored[field]) ? stored[field] : []).map((e) => e?.name).filter((n) => typeof n === "string" && !named2.has(n));
40943
+ if (kept.length === 0) continue;
40944
+ out.push(
40945
+ `${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"}]}.`
40946
+ );
40947
+ }
40948
+ return out;
40949
+ }
40950
+ var PATH_SAFE_SEGMENT = /^[A-Za-z0-9_-]+$/;
40951
+ function pathSafeIdProblem(id) {
40952
+ if (typeof id !== "string" || id === "") return "it is empty";
40953
+ const segments = id.split("::");
40954
+ const named2 = id.startsWith("::") ? segments.slice(1) : segments;
40955
+ const bad = named2.find((s) => !PATH_SAFE_SEGMENT.test(s));
40956
+ if (bad === void 0) return null;
40957
+ if (/[\\/]/.test(bad)) return `"${bad}" holds a path separator`;
40958
+ if (bad === "") return "it has an empty segment";
40959
+ if (/^\.+$/.test(bad) || bad.includes(".")) return `"${bad}" holds a dot`;
40960
+ return `"${bad}" holds a character outside letters, digits, "-" and "_"`;
40961
+ }
40962
+ function assertPathSafe(id, what) {
40963
+ const problem = pathSafeIdProblem(id);
40964
+ if (problem !== null) {
40965
+ 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.`);
40966
+ }
40967
+ }
40968
+ function storedUnder(storedId, askedId) {
40969
+ return typeof storedId !== "string" || storedId === askedId.split("::").pop();
40970
+ }
40261
40971
  function computeSpecTreeSignature(dirs, files = []) {
40262
40972
  const parts = [];
40263
40973
  for (const f of files) {
@@ -41806,10 +42516,117 @@ var SpecWorkspace = class {
41806
42516
  const mapped = mapSpecReferences(kind, spec, (position, value) => position === "signatureFrom" && !components.has(value) ? value : this.writeReference(from, spec.id, position, value, carrying));
41807
42517
  return { ...mapped, id: localOf(from, spec.id).split("::").pop() };
41808
42518
  }
42519
+ /**
42520
+ * The keys an `alias::name` read lands on when no spec holds the id as
42521
+ * written: the id bound through the bound project's alias table as a write
42522
+ * binds it (a public name to the member's key), and the member's own key for
42523
+ * a name it does not export — so a read resolves the form the guide
42524
+ * prescribes exactly as a write resolves it to refuse. None for a bare id.
42525
+ */
42526
+ aliasedReadKeys(id) {
42527
+ if (!id.includes("::") || pathSafeIdProblem(id) !== null) return [];
42528
+ const out = [];
42529
+ try {
42530
+ const bound2 = this.bindReference("", id);
42531
+ if (bound2 !== id) out.push(bound2);
42532
+ } catch {
42533
+ }
42534
+ const at = id.indexOf("::");
42535
+ const namespace = this.cachedRawRoots[0]?.record.aliases.get(id.slice(0, at));
42536
+ if (namespace !== void 0 && namespace !== "" && this.rootCovering(namespace) !== null) out.push(`${namespace}::${id.slice(at + 2)}`);
42537
+ return [...new Set(out)];
42538
+ }
42539
+ /**
42540
+ * The references a delta writes in ANOTHER TEXT than the stored spec holds
42541
+ * at the same position while both bind to one target (core_orchestrator
42542
+ * updateSpec step 14): the stored file's authored texts read per position,
42543
+ * the delta's read the same way, and a delta text the position does not hold
42544
+ * whose binding equals a stored text's binding is a respelling of it.
42545
+ */
42546
+ referenceRespellingsOf(kind, id, delta) {
42547
+ if (kind === "system") return [];
42548
+ const file = this.storedFileOf(kind, id);
42549
+ if (!file || !pathExists(file)) return [];
42550
+ let stored;
42551
+ try {
42552
+ stored = JSON.parse(JSON.stringify(readSpecFile(file)));
42553
+ } catch {
42554
+ return [];
42555
+ }
42556
+ const held = /* @__PURE__ */ new Map();
42557
+ respellStoredReferences(kind, stored, (position, value) => {
42558
+ held.set(position, (held.get(position) ?? /* @__PURE__ */ new Set()).add(value));
42559
+ return value;
42560
+ });
42561
+ const written = [];
42562
+ respellStoredReferences(kind, JSON.parse(JSON.stringify(delta)), (position, value) => {
42563
+ written.push({ position, value });
42564
+ return value;
42565
+ });
42566
+ const prefix = this.writePrefixFor(id);
42567
+ const bind = (value) => {
42568
+ try {
42569
+ return this.bindReference(prefix, value);
42570
+ } catch {
42571
+ return value;
42572
+ }
42573
+ };
42574
+ const out = [];
42575
+ for (const w of written) {
42576
+ const texts = held.get(w.position);
42577
+ if (!texts || texts.has(w.value)) continue;
42578
+ const target = bind(w.value);
42579
+ const from = [...texts].find((s) => s !== w.value && bind(s) === target);
42580
+ if (from !== void 0 && !out.some((o) => o.position === w.position && o.from === from)) out.push({ position: w.position, from, to: w.value });
42581
+ }
42582
+ return out;
42583
+ }
42584
+ /**
42585
+ * spec_loader.readPinnedSpec — an EXTERNAL's spec read by `alias::name` from
42586
+ * the pinned snapshot the bound project holds of it: the contract entry (a
42587
+ * component or an interface asked for, or no kind) or the exported type (a
42588
+ * type). Null when the alias is no declared external (a member's alias is
42589
+ * read under its key instead), its pin is absent or unreadable, or the pin
42590
+ * holds no such public name. Read-only, never cached.
42591
+ */
42592
+ readPinnedSpec(kind, id) {
42593
+ const at = id.indexOf("::");
42594
+ const pin2 = at > 0 ? this.pinnedSnapshotOf(id.slice(0, at)) : null;
42595
+ if (!pin2) return null;
42596
+ const name = id.slice(at + 2);
42597
+ const entry = kind === "type" ? void 0 : pin2.snapshot.interfaces.find((e) => e.id === name);
42598
+ if (entry) return { ...pin2.base, kind: "interface", id: name, spec: entry };
42599
+ const definition = kind === void 0 || kind === "type" ? pinnedTypeDefinition(pin2.snapshot, name) : void 0;
42600
+ return definition ? { ...pin2.base, kind: "type", id: name, spec: definition } : null;
42601
+ }
42602
+ /** 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. */
42603
+ pinnedSnapshotOf(alias) {
42604
+ this.scanAll();
42605
+ const root = this.cachedRawRoots[0];
42606
+ const config = root?.record.config;
42607
+ if (!root || !config || root.record.aliases.has(alias)) return null;
42608
+ const declared = declaredExternals(config).find((d) => d.alias === alias && !d.problem);
42609
+ const file = path23.join(aiPathsAt(root.dir).root(), "externals", `${alias}.yaml`);
42610
+ if (!declared || !pathExists(file)) return null;
42611
+ try {
42612
+ const snapshot = SurfaceSnapshotSchema.parse(readSpecFile(file));
42613
+ const rel2 = path23.relative(root.dir, file).split(path23.sep).join("/");
42614
+ return { snapshot, base: { alias, project: declared.project, file: rel2, ...snapshot.stateId !== void 0 ? { stateId: snapshot.stateId } : {} } };
42615
+ } catch {
42616
+ return null;
42617
+ }
42618
+ }
41809
42619
  // -------------------------------------------------------------------------
41810
42620
  // Path builders
42621
+ //
42622
+ // Every id a path is built from must be path-safe FIRST: a subsystem, a
42623
+ // component, a contract, an implementation or a type id becomes a folder or
42624
+ // a file name, and one carrying a separator or a `..` segment would build a
42625
+ // path out of the tree — into another project's specs. Refused here, before
42626
+ // any path exists, whichever write asked.
41811
42627
  // -------------------------------------------------------------------------
41812
42628
  getSubsystemPath(id) {
42629
+ assertPathSafe(id, "subsystem");
41813
42630
  const index = this.scanAll();
41814
42631
  if (index.paths.subsystem[id]) {
41815
42632
  return index.paths.subsystem[id];
@@ -41834,6 +42651,8 @@ var SpecWorkspace = class {
41834
42651
  return path23.join(this.paths.specsDir(), id, ".index.yaml");
41835
42652
  }
41836
42653
  getComponentPath(id, subsystemId) {
42654
+ assertPathSafe(id, "component");
42655
+ if (subsystemId !== void 0) assertPathSafe(subsystemId, "subsystem");
41837
42656
  const index = this.scanAll();
41838
42657
  if (index.paths.component[id]) {
41839
42658
  return index.paths.component[id];
@@ -41868,6 +42687,8 @@ var SpecWorkspace = class {
41868
42687
  return path23.join(this.paths.specsDir(), targetSubsystem, id, ".index.yaml");
41869
42688
  }
41870
42689
  getInterfacePath(id, componentId) {
42690
+ assertPathSafe(id, "interface");
42691
+ if (componentId !== void 0) assertPathSafe(componentId, "component");
41871
42692
  const index = this.scanAll();
41872
42693
  if (index.paths.interface[id]) {
41873
42694
  return index.paths.interface[id];
@@ -41903,6 +42724,8 @@ var SpecWorkspace = class {
41903
42724
  return path23.join(this.paths.specsDir(), "default", targetComponent2, ".interface.yaml");
41904
42725
  }
41905
42726
  getImplementationPath(id, contractId) {
42727
+ assertPathSafe(id, "implementation");
42728
+ if (contractId !== void 0) assertPathSafe(contractId, "interface");
41906
42729
  const index = this.scanAll();
41907
42730
  if (index.paths.implementation[id]) {
41908
42731
  return index.paths.implementation[id];
@@ -41938,6 +42761,9 @@ var SpecWorkspace = class {
41938
42761
  return path23.join(this.paths.specsDir(), "default", targetContract, ".implementation.yaml");
41939
42762
  }
41940
42763
  getTypePath(id, subsystemId, group) {
42764
+ assertPathSafe(id, "type");
42765
+ if (subsystemId !== void 0 && subsystemId !== "") assertPathSafe(subsystemId, "subsystem");
42766
+ if (group !== void 0 && group !== "") assertPathSafe(group, "group");
41941
42767
  const index = this.scanAll();
41942
42768
  if (index.paths.type[id]) return index.paths.type[id];
41943
42769
  if (id.includes("::")) {
@@ -42080,11 +42906,15 @@ var SpecWorkspace = class {
42080
42906
  const index = this.scanAll();
42081
42907
  const cached = index.subsystems.find((s) => s.id === id);
42082
42908
  if (cached) return cached;
42909
+ const aliased = this.aliasedReadKeys(id).map((key2) => index.subsystems.find((s) => s.id === key2)).find((s) => s !== void 0);
42910
+ if (aliased) return aliased;
42911
+ if (pathSafeIdProblem(id) !== null) return null;
42083
42912
  const p = this.getSubsystemPath(id);
42084
42913
  if (!pathExists(p)) return null;
42085
42914
  try {
42086
42915
  const raw = readSpecFile(p);
42087
- return SubsystemSpecSchema.parse(raw);
42916
+ const parsed = SubsystemSpecSchema.parse(raw);
42917
+ return storedUnder(parsed.id, id) ? parsed : null;
42088
42918
  } catch (e) {
42089
42919
  this.loaderIssues.push({
42090
42920
  severity: "error",
@@ -42252,10 +43082,14 @@ var SpecWorkspace = class {
42252
43082
  const index = this.scanAll();
42253
43083
  const spec = index.components.find((c) => c.id === id);
42254
43084
  if (spec) return spec;
43085
+ const aliased = this.aliasedReadKeys(id).map((key2) => index.components.find((c) => c.id === key2)).find((c) => c !== void 0);
43086
+ if (aliased) return aliased;
43087
+ if (pathSafeIdProblem(id) !== null) return null;
42255
43088
  const p = this.getComponentPath(id);
42256
43089
  if (!pathExists(p)) return null;
42257
43090
  try {
42258
43091
  const raw = readSpecFile(p);
43092
+ if (!storedUnder(raw.id, id)) return null;
42259
43093
  readRetiredReachForms("component", raw);
42260
43094
  return attachRetiredMounts(ComponentSpecSchema.parse(raw), raw);
42261
43095
  } catch (e) {
@@ -42355,10 +43189,14 @@ var SpecWorkspace = class {
42355
43189
  const index = this.scanAll();
42356
43190
  const spec = index.interfaces.find((i) => i.id === id);
42357
43191
  if (spec) return spec;
43192
+ const aliased = this.aliasedReadKeys(id).map((key2) => index.interfaces.find((i) => i.id === key2)).find((i) => i !== void 0);
43193
+ if (aliased) return aliased;
43194
+ if (pathSafeIdProblem(id) !== null) return null;
42358
43195
  const p = this.getInterfacePath(id);
42359
43196
  if (!pathExists(p)) return null;
42360
43197
  try {
42361
43198
  const raw = readSpecFile(p);
43199
+ if (!storedUnder(raw.id, id)) return null;
42362
43200
  const owner = typeof raw.component === "string" ? index.components.find((c) => c.id === raw.component) : void 0;
42363
43201
  readRetiredReachForms("interface", raw, owner?.componentType === "Portal");
42364
43202
  return resolveTree([interfaceCanonicalTypes(InterfaceSpecSchema.parse(raw)).spec], index.components, index.types).interfaces[0];
@@ -42454,10 +43292,14 @@ var SpecWorkspace = class {
42454
43292
  const index = this.scanAll();
42455
43293
  const spec = index.implementations.find((impl) => impl.id === id);
42456
43294
  if (spec) return spec;
43295
+ const aliased = this.aliasedReadKeys(id).map((key2) => index.implementations.find((impl) => impl.id === key2)).find((impl) => impl !== void 0);
43296
+ if (aliased) return aliased;
43297
+ if (pathSafeIdProblem(id) !== null) return null;
42457
43298
  const p = this.getImplementationPath(id);
42458
43299
  if (!pathExists(p)) return null;
42459
43300
  try {
42460
43301
  const raw = readSpecFile(p);
43302
+ if (!storedUnder(raw.id, id)) return null;
42461
43303
  return ImplementationSpecSchema.parse(raw);
42462
43304
  } catch (e) {
42463
43305
  this.loaderIssues.push({
@@ -42507,7 +43349,8 @@ var SpecWorkspace = class {
42507
43349
  return this.scanAll().types;
42508
43350
  }
42509
43351
  loadTypeSpec(id) {
42510
- return this.scanAll().types.find((t) => t.id === id) ?? null;
43352
+ const types = this.scanAll().types;
43353
+ return types.find((t) => t.id === id) ?? this.aliasedReadKeys(id).map((key2) => types.find((t) => t.id === key2)).find((t) => t !== void 0) ?? null;
42511
43354
  }
42512
43355
  /**
42513
43356
  * Returns non-fatal placement notices (empty when there is nothing to
@@ -43720,6 +44563,8 @@ var SpecWorkspace = class {
43720
44563
  res.dispatch = inDeltaOrder(key2, existing.dispatch, value, mergeKeyedArray(existing.dispatch, value, (b) => String(b?.capability)));
43721
44564
  } else if (key2 === "lifecycle" && Array.isArray(value) && Array.isArray(existing.lifecycle)) {
43722
44565
  res.lifecycle = inDeltaOrder(key2, existing.lifecycle, value, mergeKeyedArray(existing.lifecycle, value, (le) => `${le?.phase} ${le?.component} ${le?.method}`));
44566
+ } else if (kind === "implementation" && delta2 === qualifiedDelta && REPLACED_LINKAGE_LISTS.has(key2) && Array.isArray(value) && !value.some(isValueMarker)) {
44567
+ res[key2] = value;
43723
44568
  } else if (Array.isArray(value) && isPlainValueList(key2, existing[key2], value)) {
43724
44569
  res[key2] = inDeltaOrder(key2, existing[key2] ?? [], value, mergePlainValues(key2, existing[key2] ?? [], value));
43725
44570
  } else if (Array.isArray(value) && value.length > 0 && Array.isArray(existing[key2]) && isIdentifiedArray(key2, existing[key2], value)) {
@@ -43820,12 +44665,14 @@ var SpecWorkspace = class {
43820
44665
  const canonicalStored = canonical3(storedBefore);
43821
44666
  const canonicalMerged = canonical3(mergedResult);
43822
44667
  const changes = specChanges(canonicalStored, canonicalMerged);
44668
+ const referenceRespellings = this.referenceRespellingsOf(kind, id, mergeableDelta);
44669
+ for (const r of referenceRespellings) changes.push({ path: respelledField(kind, r.position), change: "set", before: r.from, after: r.to });
43823
44670
  const ineffective = ineffectiveDeltaPaths(
43824
44671
  { ...qualifiedDelta, ...unsetFields.length ? { unset: unsetFields } : {} },
43825
44672
  canonicalMerged,
43826
44673
  canonicalStored,
43827
44674
  respellings
43828
- );
44675
+ ).filter((line) => !referenceRespellings.some((r) => line.startsWith(respelledField(kind, r.position)) && line.includes("already held")));
43829
44676
  if (changes.length === 0) {
43830
44677
  return {
43831
44678
  kind,
@@ -43834,7 +44681,7 @@ var SpecWorkspace = class {
43834
44681
  dryRun,
43835
44682
  changes,
43836
44683
  ineffective,
43837
- notices,
44684
+ notices: [...notices, ...omittedListNotices(kind, storedBefore, mergeableDelta)],
43838
44685
  respellings: [],
43839
44686
  testsToRevisit: [],
43840
44687
  summary: `No change to ${kind} "${id}" \u2014 the delta matches what is stored, so nothing ${dryRun ? "would be" : "was"} written.`
@@ -43864,6 +44711,9 @@ var SpecWorkspace = class {
43864
44711
  allowStatusDemotion: Object.prototype.hasOwnProperty.call(delta, "status")
43865
44712
  };
43866
44713
  notices.push(...this.saveOfKind(kind, mergedResult, opts));
44714
+ if (referenceRespellings.length > 0) {
44715
+ this.respellReferences(kind, id, referenceRespellings.map((r) => ({ kind, specId: id, position: r.position, from: r.from, to: r.to })));
44716
+ }
43867
44717
  return {
43868
44718
  kind,
43869
44719
  id,
@@ -44527,6 +45377,23 @@ function registryInvalidateCache() {
44527
45377
  function invalidateSpecCache() {
44528
45378
  registryInvalidateCache();
44529
45379
  }
45380
+ function registryLockSpecTree() {
45381
+ lockTree(getProjectRoot());
45382
+ for (const ws of workspaces.values()) ws.expireFreshness();
45383
+ }
45384
+ function lockTree2() {
45385
+ registryLockSpecTree();
45386
+ }
45387
+ function pinnedTypeDefinition(snapshot, name) {
45388
+ const exported = (snapshot.exportedTypes ?? []).find((t) => t.id === name);
45389
+ return exported ? snapshot.types.find((t) => t.id === exported.type) : void 0;
45390
+ }
45391
+ function readPinnedSpec(kind, id) {
45392
+ return current().readPinnedSpec(kind, id);
45393
+ }
45394
+ function unlockTree2() {
45395
+ unlockTree(getProjectRoot());
45396
+ }
44530
45397
  function getLoaderIssues() {
44531
45398
  return current().loaderIssues;
44532
45399
  }
@@ -45006,9 +45873,49 @@ function listConsumers(search) {
45006
45873
  const bound2 = path25.resolve(getProjectRoot());
45007
45874
  const { top } = climb(bound2);
45008
45875
  const family = familyConsumers(bound2, top);
45009
- const seen = new Set(family.map((c) => dirKey(c.directory)));
45010
- const found = searchedConsumers(bound2, search ?? []).filter((c) => !seen.has(dirKey(c.directory)));
45011
- return [...family, ...found];
45876
+ const seen = new Set([bound2, top, ...family.map((c) => c.directory)].map(dirKey));
45877
+ const parents = composingParents(bound2, top, seen);
45878
+ for (const c of parents) seen.add(dirKey(c.directory));
45879
+ const found = searchedConsumers(bound2, search ?? [], seen).filter((c) => !seen.has(dirKey(c.directory)));
45880
+ return [...family, ...parents, ...found];
45881
+ }
45882
+ function composesByPath(config, root, bound2) {
45883
+ 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));
45884
+ }
45885
+ function composingParents(bound2, top, seen) {
45886
+ if (getHostedLookup() !== null) return [];
45887
+ const reach2 = getRequestParentReach();
45888
+ if (reach2 && !reach2.parentReach) return [];
45889
+ const folders = [...new Set([path25.dirname(top), path25.dirname(bound2)].map((d) => path25.resolve(d)))];
45890
+ const out = [];
45891
+ for (const root of [...new Set(folders.flatMap(enclosedRoots).map((r) => path25.resolve(r)))]) {
45892
+ if (seen.has(dirKey(root)) || out.some((c) => dirKey(c.directory) === dirKey(root))) continue;
45893
+ const config = membersConfigAt(root);
45894
+ if (!config || !composesByPath(config, root, bound2)) continue;
45895
+ out.push(...familyConsumers(bound2, root).filter((c) => !seen.has(dirKey(c.directory))));
45896
+ }
45897
+ return out;
45898
+ }
45899
+ var ENCLOSING_FOLDER_LIMIT = 256;
45900
+ function enclosedRoots(folder) {
45901
+ let entries;
45902
+ try {
45903
+ entries = fs17.readdirSync(folder, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".") && d.name !== "node_modules");
45904
+ } catch {
45905
+ return [];
45906
+ }
45907
+ if (entries.length > ENCLOSING_FOLDER_LIMIT) return [];
45908
+ return entries.map((d) => path25.join(folder, d.name)).filter((dir) => fs17.existsSync(path25.join(dir, ".wai", "project.yaml")));
45909
+ }
45910
+ function membersConfigAt(root) {
45911
+ try {
45912
+ const text3 = fs17.readFileSync(path25.join(root, ".wai", "project.yaml"), "utf8");
45913
+ if (!/^members\s*:/m.test(text3)) return null;
45914
+ const parsed = parseYaml(text3);
45915
+ return parsed && typeof parsed === "object" ? parsed : null;
45916
+ } catch {
45917
+ return null;
45918
+ }
45012
45919
  }
45013
45920
  function familyConsumers(bound2, top) {
45014
45921
  return runWithProjectRoot(top, () => {
@@ -45051,7 +45958,7 @@ function projectRootsUnder(folder) {
45051
45958
  if (isRoot(at)) return [at];
45052
45959
  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);
45053
45960
  }
45054
- function searchedConsumers(bound2, search) {
45961
+ function searchedConsumers(bound2, search, seen = /* @__PURE__ */ new Set()) {
45055
45962
  if (search.length === 0) return [];
45056
45963
  if (getHostedLookup() !== null) throw new Error("searching folders for consumers is a local read: a hosted request reads no folder outside its project");
45057
45964
  const producerId = runWithProjectRoot(bound2, () => {
@@ -45061,6 +45968,13 @@ function searchedConsumers(bound2, search) {
45061
45968
  const roots = [...new Set(search.flatMap(projectRootsUnder).map((r) => path25.resolve(r)))].filter((r) => dirKey(r) !== dirKey(bound2));
45062
45969
  const out = [];
45063
45970
  for (const root of roots) {
45971
+ if (seen.has(dirKey(root))) continue;
45972
+ const members = membersConfigAt(root);
45973
+ const composing = members !== null && composesByPath(members, root, bound2);
45974
+ if (composing) {
45975
+ out.push(...familyConsumers(bound2, root).filter((c) => !seen.has(dirKey(c.directory))).map((c) => ({ ...c, found: "search" })));
45976
+ continue;
45977
+ }
45064
45978
  const answer = runWithProjectRoot(root, () => {
45065
45979
  let config;
45066
45980
  try {
@@ -45088,6 +46002,47 @@ function searchedConsumers(bound2, search) {
45088
46002
  }
45089
46003
  return out.sort((a, b) => a.directory < b.directory ? -1 : a.directory > b.directory ? 1 : 0);
45090
46004
  }
46005
+ function recordedMemberSubjects(root) {
46006
+ try {
46007
+ const record = JSON.parse(fs17.readFileSync(path25.join(root, ".wai", "lock.json"), "utf8"));
46008
+ return new Map(Object.entries(record.members ?? {}).filter(([, m]) => typeof m?.subject === "string").map(([alias, m]) => [alias, m.subject]));
46009
+ } catch {
46010
+ return /* @__PURE__ */ new Map();
46011
+ }
46012
+ }
46013
+ function memberRevisions() {
46014
+ if (getHostedLookup() !== null) return [];
46015
+ const bound2 = path25.resolve(getProjectRoot());
46016
+ const family = graph();
46017
+ const own2 = consumerNode(family, bound2);
46018
+ if (!own2) return [];
46019
+ const subjects = recordedMemberSubjects(bound2);
46020
+ const out = [];
46021
+ for (const node of family.nodes) {
46022
+ if (node.parent !== own2.namespace || node === own2 || node.mountAlias === void 0) continue;
46023
+ const alias = node.mountAlias;
46024
+ const usage = exportUsage(own2.namespace, node.namespace);
46025
+ const entry = { alias, project: node.id ?? node.namespace, root: node.directory, usage };
46026
+ const repo = repositoryRoot(node.directory);
46027
+ if (repo) {
46028
+ const lockPath3 = path25.join(".wai", "lock.json");
46029
+ const subject = subjects.get(alias);
46030
+ const digest3 = subject?.slice(subject.lastIndexOf(":") + 1);
46031
+ const recorded = digest3 ? commitIntroducing(node.directory, lockPath3, digest3) : null;
46032
+ const commit2 = recorded ?? lastCommitOf(node.directory, lockPath3);
46033
+ if (commit2) {
46034
+ try {
46035
+ const relative27 = path25.relative(repo, node.directory);
46036
+ const directory = fetch(repo, commit2, relative27 === "" ? void 0 : relative27);
46037
+ entry.revision = { directory, commit: commit2, label: recorded ? `the approval this project's lock records for it (${commit2.slice(0, 12)})` : `its own last committed approval (${commit2.slice(0, 12)})` };
46038
+ } catch {
46039
+ }
46040
+ }
46041
+ }
46042
+ out.push(entry);
46043
+ }
46044
+ return out;
46045
+ }
45091
46046
  function approvedRevision(against) {
45092
46047
  const bound2 = path25.resolve(getProjectRoot());
45093
46048
  const repo = repositoryRoot(bound2);
@@ -46704,6 +47659,19 @@ function moveMemberSpecs(scan2, remap) {
46704
47659
  });
46705
47660
  }
46706
47661
  const rewritten = rewriteRefFields(parentSpecsDir, remap);
47662
+ for (const file of listFilesRecursive(parentSpecsDir, ".yaml")) {
47663
+ let raw;
47664
+ try {
47665
+ raw = readYamlFile(file);
47666
+ } catch {
47667
+ continue;
47668
+ }
47669
+ const kind = raw && typeof raw === "object" ? specKind(raw) : void 0;
47670
+ if (!kind || !respellTypeTokens(raw, remap)) continue;
47671
+ writeYamlFile(file, raw);
47672
+ const id = kind === "system" ? "system" : String(raw.id);
47673
+ if (!rewritten.some((s) => s.kind === kind && s.id === id)) rewritten.push({ kind, id });
47674
+ }
46707
47675
  invalidateSpecCache();
46708
47676
  return rewritten;
46709
47677
  }
@@ -47253,6 +48221,8 @@ function patchSubsystemIndex(indexPath, mutate) {
47253
48221
  writeYamlFile(indexPath, raw);
47254
48222
  }
47255
48223
  function renameComponent(componentId, newId, dryRun) {
48224
+ const foreign = foreignIdRefusal(componentId, "chained-component", "rename it", `sdd_rename_component ${componentId.slice(componentId.lastIndexOf("::") + 2)} <new id>`);
48225
+ if (foreign !== null) throw new WaironError(foreign);
47256
48226
  const component = loadComponentSpec(componentId);
47257
48227
  if (!component) {
47258
48228
  throw new WaironError(`component-missing: no component has the id "${componentId}".`);
@@ -47439,6 +48409,8 @@ function renameSpecId(kind, id, newId, dryRun) {
47439
48409
  const owner = kind === "component" ? "sdd_rename_component" : kind === "type" ? "sdd_rename_type" : "no tool yet";
47440
48410
  throw new WaironError(`invalid-kind: sdd_rename_spec renames a contract (kind interface) or an implementation on its own; a ${kind} is renamed with ${owner}.`);
47441
48411
  }
48412
+ const foreign = foreignIdRefusal(id, "chained-spec", "rename it", `sdd_rename_spec ${kind} ${id.slice(id.lastIndexOf("::") + 2)} <new id>`);
48413
+ if (foreign !== null) throw new WaironError(foreign);
47442
48414
  const spec = kind === "interface" ? loadInterfaceSpec(id) : loadImplementationSpec(id);
47443
48415
  if (!spec) throw new WaironError(`spec-missing: no ${kind} has the id "${id}".`);
47444
48416
  if (id.includes("::")) {
@@ -47595,11 +48567,11 @@ function moveTypeSpec(id, target, subsystems, dryRun) {
47595
48567
  if (target === void 0 && exportsMoving.length + repointed.length > 0) {
47596
48568
  throw new WaironError(`exported: subsystem "${from}" exports type "${bare2}" (${exportsMoving.length + repointed.length} entr${exportsMoving.length + repointed.length === 1 ? "y" : "ies"}), and a system-level type is published by the L0 alone \u2014 move its export entries to the L0 first, or move the type to another subsystem.`);
47597
48569
  }
47598
- const oldToken = from ? `${from}::${bare2}` : void 0;
47599
- const newToken = target ? `${target}::${bare2}` : bare2;
48570
+ const ownId = effectiveProjectId(projectConfigRepository.load() ?? { name: loadSystemSpec()?.name ?? "" });
48571
+ const moveToken = movedTypeToken(from, bare2, target, ownId);
47600
48572
  const specsDir = aiPathsAt(getProjectRoot()).specsDir();
47601
48573
  const rewriteAll2 = (write6) => {
47602
- if (oldToken === void 0) return [];
48574
+ if (from === "") return [];
47603
48575
  const out = [];
47604
48576
  for (const file of listFilesRecursive(specsDir, ".yaml")) {
47605
48577
  let raw;
@@ -47609,7 +48581,7 @@ function moveTypeSpec(id, target, subsystems, dryRun) {
47609
48581
  continue;
47610
48582
  }
47611
48583
  const kind = raw && typeof raw === "object" ? specKind(raw) : void 0;
47612
- if (!kind || !respellQualifiedType(raw, oldToken, newToken)) continue;
48584
+ if (!kind || !respellTypeTokens(raw, moveToken)) continue;
47613
48585
  if (write6) writeYamlFile(file, raw);
47614
48586
  out.push({ kind, id: kind === "system" ? "system" : String(raw.id) });
47615
48587
  }
@@ -47656,22 +48628,33 @@ function moveTypeSpec(id, target, subsystems, dryRun) {
47656
48628
  return report3;
47657
48629
  }
47658
48630
  var TYPE_POSITION_KEYS = /* @__PURE__ */ new Set(["type", "returns", "signature", "signatureFrom", "typeDef", "references", "linkedEntity", "holds"]);
47659
- function respellQualifiedType(raw, oldToken, newToken) {
48631
+ var QUALIFIED_TYPE_TOKEN = /(^|[^A-Za-z0-9_:.-])((?:::)?[A-Za-z0-9_][A-Za-z0-9_-]*(?:(?:::|\.)[A-Za-z0-9_][A-Za-z0-9_-]*)+)/g;
48632
+ function respellTypeTokens(raw, map) {
47660
48633
  let changed = false;
47661
- const escaped = oldToken.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
48634
+ const respell = (value) => value.replace(QUALIFIED_TYPE_TOKEN, (_m, lead, token) => `${lead}${map(token)}`);
47662
48635
  const walk2 = (node) => {
47663
48636
  if (Array.isArray(node)) {
47664
48637
  node.forEach(walk2);
47665
48638
  return;
47666
48639
  }
47667
48640
  if (!node || typeof node !== "object") return;
47668
- for (const [key2, value] of Object.entries(node)) {
48641
+ const holder = node;
48642
+ for (const [key2, value] of Object.entries(holder)) {
47669
48643
  if (typeof value === "string" && TYPE_POSITION_KEYS.has(key2)) {
47670
- const next = value.replace(new RegExp(`(?<![A-Za-z0-9_:-])${escaped}(?![A-Za-z0-9_-])`, "g"), newToken);
48644
+ const next = respell(value);
47671
48645
  if (next !== value) {
47672
- node[key2] = next;
48646
+ holder[key2] = next;
47673
48647
  changed = true;
47674
48648
  }
48649
+ } else if (key2 === "assertsInvariants" && Array.isArray(value)) {
48650
+ value.forEach((ref, i) => {
48651
+ if (typeof ref !== "string") return;
48652
+ const next = respell(ref);
48653
+ if (next !== ref) {
48654
+ value[i] = next;
48655
+ changed = true;
48656
+ }
48657
+ });
47675
48658
  } else if (value && typeof value === "object" && key2 !== "previousIds" && key2 !== "previousNames") {
47676
48659
  walk2(value);
47677
48660
  }
@@ -47680,6 +48663,23 @@ function respellQualifiedType(raw, oldToken, newToken) {
47680
48663
  walk2(raw);
47681
48664
  return changed;
47682
48665
  }
48666
+ function movedTypeToken(from, bare2, target, ownId) {
48667
+ return (token) => {
48668
+ const lead = token.startsWith("::") ? "::" : "";
48669
+ const parts = token.slice(lead.length).split(/(::|\.)/);
48670
+ const segment = (i) => parts[2 * i];
48671
+ const count = (parts.length + 1) / 2;
48672
+ for (let i = 0; i + 1 < count && i <= 1; i++) {
48673
+ if (nameKey(segment(i)) !== nameKey(from) || nameKey(segment(i + 1)) !== nameKey(bare2)) continue;
48674
+ if (i === 1 && (lead !== "" || ownId === null || nameKey(segment(0)) !== nameKey(ownId))) continue;
48675
+ const next = [...parts];
48676
+ if (target !== void 0) next[2 * i] = target;
48677
+ else next.splice(2 * i, 2);
48678
+ return `${lead}${next.join("")}`;
48679
+ }
48680
+ return token;
48681
+ };
48682
+ }
47683
48683
  function publishedUsesOf(removed, methods) {
47684
48684
  const contracts = loadInterfaceSpecs();
47685
48685
  const uses = [];
@@ -47748,7 +48748,16 @@ var PROSE_FIELDS2 = /* @__PURE__ */ new Set([
47748
48748
  "note",
47749
48749
  "caller"
47750
48750
  ]);
48751
+ function shouldPinSymbol(implementation, methodName, pinSymbol) {
48752
+ const realization = implementation.methods.find((m) => m.name === methodName);
48753
+ if (!realization || realization.symbol !== void 0 || pinSymbol === false) return false;
48754
+ if (pinSymbol === true) return true;
48755
+ const file = realization.sourcePath ?? implementation.sourcePath;
48756
+ return file !== void 0 && file !== "" && fs19.existsSync(path27.resolve(getProjectRoot(), file));
48757
+ }
47751
48758
  function renameMethod(componentId, methodName, newName, pinSymbol, dryRun) {
48759
+ const foreign = foreignIdRefusal(componentId, "chained-component", "rename its method", `sdd_rename_method ${componentId.slice(componentId.lastIndexOf("::") + 2)} <method> <new name>`);
48760
+ if (foreign !== null) throw new WaironError(foreign);
47752
48761
  const component = loadComponentSpec(componentId);
47753
48762
  if (!component) {
47754
48763
  throw new WaironError(`component-missing: no component has the id "${componentId}".`);
@@ -47809,7 +48818,7 @@ function renameMethod(componentId, methodName, newName, pinSymbol, dryRun) {
47809
48818
  );
47810
48819
  if (dryRun) {
47811
48820
  const realizing = implementations.filter((impl) => movingContracts.has(impl.contract) && impl.methods.some((m) => m.name === methodName));
47812
- const wouldPin = pinSymbol !== false && realizing.some((impl) => impl.methods.find((m) => m.name === methodName)?.symbol === void 0);
48821
+ const wouldPin = realizing.some((impl) => shouldPinSymbol(impl, methodName, pinSymbol));
47813
48822
  const wouldRewrite = [...rewriteRefFields(reachDir, methodRemap, void 0, true), ...rekeyLintAllows(reachDir, rename, true)];
47814
48823
  return {
47815
48824
  component: componentId,
@@ -47841,7 +48850,7 @@ function renameMethod(componentId, methodName, newName, pinSymbol, dryRun) {
47841
48850
  if (!movingContracts.has(implementation.contract)) continue;
47842
48851
  const realization = implementation.methods.find((m) => m.name === methodName);
47843
48852
  if (!realization) continue;
47844
- const pin2 = realization.symbol === void 0 && pinSymbol !== false;
48853
+ const pin2 = shouldPinSymbol(implementation, methodName, pinSymbol);
47845
48854
  if (pin2) pinnedSymbol = methodName;
47846
48855
  saveImplementationSpec({
47847
48856
  ...implementation,
@@ -47935,15 +48944,29 @@ function respellLike(written, oldId, newId) {
47935
48944
  return /^[A-Z]/.test(written) ? pascal(newId) : newId;
47936
48945
  }
47937
48946
  function memberRootWay(qualifiedId, call) {
48947
+ return projectRootWay(qualifiedId, call) ?? `It belongs to the member project "${qualifiedId.slice(0, qualifiedId.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.`;
48948
+ }
48949
+ function projectRootWay(qualifiedId, call) {
47938
48950
  const alias = qualifiedId.slice(0, qualifiedId.indexOf("::"));
47939
- let folder;
47940
48951
  try {
47941
48952
  const node = graph().nodes.find((n) => n.namespace === alias || n.parent === "" && n.mountAlias === alias);
47942
- if (node) folder = path27.relative(getProjectRoot(), node.directory).split(path27.sep).join("/") || ".";
48953
+ if (node) {
48954
+ const folder = path27.relative(getProjectRoot(), node.directory).split(path27.sep).join("/") || ".";
48955
+ 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.`;
48956
+ }
47943
48957
  } catch {
47944
48958
  }
47945
- const where = folder ? `the member project "${alias}" at ${folder}` : `the member project "${alias}"`;
47946
- 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.`;
48959
+ const external = ownGet(projectConfigRepository.load()?.externals, alias);
48960
+ if (external !== void 0) {
48961
+ const producer = external?.project ?? alias;
48962
+ 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.`;
48963
+ }
48964
+ return null;
48965
+ }
48966
+ function foreignIdRefusal(id, code, doing, call) {
48967
+ if (!id.includes("::")) return null;
48968
+ const way = projectRootWay(id, call);
48969
+ return way === null ? null : `${code}: "${id}" lives in another project; ${doing} from that project's own root. ${way}`;
47947
48970
  }
47948
48971
  function renameType(typeId, newId, dryRun) {
47949
48972
  const all = loadTypeSpecs();
@@ -48219,6 +49242,8 @@ function publishedCarriers(type) {
48219
49242
  return mergeUses([...out.values()]);
48220
49243
  }
48221
49244
  function renameParam(componentId, methodName, param, newName, dryRun) {
49245
+ const foreign = foreignIdRefusal(componentId, "chained-component", "rename its parameter", `sdd_rename_param ${componentId.slice(componentId.lastIndexOf("::") + 2)} ${methodName} ${param} <new name>`);
49246
+ if (foreign !== null) throw new WaironError(foreign);
48222
49247
  const component = loadComponentSpec(componentId);
48223
49248
  if (!component) throw new WaironError(`component-missing: no component has the id "${componentId}".`);
48224
49249
  if (componentId.includes("::")) {
@@ -48557,8 +49582,8 @@ function walkForPackages(projectRoot, currentDir, depth, results) {
48557
49582
  }
48558
49583
  }
48559
49584
  function pathToId(relPath) {
48560
- const basename15 = path28.basename(relPath);
48561
- return basename15.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
49585
+ const basename16 = path28.basename(relPath);
49586
+ return basename16.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
48562
49587
  }
48563
49588
  function pathToName(relPath) {
48564
49589
  const id = pathToId(relPath);
@@ -49211,7 +50236,10 @@ function resolveLayer(delegate, implementers = false) {
49211
50236
  if (owners.size === 1 && owners.has(comp.id)) add2(file);
49212
50237
  }
49213
50238
  for (const type of types.filter((t) => t.componentClass === comp.id && t.subsystem === comp.subsystem)) {
49214
- for (const file of typeSourceFiles(type)) add2(file);
50239
+ for (const file of typeSourceFiles(type)) {
50240
+ const owners = claims.get(file);
50241
+ if (!owners || [...owners].every((o) => o === comp.id)) add2(file);
50242
+ }
49215
50243
  }
49216
50244
  const namesAny = compImpls.some((impl) => codeLocationsOf(impl).length > 0);
49217
50245
  if (ownedPaths.length === 0 && !namesAny) {
@@ -49252,6 +50280,14 @@ function resolveLayer(delegate, implementers = false) {
49252
50280
  updatedAt: comp.updatedAt
49253
50281
  });
49254
50282
  }
50283
+ const implementerRecords = agents.filter((a) => a.template === "implementer");
50284
+ const fencedBy = /* @__PURE__ */ new Map();
50285
+ for (const r of implementerRecords) for (const p of r.ownedPaths) fencedBy.set(p, (fencedBy.get(p) ?? 0) + 1);
50286
+ for (const r of implementerRecords) {
50287
+ if (!r.ownedPaths.some((p) => (fencedBy.get(p) ?? 0) > 1)) continue;
50288
+ r.ownedPaths = r.ownedPaths.filter((p) => (fencedBy.get(p) ?? 0) <= 1);
50289
+ r.writePaths = r.ownedPaths;
50290
+ }
49255
50291
  }
49256
50292
  const now = (/* @__PURE__ */ new Date()).toISOString();
49257
50293
  for (const dom of loadConfig().domains) {
@@ -49316,7 +50352,7 @@ function composeAgentBrief(agentId) {
49316
50352
  ${codeFence.map(shown2).join("\n")}
49317
50353
  ` : sharedPaths.length > 0 && namesAnyCode(record) ? "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";
49318
50354
  const sharedSection = sharedPaths.length > 0 ? `
49319
- 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:
50355
+ 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:
49320
50356
 
49321
50357
  ${sharedPaths.map(shown2).join("\n")}
49322
50358
  ` : "";
@@ -49326,6 +50362,9 @@ ${sharedPaths.map(shown2).join("\n")}
49326
50362
  ## Code write fence
49327
50363
 
49328
50364
  ${ownSection}${sharedSection}${rule}`;
50365
+ instructions = `${instructions.trimEnd()}
50366
+
50367
+ ${linkageSection(record)}`;
49329
50368
  }
49330
50369
  const guidance = loadAgentOverride(agentId);
49331
50370
  if (guidance !== null) {
@@ -49352,9 +50391,15 @@ ${typeMapping.map((line) => `- ${line}`).join("\n")}
49352
50391
  let readPaths = record.readPaths;
49353
50392
  if (externals.length > 0) {
49354
50393
  const bindings = bindingModulesOf2(record);
50394
+ const membersOnly = externals.every((e) => e.member);
50395
+ const pinsOnly = externals.every((e) => !e.member);
50396
+ 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)";
50397
+ const comparedWith = membersOnly ? "that table" : pinsOnly ? "the pin" : "the pin or the table";
49355
50398
  const bindingNote = bindings.length > 0 ? `
49356
- The binding modules these implementations name are the one place the code spells the producer's names \u2014 keep them exactly as the pin has them (\`validate\` compares them with it and reports a stale name, parameter or field as BINDING_DRIFT, with the rename to follow): ${bindings.map((b) => `\`${b}\``).join(", ")}.
49357
- ` : "\nWhen the code reaches a producer through a hand-written binding module (a typed binding to a native library, a client stub), name it on the implementation as `bindings` \u2014 code linkage, no re-lock \u2014 so `validate` compares it with the pin.\n";
50399
+ 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(", ")}.
50400
+ ` : `
50401
+ 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}.
50402
+ `;
49358
50403
  instructions = `${instructions.trimEnd()}
49359
50404
 
49360
50405
  ## Externals used
@@ -49511,6 +50556,23 @@ var SHARED_SETUP_FILES = [
49511
50556
  var SHARED_ROOT_FILES = ["index.ts", "index.js", "lib.rs", "main.rs", "mod.rs", "__init__.py"];
49512
50557
  var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".rs", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".java", ".kt", ".cs"]);
49513
50558
  var MAX_UNNAMED_SIBLINGS = 8;
50559
+ function linkageSection(record) {
50560
+ const comps = scopeComponents(record);
50561
+ const ids = new Set(comps.map((c) => c.id));
50562
+ const contractOwner = new Map(loadInterfaceSpecs().map((i) => [i.id, i.component]));
50563
+ const impls = loadImplementationSpecs().filter((impl) => ids.has(contractOwner.get(impl.contract) ?? ""));
50564
+ const parts = [];
50565
+ if (comps.some((c) => c.componentType === "Portal")) {
50566
+ 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");
50567
+ }
50568
+ const declared = impls.filter((impl) => (impl.injectedParams ?? []).length > 0);
50569
+ 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.` : "";
50570
+ parts.push(`## Code linkage you own
50571
+
50572
+ \`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}
50573
+ `);
50574
+ return parts.join("\n");
50575
+ }
49514
50576
  function namesAnyCode(record) {
49515
50577
  const ids = new Set(scopeComponents(record).map((c) => c.id));
49516
50578
  const contracts = new Set(loadInterfaceSpecs().filter((i) => ids.has(i.component)).map((i) => i.id));
@@ -49544,15 +50606,18 @@ function sharedFilesOf(record, fence) {
49544
50606
  const parts = folder === "." ? [] : folder.split("/");
49545
50607
  for (let i = 1; i <= parts.length; i++) chain.add(parts.slice(0, i).join("/"));
49546
50608
  }
49547
- const join31 = (dir, file) => dir === "." ? file : `${dir}/${file}`;
50609
+ const join32 = (dir, file) => dir === "." ? file : `${dir}/${file}`;
49548
50610
  for (const dir of [...chain].sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b))) {
49549
- for (const file of SHARED_SETUP_FILES) if (fs22.existsSync(path31.join(root, dir, file))) add2(join31(dir, file));
49550
- }
49551
- for (const dir of ownFolders) {
49552
- for (const file of SHARED_ROOT_FILES) if (fs22.existsSync(path31.join(root, dir, file))) add2(join31(dir, file));
50611
+ for (const file of SHARED_SETUP_FILES) if (fs22.existsSync(path31.join(root, dir, file))) add2(join32(dir, file));
49553
50612
  }
49554
50613
  const named2 = new Set(claims.keys());
49555
50614
  for (const type of types) for (const file of typeSourceFiles(type)) named2.add(file);
50615
+ for (const dir of ownFolders) {
50616
+ for (const file of SHARED_ROOT_FILES) {
50617
+ const rootFile = join32(dir, file);
50618
+ if (named2.has(rootFile) || fs22.existsSync(path31.join(root, dir, file))) add2(rootFile);
50619
+ }
50620
+ }
49556
50621
  let siblings = 0;
49557
50622
  for (const dir of ownFolders) {
49558
50623
  let entries = [];
@@ -49562,7 +50627,7 @@ function sharedFilesOf(record, fence) {
49562
50627
  continue;
49563
50628
  }
49564
50629
  for (const entry of entries.filter((e) => e.isFile()).sort((a, b) => a.name.localeCompare(b.name))) {
49565
- const file = join31(dir, entry.name);
50630
+ const file = join32(dir, entry.name);
49566
50631
  if (siblings >= MAX_UNNAMED_SIBLINGS) break;
49567
50632
  if (!SOURCE_EXTENSIONS2.has(path31.extname(entry.name)) || named2.has(file) || owned.has(file) || out.includes(file)) continue;
49568
50633
  if (/\.(test|spec)\.[a-z]+$/.test(entry.name)) continue;
@@ -49624,7 +50689,9 @@ function externalsUsedBy(record) {
49624
50689
  const node = externals.has(alias) ? void 0 : memberOf(alias);
49625
50690
  if (node) {
49626
50691
  const system = path31.relative(root, path31.join(node.directory, ".wai", "specs", ".index.yaml")).replace(/\\/g, "/");
49627
- out.push({ alias, names: [...names].sort(), pin: system, pinned: true, bindings: /* @__PURE__ */ new Map(), member: { id: node.id ?? node.namespace, system } });
50692
+ const publicName = memberPublicNames(path31.join(node.directory, ".wai", "specs", ".index.yaml"));
50693
+ const spelled = [...new Set([...names].map((n) => publicName.get(n) ?? n))].sort();
50694
+ out.push({ alias, names: spelled, pin: system, pinned: true, bindings: /* @__PURE__ */ new Map(), member: { id: node.id ?? node.namespace, system } });
49628
50695
  continue;
49629
50696
  }
49630
50697
  const pin2 = `.wai/externals/${alias}.yaml`;
@@ -49642,6 +50709,19 @@ function externalsUsedBy(record) {
49642
50709
  }
49643
50710
  return out;
49644
50711
  }
50712
+ function memberPublicNames(systemSpec) {
50713
+ const out = /* @__PURE__ */ new Map();
50714
+ try {
50715
+ const doc = readYamlFile(systemSpec);
50716
+ for (const entry of doc?.publicInterfaces ?? []) {
50717
+ const exported = entry.as ?? entry.name;
50718
+ const internal = entry.component ?? entry.typeDef;
50719
+ if (internal && exported && !out.has(internal)) out.set(internal, exported);
50720
+ }
50721
+ } catch {
50722
+ }
50723
+ return out;
50724
+ }
49645
50725
  function bindingModulesOf2(record) {
49646
50726
  const ids = new Set(implementedComponents(record).map((c) => c.id));
49647
50727
  const contracts = new Set(loadInterfaceSpecs().filter((i) => ids.has(i.component)).map((i) => i.id));
@@ -51337,6 +52417,19 @@ function readStampVersion(content) {
51337
52417
  // src/utils/ai-guide.ts
51338
52418
  var GUIDE_MARKER_START = "<!-- wairon-guide-start -->";
51339
52419
  var GUIDE_MARKER_END = "<!-- wairon-guide-end -->";
52420
+ var HUMAN_COMMANDS = `### What the human runs \u2014 recommend these, never run them
52421
+ 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:
52422
+ - \`wairon lock\` \u2014 approve the design (then commit \`.wai/lock.json\`); \`wairon lock-check\` is the CI merge gate on that approval.
52423
+ - \`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.
52424
+ - \`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.
52425
+ - \`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).
52426
+ - \`wairon network declare\` \u2014 declare this project's network boundary (your tool for it is \`sdd_set_network\`).
52427
+ - \`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\`.
52428
+ - \`wairon doctor --fix\` \u2014 rewrite deprecated forms; \`wairon generate\` \u2014 refresh the generated guides, skills and context.
52429
+ - \`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.
52430
+ - \`wairon status\` \u2014 readiness and approval; \`wairon diagram\` \u2014 architecture diagrams; \`wairon network flows\` \u2014 the allowed-flows matrix.`;
52431
+ var 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.
52432
+ - **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.`;
51340
52433
  var GLOBAL_GUIDE_BODY = `## wairon \u2014 Spec-Driven Development (optional)
51341
52434
 
51342
52435
  If \`.wai/specs/\` exists, the wairon SDD workflow is active; otherwise ignore it. wairon does not orchestrate sessions \u2014 it equips yours.
@@ -51353,7 +52446,12 @@ If \`.wai/specs/\` exists, the wairon SDD workflow is active; otherwise ignore i
51353
52446
  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.
51354
52447
  7. **Consistency**: Code must match L3 interfaces and L5 narratives exactly. If the spec is wrong, stop and update the spec.
51355
52448
  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.
51356
- 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.`;
52449
+ 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.
52450
+
52451
+ ### Code linkage facts
52452
+ ${LINKAGE_FACTS}
52453
+
52454
+ ${HUMAN_COMMANDS}`;
51357
52455
  var LOCAL_GUIDE_BODY = `## Wairon \u2014 Spec-Driven Development (you are operating inside it)
51358
52456
 
51359
52457
  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.
@@ -51369,14 +52467,17 @@ This project uses **wairon**. System specs live under \`.wai/specs/\` (L0 System
51369
52467
  - **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.
51370
52468
  - **\`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\`).
51371
52469
  - **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\`).
51372
- - **Do not run the \`wairon\` CLI**: Use \`sdd_validate_tree\` and \`sdd_get_status\` instead of CLI commands.
52470
+ - **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).
51373
52471
  - **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.
51374
52472
  - **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.
51375
52473
  - **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.
51376
52474
  - **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)\`.
52475
+ ${LINKAGE_FACTS}
51377
52476
  - **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).
51378
52477
  - **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.
51379
52478
 
52479
+ ${HUMAN_COMMANDS}
52480
+
51380
52481
  ### Rules (enforced by \`sdd_validate_tree\`)
51381
52482
  1. **Design before code**: Complete spec and pass validator before writing source code.
51382
52483
  2. **Human-in-the-loop**: Ask user approval for each spec layer before proceeding.
@@ -52001,6 +53102,15 @@ function getStatusReport(options = {}, decor) {
52001
53102
  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}`)));
52002
53103
  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))) {
52003
53104
  output += `${mark.structure(" ")}${mark.missing(`[Project] ${absent.alias ?? absent.key} (no project on disk)`)}${approvalTag(absent)}
53105
+ `;
53106
+ }
53107
+ const dialledOff = implementations.flatMap((impl) => {
53108
+ if (impl.conformance === "off") return [`${impl.id} (every method)`];
53109
+ const off = impl.methods.filter((m) => m.conformance === "off").map((m) => m.name);
53110
+ return off.length > 0 ? [`${impl.id} (${off.join(", ")})`] : [];
53111
+ });
53112
+ if (dialledOff.length > 0) {
53113
+ output += `${mark.layer("system", "Conformance off:")} ${dialledOff.join(", ")} \u2014 realization not checked (method, parameters, async, narrated calls); the doctrine checks still run
52004
53114
  `;
52005
53115
  }
52006
53116
  const own2 = options.approvals?.find((a) => a.key === "");
@@ -52218,7 +53328,14 @@ brief with \`sdd_get_agent_brief\`, and spawn a scoped subagent from it \u2014 i
52218
53328
  its \`sharedPaths\` as the files it may touch only for its own needs. Briefs are composed
52219
53329
  from the current spec tree on every call, so a re-lock never requires a session
52220
53330
  restart \u2014 fetch fresh per delegation. \`wairon-skill://sdd-delegate\` carries the
52221
- full flow.`;
53331
+ full flow.
53332
+
53333
+ ## Facts to state, not guess
53334
+
53335
+ \`injectedParams\` are never set at design time: the implementer sets them once its
53336
+ code takes one. The human runs the CLI: \`wairon lock\`; \`wairon validate --ci\`
53337
+ (fails on errors and non-draft warnings, never on notices); \`wairon surface export
53338
+ --format openapi --portal <id>\` for a Portal's OpenAPI.`;
52222
53339
  }
52223
53340
  function buildServerInstructions() {
52224
53341
  const profile = governingProfile();
@@ -52384,6 +53501,7 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
52384
53501
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
52385
53502
  var import_zod13 = require("zod");
52386
53503
  var import_types17 = require("@modelcontextprotocol/sdk/types.js");
53504
+ var fs33 = __toESM(require("fs"));
52387
53505
  var path51 = __toESM(require("path"));
52388
53506
  var crypto10 = __toESM(require("crypto"));
52389
53507
 
@@ -52840,6 +53958,13 @@ function writeSpec(restatement) {
52840
53958
  CREATE_TOOL[restatement.kind]
52841
53959
  );
52842
53960
  if (foreign !== void 0) throw new Error(foreign);
53961
+ const pathNames = pathBoundNames(restatement, id, parentRef);
53962
+ const notAnId = pathNames.find((n) => idPathProblem(n.value) !== null);
53963
+ if (notAnId) {
53964
+ throw new Error(
53965
+ `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.`
53966
+ );
53967
+ }
52843
53968
  const parent = parentRef ? loadSpec(parentRef.kind, parentRef.id) : null;
52844
53969
  const existing = loadSpec(restatement.kind, id);
52845
53970
  if (!existing && TRACED_KINDS.has(restatement.kind)) {
@@ -52902,6 +54027,24 @@ function writeSpec(restatement) {
52902
54027
  ...cascaded.length > 0 ? { cascaded } : {}
52903
54028
  };
52904
54029
  }
54030
+ var ID_SEGMENT = /^[A-Za-z0-9_-]+$/;
54031
+ function idPathProblem(value) {
54032
+ const segments = value.split("::");
54033
+ const named2 = value.startsWith("::") ? segments.slice(1) : segments;
54034
+ const bad = named2.find((s) => !ID_SEGMENT.test(s));
54035
+ if (bad === void 0) return null;
54036
+ if (/[\\/]/.test(bad)) return `"${bad}" holds a path separator`;
54037
+ if (bad.includes(".")) return `"${bad}" holds a dot`;
54038
+ return bad === "" ? "it has an empty segment" : `"${bad}" holds a character outside letters, digits, "-" and "_"`;
54039
+ }
54040
+ function pathBoundNames(restatement, id, parentRef) {
54041
+ const out = [];
54042
+ if (restatement.kind !== "system" && typeof id === "string") out.push({ what: `${restatement.kind} id`, value: id });
54043
+ if (parentRef && typeof parentRef.id === "string" && parentRef.kind !== "system") out.push({ what: `${parentRef.kind} it is written under`, value: parentRef.id });
54044
+ const group = restatement.spec.group;
54045
+ if (restatement.kind === "type" && typeof group === "string" && group !== "") out.push({ what: "type group", value: group });
54046
+ return out;
54047
+ }
52905
54048
  var CREATE_TOOL = {
52906
54049
  system: "sdd_initialize_system",
52907
54050
  subsystem: "sdd_add_subsystem",
@@ -53044,9 +54187,9 @@ function referencedRefusal(kind, id, references) {
53044
54187
  ${references.map((r) => `- ${r.kind} "${r.id}" (${r.position}) -> ${r.target.kind} "${r.target.id}"`).join("\n")}
53045
54188
  Edit those specs first, or pass force: true to delete anyway and leave them dangling. Nothing was deleted; dryRun lists the whole plan.`;
53046
54189
  }
53047
- function updateSpecGated(kind, id, delta, dryRun) {
54190
+ function updateSpecGated(kind, id, delta, dryRun, tool = "sdd_update_spec") {
53048
54191
  const bound2 = candidateOptions();
53049
- const foreign = crossProjectWriteRefusal([{ kind, id }], "sdd_update_spec");
54192
+ const foreign = crossProjectWriteRefusal([{ kind, id }], tool);
53050
54193
  if (foreign !== void 0) throw new Error(foreign);
53051
54194
  const testRoots = bound2.rules?.conformance?.testRoots ?? [];
53052
54195
  const stored = loadSpec(kind, id);
@@ -53140,30 +54283,69 @@ function moveMethods2(from, to, methods, dryRun) {
53140
54283
  if (foreign !== void 0) throw new Error(foreign);
53141
54284
  return moveMethods(from, to, methods, methodMoveGate(from), dryRun);
53142
54285
  }
53143
- function moveSpec2(kind, id, subsystem, dryRun) {
54286
+ function moveSpec2(kind, id, subsystem, dryRun, together) {
54287
+ const ids = [.../* @__PURE__ */ new Set([id, ...together ?? []])];
53144
54288
  const foreign = crossProjectWriteRefusal(
53145
- [{ kind, id }, ...subsystem ? [{ kind: "subsystem", id: subsystem }] : []],
54289
+ [...ids.map((sid) => ({ kind, id: sid })), ...subsystem ? [{ kind: "subsystem", id: subsystem }] : []],
53146
54290
  "sdd_move_spec"
53147
54291
  );
53148
54292
  if (foreign !== void 0) throw new Error(foreign);
53149
- if (kind !== "component") return moveSpec(kind, id, subsystem, dryRun);
54293
+ if (kind !== "component") {
54294
+ const plans2 = ids.map((sid) => moveSpec(kind, sid, subsystem, true));
54295
+ if (dryRun) return foldMoves(plans2, true);
54296
+ return foldMoves(ids.map((sid) => moveSpec(kind, sid, subsystem, false)), false);
54297
+ }
53150
54298
  const bound2 = candidateOptions();
53151
- const plan6 = moveSpec(kind, id, subsystem, true);
54299
+ const owners = ids.filter((sid) => !ids.some((other) => other !== sid && ownsTransitively(other, sid)));
54300
+ const plans = owners.map((sid) => moveSpec(kind, sid, subsystem, true));
54301
+ const plan6 = foldMoves(plans, true);
53152
54302
  const moving = plan6.moved.filter((ref) => ref.kind === "component").map((ref) => loadSpec("component", ref.id)).filter((spec) => spec !== null).map((spec) => ({ ...spec, subsystem: plan6.to }));
53153
54303
  const [first, ...rest] = moving;
53154
54304
  const verdict = first ? introducedFindings("component", first, bound2, rest) : { errors: [], warnings: [], notices: [] };
53155
54305
  if (verdict.errors.length > 0) {
54306
+ const left = collaboratorsLeftBehind(moving.map((c) => c.id), plans.map((p) => p.from));
54307
+ const named2 = ids.length > 1 ? `components ${ids.map((sid) => `"${sid}"`).join(", ")}` : `component "${id}"`;
53156
54308
  throw new Error(
53157
- `Refused: moving component "${id}" to subsystem "${plan6.to}" would introduce ${verdict.errors.length === 1 ? "an error" : `${verdict.errors.length} errors`} validate reports \u2014 a forbidden edge between two blocks, or an edge into another subsystem whose target is not a Portal:
54309
+ `Refused: moving ${named2} to subsystem "${plan6.to}" would introduce ${verdict.errors.length === 1 ? "an error" : `${verdict.errors.length} errors`} validate reports \u2014 a forbidden edge between two blocks, or an edge into another subsystem whose target is not a Portal:
53158
54310
  ${verdict.errors.map((e) => `- ${e.code}: ${e.message}`).join("\n")}
53159
54311
 
53160
- Nothing was written. Route each such dependency through a client Adapter calling the other subsystem's Portal, or move its collaborators with it.`
54312
+ 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).` : ".")
53161
54313
  );
53162
54314
  }
53163
54315
  const notices = noticesFrom(verdict);
53164
- const report3 = dryRun ? plan6 : moveSpec(kind, id, subsystem, false);
54316
+ const report3 = dryRun ? plan6 : foldMoves(owners.map((sid) => moveSpec(kind, sid, subsystem, false)), false);
53165
54317
  return { ...report3, notices: [...report3.notices, ...notices] };
53166
54318
  }
54319
+ function ownsTransitively(owner, member) {
54320
+ const seen = /* @__PURE__ */ new Set();
54321
+ const visit = (cid) => {
54322
+ if (seen.has(cid)) return false;
54323
+ seen.add(cid);
54324
+ const owns = loadSpec("component", cid)?.owns ?? [];
54325
+ return owns.includes(member) || owns.some(visit);
54326
+ };
54327
+ return visit(owner);
54328
+ }
54329
+ function collaboratorsLeftBehind(moved, from) {
54330
+ const movedSet = new Set(moved);
54331
+ const sources = new Set(from);
54332
+ const all = scanAllSpecs({ memberDepth: 0 }).components;
54333
+ const components = all.filter((c) => sources.has(c.subsystem) && !movedSet.has(c.id));
54334
+ const movedSpecs = all.filter((c) => movedSet.has(c.id));
54335
+ const linked = components.filter((c) => (c.dependsOn ?? []).some((d) => movedSet.has(d)) || movedSpecs.some((m) => (m.dependsOn ?? []).includes(c.id)));
54336
+ const owned = new Set(components.flatMap((c) => c.owns ?? []));
54337
+ return [...new Set(linked.map((c) => owned.has(c.id) ? components.find((o) => (o.owns ?? []).includes(c.id))?.id ?? c.id : c.id))].sort();
54338
+ }
54339
+ function foldMoves(moves, dryRun) {
54340
+ const [first] = moves;
54341
+ return {
54342
+ ...first,
54343
+ moved: moves.flatMap((m) => m.moved),
54344
+ rewritten: [...new Set(moves.flatMap((m) => m.rewritten))],
54345
+ notices: moves.flatMap((m) => m.notices),
54346
+ ...dryRun ? { dryRun: true } : {}
54347
+ };
54348
+ }
53167
54349
 
53168
54350
  // src/mcp/build.ts
53169
54351
  var fs26 = __toESM(require("fs"));
@@ -53240,7 +54422,7 @@ function buildServerInstructions3() {
53240
54422
  }
53241
54423
 
53242
54424
  // src/network/adapters/files.ts
53243
- var import_yaml15 = require("yaml");
54425
+ var import_yaml16 = require("yaml");
53244
54426
 
53245
54427
  // src/network/types.ts
53246
54428
  function workload(party) {
@@ -55077,7 +56259,10 @@ function planAttach(family, bound2, request) {
55077
56259
  const config = configAt3(node.directory) ?? {};
55078
56260
  const held = ownGet(config.members, alias);
55079
56261
  const heldPath = held !== void 0 ? memberLocationOf(held) : void 0;
55080
- if (dir !== null && heldPath !== void 0 && path45.resolve(node.directory, heldPath) === dir) return plan6;
56262
+ if (dir !== null && heldPath !== void 0 && path45.resolve(node.directory, heldPath) === dir) {
56263
+ plan6.notes.push(`"${alias}" is already attached: ${label4(bound2)}'s members already declare ${alias} \u2192 ${heldPath}. Nothing to do.`);
56264
+ return plan6;
56265
+ }
55081
56266
  if (!EXTERNAL_ALIAS_RE.test(alias)) refuse(plan6, "alias-invalid", bound2, `"${alias}" is no alias: ${aliasGrammarProblem(alias)}`);
55082
56267
  else if (ownGet(config.members, alias) !== void 0 || ownGet(config.externals, alias) !== void 0) refuse(plan6, "alias-taken", bound2, `${label4(bound2)} already declares "${alias}"`);
55083
56268
  if (dir !== null) {
@@ -57775,6 +58960,16 @@ var getSpecOutput = {
57775
58960
  }).optional().describe(
57776
58961
  "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."
57777
58962
  ),
58963
+ pinnedSnapshot: import_zod13.z.object({
58964
+ alias: import_zod13.z.string().describe("The external's alias the read went through."),
58965
+ project: import_zod13.z.string().describe("The producer project the alias names."),
58966
+ file: import_zod13.z.string().describe("The pinned snapshot file the entry was read from, project-relative."),
58967
+ stateId: import_zod13.z.string().optional().describe("The producer's StateId the pin was taken at, when recorded."),
58968
+ readOnly: import_zod13.z.literal(true).describe("Always true: a pinned entry is the pin's record, written from the producer's own root."),
58969
+ note: import_zod13.z.string().describe("One sentence saying where the entry comes from and where it is written.")
58970
+ }).optional().describe(
58971
+ "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."
58972
+ ),
57778
58973
  variantGuidance: import_zod13.z.object({
57779
58974
  variant: import_zod13.z.string().describe("The variant id the component declares."),
57780
58975
  base: import_zod13.z.string().describe("The core stereotype it specializes."),
@@ -57973,14 +59168,44 @@ function withArgumentNames(description, inputSchema) {
57973
59168
  if (names.length === 0) return description;
57974
59169
  return `${description.trimEnd()} Arguments: ${names.join(", ")}.`;
57975
59170
  }
59171
+ var sessionFolder = null;
59172
+ function sameFolder(a, b) {
59173
+ const norm = (p) => {
59174
+ const resolved = path51.resolve(p);
59175
+ return process.platform === "win32" || process.platform === "darwin" ? resolved.toLowerCase() : resolved;
59176
+ };
59177
+ return norm(a) === norm(b);
59178
+ }
59179
+ function initializeFromElsewhereRefusal() {
59180
+ const existing = loadSystemSpec();
59181
+ if (sessionFolder === null || existing === null) return null;
59182
+ const root = getProjectRoot();
59183
+ if (sameFolder(sessionFolder, root)) return null;
59184
+ const rel2 = path51.relative(root, sessionFolder).split(path51.sep).join("/");
59185
+ const inside = rel2 !== "" && !rel2.startsWith("..") && !path51.isAbsolute(rel2);
59186
+ const alias = path51.basename(sessionFolder).toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "member";
59187
+ 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}.`;
59188
+ }
59189
+ function underTreeLock(handler) {
59190
+ try {
59191
+ lockTree2();
59192
+ } catch (e) {
59193
+ return errText(e instanceof Error ? e.message : String(e));
59194
+ }
59195
+ try {
59196
+ return handler();
59197
+ } finally {
59198
+ unlockTree2();
59199
+ }
59200
+ }
57976
59201
  function reg(server, name, config, cb) {
57977
59202
  const guarded2 = (args) => {
57978
59203
  const freshness = assessBuildFreshness(serverBuildStamps.get(server) ?? null);
57979
59204
  if (freshness.writesRefused && SPEC_WRITE_TOOLS.has(name)) {
57980
59205
  return errText(`${name} refused \u2014 ${freshness.reason}`);
57981
59206
  }
57982
- const answered = cb(args);
57983
- const result = freshness.state === "fresh" ? answered : markStale(answered, freshness, name === "sdd_get_status" ? "lead" : "trail");
59207
+ const answered2 = SPEC_WRITE_TOOLS.has(name) ? underTreeLock(() => cb(args)) : cb(args);
59208
+ const result = freshness.state === "fresh" ? answered2 : markStale(answered2, freshness, name === "sdd_get_status" ? "lead" : "trail");
57984
59209
  if (result.isError !== true && SPEC_WRITE_TOOLS.has(name)) listChangedEmitters.get(server)?.();
57985
59210
  return result;
57986
59211
  };
@@ -58126,12 +59351,14 @@ function createBareMcpServer(options = {}) {
58126
59351
  server,
58127
59352
  "sdd_initialize_system",
58128
59353
  {
58129
- 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.",
59354
+ 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.",
58130
59355
  inputSchema: systemInput,
58131
59356
  outputSchema: specWriteReceiptOutput
58132
59357
  },
58133
59358
  ({ name, vision, boundaries, globalRequirements, targetLanguage }) => {
58134
59359
  try {
59360
+ const elsewhere = initializeFromElsewhereRefusal();
59361
+ if (elsewhere !== null) return errText(elsewhere);
58135
59362
  const restatement = {
58136
59363
  kind: "system",
58137
59364
  spec: {
@@ -58237,7 +59464,7 @@ function createBareMcpServer(options = {}) {
58237
59464
  if (!sub) return errText(`Subsystem "${subsystem}" does not exist.`);
58238
59465
  const delta = publicInterfacesReplacement(subsystem, sub.publicInterfaces ?? [], publicInterfaces);
58239
59466
  if (typeof delta === "string") return errText(delta);
58240
- const report3 = updateSpecGated("subsystem", subsystem, delta);
59467
+ const report3 = updateSpecGated("subsystem", subsystem, delta, void 0, "sdd_set_public_interfaces");
58241
59468
  return structured(
58242
59469
  `Updated public interfaces for subsystem "${subsystem}" (${publicInterfaces.length} ${publicInterfaces.length === 1 ? "entry" : "entries"}).
58243
59470
  ${renderChangeReport(report3)}`,
@@ -58263,9 +59490,15 @@ ${renderChangeReport(report3)}`,
58263
59490
  },
58264
59491
  ({ alias, source, description, as }) => {
58265
59492
  try {
59493
+ const root = getProjectRoot();
59494
+ const dir = typeof source === "string" && !/^[a-z][a-z0-9+.-]*:|^git@|#/i.test(source.trim()) ? path51.resolve(root, source.trim()) : null;
59495
+ const relToRoot = dir === null ? null : path51.relative(root, dir);
59496
+ const outside = dir !== null && relToRoot !== null && (relToRoot.startsWith("..") || path51.isAbsolute(relToRoot));
59497
+ const createdOutside = outside && !fs33.existsSync(dir);
58266
59498
  const creation = createMember(alias, source, description, as);
58267
59499
  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.`;
58268
- return structured(`Added the ${creation.as} "${alias}" at ${source}${creation.commit ? ` (pinned at ${creation.commit})` : ""}, declared in project.yaml \`members\`.${next}`, creation);
59500
+ const created = createdOutside ? `Created a new folder OUTSIDE this project, at ${dir} (beside ${root}). ` : "";
59501
+ return structured(`${created}Added the ${creation.as} "${alias}" at ${source}${creation.commit ? ` (pinned at ${creation.commit})` : ""}, declared in project.yaml \`members\`.${next}`, creation);
58269
59502
  } catch (e) {
58270
59503
  return errText(String(e));
58271
59504
  }
@@ -58474,12 +59707,12 @@ ${renderChangeReport(report3)}`,
58474
59707
  server,
58475
59708
  "sdd_rename_method",
58476
59709
  {
58477
- 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.",
59710
+ 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.",
58478
59711
  inputSchema: {
58479
59712
  id: import_zod13.z.string().describe("The component whose method is renamed (namespaced if needed)"),
58480
59713
  method: import_zod13.z.string().describe("The method name as it stands"),
58481
59714
  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)"),
58482
- 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"),
59715
+ 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"),
58483
59716
  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"),
58484
59717
  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")
58485
59718
  }
@@ -58621,17 +59854,18 @@ ${renderChangeReport(report3)}`,
58621
59854
  server,
58622
59855
  "sdd_move_spec",
58623
59856
  {
58624
- description: "Move a component or a type to another subsystem of the bound project with everything that follows it. A component takes the members it owns, its contracts and their implementations (their files move to the new subsystem's folder in the nested layout); ids never change, so references by id stay, and what names the OLD subsystem follows: its export entries and lifecycle entrypoints for the moved components, and an L0 re-export from it. A type takes its file, its L1 export entries and every reference qualified by the old subsystem (`old::type` becomes `new::type`); omit subsystem to make a type system-level. Gated like every write: a move that would create a forbidden doctrine edge, or a cross-subsystem edge into a non-Portal, is refused with the rule's own words, and a curable boundary finding (a direct cross-subsystem edge into a Portal, which a client Adapter or a trustedLink would license) is named among the notices. Refuses, writing nothing: a kind other than component or type, a spec that does not exist or lives in another project, a subsystem the bound tree does not have or the one it already lives in, a component a pattern owns (move its owner), a type id the target already holds. With dryRun it answers the plan and the notices, and writes nothing.",
59857
+ 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.",
58625
59858
  inputSchema: {
58626
59859
  kind: import_zod13.z.enum(["component", "type"]).describe("component or type"),
58627
59860
  id: import_zod13.z.string().describe("The spec to move (a type by its bare id, or as <subsystem>::<id>)"),
58628
59861
  subsystem: import_zod13.z.string().optional().describe("The subsystem it moves to; omitted for a type made system-level"),
58629
- dryRun: import_zod13.z.boolean().optional().describe("Answer what the move would do \u2014 refused exactly as the move would be \u2014 and write nothing")
59862
+ 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"),
59863
+ 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")
58630
59864
  }
58631
59865
  },
58632
- ({ kind, id, subsystem, dryRun }) => {
59866
+ ({ kind, id, subsystem, dryRun, together }) => {
58633
59867
  try {
58634
- return json(moveSpec2(kind, id, subsystem, dryRun));
59868
+ return json(moveSpec2(kind, id, subsystem, dryRun, together));
58635
59869
  } catch (e) {
58636
59870
  return errText(String(e));
58637
59871
  }
@@ -58885,7 +60119,7 @@ ${renderChangeReport(report3)}`,
58885
60119
  methods.push({ name: e.method, endpoint: parsed.data });
58886
60120
  bound2.push(`${e.method}\u2192${transport}`);
58887
60121
  }
58888
- const report3 = updateSpecGated("interface", interfaceId, { methods }, dryRun);
60122
+ const report3 = updateSpecGated("interface", interfaceId, { methods }, dryRun, "sdd_set_endpoints");
58889
60123
  return structured(
58890
60124
  `${dryRun ? "Would bind" : "Bound"} ${bound2.length} endpoint(s) on "${interfaceId}": ${bound2.join(", ")}.
58891
60125
  ${renderChangeReport(report3)}`,
@@ -58957,7 +60191,7 @@ ${renderChangeReport(report3)}`,
58957
60191
  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)"),
58958
60192
  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"),
58959
60193
  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.`),
58960
- injectedParams: import_zod13.z.array(import_zod13.z.string()).optional().describe("The parameter names this realization takes BEFORE the ones its contract declares \u2014 a config object, a data root, the transport handles a portal is handed. Supplied by whatever wires the component up, never by the caller the contract describes, which is why they belong here and not to the contract: another realization may hold them as fields instead. Declared rather than guessed, because a leading parameter the contract does not name cannot be told from one it named under a different name (`seed(config)` realized as `bootstrapInstance(cfg)`); a leading parameter this list does not name is reported (UNDECLARED_PARAM). A name matches with or without a leading `_` (`ctx` names `_ctx`). A LEADING run of these names is dropped, and so is a TRAILING run of names the contract does not declare (a handler's `res` after the contract's own parameters); one appearing in the middle of the contract's parameters is an argument of the caller's list, not wiring"),
60194
+ 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"),
58961
60195
  detail: detailEnum.optional().describe("Spec-level narrative detail default for all methods"),
58962
60196
  conformance: conformanceEnum.optional().describe("Spec-level conformance tier default: declared | anchored | off (omitted = stereotype default: Portal \u2192 anchored, else declared)"),
58963
60197
  methods: import_zod13.z.array(import_zod13.z.object(implMethodShape).strict()).optional().describe("Method implementations containing L5 narratives"),
@@ -58968,7 +60202,7 @@ ${renderChangeReport(report3)}`,
58968
60202
  server,
58969
60203
  "sdd_write_narrative",
58970
60204
  {
58971
- description: "Write L4 Concrete Implementation spec containing L5 method narratives. Narratives are a FLAT ordered step list; flow steps (branch/switch/loop/try/parallel/jump/return/throw) jump by step number \u2014 blocks are just skipped regions. Steps may declare a `label` anchor, and every jump field has a *Label twin (toLabel, onTrueLabel, endLabel, \u2026) resolved to step numbers at write time \u2014 prefer labels over hand-counted numbers; an unresolvable label rejects the write. Detail dial per method: full (narrative required) | calls-only (call choreography suffices) | intent (prose instead of steps); omitted = stereotype default (Portal/Observer/Adapter: calls-only, Store/Index/Registry: intent, else full). A method whose narrative shows no steps declares the calls it makes in `calls` (one \"<component>.<method>\" each): the reachability walk takes exactly those edges and no others, so an intent-level method that reaches a collaborator must name it or that collaborator is reported unused. Conformance dial per method or spec: declared | anchored | off \u2014 how strictly structural conformance requires contract methods to be realized in their source file (omitted = Portal: anchored, else declared). A method whose body lives in its own file names it in the method's sourcePath; the implementation's sourcePath is the default for every method that names none. Parameters the realization takes BEFORE its contract's own \u2014 a config object, a data root, a portal's transport handles \u2014 or after them (a handler's response handle), are wiring, and are declared once for the spec in injectedParams (matched with or without a leading `_`); a leading or trailing parameter it does not name is reported against the contract (UNDECLARED_PARAM). Re-authoring an existing id REPLACES the method list: a method left out of the input is REMOVED together with its narrative (and reported); spec-level lint/ext are carried forward, and the stored status is kept unless this input states a higher one. The answer carries a write receipt as structured content beside the sentence \u2014 the status written, whether a spec already held the id, and the notices a restatement raised, each as its own entry.",
60205
+ 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.",
58972
60206
  inputSchema: implInput,
58973
60207
  outputSchema: specWriteReceiptOutput
58974
60208
  },
@@ -59152,7 +60386,7 @@ ${renderChangeReport(report3)}`,
59152
60386
  server,
59153
60387
  "sdd_get_spec",
59154
60388
  {
59155
- 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.`,
60389
+ 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.`,
59156
60390
  inputSchema: {
59157
60391
  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"),
59158
60392
  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")'),
@@ -59162,11 +60396,34 @@ ${renderChangeReport(report3)}`,
59162
60396
  },
59163
60397
  ({ kind: askedKind, id, methods }) => {
59164
60398
  try {
60399
+ const pinnedAnswer = (asked) => {
60400
+ if (!id.includes("::") || methods !== void 0) return null;
60401
+ const read2 = readPinnedSpec(asked, id);
60402
+ if (!read2) return null;
60403
+ const marker = {
60404
+ alias: read2.alias,
60405
+ project: read2.project,
60406
+ file: read2.file,
60407
+ ...read2.stateId !== void 0 ? { stateId: read2.stateId } : {},
60408
+ readOnly: true,
60409
+ 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.`
60410
+ };
60411
+ return structured(JSON.stringify({ ...read2.spec, pinnedSnapshot: marker }, null, 2), {
60412
+ kind: read2.kind === "type" ? "type" : asked ?? "interface",
60413
+ id,
60414
+ spec: read2.spec,
60415
+ pinnedSnapshot: marker
60416
+ });
60417
+ };
59165
60418
  let kind;
59166
60419
  if (askedKind) {
59167
60420
  kind = askedKind;
59168
60421
  } else {
59169
60422
  const holders = specKindsHolding(id);
60423
+ if (holders.length === 0) {
60424
+ const pinned = pinnedAnswer(void 0);
60425
+ if (pinned) return pinned;
60426
+ }
59170
60427
  if (holders.length !== 1) {
59171
60428
  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.`);
59172
60429
  }
@@ -59194,7 +60451,7 @@ ${renderChangeReport(report3)}`,
59194
60451
  result = loadTypeSpec(id);
59195
60452
  break;
59196
60453
  }
59197
- if (!result) return errText(`Spec of kind "${kind}" with ID "${id}" does not exist.`);
60454
+ if (!result) return pinnedAnswer(kind) ?? errText(`Spec of kind "${kind}" with ID "${id}" does not exist.`);
59198
60455
  let resolvedSignatures = [];
59199
60456
  if (kind === "interface") {
59200
60457
  const answer = storedContractAnswer(result);
@@ -60005,6 +61262,7 @@ function loadProjectConfig3() {
60005
61262
  localApprover,
60006
61263
  localGuideFilePath,
60007
61264
  localTypesOf,
61265
+ lockTree,
60008
61266
  logger,
60009
61267
  makeScopeFilter,
60010
61268
  markSelectionsBundled,
@@ -60014,6 +61272,7 @@ function loadProjectConfig3() {
60014
61272
  memberDeclarationOf,
60015
61273
  memberDigest,
60016
61274
  memberLocationOf,
61275
+ memberRevisions,
60017
61276
  memberTypesOf,
60018
61277
  methodCasingFor,
60019
61278
  methodGenericParameters,
@@ -60062,6 +61321,7 @@ function loadProjectConfig3() {
60062
61321
  readJsonFile,
60063
61322
  readLockRecord,
60064
61323
  readLockState,
61324
+ readPinnedSpec,
60065
61325
  readRetiredReachForms,
60066
61326
  readStampVersion,
60067
61327
  readYamlFile,
@@ -60170,6 +61430,7 @@ function loadProjectConfig3() {
60170
61430
  typeSourceFiles,
60171
61431
  typeSpellingFacts,
60172
61432
  uninstallPack,
61433
+ unlockTree,
60173
61434
  updateMember,
60174
61435
  updateSpec,
60175
61436
  upsertPackSelection,