@wairon/cli 5.1.1-dev.101 → 5.1.1-dev.103

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
@@ -3316,8 +3316,8 @@ var require_dist = __commonJS({
3316
3316
  return normalized === "." ? ".wai/" : `${normalized}/.wai/`;
3317
3317
  }
3318
3318
  function normalizeRootPath(relativePath) {
3319
- const posix5 = relativePath.split(path210.sep).join("/").replace(/^\.\//, "").replace(/\/+$/, "");
3320
- return posix5 === "" || posix5 === "." ? "." : posix5;
3319
+ const posix7 = relativePath.split(path210.sep).join("/").replace(/^\.\//, "").replace(/\/+$/, "");
3320
+ return posix7 === "" || posix7 === "." ? "." : posix7;
3321
3321
  }
3322
3322
  function slugify(projectName) {
3323
3323
  const slug2 = projectName.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -3566,6 +3566,7 @@ __export(src_exports, {
3566
3566
  aiDir: () => aiDir,
3567
3567
  aiDirAt: () => aiDirAt,
3568
3568
  aiPathsAt: () => aiPathsAt,
3569
+ aliasTargetOf: () => aliasTargetOf,
3569
3570
  ambiguityMessage: () => ambiguityMessage,
3570
3571
  approvalKeyIn: () => approvalKeyIn,
3571
3572
  approvalRecord: () => approvalRecord,
@@ -3709,6 +3710,7 @@ __export(src_exports, {
3709
3710
  keyIn: () => keyIn,
3710
3711
  landReference: () => landReference,
3711
3712
  languageOfSourcePath: () => languageOfSourcePath,
3713
+ lineEndingFor: () => lineEndingFor,
3712
3714
  listDirectChainedSubprojects: () => listDirectChainedSubprojects,
3713
3715
  listExternals: () => listExternals2,
3714
3716
  listFiles: () => listFiles,
@@ -3750,6 +3752,7 @@ __export(src_exports, {
3750
3752
  memberDeclarationOf: () => memberDeclarationOf,
3751
3753
  memberDigest: () => memberDigest,
3752
3754
  memberLocationOf: () => memberLocationOf,
3755
+ memberTypesOf: () => memberTypesOf,
3753
3756
  methodGenericParameters: () => methodGenericParameters,
3754
3757
  methodSourceFile: () => methodSourceFile,
3755
3758
  methodTypeRefs: () => methodTypeRefs,
@@ -3808,7 +3811,7 @@ __export(src_exports, {
3808
3811
  renameId: () => renameProjectId,
3809
3812
  renameMethod: () => renameMethod,
3810
3813
  renameType: () => renameType,
3811
- renderDiagram: () => renderDiagram,
3814
+ renderDiagram: () => renderDiagram2,
3812
3815
  repairForeignStepFields: () => repairForeignStepFields,
3813
3816
  repairSignatures: () => repairSignatures,
3814
3817
  repairTypeSpellings: () => repairTypeSpellings,
@@ -3822,6 +3825,7 @@ __export(src_exports, {
3822
3825
  resolveExternals: () => resolveDeclared,
3823
3826
  resolveImport: () => resolveImport,
3824
3827
  resolveInstalledPack: () => resolveInstalledPack,
3828
+ resolveProjectBinding: () => resolveProjectBinding,
3825
3829
  resolveProjectExports: () => resolveProjectExports,
3826
3830
  resolveSignatures: () => resolveSignatures,
3827
3831
  resolveSubprojectForNamespace: () => resolveSubprojectForNamespace,
@@ -3867,6 +3871,7 @@ __export(src_exports, {
3867
3871
  typeIsMany: () => typeIsMany,
3868
3872
  typeMatchesRef: () => typeMatchesRef,
3869
3873
  typeNamedRefs: () => typeNamedRefs,
3874
+ typeProblemEnumProposal: () => typeProblemEnumProposal,
3870
3875
  typeProblemIntProposal: () => typeProblemIntProposal,
3871
3876
  typeSourceFiles: () => typeSourceFiles,
3872
3877
  typeSpellingFacts: () => typeSpellingFacts,
@@ -3880,11 +3885,12 @@ __export(src_exports, {
3880
3885
  validateProject: () => validateProject,
3881
3886
  validateProjectConfig: () => validateProjectConfig,
3882
3887
  validateRegistry: () => validateRegistry,
3888
+ withLineEndings: () => withLineEndings,
3883
3889
  withPack: () => withPack,
3884
3890
  writeFile: () => writeFile,
3885
3891
  writeFileIfChanged: () => writeFileIfChanged,
3886
3892
  writeJsonFile: () => writeJsonFile,
3887
- writeLockRecord: () => writeLockRecord,
3893
+ writeLockRecord: () => writeLockRecord2,
3888
3894
  writeReachDenials: () => writeReachDenials,
3889
3895
  writeReachPermits: () => writeReachPermits,
3890
3896
  writeRootGuideDelegator: () => writeRootGuideDelegator,
@@ -3894,8 +3900,8 @@ __export(src_exports, {
3894
3900
  module.exports = __toCommonJS(src_exports);
3895
3901
 
3896
3902
  // src/config/project-config.ts
3897
- var fs2 = __toESM(require("fs"));
3898
- var path3 = __toESM(require("path"));
3903
+ var fs3 = __toESM(require("fs"));
3904
+ var path5 = __toESM(require("path"));
3899
3905
  var YAML = __toESM(require("yaml"));
3900
3906
  var import_zod4 = require("zod");
3901
3907
 
@@ -3903,22 +3909,125 @@ var import_zod4 = require("zod");
3903
3909
  var fs = __toESM(require("fs"));
3904
3910
  var path = __toESM(require("path"));
3905
3911
  var import_node_async_hooks = require("async_hooks");
3912
+ var yaml = __toESM(require("js-yaml"));
3906
3913
  function ensureDir(dirPath) {
3907
3914
  fs.mkdirSync(dirPath, { recursive: true });
3908
3915
  }
3909
3916
  function writeFile(filePath, content) {
3910
3917
  ensureDir(path.dirname(filePath));
3911
- fs.writeFileSync(filePath, content, "utf-8");
3918
+ fs.writeFileSync(filePath, withLineEndings(filePath, content), "utf-8");
3912
3919
  }
3913
3920
  function writeFileIfChanged(filePath, content) {
3921
+ const text3 = withLineEndings(filePath, content);
3914
3922
  if (fs.existsSync(filePath)) {
3915
3923
  const existing = fs.readFileSync(filePath, "utf-8");
3916
- if (existing === content) return false;
3924
+ if (existing === text3) return false;
3917
3925
  }
3918
3926
  ensureDir(path.dirname(filePath));
3919
- fs.writeFileSync(filePath, content, "utf-8");
3927
+ fs.writeFileSync(filePath, text3, "utf-8");
3920
3928
  return true;
3921
3929
  }
3930
+ var EOL_SAMPLE_BYTES = 64 * 1024;
3931
+ function endingsOf(text3) {
3932
+ const crlf = text3.split("\r\n").length - 1;
3933
+ const lf = text3.split("\n").length - 1 - crlf;
3934
+ if (crlf === 0 && lf === 0) return null;
3935
+ return crlf > lf ? "\r\n" : "\n";
3936
+ }
3937
+ function sampleOf(file) {
3938
+ let fd;
3939
+ try {
3940
+ fd = fs.openSync(file, "r");
3941
+ const buffer = Buffer.alloc(EOL_SAMPLE_BYTES);
3942
+ const read2 = fs.readSync(fd, buffer, 0, EOL_SAMPLE_BYTES, 0);
3943
+ return buffer.subarray(0, read2).toString("utf-8");
3944
+ } catch {
3945
+ return null;
3946
+ } finally {
3947
+ if (fd !== void 0) fs.closeSync(fd);
3948
+ }
3949
+ }
3950
+ function attributeMatches(pattern, relative20) {
3951
+ const glob = pattern.replace(/^\//, "");
3952
+ const target = glob.includes("/") ? relative20 : path.posix.basename(relative20);
3953
+ const source = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0000/g, "(?:.*/)?");
3954
+ return new RegExp(`^${source}$`).test(target);
3955
+ }
3956
+ function attributeEol(attributesFile, file) {
3957
+ const text3 = readFileOrNull(attributesFile);
3958
+ if (text3 === null) return null;
3959
+ const relative20 = path.relative(path.dirname(attributesFile), file).split(path.sep).join("/");
3960
+ let eol = null;
3961
+ for (const raw of text3.split(/\r?\n/)) {
3962
+ const line = raw.trim();
3963
+ if (line === "" || line.startsWith("#")) continue;
3964
+ const [pattern, ...attrs] = line.split(/\s+/);
3965
+ if (!attributeMatches(pattern, relative20)) continue;
3966
+ for (const attr of attrs) {
3967
+ if (attr === "eol=crlf") eol = "\r\n";
3968
+ else if (attr === "eol=lf") eol = "\n";
3969
+ else if (attr === "-text" || attr === "binary") eol = null;
3970
+ }
3971
+ }
3972
+ return eol;
3973
+ }
3974
+ function conventionFolders(dir) {
3975
+ const folders = [];
3976
+ let at = path.resolve(dir);
3977
+ for (let depth = 0; depth < 8; depth++) {
3978
+ folders.push(at);
3979
+ if (fs.existsSync(path.join(at, ".git")) || fs.existsSync(path.join(at, ".wai"))) return folders;
3980
+ const parent = path.dirname(at);
3981
+ if (parent === at) break;
3982
+ at = parent;
3983
+ }
3984
+ return folders.slice(0, 1);
3985
+ }
3986
+ function folderEndings(folder, ext, exclude) {
3987
+ let names;
3988
+ try {
3989
+ names = fs.readdirSync(folder);
3990
+ } catch {
3991
+ return null;
3992
+ }
3993
+ const candidates = names.filter((n) => path.join(folder, n) !== exclude && /\.(ya?ml|json|md|ts|js|txt)$/i.test(n)).sort((a, b) => Number(path.extname(b) === ext) - Number(path.extname(a) === ext)).slice(0, 24);
3994
+ let crlf = 0;
3995
+ let lf = 0;
3996
+ for (const name of candidates) {
3997
+ const full = path.join(folder, name);
3998
+ try {
3999
+ if (!fs.statSync(full).isFile()) continue;
4000
+ } catch {
4001
+ continue;
4002
+ }
4003
+ const sample = sampleOf(full);
4004
+ const endings = sample === null ? null : endingsOf(sample);
4005
+ if (endings === "\r\n") crlf++;
4006
+ else if (endings === "\n") lf++;
4007
+ }
4008
+ if (crlf === lf) return null;
4009
+ return crlf > lf ? "\r\n" : "\n";
4010
+ }
4011
+ function lineEndingFor(filePath) {
4012
+ const own = sampleOf(filePath);
4013
+ const existing = own === null ? null : endingsOf(own);
4014
+ if (existing) return existing;
4015
+ const folders = conventionFolders(path.dirname(filePath));
4016
+ for (const folder of folders) {
4017
+ const eol = attributeEol(path.join(folder, ".gitattributes"), filePath);
4018
+ if (eol) return eol;
4019
+ }
4020
+ const ext = path.extname(filePath);
4021
+ for (const folder of folders) {
4022
+ const endings = folderEndings(folder, ext, path.resolve(filePath));
4023
+ if (endings) return endings;
4024
+ }
4025
+ return "\n";
4026
+ }
4027
+ function withLineEndings(filePath, content) {
4028
+ const normalised = content.replace(/\r\n/g, "\n");
4029
+ return lineEndingFor(filePath) === "\r\n" ? normalised.replace(/\n/g, "\r\n") : normalised;
4030
+ }
3922
4031
  function readFileOrNull(filePath) {
3923
4032
  try {
3924
4033
  return fs.readFileSync(filePath, "utf-8");
@@ -4017,22 +4126,74 @@ function setProjectRoot(dir) {
4017
4126
  function getProjectRootOverride() {
4018
4127
  return projectRootOverride;
4019
4128
  }
4020
- function findSystemRoot(startDir) {
4129
+ function holdsSystemSpec(dir) {
4130
+ for (const base of [".wai", ".wairon"]) {
4131
+ const specs = path.join(dir, base, "specs");
4132
+ if (fs.existsSync(path.join(specs, ".index.yaml")) || fs.existsSync(path.join(specs, "system.yaml"))) return true;
4133
+ if (base === ".wai" && fs.existsSync(path.join(dir, ".wai"))) break;
4134
+ }
4135
+ return false;
4136
+ }
4137
+ function findRepositoryRoot(startDir) {
4021
4138
  let dir = path.resolve(startDir);
4022
4139
  while (true) {
4023
- const isWai = fs.existsSync(path.join(dir, ".wai"));
4024
- const isWairon = !isWai && fs.existsSync(path.join(dir, ".wairon"));
4025
- const base = isWai ? ".wai" : isWairon ? ".wairon" : null;
4026
- if (base) {
4027
- if (fs.existsSync(path.join(dir, base, "specs", ".index.yaml")) || fs.existsSync(path.join(dir, base, "specs", "system.yaml"))) {
4028
- return dir;
4029
- }
4140
+ if (fs.existsSync(path.join(dir, ".git"))) return dir;
4141
+ const parent = path.dirname(dir);
4142
+ if (parent === dir) return null;
4143
+ dir = parent;
4144
+ }
4145
+ }
4146
+ function isAtOrBelow(outer, inner2) {
4147
+ const rel2 = path.relative(outer, inner2);
4148
+ return rel2 === "" || !rel2.startsWith("..") && !path.isAbsolute(rel2);
4149
+ }
4150
+ function declaredMemberDirs(projectRoot) {
4151
+ let parsed;
4152
+ try {
4153
+ const text3 = fs.readFileSync(path.join(projectRoot, ".wai", "project.yaml"), "utf-8");
4154
+ parsed = yaml.load(text3);
4155
+ } catch {
4156
+ return [];
4157
+ }
4158
+ const members = parsed?.members;
4159
+ if (!members || typeof members !== "object") return [];
4160
+ const dirs = [];
4161
+ for (const value of Object.values(members)) {
4162
+ const declaration2 = value;
4163
+ const source = typeof declaration2 === "string" ? declaration2 : typeof declaration2?.source === "string" ? declaration2.source : typeof declaration2?.path === "string" ? declaration2.path : void 0;
4164
+ if (!source) continue;
4165
+ const text3 = source.trim();
4166
+ if (text3.startsWith("hosted:") || /^[a-z][a-z0-9+.-]*:\/\//i.test(text3) || /^[\w.-]+@[\w.-]+:/.test(text3) || path.isAbsolute(text3)) continue;
4167
+ dirs.push(path.resolve(projectRoot, text3));
4168
+ }
4169
+ return dirs;
4170
+ }
4171
+ function resolveProjectBinding(startDir) {
4172
+ const start = path.resolve(startDir);
4173
+ const repositoryRoot2 = findRepositoryRoot(start) ?? void 0;
4174
+ const withRepo = (b) => repositoryRoot2 ? { ...b, repositoryRoot: repositoryRoot2 } : b;
4175
+ let dir = start;
4176
+ while (true) {
4177
+ if (holdsSystemSpec(dir)) return withRepo({ root: dir, via: dir === start ? "here" : "ancestor" });
4178
+ if (repositoryRoot2 && dir === repositoryRoot2) break;
4179
+ const parent = path.dirname(dir);
4180
+ if (parent === dir) return null;
4181
+ dir = parent;
4182
+ }
4183
+ dir = path.dirname(repositoryRoot2);
4184
+ while (true) {
4185
+ if (holdsSystemSpec(dir)) {
4186
+ const declares2 = declaredMemberDirs(dir).some((member) => isAtOrBelow(repositoryRoot2, member) && isAtOrBelow(member, start));
4187
+ if (declares2) return withRepo({ root: dir, via: "declared-member" });
4030
4188
  }
4031
4189
  const parent = path.dirname(dir);
4032
4190
  if (parent === dir) return null;
4033
4191
  dir = parent;
4034
4192
  }
4035
4193
  }
4194
+ function findSystemRoot(startDir) {
4195
+ return resolveProjectBinding(startDir)?.root ?? null;
4196
+ }
4036
4197
  function getProjectRoot() {
4037
4198
  const scoped = requestRootStore.getStore()?.root;
4038
4199
  if (scoped) return scoped;
@@ -4064,44 +4225,67 @@ function aiDirAt(root, ...segments) {
4064
4225
  return path.join(base, ...segments);
4065
4226
  }
4066
4227
 
4067
- // src/utils/yaml.ts
4068
- var yaml = __toESM(require("js-yaml"));
4069
- function parseYaml(content, sourcePath) {
4070
- try {
4071
- return yaml.load(content);
4072
- } catch (err) {
4073
- const loc = sourcePath ? ` (${sourcePath})` : "";
4074
- throw new Error(`Failed to parse YAML${loc}: ${String(err)}`);
4075
- }
4076
- }
4077
- function serializeYaml(value) {
4078
- return yaml.dump(value, {
4079
- indent: 2,
4080
- lineWidth: 100,
4081
- noRefs: true,
4082
- sortKeys: false
4083
- });
4084
- }
4085
- function readYamlFile(filePath) {
4086
- const content = readFileOrNull(filePath);
4087
- if (content === null) return null;
4088
- return parseYaml(content, filePath);
4089
- }
4090
- function writeYamlFile(filePath, value) {
4091
- writeFile(filePath, serializeYaml(value));
4228
+ // src/config/defaults.ts
4229
+ var os = __toESM(require("os"));
4230
+ var path2 = __toESM(require("path"));
4231
+ var DEFAULT_TARGET_DIRS = {
4232
+ claude: ".claude/agents",
4233
+ gemini: ".gemini/agents",
4234
+ agy: ".gemini/agents",
4235
+ cursor: ".cursor/rules",
4236
+ copilot: ".github/prompts",
4237
+ codex: ".codex/agents"
4238
+ };
4239
+ function defaultTargetConfig(type) {
4240
+ return {
4241
+ type,
4242
+ outputDir: DEFAULT_TARGET_DIRS[type],
4243
+ enabled: true
4244
+ };
4092
4245
  }
4093
- function readJsonFile(filePath) {
4094
- const content = readFileOrNull(filePath);
4095
- if (content === null) return null;
4096
- try {
4097
- return JSON.parse(content);
4098
- } catch (err) {
4099
- throw new Error(`Failed to parse JSON (${filePath}): ${String(err)}`);
4246
+ var WAIRON_VERSION = "5.1.1-dev.103";
4247
+ var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
4248
+ var ARCHITECT_AGENT_ID = "agent-architect";
4249
+ var ARCHITECT_TEMPLATE_ID = "architect";
4250
+ function globalTemplatesDir(projectOverride) {
4251
+ if (process.env.WAIRON_TEMPLATES_DIR) {
4252
+ return process.env.WAIRON_TEMPLATES_DIR;
4100
4253
  }
4254
+ if (projectOverride) {
4255
+ return projectOverride;
4256
+ }
4257
+ return path2.join(os.homedir(), ".wairon", "templates");
4101
4258
  }
4102
- function writeJsonFile(filePath, value) {
4103
- writeFile(filePath, JSON.stringify(value, null, 2) + "\n");
4259
+ var BACKEND_COMMANDS = {
4260
+ claude: "claude",
4261
+ gemini: "gemini"
4262
+ };
4263
+ function backendCommand(backend) {
4264
+ return BACKEND_COMMANDS[backend] ?? backend;
4104
4265
  }
4266
+ var SUPPORTED_ALIASES = ["wai"];
4267
+ var SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
4268
+ "node_modules",
4269
+ "dist",
4270
+ "build",
4271
+ "out",
4272
+ ".git",
4273
+ ".cache",
4274
+ "coverage",
4275
+ "__pycache__",
4276
+ ".venv",
4277
+ "venv",
4278
+ "target",
4279
+ // Rust
4280
+ "vendor"
4281
+ ]);
4282
+
4283
+ // src/core/adapters/git-source.ts
4284
+ var import_child_process = require("child_process");
4285
+ var crypto = __toESM(require("crypto"));
4286
+ var fs2 = __toESM(require("fs"));
4287
+ var os2 = __toESM(require("os"));
4288
+ var path3 = __toESM(require("path"));
4105
4289
 
4106
4290
  // src/utils/errors.ts
4107
4291
  var WaironError = class extends Error {
@@ -4151,6 +4335,167 @@ ${reasons.map((r) => ` - ${r}`).join("\n")}`);
4151
4335
  }
4152
4336
  };
4153
4337
 
4338
+ // src/core/adapters/git-source.ts
4339
+ var SourceUnavailable = class extends WaironError {
4340
+ constructor(message) {
4341
+ super(message);
4342
+ this.name = "SourceUnavailable";
4343
+ }
4344
+ };
4345
+ function osUserCache() {
4346
+ if (process.platform === "win32") return process.env.LOCALAPPDATA ?? path3.join(os2.homedir(), "AppData", "Local");
4347
+ if (process.platform === "darwin") return path3.join(os2.homedir(), "Library", "Caches");
4348
+ return process.env.XDG_CACHE_HOME ?? path3.join(os2.homedir(), ".cache");
4349
+ }
4350
+ var cacheRoots = /* @__PURE__ */ new Map();
4351
+ function cacheRoot() {
4352
+ const setting = process.env.WAIRON_CACHE_DIR ?? "";
4353
+ let root = cacheRoots.get(setting);
4354
+ if (root === void 0) {
4355
+ root = setting !== "" ? path3.resolve(setting) : path3.join(osUserCache(), "wairon");
4356
+ cacheRoots.set(setting, root);
4357
+ }
4358
+ return root;
4359
+ }
4360
+ function git(args, cwd) {
4361
+ return (0, import_child_process.execFileSync)("git", args, {
4362
+ cwd,
4363
+ encoding: "utf8",
4364
+ stdio: ["ignore", "pipe", "pipe"],
4365
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "echo" }
4366
+ });
4367
+ }
4368
+ function reasonOf(e) {
4369
+ const stderr = e?.stderr;
4370
+ const text3 = typeof stderr === "string" && stderr.trim() !== "" ? stderr : e instanceof Error ? e.message : String(e);
4371
+ return text3.trim().split(/\r?\n/).filter((l) => l.trim() !== "").slice(-1)[0] ?? "git failed";
4372
+ }
4373
+ function cacheEntryOf(url, commit2) {
4374
+ const repo = crypto.createHash("sha256").update(url).digest("hex");
4375
+ return path3.join(cacheRoot(), "git", repo, commit2);
4376
+ }
4377
+ function within(entry, url, commit2, dir) {
4378
+ const at = dir ? path3.join(entry, dir) : entry;
4379
+ if (!fs2.existsSync(at)) {
4380
+ throw new SourceUnavailable(`the commit ${commit2} of ${url} holds no "${dir}"`);
4381
+ }
4382
+ return at;
4383
+ }
4384
+ function fetchInto(tmp, url, commit2) {
4385
+ git(["init", "-q"], tmp);
4386
+ try {
4387
+ git(["fetch", "-q", "--depth", "1", url, commit2], tmp);
4388
+ } catch {
4389
+ git(["fetch", "-q", url, "+refs/heads/*:refs/remotes/source/*", "+refs/tags/*:refs/tags/*"], tmp);
4390
+ }
4391
+ git(["-c", "advice.detachedHead=false", "checkout", "-q", commit2], tmp);
4392
+ fs2.rmSync(path3.join(tmp, ".git"), { recursive: true, force: true });
4393
+ }
4394
+ function fetch(url, commit2, dir) {
4395
+ const entry = cacheEntryOf(url, commit2);
4396
+ if (fs2.existsSync(entry)) return within(entry, url, commit2, dir);
4397
+ fs2.mkdirSync(path3.dirname(entry), { recursive: true });
4398
+ const tmp = `${entry}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
4399
+ fs2.mkdirSync(tmp, { recursive: true });
4400
+ try {
4401
+ fetchInto(tmp, url, commit2);
4402
+ try {
4403
+ fs2.renameSync(tmp, entry);
4404
+ } catch (e) {
4405
+ if (!fs2.existsSync(entry)) throw e;
4406
+ fs2.rmSync(tmp, { recursive: true, force: true });
4407
+ }
4408
+ } catch (e) {
4409
+ fs2.rmSync(tmp, { recursive: true, force: true });
4410
+ if (fs2.readdirSync(path3.dirname(entry)).length === 0) fs2.rmdirSync(path3.dirname(entry));
4411
+ throw e instanceof SourceUnavailable ? e : new SourceUnavailable(`cannot fetch ${commit2} of ${url}: ${reasonOf(e)}`);
4412
+ }
4413
+ return within(entry, url, commit2, dir);
4414
+ }
4415
+ function resolve3(url, ref) {
4416
+ let listed;
4417
+ try {
4418
+ listed = git(["ls-remote", url, ref ?? "HEAD"]);
4419
+ } catch (e) {
4420
+ throw new SourceUnavailable(`cannot reach ${url}: ${reasonOf(e)}`);
4421
+ }
4422
+ const wanted = commitListed(listed, ref ?? "HEAD");
4423
+ if (!wanted) throw new SourceUnavailable(`${url} lists no ref "${ref ?? "HEAD"}"`);
4424
+ return wanted;
4425
+ }
4426
+ function commitListed(listed, ref) {
4427
+ const lines = listed.split(/\r?\n/).filter((l) => l.trim() !== "").map((l) => l.split(/\s+/));
4428
+ const names = [ref, `refs/heads/${ref}`, `refs/tags/${ref}^{}`, `refs/tags/${ref}`];
4429
+ for (const name of names) {
4430
+ const line = lines.find(([, listedName]) => listedName === name);
4431
+ if (line) return line[0];
4432
+ }
4433
+ return void 0;
4434
+ }
4435
+ function trackedFiles(directory, pathspecs) {
4436
+ if (pathspecs.length === 0) return [];
4437
+ try {
4438
+ return git(["ls-files", "-z", "--", ...pathspecs], directory).split("\0").filter((p) => p !== "");
4439
+ } catch {
4440
+ return null;
4441
+ }
4442
+ }
4443
+ function head(directory) {
4444
+ try {
4445
+ return git(["rev-parse", "HEAD"], directory).trim() || null;
4446
+ } catch {
4447
+ return null;
4448
+ }
4449
+ }
4450
+ function repositoryRoot(directory) {
4451
+ let at = path3.resolve(directory);
4452
+ for (; ; ) {
4453
+ if (fs2.existsSync(path3.join(at, ".git"))) return at;
4454
+ const up = path3.dirname(at);
4455
+ if (up === at) return null;
4456
+ at = up;
4457
+ }
4458
+ }
4459
+
4460
+ // src/utils/yaml.ts
4461
+ var yaml2 = __toESM(require("js-yaml"));
4462
+ function parseYaml(content, sourcePath) {
4463
+ try {
4464
+ return yaml2.load(content);
4465
+ } catch (err) {
4466
+ const loc = sourcePath ? ` (${sourcePath})` : "";
4467
+ throw new Error(`Failed to parse YAML${loc}: ${String(err)}`);
4468
+ }
4469
+ }
4470
+ function serializeYaml(value) {
4471
+ return yaml2.dump(value, {
4472
+ indent: 2,
4473
+ lineWidth: 100,
4474
+ noRefs: true,
4475
+ sortKeys: false
4476
+ });
4477
+ }
4478
+ function readYamlFile(filePath) {
4479
+ const content = readFileOrNull(filePath);
4480
+ if (content === null) return null;
4481
+ return parseYaml(content, filePath);
4482
+ }
4483
+ function writeYamlFile(filePath, value) {
4484
+ writeFile(filePath, serializeYaml(value));
4485
+ }
4486
+ function readJsonFile(filePath) {
4487
+ const content = readFileOrNull(filePath);
4488
+ if (content === null) return null;
4489
+ try {
4490
+ return JSON.parse(content);
4491
+ } catch (err) {
4492
+ throw new Error(`Failed to parse JSON (${filePath}): ${String(err)}`);
4493
+ }
4494
+ }
4495
+ function writeJsonFile(filePath, value) {
4496
+ writeFile(filePath, JSON.stringify(value, null, 2) + "\n");
4497
+ }
4498
+
4154
4499
  // src/models/identity-rename.ts
4155
4500
  var EDGE = " -> ";
4156
4501
  var QUALIFIED_UNIT = /^(\d+:)?([^.\s]+)\.([^.\s]+)$/;
@@ -4186,7 +4531,7 @@ function rekeyAnchor(rename, anchor) {
4186
4531
  }
4187
4532
 
4188
4533
  // src/models/project.ts
4189
- var path2 = __toESM(require("path"));
4534
+ var path4 = __toESM(require("path"));
4190
4535
  var import_zod3 = require("zod");
4191
4536
 
4192
4537
  // src/models/agent.ts
@@ -4346,6 +4691,14 @@ var AgentBriefSchema = import_zod2.z.object({
4346
4691
  * agent implements nothing.
4347
4692
  */
4348
4693
  typeMapping: import_zod2.z.array(import_zod2.z.string()).optional(),
4694
+ /**
4695
+ * Where an implementing agent may write CODE: each source file its
4696
+ * implementations name, plus the folder they share below the project root as
4697
+ * `<folder>/**`. Empty when nothing names a file yet (the instructions then
4698
+ * ask the spawning session to name the planned files); absent for an agent
4699
+ * that implements nothing.
4700
+ */
4701
+ codeFence: import_zod2.z.array(import_zod2.z.string()).optional(),
4349
4702
  /**
4350
4703
  * Set only on a brief for a member's agent (an id qualified `<alias>::`): the
4351
4704
  * member's directory relative to the root the brief was asked from. The
@@ -4736,7 +5089,7 @@ var ExternalDeclarationSchema = import_zod3.z.object({
4736
5089
  /**
4737
5090
  * The producer's public names this project imports, so its specs may name
4738
5091
  * them bare: `['*']` imports every public name the producer exports to this
4739
- * project, `[waffler-error]` only those. Plain strings: a malformed entry is
5092
+ * project, `[shared-error]` only those. Plain strings: a malformed entry is
4740
5093
  * REPORTED (the declaration's problem), never unreadable.
4741
5094
  */
4742
5095
  use: import_zod3.z.array(import_zod3.z.string()).optional(),
@@ -4939,7 +5292,7 @@ var PACK_EXT_RE = /\.(ya?ml|cjs|js)$/i;
4939
5292
  function declaredPackNames(config) {
4940
5293
  const selection = config.profileSelection;
4941
5294
  return [.../* @__PURE__ */ new Set([
4942
- ...(config.extensions?.packs ?? []).map((entry) => typeof entry === "string" ? path2.basename(entry).replace(PACK_EXT_RE, "") : entry.name),
5295
+ ...(config.extensions?.packs ?? []).map((entry) => typeof entry === "string" ? path4.basename(entry).replace(PACK_EXT_RE, "") : entry.name),
4943
5296
  ...selection?.requiredPackNames ?? [],
4944
5297
  ...selection?.defaultPackNames ?? []
4945
5298
  ])];
@@ -5237,13 +5590,13 @@ function isPart(config) {
5237
5590
 
5238
5591
  // src/config/project-config.ts
5239
5592
  function waiDirAt(root) {
5240
- const wai = path3.join(root, ".wai");
5241
- const legacy = path3.join(root, ".wairon");
5242
- return !fs2.existsSync(wai) && fs2.existsSync(legacy) ? legacy : wai;
5593
+ const wai = path5.join(root, ".wai");
5594
+ const legacy = path5.join(root, ".wairon");
5595
+ return !fs3.existsSync(wai) && fs3.existsSync(legacy) ? legacy : wai;
5243
5596
  }
5244
5597
  function projectConfigFsAdapterAt(rootDir) {
5245
- const root = path3.resolve(rootDir);
5246
- const file = () => path3.join(waiDirAt(root), "project.yaml");
5598
+ const root = path5.resolve(rootDir);
5599
+ const file = () => path5.join(waiDirAt(root), "project.yaml");
5247
5600
  return {
5248
5601
  readDocument() {
5249
5602
  return readYamlFile(file());
@@ -5257,7 +5610,7 @@ function projectConfigFsAdapterAt(rootDir) {
5257
5610
  writeFile(file(), editDocumentText(text3, document, root));
5258
5611
  },
5259
5612
  documentExists() {
5260
- return fs2.existsSync(file());
5613
+ return fs3.existsSync(file());
5261
5614
  },
5262
5615
  readText() {
5263
5616
  return readFileOrNull(file());
@@ -5279,8 +5632,9 @@ function storeOver(adapter, root) {
5279
5632
  if (!checked.success) {
5280
5633
  throw new WaironError(`Refusing to write an invalid .wai/project.yaml at ${root}: ${checked.error.message}`);
5281
5634
  }
5282
- const document = config.partOf !== void 0 ? { schemaVersion: config.schemaVersion, partOf: config.partOf } : config;
5283
- adapter.writeDocument(overlayKnownFields(ProjectConfigSchema, document, currentDocument()));
5635
+ const onDisk = currentDocument();
5636
+ const document = config.partOf !== void 0 ? { schemaVersion: config.schemaVersion, partOf: config.partOf } : withoutDefaultedMaterialization(config, onDisk);
5637
+ adapter.writeDocument(overlayKnownFields(ProjectConfigSchema, document, onDisk));
5284
5638
  },
5285
5639
  exists() {
5286
5640
  return adapter.documentExists();
@@ -5680,9 +6034,69 @@ function asReadable(document) {
5680
6034
  if (!isPlainObject(document) || document.partOf === void 0) return document;
5681
6035
  return { name: "", createdAt: PART_EPOCH, updatedAt: PART_EPOCH, ...document };
5682
6036
  }
6037
+ var MANAGED_AGENT_MARKER = "wairon:managed";
6038
+ var MANAGED_AGENT_NAME = /-(owner|implementer|architect)\.(md|ya?ml)$/;
6039
+ function holdsMaterializedAgentFiles(document, root) {
6040
+ const dirs = new Set(Object.values(DEFAULT_TARGET_DIRS));
6041
+ for (const target of Array.isArray(document.targets) ? document.targets : []) {
6042
+ const outputDir = isPlainObject(target) ? target.outputDir : void 0;
6043
+ if (typeof outputDir === "string") dirs.add(outputDir);
6044
+ }
6045
+ const base = path5.resolve(root);
6046
+ const inside = [];
6047
+ for (const dir of dirs) {
6048
+ const rel2 = path5.relative(base, path5.resolve(base, dir));
6049
+ if (rel2.startsWith("..") || path5.isAbsolute(rel2)) continue;
6050
+ let present = false;
6051
+ try {
6052
+ present = fs3.statSync(path5.join(base, rel2)).isDirectory();
6053
+ } catch {
6054
+ present = false;
6055
+ }
6056
+ if (present) inside.push(rel2 === "" ? "." : rel2.split(path5.sep).join("/"));
6057
+ }
6058
+ if (inside.length === 0) return false;
6059
+ const tracked = trackedFiles(base, inside);
6060
+ const candidates = [];
6061
+ if (tracked === null) {
6062
+ for (const dir of inside) {
6063
+ let names;
6064
+ try {
6065
+ names = fs3.readdirSync(path5.join(base, dir));
6066
+ } catch {
6067
+ continue;
6068
+ }
6069
+ for (const name of names) candidates.push(path5.posix.join(dir, name));
6070
+ }
6071
+ } else {
6072
+ const folders = new Set(inside.map((dir) => path5.posix.normalize(dir)));
6073
+ for (const file of tracked) if (folders.has(path5.posix.dirname(file))) candidates.push(file);
6074
+ }
6075
+ for (const file of candidates) {
6076
+ const head2 = readFileOrNull(path5.join(base, file));
6077
+ if (head2 === null) continue;
6078
+ if (MANAGED_AGENT_NAME.test(path5.posix.basename(file))) return true;
6079
+ if (head2.slice(0, 4096).includes(MANAGED_AGENT_MARKER)) return true;
6080
+ }
6081
+ return false;
6082
+ }
6083
+ function withInferredMaterialization(document, root) {
6084
+ if (!isPlainObject(document) || document.partOf !== void 0) return document;
6085
+ const rules = isPlainObject(document.rules) ? document.rules : void 0;
6086
+ if (rules !== void 0 && hasOwn(rules, "materializeAgentFiles")) return document;
6087
+ if (!holdsMaterializedAgentFiles(document, root)) return document;
6088
+ return { ...document, rules: { ...rules ?? {}, materializeAgentFiles: true } };
6089
+ }
6090
+ function withoutDefaultedMaterialization(config, onDisk) {
6091
+ if (!isPlainObject(onDisk) || config.rules?.materializeAgentFiles !== false) return config;
6092
+ const stated = isPlainObject(onDisk.rules) && hasOwn(onDisk.rules, "materializeAgentFiles");
6093
+ if (stated) return config;
6094
+ const { materializeAgentFiles: _defaulted, ...rules } = config.rules;
6095
+ return { ...config, rules };
6096
+ }
5683
6097
  function parseConfig(document, root) {
5684
6098
  try {
5685
- return ProjectConfigSchema.parse(asReadable(document));
6099
+ return ProjectConfigSchema.parse(asReadable(withInferredMaterialization(document, root)));
5686
6100
  } catch (e) {
5687
6101
  throw new WaironError(
5688
6102
  `Invalid .wai/project.yaml: ${e instanceof Error ? e.message : String(e)}
@@ -6123,15 +6537,15 @@ function indexOver(store, root) {
6123
6537
  specsDir() {
6124
6538
  try {
6125
6539
  const declared = store.readField("paths.specsDir");
6126
- if (declared) return path3.resolve(root, declared);
6540
+ if (declared) return path5.resolve(root, declared);
6127
6541
  } catch {
6128
6542
  }
6129
- return path3.join(waiDirAt(root), "specs");
6543
+ return path5.join(waiDirAt(root), "specs");
6130
6544
  }
6131
6545
  };
6132
6546
  }
6133
6547
  function projectConfigRepositoryOver(adapter, rootDir) {
6134
- const root = path3.resolve(rootDir);
6548
+ const root = path5.resolve(rootDir);
6135
6549
  const store = storeOver(adapter, root);
6136
6550
  const index = indexOver(store, root);
6137
6551
  const registry = registryOver(store, root);
@@ -6324,15 +6738,15 @@ var fs14 = __toESM(require("fs"));
6324
6738
  var path21 = __toESM(require("path"));
6325
6739
 
6326
6740
  // src/config/paths.ts
6327
- var fs3 = __toESM(require("fs"));
6328
- var path4 = __toESM(require("path"));
6741
+ var fs4 = __toESM(require("fs"));
6742
+ var path6 = __toESM(require("path"));
6329
6743
  function aiPathsAt(rootDir) {
6330
- const resolvedRoot = path4.resolve(rootDir);
6744
+ const resolvedRoot = path6.resolve(rootDir);
6331
6745
  const aiDirAt2 = (...segments) => {
6332
- const waiPath = path4.join(resolvedRoot, ".wai");
6333
- const waironPath = path4.join(resolvedRoot, ".wairon");
6334
- const base = !fs3.existsSync(waiPath) && fs3.existsSync(waironPath) ? waironPath : waiPath;
6335
- return path4.join(base, ...segments);
6746
+ const waiPath = path6.join(resolvedRoot, ".wai");
6747
+ const waironPath = path6.join(resolvedRoot, ".wairon");
6748
+ const base = !fs4.existsSync(waiPath) && fs4.existsSync(waironPath) ? waironPath : waiPath;
6749
+ return path6.join(base, ...segments);
6336
6750
  };
6337
6751
  const specsDir = () => projectConfigRepositoryAt(resolvedRoot).specsDir();
6338
6752
  return {
@@ -6349,12 +6763,12 @@ function aiPathsAt(rootDir) {
6349
6763
  contextDomainsMd: () => aiDirAt2("context", "domains.md"),
6350
6764
  contextWaironGuideMd: () => aiDirAt2("context", "wairon-guide.md"),
6351
6765
  specsDir,
6352
- specsSystem: () => path4.join(specsDir(), ".index.yaml"),
6353
- specsSubsystemsDir: () => path4.join(specsDir(), "subsystems"),
6354
- specsComponentsDir: () => path4.join(specsDir(), "components"),
6355
- specsInterfacesDir: () => path4.join(specsDir(), "interfaces"),
6356
- specsImplementationsDir: () => path4.join(specsDir(), "implementations"),
6357
- specsTypesDir: () => path4.join(specsDir(), "types")
6766
+ specsSystem: () => path6.join(specsDir(), ".index.yaml"),
6767
+ specsSubsystemsDir: () => path6.join(specsDir(), "subsystems"),
6768
+ specsComponentsDir: () => path6.join(specsDir(), "components"),
6769
+ specsInterfacesDir: () => path6.join(specsDir(), "interfaces"),
6770
+ specsImplementationsDir: () => path6.join(specsDir(), "implementations"),
6771
+ specsTypesDir: () => path6.join(specsDir(), "types")
6358
6772
  };
6359
6773
  }
6360
6774
  var AI_PATHS = {
@@ -6388,8 +6802,8 @@ function assertProjectInitialized() {
6388
6802
  }
6389
6803
 
6390
6804
  // src/core/statehash.ts
6391
- var crypto = __toESM(require("crypto"));
6392
- var path5 = __toESM(require("path"));
6805
+ var crypto2 = __toESM(require("crypto"));
6806
+ var path7 = __toESM(require("path"));
6393
6807
 
6394
6808
  // src/utils/canonical-json.ts
6395
6809
  function canonicalize(value) {
@@ -6428,7 +6842,7 @@ function digestTree(keep, asStored = (impl) => impl) {
6428
6842
  implementations: kind(loadImplementationSpecs().map(asStored)),
6429
6843
  types: kind(loadTypeSpecs())
6430
6844
  };
6431
- const digest3 = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
6845
+ const digest3 = crypto2.createHash("sha256").update(canonicalize(tree)).digest("hex");
6432
6846
  return { algorithm: CONTENT_ALGORITHM, digest: digest3 };
6433
6847
  }
6434
6848
  function computeStateId() {
@@ -6448,11 +6862,11 @@ function computeOwnStateId() {
6448
6862
  return digestTree((id) => (owners.get(id) ?? "") === "", asStored);
6449
6863
  }
6450
6864
  function isInside(dir, file) {
6451
- const rel2 = path5.relative(dir, file);
6452
- return rel2 !== "" && !rel2.startsWith("..") && !path5.isAbsolute(rel2);
6865
+ const rel2 = path7.relative(dir, file);
6866
+ return rel2 !== "" && !rel2.startsWith("..") && !path7.isAbsolute(rel2);
6453
6867
  }
6454
6868
  function fromPartRoot(impl, root, partDir) {
6455
- const from = (p) => path5.isAbsolute(p) ? p : path5.relative(partDir, path5.resolve(root, p)).split(path5.sep).join("/");
6869
+ const from = (p) => path7.isAbsolute(p) ? p : path7.relative(partDir, path7.resolve(root, p)).split(path7.sep).join("/");
6456
6870
  return {
6457
6871
  ...impl,
6458
6872
  ...impl.sourcePath ? { sourcePath: from(impl.sourcePath) } : {},
@@ -6465,8 +6879,8 @@ function stateIdEquals(a, b) {
6465
6879
  }
6466
6880
 
6467
6881
  // src/core/lockfile.ts
6468
- var fs4 = __toESM(require("fs"));
6469
- var path6 = __toESM(require("path"));
6882
+ var fs5 = __toESM(require("fs"));
6883
+ var path8 = __toESM(require("path"));
6470
6884
  function lockPath() {
6471
6885
  return aiDir("lock.json");
6472
6886
  }
@@ -6480,14 +6894,14 @@ function normalizeApprover(value) {
6480
6894
  }
6481
6895
  function readLockRecord() {
6482
6896
  try {
6483
- return normalizeRecord(JSON.parse(fs4.readFileSync(lockPath(), "utf8")));
6897
+ return normalizeRecord(JSON.parse(fs5.readFileSync(lockPath(), "utf8")));
6484
6898
  } catch {
6485
6899
  return null;
6486
6900
  }
6487
6901
  }
6488
6902
  function readLockRecordAt(root) {
6489
6903
  try {
6490
- return normalizeRecord(JSON.parse(fs4.readFileSync(aiDirAt(root, "lock.json"), "utf8")));
6904
+ return normalizeRecord(JSON.parse(fs5.readFileSync(aiDirAt(root, "lock.json"), "utf8")));
6491
6905
  } catch {
6492
6906
  return null;
6493
6907
  }
@@ -6498,10 +6912,10 @@ function normalizeRecord(raw) {
6498
6912
  }
6499
6913
  function writeLockRecord(record) {
6500
6914
  const p = lockPath();
6501
- fs4.mkdirSync(path6.dirname(p), { recursive: true });
6915
+ fs5.mkdirSync(path8.dirname(p), { recursive: true });
6502
6916
  const tmp = `${p}.tmp`;
6503
- fs4.writeFileSync(tmp, JSON.stringify(withSortedMaps(record), null, 2) + "\n");
6504
- fs4.renameSync(tmp, p);
6917
+ fs5.writeFileSync(tmp, withLineEndings(p, JSON.stringify(withSortedMaps(record), null, 2) + "\n"));
6918
+ fs5.renameSync(tmp, p);
6505
6919
  }
6506
6920
  function withSortedMaps(record) {
6507
6921
  const sorted = (m) => {
@@ -6521,8 +6935,8 @@ function withSortedMaps(record) {
6521
6935
  }
6522
6936
 
6523
6937
  // src/core/spec-files.ts
6524
- var fs5 = __toESM(require("fs"));
6525
- var path7 = __toESM(require("path"));
6938
+ var fs6 = __toESM(require("fs"));
6939
+ var path9 = __toESM(require("path"));
6526
6940
  var SPEC_FILE_EXTENSION = ".yaml";
6527
6941
  function readSpecFile(specPath) {
6528
6942
  return readYamlFile(specPath);
@@ -6534,136 +6948,17 @@ function listSpecFiles(specsDir) {
6534
6948
  return listFilesRecursive(specsDir, SPEC_FILE_EXTENSION);
6535
6949
  }
6536
6950
  function removeSpecFile(specPath, specsRoot) {
6537
- if (!fs5.existsSync(specPath)) return false;
6538
- fs5.unlinkSync(specPath);
6539
- pruneEmptyDirs(path7.dirname(specPath), path7.resolve(specsRoot));
6951
+ if (!fs6.existsSync(specPath)) return false;
6952
+ fs6.unlinkSync(specPath);
6953
+ pruneEmptyDirs(path9.dirname(specPath), path9.resolve(specsRoot));
6540
6954
  return true;
6541
6955
  }
6542
6956
  function pruneEmptyDirs(dir, specsRoot) {
6543
6957
  let at = dir;
6544
6958
  while (at !== specsRoot && at.startsWith(specsRoot)) {
6545
- if (!fs5.existsSync(at) || fs5.readdirSync(at).length > 0) return;
6546
- fs5.rmdirSync(at);
6547
- at = path7.dirname(at);
6548
- }
6549
- }
6550
-
6551
- // src/core/adapters/git-source.ts
6552
- var import_child_process = require("child_process");
6553
- var crypto2 = __toESM(require("crypto"));
6554
- var fs6 = __toESM(require("fs"));
6555
- var os = __toESM(require("os"));
6556
- var path8 = __toESM(require("path"));
6557
- var SourceUnavailable = class extends WaironError {
6558
- constructor(message) {
6559
- super(message);
6560
- this.name = "SourceUnavailable";
6561
- }
6562
- };
6563
- function osUserCache() {
6564
- if (process.platform === "win32") return process.env.LOCALAPPDATA ?? path8.join(os.homedir(), "AppData", "Local");
6565
- if (process.platform === "darwin") return path8.join(os.homedir(), "Library", "Caches");
6566
- return process.env.XDG_CACHE_HOME ?? path8.join(os.homedir(), ".cache");
6567
- }
6568
- var cacheRoots = /* @__PURE__ */ new Map();
6569
- function cacheRoot() {
6570
- const setting = process.env.WAIRON_CACHE_DIR ?? "";
6571
- let root = cacheRoots.get(setting);
6572
- if (root === void 0) {
6573
- root = setting !== "" ? path8.resolve(setting) : path8.join(osUserCache(), "wairon");
6574
- cacheRoots.set(setting, root);
6575
- }
6576
- return root;
6577
- }
6578
- function git(args, cwd) {
6579
- return (0, import_child_process.execFileSync)("git", args, {
6580
- cwd,
6581
- encoding: "utf8",
6582
- stdio: ["ignore", "pipe", "pipe"],
6583
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "echo" }
6584
- });
6585
- }
6586
- function reasonOf(e) {
6587
- const stderr = e?.stderr;
6588
- const text3 = typeof stderr === "string" && stderr.trim() !== "" ? stderr : e instanceof Error ? e.message : String(e);
6589
- return text3.trim().split(/\r?\n/).filter((l) => l.trim() !== "").slice(-1)[0] ?? "git failed";
6590
- }
6591
- function cacheEntryOf(url, commit2) {
6592
- const repo = crypto2.createHash("sha256").update(url).digest("hex");
6593
- return path8.join(cacheRoot(), "git", repo, commit2);
6594
- }
6595
- function within(entry, url, commit2, dir) {
6596
- const at = dir ? path8.join(entry, dir) : entry;
6597
- if (!fs6.existsSync(at)) {
6598
- throw new SourceUnavailable(`the commit ${commit2} of ${url} holds no "${dir}"`);
6599
- }
6600
- return at;
6601
- }
6602
- function fetchInto(tmp, url, commit2) {
6603
- git(["init", "-q"], tmp);
6604
- try {
6605
- git(["fetch", "-q", "--depth", "1", url, commit2], tmp);
6606
- } catch {
6607
- git(["fetch", "-q", url, "+refs/heads/*:refs/remotes/source/*", "+refs/tags/*:refs/tags/*"], tmp);
6608
- }
6609
- git(["-c", "advice.detachedHead=false", "checkout", "-q", commit2], tmp);
6610
- fs6.rmSync(path8.join(tmp, ".git"), { recursive: true, force: true });
6611
- }
6612
- function fetch(url, commit2, dir) {
6613
- const entry = cacheEntryOf(url, commit2);
6614
- if (fs6.existsSync(entry)) return within(entry, url, commit2, dir);
6615
- fs6.mkdirSync(path8.dirname(entry), { recursive: true });
6616
- const tmp = `${entry}.tmp-${process.pid}-${crypto2.randomBytes(4).toString("hex")}`;
6617
- fs6.mkdirSync(tmp, { recursive: true });
6618
- try {
6619
- fetchInto(tmp, url, commit2);
6620
- try {
6621
- fs6.renameSync(tmp, entry);
6622
- } catch (e) {
6623
- if (!fs6.existsSync(entry)) throw e;
6624
- fs6.rmSync(tmp, { recursive: true, force: true });
6625
- }
6626
- } catch (e) {
6627
- fs6.rmSync(tmp, { recursive: true, force: true });
6628
- if (fs6.readdirSync(path8.dirname(entry)).length === 0) fs6.rmdirSync(path8.dirname(entry));
6629
- throw e instanceof SourceUnavailable ? e : new SourceUnavailable(`cannot fetch ${commit2} of ${url}: ${reasonOf(e)}`);
6630
- }
6631
- return within(entry, url, commit2, dir);
6632
- }
6633
- function resolve7(url, ref) {
6634
- let listed;
6635
- try {
6636
- listed = git(["ls-remote", url, ref ?? "HEAD"]);
6637
- } catch (e) {
6638
- throw new SourceUnavailable(`cannot reach ${url}: ${reasonOf(e)}`);
6639
- }
6640
- const wanted = commitListed(listed, ref ?? "HEAD");
6641
- if (!wanted) throw new SourceUnavailable(`${url} lists no ref "${ref ?? "HEAD"}"`);
6642
- return wanted;
6643
- }
6644
- function commitListed(listed, ref) {
6645
- const lines = listed.split(/\r?\n/).filter((l) => l.trim() !== "").map((l) => l.split(/\s+/));
6646
- const names = [ref, `refs/heads/${ref}`, `refs/tags/${ref}^{}`, `refs/tags/${ref}`];
6647
- for (const name of names) {
6648
- const line = lines.find(([, listedName]) => listedName === name);
6649
- if (line) return line[0];
6650
- }
6651
- return void 0;
6652
- }
6653
- function head(directory) {
6654
- try {
6655
- return git(["rev-parse", "HEAD"], directory).trim() || null;
6656
- } catch {
6657
- return null;
6658
- }
6659
- }
6660
- function repositoryRoot(directory) {
6661
- let at = path8.resolve(directory);
6662
- for (; ; ) {
6663
- if (fs6.existsSync(path8.join(at, ".git"))) return at;
6664
- const up = path8.dirname(at);
6665
- if (up === at) return null;
6666
- at = up;
6959
+ if (!fs6.existsSync(at) || fs6.readdirSync(at).length > 0) return;
6960
+ fs6.rmdirSync(at);
6961
+ at = path9.dirname(at);
6667
6962
  }
6668
6963
  }
6669
6964
 
@@ -8062,7 +8357,11 @@ var GENERIC_ALIASES = /* @__PURE__ */ new Map([
8062
8357
  ["HashMap", { form: "map", arity: 2 }],
8063
8358
  ["Option", { form: "optional", arity: 1 }],
8064
8359
  ["Optional", { form: "optional", arity: 1 }],
8065
- ["Promise", { form: "async", arity: 1 }]
8360
+ ["Promise", { form: "async", arity: 1 }],
8361
+ // The success-or-failure type the mainstream languages with typed failures
8362
+ // share (Rust, Swift, Kotlin, F#); Rust's spelling is its alias.
8363
+ ["result", { form: "result", arity: 2 }],
8364
+ ["Result", { form: "result", arity: 2 }]
8066
8365
  ]);
8067
8366
  var NOT_NEUTRAL = /* @__PURE__ */ new Map([
8068
8367
  ["uuid", "a named value-object holding a string (e.g. order_id)"],
@@ -8071,7 +8370,6 @@ var NOT_NEUTRAL = /* @__PURE__ */ new Map([
8071
8370
  ["byte", "int, or bytes for binary data"],
8072
8371
  ["time", "datetime for an instant, or duration for an elapsed time"],
8073
8372
  ["tuple", "a named value-object"],
8074
- ["result", "the success type as the returns (failures as findings or throws), or a named value-object"],
8075
8373
  ["error", "a named value-object"],
8076
8374
  ["never", "void"],
8077
8375
  ["box", "the inner type (ownership is an implementation detail)"],
@@ -8266,9 +8564,9 @@ var RawParser = class {
8266
8564
  }
8267
8565
  name += this.ident();
8268
8566
  while (this.isP("::") || this.isP(".")) {
8269
- const sep14 = this.peek().v;
8567
+ const sep15 = this.peek().v;
8270
8568
  this.at++;
8271
- name += sep14 + this.ident();
8569
+ name += sep15 + this.ident();
8272
8570
  }
8273
8571
  if (!this.isP("<")) return { k: "name", name };
8274
8572
  this.at++;
@@ -8295,6 +8593,10 @@ var RawParser = class {
8295
8593
  const close2 = this.closingParen();
8296
8594
  const after = close2 === -1 ? void 0 : this.tokens[close2 + 1];
8297
8595
  if (!after || after.t !== "p" || after.v !== "=>") {
8596
+ if (this.isP(")", 1)) {
8597
+ this.at += 2;
8598
+ return { k: "name", name: "void" };
8599
+ }
8298
8600
  this.expect("(");
8299
8601
  const inner2 = this.type();
8300
8602
  this.expect(")");
@@ -8403,7 +8705,7 @@ function optionalOf(inner2) {
8403
8705
  return { form: "optional", args: [inner2] };
8404
8706
  }
8405
8707
  function inner(ctx) {
8406
- return { ...ctx, top: false, voidAllowed: false, omittable: false };
8708
+ return { ...ctx, top: false, voidAllowed: false, resultAllowed: false, omittable: false };
8407
8709
  }
8408
8710
  function normalise(node, ctx, notes) {
8409
8711
  switch (node.k) {
@@ -8437,9 +8739,21 @@ function asyncOf(innerNode, ctx, notes, spelled) {
8437
8739
  if (!ctx.top || !RETURNS_POSITIONS.has(ctx.position)) {
8438
8740
  throw new TypeProblemError("TYPE_POSITION_INVALID", `${spelled} may stand only at the top of a returns, not in a ${ctx.position}`);
8439
8741
  }
8440
- const read2 = normalise(innerNode, { ...inner(ctx), voidAllowed: true }, notes);
8742
+ const read2 = normalise(innerNode, { ...inner(ctx), voidAllowed: true, resultAllowed: true }, notes);
8441
8743
  return { form: "async", args: [read2] };
8442
8744
  }
8745
+ function resultOf(args, ctx, notes, spelled) {
8746
+ if (!ctx.resultAllowed) {
8747
+ throw new TypeProblemError(
8748
+ "TYPE_POSITION_INVALID",
8749
+ `${spelled} may stand only as a whole returns (or under its async), not in a ${ctx.top ? ctx.position : "collection, union, optional or argument"}`,
8750
+ "the success type here (a failure belongs to a method's returns)"
8751
+ );
8752
+ }
8753
+ const ok = normalise(args[0], { ...inner(ctx), voidAllowed: true }, notes);
8754
+ const err = normalise(args[1], inner(ctx), notes);
8755
+ return { form: "result", args: [ok, err] };
8756
+ }
8443
8757
  function bareName(name, ctx, notes) {
8444
8758
  const lower = name.toLowerCase();
8445
8759
  if (NONES.has(name)) {
@@ -8477,6 +8791,7 @@ function appliedName(name, args, ctx, notes) {
8477
8791
  throw new TypeProblemError("TYPE_EXPRESSION_INVALID", `"${name}" takes ${alias.arity} type argument${alias.arity === 1 ? "" : "s"}, ${args.length} given`);
8478
8792
  }
8479
8793
  if (alias.form === "async") return asyncOf(args[0], ctx, notes, `${name}<...>`);
8794
+ if (alias.form === "result") return resultOf(args, ctx, notes, `${name}<...>`);
8480
8795
  if (alias.form === "optional") return optionalOf(normalise(args[0], inner(ctx), notes));
8481
8796
  const read2 = args.map((a) => normalise(a, inner(ctx), notes));
8482
8797
  if (alias.form === "map") assertMapKey(read2[0], args[0]);
@@ -8550,6 +8865,8 @@ function canonicalTypeText(expr) {
8550
8865
  return `${expr.form}<${canonicalTypeText(expr.args[0])}>`;
8551
8866
  case "map":
8552
8867
  return `map<${canonicalTypeText(expr.args[0])}, ${canonicalTypeText(expr.args[1])}>`;
8868
+ case "result":
8869
+ return `result<${canonicalTypeText(expr.args[0])}, ${canonicalTypeText(expr.args[1])}>`;
8553
8870
  case "optional": {
8554
8871
  const innerText = canonicalTypeText(expr.args[0]);
8555
8872
  return expr.args[0].form === "union" ? `(${innerText})?` : `${innerText}?`;
@@ -8569,11 +8886,12 @@ function typeNamedRefs(expr) {
8569
8886
  }
8570
8887
  function typeIsMany(expr) {
8571
8888
  if (expr.form === "list" || expr.form === "set" || expr.form === "map") return true;
8572
- if (expr.form === "optional" || expr.form === "async") return typeIsMany(expr.args[0]);
8889
+ if (expr.form === "optional" || expr.form === "async" || expr.form === "result") return typeIsMany(expr.args[0]);
8573
8890
  return false;
8574
8891
  }
8575
8892
  function contextFor(position, omittable2, looseNumber = false) {
8576
- return { position, top: true, voidAllowed: RETURNS_POSITIONS.has(position), omittable: omittable2, looseNumber };
8893
+ const returns = RETURNS_POSITIONS.has(position);
8894
+ return { position, top: true, voidAllowed: returns, resultAllowed: returns, omittable: omittable2, looseNumber };
8577
8895
  }
8578
8896
  function parseTypePosition(text3, position, omittable2 = false) {
8579
8897
  let raw;
@@ -8764,6 +9082,47 @@ function typeProblemIntProposal(problem) {
8764
9082
  if (!parse.expression || parse.problem) return null;
8765
9083
  return { specId: problem.specId, kind: problem.kind, path: problem.path, written: problem.written, stored: parse.canonical };
8766
9084
  }
9085
+ function stringLiteralValue(text3) {
9086
+ const quote = text3[0];
9087
+ if (quote !== "'" && quote !== '"' && quote !== "`" || text3[text3.length - 1] !== quote) return null;
9088
+ return text3.slice(1, -1).replace(/\\(.)/g, "$1");
9089
+ }
9090
+ function kebabOf(name) {
9091
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[\s_.]+/g, "-").toLowerCase();
9092
+ }
9093
+ function typeProblemEnumProposal(problem) {
9094
+ if (problem.code !== "TYPE_FORM_UNSUPPORTED") return null;
9095
+ if (problem.specId === void 0 || problem.kind === void 0 || problem.path === void 0) return null;
9096
+ let raw;
9097
+ try {
9098
+ raw = readRaw(problem.written);
9099
+ } catch (e) {
9100
+ if (e instanceof TypeSyntaxError) return null;
9101
+ throw e;
9102
+ }
9103
+ const members = raw.k === "union" ? raw.members : [raw];
9104
+ const values = [];
9105
+ let optional = false;
9106
+ for (const member of members) {
9107
+ if (member.k === "name" && member.args === void 0 && NONES.has(member.name)) {
9108
+ optional = true;
9109
+ continue;
9110
+ }
9111
+ const value = member.k === "literal" ? stringLiteralValue(member.text) : null;
9112
+ if (value === null) return null;
9113
+ if (!values.includes(value)) values.push(value);
9114
+ }
9115
+ if (values.length === 0) return null;
9116
+ return {
9117
+ specId: problem.specId,
9118
+ kind: problem.kind,
9119
+ path: problem.path,
9120
+ written: problem.written,
9121
+ enumId: kebabOf(positionNameOf(problem.path)),
9122
+ values,
9123
+ optional
9124
+ };
9125
+ }
8767
9126
  function emptyTypeSpellingFacts() {
8768
9127
  return { respellings: [], problems: [] };
8769
9128
  }
@@ -8814,7 +9173,7 @@ function extractTypesFromSignature(signature, returns) {
8814
9173
  let sigCleaned = signature.replace(/\/\/.*$/gm, "").replace(/#.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
8815
9174
  sigCleaned = sigCleaned.replace(/(?<=\S)\s*\([^)]*\)\s*$/, "");
8816
9175
  sigCleaned = sigCleaned.replace(/(?<=\S)\s+[-—:]\s+[a-z\s_-]+$/, "");
8817
- const returnsCleaned = returns.replace(/\/\/.*$/gm, "").replace(/#.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/[a-zA-Z0-9_-]+\s*\??\s*:/g, "");
9176
+ const returnsCleaned = returns.replace(/\/\/.*$/gm, "").replace(/#.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(?<![:\w-])[a-zA-Z0-9_-]+\s*\??\s*:(?!:)/g, "");
8818
9177
  types.push(...extractTypeIdentifiers(returnsCleaned));
8819
9178
  const openParen = sigCleaned.indexOf("(");
8820
9179
  const closeParen = sigCleaned.lastIndexOf(")");
@@ -8845,7 +9204,7 @@ function extractTypesFromSignature(signature, returns) {
8845
9204
  const colonIndex = param.indexOf(":");
8846
9205
  if (colonIndex !== -1) {
8847
9206
  const paramType = param.slice(colonIndex + 1).trim();
8848
- const paramTypeCleaned = paramType.replace(/[a-zA-Z0-9_-]+\s*\??\s*:/g, "");
9207
+ const paramTypeCleaned = paramType.replace(/(?<![:\w-])[a-zA-Z0-9_-]+\s*\??\s*:(?!:)/g, "");
8849
9208
  types.push(...extractTypeIdentifiers(paramTypeCleaned));
8850
9209
  }
8851
9210
  }
@@ -8914,6 +9273,7 @@ function lastSegment(name) {
8914
9273
  return name.split(/::|\./).filter(Boolean).pop() ?? name;
8915
9274
  }
8916
9275
  function sameType(code, spec, codeNames) {
9276
+ if (spec.form === "result" && code.form !== "result") return sameType(code, spec.args[0], codeNames);
8917
9277
  if (code.form === "primitive" && code.name === "number") return spec.form === "primitive" && (spec.name === "int" || spec.name === "float");
8918
9278
  if (code.form === "named" && (spec.form === "named" || spec.form === "applied")) {
8919
9279
  if (spec.form === "applied") return false;
@@ -8950,6 +9310,8 @@ function writeTypeScript(expr) {
8950
9310
  return expr.args.map(writeTypeScript).join(" | ");
8951
9311
  case "async":
8952
9312
  return `Promise<${writeTypeScript(expr.args[0])}>`;
9313
+ case "result":
9314
+ return writeTypeScript(expr.args[0]);
8953
9315
  case "applied":
8954
9316
  return `${expr.name}<${expr.args.map(writeTypeScript).join(", ")}>`;
8955
9317
  }
@@ -8992,6 +9354,7 @@ var typescriptDialect = {
8992
9354
  "T? \u2192 T | null",
8993
9355
  "A | B \u2192 A | B",
8994
9356
  "async T \u2192 Promise<T>",
9357
+ "result<T, E> \u2192 T, throwing E on failure (result<void, E> \u2192 void)",
8995
9358
  "an enum E \u2192 type E = 'a' | 'b' (a string-literal union alias)",
8996
9359
  "a named scalar E holding P \u2192 type E = P (an alias of the primitive)"
8997
9360
  ];
@@ -9266,7 +9629,7 @@ function jumpFieldsOf(s) {
9266
9629
  }
9267
9630
 
9268
9631
  // src/models/code-model.ts
9269
- var path9 = __toESM(require("path"));
9632
+ var path10 = __toESM(require("path"));
9270
9633
  function pathKey(sourcePath) {
9271
9634
  return sourcePath.replace(/\\/g, "/").replace(/^\.\//, "");
9272
9635
  }
@@ -9290,6 +9653,12 @@ function fieldTypesOf(facts, field) {
9290
9653
  function localTypesOf(facts, name) {
9291
9654
  return ownEntry(facts.localTypes, name) ?? NO_DECLARED_TYPES;
9292
9655
  }
9656
+ function memberTypesOf(facts, shape, property) {
9657
+ return ownEntry(facts.memberTypes, `${shape}.${property}`) ?? NO_DECLARED_TYPES;
9658
+ }
9659
+ function aliasTargetOf(facts, name) {
9660
+ return ownEntry(facts.aliasTargets, name);
9661
+ }
9293
9662
  function typeBindingOf(facts, name) {
9294
9663
  return ownEntry(facts.typeOnlyBindings, name) ?? ownEntry(facts.importBindings, name)?.from;
9295
9664
  }
@@ -9340,7 +9709,7 @@ var EXTENSION_LANGUAGE = {
9340
9709
  ".swift": "swift"
9341
9710
  };
9342
9711
  function languageOfSourcePath(sourcePath) {
9343
- return EXTENSION_LANGUAGE[path9.extname(sourcePath).toLowerCase()];
9712
+ return EXTENSION_LANGUAGE[path10.extname(sourcePath).toLowerCase()];
9344
9713
  }
9345
9714
  function dialectOf(facts) {
9346
9715
  return facts.language ? typeDialectFor(facts.language) : null;
@@ -9353,7 +9722,7 @@ function resolveImport(fromFile, specifier, knownPaths, packages) {
9353
9722
  const mapped = ownEntry(packages, specifier);
9354
9723
  return mapped !== void 0 && knownPaths.has(mapped) ? mapped : void 0;
9355
9724
  }
9356
- const joined = pathKey(path9.posix.normalize(path9.posix.join(path9.posix.dirname(fromFile), specifier)));
9725
+ const joined = pathKey(path10.posix.normalize(path10.posix.join(path10.posix.dirname(fromFile), specifier)));
9357
9726
  const candidates = [
9358
9727
  joined,
9359
9728
  joined.replace(/\.js$/, ".ts"),
@@ -9500,7 +9869,7 @@ function exportTargetKey(e) {
9500
9869
  }
9501
9870
 
9502
9871
  // src/models/project-family.ts
9503
- var path10 = __toESM(require("path"));
9872
+ var path11 = __toESM(require("path"));
9504
9873
  function walkMemberPath(byKey3, start, segments) {
9505
9874
  let node = start;
9506
9875
  for (const segment of segments.slice(0, -1)) {
@@ -9562,15 +9931,15 @@ function keyIn(project, localId) {
9562
9931
  return project ? `${project}::${localId}` : localId;
9563
9932
  }
9564
9933
  function approvalKeyIn(file, root, parts) {
9565
- const resolved = path10.resolve(file);
9934
+ const resolved = path11.resolve(file);
9566
9935
  for (const part of parts) {
9567
9936
  if (part.directory === void 0) continue;
9568
- const rel2 = path10.relative(part.directory, resolved);
9569
- if (rel2 !== "" && !rel2.startsWith("..") && !path10.isAbsolute(rel2)) {
9570
- return `members/${part.alias}/${rel2.split(path10.sep).join("/")}`;
9937
+ const rel2 = path11.relative(part.directory, resolved);
9938
+ if (rel2 !== "" && !rel2.startsWith("..") && !path11.isAbsolute(rel2)) {
9939
+ return `members/${part.alias}/${rel2.split(path11.sep).join("/")}`;
9571
9940
  }
9572
9941
  }
9573
- return path10.relative(root, resolved).split(path10.sep).join("/");
9942
+ return path11.relative(root, resolved).split(path11.sep).join("/");
9574
9943
  }
9575
9944
  function familyNode(family, namespace) {
9576
9945
  return family.nodes.find((n) => n.namespace === namespace) ?? null;
@@ -9703,7 +10072,7 @@ var DesignDependencyRoleSchema = import_zod9.z.enum(DESIGN_DEPENDENCY_ROLES);
9703
10072
  var ExtSchema = import_zod9.z.record(import_zod9.z.unknown());
9704
10073
  var TypeExpressionSchema = import_zod9.z.lazy(
9705
10074
  () => import_zod9.z.object({
9706
- form: import_zod9.z.enum(["primitive", "named", "list", "set", "map", "optional", "union", "async", "applied"]),
10075
+ form: import_zod9.z.enum(["primitive", "named", "list", "set", "map", "optional", "union", "async", "applied", "result"]),
9707
10076
  name: import_zod9.z.string().optional(),
9708
10077
  args: import_zod9.z.array(TypeExpressionSchema)
9709
10078
  }).passthrough()
@@ -9940,7 +10309,7 @@ function resolveNarrativeLabels(methodName, steps) {
9940
10309
 
9941
10310
  // src/core/canvas.ts
9942
10311
  var fs7 = __toESM(require("fs"));
9943
- var path11 = __toESM(require("path"));
10312
+ var path12 = __toESM(require("path"));
9944
10313
 
9945
10314
  // src/core/canvas-layout.ts
9946
10315
  function computeLayout(model, collapsed) {
@@ -10711,9 +11080,9 @@ function embedJson(value) {
10711
11080
  }
10712
11081
  function loadCytoscapeLib() {
10713
11082
  const candidates = [
10714
- path11.resolve(__dirname, "..", "templates", "canvas", "cytoscape.min.js"),
11083
+ path12.resolve(__dirname, "..", "templates", "canvas", "cytoscape.min.js"),
10715
11084
  // src/core & dist/cli
10716
- path11.resolve(__dirname, "templates", "canvas", "cytoscape.min.js")
11085
+ path12.resolve(__dirname, "templates", "canvas", "cytoscape.min.js")
10717
11086
  // dist (library entry)
10718
11087
  ];
10719
11088
  for (const p of candidates) {
@@ -15096,17 +15465,17 @@ function safeFilenamePart(value) {
15096
15465
 
15097
15466
  // src/core/externals.ts
15098
15467
  var fs8 = __toESM(require("fs"));
15099
- var path12 = __toESM(require("path"));
15468
+ var path13 = __toESM(require("path"));
15100
15469
  function lockPath2(root) {
15101
- return path12.join(root, ".wai", "externals.lock.yaml");
15470
+ return path13.join(root, ".wai", "externals.lock.yaml");
15102
15471
  }
15103
15472
  function snapshotPath(root, alias) {
15104
- return path12.join(root, ".wai", "externals", `${alias}.yaml`);
15473
+ return path13.join(root, ".wai", "externals", `${alias}.yaml`);
15105
15474
  }
15106
15475
  function writeAtomically(file, text3) {
15107
- fs8.mkdirSync(path12.dirname(file), { recursive: true });
15476
+ fs8.mkdirSync(path13.dirname(file), { recursive: true });
15108
15477
  const tmp = `${file}.${process.pid}.tmp`;
15109
- fs8.writeFileSync(tmp, text3);
15478
+ fs8.writeFileSync(tmp, withLineEndings(file, text3));
15110
15479
  fs8.renameSync(tmp, file);
15111
15480
  }
15112
15481
  function sortedLock(lock) {
@@ -15149,7 +15518,7 @@ var externalsFileAdapter = {
15149
15518
  const file = snapshotPath(getProjectRoot(), alias);
15150
15519
  if (!fs8.existsSync(file)) return false;
15151
15520
  fs8.unlinkSync(file);
15152
- const dir = path12.dirname(file);
15521
+ const dir = path13.dirname(file);
15153
15522
  if (fs8.existsSync(dir) && fs8.readdirSync(dir).length === 0) fs8.rmdirSync(dir);
15154
15523
  return true;
15155
15524
  },
@@ -15233,7 +15602,7 @@ function unpin(alias) {
15233
15602
  }
15234
15603
 
15235
15604
  // src/core/openapi.ts
15236
- var yaml2 = __toESM(require("js-yaml"));
15605
+ var yaml3 = __toESM(require("js-yaml"));
15237
15606
  var PRIMITIVE_SCHEMAS = {
15238
15607
  string: { type: "string" },
15239
15608
  int: { type: "integer" },
@@ -15282,6 +15651,8 @@ function schemaOfExpression(expr, closureIds) {
15282
15651
  return { oneOf: expr.args.map((member) => schemaOfExpression(member, closureIds)) };
15283
15652
  case "async":
15284
15653
  return schemaOfExpression(expr.args[0], closureIds);
15654
+ case "result":
15655
+ return schemaOfExpression(expr.args[0], closureIds);
15285
15656
  }
15286
15657
  }
15287
15658
  function schemaFor(typeRef2, closureIds) {
@@ -15291,7 +15662,8 @@ function schemaFor(typeRef2, closureIds) {
15291
15662
  }
15292
15663
  function returnsNothing(returns) {
15293
15664
  const expr = expressionOf(returns);
15294
- const inner2 = expr?.form === "async" ? expr.args[0] : expr;
15665
+ const awaited = expr?.form === "async" ? expr.args[0] : expr;
15666
+ const inner2 = awaited?.form === "result" ? awaited.args[0] : awaited;
15295
15667
  return inner2?.form === "primitive" && inner2.name === "void";
15296
15668
  }
15297
15669
  function enumDescription(values) {
@@ -15515,7 +15887,7 @@ function toOpenApiSet(snapshot) {
15515
15887
  }
15516
15888
  function isOpenApiDocument(body) {
15517
15889
  try {
15518
- const parsed = yaml2.load(body);
15890
+ const parsed = yaml3.load(body);
15519
15891
  return !!parsed && typeof parsed === "object" && typeof parsed.openapi === "string";
15520
15892
  } catch {
15521
15893
  return false;
@@ -15626,7 +15998,7 @@ function authFromSecurityScheme(scheme) {
15626
15998
  function fromOpenApi(document, projectName) {
15627
15999
  let parsed;
15628
16000
  try {
15629
- parsed = yaml2.load(document);
16001
+ parsed = yaml3.load(document);
15630
16002
  } catch (e) {
15631
16003
  throw new Error(`Invalid surface document: not parseable as JSON/YAML (${e instanceof Error ? e.message : String(e)})`);
15632
16004
  }
@@ -15743,61 +16115,6 @@ function fromOpenApi(document, projectName) {
15743
16115
  });
15744
16116
  }
15745
16117
 
15746
- // src/config/defaults.ts
15747
- var os2 = __toESM(require("os"));
15748
- var path13 = __toESM(require("path"));
15749
- var DEFAULT_TARGET_DIRS = {
15750
- claude: ".claude/agents",
15751
- gemini: ".gemini/agents",
15752
- agy: ".gemini/agents",
15753
- cursor: ".cursor/rules",
15754
- copilot: ".github/prompts",
15755
- codex: ".codex/agents"
15756
- };
15757
- function defaultTargetConfig(type) {
15758
- return {
15759
- type,
15760
- outputDir: DEFAULT_TARGET_DIRS[type],
15761
- enabled: true
15762
- };
15763
- }
15764
- var WAIRON_VERSION = "5.1.1-dev.101";
15765
- var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
15766
- var ARCHITECT_AGENT_ID = "agent-architect";
15767
- var ARCHITECT_TEMPLATE_ID = "architect";
15768
- function globalTemplatesDir(projectOverride) {
15769
- if (process.env.WAIRON_TEMPLATES_DIR) {
15770
- return process.env.WAIRON_TEMPLATES_DIR;
15771
- }
15772
- if (projectOverride) {
15773
- return projectOverride;
15774
- }
15775
- return path13.join(os2.homedir(), ".wairon", "templates");
15776
- }
15777
- var BACKEND_COMMANDS = {
15778
- claude: "claude",
15779
- gemini: "gemini"
15780
- };
15781
- function backendCommand(backend) {
15782
- return BACKEND_COMMANDS[backend] ?? backend;
15783
- }
15784
- var SUPPORTED_ALIASES = ["wai"];
15785
- var SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
15786
- "node_modules",
15787
- "dist",
15788
- "build",
15789
- "out",
15790
- ".git",
15791
- ".cache",
15792
- "coverage",
15793
- "__pycache__",
15794
- ".venv",
15795
- "venv",
15796
- "target",
15797
- // Rust
15798
- "vendor"
15799
- ]);
15800
-
15801
16118
  // src/core/design-export.ts
15802
16119
  var NoSystemSpecError = class extends Error {
15803
16120
  constructor() {
@@ -16511,7 +16828,9 @@ function writeSurfaceFile(targetPath, snapshot, renderedSet) {
16511
16828
  const resolved = path14.resolve(targetPath);
16512
16829
  fs9.mkdirSync(path14.dirname(resolved), { recursive: true });
16513
16830
  if (!renderedSet || renderedSet.length === 0) {
16514
- writeYamlFile(resolved, snapshot);
16831
+ if (path14.extname(resolved).toLowerCase() === ".json") fs9.writeFileSync(resolved, `${JSON.stringify(snapshot, null, 2)}
16832
+ `);
16833
+ else writeYamlFile(resolved, snapshot);
16515
16834
  return [resolved];
16516
16835
  }
16517
16836
  if (renderedSet.length === 1) {
@@ -17292,7 +17611,7 @@ function approvalFinding(entry, config) {
17292
17611
  return finding(
17293
17612
  config,
17294
17613
  "MEMBER_DRIFTED",
17295
- `The member ${named(entry.key)} was approved under an earlier gate identity \u2014 the gate identity gained inputs in stage 5 (members' composition subjects, \`composition\`; code conformance moved beside the claim), which says nothing about its design. Re-lock it once at its own root (\`wairon lock\`).`,
17614
+ `The member ${named(entry.key)} was approved under an earlier gate identity \u2014 the gate identity gained inputs in this release (members' composition subjects, \`composition\`; code conformance moved beside the claim), which says nothing about its design. Re-lock it once at its own root (\`wairon lock\`).`,
17296
17615
  entry.key
17297
17616
  );
17298
17617
  }
@@ -18298,6 +18617,16 @@ function verifyIntegrity(selection, packDir, where) {
18298
18617
  message: `Pack "${wanted}" resolved to content that does not match the pinned integrity: expected ${selection.integrity}, found ${actual} (${where}). The pack was modified after it was pinned \u2014 reinstall or re-bundle the pinned version, or update the pin with \`wairon pack use ${selection.name} --pin\` if the change is intended.`
18299
18618
  };
18300
18619
  }
18620
+ function bundlesSingleFile(bundleDir, storePath) {
18621
+ try {
18622
+ if (!fs12.statSync(storePath).isFile()) return false;
18623
+ const entry = packDirEntry(bundleDir);
18624
+ if (!entry || fs12.readdirSync(bundleDir).length !== 1) return false;
18625
+ return fs12.readFileSync(entry).equals(fs12.readFileSync(storePath));
18626
+ } catch {
18627
+ return false;
18628
+ }
18629
+ }
18301
18630
  function detectBundleStoreDrift(selection, bundleDir) {
18302
18631
  const bundledVersion = path17.basename(bundleDir);
18303
18632
  const installed = resolveInstalledPack(selection.name, bundledVersion);
@@ -18305,6 +18634,7 @@ function detectBundleStoreDrift(selection, bundleDir) {
18305
18634
  const bundleDigest = computePackDigest(bundleDir);
18306
18635
  const storeDigest = computePackDigest(installed.path);
18307
18636
  if (bundleDigest === storeDigest) return void 0;
18637
+ if (bundlesSingleFile(bundleDir, installed.path)) return void 0;
18308
18638
  return {
18309
18639
  code: "PACK_STORE_DRIFT",
18310
18640
  severity: "warning",
@@ -18437,17 +18767,47 @@ function buildCodeIndex(model) {
18437
18767
  };
18438
18768
  const possibleOriginsOf = (site, from) => {
18439
18769
  const proven = originOf2(site, from);
18440
- if (!site.field && !site.constructed && !site.via && !site.returnedBy && !site.enclosingClass) return proven;
18770
+ if (!site.field && !site.constructed && !site.via && !site.returnedBy && !site.enclosingClass && !site.receiverPath) {
18771
+ return proven;
18772
+ }
18441
18773
  const resolved = scopeOf2(site, from);
18442
18774
  if (!resolved) return proven;
18443
18775
  const { path: scope, facts: f } = resolved;
18444
18776
  const out = new Set(proven);
18445
- const widenThrough = (name, specifier) => {
18446
- const landings = specifier !== void 0 ? landingFrom(scope, specifier) : declarationsAt(scope).has(name) ? republishedFrom(scope) : NO_ORIGIN;
18777
+ const widenThroughAt = (at, name, specifier) => {
18778
+ const landings = specifier !== void 0 ? landingFrom(at, specifier) : declarationsAt(at).has(name) ? republishedFrom(at) : NO_ORIGIN;
18447
18779
  for (const candidate of landings) out.add(candidate);
18448
18780
  };
18449
- const widenThroughType = (typeName) => widenThrough(typeName, typeBindingOf(f, typeName));
18781
+ const widenThrough = (name, specifier) => widenThroughAt(scope, name, specifier);
18782
+ const widenThroughType = (typeName, at = scope, atFacts = f) => {
18783
+ widenThroughAt(at, typeName, typeBindingOf(atFacts, typeName));
18784
+ for (const decl of typeDeclarationsOf(at, atFacts, typeName)) {
18785
+ const target = aliasTargetOf(decl.facts, decl.name);
18786
+ if (target !== void 0) widenThroughAt(decl.path, target, typeBindingOf(decl.facts, target));
18787
+ }
18788
+ };
18450
18789
  if (site.field) for (const typeName of fieldTypesOf(f, site.field)) widenThroughType(typeName);
18790
+ if (site.receiverPath && site.receiverPath.length >= 2) {
18791
+ const [root, ...rest] = site.receiverPath;
18792
+ const links = [...rest];
18793
+ let current2 = [];
18794
+ if (root === "this") {
18795
+ const field = links.shift();
18796
+ if (field !== void 0) current2 = fieldTypesOf(f, field).map((type) => ({ at: scope, facts: f, type }));
18797
+ } else {
18798
+ current2 = localTypesOf(f, root).map((type) => ({ at: scope, facts: f, type }));
18799
+ }
18800
+ for (const link of links) {
18801
+ const next = [];
18802
+ for (const c of current2) {
18803
+ for (const decl of typeDeclarationsOf(c.at, c.facts, c.type, true)) {
18804
+ for (const type of memberTypesOf(decl.facts, decl.name, link)) next.push({ at: decl.path, facts: decl.facts, type });
18805
+ }
18806
+ }
18807
+ current2 = next;
18808
+ }
18809
+ for (const c of current2) widenThroughType(c.type, c.at, c.facts);
18810
+ }
18451
18811
  if (site.constructed) widenThrough(site.constructed, importBindingOf(f, site.constructed)?.from);
18452
18812
  if (site.via) for (const typeName of localTypesOf(f, site.via)) widenThroughType(typeName);
18453
18813
  if (site.enclosingClass !== void 0 && holdsMemberBody(f, site.name, site.enclosingClass)) {
@@ -18469,6 +18829,34 @@ function buildCodeIndex(model) {
18469
18829
  }
18470
18830
  return out;
18471
18831
  };
18832
+ const typeDeclarationsOf = (at, atFacts, typeName, throughAlias2 = false) => {
18833
+ const found = [];
18834
+ const exactFacts = (p) => {
18835
+ const x = facts.get(p);
18836
+ return x?.status === "analyzed" && x.analysisGrade === "exact" ? x : void 0;
18837
+ };
18838
+ const direct = (where, whereFacts, name) => {
18839
+ if (whereFacts.declaredNames.includes(name) && typeBindingOf(whereFacts, name) === void 0) {
18840
+ return [{ path: where, facts: whereFacts, name }];
18841
+ }
18842
+ const specifier = typeBindingOf(whereFacts, name);
18843
+ if (specifier === void 0) return [];
18844
+ const published = importBindingOf(whereFacts, name)?.imported ?? name;
18845
+ const out = [];
18846
+ for (const candidate of landingFrom(where, specifier)) {
18847
+ const there = exactFacts(candidate);
18848
+ if (there && there.declaredNames.includes(published)) out.push({ path: candidate, facts: there, name: published });
18849
+ }
18850
+ return out;
18851
+ };
18852
+ for (const decl of direct(at, atFacts, typeName)) {
18853
+ found.push(decl);
18854
+ if (!throughAlias2) continue;
18855
+ const target = aliasTargetOf(decl.facts, decl.name);
18856
+ if (target !== void 0) found.push(...direct(decl.path, decl.facts, target));
18857
+ }
18858
+ return found;
18859
+ };
18472
18860
  const holdsMemberBody = (f, name, container) => f.status === "analyzed" && f.analysisGrade === "exact" && !!f.functionBodies && Object.prototype.hasOwnProperty.call(f.functionBodies, name) && f.functionBodies[name].some((b) => b.container === container);
18473
18861
  const declarationsAt = (path49) => namesAt(declarations, pathKey(path49), (f) => /* @__PURE__ */ new Set([...f.declaredNames, ...f.exportedNames]));
18474
18862
  const forwardsOf = (start, name, container) => {
@@ -18863,7 +19251,7 @@ var localIdsRule = {
18863
19251
  var projectIdentityRule = {
18864
19252
  name: "project-identity",
18865
19253
  judges: "design",
18866
- description: "The bound project is keyed on its id, so the id must be declared, well-formed, stable and its own: a project that declares none answers to its display name slugified until it writes one (PROJECT_ID_DEFAULTED), a project whose name yields no id or whose declared id breaks the grammar has nothing reliable to key on (PROJECT_ID_AMBIGUOUS), an id the rename migration moved from the one the lock approved is owed a re-lock and nothing more (PROJECT_ID_RENAMED: the approved id is one of the configuration's previousIds), and any other id that differs from the one the lock approved has moved under everything that keyed on it (PROJECT_ID_CHANGED). It judges the bound project's own identity only. The family half \u2014 a member with a defaulted id (named with its alias as the id to declare), a member with no id, and two members resolving to one id \u2014 is a family check since stage 4 (family_validator.checkMembers), which reads the family root's graph.",
19254
+ description: "The bound project is keyed on its id, so the id must be declared, well-formed, stable and its own: a project that declares none answers to its display name slugified until it writes one (PROJECT_ID_DEFAULTED), a project whose name yields no id or whose declared id breaks the grammar has nothing reliable to key on (PROJECT_ID_AMBIGUOUS), an id the rename migration moved from the one the lock approved is owed a re-lock and nothing more (PROJECT_ID_RENAMED: the approved id is one of the configuration's previousIds), and any other id that differs from the one the lock approved has moved under everything that keyed on it (PROJECT_ID_CHANGED). It judges the bound project's own identity only. The family half \u2014 a member with a defaulted id (named with its alias as the id to declare), a member with no id, and two members resolving to one id \u2014 is a family check (family_validator.checkMembers), which reads the family root's graph.",
18867
19255
  codes: [
18868
19256
  { code: "PROJECT_ID_AMBIGUOUS", defaultSeverity: "warning", summary: "Project has no usable id: its name yields no slug, or its declared id breaks the grammar" },
18869
19257
  { code: "PROJECT_ID_CHANGED", defaultSeverity: "error", summary: "Project id differs from the id the lock approved" },
@@ -18881,7 +19269,7 @@ function judgeOwnIdentity(ctx, identity2) {
18881
19269
  ctx.addIssue(
18882
19270
  "notice",
18883
19271
  "PROJECT_ID_DEFAULTED",
18884
- `Project "${identity2.name}" declares no id in .wai/project.yaml, so it answers to "${identity2.id}", derived from its display name \u2014 renaming the project would move it. No save writes it for you; declare it with \`id: ${identity2.id}\` (stage 3 requires an explicit id).`
19272
+ `Project "${identity2.name}" declares no id in .wai/project.yaml, so it answers to "${identity2.id}", derived from its display name \u2014 renaming the project would move it. No save writes it for you; declare it with \`id: ${identity2.id}\` (a project is keyed on an explicit id).`
18885
19273
  );
18886
19274
  }
18887
19275
  const ambiguous = problem("ambiguous");
@@ -18978,10 +19366,11 @@ var fieldTypeReferencesRule = {
18978
19366
  for (const ref of refs) {
18979
19367
  if (!ctx.isTypeResolved(ref, /* @__PURE__ */ new Set())) {
18980
19368
  const hint = ctx.importHint(ref);
19369
+ const spelled = ctx.aliasSpelling(ref);
18981
19370
  ctx.addIssue(
18982
19371
  "error",
18983
19372
  "UNDEFINED_TYPE_REFERENCE",
18984
- `Type "${t.id}" field "${field.name}" references undefined type "${ref}" in "${field.type}".${hint ? ` A declared dependency exports it without this project importing it \u2014 add \`${hint}\`.` : ""}`,
19373
+ `Type "${t.id}" field "${field.name}" references undefined type "${ref}" in "${field.type}".${hint ? ` A declared dependency exports it without this project importing it \u2014 add \`${hint}\`.` : ""}${spelled ? ` "${ref.split(".")[0]}" is another project here, not a subsystem of this one \u2014 write \`${spelled}\`.` : ""}`,
18985
19374
  t.id,
18986
19375
  isDraftCtx
18987
19376
  );
@@ -19016,10 +19405,11 @@ var signatureTypeReferencesRule = {
19016
19405
  for (const ref of refs) {
19017
19406
  if (!ctx.isTypeResolved(ref, allGenerics)) {
19018
19407
  const hint = ctx.importHint(ref);
19408
+ const spelled = ctx.aliasSpelling(ref);
19019
19409
  ctx.addIssue(
19020
19410
  "error",
19021
19411
  "UNDEFINED_TYPE_REFERENCE",
19022
- `Method "${m.name}" on interface "${intf.id}" references undefined type "${ref}" in signature.${hint ? ` A declared dependency exports it without this project importing it \u2014 add \`${hint}\`.` : ""}`,
19412
+ `Method "${m.name}" on interface "${intf.id}" references undefined type "${ref}" in signature.${hint ? ` A declared dependency exports it without this project importing it \u2014 add \`${hint}\`.` : ""}${spelled ? ` "${ref.split(".")[0]}" is another project here, not a subsystem of this one \u2014 write \`${spelled}\`.` : ""}`,
19023
19413
  intf.id,
19024
19414
  isDraftCtx
19025
19415
  );
@@ -19038,10 +19428,11 @@ var signatureTypeReferencesRule = {
19038
19428
  for (const { ref, by } of named2) {
19039
19429
  if (ctx.isTypeResolved(ref, typeGenerics)) continue;
19040
19430
  const hint = ctx.importHint(ref);
19431
+ const spelled = ctx.aliasSpelling(ref);
19041
19432
  ctx.addIssue(
19042
19433
  "error",
19043
19434
  "UNDEFINED_TYPE_REFERENCE",
19044
- `${by} references undefined type "${ref}" in signature.${hint ? ` A declared dependency exports it without this project importing it \u2014 add \`${hint}\`.` : ""}`,
19435
+ `${by} references undefined type "${ref}" in signature.${hint ? ` A declared dependency exports it without this project importing it \u2014 add \`${hint}\`.` : ""}${spelled ? ` "${ref.split(".")[0]}" is another project here, not a subsystem of this one \u2014 write \`${spelled}\`.` : ""}`,
19045
19436
  t.id,
19046
19437
  isDraftCtx
19047
19438
  );
@@ -19270,10 +19661,10 @@ function problemMessage(problem, specId) {
19270
19661
  var typeExpressionsRule = {
19271
19662
  name: "type-expressions",
19272
19663
  judges: "design",
19273
- description: "Reports what the scan's type canonicalisation recorded (ctx.typeSpellingFacts), one finding per position, located by spec and path: a stored text that does not parse (TYPE_EXPRESSION_INVALID); one that breaks a position rule \u2014 void or async out of place, a map key that is not string, int or an enum, a named scalar's holds that is not one primitive other than void and any, `T??` (TYPE_POSITION_INVALID); one using a form the grammar leaves out \u2014 an inline object shape, an inline function type, a string-literal union, a union mixing in a primitive or a collection, an intersection, a utility type or a tuple (TYPE_FORM_UNSUPPORTED); one naming `number` or a legacy builtin with no neutral meaning (TYPE_NOT_NEUTRAL); and one that is an alias of its canonical spelling (TYPE_SPELLING_STALE, which any save or doctor --fix repairs). Every message names the replacement: `number` asks \"int or float?\" (and says when the doctor repair proposes int), a function type names a signature type, a literal union an enum, an inline object or a mixed union a named value-object. The writer refuses all but the stale spelling at write time; these findings are how a tree that held them before the grammar existed is told, while it still loads and its consumers read the position as opaque any \u2014 which is why only what no real tree holds (a text that does not parse, a broken position rule) is an error. No finding when the run carries no facts (a candidate run).",
19664
+ description: "Reports what the scan's type canonicalisation recorded (ctx.typeSpellingFacts), one finding per position, located by spec and path: a stored text that does not parse (TYPE_EXPRESSION_INVALID); one that breaks a position rule \u2014 void, async or result out of place, a map key that is not string, int or an enum, a named scalar's holds that is not one primitive other than void and any, `T??` (TYPE_POSITION_INVALID); one using a form the grammar leaves out \u2014 an inline object shape, an inline function type, a string-literal union, a union mixing in a primitive or a collection, an intersection, a utility type or a tuple (TYPE_FORM_UNSUPPORTED); one naming `number` or a legacy builtin with no neutral meaning (TYPE_NOT_NEUTRAL); and one that is an alias of its canonical spelling (TYPE_SPELLING_STALE, which any save or doctor --fix repairs). Every message names the replacement: `number` asks \"int or float?\" (and says when the doctor repair proposes int), a function type names a signature type, a literal union an enum (which the doctor repair proposes), an inline object or a mixed union a named value-object. The writer refuses all but the stale spelling at write time; on load every one of them is a warning \u2014 these findings are how a tree that held them before the grammar existed is told, while it still loads, stays lockable with its debt in view, and its consumers read the position as opaque any, which never invents a shape. No finding when the run carries no facts (a candidate run).",
19274
19665
  codes: [
19275
- { code: "TYPE_EXPRESSION_INVALID", defaultSeverity: "error", summary: "A structured type position does not parse under the type grammar" },
19276
- { code: "TYPE_POSITION_INVALID", defaultSeverity: "error", summary: "A type position breaks a position rule: void or async out of place, a non-scalar map key, or T??" },
19666
+ { code: "TYPE_EXPRESSION_INVALID", defaultSeverity: "warning", summary: "A structured type position does not parse under the type grammar" },
19667
+ { code: "TYPE_POSITION_INVALID", defaultSeverity: "warning", summary: "A type position breaks a position rule: void, async or result out of place, a non-scalar map key, or T??" },
19277
19668
  { code: "TYPE_FORM_UNSUPPORTED", defaultSeverity: "warning", summary: "A type position uses a form the grammar leaves out \u2014 inline object, inline function type, string-literal union, union mixing in a primitive or collection, intersection, utility type or tuple \u2014 and the message names its named replacement" },
19278
19669
  { code: "TYPE_NOT_NEUTRAL", defaultSeverity: "warning", summary: "A type position names number (int or float?) or a legacy builtin with no neutral meaning, and the message names the replacement" },
19279
19670
  { code: "TYPE_SPELLING_STALE", defaultSeverity: "warning", summary: "A stored type position is an alias of its canonical spelling; any save or doctor --fix rewrites it" }
@@ -19283,9 +19674,8 @@ var typeExpressionsRule = {
19283
19674
  if (!facts) return;
19284
19675
  for (const problem of facts.problems) {
19285
19676
  if (problem.specId === void 0 || !ctx.isSpecInScope(problem.specId)) continue;
19286
- const severity = problem.code === "TYPE_EXPRESSION_INVALID" || problem.code === "TYPE_POSITION_INVALID" ? "error" : "warning";
19287
19677
  ctx.addIssue(
19288
- severity,
19678
+ "warning",
19289
19679
  problem.code,
19290
19680
  problemMessage(problem, problem.specId),
19291
19681
  problem.specId,
@@ -19619,7 +20009,7 @@ var publicSurfaceBoundContractRule = {
19619
20009
  var exportTablesRule = {
19620
20010
  name: "export-tables",
19621
20011
  judges: "design",
19622
- description: "Every level's export table must resolve the way a module resolver resolves `export \u2026 from`: one public name binds one target (EXPORT_ID_DUPLICATE when two wildcards \u2014 or two explicit entries \u2014 bind a name to different targets, or two public names share one nameKey, since a bare import could not tell them apart; the name is left out), no chain leads back to itself (EXPORT_CYCLE \u2014 an error for a named chain that never reaches a component or type, a warning for a wildcard cycle, which resolves to its union), every entry names a source and an item that exist, that the source exports and, for an own type, that the level owns \u2014 a subsystem-owned type at L1, a project-level type (no subsystem) for an L0 `{ typeDef }` without `from` \u2014 under a public name of [a-z0-9-_]+ (EXPORT_INVALID), every re-exported component can serve a boundary caller \u2014 a Portal, a gateway being a Portal variant, or an Observer for events (EXPORT_UNCONSUMABLE, at L0 and for L1 re-exports; an error since stage 4), and an L0 entry that re-exports a member's or an external's export never declares a wider audience than that export has (EXPORT_WIDENS_AUDIENCE): a re-export can narrow reach, never widen it. The rule judges the facts the export resolver recorded in the scan \u2014 for the bound root's table and every contained member's \u2014 and resolves nothing itself.",
20012
+ description: "Every level's export table must resolve the way a module resolver resolves `export \u2026 from`: one public name binds one target (EXPORT_ID_DUPLICATE when two wildcards \u2014 or two explicit entries \u2014 bind a name to different targets, or two public names share one nameKey, since a bare import could not tell them apart; the name is left out), no chain leads back to itself (EXPORT_CYCLE \u2014 an error for a named chain that never reaches a component or type, a warning for a wildcard cycle, which resolves to its union), every entry names a source and an item that exist, that the source exports and, for an own type, that the level owns \u2014 a subsystem-owned type at L1, a project-level type (no subsystem) for an L0 `{ typeDef }` without `from` \u2014 under a public name of [a-z0-9-_]+ (EXPORT_INVALID), every re-exported component can serve a boundary caller \u2014 a Portal, a gateway being a Portal variant, or an Observer for events (EXPORT_UNCONSUMABLE, at L0 and for L1 re-exports), and an L0 entry that re-exports a member's or an external's export never declares a wider audience than that export has (EXPORT_WIDENS_AUDIENCE): a re-export can narrow reach, never widen it. The rule judges the facts the export resolver recorded in the scan \u2014 for the bound root's table and every contained member's \u2014 and resolves nothing itself.",
19623
20013
  codes: [
19624
20014
  { code: "EXPORT_ID_DUPLICATE", defaultSeverity: "error", summary: "One public name bound to different targets by two wildcard re-exports or two explicit entries" },
19625
20015
  { code: "EXPORT_CYCLE", defaultSeverity: "error", summary: "A re-export chain leads back to itself (a warning for a wildcard cycle, which resolves to its union)" },
@@ -19781,7 +20171,7 @@ function whyNoRewrite(ref) {
19781
20171
  var referenceFormsRule = {
19782
20172
  name: "reference-forms",
19783
20173
  judges: "design",
19784
- description: "A cross-project reference is `alias::name` (or a bare name a `use` imports): the alias is one of the referring project's members or externals, the name a public name of that project's L0 table. The older forms still bind for one release and are reported (DEPRECATED_REFERENCE_FORM), each naming the spec, where in it, the form as written and the text to write instead (the scan's rewrite: a bare local id, or `alias::publicName`): a leading `::`, which escapes to the root of whatever checkout loads it; `super::`, which climbs the containing projects \u2014 a place in one family, not a name; a member path (`desktop::shell` written inside desktop, `waffler_core::transpiler::x` written inside transpiler), a first segment the referring project does not declare, read from the bound root; and a self-prefix \u2014 the referring project's own id as the first segment, with `::` or `.` (`registry.advisory-channel` inside registry), which stage 3's migration did not rewrite and stage 4's positional step does. Wairon's own writer emits none of them. A form with no rewrite (its target is out of reach) is reported with the reason. It reads the project graph's authored references, which the scan records before binding erases the form.",
20174
+ description: "A cross-project reference is `alias::name` (or a bare name a `use` imports): the alias is one of the referring project's members or externals, the name a public name of that project's L0 table. The older forms still bind for one release and are reported (DEPRECATED_REFERENCE_FORM), each naming the spec, where in it, the form as written and the text to write instead (the scan's rewrite: a bare local id, or `alias::publicName`): a leading `::`, which escapes to the root of whatever checkout loads it; `super::`, which climbs the containing projects \u2014 a place in one family, not a name; a member path (`desktop::shell` written inside desktop, `app_core::engine::x` written inside engine), a first segment the referring project does not declare, read from the bound root; and a self-prefix \u2014 the referring project's own id as the first segment, with `::` or `.` (`registry.advisory-channel` inside registry), which `wairon doctor --fix` rewrites to the bare local id. Wairon's own writer emits none of them. A form with no rewrite (its target is out of reach) is reported with the reason. It reads the project graph's authored references, which the scan records before binding erases the form.",
19785
20175
  codes: [
19786
20176
  { code: "DEPRECATED_REFERENCE_FORM", defaultSeverity: "notice", summary: "A reference written with a leading ::, super:: or a member path where a local id or alias::name belongs" }
19787
20177
  ],
@@ -19812,9 +20202,9 @@ function carriedFields(mount) {
19812
20202
  var memberDeclarationsRule = {
19813
20203
  name: "member-declarations",
19814
20204
  judges: "design",
19815
- description: "A project declares its members in project.yaml `members`, each by one source in one grammar. A deprecated declaration form loads for one release and is reported (DEPRECATED_MOUNT_FORM, notice): the L1 form \u2014 a subsystem carrying projectPath \u2014 naming the mount, its path, the `members` entry that replaces it, and every field it carries that `doctor --fix` will move into the member (or stop on); a mount whose alias `members` also declares is reported as ignored; and (stage 8) the long-form `path` key, naming the `source` (or shorthand) `doctor --fix` rewrites it to. Since stage 8 it also judges what each member of the bound project IS against what is declared or written about it, from the scan's problems at the bound root (a member's own members are that member's gate's): what a member is follows from its content \u2014 an id, an L0 or a lock make it a project, anything else a part \u2014 and every contradiction is ONE finding, MEMBER_KIND_MISMATCH (error), naming the alias and the contradiction: an asserted `as` the content contradicts; a part whose configuration declares project fields (members, externals, rules, extensions, composition, targets) without the id that would make it a project (add the id, or move them to the parent); a non-contained part with no PartOf, or one naming another project (opened alone it could not say what it belongs to); a contained member declaring PartOf; a `use` on a member whose content is a part (its names are already this project's own). A part whose files could not be read \u2014 its directory absent, or its git commit not cached and not fetchable (offline, refused, unknown) \u2014 is PART_UNAVAILABLE (error): the project's own subsystems are missing, so its verdict cannot be a pass. The member findings name no spec: members live in configuration. It reads the project graph's nodes, their legacy mounts, the bound root's declared members and its problems.",
20205
+ description: "A project declares its members in project.yaml `members`, each by one source in one grammar. A deprecated declaration form loads for one release and is reported (DEPRECATED_MOUNT_FORM, notice): the L1 form \u2014 a subsystem carrying projectPath \u2014 naming the mount, its path, the `members` entry that replaces it, and every field it carries that `doctor --fix` will move into the member (or stop on); a mount whose alias `members` also declares is reported as ignored; and the deprecated long-form `path` key, naming the `source` (or shorthand) `doctor --fix` rewrites it to. It also judges what each member of the bound project IS against what is declared or written about it, from the scan's problems at the bound root (a member's own members are that member's gate's): what a member is follows from its content \u2014 an id, an L0 or a lock make it a project, anything else a part \u2014 and every contradiction is ONE finding, MEMBER_KIND_MISMATCH (error), naming the alias and the contradiction: an asserted `as` the content contradicts; a part whose configuration declares project fields (members, externals, rules, extensions, composition, targets) without the id that would make it a project (add the id, or move them to the parent); a non-contained part with no PartOf, or one naming another project (opened alone it could not say what it belongs to); a contained member declaring PartOf; a `use` on a member whose content is a part (its names are already this project's own). A part whose files could not be read \u2014 its directory absent, or its git commit not cached and not fetchable (offline, refused, unknown) \u2014 is PART_UNAVAILABLE (error): the project's own subsystems are missing, so its verdict cannot be a pass. The member findings name no spec: members live in configuration. It reads the project graph's nodes, their legacy mounts, the bound root's declared members and its problems.",
19816
20206
  codes: [
19817
- { code: "DEPRECATED_MOUNT_FORM", defaultSeverity: "notice", summary: "A member declared in a deprecated form \u2014 an L1 subsystem carrying projectPath, or (stage 8) the long-form `path` key \u2014 which doctor --fix rewrites to a `members` entry with one `source`." },
20207
+ { code: "DEPRECATED_MOUNT_FORM", defaultSeverity: "notice", summary: "A member declared in a deprecated form \u2014 an L1 subsystem carrying projectPath, or the long-form `path` key \u2014 which doctor --fix rewrites to a `members` entry with one `source`." },
19818
20208
  { code: "MEMBER_KIND_MISMATCH", defaultSeverity: "error", summary: "What a member's content makes it (an id, L0 or lock: a project; else a part) contradicts what is declared or written about it: an asserted `as`, project fields in a part, a missing or wrong PartOf, a `use` on a part." },
19819
20209
  { code: "PART_UNAVAILABLE", defaultSeverity: "error", summary: "A part's files could not be read (absent directory, unpinned or unfetchable git commit): the project's own subsystems are missing." }
19820
20210
  ],
@@ -19852,7 +20242,7 @@ var memberDeclarationsRule = {
19852
20242
  ctx.addIssue(
19853
20243
  "notice",
19854
20244
  "DEPRECATED_MOUNT_FORM",
19855
- `This project declares the member "${member.alias}" with the long-form \`path\` key, which stage 8 replaced by the one location key \`source\`. \`doctor --fix\` rewrites it to ${rest ? `\`source: ${written}\` in its long form` : `the shorthand \`${member.alias}: ${written}\``} \u2014 its meaning unchanged.`
20245
+ `This project declares the member "${member.alias}" with the long-form \`path\` key, which the one location key replaced: \`source\`. \`doctor --fix\` rewrites it to ${rest ? `\`source: ${written}\` in its long form` : `the shorthand \`${member.alias}: ${written}\``} \u2014 its meaning unchanged.`
19856
20246
  );
19857
20247
  }
19858
20248
  for (const problem of family.problems) {
@@ -19889,7 +20279,7 @@ function declaration(producer) {
19889
20279
  var projectBoundariesRule = {
19890
20280
  name: "project-boundaries",
19891
20281
  judges: "design",
19892
- description: "The one judge of every cross-project reference the bound project writes (decision 9 \u2014 every project is a crate of its own, reached only through its L0 exports and only as a dependency it declared). It reads each of the project's own references from the project graph \u2014 `::` forms and bare names bound through `use` imports alike \u2014 takes its resolution from resolveCrossProject, decided before any severity from the project's own files alone, and reports every outcome but resolved once, carrying the resolution: forbidden is EXTERNAL_UNDECLARED (the reference reaches a project the referrer declares neither as a member nor as an external, or names a first segment that is none of its subsystems, aliases or foreign providers); missing is EXTERNAL_NOT_EXPORTED (the producer's table \u2014 a contained member's live one, or the external's pin \u2014 has no public name covering the referrer, or not the used method, or exports the type only inside a signature's closure); an ambiguous import is IMPORT_AMBIGUOUS (two imports supply one bare name: two explicit `use` names, or two `*` with no explicit one \u2014 name it `alias::name` or narrow a `use`); unavailable is EXTERNAL_CHECK_UNAVAILABLE (no pin, an unreadable pin, a member absent on disk, or a deprecated form naming no alias \u2014 never a pass). A bare name no import supplies is not this rule's: it is the unresolved-reference finding it always was, with rule_context.importHint appended. A trustedLinks entry naming a subsystem of another project (`alias::sub`) is TRUSTED_LINK_CROSSES_PROJECT: a sanctioned fast lane stays inside one project. Stage 4 makes the three stage-2 notices errors now that the owner's gate judges them exactly; a reference is never left to a parent's verdict.",
20282
+ description: "The one judge of every cross-project reference the bound project writes (every project is a crate of its own, reached only through its L0 exports and only as a dependency it declared). It reads each of the project's own references from the project graph \u2014 `::` forms and bare names bound through `use` imports alike \u2014 takes its resolution from resolveCrossProject, decided before any severity from the project's own files alone, and reports every outcome but resolved once, carrying the resolution: forbidden is EXTERNAL_UNDECLARED (the reference reaches a project the referrer declares neither as a member nor as an external, or names a first segment that is none of its subsystems, aliases or foreign providers); missing is EXTERNAL_NOT_EXPORTED (the producer's table \u2014 a contained member's live one, or the external's pin \u2014 has no public name covering the referrer, or not the used method, or exports the type only inside a signature's closure); an ambiguous import is IMPORT_AMBIGUOUS (two imports supply one bare name: two explicit `use` names, or two `*` with no explicit one \u2014 name it `alias::name` or narrow a `use`); unavailable is EXTERNAL_CHECK_UNAVAILABLE (no pin, an unreadable pin, a member absent on disk, or a deprecated form naming no alias \u2014 never a pass). A bare name no import supplies is not this rule's: it is the unresolved-reference finding it always was, with rule_context.importHint appended. A trustedLinks entry naming a subsystem of another project (`alias::sub`) is TRUSTED_LINK_CROSSES_PROJECT: a sanctioned fast lane stays inside one project. An undeclared, unexported or ambiguous reference is an error, since the owner's gate judges it exactly; a reference is never left to a parent's verdict.",
19893
20283
  codes: [
19894
20284
  { code: "EXTERNAL_UNDECLARED", defaultSeverity: "error", summary: "A reference reaches another project the referring project declares neither as a member nor as an external, or names a first segment that is none of its subsystems, aliases or foreign providers" },
19895
20285
  { code: "EXTERNAL_NOT_EXPORTED", defaultSeverity: "error", summary: "A reference reaches another project at anything but a public name, or a member of one, whose audience covers the referrer" },
@@ -22435,7 +22825,7 @@ var reproducibilityRule = {
22435
22825
  ctx.addIssue(
22436
22826
  "warning",
22437
22827
  "PACK_SOURCE_UNFETCHABLE",
22438
- `Pack selection "${selection.name}" records no source and is not bundled, so a fresh machine or CI runner cannot obtain it \u2014 \`wairon pack sync\` has nothing to fetch. Record a URL (\`wairon pack use ${selection.name} --source <url>\`) or commit a copy (\`wairon pack bundle ${selection.name}\`).`
22828
+ `Pack selection "${selection.name}" records no source and is not bundled, so a fresh machine or CI runner cannot obtain it \u2014 \`wairon pack sync\` has nothing to fetch. Commit a copy the repository carries (\`wairon pack bundle ${selection.name}\`, or \`wairon pack bundle --all\` for every selection), or record a URL \`wairon pack sync\` fetches it from (\`wairon pack use ${selection.name} --source <url>\`). A selection \`wairon doctor --fix\` recorded from a pack installed from a local path has no URL to record: bundle it.`
22439
22829
  );
22440
22830
  }
22441
22831
  }
@@ -23422,10 +23812,22 @@ var invariantBackingRule = {
23422
23812
  };
23423
23813
 
23424
23814
  // src/core/rules/wiring/invariant-references.ts
23815
+ function unknownInvariantMessage(where, ref, types, aliases) {
23816
+ const parts = splitInvariantRef(ref);
23817
+ const qualifier = parts ? parts.typeRef.lastIndexOf("::") : -1;
23818
+ if (parts && qualifier > 0) {
23819
+ const bare = `${parts.typeRef.slice(qualifier + 2)}.${parts.invariantId}`;
23820
+ if (resolveInvariantRef(bare, types, aliases)) {
23821
+ const prefix = parts.typeRef.slice(0, qualifier);
23822
+ return `${where} asserts invariant "${ref}", whose qualifier "${prefix}::" names no project this one reaches the type through \u2014 the invariant is declared here, so write "${bare}" (a project's own alias, written from inside it, is not a qualifier it reads as its own; \`wairon doctor --fix\` rewrites it in a migrated family).`;
23823
+ }
23824
+ }
23825
+ return `${where} asserts invariant "${ref}", but no entity declares it (expected "<type-id>.<invariant-id>" naming a declared entry in that entity's invariants).`;
23826
+ }
23425
23827
  var invariantReferencesRule = {
23426
23828
  name: "invariant-references",
23427
23829
  judges: "design",
23428
- description: "Every invariant a narrative step asserts (step.assertsInvariants) must resolve to a declared entity invariant: the reference splits at its last dot into a type reference and an invariant id, and some entity matching that type reference must declare that id. A reference nothing declares is an error \u2014 the step claims a property that exists nowhere.",
23830
+ description: "Every invariant a narrative step asserts (step.assertsInvariants) must resolve to a declared entity invariant: the reference splits at its last dot into a type reference and an invariant id, and some entity matching that type reference must declare that id. A reference nothing declares is an error \u2014 the step claims a property that exists nowhere. The message says which: when the type reference carries a qualifier (`alias::type`) and the bare reference does resolve, the cause is the qualifier \u2014 typically the project's own alias written from inside it \u2014 and the message names it and the bare reference to write instead, never claiming that no entity declares the invariant.",
23429
23831
  codes: [
23430
23832
  { code: "UNKNOWN_INVARIANT_REF", defaultSeverity: "error", summary: "A narrative step asserts an invariant that no entity declares" }
23431
23833
  ],
@@ -23440,7 +23842,7 @@ var invariantReferencesRule = {
23440
23842
  ctx.addIssue(
23441
23843
  "error",
23442
23844
  "UNKNOWN_INVARIANT_REF",
23443
- `Step ${step.stepNumber} of "${method.name}" in implementation "${impl.id}" asserts invariant "${ref}", but no entity declares it (expected "<type-id>.<invariant-id>" naming a declared entry in that entity's invariants).`,
23845
+ unknownInvariantMessage(`Step ${step.stepNumber} of "${method.name}" in implementation "${impl.id}"`, ref, ctx.types, aliases),
23444
23846
  impl.id,
23445
23847
  isDraftCtx
23446
23848
  );
@@ -23761,19 +24163,40 @@ function closedCallSites(code, file, fn, stopAt, container) {
23761
24163
  return out;
23762
24164
  }
23763
24165
  var anchorKey = (symbol, exportedVia) => exportedVia !== void 0 ? `${exportedVia}.${symbol}` : symbol;
24166
+ var bodyKey = (file, symbol, exportedVia) => `${file}|${anchorKey(symbol, exportedVia)}`;
23764
24167
  function resolveCallTarget(ctx, componentId, methodName) {
23765
24168
  const accepted = /* @__PURE__ */ new Set([methodName]);
23766
24169
  const files = /* @__PURE__ */ new Set();
24170
+ const bodies = /* @__PURE__ */ new Set();
24171
+ const anchors = [];
23767
24172
  for (const intf of ctx.interfacesByComponent.get(componentId) ?? []) {
23768
24173
  for (const impl of ctx.implementationsByContract.get(intf.id) ?? []) {
23769
24174
  const method = impl.methods.find((m) => m.name === methodName);
23770
24175
  if (!method && !intf.methods.some((m) => m.name === methodName)) continue;
23771
24176
  if (method?.symbol) accepted.add(method.symbol);
23772
24177
  const file = methodSourceFile(method ?? {}, impl.sourcePath);
23773
- if (file) files.add(pathKey(file));
24178
+ if (!file) continue;
24179
+ files.add(pathKey(file));
24180
+ bodies.add(bodyKey(pathKey(file), method?.symbol ?? methodName, method?.exportedVia));
24181
+ anchors.push({ file: pathKey(file), symbol: method?.symbol ?? methodName, exportedVia: method?.exportedVia });
23774
24182
  }
23775
24183
  }
23776
- return { accepted, files };
24184
+ return { accepted, files, bodies, anchors };
24185
+ }
24186
+ function targetForwards(code, target) {
24187
+ const out = /* @__PURE__ */ new Set();
24188
+ for (const anchor of target.anchors) {
24189
+ for (const p of code.forwardsOf(anchor.file, anchor.symbol, anchor.exportedVia)) out.add(`${p.file}#${p.name}`);
24190
+ if (anchor.exportedVia !== void 0) continue;
24191
+ const facts = code.factsAt(anchor.file);
24192
+ if (!facts || facts.status !== "analyzed" || facts.analysisGrade !== "exact") continue;
24193
+ const binding = importBindingOf(facts, anchor.symbol);
24194
+ if (!binding || binding.namespace) continue;
24195
+ const module2 = resolveImport(anchor.file, binding.from, code.paths, code.packages);
24196
+ if (!module2) continue;
24197
+ for (const p of code.forwardsOf(module2, binding.imported ?? anchor.symbol)) out.add(`${p.file}#${p.name}`);
24198
+ }
24199
+ return out;
23777
24200
  }
23778
24201
  function isTarget(pairs, target) {
23779
24202
  return pairs.some((p) => target.files.has(p.file) && target.accepted.has(p.name));
@@ -23814,6 +24237,7 @@ function describeSite(site) {
23814
24237
  if (site.field) return `this.${site.field}.${site.name}(\u2026)`;
23815
24238
  if (site.constructed) return `new ${site.constructed}(\u2026).${site.name}(\u2026)`;
23816
24239
  if (site.returnedBy) return `${site.returnedBy}(\u2026).${site.name}(\u2026)`;
24240
+ if (site.receiverPath) return `${site.receiverPath.join(".")}.${site.name}(\u2026)`;
23817
24241
  if (site.enclosingClass) return `this.${site.name}(\u2026)`;
23818
24242
  return `<receiver>.${site.name}(\u2026)`;
23819
24243
  }
@@ -23827,10 +24251,10 @@ function describeMiss(m) {
23827
24251
  var callConformanceRule = {
23828
24252
  name: "call-conformance",
23829
24253
  judges: "code",
23830
- description: "Code\u2194spec Level 3: the CLAIMED call \u2194 realized call relation, judged both ways against the method's own source file (its sourcePath, else the implementation's), at exact analysis grade only. A method claims a call two ways and they face one check: a narrative `call` step, and an entry of the declared `calls` a narrative-less method reaches its collaborators through. They assert the same thing, and the step number is no part of the claim \u2014 this check never reads order, so it is only how a finding points at one. Forward: every claim must be realized by a call whose callee RESOLVES TO one of the target method's own source files \u2014 the target's contract name or a per-method `symbol` override, closed transitively over the named helpers the realized function calls, and resolved against every file the call CAN have reached: a `this.<field>.<method>()` receiver followed through the field's DECLARED TYPE, a `new Class(...).<method>()` receiver through the module its CLASS NAME came from, a plain `<name>.<method>()` receiver through the type the file ANNOTATES that name with \u2014 a parameter's, or an annotated variable's, which is how a module that wires its collaborators as closures writes every one of its calls \u2014 a `fn(...).<method>()` receiver through the type fn RETURNS where the code settles it, and a `this.<method>()` receiver through the class whose own method it sits in; a package specifier naming a package of this repository resolves like a relative one, and an export alias (`export { a as b }`, an adapter object property `{ b: a }`) forwards by identity like a re-export. A matching call whose origin a pure model cannot resolve is reported apart as CALL_ORIGIN_UNRESOLVED, which NAMES the shape it could not follow and asks for nothing \u2014 a coverage hole in the reader, reported for the reason CONFORMANCE_DEGRADED is: a silently degraded gate is worse than a degraded one. A finding names a landing only from the PROVEN tier so that widening what a call reached can accept a claim but never accuse one, and a target that names no file of its own falls back to name membership. Converse: a call that resolves to a modelled method of ANOTHER component in the SAME file crosses a component boundary while looking local, so the method must claim it \u2014 in a step or in `calls` (UNDECLARED_COLOCATED_CALL) \u2014 judged on the proven tier alone, and a same-file private helper is no modelled method and is never reported; a function forwarding by identity to a modelled method written in another file IS that method, so what it calls is that method's subject. A method that claims nothing at all is judged in neither direction: what it leaves unsaid is UNUSED_*'s subject. Order, arguments and conditions stay unverified, dispatch steps (runtime-table routed) are no claim about a call site, a malformed declared reference is MALFORMED_DECLARED_CALL's finding and names no target here, the conformance dial (off) skips, and weaker analysis grades never guess.",
24254
+ description: "Code\u2194spec Level 3: the CLAIMED call \u2194 realized call relation, judged both ways against the method's own source file (its sourcePath, else the implementation's), at exact analysis grade only. A method claims a call two ways and they face one check: a narrative `call` step, and an entry of the declared `calls` a narrative-less method reaches its collaborators through. They assert the same thing, and the step number is no part of the claim \u2014 this check never reads order, so it is only how a finding points at one. Forward: every claim must be realized by a call whose callee RESOLVES TO one of the target method's own source files \u2014 the target's contract name or a per-method `symbol` override, closed transitively over the named helpers the realized function calls, and resolved against every file the call CAN have reached: a `this.<field>.<method>()` receiver followed through the field's DECLARED TYPE, a `new Class(...).<method>()` receiver through the module its CLASS NAME came from, a plain `<name>.<method>()` receiver through the type the file ANNOTATES that name with \u2014 a parameter's, or an annotated variable's, which is how a module that wires its collaborators as closures writes every one of its calls \u2014 a `fn(...).<method>()` receiver through the type fn RETURNS where the code settles it, and a `this.<method>()` receiver through the class whose own method it sits in, and a receiver CHAIN (`this.<field>.<property>.<method>()`, `<name>.<property>.<method>()`) link by link through each link's declared type \u2014 a declared type read through a member-preserving utility (Pick, Omit, Partial, Required, Readonly) and one alias hop; a package specifier naming a package of this repository resolves like a relative one, and an export alias (`export { a as b }`, an adapter object property `{ b: a }`) forwards by identity like a re-export. A matching call whose origin a pure model cannot resolve is reported apart as CALL_ORIGIN_UNRESOLVED, which NAMES the shape it could not follow and asks for nothing \u2014 a coverage hole in the reader, reported for the reason CONFORMANCE_DEGRADED is: a silently degraded gate is worse than a degraded one. A finding names a landing only from the PROVEN tier so that widening what a call reached can accept a claim but never accuse one, and a target that names no file of its own falls back to name membership. N:1 identity \u2014 a caller that IS the target \u2014 is judged on the body, the same anchor in the same file, never on a shared name: a Portal method forwarding to a same-named Orchestrator method elsewhere must make that call. Converse: a call that resolves to a modelled method of ANOTHER component in the SAME file crosses a component boundary while looking local, so the method must claim it \u2014 in a step or in `calls` (UNDECLARED_COLOCATED_CALL) \u2014 judged on the proven tier alone, and a same-file private helper is no modelled method and is never reported; a function forwarding by identity to a modelled method written in another file IS that method, so what it calls is that method's subject. A method that claims nothing at all is judged in neither direction: what it leaves unsaid is UNUSED_*'s subject. Order, arguments and conditions stay unverified, dispatch steps (runtime-table routed) are no claim about a call site, a malformed declared reference is MALFORMED_DECLARED_CALL's finding and names no target here, the conformance dial (off) skips, and weaker analysis grades never guess.",
23831
24255
  codes: [
23832
24256
  { code: "CALL_STEP_UNREALIZED", defaultSeverity: "warning", summary: "Narrative call step or declared call realized by no call that resolves to the target method's own source file \u2014 the call is absent, or it lands in another module", carryable: true },
23833
- { code: "CALL_ORIGIN_UNRESOLVED", defaultSeverity: "warning", summary: "Narrative call step or declared call whose target name IS called, but only from call sites written in a shape this analysis cannot resolve to a file \u2014 the claim was not checked, and is neither proven realized nor accused", carryable: true },
24257
+ { code: "CALL_ORIGIN_UNRESOLVED", defaultSeverity: "warning", summary: "Narrative call step or declared call whose target name IS called, but only from call sites written in a shape this analysis cannot resolve to a file \u2014 the claim was not checked, and is neither proven realized nor accused. What resolves is a name bound to a module of THIS project: a bare or namespaced call through such an import binding, a this.<field> receiver whose declared type names one, a receiver chain (this.<field>.<property>, <name>.<property>) each of whose links the code declares a type for, a new Class(\u2026) receiver whose class does, a receiver name the file annotates with such a type, a fn(\u2026) receiver whose return the code settles, and this inside a class's own method \u2014 a declared type read through Pick/Omit/Partial/Required/Readonly and one alias hop. What does not: a third-party package import, a value the module assembled, and a receiver the file declares nothing for", carryable: true },
23834
24258
  { code: "UNDECLARED_COLOCATED_CALL", defaultSeverity: "warning", summary: "The realized function calls a modelled method of another component living in the same source file, and neither a narrative step nor a declared call names it", carryable: true }
23835
24259
  ],
23836
24260
  check(ctx) {
@@ -23858,9 +24282,9 @@ var callConformanceRule = {
23858
24282
  const here = pathKey(file);
23859
24283
  const fnSymbol = implMethod.symbol ?? implMethod.name;
23860
24284
  const colocated = modelledAt.get(here) ?? { byKey: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() };
23861
- const ownerOf4 = (site, bodyKey) => {
24285
+ const ownerOf4 = (site, bodyKey2) => {
23862
24286
  const inHere = pathKey(site.from ?? here) === here;
23863
- if (inHere && bodyKey !== void 0 && !bodyKey.startsWith("*")) return colocated.byKey.get(bodyKey);
24287
+ if (inHere && bodyKey2 !== void 0 && !bodyKey2.startsWith("*")) return colocated.byKey.get(bodyKey2);
23864
24288
  return colocated.byName.get(site.name);
23865
24289
  };
23866
24290
  const sites = closedCallSites(code, file, fnSymbol, void 0, implMethod.exportedVia);
@@ -23869,7 +24293,9 @@ var callConformanceRule = {
23869
24293
  for (const claim of claimsOf(implMethod)) {
23870
24294
  if (!ctx.componentMap.has(claim.component)) continue;
23871
24295
  const target = resolveCallTarget(ctx, claim.component, claim.method);
23872
- if (target.accepted.has(fnSymbol)) continue;
24296
+ if (target.bodies.has(bodyKey(here, fnSymbol, implMethod.exportedVia))) continue;
24297
+ const sharedBody = targetForwards(code, target);
24298
+ if (code.forwardsOf(file, fnSymbol, implMethod.exportedVia).some((p) => p.file !== here && sharedBody.has(`${p.file}#${p.name}`))) continue;
23873
24299
  if (isTarget(code.forwardsOf(file, fnSymbol, implMethod.exportedVia), target)) continue;
23874
24300
  const matching = sites.filter((s) => target.accepted.has(s.name));
23875
24301
  const ref = `${claim.component}.${claim.method}`;
@@ -23913,7 +24339,10 @@ var callConformanceRule = {
23913
24339
  ctx.addIssue(
23914
24340
  "warning",
23915
24341
  "CALL_ORIGIN_UNRESOLVED",
23916
- `Method "${implMethod.name}" in implementation "${impl.id}": ${unresolved.length} ${claimNoun(implMethod)} ARE called by name inside the function "${fnSymbol}" in "${file}", but every site calling them is written in a shape this analysis cannot resolve to a file \u2014 ${unresolved.map(describeMiss).join("; ")}. What resolves is a name bound to a module of THIS project: a bare or namespaced call through such an import binding (a package of this repository included), a \`this.<field>\` receiver whose declared type names one, a \`new Class(\u2026)\` receiver whose class does, a receiver NAME the file annotates with a type that does, a \`fn(\u2026)\` receiver whose function's return the code settles on a type with that method's body, and \`this\` inside a class's own method that has it. What does not: an import from a third-party PACKAGE, a receiver holding a value the module assembled, a receiver this model records no name for, and one the file annotates with nothing. This reports what was not checked, not what is wrong \u2014 the claim is neither proven realized nor accused of being missing, and no working call is asked to be rewritten to suit the reader.`,
24342
+ // The sites, and what the answer means — once. Which receiver
24343
+ // forms resolve is the code's summary and the rule's description
24344
+ // (`wairon rules list`), not prose every finding repeats.
24345
+ `Method "${implMethod.name}" in implementation "${impl.id}": ${unresolved.length} ${claimNoun(implMethod)} not checked \u2014 called by name inside "${fnSymbol}" in "${file}", but only through receivers this analysis cannot resolve to a file: ${unresolved.map(describeMiss).join("; ")}. Neither proven realized nor accused; the receiver forms that do resolve are listed under CALL_ORIGIN_UNRESOLVED in \`wairon rules list\`.`,
23917
24346
  impl.id,
23918
24347
  entry.draftContext,
23919
24348
  void 0,
@@ -23930,8 +24359,8 @@ var callConformanceRule = {
23930
24359
  }
23931
24360
  const crossings = /* @__PURE__ */ new Map();
23932
24361
  const reached = /* @__PURE__ */ new Map();
23933
- const stopAtModelled = (site, bodyKey) => {
23934
- const owner = ownerOf4(site, bodyKey);
24362
+ const stopAtModelled = (site, bodyKey2) => {
24363
+ const owner = ownerOf4(site, bodyKey2);
23935
24364
  reached.set(site, owner);
23936
24365
  return owner !== void 0;
23937
24366
  };
@@ -24020,7 +24449,7 @@ var hiddenStateRule = {
24020
24449
  var dependencyConformanceRule = {
24021
24450
  name: "dependency-conformance",
24022
24451
  judges: "code",
24023
- description: "Code\u2194spec Level 2: runtime import edges between component-mapped source files (a component maps to every file its implementations and their methods name) must be justified by declared relations \u2014 a direct dependsOn/owns pair, a shared component, membership in a depended-on pattern, or (across subsystems) a declared edge to the target subsystem's published surface (UNDECLARED_DEPENDENCY). Conversely, a declared dependsOn/owns edge between components realized in different files should be visible as an import between any file of the source and any file of the target (UNREALIZED_DEPENDENCY \u2014 DI indirection can defeat this, hence warning). Only exact-grade analyzed files participate; type-only imports are exempt; chained subprojects validate standalone.",
24452
+ description: "Code\u2194spec Level 2: runtime import edges between component-mapped source files (a component maps to every file its implementations and their methods name) must be justified by declared relations \u2014 a direct dependsOn/owns pair, a shared component, membership in a depended-on pattern, or (across subsystems) a declared edge to the target subsystem's published surface (UNDECLARED_DEPENDENCY). Conversely, a declared dependsOn/owns edge between components realized in different files should be visible as an import between any file of the source and any file of the target (UNREALIZED_DEPENDENCY \u2014 DI indirection can defeat this, hence warning). A type-only import never accuses, but it does realize a declared edge: a constructor-injected collaborator is reached through its type, and `import type` is what dependency injection writes down. Only exact-grade analyzed files participate; chained subprojects validate standalone.",
24024
24453
  codes: [
24025
24454
  { code: "UNDECLARED_DEPENDENCY", defaultSeverity: "warning", summary: "A runtime import between component-mapped files has no declared dependsOn/owns (or published-surface) justification" },
24026
24455
  { code: "UNREALIZED_DEPENDENCY", defaultSeverity: "warning", summary: "A declared dependsOn/owns edge between components in different files is realized by no import between any of their files" }
@@ -24088,6 +24517,23 @@ var dependencyConformanceRule = {
24088
24517
  );
24089
24518
  }
24090
24519
  }
24520
+ const typeTraces = /* @__PURE__ */ new Map();
24521
+ const typeTracesOf = (path49) => {
24522
+ let out = typeTraces.get(path49);
24523
+ if (out) return out;
24524
+ out = /* @__PURE__ */ new Set();
24525
+ const facts = code.factsAt(path49);
24526
+ for (const specifier of new Set(Object.values(facts?.typeOnlyBindings ?? {}))) {
24527
+ const to = resolveImport(path49, specifier, mappedPaths, code.packages);
24528
+ if (to && to !== path49) out.add(to);
24529
+ }
24530
+ typeTraces.set(path49, out);
24531
+ return out;
24532
+ };
24533
+ const typeConnects = (from, to) => from.some((f) => {
24534
+ const traces = typeTracesOf(f);
24535
+ return to.some((t) => traces.has(t));
24536
+ });
24091
24537
  for (const component of ctx.components) {
24092
24538
  const fromFiles = filesOf(component.id);
24093
24539
  if (fromFiles.length === 0) continue;
@@ -24101,6 +24547,7 @@ var dependencyConformanceRule = {
24101
24547
  if (toFiles.length === 0) continue;
24102
24548
  if (fromFiles.some((f) => toFiles.includes(f))) continue;
24103
24549
  if (graph2.connects(fromFiles, toFiles, isMountingDeclarer)) continue;
24550
+ if (typeConnects(fromFiles, toFiles)) continue;
24104
24551
  const impls = mappedImplsOf(component.id);
24105
24552
  const declaredDependsOn = component.dependsOn.includes(targetId);
24106
24553
  const declaredOwns = (component.owns ?? []).includes(targetId);
@@ -24109,7 +24556,7 @@ var dependencyConformanceRule = {
24109
24556
  ctx.addIssue(
24110
24557
  "warning",
24111
24558
  "UNREALIZED_DEPENDENCY",
24112
- `Component "${component.id}" declares ${relation} "${targetId}", but no runtime import connects their source files (${fromFiles.join(", ")} \u219B ${crossSubsystem ? `subsystem ${target.subsystem}` : filesOf(targetId).join(", ")}) \u2014 either the collaboration is wired indirectly (DI) or the declared edge is stale.`,
24559
+ `Component "${component.id}" declares ${relation} "${targetId}", but no import \u2014 runtime or type-only \u2014 connects their source files (${fromFiles.join(", ")} \u219B ${crossSubsystem ? `subsystem ${target.subsystem}` : filesOf(targetId).join(", ")}) \u2014 either the collaboration is wired indirectly (DI) or the declared edge is stale.`,
24113
24560
  impls[0]?.id ?? component.id,
24114
24561
  draft,
24115
24562
  void 0,
@@ -24182,11 +24629,13 @@ var portalImportsRule = {
24182
24629
  var integrationSimDeclarationRule = {
24183
24630
  name: "integration-sim-declaration",
24184
24631
  judges: "code",
24185
- description: "Once a subsystem has adopted integration sims (its first declared L4 simPath), every OTHER complete, non-leaf implementation of that subsystem must declare one too: its unit suite proves contract shape against mocks, not that the wired components run together. A leaf component's unit suite IS its sim, and a subsystem that has adopted nothing is never flooded.",
24632
+ description: "Once a subsystem has adopted integration sims (its first declared L4 simPath), every OTHER complete, non-leaf implementation of that subsystem must declare one too: its unit suite proves contract shape against mocks, not that the wired components run together. A leaf component's unit suite IS its sim, a subsystem that has adopted nothing is never flooded, and an implementation with no code yet \u2014 no source file named, or only files that are missing \u2014 has nothing to wire and is not asked for a harness.",
24186
24633
  codes: [
24187
24634
  { code: "MISSING_INTEGRATION_SIM", defaultSeverity: "warning", summary: "Complete non-leaf implementation in a sim-adopting subsystem declares no simPath" }
24188
24635
  ],
24189
24636
  check(ctx) {
24637
+ const code = ctx.codeIndex();
24638
+ const hasCode = (files) => files.some((file) => code.factsAt(pathKey(file))?.status !== "missing");
24190
24639
  const adopted = /* @__PURE__ */ new Set();
24191
24640
  for (const impl of ctx.implementations) {
24192
24641
  if (!impl.simPath) continue;
@@ -24205,6 +24654,7 @@ var integrationSimDeclarationRule = {
24205
24654
  if (ctx.isImplementationDraft(impl)) continue;
24206
24655
  const deps = [.../* @__PURE__ */ new Set([...comp.dependsOn, ...comp.owns])].filter((d) => ctx.componentMap.has(d));
24207
24656
  if (deps.length === 0) continue;
24657
+ if (!hasCode(implementationSourceFiles(impl))) continue;
24208
24658
  ctx.addIssue(
24209
24659
  "warning",
24210
24660
  "MISSING_INTEGRATION_SIM",
@@ -25025,7 +25475,7 @@ var NOTHING_PROMISED = /* @__PURE__ */ new Set();
25025
25475
  var exportConformanceRule = {
25026
25476
  name: "export-conformance",
25027
25477
  judges: "code",
25028
- description: "Code-to-contract for the SURFACE: does this file publish anything the components it realizes never promised, that another component then takes? The rest of the conformance set reads contract-to-code \u2014 it asks whether the code holds what a spec claims \u2014 so a file could export whatever it liked under a component's name and nothing looked. That is one half of the same hole as a contract promising a parameter it never passes. A method's `exportedVia` names the export a consumer imports to REACH it \u2014 the value that composes it, which `symbol` cannot name because `symbol` names the function inside \u2014 and a listener's mount names the router entry it calls to hand a portal its requests; both are declared publication, and a declared handle the file does not actually export is itself a finding, so naming one can never become a free-text suppression. Both questions are asked only of a file read at exact grade, and a taker counts only when its own import specifier resolves to the file it is accused of taking from: below that nothing separates an export from a mention, or a shared word from a shared module.",
25478
+ description: "Code-to-contract for the SURFACE: does this file publish anything the components it realizes never promised, that another component then takes? The rest of the conformance set reads contract-to-code \u2014 it asks whether the code holds what a spec claims \u2014 so a file could export whatever it liked under a component's name and nothing looked. That is one half of the same hole as a contract promising a parameter it never passes. A method's `exportedVia` names the export a consumer imports to REACH it \u2014 the value that composes it, which `symbol` cannot name because `symbol` names the function inside \u2014 and a listener's mount names the router entry it calls to hand a portal its requests; both are declared publication, and a declared handle the file does not actually export is itself a finding, so naming one can never become a free-text suppression. Both questions are asked only of a file read at exact grade, and a taker counts only when its own import specifier resolves to the file it is accused of taking from: below that nothing separates an export from a mention, or a shared word from a shared module. A taker inside the owns fence is no crossing: a pattern constructs the blocks it owns, so the owner's file, or another member of the same pattern, importing a member's class is the owns relation made real.",
25029
25479
  codes: [
25030
25480
  {
25031
25481
  code: "UNDECLARED_EXPORT",
@@ -25047,6 +25497,7 @@ var exportConformanceRule = {
25047
25497
  check(ctx) {
25048
25498
  const code = ctx.codeIndex();
25049
25499
  const realization = ctx.realizationIndex();
25500
+ const ownership = ctx.ownershipIndex();
25050
25501
  const promised = /* @__PURE__ */ new Map();
25051
25502
  const promise = (file, name) => {
25052
25503
  if (!name) return;
@@ -25173,13 +25624,20 @@ var exportConformanceRule = {
25173
25624
  const declared = promised.get(file) ?? NOTHING_PROMISED;
25174
25625
  const taken = takenFrom.get(file);
25175
25626
  const owners = new Set(realization.componentsAt(file).map((c) => c.id));
25627
+ const fence = new Set(owners);
25628
+ for (const id of owners) {
25629
+ const pattern = ownership.ownerOf(id);
25630
+ if (pattern === void 0) continue;
25631
+ fence.add(pattern);
25632
+ for (const member of ownership.ownedMembers) if (ownership.ownerOf(member) === pattern) fence.add(member);
25633
+ }
25176
25634
  const crossed = [];
25177
25635
  for (const name of [...new Set(facts.exportedNames)].sort()) {
25178
25636
  if (declared.has(name)) continue;
25179
25637
  const takers = taken?.get(name);
25180
25638
  if (!takers) continue;
25181
25639
  const crossing = [...takers].some(
25182
- (taker) => taker !== file && !realization.componentsAt(taker).some((c) => owners.has(c.id))
25640
+ (taker) => taker !== file && !realization.componentsAt(taker).some((c) => fence.has(c.id))
25183
25641
  );
25184
25642
  if (crossing) crossed.push(name);
25185
25643
  }
@@ -26881,6 +27339,8 @@ function walkExact(ts, sourceText, fileName) {
26881
27339
  const typeOnlyBindings = /* @__PURE__ */ new Map();
26882
27340
  const fieldTypes = /* @__PURE__ */ new Map();
26883
27341
  const localTypes = /* @__PURE__ */ new Map();
27342
+ const memberTypes = /* @__PURE__ */ new Map();
27343
+ const aliasTargets = /* @__PURE__ */ new Map();
26884
27344
  const typeShapes = /* @__PURE__ */ new Map();
26885
27345
  const functionParams = /* @__PURE__ */ new Map();
26886
27346
  const functionRoutes = /* @__PURE__ */ new Map();
@@ -26890,7 +27350,7 @@ function walkExact(ts, sourceText, fileName) {
26890
27350
  const mutableBindings = /* @__PURE__ */ new Set();
26891
27351
  const schemaConstants = /* @__PURE__ */ new Map();
26892
27352
  const derivedAliases = [];
26893
- const receiverKey = (site) => site.via ?? (site.field ? `this.${site.field}` : site.constructed ? `new ${site.constructed}` : site.returnedBy ? `${site.returnedBy}()` : site.enclosingClass ? `this:${site.enclosingClass}` : "");
27353
+ const receiverKey = (site) => site.via ?? (site.field ? `this.${site.field}` : site.constructed ? `new ${site.constructed}` : site.returnedBy ? `${site.returnedBy}()` : site.enclosingClass ? `this:${site.enclosingClass}` : site.receiverPath ? site.receiverPath.join(".") : "");
26894
27354
  const siteKey = (site) => `${site.member ? "m" : "b"}:${receiverKey(site)}:${site.name}`;
26895
27355
  const addBindingNames = (name) => {
26896
27356
  if (ts.isIdentifier(name)) declared.add(name.text);
@@ -26905,7 +27365,34 @@ function walkExact(ts, sourceText, fileName) {
26905
27365
  if (ts.isPrivateIdentifier(name)) return name.text;
26906
27366
  return void 0;
26907
27367
  };
26908
- const typeReferenceName = (type) => type && ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) ? type.typeName.text : void 0;
27368
+ const MEMBER_PRESERVING = /* @__PURE__ */ new Set(["Pick", "Omit", "Partial", "Required", "Readonly"]);
27369
+ const typeReferenceName = (type) => {
27370
+ let at = type;
27371
+ for (let depth = 0; at && depth < 4; depth++) {
27372
+ if (!ts.isTypeReferenceNode(at) || !ts.isIdentifier(at.typeName)) return void 0;
27373
+ const name = at.typeName.text;
27374
+ if (!MEMBER_PRESERVING.has(name) || !at.typeArguments?.length) return name;
27375
+ at = at.typeArguments[0];
27376
+ }
27377
+ return void 0;
27378
+ };
27379
+ const recordMemberType = (shape, name, type) => {
27380
+ if (!shape) return;
27381
+ const typeName = typeReferenceName(type);
27382
+ if (!typeName) return;
27383
+ const property = propertyNameText(name);
27384
+ if (!property) return;
27385
+ const key = `${shape}.${property}`;
27386
+ const named2 = memberTypes.get(key) ?? /* @__PURE__ */ new Set();
27387
+ named2.add(typeName);
27388
+ memberTypes.set(key, named2);
27389
+ };
27390
+ const shapeNameOf = (holder) => {
27391
+ if (!holder) return void 0;
27392
+ if ((ts.isClassDeclaration(holder) || ts.isInterfaceDeclaration(holder)) && holder.name) return holder.name.text;
27393
+ if (ts.isTypeLiteralNode(holder) && holder.parent && ts.isTypeAliasDeclaration(holder.parent)) return holder.parent.name.text;
27394
+ return void 0;
27395
+ };
26909
27396
  const isParameterProperty = (node) => !!node.modifiers?.some((m) => m.kind === ts.SyntaxKind.PublicKeyword || m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword || m.kind === ts.SyntaxKind.ReadonlyKeyword);
26910
27397
  const recordFieldType = (name, type) => {
26911
27398
  const typeName = typeReferenceName(type);
@@ -27102,7 +27589,18 @@ function walkExact(ts, sourceText, fileName) {
27102
27589
  } else if (receiver.kind === ts.SyntaxKind.ThisKeyword) {
27103
27590
  const owner = enclosingClassOf(node);
27104
27591
  addSite(owner !== void 0 ? { name, member: true, enclosingClass: owner } : { name, member: true });
27105
- } else addSite({ name, member: true });
27592
+ } else {
27593
+ const chain = [];
27594
+ let at = receiver;
27595
+ while (ts.isPropertyAccessExpression(at) && !at.questionDotToken && ts.isIdentifier(at.name)) {
27596
+ chain.unshift(at.name.text);
27597
+ at = at.expression;
27598
+ }
27599
+ if (ts.isIdentifier(at)) chain.unshift(at.text);
27600
+ else if (at.kind === ts.SyntaxKind.ThisKeyword) chain.unshift("this");
27601
+ else chain.length = 0;
27602
+ addSite(chain.length >= 2 ? { name, member: true, receiverPath: chain } : { name, member: true });
27603
+ }
27106
27604
  }
27107
27605
  }
27108
27606
  ts.forEachChild(node, count);
@@ -27237,9 +27735,14 @@ function walkExact(ts, sourceText, fileName) {
27237
27735
  functionRoutes.set(fnName, known);
27238
27736
  }
27239
27737
  }
27240
- if (ts.isPropertyDeclaration(node)) recordFieldType(node.name, node.type);
27241
- else if (ts.isParameter(node) && node.parent && ts.isConstructorDeclaration(node.parent) && isParameterProperty(node)) {
27738
+ if (ts.isPropertyDeclaration(node)) {
27739
+ recordFieldType(node.name, node.type);
27740
+ recordMemberType(shapeNameOf(node.parent), node.name, node.type);
27741
+ } else if (ts.isParameter(node) && node.parent && ts.isConstructorDeclaration(node.parent) && isParameterProperty(node)) {
27242
27742
  recordFieldType(node.name, node.type);
27743
+ recordMemberType(shapeNameOf(node.parent.parent), node.name, node.type);
27744
+ } else if (ts.isPropertySignature(node)) {
27745
+ recordMemberType(shapeNameOf(node.parent), node.name, node.type);
27243
27746
  }
27244
27747
  if (ts.isParameter(node) || ts.isVariableDeclaration(node)) recordLocalType(node.name, node.type);
27245
27748
  if (ts.isInterfaceDeclaration(node)) {
@@ -27253,6 +27756,10 @@ function walkExact(ts, sourceText, fileName) {
27253
27756
  if (ts.isTypeLiteralNode(node.type)) recordDeclaredShape(node.name.text, node.type.members, false);
27254
27757
  else {
27255
27758
  if (!aliasTypes.has(node.name.text)) aliasTypes.set(node.name.text, node.type.getText(sf));
27759
+ const named2 = typeReferenceName(node.type);
27760
+ if (named2 !== void 0 && named2 !== node.name.text && !aliasTargets.has(node.name.text)) {
27761
+ aliasTargets.set(node.name.text, named2);
27762
+ }
27256
27763
  const constant = inferredSchemaConstant(node.type);
27257
27764
  if (constant) derivedAliases.push({ name: node.name.text, constant });
27258
27765
  const literals = stringLiteralUnion(node.type);
@@ -27413,7 +27920,7 @@ function walkExact(ts, sourceText, fileName) {
27413
27920
  }
27414
27921
  const reexportOnly = sf.statements.length > 0 && sf.statements.every((st) => ts.isExportDeclaration(st) && !!st.moduleSpecifier);
27415
27922
  for (const alias of propertyAliases) if (exported.has(alias.container)) exportAliases.push(alias);
27416
- return { declared, anchors, exported, imports, reexports, starExports, namedReexports, exportAliases, returnTypes, complexity, calls, bodies, importBindings, typeOnlyBindings, fieldTypes, localTypes, typeShapes, functionParams, functionRoutes, asyncFunctions, enumValues, aliasTypes, mutableBindings, reexportOnly };
27923
+ return { declared, anchors, exported, imports, reexports, starExports, namedReexports, exportAliases, returnTypes, complexity, calls, bodies, importBindings, typeOnlyBindings, fieldTypes, localTypes, memberTypes, aliasTargets, typeShapes, functionParams, functionRoutes, asyncFunctions, enumValues, aliasTypes, mutableBindings, reexportOnly };
27417
27924
  }
27418
27925
  var NO_PACKAGES = /* @__PURE__ */ new Map();
27419
27926
  function resolveModule(fromFile, specifier, packages) {
@@ -27785,6 +28292,8 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
27785
28292
  typeOnlyBindings: Object.fromEntries(facts.typeOnlyBindings),
27786
28293
  fieldTypes: Object.fromEntries([...facts.fieldTypes].map(([k, v]) => [k, [...v]])),
27787
28294
  localTypes: Object.fromEntries([...facts.localTypes].map(([k, v]) => [k, [...v]])),
28295
+ memberTypes: Object.fromEntries([...facts.memberTypes].map(([k, v]) => [k, [...v]])),
28296
+ aliasTargets: Object.fromEntries(facts.aliasTargets),
27788
28297
  typeShapes: Object.fromEntries(facts.typeShapes),
27789
28298
  functionParams: Object.fromEntries(facts.functionParams),
27790
28299
  functionRoutes: Object.fromEntries([...facts.functionRoutes].map(([k, v]) => [k, [...v.values()]])),
@@ -28229,8 +28738,16 @@ function buildRuleContext(opts) {
28229
28738
  }
28230
28739
  if (ownerId && nameKey(first) === nameKey(ownerId)) return ownTypes.some((spec) => typeMatchesRef(spec, rest.join("::")));
28231
28740
  if (ref.includes("::") && authoredTypeRefs.has(ref)) return true;
28741
+ return false;
28742
+ };
28743
+ const aliasSpelling = (ref) => {
28744
+ const segments = ref.split(".");
28745
+ if (segments.length !== 2 || ref.includes("::")) return null;
28746
+ const [first, name] = segments;
28747
+ if (subsystems.some((s) => nameKey(s.id.split("::").pop()) === nameKey(first) && memberOf(s.id) === void 0)) return null;
28232
28748
  const via = aliasOf(first);
28233
- return via !== void 0 && rest.length === 1 && exportedBy(via, nameKey(rest[0]), "type") !== void 0;
28749
+ const publicName = via ? exportedBy(via, nameKey(name), "type") : void 0;
28750
+ return via && publicName ? `${via.alias}::${publicName}` : null;
28234
28751
  };
28235
28752
  const foreignSurfaceRef = (ref) => {
28236
28753
  const segments = ref.split("::").filter((seg) => seg && seg !== "super");
@@ -28604,6 +29121,7 @@ function buildRuleContext(opts) {
28604
29121
  isImplementationDraft,
28605
29122
  getComponentProfile,
28606
29123
  isTypeResolved,
29124
+ aliasSpelling,
28607
29125
  targetLanguageFor,
28608
29126
  isSpecInScope,
28609
29127
  isInChainedSubproject,
@@ -29774,8 +30292,8 @@ function generateSequenceDiagram(componentId, methodName, options) {
29774
30292
  const steps = [...methodImpl.narrative].sort((a, b) => a.stepNumber - b.stepNumber);
29775
30293
  for (const step of steps) {
29776
30294
  for (const par of parallelArms) {
29777
- const sep14 = par.sepByStep.get(step.stepNumber);
29778
- if (sep14 !== void 0) lines.push(` and ${escapeLabel(sep14)}`);
30295
+ const sep15 = par.sepByStep.get(step.stepNumber);
30296
+ if (sep15 !== void 0) lines.push(` and ${escapeLabel(sep15)}`);
29779
30297
  }
29780
30298
  switch (step.type) {
29781
30299
  case "local":
@@ -30505,6 +31023,15 @@ function classifySource(world, sourceId, label7, sources) {
30505
31023
  detail: `"${label7}" re-exports from "${sourceId}", which is neither a subsystem of this project, a member nor a declared external the scan read \u2014 a project re-exports only from its subsystems, members and externals of its family`
30506
31024
  };
30507
31025
  }
31026
+ function duplicateDetail(entries, index) {
31027
+ const e = entries[index];
31028
+ if (e.from !== void 0 || e.component !== void 0 || e.interface !== void 0 || e.typeDef !== void 0) return null;
31029
+ const name = e.as ?? e.id;
31030
+ if (name === void 0) return null;
31031
+ const other = entries.findIndex((x, i) => i !== index && (x.as ?? x.id) === name);
31032
+ if (other === -1) return null;
31033
+ return `has a duplicate entry "${name}": entry #${index + 1} names no source and no item, while entry #${other + 1} declares the same id \u2014 delete the copy (entry #${index + 1}), or give it a source and an item under an id of its own`;
31034
+ }
30508
31035
  function bindOwnType(world, owner, e, problems) {
30509
31036
  const type = (world.types.get(e.typeDef) ?? []).find((t) => !t.subsystem);
30510
31037
  if (!type) {
@@ -30576,7 +31103,8 @@ function resolveProjectTable(system, subsystems, components, interfaces, types,
30576
31103
  const owner = sources.namespace ? sources.namespace : system.name;
30577
31104
  const problems = [];
30578
31105
  const candidates = [];
30579
- (system.publicInterfaces ?? []).forEach((e, index) => {
31106
+ const entries = system.publicInterfaces ?? [];
31107
+ entries.forEach((e, index) => {
30580
31108
  const label7 = e.as ?? e.id ?? e.interface ?? e.component ?? e.typeDef ?? `#${index + 1}`;
30581
31109
  const source = classifySource(world, sourceOf(world, e), label7, sources);
30582
31110
  switch (source.kind) {
@@ -30602,7 +31130,7 @@ function resolveProjectTable(system, subsystems, components, interfaces, types,
30602
31130
  return;
30603
31131
  }
30604
31132
  default:
30605
- problems.push({ kind: "invalid", owner, publicName: label7, detail: source.detail });
31133
+ problems.push({ kind: "invalid", owner, publicName: label7, detail: duplicateDetail(entries, index) ?? source.detail });
30606
31134
  }
30607
31135
  });
30608
31136
  return settle(owner, "project", candidates, problems);
@@ -31125,6 +31653,28 @@ function mapSignatureSource(value, map) {
31125
31653
  const head2 = map("signatureFrom", parts.head);
31126
31654
  return head2 === parts.head ? value : `${head2}.${parts.tail}`;
31127
31655
  }
31656
+ var TYPE_EXPRESSION_PATHS = {
31657
+ interface: ["methods.params.type", "methods.returns", "methods.signature", "methods.signatureFrom"],
31658
+ type: ["fields.type", "params.type", "returns", "methods.params.type", "methods.returns", "methods.signature"]
31659
+ };
31660
+ function typeExpressionSlots(kind, doc) {
31661
+ const out = [];
31662
+ const paths = kind === "interface" || kind === "type" ? TYPE_EXPRESSION_PATHS[kind] : [];
31663
+ const walk2 = (node, segments, at, method) => {
31664
+ if (!node || typeof node !== "object") return;
31665
+ const holder = node;
31666
+ const [head2, ...rest] = segments;
31667
+ if (rest.length === 0) {
31668
+ if (typeof holder[head2] === "string") out.push({ holder, key: head2, path: at, ...method ? { method } : {} });
31669
+ return;
31670
+ }
31671
+ const next = holder[head2];
31672
+ const items = Array.isArray(next) ? next : next && typeof next === "object" ? [next] : [];
31673
+ for (const item of items) walk2(item, rest, at, head2 === "methods" ? item : method);
31674
+ };
31675
+ for (const at of paths) walk2(doc, at.split("."), at, void 0);
31676
+ return out;
31677
+ }
31128
31678
  function mapSpecReferences(kind, spec, map) {
31129
31679
  switch (kind) {
31130
31680
  case "subsystem": {
@@ -31195,6 +31745,16 @@ function qualifiedTypeNames(typeStr) {
31195
31745
  }
31196
31746
  return out;
31197
31747
  }
31748
+ function invariantTypeRef(ref) {
31749
+ const at = ref.lastIndexOf(".");
31750
+ return at > 0 && at < ref.length - 1 ? ref.slice(0, at) : void 0;
31751
+ }
31752
+ function mapInvariantRef(ref, map) {
31753
+ const typeRef2 = invariantTypeRef(ref);
31754
+ if (typeRef2 === void 0 || !typeRef2.includes("::")) return ref;
31755
+ const next = map("type", typeRef2);
31756
+ return next === typeRef2 ? ref : `${next}${ref.slice(typeRef2.length)}`;
31757
+ }
31198
31758
  function rawReferences(kind, spec) {
31199
31759
  const out = [];
31200
31760
  const add2 = (position, value) => {
@@ -31205,11 +31765,8 @@ function rawReferences(kind, spec) {
31205
31765
  for (const link of spec.trustedLinks ?? []) add2("trustedLinks", link.subsystem);
31206
31766
  break;
31207
31767
  case "interface":
31208
- for (const m of spec.methods) {
31209
- qualifiedTypeNames(m.returns).forEach((t) => add2("type", t));
31210
- m.params?.forEach((p) => qualifiedTypeNames(p.type).forEach((t) => add2("type", t)));
31211
- if (m.signatureFrom !== void 0 && !signatureSourceParts(m.signatureFrom)) add2("type", m.signatureFrom);
31212
- }
31768
+ case "type":
31769
+ typeSlotReferences(kind, spec).forEach((t) => add2("type", t));
31213
31770
  break;
31214
31771
  case "implementation":
31215
31772
  for (const m of spec.methods) {
@@ -31217,22 +31774,40 @@ function rawReferences(kind, spec) {
31217
31774
  for (const step of m.narrative) {
31218
31775
  const source = step.auth?.from;
31219
31776
  if (typeof source === "string" && source.startsWith(COMPONENT_AUTH_SOURCE)) add2("auth", source.slice(COMPONENT_AUTH_SOURCE.length));
31777
+ for (const ref of step.assertsInvariants ?? []) add2("type", invariantTypeRef(ref));
31220
31778
  }
31221
31779
  }
31222
31780
  break;
31223
- case "type": {
31224
- const t = spec;
31225
- for (const f of t.fields) qualifiedTypeNames(f.type).forEach((n) => add2("type", n));
31226
- t.params?.forEach((p) => qualifiedTypeNames(p.type).forEach((n) => add2("type", n)));
31227
- qualifiedTypeNames(t.returns).forEach((n) => add2("type", n));
31228
- for (const m of t.methods ?? []) m.params?.forEach((p) => qualifiedTypeNames(p.type).forEach((n) => add2("type", n)));
31229
- break;
31230
- }
31231
31781
  default:
31232
31782
  break;
31233
31783
  }
31234
31784
  return out;
31235
31785
  }
31786
+ function typeSlotReferences(kind, spec) {
31787
+ const out = [];
31788
+ const structured2 = /* @__PURE__ */ new Set();
31789
+ const slots = typeExpressionSlots(kind, spec);
31790
+ for (const slot of slots) {
31791
+ const value = slot.holder[slot.key];
31792
+ if (slot.path === "methods.signature") continue;
31793
+ if (slot.path === "methods.signatureFrom") {
31794
+ if (!signatureSourceParts(value)) out.push(value);
31795
+ continue;
31796
+ }
31797
+ for (const name of qualifiedTypeNames(value)) {
31798
+ out.push(name);
31799
+ structured2.add(name);
31800
+ }
31801
+ }
31802
+ for (const slot of slots.filter((s) => s.path === "methods.signature")) {
31803
+ for (const name of qualifiedTypeNames(slot.holder[slot.key])) {
31804
+ if (structured2.has(name)) continue;
31805
+ structured2.add(name);
31806
+ out.push(name);
31807
+ }
31808
+ }
31809
+ return out;
31810
+ }
31236
31811
  var IMPORTABLE_POSITIONS = /* @__PURE__ */ new Set(["dependsOn", "dispatch", "mounts", "narrative"]);
31237
31812
  function bareRawReferences(kind, spec) {
31238
31813
  const out = [];
@@ -31259,7 +31834,11 @@ function bareRawReferences(kind, spec) {
31259
31834
  const t = spec;
31260
31835
  for (const f of t.fields) for (const ref of fieldTypeRefs(t, f.type)) add2("type", ref);
31261
31836
  for (const ref of signatureTypeRefs(t)) add2("type", ref);
31262
- for (const m of t.methods ?? []) for (const p of m.params ?? []) for (const ref of fieldTypeRefs(t, p.type)) add2("type", ref);
31837
+ const own = lower(typeGenericParameters(t));
31838
+ for (const m of t.methods ?? []) {
31839
+ const generics = /* @__PURE__ */ new Set([...own, ...lower(methodGenericParameters(m))]);
31840
+ for (const ref of methodTypeRefs(m)) add2("type", ref, generics);
31841
+ }
31263
31842
  break;
31264
31843
  }
31265
31844
  case "implementation":
@@ -31272,6 +31851,9 @@ function bareRawReferences(kind, spec) {
31272
31851
  }
31273
31852
  return out;
31274
31853
  }
31854
+ function respellTypeNames(typeStr, map) {
31855
+ return mapTypeNames(typeStr, (_position, name) => map(name));
31856
+ }
31275
31857
  function mapTypeNames(typeStr, map) {
31276
31858
  if (typeStr === void 0) return void 0;
31277
31859
  return typeStr.replace(
@@ -31295,12 +31877,9 @@ function respellStoredReferences(kind, doc, map) {
31295
31877
  const values = holder[key];
31296
31878
  if (Array.isArray(values)) holder[key] = values.map((v) => typeof v === "string" ? map(position, v) : v);
31297
31879
  };
31298
- const typed = (holder, key) => {
31299
- if (typeof holder[key] === "string") holder[key] = mapTypeNames(holder[key], map);
31300
- };
31301
31880
  const published = (entries) => {
31302
31881
  for (const e of list(entries)) {
31303
- for (const key of ["from", "component", "interface", "typeDef"]) at(e, key, "publicInterfaces");
31882
+ for (const key of ["from", "subsystem", "component", "interface", "typeDef"]) at(e, key, "publicInterfaces");
31304
31883
  each(e, "consumers", "publicInterfaces");
31305
31884
  }
31306
31885
  };
@@ -31322,20 +31901,16 @@ function respellStoredReferences(kind, doc, map) {
31322
31901
  break;
31323
31902
  case "interface":
31324
31903
  at(doc, "component", "contract");
31325
- for (const m of list(doc.methods)) {
31326
- typed(m, "signature");
31327
- typed(m, "returns");
31328
- for (const p of list(m.params)) typed(p, "type");
31329
- if (typeof m.signatureFrom === "string") {
31330
- m.signatureFrom = signatureSourceParts(m.signatureFrom) ? mapSignatureSource(m.signatureFrom, map) : mapTypeNames(m.signatureFrom, map);
31331
- }
31332
- }
31904
+ typedSlots("interface", doc, map);
31333
31905
  break;
31334
31906
  case "implementation":
31335
31907
  at(doc, "contract", "contract");
31336
31908
  for (const m of list(doc.methods)) {
31337
31909
  for (const step of list(m.narrative)) {
31338
31910
  at(step, "targetComponent", "narrative");
31911
+ if (Array.isArray(step.assertsInvariants)) {
31912
+ step.assertsInvariants = step.assertsInvariants.map((ref) => typeof ref === "string" ? mapInvariantRef(ref, map) : ref);
31913
+ }
31339
31914
  const auth = step.auth;
31340
31915
  if (auth && typeof auth.from === "string" && auth.from.startsWith(COMPONENT_AUTH_SOURCE)) {
31341
31916
  auth.from = `${COMPONENT_AUTH_SOURCE}${map("auth", auth.from.slice(COMPONENT_AUTH_SOURCE.length))}`;
@@ -31353,34 +31928,33 @@ function respellStoredReferences(kind, doc, map) {
31353
31928
  case "type":
31354
31929
  at(doc, "subsystem", "subsystem");
31355
31930
  at(doc, "group", "group");
31356
- for (const f of list(doc.fields)) typed(f, "type");
31357
- for (const p of list(doc.params)) typed(p, "type");
31358
- typed(doc, "returns");
31359
- for (const m of list(doc.methods)) for (const p of list(m.params)) typed(p, "type");
31931
+ typedSlots("type", doc, map);
31360
31932
  break;
31361
31933
  default:
31362
31934
  break;
31363
31935
  }
31364
31936
  }
31937
+ function typedSlots(kind, doc, map) {
31938
+ for (const slot of typeExpressionSlots(kind, doc)) {
31939
+ const value = slot.holder[slot.key];
31940
+ slot.holder[slot.key] = slot.path === "methods.signatureFrom" && signatureSourceParts(value) ? mapSignatureSource(value, map) : mapTypeNames(value, map);
31941
+ }
31942
+ }
31365
31943
  function mapRawReferences(kind, spec, map) {
31366
31944
  switch (kind) {
31367
31945
  case "subsystem": {
31368
31946
  const s = spec;
31369
31947
  return { ...s, trustedLinks: (s.trustedLinks ?? []).map((l) => ({ ...l, subsystem: map("trustedLinks", l.subsystem) })) };
31370
31948
  }
31371
- case "interface": {
31372
- const i = spec;
31373
- return {
31374
- ...i,
31375
- methods: i.methods.map((m) => ({
31376
- ...m,
31377
- ...m.signature !== void 0 ? { signature: mapTypeNames(m.signature, map) } : {},
31378
- ...m.returns !== void 0 ? { returns: mapTypeNames(m.returns, map) } : {},
31379
- ...m.params ? { params: m.params.map((p) => ({ ...p, type: mapTypeNames(p.type, map) })) } : {},
31380
- // A type source is a raw type position; a method source's head is bound, never raw.
31381
- ...m.signatureFrom !== void 0 && !signatureSourceParts(m.signatureFrom) ? { signatureFrom: mapTypeNames(m.signatureFrom, map) } : {}
31382
- }))
31383
- };
31949
+ case "interface":
31950
+ case "type": {
31951
+ const copy = JSON.parse(JSON.stringify(spec));
31952
+ for (const slot of typeExpressionSlots(kind, copy)) {
31953
+ const value = slot.holder[slot.key];
31954
+ if (slot.path === "methods.signatureFrom" && signatureSourceParts(value)) continue;
31955
+ slot.holder[slot.key] = mapTypeNames(value, map);
31956
+ }
31957
+ return copy;
31384
31958
  }
31385
31959
  case "implementation": {
31386
31960
  const impl = spec;
@@ -31395,7 +31969,8 @@ function mapRawReferences(kind, spec, map) {
31395
31969
  return call && next !== call.compId ? `${next}.${call.methodName}` : entry;
31396
31970
  })
31397
31971
  } : {},
31398
- narrative: m.narrative.map((step) => {
31972
+ narrative: m.narrative.map((authored) => {
31973
+ const step = authored.assertsInvariants ? { ...authored, assertsInvariants: authored.assertsInvariants.map((ref) => mapInvariantRef(ref, map)) } : authored;
31399
31974
  const source = step.auth?.from;
31400
31975
  if (typeof source !== "string" || !source.startsWith(COMPONENT_AUTH_SOURCE)) return step;
31401
31976
  const auth = step.auth;
@@ -31404,17 +31979,6 @@ function mapRawReferences(kind, spec, map) {
31404
31979
  }))
31405
31980
  };
31406
31981
  }
31407
- case "type": {
31408
- const t = spec;
31409
- const typedParams = (params) => params ? { params: params.map((p) => ({ ...p, type: mapTypeNames(p.type, map) })) } : {};
31410
- return {
31411
- ...t,
31412
- fields: t.fields.map((f) => ({ ...f, type: mapTypeNames(f.type, map) })),
31413
- ...typedParams(t.params),
31414
- ...t.returns !== void 0 ? { returns: mapTypeNames(t.returns, map) } : {},
31415
- methods: (t.methods ?? []).map((m) => ({ ...m, ...typedParams(m.params) }))
31416
- };
31417
- }
31418
31982
  default:
31419
31983
  return spec;
31420
31984
  }
@@ -33491,12 +34055,12 @@ var SpecWorkspace = class {
33491
34055
  return this.relativizeSpec("interface", stored);
33492
34056
  }
33493
34057
  prepareImplementationForWrite(spec, carry = true) {
33494
- const relative19 = this.relativizeSpec("implementation", spec, carry);
34058
+ const relative20 = this.relativizeSpec("implementation", spec, carry);
33495
34059
  const member = this.getSubprojectPrefix(spec.id);
33496
34060
  const memberRoot = member ? this.resolveSubprojectForNamespace(member) : null;
33497
- if (memberRoot) return childRelativeFilePaths(relative19, this.rootDir, memberRoot);
34061
+ if (memberRoot) return childRelativeFilePaths(relative20, this.rootDir, memberRoot);
33498
34062
  const partDir = this.partHolding(this.getImplementationPath(spec.id, spec.contract))?.part.directory;
33499
- return partDir ? partRelativeFilePaths(relative19, this.rootDir, partDir) : relative19;
34063
+ return partDir ? partRelativeFilePaths(relative20, this.rootDir, partDir) : relative20;
33500
34064
  }
33501
34065
  /**
33502
34066
  * A type written in its stored form — every type position canonical (one
@@ -35801,6 +36365,12 @@ function readLockState(current2) {
35801
36365
  if (!record) return { state: "unlocked", record: null, current: current2 };
35802
36366
  return { state: stateIdEquals(record.stateId, current2) ? "locked" : "stale", record, current: current2 };
35803
36367
  }
36368
+ function writeLockRecord2(record) {
36369
+ writeLockRecord(record);
36370
+ }
36371
+ function renderDiagram2(format, relations) {
36372
+ return renderDiagram(format, relations);
36373
+ }
35804
36374
  function createProjectConfig(config) {
35805
36375
  projectConfigRepository.create(config);
35806
36376
  }
@@ -36047,7 +36617,7 @@ function resolveGit(external, config) {
36047
36617
  const declared = gitDeclarationOf(config, external);
36048
36618
  if (!declared) return { ...external, problem: "git producer unavailable: its declaration names no repository" };
36049
36619
  try {
36050
- const commit2 = external.role === "member" && declared.commit ? declared.commit : resolve7(declared.url, declared.ref);
36620
+ const commit2 = external.role === "member" && declared.commit ? declared.commit : resolve3(declared.url, declared.ref);
36051
36621
  const directory = fetch(declared.url, commit2, declared.dir);
36052
36622
  return { ...external, directory: path23.resolve(directory), commit: commit2, problem: void 0 };
36053
36623
  } catch (e) {
@@ -36476,7 +37046,7 @@ function scaffoldPart(alias, memberDir, contained) {
36476
37046
  }
36477
37047
  function createGitMember(alias, source, description, as) {
36478
37048
  const url = source.git;
36479
- const commit2 = source.commit ?? resolve7(url, source.ref);
37049
+ const commit2 = source.commit ?? resolve3(url, source.ref);
36480
37050
  const dir = fetch(url, commit2, source.dir);
36481
37051
  const kind = holdsProjectContent(dir) ? "project" : "part";
36482
37052
  if (as !== void 0 && as !== kind) {
@@ -36634,7 +37204,7 @@ function isWithinDir2(dir, file) {
36634
37204
  var MOVED_REF_POSITIONS = /* @__PURE__ */ new Set(["component", "interface"]);
36635
37205
  var INTO_MEMBER_POSITIONS = /* @__PURE__ */ new Set([...MOVED_REF_POSITIONS, "type"]);
36636
37206
  function externalizeSubsystem(subsystemId, path49, as) {
36637
- externalizeInto(subsystemId, path49, as);
37207
+ return externalizeInto(subsystemId, path49, as);
36638
37208
  }
36639
37209
  function externalizeInto(subsystemId, partPath, as) {
36640
37210
  if (subsystemId.includes("::")) {
@@ -36653,7 +37223,8 @@ function externalizeInto(subsystemId, partPath, as) {
36653
37223
  const parentRoot = getProjectRoot();
36654
37224
  const parentSpecsDir = aiPathsAt(parentRoot).specsDir();
36655
37225
  const fooDir = path25.join(parentSpecsDir, subsystemId);
36656
- if (!fs16.existsSync(fooDir)) throw new WaironError(`subsystem specs directory not found: ${fooDir}`);
37226
+ const owned = subsystemFiles(subsystemId, parentSpecsDir);
37227
+ if (owned.files.length === 0) throw new WaironError(`subsystem specs not found for "${subsystemId}" under ${parentSpecsDir}`);
36657
37228
  const relPath = toPosixPath(partPath);
36658
37229
  const partDir = assertContainedProjectPath(parentRoot, relPath);
36659
37230
  if (path25.resolve(partDir) === path25.resolve(parentRoot)) throw new WaironError(`cannot externalize "${subsystemId}": the path "${relPath}" is the project root itself.`);
@@ -36666,19 +37237,74 @@ function externalizeInto(subsystemId, partPath, as) {
36666
37237
  if (!joined && (declaredMembers(projectConfigRepository.load() ?? {}).some((m) => m.alias === alias) || projectConfigRepository.load()?.externals?.[alias] !== void 0)) {
36667
37238
  throw new WaironError(`cannot externalize "${subsystemId}": this project already declares the alias "${alias}".`);
36668
37239
  }
36669
- const targetDir = path25.join(aiPathsAt(partDir).specsDir(), subsystemId);
36670
- if (fs16.existsSync(targetDir)) throw new WaironError(`target already contains a "${subsystemId}" subsystem: ${targetDir}`);
37240
+ const partSpecsDir = aiPathsAt(partDir).specsDir();
37241
+ const targetDir = path25.join(partSpecsDir, subsystemId);
37242
+ if (owned.home !== void 0 && fs16.existsSync(targetDir)) throw new WaironError(`target already contains a "${subsystemId}" subsystem: ${targetDir}`);
37243
+ const destinationOf = (file) => path25.join(partSpecsDir, path25.relative(parentSpecsDir, file));
37244
+ const strays = owned.files.filter((f) => !(owned.home !== void 0 && isWithinDir2(owned.home, f)));
37245
+ const taken = strays.filter((f) => fs16.existsSync(destinationOf(f)));
37246
+ if (taken.length > 0) throw new WaironError(`cannot externalize "${subsystemId}": the part already holds ${taken.map((f) => toPosixPath(path25.relative(partDir, destinationOf(f)))).join(", ")}.`);
36671
37247
  if (as === "project") {
36672
37248
  if (joined) throw new WaironError(`cannot externalize "${subsystemId}" as a project into the existing part "${joined.alias}": externalize it into the part, then promote the part (\`wairon member promote ${joined.alias}\`).`);
36673
- const refusals = promoteRefusals(family, specFilesUnder(fooDir), ownSpecFiles(family, fooDir), alias);
37249
+ const inside = new Set(owned.files.map((f) => path25.resolve(f)));
37250
+ const refusals = promoteRefusals(family, owned.files, ownSpecFiles(family, fooDir).filter((f) => !inside.has(path25.resolve(f))), alias);
36674
37251
  if (refusals.length > 0) throw new WaironError(`cannot externalize "${subsystemId}" as a project: ${refusals.join("; ")}.`);
36675
37252
  }
36676
- ensureDir(path25.dirname(targetDir));
36677
- fs16.renameSync(fooDir, targetDir);
36678
- rebaseImplementationPaths(targetDir, parentRoot, partDir);
37253
+ const moved = [];
37254
+ if (owned.home !== void 0) {
37255
+ ensureDir(path25.dirname(targetDir));
37256
+ fs16.renameSync(owned.home, targetDir);
37257
+ moved.push(...specFilesUnder(targetDir));
37258
+ }
37259
+ for (const file of strays) {
37260
+ const to = destinationOf(file);
37261
+ ensureDir(path25.dirname(to));
37262
+ fs16.renameSync(file, to);
37263
+ moved.push(to);
37264
+ removeEmptyDirsUpTo(path25.dirname(file), parentSpecsDir);
37265
+ }
37266
+ rebaseImplementationPaths(partSpecsDir, parentRoot, partDir, moved);
36679
37267
  if (!joined) projectConfigRepository.declareMember(alias, { source: relPath });
36680
37268
  invalidateSpecCache();
36681
- if (as === "project") promoteMember(alias);
37269
+ return as === "project" ? promoteMember(alias) : void 0;
37270
+ }
37271
+ function subsystemFiles(subsystemId, specsDir) {
37272
+ const subPath = getSubsystemPath(subsystemId);
37273
+ const nested = subPath.endsWith(".index.yaml") && path25.resolve(path25.dirname(subPath)) === path25.resolve(path25.join(specsDir, subsystemId)) && fs16.existsSync(subPath);
37274
+ const home = nested ? path25.dirname(subPath) : void 0;
37275
+ const files = new Set(home !== void 0 ? specFilesUnder(home).map((f) => path25.resolve(f)) : []);
37276
+ if (!nested && fs16.existsSync(subPath)) files.add(path25.resolve(subPath));
37277
+ const l0 = path25.resolve(aiPathsAt(getProjectRoot()).specsSystem());
37278
+ const others = () => specFilesUnder(specsDir).filter((f) => path25.resolve(f) !== l0 && !files.has(path25.resolve(f)));
37279
+ const add2 = (file) => {
37280
+ if (path25.basename(file) === ".index.yaml") for (const f of specFilesUnder(path25.dirname(file))) files.add(path25.resolve(f));
37281
+ else files.add(path25.resolve(file));
37282
+ };
37283
+ for (const file of others()) {
37284
+ let raw;
37285
+ try {
37286
+ raw = readYamlFile(file);
37287
+ } catch {
37288
+ continue;
37289
+ }
37290
+ const kind = raw && typeof raw === "object" ? specKind(raw) : void 0;
37291
+ if (kind === "subsystem" || kind === "system" || raw?.subsystem !== subsystemId) continue;
37292
+ add2(file);
37293
+ }
37294
+ const owned = () => rawSpecs([...files]);
37295
+ const components = new Set(owned().filter((x) => x.kind === "component").map((x) => x.id));
37296
+ for (const spec of rawSpecs(others())) if (spec.kind === "interface" && components.has(spec.raw?.component)) add2(spec.file);
37297
+ const contracts = new Set(owned().filter((x) => x.kind === "interface").map((x) => x.id));
37298
+ for (const spec of rawSpecs(others())) if (spec.kind === "implementation" && contracts.has(spec.raw?.contract)) add2(spec.file);
37299
+ return { home, files: [...files] };
37300
+ }
37301
+ function removeEmptyDirsUpTo(dir, stop) {
37302
+ let at = path25.resolve(dir);
37303
+ const end = path25.resolve(stop);
37304
+ while (at !== end && isWithinDir2(end, at) && fs16.existsSync(at) && fs16.readdirSync(at).length === 0) {
37305
+ fs16.rmdirSync(at);
37306
+ at = path25.dirname(at);
37307
+ }
36682
37308
  }
36683
37309
  function specFilesUnder(dir) {
36684
37310
  return fs16.existsSync(dir) ? listFilesRecursive(dir, ".yaml") : [];
@@ -36704,7 +37330,8 @@ function rawSpecs(files) {
36704
37330
  return out;
36705
37331
  }
36706
37332
  function typeTokens(expression) {
36707
- return typeof expression === "string" ? [...expression.matchAll(/[A-Za-z_][A-Za-z0-9_-]*/g)].map((m) => m[0]) : [];
37333
+ if (typeof expression !== "string") return [];
37334
+ return [...expression.matchAll(/(?:::)?[A-Za-z_][A-Za-z0-9_-]*(?:(?:::|\.)[A-Za-z_][A-Za-z0-9_-]*)*/g)].map((m) => m[0]).filter((t) => !t.includes(".") && !t.includes("::"));
36708
37335
  }
36709
37336
  function referencesOf(spec, wholeParams) {
36710
37337
  const out = [];
@@ -36713,13 +37340,13 @@ function referencesOf(spec, wholeParams) {
36713
37340
  return ref;
36714
37341
  });
36715
37342
  const expressions = [];
36716
- if (spec.kind === "interface") {
36717
- for (const m of spec.raw.methods ?? []) {
36718
- expressions.push(m?.returns);
36719
- for (const p of m?.params ?? []) if (wholeParams || !(typeof p?.type === "string" && /^[A-Za-z_][A-Za-z0-9_-]*$/.test(p.type))) expressions.push(p?.type);
36720
- }
37343
+ for (const slot of typeExpressionSlots(spec.kind, spec.raw)) {
37344
+ const value = slot.holder[slot.key];
37345
+ if (slot.path === "methods.signatureFrom") continue;
37346
+ if (slot.path === "methods.signature" && Array.isArray(slot.method?.params)) continue;
37347
+ if (slot.path === "methods.params.type" && spec.kind === "interface" && !wholeParams && /^[A-Za-z_][A-Za-z0-9_-]*$/.test(String(value))) continue;
37348
+ expressions.push(slot.path === "methods.signature" ? String(value).replace(/^\s*[A-Za-z_][\w-]*\s*(?:<[^>]*>)?\s*\(/, "(").replace(/(?<![:\w-])[A-Za-z_][\w-]*\s*\??\s*:(?!:)/g, "") : value);
36721
37349
  }
36722
- if (spec.kind === "type") for (const f of spec.raw.fields ?? []) expressions.push(f?.type);
36723
37350
  for (const token of expressions.flatMap(typeTokens)) out.push({ ref: token, position: "bare-type" });
36724
37351
  return out;
36725
37352
  }
@@ -36727,6 +37354,9 @@ function crossingsOf(inside, outside) {
36727
37354
  const idsOf = (specs) => new Set(specs.filter((s) => s.kind !== "system").map((s) => s.id));
36728
37355
  const insideIds = idsOf(inside);
36729
37356
  const outsideIds = idsOf(outside);
37357
+ const targetsOf2 = (specs) => new Set(specs.filter((s) => s.kind === "component" || s.kind === "interface" || s.kind === "type").map((s) => s.id));
37358
+ const insideTargets = targetsOf2(inside);
37359
+ const outsideTargets = targetsOf2(outside);
36730
37360
  const typeIds = (specs) => new Map(specs.filter((s) => s.kind === "type").map((s) => [nameKey(s.id), s.id]));
36731
37361
  const insideTypes = typeIds(inside);
36732
37362
  const outsideTypes = typeIds(outside);
@@ -36744,13 +37374,14 @@ function crossingsOf(inside, outside) {
36744
37374
  }
36745
37375
  return found;
36746
37376
  };
36747
- return { out: collect(inside, outsideIds, outsideTypes, true), in: collect(outside, insideIds, insideTypes, false), insideIds, outsideIds };
37377
+ return { out: collect(inside, outsideTargets, outsideTypes, true), in: collect(outside, insideTargets, insideTypes, false), insideIds, outsideIds, insideTargets, outsideTargets };
36748
37378
  }
36749
37379
  function promoteRefusals(family, insideFiles, outsideFiles, id) {
36750
37380
  const out = [];
36751
37381
  const inside = rawSpecs(insideFiles);
36752
37382
  const outside = rawSpecs(outsideFiles);
36753
37383
  const crossing = crossingsOf(inside, outside);
37384
+ out.push(...unreachableUpward(inside, outside));
36754
37385
  const subsystemsIn = new Set(inside.filter((s) => s.kind === "subsystem").map((s) => s.id));
36755
37386
  for (const s of [...inside, ...outside].filter((x) => x.kind === "subsystem")) {
36756
37387
  for (const link of s.raw.trustedLinks ?? []) {
@@ -36792,13 +37423,19 @@ function climbsOf(family, ids, parentId) {
36792
37423
  return { map, refused: [...new Set(refused2)] };
36793
37424
  }
36794
37425
  var PROJECT_ID_RE_LOCAL = /^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$/;
36795
- function rewriteRefsIn(specs, remap) {
37426
+ function rewriteRefsIn(specs, remap, record) {
36796
37427
  const rewritten = [];
36797
37428
  for (const spec of specs) {
36798
37429
  const raw = readYamlFile(spec.file);
36799
- if (!rewriteSpecRefs(raw, (ref, position) => remap(ref, position))) continue;
37430
+ const edits = [];
37431
+ if (!rewriteSpecRefs(raw, (ref, position) => {
37432
+ const to = remap(ref, position);
37433
+ if (to !== ref) edits.push({ kind: spec.kind, specId: spec.id, position, from: ref, to });
37434
+ return to;
37435
+ })) continue;
36800
37436
  writeYamlFile(spec.file, raw);
36801
37437
  rewritten.push({ kind: spec.kind, id: spec.id });
37438
+ if (record) for (const e of edits) recordRespelling(record.result, e, record.member);
36802
37439
  }
36803
37440
  return rewritten;
36804
37441
  }
@@ -36848,15 +37485,19 @@ function promoteMember(alias, id) {
36848
37485
  patchKeepingStamp(s.file, (raw) => raw.parentSystem === projectName ? false : (raw.parentSystem = projectName, true));
36849
37486
  }
36850
37487
  if (declaration2?.as === "part") projectConfigRepository.updateMember(alias, { as: "" });
36851
- const parentSide = rewriteRefsIn(crossing.in.map((c) => c.spec).filter(unique), (ref, position) => INTO_MEMBER_POSITIONS.has(position) && crossing.insideIds.has(ref) ? `${alias}::${ref}` : ref);
37488
+ const result = { respelled: [], memberRespelled: [], exported: [], imported: [], consumersDropped: [] };
37489
+ const parentSide = rewriteRefsIn(crossing.in.map((c) => c.spec).filter(unique), (ref, position) => INTO_MEMBER_POSITIONS.has(position) && crossing.insideTargets.has(ref) ? `${alias}::${ref}` : ref, { result, member: false });
36852
37490
  const climbs = climbsOf(family, crossing.insideIds, parentId).map;
36853
37491
  const climbing = inside.filter((s) => family.authoredReferences.some((r) => r.form === "super" && r.specId === s.id));
36854
- const memberSide = rewriteRefsIn([...crossing.out.map((c) => c.spec), ...climbing].filter(unique), (ref, position) => climbs.get(ref) ?? (MOVED_REF_POSITIONS.has(position) && crossing.outsideIds.has(ref) ? `${parentId}::${ref}` : ref));
37492
+ const memberSide = rewriteRefsIn([...crossing.out.map((c) => c.spec), ...climbing].filter(unique), (ref, position) => climbs.get(ref) ?? (MOVED_REF_POSITIONS.has(position) && crossing.outsideTargets.has(ref) ? `${parentId}::${ref}` : ref), { result, member: true });
37493
+ const qualified = respellQualified(inside, outside, { alias, parentId, newId }, result);
36855
37494
  invalidateSpecCache();
36856
- if (crossing.out.length > 0 || [...climbs.values()].some((t) => t.startsWith(`${parentId}::`))) runWithProjectRoot(dir, () => projectConfigRepository.declareExternal(parentId, {}));
37495
+ dropCrossingConsumers(inside, outside, result);
37496
+ if (crossing.out.length > 0 || qualified.out.length > 0 || [...climbs.values()].some((t) => t.startsWith(`${parentId}::`))) runWithProjectRoot(dir, () => projectConfigRepository.declareExternal(parentId, {}));
36857
37497
  for (const s of inside.filter((x) => x.kind === "subsystem")) restateReExports(s.id, alias);
36858
- exportCrossings(crossing.out, dir, alias, parentId, "out");
36859
- exportCrossings(crossing.in, dir, alias, parentId, "in");
37498
+ exportCrossings([...crossing.out, ...qualified.out], dir, alias, parentId, "out", result);
37499
+ exportCrossings([...crossing.in, ...qualified.in], dir, alias, parentId, "in", result);
37500
+ exportReExported(inside, dir, alias, result);
36860
37501
  invalidateSpecCache();
36861
37502
  for (const spec of parentSide) if (spec.kind !== "system") normalizeReferences(spec.kind, spec.id);
36862
37503
  const memberKey = graph().nodes.find((n) => n.parent === "" && n.mountAlias === alias)?.namespace;
@@ -36875,6 +37516,165 @@ function promoteMember(alias, id) {
36875
37516
  });
36876
37517
  }
36877
37518
  invalidateSpecCache();
37519
+ return result;
37520
+ }
37521
+ function recordRespelling(result, edit4, member) {
37522
+ if (edit4.from === edit4.to) return;
37523
+ const list = member ? result.memberRespelled : result.respelled;
37524
+ if (list.some((e) => e.kind === edit4.kind && e.specId === edit4.specId && e.position === edit4.position && e.from === edit4.from)) return;
37525
+ list.push(edit4);
37526
+ }
37527
+ function assertedTypeRef(ref) {
37528
+ const at = ref.lastIndexOf(".");
37529
+ return at > 0 && at < ref.length - 1 ? ref.slice(0, at) : void 0;
37530
+ }
37531
+ function sidedTypes(inside, outside) {
37532
+ const types = [
37533
+ ...inside.filter((s) => s.kind === "type").map((s) => ({ spec: s, side: "inside" })),
37534
+ ...outside.filter((s) => s.kind === "type").map((s) => ({ spec: s, side: "outside" }))
37535
+ ];
37536
+ const subsystems = new Set([...inside, ...outside].filter((s) => s.kind === "subsystem").map((s) => nameKey(s.id)));
37537
+ return {
37538
+ qualified: (token) => {
37539
+ const segments = token.split(/::|\./);
37540
+ if (segments.length !== 2 || segments.some((x) => x === "") || !subsystems.has(nameKey(segments[0]))) return void 0;
37541
+ const found = types.filter((t) => typeof t.spec.raw.subsystem === "string" && nameKey(t.spec.raw.subsystem) === nameKey(segments[0]) && nameKey(t.spec.id) === nameKey(segments[1]));
37542
+ return found.length === 1 ? found[0] : void 0;
37543
+ },
37544
+ bare: (token) => {
37545
+ const found = types.filter((t) => nameKey(t.spec.id) === nameKey(token));
37546
+ return found.length === 1 ? found[0] : void 0;
37547
+ }
37548
+ };
37549
+ }
37550
+ function unreachableUpward(inside, outside) {
37551
+ const out = [];
37552
+ const types = sidedTypes(inside, outside);
37553
+ const outsideComponents = new Set(outside.filter((x) => x.kind === "component").map((x) => x.id));
37554
+ const target = (ref) => ref.includes(".") || ref.includes("::") ? types.qualified(ref) : types.bare(ref);
37555
+ for (const spec of inside) {
37556
+ if (spec.kind === "interface") {
37557
+ for (const m of spec.raw.methods ?? []) {
37558
+ const source = m?.signatureFrom;
37559
+ if (typeof source !== "string") continue;
37560
+ const head2 = source.includes(".") ? source.slice(0, source.lastIndexOf(".")) : void 0;
37561
+ if (target(source)?.side === "outside" || head2 !== void 0 && outsideComponents.has(head2)) {
37562
+ out.push(`"${spec.id}" method "${m.name}" takes its signature from "${source}", which stays in this project: a signatureFrom is not read across a project boundary \u2014 state its params and returns first`);
37563
+ }
37564
+ }
37565
+ }
37566
+ if (spec.kind === "implementation") {
37567
+ for (const m of spec.raw.methods ?? []) {
37568
+ for (const step of m?.narrative ?? []) {
37569
+ for (const ref of step?.assertsInvariants ?? []) {
37570
+ const typeRef2 = typeof ref === "string" ? assertedTypeRef(ref) : void 0;
37571
+ if (typeRef2 !== void 0 && !typeRef2.includes("::") && target(typeRef2)?.side === "outside") {
37572
+ out.push(`"${spec.id}" asserts the invariant "${ref}" of an entity that stays in this project: an invariant is asserted by its own project's write paths \u2014 move the assertion first`);
37573
+ }
37574
+ }
37575
+ }
37576
+ }
37577
+ }
37578
+ }
37579
+ return out;
37580
+ }
37581
+ function respellQualified(inside, outside, names, result) {
37582
+ const crossings = { out: [], in: [] };
37583
+ const { qualified: qualifiedTarget, bare: bareTarget } = sidedTypes(inside, outside);
37584
+ const respell = (spec, side, token, target, selfPrefix2) => {
37585
+ if (!target) return token;
37586
+ if (target.side !== side) {
37587
+ const to = `${side === "outside" ? names.alias : names.parentId}::${target.spec.id}`;
37588
+ (side === "inside" ? crossings.out : crossings.in).push({ spec, target: target.spec.id, position: "type" });
37589
+ return to;
37590
+ }
37591
+ return side === "inside" && selfPrefix2 ? target.spec.id : token;
37592
+ };
37593
+ for (const [specs, side] of [[inside, "inside"], [outside, "outside"]]) {
37594
+ for (const spec of specs) {
37595
+ if (!fs16.existsSync(spec.file)) continue;
37596
+ const raw = readYamlFile(spec.file);
37597
+ if (!raw || typeof raw !== "object") continue;
37598
+ const edits = [];
37599
+ const record = (from, to) => {
37600
+ if (from !== to) edits.push({ kind: spec.kind, specId: spec.id, position: "type", from, to });
37601
+ };
37602
+ const mapToken = (token) => {
37603
+ const target = qualifiedTarget(token);
37604
+ const to = respell(spec, side, token, target, nameKey(token.split(/::|\./)[0]) === nameKey(names.newId));
37605
+ record(token, to);
37606
+ return to;
37607
+ };
37608
+ for (const slot of typeExpressionSlots(spec.kind, raw)) {
37609
+ const value = slot.holder[slot.key];
37610
+ if (slot.path === "methods.signatureFrom" && !qualifiedTarget(value)) continue;
37611
+ slot.holder[slot.key] = respellTypeNames(value, mapToken);
37612
+ }
37613
+ if (spec.kind === "implementation") {
37614
+ for (const m of raw.methods ?? []) {
37615
+ for (const step of m?.narrative ?? []) {
37616
+ if (!Array.isArray(step?.assertsInvariants)) continue;
37617
+ step.assertsInvariants = step.assertsInvariants.map((ref) => {
37618
+ const typeRef2 = typeof ref === "string" ? assertedTypeRef(ref) : void 0;
37619
+ if (typeRef2 === void 0 || typeRef2.includes("::") && !qualifiedTarget(typeRef2)) return ref;
37620
+ const target = typeRef2.includes(".") || typeRef2.includes("::") ? qualifiedTarget(typeRef2) : bareTarget(typeRef2);
37621
+ const to = respell(spec, side, typeRef2, target, typeRef2.includes(".") && nameKey(typeRef2.split(".")[0]) === nameKey(names.newId));
37622
+ if (to === typeRef2) return ref;
37623
+ record(ref, `${to}${ref.slice(typeRef2.length)}`);
37624
+ return `${to}${ref.slice(typeRef2.length)}`;
37625
+ });
37626
+ }
37627
+ }
37628
+ }
37629
+ if (edits.length === 0) continue;
37630
+ writeYamlFile(spec.file, raw);
37631
+ for (const e of edits) recordRespelling(result, e, side === "inside");
37632
+ }
37633
+ }
37634
+ return crossings;
37635
+ }
37636
+ function dropCrossingConsumers(inside, outside, result) {
37637
+ const idsOf = (specs) => new Set(specs.filter((s) => s.kind === "subsystem").map((s) => s.id));
37638
+ const sides = [[inside, idsOf(outside)], [outside, idsOf(inside)]];
37639
+ for (const [specs, across] of sides) {
37640
+ for (const spec of specs.filter((s) => s.kind === "subsystem")) {
37641
+ patchKeepingStamp(spec.file, (raw) => {
37642
+ let changed = false;
37643
+ for (const entry of raw.publicInterfaces ?? []) {
37644
+ if (!Array.isArray(entry?.consumers)) continue;
37645
+ const dropped = entry.consumers.filter((c) => typeof c === "string" && across.has(c));
37646
+ if (dropped.length === 0) continue;
37647
+ const kept = entry.consumers.filter((c) => !dropped.includes(c));
37648
+ if (kept.length > 0) entry.consumers = kept;
37649
+ else delete entry.consumers;
37650
+ changed = true;
37651
+ const what = entry.component ?? entry.typeDef ?? entry.interface ?? entry.from ?? "(entry)";
37652
+ result.consumersDropped.push(`${spec.id} ${what}: ${dropped.join(", ")}${kept.length === 0 ? " (the list is gone: the L0 export table governs who outside may use it)" : ""}`);
37653
+ }
37654
+ return changed;
37655
+ });
37656
+ }
37657
+ }
37658
+ }
37659
+ function exportReExported(inside, memberDir, alias, result) {
37660
+ invalidateSpecCache();
37661
+ const entries = (loadSystemSpec()?.publicInterfaces ?? []).filter((e) => e.from === alias);
37662
+ if (entries.length === 0) return;
37663
+ const targets = entries.map((e) => e.component ?? e.typeDef).filter((t) => t !== void 0);
37664
+ const whole = entries.some((e) => e.component === void 0 && e.typeDef === void 0);
37665
+ runWithProjectRoot(memberDir, () => {
37666
+ for (const name of exportItems(targets)) result.exported.push(`${alias} L0: ${name} (re-exported by the parent)`);
37667
+ if (!whole) return;
37668
+ invalidateSpecCache();
37669
+ const system = loadSystemSpec();
37670
+ if (!system) return;
37671
+ const own = system.publicInterfaces ?? [];
37672
+ const added = inside.filter((s) => s.kind === "subsystem" && !own.some((e) => e.from === s.id && e.component === void 0 && e.typeDef === void 0)).map((s) => ({ from: s.id, audience: "project" }));
37673
+ if (added.length === 0) return;
37674
+ saveSystemSpec({ ...system, publicInterfaces: [...own, ...added] });
37675
+ for (const e of added) result.exported.push(`${alias} L0: everything ${e.from} exports (re-exported whole by the parent)`);
37676
+ });
37677
+ invalidateSpecCache();
36878
37678
  }
36879
37679
  function promotedVision(id, parentName) {
36880
37680
  return `Project ${id}, promoted from a part of ${parentName}`;
@@ -36882,43 +37682,57 @@ function promotedVision(id, parentName) {
36882
37682
  function unique(value, index, all) {
36883
37683
  return all.indexOf(value) === index;
36884
37684
  }
36885
- function exportCrossings(crossings, memberDir, alias, parentId, direction) {
37685
+ function exportCrossings(crossings, memberDir, alias, parentId, direction, result) {
36886
37686
  const targets = [...new Set(crossings.map((c) => c.target))];
36887
37687
  const bare = [...new Set(crossings.filter((c) => c.position === "bare-type").map((c) => c.target))];
36888
37688
  if (direction === "out") {
36889
- exportItems(targets);
36890
- if (bare.length > 0) projectConfigRepositoryAt(memberDir).importNames(parentId, bare);
37689
+ for (const name of exportItems(targets)) result.exported.push(`${parentId} L0: ${name}`);
37690
+ if (bare.length > 0) {
37691
+ projectConfigRepositoryAt(memberDir).importNames(parentId, bare);
37692
+ result.imported.push(`${alias}: externals.${parentId}.use gains ${bare.join(", ")}`);
37693
+ }
36891
37694
  } else {
36892
- runWithProjectRoot(memberDir, () => exportItems(targets));
36893
- if (bare.length > 0) projectConfigRepository.importNames(alias, bare);
37695
+ for (const name of runWithProjectRoot(memberDir, () => exportItems(targets))) result.exported.push(`${alias} L0: ${name}`);
37696
+ if (bare.length > 0) {
37697
+ projectConfigRepository.importNames(alias, bare);
37698
+ result.imported.push(`${parentId}: members.${alias}.use gains ${bare.join(", ")}`);
37699
+ }
36894
37700
  }
36895
37701
  }
36896
37702
  function localIn(key, project) {
36897
37703
  return project && key.startsWith(`${project}::`) ? key.slice(project.length + 2) : key;
36898
37704
  }
36899
37705
  function exportItems(ids) {
36900
- if (ids.length === 0) return;
37706
+ if (ids.length === 0) return [];
36901
37707
  invalidateSpecCache();
36902
37708
  const system = loadSystemSpec();
36903
- if (!system) return;
37709
+ if (!system) return [];
36904
37710
  const entries = [...system.publicInterfaces ?? []];
36905
37711
  const types = loadTypeSpecs();
37712
+ const added = [];
36906
37713
  for (const id of ids) {
36907
37714
  if (!PUBLIC_NAME_RE.test(id)) continue;
36908
37715
  const type = types.find((t) => t.id === id);
36909
37716
  if (type) {
36910
37717
  if (type.subsystem) exportTypeFromSubsystem(type.subsystem, id);
36911
- if (!entries.some((e) => e.typeDef === id && e.from === void 0)) entries.push({ typeDef: id, audience: "project" });
37718
+ if (!entries.some((e) => e.typeDef === id && (e.from === void 0 || e.from === type.subsystem))) {
37719
+ entries.push({ ...type.subsystem ? { from: type.subsystem } : {}, typeDef: id, audience: "project" });
37720
+ added.push(type.subsystem ? `${id} (from ${type.subsystem})` : id);
37721
+ }
36912
37722
  continue;
36913
37723
  }
36914
37724
  const component = loadComponentSpec(id);
36915
37725
  if (!component) continue;
36916
37726
  const exported = resolveSubsystemExports(component.subsystem).entries.some((e) => e.kind === "component" && e.component === id);
36917
- if (exported && !entries.some((e) => e.component === id)) entries.push({ from: component.subsystem, component: id, audience: "project" });
37727
+ if (exported && !entries.some((e) => e.component === id)) {
37728
+ entries.push({ from: component.subsystem, component: id, audience: "project" });
37729
+ added.push(`${id} (from ${component.subsystem})`);
37730
+ }
36918
37731
  }
36919
37732
  if (entries.length !== (system.publicInterfaces ?? []).length) {
36920
37733
  saveSystemSpec({ ...system, publicInterfaces: entries });
36921
37734
  }
37735
+ return added;
36922
37736
  }
36923
37737
  function exportTypeFromSubsystem(subsystemId, typeId) {
36924
37738
  const sub = loadSubsystemSpec(subsystemId);
@@ -36962,7 +37776,7 @@ function internalizeMember(alias, destination) {
36962
37776
  if (refusals.length > 0) {
36963
37777
  throw new WaironError(`cannot internalize "${alias}": the member cannot be taken in whole \u2014 ${refusals.join("; ")}.`);
36964
37778
  }
36965
- const result = { subsystems: scan.subsystems, types: scan.types, placed: [], externals: [], deleted: [], notCarried: [] };
37779
+ const result = { subsystems: scan.subsystems, types: scan.types, placed: [], externals: [], deleted: [], notCarried: [], consumersRestored: [] };
36966
37780
  const intoMember = crossingReferences(family, "", member.namespace);
36967
37781
  const backOut = crossingReferences(family, member.namespace, "");
36968
37782
  deleteMount(alias);
@@ -36992,7 +37806,7 @@ function internalizePart(alias, part) {
36992
37806
  projectConfigRepository.removeMember(alias);
36993
37807
  const deleted = deleteMemberWai(partDir);
36994
37808
  invalidateSpecCache();
36995
- return { subsystems: part.subsystems, types, placed: [], externals: [], deleted, notCarried: [] };
37809
+ return { subsystems: part.subsystems, types, placed: [], externals: [], deleted, notCarried: [], consumersRestored: [] };
36996
37810
  }
36997
37811
  function scanMember(family, alias, member, destination) {
36998
37812
  const root = getProjectRoot();
@@ -37341,7 +38155,7 @@ function demoteMember(alias, destination) {
37341
38155
  refusals.push(`its metadata needs a home subsystem (--home) \u2014 ${home === "" ? "none was named" : `"${home}" is no subsystem of the member or of this project`}; its subsystems are ${subsystems.join(", ") || "none"}`);
37342
38156
  }
37343
38157
  if (refusals.length > 0) throw new WaironError(`demote refused: "${alias}" cannot become a part \u2014 ${refusals.join("; ")}.`);
37344
- const result = { subsystems, types, placed: [], externals: [], deleted: [], notCarried: [] };
38158
+ const result = { subsystems, types, placed: [], externals: [], deleted: [], notCarried: [], consumersRestored: [] };
37345
38159
  const intoMember = localsOf(dir);
37346
38160
  const backOut = localsOf(root);
37347
38161
  invalidateSpecCache();
@@ -37358,6 +38172,10 @@ function demoteMember(alias, destination) {
37358
38172
  const parentSide = rewriteRefsIn(ownSpecs, across(/* @__PURE__ */ new Set([alias]), intoMember, false));
37359
38173
  const memberSide = rewriteRefsIn(memberSpecs, across(parentAliases, backOut, true));
37360
38174
  for (const pa of parentAliases) for (const name of config?.externals?.[pa]?.use ?? []) if (name !== "*") crossed.add(backOut.get(name) ?? name);
38175
+ const memberNow = rawSpecs(specFilesUnder(aiPathsAt(dir).specsDir()).filter((f) => path25.resolve(f) !== path25.resolve(aiPathsAt(dir).specsSystem())));
38176
+ const parentNow = rawSpecs(ownSpecFiles(family, aiPathsAt(dir).specsDir()));
38177
+ respellQualifiedBack(parentNow, memberNow, { intoMember: (a) => a === alias, backOut: (a) => parentAliases.has(a) }, { intoMember, backOut }, crossed);
38178
+ restoreCrossingConsumers(memberNow, parentNow, result);
37361
38179
  const parentName = loadSystemSpec()?.name;
37362
38180
  if (parentName) {
37363
38181
  for (const s of memberSpecs.filter((x) => x.kind === "subsystem")) {
@@ -37382,6 +38200,79 @@ function demoteMember(alias, destination) {
37382
38200
  invalidateSpecCache();
37383
38201
  return result;
37384
38202
  }
38203
+ function respellQualifiedBack(parentSide, memberSide, aliases, tables, crossed) {
38204
+ const types = [...parentSide, ...memberSide].filter((s) => s.kind === "type");
38205
+ const localForm = (id) => {
38206
+ const found = types.filter((t) => t.id === id);
38207
+ if (found.length !== 1) return void 0;
38208
+ const sub = found[0].raw?.subsystem;
38209
+ return typeof sub === "string" && sub !== "" ? `${sub}.${id}` : id;
38210
+ };
38211
+ for (const [specs, across, table, record] of [
38212
+ [parentSide, aliases.intoMember, tables.intoMember, false],
38213
+ [memberSide, aliases.backOut, tables.backOut, true]
38214
+ ]) {
38215
+ const local = (token) => {
38216
+ const segments = token.split("::");
38217
+ if (segments.length !== 2 || !across(segments[0])) return token;
38218
+ const id = table.get(segments[1]) ?? segments[1];
38219
+ const to = localForm(id);
38220
+ if (to === void 0) return token;
38221
+ if (record) crossed.add(id);
38222
+ return to;
38223
+ };
38224
+ for (const spec of specs) {
38225
+ if (!fs16.existsSync(spec.file)) continue;
38226
+ const raw = readYamlFile(spec.file);
38227
+ if (!raw || typeof raw !== "object") continue;
38228
+ let changed = false;
38229
+ for (const slot of typeExpressionSlots(spec.kind, raw)) {
38230
+ const value = slot.holder[slot.key];
38231
+ const next = respellTypeNames(value, local);
38232
+ if (next !== value) {
38233
+ slot.holder[slot.key] = next;
38234
+ changed = true;
38235
+ }
38236
+ }
38237
+ if (spec.kind === "implementation") {
38238
+ for (const m of raw.methods ?? []) {
38239
+ for (const step of m?.narrative ?? []) {
38240
+ if (!Array.isArray(step?.assertsInvariants)) continue;
38241
+ step.assertsInvariants = step.assertsInvariants.map((ref) => {
38242
+ const typeRef2 = typeof ref === "string" ? assertedTypeRef(ref) : void 0;
38243
+ if (typeRef2 === void 0 || !typeRef2.includes("::")) return ref;
38244
+ const to = local(typeRef2);
38245
+ if (to === typeRef2) return ref;
38246
+ changed = true;
38247
+ return `${to}${ref.slice(typeRef2.length)}`;
38248
+ });
38249
+ }
38250
+ }
38251
+ }
38252
+ if (changed) writeYamlFile(spec.file, raw);
38253
+ }
38254
+ }
38255
+ }
38256
+ function restoreCrossingConsumers(memberSide, parentSide, result) {
38257
+ const sides = [[memberSide, parentSide], [parentSide, memberSide]];
38258
+ for (const [providers, consumers] of sides) {
38259
+ const dependents = consumers.filter((c) => c.kind === "component" && typeof c.raw?.subsystem === "string" && Array.isArray(c.raw?.dependsOn));
38260
+ for (const spec of providers.filter((x) => x.kind === "subsystem")) {
38261
+ patchKeepingStamp(spec.file, (raw) => {
38262
+ let changed = false;
38263
+ for (const entry of raw.publicInterfaces ?? []) {
38264
+ if (!Array.isArray(entry?.consumers) || entry.consumers.length === 0 || typeof entry.component !== "string") continue;
38265
+ const users = [...new Set(dependents.filter((c) => c.raw.dependsOn.includes(entry.component)).map((c) => c.raw.subsystem))].filter((sub) => sub !== spec.id && !entry.consumers.includes(sub));
38266
+ if (users.length === 0) continue;
38267
+ entry.consumers = [...entry.consumers, ...users];
38268
+ changed = true;
38269
+ result.consumersRestored.push(`${spec.id} ${entry.component}: ${users.join(", ")}`);
38270
+ }
38271
+ return changed;
38272
+ });
38273
+ }
38274
+ }
38275
+ }
37385
38276
  function deleteProjectContent(memberDir) {
37386
38277
  const wai = path25.join(memberDir, ".wai");
37387
38278
  const deleted = [];
@@ -37411,9 +38302,9 @@ function retireBoundaryExports(family, used, memberKey) {
37411
38302
  const { publicInterfaces: _dropped, ...rest } = system;
37412
38303
  saveSystemSpec(kept.length > 0 ? { ...rest, publicInterfaces: kept } : rest);
37413
38304
  invalidateSpecCache();
37414
- for (const e of retired.filter((x) => x.typeDef !== void 0 && x.from === void 0)) {
38305
+ for (const e of retired.filter((x) => x.typeDef !== void 0)) {
37415
38306
  const type = loadTypeSpecs().find((t) => t.id === e.typeDef);
37416
- if (!type?.subsystem) continue;
38307
+ if (!type?.subsystem || e.from !== void 0 && e.from !== type.subsystem) continue;
37417
38308
  patchKeepingStamp(getSubsystemPath(type.subsystem), (raw) => {
37418
38309
  const before = (raw.publicInterfaces ?? []).length;
37419
38310
  raw.publicInterfaces = (raw.publicInterfaces ?? []).filter((pi) => !(pi?.typeDef === e.typeDef && pi?.from === void 0 && Object.keys(pi).length === 1));
@@ -37436,7 +38327,7 @@ function advanceMember(alias, ref, commit2, dryRun) {
37436
38327
  const url = member.source.git;
37437
38328
  const from = member.source.commit ?? "";
37438
38329
  const followed = commit2 === void 0 ? ref ?? member.source.ref : void 0;
37439
- const to = commit2 ?? resolve7(url, followed);
38330
+ const to = commit2 ?? resolve3(url, followed);
37440
38331
  const after = specContents(fetch(url, to, member.source.dir));
37441
38332
  const before = specContents(from !== "" ? fetch(url, from, member.source.dir) : null);
37442
38333
  const added = [...after.keys()].filter((f) => !before.has(f)).sort();
@@ -37582,7 +38473,7 @@ function renameComponent(componentId, newId) {
37582
38473
  }
37583
38474
  if (componentId.includes("::")) {
37584
38475
  throw new WaironError(
37585
- `chained-component: "${componentId}" lives in a chained subproject; rename it from that project's own root.`
38476
+ `chained-component: "${componentId}" lives in another project; rename it from that project's own root. ${memberRootWay(componentId, `sdd_rename_component ${componentId.slice(componentId.lastIndexOf("::") + 2)} <new id>`)}`
37586
38477
  );
37587
38478
  }
37588
38479
  if (!SpecIdSchema.safeParse(newId).success) {
@@ -37745,7 +38636,7 @@ function renameMethod(componentId, methodName, newName, pinSymbol) {
37745
38636
  }
37746
38637
  if (componentId.includes("::")) {
37747
38638
  throw new WaironError(
37748
- `chained-component: "${componentId}" lives in a chained subproject; rename its method from that project's own root.`
38639
+ `chained-component: "${componentId}" lives in another project; rename its method from that project's own root. ${memberRootWay(componentId, `sdd_rename_method ${componentId.slice(componentId.lastIndexOf("::") + 2)} <method> <new name>`)}`
37749
38640
  );
37750
38641
  }
37751
38642
  if (!METHOD_NAME.test(newName)) {
@@ -37903,18 +38794,29 @@ function respellLike(written, oldId, newId) {
37903
38794
  if (written.includes("-")) return kebab(newId);
37904
38795
  return /^[A-Z]/.test(written) ? pascal(newId) : newId;
37905
38796
  }
38797
+ function memberRootWay(qualifiedId, call) {
38798
+ const alias = qualifiedId.slice(0, qualifiedId.indexOf("::"));
38799
+ let folder;
38800
+ try {
38801
+ const node = graph().nodes.find((n) => n.namespace === alias || n.parent === "" && n.mountAlias === alias);
38802
+ if (node) folder = path25.relative(getProjectRoot(), node.directory).split(path25.sep).join("/") || ".";
38803
+ } catch {
38804
+ }
38805
+ const where = folder ? `the member project "${alias}" at ${folder}` : `the member project "${alias}"`;
38806
+ return `It belongs to ${where}: open a session in that folder (its own guide and .mcp.json \u2014 run \`wairon generate\` and \`wairon mcp install --backend claude\` there first if it has none) and call ${call} there.`;
38807
+ }
37906
38808
  function renameType(typeId, newId) {
37907
38809
  const all = loadTypeSpecs();
37908
38810
  const cut = typeId.lastIndexOf("::");
37909
38811
  const qualifier = cut >= 0 ? typeId.slice(0, cut) : void 0;
37910
38812
  const ownSubsystem = qualifier !== void 0 && loadSubsystemSpecs().some((s) => s.id === qualifier);
37911
38813
  if (qualifier !== void 0 && !ownSubsystem) {
37912
- throw new WaironError(`chained-type: "${typeId}" lives in another project; rename it from that project's own root.`);
38814
+ throw new WaironError(`chained-type: "${typeId}" lives in another project; rename it from that project's own root. ${memberRootWay(typeId, `sdd_rename_type ${typeId.slice(cut + 2)} <new id>`)} An exported type keeps its public name through \`as\`, so this project's references do not break.`);
37913
38815
  }
37914
38816
  const type = qualifier !== void 0 ? all.find((t) => t.id === typeId.slice(cut + 2) && t.subsystem === qualifier) ?? null : loadTypeSpec(typeId);
37915
38817
  if (!type) throw new WaironError(`type-missing: no type has the id "${typeId}".`);
37916
38818
  if (type.id.includes("::")) {
37917
- throw new WaironError(`chained-type: "${type.id}" lives in another project; rename it from that project's own root.`);
38819
+ throw new WaironError(`chained-type: "${type.id}" lives in another project; rename it from that project's own root. ${memberRootWay(type.id, `sdd_rename_type ${type.id.slice(type.id.lastIndexOf("::") + 2)} <new id>`)}`);
37918
38820
  }
37919
38821
  const oldId = type.id;
37920
38822
  if (!SpecIdSchema.safeParse(newId).success) {
@@ -38202,8 +39104,8 @@ function walkForPackages(projectRoot, currentDir, depth, results) {
38202
39104
  }
38203
39105
  }
38204
39106
  function pathToId(relPath) {
38205
- const basename12 = path26.basename(relPath);
38206
- return basename12.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
39107
+ const basename13 = path26.basename(relPath);
39108
+ return basename13.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
38207
39109
  }
38208
39110
  function pathToName(relPath) {
38209
39111
  const id = pathToId(relPath);
@@ -38687,7 +39589,7 @@ function isWithinDir3(dir, target) {
38687
39589
  const rel2 = path29.relative(path29.resolve(dir), path29.resolve(target));
38688
39590
  return rel2 === "" || !rel2.startsWith("..") && !path29.isAbsolute(rel2);
38689
39591
  }
38690
- function resolveLayer(delegate) {
39592
+ function resolveLayer(delegate, implementers = false) {
38691
39593
  projectFilesCache.clear();
38692
39594
  const system = loadSystemSpec();
38693
39595
  if (!system) return [];
@@ -38818,7 +39720,7 @@ function resolveLayer(delegate) {
38818
39720
  updatedAt: sub.updatedAt
38819
39721
  });
38820
39722
  }
38821
- if (config.rules.generateComponentImplementers) {
39723
+ if (config.rules.generateComponentImplementers || implementers) {
38822
39724
  for (const comp of components) {
38823
39725
  const compInterfaces = interfaces.filter((i) => i.component === comp.id);
38824
39726
  const compInterfaceIds = compInterfaces.map((i) => i.id);
@@ -38886,7 +39788,7 @@ function resolveLayer(delegate) {
38886
39788
  }
38887
39789
  var UnknownAgentError = class extends WaironError {
38888
39790
  constructor(agentId, knownIds) {
38889
- super(`Unknown agent id: "${agentId}". Known agent ids: ${knownIds.join(", ")}`);
39791
+ super(`Unknown agent id: "${agentId}". Known agent ids: ${knownIds.join(", ")} \u2014 or any component id of this project, which composes that component's implementer brief.`);
38890
39792
  this.name = "UnknownAgentError";
38891
39793
  }
38892
39794
  };
@@ -38897,7 +39799,7 @@ function composeAgentBrief(agentId) {
38897
39799
  return throughMount(brief, hop.alias, hop.relative);
38898
39800
  }
38899
39801
  const records = resolveAgentTopology();
38900
- const record = records.find((r) => r.id === agentId);
39802
+ const record = records.find((r) => r.id === agentId) ?? componentImplementer(agentId);
38901
39803
  if (!record) {
38902
39804
  throw new UnknownAgentError(agentId, records.map((r) => r.id));
38903
39805
  }
@@ -38916,6 +39818,17 @@ function composeAgentBrief(agentId) {
38916
39818
  domainName: "",
38917
39819
  variantGuidance: record.variantGuidance ?? ""
38918
39820
  });
39821
+ const codeFence = codeFenceOf(record);
39822
+ if (codeFence) {
39823
+ instructions = `${instructions.trimEnd()}
39824
+
39825
+ ## Code write fence
39826
+
39827
+ ${codeFence.length > 0 ? `Write code only here (and tests beside it, as the project lays tests out):
39828
+
39829
+ ${codeFence.map((p) => `- \`${p}\``).join("\n")}
39830
+ ` : "No implementation names a source file yet, so no code location is declared. The spawning session must name the file(s) this agent may create (the planned sourcePath); set `sourcePath` on the implementation once the file exists.\n"}`;
39831
+ }
38919
39832
  const guidance = loadAgentOverride(agentId);
38920
39833
  if (guidance !== null) {
38921
39834
  instructions = `${instructions.trimEnd()}
@@ -38950,10 +39863,34 @@ ${typeMapping.map((line) => `- ${line}`).join("\n")}
38950
39863
  instructions,
38951
39864
  variantGuidance: record.variantGuidance || void 0,
38952
39865
  ...typeMapping ? { typeMapping } : {},
39866
+ ...codeFence ? { codeFence } : {},
38953
39867
  profile: budget ? profile : void 0,
38954
39868
  budget
38955
39869
  };
38956
39870
  }
39871
+ function componentImplementer(agentId) {
39872
+ const componentId = agentId.endsWith("-implementer") ? agentId.slice(0, -"-implementer".length) : agentId;
39873
+ if (componentId === "" || componentId.includes("::")) return null;
39874
+ return resolveLayer(true, true).find((r) => r.id === `${componentId}-implementer`) ?? null;
39875
+ }
39876
+ function codeFenceOf(record) {
39877
+ const implementsComponents = record.template === "implementer" || record.template === "domain-owner" && record.creationReason.startsWith("Automatically inferred from L1");
39878
+ if (!implementsComponents) return null;
39879
+ const files = record.ownedPaths.filter((p) => !p.startsWith(".wai/") && !p.includes("/.wai/") && !/[*?]/.test(p));
39880
+ if (files.length === 0) return [];
39881
+ const dirs = files.map((f) => path29.posix.dirname(f.replace(/\\/g, "/")));
39882
+ let shared = dirs[0].split("/");
39883
+ for (const dir of dirs.slice(1)) {
39884
+ const parts = dir.split("/");
39885
+ let i = 0;
39886
+ while (i < shared.length && i < parts.length && shared[i] === parts[i]) i++;
39887
+ shared = shared.slice(0, i);
39888
+ }
39889
+ const folder = shared.join("/");
39890
+ const fence = [...files];
39891
+ if (folder !== "" && folder !== ".") fence.push(`${folder}/**`);
39892
+ return fence;
39893
+ }
38957
39894
  function implementationLanguage(record) {
38958
39895
  const owned = new Set(record.ownedPaths);
38959
39896
  const implementations = loadImplementationSpecs().filter((impl) => implementationSourceFiles(impl).some((file) => owned.has(file)));
@@ -38977,17 +39914,18 @@ function memberHop(agentId) {
38977
39914
  const alias = agentId.slice(0, at);
38978
39915
  const member = graph().nodes.find((n) => n.parent === "" && n.mountAlias === alias);
38979
39916
  if (!member) return null;
38980
- const relative19 = path29.relative(getProjectRoot(), member.directory).split(path29.sep).join("/");
38981
- return { alias, rest: agentId.slice(at + 2), directory: member.directory, relative: relative19 };
39917
+ const relative20 = path29.relative(getProjectRoot(), member.directory).split(path29.sep).join("/");
39918
+ return { alias, rest: agentId.slice(at + 2), directory: member.directory, relative: relative20 };
38982
39919
  }
38983
- function throughMount(brief, alias, relative19) {
38984
- const under = (p) => path29.posix.join(relative19, p);
39920
+ function throughMount(brief, alias, relative20) {
39921
+ const under = (p) => path29.posix.join(relative20, p);
38985
39922
  return {
38986
39923
  ...brief,
38987
39924
  agentId: `${alias}::${brief.agentId}`,
38988
39925
  ownedPaths: brief.ownedPaths.map(under),
39926
+ ...brief.codeFence ? { codeFence: brief.codeFence.map(under) } : {},
38989
39927
  ...brief.readPaths ? { readPaths: brief.readPaths.map(under) } : {},
38990
- root: brief.root ? under(brief.root) : relative19
39928
+ root: brief.root ? under(brief.root) : relative20
38991
39929
  };
38992
39930
  }
38993
39931
  function loadRegistry() {
@@ -39266,19 +40204,19 @@ function commitStagedTree(stagingDir, destDir, roots, backupExisting) {
39266
40204
  const backupDir = path34.join(root, BACKUP_DIR, timestampSlug());
39267
40205
  let backedUp = false;
39268
40206
  for (const rootRel of roots) {
39269
- const relative19 = rootRel === "." ? "" : rootRel;
39270
- const stagedSpecDir = path34.join(stagingDir, relative19, ".wai");
40207
+ const relative20 = rootRel === "." ? "" : rootRel;
40208
+ const stagedSpecDir = path34.join(stagingDir, relative20, ".wai");
39271
40209
  if (!fs20.existsSync(stagedSpecDir)) continue;
39272
- const liveSpecDir = liveSpecDirFor(path34.join(root, relative19));
40210
+ const liveSpecDir = liveSpecDirFor(path34.join(root, relative20));
39273
40211
  if (backupExisting && liveSpecDir && fs20.existsSync(liveSpecDir)) {
39274
- const backupTarget = path34.join(backupDir, relative19, path34.basename(liveSpecDir));
40212
+ const backupTarget = path34.join(backupDir, relative20, path34.basename(liveSpecDir));
39275
40213
  fs20.mkdirSync(path34.dirname(backupTarget), { recursive: true });
39276
40214
  fs20.renameSync(liveSpecDir, backupTarget);
39277
40215
  backedUp = true;
39278
40216
  } else if (liveSpecDir && fs20.existsSync(liveSpecDir)) {
39279
40217
  fs20.rmSync(liveSpecDir, { recursive: true, force: true });
39280
40218
  }
39281
- const target = path34.join(root, relative19, ".wai");
40219
+ const target = path34.join(root, relative20, ".wai");
39282
40220
  fs20.mkdirSync(path34.dirname(target), { recursive: true });
39283
40221
  if (fs20.existsSync(target)) fs20.rmSync(target, { recursive: true, force: true });
39284
40222
  fs20.renameSync(stagedSpecDir, target);
@@ -39557,7 +40495,7 @@ function repairTypeSpellings(apply3) {
39557
40495
  const key = `${kind}:${specId}`;
39558
40496
  let repair = repairs.get(key);
39559
40497
  if (!repair) {
39560
- repair = { specId, kind, rewritten: [], proposals: [], authorNeeded: [] };
40498
+ repair = { specId, kind, rewritten: [], proposals: [], enumProposals: [], authorNeeded: [] };
39561
40499
  repairs.set(key, repair);
39562
40500
  }
39563
40501
  return repair;
@@ -39572,7 +40510,9 @@ function repairTypeSpellings(apply3) {
39572
40510
  if (problem.specId === void 0 || problem.kind === void 0 || !own(problem.specId)) continue;
39573
40511
  const repair = repairOf(problem.specId, problem.kind);
39574
40512
  const proposal = typeProblemIntProposal(problem);
40513
+ const enumProposal = proposal ? null : typeProblemEnumProposal(problem);
39575
40514
  if (proposal) repair.proposals.push(proposal);
40515
+ else if (enumProposal) repair.enumProposals.push(enumProposal);
39576
40516
  else repair.authorNeeded.push(problem);
39577
40517
  }
39578
40518
  const planned = [...repairs.values()];
@@ -39713,13 +40653,14 @@ Approved: ${approval.lockedAt} by ${by} \u2014 no spec has changed since.${child
39713
40653
  ${headline} \u2014 approved ${approval.lockedAt} by ${by}:
39714
40654
  ` + named2.map((p) => ` ${p}`).join("\n") + (rest > 0 ? `
39715
40655
  \u2026 and ${rest} more` : "") + childNote + "\n",
39716
- drifted: true
40656
+ drifted: true,
40657
+ moved: [...diff2.changed.map((p) => `~ ${p}`), ...diff2.added.map((p) => `+ ${p}`), ...diff2.removed.map((p) => `- ${p}`)]
39717
40658
  };
39718
40659
  } catch {
39719
40660
  return quiet;
39720
40661
  }
39721
40662
  }
39722
- var GATE_UPGRADE_NOTE = "the gate identity gained inputs in stage 5 (members' composition subjects, `composition`; code conformance moved beside the claim)";
40663
+ var GATE_UPGRADE_NOTE = "the gate identity gained inputs in this release (members' composition subjects, `composition`; code conformance moved beside the claim)";
39723
40664
  function memberNotes(approvals) {
39724
40665
  const own = approvals.find((a) => a.key === "");
39725
40666
  const direct = approvals.filter((a) => a.parent === "" && a.as !== "part");
@@ -39801,33 +40742,34 @@ var GLOBAL_GUIDE_BODY = `## wairon \u2014 Spec-Driven Development (optional)
39801
40742
  If \`.wai/specs/\` exists, the wairon SDD workflow is active; otherwise ignore it. wairon does not orchestrate sessions \u2014 it equips yours.
39802
40743
 
39803
40744
  ### In SDD Projects:
39804
- - **Source of Truth**: All architecture lives in the spec tree under \`.wai/specs/\` (L0 System \u2192 L1 Subsystem \u2192 L2 Component \u2192 L3 Interface \u2192 L4 Implementation \u2192 L5 Narrative). Do not edit generated agent config files under \`.claude/agents/\` (rebuilt via \`wairon generate\`).
40745
+ - **Source of Truth**: All architecture lives in the spec tree under \`.wai/specs/\` (L0 System \u2192 L1 Subsystem \u2192 L2 Component \u2192 L3 Interface \u2192 L4 Implementation \u2192 L5 Narrative). Agent files under \`.claude/agents/\` are an opt-in generated view (\`rules.materializeAgentFiles\`) \u2014 never edit them; agents are served as live briefs.
39805
40746
  - **Validation**: Conformance checks (stereotype rules, cycle checks, reference integrity) are run via the \`sdd_validate_tree\` MCP tool.
39806
40747
  - **Operating Rules**:
39807
40748
  1. **Skills**: Use \`sdd-architect\` to design (and \`sdd-implement\`, \`sdd-narrative\`, \`sdd-auditor\`). Refer to project's local guide file for detailed constraints.
39808
40749
  2. **MCP Tools Only**: Author/validate specs *only* via \`sdd_*\` tools (e.g. \`sdd_initialize_system\`, \`sdd_validate_tree\`).
39809
40750
  3. **No CLI Exec**: Do not run the \`wairon\` CLI (human tool). Use MCP tools \`sdd_validate_tree\` and \`sdd_get_status\` instead.
39810
40751
  4. **Delegation**: Delegate implementation via the \`sdd-delegate\` skill \u2014 live agent briefs (\`sdd_get_agent_brief\` MCP tool / \`wairon-agent://\` resource) are composed per call and always current; no session restart. Generated agent files are an optional materialized view of the same topology. User-owned per-agent guidance may live in \`.wai/agents/<agent-id>.md\` (folded into every brief; scaffold via \`wairon agent customize <id>\`).
39811
- 5. **Design First**: Complete spec and pass \`sdd_validate_tree\` before writing code.
40752
+ 5. **Design First, then approval**: Complete the spec and pass \`sdd_validate_tree\`, then ask the human to approve it with \`wairon lock\` before writing code. Approval is the lock record (\`.wai/lock.json\`), not a spec's \`status\` \u2014 the lock does not rewrite statuses; \`sdd_get_status\` reports the approval state.
39812
40753
  6. **Consistency**: Code must match L3 interfaces and L5 narratives exactly. If the spec is wrong, stop and update the spec.
39813
- 7. **Members & References**: A project may contain other wairon projects as **members**, declared in its \`.wai/project.yaml\` \`members\` (create one with \`sdd_add_member\`); each member has its own spec tree, designed from its own root. Reference what another project exports as \`alias::name\` (the alias is a member or a declared external, the name a public name of its L0 export table); an id without \`::\` is local. A leading \`::\`, \`super::\`, member paths and an L1 subsystem carrying \`projectPath\` are deprecated: they still resolve for one release, are reported, and \`wairon doctor --fix\` rewrites them.`;
40754
+ 7. **Members & References**: A project may declare **members** in its \`.wai/project.yaml\` \`members\` (create one with \`sdd_add_member\`). A **part** (the default) stores some of this project's subsystems in another folder or repository: local ids, this project's lock. A **project** member is an independent boundary with its own spec tree and lock, designed from its own root. Reference what another project exports as \`alias::name\` (the alias is a member or a declared external, the name a public name of its L0 export table); an id without \`::\` is local. A leading \`::\`, \`super::\`, member paths and an L1 subsystem carrying \`projectPath\` are deprecated: they still resolve for one release, are reported, and \`wairon doctor --fix\` rewrites them.`;
39814
40755
  var LOCAL_GUIDE_BODY = `## Wairon \u2014 Spec-Driven Development (you are operating inside it)
39815
40756
 
39816
40757
  This project uses **wairon**. System specs live under \`.wai/specs/\` (L0 System \u2192 L1 Subsystem \u2192 L2 Component \u2192 L3 Interface \u2192 L4 Implementation \u2192 Narrative); agent topology and code are derived from it.
39817
40758
 
39818
40759
  **Do NOT search files or read agent configs to learn about wairon or SDD. Use the context here and the \`sdd-architect\` skill to start.**
39819
40760
 
39820
- **Your first move: call the \`sdd_get_status\` MCP tool** (or \`wairon/sdd_get_status\`) to see the spec tree. Do not parse files or run CLI commands manually.
40761
+ **Your first move: call the \`sdd_get_status\` MCP tool** (or \`wairon/sdd_get_status\`) to see the spec tree. Do not parse files or run CLI commands manually. If no \`sdd_*\` tools are available, this folder is not the root of the project this guide came from \u2014 say so instead of guessing.
39821
40762
 
39822
40763
  ### How you operate
39823
40764
  - **To design/modify specs**: Use **\`sdd-architect\`** skill (in \`.claude/skills/\` or \`.gemini/skills/\`).
39824
40765
  - **Manage specs via MCP tools only**: Use \`sdd_initialize_system\`, \`sdd_add_subsystem\`, \`sdd_add_component\`, \`sdd_define_interface\`, \`sdd_write_narrative\`, \`sdd_add_type\`, \`sdd_get_spec\`, \`sdd_delete_spec\`, \`sdd_validate_tree\`, and \`sdd_get_status\` (namespaced if needed). Do not edit specs manually.
39825
40766
  - **Growing a system \u2014 subsystems \u2192 parts \u2192 projects**: boundaries are earned. Start with subsystems in one folder. A piece that needs its own folder or repository but stays the same system is a **part**: declared in \`.wai/project.yaml\` \`members\` by one location key (\`scheduler: services/scheduler\`, \`admin: ../admin\`, \`payments: git@host:acme/payments.git#<commit>\`; long form \`{ source, as, description }\`), its subsystems this project's own \u2014 local ids, this project's lock. \`sdd_add_member\` creates a part by default (\`as: project\` for a project) and \`sdd_externalize_subsystem\` moves a subsystem into one. A **project** is an independent boundary \u2014 its own id, exports, lock \u2014 reached as \`alias::name\`; make one only when it needs its own team, release, approval or public surface: \`sdd_promote_member\` (\`sdd_demote_member\` back). What a member is follows from its content (an id, an L0 or a lock make a project).
39826
- - **Members & cross-project references**: relocate a member with \`sdd_move_member\`. Every other change of the family's shape is a **family migration**, each with \`dryRun\`: promote/demote, make an existing project a member with \`sdd_attach_member\`, take one out and back with \`sdd_detach_member\` / \`sdd_adopt_member\`, rename a project's id with \`sdd_rename_project\` or an alias with \`sdd_rename_member_alias\`, move a subsystem into a part with \`sdd_externalize_subsystem\` and fold a member back in with \`sdd_internalize_member\`. Run it with \`dryRun: true\` first and show the plan; applied, it writes every project it touches or none, and never locks \u2014 it names the projects to re-lock. A project member is never a subsystem of its parent: it has its own \`.wai/\` tree and is designed from its own root.
40767
+ - **Members & cross-project references**: relocate a member with \`sdd_move_member\`. Every other change of the family's shape is a **family migration**, each with \`dryRun\`: promote/demote, make an existing project a member with \`sdd_attach_member\`, take one out and back with \`sdd_detach_member\` / \`sdd_adopt_member\`, rename a project's id with \`sdd_rename_project\` or an alias with \`sdd_rename_member_alias\`, move a subsystem into a part with \`sdd_externalize_subsystem\` and fold a member back in with \`sdd_internalize_member\`. Run it with \`dryRun: true\` first and show the plan (the human's CLI calls the same plan \`--report\`); applied, it writes every project it touches or none, and never locks \u2014 it names the projects to re-lock. A project member is never a subsystem of its parent: it has its own \`.wai/\` tree and is designed from its own root \u2014 its specs, its L0 export table and its \`project.yaml\` are written by a session opened in that member's folder (its own guide and \`.mcp.json\`), and the tools here refuse such a write naming that folder.
39827
40768
  - **\`alias::name\`**: An id without \`::\` is local to the project that writes it. Anything another project provides is referenced as \`alias::name\` \u2014 the alias is one of your members or declared \`externals\`, the name a public name in that project's L0 export table. A reference to something it does not export is reported (\`EXTERNAL_NOT_EXPORTED\`).
39828
40769
  - **Deprecated forms** (they still resolve for one release, are reported, and \`wairon doctor --fix\` rewrites them): a leading \`::\` (\`::shared::error-type\`), \`super::\` (\`super::sibling_comp\`), member paths (\`billing::invoice::invoice_portal\`), and an L1 subsystem carrying \`projectPath\` (\`DEPRECATED_MOUNT_FORM\`).
39829
40770
  - **Do not run the \`wairon\` CLI**: Use \`sdd_validate_tree\` and \`sdd_get_status\` instead of CLI commands.
39830
- - **Handoff to implementation**: Once design is complete and validates cleanly, tell the human: *"The specs are complete and validate. Please run \`wairon lock\` to confirm and freeze them."* No session restart is needed after the lock \u2014 delegate implementation right away via the \`sdd-delegate\` skill.
40771
+ - **Handoff to implementation**: Once design is complete and validates cleanly, tell the human: *"The specs are complete and validate. Please run \`wairon lock\` to approve them, and commit \`.wai/lock.json\`."* The lock records the approval; it does not rewrite spec files or their \`status\`. No session restart is needed after the lock \u2014 delegate implementation right away via the \`sdd-delegate\` skill.
40772
+ - **Approval, not status**: a design is ready to implement when it is APPROVED \u2014 \`sdd_get_status\` reports the approval state (approved, or which specs changed since), and \`wairon lock-check\` gives the same verdict in CI. A spec's \`status\` (draft/design/complete) is authoring readiness only; never wait for it to become \`complete\`.
39831
40773
  - **To implement code**: Delegate via the \`sdd-delegate\` skill: fetch the component's live brief with the \`sdd_get_agent_brief\` MCP tool (or the \`wairon-agent://\` resource) and spawn a subagent from it. Briefs are composed per call from the current spec tree, so they are always current \u2014 never wait for a restart. Implementations must match L3 interfaces and L5 narratives exactly. Generated agent files under \`.claude/agents/\` are an optional materialized view of the same topology \u2014 the live briefs are canonical.
39832
40774
 
39833
40775
  ### Rules (enforced by \`sdd_validate_tree\`)
@@ -39867,7 +40809,7 @@ ${GUIDE_MARKER_END}
39867
40809
  const stripped = stripGuideSection(existing);
39868
40810
  const newContent = stripped.trimEnd() + section;
39869
40811
  fs21.mkdirSync(path36.dirname(filePath), { recursive: true });
39870
- fs21.writeFileSync(filePath, newContent, "utf-8");
40812
+ writeFile(filePath, newContent);
39871
40813
  }
39872
40814
  function stripGuideSection(content) {
39873
40815
  const start = content.indexOf(GUIDE_MARKER_START);
@@ -39890,7 +40832,7 @@ To design or modify the system, invoke the **\`sdd-architect\`** skill
39890
40832
  (in \`.claude/skills/\`). Author and validate specs with the \`sdd_*\` MCP tools;
39891
40833
  the \`wairon\` CLI is the human developer's tool, not yours.
39892
40834
  `;
39893
- fs21.writeFileSync(filePath, content, "utf-8");
40835
+ writeFile(filePath, content);
39894
40836
  } else if (targetType === "gemini" || targetType === "agy") {
39895
40837
  const filePath = path36.join(projectRoot, "GEMINI.md");
39896
40838
  const content = `# Wairon SDD Project
@@ -39899,7 +40841,7 @@ ${versionStamp()}
39899
40841
  ${LOCAL_GUIDE_BODY}
39900
40842
  ${GUIDE_MARKER_END}
39901
40843
  `;
39902
- fs21.writeFileSync(filePath, content, "utf-8");
40844
+ writeFile(filePath, content);
39903
40845
  } else if (targetType === "cursor") {
39904
40846
  const filePath = path36.join(projectRoot, ".cursorrules");
39905
40847
  const content = `# Wairon SDD Project
@@ -39908,7 +40850,7 @@ This project uses the Wairon Spec-Driven Development (SDD) framework.
39908
40850
 
39909
40851
  Refer to the rules in [.cursor/rules/](.cursor/rules/) for full instructions.
39910
40852
  `;
39911
- fs21.writeFileSync(filePath, content, "utf-8");
40853
+ writeFile(filePath, content);
39912
40854
  } else if (targetType === "copilot") {
39913
40855
  const filePath = path36.join(projectRoot, ".github", "copilot-instructions.md");
39914
40856
  fs21.mkdirSync(path36.dirname(filePath), { recursive: true });
@@ -39918,14 +40860,14 @@ This project uses the Wairon Spec-Driven Development (SDD) framework.
39918
40860
 
39919
40861
  Refer to the prompts in [.github/prompts/](.github/prompts/) for instructions.
39920
40862
  `;
39921
- fs21.writeFileSync(filePath, content, "utf-8");
40863
+ writeFile(filePath, content);
39922
40864
  } else if (targetType === "codex") {
39923
40865
  const filePath = path36.join(projectRoot, ".codexrules");
39924
40866
  const content = `# Wairon SDD Project
39925
40867
 
39926
40868
  Refer to [.codex/agents/](.codex/agents/) for full instructions.
39927
40869
  `;
39928
- fs21.writeFileSync(filePath, content, "utf-8");
40870
+ writeFile(filePath, content);
39929
40871
  }
39930
40872
  }
39931
40873
  var GUIDE_TARGETS = ["claude", "gemini", "agy", "cursor", "copilot", "codex"];
@@ -40080,9 +41022,17 @@ function syncContextFiles() {
40080
41022
  var path37 = __toESM(require("path"));
40081
41023
  function approvalTag(entry) {
40082
41024
  if (!entry) return "";
40083
- const state = entry.upgraded ? `${entry.state}, re-lock once` : entry.state;
40084
- return entry.pinned ? ` [${state} \xB7 pin ${entry.pinned}]` : ` [${state}]`;
40085
- }
41025
+ const words2 = APPROVAL_WORDS[entry.state];
41026
+ const state = entry.upgraded ? `${words2}, re-lock once` : words2;
41027
+ return entry.pinned ? ` [${state} \xB7 ${PIN_WORDS[entry.pinned]}]` : ` [${state}]`;
41028
+ }
41029
+ var APPROVAL_WORDS = { approved: "approved", drifted: "drifted", never: "never approved" };
41030
+ var PIN_WORDS = { matches: "pinned", moved: "pin moved", unpinned: "not pinned" };
41031
+ var OWN_APPROVAL_WORDS = {
41032
+ approved: "approved",
41033
+ drifted: "drifted \u2014 its approval no longer covers this tree (`wairon lock` re-approves)",
41034
+ never: "never approved (`wairon lock` approves the design)"
41035
+ };
40086
41036
  var asWritten = (text3) => text3;
40087
41037
  function fillDecor(decor) {
40088
41038
  return {
@@ -40168,11 +41118,15 @@ function getStatusReport(options = {}, decor) {
40168
41118
  }
40169
41119
  componentScores.set(comp.id, score);
40170
41120
  }
41121
+ const typeOwners = new Set(loadTypeSpecs().map((t) => t.subsystem).filter((id) => typeof id === "string"));
40171
41122
  const getSubsystemScore = (subId) => {
40172
41123
  const sub = subsystems.find((s) => s.id === subId);
40173
41124
  if (!sub) return 0;
40174
41125
  const subComps = components.filter((c) => c.subsystem === subId);
40175
- if (subComps.length === 0) return 0;
41126
+ if (subComps.length === 0) {
41127
+ if (!typeOwners.has(subId)) return 0;
41128
+ return sub.status === "complete" ? 100 : 50;
41129
+ }
40176
41130
  const totalScore = subComps.reduce((acc, c) => acc + (componentScores.get(c.id) ?? 0), 0);
40177
41131
  let avg = Math.round(totalScore / subComps.length);
40178
41132
  if (sub.status === "draft" || sub.status === "design") {
@@ -40295,8 +41249,7 @@ function getStatusReport(options = {}, decor) {
40295
41249
  `;
40296
41250
  }
40297
41251
  const own = options.approvals?.find((a) => a.key === "");
40298
- const hasMembers = (options.approvals ?? []).some((a) => a.key !== "" && a.as !== "part");
40299
- if (own && (own.state !== "never" || hasMembers)) output += `${mark.layer("system", "Approval:")} this project is ${own.state}${own.upgraded ? " (locked under the pre-stage-5 gate identity \u2014 re-lock once)" : ""}
41252
+ if (own) output += `${mark.layer("system", "Approval:")} this project is ${OWN_APPROVAL_WORDS[own.state]}${own.upgraded ? " (locked under an earlier gate identity \u2014 re-lock once)" : ""}
40300
41253
  `;
40301
41254
  return { text: output, failed: false };
40302
41255
  }
@@ -41478,7 +42431,7 @@ var KINDS_AT = {
41478
42431
  auth: ["implementation"],
41479
42432
  subsystem: ["component", "type"],
41480
42433
  group: ["type"],
41481
- type: ["interface", "type"]
42434
+ type: ["interface", "type", "implementation"]
41482
42435
  };
41483
42436
  function kindOf(ref) {
41484
42437
  const kinds = KINDS_AT[ref.position] ?? ["component", "interface", "implementation", "type", "subsystem"];
@@ -42558,7 +43511,7 @@ function emptyPlan(family, request) {
42558
43511
  const whole = !(reach2 !== null && !reach2.parentReach);
42559
43512
  return { request, familyRoot: familyNode(family, "").directory, whole, edits: [], refusals: [], changes: [], relock: [], notes: [] };
42560
43513
  }
42561
- var posix3 = (p) => p.split(path43.sep).join("/");
43514
+ var posix5 = (p) => p.split(path43.sep).join("/");
42562
43515
  var refuse = (plan6, code, project, detail) => {
42563
43516
  plan6.refusals.push({ code, project, detail });
42564
43517
  };
@@ -42636,7 +43589,7 @@ function attachEdits(plan6, family, bound2, dir, alias, bindingConfig) {
42636
43589
  edit(plan6, key, "id", `id: ${id} (declared${approved !== void 0 ? ", the id its lock approved" : ""})`, { root: dir, call: "setId", args: [id] });
42637
43590
  }
42638
43591
  if (!attached) plan6.notes.push(`${dir} has no project.yaml of its own \u2014 \`wairon doctor --fix\` writes one declaring "${alias}" as its id`);
42639
- const relPath = posix3(path43.relative(familyNode(family, bound2).directory, dir));
43592
+ const relPath = posix5(path43.relative(familyNode(family, bound2).directory, dir));
42640
43593
  const declaration2 = { path: relPath, ...plan6.request.description !== void 0 ? { description: plan6.request.description } : {} };
42641
43594
  edit(plan6, bound2, "member", `members: ${alias} \u2192 ${relPath} (an existing project, kept as it is)`, { root: familyNode(family, bound2).directory, call: "declareMember", args: [alias, declaration2] });
42642
43595
  for (const requirement of requiredPolicies(bindingConfig)) {
@@ -42683,8 +43636,8 @@ function planDetach(family, bound2, request) {
42683
43636
  if (plan6.request.widen) planWidening(plan6, member, narrow);
42684
43637
  if (plan6.refusals.length > 0) return plan6;
42685
43638
  const boundDir = familyNode(family, bound2).directory;
42686
- const held = memberDeclarationOf(configAt3(boundDir)?.members?.[alias] ?? posix3(path43.relative(boundDir, member.directory)));
42687
- const declared = { ...held, path: memberLocationOf(held) ?? posix3(path43.relative(boundDir, member.directory)) };
43639
+ const held = memberDeclarationOf(configAt3(boundDir)?.members?.[alias] ?? posix5(path43.relative(boundDir, member.directory)));
43640
+ const declared = { ...held, path: memberLocationOf(held) ?? posix5(path43.relative(boundDir, member.directory)) };
42688
43641
  detachEdits(plan6, family, bound2, member, alias, declared, consumers, outward);
42689
43642
  return plan6;
42690
43643
  }
@@ -42724,7 +43677,7 @@ function planWidening(plan6, member, narrow) {
42724
43677
  function detachEdits(plan6, family, bound2, member, alias, declared, consumers, outward) {
42725
43678
  const boundDir = familyNode(family, bound2).directory;
42726
43679
  const relocation = plan6.request.relocation;
42727
- const sourceTo = (from, producer) => relocation ? { hosted: producer.id ?? producer.mountAlias ?? producer.namespace } : { path: posix3(path43.relative(from, producer.directory)) };
43680
+ const sourceTo = (from, producer) => relocation ? { hosted: producer.id ?? producer.mountAlias ?? producer.namespace } : { path: posix5(path43.relative(from, producer.directory)) };
42728
43681
  const where = (source) => source.hosted !== void 0 ? `hosted record ${source.hosted}` : source.path;
42729
43682
  edit(plan6, bound2, "member", `members: ${alias} removed (${declared.path} ${relocation ? `moves to ${relocation.to}` : "stays as it is"})`, { root: boundDir, call: "removeMember", args: [alias] });
42730
43683
  const boundSource = relocation ? sourceTo(boundDir, member) : { path: declared.path };
@@ -42798,7 +43751,7 @@ function adoptEdits(plan6, family, bound2, alias, external, dir, at = dir) {
42798
43751
  edit(plan6, bound2, "pin", `pin ${alias} removed (the family supplies it now)`, { root: boundDir, call: "unpin", args: [alias] });
42799
43752
  edit(plan6, bound2, "external", `externals: ${alias} removed`, { root: boundDir, call: "removeExternal", args: [alias] });
42800
43753
  const declaration2 = {
42801
- path: posix3(path43.relative(boundDir, at)),
43754
+ path: posix5(path43.relative(boundDir, at)),
42802
43755
  ...external.description !== void 0 ? { description: external.description } : {},
42803
43756
  ...external.use && external.use.length > 0 ? { use: external.use } : {}
42804
43757
  };
@@ -42909,7 +43862,7 @@ var KINDS_AT2 = {
42909
43862
  auth: ["implementation"],
42910
43863
  subsystem: ["component", "type"],
42911
43864
  group: ["type"],
42912
- type: ["interface", "type"]
43865
+ type: ["interface", "type", "implementation"]
42913
43866
  };
42914
43867
  function kindOf2(ref) {
42915
43868
  const kinds = KINDS_AT2[ref.position] ?? ["component", "interface", "implementation", "type", "subsystem"];
@@ -43057,9 +44010,33 @@ function planAliasRename(family, bound2, request) {
43057
44010
  if ([...family.owners].some(([key, owner]) => owner === bound2 && key.split("::").pop() === next)) {
43058
44011
  plan6.notes.push(`${label5(bound2)} has a spec whose id is "${next}" \u2014 a local id equal to an alias (LOCAL_ID_SHADOWS_PROJECT reports it)`);
43059
44012
  }
44013
+ repinIntactConsumers(plan6, family, bound2);
43060
44014
  return plan6;
43061
44015
  }
43062
- var PHASES = ["rewriteReferences", "renameAlias", "repointExternal", "renameId", "renamePin"];
44016
+ function repinIntactConsumers(plan6, family, bound2) {
44017
+ const boundNode2 = familyNode(family, bound2);
44018
+ for (const node of family.nodes) {
44019
+ if (path44.resolve(node.directory) === path44.resolve(boundNode2.directory)) continue;
44020
+ let statuses = [];
44021
+ try {
44022
+ statuses = runWithProjectRoot(node.directory, () => getExternalsStatus2());
44023
+ } catch {
44024
+ continue;
44025
+ }
44026
+ for (const status of statuses) {
44027
+ const producer = node.aliases.get(status.alias);
44028
+ const pinsBound = producer !== void 0 ? producer === bound2 : boundNode2.id !== void 0 && status.project === boundNode2.id;
44029
+ if (!status.pinned || status.outOfReach || !pinsBound) continue;
44030
+ const broken = status.uses.filter((u) => u.state === "changed" || u.state === "removed");
44031
+ if (broken.length > 0) {
44032
+ plan6.notes.push(`${label5(node.namespace)}'s pin of "${status.alias}" is already incompatible (${broken.map((u) => u.publicName ?? u.member ?? "?").join(", ")}): not re-pinned \u2014 adapt its uses, then run \`wairon externals pin ${status.alias}\` there.`);
44033
+ continue;
44034
+ }
44035
+ edit2(plan6, node.namespace, "pin", `pin ${status.alias} taken again: the names it uses of ${label5(bound2)} are unchanged`, { root: node.directory, call: "pinExternals", args: [[status.alias]] });
44036
+ }
44037
+ }
44038
+ }
44039
+ var PHASES = ["rewriteReferences", "renameAlias", "repointExternal", "renameId", "renamePin", "pinExternals"];
43063
44040
  function write4(plan6, rehearsal) {
43064
44041
  const planned = plan6.edits.filter((e) => e.write !== void 0);
43065
44042
  const unknown = planned.find((x) => !PHASES.includes(x.write.call));
@@ -43074,6 +44051,7 @@ function write4(plan6, rehearsal) {
43074
44051
  for (const e of planned.filter((x) => x.write.call === "renameId")) writeOne2(rehearsal, e.write);
43075
44052
  }
43076
44053
  for (const e of planned.filter((x) => x.write.call === "renamePin")) writeOne2(rehearsal, e.write);
44054
+ for (const e of planned.filter((x) => x.write.call === "pinExternals")) writeOne2(rehearsal, e.write);
43077
44055
  }
43078
44056
  function referencesBySpec(planned) {
43079
44057
  const out = /* @__PURE__ */ new Map();
@@ -43097,6 +44075,8 @@ function writeOne2(rehearsal, w) {
43097
44075
  return renameProjectId(args[0], args[1]);
43098
44076
  case "renamePin":
43099
44077
  return renamePin2(args[0], args[1], args[2]);
44078
+ case "pinExternals":
44079
+ return pinExternals2(args[0]);
43100
44080
  default:
43101
44081
  throw new Error(`identity_migration makes no "${w.call}" write`);
43102
44082
  }
@@ -43110,7 +44090,7 @@ function emptyPlan3(family, request) {
43110
44090
  const whole = !(reach2 !== null && !reach2.parentReach);
43111
44091
  return { request, familyRoot: familyNode(family, "").directory, whole, edits: [], refusals: [], changes: [], relock: [], notes: [] };
43112
44092
  }
43113
- var posix4 = (p) => p.split(path45.sep).join("/");
44093
+ var posix6 = (p) => p.split(path45.sep).join("/");
43114
44094
  var label6 = (key) => key === "" ? "the top project" : `"${key}"`;
43115
44095
  var refuse3 = (plan6, code, project, detail) => {
43116
44096
  plan6.refusals.push({ code, project, detail });
@@ -43141,7 +44121,7 @@ var KINDS_AT3 = {
43141
44121
  auth: ["implementation"],
43142
44122
  subsystem: ["component", "type"],
43143
44123
  group: ["type"],
43144
- type: ["interface", "type"]
44124
+ type: ["interface", "type", "implementation"]
43145
44125
  };
43146
44126
  function kindOf3(ref) {
43147
44127
  const kinds = KINDS_AT3[ref.position] ?? ["component", "interface", "implementation", "type", "subsystem"];
@@ -43320,7 +44300,7 @@ function respellConsumer(plan6, family, c, own) {
43320
44300
  function repointConsumer(plan6, c, boundNode2) {
43321
44301
  const declared = configAt5(c.node.directory)?.externals?.[c.alias];
43322
44302
  const project = boundNode2.id !== void 0 && boundNode2.id !== c.alias ? boundNode2.id : null;
43323
- const source = declared?.source ? { path: posix4(path45.relative(c.node.directory, boundNode2.directory)) } : null;
44303
+ const source = declared?.source ? { path: posix6(path45.relative(c.node.directory, boundNode2.directory)) } : null;
43324
44304
  edit3(plan6, c.node.namespace, "external", `externals: ${c.alias} names ${boundNode2.id ?? "the top project"}${source ? ` at ${source.path}` : ""}`, { root: c.node.directory, call: "repointExternal", args: [c.alias, project, source] });
43325
44305
  plan6.notes.push(`${label6(c.node.namespace)}'s pin of "${c.alias}" is taken again against ${boundNode2.id ?? "the top project"}: the producer changed, so its lock reads drifted until it is re-locked.`);
43326
44306
  return c;
@@ -43356,8 +44336,8 @@ function planExternalize(family, bound2, request) {
43356
44336
  plan6,
43357
44337
  bound2,
43358
44338
  "move",
43359
- `externalize ${id}: its specs into ${joined ? `the part "${joined.alias}"` : `a part at ${posix4(path45.relative(node.directory, dir))}, declared as "${id}"`} \u2014 a storage move; no reference, export or pin changes`,
43360
- { root: node.directory, call: "externalizeSubsystem", args: [id, posix4(path45.relative(node.directory, dir))] }
44339
+ `externalize ${id}: its specs into ${joined ? `the part "${joined.alias}"` : `a part at ${posix6(path45.relative(node.directory, dir))}, declared as "${id}"`} \u2014 a storage move; no reference, export or pin changes`,
44340
+ { root: node.directory, call: "externalizeSubsystem", args: [id, posix6(path45.relative(node.directory, dir))] }
43361
44341
  );
43362
44342
  plan6.notes.push("Source code is not moved: move the subsystem's code yourself if it should live beside its specs.");
43363
44343
  return plan6;
@@ -43366,8 +44346,9 @@ function planExternalize(family, bound2, request) {
43366
44346
  crossingsBack(plan6, family, bound2, subKey);
43367
44347
  checkSiblings(plan6, family, bound2, id, subKey, table);
43368
44348
  if (plan6.refusals.length > 0) return plan6;
43369
- edit3(plan6, bound2, "move", `externalize ${id} as a project: its specs into a member at ${posix4(path45.relative(node.directory, dir))}, declared as "${id}", then promoted`, { root: node.directory, call: "externalizeSubsystem", args: [id, posix4(path45.relative(node.directory, dir)), "project"] });
44349
+ edit3(plan6, bound2, "move", `externalize ${id} as a project: its specs into a member at ${posix6(path45.relative(node.directory, dir))}, declared as "${id}", then promoted`, { root: node.directory, call: "externalizeSubsystem", args: [id, posix6(path45.relative(node.directory, dir)), "project"] });
43370
44350
  edit3(plan6, id, "pin", `pin the new member's externals (its external for ${node.id ?? "this project"}, when its specs reference it)`, { root: dir, call: "pinExternals", args: [[]] });
44351
+ repinIntactConsumers2(plan6, family, bound2, [dir]);
43371
44352
  plan6.notes.push("Source code is not moved: move the subsystem's code into the member yourself.");
43372
44353
  return plan6;
43373
44354
  }
@@ -43496,6 +44477,7 @@ function planPromote(family, bound2, request) {
43496
44477
  );
43497
44478
  edit3(plan6, newId, "pin", `pin the new project's external for ${node.id ?? "this project"} (when its specs reference it)`, { root: part.directory, call: "pinExternals", args: [[]] });
43498
44479
  if (part.storage !== "contained") edit3(plan6, bound2, "pin", `pin ${alias} (a project stored outside ${label6(bound2)} is judged against its pin)`, { root: node.directory, call: "pinExternals", args: [[alias]] });
44480
+ repinIntactConsumers2(plan6, family, bound2, [part.directory]);
43499
44481
  plan6.notes.push(`"${newId}" has no lock yet: lock it at its own root (\`wairon lock\`) once it validates.`);
43500
44482
  return plan6;
43501
44483
  }
@@ -43562,16 +44544,19 @@ function write5(plan6, rehearsal) {
43562
44544
  return;
43563
44545
  }
43564
44546
  if (plan6.request.verb === "externalize") {
43565
- for (const e of planned.filter((x) => x.write.call === "externalizeSubsystem")) writeOne3(rehearsal, e.write);
44547
+ for (const e of planned.filter((x) => x.write.call === "externalizeSubsystem")) recordCrossing(plan6, e.project, e.write.args[0], writeOne3(rehearsal, e.write));
43566
44548
  for (const e of planned.filter((x) => x.write.call === "pinExternals")) writeOne3(rehearsal, e.write);
43567
44549
  return;
43568
44550
  }
43569
44551
  if (plan6.request.verb === "promote") {
43570
- for (const e of planned.filter((x) => x.write.call === "promoteMember")) writeOne3(rehearsal, e.write);
44552
+ for (const e of planned.filter((x) => x.write.call === "promoteMember")) recordCrossing(plan6, e.project, e.write.args[1] ?? e.write.args[0], writeOne3(rehearsal, e.write));
43571
44553
  for (const e of planned.filter((x) => x.write.call === "pinExternals")) writeOne3(rehearsal, e.write);
43572
44554
  return;
43573
44555
  }
43574
- for (const e of planned.filter((x) => x.write.call === "demoteMember")) writeOne3(rehearsal, e.write);
44556
+ for (const e of planned.filter((x) => x.write.call === "demoteMember")) {
44557
+ const demoted = writeOne3(rehearsal, e.write);
44558
+ for (const line of demoted?.consumersRestored ?? []) plan6.edits.push({ project: e.project, kind: "reference", detail: `consumers across the old boundary restored: ${line}` });
44559
+ }
43575
44560
  for (const e of planned.filter((x) => x.write.call === "unpin")) writeOne3(rehearsal, e.write);
43576
44561
  }
43577
44562
  function referencesBySpec2(planned) {
@@ -43584,9 +44569,46 @@ ${id}`) ?? [], ...edits]);
43584
44569
  }
43585
44570
  return out;
43586
44571
  }
44572
+ function repinIntactConsumers2(plan6, family, bound2, created) {
44573
+ const boundNode2 = familyNode(family, bound2);
44574
+ const skip = new Set([boundNode2.directory, ...created].map((d) => path45.resolve(d)));
44575
+ for (const node of family.nodes) {
44576
+ if (skip.has(path45.resolve(node.directory))) continue;
44577
+ let statuses = [];
44578
+ try {
44579
+ statuses = runWithProjectRoot(node.directory, () => getExternalsStatus2());
44580
+ } catch {
44581
+ continue;
44582
+ }
44583
+ for (const status of statuses) {
44584
+ const producer = node.aliases.get(status.alias);
44585
+ const pinsBound = producer !== void 0 ? producer === bound2 : boundNode2.id !== void 0 && status.project === boundNode2.id;
44586
+ if (!status.pinned || status.outOfReach || !pinsBound) continue;
44587
+ const broken = status.uses.filter((u) => u.state === "changed" || u.state === "removed");
44588
+ if (broken.length > 0) {
44589
+ plan6.notes.push(`${label6(node.namespace)}'s pin of "${status.alias}" is already incompatible (${broken.map((u) => u.publicName ?? u.member ?? "?").join(", ")}): not re-pinned \u2014 adapt its uses, then run \`wairon externals pin ${status.alias}\` there.`);
44590
+ continue;
44591
+ }
44592
+ edit3(plan6, node.namespace, "pin", `pin ${status.alias} taken again: the names it uses of ${label6(bound2)} only move with the boundary`, { root: node.directory, call: "pinExternals", args: [[status.alias]] });
44593
+ }
44594
+ }
44595
+ }
44596
+ function recordCrossing(plan6, bound2, member, result) {
44597
+ const crossed = result;
44598
+ if (!crossed) return;
44599
+ for (const r of crossed.respelled) {
44600
+ plan6.edits.push({ project: bound2, kind: "reference", detail: `${r.specId} (${r.position}): ${r.from} \u2192 ${r.to}`, reference: r });
44601
+ }
44602
+ for (const r of crossed.memberRespelled) {
44603
+ plan6.edits.push({ project: member, kind: "reference", detail: `${r.specId} (${r.position}): ${r.from} \u2192 ${r.to}`, reference: r });
44604
+ }
44605
+ for (const line of crossed.exported) plan6.edits.push({ project: bound2, kind: "export", detail: `export ${line}` });
44606
+ for (const line of crossed.imported) plan6.edits.push({ project: bound2, kind: "external", detail: `import: ${line}` });
44607
+ for (const line of crossed.consumersDropped) plan6.edits.push({ project: bound2, kind: "reference", detail: `consumers across the boundary to ${member} dropped: ${line}` });
44608
+ }
43587
44609
  function writeOne3(rehearsal, w) {
43588
44610
  const args = w.args;
43589
- runWithProjectRoot(rehearsalRoot(rehearsal, w.root), () => {
44611
+ return runWithProjectRoot(rehearsalRoot(rehearsal, w.root), () => {
43590
44612
  switch (w.call) {
43591
44613
  case "internalizeMember":
43592
44614
  return internalizeMember(args[0], args[1]);
@@ -43621,7 +44643,7 @@ var path46 = __toESM(require("path"));
43621
44643
  var fs26 = __toESM(require("fs"));
43622
44644
  var os7 = __toESM(require("os"));
43623
44645
  var nodePath2 = __toESM(require("path"));
43624
- var yaml3 = __toESM(require("js-yaml"));
44646
+ var yaml4 = __toESM(require("js-yaml"));
43625
44647
  var TRANSACTIONS2 = nodePath2.join(".wai", "transactions");
43626
44648
  var transactionsDir = (owner) => nodePath2.join(owner, TRANSACTIONS2);
43627
44649
  var transactionDir = (owner, id) => nodePath2.join(transactionsDir(owner), id);
@@ -43688,7 +44710,7 @@ function dropRehearsal(id) {
43688
44710
  function saveJournal(journal) {
43689
44711
  const ignore = nodePath2.join(transactionsDir(journal.owner), ".gitignore");
43690
44712
  if (!fs26.existsSync(ignore)) writeDurably(ignore, "*\n");
43691
- writeDurably(nodePath2.join(transactionDir(journal.owner, journal.id), "journal.yaml"), yaml3.dump(journal, { noRefs: true, lineWidth: 200 }));
44713
+ writeDurably(nodePath2.join(transactionDir(journal.owner, journal.id), "journal.yaml"), yaml4.dump(journal, { noRefs: true, lineWidth: 200 }));
43692
44714
  }
43693
44715
  function readJournal(owner, id) {
43694
44716
  const file = nodePath2.join(transactionDir(owner, id), "journal.yaml");
@@ -43700,7 +44722,7 @@ function readJournal(owner, id) {
43700
44722
  throw e;
43701
44723
  }
43702
44724
  try {
43703
- return yaml3.load(text3);
44725
+ return yaml4.load(text3);
43704
44726
  } catch (e) {
43705
44727
  throw new Error(`${file} is not a readable transaction journal: ${e instanceof Error ? e.message : String(e)}`);
43706
44728
  }
@@ -44495,7 +45517,7 @@ function errText(message) {
44495
45517
  function errMessage(e) {
44496
45518
  return errText(e instanceof Error ? e.message : String(e));
44497
45519
  }
44498
- var TYPE_REF_GRAMMAR = (what) => `${what}, in the neutral type grammar: a primitive (string, int, float, bool, bytes, date, datetime, duration, any; void only as a whole returns), a defined type id ("billing.Invoice", "alias::name"), list<T>, set<T>, map<K, V> (K is string, int or an enum), T? (T or no value), a union of NAMED types ("Invoice | Receipt"), and on a returns only, async T ("async void"). Every named type must resolve. TypeScript spellings are accepted and respelled \u2014 "Invoice[]" \u2192 list<Invoice>, "Invoice | null" \u2192 Invoice?, boolean \u2192 bool, Record<string, V> \u2192 map<string, V>, Promise<T> \u2192 async T \u2014 and the answer lists each respelling. Refused, naming the replacement: number ("int or float?"), inline object or function types (name a value-object or a signature type), string-literal unions (name an enum), and unions mixing in a primitive or a collection.`;
45520
+ var TYPE_REF_GRAMMAR = (what) => `${what}, in the neutral type grammar: a primitive (string, int, float, bool, bytes, date, datetime, duration, any; void only as a whole returns), a defined type id ("billing.Invoice", "alias::name"), list<T>, set<T>, map<K, V> (K is string, int or an enum), T? (T or no value), a union of NAMED types ("Invoice | Receipt"), and on a returns only, async T ("async void") and result<T, E> (completes with T or fails with E; "async result<void, E>"). Every named type must resolve. TypeScript and Rust spellings are accepted and respelled \u2014 "Invoice[]" \u2192 list<Invoice>, "Invoice | null" \u2192 Invoice?, boolean \u2192 bool, Record<string, V> \u2192 map<string, V>, Promise<T> \u2192 async T, Result<(), E> \u2192 result<void, E> \u2014 and the answer lists each respelling. Refused, naming the replacement: number ("int or float?"), inline object or function types (name a value-object or a signature type), string-literal unions (name an enum), and unions mixing in a primitive or a collection.`;
44499
45521
  function structured(content, data) {
44500
45522
  return { content: [{ type: "text", text: content }], structuredContent: data };
44501
45523
  }
@@ -44519,10 +45541,19 @@ ${body}`);
44519
45541
  }
44520
45542
  function migrationView(report3, dryRun) {
44521
45543
  const plan6 = report3.plan;
45544
+ const request = plan6.request;
45545
+ const madeProject = request.verb === "promote" || request.verb === "externalize" && request.as === "project";
45546
+ const madeAt = madeProject && report3.applied ? plan6.changes.find((c) => c.action === "create" && c.path.replace(/\\/g, "/") === ".wai/project.yaml")?.project : void 0;
44522
45547
  return {
44523
45548
  dryRun,
44524
45549
  applied: report3.applied,
44525
45550
  relock: report3.applied ? report3.relock : plan6.relock,
45551
+ ...madeAt ? {
45552
+ newProject: {
45553
+ root: madeAt,
45554
+ next: `The new project at ${madeAt} has no guide, skills or MCP registration of its own yet. The human runs, in that folder: \`wairon generate\` and \`wairon mcp install --backend claude\` (the CLI's \`wairon member promote\` / \`subsystem externalize --as project\` do this themselves). Its own session then designs it \u2014 its specs, L0 export table and project.yaml are written from its own root.`
45555
+ }
45556
+ } : {},
44526
45557
  ...report3.outcome ? { outcome: { committed: report3.outcome.committed, restored: report3.outcome.restored, unrestored: report3.outcome.unrestored, ...report3.outcome.failure ? { failure: report3.outcome.failure } : {} } } : {},
44527
45558
  plan: {
44528
45559
  verb: plan6.request.verb,
@@ -44595,6 +45626,28 @@ function renderChangeReport(report3) {
44595
45626
  return lines.join("\n");
44596
45627
  }
44597
45628
  var SPEC_KINDS = ["system", "subsystem", "component", "interface", "implementation", "type"];
45629
+ function specKindsHolding(id) {
45630
+ const system = loadSystemSpec();
45631
+ const held = {
45632
+ system: !!system && (id === "system" || id === system.name),
45633
+ subsystem: !!loadSubsystemSpec(id),
45634
+ component: !!loadComponentSpec(id),
45635
+ interface: !!loadInterfaceSpec(id),
45636
+ implementation: !!loadImplementationSpec(id),
45637
+ type: !!loadTypeSpec(id)
45638
+ };
45639
+ return SPEC_KINDS.filter((k) => held[k]);
45640
+ }
45641
+ var PORTAL_TYPE_TRANSPORT = {
45642
+ HTTP_API: "HTTP",
45643
+ gRPC: "gRPC",
45644
+ GraphQL: "GraphQL",
45645
+ MessageBus: "MessageBus",
45646
+ NamedPipe: "NamedPipe",
45647
+ IPC: "IPC",
45648
+ CLI: "CLI",
45649
+ Custom: void 0
45650
+ };
44598
45651
  var STATUS_VALUES = ["draft", "design", "complete"];
44599
45652
  var testsToRevisitOutput = import_zod12.z.array(import_zod12.z.object({
44600
45653
  method: import_zod12.z.string().describe("The contract method this write changed or deleted."),
@@ -44737,7 +45790,7 @@ var memberCreationOutput = {
44737
45790
  profile: import_zod12.z.string().optional()
44738
45791
  })).describe("Requirements no installed version satisfies: nothing was written for them, and the next family run reports each as POLICY_NOT_ADOPTED."),
44739
45792
  projectType: import_zod12.z.string().optional().describe("The profile written as the new member's projectType, when exactly one adopted requirement names one."),
44740
- as: import_zod12.z.string().describe("part | project: what was created (stage 8; a part by default)."),
45793
+ as: import_zod12.z.string().describe("part | project: what was created (a part by default)."),
44741
45794
  storage: import_zod12.z.string().describe("contained | path | git: where its files live."),
44742
45795
  commit: import_zod12.z.string().optional().describe("git: the commit the member was pinned at."),
44743
45796
  ...staleServerOutput
@@ -45319,7 +46372,7 @@ ${renderChangeReport(report3)}`,
45319
46372
  server,
45320
46373
  "sdd_add_member",
45321
46374
  {
45322
- description: "Create a member of the bound project at `source`, in the one members grammar: `services/scheduler` (contained), `../admin` (a sibling checkout), `git@host:acme/payments.git` (a git repository, pinned at its default branch head unless `#<commit>` is given). By default a PART (stage 8): a piece of this project stored in another folder or repository, whose subsystems are this project's own (local ids, the ordinary subsystem rules, this project's lock); it gets only a specs folder, plus a configuration naming this project when it is not contained. With `as: project` an independent PROJECT instead: its project content is created \u2014 a project.yaml declaring the alias as its id, its L0, and the bound project's required packs (composition.requirePolicies) once \u2014 and it is reached only as `alias::name` through its L0 exports. What a member is follows from that content. A git member is not scaffolded; the repository's content decides what it is. Boundaries are earned: create a part unless the piece needs its own team, release, approval or public surface; sdd_promote_member makes one later. Idempotent \u2014 the same call again writes nothing; a different member under the alias is refused.",
46375
+ description: "Create a member of the bound project at `source`, in the one members grammar: `services/scheduler` (contained), `../admin` (a sibling checkout), `git@host:acme/payments.git` (a git repository, pinned at its default branch head unless `#<commit>` is given). By default a PART: a piece of this project stored in another folder or repository, whose subsystems are this project's own (local ids, the ordinary subsystem rules, this project's lock); it gets only a specs folder, plus a configuration naming this project when it is not contained. With `as: project` an independent PROJECT instead: its project content is created \u2014 a project.yaml declaring the alias as its id, its L0, and the bound project's required packs (composition.requirePolicies) once \u2014 and it is reached only as `alias::name` through its L0 exports. What a member is follows from that content. A git member is not scaffolded; the repository's content decides what it is. Boundaries are earned: create a part unless the piece needs its own team, release, approval or public surface; sdd_promote_member makes one later. Idempotent \u2014 the same call again writes nothing; a different member under the alias is refused.",
45323
46376
  inputSchema: {
45324
46377
  alias: import_zod12.z.string().describe("The member's alias ([a-z0-9-_]+); a project's new id"),
45325
46378
  source: import_zod12.z.string().describe("A contained path, a `../` sibling path, or a git URL (optionally `#<commit>`)"),
@@ -45389,7 +46442,7 @@ ${renderChangeReport(report3)}`,
45389
46442
  server,
45390
46443
  "sdd_externalize_subsystem",
45391
46444
  {
45392
- description: "Move an internal subsystem out to the path. By default (stage 8) into a PART \u2014 a storage move: the subsystem's specs move to that directory (a new part, or the existing part there), and nothing else changes: no reference, export, external or lock entry, the same verdict. With `as: project`, into a member PROJECT instead, family-wide and all-or-nothing \u2014 the move followed by a promote: what crosses the new boundary exported and imported, references respelled `alias::name`, the new member's external for this project pinned, and every other family project's references checked to still resolve (a reference that would need a component made public refuses the plan). Source code is not moved. dryRun answers the plan and writes nothing; run it first and show the plan. Never locks: the answer names the projects to re-lock.",
46445
+ description: "Move an internal subsystem out to the path. By default into a PART \u2014 a storage move: the subsystem's specs move to that directory (a new part, or the existing part there), and nothing else changes: no reference, export, external or lock entry, the same verdict. With `as: project`, into a member PROJECT instead, family-wide and all-or-nothing \u2014 the move followed by a promote: what crosses the new boundary exported and imported, references respelled `alias::name`, the new member's external for this project pinned, and every other family project's references checked to still resolve (a reference that would need a component made public refuses the plan). Source code is not moved. dryRun answers the plan and writes nothing; run it first and show the plan. Never locks: the answer names the projects to re-lock.",
45393
46446
  inputSchema: {
45394
46447
  subsystem: import_zod12.z.string().describe("The subsystem to move out"),
45395
46448
  path: import_zod12.z.string().describe("The part's root, relative to the bound project (a new directory, or an existing part's)"),
@@ -45403,7 +46456,7 @@ ${renderChangeReport(report3)}`,
45403
46456
  server,
45404
46457
  "sdd_promote_member",
45405
46458
  {
45406
- description: "Promote a part of the bound project to an independent project in place (stage 8) \u2014 its files stay where they are; it gets an id, an L0 exporting exactly what the rest of the project uses of it, its references across the new boundary respelled `alias::name`, the parent declared as its external, and pins on both sides. Refused (each reason listed) for a part fetched from git, a trusted link that would cross the new boundary, a used component its subsystem does not publish, or an id already taken. All-or-nothing; dryRun answers the plan and writes nothing \u2014 run it first and show the plan. Never locks: the answer names the project to re-lock and the new one to lock.",
46459
+ description: "Promote a part of the bound project to an independent project in place \u2014 its files stay where they are; it gets an id, an L0 exporting exactly what the rest of the project uses of it, its references across the new boundary respelled `alias::name`, the parent declared as its external, and pins on both sides. Refused (each reason listed) for a part fetched from git, a trusted link that would cross the new boundary, a used component its subsystem does not publish, or an id already taken. All-or-nothing; dryRun answers the plan and writes nothing \u2014 run it first and show the plan. Never locks: the answer names the project to re-lock and the new one to lock.",
45407
46460
  inputSchema: {
45408
46461
  alias: import_zod12.z.string().describe("The part's alias"),
45409
46462
  id: import_zod12.z.string().optional().describe("The new project's id (default: the alias)"),
@@ -45416,7 +46469,7 @@ ${renderChangeReport(report3)}`,
45416
46469
  server,
45417
46470
  "sdd_demote_member",
45418
46471
  {
45419
- description: "Demote a project member of the bound project to a part in place (stage 8) \u2014 sdd_promote_member's inverse: its files stay where they are; its own metadata goes to the homes the destination names (as sdd_internalize_member sends it), its L0, lock and pins end, its references across the old boundary become local ids, and its configuration is reduced to naming its parent. Refused while another family project consumes it (a part has no exports \u2014 each consumer listed), for a member fetched from git, or for internalize's reasons. All-or-nothing; dryRun answers the plan and writes nothing \u2014 run it first and show the plan. Never locks.",
46472
+ description: "Demote a project member of the bound project to a part in place \u2014 sdd_promote_member's inverse: its files stay where they are; its own metadata goes to the homes the destination names (as sdd_internalize_member sends it), its L0, lock and pins end, its references across the old boundary become local ids, and its configuration is reduced to naming its parent. Refused while another family project consumes it (a part has no exports \u2014 each consumer listed), for a member fetched from git, or for internalize's reasons. All-or-nothing; dryRun answers the plan and writes nothing \u2014 run it first and show the plan. Never locks.",
45420
46473
  inputSchema: {
45421
46474
  alias: import_zod12.z.string().describe("The project member's alias"),
45422
46475
  destination: import_zod12.z.object({
@@ -45759,7 +46812,7 @@ ${renderChangeReport(report3)}`,
45759
46812
  interface: import_zod12.z.string().describe('The L3 interface ID (e.g. "ibilling-gateway")'),
45760
46813
  endpoints: import_zod12.z.array(import_zod12.z.object({
45761
46814
  method: import_zod12.z.string().describe("The NAME of the interface method to bind (not the HTTP verb)"),
45762
- transport: import_zod12.z.enum(["HTTP", "gRPC", "GraphQL", "MessageBus", "NamedPipe", "IPC", "CLI", "Custom"]).describe("Wire protocol; must match the Portal's portalType"),
46815
+ transport: import_zod12.z.enum(["HTTP", "HTTP_API", "gRPC", "GraphQL", "MessageBus", "NamedPipe", "IPC", "CLI", "Custom"]).describe("Wire protocol; must be the one the Portal's portalType implies. Either vocabulary is accepted: the endpoint's (HTTP) or the portalType's (HTTP_API), stored as HTTP; any other transport is refused, naming the one the portalType implies"),
45763
46816
  httpMethod: import_zod12.z.enum(["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]).optional().describe("HTTP: verb"),
45764
46817
  path: import_zod12.z.string().optional().describe('HTTP: route path, e.g. "/v1/checkout"'),
45765
46818
  service: import_zod12.z.string().optional().describe("gRPC: service name"),
@@ -45782,8 +46835,10 @@ ${renderChangeReport(report3)}`,
45782
46835
  try {
45783
46836
  const intf = loadInterfaceSpec(interfaceId);
45784
46837
  if (!intf) return errText(`Interface "${interfaceId}" does not exist.`);
45785
- const buildRaw = (e) => {
45786
- switch (e.transport) {
46838
+ const owner = intf.component ? loadComponentSpec(intf.component) : null;
46839
+ const implied = owner?.componentType === "Portal" && owner.portalType ? PORTAL_TYPE_TRANSPORT[owner.portalType] : void 0;
46840
+ const buildRaw = (e, transport) => {
46841
+ switch (transport) {
45787
46842
  case "HTTP":
45788
46843
  return { transport: "HTTP", method: e.httpMethod, path: e.path };
45789
46844
  case "gRPC":
@@ -45807,13 +46862,19 @@ ${renderChangeReport(report3)}`,
45807
46862
  for (const e of endpoints) {
45808
46863
  const m = intf.methods.find((x) => x.name === e.method);
45809
46864
  if (!m) return errText(`Method "${e.method}" not found on interface "${interfaceId}". Define it via sdd_define_interface first.`);
45810
- const parsed = EndpointSchema.safeParse(buildRaw(e));
46865
+ const transport = e.transport === "HTTP_API" ? "HTTP" : e.transport;
46866
+ if (implied && transport !== implied) {
46867
+ return errText(
46868
+ `Method "${e.method}": transport "${e.transport}" does not match ${owner.id}'s portalType ${owner.portalType}, which implies transport "${implied}". Bind it as "${implied}", or change the Portal's portalType.`
46869
+ );
46870
+ }
46871
+ const parsed = EndpointSchema.safeParse(buildRaw(e, transport));
45811
46872
  if (!parsed.success) {
45812
46873
  const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
45813
- return errText(`Invalid ${e.transport} endpoint for method "${e.method}": ${detail}`);
46874
+ return errText(`Invalid ${transport} endpoint for method "${e.method}": ${detail}`);
45814
46875
  }
45815
46876
  methods.push({ name: e.method, endpoint: parsed.data });
45816
- bound2.push(`${e.method}\u2192${e.transport}`);
46877
+ bound2.push(`${e.method}\u2192${transport}`);
45817
46878
  }
45818
46879
  const report3 = updateSpecGated("interface", interfaceId, { methods });
45819
46880
  return structured(
@@ -46074,14 +47135,24 @@ ${renderChangeReport(report3)}`,
46074
47135
  {
46075
47136
  description: `Get/read the parsed JSON contents of a specific spec from the spec tree. Returns structural contents without file system path searching. Pass "methods" to read only the named methods of a contract, an implementation or a type \u2014 a 45-method spec fetched whole to look at one of them is the read side of the same waste a restatement is on the write side; the answer then carries a "partialResult" marker naming what was left out, and must never be re-authored from. For a variant-tagged COMPONENT the result also carries a derived, read-only "variantGuidance" (the variant's base, its implementation guidance, and the same-variant sibling components to implement alike) \u2014 it is resolved from the variant registry, not part of the spec, so never write it back. A contract's methods come back in their STORED form \u2014 a method that takes its signature from a source carries its signatureFrom, not the params the loader resolves into it \u2014 so the answer can be re-authored from as it is; the resolved params, returns and text of each such method come back as a derived, read-only "resolvedSignatures" marker. The structured content carries the same answer with the derived markers KEPT SEPARATE from the stored spec ({kind, id, spec, partialResult?, variantGuidance?, resolvedSignatures?}), so nothing derived can be mistaken for something stored; the text block folds them in as it always has.`,
46076
47137
  inputSchema: {
46077
- kind: import_zod12.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).describe("The kind of specification"),
47138
+ kind: import_zod12.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).optional().describe("The kind of specification. Omit it to infer the kind from the id: the read is refused, naming the candidate kinds, when the id names specs at more than one level (an interface and an implementation sharing an id, say), or at none"),
46078
47139
  id: import_zod12.z.string().describe('The identifier of the spec to fetch (the L0 system spec is a singleton \u2014 pass the system name or "system")'),
46079
47140
  methods: import_zod12.z.array(import_zod12.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.")
46080
47141
  },
46081
47142
  outputSchema: getSpecOutput
46082
47143
  },
46083
- ({ kind, id, methods }) => {
47144
+ ({ kind: askedKind, id, methods }) => {
46084
47145
  try {
47146
+ let kind;
47147
+ if (askedKind) {
47148
+ kind = askedKind;
47149
+ } else {
47150
+ const holders = specKindsHolding(id);
47151
+ if (holders.length !== 1) {
47152
+ return errText(holders.length === 0 ? `No spec of any kind has the ID "${id}".` : `The ID "${id}" names specs of more than one kind (${holders.join(", ")}). Pass "kind" to choose one.`);
47153
+ }
47154
+ kind = holders[0];
47155
+ }
46085
47156
  let result = null;
46086
47157
  let partial2 = null;
46087
47158
  switch (kind) {
@@ -46275,9 +47346,9 @@ ${testsBlock}` : ""}`, deletion);
46275
47346
  server,
46276
47347
  "sdd_get_agent_brief",
46277
47348
  {
46278
- 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).",
47349
+ description: "Compose the LIVE delegation brief for one resolved agent: the fully rendered instruction body plus the ownedPaths/readPaths scope fence and the codeFence (where the agent may write CODE: its source files and their shared folder; empty when no file is named yet \u2014 then name the planned files when spawning) \u2014 the dynamic replacement for generated per-component agent files. A COMPONENT id (or `<component>-implementer`) composes that one component's implementer brief, with no agent files and no setting needed. 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).",
46279
47350
  inputSchema: {
46280
- agentId: import_zod12.z.string().describe("The resolved agent id (list via listAgents or resources/list)")
47351
+ agentId: import_zod12.z.string().describe("The resolved agent id (list via listAgents or resources/list), or a component id for that component's implementer brief")
46281
47352
  }
46282
47353
  },
46283
47354
  ({ agentId }) => {
@@ -46491,6 +47562,7 @@ function loadProjectConfig3() {
46491
47562
  aiDir,
46492
47563
  aiDirAt,
46493
47564
  aiPathsAt,
47565
+ aliasTargetOf,
46494
47566
  ambiguityMessage,
46495
47567
  approvalKeyIn,
46496
47568
  approvalRecord,
@@ -46634,6 +47706,7 @@ function loadProjectConfig3() {
46634
47706
  keyIn,
46635
47707
  landReference,
46636
47708
  languageOfSourcePath,
47709
+ lineEndingFor,
46637
47710
  listDirectChainedSubprojects,
46638
47711
  listExternals,
46639
47712
  listFiles,
@@ -46675,6 +47748,7 @@ function loadProjectConfig3() {
46675
47748
  memberDeclarationOf,
46676
47749
  memberDigest,
46677
47750
  memberLocationOf,
47751
+ memberTypesOf,
46678
47752
  methodGenericParameters,
46679
47753
  methodSourceFile,
46680
47754
  methodTypeRefs,
@@ -46747,6 +47821,7 @@ function loadProjectConfig3() {
46747
47821
  resolveExternals,
46748
47822
  resolveImport,
46749
47823
  resolveInstalledPack,
47824
+ resolveProjectBinding,
46750
47825
  resolveProjectExports,
46751
47826
  resolveSignatures,
46752
47827
  resolveSubprojectForNamespace,
@@ -46792,6 +47867,7 @@ function loadProjectConfig3() {
46792
47867
  typeIsMany,
46793
47868
  typeMatchesRef,
46794
47869
  typeNamedRefs,
47870
+ typeProblemEnumProposal,
46795
47871
  typeProblemIntProposal,
46796
47872
  typeSourceFiles,
46797
47873
  typeSpellingFacts,
@@ -46805,6 +47881,7 @@ function loadProjectConfig3() {
46805
47881
  validateProject,
46806
47882
  validateProjectConfig,
46807
47883
  validateRegistry,
47884
+ withLineEndings,
46808
47885
  withPack,
46809
47886
  writeFile,
46810
47887
  writeFileIfChanged,