@wairon/cli 5.1.1-dev.75 → 5.1.1-dev.77

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
@@ -1672,15 +1672,15 @@ var require_node = __commonJS({
1672
1672
  var slzh = function(d, b) {
1673
1673
  return b + 30 + b2(d, b + 26) + b2(d, b + 28);
1674
1674
  };
1675
- var zh = function(d, b, z11) {
1675
+ var zh = function(d, b, z12) {
1676
1676
  var fnl = b2(d, b + 28), efl = b2(d, b + 30), fn = strFromU8(d.subarray(b + 46, b + 46 + fnl), !(b2(d, b + 8) & 2048)), es = b + 46 + fnl;
1677
- var _a2 = z64hs(d, es, efl, z11, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2];
1677
+ var _a2 = z64hs(d, es, efl, z12, b4(d, b + 20), b4(d, b + 24), b4(d, b + 42)), sc = _a2[0], su = _a2[1], off = _a2[2];
1678
1678
  return [b2(d, b + 10), sc, su, fn, es + efl + b2(d, b + 32), off];
1679
1679
  };
1680
- var z64hs = function(d, b, l, z11, sc, su, off) {
1680
+ var z64hs = function(d, b, l, z12, sc, su, off) {
1681
1681
  var nsc = sc == 4294967295, nsu = su == 4294967295, noff = off == 4294967295, e = b + l;
1682
1682
  var nf = nsc + nsu + noff;
1683
- if (z11 && nf) {
1683
+ if (z12 && nf) {
1684
1684
  for (; b + 4 < e; b += 4 + b2(d, b + 2)) {
1685
1685
  if (b2(d, b) == 1) {
1686
1686
  return [
@@ -1691,7 +1691,7 @@ var require_node = __commonJS({
1691
1691
  ];
1692
1692
  }
1693
1693
  }
1694
- if (z11 < 2)
1694
+ if (z12 < 2)
1695
1695
  err(13);
1696
1696
  }
1697
1697
  return [sc, su, off, 0];
@@ -2300,18 +2300,18 @@ var require_node = __commonJS({
2300
2300
  if (lft) {
2301
2301
  var c = lft;
2302
2302
  var o = b4(data, e + 16);
2303
- var z11 = b4(data, e - 20) == 117853008;
2304
- if (z11) {
2303
+ var z12 = b4(data, e - 20) == 117853008;
2304
+ if (z12) {
2305
2305
  var ze = b4(data, e - 12);
2306
- z11 = b4(data, ze) == 101075792;
2307
- if (z11) {
2306
+ z12 = b4(data, ze) == 101075792;
2307
+ if (z12) {
2308
2308
  c = lft = b4(data, ze + 32);
2309
2309
  o = b4(data, ze + 48);
2310
2310
  }
2311
2311
  }
2312
2312
  var fltr = opts && opts.filter;
2313
2313
  var _loop_3 = function(i3) {
2314
- var _a2 = zh(data, o, z11), c_1 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
2314
+ var _a2 = zh(data, o, z12), c_1 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
2315
2315
  o = no;
2316
2316
  var cbl = function(e2, d) {
2317
2317
  if (e2) {
@@ -2366,18 +2366,18 @@ var require_node = __commonJS({
2366
2366
  if (!c)
2367
2367
  return {};
2368
2368
  var o = b4(data, e + 16);
2369
- var z11 = b4(data, e - 20) == 117853008;
2370
- if (z11) {
2369
+ var z12 = b4(data, e - 20) == 117853008;
2370
+ if (z12) {
2371
2371
  var ze = b4(data, e - 12);
2372
- z11 = b4(data, ze) == 101075792;
2373
- if (z11) {
2372
+ z12 = b4(data, ze) == 101075792;
2373
+ if (z12) {
2374
2374
  c = b4(data, ze + 32);
2375
2375
  o = b4(data, ze + 48);
2376
2376
  }
2377
2377
  }
2378
2378
  var fltr = opts && opts.filter;
2379
2379
  for (var i2 = 0; i2 < c; ++i2) {
2380
- var _a2 = zh(data, o, z11), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
2380
+ var _a2 = zh(data, o, z12), c_2 = _a2[0], sc = _a2[1], su = _a2[2], fn = _a2[3], no = _a2[4], off = _a2[5], b = slzh(data, off);
2381
2381
  o = no;
2382
2382
  if (!fltr || fltr({
2383
2383
  name: fn,
@@ -2497,7 +2497,7 @@ var require_dist = __commonJS({
2497
2497
  scaffoldPack: () => scaffoldPack2
2498
2498
  });
2499
2499
  module2.exports = __toCommonJS2(src_exports2);
2500
- var path29 = __toESM2(require("path"));
2500
+ var path210 = __toESM2(require("path"));
2501
2501
  var import_js_yaml3 = require("js-yaml");
2502
2502
  var import_js_yaml = require("js-yaml");
2503
2503
  var import_crypto = require("crypto");
@@ -2510,8 +2510,8 @@ var require_dist = __commonJS({
2510
2510
  maxCompressionRatio: 100,
2511
2511
  maxDepth: 16
2512
2512
  };
2513
- function parseManifest(text) {
2514
- const raw = (0, import_js_yaml.load)(text);
2513
+ function parseManifest(text2) {
2514
+ const raw = (0, import_js_yaml.load)(text2);
2515
2515
  const manifest = validateEnvelope(raw);
2516
2516
  if (Math.trunc(manifest.formatVersion) > SUPPORTED_FORMAT_VERSION) {
2517
2517
  throw new Error("unsupported pack-archive formatVersion (newer than this wairon understands)");
@@ -2689,8 +2689,8 @@ var require_dist = __commonJS({
2689
2689
  var DERIVED_ROOTS = ["generated", "docs"];
2690
2690
  var EXECUTABLE_EXT_RE = /\.(c|m)?js$/i;
2691
2691
  var WAI_SEGMENT = ".wai";
2692
- function parseTreeManifest(text) {
2693
- const raw = (0, import_js_yaml2.load)(text);
2692
+ function parseTreeManifest(text2) {
2693
+ const raw = (0, import_js_yaml2.load)(text2);
2694
2694
  const manifest = validateEnvelope2(raw);
2695
2695
  if (Math.trunc(manifest.formatVersion) > SUPPORTED_FORMAT_VERSION2) {
2696
2696
  throw new Error("unsupported tree-archive formatVersion (newer than this wairon understands)");
@@ -3060,8 +3060,8 @@ var require_dist = __commonJS({
3060
3060
  function slug(name) {
3061
3061
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3062
3062
  }
3063
- var fs18 = __toESM2(require("fs"));
3064
- var path30 = __toESM2(require("path"));
3063
+ var fs20 = __toESM2(require("fs"));
3064
+ var path31 = __toESM2(require("path"));
3065
3065
  var import_fflate = require_node();
3066
3066
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".hg", ".svn"]);
3067
3067
  function listEntries(archive) {
@@ -3099,9 +3099,9 @@ var require_dist = __commonJS({
3099
3099
  }
3100
3100
  function writeTree(destDir, files) {
3101
3101
  for (const file of files) {
3102
- const absolute = path30.join(destDir, file.path);
3103
- fs18.mkdirSync(path30.dirname(absolute), { recursive: true });
3104
- fs18.writeFileSync(absolute, file.contents);
3102
+ const absolute = path31.join(destDir, file.path);
3103
+ fs20.mkdirSync(path31.dirname(absolute), { recursive: true });
3104
+ fs20.writeFileSync(absolute, file.contents);
3105
3105
  }
3106
3106
  }
3107
3107
  function classifyKind(name, symlinks) {
@@ -3110,14 +3110,14 @@ var require_dist = __commonJS({
3110
3110
  return "file";
3111
3111
  }
3112
3112
  function walkPackDir(root, current2, out) {
3113
- for (const entry of fs18.readdirSync(current2, { withFileTypes: true })) {
3113
+ for (const entry of fs20.readdirSync(current2, { withFileTypes: true })) {
3114
3114
  if (entry.isDirectory()) {
3115
3115
  if (SKIP_DIRS.has(entry.name)) continue;
3116
- walkPackDir(root, path30.join(current2, entry.name), out);
3116
+ walkPackDir(root, path31.join(current2, entry.name), out);
3117
3117
  } else if (entry.isFile()) {
3118
- const absolute = path30.join(current2, entry.name);
3119
- const relative22 = path30.relative(root, absolute).split(path30.sep).join("/");
3120
- out.push({ path: relative22, contents: fs18.readFileSync(absolute) });
3118
+ const absolute = path31.join(current2, entry.name);
3119
+ const relative22 = path31.relative(root, absolute).split(path31.sep).join("/");
3120
+ out.push({ path: relative22, contents: fs20.readFileSync(absolute) });
3121
3121
  }
3122
3122
  }
3123
3123
  }
@@ -3167,7 +3167,7 @@ var require_dist = __commonJS({
3167
3167
  function scaffoldPack(request) {
3168
3168
  const files = render(request);
3169
3169
  writeTree(request.targetDir, files);
3170
- return files.map((file) => path29.join(request.targetDir, file.path));
3170
+ return files.map((file) => path210.join(request.targetDir, file.path));
3171
3171
  }
3172
3172
  function buildPack(sourceDir) {
3173
3173
  const files = readPackDir(sourceDir);
@@ -3224,7 +3224,7 @@ var require_dist = __commonJS({
3224
3224
  }
3225
3225
  const manifest = parseManifest(decode(envelope.contents));
3226
3226
  verifyIntegrity2(manifest, files);
3227
- const directory = path29.resolve(destDir);
3227
+ const directory = path210.resolve(destDir);
3228
3228
  writeTree(directory, files);
3229
3229
  const result = {
3230
3230
  directory,
@@ -3302,7 +3302,7 @@ var require_dist = __commonJS({
3302
3302
  }
3303
3303
  const manifest = parseTreeManifest(decode(envelope.contents));
3304
3304
  verifyTreeIntegrity(manifest, files);
3305
- const directory = path29.resolve(destDir);
3305
+ const directory = path210.resolve(destDir);
3306
3306
  writeTree(directory, files);
3307
3307
  const result = {
3308
3308
  destDir: directory,
@@ -3317,7 +3317,7 @@ var require_dist = __commonJS({
3317
3317
  return normalized === "." ? ".wai/" : `${normalized}/.wai/`;
3318
3318
  }
3319
3319
  function normalizeRootPath(relativePath) {
3320
- const posix2 = relativePath.split(path29.sep).join("/").replace(/^\.\//, "").replace(/\/+$/, "");
3320
+ const posix2 = relativePath.split(path210.sep).join("/").replace(/^\.\//, "").replace(/\/+$/, "");
3321
3321
  return posix2 === "" || posix2 === "." ? "." : posix2;
3322
3322
  }
3323
3323
  function slugify(projectName) {
@@ -3616,7 +3616,7 @@ __export(src_exports, {
3616
3616
  loadImplementationSpecs: () => loadImplementationSpecs,
3617
3617
  loadInterfaceSpec: () => loadInterfaceSpec,
3618
3618
  loadInterfaceSpecs: () => loadInterfaceSpecs,
3619
- loadProjectConfig: () => loadProjectConfig2,
3619
+ loadProjectConfig: () => loadProjectConfig3,
3620
3620
  loadProjectExtensions: () => loadProjectExtensions,
3621
3621
  loadProjectVariants: () => loadProjectVariants,
3622
3622
  loadRegistry: () => loadRegistry,
@@ -3687,6 +3687,7 @@ __export(src_exports, {
3687
3687
  saveSpec: () => saveSpec,
3688
3688
  saveTopologyConfig: () => saveTopologyConfig,
3689
3689
  scanAllSpecs: () => scanAllSpecs,
3690
+ schemaFingerprint: () => schemaFingerprint,
3690
3691
  serializeYaml: () => serializeYaml,
3691
3692
  setExecutionTier: () => setExecutionTier,
3692
3693
  setLogLevel: () => setLogLevel,
@@ -3918,6 +3919,40 @@ var BundleNotFoundError = class extends WaironError {
3918
3919
  }
3919
3920
  };
3920
3921
 
3922
+ // src/models/identity-rename.ts
3923
+ var EDGE = " -> ";
3924
+ var QUALIFIED_UNIT = /^(\d+:)?([^.\s]+)\.([^.\s]+)$/;
3925
+ function movedComponent(rename, id) {
3926
+ return rename.components?.find((c) => c.from === id)?.to ?? id;
3927
+ }
3928
+ function rekeyUnit(rename, unit) {
3929
+ if (unit.includes(EDGE)) return unit.split(EDGE).map((side) => movedComponent(rename, side)).join(EDGE);
3930
+ const qualified = QUALIFIED_UNIT.exec(unit);
3931
+ if (!qualified) return unit;
3932
+ const [, step = "", component, method] = qualified;
3933
+ const moved = rename.methods?.find((m) => m.component === component && m.method === method);
3934
+ if (moved) return `${step}${moved.toComponent}.${moved.toMethod}`;
3935
+ return `${step}${movedComponent(rename, component)}.${method}`;
3936
+ }
3937
+ function rekeyAnchor(rename, anchor) {
3938
+ const { spec, at, covers } = anchor;
3939
+ const movedSpec = rename.specs?.find((s) => s.from === spec && (!s.sites || at !== void 0 && s.sites.includes(at)));
3940
+ let site = at;
3941
+ if (at !== void 0) {
3942
+ if (at.includes(EDGE)) {
3943
+ site = rekeyUnit(rename, at);
3944
+ } else {
3945
+ const moved = rename.methods?.find((m) => m.method === at && m.specs?.includes(spec));
3946
+ if (moved) site = moved.toMethod;
3947
+ }
3948
+ }
3949
+ return {
3950
+ spec: movedSpec?.to ?? spec,
3951
+ ...site !== void 0 ? { at: site } : {},
3952
+ ...covers !== void 0 ? { covers: covers.map((unit) => rekeyUnit(rename, unit)) } : {}
3953
+ };
3954
+ }
3955
+
3921
3956
  // src/models/project.ts
3922
3957
  var path2 = __toESM(require("path"));
3923
3958
  var import_zod3 = require("zod");
@@ -3975,6 +4010,21 @@ var ExecutionBudgetSchema = import_zod.z.object({
3975
4010
  allowNestedDelegation: import_zod.z.boolean(),
3976
4011
  mcp: McpAccessSchema
3977
4012
  });
4013
+ function summarize(budget, profile) {
4014
+ const lines = [
4015
+ `- **Work shape**: ${profile.breadth} breadth, ${profile.reasoningDepth} reasoning${profile.writes ? "" : ", read-only"}${profile.delegates ? ", delegating" : ""}`,
4016
+ `- **Why**: ${profile.rationale}`
4017
+ ];
4018
+ if (budget.modelTier) lines.push(`- **Capability tier**: ${budget.modelTier}`);
4019
+ if (budget.effort) lines.push(`- **Effort**: ${budget.effort}`);
4020
+ if (budget.maxTurns !== void 0) {
4021
+ lines.push(`- **Turn ceiling**: ${budget.maxTurns} (a circuit breaker \u2014 hitting it should read as a scoping error, not a limit to work up to)`);
4022
+ }
4023
+ lines.push(`- **Tool grant**: ${budget.toolClass}`);
4024
+ lines.push(`- **May delegate further**: ${budget.allowNestedDelegation ? "yes" : "no"}`);
4025
+ lines.push(`- **MCP access**: ${budget.mcp}`);
4026
+ return lines;
4027
+ }
3978
4028
  var BudgetTierSchema = import_zod.z.enum(["off", "free", "default", "trade", "aggressive"]);
3979
4029
  var ExecutionConfigSchema = import_zod.z.object({
3980
4030
  /**
@@ -4538,6 +4588,12 @@ function projectConfigFsAdapterAt(rootDir) {
4538
4588
  },
4539
4589
  documentExists() {
4540
4590
  return fs2.existsSync(file());
4591
+ },
4592
+ readText() {
4593
+ return readFileOrNull(file());
4594
+ },
4595
+ writeText(text2) {
4596
+ writeFile(file(), text2);
4541
4597
  }
4542
4598
  };
4543
4599
  }
@@ -4563,8 +4619,158 @@ function storeOver(adapter, root) {
4563
4619
  },
4564
4620
  readField(field) {
4565
4621
  return valueAt(currentDocument(), field);
4622
+ },
4623
+ rewriteScalars(edits, dryRun = false) {
4624
+ if (edits.length === 0) return;
4625
+ const text2 = adapter.readText();
4626
+ if (text2 === null) throw new ProjectNotInitializedError();
4627
+ const next = rewriteScalarsInText(text2, edits, root);
4628
+ if (!dryRun) adapter.writeText(next);
4629
+ }
4630
+ };
4631
+ }
4632
+ var KEY_RE = /^('(?:[^']|'')*'|"(?:[^"\\]|\\.)*"|[^\s'"#{}[\],&*!|>%@`][^:#]*?)\s*:(?=\s|$)/;
4633
+ function tokenize(lines) {
4634
+ const tokens = [];
4635
+ let blockAbove = null;
4636
+ lines.forEach((raw, line) => {
4637
+ const content = raw.replace(/\r?\n$/, "");
4638
+ const indent = content.length - content.trimStart().length;
4639
+ const trimmed = content.trim();
4640
+ if (blockAbove !== null) {
4641
+ if (trimmed === "" || indent > blockAbove) return;
4642
+ blockAbove = null;
4643
+ }
4644
+ if (trimmed === "" || trimmed.startsWith("#")) return;
4645
+ let col = indent;
4646
+ let rest = content.slice(indent);
4647
+ while (rest === "-" || rest.startsWith("- ")) {
4648
+ tokens.push({ kind: "item", col, line });
4649
+ const after = rest.slice(1);
4650
+ const pad = after.length - after.trimStart().length;
4651
+ col += 1 + pad;
4652
+ rest = after.trimStart();
4653
+ }
4654
+ if (rest === "") return;
4655
+ const key = KEY_RE.exec(rest);
4656
+ if (key) {
4657
+ const name = key[1].startsWith("'") ? key[1].slice(1, -1).replace(/''/g, "'") : key[1].startsWith('"') ? JSON.parse(key[1]) : key[1].trim();
4658
+ const afterKey = rest.slice(key[0].length);
4659
+ const value = afterKey.trimStart();
4660
+ const token = { kind: "key", col, line, name };
4661
+ if (value !== "" && !value.startsWith("#")) token.valueStart = col + key[0].length + (afterKey.length - value.length);
4662
+ tokens.push(token);
4663
+ if (/^[|>][-+0-9]*\s*(#.*)?$/.test(value)) blockAbove = col;
4664
+ return;
4665
+ }
4666
+ tokens.push({ kind: "scalar", col, line, valueStart: col });
4667
+ if (/^[|>][-+0-9]*\s*(#.*)?$/.test(rest)) blockAbove = col - 2;
4668
+ });
4669
+ return tokens;
4670
+ }
4671
+ function childrenOf(tokens, index) {
4672
+ const parent = tokens[index];
4673
+ const out = [];
4674
+ for (let i = index + 1; i < tokens.length; i += 1) {
4675
+ const t = tokens[i];
4676
+ const nested = t.col > parent.col || parent.kind === "key" && t.kind === "item" && t.col === parent.col;
4677
+ if (!nested) break;
4678
+ out.push(t);
4679
+ }
4680
+ return out;
4681
+ }
4682
+ function locateScalar(tokens, path31) {
4683
+ let region = tokens;
4684
+ for (let k = 0; k < path31.length; k += 1) {
4685
+ const segment = path31[k];
4686
+ if (region.length === 0) return `nothing is nested at ${path31.slice(0, k).join(".")}`;
4687
+ const base = Math.min(...region.map((t) => t.col));
4688
+ const last = k === path31.length - 1;
4689
+ let at;
4690
+ if (typeof segment === "string") {
4691
+ at = region.findIndex((t) => t.kind === "key" && t.col === base && t.name === segment);
4692
+ if (at < 0) return `no block key "${segment}" at ${path31.slice(0, k).join(".") || "the document root"}`;
4693
+ if (last) return region[at].valueStart !== void 0 ? region[at] : `"${segment}" holds no inline scalar`;
4694
+ } else {
4695
+ const items = region.map((t, i) => ({ t, i })).filter(({ t }) => t.kind === "item" && t.col === base);
4696
+ if (segment >= items.length) return `no block sequence item ${segment} at ${path31.slice(0, k).join(".")}`;
4697
+ at = items[segment].i;
4698
+ if (last) {
4699
+ const value = region[at + 1];
4700
+ return value && value.kind === "scalar" && value.line === region[at].line ? value : `item ${segment} holds no inline scalar`;
4701
+ }
4702
+ }
4703
+ const regionStart = tokens.indexOf(region[at]);
4704
+ region = childrenOf(tokens, regionStart);
4705
+ }
4706
+ return "an empty path addresses no scalar";
4707
+ }
4708
+ function readScalar(text2) {
4709
+ if (text2.startsWith("'")) {
4710
+ let i = 1;
4711
+ let value = "";
4712
+ while (i < text2.length) {
4713
+ if (text2[i] === "'") {
4714
+ if (text2[i + 1] === "'") {
4715
+ value += "'";
4716
+ i += 2;
4717
+ continue;
4718
+ }
4719
+ return { value, style: "single", end: i + 1 };
4720
+ }
4721
+ value += text2[i];
4722
+ i += 1;
4723
+ }
4724
+ return null;
4725
+ }
4726
+ if (text2.startsWith('"')) {
4727
+ const closing = /^"(?:[^"\\]|\\.)*"/.exec(text2);
4728
+ if (!closing) return null;
4729
+ try {
4730
+ return { value: JSON.parse(closing[0]), style: "double", end: closing[0].length };
4731
+ } catch {
4732
+ return null;
4566
4733
  }
4734
+ }
4735
+ const comment = text2.search(/\s#/);
4736
+ const body = (comment < 0 ? text2 : text2.slice(0, comment)).trimEnd();
4737
+ return { value: body, style: "plain", end: body.length };
4738
+ }
4739
+ function writeScalar(value, style) {
4740
+ if (style === "double") return JSON.stringify(value);
4741
+ if (style === "plain" && /^[A-Za-z0-9_][A-Za-z0-9_./-]*$/.test(value)) return value;
4742
+ return `'${value.replace(/'/g, "''")}'`;
4743
+ }
4744
+ function setAt(document, path31, value) {
4745
+ let node = document;
4746
+ for (const segment of path31.slice(0, -1)) node = node[segment];
4747
+ node[path31[path31.length - 1]] = value;
4748
+ }
4749
+ function rewriteScalarsInText(text2, edits, root) {
4750
+ const refuse = (why) => {
4751
+ throw new WaironError(`Cannot rewrite .wai/project.yaml at ${root} without disturbing its formatting: ${why}. Nothing was written.`);
4567
4752
  };
4753
+ const lines = text2.split(/(?<=\n)/);
4754
+ const tokens = tokenize(lines);
4755
+ const expected = parseYaml(text2);
4756
+ for (const edit of edits) {
4757
+ const where = edit.path.join(".");
4758
+ const token = locateScalar(tokens, edit.path);
4759
+ if (typeof token === "string") refuse(`${where}: ${token}`);
4760
+ const { line, valueStart } = token;
4761
+ const content = lines[line].replace(/\r?\n$/, "");
4762
+ const ending = lines[line].slice(content.length);
4763
+ const scalar = readScalar(content.slice(valueStart));
4764
+ if (!scalar) refuse(`${where}: the scalar there is not written on one line`);
4765
+ if (scalar.value !== edit.from) refuse(`${where} holds "${scalar.value}", not "${edit.from}"`);
4766
+ lines[line] = content.slice(0, valueStart) + writeScalar(edit.to, scalar.style) + content.slice(valueStart + scalar.end) + ending;
4767
+ setAt(expected, edit.path, edit.to);
4768
+ }
4769
+ const next = lines.join("");
4770
+ if (JSON.stringify(parseYaml(next)) !== JSON.stringify(expected)) {
4771
+ refuse("the edited text does not read back as exactly the intended change");
4772
+ }
4773
+ return next;
4568
4774
  }
4569
4775
  function parseConfig(document, root) {
4570
4776
  try {
@@ -4707,6 +4913,35 @@ function registryOver(store, root) {
4707
4913
  ...config,
4708
4914
  extensions: { ...config.extensions, packs: [...packsOf(config), ...selections], useGlobalPacks: false }
4709
4915
  });
4916
+ },
4917
+ rekeyCarried(rename, dryRun = false) {
4918
+ const groups = store.readField("rules.conformance.carried");
4919
+ if (!Array.isArray(groups)) return [];
4920
+ const edits = [];
4921
+ const rekeys = [];
4922
+ groups.forEach((group, g) => {
4923
+ const findings = group?.findings;
4924
+ if (!Array.isArray(findings)) return;
4925
+ findings.forEach((finding, f) => {
4926
+ const entry = finding;
4927
+ if (!entry || typeof entry.spec !== "string") return;
4928
+ const at = typeof entry.at === "string" ? entry.at : void 0;
4929
+ const covers = Array.isArray(entry.covers) ? entry.covers.filter((u) => typeof u === "string") : void 0;
4930
+ const next = rekeyAnchor(rename, { spec: entry.spec, at, covers });
4931
+ const base = ["rules", "conformance", "carried", g, "findings", f];
4932
+ const named = { code: String(entry.code), spec: entry.spec, at: at ?? "" };
4933
+ const change = (path31, field, from, to) => {
4934
+ if (from === to) return;
4935
+ edits.push({ path: [...base, ...path31], from, to });
4936
+ rekeys.push({ ...named, field, from, to });
4937
+ };
4938
+ change(["spec"], "spec", entry.spec, next.spec);
4939
+ if (at !== void 0) change(["at"], "at", at, next.at);
4940
+ (covers ?? []).forEach((unit, u) => change(["covers", u], "covers", unit, next.covers[u]));
4941
+ });
4942
+ });
4943
+ store.rewriteScalars(edits, dryRun);
4944
+ return rekeys;
4710
4945
  }
4711
4946
  };
4712
4947
  }
@@ -4791,6 +5026,9 @@ function projectConfigRepositoryOver(adapter, rootDir) {
4791
5026
  },
4792
5027
  pinGlobalPacksAsSelections(selections) {
4793
5028
  registry.pinGlobalPacksAsSelections(selections);
5029
+ },
5030
+ rekeyCarried(rename, dryRun) {
5031
+ return registry.rekeyCarried(rename, dryRun);
4794
5032
  }
4795
5033
  };
4796
5034
  }
@@ -4840,6 +5078,9 @@ var projectConfigRepository = {
4840
5078
  },
4841
5079
  pinGlobalPacksAsSelections(selections) {
4842
5080
  bound().pinGlobalPacksAsSelections(selections);
5081
+ },
5082
+ rekeyCarried(rename, dryRun) {
5083
+ return bound().rekeyCarried(rename, dryRun);
4843
5084
  }
4844
5085
  };
4845
5086
 
@@ -5609,9 +5850,9 @@ var MethodSignatureSchema = import_zod8.z.object({
5609
5850
  ext: ExtDataSchema.optional()
5610
5851
  });
5611
5852
  var INTENT_FLOOR_MIN_CHARS = 40;
5612
- function passesIntentFloor(text, methodName) {
5613
- if (!text) return false;
5614
- const t = text.trim();
5853
+ function passesIntentFloor(text2, methodName) {
5854
+ if (!text2) return false;
5855
+ const t = text2.trim();
5615
5856
  if (t.length < INTENT_FLOOR_MIN_CHARS) return false;
5616
5857
  const norm = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
5617
5858
  if (norm(t) === norm(methodName)) return false;
@@ -6357,10 +6598,10 @@ function stepGraph(method) {
6357
6598
  const s = byNum.get(n);
6358
6599
  if (s.type !== "parallel" || s.endStep === void 0 || !s.branches?.length) continue;
6359
6600
  const entries = s.branches.map((b) => b.step).sort((a, b) => a - b);
6360
- const join17 = fallNext(s.endStep);
6601
+ const join19 = fallNext(s.endStep);
6361
6602
  for (let i = 0; i < entries.length; i++) {
6362
6603
  const armEnd = i + 1 < entries.length ? prevOf(entries[i + 1]) : s.endStep;
6363
- if (armEnd !== void 0 && armEnd >= entries[i]) armEndJoin.set(armEnd, join17);
6604
+ if (armEnd !== void 0 && armEnd >= entries[i]) armEndJoin.set(armEnd, join19);
6364
6605
  }
6365
6606
  }
6366
6607
  const successorsOf = (n) => {
@@ -7060,12 +7301,12 @@ function buildExcalidrawScene(model, L) {
7060
7301
  locked: false
7061
7302
  };
7062
7303
  }
7063
- function boundLabel(rect, text, fontSize, verticalAlign) {
7304
+ function boundLabel(rect, text2, fontSize, verticalAlign) {
7064
7305
  const id = rect.id + "-label";
7065
7306
  const label = base(id, "text", { x: rect.x + 8, y: rect.y + 6, w: rect.width - 16, h: 20 });
7066
7307
  label.roundness = null;
7067
- label.text = text;
7068
- label.originalText = text;
7308
+ label.text = text2;
7309
+ label.originalText = text2;
7069
7310
  label.fontSize = fontSize;
7070
7311
  label.fontFamily = 1;
7071
7312
  label.textAlign = "center";
@@ -11573,6 +11814,161 @@ var SURFACES_DIRNAME = "surfaces";
11573
11814
  function surfacesDir(rootDir) {
11574
11815
  return path9.join(rootDir, ".wai", SURFACES_DIRNAME);
11575
11816
  }
11817
+ function audienceRank(audience) {
11818
+ const idx = SURFACE_AUDIENCES.indexOf(audience ?? "instance");
11819
+ return idx === -1 ? SURFACE_AUDIENCES.indexOf("instance") : idx;
11820
+ }
11821
+ function stateIdString() {
11822
+ const s = computeStateId();
11823
+ return `${s.algorithm}:${s.digest}`;
11824
+ }
11825
+ function computeTypeClosure(entries, types) {
11826
+ const included = /* @__PURE__ */ new Map();
11827
+ const queue = [];
11828
+ const enqueueRef = (ref) => {
11829
+ if (BUILTIN_TYPES.has(ref.toLowerCase())) return;
11830
+ for (const spec of types) {
11831
+ const qualifiedId = spec.subsystem && !spec.id.startsWith(`${spec.subsystem}::`) ? `${spec.subsystem}::${spec.id}` : spec.id;
11832
+ if (matchTypeRef(ref, qualifiedId) && !included.has(spec.id)) {
11833
+ included.set(spec.id, spec);
11834
+ queue.push(spec.id);
11835
+ }
11836
+ }
11837
+ };
11838
+ for (const entry of entries) {
11839
+ for (const m of entry.methods) {
11840
+ for (const ref of methodTypeRefs(m)) enqueueRef(ref);
11841
+ }
11842
+ }
11843
+ while (queue.length) {
11844
+ const spec = included.get(queue.shift());
11845
+ for (const field of spec.fields) {
11846
+ for (const ref of extractTypeIdentifiers(field.type)) enqueueRef(ref);
11847
+ }
11848
+ }
11849
+ return [...included.values()].map((t) => ({
11850
+ id: t.id,
11851
+ name: t.name,
11852
+ kind: t.kind,
11853
+ fields: t.fields.map((f) => ({
11854
+ name: f.name,
11855
+ type: f.type,
11856
+ ...f.description ? { description: f.description } : {},
11857
+ ...f.optional ? { optional: true } : {}
11858
+ }))
11859
+ }));
11860
+ }
11861
+ function projectOwnSurface(maxAudience) {
11862
+ const system = loadSystemSpec();
11863
+ if (!system) {
11864
+ throw new Error("Cannot project a surface: the L0 system spec is missing.");
11865
+ }
11866
+ const subsystems = loadSubsystemSpecs();
11867
+ const components = loadComponentSpecs();
11868
+ const interfaces = loadInterfaceSpecs();
11869
+ const types = loadTypeSpecs();
11870
+ const floor = audienceRank(maxAudience);
11871
+ const rawEntries = system.publicInterfaces ?? [];
11872
+ const entries = [];
11873
+ for (const raw of rawEntries) {
11874
+ const audience = raw.audience ?? "instance";
11875
+ if (audienceRank(audience) < floor) continue;
11876
+ if (!raw.component) continue;
11877
+ const comp = components.find((c) => c.id === raw.component);
11878
+ if (!comp) continue;
11879
+ const compInterfaces = interfaces.filter((i) => i.component === comp.id && (!raw.interface || i.id === raw.interface));
11880
+ const methods = compInterfaces.flatMap((i) => i.methods);
11881
+ const subsystemType = subsystems.find((s) => s.id === comp.subsystem)?.publicInterfaces.find((pi) => pi.component === comp.id)?.type;
11882
+ entries.push({
11883
+ id: raw.id ?? raw.interface ?? comp.id,
11884
+ name: raw.name ?? comp.name,
11885
+ audience,
11886
+ type: raw.type ?? subsystemType ?? "Custom",
11887
+ component: comp.id,
11888
+ methods,
11889
+ ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
11890
+ // Project the backing Portal's auth + basePath so the codec can emit
11891
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
11892
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
11893
+ ...comp.basePath ? { basePath: comp.basePath } : {},
11894
+ details: raw.details ?? "",
11895
+ ...raw.version ? { version: raw.version } : {},
11896
+ ...raw.stability ? { stability: raw.stability } : {}
11897
+ });
11898
+ }
11899
+ return SurfaceSnapshotSchema.parse({
11900
+ projectName: system.name,
11901
+ origin: "generated",
11902
+ stateId: stateIdString(),
11903
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
11904
+ interfaces: entries,
11905
+ types: computeTypeClosure(entries, types)
11906
+ });
11907
+ }
11908
+ function projectChildSurface() {
11909
+ return projectOwnSurface("project");
11910
+ }
11911
+ function localName(id) {
11912
+ return id.split("::").pop();
11913
+ }
11914
+ var CROSS_BOUNDARY_TARGETS = /* @__PURE__ */ new Set(["Portal", "Observer"]);
11915
+ function projectSubsystemSurface(subsystemId) {
11916
+ const system = loadSystemSpec();
11917
+ if (!system) {
11918
+ throw new Error("Cannot project a subsystem surface: the L0 system spec is missing.");
11919
+ }
11920
+ const subsystems = loadSubsystemSpecs();
11921
+ const target = subsystems.find((s) => s.id === subsystemId);
11922
+ if (!target) {
11923
+ throw new Error(`Cannot project a subsystem surface: subsystem "${subsystemId}" does not exist.`);
11924
+ }
11925
+ const components = loadComponentSpecs();
11926
+ const interfaces = loadInterfaceSpecs();
11927
+ const types = loadTypeSpecs();
11928
+ const entries = [];
11929
+ const unprojectable = [];
11930
+ for (const pub of target.publicInterfaces ?? []) {
11931
+ if (!pub.component) continue;
11932
+ const comp = components.find((c) => c.id === pub.component || c.id === `${subsystemId}::${pub.component}`);
11933
+ if (!comp) continue;
11934
+ if (!CROSS_BOUNDARY_TARGETS.has(comp.componentType)) {
11935
+ unprojectable.push({ component: pub.component, componentType: comp.componentType });
11936
+ continue;
11937
+ }
11938
+ const compInterfaces = interfaces.filter((i) => i.component === comp.id && (!pub.interface || i.id === pub.interface || i.id === `${subsystemId}::${pub.interface}`));
11939
+ const methods = compInterfaces.flatMap((i) => i.methods);
11940
+ entries.push({
11941
+ id: localName(pub.interface ?? comp.id),
11942
+ name: comp.name,
11943
+ // Family ceiling: a sibling surface is consumable by the system family only.
11944
+ audience: "project",
11945
+ type: pub.type ?? "Custom",
11946
+ // The snapshot carries the LOCAL portal name — consumers resolve cross-tree
11947
+ // refs by their final segment.
11948
+ component: localName(comp.id),
11949
+ methods,
11950
+ ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
11951
+ // Project the backing component's auth + basePath so the codec can emit
11952
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
11953
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
11954
+ ...comp.basePath ? { basePath: comp.basePath } : {},
11955
+ details: pub.details ?? ""
11956
+ });
11957
+ }
11958
+ for (const skipped of unprojectable) {
11959
+ console.error(
11960
+ `[surfaces] skipped "${subsystemId}::${skipped.component}": a published ${skipped.componentType} can never serve a cross-boundary caller, so it stays out of every chained child's sibling surface \u2014 publish this surface through a Portal, or an Observer (for events).`
11961
+ );
11962
+ }
11963
+ return SurfaceSnapshotSchema.parse({
11964
+ projectName: `${system.name}::${subsystemId}`,
11965
+ origin: "generated",
11966
+ stateId: stateIdString(),
11967
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
11968
+ interfaces: entries,
11969
+ types: computeTypeClosure(entries, types)
11970
+ });
11971
+ }
11576
11972
  function listSnapshots(rootDir = getProjectRoot()) {
11577
11973
  const dir = surfacesDir(rootDir);
11578
11974
  if (!fs7.existsSync(dir)) return [];
@@ -11597,6 +11993,43 @@ function listMountSnapshots(mounts) {
11597
11993
  }
11598
11994
  return out;
11599
11995
  }
11996
+ function projectFamilySurfaces(parent) {
11997
+ return runWithProjectRoot(parent.parentRoot, () => {
11998
+ const siblings = loadSubsystemSpecs().filter((s) => !s.id.includes("::") && s.id !== parent.subsystemId);
11999
+ return [projectChildSurface(), ...siblings.map((s) => projectSubsystemSurface(s.id))];
12000
+ });
12001
+ }
12002
+ function projectedFamilyContent(parent) {
12003
+ try {
12004
+ return new Map(projectFamilySurfaces(parent).map((s) => [s.projectName, surfaceContentKey(s)]));
12005
+ } catch {
12006
+ return null;
12007
+ }
12008
+ }
12009
+ function listExternalInterfaces() {
12010
+ const snapshots = listSnapshots();
12011
+ const chainingParent = resolveChainingParent();
12012
+ const projected = chainingParent ? projectedFamilyContent(chainingParent) : null;
12013
+ return snapshots.map((snapshot) => {
12014
+ const generated = snapshot.origin === "generated";
12015
+ const sourceKind = !generated ? "foreign" : snapshot.projectName.includes("::") ? "sibling" : "parent";
12016
+ const freshness = generated && projected ? projected.get(snapshot.projectName) === surfaceContentKey(snapshot) ? "fresh" : "stale" : "unverifiable";
12017
+ return {
12018
+ projectName: snapshot.projectName,
12019
+ origin: snapshot.origin,
12020
+ sourceKind,
12021
+ generatedAt: snapshot.generatedAt,
12022
+ ...snapshot.stateId ? { stateId: snapshot.stateId } : {},
12023
+ ...snapshot.version ? { version: snapshot.version } : {},
12024
+ freshness,
12025
+ interfaceIds: snapshot.interfaces.map((e) => e.id)
12026
+ };
12027
+ });
12028
+ }
12029
+ function surfaceContentKey(snapshot) {
12030
+ const { stateId, generatedAt, origin, ...content } = snapshot;
12031
+ return JSON.stringify(content);
12032
+ }
11600
12033
 
11601
12034
  // src/core/extensions.ts
11602
12035
  var fs9 = __toESM(require("fs"));
@@ -11650,7 +12083,7 @@ function defaultTargetConfig(type) {
11650
12083
  enabled: true
11651
12084
  };
11652
12085
  }
11653
- var WAIRON_VERSION = "5.1.1-dev.75";
12086
+ var WAIRON_VERSION = "5.1.1-dev.77";
11654
12087
  var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
11655
12088
  var ARCHITECT_AGENT_ID = "agent-architect";
11656
12089
  var ARCHITECT_TEMPLATE_ID = "architect";
@@ -11901,7 +12334,7 @@ var PackSkillSchema = import_zod9.z.object({
11901
12334
  });
11902
12335
  var PackInstructionBlockSchema = import_zod9.z.union([
11903
12336
  // Scalar shorthand: `instructions: >- …` — the common case, unscoped.
11904
- import_zod9.z.string().min(1).transform((text) => ({ text })),
12337
+ import_zod9.z.string().min(1).transform((text2) => ({ text: text2 })),
11905
12338
  import_zod9.z.object({
11906
12339
  text: import_zod9.z.string().min(1),
11907
12340
  profile: import_zod9.z.array(import_zod9.z.string().min(1)).min(1).optional()
@@ -12361,21 +12794,21 @@ function buildCodeIndex(model) {
12361
12794
  const declarations = /* @__PURE__ */ new Map();
12362
12795
  const anchors = /* @__PURE__ */ new Map();
12363
12796
  const findingAnchors = /* @__PURE__ */ new Map();
12364
- const namesAt = (cache, path29, make) => {
12365
- const cached = cache.get(path29);
12797
+ const namesAt = (cache, path31, make) => {
12798
+ const cached = cache.get(path31);
12366
12799
  if (cached) return cached;
12367
- const f = facts.get(path29);
12800
+ const f = facts.get(path31);
12368
12801
  if (!f) return NO_NAMES;
12369
12802
  const built = make(f);
12370
- cache.set(path29, built);
12803
+ cache.set(path31, built);
12371
12804
  return built;
12372
12805
  };
12373
12806
  const republished = /* @__PURE__ */ new Map();
12374
- const republishedFrom = (path29) => {
12375
- const cached = republished.get(path29);
12807
+ const republishedFrom = (path31) => {
12808
+ const cached = republished.get(path31);
12376
12809
  if (cached) return cached;
12377
- const out = /* @__PURE__ */ new Set([path29]);
12378
- const queue = [path29];
12810
+ const out = /* @__PURE__ */ new Set([path31]);
12811
+ const queue = [path31];
12379
12812
  while (queue.length) {
12380
12813
  const from = queue.pop();
12381
12814
  const f = facts.get(from);
@@ -12388,7 +12821,7 @@ function buildCodeIndex(model) {
12388
12821
  }
12389
12822
  }
12390
12823
  }
12391
- republished.set(path29, out);
12824
+ republished.set(path31, out);
12392
12825
  return out;
12393
12826
  };
12394
12827
  const NO_ORIGIN = /* @__PURE__ */ new Set();
@@ -12397,9 +12830,9 @@ function buildCodeIndex(model) {
12397
12830
  return to ? republishedFrom(to) : NO_ORIGIN;
12398
12831
  };
12399
12832
  const scopeOf = (site, from) => {
12400
- const path29 = pathKey(site.from ?? from);
12401
- const f = facts.get(path29);
12402
- return f?.status === "analyzed" && f.analysisGrade === "exact" ? { path: path29, facts: f } : void 0;
12833
+ const path31 = pathKey(site.from ?? from);
12834
+ const f = facts.get(path31);
12835
+ return f?.status === "analyzed" && f.analysisGrade === "exact" ? { path: path31, facts: f } : void 0;
12403
12836
  };
12404
12837
  const originOf2 = (site, from) => {
12405
12838
  const resolved = scopeOf(site, from);
@@ -12438,16 +12871,39 @@ function buildCodeIndex(model) {
12438
12871
  if (site.via) for (const typeName of localTypesOf(f, site.via)) widenThroughType(typeName);
12439
12872
  return out;
12440
12873
  };
12441
- const declarationsAt = (path29) => namesAt(declarations, pathKey(path29), (f) => /* @__PURE__ */ new Set([...f.declaredNames, ...f.exportedNames]));
12874
+ const declarationsAt = (path31) => namesAt(declarations, pathKey(path31), (f) => /* @__PURE__ */ new Set([...f.declaredNames, ...f.exportedNames]));
12875
+ const forwardsOf = (start, name) => {
12876
+ const out = [];
12877
+ const seen = /* @__PURE__ */ new Set();
12878
+ const queue = [{ file: pathKey(start), name }];
12879
+ while (queue.length) {
12880
+ const at = queue.shift();
12881
+ const key = `${at.file}#${at.name}`;
12882
+ if (seen.has(key)) continue;
12883
+ seen.add(key);
12884
+ out.push(at);
12885
+ for (const binding of facts.get(at.file)?.reexportBindings ?? []) {
12886
+ const star = binding.exported === "*";
12887
+ if (!star && binding.exported !== at.name) continue;
12888
+ if (star && at.name === "default") continue;
12889
+ const to = resolveImport(at.file, binding.from, paths);
12890
+ if (!to) continue;
12891
+ if (star && !facts.get(to)?.exportedNames.includes(at.name)) continue;
12892
+ queue.push({ file: to, name: star ? at.name : binding.local });
12893
+ }
12894
+ }
12895
+ return out;
12896
+ };
12442
12897
  return {
12443
12898
  paths,
12444
12899
  exactPaths,
12445
- factsAt: (path29) => facts.get(pathKey(path29)),
12900
+ factsAt: (path31) => facts.get(pathKey(path31)),
12446
12901
  declarationsAt,
12447
- anchorsAt: (path29) => namesAt(anchors, pathKey(path29), (f) => /* @__PURE__ */ new Set([...f.declaredNames, ...f.exportedNames, ...f.anchoredNames])),
12448
- findingAnchorsAt: (path29) => namesAt(findingAnchors, pathKey(path29), (f) => new Set(f.anchoredNames)),
12902
+ anchorsAt: (path31) => namesAt(anchors, pathKey(path31), (f) => /* @__PURE__ */ new Set([...f.declaredNames, ...f.exportedNames, ...f.anchoredNames])),
12903
+ findingAnchorsAt: (path31) => namesAt(findingAnchors, pathKey(path31), (f) => new Set(f.anchoredNames)),
12449
12904
  originOf: originOf2,
12450
- possibleOriginsOf
12905
+ possibleOriginsOf,
12906
+ forwardsOf
12451
12907
  };
12452
12908
  }
12453
12909
  function buildRealizationIndex(ctx) {
@@ -12483,10 +12939,10 @@ function buildRealizationIndex(ctx) {
12483
12939
  return {
12484
12940
  paths,
12485
12941
  implementationsOf: (compId) => implsByComponent.get(compId) ?? NONE_IMPL,
12486
- implementationsAt: (path29) => implsByPath.get(pathKey(path29)) ?? NONE_IMPL,
12942
+ implementationsAt: (path31) => implsByPath.get(pathKey(path31)) ?? NONE_IMPL,
12487
12943
  filesOf: (compId) => filesByComponent.get(compId) ?? NONE_PATH,
12488
12944
  filesIn: (subsystemId) => filesBySubsystem.get(subsystemId) ?? NONE_PATH,
12489
- componentsAt: (path29) => componentsByPath.get(pathKey(path29)) ?? NONE_COMP
12945
+ componentsAt: (path31) => componentsByPath.get(pathKey(path31)) ?? NONE_COMP
12490
12946
  };
12491
12947
  }
12492
12948
  function buildImportGraph(index, universe) {
@@ -12498,22 +12954,22 @@ function buildImportGraph(index, universe) {
12498
12954
  if (to && to !== from) into.add(to);
12499
12955
  }
12500
12956
  };
12501
- const importsOf = (path29) => {
12502
- const cached = imports.get(path29);
12957
+ const importsOf = (path31) => {
12958
+ const cached = imports.get(path31);
12503
12959
  if (cached) return cached;
12504
- const facts = index.factsAt(path29);
12960
+ const facts = index.factsAt(path31);
12505
12961
  const out = /* @__PURE__ */ new Set();
12506
- if (facts?.status === "analyzed") resolveAll(path29, facts.imports, out);
12507
- imports.set(path29, out);
12962
+ if (facts?.status === "analyzed") resolveAll(path31, facts.imports, out);
12963
+ imports.set(path31, out);
12508
12964
  return out;
12509
12965
  };
12510
- const tracesOf = (path29) => {
12511
- const cached = traces.get(path29);
12966
+ const tracesOf = (path31) => {
12967
+ const cached = traces.get(path31);
12512
12968
  if (cached) return cached;
12513
- const facts = index.factsAt(path29);
12514
- const out = new Set(importsOf(path29));
12515
- if (facts?.status === "analyzed") resolveAll(path29, facts.reexports, out);
12516
- traces.set(path29, out);
12969
+ const facts = index.factsAt(path31);
12970
+ const out = new Set(importsOf(path31));
12971
+ if (facts?.status === "analyzed") resolveAll(path31, facts.reexports, out);
12972
+ traces.set(path31, out);
12517
12973
  return out;
12518
12974
  };
12519
12975
  return {
@@ -13850,11 +14306,11 @@ var unconditionalCallCyclesRule = {
13850
14306
  const memberEdges = keys.flatMap((k) => (adjacency.get(k) ?? []).filter((e) => inScc.has(e.toKey)));
13851
14307
  if (memberEdges.length === 0) continue;
13852
14308
  const anchor = [...memberEdges].sort((a, b) => byCodeUnit(a.fromKey, b.fromKey) || byCodeUnit(a.impl.id, b.impl.id) || a.stepNumber - b.stepNumber)[0];
13853
- const path29 = [...keys].sort(byCodeUnit).join(" \u2192 ");
14309
+ const path31 = [...keys].sort(byCodeUnit).join(" \u2192 ");
13854
14310
  ctx.addIssue(
13855
14311
  "warning",
13856
14312
  "UNCONDITIONAL_CALL_CYCLE",
13857
- `Call cycle with no guard: ${path29} \u2014 every call edge in this cycle is unavoidable on all paths of its narrative (e.g. step ${anchor.stepNumber} of "${anchor.methodName}" in "${anchor.impl.id}" always calls ${anchor.toLabel}). This recurses without a base case, by construction. Guard at least one edge with a branch/return before the call, or lint.allow with the termination argument.`,
14313
+ `Call cycle with no guard: ${path31} \u2014 every call edge in this cycle is unavoidable on all paths of its narrative (e.g. step ${anchor.stepNumber} of "${anchor.methodName}" in "${anchor.impl.id}" always calls ${anchor.toLabel}). This recurses without a base case, by construction. Guard at least one edge with a branch/return before the call, or lint.allow with the termination argument.`,
13858
14314
  anchor.impl.id,
13859
14315
  memberEdges.some((e) => ctx.isImplementationDraft(e.impl))
13860
14316
  );
@@ -15521,8 +15977,8 @@ var lifecycleRule = {
15521
15977
  };
15522
15978
 
15523
15979
  // src/core/rules/wiring/portal-mounts.ts
15524
- function liesUnder(path29, prefix) {
15525
- return path29 === prefix || path29.startsWith(prefix + "/");
15980
+ function liesUnder(path31, prefix) {
15981
+ return path31 === prefix || path31.startsWith(prefix + "/");
15526
15982
  }
15527
15983
  var portalMountsRule = {
15528
15984
  name: "portal-mounts",
@@ -15975,8 +16431,8 @@ function componentOfImpl(ctx, implContract) {
15975
16431
  }
15976
16432
  var CLAIM_PHRASES = /\b(persist(s|ed|ent|ing|ence)?|survives?\s+(a\s+)?restart|writ(es?|ten)\s+to\s+disk|registered\s+into|durabl[ey])\b/i;
15977
16433
  var QUOTED_SPAN = /"[^"]*"|“[^”]*”|`[^`]*`|(?<![\p{L}\p{N}])['‘](?:[^'‘’]|(?<=[\p{L}\p{N}])['’](?=[\p{L}\p{N}]))*['’](?![\p{L}\p{N}])/gu;
15978
- function withoutQuotedText(text) {
15979
- return text.replace(QUOTED_SPAN, " ");
16434
+ function withoutQuotedText(text2) {
16435
+ return text2.replace(QUOTED_SPAN, " ");
15980
16436
  }
15981
16437
  var proseClaimRule = {
15982
16438
  name: "prose-claims",
@@ -16510,6 +16966,25 @@ function resolveCallTarget(ctx, componentId, methodName) {
16510
16966
  }
16511
16967
  return { accepted, files };
16512
16968
  }
16969
+ function isTarget(pairs, target) {
16970
+ return pairs.some((p) => target.files.has(p.file) && target.accepted.has(p.name));
16971
+ }
16972
+ function invokedAs(code, site, file) {
16973
+ const scope = pathKey(site.from ?? file);
16974
+ const facts = code.factsAt(scope);
16975
+ if (!facts || facts.status !== "analyzed" || facts.analysisGrade !== "exact") return [];
16976
+ if (!site.member) {
16977
+ const binding = importBindingOf(facts, site.name);
16978
+ if (!binding || binding.namespace) return [];
16979
+ const module3 = resolveImport(scope, binding.from, code.paths);
16980
+ return module3 ? code.forwardsOf(module3, binding.imported ?? site.name) : [];
16981
+ }
16982
+ if (!site.via) return [];
16983
+ const receiver = importBindingOf(facts, site.via);
16984
+ if (!receiver?.namespace) return [];
16985
+ const module2 = resolveImport(scope, receiver.from, code.paths);
16986
+ return module2 ? code.forwardsOf(module2, site.name) : [];
16987
+ }
16513
16988
  function claimsOf(implMethod) {
16514
16989
  const claims = [];
16515
16990
  for (const step of implMethod.narrative) {
@@ -16575,6 +17050,7 @@ var callConformanceRule = {
16575
17050
  if (!ctx.componentMap.has(claim.component)) continue;
16576
17051
  const target = resolveCallTarget(ctx, claim.component, claim.method);
16577
17052
  if (target.accepted.has(fnSymbol)) continue;
17053
+ if (isTarget(code.forwardsOf(file, fnSymbol), target)) continue;
16578
17054
  const matching = sites.filter((s) => target.accepted.has(s.name));
16579
17055
  const ref = `${claim.component}.${claim.method}`;
16580
17056
  if (target.files.size === 0) {
@@ -16583,6 +17059,7 @@ var callConformanceRule = {
16583
17059
  }
16584
17060
  continue;
16585
17061
  }
17062
+ if (sites.some((s) => !target.accepted.has(s.name) && isTarget(invokedAs(code, s, file), target))) continue;
16586
17063
  const landed = /* @__PURE__ */ new Set();
16587
17064
  let realized = false;
16588
17065
  for (const site of matching) {
@@ -16690,11 +17167,11 @@ var hiddenStateRule = {
16690
17167
  byPath.set(key, list);
16691
17168
  }
16692
17169
  }
16693
- for (const path29 of code.exactPaths) {
16694
- const facts = code.factsAt(path29);
17170
+ for (const path31 of code.exactPaths) {
17171
+ const facts = code.factsAt(path31);
16695
17172
  const bindings = facts.topLevelMutableBindings ?? [];
16696
17173
  if (bindings.length === 0) continue;
16697
- const mapped = byPath.get(path29) ?? [];
17174
+ const mapped = byPath.get(path31) ?? [];
16698
17175
  if (mapped.length === 0) continue;
16699
17176
  if (!mapped.every((m) => isStatelessLogic(m.component))) continue;
16700
17177
  const anchor = [...mapped].sort((a, b) => a.impl.id.localeCompare(b.impl.id))[0];
@@ -16760,7 +17237,7 @@ var dependencyConformanceRule = {
16760
17237
  }
16761
17238
  return false;
16762
17239
  };
16763
- const draftAt = (path29) => realization.implementationsAt(path29).some((impl) => ctx.isImplementationDraft(impl));
17240
+ const draftAt = (path31) => realization.implementationsAt(path31).some((impl) => ctx.isImplementationDraft(impl));
16764
17241
  for (const fromPath of mappedPaths) {
16765
17242
  const fromComponents = realization.componentsAt(fromPath);
16766
17243
  for (const toPath of graph.importsOf(fromPath)) {
@@ -16845,7 +17322,7 @@ var portalImportsRule = {
16845
17322
  }
16846
17323
  return files;
16847
17324
  };
16848
- const draftAt = (path29) => realization.implementationsAt(path29).some((impl) => ctx.isImplementationDraft(impl));
17325
+ const draftAt = (path31) => realization.implementationsAt(path31).some((impl) => ctx.isImplementationDraft(impl));
16849
17326
  for (const fromPath of mappedPaths) {
16850
17327
  const fromSubsystems = new Set(realization.componentsAt(fromPath).map((c) => c.subsystem));
16851
17328
  for (const toPath of graph.importsOf(fromPath)) {
@@ -17306,7 +17783,7 @@ var paramConformanceRule = {
17306
17783
  codeNameOf.set(type.id, named);
17307
17784
  if (type.subsystem) codeNameOf.set(`${type.subsystem}.${type.id}`, named);
17308
17785
  }
17309
- const normalize3 = (text) => text.replace(/\s+/g, " ").trim();
17786
+ const normalize3 = (text2) => text2.replace(/\s+/g, " ").trim();
17310
17787
  const typeAgrees = (declared, realized) => {
17311
17788
  if (!realized.type) return false;
17312
17789
  const stated = normalize3(declared.type);
@@ -17381,8 +17858,8 @@ var paramConformanceRule = {
17381
17858
  };
17382
17859
 
17383
17860
  // src/core/rules/conformance/route-coverage.ts
17384
- function liesUnder2(path29, prefix) {
17385
- return path29 === prefix || path29.startsWith(prefix + "/");
17861
+ function liesUnder2(path31, prefix) {
17862
+ return path31 === prefix || path31.startsWith(prefix + "/");
17386
17863
  }
17387
17864
  function normalizeSegment(segment) {
17388
17865
  return /^\{.+\}$/.test(segment) || segment.startsWith(":") ? "*" : segment;
@@ -18137,9 +18614,9 @@ function makeMatcher(tech) {
18137
18614
  const joined = parts.join("");
18138
18615
  if (joined.length < 3) return null;
18139
18616
  const n = parts.length;
18140
- return (text) => {
18141
- if (!text) return false;
18142
- const words2 = text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
18617
+ return (text2) => {
18618
+ if (!text2) return false;
18619
+ const words2 = text2.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
18143
18620
  if (words2.some((w) => w.includes(joined))) return true;
18144
18621
  if (n < 2) return false;
18145
18622
  for (let i = 0; i + n <= words2.length; i++) {
@@ -19077,6 +19554,9 @@ function ruleSequence() {
19077
19554
  const audits = [carriedDebtRule, lintAllowsRule].filter((r) => ruleSet.includes(r));
19078
19555
  return [...ruleSet.filter((r) => !audits.includes(r)), ...audits];
19079
19556
  }
19557
+ function specScopedRules() {
19558
+ return ruleSequence().filter((r) => r.scope === "spec");
19559
+ }
19080
19560
  function knownIssueCodes() {
19081
19561
  return listRules().flatMap((r) => r.codes);
19082
19562
  }
@@ -19211,8 +19691,8 @@ LANGUAGE_PATTERNS.cpp = LANGUAGE_PATTERNS.c;
19211
19691
  var PATTERN_ANALYSIS_MAX_BYTES = 1e6;
19212
19692
  var IDENTIFIER_RE = /[A-Za-z_$][\w$]*/g;
19213
19693
  var STRING_RE = /'([^'\\\n]*(?:\\.[^'\\\n]*)*)'|"([^"\\\n]*(?:\\.[^"\\\n]*)*)"|`([^`\\]*(?:\\.[^`\\]*)*)`/g;
19214
- function stripComments(text, patterns) {
19215
- let out = text;
19694
+ function stripComments(text2, patterns) {
19695
+ let out = text2;
19216
19696
  for (const [open, close] of patterns.blockComments) {
19217
19697
  const re = new RegExp(`${escapeRe(open)}[\\s\\S]*?${escapeRe(close)}`, "g");
19218
19698
  out = out.replace(re, " ");
@@ -19238,16 +19718,16 @@ function boundNames(captured, localOnly) {
19238
19718
  }
19239
19719
  return names;
19240
19720
  }
19241
- function collectStrings(text) {
19721
+ function collectStrings(text2) {
19242
19722
  const anchors = /* @__PURE__ */ new Set();
19243
- for (const m of text.matchAll(STRING_RE)) {
19723
+ for (const m of text2.matchAll(STRING_RE)) {
19244
19724
  const value = m[1] ?? m[2] ?? m[3];
19245
19725
  if (value) anchors.add(value);
19246
19726
  }
19247
19727
  return anchors;
19248
19728
  }
19249
- function analyzeWithPatterns(text, patterns) {
19250
- const stripped = stripComments(text, patterns);
19729
+ function analyzeWithPatterns(text2, patterns) {
19730
+ const stripped = stripComments(text2, patterns);
19251
19731
  const declared = /* @__PURE__ */ new Set();
19252
19732
  const exported = /* @__PURE__ */ new Set();
19253
19733
  const imports = /* @__PURE__ */ new Set();
@@ -19275,7 +19755,7 @@ function analyzeWithPatterns(text, patterns) {
19275
19755
  }
19276
19756
  }
19277
19757
  }
19278
- const anchors = collectStrings(text);
19758
+ const anchors = collectStrings(text2);
19279
19759
  for (const m of stripped.matchAll(IDENTIFIER_RE)) anchors.add(m[0]);
19280
19760
  return {
19281
19761
  analysisGrade: "pattern",
@@ -19286,11 +19766,11 @@ function analyzeWithPatterns(text, patterns) {
19286
19766
  reexports: []
19287
19767
  };
19288
19768
  }
19289
- function analyzeGeneric(text) {
19769
+ function analyzeGeneric(text2) {
19290
19770
  const words2 = /* @__PURE__ */ new Set();
19291
- for (const m of text.matchAll(IDENTIFIER_RE)) words2.add(m[0]);
19771
+ for (const m of text2.matchAll(IDENTIFIER_RE)) words2.add(m[0]);
19292
19772
  const anchors = new Set(words2);
19293
- for (const s of collectStrings(text)) anchors.add(s);
19773
+ for (const s of collectStrings(text2)) anchors.add(s);
19294
19774
  return {
19295
19775
  analysisGrade: "generic",
19296
19776
  declaredNames: [...words2],
@@ -19860,14 +20340,14 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
19860
20340
  files.push({ path: sourcePath, status: "unreadable", ...empty });
19861
20341
  continue;
19862
20342
  }
19863
- const text = buffer.toString("utf8");
20343
+ const text2 = buffer.toString("utf8");
19864
20344
  const language = EXTENSION_LANGUAGE[path13.extname(sourcePath).toLowerCase()];
19865
20345
  let analyzed;
19866
20346
  if (language === "typescript" || language === "javascript") {
19867
20347
  const ts = resolveTypeScript(projectRoot);
19868
20348
  if (ts) {
19869
20349
  try {
19870
- const facts = walkExact(ts, text, absolute);
20350
+ const facts = walkExact(ts, text2, absolute);
19871
20351
  chaseReexports(ts, facts, absolute, projectRoot, /* @__PURE__ */ new Set([absolute]), exactCache);
19872
20352
  analyzed = {
19873
20353
  analysisGrade: "exact",
@@ -19876,6 +20356,10 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
19876
20356
  exportedNames: [...facts.exported],
19877
20357
  imports: [...facts.imports],
19878
20358
  reexports: [...facts.reexports],
20359
+ reexportBindings: [
20360
+ ...facts.namedReexports.map(({ exported, local, from }) => ({ exported, local, from })),
20361
+ ...facts.starExports.map((from) => ({ exported: "*", local: "*", from }))
20362
+ ],
19879
20363
  functionComplexity: Object.fromEntries(facts.complexity),
19880
20364
  functionCallSites: Object.fromEntries([...facts.calls].map(([k, v]) => [k, [...v.values()]])),
19881
20365
  importBindings: Object.fromEntries(facts.importBindings),
@@ -19889,20 +20373,159 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
19889
20373
  reexportOnly: facts.reexportOnly
19890
20374
  };
19891
20375
  } catch {
19892
- analyzed = analyzeGeneric(text);
20376
+ analyzed = analyzeGeneric(text2);
19893
20377
  }
19894
20378
  } else {
19895
- analyzed = analyzeWithPatterns(text, LANGUAGE_PATTERNS[language]);
20379
+ analyzed = analyzeWithPatterns(text2, LANGUAGE_PATTERNS[language]);
19896
20380
  }
19897
- } else if (language && LANGUAGE_PATTERNS[language] && text.length <= PATTERN_ANALYSIS_MAX_BYTES) {
19898
- analyzed = analyzeWithPatterns(text, LANGUAGE_PATTERNS[language]);
20381
+ } else if (language && LANGUAGE_PATTERNS[language] && text2.length <= PATTERN_ANALYSIS_MAX_BYTES) {
20382
+ analyzed = analyzeWithPatterns(text2, LANGUAGE_PATTERNS[language]);
19899
20383
  } else {
19900
- analyzed = analyzeGeneric(text);
20384
+ analyzed = analyzeGeneric(text2);
19901
20385
  }
19902
20386
  files.push({ path: sourcePath, status: "analyzed", language, ...analyzed });
19903
20387
  }
19904
20388
  return { files, projectRoot, rootFiles };
19905
20389
  }
20390
+ var IMPORT_FROM_RE = /(?:^|;)[ \t]*import\s+(?!\()([^'";]*?)\s*from\s*['"]([^'"]+)['"]/gm;
20391
+ var DYNAMIC_IMPORT_RE = /\{([^{}]*)\}\s*=\s*await\s+import\(\s*['"]([^'"]+)['"]\s*\)/g;
20392
+ var IDENTIFIER_ONLY_RE = /^[A-Za-z_$][\w$]*$/;
20393
+ function clauseBindings(clause, specifier) {
20394
+ const out = [];
20395
+ const body = clause.replace(/^\s*type\s+/, "");
20396
+ const named = /\{([^}]*)\}/.exec(body);
20397
+ if (named) {
20398
+ for (const piece of named[1].split(",")) {
20399
+ const sides = piece.replace(/^\s*type\s+/, "").split(/\s+as\s+/).map((s) => s.trim());
20400
+ const exported = sides[0];
20401
+ const local = sides[1] ?? exported;
20402
+ if (IDENTIFIER_ONLY_RE.test(exported) && IDENTIFIER_ONLY_RE.test(local)) out.push({ exported, local, specifier });
20403
+ }
20404
+ }
20405
+ const namespace = /\*\s*as\s+([A-Za-z_$][\w$]*)/.exec(body);
20406
+ if (namespace) out.push({ exported: "*", local: namespace[1], specifier });
20407
+ const head = body.replace(/\{[^}]*\}/, "").replace(/\*\s*as\s+[A-Za-z_$][\w$]*/, "");
20408
+ const defaulted = /^\s*([A-Za-z_$][\w$]*)/.exec(head);
20409
+ if (defaulted) out.push({ exported: "default", local: defaulted[1], specifier });
20410
+ return out;
20411
+ }
20412
+ function readTestFiles(testRoots, projectRoot) {
20413
+ const scanned = [];
20414
+ for (const key of walkDeclaredRoots(testRoots, [], projectRoot)) {
20415
+ let text2;
20416
+ try {
20417
+ const buffer = fs10.readFileSync(path13.resolve(projectRoot, key));
20418
+ if (looksBinary(buffer)) continue;
20419
+ text2 = buffer.toString("utf8");
20420
+ } catch {
20421
+ continue;
20422
+ }
20423
+ const imports = /* @__PURE__ */ new Set();
20424
+ for (const m of text2.matchAll(NAMED_IMPORT_BINDINGS_RE)) {
20425
+ for (const name of boundNames(m[1] ?? "", false)) imports.add(name);
20426
+ }
20427
+ const code = stripComments(text2, JS_PATTERNS);
20428
+ const bindings = [];
20429
+ for (const m of code.matchAll(IMPORT_FROM_RE)) bindings.push(...clauseBindings(m[1], m[2]));
20430
+ for (const m of code.matchAll(DYNAMIC_IMPORT_RE)) bindings.push(...clauseBindings(`{${m[1].replace(/:/g, " as ")}}`, m[2]));
20431
+ const words2 = /* @__PURE__ */ new Set();
20432
+ for (const m of text2.matchAll(IDENTIFIER_RE)) words2.add(m[0]);
20433
+ scanned.push({ path: key, imports, bindings, words: words2 });
20434
+ }
20435
+ return scanned;
20436
+ }
20437
+ var NAMED_REEXPORT_RE = /\bexport\s+(?:type\s+)?\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]/g;
20438
+ var STAR_REEXPORT_RE = /\bexport\s+\*\s+from\s*['"]([^'"]+)['"]/g;
20439
+ function reexportReader() {
20440
+ const cache = /* @__PURE__ */ new Map();
20441
+ return (file) => {
20442
+ const cached = cache.get(file);
20443
+ if (cached) return cached;
20444
+ const found = { named: [], stars: [] };
20445
+ try {
20446
+ const code = stripComments(fs10.readFileSync(file, "utf8"), JS_PATTERNS);
20447
+ for (const m of code.matchAll(NAMED_REEXPORT_RE)) {
20448
+ for (const piece of m[1].split(",")) {
20449
+ const sides = piece.replace(/^\s*type\s+/, "").split(/\s+as\s+/).map((s) => s.trim());
20450
+ if (!IDENTIFIER_ONLY_RE.test(sides[0] ?? "")) continue;
20451
+ found.named.push({ local: sides[0], exported: sides[1] ?? sides[0], specifier: m[2] });
20452
+ }
20453
+ }
20454
+ for (const m of code.matchAll(STAR_REEXPORT_RE)) found.stars.push(m[1]);
20455
+ } catch {
20456
+ }
20457
+ cache.set(file, found);
20458
+ return found;
20459
+ };
20460
+ }
20461
+ function publishesFrom(module2, name, target, symbol, readReexports, visited = /* @__PURE__ */ new Set()) {
20462
+ if (path13.resolve(module2) === target && name === symbol) return true;
20463
+ const key = `${module2}#${name}`;
20464
+ if (visited.has(key)) return false;
20465
+ visited.add(key);
20466
+ const reexports = readReexports(module2);
20467
+ for (const re of reexports.named) {
20468
+ if (re.exported !== name) continue;
20469
+ const next = resolveRelativeModule(module2, re.specifier);
20470
+ if (next && publishesFrom(next, re.local, target, symbol, readReexports, visited)) return true;
20471
+ }
20472
+ if (name === "default") return false;
20473
+ for (const specifier of reexports.stars) {
20474
+ const next = resolveRelativeModule(module2, specifier);
20475
+ if (next && publishesFrom(next, name, target, symbol, readReexports, visited)) return true;
20476
+ }
20477
+ return false;
20478
+ }
20479
+ function findTestsReferencing(methods, projectRoot, testRoots = []) {
20480
+ const scanned = readTestFiles(testRoots, projectRoot);
20481
+ if (scanned.length === 0) return [];
20482
+ const indiscriminateAbove = scanned.length / 10;
20483
+ const searches = [];
20484
+ for (const method of methods) {
20485
+ const symbol = method.symbol ?? method.name;
20486
+ let search = searches.find((s) => s.method === method.name && s.symbol === symbol);
20487
+ if (!search) {
20488
+ search = { method: method.name, symbol, files: [] };
20489
+ searches.push(search);
20490
+ }
20491
+ const file = method.sourcePath ? path13.resolve(projectRoot, method.sourcePath) : void 0;
20492
+ if (file && !search.files.includes(file)) search.files.push(file);
20493
+ }
20494
+ const readReexports = reexportReader();
20495
+ const found = [];
20496
+ for (const { method, symbol, files } of searches) {
20497
+ const imported = [];
20498
+ const mentioned = [];
20499
+ for (const test of scanned) {
20500
+ if (files.length === 0) {
20501
+ if (test.imports.has(symbol)) imported.push(test.path);
20502
+ else if (test.words.has(symbol)) mentioned.push(test.path);
20503
+ continue;
20504
+ }
20505
+ const testFile = path13.resolve(projectRoot, test.path);
20506
+ const fromMethod = (binding, name) => {
20507
+ const module2 = resolveRelativeModule(testFile, binding.specifier);
20508
+ return module2 !== null && files.some((target) => publishesFrom(module2, name, target, symbol, readReexports));
20509
+ };
20510
+ const naming = test.bindings.filter((b) => b.exported === symbol || b.local === symbol);
20511
+ if (test.bindings.some((b) => b.exported !== "*" && fromMethod(b, b.exported)) || test.words.has(symbol) && test.bindings.some((b) => b.exported === "*" && fromMethod(b, symbol))) {
20512
+ imported.push(test.path);
20513
+ } else if (naming.length === 0 && test.words.has(symbol)) {
20514
+ mentioned.push(test.path);
20515
+ }
20516
+ }
20517
+ const indiscriminate = mentioned.length > indiscriminateAbove;
20518
+ if (imported.length === 0 && mentioned.length === 0) continue;
20519
+ found.push({
20520
+ method,
20521
+ symbol,
20522
+ imported,
20523
+ mentioned: indiscriminate ? [] : mentioned,
20524
+ indiscriminate
20525
+ });
20526
+ }
20527
+ return found;
20528
+ }
19906
20529
 
19907
20530
  // src/core/rules/index.ts
19908
20531
  function composeRuleSequence(extraRules = []) {
@@ -20275,7 +20898,7 @@ function buildRuleContext(opts) {
20275
20898
  }
20276
20899
  const severity = getRuleSeverity(code, defaultSeverity, isDraftContext, owner);
20277
20900
  if (severity === "off") return;
20278
- let text = message;
20901
+ let text2 = message;
20279
20902
  if (specId) {
20280
20903
  const key = `${specId}\0${code}`;
20281
20904
  let seen = sitesSeen.get(key);
@@ -20293,7 +20916,7 @@ function buildRuleContext(opts) {
20293
20916
  if (grew.length === 0) {
20294
20917
  if (severity === "warning") return;
20295
20918
  } else {
20296
- text = `${text} A lint.allow covers this site, but not ${grew.length} part(s) of it \u2014 ${grew.map((u) => `"${u}"`).join("; ")} ${grew.length === 1 ? "is" : "are"} new. Decide on them: add them to the allow's \`covers\` with a reason that is actually true, or fix them.`;
20919
+ text2 = `${text2} A lint.allow covers this site, but not ${grew.length} part(s) of it \u2014 ${grew.map((u) => `"${u}"`).join("; ")} ${grew.length === 1 ? "is" : "are"} new. Decide on them: add them to the allow's \`covers\` with a reason that is actually true, or fix them.`;
20297
20920
  }
20298
20921
  }
20299
20922
  }
@@ -20308,13 +20931,13 @@ function buildRuleContext(opts) {
20308
20931
  else grew.push(unit);
20309
20932
  }
20310
20933
  if (grew.length === 0) return;
20311
- text = `${text} The conformance debt register carries this finding, but not ${grew.length} part(s) of it \u2014 ${grew.map((u) => `"${u}"`).join("; ")} ${grew.length === 1 ? "is" : "are"} new. Fix them, or add them to the entry's \`covers\` with a reason that is actually true.`;
20934
+ text2 = `${text2} The conformance debt register carries this finding, but not ${grew.length} part(s) of it \u2014 ${grew.map((u) => `"${u}"`).join("; ")} ${grew.length === 1 ? "is" : "are"} new. Fix them, or add them to the entry's \`covers\` with a reason that is actually true.`;
20312
20935
  }
20313
20936
  }
20314
20937
  issues.push({
20315
20938
  severity,
20316
20939
  code,
20317
- message: text,
20940
+ message: text2,
20318
20941
  specId,
20319
20942
  ...isDraftContext ? { draftContext: true } : {},
20320
20943
  ...surfaceResolved ? { surfaceResolved: true } : {}
@@ -20399,6 +21022,54 @@ function buildRuleContext(opts) {
20399
21022
  return ctx;
20400
21023
  }
20401
21024
 
21025
+ // src/core/rules/candidate.ts
21026
+ function stubSystem() {
21027
+ const now = (/* @__PURE__ */ new Date()).toISOString();
21028
+ return {
21029
+ schemaVersion: "1.0.0",
21030
+ name: "candidate",
21031
+ vision: "",
21032
+ boundaries: [],
21033
+ globalRequirements: [],
21034
+ databases: [],
21035
+ createdAt: now,
21036
+ updatedAt: now
21037
+ };
21038
+ }
21039
+ function validateComponentCandidate(candidate, opts = {}) {
21040
+ const issues = [];
21041
+ registerBuiltinRules();
21042
+ if (opts.extensions?.rules?.length) registerPackRules(opts.extensions.rules);
21043
+ const knownCodes = /* @__PURE__ */ new Set([
21044
+ ...knownIssueCodes().map((rc) => rc.code),
21045
+ ...(opts.extensions?.assertions ?? []).map((a) => a.fullCode)
21046
+ ]);
21047
+ const ctx = buildRuleContext({
21048
+ system: stubSystem(),
21049
+ subsystems: [],
21050
+ components: [candidate],
21051
+ interfaces: [],
21052
+ implementations: [],
21053
+ types: [],
21054
+ rules: opts.rules,
21055
+ projectType: opts.projectType ?? "backend",
21056
+ extensions: opts.extensions,
21057
+ // Only a tree-scoped rule reads these (roundtrip-serialization); no
21058
+ // spec-scoped rule does, so a candidate carries none.
21059
+ roundTripIssues: [],
21060
+ knownIssueCodes: knownCodes,
21061
+ issues
21062
+ });
21063
+ for (const rule of specScopedRules()) {
21064
+ rule.check(ctx);
21065
+ }
21066
+ const own = issues.filter((i) => !i.specId || i.specId === candidate.id);
21067
+ return {
21068
+ errors: own.filter((i) => i.severity === "error"),
21069
+ warnings: own.filter((i) => i.severity === "warning")
21070
+ };
21071
+ }
21072
+
20402
21073
  // src/core/validation.ts
20403
21074
  var path14 = __toESM(require("path"));
20404
21075
  function projectPackSelections() {
@@ -20408,6 +21079,9 @@ function projectPackSelections() {
20408
21079
  return [];
20409
21080
  }
20410
21081
  }
21082
+ function findTestsReferencing2(methods, projectRoot, testRoots = []) {
21083
+ return findTestsReferencing(methods, projectRoot, testRoots);
21084
+ }
20411
21085
  var RESOLUTION_FAILURE_CODES = /* @__PURE__ */ new Set([
20412
21086
  "UNDEFINED_TYPE_REFERENCE",
20413
21087
  "INVALID_DEPENDENCY_REFERENCE",
@@ -20446,6 +21120,67 @@ function stricterSeverities(parent, child) {
20446
21120
  function issue(severity, code, message, agentId, specId) {
20447
21121
  return { severity, code, message, agentId, specId };
20448
21122
  }
21123
+ function validateRegistry(registry, rules) {
21124
+ const issues = [];
21125
+ const idCounts = /* @__PURE__ */ new Map();
21126
+ for (const agent of registry.agents) {
21127
+ idCounts.set(agent.id, (idCounts.get(agent.id) ?? 0) + 1);
21128
+ }
21129
+ for (const [id, count] of idCounts) {
21130
+ if (count > 1) {
21131
+ issues.push(issue("error", "DUPLICATE_AGENT_ID", `Duplicate agent id: "${id}"`, id));
21132
+ }
21133
+ }
21134
+ for (const agent of registry.agents) {
21135
+ validateAgent(agent, rules, issues);
21136
+ }
21137
+ if (rules.noOverlappingOwnership) {
21138
+ checkOverlappingOwnership(registry.agents, issues);
21139
+ }
21140
+ return {
21141
+ valid: issues.every((i) => i.severity !== "error"),
21142
+ issues
21143
+ };
21144
+ }
21145
+ function validateAgent(agent, rules, issues) {
21146
+ const isMeta = agent.tags.some((t) => rules.metaAgentTags.includes(t));
21147
+ if (rules.requireOwnedPaths && !isMeta && agent.ownedPaths.length === 0) {
21148
+ issues.push(
21149
+ issue(
21150
+ "warning",
21151
+ "NO_OWNED_PATHS",
21152
+ `Agent "${agent.id}" has no ownedPaths. Add paths or tag as meta/guardian.`,
21153
+ agent.id
21154
+ )
21155
+ );
21156
+ }
21157
+ if (agent.targets.length === 0) {
21158
+ issues.push(
21159
+ issue("warning", "NO_TARGETS", `Agent "${agent.id}" has no output targets configured.`, agent.id)
21160
+ );
21161
+ }
21162
+ }
21163
+ function checkOverlappingOwnership(agents, issues) {
21164
+ const pathToAgents = /* @__PURE__ */ new Map();
21165
+ for (const agent of agents) {
21166
+ for (const p of agent.ownedPaths) {
21167
+ const owners = pathToAgents.get(p) ?? [];
21168
+ owners.push(agent.id);
21169
+ pathToAgents.set(p, owners);
21170
+ }
21171
+ }
21172
+ for (const [p, owners] of pathToAgents) {
21173
+ if (owners.length > 1) {
21174
+ issues.push(
21175
+ issue(
21176
+ "error",
21177
+ "OVERLAPPING_OWNERSHIP",
21178
+ `Path "${p}" is claimed by multiple agents: ${owners.join(", ")}`
21179
+ )
21180
+ );
21181
+ }
21182
+ }
21183
+ }
20449
21184
  function validateSddTree(rulesOrOptions, projectType = "backend") {
20450
21185
  let rules = rulesOrOptions;
20451
21186
  let scopeSubsystem;
@@ -20631,16 +21366,16 @@ function resolveThroughParent(boundRoot, treatAllAsComplete, childRules) {
20631
21366
  }));
20632
21367
  return { root: top, scope, issues };
20633
21368
  }
20634
- function stripNamespace(text, scope) {
21369
+ function stripNamespace(text2, scope) {
20635
21370
  const prefix = `${scope}::`;
20636
21371
  let out = "";
20637
21372
  let from = 0;
20638
- for (let at = text.indexOf(prefix); at !== -1; at = text.indexOf(prefix, from)) {
20639
- const atBoundary = at === 0 || !isIdChar(text[at - 1]);
20640
- out += text.slice(from, at) + (atBoundary ? "" : prefix);
21373
+ for (let at = text2.indexOf(prefix); at !== -1; at = text2.indexOf(prefix, from)) {
21374
+ const atBoundary = at === 0 || !isIdChar(text2[at - 1]);
21375
+ out += text2.slice(from, at) + (atBoundary ? "" : prefix);
20641
21376
  from = at + prefix.length;
20642
21377
  }
20643
- return out + text.slice(from);
21378
+ return out + text2.slice(from);
20644
21379
  }
20645
21380
  function isIdChar(ch) {
20646
21381
  return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch >= "0" && ch <= "9" || ch === "_" || ch === "-" || ch === "." || ch === ":";
@@ -20833,11 +21568,11 @@ var IdPool = class {
20833
21568
  return candidate;
20834
21569
  }
20835
21570
  };
20836
- function escapeLabel(text) {
20837
- return text.replace(/"/g, "#quot;");
21571
+ function escapeLabel(text2) {
21572
+ return text2.replace(/"/g, "#quot;");
20838
21573
  }
20839
- function truncate(text, max) {
20840
- const clean = text.replace(/\s+/g, " ").trim();
21574
+ function truncate(text2, max) {
21575
+ const clean = text2.replace(/\s+/g, " ").trim();
20841
21576
  return clean.length <= max ? clean : `${clean.slice(0, max - 1)}\u2026`;
20842
21577
  }
20843
21578
  function nodeDecl(id, label, comp) {
@@ -21684,8 +22419,8 @@ function rewriteSpecRefs(raw, remap) {
21684
22419
  }
21685
22420
  function summarizeValue(value) {
21686
22421
  if (value === void 0) return "";
21687
- const text = typeof value === "string" ? value : JSON.stringify(value) ?? String(value);
21688
- return text.length > 120 ? `${text.slice(0, 117)}...` : text;
22422
+ const text2 = typeof value === "string" ? value : JSON.stringify(value) ?? String(value);
22423
+ return text2.length > 120 ? `${text2.slice(0, 117)}...` : text2;
21689
22424
  }
21690
22425
  function identityKeyOf(field, item) {
21691
22426
  if (item === null || typeof item !== "object") return null;
@@ -21760,6 +22495,9 @@ function specChanges(before, after, prefix = "") {
21760
22495
  if (after.length === 0 && before.length > 0) {
21761
22496
  return [{ path: prefix, change: "cleared", before: summarizeValue(before) }];
21762
22497
  }
22498
+ if ((prefix.split(".").pop() ?? prefix) === "narrative" && [...before, ...after].every((i) => i !== null && typeof i === "object" && !Array.isArray(i))) {
22499
+ return narrativeChanges(before, after, prefix);
22500
+ }
21763
22501
  const identified = [...before, ...after].every((i) => reportIdentityOf(prefix.split(".").pop() ?? prefix, i) !== null);
21764
22502
  if (!identified) {
21765
22503
  return JSON.stringify(before) === JSON.stringify(after) ? [] : [{ path: prefix, change: "set", before: summarizeValue(before), after: summarizeValue(after) }];
@@ -21793,6 +22531,152 @@ function specChanges(before, after, prefix = "") {
21793
22531
  if (JSON.stringify(before) === JSON.stringify(after)) return [];
21794
22532
  return [{ path: prefix, change: "set", before: summarizeValue(before), after: summarizeValue(after) }];
21795
22533
  }
22534
+ var STEP_JUMP_FIELDS = ["onTrueStep", "onFalseStep", "defaultStep", "endStep", "finallyStep", "toStep"];
22535
+ var STEP_JUMP_LISTS = [["cases", "value"], ["catches", "error"], ["branches", "name"]];
22536
+ function jumpsOf(step) {
22537
+ const jumps = /* @__PURE__ */ new Map();
22538
+ for (const field of STEP_JUMP_FIELDS) {
22539
+ if (typeof step[field] === "number") jumps.set(field, step[field]);
22540
+ }
22541
+ for (const [list, name] of STEP_JUMP_LISTS) {
22542
+ const entries = step[list];
22543
+ if (!Array.isArray(entries)) continue;
22544
+ entries.forEach((entry, i) => {
22545
+ if (entry && typeof entry.step === "number") jumps.set(`${list}.${String(entry[name] ?? i)}.step`, entry.step);
22546
+ });
22547
+ }
22548
+ return jumps;
22549
+ }
22550
+ function withoutNumbers(step) {
22551
+ const out = { ...step };
22552
+ delete out.stepNumber;
22553
+ for (const field of STEP_JUMP_FIELDS) delete out[field];
22554
+ for (const [list] of STEP_JUMP_LISTS) {
22555
+ if (Array.isArray(out[list])) {
22556
+ out[list] = out[list].map((entry) => {
22557
+ if (!entry || typeof entry !== "object") return entry;
22558
+ const { step: _step, ...rest } = entry;
22559
+ return rest;
22560
+ });
22561
+ }
22562
+ }
22563
+ return out;
22564
+ }
22565
+ function canonicalJson(value) {
22566
+ return JSON.stringify(value, (_key, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, v[k]])) : v);
22567
+ }
22568
+ function commonRun(a, b, same) {
22569
+ const rows = a.length + 1;
22570
+ const cols = b.length + 1;
22571
+ const table = new Uint32Array(rows * cols);
22572
+ for (let i2 = a.length - 1; i2 >= 0; i2--) {
22573
+ for (let j2 = b.length - 1; j2 >= 0; j2--) {
22574
+ table[i2 * cols + j2] = same(a[i2], b[j2]) ? table[(i2 + 1) * cols + j2 + 1] + 1 : Math.max(table[(i2 + 1) * cols + j2], table[i2 * cols + j2 + 1]);
22575
+ }
22576
+ }
22577
+ const pairs = [];
22578
+ let i = 0;
22579
+ let j = 0;
22580
+ while (i < a.length && j < b.length) {
22581
+ if (same(a[i], b[j])) {
22582
+ pairs.push([i, j]);
22583
+ i++;
22584
+ j++;
22585
+ } else if (table[(i + 1) * cols + j] >= table[i * cols + j + 1]) i++;
22586
+ else j++;
22587
+ }
22588
+ return pairs;
22589
+ }
22590
+ function recognisablySame(a, b) {
22591
+ if (typeof a.description === "string" && a.description === b.description) return true;
22592
+ if (typeof a.label === "string" && a.label === b.label) return true;
22593
+ return typeof a.targetComponent === "string" && a.type === b.type && a.targetComponent === b.targetComponent && a.targetMethod === b.targetMethod;
22594
+ }
22595
+ function pairSteps(before, after) {
22596
+ const keysBefore = before.map((s) => canonicalJson(withoutNumbers(s)));
22597
+ const keysAfter = after.map((s) => canonicalJson(withoutNumbers(s)));
22598
+ const anchors = commonRun(keysBefore, keysAfter, (x, y) => x === y);
22599
+ const pairs = [];
22600
+ const bounds = [[-1, -1], ...anchors, [before.length, after.length]];
22601
+ for (let g = 0; g < bounds.length - 1; g++) {
22602
+ const [oi, ni] = bounds[g];
22603
+ const [oj, nj] = bounds[g + 1];
22604
+ const gapBefore = before.slice(oi + 1, oj).map((step, k) => ({ step, at: oi + 1 + k }));
22605
+ const gapAfter = after.slice(ni + 1, nj).map((step, k) => ({ step, at: ni + 1 + k }));
22606
+ const similar = commonRun(gapBefore, gapAfter, (x, y) => recognisablySame(x.step, y.step)).map(([x, y]) => [gapBefore[x].at, gapAfter[y].at]);
22607
+ const inner = [[oi, ni], ...similar, [oj, nj]];
22608
+ for (let k = 0; k < inner.length - 1; k++) {
22609
+ const [a0, b0] = inner[k];
22610
+ const [a1, b1] = inner[k + 1];
22611
+ if (a1 - a0 === b1 - b0) for (let d = 1; d < a1 - a0; d++) pairs.push([a0 + d, b0 + d]);
22612
+ if (k < inner.length - 2) pairs.push(inner[k + 1]);
22613
+ }
22614
+ if (g < bounds.length - 2) pairs.push(bounds[g + 1]);
22615
+ }
22616
+ return pairs.sort((x, y) => x[0] - y[0]);
22617
+ }
22618
+ function numberOf(step, index) {
22619
+ return typeof step.stepNumber === "number" ? step.stepNumber : index + 1;
22620
+ }
22621
+ function renumberedRuns(moves) {
22622
+ const runs = [];
22623
+ const range = (a, b) => a === b ? `step ${a}` : `steps ${a}-${b}`;
22624
+ let start = 0;
22625
+ for (let k = 1; k <= moves.length; k++) {
22626
+ const continues = k < moves.length && moves[k][0] === moves[k - 1][0] + 1 && moves[k][1] - moves[k][0] === moves[start][1] - moves[start][0];
22627
+ if (continues) continue;
22628
+ const [firstOld, firstNew] = moves[start];
22629
+ const [lastOld, lastNew] = moves[k - 1];
22630
+ runs.push({ from: range(firstOld, lastOld), to: range(firstNew, lastNew) });
22631
+ start = k;
22632
+ }
22633
+ return runs;
22634
+ }
22635
+ function narrativeChanges(before, after, prefix) {
22636
+ const pairs = pairSteps(before, after);
22637
+ const oldToNew = /* @__PURE__ */ new Map();
22638
+ for (const [o, n] of pairs) oldToNew.set(numberOf(before[o], o), numberOf(after[n], n));
22639
+ const pairedOld = new Set(pairs.map(([o]) => o));
22640
+ const pairedNew = new Set(pairs.map(([, n]) => n));
22641
+ const removed = before.flatMap((step, o) => pairedOld.has(o) ? [] : [{
22642
+ path: `${prefix}.step ${numberOf(step, o)}`,
22643
+ change: "removed",
22644
+ before: summarizeValue(step)
22645
+ }]);
22646
+ const added = after.flatMap((step, n) => pairedNew.has(n) ? [] : [{
22647
+ path: `${prefix}.step ${numberOf(step, n)}`,
22648
+ change: "added",
22649
+ after: summarizeValue(step)
22650
+ }]);
22651
+ const edited = [];
22652
+ const relocated = [];
22653
+ const moves = [];
22654
+ for (const [o, n] of pairs) {
22655
+ const oldNumber = numberOf(before[o], o);
22656
+ const newNumber = numberOf(after[n], n);
22657
+ const at = oldNumber === newNumber ? `${prefix}.step ${newNumber}` : `${prefix}.step ${newNumber} (was ${oldNumber})`;
22658
+ edited.push(...specChanges(withoutNumbers(before[o]), withoutNumbers(after[n]), at));
22659
+ const oldJumps = jumpsOf(before[o]);
22660
+ const newJumps = jumpsOf(after[n]);
22661
+ for (const field of /* @__PURE__ */ new Set([...oldJumps.keys(), ...newJumps.keys()])) {
22662
+ const was = oldJumps.get(field);
22663
+ const now = newJumps.get(field);
22664
+ if (was === now) continue;
22665
+ if (was === void 0) edited.push({ path: `${at}.${field}`, change: "added", after: String(now) });
22666
+ else if (now === void 0) edited.push({ path: `${at}.${field}`, change: "removed", before: String(was) });
22667
+ else if (oldToNew.get(was) === now) relocated.push({ path: `${at}.${field}`, change: "relocated", before: String(was), after: String(now) });
22668
+ else edited.push({ path: `${at}.${field}`, change: "set", before: String(was), after: String(now) });
22669
+ }
22670
+ if (oldNumber !== newNumber && !edited.some((c) => c.path.startsWith(`${at}.`))) moves.push([oldNumber, newNumber]);
22671
+ }
22672
+ const renumbered = renumberedRuns(moves).map(({ from, to }) => ({
22673
+ path: prefix,
22674
+ change: "renumbered",
22675
+ before: from,
22676
+ after: to
22677
+ }));
22678
+ return [...removed, ...added, ...edited, ...renumbered, ...relocated];
22679
+ }
21796
22680
  var DELTA_VERBS = /* @__PURE__ */ new Set(["action", "remove", "captureJumps"]);
21797
22681
  var JUMP_LABEL_TWINS = /* @__PURE__ */ new Set([
21798
22682
  "onTrueLabel",
@@ -21850,13 +22734,13 @@ function ineffectiveDeltaPaths(delta, merged, stored) {
21850
22734
  const isPlain = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
21851
22735
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
21852
22736
  const at = (prefix, key) => prefix ? `${prefix}.${key}` : key;
21853
- const say = (path29, why) => {
21854
- out.push(`${path29} \u2014 ${why}`);
22737
+ const say = (path31, why) => {
22738
+ out.push(`${path31} \u2014 ${why}`);
21855
22739
  };
21856
- const walkValue = (d, m, s, path29, field) => {
22740
+ const walkValue = (d, m, s, path31, field) => {
21857
22741
  if (Array.isArray(d)) {
21858
22742
  if (!Array.isArray(m)) {
21859
- say(path29, "the level does not have this field, so the write dropped it");
22743
+ say(path31, "the level does not have this field, so the write dropped it");
21860
22744
  return;
21861
22745
  }
21862
22746
  if (field === "narrative" && d.some(carriesEditMarker)) return;
@@ -21864,32 +22748,32 @@ function ineffectiveDeltaPaths(delta, merged, stored) {
21864
22748
  if (carriesEditMarker(element)) continue;
21865
22749
  const key = reportIdentityOf(field, element);
21866
22750
  if (key === null) {
21867
- if (!same(m, d)) say(path29, "the level does not keep this value, so the write dropped it");
21868
- else if (same(s, d)) say(path29, "the stored spec already held this value");
22751
+ if (!same(m, d)) say(path31, "the level does not keep this value, so the write dropped it");
22752
+ else if (same(s, d)) say(path31, "the stored spec already held this value");
21869
22753
  return;
21870
22754
  }
21871
22755
  const mMatch = m.find((i) => reportIdentityOf(field, i) === key);
21872
22756
  const sMatch = Array.isArray(s) ? s.find((i) => reportIdentityOf(field, i) === key) : void 0;
21873
22757
  if (mMatch === void 0) {
21874
- say(at(path29, key), "the write did not store this element");
22758
+ say(at(path31, key), "the write did not store this element");
21875
22759
  continue;
21876
22760
  }
21877
- walk2(element, mMatch, sMatch, at(path29, key), field);
22761
+ walk2(element, mMatch, sMatch, at(path31, key), field);
21878
22762
  }
21879
22763
  return;
21880
22764
  }
21881
22765
  if (isPlain(d)) {
21882
22766
  if (!isPlain(m)) {
21883
- say(path29, "the level does not have this field, so the write dropped it");
22767
+ say(path31, "the level does not have this field, so the write dropped it");
21884
22768
  return;
21885
22769
  }
21886
- walk2(d, m, s, path29, field);
22770
+ walk2(d, m, s, path31, field);
21887
22771
  return;
21888
22772
  }
21889
- if (!same(m, d)) say(path29, "the write did not store it \u2014 the level's schema does not keep this value");
21890
- else if (same(s, d)) say(path29, "the stored spec already held this value");
22773
+ if (!same(m, d)) say(path31, "the write did not store it \u2014 the level's schema does not keep this value");
22774
+ else if (same(s, d)) say(path31, "the stored spec already held this value");
21891
22775
  };
21892
- function walk2(d, m, s, path29, container) {
22776
+ function walk2(d, m, s, path31, container) {
21893
22777
  for (const [key, value] of Object.entries(d)) {
21894
22778
  if (DELTA_VERBS.has(key) || JUMP_LABEL_TWINS.has(key)) continue;
21895
22779
  if (key === "label" && LABEL_TWIN_CONTAINERS.has(container)) continue;
@@ -21899,13 +22783,13 @@ function ineffectiveDeltaPaths(delta, merged, stored) {
21899
22783
  if (typeof name !== "string") continue;
21900
22784
  const had = isPlain(s) && s[name] !== void 0;
21901
22785
  const still = isPlain(m) && m[name] !== void 0;
21902
- if (!had) say(at(path29, name), "unset named a field the stored spec did not have");
21903
- else if (still) say(at(path29, name), "unset did not remove it");
22786
+ if (!had) say(at(path31, name), "unset named a field the stored spec did not have");
22787
+ else if (still) say(at(path31, name), "unset did not remove it");
21904
22788
  }
21905
22789
  continue;
21906
22790
  }
21907
22791
  if (value === void 0 || value === null) continue;
21908
- const here = at(path29, key);
22792
+ const here = at(path31, key);
21909
22793
  const mValue = isPlain(m) ? m[key] : void 0;
21910
22794
  const sValue = isPlain(s) ? s[key] : void 0;
21911
22795
  if (mValue === void 0) {
@@ -23756,14 +24640,14 @@ var SpecWorkspace = class {
23756
24640
  - ${labelErrors.join("\n- ")}`);
23757
24641
  }
23758
24642
  }
23759
- const canonical = (spec) => {
24643
+ const canonical2 = (spec) => {
23760
24644
  const parsed = deltaSchema.safeParse(spec);
23761
24645
  const value = { ...parsed.success ? parsed.data : spec };
23762
24646
  delete value.updatedAt;
23763
24647
  return value;
23764
24648
  };
23765
- const canonicalStored = canonical(storedBefore);
23766
- const canonicalMerged = canonical(mergedResult);
24649
+ const canonicalStored = canonical2(storedBefore);
24650
+ const canonicalMerged = canonical2(mergedResult);
23767
24651
  const changes = specChanges(canonicalStored, canonicalMerged);
23768
24652
  const ineffective = ineffectiveDeltaPaths(
23769
24653
  { ...qualifiedDelta, ...unsetFields.length ? { unset: unsetFields } : {} },
@@ -23859,6 +24743,8 @@ var SpecWorkspace = class {
23859
24743
  const plan = this.planMethodMove(source, to, methods, contracts);
23860
24744
  const repointed = this.repointToNewHome(from, to, methods, plan);
23861
24745
  const mentions = this.moveMentions(from, plan);
24746
+ const rename = moveRename(from, to, methods, plan);
24747
+ this.rekeyMoveAllows(plan, rename);
23862
24748
  if (hooks) {
23863
24749
  const verdict = judgeMoveResult(hooks, plan);
23864
24750
  plan.notices.push(...verdict.notices);
@@ -23876,10 +24762,13 @@ var SpecWorkspace = class {
23876
24762
  notices: plan.notices,
23877
24763
  summary: refusedMoveSummary(from, to, methods, verdict.refusals, alternatives),
23878
24764
  dryRun,
23879
- mentions
24765
+ mentions,
24766
+ created: [],
24767
+ carried: []
23880
24768
  };
23881
24769
  }
23882
24770
  }
24771
+ const carried = projectConfigRepository.rekeyCarried(rename, true);
23883
24772
  if (dryRun) {
23884
24773
  const edits2 = moveChangeReports([...plan.edits.values()], true);
23885
24774
  return {
@@ -23892,12 +24781,16 @@ var SpecWorkspace = class {
23892
24781
  refusals: [],
23893
24782
  alternatives: [],
23894
24783
  notices: plan.notices,
23895
- summary: `Dry run: moving ${namedMethods(methods)} from "${from}" to "${to}" would touch ${edits2.length} spec${edits2.length === 1 ? "" : "s"} and re-point ${repointed} reference${repointed === 1 ? "" : "s"}. Nothing was written.`,
24784
+ summary: `Dry run: moving ${namedMethods(methods)} from "${from}" to "${to}" would touch ${edits2.length} spec${edits2.length === 1 ? "" : "s"}${createdClause(plan.created, "would create")} and re-point ${repointed} reference${repointed === 1 ? "" : "s"}${registerClause(carried, "would rekey")}. Nothing was written.`,
23896
24785
  dryRun: true,
23897
- mentions
24786
+ mentions,
24787
+ created: plan.created,
24788
+ carried
23898
24789
  };
23899
24790
  }
23900
- const edits = this.writeMoveEdits([...plan.edits.values()]);
24791
+ const edits = this.writeMoveEdits([...plan.edits.values()], () => {
24792
+ projectConfigRepository.rekeyCarried(rename);
24793
+ });
23901
24794
  return {
23902
24795
  moved: true,
23903
24796
  from,
@@ -23908,11 +24801,49 @@ var SpecWorkspace = class {
23908
24801
  refusals: [],
23909
24802
  alternatives: [],
23910
24803
  notices: plan.notices,
23911
- summary: `Moved ${namedMethods(methods)} from "${from}" to "${to}": ${edits.length} spec${edits.length === 1 ? "" : "s"} written, ${repointed} reference${repointed === 1 ? "" : "s"} re-pointed${mentions.length ? `, ${mentions.length} still naming the old home in prose or on the wire` : ""}.`,
24804
+ summary: `Moved ${namedMethods(methods)} from "${from}" to "${to}": ${edits.length} spec${edits.length === 1 ? "" : "s"} written${createdClause(plan.created, "created")}, ${repointed} reference${repointed === 1 ? "" : "s"} re-pointed${registerClause(carried, "rekeyed")}${mentions.length ? `, ${mentions.length} still naming the old home in prose or on the wire` : ""}.`,
23912
24805
  dryRun: false,
23913
- mentions
24806
+ mentions,
24807
+ created: plan.created,
24808
+ carried
23914
24809
  };
23915
24810
  }
24811
+ /**
24812
+ * Rekey every lint allow keyed on a moved method, staging each spec it
24813
+ * changes like any other reference the move follows. An allow anchored at a
24814
+ * moved method on a spec the method left moves to the spec it arrived in; a
24815
+ * covered `<from>.<method>` unit anywhere names `<to>.<method>`. An allow
24816
+ * whose new home the plan does not stage stays where it is.
24817
+ */
24818
+ rekeyMoveAllows(plan, rename) {
24819
+ const subjects = [...this.everySpecInTree()];
24820
+ for (const edit of plan.edits.values()) if (edit.before === null) subjects.push({ kind: edit.kind, id: edit.id, spec: edit.after });
24821
+ const stagedAfter = (kind, id) => plan.edits.get(`${kind}:${id}`)?.after;
24822
+ for (const { kind, id, spec } of subjects) {
24823
+ if (id.includes("::")) continue;
24824
+ const allows = spec?.lint?.allow ?? [];
24825
+ if (allows.length === 0) continue;
24826
+ const staged = plan.edits.get(`${kind}:${id}`);
24827
+ const subject = staged ? staged.after : cloneSpec(spec);
24828
+ const kept = [];
24829
+ let changed = false;
24830
+ for (const allow of subject.lint?.allow ?? []) {
24831
+ const next = rekeyAnchor(rename, { spec: id, ...allow.at !== void 0 ? { at: allow.at } : {}, ...allow.covers ? { covers: allow.covers } : {} });
24832
+ const rekeyed = { ...allow, ...next.at !== void 0 ? { at: next.at } : {}, ...next.covers ? { covers: next.covers } : {} };
24833
+ if (JSON.stringify(rekeyed) !== JSON.stringify(allow)) changed = true;
24834
+ const home = next.spec !== id ? stagedAfter(kind, next.spec) : void 0;
24835
+ if (home) {
24836
+ home.lint = { ...home.lint ?? {}, allow: [...home.lint?.allow ?? [], rekeyed] };
24837
+ changed = true;
24838
+ continue;
24839
+ }
24840
+ kept.push(rekeyed);
24841
+ }
24842
+ if (!changed) continue;
24843
+ subject.lint = { ...subject.lint, allow: kept };
24844
+ if (!staged) this.stageMoveEdit(plan, kind, id, spec, subject);
24845
+ }
24846
+ }
23916
24847
  /**
23917
24848
  * Why this move cannot be attempted at all, or null when the request is
23918
24849
  * answerable. Every check here is about what the CALLER named, so its answer
@@ -23943,15 +24874,26 @@ var SpecWorkspace = class {
23943
24874
  }
23944
24875
  const receiving = this.moveTargetContract(to, ofTarget);
23945
24876
  if (typeof receiving === "string") return receiving;
23946
- const travelling = this.loadImplementationSpecs().some((impl) => ofSource.some((i) => i.id === impl.contract) && impl.methods.some((m) => methods.includes(m.name)));
23947
- if (travelling && !this.loadImplementationSpecs().some((i) => i.contract === receiving.id)) {
23948
- return `no implementation realizes "${receiving.id}", so the moved methods' narratives would have nowhere to arrive.`;
24877
+ const contractId = receiving?.id ?? `i${to}`;
24878
+ const holder = contracts.find((i) => i.id === contractId);
24879
+ if (!receiving && holder) {
24880
+ return `"${to}" declares no contract, and the one the move would create for it, "${contractId}", is already the id of "${holder.component}"'s contract \u2014 rename that contract, or give "${to}" its own first.`;
24881
+ }
24882
+ const implementations = this.loadImplementationSpecs();
24883
+ const travelling = implementations.some((impl) => ofSource.some((i) => i.id === impl.contract) && impl.methods.some((m) => methods.includes(m.name)));
24884
+ const implId = `${to}_impl`;
24885
+ const implHolder = implementations.find((i) => i.id === implId);
24886
+ if (travelling && !implementations.some((i) => i.contract === contractId) && implHolder) {
24887
+ return `no implementation realizes "${contractId}", and the one the move would create for it, "${implId}", is already the id of the implementation of "${implHolder.contract}" \u2014 rename it, or realize "${contractId}" first.`;
23949
24888
  }
23950
24889
  return null;
23951
24890
  }
23952
- /** The contract of `to` that receives the methods, or why that cannot be decided. */
24891
+ /**
24892
+ * The contract of `to` that receives the methods; null when it declares none
24893
+ * yet, so the move creates `i<to>`; or why the choice cannot be decided.
24894
+ */
23953
24895
  moveTargetContract(to, ofTarget) {
23954
- if (ofTarget.length === 0) return `"${to}" declares no contract, so the methods would have nowhere to arrive.`;
24896
+ if (ofTarget.length === 0) return null;
23955
24897
  if (ofTarget.length === 1) return ofTarget[0];
23956
24898
  const named = ofTarget.find((i) => i.id === `i${to}` || i.id.endsWith(`::i${to}`));
23957
24899
  return named ?? `"${to}" declares ${ofTarget.length} contracts and none is named "i${to}", so which one receives the methods is not decidable.`;
@@ -23964,7 +24906,7 @@ var SpecWorkspace = class {
23964
24906
  */
23965
24907
  planMethodMove(source, to, methods, contracts) {
23966
24908
  const target = this.loadComponentSpec(to);
23967
- const receiving = this.moveTargetContract(to, contracts.filter((i) => i.component === to));
24909
+ const stated = this.moveTargetContract(to, contracts.filter((i) => i.component === to));
23968
24910
  const implementations = this.loadImplementationSpecs();
23969
24911
  const plan = {
23970
24912
  edits: /* @__PURE__ */ new Map(),
@@ -23973,10 +24915,15 @@ var SpecWorkspace = class {
23973
24915
  reach: [],
23974
24916
  newDependencies: [],
23975
24917
  wireBound: [],
23976
- notices: []
24918
+ notices: [],
24919
+ created: [],
24920
+ rehomed: []
23977
24921
  };
24922
+ const leftImplementations = [];
24923
+ const receiving = stated ?? createdContract(target, source.id, methods, contracts);
23978
24924
  const arriving = [];
23979
24925
  const travelling = [];
24926
+ const realizedIn = /* @__PURE__ */ new Map();
23980
24927
  for (const contract of contracts.filter((i) => i.component === source.id)) {
23981
24928
  const leaving = contract.methods.filter((m) => methods.includes(m.name));
23982
24929
  if (leaving.length === 0) continue;
@@ -23984,6 +24931,7 @@ var SpecWorkspace = class {
23984
24931
  const remaining = cloneSpec(contract);
23985
24932
  remaining.methods = remaining.methods.filter((m) => !methods.includes(m.name));
23986
24933
  this.stageMoveEdit(plan, "interface", contract.id, contract, remaining);
24934
+ plan.rehomed.push({ from: contract.id, to: receiving.id });
23987
24935
  if (contract.methods.length === leaving.length) {
23988
24936
  plan.notices.push(`"${contract.id}" is left with no methods \u2014 "${source.id}" no longer declares anything on it.`);
23989
24937
  }
@@ -23991,17 +24939,20 @@ var SpecWorkspace = class {
23991
24939
  const moving = impl.methods.filter((m) => methods.includes(m.name));
23992
24940
  if (moving.length === 0) continue;
23993
24941
  travelling.push(...cloneSpec(moving));
24942
+ for (const entry of moving) if (impl.sourcePath) realizedIn.set(entry.name, impl.sourcePath);
23994
24943
  const kept = cloneSpec(impl);
23995
24944
  kept.methods = kept.methods.filter((m) => !methods.includes(m.name));
23996
24945
  this.stageMoveEdit(plan, "implementation", impl.id, impl, kept);
24946
+ leftImplementations.push(impl.id);
23997
24947
  }
23998
24948
  }
23999
- this.stageMoveEdit(plan, "interface", receiving.id, receiving, {
24949
+ this.stageMoveEdit(plan, "interface", receiving.id, stated, {
24000
24950
  ...cloneSpec(receiving),
24001
24951
  methods: [...cloneSpec(receiving.methods), ...arriving]
24002
24952
  });
24953
+ if (!stated) plan.created.push(`interface "${receiving.id}"`);
24003
24954
  if (arriving.some((m) => m.endpoint)) plan.wireBound.push(receiving.id);
24004
- if (receiving.status !== void 0 && receiving.status !== statusOfContracts(contracts, source.id)) {
24955
+ if (stated && receiving.status !== void 0 && receiving.status !== statusOfContracts(contracts, source.id)) {
24005
24956
  plan.notices.push(`"${receiving.id}" is ${receiving.status} where the methods came from a ${statusOfContracts(contracts, source.id)} contract \u2014 the status of a moved method is the status of its new home.`);
24006
24957
  }
24007
24958
  for (const entry of travelling) {
@@ -24011,10 +24962,23 @@ var SpecWorkspace = class {
24011
24962
  }
24012
24963
  const targetImpl = implementations.find((i) => i.contract === receiving.id);
24013
24964
  if (travelling.length > 0 && targetImpl) {
24965
+ for (const entry of travelling) {
24966
+ const file = realizedIn.get(entry.name);
24967
+ if (entry.sourcePath || !file || file === targetImpl.sourcePath) continue;
24968
+ entry.sourcePath = file;
24969
+ plan.notices.push(`"${entry.name}" keeps ${file} as its own sourcePath: it was realized there by default, and "${targetImpl.id}" names ${targetImpl.sourcePath ? targetImpl.sourcePath : "no file"} \u2014 moving a method is not moving its code.`);
24970
+ }
24014
24971
  this.stageMoveEdit(plan, "implementation", targetImpl.id, targetImpl, {
24015
24972
  ...cloneSpec(targetImpl),
24016
24973
  methods: [...cloneSpec(targetImpl.methods), ...travelling]
24017
24974
  });
24975
+ for (const id of leftImplementations) plan.rehomed.push({ from: id, to: targetImpl.id });
24976
+ } else if (travelling.length > 0) {
24977
+ const created = createdImplementation(target, receiving.id, source.id, travelling, implementations, contracts);
24978
+ this.stageMoveEdit(plan, "implementation", created.spec.id, null, created.spec);
24979
+ for (const id of leftImplementations) plan.rehomed.push({ from: id, to: created.spec.id });
24980
+ plan.created.push(created.named);
24981
+ plan.notices.push(...created.notices);
24018
24982
  }
24019
24983
  plan.reach = reachOf(travelling, source.id, new Set(methods)).filter((id) => id !== to);
24020
24984
  const held = /* @__PURE__ */ new Set([...target.dependsOn ?? [], ...target.owns ?? []]);
@@ -24058,6 +25022,9 @@ var SpecWorkspace = class {
24058
25022
  }
24059
25023
  this.stageMoveEdit(plan, kind, id, spec, subject);
24060
25024
  }
25025
+ for (const edit of plan.edits.values()) {
25026
+ if (edit.before === null) rewriteSpecRefs(edit.after, remap);
25027
+ }
24061
25028
  plan.notices.push(...this.callersOwingADependency(to, plan));
24062
25029
  return repointed;
24063
25030
  }
@@ -24139,7 +25106,7 @@ var SpecWorkspace = class {
24139
25106
  * here: every save does it already, and a second call would only be a hop
24140
25107
  * across a component boundary that no narrative claims.
24141
25108
  */
24142
- writeMoveEdits(edits) {
25109
+ writeMoveEdits(edits, finish) {
24143
25110
  const reports = moveChangeReports(edits, false);
24144
25111
  const written = [];
24145
25112
  try {
@@ -24148,10 +25115,12 @@ var SpecWorkspace = class {
24148
25115
  report.notices.push(...this.saveMovedSpec(edit.kind, edit.after));
24149
25116
  written.push(edit);
24150
25117
  }
25118
+ finish?.();
24151
25119
  } catch (e) {
24152
25120
  for (const edit of [...written].reverse()) {
24153
25121
  try {
24154
- this.saveMovedSpec(edit.kind, edit.before);
25122
+ if (edit.before === null) this.delete(edit.kind, edit.id);
25123
+ else this.saveMovedSpec(edit.kind, edit.before);
24155
25124
  } catch {
24156
25125
  }
24157
25126
  }
@@ -24241,7 +25210,7 @@ function weighMoveHome(hooks, candidate, reach, dependencyLimit) {
24241
25210
  };
24242
25211
  }
24243
25212
  function moveChangeReports(edits, dryRun) {
24244
- return edits.map((edit) => ({ edit, changes: specChanges(edit.before, edit.after) })).filter(({ changes }) => changes.length > 0).map(({ edit, changes }) => ({
25213
+ return edits.map((edit) => ({ edit, changes: specChanges(edit.before ?? {}, edit.after) })).filter(({ changes }) => changes.length > 0).map(({ edit, changes }) => ({
24245
25214
  kind: edit.kind,
24246
25215
  id: edit.id,
24247
25216
  written: !dryRun,
@@ -24250,9 +25219,66 @@ function moveChangeReports(edits, dryRun) {
24250
25219
  ineffective: [],
24251
25220
  notices: [],
24252
25221
  testsToRevisit: [],
24253
- summary: `${dryRun ? "Would move" : "Moved"} into ${edit.kind} "${edit.id}": ${changes.length} change${changes.length === 1 ? "" : "s"}.`
25222
+ summary: edit.before === null ? `${dryRun ? "Would create" : "Created"} ${edit.kind} "${edit.id}" to receive the moved methods.` : `${dryRun ? "Would move" : "Moved"} into ${edit.kind} "${edit.id}": ${changes.length} change${changes.length === 1 ? "" : "s"}.`
24254
25223
  }));
24255
25224
  }
25225
+ function moveRename(from, to, methods, plan) {
25226
+ return {
25227
+ specs: plan.rehomed.map(({ from: left, to: arrived }) => ({ from: left, to: arrived, sites: [...methods] })),
25228
+ methods: methods.map((method) => ({ component: from, method, toComponent: to, toMethod: method }))
25229
+ };
25230
+ }
25231
+ function registerClause(carried, verb) {
25232
+ return carried.length ? `, ${verb} ${carried.length} debt-register key${carried.length === 1 ? "" : "s"}` : "";
25233
+ }
25234
+ function createdClause(created, verb) {
25235
+ return created.length ? `, ${verb} ${created.join(" and ")}` : "";
25236
+ }
25237
+ function createdContract(target, from, methods, contracts) {
25238
+ const now = (/* @__PURE__ */ new Date()).toISOString();
25239
+ const status = statusOfContracts(contracts, from);
25240
+ return {
25241
+ id: `i${target.id}`,
25242
+ name: `${target.name} Interface`,
25243
+ description: `The contract of ${target.name}, created by moving ${namedMethods(methods)} here from "${from}".`,
25244
+ component: target.id,
25245
+ methods: [],
25246
+ ...status ? { status } : {},
25247
+ createdAt: now,
25248
+ updatedAt: now
25249
+ };
25250
+ }
25251
+ function createdImplementation(target, contractId, from, travelling, implementations, contracts) {
25252
+ const ofSource = new Set(contracts.filter((i) => i.component === from).map((i) => i.id));
25253
+ const leaving = implementations.filter((impl) => ofSource.has(impl.contract) && impl.methods.some((m) => travelling.some((t) => t.name === m.name)));
25254
+ const paths = [...new Set(leaving.map((impl) => impl.sourcePath))];
25255
+ const inherited = paths.length === 1 ? paths[0] : void 0;
25256
+ const notices = [];
25257
+ if (paths.length > 1) {
25258
+ for (const entry of travelling) {
25259
+ if (entry.sourcePath) continue;
25260
+ const home = leaving.find((impl) => impl.methods.some((m) => m.name === entry.name));
25261
+ if (home?.sourcePath) entry.sourcePath = home.sourcePath;
25262
+ }
25263
+ notices.push(`"${target.id}_impl" is created without a sourcePath: the moved entries come from implementations with different ones, so each keeps the file it was realized in as its own sourcePath.`);
25264
+ }
25265
+ const now = (/* @__PURE__ */ new Date()).toISOString();
25266
+ const status = leaving[0]?.status;
25267
+ const spec = {
25268
+ id: `${target.id}_impl`,
25269
+ name: `${target.name} Implementation`,
25270
+ description: `The implementation of ${target.name}, created by moving methods here from "${from}".`,
25271
+ contract: contractId,
25272
+ ...inherited ? { sourcePath: inherited } : {},
25273
+ methods: travelling,
25274
+ ...status ? { status } : {},
25275
+ createdAt: now,
25276
+ updatedAt: now
25277
+ };
25278
+ const origin = leaving.length === 1 ? ` from "${leaving[0].id}"` : "";
25279
+ const named = inherited ? `implementation "${spec.id}" (sourcePath ${inherited}, inherited${origin})` : `implementation "${spec.id}" (no sourcePath)`;
25280
+ return { spec, named, notices };
25281
+ }
24256
25282
  function refusedMoveSummary(from, to, methods, refusals, alternatives) {
24257
25283
  const cheapest = alternatives.find((c) => c.legal);
24258
25284
  return `Refused: moving ${namedMethods(methods)} from "${from}" to "${to}" is refused by ${refusals.join(", ")}. Nothing was written. ` + (cheapest ? `The cheapest legal home is "${cheapest.component}" (${cheapest.newDependencies.length} new dependenc${cheapest.newDependencies.length === 1 ? "y" : "ies"}, ${cheapest.dependencyCount} of ${cheapest.dependencyLimit}).` : `No component of this subsystem can take them as they stand \u2014 ${alternatives.length} were weighed.`);
@@ -24894,7 +25920,8 @@ function rewriteRefFields(specsDir, remap, excludeDir) {
24894
25920
  }
24895
25921
  return rewritten;
24896
25922
  }
24897
- function rebaseImplementationPaths(specsDir, fromRoot, toRoot) {
25923
+ function rekeyLintAllows(specsDir, rename) {
25924
+ const rewritten = [];
24898
25925
  for (const file of listFilesRecursive(specsDir, ".yaml")) {
24899
25926
  let raw;
24900
25927
  try {
@@ -24902,13 +25929,48 @@ function rebaseImplementationPaths(specsDir, fromRoot, toRoot) {
24902
25929
  } catch {
24903
25930
  continue;
24904
25931
  }
24905
- if (!raw || typeof raw !== "object" || !("contract" in raw)) continue;
25932
+ const allows = raw?.lint?.allow;
25933
+ const kind = raw && typeof raw === "object" ? specKind(raw) : null;
25934
+ if (!kind || !Array.isArray(allows)) continue;
25935
+ const specId = kind === "system" ? "system" : String(raw.id);
24906
25936
  let changed = false;
24907
- const rebase = (holder, key) => {
24908
- const p = holder[key];
24909
- if (typeof p !== "string" || p === "" || path17.isAbsolute(p)) return;
24910
- const next = toPosixPath(path17.relative(toRoot, path17.resolve(fromRoot, p)));
24911
- if (next !== p) {
25937
+ for (const allow of allows) {
25938
+ if (!allow || typeof allow !== "object") continue;
25939
+ const next = rekeyAnchor(rename, {
25940
+ spec: specId,
25941
+ ...typeof allow.at === "string" ? { at: allow.at } : {},
25942
+ ...Array.isArray(allow.covers) ? { covers: allow.covers } : {}
25943
+ });
25944
+ if (next.at !== void 0 && next.at !== allow.at) {
25945
+ allow.at = next.at;
25946
+ changed = true;
25947
+ }
25948
+ if (next.covers && JSON.stringify(next.covers) !== JSON.stringify(allow.covers)) {
25949
+ allow.covers = next.covers;
25950
+ changed = true;
25951
+ }
25952
+ }
25953
+ if (!changed) continue;
25954
+ writeYamlFile(file, raw);
25955
+ rewritten.push({ kind, id: specId });
25956
+ }
25957
+ return rewritten;
25958
+ }
25959
+ function rebaseImplementationPaths(specsDir, fromRoot, toRoot) {
25960
+ for (const file of listFilesRecursive(specsDir, ".yaml")) {
25961
+ let raw;
25962
+ try {
25963
+ raw = readYamlFile(file);
25964
+ } catch {
25965
+ continue;
25966
+ }
25967
+ if (!raw || typeof raw !== "object" || !("contract" in raw)) continue;
25968
+ let changed = false;
25969
+ const rebase = (holder, key) => {
25970
+ const p = holder[key];
25971
+ if (typeof p !== "string" || p === "" || path17.isAbsolute(p)) return;
25972
+ const next = toPosixPath(path17.relative(toRoot, path17.resolve(fromRoot, p)));
25973
+ if (next !== p) {
24912
25974
  holder[key] = next;
24913
25975
  changed = true;
24914
25976
  }
@@ -24963,24 +26025,32 @@ function renameComponent(componentId, newId) {
24963
26025
  ...movingInterface ? [{ kind: "interface", from: movingInterface.id, to: interfaceId }] : [],
24964
26026
  ...movingImplementation ? [{ kind: "implementation", from: movingImplementation.id, to: implementationId }] : []
24965
26027
  ];
24966
- const rewritten = rewriteRefFields(aiPathsAt(getProjectRoot()).specsDir(), (ref, position) => {
26028
+ const rename = {
26029
+ specs: renamed.map(({ from, to }) => ({ from, to })),
26030
+ components: [{ from: componentId, to: newId }]
26031
+ };
26032
+ projectConfigRepository.rekeyCarried(rename, true);
26033
+ const rewrittenRefs = rewriteRefFields(aiPathsAt(getProjectRoot()).specsDir(), (ref, position) => {
24967
26034
  if (position === "component" || position === "entity-class" || position === "auth-source") {
24968
26035
  return ref === componentId ? newId : ref;
24969
26036
  }
24970
26037
  if (position === "interface" && ref === movingInterface?.id) return interfaceId;
24971
26038
  return ref;
24972
- }).filter((spec) => !renamed.some((moved) => moved.kind === spec.kind && moved.from === spec.id)).map((spec) => spec.id);
26039
+ });
26040
+ const allowsRekeyed = rekeyLintAllows(aiPathsAt(getProjectRoot()).specsDir(), rename);
26041
+ const rewritten = [...new Set([...rewrittenRefs, ...allowsRekeyed].filter((spec) => !renamed.some((moved) => moved.kind === spec.kind && moved.from === spec.id)).map((spec) => spec.id))];
24973
26042
  invalidateSpecCache();
24974
26043
  moveRenamedSpecs(componentId, newId, component, movingInterface, movingImplementation);
24975
- return { renamed, rewritten };
26044
+ const carried = projectConfigRepository.rekeyCarried(rename);
26045
+ return { renamed, rewritten, carried };
24976
26046
  }
24977
26047
  function moveRenamedSpecs(componentId, newId, component, movingInterface, movingImplementation) {
24978
26048
  const interfaceId = `i${newId}`;
24979
26049
  const implementationId = `${newId}_impl`;
24980
- const movedComponent = loadComponentSpec(componentId) ?? component;
26050
+ const movedComponent2 = loadComponentSpec(componentId) ?? component;
24981
26051
  const movedInterface = movingInterface ? loadInterfaceSpec(movingInterface.id) ?? movingInterface : null;
24982
26052
  const movedImplementation = movingImplementation ? loadImplementationSpec(movingImplementation.id) ?? movingImplementation : null;
24983
- saveComponentSpec({ ...movedComponent, id: newId });
26053
+ saveComponentSpec({ ...movedComponent2, id: newId });
24984
26054
  if (movedInterface) saveInterfaceSpec({ ...movedInterface, id: interfaceId, component: newId });
24985
26055
  if (movedImplementation) {
24986
26056
  const oldFile = getImplementationPath(movedImplementation.id);
@@ -25045,6 +26115,19 @@ function renameMethod(componentId, methodName, newName, pinSymbol) {
25045
26115
  const implementations = loadImplementationSpecs();
25046
26116
  const movingContracts = new Set(moving.map((i) => i.id));
25047
26117
  const renamed = [];
26118
+ const rename = {
26119
+ methods: [{
26120
+ component: componentId,
26121
+ method: methodName,
26122
+ toComponent: componentId,
26123
+ toMethod: newName,
26124
+ specs: [
26125
+ ...moving.map((i) => i.id),
26126
+ ...implementations.filter((impl) => movingContracts.has(impl.contract) && impl.methods.some((m) => m.name === methodName)).map((impl) => impl.id)
26127
+ ]
26128
+ }]
26129
+ };
26130
+ projectConfigRepository.rekeyCarried(rename, true);
25048
26131
  for (const contract of moving) {
25049
26132
  saveInterfaceSpec({
25050
26133
  ...contract,
@@ -25067,8 +26150,11 @@ function renameMethod(componentId, methodName, newName, pinSymbol) {
25067
26150
  }
25068
26151
  invalidateSpecCache();
25069
26152
  const specsDir = aiPathsAt(getProjectRoot()).specsDir();
25070
- const rewritten = rewriteRefFields(specsDir, (ref, position, owner) => position === "method" && owner === componentId && ref === methodName ? newName : ref).map((spec) => spec.id);
26153
+ const retargeted = rewriteRefFields(specsDir, (ref, position, owner) => position === "method" && owner === componentId && ref === methodName ? newName : ref);
26154
+ const allowsRekeyed = rekeyLintAllows(specsDir, rename);
26155
+ const rewritten = [...new Set([...retargeted, ...allowsRekeyed].map((spec) => spec.id))];
25071
26156
  const mentions = collectMentions(specsDir, methodName, moving);
26157
+ const carried = projectConfigRepository.rekeyCarried(rename);
25072
26158
  return {
25073
26159
  component: componentId,
25074
26160
  from: methodName,
@@ -25076,7 +26162,8 @@ function renameMethod(componentId, methodName, newName, pinSymbol) {
25076
26162
  renamed,
25077
26163
  rewritten,
25078
26164
  mentions,
25079
- ...pinnedSymbol ? { pinnedSymbol } : {}
26165
+ ...pinnedSymbol ? { pinnedSymbol } : {},
26166
+ carried
25080
26167
  };
25081
26168
  }
25082
26169
  function renameInSignature(signature, from, to) {
@@ -25727,8 +26814,8 @@ function inferSourcePathForComponent(comp, subsystems) {
25727
26814
  return null;
25728
26815
  }
25729
26816
  }
25730
- function summarize(text, max = 140) {
25731
- const clean = text.replace(/\s+/g, " ").trim();
26817
+ function summarize2(text2, max = 140) {
26818
+ const clean = text2.replace(/\s+/g, " ").trim();
25732
26819
  const period = clean.indexOf(". ");
25733
26820
  const firstSentence = period > 0 ? clean.slice(0, period + 1) : clean;
25734
26821
  if (firstSentence.length <= max) return firstSentence;
@@ -25754,7 +26841,7 @@ function resolveAgentTopology() {
25754
26841
  agents.push({
25755
26842
  id: "system-architect",
25756
26843
  name: `${system.name} Architect`,
25757
- description: `Global architect for ${system.name} \u2014 owns the spec tree and topology. ${summarize(system.vision)}`,
26844
+ description: `Global architect for ${system.name} \u2014 owns the spec tree and topology. ${summarize2(system.vision)}`,
25758
26845
  template: "architect",
25759
26846
  creationReason: "Automatically inferred from L0 system spec",
25760
26847
  ownedPaths: [".wai/specs/**"],
@@ -25834,7 +26921,7 @@ function resolveAgentTopology() {
25834
26921
  agents.push({
25835
26922
  id: `${sub.id}-owner`,
25836
26923
  name: `${sub.name} Owner`,
25837
- description: `Owns the ${sub.id} subsystem. ${summarize(sub.description)}`,
26924
+ description: `Owns the ${sub.id} subsystem. ${summarize2(sub.description)}`,
25838
26925
  template: "domain-owner",
25839
26926
  creationReason: `Automatically inferred from L1 subsystem spec: ${sub.id}`,
25840
26927
  domainRoot: sub.id,
@@ -25899,7 +26986,7 @@ function resolveAgentTopology() {
25899
26986
  agents.push({
25900
26987
  id: `${dom.id}-owner`,
25901
26988
  name: `${dom.name ?? dom.id} Owner`,
25902
- description: dom.description ? summarize(dom.description) : `Owner agent for the free-standing "${dom.id}" domain.`,
26989
+ description: dom.description ? summarize2(dom.description) : `Owner agent for the free-standing "${dom.id}" domain.`,
25903
26990
  template: "domain-owner",
25904
26991
  creationReason: "Inferred from a free-standing domain in .wai/topology.yaml",
25905
26992
  domainRoot: dom.id,
@@ -26243,19 +27330,19 @@ function commitStagedTree(stagingDir, destDir, roots, backupExisting) {
26243
27330
  const backupDir = path26.join(root, BACKUP_DIR, timestampSlug());
26244
27331
  let backedUp = false;
26245
27332
  for (const rootRel of roots) {
26246
- const relative9 = rootRel === "." ? "" : rootRel;
26247
- const stagedSpecDir = path26.join(stagingDir, relative9, ".wai");
27333
+ const relative10 = rootRel === "." ? "" : rootRel;
27334
+ const stagedSpecDir = path26.join(stagingDir, relative10, ".wai");
26248
27335
  if (!fs16.existsSync(stagedSpecDir)) continue;
26249
- const liveSpecDir = liveSpecDirFor(path26.join(root, relative9));
27336
+ const liveSpecDir = liveSpecDirFor(path26.join(root, relative10));
26250
27337
  if (backupExisting && liveSpecDir && fs16.existsSync(liveSpecDir)) {
26251
- const backupTarget = path26.join(backupDir, relative9, path26.basename(liveSpecDir));
27338
+ const backupTarget = path26.join(backupDir, relative10, path26.basename(liveSpecDir));
26252
27339
  fs16.mkdirSync(path26.dirname(backupTarget), { recursive: true });
26253
27340
  fs16.renameSync(liveSpecDir, backupTarget);
26254
27341
  backedUp = true;
26255
27342
  } else if (liveSpecDir && fs16.existsSync(liveSpecDir)) {
26256
27343
  fs16.rmSync(liveSpecDir, { recursive: true, force: true });
26257
27344
  }
26258
- const target = path26.join(root, relative9, ".wai");
27345
+ const target = path26.join(root, relative10, ".wai");
26259
27346
  fs16.mkdirSync(path26.dirname(target), { recursive: true });
26260
27347
  if (fs16.existsSync(target)) fs16.rmSync(target, { recursive: true, force: true });
26261
27348
  fs16.renameSync(stagedSpecDir, target);
@@ -26960,13 +28047,13 @@ function syncContextFiles() {
26960
28047
  }
26961
28048
 
26962
28049
  // src/core/status.ts
26963
- var asWritten = (text) => text;
28050
+ var asWritten = (text2) => text2;
26964
28051
  function fillDecor(decor) {
26965
28052
  return {
26966
28053
  structure: decor?.structure ?? asWritten,
26967
28054
  emphasis: decor?.emphasis ?? asWritten,
26968
- layer: decor?.layer ?? ((_kind, text) => text),
26969
- score: decor?.score ?? ((_pct, text) => text),
28055
+ layer: decor?.layer ?? ((_kind, text2) => text2),
28056
+ score: decor?.score ?? ((_pct, text2) => text2),
26970
28057
  draft: decor?.draft ?? asWritten,
26971
28058
  present: decor?.present ?? asWritten,
26972
28059
  missing: decor?.missing ?? asWritten
@@ -26978,13 +28065,13 @@ function getStatusReport(options = {}, decor) {
26978
28065
  const system = loadSystemSpec();
26979
28066
  const loaderErrors = getLoaderIssues();
26980
28067
  if (loaderErrors.length > 0) {
26981
- let errText = "Failed to parse specification files:\n";
28068
+ let errText2 = "Failed to parse specification files:\n";
26982
28069
  for (const issue2 of loaderErrors) {
26983
28070
  const prefix = issue2.specId ? mark.structure(`[${issue2.specId}] `) : "";
26984
- errText += `${prefix}[${issue2.code}] ${issue2.message}
28071
+ errText2 += `${prefix}[${issue2.code}] ${issue2.message}
26985
28072
  `;
26986
28073
  }
26987
- return { text: errText, failed: true };
28074
+ return { text: errText2, failed: true };
26988
28075
  }
26989
28076
  if (!system) {
26990
28077
  return { text: "L0 System specification (system.yaml) is missing.", failed: true };
@@ -27178,17 +28265,2326 @@ var logger = {
27178
28265
  }
27179
28266
  };
27180
28267
 
28268
+ // src/core/skills.ts
28269
+ var path29 = __toESM(require("path"));
28270
+ var fs18 = __toESM(require("fs"));
28271
+
28272
+ // src/core/instructions.ts
28273
+ function governingProfile() {
28274
+ try {
28275
+ return loadProjectConfig()?.projectType ?? null;
28276
+ } catch {
28277
+ return null;
28278
+ }
28279
+ }
28280
+ function blockApplies(block, profile) {
28281
+ if (!block.profile || block.profile.length === 0) return true;
28282
+ return profile !== null && block.profile.includes(profile);
28283
+ }
28284
+ function skillPointers(skills) {
28285
+ if (skills.length === 0) return "(none published by this server)";
28286
+ return skills.map((s) => `- \`${s.resourceUri}\` \u2014 ${s.description || s.name}`).join("\n");
28287
+ }
28288
+ function waironInstructions(profile, packNames, skills, variants) {
28289
+ return `# wairon \u2014 Spec-Driven Development (SDD)
28290
+
28291
+ This server owns one system's **spec tree**: its architecture, specified top-down
28292
+ and machine-checked. Author it ONLY through this server's \`sdd_*\` tools \u2014 never by
28293
+ writing spec files directly, and never via the \`wairon\` CLI (that is the human
28294
+ developer's tool).
28295
+
28296
+ ## The tree: L0 \u2192 L5
28297
+
28298
+ - **L0 System** \u2014 vision, boundaries, global requirements, target language.
28299
+ - **L1 Subsystem** \u2014 an isolated service, its \`publicInterfaces\`, its trusted links.
28300
+ - **L2 Component** \u2014 ONE building block (\`Portal\`, \`Orchestrator\`, \`Supervisor\`,
28301
+ \`Actor\`, \`Store\`, \`Index\`, \`Query\`, \`Registry\`, \`Adapter\`, \`Observer\`, and
28302
+ \`View\` for UIs) or pattern (\`Repository\`, and \`FeatureComponent\` or
28303
+ \`RouterComponent\` for UIs), plus \`owns\` (a pattern's private members) and
28304
+ \`dependsOn\` (collaborators). Logic is an \`Orchestrator\` whose \`dependencyClass\`
28305
+ (\`pure\` or \`read\`; unset, a workflow) bounds what it may depend on; a gateway is
28306
+ a \`Portal\` with the \`gateway\` variant. \`Specialist\` and \`Gateway\` are retired.
28307
+ - **L3 Interface** \u2014 the contract: method signatures with structured \`params\`,
28308
+ plus wire \`endpoint\` bindings for a Portal's methods.
28309
+ - **L4 Implementation** \u2014 a concrete realization of one L3 contract: narrative
28310
+ detail level, bound \`technologies\` (the vendor swap seam), optional \`sourcePath\`.
28311
+ - **L5 Narrative** \u2014 each method's logic as a FLAT numbered step list; \`call\` steps
28312
+ resolve to real dependency methods, and flow steps jump by step number.
28313
+
28314
+ These stereotypes are not labels \u2014 they carry enforced dependency rules (e.g. a
28315
+ Portal may not depend on a Store; state lives in a Store, never as fields inside
28316
+ an Orchestrator). \`sdd_validate_tree\` is the gate, and it is the authority on
28317
+ whether a design is legal.
28318
+
28319
+ ## Authoring order
28320
+
28321
+ 1. \`sdd_initialize_system\` (L0), then \`sdd_add_subsystem\` (L1).
28322
+ 2. Then, **one subsystem at a time**: \`sdd_add_component\` (L2) \u2192 \`sdd_define_interface\`
28323
+ (L3) \u2192 \`sdd_set_endpoints\` for Portals \u2192 \`sdd_write_narrative\` (L4+L5).
28324
+ 3. Run \`sdd_validate_tree\` as you go \u2014 early and per subsystem, not once at the end.
28325
+ \`sdd_get_status\` reports completeness; \`sdd_get_spec\` reads any spec back as JSON.
28326
+ 4. Edit granularly with \`sdd_update_spec\` (it renumbers narrative steps and
28327
+ relocates jump targets for you).
28328
+
28329
+ ## The tool schemas are the field reference
28330
+
28331
+ Every \`sdd_*\` tool's input schema is self-describing and authoritative \u2014 read the
28332
+ schema rather than guessing field names, and prefer structured \`params\` over prose
28333
+ signatures. Specs are ${profile === null ? "validated against the project's configured profile" : `governed by the **\`${profile}\`** profile`}, which decides which stereotypes are legal
28334
+ here, which narrative flow constructs the target language supports, and which
28335
+ extra rules apply. A subsystem may override the profile for itself.
28336
+
28337
+ ## Loaded extension packs
28338
+
28339
+ ${packNames.length === 0 ? "None \u2014 only wairon's built-in doctrine applies." : `${packNames.map((n) => `\`${n}\``).join(", ")} \u2014 these inject profiles, language tables, patterns, and
28340
+ additional conformance rules. Their doctrine is enforced by the same gate.`}
28341
+
28342
+ ${variants.length === 0 ? "" : `## Component variants in play
28343
+
28344
+ This project defines variants \u2014 named, base-anchored kinds carrying implementation
28345
+ guidance, so every component of the same kind is built alike: ${variants.map((v) => `\`${v.id}\` (a kind of ${v.base})`).join(", ")}.
28346
+ Tag a component with \`variant\` where it fits, and read a component's resolved
28347
+ guidance and its same-variant siblings from \`sdd_get_spec\` (kind: component).
28348
+
28349
+ `}## Read the skills BEFORE authoring
28350
+
28351
+ This server publishes its skills as MCP resources. **Read \`wairon-skill://sdd-architect\`
28352
+ before you design anything**, and the others when their turn comes \u2014 they carry the
28353
+ architecture standard, the narrative step reference, and the review checklist that
28354
+ this briefing only points at:
28355
+
28356
+ ${skillPointers(skills)}
28357
+
28358
+ ## Delegating implementation
28359
+
28360
+ Delegate component work from a LIVE agent brief, never a generated file: discover
28361
+ agents via \`listAgents\` (or the \`wairon-agent://\` resources), fetch the target's
28362
+ brief with \`sdd_get_agent_brief\`, and spawn a scoped subagent from it \u2014 its
28363
+ \`instructions\` as prompt, its \`ownedPaths\` as write fence. Briefs are composed
28364
+ from the current spec tree on every call, so a re-lock never requires a session
28365
+ restart \u2014 fetch fresh per delegation. \`wairon-skill://sdd-delegate\` carries the
28366
+ full flow.`;
28367
+ }
28368
+ function buildServerInstructions() {
28369
+ const profile = governingProfile();
28370
+ let skills = [];
28371
+ let blocks = [];
28372
+ let packNames = [];
28373
+ const variants = [];
28374
+ try {
28375
+ skills = listSkillResources();
28376
+ const extensions = loadProjectExtensions();
28377
+ blocks = extensions.instructions;
28378
+ packNames = extensions.packNames;
28379
+ } catch {
28380
+ }
28381
+ const sections = [waironInstructions(profile, packNames, skills, variants)];
28382
+ for (const block of blocks) {
28383
+ if (!blockApplies(block, profile)) continue;
28384
+ sections.push(`## From pack "${block.pack}"
28385
+
28386
+ ${block.text.trim()}`);
28387
+ }
28388
+ return sections.join("\n\n");
28389
+ }
28390
+
27181
28391
  // src/core/skills.ts
27182
28392
  var SKILL_NAMES = ["sdd-architect", "sdd-narrative", "sdd-auditor", "sdd-implement", "sdd-delegate"];
28393
+ function packSkillId(skill) {
28394
+ return `${packNamespace(skill.pack)}-${skill.id}`;
28395
+ }
28396
+ function packNamespace(pack) {
28397
+ return pack.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
28398
+ }
28399
+ function extensionsFor(builtin, packSkills) {
28400
+ return packSkills.filter((s) => s.extends === builtin && fs18.existsSync(s.sourcePath));
28401
+ }
28402
+ function composeBuiltinSkill(name, packSkills) {
28403
+ const srcPath = skillTemplatePath(name);
28404
+ const base = fs18.existsSync(srcPath) ? fs18.readFileSync(srcPath, "utf-8") : "";
28405
+ const sections = extensionsFor(name, packSkills);
28406
+ if (sections.length === 0) return base;
28407
+ const parts = [base.trimEnd()];
28408
+ for (const section of sections) {
28409
+ const body = fs18.readFileSync(section.sourcePath, "utf-8").replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
28410
+ parts.push(`## Platform: ${section.pack}`, body.trim());
28411
+ }
28412
+ return `${parts.join("\n\n")}
28413
+ `;
28414
+ }
28415
+ function readFrontmatter(raw, fallbackName) {
28416
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
28417
+ const block = match ? match[1] : "";
28418
+ const field = (key) => {
28419
+ const m = new RegExp(`^${key}:\\s*(.*)$`, "m").exec(block);
28420
+ return m ? m[1].trim() : "";
28421
+ };
28422
+ return { name: field("name") || fallbackName, description: field("description") };
28423
+ }
28424
+ function builtinSkillsDir() {
28425
+ return path29.resolve(__dirname, "..", "templates", "skills");
28426
+ }
28427
+ function skillTemplatePath(name) {
28428
+ return path29.join(builtinSkillsDir(), `${name}.md`);
28429
+ }
27183
28430
  function activeTargetTypes2() {
27184
28431
  const config = loadProjectConfig();
27185
28432
  if (!config) return [];
27186
28433
  return activeTargetTypes(config);
27187
28434
  }
28435
+ var SKILL_RESOURCE_SCHEME = "wairon-skill";
27188
28436
  var RESOURCE_SKILL_IDS = [...SKILL_NAMES].sort();
28437
+ var SkillResourceNotFoundError = class extends Error {
28438
+ constructor(resourceId) {
28439
+ super(`Unknown SDD skill resource id: "${resourceId}".`);
28440
+ this.name = "SkillResourceNotFoundError";
28441
+ }
28442
+ };
28443
+ function readSkillFrontmatter(name) {
28444
+ return readFrontmatter(fs18.readFileSync(skillTemplatePath(name), "utf-8"), name);
28445
+ }
28446
+ function listSkillResources() {
28447
+ const builtin = RESOURCE_SKILL_IDS.map((id) => {
28448
+ const fm = readSkillFrontmatter(id);
28449
+ return {
28450
+ id,
28451
+ name: fm.name,
28452
+ description: fm.description,
28453
+ version: WAIRON_VERSION,
28454
+ resourceUri: `${SKILL_RESOURCE_SCHEME}://${id}`,
28455
+ defaultForHostedMcp: true
28456
+ };
28457
+ });
28458
+ const pack = loadProjectExtensions().skills.filter((s) => s.extends === void 0 && fs18.existsSync(s.sourcePath)).map((skill) => {
28459
+ const id = packSkillId(skill);
28460
+ const fm = readFrontmatter(fs18.readFileSync(skill.sourcePath, "utf-8"), id);
28461
+ return {
28462
+ id,
28463
+ name: fm.name,
28464
+ description: fm.description,
28465
+ version: skill.packVersion ?? WAIRON_VERSION,
28466
+ resourceUri: `${SKILL_RESOURCE_SCHEME}://${id}`,
28467
+ defaultForHostedMcp: true
28468
+ };
28469
+ });
28470
+ return [...builtin, ...pack];
28471
+ }
28472
+ function readSkillResource(resourceId) {
28473
+ const packSkills = loadProjectExtensions().skills;
28474
+ if (SKILL_NAMES.includes(resourceId)) return composeBuiltinSkill(resourceId, packSkills);
28475
+ const packSkill = packSkills.find((s) => s.extends === void 0 && packSkillId(s) === resourceId);
28476
+ if (packSkill) return fs18.readFileSync(packSkill.sourcePath, "utf-8");
28477
+ return fs18.readFileSync(skillTemplatePath(resourceId), "utf-8");
28478
+ }
28479
+ function listResources() {
28480
+ return listSkillResources();
28481
+ }
28482
+ function buildServerInstructions2() {
28483
+ return buildServerInstructions();
28484
+ }
28485
+ function readResource(resourceId) {
28486
+ const known = listSkillResources().some((d) => d.id === resourceId);
28487
+ if (!known) throw new SkillResourceNotFoundError(resourceId);
28488
+ return readSkillResource(resourceId);
28489
+ }
27189
28490
 
27190
- // src/index.ts
28491
+ // src/mcp/server.ts
28492
+ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
28493
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
28494
+ var import_zod11 = require("zod");
28495
+ var import_types4 = require("@modelcontextprotocol/sdk/types.js");
28496
+ var crypto4 = __toESM(require("crypto"));
28497
+
28498
+ // src/core/adapters/authoring-core.ts
27191
28499
  function loadProjectConfig2() {
28500
+ try {
28501
+ return loadProjectConfig();
28502
+ } catch {
28503
+ return null;
28504
+ }
28505
+ }
28506
+
28507
+ // src/models/candidate.ts
28508
+ function formatCandidateRefusal(verdict) {
28509
+ const lines = verdict.errors.map((e) => `- ${e.code}: ${e.message}`);
28510
+ return `Refused: this component's fields do not match its componentType.
28511
+ ${lines.join("\n")}
28512
+
28513
+ Nothing was written. Fix the fields and retry \u2014 on an EXISTING spec, remove a field with sdd_update_spec's "unset" (e.g. {"unset": ["basePath"]}); passing null leaves it in place.`;
28514
+ }
28515
+
28516
+ // src/core/authoring.ts
28517
+ function candidateOptions() {
28518
+ const config = loadProjectConfig2();
28519
+ return config ? { rules: config.rules, projectType: config.projectType } : {};
28520
+ }
28521
+ function noticesFrom(verdict) {
28522
+ return verdict.warnings.map((w) => `${w.code}: ${w.message}`);
28523
+ }
28524
+ function componentCandidateGate(options = candidateOptions()) {
28525
+ return {
28526
+ gate: (kind, merged) => {
28527
+ if (kind !== "component") return;
28528
+ const verdict = validateComponentCandidate(merged, options);
28529
+ if (verdict.errors.length) throw new Error(formatCandidateRefusal(verdict));
28530
+ return noticesFrom(verdict);
28531
+ }
28532
+ };
28533
+ }
28534
+ var STORE_MANAGED_FIELDS = /* @__PURE__ */ new Set(["status", "updatedAt"]);
28535
+ var ALWAYS_CARRIED_FIELDS = /* @__PURE__ */ new Set(["ext"]);
28536
+ var STATUS_ORDER = ["draft", "design", "complete"];
28537
+ var STATUSED_KINDS = /* @__PURE__ */ new Set(["subsystem", "component", "interface", "implementation"]);
28538
+ var MISSING_PARENT = {
28539
+ subsystem: () => "System spec must be initialized (sdd_initialize_system) first.",
28540
+ component: (id) => `Parent subsystem "${id}" does not exist.`,
28541
+ interface: (id) => `Component "${id}" does not exist.`,
28542
+ implementation: (id) => `Interface contract "${id}" does not exist.`,
28543
+ type: (id) => `Owning subsystem "${id}" does not exist \u2014 name a subsystem the tree has, or omit subsystem for a system-level value object.`
28544
+ };
28545
+ var REWRITE_LABEL = {
28546
+ system: "System spec",
28547
+ subsystem: "Subsystem",
28548
+ component: "Component",
28549
+ interface: "Interface",
28550
+ implementation: "Implementation",
28551
+ type: "Type"
28552
+ };
28553
+ function restatementParent(restatement) {
28554
+ const spec = restatement.spec;
28555
+ switch (restatement.kind) {
28556
+ case "subsystem":
28557
+ return { kind: "system", id: "system" };
28558
+ case "component":
28559
+ return { kind: "subsystem", id: spec.subsystem };
28560
+ case "interface":
28561
+ return { kind: "component", id: spec.component };
28562
+ case "implementation":
28563
+ return { kind: "interface", id: spec.contract };
28564
+ case "type":
28565
+ return typeof spec.subsystem === "string" && spec.subsystem !== "" ? { kind: "subsystem", id: spec.subsystem } : null;
28566
+ default:
28567
+ return null;
28568
+ }
28569
+ }
28570
+ function applyRestatement(restatement, existing, parent) {
28571
+ const replacedExisting = existing !== null;
28572
+ const parentRef = restatementParent(restatement);
28573
+ if (parentRef && !parent) return refused(restatement, replacedExisting, MISSING_PARENT[restatement.kind](parentRef.id));
28574
+ const status = statusForCreate(restatement, existing);
28575
+ if ("refusal" in status) return refused(restatement, replacedExisting, status.refusal);
28576
+ const candidate = JSON.parse(JSON.stringify(restatement.spec));
28577
+ if (restatement.kind === "subsystem") candidate.parentSystem = parent.name;
28578
+ const carried = carryInto(restatement, existing, candidate);
28579
+ const cleared = clearedByOmission(existing, candidate, restatement.fields);
28580
+ stampLifecycle(candidate, existing, status.status);
28581
+ const labelErrors = resolveLabelsOf(restatement.kind, candidate);
28582
+ if (labelErrors.length) {
28583
+ return refused(restatement, replacedExisting, `Unresolved narrative label references \u2014 nothing was saved:
28584
+ - ${labelErrors.join("\n- ")}`);
28585
+ }
28586
+ const parsed = parseCandidate(restatement.kind, candidate);
28587
+ if ("refusal" in parsed) return refused(restatement, replacedExisting, parsed.refusal);
28588
+ return {
28589
+ spec: parsed.spec,
28590
+ ...status.status ? { status: status.status } : {},
28591
+ replacedExisting,
28592
+ notices: existing ? rewriteNotices(restatement.kind, existing, candidate, carried, cleared) : [],
28593
+ changedMethods: changedMethodsOf(restatement.kind, existing, candidate)
28594
+ };
28595
+ }
28596
+ var SPEC_SCHEMA = {
28597
+ system: SystemSpecSchema,
28598
+ subsystem: SubsystemSpecSchema,
28599
+ component: ComponentSpecSchema,
28600
+ interface: InterfaceSpecSchema,
28601
+ implementation: ImplementationSpecSchema,
28602
+ type: TypeSpecSchema
28603
+ };
28604
+ function parseCandidate(kind, candidate) {
28605
+ const masked = /* @__PURE__ */ new Map();
28606
+ const mask = (value) => {
28607
+ if (typeof value === "string") {
28608
+ if (!value.includes("::")) return value;
28609
+ const token = `nsref_${masked.size}_masked`;
28610
+ masked.set(token, value);
28611
+ return token;
28612
+ }
28613
+ if (Array.isArray(value)) return value.map(mask);
28614
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, mask(v)]));
28615
+ return value;
28616
+ };
28617
+ const restore = (value) => {
28618
+ if (typeof value === "string") return masked.get(value) ?? value;
28619
+ if (Array.isArray(value)) return value.map(restore);
28620
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, restore(v)]));
28621
+ return value;
28622
+ };
28623
+ const result = SPEC_SCHEMA[kind].safeParse(mask(candidate));
28624
+ if (result.success) return { spec: restore(result.data) };
28625
+ const id = kind === "system" ? "system" : String(candidate.id);
28626
+ const issues = result.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
28627
+ return {
28628
+ refusal: `Refusing to write ${kind} "${id}": the spec it would become does not parse \u2014 ${issues}. Nothing was written. State the missing or malformed field and call again.`
28629
+ };
28630
+ }
28631
+ function refused(restatement, replacedExisting, refusal2) {
28632
+ return { spec: restatement.spec, refusal: refusal2, replacedExisting, notices: [], changedMethods: [] };
28633
+ }
28634
+ function statusForCreate(restatement, existing) {
28635
+ if (!STATUSED_KINDS.has(restatement.kind)) return {};
28636
+ const stated = restatement.status;
28637
+ const stored = existing?.status;
28638
+ const held = stored !== void 0 && STATUS_ORDER.includes(stored) ? stored : void 0;
28639
+ if (stated === void 0) return { status: held ?? "draft" };
28640
+ if (held === void 0 || STATUS_ORDER.indexOf(stated) >= STATUS_ORDER.indexOf(held)) return { status: stated };
28641
+ const label = `${restatement.kind} "${restatement.spec.id}"`;
28642
+ return {
28643
+ refusal: `Refusing to re-author ${label} at status "${stated}": it is stored at "${held}", and a create tool never lowers a spec's status \u2014 a restatement that reopened a frozen spec would undo a lock without saying so. Nothing was written. To reopen it deliberately, use sdd_update_spec with {"status": "${stated}"}; to keep the level it has, leave status out.`
28644
+ };
28645
+ }
28646
+ function carryInto(restatement, existing, candidate) {
28647
+ if (!existing) return [];
28648
+ const carried = ["createdAt"];
28649
+ if (restatement.memberFields && Array.isArray(candidate.methods)) {
28650
+ const stored = existing.methods ?? [];
28651
+ for (const method of candidate.methods) {
28652
+ const prev = stored.find((p) => p.name === method.name);
28653
+ for (const field of carryUnexpressed(prev, method, restatement.memberFields)) carried.push(`${field} (${String(method.name)})`);
28654
+ }
28655
+ }
28656
+ carried.push(...carryUnexpressed(existing, candidate, [...restatement.fields, "createdAt"]));
28657
+ return carried;
28658
+ }
28659
+ function carryUnexpressed(existing, next, expressed) {
28660
+ if (!existing) return [];
28661
+ const carried = [];
28662
+ for (const [field, value] of Object.entries(existing)) {
28663
+ if (value === void 0) continue;
28664
+ if (STORE_MANAGED_FIELDS.has(field)) continue;
28665
+ if (expressed.includes(field) && !ALWAYS_CARRIED_FIELDS.has(field)) continue;
28666
+ if (next[field] !== void 0) continue;
28667
+ next[field] = value;
28668
+ carried.push(field);
28669
+ }
28670
+ return carried;
28671
+ }
28672
+ function isEmptyValue(value) {
28673
+ if (value === void 0 || value === null) return true;
28674
+ if (Array.isArray(value)) return value.length === 0;
28675
+ if (typeof value === "string") return value.trim() === "";
28676
+ if (typeof value === "object") return Object.keys(value).length === 0;
28677
+ return false;
28678
+ }
28679
+ function clearedByOmission(existing, next, expressed) {
28680
+ if (!existing) return [];
28681
+ const stored = existing;
28682
+ const cleared = [];
28683
+ for (const field of expressed) {
28684
+ const before = stored[field];
28685
+ if (STORE_MANAGED_FIELDS.has(field) || ALWAYS_CARRIED_FIELDS.has(field)) continue;
28686
+ if (isEmptyValue(before) || !isEmptyValue(next[field])) continue;
28687
+ cleared.push(Array.isArray(before) ? `${field} (had ${before.length})` : field);
28688
+ }
28689
+ return cleared;
28690
+ }
28691
+ function stampLifecycle(candidate, existing, status) {
28692
+ const now = (/* @__PURE__ */ new Date()).toISOString();
28693
+ candidate.createdAt = existing?.createdAt ?? now;
28694
+ candidate.updatedAt = now;
28695
+ if (status) candidate.status = status;
28696
+ else delete candidate.status;
28697
+ }
28698
+ function resolveLabelsOf(kind, candidate) {
28699
+ if (kind !== "implementation") return [];
28700
+ const methods = candidate.methods ?? [];
28701
+ return methods.flatMap((m) => resolveNarrativeLabels(m.name, m.narrative ?? []));
28702
+ }
28703
+ function removedByName(before, after) {
28704
+ if (!before?.length) return [];
28705
+ const kept = new Set((after ?? []).map((m) => m.name));
28706
+ return before.map((m) => m.name).filter((n) => !kept.has(n));
28707
+ }
28708
+ function rewriteNotices(kind, existing, candidate, carried, cleared) {
28709
+ const named = kind === "system" ? existing.name : String(candidate.id);
28710
+ const notices = [`${REWRITE_LABEL[kind]} "${named}" already existed \u2014 re-authored in place; this input REPLACES what it expresses.`];
28711
+ if (carried.length) notices.push(`Carried forward (not expressible through this tool): ${carried.join(", ")}.`);
28712
+ const removal = removalNotice(kind, existing, candidate);
28713
+ if (removal) notices.push(removal);
28714
+ if (cleared.length) {
28715
+ notices.push(
28716
+ `CLEARED by omission: ${cleared.join(", ")} \u2014 the argument was not repeated, so the previous value is gone. Use sdd_update_spec if you meant to leave it untouched.`
28717
+ );
28718
+ }
28719
+ return notices;
28720
+ }
28721
+ function removalNotice(kind, existing, candidate) {
28722
+ const before = existing;
28723
+ let removed = [];
28724
+ let noun = "method";
28725
+ let suffix = "";
28726
+ if (kind === "interface" || kind === "implementation") {
28727
+ removed = removedByName(before.methods, candidate.methods);
28728
+ suffix = kind === "interface" ? " (endpoint bindings included)" : " (L5 narratives included)";
28729
+ } else if (kind === "type") {
28730
+ removed = [
28731
+ ...removedByName(before.fields, candidate.fields).map((n) => `field ${n}`),
28732
+ ...removedByName(before.methods, candidate.methods).map((n) => `method ${n}`)
28733
+ ];
28734
+ noun = "member";
28735
+ }
28736
+ if (!removed.length) return void 0;
28737
+ return `REMOVED by this restatement: ${removed.length === 1 ? noun : `${noun}s`} ${removed.map((n) => `"${n}"`).join(", ")}${suffix} \u2014 absent from the input, so no longer in the spec. Restate them to keep them, or use sdd_update_spec to edit one member at a time.`;
28738
+ }
28739
+ var METHOD_SCHEMA = {
28740
+ interface: MethodSignatureSchema,
28741
+ implementation: MethodImplementationSchema,
28742
+ type: TypeMethodSchema
28743
+ };
28744
+ function changedMethodsOf(kind, existing, candidate) {
28745
+ if (!existing || !(kind in METHOD_SCHEMA)) return [];
28746
+ const schema = METHOD_SCHEMA[kind];
28747
+ const stored = existing.methods ?? [];
28748
+ const next = candidate.methods ?? [];
28749
+ return stored.filter((method) => {
28750
+ const restated = next.find((m) => m.name === method.name);
28751
+ if (!restated) return true;
28752
+ const parsed = schema.safeParse(restated);
28753
+ return !parsed.success || canonical(parsed.data) !== canonical(method);
28754
+ }).map((method) => method.name);
28755
+ }
28756
+ function canonical(value) {
28757
+ return JSON.stringify(value, (_key, v) => v && typeof v === "object" && !Array.isArray(v) ? Object.fromEntries(Object.entries(v).filter(([, x]) => x !== void 0).sort(([a], [b]) => a.localeCompare(b))) : v);
28758
+ }
28759
+ function writeSpec(restatement) {
28760
+ const bound2 = candidateOptions();
28761
+ const parentRef = restatementParent(restatement);
28762
+ const parent = parentRef ? loadSpec(parentRef.kind, parentRef.id) : null;
28763
+ const id = restatement.kind === "system" ? "system" : restatement.spec.id;
28764
+ const existing = loadSpec(restatement.kind, id);
28765
+ const application = applyRestatement(restatement, existing, parent);
28766
+ if (application.refusal !== void 0) throw new Error(application.refusal);
28767
+ const gateNotices = restatement.kind === "component" ? judgeComponent(application.spec, bound2) : [];
28768
+ const persisted = persistCandidate(restatement.kind, application.spec);
28769
+ const testsToRevisit = testsInvalidatedBy(restatement.kind, id, application.changedMethods, [existing, application.spec], bound2);
28770
+ return {
28771
+ kind: restatement.kind,
28772
+ id,
28773
+ name: application.spec.name,
28774
+ replacedExisting: application.replacedExisting,
28775
+ ...application.status ? { status: application.status } : {},
28776
+ notices: [...gateNotices, ...persisted.notices, ...application.notices],
28777
+ ...persisted.scaffoldedProjectPath ? { scaffoldedProjectPath: persisted.scaffoldedProjectPath } : {},
28778
+ testsToRevisit
28779
+ };
28780
+ }
28781
+ function judgeComponent(candidate, options) {
28782
+ const verdict = validateComponentCandidate(candidate, options);
28783
+ if (verdict.errors.length) throw new Error(formatCandidateRefusal(verdict));
28784
+ return noticesFrom(verdict);
28785
+ }
28786
+ function persistCandidate(kind, spec) {
28787
+ const projectPath = kind === "subsystem" ? spec.projectPath : void 0;
28788
+ if (projectPath && projectPath.trim() !== "") {
28789
+ createChainedSubsystem(spec, spec.name);
28790
+ return { notices: [], scaffoldedProjectPath: projectPath };
28791
+ }
28792
+ return { notices: saveSpec(kind, spec) };
28793
+ }
28794
+ function testsInvalidatedBy(kind, id, methods, specs, options, written = []) {
28795
+ const testRoots = options.rules?.conformance?.testRoots ?? [];
28796
+ if (methods.length === 0 || testRoots.length === 0) return [];
28797
+ return findTestsReferencing2(searchedMethods(kind, id, methods, specs, written), getProjectRoot(), testRoots);
28798
+ }
28799
+ function searchedMethods(kind, id, names, specs, written) {
28800
+ const stated = (name) => written.find((m) => m?.name === name)?.symbol;
28801
+ if (kind === "interface") {
28802
+ const realizations = loadImplementationSpecs().filter((impl) => impl.contract === id);
28803
+ return names.flatMap((name) => {
28804
+ const found = realizations.flatMap((impl) => impl.methods.filter((m) => m.name === name).map((m) => ({ name, symbol: stated(name) ?? m.symbol, sourcePath: m.sourcePath ?? impl.sourcePath })));
28805
+ return found.length > 0 ? found : [{ name, symbol: stated(name) }];
28806
+ });
28807
+ }
28808
+ return names.map((name) => {
28809
+ const holders = specs.map((spec) => {
28810
+ const holder = spec;
28811
+ const method = holder?.methods?.find((m) => m.name === name);
28812
+ return method ? { method, file: method.sourcePath ?? holder?.sourcePath } : null;
28813
+ }).filter((h) => h !== null);
28814
+ const symbol = stated(name) ?? holders.map((h) => h.method.symbol).find((s) => s !== void 0);
28815
+ const sourcePath = holders.map((h) => h.file).find((f) => f !== void 0) ?? specs.map((spec) => spec?.sourcePath).find((f) => f !== void 0);
28816
+ return { name, symbol, sourcePath };
28817
+ });
28818
+ }
28819
+ function deleteSpec2(kind, id) {
28820
+ const bound2 = candidateOptions();
28821
+ const stored = loadSpec(kind, id);
28822
+ const deleted = deleteSpec(kind, id);
28823
+ const methods = deleted ? (stored?.methods ?? []).map((m) => m.name) : [];
28824
+ const testsToRevisit = testsInvalidatedBy(kind, id, methods, [stored], bound2);
28825
+ return { kind, id, deleted, testsToRevisit };
28826
+ }
28827
+ function updateSpecGated(kind, id, delta, dryRun) {
28828
+ const bound2 = candidateOptions();
28829
+ const gate = componentCandidateGate(bound2);
28830
+ const testRoots = bound2.rules?.conformance?.testRoots ?? [];
28831
+ const stored = testRoots.length > 0 ? loadSpec(kind, id) : null;
28832
+ const report = updateSpec(kind, id, delta, gate, dryRun);
28833
+ const changed = changedMethods(report);
28834
+ if (changed.length > 0 && testRoots.length > 0) {
28835
+ const written = Array.isArray(delta?.methods) ? delta.methods : [];
28836
+ report.testsToRevisit = testsInvalidatedBy(kind, id, changed, [stored], bound2, written);
28837
+ }
28838
+ return report;
28839
+ }
28840
+ function changedMethods(report) {
28841
+ const named = /* @__PURE__ */ new Set();
28842
+ for (const change of report.changes) {
28843
+ const [field, name] = change.path.split(".");
28844
+ if (field === "methods" && name) named.add(name);
28845
+ }
28846
+ return [...named];
28847
+ }
28848
+ function methodMoveGate(source) {
28849
+ const bound2 = candidateOptions();
28850
+ return {
28851
+ gate: (kind, merged) => {
28852
+ if (kind !== "component") return;
28853
+ const { codes, verdict, ceiling } = judgeMoveHome(merged, source, bound2);
28854
+ if (codes.length === 0) return noticesFrom(verdict);
28855
+ throw new Error(
28856
+ `${codes.join(", ")} refuses "${merged.id}" as the home for these methods. ` + (verdict.errors.length ? formatCandidateRefusal(verdict) : `It would reach ${merged.dependsOn?.length ?? 0} dependencies against a ceiling of ${ceiling}.`)
28857
+ );
28858
+ },
28859
+ assess: (kind, merged) => kind === "component" ? judgeMoveHome(merged, source, bound2).codes : []
28860
+ };
28861
+ }
28862
+ function judgeMoveHome(component, source, options) {
28863
+ const ceiling = options.rules?.complexity?.maxComponentDependencies;
28864
+ const verdict = validateComponentCandidate(component, options);
28865
+ const codes = verdict.errors.map((e) => e.code);
28866
+ if (component.id !== source && ceiling !== void 0 && (component.dependsOn?.length ?? 0) > ceiling) {
28867
+ codes.push("EXCESSIVE_DEPENDENCIES");
28868
+ }
28869
+ return { codes: [...new Set(codes)], verdict, ceiling };
28870
+ }
28871
+ function moveMethods2(from, to, methods, dryRun) {
28872
+ return moveMethods(from, to, methods, methodMoveGate(from), dryRun);
28873
+ }
28874
+
28875
+ // src/mcp/build.ts
28876
+ var fs19 = __toESM(require("fs"));
28877
+ var path30 = __toESM(require("path"));
28878
+ var import_child_process2 = require("child_process");
28879
+ function captureBuildStamp(entryPath) {
28880
+ try {
28881
+ const s = fs19.statSync(entryPath);
28882
+ return { path: entryPath, mtimeMs: s.mtimeMs, size: s.size };
28883
+ } catch {
28884
+ return null;
28885
+ }
28886
+ }
28887
+ function isBuildStale(stamp) {
28888
+ if (!stamp) return false;
28889
+ try {
28890
+ const s = fs19.statSync(stamp.path);
28891
+ return s.mtimeMs !== stamp.mtimeMs || s.size !== stamp.size;
28892
+ } catch {
28893
+ return false;
28894
+ }
28895
+ }
28896
+ var fingerprintCache = /* @__PURE__ */ new Map();
28897
+ function readBuildFingerprint(stamp) {
28898
+ const current2 = fs19.statSync(stamp.path);
28899
+ const key = `${stamp.path}|${current2.mtimeMs}|${current2.size}`;
28900
+ const cached = fingerprintCache.get(key);
28901
+ if (cached !== void 0) return cached;
28902
+ const modulePath = fingerprintModuleFor(stamp.path);
28903
+ const loaderFlags = modulePath.endsWith(".ts") ? process.execArgv : [];
28904
+ let printed;
28905
+ try {
28906
+ printed = (0, import_child_process2.execFileSync)(
28907
+ process.execPath,
28908
+ [...loaderFlags, "-e", "const m = require(process.argv[1]); process.stdout.write(String(m.schemaFingerprint()));", modulePath],
28909
+ { encoding: "utf8", timeout: 6e4, stdio: ["ignore", "pipe", "pipe"], windowsHide: true }
28910
+ ).trim();
28911
+ } catch (e) {
28912
+ const stderr = String(e.stderr ?? "").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
28913
+ const cause = stderr.find((l) => /^[A-Za-z]*Error\b/.test(l)) ?? stderr[0] ?? e.message;
28914
+ throw new Error(`the build at ${modulePath} could not be asked for its schema fingerprint (${cause})`);
28915
+ }
28916
+ if (!printed) throw new Error(`the build at ${modulePath} answered no schema fingerprint`);
28917
+ fingerprintCache.set(key, printed);
28918
+ return printed;
28919
+ }
28920
+ function fingerprintModuleFor(entryPath) {
28921
+ let dir = path30.dirname(entryPath);
28922
+ for (; ; ) {
28923
+ const manifest = path30.join(dir, "package.json");
28924
+ if (fs19.existsSync(manifest)) {
28925
+ try {
28926
+ const pkg = JSON.parse(fs19.readFileSync(manifest, "utf8"));
28927
+ const inDist = path30.relative(path30.join(dir, "dist"), entryPath);
28928
+ if (pkg.name === "@wairon/cli" && pkg.main && !inDist.startsWith("..") && !path30.isAbsolute(inDist)) {
28929
+ return path30.resolve(dir, pkg.main);
28930
+ }
28931
+ } catch {
28932
+ }
28933
+ return entryPath;
28934
+ }
28935
+ const parent = path30.dirname(dir);
28936
+ if (parent === dir) return entryPath;
28937
+ dir = parent;
28938
+ }
28939
+ }
28940
+
28941
+ // src/mcp/server.ts
28942
+ function statusFamilyContext() {
28943
+ const lines = [];
28944
+ try {
28945
+ const parent = resolveChainingParent();
28946
+ if (parent) {
28947
+ let parentName;
28948
+ try {
28949
+ const system = readYamlFile(aiPathsAt(parent.parentRoot).specsSystem());
28950
+ if (typeof system?.name === "string") parentName = system.name;
28951
+ } catch {
28952
+ }
28953
+ lines.push(
28954
+ `Family: this project is a chained subproject, mounted as subsystem "${parent.subsystemId}" of ${parentName ? `the parent project "${parentName}"` : "its parent project"} \u2014 the surfaces it can consume are listed by sdd_list_external_interfaces.`
28955
+ );
28956
+ }
28957
+ const mounts = loadSubsystemSpecs().filter((s) => s.projectPath && !s.id.includes("::"));
28958
+ if (mounts.length > 0) {
28959
+ lines.push(`Family: chained subprojects mounted here \u2014 ${mounts.map((s) => `${s.id} (${s.projectPath})`).join(", ")}.`);
28960
+ }
28961
+ } catch {
28962
+ }
28963
+ return lines.length > 0 ? `${lines.join("\n")}
28964
+
28965
+ ` : "";
28966
+ }
28967
+ function resolveComponentVariantGuidance(component) {
28968
+ if (!component.variant) return null;
28969
+ const byId = new Map(loadProjectVariants().map((v) => [v.id, v]));
28970
+ return resolveVariantGuidance(component, loadComponentSpecs(), byId);
28971
+ }
28972
+ function qualifiedComponentId(id) {
28973
+ const ids = loadComponentSpecs().map((c) => c.id);
28974
+ if (ids.includes(id)) return id;
28975
+ const bare = id.startsWith("::") ? id.slice(2) : id;
28976
+ if (ids.includes(bare)) return bare;
28977
+ const chained = ids.filter((known) => known.endsWith(`::${bare}`));
28978
+ return chained.length === 1 ? chained[0] : id;
28979
+ }
28980
+ function publicInterfacesReplacement(subsystem, stored, list) {
28981
+ const identity = (pi) => pi.component ? `${pi.component}${pi.interface ? `.${pi.interface}` : ""}` : `${pi.type} ${pi.details}`;
28982
+ const restated = /* @__PURE__ */ new Map();
28983
+ for (const pi of list) restated.set(identity(pi), (restated.get(identity(pi)) ?? 0) + 1);
28984
+ const twice = [...restated].filter(([, n]) => n > 1).map(([key]) => `"${key}"`);
28985
+ if (twice.length) {
28986
+ return `Refusing to set the public interfaces of subsystem "${subsystem}": the list names ${twice.join(", ")} more than once. An entry is identified by the component and interface it is bound to \u2014 or, not yet bound, by its type and details \u2014 so two entries sharing one are the same entry stated twice. Drop the duplicate. Nothing was written.`;
28987
+ }
28988
+ const seen = /* @__PURE__ */ new Map();
28989
+ const deletes = [];
28990
+ for (const pi of stored) {
28991
+ const key = identity(pi);
28992
+ seen.set(key, (seen.get(key) ?? 0) + 1);
28993
+ if (seen.get(key) > (restated.get(key) ?? 0)) {
28994
+ deletes.push(pi.component ? { component: pi.component, interface: pi.interface, action: "delete" } : { type: pi.type, details: pi.details, action: "delete" });
28995
+ }
28996
+ }
28997
+ const restatedEntries = list.map((pi) => {
28998
+ const prev = stored.find((s) => identity(s) === identity(pi));
28999
+ const dropped = prev ? Object.keys(prev).filter((f) => prev[f] !== void 0 && pi[f] === void 0) : [];
29000
+ return dropped.length ? { ...pi, unset: dropped } : pi;
29001
+ });
29002
+ return { publicInterfaces: [...deletes, ...restatedEntries] };
29003
+ }
29004
+ function text(content) {
29005
+ return { content: [{ type: "text", text: content }] };
29006
+ }
29007
+ function json(value) {
29008
+ return text(JSON.stringify(value, null, 2));
29009
+ }
29010
+ function errText(message) {
29011
+ return { content: [{ type: "text", text: `Error: ${message.replace(/^(?:\s*Error:\s*)+/, "")}` }], isError: true };
29012
+ }
29013
+ function errMessage(e) {
29014
+ return errText(e instanceof Error ? e.message : String(e));
29015
+ }
29016
+ var TYPE_REF_GRAMMAR = (what) => `${what}: a primitive/builtin or a defined type id (qualified across subsystems, e.g. "billing.Invoice" or "billing::Invoice"). Generics, arrays and UNIONS are all read, at any depth: "Invoice | null" (the commonest shape there is), "Invoice | undefined", "Invoice | Receipt", "Promise<Invoice | null>", "Map<string, Invoice | null>", "Invoice[] | null", "(Invoice | null)[]". Every identifier the string names must resolve \u2014 a union of two defined types means BOTH must exist. A union of string literals ("read" | "write") names no type and resolves to nothing.`;
29017
+ function structured(content, data) {
29018
+ return { content: [{ type: "text", text: content }], structuredContent: data };
29019
+ }
29020
+ function jsonStructured(value) {
29021
+ return structured(JSON.stringify(value, null, 2), value);
29022
+ }
29023
+ var staleServerOutput = {
29024
+ staleServer: import_zod11.z.boolean().optional().describe(
29025
+ "True when the wairon build on disk changed after this server started \u2014 the structured twin of the text answer's STALE SERVER banner. When the rebuild left the spec and tool schemas alone, writes still go through; reconnect to run the new code."
29026
+ ),
29027
+ writesRefused: import_zod11.z.boolean().optional().describe(
29028
+ "True when the rebuild changed a spec or tool schema (or its schemas could not be compared): every spec write through this server is refused and writes nothing, because it would silently drop the fields only the new build knows. The human has to reconnect the server (/mcp reconnect wairon)."
29029
+ )
29030
+ };
29031
+ function renderChangeReport(report) {
29032
+ const lines = [report.dryRun ? `DRY RUN \u2014 nothing was written. ${report.summary}` : report.summary];
29033
+ for (const change of report.changes) {
29034
+ const to = change.after === void 0 ? "" : `: ${JSON.stringify(change.after)}`;
29035
+ const from = change.before === void 0 ? "" : ` (was ${JSON.stringify(change.before)})`;
29036
+ lines.push(`- ${change.path} ${report.dryRun ? `would be ${change.change}` : change.change}${to}${from}`);
29037
+ }
29038
+ if (report.ineffective.length) {
29039
+ lines.push("", "NO EFFECT:", ...report.ineffective.map((i) => `- ${i}`));
29040
+ }
29041
+ if (report.notices.length) lines.push("", "NOTICE:", ...report.notices.map((n) => `- ${n}`));
29042
+ lines.push(...renderTestsToRevisit(report.testsToRevisit));
29043
+ return lines.join("\n");
29044
+ }
29045
+ var SPEC_KINDS = ["system", "subsystem", "component", "interface", "implementation", "type"];
29046
+ var STATUS_VALUES = ["draft", "design", "complete"];
29047
+ var testsToRevisitOutput = import_zod11.z.array(import_zod11.z.object({
29048
+ method: import_zod11.z.string().describe("The contract method this write changed or deleted."),
29049
+ symbol: import_zod11.z.string().describe("The code-level name searched for: the method's `symbol` when it declares one, else its name."),
29050
+ imported: import_zod11.z.array(import_zod11.z.string()).describe("Test files that IMPORT that symbol \u2014 the high-confidence list, because an import is a binding and not a coincidence."),
29051
+ mentioned: import_zod11.z.array(import_zod11.z.string()).describe("Test files that name the symbol without importing it \u2014 usually a test driving it through a portal. Empty when `indiscriminate` is true."),
29052
+ indiscriminate: import_zod11.z.boolean().describe("True when the bare name matched more than a tenth of the test suite, so the mention list was withheld as noise.")
29053
+ })).describe(
29054
+ "The tests that encode a method this write changed or deleted, one entry per such method. Empty when the write touched no method, when the project declares no `rules.conformance.testRoots`, or when nothing references them. Read it before promising that existing tests still pass."
29055
+ );
29056
+ function renderTestsToRevisit(entries) {
29057
+ if (!entries.length) return [];
29058
+ const lines = ["", "TESTS TO REVISIT:"];
29059
+ for (const entry of entries) {
29060
+ lines.push(`- ${entry.method} (searched as "${entry.symbol}")`);
29061
+ if (entry.imported.length) lines.push(` imports it: ${entry.imported.join(", ")}`);
29062
+ if (entry.mentioned.length) lines.push(` names it: ${entry.mentioned.join(", ")}`);
29063
+ if (entry.indiscriminate) {
29064
+ lines.push(" names it: withheld \u2014 the bare name matches more than a tenth of the suite, so the list carries no signal.");
29065
+ }
29066
+ }
29067
+ return lines;
29068
+ }
29069
+ var specWriteReceiptOutput = {
29070
+ kind: import_zod11.z.enum(SPEC_KINDS).describe("The spec kind written."),
29071
+ id: import_zod11.z.string().describe('The id the spec is stored under; "system" for the L0 singleton.'),
29072
+ name: import_zod11.z.string().describe("The spec's display name as written."),
29073
+ replacedExisting: import_zod11.z.boolean().describe(
29074
+ "True when a spec already held this id and the call re-authored it in place rather than adding one. The notices then say what was carried forward, removed or cleared by the restatement."
29075
+ ),
29076
+ status: import_zod11.z.enum(STATUS_VALUES).optional().describe(
29077
+ "The lifecycle status written. Absent for the L0 and a type, which carry none."
29078
+ ),
29079
+ notices: import_zod11.z.array(import_zod11.z.string()).describe(
29080
+ "The notice lines the text answer lists, one per entry: what was carried forward, what a restatement removed, what an omission cleared, and every gate warning the write raised. Empty for a clean create."
29081
+ ),
29082
+ scaffoldedProjectPath: import_zod11.z.string().optional().describe(
29083
+ "The child project directory a chained subsystem's create scaffolded; absent for every other write."
29084
+ ),
29085
+ testsToRevisit: testsToRevisitOutput,
29086
+ ...staleServerOutput
29087
+ };
29088
+ function writeReceipt(sentence, receipt) {
29089
+ const noticeBlock = receipt.notices.length ? `
29090
+
29091
+ NOTICE:
29092
+ - ${receipt.notices.join("\n- ")}` : "";
29093
+ const testsBlock = renderTestsToRevisit(receipt.testsToRevisit).join("\n");
29094
+ return structured(`${sentence}${noticeBlock}${testsBlock ? `
29095
+ ${testsBlock}` : ""}`, receipt);
29096
+ }
29097
+ var specDeletionOutput = {
29098
+ kind: import_zod11.z.enum(SPEC_KINDS).describe("The spec kind deleted."),
29099
+ id: import_zod11.z.string().describe("The id the spec was stored under."),
29100
+ deleted: import_zod11.z.boolean().describe("True when a spec document was removed."),
29101
+ testsToRevisit: testsToRevisitOutput,
29102
+ ...staleServerOutput
29103
+ };
29104
+ var specChangeOutput = {
29105
+ path: import_zod11.z.string().describe("The dotted path the change addresses, with names and indexes."),
29106
+ change: import_zod11.z.enum(["set", "added", "removed", "cleared", "renumbered", "relocated"]).describe(
29107
+ "What happened at that path. A narrative is compared by step identity: `renumbered` is a run of unchanged steps shifted by an insert or delete (before/after name the ranges), `relocated` a jump field that followed its target to the target's new number."
29108
+ ),
29109
+ before: import_zod11.z.string().optional().describe("The previous value, summarized; absent when there was none."),
29110
+ after: import_zod11.z.string().optional().describe("The new value, summarized; absent when there is none.")
29111
+ };
29112
+ var specChangeReportOutput = {
29113
+ kind: import_zod11.z.enum(SPEC_KINDS).describe("The spec kind written."),
29114
+ id: import_zod11.z.string().describe("The stored spec's id."),
29115
+ written: import_zod11.z.boolean().describe("False when the merged spec equals what is stored: nothing reached disk."),
29116
+ dryRun: import_zod11.z.boolean().describe(
29117
+ "True when the caller asked what the delta WOULD do. `written` is false for a dry run exactly as it is for a delta that changed nothing, and only this field tells the two apart."
29118
+ ),
29119
+ changes: import_zod11.z.array(import_zod11.z.object(specChangeOutput)).describe(
29120
+ "Every change the write made; empty exactly when `written` is false."
29121
+ ),
29122
+ ineffective: import_zod11.z.array(import_zod11.z.string()).describe(
29123
+ "Every path the delta named that the write did not act on, each with why. Read it: a nested typo the permissive delta cannot refuse shows up here and nowhere else."
29124
+ ),
29125
+ notices: import_zod11.z.array(import_zod11.z.string()).describe("Store placement notices, gate warnings and delta notices."),
29126
+ summary: import_zod11.z.string().describe("One line for people."),
29127
+ testsToRevisit: testsToRevisitOutput,
29128
+ ...staleServerOutput
29129
+ };
29130
+ var validationIssueOutput = {
29131
+ severity: import_zod11.z.enum(["error", "warning"]).describe("The finding's severity after project overrides."),
29132
+ code: import_zod11.z.string().describe("The rule code, UPPER_SNAKE \u2014 the stable handle to filter and suppress by."),
29133
+ message: import_zod11.z.string().describe("What is wrong, named."),
29134
+ agentId: import_zod11.z.string().optional().describe("The agent the finding concerns, when it concerns one."),
29135
+ specId: import_zod11.z.string().optional().describe("The spec the finding concerns, when it concerns one."),
29136
+ draftContext: import_zod11.z.boolean().optional().describe(
29137
+ "True when the finding was raised against a draft/design spec \u2014 what the --ci gate waives."
29138
+ ),
29139
+ surfaceResolved: import_zod11.z.boolean().optional().describe(
29140
+ "True when the finding was verified against a vendored surface snapshot, so it is a contract verdict rather than a resolution failure."
29141
+ )
29142
+ };
29143
+ var validateTreeOutput = {
29144
+ valid: import_zod11.z.boolean().describe("False when the tree holds at least one error."),
29145
+ errors: import_zod11.z.array(import_zod11.z.object(validationIssueOutput)).describe("Every finding of severity error."),
29146
+ warnings: import_zod11.z.array(import_zod11.z.object(validationIssueOutput)).describe("Every finding of severity warning."),
29147
+ resolvedThrough: import_zod11.z.object({
29148
+ root: import_zod11.z.string().describe("The top root that was validated."),
29149
+ scope: import_zod11.z.string().describe("The mount chain the verdict was scoped to.")
29150
+ }).optional().describe(
29151
+ "Present when a chained subproject's verdict was resolved through its parent; absent when the tree was validated on its own."
29152
+ ),
29153
+ ...staleServerOutput
29154
+ };
29155
+ var getSpecOutput = {
29156
+ kind: import_zod11.z.enum(SPEC_KINDS).describe("The kind of spec read."),
29157
+ id: import_zod11.z.string().describe("The id it was read by."),
29158
+ spec: import_zod11.z.record(import_zod11.z.unknown()).describe(
29159
+ "The stored spec, exactly as the text block renders it \u2014 minus the two derived markers below, which the text folds in and this answer keeps separate so nothing derived can be mistaken for stored."
29160
+ ),
29161
+ partialResult: import_zod11.z.object({
29162
+ shown: import_zod11.z.array(import_zod11.z.string()).describe("The method names this answer carries."),
29163
+ omitted: import_zod11.z.number().describe("How many of the spec's methods are NOT in it."),
29164
+ warning: import_zod11.z.string().describe("Why it must never be re-authored from.")
29165
+ }).optional().describe(
29166
+ "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."
29167
+ ),
29168
+ variantGuidance: import_zod11.z.object({
29169
+ variant: import_zod11.z.string().describe("The variant id the component declares."),
29170
+ base: import_zod11.z.string().describe("The core stereotype it specializes."),
29171
+ guidance: import_zod11.z.string().describe("How to implement a component of this variant."),
29172
+ siblings: import_zod11.z.array(import_zod11.z.string()).describe("Other components of the same variant, to implement alike."),
29173
+ target: import_zod11.z.string().optional().describe("The target language the variant scopes itself to, if any."),
29174
+ profile: import_zod11.z.string().optional().describe("The architectural profile it scopes itself to, if any.")
29175
+ }).optional().describe(
29176
+ "Derived, read-only guidance for a variant-tagged component \u2014 resolved from the variant registry, not part of the spec. Never write it back."
29177
+ ),
29178
+ ...staleServerOutput
29179
+ };
29180
+ var FINGERPRINT_VERSION = 1;
29181
+ function schemaShape(schema, seen = /* @__PURE__ */ new Map()) {
29182
+ const def = schema?._def;
29183
+ if (!def) return null;
29184
+ const known = seen.get(schema);
29185
+ if (known !== void 0) return { ref: known };
29186
+ seen.set(schema, seen.size);
29187
+ const inner = (s) => schemaShape(s, seen);
29188
+ const kind = String(def.typeName);
29189
+ switch (kind) {
29190
+ case "ZodObject": {
29191
+ const shape = typeof def.shape === "function" ? def.shape() : def.shape;
29192
+ return {
29193
+ kind,
29194
+ keys: Object.keys(shape).sort().map((key) => [key, inner(shape[key])]),
29195
+ unknownKeys: def.unknownKeys,
29196
+ catchall: inner(def.catchall)
29197
+ };
29198
+ }
29199
+ case "ZodArray":
29200
+ return { kind, of: inner(def.type), min: def.minLength?.value, max: def.maxLength?.value };
29201
+ case "ZodOptional":
29202
+ case "ZodNullable":
29203
+ case "ZodReadonly":
29204
+ case "ZodBranded":
29205
+ case "ZodCatch":
29206
+ return { kind, of: inner(def.innerType ?? def.type) };
29207
+ case "ZodDefault":
29208
+ return { kind, of: inner(def.innerType), default: JSON.stringify(def.defaultValue?.()) };
29209
+ case "ZodEffects":
29210
+ return { kind, effect: def.effect?.type, of: inner(def.schema) };
29211
+ case "ZodLazy":
29212
+ return { kind, of: inner(def.getter()) };
29213
+ case "ZodUnion":
29214
+ case "ZodDiscriminatedUnion": {
29215
+ const options = def.options instanceof Map ? [...def.options.values()] : def.options;
29216
+ return { kind, discriminator: def.discriminator, of: options.map(inner) };
29217
+ }
29218
+ case "ZodIntersection":
29219
+ return { kind, left: inner(def.left), right: inner(def.right) };
29220
+ case "ZodTuple":
29221
+ return { kind, of: def.items.map(inner), rest: inner(def.rest) };
29222
+ case "ZodRecord":
29223
+ case "ZodMap":
29224
+ return { kind, key: inner(def.keyType), value: inner(def.valueType) };
29225
+ case "ZodSet":
29226
+ return { kind, of: inner(def.valueType) };
29227
+ case "ZodEnum":
29228
+ return { kind, values: def.values };
29229
+ case "ZodNativeEnum":
29230
+ return { kind, values: def.values };
29231
+ case "ZodLiteral":
29232
+ return { kind, value: def.value };
29233
+ case "ZodPipeline":
29234
+ return { kind, in: inner(def.in), out: inner(def.out) };
29235
+ default:
29236
+ return { kind, checks: def.checks?.map((c) => [c.kind, c.value ?? null]) ?? [] };
29237
+ }
29238
+ }
29239
+ var runningFingerprint;
29240
+ function schemaFingerprint() {
29241
+ if (runningFingerprint !== void 0) return runningFingerprint;
29242
+ const specs = {
29243
+ system: schemaShape(SystemSpecSchema),
29244
+ subsystem: schemaShape(SubsystemSpecSchema),
29245
+ component: schemaShape(ComponentSpecSchema),
29246
+ interface: schemaShape(InterfaceSpecSchema),
29247
+ implementation: schemaShape(ImplementationSpecSchema),
29248
+ type: schemaShape(TypeSpecSchema)
29249
+ };
29250
+ const registered = createMcpServer()._registeredTools;
29251
+ const tools = [...SPEC_WRITE_TOOLS].sort().map((name) => [name, schemaShape(registered[name]?.inputSchema)]);
29252
+ runningFingerprint = crypto4.createHash("sha256").update(JSON.stringify({ version: FINGERPRINT_VERSION, specs, tools })).digest("hex");
29253
+ return runningFingerprint;
29254
+ }
29255
+ var RECONNECT = "The human has to reconnect this MCP server (in Claude Code: /mcp reconnect wairon) \u2014 an agent cannot.";
29256
+ function assessBuildFreshness(stamp) {
29257
+ if (!isBuildStale(stamp)) return { state: "fresh", writesRefused: false, reason: "" };
29258
+ const running = schemaFingerprint();
29259
+ let onDisk;
29260
+ try {
29261
+ onDisk = readBuildFingerprint(stamp);
29262
+ } catch (e) {
29263
+ return {
29264
+ state: "unreadable",
29265
+ writesRefused: true,
29266
+ reason: `the wairon build on disk changed after this MCP server started, and its schemas could not be compared (${e instanceof Error ? e.message : String(e)}), so a schema change cannot be ruled out. Every spec write through this server is REFUSED and writes nothing. ${RECONNECT}`
29267
+ };
29268
+ }
29269
+ if (onDisk === running) {
29270
+ return {
29271
+ state: "build-changed",
29272
+ writesRefused: false,
29273
+ reason: "the wairon build on disk changed after this MCP server started. Its spec and tool schemas did not change, so spec writes still go through; reconnect the server (/mcp reconnect wairon) to run the new code."
29274
+ };
29275
+ }
29276
+ return {
29277
+ state: "schema-changed",
29278
+ writesRefused: true,
29279
+ reason: `the wairon build on disk changed after this MCP server started, and its spec or tool schemas changed with it. A write through this server would silently drop the fields only the new build knows, so every spec write is REFUSED and writes nothing. ${RECONNECT}`
29280
+ };
29281
+ }
29282
+ function markStale(result, freshness = assessedAsChanged, placement = "trail") {
29283
+ const marked = result.structuredContent ? {
29284
+ ...result,
29285
+ structuredContent: {
29286
+ ...result.structuredContent,
29287
+ staleServer: true,
29288
+ ...freshness.writesRefused ? { writesRefused: true } : {}
29289
+ }
29290
+ } : result;
29291
+ const banner = `\u26A0 STALE SERVER: ${freshness.reason}`;
29292
+ const first = marked.content?.[0];
29293
+ if (first && first.type === "text") {
29294
+ const textWithBanner = placement === "lead" ? `${banner}
29295
+
29296
+ ${first.text}` : `${first.text}
29297
+
29298
+ ${banner}`;
29299
+ return { ...marked, content: [{ ...first, text: textWithBanner }, ...marked.content.slice(1)] };
29300
+ }
29301
+ return marked;
29302
+ }
29303
+ var assessedAsChanged = {
29304
+ state: "build-changed",
29305
+ writesRefused: false,
29306
+ reason: "the wairon build on disk changed after this MCP server started. Reconnect the server (/mcp reconnect wairon) before further spec edits \u2014 a write through a stale server can silently drop fields a newer schema introduced."
29307
+ };
29308
+ var serverBuildStamps = /* @__PURE__ */ new WeakMap();
29309
+ var SERVER_BUILD_STAMP = captureBuildStamp(__filename);
29310
+ var SPEC_WRITE_TOOLS = /* @__PURE__ */ new Set([
29311
+ "sdd_initialize_system",
29312
+ "sdd_add_subsystem",
29313
+ "sdd_set_public_interfaces",
29314
+ "sdd_set_subsystem_project_path",
29315
+ "sdd_move_subsystem_project",
29316
+ "sdd_externalize_subsystem",
29317
+ "sdd_internalize_subsystem",
29318
+ "sdd_rename_component",
29319
+ "sdd_rename_method",
29320
+ "sdd_add_component",
29321
+ "sdd_define_interface",
29322
+ "sdd_set_endpoints",
29323
+ "sdd_write_narrative",
29324
+ "sdd_add_type",
29325
+ "sdd_delete_spec",
29326
+ "sdd_update_spec",
29327
+ "sdd_move_methods"
29328
+ ]);
29329
+ var listChangedEmitters = /* @__PURE__ */ new WeakMap();
29330
+ function reg(server, name, config, cb) {
29331
+ const guarded = (args) => {
29332
+ const freshness = assessBuildFreshness(serverBuildStamps.get(server) ?? null);
29333
+ if (freshness.writesRefused && SPEC_WRITE_TOOLS.has(name)) {
29334
+ return errText(`${name} refused \u2014 ${freshness.reason}`);
29335
+ }
29336
+ const answered = cb(args);
29337
+ const result = freshness.state === "fresh" ? answered : markStale(answered, freshness, name === "sdd_get_status" ? "lead" : "trail");
29338
+ if (result.isError !== true && SPEC_WRITE_TOOLS.has(name)) listChangedEmitters.get(server)?.();
29339
+ return result;
29340
+ };
29341
+ const strictConfig = config.inputSchema ? { ...config, inputSchema: import_zod11.z.object(config.inputSchema).strict() } : config;
29342
+ server.registerTool(name, strictConfig, guarded);
29343
+ }
29344
+ var statusInput = import_zod11.z.enum(STATUS_VALUES).optional().describe(
29345
+ "The spec's lifecycle status. Omitted means draft for a NEW spec and the status already stored for a re-authoring, so a restatement never reopens a frozen spec. State it to author straight at design or complete instead of promoting afterwards. A status that would LOWER the stored one is refused \u2014 reopening a spec for revision is sdd_update_spec's job, which sets the demotion deliberately."
29346
+ );
29347
+ var SKILL_RESOURCE_MIME = "text/markdown";
29348
+ function skillIdFromResourceUri(uri) {
29349
+ try {
29350
+ const u = new URL(uri);
29351
+ return u.host || u.pathname.replace(/^\/+/, "") || uri;
29352
+ } catch {
29353
+ return uri;
29354
+ }
29355
+ }
29356
+ var AGENT_BRIEF_SCHEME = "wairon-agent://";
29357
+ function listAgentBriefResources() {
29358
+ try {
29359
+ return resolveAgentTopology().map((a) => ({
29360
+ uri: `${AGENT_BRIEF_SCHEME}${a.id}`,
29361
+ name: a.name,
29362
+ description: a.description,
29363
+ mimeType: SKILL_RESOURCE_MIME
29364
+ }));
29365
+ } catch {
29366
+ return [];
29367
+ }
29368
+ }
29369
+ function renderAgentBriefMarkdown(brief) {
29370
+ const lines = [
29371
+ `# Agent brief: ${brief.agentId}`,
29372
+ "",
29373
+ `- **Name**: ${brief.name}`,
29374
+ `- **Template**: ${brief.template}`,
29375
+ ...brief.domainRoot ? [`- **Domain root**: ${brief.domainRoot}`] : [],
29376
+ `- **Owned paths**: ${brief.ownedPaths.join(", ")}`,
29377
+ ...brief.readPaths?.length ? [`- **Read paths**: ${brief.readPaths.join(", ")}`] : [],
29378
+ "",
29379
+ "## Instructions",
29380
+ "",
29381
+ brief.instructions
29382
+ ];
29383
+ if (brief.variantGuidance && !brief.instructions.includes(brief.variantGuidance)) {
29384
+ lines.push("", "## Variant guidance", "", brief.variantGuidance);
29385
+ }
29386
+ if (brief.budget && brief.profile) {
29387
+ lines.push(
29388
+ "",
29389
+ "## Execution budget",
29390
+ "",
29391
+ ...summarize(brief.budget, brief.profile),
29392
+ "",
29393
+ "Advisory \u2014 apply these when spawning. Map the capability tier onto your host tool's models; a tool that cannot express a field should ignore it rather than approximate it."
29394
+ );
29395
+ }
29396
+ return `${lines.join("\n")}
29397
+ `;
29398
+ }
29399
+ function registerSkillResources(server, listChanged) {
29400
+ server.server.registerCapabilities({ resources: listChanged ? { listChanged: true } : {} });
29401
+ server.server.setRequestHandler(import_types4.ListResourcesRequestSchema, () => ({
29402
+ resources: [
29403
+ ...listResources().map((d) => ({
29404
+ uri: d.resourceUri,
29405
+ name: d.name,
29406
+ description: d.description,
29407
+ mimeType: SKILL_RESOURCE_MIME
29408
+ })),
29409
+ ...listAgentBriefResources()
29410
+ ]
29411
+ }));
29412
+ server.server.setRequestHandler(import_types4.ReadResourceRequestSchema, (request) => {
29413
+ const uri = request.params.uri;
29414
+ if (uri.startsWith(AGENT_BRIEF_SCHEME)) {
29415
+ try {
29416
+ const brief = composeAgentBrief(uri.slice(AGENT_BRIEF_SCHEME.length));
29417
+ return { contents: [{ uri, mimeType: SKILL_RESOURCE_MIME, text: renderAgentBriefMarkdown(brief) }] };
29418
+ } catch (e) {
29419
+ throw new import_types4.McpError(import_types4.ErrorCode.InvalidParams, e instanceof Error ? e.message : String(e));
29420
+ }
29421
+ }
29422
+ try {
29423
+ const content = readResource(skillIdFromResourceUri(uri));
29424
+ return { contents: [{ uri, mimeType: SKILL_RESOURCE_MIME, text: content }] };
29425
+ } catch (e) {
29426
+ const message = e instanceof Error ? e.message : String(e);
29427
+ throw new import_types4.McpError(import_types4.ErrorCode.InvalidParams, message);
29428
+ }
29429
+ });
29430
+ }
29431
+ function registerSkillPrompts(server, listChanged) {
29432
+ server.server.registerCapabilities({ prompts: listChanged ? { listChanged: true } : {} });
29433
+ server.server.setRequestHandler(import_types4.ListPromptsRequestSchema, () => ({
29434
+ prompts: listResources().map((d) => ({
29435
+ name: d.id,
29436
+ title: d.name,
29437
+ description: d.description
29438
+ }))
29439
+ }));
29440
+ server.server.setRequestHandler(import_types4.GetPromptRequestSchema, (request) => {
29441
+ const name = request.params.name;
29442
+ try {
29443
+ const content = readResource(name);
29444
+ return {
29445
+ description: `The wairon "${name}" skill.`,
29446
+ messages: [{ role: "user", content: { type: "text", text: content } }]
29447
+ };
29448
+ } catch (e) {
29449
+ throw new import_types4.McpError(import_types4.ErrorCode.InvalidParams, e instanceof Error ? e.message : String(e));
29450
+ }
29451
+ });
29452
+ }
29453
+ function createMcpServer(options = {}) {
29454
+ const server = new import_mcp.McpServer({
29455
+ name: "wairon",
29456
+ version: WAIRON_VERSION
29457
+ }, {
29458
+ instructions: buildServerInstructions2()
29459
+ });
29460
+ serverBuildStamps.set(server, options.buildStamp === void 0 ? SERVER_BUILD_STAMP : options.buildStamp);
29461
+ if (!options.hostedTools) {
29462
+ listChangedEmitters.set(server, () => {
29463
+ try {
29464
+ void server.server.sendResourceListChanged().catch(() => {
29465
+ });
29466
+ void server.server.sendPromptListChanged().catch(() => {
29467
+ });
29468
+ } catch {
29469
+ }
29470
+ });
29471
+ }
29472
+ reg(
29473
+ server,
29474
+ "listAgents",
29475
+ {
29476
+ description: "List all AI agents registered in this project. Returns id, name, description, domainRoot, template, tags, and status for each agent. Filter by domainId to scope to one domain.",
29477
+ inputSchema: { domainId: import_zod11.z.string().optional() }
29478
+ },
29479
+ ({ domainId }) => {
29480
+ try {
29481
+ const registry = loadRegistry();
29482
+ const agents = domainId ? registry.agents.filter((a) => a.domainRoot === domainId) : registry.agents;
29483
+ return json(agents.map((a) => ({
29484
+ id: a.id,
29485
+ name: a.name,
29486
+ description: a.description,
29487
+ domainRoot: a.domainRoot,
29488
+ template: a.template,
29489
+ tags: a.tags,
29490
+ status: a.status
29491
+ })));
29492
+ } catch (e) {
29493
+ return errText(String(e));
29494
+ }
29495
+ }
29496
+ );
29497
+ reg(
29498
+ server,
29499
+ "getAgent",
29500
+ {
29501
+ description: "Get full details of a specific agent by id, including ownership rules, context, and output targets.",
29502
+ inputSchema: { id: import_zod11.z.string() }
29503
+ },
29504
+ ({ id }) => {
29505
+ try {
29506
+ const registry = loadRegistry();
29507
+ const agent = registry.agents.find((a) => a.id === id);
29508
+ if (!agent) return errText(`Agent "${id}" not found.`);
29509
+ return json(agent);
29510
+ } catch (e) {
29511
+ return errText(String(e));
29512
+ }
29513
+ }
29514
+ );
29515
+ reg(
29516
+ server,
29517
+ "listDomains",
29518
+ {
29519
+ description: "List all domains in this project: subsystem-derived (boundTo set) plus free-standing domains from .wai/topology.yaml."
29520
+ },
29521
+ () => {
29522
+ try {
29523
+ return json(resolveDomains2());
29524
+ } catch (e) {
29525
+ return errText(String(e));
29526
+ }
29527
+ }
29528
+ );
29529
+ reg(
29530
+ server,
29531
+ "validateTopology",
29532
+ {
29533
+ description: "Validate the project's agent topology. Returns errors and warnings (duplicate ids, overlapping ownership, missing paths, etc.). Supports optional subsystem scoping.",
29534
+ inputSchema: {
29535
+ subsystem: import_zod11.z.string().optional().describe("Only validate topology for agents under the specified subsystem")
29536
+ }
29537
+ },
29538
+ ({ subsystem }) => {
29539
+ try {
29540
+ let registry = loadRegistry();
29541
+ if (subsystem) {
29542
+ registry = {
29543
+ ...registry,
29544
+ agents: registry.agents.filter((a) => a.domainRoot === subsystem || a.domainRoot?.startsWith(`${subsystem}::`))
29545
+ };
29546
+ }
29547
+ const config = loadProjectConfig();
29548
+ if (!config) throw new ProjectNotInitializedError();
29549
+ const result = validateRegistry(registry, config.rules);
29550
+ return json({
29551
+ valid: result.issues.filter((i) => i.severity === "error").length === 0,
29552
+ errors: result.issues.filter((i) => i.severity === "error"),
29553
+ warnings: result.issues.filter((i) => i.severity === "warning")
29554
+ });
29555
+ } catch (e) {
29556
+ return errText(String(e));
29557
+ }
29558
+ }
29559
+ );
29560
+ reg(
29561
+ server,
29562
+ "getProjectConfig",
29563
+ {
29564
+ description: "Get the current project configuration (name, targets, rules, paths)."
29565
+ },
29566
+ () => {
29567
+ try {
29568
+ const config = loadProjectConfig();
29569
+ if (!config) throw new ProjectNotInitializedError();
29570
+ return json(config);
29571
+ } catch (e) {
29572
+ return errText(String(e));
29573
+ }
29574
+ }
29575
+ );
29576
+ const systemInput = {
29577
+ name: import_zod11.z.string().describe("Overarching name of the project/system"),
29578
+ vision: import_zod11.z.string().describe("Vision, mission, and core goals of the system"),
29579
+ boundaries: import_zod11.z.array(import_zod11.z.union([import_zod11.z.string(), import_zod11.z.object({ name: import_zod11.z.string(), description: import_zod11.z.string().optional() }).strict()])).optional().describe("System boundary rules or scope statements (strings or name/description objects)"),
29580
+ globalRequirements: import_zod11.z.array(import_zod11.z.union([import_zod11.z.string(), import_zod11.z.object({ description: import_zod11.z.string() }).strict()])).optional().describe("Global functional and non-functional requirements (strings or description objects)"),
29581
+ targetLanguage: import_zod11.z.string().optional().describe('Default implementation language for the system (e.g. "typescript", "rust", "python"). Subsystems may override. Enables language-aware validation.')
29582
+ };
29583
+ const systemInputFields = Object.keys(systemInput);
29584
+ reg(
29585
+ server,
29586
+ "sdd_initialize_system",
29587
+ {
29588
+ 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.",
29589
+ inputSchema: systemInput,
29590
+ outputSchema: specWriteReceiptOutput
29591
+ },
29592
+ ({ name, vision, boundaries, globalRequirements, targetLanguage }) => {
29593
+ try {
29594
+ const restatement = {
29595
+ kind: "system",
29596
+ spec: {
29597
+ name,
29598
+ vision,
29599
+ boundaries: boundaries ?? [],
29600
+ globalRequirements: globalRequirements ?? [],
29601
+ ...targetLanguage ? { targetLanguage } : {}
29602
+ },
29603
+ fields: systemInputFields
29604
+ };
29605
+ const receipt = writeSpec(restatement);
29606
+ return writeReceipt(
29607
+ `Successfully ${receipt.replacedExisting ? "re-authored" : "initialized"} L0 System Spec for "${name}".`,
29608
+ receipt
29609
+ );
29610
+ } catch (e) {
29611
+ return errMessage(e);
29612
+ }
29613
+ }
29614
+ );
29615
+ const subsystemInput = {
29616
+ id: import_zod11.z.string().describe("Lowercase identifier for the subsystem"),
29617
+ name: import_zod11.z.string().describe("Human-readable display name"),
29618
+ description: import_zod11.z.string().describe("Purpose and details of the subsystem"),
29619
+ publicInterfaces: import_zod11.z.array(import_zod11.z.object({
29620
+ type: import_zod11.z.enum(["REST", "GraphQL", "MessageBus", "RPC", "Custom"]),
29621
+ details: import_zod11.z.string(),
29622
+ component: import_zod11.z.string().optional().describe("The L2 component id that realizes this interface (this subsystem's published surface)"),
29623
+ interface: import_zod11.z.string().optional().describe("Optional L3 interface id on that component backing this entry"),
29624
+ consumers: import_zod11.z.array(import_zod11.z.string()).optional().describe("The subsystem ids this surface is published to. Absent, any subsystem may depend on the component; present, only these may \u2014 when every entry publishing the component names consumers, the union of the lists is the whole set, and a dependency from any other subsystem is CROSS_SUBSYSTEM_UNLISTED_CONSUMER. Every id must name a subsystem of the tree (PUBLIC_INTERFACE_UNKNOWN_CONSUMER).")
29625
+ }).strict()).optional().describe("Public entrypoints exposed by this subsystem, each bound to a realizing component"),
29626
+ projectPath: import_zod11.z.string().optional().describe("Relative path to external project root for subsystem chaining"),
29627
+ targetLanguage: import_zod11.z.string().optional().describe("Override of the system-level targetLanguage for this subsystem"),
29628
+ profile: import_zod11.z.string().optional().describe("Architectural profile override for this subsystem (built-ins: backend, frontend-reactive, frontend-controller, lowlevel-os, game-ecs, realtime-embedded, plc-cyclic; extension packs may add more \u2014 unknown names get UNKNOWN_PROFILE)"),
29629
+ designDepth: import_zod11.z.enum(["components", "interfaces", "implementations", "narratives"]).optional().describe("How deep THIS subsystem commits to designing (overrides project rules.designDepth; default narratives = full depth). Expectation checks below the depth are gated \u2014 soundness of authored content always applies."),
29630
+ trustedLinks: import_zod11.z.array(import_zod11.z.object({
29631
+ subsystem: import_zod11.z.string().describe("Peer subsystem id"),
29632
+ reason: import_zod11.z.string().describe('Why the coupling is sanctioned (e.g. "dispatch latency fast lane")')
29633
+ }).strict()).optional().describe("Sanctioned tight couplings with peers \u2014 required to acknowledge a mutual subsystem dependency; the Adapter \u2192 published Portal shape still applies."),
29634
+ lifecycle: import_zod11.z.array(import_zod11.z.object({
29635
+ phase: import_zod11.z.enum(["init", "shutdown", "cyclic", "interrupt", "scheduled"]).describe("Which lifecycle/execution flow this roots (cyclic = every scan/tick, interrupt = hardware/OS interrupt, scheduled = timer/cron)"),
29636
+ component: import_zod11.z.string().describe("Component id whose method the runtime invokes at this phase"),
29637
+ method: import_zod11.z.string().describe("Method name on that component's interface"),
29638
+ description: import_zod11.z.string().optional()
29639
+ }).strict()).optional().describe("Declared execution-flow roots \u2014 reachability entrypoints alongside Portals/Observers. Only init flows feed the durable-Store hydration check (MISSING_HYDRATION); cyclic/interrupt/scheduled root non-request/response execution models (PLC scan, ISR, cron)."),
29640
+ status: statusInput
29641
+ };
29642
+ const subsystemInputFields = [...Object.keys(subsystemInput), "parentSystem"];
29643
+ reg(
29644
+ server,
29645
+ "sdd_add_subsystem",
29646
+ {
29647
+ description: "Add an L1 Subsystem / Service under the system boundary. publicInterfaces should bind each entry to the component that realizes it (the subsystem's published surface); if components do not exist yet, add them later with sdd_set_public_interfaces. Re-running it on an existing id RE-AUTHORS it: the fields above are replaced, 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 child project directory a chained subsystem scaffolded.",
29648
+ inputSchema: subsystemInput,
29649
+ outputSchema: specWriteReceiptOutput
29650
+ },
29651
+ ({ id, name, description, publicInterfaces, projectPath, targetLanguage, profile, designDepth, trustedLinks, lifecycle, status }) => {
29652
+ try {
29653
+ const restatement = {
29654
+ kind: "subsystem",
29655
+ spec: {
29656
+ id,
29657
+ name,
29658
+ description,
29659
+ publicInterfaces: publicInterfaces ?? [],
29660
+ projectPath,
29661
+ ...targetLanguage ? { targetLanguage } : {},
29662
+ ...profile ? { profile } : {},
29663
+ ...designDepth ? { designDepth } : {},
29664
+ ...lifecycle ? { lifecycle } : {},
29665
+ trustedLinks: trustedLinks ?? []
29666
+ },
29667
+ fields: subsystemInputFields,
29668
+ status
29669
+ };
29670
+ const receipt = writeSpec(restatement);
29671
+ if (receipt.scaffoldedProjectPath) {
29672
+ return writeReceipt(
29673
+ `Successfully added external subsystem "${name}" (${id}) and scaffolded its child project at ${receipt.scaffoldedProjectPath}.`,
29674
+ receipt
29675
+ );
29676
+ }
29677
+ return writeReceipt(`Successfully added L1 Subsystem Spec "${name}" (${id}).`, receipt);
29678
+ } catch (e) {
29679
+ return errMessage(e);
29680
+ }
29681
+ }
29682
+ );
29683
+ reg(
29684
+ server,
29685
+ "sdd_set_public_interfaces",
29686
+ {
29687
+ description: "Set (replace) an existing subsystem's publicInterfaces, binding each to the component (and optional interface) that realizes it. Use this to backfill bindings once the subsystem's components exist \u2014 cross-subsystem dependencies may only target a published public component. A replacement, not a merge: an entry left out is removed. An entry's identity is its component and interface, so a list naming one identity twice is refused. Written through the authoring seam as one gated delta, and answered with the change report \u2014 every entry added, changed or removed, or that nothing changed \u2014 as structured content beside the sentence.",
29688
+ inputSchema: {
29689
+ subsystem: import_zod11.z.string().describe("The L1 subsystem id to update"),
29690
+ publicInterfaces: import_zod11.z.array(import_zod11.z.object({
29691
+ type: import_zod11.z.enum(["REST", "GraphQL", "MessageBus", "RPC", "Custom"]),
29692
+ details: import_zod11.z.string(),
29693
+ component: import_zod11.z.string().optional().describe("The L2 component id that realizes this interface"),
29694
+ interface: import_zod11.z.string().optional().describe("Optional L3 interface id on that component"),
29695
+ consumers: import_zod11.z.array(import_zod11.z.string()).optional().describe("The subsystem ids this surface is published to. Absent, any subsystem may depend on the component; present, only these may \u2014 when every entry publishing the component names consumers, the union of the lists is the whole set, and a dependency from any other subsystem is CROSS_SUBSYSTEM_UNLISTED_CONSUMER. Every id must name a subsystem of the tree (PUBLIC_INTERFACE_UNKNOWN_CONSUMER).")
29696
+ }).strict()).describe("The full replacement list of public interfaces for this subsystem")
29697
+ },
29698
+ outputSchema: specChangeReportOutput
29699
+ },
29700
+ ({ subsystem, publicInterfaces }) => {
29701
+ try {
29702
+ const sub = loadSubsystemSpec(subsystem);
29703
+ if (!sub) return errText(`Subsystem "${subsystem}" does not exist.`);
29704
+ const delta = publicInterfacesReplacement(subsystem, sub.publicInterfaces ?? [], publicInterfaces);
29705
+ if (typeof delta === "string") return errText(delta);
29706
+ const report = updateSpecGated("subsystem", subsystem, delta);
29707
+ return structured(
29708
+ `Updated public interfaces for subsystem "${subsystem}" (${publicInterfaces.length} ${publicInterfaces.length === 1 ? "entry" : "entries"}).
29709
+ ${renderChangeReport(report)}`,
29710
+ report
29711
+ );
29712
+ } catch (e) {
29713
+ return errMessage(e);
29714
+ }
29715
+ }
29716
+ );
29717
+ reg(
29718
+ server,
29719
+ "sdd_set_subsystem_project_path",
29720
+ {
29721
+ description: "Set (or clear) the projectPath of an L1 Subsystem for subsystem chaining. projectPath should be a relative path to the external project root directory. It rewrites the link only (sdd_move_subsystem_project relocates the directory with it). Written through the authoring seam as a gated delta, and answered with the change report as structured content beside the sentence.",
29722
+ inputSchema: {
29723
+ subsystem: import_zod11.z.string().describe("The L1 subsystem id to update"),
29724
+ projectPath: import_zod11.z.string().optional().describe("Relative path to external project root (or omit to clear)")
29725
+ },
29726
+ outputSchema: specChangeReportOutput
29727
+ },
29728
+ ({ subsystem, projectPath }) => {
29729
+ try {
29730
+ const delta = projectPath ? { projectPath } : { unset: ["projectPath"] };
29731
+ const report = updateSpecGated("subsystem", subsystem, delta);
29732
+ return structured(
29733
+ `Updated projectPath for subsystem "${subsystem}" to: ${projectPath || "none (cleared)"}
29734
+ ${renderChangeReport(report)}`,
29735
+ report
29736
+ );
29737
+ } catch (e) {
29738
+ return errMessage(e);
29739
+ }
29740
+ }
29741
+ );
29742
+ reg(
29743
+ server,
29744
+ "sdd_move_subsystem_project",
29745
+ {
29746
+ description: "Relocate an external subsystem: move its subproject directory on disk to newProjectPath and update its projectPath link in one step. Errors if the subsystem has no projectPath (not an external subproject).",
29747
+ inputSchema: {
29748
+ subsystem: import_zod11.z.string().describe("The external L1 subsystem id to relocate"),
29749
+ newProjectPath: import_zod11.z.string().describe("The new relative path for the subproject directory")
29750
+ }
29751
+ },
29752
+ ({ subsystem, newProjectPath }) => {
29753
+ try {
29754
+ moveSubsystemProject(subsystem, newProjectPath);
29755
+ return text(`Moved subsystem "${subsystem}" to: ${newProjectPath}`);
29756
+ } catch (e) {
29757
+ return errText(String(e));
29758
+ }
29759
+ }
29760
+ );
29761
+ reg(
29762
+ server,
29763
+ "sdd_externalize_subsystem",
29764
+ {
29765
+ description: "Migrate an internal subsystem into a standalone subproject at projectPath: move its spec subtree out, mount it via projectPath, and rewrite cross-subsystem references to the new namespaced ids. Source code is not moved. Errors if the subsystem is missing or already external.",
29766
+ inputSchema: {
29767
+ subsystem: import_zod11.z.string().describe("The internal L1 subsystem id to externalize"),
29768
+ projectPath: import_zod11.z.string().describe("Relative destination directory for the new subproject")
29769
+ }
29770
+ },
29771
+ ({ subsystem, projectPath }) => {
29772
+ try {
29773
+ externalizeSubsystem(subsystem, projectPath);
29774
+ return text(`Externalized subsystem "${subsystem}" into subproject: ${projectPath}. Move its source code there and re-validate.`);
29775
+ } catch (e) {
29776
+ return errText(String(e));
29777
+ }
29778
+ }
29779
+ );
29780
+ reg(
29781
+ server,
29782
+ "sdd_internalize_subsystem",
29783
+ {
29784
+ description: "Migrate an external subsystem back into the parent tree: move its subproject spec subtree back under the parent, drop projectPath, delete the child .wai project, and rewrite references back to bare ids. Errors if the subsystem is not external or its subproject is not a single flat subsystem.",
29785
+ inputSchema: {
29786
+ subsystem: import_zod11.z.string().describe("The external L1 subsystem id to internalize")
29787
+ }
29788
+ },
29789
+ ({ subsystem }) => {
29790
+ try {
29791
+ internalizeSubsystem(subsystem);
29792
+ return text(`Internalized subsystem "${subsystem}" back into this project (child .wai removed).`);
29793
+ } catch (e) {
29794
+ return errText(String(e));
29795
+ }
29796
+ }
29797
+ );
29798
+ reg(
29799
+ server,
29800
+ "sdd_rename_component",
29801
+ {
29802
+ description: "Rename a component and rewrite every reference to it in the bound tree: dependsOn, owns and dispatch entries, lifecycle entrypoints, published interfaces at L1 and L0, narrative call/dispatch/register targets, an interface's component, an implementation's contract and an entity's componentClass. The interface named i<id> and the implementation named <id>_impl move to the new ids and files; interfaces and implementations named otherwise keep their ids. Findings keyed on the component follow it too: every lint allow naming it on an edge site or in a covered unit, and every entry of the debt register (.wai/project.yaml rules.conformance.carried) anchored on a renamed spec or naming the component in its site or units \u2014 rewritten in place, every comment and all other formatting of the register kept, so unchanged debt never reads as paid and new at once. A register that cannot be rewritten that precisely is refused before the first write. Display names are left as they are. Refuses, writing nothing: a component that does not exist (component-missing), one inside a chained subproject (chained-component \u2014 rename it from that project's own root), a new id that is not a lowercase identifier without a namespace separator (invalid-id), and a new id \u2014 or i<newId> or <newId>_impl \u2014 already in use (id-taken). Returns the specs moved, each with its old and new id, the ids of the other specs rewritten (lint allows included), and `carried`: each register edit \u2014 the entry by code, spec and site as it stood, the key changed (spec, at or covers), from and to.",
29803
+ inputSchema: {
29804
+ id: import_zod11.z.string().describe("The component to rename (namespaced if needed)"),
29805
+ newId: import_zod11.z.string().describe("Its new id: a lowercase identifier with no namespace separator")
29806
+ }
29807
+ },
29808
+ ({ id, newId }) => {
29809
+ try {
29810
+ return json(renameComponent(qualifiedComponentId(id), newId));
29811
+ } catch (e) {
29812
+ return errText(String(e));
29813
+ }
29814
+ }
29815
+ );
29816
+ reg(
29817
+ server,
29818
+ "sdd_rename_method",
29819
+ {
29820
+ 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 a camel-case identifier (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.",
29821
+ inputSchema: {
29822
+ id: import_zod11.z.string().describe("The component whose method is renamed (namespaced if needed)"),
29823
+ method: import_zod11.z.string().describe("The method name as it stands"),
29824
+ newName: import_zod11.z.string().describe("Its new name: a camel-case identifier"),
29825
+ pinSymbol: import_zod11.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")
29826
+ }
29827
+ },
29828
+ ({ id, method, newName, pinSymbol }) => {
29829
+ try {
29830
+ return json(renameMethod(qualifiedComponentId(id), method, newName, pinSymbol));
29831
+ } catch (e) {
29832
+ return errText(String(e));
29833
+ }
29834
+ }
29835
+ );
29836
+ reg(
29837
+ server,
29838
+ "sdd_move_methods",
29839
+ {
29840
+ description: "Move contract methods from one component to another, carrying each method's narrative, calls and bindings, and re-pointing every reference that named the old home: narrative call/register/dispatch steps, dispatch-table bindings, lifecycle entrypoints and `calls` entries. The target gains the dependencies the moved narratives call. Findings keyed on a moved method follow it: a lint allow anchored at it moves to the receiving spec, one covering `<from>.<method>` names `<to>.<method>`, and the debt register (.wai/project.yaml rules.conformance.carried) is rekeyed the same way in place, every comment and all other formatting kept \u2014 the report's `carried` lists each register edit, and a register that cannot be rewritten that precisely is refused before the first write. ONE all-or-nothing gated write: if a rule refuses the move nothing is written and the report names the rule plus where these methods could live instead, cheapest legal home first, with the requested target among them so its shortfall reads beside the others. Prose is never rewritten and a wire endpoint keeps its address \u2014 moving a method between components must not silently re-address an RPC; both are reported as mentions. A target that declares no contract yet receives one, i<target>, and when implementation entries travel and no implementation realizes the receiving contract, <target>_impl is created for them, inheriting the source implementation's sourcePath; the report's `created` names every spec the move created. Refuses before the first write (unmovable request): a component or method that does not exist, a method name already declared on the target, a source and target that are the same component, a target with several contracts none named i<target>, a contract or implementation to create whose id another spec already holds, and a component inside a chained subproject (move it from that project's own root). A dry run runs the whole move, gate included, and reports what it would have done.",
29841
+ inputSchema: {
29842
+ from: import_zod11.z.string().describe("The component the methods belong to (namespaced if needed)"),
29843
+ to: import_zod11.z.string().describe("The component that should receive them (namespaced if needed)"),
29844
+ methods: import_zod11.z.array(import_zod11.z.string()).describe("The method names to move; every one must exist on the source contract"),
29845
+ dryRun: import_zod11.z.boolean().optional().describe("Answer with what the move would do and write nothing")
29846
+ }
29847
+ },
29848
+ ({ from, to, methods, dryRun }) => {
29849
+ try {
29850
+ const source = qualifiedComponentId(from);
29851
+ const target = qualifiedComponentId(to);
29852
+ if (!loadComponentSpec(source)) return errText(`no component has the id "${from}".`);
29853
+ if (!loadComponentSpec(target)) return errText(`no component has the id "${to}".`);
29854
+ return json(moveMethods2(source, target, methods, dryRun));
29855
+ } catch (e) {
29856
+ return errText(String(e));
29857
+ }
29858
+ }
29859
+ );
29860
+ const componentInput = {
29861
+ id: import_zod11.z.string().describe("Lowercase identifier for the component"),
29862
+ name: import_zod11.z.string().describe("Human-readable display name"),
29863
+ description: import_zod11.z.string().describe("Responsibility / internal architecture details"),
29864
+ subsystem: import_zod11.z.string().describe("The L1 subsystem ID this component belongs to"),
29865
+ componentType: import_zod11.z.enum(["Portal", "Orchestrator", "Supervisor", "Actor", "Store", "Index", "Query", "Registry", "Adapter", "Observer", "Repository"]).describe("The building block, or the pattern Repository. Specialist and Gateway are retired and cannot be authored: logic is an Orchestrator with a dependencyClass, a gateway is a Portal with the gateway variant"),
29866
+ owns: import_zod11.z.array(import_zod11.z.string()).optional().describe("Member block ids privately owned by this component (patterns only)"),
29867
+ dependsOn: import_zod11.z.array(import_zod11.z.string()).optional().describe("IDs of other components this collaborates with (facades or standalone blocks)"),
29868
+ portalType: import_zod11.z.enum(["HTTP_API", "gRPC", "GraphQL", "MessageBus", "CLI", "NamedPipe", "IPC", "Custom"]).optional().describe("Portal-only, and expected on every Portal. OMIT IT on any other componentType \u2014 passing it there is refused at the write (UNEXPECTED_PORTAL_FIELD), nothing is saved."),
29869
+ basePath: import_zod11.z.string().optional().describe("Portal-only: base path/prefix all the portal's endpoints mount under. OMIT IT on any other componentType \u2014 passing it there is refused at the write (UNEXPECTED_PORTAL_FIELD), nothing is saved."),
29870
+ dispatch: import_zod11.z.array(import_zod11.z.object({
29871
+ capability: import_zod11.z.string().describe('Capability name exactly as dispatched at runtime (e.g. "shadow_module.get")'),
29872
+ component: import_zod11.z.string().describe("Component id serving this capability (must also appear under dependsOn/owns)"),
29873
+ method: import_zod11.z.string().describe("Method name on the serving component's interface"),
29874
+ description: import_zod11.z.string().optional()
29875
+ }).strict()).optional().describe("Portal-only: capability \u2192 component.method dispatch table for generic-handle portals. Gives the reachability walker real edges and is validated against target interfaces (UNSERVED_CAPABILITY)."),
29876
+ mounts: import_zod11.z.array(import_zod11.z.object({
29877
+ portal: import_zod11.z.string().describe("The mounted Portal's component id"),
29878
+ prefixes: import_zod11.z.array(import_zod11.z.string()).describe('The path prefixes this listener routes to the portal. A path lies under a prefix when it equals it or continues it past a slash, so "/" covers only the root itself'),
29879
+ via: import_zod11.z.string().optional().describe("The router entry the listener calls to hand the portal its request, exported by the PORTAL's own file (held to it: UNREALIZED_EXPORT_HANDLE). Omit when the listener calls the portal's contract methods directly, route by route")
29880
+ }).strict()).optional().describe("Portal-only: the portals this LISTENER serves, each under its path prefixes and through its router entry. Declaring the field \u2014 even as an empty array \u2014 marks this portal as a listener, the one kind of portal nothing else needs to mount. Checked by portal-mounts: a mount must name a Portal (MOUNT_TARGET_NOT_PORTAL), every HTTP endpoint of a mounted portal must lie under one of its prefixes (ENDPOINT_OUTSIDE_MOUNT), and a portal with HTTP endpoints that is neither a listener nor mounted by one is reported (UNMOUNTED_PORTAL)."),
29881
+ durability: import_zod11.z.enum(["ram-projection", "durable", "read-through", "cache"]).optional().describe("Store-only \u2014 OMIT IT on any other componentType, where it is refused at the write (DURABILITY_ON_NON_STORE) and nothing is saved. Every Store should declare one (MISSING_DURABILITY): durable = persisted RAM projection (hydration read-back from a lifecycle init entrypoint required \u2014 MISSING_HYDRATION); read-through = persisted with no RAM copy (every read is the read-back, hydration exempt); ram-projection = rebuilt not restored; cache = evictable loss-safe memo state."),
29882
+ dependencyClass: import_zod11.z.enum(["pure", "read"]).optional().describe("Orchestrator-only \u2014 OMIT IT on any other componentType, where it is refused at the write (DEPENDENCY_CLASS_ON_NON_ORCHESTRATOR) and nothing is saved. Declares what logic may depend on, enforced as a Store's durability is (DEPENDENCY_CLASS_VIOLATION): pure = depends only on pure Orchestrators (a computation over the values it is handed, e.g. an arbiter or codec); read = also on read Orchestrators, Repositories, Indexes and Adapters, never calling their write methods; unset = a workflow."),
29883
+ emits: import_zod11.z.array(import_zod11.z.object({
29884
+ topic: import_zod11.z.string().describe("Topic/channel name exactly as used on the bus"),
29885
+ event: import_zod11.z.string().optional().describe("Optional event name within the topic (informational; pairing is by topic)"),
29886
+ description: import_zod11.z.string().optional()
29887
+ }).strict()).optional().describe("Topics this component publishes \u2014 every emitted topic needs a subscriber somewhere in the tree (UNCONSUMED_TOPIC)."),
29888
+ subscribesTo: import_zod11.z.array(import_zod11.z.object({
29889
+ topic: import_zod11.z.string().describe("Topic/channel name exactly as used on the bus"),
29890
+ event: import_zod11.z.string().optional(),
29891
+ description: import_zod11.z.string().optional()
29892
+ }).strict()).optional().describe("Topics this component consumes (typical on Observers) \u2014 every subscription needs an emitter somewhere in the tree (UNSOURCED_SUBSCRIPTION)."),
29893
+ ext: import_zod11.z.record(import_zod11.z.unknown()).optional().describe('Opaque pack/tool extension data (namespaced keys, e.g. "mypack:priority") \u2014 preserved verbatim, never validated or interpreted by the core'),
29894
+ status: statusInput
29895
+ };
29896
+ const componentInputFields = Object.keys(componentInput);
29897
+ reg(
29898
+ server,
29899
+ "sdd_add_component",
29900
+ {
29901
+ description: `Add an L2 Component under a subsystem. componentType is a building block (Portal, Orchestrator, Supervisor, Actor, Store, Index, Query, Registry, Adapter, Observer) or the pattern Repository. Specialist and Gateway are retired (STEREOTYPE_RETIRED) and cannot be authored: logic is an Orchestrator with a dependencyClass (pure | read; unset = a workflow), and a gateway is a Portal with the gateway variant (set variant with sdd_update_spec). Patterns set "owns" (their private member blocks); all components set "dependsOn" (collaborators \u2014 facades or standalone blocks). Held/persisted state (configs, permissions, sessions, caches): model the Repository recipe \u2014 a Store + Registry (write) + Index (read), plus a Query for computed reads over the Store, owned by a Repository facade consumers depend on; a deliberately standalone Store is the sanctioned lightweight form (workflow-layer consumers + lint.allow on UNOWNED_STORE). Never hold state as fields inside an Orchestrator because a Store link was refused. Re-running it on an existing id RE-AUTHORS it: the fields above are replaced (an omitted array is CLEARED), while lint.allow, a Portal's auth, variant, patterns and externalLinks are carried forward \u2014 edit those with sdd_update_spec. 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 every gate notice the write raised, each as its own entry.`,
29902
+ inputSchema: componentInput,
29903
+ outputSchema: specWriteReceiptOutput
29904
+ },
29905
+ ({ id, name, description, subsystem, componentType, owns, dependsOn, portalType, basePath, dispatch, mounts, durability, dependencyClass, emits, subscribesTo, ext, status }) => {
29906
+ try {
29907
+ const restatement = {
29908
+ kind: "component",
29909
+ spec: {
29910
+ id,
29911
+ name,
29912
+ description,
29913
+ subsystem,
29914
+ componentType,
29915
+ owns: owns ?? [],
29916
+ dependsOn: dependsOn ?? [],
29917
+ ...portalType ? { portalType } : {},
29918
+ ...basePath ? { basePath } : {},
29919
+ ...dispatch ? { dispatch } : {},
29920
+ // An EMPTY list is kept: declaring the field is what marks a listener.
29921
+ ...mounts ? { mounts } : {},
29922
+ ...durability ? { durability } : {},
29923
+ ...dependencyClass ? { dependencyClass } : {},
29924
+ ...emits ? { emits } : {},
29925
+ ...subscribesTo ? { subscribesTo } : {},
29926
+ ...ext ? { ext } : {}
29927
+ },
29928
+ fields: componentInputFields,
29929
+ status
29930
+ };
29931
+ const receipt = writeSpec(restatement);
29932
+ return writeReceipt(
29933
+ `Successfully ${receipt.replacedExisting ? "re-authored" : "added"} L2 Component Spec "${name}" (${id}, ${componentType}).`,
29934
+ receipt
29935
+ );
29936
+ } catch (e) {
29937
+ return errMessage(e);
29938
+ }
29939
+ }
29940
+ );
29941
+ const interfaceMethodShape = {
29942
+ name: import_zod11.z.string(),
29943
+ description: import_zod11.z.string(),
29944
+ signature: import_zod11.z.string(),
29945
+ returns: import_zod11.z.string().describe(TYPE_REF_GRAMMAR("The type the method answers with")),
29946
+ params: import_zod11.z.array(import_zod11.z.object({
29947
+ name: import_zod11.z.string(),
29948
+ type: import_zod11.z.string().describe(TYPE_REF_GRAMMAR("The parameter's type")),
29949
+ description: import_zod11.z.string().optional(),
29950
+ optional: import_zod11.z.boolean().optional().describe(
29951
+ 'Whether the parameter may be OMITTED by a caller. It is not nullability: a parameter that must be passed but may be passed as nothing is a required parameter whose type is a union \u2014 "ProjectConfig | null". Say whichever is true; they are different contracts.'
29952
+ )
29953
+ }).strict()).optional().describe("Structured parameters \u2014 authoritative for type checking (the prose signature becomes display-only). Strongly preferred."),
29954
+ guarantees: import_zod11.z.array(import_zod11.z.string().min(1)).optional().describe("Semantic guarantees the method promises (combinable); any guarantee a narrative step asserts must be declared here. Builtin tokens: idempotent | atomic | transactional | exactly-once; extension packs may declare more (any other token is UNKNOWN_GUARANTEE)"),
29955
+ effect: import_zod11.z.enum(["read", "write"]).optional().describe("State-effect direction on the component's held state \u2014 required on a durable Store's contract methods so the durability round-trip rule can pair writes with hydration read-backs"),
29956
+ invokedBy: import_zod11.z.object({
29957
+ kind: import_zod11.z.enum(["runtime", "external", "sibling-subsystem"]).describe("Who owns the out-of-graph invocation: runtime (timer/signal/shutdown hook), external (a system outside this project), sibling-subsystem (a modeled sibling whose edge is not narrated here)"),
29958
+ caller: import_zod11.z.string().optional().describe("WHO invokes it and when, as reviewable prose \u2014 missing or placeholder-thin prose is INVOKED_BY_UNDESCRIBED")
29959
+ }).strict().optional().describe("Typed acknowledgment of a real caller OUTSIDE the modeled narrative graph. Unused-detection seeds the method as an entrypoint so reachability propagates through its narrative (unlike lint.allow); a method the internal walk already reaches is flagged stale (INVOKED_BY_REDUNDANT). Prefer a `register` narrative step when the wiring is internal."),
29960
+ findings: import_zod11.z.array(import_zod11.z.object({
29961
+ code: import_zod11.z.string().describe("UPPER_SNAKE finding code, unique within the method; a pack's codes carry the pack prefix (<PACK>_<CODE>)"),
29962
+ severity: import_zod11.z.enum(["error", "warning"]).describe("Default severity, before project severity overrides and draft-context downgrades"),
29963
+ summary: import_zod11.z.string().describe("One line saying what the finding means")
29964
+ }).strict()).optional().describe("The finding codes this method can report, each with its default severity and summary. Each declared code must be anchored in the method's source file, as a string literal or a property-access name (UNREALIZED_FINDING). A code is declared once per method; sdd_update_spec upserts and deletes findings by code."),
29965
+ ext: import_zod11.z.record(import_zod11.z.unknown()).optional().describe("Opaque pack/tool extension data for this method (namespaced keys) \u2014 preserved verbatim")
29966
+ };
29967
+ const interfaceMethodInputFields = Object.keys(interfaceMethodShape);
29968
+ const interfaceInput = {
29969
+ id: import_zod11.z.string().describe('Lowercase identifier prefixed with "i", e.g. "istorage"'),
29970
+ name: import_zod11.z.string().describe("Human-readable contract name"),
29971
+ description: import_zod11.z.string().describe("Contract description and obligations"),
29972
+ component: import_zod11.z.string().describe("The L2 component ID this interface belongs to"),
29973
+ methods: import_zod11.z.array(import_zod11.z.object(interfaceMethodShape).strict()).optional().describe("List of method signature contracts"),
29974
+ status: statusInput
29975
+ };
29976
+ const interfaceInputFields = Object.keys(interfaceInput);
29977
+ reg(
29978
+ server,
29979
+ "sdd_define_interface",
29980
+ {
29981
+ description: "Define an L3 Contract / Interface with method signatures for a component. Prefer supplying structured `params` per method \u2014 they are the authoritative source for type checking (the free-form signature string then becomes display-only and is never heuristically parsed). A method declares the finding codes it reports in `findings` ({code, severity, summary}); each code must be anchored in the method's source file, as a string literal or a property-access name (UNREALIZED_FINDING). Re-defining an existing id REPLACES the method list: a method left out of the input is REMOVED (and reported); spec-level lint/ext and each method's endpoint binding 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.",
29982
+ inputSchema: interfaceInput,
29983
+ outputSchema: specWriteReceiptOutput
29984
+ },
29985
+ ({ id, name, description, component, methods, status }) => {
29986
+ try {
29987
+ const restatement = {
29988
+ kind: "interface",
29989
+ spec: { id, name, description, component, methods: methods ?? [] },
29990
+ fields: interfaceInputFields,
29991
+ memberFields: interfaceMethodInputFields,
29992
+ status
29993
+ };
29994
+ const receipt = writeSpec(restatement);
29995
+ return writeReceipt(
29996
+ `Successfully ${receipt.replacedExisting ? "re-authored" : "defined"} L3 Interface Contract "${name}" (${id}).`,
29997
+ receipt
29998
+ );
29999
+ } catch (e) {
30000
+ return errMessage(e);
30001
+ }
30002
+ }
30003
+ );
30004
+ reg(
30005
+ server,
30006
+ "sdd_set_endpoints",
30007
+ {
30008
+ description: "Bind concrete wire endpoints to existing L3 interface methods (required for every Portal). Pick `transport` and fill that transport's address fields. Run after sdd_define_interface. Written through the authoring seam as one gated delta, and answered with the change report \u2014 every binding set or changed, or that nothing changed \u2014 as structured content beside the sentence.",
30009
+ inputSchema: {
30010
+ interface: import_zod11.z.string().describe('The L3 interface ID (e.g. "ibilling-gateway")'),
30011
+ endpoints: import_zod11.z.array(import_zod11.z.object({
30012
+ method: import_zod11.z.string().describe("The NAME of the interface method to bind (not the HTTP verb)"),
30013
+ transport: import_zod11.z.enum(["HTTP", "gRPC", "GraphQL", "MessageBus", "NamedPipe", "IPC", "CLI", "Custom"]).describe("Wire protocol; must match the Portal's portalType"),
30014
+ httpMethod: import_zod11.z.enum(["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]).optional().describe("HTTP: verb"),
30015
+ path: import_zod11.z.string().optional().describe('HTTP: route path, e.g. "/v1/checkout"'),
30016
+ service: import_zod11.z.string().optional().describe("gRPC: service name"),
30017
+ rpcMethod: import_zod11.z.string().optional().describe("gRPC: rpc method name"),
30018
+ operation: import_zod11.z.enum(["query", "mutation", "subscription"]).optional().describe("GraphQL: operation kind"),
30019
+ field: import_zod11.z.string().optional().describe("GraphQL: root field name"),
30020
+ topic: import_zod11.z.string().optional().describe("MessageBus: topic"),
30021
+ event: import_zod11.z.string().optional().describe("MessageBus: event name"),
30022
+ queue: import_zod11.z.string().optional().describe("MessageBus: optional queue/consumer group"),
30023
+ direction: import_zod11.z.enum(["subscribe", "publish"]).optional().describe("MessageBus: subscribe (default) or publish"),
30024
+ pipe: import_zod11.z.string().optional().describe('NamedPipe: pipe name, e.g. "\\\\.\\pipe\\gk-events"'),
30025
+ channel: import_zod11.z.string().optional().describe("IPC: channel name"),
30026
+ command: import_zod11.z.string().optional().describe("CLI: command/subcommand"),
30027
+ address: import_zod11.z.string().optional().describe("Custom: free-form address")
30028
+ }).strict()).describe("One binding per method")
30029
+ },
30030
+ outputSchema: specChangeReportOutput
30031
+ },
30032
+ ({ interface: interfaceId, endpoints }) => {
30033
+ try {
30034
+ const intf = loadInterfaceSpec(interfaceId);
30035
+ if (!intf) return errText(`Interface "${interfaceId}" does not exist.`);
30036
+ const buildRaw = (e) => {
30037
+ switch (e.transport) {
30038
+ case "HTTP":
30039
+ return { transport: "HTTP", method: e.httpMethod, path: e.path };
30040
+ case "gRPC":
30041
+ return { transport: "gRPC", service: e.service, method: e.rpcMethod };
30042
+ case "GraphQL":
30043
+ return { transport: "GraphQL", operation: e.operation, field: e.field };
30044
+ case "MessageBus":
30045
+ return { transport: "MessageBus", topic: e.topic, event: e.event, queue: e.queue, direction: e.direction ?? "subscribe" };
30046
+ case "NamedPipe":
30047
+ return { transport: "NamedPipe", pipe: e.pipe };
30048
+ case "IPC":
30049
+ return { transport: "IPC", channel: e.channel };
30050
+ case "CLI":
30051
+ return { transport: "CLI", command: e.command };
30052
+ case "Custom":
30053
+ return { transport: "Custom", address: e.address };
30054
+ }
30055
+ };
30056
+ const bound2 = [];
30057
+ const methods = [];
30058
+ for (const e of endpoints) {
30059
+ const m = intf.methods.find((x) => x.name === e.method);
30060
+ if (!m) return errText(`Method "${e.method}" not found on interface "${interfaceId}". Define it via sdd_define_interface first.`);
30061
+ const parsed = EndpointSchema.safeParse(buildRaw(e));
30062
+ if (!parsed.success) {
30063
+ const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
30064
+ return errText(`Invalid ${e.transport} endpoint for method "${e.method}": ${detail}`);
30065
+ }
30066
+ methods.push({ name: e.method, endpoint: parsed.data });
30067
+ bound2.push(`${e.method}\u2192${e.transport}`);
30068
+ }
30069
+ const report = updateSpecGated("interface", interfaceId, { methods });
30070
+ return structured(
30071
+ `Bound ${bound2.length} endpoint(s) on "${interfaceId}": ${bound2.join(", ")}.
30072
+ ${renderChangeReport(report)}`,
30073
+ report
30074
+ );
30075
+ } catch (e) {
30076
+ return errMessage(e);
30077
+ }
30078
+ }
30079
+ );
30080
+ const stepNo = () => import_zod11.z.number().int().positive();
30081
+ const labelRef = () => import_zod11.z.string().min(1);
30082
+ const narrativeStepInput = import_zod11.z.object({
30083
+ stepNumber: stepNo().optional().describe("Defaults to the 1-based array position \u2014 jump fields reference these numbers"),
30084
+ label: labelRef().optional().describe("Optional symbolic anchor for this step (unique per narrative). Every jump-by-number field has a *Label twin resolved against these anchors at write time \u2014 prefer labels over hand-counted step numbers"),
30085
+ description: import_zod11.z.string(),
30086
+ type: import_zod11.z.enum(["local", "call", "dispatch", "register", "branch", "switch", "loop", "try", "parallel", "jump", "return", "throw"]),
30087
+ targetComponent: import_zod11.z.string().optional().describe("call/register/dispatch: L2 component id (for dispatch, the Portal routed through)"),
30088
+ targetMethod: import_zod11.z.string().optional().describe("call/register: method name on the target. A register step hands the target method to the runtime as a callback (timer, event listener, shutdown hook): reachability follows the edge, but it is never an invocation \u2014 exempt from call-graph conformance, call-cycle detection, and the durability boot walk"),
30089
+ auth: import_zod11.z.object({ from: import_zod11.z.string(), note: import_zod11.z.string().optional() }).strict().optional().describe("call/dispatch: the credential this step presents to an AUTHED callee Portal and WHERE it loads from (`from`). Opaque form (env:API_KEY, a config key, vault:path) = a design note wairon never resolves; modeled form `component:<id>` references the Adapter/Store that provides the secret and is validated (must resolve, be an Adapter/Store, and be wired to the presenter). Absence on a call into a Portal whose auth \u2260 none warns (PORTAL_AUTH_UNMET). The authenticated call itself should be made by an Adapter (AUTH_PRESENTER_NOT_ADAPTER)."),
30090
+ detach: import_zod11.z.boolean().optional().describe("call/dispatch: fire-and-forget \u2014 issue the call and continue without awaiting the result (no later step consumes it)"),
30091
+ capability: import_zod11.z.string().optional().describe("dispatch: the capability routed through the target Portal's dispatch table (validated against it \u2014 UNSERVED_CAPABILITY)"),
30092
+ assertsGuarantees: import_zod11.z.array(import_zod11.z.string().min(1)).optional().describe("Semantic guarantees this step relies on \u2014 each must be declared in the called method's L3 guarantees (NARRATIVE_SEMANTIC_UNBACKED otherwise). Builtin tokens: idempotent | atomic | transactional | exactly-once; extension packs may declare more (any other token is UNKNOWN_GUARANTEE)"),
30093
+ assertsInvariants: import_zod11.z.array(import_zod11.z.string()).optional().describe(`Declared entity invariants this step upholds, as "<type-id>.<invariant-id>" refs (UNKNOWN_INVARIANT_REF when unresolved); write-effect methods of the entity's componentClass must carry one per declared invariant (UNASSERTED_INVARIANT)`),
30094
+ condition: import_zod11.z.string().optional().describe("branch / while / doWhile"),
30095
+ onTrueStep: stepNo().optional().describe("branch: default = next step"),
30096
+ onTrueLabel: labelRef().optional().describe("branch: symbolic alternative to onTrueStep"),
30097
+ onFalseStep: stepNo().optional().describe("branch: required (or onFalseLabel)"),
30098
+ onFalseLabel: labelRef().optional().describe("branch: symbolic alternative to onFalseStep"),
30099
+ on: import_zod11.z.string().optional().describe("switch: the dispatched value"),
30100
+ cases: import_zod11.z.array(import_zod11.z.object({ value: import_zod11.z.string(), step: stepNo().optional(), label: labelRef().optional() }).strict()).optional().describe("switch: required; each case targets its region by step number or label"),
30101
+ defaultStep: stepNo().optional().describe("switch: default = next step"),
30102
+ defaultLabel: labelRef().optional().describe("switch: symbolic alternative to defaultStep"),
30103
+ loopKind: import_zod11.z.enum(["forEach", "for", "while", "doWhile"]).optional().describe('loop: default forEach when "over" is set, else while'),
30104
+ over: import_zod11.z.string().optional().describe("loop forEach/for: iteration source"),
30105
+ endStep: stepNo().optional().describe("loop/try: last step of the body region (required, or endLabel)"),
30106
+ endLabel: labelRef().optional().describe("loop/try: symbolic alternative to endStep"),
30107
+ catches: import_zod11.z.array(import_zod11.z.object({ error: import_zod11.z.string(), step: stepNo().optional(), label: labelRef().optional() }).strict()).optional().describe("try: handler regions, each targeted by step number or label"),
30108
+ finallyStep: stepNo().optional().describe("try: first step of the always-runs region"),
30109
+ finallyLabel: labelRef().optional().describe("try: symbolic alternative to finallyStep"),
30110
+ branches: import_zod11.z.array(import_zod11.z.object({ step: stepNo().optional(), label: labelRef().optional(), name: import_zod11.z.string().optional() }).strict()).optional().describe("parallel: >= 2 arm entries (step number or label), ascending, first = the step after the header; arms are contiguous sub-regions of body next..endStep, joining after endStep once ALL complete"),
30111
+ toStep: stepNo().optional().describe("jump: required (break/continue/rejoin), or toLabel"),
30112
+ toLabel: labelRef().optional().describe("jump: symbolic alternative to toStep \u2014 resolves to the labeled step at write time"),
30113
+ outcome: import_zod11.z.string().optional().describe('return: e.g. "success", "not found"'),
30114
+ error: import_zod11.z.string().optional().describe("throw: the raised error")
30115
+ }).strict();
30116
+ const detailEnum = import_zod11.z.enum(["full", "calls-only", "intent"]);
30117
+ const conformanceEnum = import_zod11.z.enum(["declared", "anchored", "off"]);
30118
+ const implMethodShape = {
30119
+ name: import_zod11.z.string(),
30120
+ sourcePath: import_zod11.z.string().optional().describe("Optional: the source file realizing THIS method when it is not the implementation's sourcePath (e.g. a command whose body lives in its own file); the implementation's sourcePath is the default. Relative to project root \u2014 for a chained subproject's implementation (qualified id), relative to that subproject's root; a path given relative to this root that lands inside the subproject is re-expressed for you"),
30121
+ detail: detailEnum.optional().describe("Detail level for this method (overrides the spec default)"),
30122
+ intent: import_zod11.z.string().optional().describe("detail: intent \u2014 behavioral prose (what it does and how it fails); substitute for a narrative"),
30123
+ conformance: conformanceEnum.optional().describe("Conformance tier for this method (overrides the spec default)"),
30124
+ symbol: import_zod11.z.string().optional().describe("Code-level name realizing this contract method in the sourcePath file, when it legitimately differs from the intent-language contract name (e.g. put realized by saveSnapshot)"),
30125
+ exportedVia: import_zod11.z.string().optional().describe("The exported binding a consumer imports to REACH this method, when the realization is published as a value that composes it rather than exported in its own right (a rule module publishes callConformanceRule, and the method is the check inside it). Not symbol, which names the function itself: this names the route to it, so the surface check reads that export as promised. A handle the method's own source file does not actually export is reported (UNREALIZED_EXPORT_HANDLE) and allows nothing"),
30126
+ ext: import_zod11.z.record(import_zod11.z.unknown()).optional().describe("Opaque pack/tool extension data for this method (namespaced keys) \u2014 preserved verbatim"),
30127
+ narrative: import_zod11.z.array(narrativeStepInput).optional(),
30128
+ calls: import_zod11.z.array(import_zod11.z.string().min(1)).optional().describe('The calls this method makes, when its narrative does not show them (detail: intent, or calls-only with no steps authored) \u2014 each as "<component>.<method>", the spelling the debt register and lint allows name a unit with. The narrative walk takes exactly these edges and no others, so an undeclared collaborator is NOT reached: this is what keeps unused-detection honest at a lower detail dial. Each entry is validated like a call step (the component resolves, this component declares it, the method is on its contract). Refused beside a non-empty narrative \u2014 add the call step there instead')
30129
+ };
30130
+ const implMethodInputFields = Object.keys(implMethodShape);
30131
+ const implInput = {
30132
+ id: import_zod11.z.string().describe('Lowercase identifier, e.g. "vfs_storage"'),
30133
+ name: import_zod11.z.string().describe("Human-readable implementation name"),
30134
+ description: import_zod11.z.string().describe("Implementation details"),
30135
+ contract: import_zod11.z.string().describe("The L3 Interface contract ID this implements"),
30136
+ sourcePath: import_zod11.z.string().optional().describe("Optional: target source code file path relative to project root \u2014 for a chained subproject's implementation (qualified id), relative to that subproject's root; a path given relative to this root that lands inside the subproject is re-expressed for you"),
30137
+ simPath: import_zod11.z.string().optional().describe("Optional: the committed integration-sim harness file (project-relative; N:1 sharing allowed). The validator proves it exists and its import graph wires the REAL modules (this component + each direct dependency; technology adapters may stay faked) \u2014 running it is CI's job. Declaring the first simPath in a subsystem activates MISSING_INTEGRATION_SIM for its other complete non-leaf implementations"),
30138
+ technologies: import_zod11.z.array(import_zod11.z.string()).optional().describe(`External technologies this implementation binds to (e.g. ["mysql"]) \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.`),
30139
+ injectedParams: import_zod11.z.array(import_zod11.z.string()).optional().describe("The parameter names this realization takes BEFORE the ones its contract declares \u2014 a config object, a data root, the transport handles a portal is handed. Supplied by whatever wires the component up, never by the caller the contract describes, which is why they belong here and not to the contract: another realization may hold them as fields instead. Declared rather than guessed, because a leading parameter the contract does not name cannot be told from one it named under a different name (`seed(config)` realized as `bootstrapInstance(cfg)`); a leading parameter this list does not name is reported (UNDECLARED_PARAM). Only a LEADING run is dropped \u2014 one of these names appearing after the contract's own parameters is an argument in the middle of the caller's list, not wiring"),
30140
+ detail: detailEnum.optional().describe("Spec-level narrative detail default for all methods"),
30141
+ conformance: conformanceEnum.optional().describe("Spec-level conformance tier default: declared | anchored | off (omitted = stereotype default: Portal \u2192 anchored, else declared)"),
30142
+ methods: import_zod11.z.array(import_zod11.z.object(implMethodShape).strict()).optional().describe("Method implementations containing L5 narratives"),
30143
+ status: statusInput
30144
+ };
30145
+ const implInputFields = Object.keys(implInput);
30146
+ reg(
30147
+ server,
30148
+ "sdd_write_narrative",
30149
+ {
30150
+ description: "Write L4 Concrete Implementation spec containing L5 method narratives. Narratives are a FLAT ordered step list; flow steps (branch/switch/loop/try/parallel/jump/return/throw) jump by step number \u2014 blocks are just skipped regions. Steps may declare a `label` anchor, and every jump field has a *Label twin (toLabel, onTrueLabel, endLabel, \u2026) resolved to step numbers at write time \u2014 prefer labels over hand-counted numbers; an unresolvable label rejects the write. Detail dial per method: full (narrative required) | calls-only (call choreography suffices) | intent (prose instead of steps); omitted = stereotype default (Portal/Observer/Adapter: calls-only, Store/Index/Registry: intent, else full). A method whose narrative shows no steps declares the calls it makes in `calls` (one \"<component>.<method>\" each): the reachability walk takes exactly those edges and no others, so an intent-level method that reaches a collaborator must name it or that collaborator is reported unused. Conformance dial per method or spec: declared | anchored | off \u2014 how strictly structural conformance requires contract methods to be realized in their source file (omitted = Portal: anchored, else declared). A method whose body lives in its own file names it in the method's sourcePath; the implementation's sourcePath is the default for every method that names none. Parameters the realization takes BEFORE its contract's own \u2014 a config object, a data root, a portal's transport handles \u2014 are wiring, and are declared once for the spec in injectedParams; a leading parameter it does not name is reported against the contract (UNDECLARED_PARAM). Re-authoring an existing id REPLACES the method list: a method left out of the input is REMOVED together with its narrative (and reported); spec-level lint/ext are carried forward, and the stored status is kept unless this input states a higher one. The answer carries a write receipt as structured content beside the sentence \u2014 the status written, whether a spec already held the id, and the notices a restatement raised, each as its own entry.",
30151
+ inputSchema: implInput,
30152
+ outputSchema: specWriteReceiptOutput
30153
+ },
30154
+ ({ id, name, description, contract, sourcePath, simPath, technologies, injectedParams, detail, conformance, methods, status }) => {
30155
+ try {
30156
+ const restatement = {
30157
+ kind: "implementation",
30158
+ spec: {
30159
+ id,
30160
+ name,
30161
+ description,
30162
+ contract,
30163
+ sourcePath,
30164
+ simPath,
30165
+ technologies,
30166
+ injectedParams,
30167
+ detail,
30168
+ conformance,
30169
+ methods: (methods ?? []).map((m) => ({
30170
+ ...m,
30171
+ narrative: (m.narrative ?? []).map((s, i) => ({ ...s, stepNumber: s.stepNumber ?? i + 1 }))
30172
+ }))
30173
+ },
30174
+ fields: implInputFields,
30175
+ memberFields: implMethodInputFields,
30176
+ status
30177
+ };
30178
+ const receipt = writeSpec(restatement);
30179
+ return writeReceipt(
30180
+ `Successfully saved L4 Implementation Spec "${name}" (${id}) with method narratives.`,
30181
+ receipt
30182
+ );
30183
+ } catch (e) {
30184
+ return errMessage(e);
30185
+ }
30186
+ }
30187
+ );
30188
+ const typeInput = {
30189
+ kind: import_zod11.z.enum(["entity", "value-object"]).describe("entity (owned by a subsystem) or value-object (often system-level shared)"),
30190
+ id: import_zod11.z.string().describe("Lowercase identifier"),
30191
+ name: import_zod11.z.string().describe("Human-readable name"),
30192
+ description: import_zod11.z.string().optional(),
30193
+ subsystem: import_zod11.z.string().optional().describe("Owning subsystem id; omit for a system-level shared value object. A subsystem the tree does not have is refused and nothing is written"),
30194
+ group: import_zod11.z.string().optional().describe("Optional logical group ID to organize this type in subfolders"),
30195
+ fields: import_zod11.z.array(import_zod11.z.object({
30196
+ name: import_zod11.z.string(),
30197
+ type: import_zod11.z.string().describe(TYPE_REF_GRAMMAR("The field's type")),
30198
+ description: import_zod11.z.string().optional(),
30199
+ optional: import_zod11.z.boolean().optional(),
30200
+ key: import_zod11.z.enum(["primary", "unique", "foreign"]).optional().describe("Identity marker (PK/unique/FK) for ERD and database schema derivation"),
30201
+ references: import_zod11.z.string().optional().describe('For foreign keys, the referenced type/table id and optional field, e.g. "invoice.id"')
30202
+ }).strict()).optional().describe('Data fields (type is a primitive or a qualified type id, e.g. "billing.Invoice")'),
30203
+ methods: import_zod11.z.array(import_zod11.z.object({
30204
+ name: import_zod11.z.string(),
30205
+ signature: import_zod11.z.string(),
30206
+ returns: import_zod11.z.string().describe(TYPE_REF_GRAMMAR("The type the method answers with")),
30207
+ description: import_zod11.z.string().optional(),
30208
+ sourcePath: import_zod11.z.string().optional().describe("Source file realizing this method when the type's own sourcePath does not hold it \u2014 a pure type method often lives apart from the declaration"),
30209
+ symbol: import_zod11.z.string().optional().describe("Code-level name realizing this method when it differs from the method name, e.g. narrative_step.foreignFields realized by narrativeStepForeignFields")
30210
+ }).strict()).optional().describe("Pure intrinsic methods only"),
30211
+ componentClass: import_zod11.z.string().optional().describe("Optional component id that implements or owns this logical entity"),
30212
+ invariants: import_zod11.z.array(import_zod11.z.object({
30213
+ id: import_zod11.z.string().describe("Stable invariant id, unique within the entity"),
30214
+ description: import_zod11.z.string().describe("The property that must hold, stated precisely")
30215
+ }).strict()).optional().describe("Declared domain invariants (entities): every write-effect method of the componentClass must carry a narrative step asserting each (assertsInvariants) \u2014 declarations checked, enforcement never proven"),
30216
+ database: import_zod11.z.string().optional().describe("Optional database id for table-schema types"),
30217
+ table: import_zod11.z.string().optional().describe("Optional database table name for table-schema types"),
30218
+ linkedEntity: import_zod11.z.string().optional().describe("Optional logical entity id represented by this table-schema type"),
30219
+ sourcePath: import_zod11.z.string().optional().describe("Source file holding the declaration of this type (project-relative). Naming one turns the type into a claim on code: the file must resolve and the declaration must be anchored in it (UNREALIZED_TYPE)"),
30220
+ symbol: import_zod11.z.string().optional().describe('Code-level name realizing the declaration when it differs from name, e.g. a type named "Invoice Line" declared as InvoiceLine')
30221
+ };
30222
+ const typeInputFields = Object.keys(typeInput);
30223
+ reg(
30224
+ server,
30225
+ "sdd_add_type",
30226
+ {
30227
+ description: "Define an entity or value-object type (the data components operate on). Entities are owned by a subsystem; shared value objects omit subsystem (system-level). Fields are data; methods are PURE intrinsic behaviour only \u2014 anything needing a collaborator belongs on a component, taking the entity as an argument. A type may also CLAIM code: sourcePath names the file holding its declaration (and each method may name its own), symbol binds the code-level name when it differs \u2014 the file must then resolve and the declaration must be anchored in it. An owning subsystem the tree does not have is refused, writing nothing, exactly as a component under an unknown subsystem is. Re-defining an existing id REPLACES fields/methods/invariants and restates sourcePath/symbol (an omitted list or path is CLEARED, and a dropped member is reported); lint/ext are carried forward. The answer carries a write receipt as structured content beside the sentence \u2014 whether a spec already held the id, and the notices a restatement raised, each as its own entry. A type carries no lifecycle status, so the receipt states none.",
30228
+ inputSchema: typeInput,
30229
+ outputSchema: specWriteReceiptOutput
30230
+ },
30231
+ ({ kind, id, name, description, subsystem, group, fields, methods, componentClass, invariants, database, table, linkedEntity, sourcePath, symbol }) => {
30232
+ try {
30233
+ const spec = {
30234
+ kind,
30235
+ id,
30236
+ name,
30237
+ ...description ? { description } : {},
30238
+ ...subsystem ? { subsystem } : {},
30239
+ ...group ? { group } : {},
30240
+ fields: (fields ?? []).map((f) => ({
30241
+ name: f.name,
30242
+ type: f.type,
30243
+ description: f.description,
30244
+ optional: f.optional ?? false,
30245
+ ...f.key ? { key: f.key } : {},
30246
+ ...f.references ? { references: f.references } : {}
30247
+ })),
30248
+ methods: methods ?? [],
30249
+ ...componentClass ? { componentClass } : {},
30250
+ ...invariants?.length ? { invariants } : {},
30251
+ ...database ? { database } : {},
30252
+ ...table ? { table } : {},
30253
+ ...linkedEntity ? { linkedEntity } : {},
30254
+ ...sourcePath ? { sourcePath } : {},
30255
+ ...symbol ? { symbol } : {}
30256
+ };
30257
+ const receipt = writeSpec({ kind: "type", spec, fields: typeInputFields });
30258
+ return writeReceipt(
30259
+ `Successfully ${receipt.replacedExisting ? "re-authored" : "defined"} ${kind} type "${name}" (${id}).`,
30260
+ receipt
30261
+ );
30262
+ } catch (e) {
30263
+ return errMessage(e);
30264
+ }
30265
+ }
30266
+ );
30267
+ reg(
30268
+ server,
30269
+ "sdd_validate_tree",
30270
+ {
30271
+ description: "Validate the SDD spec tree, checking parent references, contract compatibility, narratives, and component type boundaries. Supports scoping and recursion controls. Findings come back as structured content too, under the schema this tool declares \u2014 errors and warnings already split, each with its code, severity, message and the spec it concerns \u2014 so a caller filters them as objects instead of parsing the JSON text block and hoping its shape holds.",
30272
+ inputSchema: {
30273
+ subsystem: import_zod11.z.string().optional().describe("Only validate the specified subsystem (granular)"),
30274
+ recursive: import_zod11.z.boolean().optional().describe("Whether to recursively validate subprojects (default: true)")
30275
+ },
30276
+ outputSchema: validateTreeOutput
30277
+ },
30278
+ ({ subsystem, recursive }) => {
30279
+ try {
30280
+ const config = loadProjectConfig();
30281
+ if (!config) throw new ProjectNotInitializedError();
30282
+ const result = validateSddTree({
30283
+ rules: config.rules,
30284
+ projectType: config.projectType,
30285
+ scopeSubsystem: subsystem,
30286
+ recursive: recursive ?? true
30287
+ });
30288
+ return jsonStructured({
30289
+ valid: result.valid,
30290
+ errors: result.issues.filter((i) => i.severity === "error"),
30291
+ warnings: result.issues.filter((i) => i.severity === "warning"),
30292
+ ...result.resolvedThrough ? { resolvedThrough: result.resolvedThrough } : {}
30293
+ });
30294
+ } catch (e) {
30295
+ return errText(String(e));
30296
+ }
30297
+ }
30298
+ );
30299
+ reg(
30300
+ server,
30301
+ "sdd_get_spec",
30302
+ {
30303
+ 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. The structured content carries the same answer with the two derived markers KEPT SEPARATE from the stored spec ({kind, id, spec, partialResult?, variantGuidance?}), so nothing derived can be mistaken for something stored; the text block folds them in as it always has.`,
30304
+ inputSchema: {
30305
+ kind: import_zod11.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).describe("The kind of specification"),
30306
+ id: import_zod11.z.string().describe('The identifier of the spec to fetch (the L0 system spec is a singleton \u2014 pass the system name or "system")'),
30307
+ methods: import_zod11.z.array(import_zod11.z.string().min(1)).optional().describe("Return only these methods by name; every other field of the spec comes back unchanged. Only an interface, an implementation or a type declares methods \u2014 asking for one elsewhere is refused, as is a name the spec does not declare (the answer names the ones it does). Omit it for the whole spec.")
30308
+ },
30309
+ outputSchema: getSpecOutput
30310
+ },
30311
+ ({ kind, id, methods }) => {
30312
+ try {
30313
+ let result = null;
30314
+ let partial = null;
30315
+ switch (kind) {
30316
+ case "system":
30317
+ result = loadSystemSpec();
30318
+ break;
30319
+ case "subsystem":
30320
+ result = loadSubsystemSpec(id);
30321
+ break;
30322
+ case "component":
30323
+ result = loadComponentSpec(id);
30324
+ break;
30325
+ case "interface":
30326
+ result = loadInterfaceSpec(id);
30327
+ break;
30328
+ case "implementation":
30329
+ result = loadImplementationSpec(id);
30330
+ break;
30331
+ case "type":
30332
+ result = loadTypeSpec(id);
30333
+ break;
30334
+ }
30335
+ if (!result) return errText(`Spec of kind "${kind}" with ID "${id}" does not exist.`);
30336
+ if (methods !== void 0) {
30337
+ const declared = result.methods;
30338
+ if (!Array.isArray(declared)) {
30339
+ return errText(
30340
+ `A ${kind} spec declares no methods, so "methods" cannot filter it. Drop the argument to read "${id}" whole; methods live on an interface, an implementation and a type.`
30341
+ );
30342
+ }
30343
+ const names = declared.map((m) => String(m?.name));
30344
+ const unknown = methods.filter((n) => !names.includes(n));
30345
+ if (unknown.length > 0) {
30346
+ return errText(
30347
+ `${kind} "${id}" does not declare ${unknown.map((n) => `"${n}"`).join(", ")}. It declares: ${names.join(", ")}. Nothing was returned, so a misspelled name never reads as a method with no content.`
30348
+ );
30349
+ }
30350
+ const kept = declared.filter((m) => methods.includes(String(m?.name)));
30351
+ partial = {
30352
+ shown: kept.map((m) => String(m?.name)),
30353
+ omitted: names.length - kept.length,
30354
+ warning: `PARTIAL: ${names.length - kept.length} of this spec's ${names.length} methods are not in this answer. Never re-author from it \u2014 sdd_define_interface and sdd_write_narrative REPLACE the method list, so every method missing here would be removed from the spec.`
30355
+ };
30356
+ result = { ...result, methods: kept };
30357
+ }
30358
+ const guidance = kind === "component" ? resolveComponentVariantGuidance(result) : null;
30359
+ const folded = {
30360
+ ...result,
30361
+ ...partial ? { partialResult: partial } : {},
30362
+ ...guidance ? { variantGuidance: guidance } : {}
30363
+ };
30364
+ return structured(JSON.stringify(folded, null, 2), {
30365
+ kind,
30366
+ id,
30367
+ spec: result,
30368
+ ...partial ? { partialResult: partial } : {},
30369
+ ...guidance ? { variantGuidance: guidance } : {}
30370
+ });
30371
+ } catch (e) {
30372
+ return errText(String(e));
30373
+ }
30374
+ }
30375
+ );
30376
+ reg(
30377
+ server,
30378
+ "sdd_delete_spec",
30379
+ {
30380
+ description: "Delete a specification file from the spec tree and clean up any empty parent directories. Answers with a structured deletion beside the sentence: whether a spec was removed, and the tests that encode a method the deletion took away (when the project declares test roots).",
30381
+ inputSchema: {
30382
+ kind: import_zod11.z.enum(["subsystem", "component", "interface", "implementation", "type"]).describe("The kind of spec to delete"),
30383
+ id: import_zod11.z.string().describe("The ID of the spec to delete")
30384
+ },
30385
+ outputSchema: specDeletionOutput
30386
+ },
30387
+ ({ kind, id }) => {
30388
+ try {
30389
+ const deletion = deleteSpec2(kind, id);
30390
+ if (!deletion.deleted) return errText(`Spec of kind "${kind}" with ID "${id}" could not be deleted (file may not exist).`);
30391
+ const testsBlock = renderTestsToRevisit(deletion.testsToRevisit).join("\n");
30392
+ return structured(`Successfully deleted ${kind} spec "${id}".${testsBlock ? `
30393
+ ${testsBlock}` : ""}`, deletion);
30394
+ } catch (e) {
30395
+ return errText(String(e));
30396
+ }
30397
+ }
30398
+ );
30399
+ reg(
30400
+ server,
30401
+ "sdd_update_spec",
30402
+ {
30403
+ description: "Update/patch an existing SDD specification (subsystem, component, interface, implementation, or type) using a granular delta. Updates fields, appends/merges array elements, or inserts/deletes narrative steps. Answers with exactly what changed, and with every path the delta named that the write did not act on. Pass dryRun to be told what it would do without writing it.",
30404
+ inputSchema: {
30405
+ kind: import_zod11.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).describe("The spec kind to update (system = the singleton L0 \u2014 vision, boundaries, globalRequirements, databases, and publicInterfaces: the project gateway surface, each entry {id, name, subsystem, component, type, details, audience: project|department|instance|partner|external}; id is informational)"),
30406
+ id: import_zod11.z.string().describe("The ID of the spec to update (namespaced if needed)"),
30407
+ delta: import_zod11.z.record(import_zod11.z.any()).describe(`The partial fields to merge into the spec. ARRAYS UPSERT, they do not replace: an array whose elements carry an identity is merged element-by-element, so a delta naming ONE element leaves the others intact. Identity is "name" or "id" by default, and per field: dispatch by "capability", a listener's mounts by "portal", lifecycle by phase+component+method, emits/subscribesTo by topic+event, trustedLinks by "subsystem", a subsystem's publicInterfaces by component+interface (or type+details for an entry not yet bound to a component), invariants and patterns by "id", lint.allow by "code" AND "at" (an allow covers one occurrence, so several may share a code on one spec), an interface method's findings by "code", boundaries by "name", globalRequirements by "description", switch cases by "value", try catches by "error". Identity merging applies at EVERY depth, including an array INSIDE an element (a method's params, a step's catches). Add "action: 'delete'" (or "remove: true") alongside that identity to REMOVE an element \u2014 including a stale lint allow. Arrays of plain STRINGS (owns, dependsOn, guarantees) carry no per-element identity and are replaced wholesale; pass [] to clear any array outright. To REMOVE an optional field entirely, list it in "unset": e.g. {"unset": ["basePath", "variant"]} \u2014 passing null/undefined means "no change" (they are skipped), and writing "" would leave the field present but empty, which is a different and usually wrong spec. Unsetting a required field is refused by schema validation, which names it. For narrative steps, match by "stepNumber" and use "action: 'insert'" (shifts subsequent steps up) or "action: 'delete'" (shifts subsequent steps down and removes it). Step entries apply in ASCENDING stepNumber order, each against the numbering the earlier entries of the SAME delta left behind \u2014 delete step 3 and step 7 becomes step 6 \u2014 so prefer labels, and restate the step's "label" or "description" on a delete to have it checked against the step actually addressed. Renumbering RELOCATES every flow jump field (onTrueStep/onFalseStep/cases.step/defaultStep/endStep/catches.step/finallyStep/toStep) in the same narrative. A delete is REJECTED when the narrative has no such step, when a jump still targets it (retarget the referrers first), when a restated label/description does not match, or when it is a loop/try/parallel header whose body would be left standing (retype the header first to dissolve the region, then delete it). Changing a step's "type" REBUILDS it for the new type: its description and label are kept and every field the new type cannot carry is dropped (returned as a NOTICE); a delta that retypes AND sets such a field is refused. A step delta is also refused when it carries a marker the merge does not recognise: a non-boolean "remove", an "action" that is neither "insert" nor "delete", a "captureJumps" outside an insert, or no "stepNumber" to address. Inserting AT a jump target relocates those jumps past the inserted step by default (a NOTICE is returned) \u2014 add "captureJumps": true on the inserted step to retarget entry jumps onto it (loop/try endStep region tails always relocate with the body and are never captured). Every jump field has a "*Label" twin (toLabel, onFalseLabel, endLabel, \u2026, and "label" on a cases/catches entry) resolved against step labels AFTER the merge, so a delta may anchor on a label only pre-existing steps carry; a label the delta supplies REPLACES the stored number it twins, while setting the number and its label together in one delta is refused as a contradiction. Reference ids in deltas may use LOCAL names \u2014 they are qualified against the spec's namespace exactly as the loader would. Per-spec lint suppression: set "lint: { allow: [{ code, at, covers, reason }] }" to silence a WARNING code on this spec only (errors always surface; stale allows are flagged). An allow covers EXACTLY the occurrence it names: "at" is the site the finding names (a contract method, an import edge "from -> to", a declared edge "component -> target") and is REQUIRED for a code whose findings report one, while a finding that reports no site is covered only by an allow that names none; "covers" lists the units an aggregating finding reports, and the allow silences it only when every one is listed. This delta is deliberately OPEN below its top level \u2014 the shapes nest further than a schema here should restate \u2014 so a key that is not a field at its depth is not refused, it is NAMED BACK under NO EFFECT in the answer, together with any value the spec already held and any "unset" that removed nothing. Read that list: it is where a nested typo shows up.`),
30408
+ dryRun: import_zod11.z.boolean().optional().describe("Ask what this delta WOULD do instead of doing it. The whole write runs, the candidate gate included, and the answer is the change report it would have produced \u2014 marked DRY RUN, with nothing stamped and not one byte of the stored file moved. Use it before a delta that renumbers a long narrative.")
30409
+ },
30410
+ outputSchema: specChangeReportOutput
30411
+ },
30412
+ ({ kind, id, delta, dryRun }) => {
30413
+ try {
30414
+ const report = updateSpecGated(kind, id, delta, dryRun);
30415
+ return structured(renderChangeReport(report), report);
30416
+ } catch (e) {
30417
+ return errText(String(e));
30418
+ }
30419
+ }
30420
+ );
30421
+ reg(
30422
+ server,
30423
+ "sdd_get_status",
30424
+ {
30425
+ description: "Get the completeness status dashboard of the SDD spec tree. Supports scoping and recursion controls.",
30426
+ inputSchema: {
30427
+ subsystem: import_zod11.z.string().optional().describe("Only show status for the specified subsystem"),
30428
+ recursive: import_zod11.z.boolean().optional().describe("Whether to recursively load subprojects (default: true)")
30429
+ }
30430
+ },
30431
+ ({ subsystem, recursive }) => {
30432
+ try {
30433
+ const report = getStatusReport({
30434
+ subsystem,
30435
+ recursive: recursive ?? true
30436
+ });
30437
+ const family = statusFamilyContext();
30438
+ const verdict = approvalVerdict();
30439
+ return text(`${family}${report.text}${verdict.text}`);
30440
+ } catch (e) {
30441
+ return errText(String(e));
30442
+ }
30443
+ }
30444
+ );
30445
+ reg(
30446
+ server,
30447
+ "sdd_list_external_interfaces",
30448
+ {
30449
+ description: "List the bound project's consumable external surfaces (parent family, siblings, foreign imports) as discovery entries with origin, provenance, and freshness \u2014 the tool an agent inside a subproject uses to SEE its outward world instead of discovering it by failed reference resolution. Full contracts stay in the vendored snapshots (.wai/surfaces/); each entry summarizes the interface ids it exposes."
30450
+ },
30451
+ () => {
30452
+ try {
30453
+ return json(listExternalInterfaces());
30454
+ } catch (e) {
30455
+ return errText(String(e));
30456
+ }
30457
+ }
30458
+ );
30459
+ reg(
30460
+ server,
30461
+ "sdd_get_agent_brief",
30462
+ {
30463
+ description: "Compose the LIVE delegation brief for one resolved agent: the fully rendered instruction body plus the ownedPaths/readPaths scope fence \u2014 the dynamic replacement for generated per-component agent files. Fetch a brief and spawn a generic subagent with it: always fresh after a re-lock, no session restart needed. List agent ids via listAgents or resources/list (the wairon-agent:// entries).",
30464
+ inputSchema: {
30465
+ agentId: import_zod11.z.string().describe("The resolved agent id (list via listAgents or resources/list)")
30466
+ }
30467
+ },
30468
+ ({ agentId }) => {
30469
+ try {
30470
+ return json(composeAgentBrief(agentId));
30471
+ } catch (e) {
30472
+ return errText(String(e));
30473
+ }
30474
+ }
30475
+ );
30476
+ registerSkillResources(server, !options.hostedTools);
30477
+ registerSkillPrompts(server, !options.hostedTools);
30478
+ try {
30479
+ const chainingParent = resolveChainingParent();
30480
+ if (chainingParent) {
30481
+ let externalSurfaceCount = 0;
30482
+ try {
30483
+ externalSurfaceCount = listExternalInterfaces().length;
30484
+ } catch {
30485
+ }
30486
+ process.stderr.write(
30487
+ `[wairon mcp] chained subproject: this root is mounted as subsystem "${chainingParent.subsystemId}" of the parent project at ${chainingParent.parentRoot} \u2014 ${externalSurfaceCount} vendored external surface(s) discoverable via sdd_list_external_interfaces
30488
+ `
30489
+ );
30490
+ }
30491
+ } catch {
30492
+ }
30493
+ advertiseHostedTools(server, options);
30494
+ return server;
30495
+ }
30496
+ function advertiseHostedTools(server, options) {
30497
+ if (options.hostedTools) {
30498
+ const hostedStub = () => errText("This hosted tool is dispatched by the hosting data plane before reaching the MCP server; it is unavailable outside a hosted request.");
30499
+ reg(server, "sdd_host_lock_project", {
30500
+ description: "Hosted project lifecycle (execute-primary): LOCK the bound project (validate-as-complete gate + commit-scoped lock record). Executes directly when your resolved permission is yes and returns the completed outcome; when it is approval, a pending approval request is created instead (await it with sdd_host_await_approval).",
30501
+ inputSchema: {}
30502
+ }, hostedStub);
30503
+ reg(server, "sdd_host_initialize_project", {
30504
+ description: "Hosted project lifecycle (execute-primary): initialize a new hosted project into its REQUIRED owner organization unit (with an optional profile selection). Executes directly when your resolved permission is yes; when it is approval, a pending approval request is created instead.",
30505
+ inputSchema: {
30506
+ id: import_zod11.z.string().describe("Requested project id"),
30507
+ displayName: import_zod11.z.string().optional(),
30508
+ description: import_zod11.z.string().optional(),
30509
+ ownerUnitId: import_zod11.z.string().describe("REQUIRED organization unit that owns the new project (every project is placed at creation)"),
30510
+ environment: import_zod11.z.string().optional()
30511
+ }
30512
+ }, hostedStub);
30513
+ reg(server, "sdd_host_get_approval_status", {
30514
+ description: "Hosted project lifecycle: read the status of one of your approval requests.",
30515
+ inputSchema: { requestId: import_zod11.z.string() }
30516
+ }, hostedStub);
30517
+ reg(server, "sdd_host_await_approval", {
30518
+ description: "Hosted project lifecycle: long-poll one of YOUR approval requests until it is decided (approved requests auto-execute, so an approval resolves as completed) or the timeout elapses. Returns the request in its current state.",
30519
+ inputSchema: {
30520
+ requestId: import_zod11.z.string(),
30521
+ timeoutSeconds: import_zod11.z.number().optional().describe("How long to wait server-side (clamped; 0 returns the current state immediately)")
30522
+ }
30523
+ }, hostedStub);
30524
+ reg(server, "sdd_landscape_list_reachable_projects", {
30525
+ description: "Hosted landscape: the projects reachable from the BOUND project through ACTIVE cross-project relations (directional, relations-only), each with the relation ids/kinds and target public interface ids.",
30526
+ inputSchema: {}
30527
+ }, hostedStub);
30528
+ reg(server, "sdd_landscape_list_reachable_project_interfaces", {
30529
+ description: "Hosted landscape: redacted, audience-filtered public interface summaries of one reachable target project (Forbidden outside the reachable set; private by default).",
30530
+ inputSchema: { projectId: import_zod11.z.string().describe("The reachable target project id") }
30531
+ }, hostedStub);
30532
+ reg(server, "sdd_landscape_list_visible_surfaces", {
30533
+ description: "Hosted landscape: the visibility-resolved discovery catalog for the BOUND project \u2014 every target the organization unit graph exposes to it (open-within-tenant, closed groups hidden, exposeTo grants honored), with audience distance and audience-filtered summaries. No relation required.",
30534
+ inputSchema: {}
30535
+ }, hostedStub);
30536
+ reg(server, "sdd_landscape_get_project_surface", {
30537
+ description: `Hosted landscape: fetch a visible target project's CONTRACT-GRADE surface snapshot (full method contracts, dispatch tables, type closure) at your audience-distance ceiling, origin "exchanged" \u2014 save it under .wai/surfaces/ (wairon surface import) so your adapters validate against the declared contract.`,
30538
+ inputSchema: { projectId: import_zod11.z.string().describe("The visible target project id") }
30539
+ }, hostedStub);
30540
+ reg(server, "sdd_host_pack_list", {
30541
+ description: "Hosted project ops: list the BOUND project's registered extension packs (requires project:read over it).",
30542
+ inputSchema: {}
30543
+ }, hostedStub);
30544
+ reg(server, "sdd_host_pack_install", {
30545
+ description: "Hosted project ops: install a DECLARATIVE extension pack (profiles + language/platform tables \u2014 never rule/code packs) into the BOUND project (requires project:admin over it).",
30546
+ inputSchema: {
30547
+ name: import_zod11.z.string().describe("Pack name (letters, digits, dot, underscore, hyphen)"),
30548
+ content: import_zod11.z.string().describe("The declarative pack YAML content")
30549
+ }
30550
+ }, hostedStub);
30551
+ reg(server, "sdd_host_policy_evaluate", {
30552
+ description: "Hosted project ops: evaluate the BOUND project's packs and profile selection against the instance pack policy \u2014 compliance without side effects (requires project:write over it).",
30553
+ inputSchema: {}
30554
+ }, hostedStub);
30555
+ reg(server, "sdd_host_policy_reconcile", {
30556
+ description: "Hosted project ops: reconcile the BOUND project against the instance pack policy \u2014 under auto_reconcile enforcement the missing required/default declarative packs are applied (requires project:write over it).",
30557
+ inputSchema: {}
30558
+ }, hostedStub);
30559
+ reg(server, "sdd_host_produce", {
30560
+ description: "Hosted project ops: run a configured producer projection (Notion/Miro) of the BOUND project to the named target (requires project:admin over it).",
30561
+ inputSchema: { target: import_zod11.z.string().describe("The configured producer target (e.g. notion, miro)") }
30562
+ }, hostedStub);
30563
+ reg(server, "sdd_host_commit_project", {
30564
+ description: "Hosted project ops: publish a DELIBERATE, .wai/-scoped commit+push of the BOUND project's specs to its bound repository (commit = local save, push = the actual backup; a clean scope publishes nothing). The data plane binds ONE project, so there is no project argument. Requires project:write over it.",
30565
+ inputSchema: {
30566
+ subsystem: import_zod11.z.string().optional().describe("Narrow staging to .wai/specs/<subsystem>/ (a convenience \u2014 git history stays per-repo)"),
30567
+ message: import_zod11.z.string().optional().describe("Commit message; defaults to a timestamped wairon message")
30568
+ }
30569
+ }, hostedStub);
30570
+ reg(server, "sdd_host_export_tree", {
30571
+ description: "Hosted spec-tree transfer: pack the BOUND project's WHOLE spec tree \u2014 its own .wai plus every chained subproject \u2014 into a .waitree archive, returned as base64 with its roots, file count and state id. The migration counterpart of an import: use it to take a hosted project local, or to move it to another instance. Requires project:read over the project. Bounded by the data-plane body cap; a very large tree exports through the web download route instead. Refuses when a chained mount cannot be packed (escaping, missing, cyclic, too deep, or holding no .wai), naming every one and why, unless allowPartial is set \u2014 then the archive is built anyway and the result lists what was skipped.",
30572
+ inputSchema: {
30573
+ allowPartial: import_zod11.z.boolean().optional().describe("Build the archive even when some chained mounts cannot be packed, listing them as skipped; default false (refuse and name them)")
30574
+ }
30575
+ }, hostedStub);
30576
+ reg(server, "sdd_host_import_tree", {
30577
+ description: "Hosted spec-tree transfer: REPLACE the BOUND project's spec tree from a base64 .waitree archive. Requires project:admin over the project (strictly above project:write \u2014 this replaces the whole design, not one spec). Refuses an occupied destination unless replaceExisting is set, always refuses executable entries (rule/code packs install only through the trusted filesystem), and moves the previous tree aside to a backup whose path is returned.",
30578
+ inputSchema: {
30579
+ archiveBase64: import_zod11.z.string().describe("The .waitree archive bytes, base64-encoded"),
30580
+ replaceExisting: import_zod11.z.boolean().optional().describe("Replace a tree already present (backed up first); without it an occupied destination is refused")
30581
+ }
30582
+ }, hostedStub);
30583
+ }
30584
+ }
30585
+
30586
+ // src/index.ts
30587
+ function loadProjectConfig3() {
27192
30588
  const config = loadProjectConfig();
27193
30589
  if (!config) throw new ProjectNotInitializedError();
27194
30590
  return config;
@@ -27497,6 +30893,7 @@ function loadProjectConfig2() {
27497
30893
  saveSpec,
27498
30894
  saveTopologyConfig,
27499
30895
  scanAllSpecs,
30896
+ schemaFingerprint,
27500
30897
  serializeYaml,
27501
30898
  setExecutionTier,
27502
30899
  setLogLevel,