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

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, "");
@@ -3709,6 +3709,7 @@ __export(src_exports, {
3709
3709
  keyIn: () => keyIn,
3710
3710
  landReference: () => landReference,
3711
3711
  languageOfSourcePath: () => languageOfSourcePath,
3712
+ lineEndingFor: () => lineEndingFor,
3712
3713
  listDirectChainedSubprojects: () => listDirectChainedSubprojects,
3713
3714
  listExternals: () => listExternals2,
3714
3715
  listFiles: () => listFiles,
@@ -3867,6 +3868,7 @@ __export(src_exports, {
3867
3868
  typeIsMany: () => typeIsMany,
3868
3869
  typeMatchesRef: () => typeMatchesRef,
3869
3870
  typeNamedRefs: () => typeNamedRefs,
3871
+ typeProblemEnumProposal: () => typeProblemEnumProposal,
3870
3872
  typeProblemIntProposal: () => typeProblemIntProposal,
3871
3873
  typeSourceFiles: () => typeSourceFiles,
3872
3874
  typeSpellingFacts: () => typeSpellingFacts,
@@ -3880,6 +3882,7 @@ __export(src_exports, {
3880
3882
  validateProject: () => validateProject,
3881
3883
  validateProjectConfig: () => validateProjectConfig,
3882
3884
  validateRegistry: () => validateRegistry,
3885
+ withLineEndings: () => withLineEndings,
3883
3886
  withPack: () => withPack,
3884
3887
  writeFile: () => writeFile,
3885
3888
  writeFileIfChanged: () => writeFileIfChanged,
@@ -3894,8 +3897,8 @@ __export(src_exports, {
3894
3897
  module.exports = __toCommonJS(src_exports);
3895
3898
 
3896
3899
  // src/config/project-config.ts
3897
- var fs2 = __toESM(require("fs"));
3898
- var path3 = __toESM(require("path"));
3900
+ var fs3 = __toESM(require("fs"));
3901
+ var path5 = __toESM(require("path"));
3899
3902
  var YAML = __toESM(require("yaml"));
3900
3903
  var import_zod4 = require("zod");
3901
3904
 
@@ -3908,17 +3911,119 @@ function ensureDir(dirPath) {
3908
3911
  }
3909
3912
  function writeFile(filePath, content) {
3910
3913
  ensureDir(path.dirname(filePath));
3911
- fs.writeFileSync(filePath, content, "utf-8");
3914
+ fs.writeFileSync(filePath, withLineEndings(filePath, content), "utf-8");
3912
3915
  }
3913
3916
  function writeFileIfChanged(filePath, content) {
3917
+ const text3 = withLineEndings(filePath, content);
3914
3918
  if (fs.existsSync(filePath)) {
3915
3919
  const existing = fs.readFileSync(filePath, "utf-8");
3916
- if (existing === content) return false;
3920
+ if (existing === text3) return false;
3917
3921
  }
3918
3922
  ensureDir(path.dirname(filePath));
3919
- fs.writeFileSync(filePath, content, "utf-8");
3923
+ fs.writeFileSync(filePath, text3, "utf-8");
3920
3924
  return true;
3921
3925
  }
3926
+ var EOL_SAMPLE_BYTES = 64 * 1024;
3927
+ function endingsOf(text3) {
3928
+ const crlf = text3.split("\r\n").length - 1;
3929
+ const lf = text3.split("\n").length - 1 - crlf;
3930
+ if (crlf === 0 && lf === 0) return null;
3931
+ return crlf > lf ? "\r\n" : "\n";
3932
+ }
3933
+ function sampleOf(file) {
3934
+ let fd;
3935
+ try {
3936
+ fd = fs.openSync(file, "r");
3937
+ const buffer = Buffer.alloc(EOL_SAMPLE_BYTES);
3938
+ const read2 = fs.readSync(fd, buffer, 0, EOL_SAMPLE_BYTES, 0);
3939
+ return buffer.subarray(0, read2).toString("utf-8");
3940
+ } catch {
3941
+ return null;
3942
+ } finally {
3943
+ if (fd !== void 0) fs.closeSync(fd);
3944
+ }
3945
+ }
3946
+ function attributeMatches(pattern, relative20) {
3947
+ const glob = pattern.replace(/^\//, "");
3948
+ const target = glob.includes("/") ? relative20 : path.posix.basename(relative20);
3949
+ const source = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0000/g, "(?:.*/)?");
3950
+ return new RegExp(`^${source}$`).test(target);
3951
+ }
3952
+ function attributeEol(attributesFile, file) {
3953
+ const text3 = readFileOrNull(attributesFile);
3954
+ if (text3 === null) return null;
3955
+ const relative20 = path.relative(path.dirname(attributesFile), file).split(path.sep).join("/");
3956
+ let eol = null;
3957
+ for (const raw of text3.split(/\r?\n/)) {
3958
+ const line = raw.trim();
3959
+ if (line === "" || line.startsWith("#")) continue;
3960
+ const [pattern, ...attrs] = line.split(/\s+/);
3961
+ if (!attributeMatches(pattern, relative20)) continue;
3962
+ for (const attr of attrs) {
3963
+ if (attr === "eol=crlf") eol = "\r\n";
3964
+ else if (attr === "eol=lf") eol = "\n";
3965
+ else if (attr === "-text" || attr === "binary") eol = null;
3966
+ }
3967
+ }
3968
+ return eol;
3969
+ }
3970
+ function conventionFolders(dir) {
3971
+ const folders = [];
3972
+ let at = path.resolve(dir);
3973
+ for (let depth = 0; depth < 8; depth++) {
3974
+ folders.push(at);
3975
+ if (fs.existsSync(path.join(at, ".git")) || fs.existsSync(path.join(at, ".wai"))) return folders;
3976
+ const parent = path.dirname(at);
3977
+ if (parent === at) break;
3978
+ at = parent;
3979
+ }
3980
+ return folders.slice(0, 1);
3981
+ }
3982
+ function folderEndings(folder, ext, exclude) {
3983
+ let names;
3984
+ try {
3985
+ names = fs.readdirSync(folder);
3986
+ } catch {
3987
+ return null;
3988
+ }
3989
+ 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);
3990
+ let crlf = 0;
3991
+ let lf = 0;
3992
+ for (const name of candidates) {
3993
+ const full = path.join(folder, name);
3994
+ try {
3995
+ if (!fs.statSync(full).isFile()) continue;
3996
+ } catch {
3997
+ continue;
3998
+ }
3999
+ const sample = sampleOf(full);
4000
+ const endings = sample === null ? null : endingsOf(sample);
4001
+ if (endings === "\r\n") crlf++;
4002
+ else if (endings === "\n") lf++;
4003
+ }
4004
+ if (crlf === lf) return null;
4005
+ return crlf > lf ? "\r\n" : "\n";
4006
+ }
4007
+ function lineEndingFor(filePath) {
4008
+ const own = sampleOf(filePath);
4009
+ const existing = own === null ? null : endingsOf(own);
4010
+ if (existing) return existing;
4011
+ const folders = conventionFolders(path.dirname(filePath));
4012
+ for (const folder of folders) {
4013
+ const eol = attributeEol(path.join(folder, ".gitattributes"), filePath);
4014
+ if (eol) return eol;
4015
+ }
4016
+ const ext = path.extname(filePath);
4017
+ for (const folder of folders) {
4018
+ const endings = folderEndings(folder, ext, path.resolve(filePath));
4019
+ if (endings) return endings;
4020
+ }
4021
+ return "\n";
4022
+ }
4023
+ function withLineEndings(filePath, content) {
4024
+ const normalised = content.replace(/\r\n/g, "\n");
4025
+ return lineEndingFor(filePath) === "\r\n" ? normalised.replace(/\n/g, "\r\n") : normalised;
4026
+ }
3922
4027
  function readFileOrNull(filePath) {
3923
4028
  try {
3924
4029
  return fs.readFileSync(filePath, "utf-8");
@@ -4064,44 +4169,67 @@ function aiDirAt(root, ...segments) {
4064
4169
  return path.join(base, ...segments);
4065
4170
  }
4066
4171
 
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));
4172
+ // src/config/defaults.ts
4173
+ var os = __toESM(require("os"));
4174
+ var path2 = __toESM(require("path"));
4175
+ var DEFAULT_TARGET_DIRS = {
4176
+ claude: ".claude/agents",
4177
+ gemini: ".gemini/agents",
4178
+ agy: ".gemini/agents",
4179
+ cursor: ".cursor/rules",
4180
+ copilot: ".github/prompts",
4181
+ codex: ".codex/agents"
4182
+ };
4183
+ function defaultTargetConfig(type) {
4184
+ return {
4185
+ type,
4186
+ outputDir: DEFAULT_TARGET_DIRS[type],
4187
+ enabled: true
4188
+ };
4092
4189
  }
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)}`);
4190
+ var WAIRON_VERSION = "5.1.1-dev.102";
4191
+ var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
4192
+ var ARCHITECT_AGENT_ID = "agent-architect";
4193
+ var ARCHITECT_TEMPLATE_ID = "architect";
4194
+ function globalTemplatesDir(projectOverride) {
4195
+ if (process.env.WAIRON_TEMPLATES_DIR) {
4196
+ return process.env.WAIRON_TEMPLATES_DIR;
4100
4197
  }
4198
+ if (projectOverride) {
4199
+ return projectOverride;
4200
+ }
4201
+ return path2.join(os.homedir(), ".wairon", "templates");
4101
4202
  }
4102
- function writeJsonFile(filePath, value) {
4103
- writeFile(filePath, JSON.stringify(value, null, 2) + "\n");
4203
+ var BACKEND_COMMANDS = {
4204
+ claude: "claude",
4205
+ gemini: "gemini"
4206
+ };
4207
+ function backendCommand(backend) {
4208
+ return BACKEND_COMMANDS[backend] ?? backend;
4104
4209
  }
4210
+ var SUPPORTED_ALIASES = ["wai"];
4211
+ var SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
4212
+ "node_modules",
4213
+ "dist",
4214
+ "build",
4215
+ "out",
4216
+ ".git",
4217
+ ".cache",
4218
+ "coverage",
4219
+ "__pycache__",
4220
+ ".venv",
4221
+ "venv",
4222
+ "target",
4223
+ // Rust
4224
+ "vendor"
4225
+ ]);
4226
+
4227
+ // src/core/adapters/git-source.ts
4228
+ var import_child_process = require("child_process");
4229
+ var crypto = __toESM(require("crypto"));
4230
+ var fs2 = __toESM(require("fs"));
4231
+ var os2 = __toESM(require("os"));
4232
+ var path3 = __toESM(require("path"));
4105
4233
 
4106
4234
  // src/utils/errors.ts
4107
4235
  var WaironError = class extends Error {
@@ -4151,6 +4279,167 @@ ${reasons.map((r) => ` - ${r}`).join("\n")}`);
4151
4279
  }
4152
4280
  };
4153
4281
 
4282
+ // src/core/adapters/git-source.ts
4283
+ var SourceUnavailable = class extends WaironError {
4284
+ constructor(message) {
4285
+ super(message);
4286
+ this.name = "SourceUnavailable";
4287
+ }
4288
+ };
4289
+ function osUserCache() {
4290
+ if (process.platform === "win32") return process.env.LOCALAPPDATA ?? path3.join(os2.homedir(), "AppData", "Local");
4291
+ if (process.platform === "darwin") return path3.join(os2.homedir(), "Library", "Caches");
4292
+ return process.env.XDG_CACHE_HOME ?? path3.join(os2.homedir(), ".cache");
4293
+ }
4294
+ var cacheRoots = /* @__PURE__ */ new Map();
4295
+ function cacheRoot() {
4296
+ const setting = process.env.WAIRON_CACHE_DIR ?? "";
4297
+ let root = cacheRoots.get(setting);
4298
+ if (root === void 0) {
4299
+ root = setting !== "" ? path3.resolve(setting) : path3.join(osUserCache(), "wairon");
4300
+ cacheRoots.set(setting, root);
4301
+ }
4302
+ return root;
4303
+ }
4304
+ function git(args, cwd) {
4305
+ return (0, import_child_process.execFileSync)("git", args, {
4306
+ cwd,
4307
+ encoding: "utf8",
4308
+ stdio: ["ignore", "pipe", "pipe"],
4309
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "echo" }
4310
+ });
4311
+ }
4312
+ function reasonOf(e) {
4313
+ const stderr = e?.stderr;
4314
+ const text3 = typeof stderr === "string" && stderr.trim() !== "" ? stderr : e instanceof Error ? e.message : String(e);
4315
+ return text3.trim().split(/\r?\n/).filter((l) => l.trim() !== "").slice(-1)[0] ?? "git failed";
4316
+ }
4317
+ function cacheEntryOf(url, commit2) {
4318
+ const repo = crypto.createHash("sha256").update(url).digest("hex");
4319
+ return path3.join(cacheRoot(), "git", repo, commit2);
4320
+ }
4321
+ function within(entry, url, commit2, dir) {
4322
+ const at = dir ? path3.join(entry, dir) : entry;
4323
+ if (!fs2.existsSync(at)) {
4324
+ throw new SourceUnavailable(`the commit ${commit2} of ${url} holds no "${dir}"`);
4325
+ }
4326
+ return at;
4327
+ }
4328
+ function fetchInto(tmp, url, commit2) {
4329
+ git(["init", "-q"], tmp);
4330
+ try {
4331
+ git(["fetch", "-q", "--depth", "1", url, commit2], tmp);
4332
+ } catch {
4333
+ git(["fetch", "-q", url, "+refs/heads/*:refs/remotes/source/*", "+refs/tags/*:refs/tags/*"], tmp);
4334
+ }
4335
+ git(["-c", "advice.detachedHead=false", "checkout", "-q", commit2], tmp);
4336
+ fs2.rmSync(path3.join(tmp, ".git"), { recursive: true, force: true });
4337
+ }
4338
+ function fetch(url, commit2, dir) {
4339
+ const entry = cacheEntryOf(url, commit2);
4340
+ if (fs2.existsSync(entry)) return within(entry, url, commit2, dir);
4341
+ fs2.mkdirSync(path3.dirname(entry), { recursive: true });
4342
+ const tmp = `${entry}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
4343
+ fs2.mkdirSync(tmp, { recursive: true });
4344
+ try {
4345
+ fetchInto(tmp, url, commit2);
4346
+ try {
4347
+ fs2.renameSync(tmp, entry);
4348
+ } catch (e) {
4349
+ if (!fs2.existsSync(entry)) throw e;
4350
+ fs2.rmSync(tmp, { recursive: true, force: true });
4351
+ }
4352
+ } catch (e) {
4353
+ fs2.rmSync(tmp, { recursive: true, force: true });
4354
+ if (fs2.readdirSync(path3.dirname(entry)).length === 0) fs2.rmdirSync(path3.dirname(entry));
4355
+ throw e instanceof SourceUnavailable ? e : new SourceUnavailable(`cannot fetch ${commit2} of ${url}: ${reasonOf(e)}`);
4356
+ }
4357
+ return within(entry, url, commit2, dir);
4358
+ }
4359
+ function resolve3(url, ref) {
4360
+ let listed;
4361
+ try {
4362
+ listed = git(["ls-remote", url, ref ?? "HEAD"]);
4363
+ } catch (e) {
4364
+ throw new SourceUnavailable(`cannot reach ${url}: ${reasonOf(e)}`);
4365
+ }
4366
+ const wanted = commitListed(listed, ref ?? "HEAD");
4367
+ if (!wanted) throw new SourceUnavailable(`${url} lists no ref "${ref ?? "HEAD"}"`);
4368
+ return wanted;
4369
+ }
4370
+ function commitListed(listed, ref) {
4371
+ const lines = listed.split(/\r?\n/).filter((l) => l.trim() !== "").map((l) => l.split(/\s+/));
4372
+ const names = [ref, `refs/heads/${ref}`, `refs/tags/${ref}^{}`, `refs/tags/${ref}`];
4373
+ for (const name of names) {
4374
+ const line = lines.find(([, listedName]) => listedName === name);
4375
+ if (line) return line[0];
4376
+ }
4377
+ return void 0;
4378
+ }
4379
+ function trackedFiles(directory, pathspecs) {
4380
+ if (pathspecs.length === 0) return [];
4381
+ try {
4382
+ return git(["ls-files", "-z", "--", ...pathspecs], directory).split("\0").filter((p) => p !== "");
4383
+ } catch {
4384
+ return null;
4385
+ }
4386
+ }
4387
+ function head(directory) {
4388
+ try {
4389
+ return git(["rev-parse", "HEAD"], directory).trim() || null;
4390
+ } catch {
4391
+ return null;
4392
+ }
4393
+ }
4394
+ function repositoryRoot(directory) {
4395
+ let at = path3.resolve(directory);
4396
+ for (; ; ) {
4397
+ if (fs2.existsSync(path3.join(at, ".git"))) return at;
4398
+ const up = path3.dirname(at);
4399
+ if (up === at) return null;
4400
+ at = up;
4401
+ }
4402
+ }
4403
+
4404
+ // src/utils/yaml.ts
4405
+ var yaml = __toESM(require("js-yaml"));
4406
+ function parseYaml(content, sourcePath) {
4407
+ try {
4408
+ return yaml.load(content);
4409
+ } catch (err) {
4410
+ const loc = sourcePath ? ` (${sourcePath})` : "";
4411
+ throw new Error(`Failed to parse YAML${loc}: ${String(err)}`);
4412
+ }
4413
+ }
4414
+ function serializeYaml(value) {
4415
+ return yaml.dump(value, {
4416
+ indent: 2,
4417
+ lineWidth: 100,
4418
+ noRefs: true,
4419
+ sortKeys: false
4420
+ });
4421
+ }
4422
+ function readYamlFile(filePath) {
4423
+ const content = readFileOrNull(filePath);
4424
+ if (content === null) return null;
4425
+ return parseYaml(content, filePath);
4426
+ }
4427
+ function writeYamlFile(filePath, value) {
4428
+ writeFile(filePath, serializeYaml(value));
4429
+ }
4430
+ function readJsonFile(filePath) {
4431
+ const content = readFileOrNull(filePath);
4432
+ if (content === null) return null;
4433
+ try {
4434
+ return JSON.parse(content);
4435
+ } catch (err) {
4436
+ throw new Error(`Failed to parse JSON (${filePath}): ${String(err)}`);
4437
+ }
4438
+ }
4439
+ function writeJsonFile(filePath, value) {
4440
+ writeFile(filePath, JSON.stringify(value, null, 2) + "\n");
4441
+ }
4442
+
4154
4443
  // src/models/identity-rename.ts
4155
4444
  var EDGE = " -> ";
4156
4445
  var QUALIFIED_UNIT = /^(\d+:)?([^.\s]+)\.([^.\s]+)$/;
@@ -4186,7 +4475,7 @@ function rekeyAnchor(rename, anchor) {
4186
4475
  }
4187
4476
 
4188
4477
  // src/models/project.ts
4189
- var path2 = __toESM(require("path"));
4478
+ var path4 = __toESM(require("path"));
4190
4479
  var import_zod3 = require("zod");
4191
4480
 
4192
4481
  // src/models/agent.ts
@@ -4736,7 +5025,7 @@ var ExternalDeclarationSchema = import_zod3.z.object({
4736
5025
  /**
4737
5026
  * The producer's public names this project imports, so its specs may name
4738
5027
  * them bare: `['*']` imports every public name the producer exports to this
4739
- * project, `[waffler-error]` only those. Plain strings: a malformed entry is
5028
+ * project, `[shared-error]` only those. Plain strings: a malformed entry is
4740
5029
  * REPORTED (the declaration's problem), never unreadable.
4741
5030
  */
4742
5031
  use: import_zod3.z.array(import_zod3.z.string()).optional(),
@@ -4939,7 +5228,7 @@ var PACK_EXT_RE = /\.(ya?ml|cjs|js)$/i;
4939
5228
  function declaredPackNames(config) {
4940
5229
  const selection = config.profileSelection;
4941
5230
  return [.../* @__PURE__ */ new Set([
4942
- ...(config.extensions?.packs ?? []).map((entry) => typeof entry === "string" ? path2.basename(entry).replace(PACK_EXT_RE, "") : entry.name),
5231
+ ...(config.extensions?.packs ?? []).map((entry) => typeof entry === "string" ? path4.basename(entry).replace(PACK_EXT_RE, "") : entry.name),
4943
5232
  ...selection?.requiredPackNames ?? [],
4944
5233
  ...selection?.defaultPackNames ?? []
4945
5234
  ])];
@@ -5237,13 +5526,13 @@ function isPart(config) {
5237
5526
 
5238
5527
  // src/config/project-config.ts
5239
5528
  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;
5529
+ const wai = path5.join(root, ".wai");
5530
+ const legacy = path5.join(root, ".wairon");
5531
+ return !fs3.existsSync(wai) && fs3.existsSync(legacy) ? legacy : wai;
5243
5532
  }
5244
5533
  function projectConfigFsAdapterAt(rootDir) {
5245
- const root = path3.resolve(rootDir);
5246
- const file = () => path3.join(waiDirAt(root), "project.yaml");
5534
+ const root = path5.resolve(rootDir);
5535
+ const file = () => path5.join(waiDirAt(root), "project.yaml");
5247
5536
  return {
5248
5537
  readDocument() {
5249
5538
  return readYamlFile(file());
@@ -5257,7 +5546,7 @@ function projectConfigFsAdapterAt(rootDir) {
5257
5546
  writeFile(file(), editDocumentText(text3, document, root));
5258
5547
  },
5259
5548
  documentExists() {
5260
- return fs2.existsSync(file());
5549
+ return fs3.existsSync(file());
5261
5550
  },
5262
5551
  readText() {
5263
5552
  return readFileOrNull(file());
@@ -5279,8 +5568,9 @@ function storeOver(adapter, root) {
5279
5568
  if (!checked.success) {
5280
5569
  throw new WaironError(`Refusing to write an invalid .wai/project.yaml at ${root}: ${checked.error.message}`);
5281
5570
  }
5282
- const document = config.partOf !== void 0 ? { schemaVersion: config.schemaVersion, partOf: config.partOf } : config;
5283
- adapter.writeDocument(overlayKnownFields(ProjectConfigSchema, document, currentDocument()));
5571
+ const onDisk = currentDocument();
5572
+ const document = config.partOf !== void 0 ? { schemaVersion: config.schemaVersion, partOf: config.partOf } : withoutDefaultedMaterialization(config, onDisk);
5573
+ adapter.writeDocument(overlayKnownFields(ProjectConfigSchema, document, onDisk));
5284
5574
  },
5285
5575
  exists() {
5286
5576
  return adapter.documentExists();
@@ -5680,9 +5970,69 @@ function asReadable(document) {
5680
5970
  if (!isPlainObject(document) || document.partOf === void 0) return document;
5681
5971
  return { name: "", createdAt: PART_EPOCH, updatedAt: PART_EPOCH, ...document };
5682
5972
  }
5973
+ var MANAGED_AGENT_MARKER = "wairon:managed";
5974
+ var MANAGED_AGENT_NAME = /-(owner|implementer|architect)\.(md|ya?ml)$/;
5975
+ function holdsMaterializedAgentFiles(document, root) {
5976
+ const dirs = new Set(Object.values(DEFAULT_TARGET_DIRS));
5977
+ for (const target of Array.isArray(document.targets) ? document.targets : []) {
5978
+ const outputDir = isPlainObject(target) ? target.outputDir : void 0;
5979
+ if (typeof outputDir === "string") dirs.add(outputDir);
5980
+ }
5981
+ const base = path5.resolve(root);
5982
+ const inside = [];
5983
+ for (const dir of dirs) {
5984
+ const rel2 = path5.relative(base, path5.resolve(base, dir));
5985
+ if (rel2.startsWith("..") || path5.isAbsolute(rel2)) continue;
5986
+ let present = false;
5987
+ try {
5988
+ present = fs3.statSync(path5.join(base, rel2)).isDirectory();
5989
+ } catch {
5990
+ present = false;
5991
+ }
5992
+ if (present) inside.push(rel2 === "" ? "." : rel2.split(path5.sep).join("/"));
5993
+ }
5994
+ if (inside.length === 0) return false;
5995
+ const tracked = trackedFiles(base, inside);
5996
+ const candidates = [];
5997
+ if (tracked === null) {
5998
+ for (const dir of inside) {
5999
+ let names;
6000
+ try {
6001
+ names = fs3.readdirSync(path5.join(base, dir));
6002
+ } catch {
6003
+ continue;
6004
+ }
6005
+ for (const name of names) candidates.push(path5.posix.join(dir, name));
6006
+ }
6007
+ } else {
6008
+ const folders = new Set(inside.map((dir) => path5.posix.normalize(dir)));
6009
+ for (const file of tracked) if (folders.has(path5.posix.dirname(file))) candidates.push(file);
6010
+ }
6011
+ for (const file of candidates) {
6012
+ const head2 = readFileOrNull(path5.join(base, file));
6013
+ if (head2 === null) continue;
6014
+ if (MANAGED_AGENT_NAME.test(path5.posix.basename(file))) return true;
6015
+ if (head2.slice(0, 4096).includes(MANAGED_AGENT_MARKER)) return true;
6016
+ }
6017
+ return false;
6018
+ }
6019
+ function withInferredMaterialization(document, root) {
6020
+ if (!isPlainObject(document) || document.partOf !== void 0) return document;
6021
+ const rules = isPlainObject(document.rules) ? document.rules : void 0;
6022
+ if (rules !== void 0 && hasOwn(rules, "materializeAgentFiles")) return document;
6023
+ if (!holdsMaterializedAgentFiles(document, root)) return document;
6024
+ return { ...document, rules: { ...rules ?? {}, materializeAgentFiles: true } };
6025
+ }
6026
+ function withoutDefaultedMaterialization(config, onDisk) {
6027
+ if (!isPlainObject(onDisk) || config.rules?.materializeAgentFiles !== false) return config;
6028
+ const stated = isPlainObject(onDisk.rules) && hasOwn(onDisk.rules, "materializeAgentFiles");
6029
+ if (stated) return config;
6030
+ const { materializeAgentFiles: _defaulted, ...rules } = config.rules;
6031
+ return { ...config, rules };
6032
+ }
5683
6033
  function parseConfig(document, root) {
5684
6034
  try {
5685
- return ProjectConfigSchema.parse(asReadable(document));
6035
+ return ProjectConfigSchema.parse(asReadable(withInferredMaterialization(document, root)));
5686
6036
  } catch (e) {
5687
6037
  throw new WaironError(
5688
6038
  `Invalid .wai/project.yaml: ${e instanceof Error ? e.message : String(e)}
@@ -6123,15 +6473,15 @@ function indexOver(store, root) {
6123
6473
  specsDir() {
6124
6474
  try {
6125
6475
  const declared = store.readField("paths.specsDir");
6126
- if (declared) return path3.resolve(root, declared);
6476
+ if (declared) return path5.resolve(root, declared);
6127
6477
  } catch {
6128
6478
  }
6129
- return path3.join(waiDirAt(root), "specs");
6479
+ return path5.join(waiDirAt(root), "specs");
6130
6480
  }
6131
6481
  };
6132
6482
  }
6133
6483
  function projectConfigRepositoryOver(adapter, rootDir) {
6134
- const root = path3.resolve(rootDir);
6484
+ const root = path5.resolve(rootDir);
6135
6485
  const store = storeOver(adapter, root);
6136
6486
  const index = indexOver(store, root);
6137
6487
  const registry = registryOver(store, root);
@@ -6324,15 +6674,15 @@ var fs14 = __toESM(require("fs"));
6324
6674
  var path21 = __toESM(require("path"));
6325
6675
 
6326
6676
  // src/config/paths.ts
6327
- var fs3 = __toESM(require("fs"));
6328
- var path4 = __toESM(require("path"));
6677
+ var fs4 = __toESM(require("fs"));
6678
+ var path6 = __toESM(require("path"));
6329
6679
  function aiPathsAt(rootDir) {
6330
- const resolvedRoot = path4.resolve(rootDir);
6680
+ const resolvedRoot = path6.resolve(rootDir);
6331
6681
  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);
6682
+ const waiPath = path6.join(resolvedRoot, ".wai");
6683
+ const waironPath = path6.join(resolvedRoot, ".wairon");
6684
+ const base = !fs4.existsSync(waiPath) && fs4.existsSync(waironPath) ? waironPath : waiPath;
6685
+ return path6.join(base, ...segments);
6336
6686
  };
6337
6687
  const specsDir = () => projectConfigRepositoryAt(resolvedRoot).specsDir();
6338
6688
  return {
@@ -6349,12 +6699,12 @@ function aiPathsAt(rootDir) {
6349
6699
  contextDomainsMd: () => aiDirAt2("context", "domains.md"),
6350
6700
  contextWaironGuideMd: () => aiDirAt2("context", "wairon-guide.md"),
6351
6701
  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")
6702
+ specsSystem: () => path6.join(specsDir(), ".index.yaml"),
6703
+ specsSubsystemsDir: () => path6.join(specsDir(), "subsystems"),
6704
+ specsComponentsDir: () => path6.join(specsDir(), "components"),
6705
+ specsInterfacesDir: () => path6.join(specsDir(), "interfaces"),
6706
+ specsImplementationsDir: () => path6.join(specsDir(), "implementations"),
6707
+ specsTypesDir: () => path6.join(specsDir(), "types")
6358
6708
  };
6359
6709
  }
6360
6710
  var AI_PATHS = {
@@ -6388,8 +6738,8 @@ function assertProjectInitialized() {
6388
6738
  }
6389
6739
 
6390
6740
  // src/core/statehash.ts
6391
- var crypto = __toESM(require("crypto"));
6392
- var path5 = __toESM(require("path"));
6741
+ var crypto2 = __toESM(require("crypto"));
6742
+ var path7 = __toESM(require("path"));
6393
6743
 
6394
6744
  // src/utils/canonical-json.ts
6395
6745
  function canonicalize(value) {
@@ -6428,7 +6778,7 @@ function digestTree(keep, asStored = (impl) => impl) {
6428
6778
  implementations: kind(loadImplementationSpecs().map(asStored)),
6429
6779
  types: kind(loadTypeSpecs())
6430
6780
  };
6431
- const digest3 = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
6781
+ const digest3 = crypto2.createHash("sha256").update(canonicalize(tree)).digest("hex");
6432
6782
  return { algorithm: CONTENT_ALGORITHM, digest: digest3 };
6433
6783
  }
6434
6784
  function computeStateId() {
@@ -6448,11 +6798,11 @@ function computeOwnStateId() {
6448
6798
  return digestTree((id) => (owners.get(id) ?? "") === "", asStored);
6449
6799
  }
6450
6800
  function isInside(dir, file) {
6451
- const rel2 = path5.relative(dir, file);
6452
- return rel2 !== "" && !rel2.startsWith("..") && !path5.isAbsolute(rel2);
6801
+ const rel2 = path7.relative(dir, file);
6802
+ return rel2 !== "" && !rel2.startsWith("..") && !path7.isAbsolute(rel2);
6453
6803
  }
6454
6804
  function fromPartRoot(impl, root, partDir) {
6455
- const from = (p) => path5.isAbsolute(p) ? p : path5.relative(partDir, path5.resolve(root, p)).split(path5.sep).join("/");
6805
+ const from = (p) => path7.isAbsolute(p) ? p : path7.relative(partDir, path7.resolve(root, p)).split(path7.sep).join("/");
6456
6806
  return {
6457
6807
  ...impl,
6458
6808
  ...impl.sourcePath ? { sourcePath: from(impl.sourcePath) } : {},
@@ -6465,8 +6815,8 @@ function stateIdEquals(a, b) {
6465
6815
  }
6466
6816
 
6467
6817
  // src/core/lockfile.ts
6468
- var fs4 = __toESM(require("fs"));
6469
- var path6 = __toESM(require("path"));
6818
+ var fs5 = __toESM(require("fs"));
6819
+ var path8 = __toESM(require("path"));
6470
6820
  function lockPath() {
6471
6821
  return aiDir("lock.json");
6472
6822
  }
@@ -6480,14 +6830,14 @@ function normalizeApprover(value) {
6480
6830
  }
6481
6831
  function readLockRecord() {
6482
6832
  try {
6483
- return normalizeRecord(JSON.parse(fs4.readFileSync(lockPath(), "utf8")));
6833
+ return normalizeRecord(JSON.parse(fs5.readFileSync(lockPath(), "utf8")));
6484
6834
  } catch {
6485
6835
  return null;
6486
6836
  }
6487
6837
  }
6488
6838
  function readLockRecordAt(root) {
6489
6839
  try {
6490
- return normalizeRecord(JSON.parse(fs4.readFileSync(aiDirAt(root, "lock.json"), "utf8")));
6840
+ return normalizeRecord(JSON.parse(fs5.readFileSync(aiDirAt(root, "lock.json"), "utf8")));
6491
6841
  } catch {
6492
6842
  return null;
6493
6843
  }
@@ -6498,10 +6848,10 @@ function normalizeRecord(raw) {
6498
6848
  }
6499
6849
  function writeLockRecord(record) {
6500
6850
  const p = lockPath();
6501
- fs4.mkdirSync(path6.dirname(p), { recursive: true });
6851
+ fs5.mkdirSync(path8.dirname(p), { recursive: true });
6502
6852
  const tmp = `${p}.tmp`;
6503
- fs4.writeFileSync(tmp, JSON.stringify(withSortedMaps(record), null, 2) + "\n");
6504
- fs4.renameSync(tmp, p);
6853
+ fs5.writeFileSync(tmp, withLineEndings(p, JSON.stringify(withSortedMaps(record), null, 2) + "\n"));
6854
+ fs5.renameSync(tmp, p);
6505
6855
  }
6506
6856
  function withSortedMaps(record) {
6507
6857
  const sorted = (m) => {
@@ -6521,8 +6871,8 @@ function withSortedMaps(record) {
6521
6871
  }
6522
6872
 
6523
6873
  // src/core/spec-files.ts
6524
- var fs5 = __toESM(require("fs"));
6525
- var path7 = __toESM(require("path"));
6874
+ var fs6 = __toESM(require("fs"));
6875
+ var path9 = __toESM(require("path"));
6526
6876
  var SPEC_FILE_EXTENSION = ".yaml";
6527
6877
  function readSpecFile(specPath) {
6528
6878
  return readYamlFile(specPath);
@@ -6534,136 +6884,17 @@ function listSpecFiles(specsDir) {
6534
6884
  return listFilesRecursive(specsDir, SPEC_FILE_EXTENSION);
6535
6885
  }
6536
6886
  function removeSpecFile(specPath, specsRoot) {
6537
- if (!fs5.existsSync(specPath)) return false;
6538
- fs5.unlinkSync(specPath);
6539
- pruneEmptyDirs(path7.dirname(specPath), path7.resolve(specsRoot));
6887
+ if (!fs6.existsSync(specPath)) return false;
6888
+ fs6.unlinkSync(specPath);
6889
+ pruneEmptyDirs(path9.dirname(specPath), path9.resolve(specsRoot));
6540
6890
  return true;
6541
6891
  }
6542
6892
  function pruneEmptyDirs(dir, specsRoot) {
6543
6893
  let at = dir;
6544
6894
  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;
6895
+ if (!fs6.existsSync(at) || fs6.readdirSync(at).length > 0) return;
6896
+ fs6.rmdirSync(at);
6897
+ at = path9.dirname(at);
6667
6898
  }
6668
6899
  }
6669
6900
 
@@ -8062,7 +8293,11 @@ var GENERIC_ALIASES = /* @__PURE__ */ new Map([
8062
8293
  ["HashMap", { form: "map", arity: 2 }],
8063
8294
  ["Option", { form: "optional", arity: 1 }],
8064
8295
  ["Optional", { form: "optional", arity: 1 }],
8065
- ["Promise", { form: "async", arity: 1 }]
8296
+ ["Promise", { form: "async", arity: 1 }],
8297
+ // The success-or-failure type the mainstream languages with typed failures
8298
+ // share (Rust, Swift, Kotlin, F#); Rust's spelling is its alias.
8299
+ ["result", { form: "result", arity: 2 }],
8300
+ ["Result", { form: "result", arity: 2 }]
8066
8301
  ]);
8067
8302
  var NOT_NEUTRAL = /* @__PURE__ */ new Map([
8068
8303
  ["uuid", "a named value-object holding a string (e.g. order_id)"],
@@ -8071,7 +8306,6 @@ var NOT_NEUTRAL = /* @__PURE__ */ new Map([
8071
8306
  ["byte", "int, or bytes for binary data"],
8072
8307
  ["time", "datetime for an instant, or duration for an elapsed time"],
8073
8308
  ["tuple", "a named value-object"],
8074
- ["result", "the success type as the returns (failures as findings or throws), or a named value-object"],
8075
8309
  ["error", "a named value-object"],
8076
8310
  ["never", "void"],
8077
8311
  ["box", "the inner type (ownership is an implementation detail)"],
@@ -8266,9 +8500,9 @@ var RawParser = class {
8266
8500
  }
8267
8501
  name += this.ident();
8268
8502
  while (this.isP("::") || this.isP(".")) {
8269
- const sep14 = this.peek().v;
8503
+ const sep15 = this.peek().v;
8270
8504
  this.at++;
8271
- name += sep14 + this.ident();
8505
+ name += sep15 + this.ident();
8272
8506
  }
8273
8507
  if (!this.isP("<")) return { k: "name", name };
8274
8508
  this.at++;
@@ -8295,6 +8529,10 @@ var RawParser = class {
8295
8529
  const close2 = this.closingParen();
8296
8530
  const after = close2 === -1 ? void 0 : this.tokens[close2 + 1];
8297
8531
  if (!after || after.t !== "p" || after.v !== "=>") {
8532
+ if (this.isP(")", 1)) {
8533
+ this.at += 2;
8534
+ return { k: "name", name: "void" };
8535
+ }
8298
8536
  this.expect("(");
8299
8537
  const inner2 = this.type();
8300
8538
  this.expect(")");
@@ -8403,7 +8641,7 @@ function optionalOf(inner2) {
8403
8641
  return { form: "optional", args: [inner2] };
8404
8642
  }
8405
8643
  function inner(ctx) {
8406
- return { ...ctx, top: false, voidAllowed: false, omittable: false };
8644
+ return { ...ctx, top: false, voidAllowed: false, resultAllowed: false, omittable: false };
8407
8645
  }
8408
8646
  function normalise(node, ctx, notes) {
8409
8647
  switch (node.k) {
@@ -8437,9 +8675,21 @@ function asyncOf(innerNode, ctx, notes, spelled) {
8437
8675
  if (!ctx.top || !RETURNS_POSITIONS.has(ctx.position)) {
8438
8676
  throw new TypeProblemError("TYPE_POSITION_INVALID", `${spelled} may stand only at the top of a returns, not in a ${ctx.position}`);
8439
8677
  }
8440
- const read2 = normalise(innerNode, { ...inner(ctx), voidAllowed: true }, notes);
8678
+ const read2 = normalise(innerNode, { ...inner(ctx), voidAllowed: true, resultAllowed: true }, notes);
8441
8679
  return { form: "async", args: [read2] };
8442
8680
  }
8681
+ function resultOf(args, ctx, notes, spelled) {
8682
+ if (!ctx.resultAllowed) {
8683
+ throw new TypeProblemError(
8684
+ "TYPE_POSITION_INVALID",
8685
+ `${spelled} may stand only as a whole returns (or under its async), not in a ${ctx.top ? ctx.position : "collection, union, optional or argument"}`,
8686
+ "the success type here (a failure belongs to a method's returns)"
8687
+ );
8688
+ }
8689
+ const ok = normalise(args[0], { ...inner(ctx), voidAllowed: true }, notes);
8690
+ const err = normalise(args[1], inner(ctx), notes);
8691
+ return { form: "result", args: [ok, err] };
8692
+ }
8443
8693
  function bareName(name, ctx, notes) {
8444
8694
  const lower = name.toLowerCase();
8445
8695
  if (NONES.has(name)) {
@@ -8477,6 +8727,7 @@ function appliedName(name, args, ctx, notes) {
8477
8727
  throw new TypeProblemError("TYPE_EXPRESSION_INVALID", `"${name}" takes ${alias.arity} type argument${alias.arity === 1 ? "" : "s"}, ${args.length} given`);
8478
8728
  }
8479
8729
  if (alias.form === "async") return asyncOf(args[0], ctx, notes, `${name}<...>`);
8730
+ if (alias.form === "result") return resultOf(args, ctx, notes, `${name}<...>`);
8480
8731
  if (alias.form === "optional") return optionalOf(normalise(args[0], inner(ctx), notes));
8481
8732
  const read2 = args.map((a) => normalise(a, inner(ctx), notes));
8482
8733
  if (alias.form === "map") assertMapKey(read2[0], args[0]);
@@ -8550,6 +8801,8 @@ function canonicalTypeText(expr) {
8550
8801
  return `${expr.form}<${canonicalTypeText(expr.args[0])}>`;
8551
8802
  case "map":
8552
8803
  return `map<${canonicalTypeText(expr.args[0])}, ${canonicalTypeText(expr.args[1])}>`;
8804
+ case "result":
8805
+ return `result<${canonicalTypeText(expr.args[0])}, ${canonicalTypeText(expr.args[1])}>`;
8553
8806
  case "optional": {
8554
8807
  const innerText = canonicalTypeText(expr.args[0]);
8555
8808
  return expr.args[0].form === "union" ? `(${innerText})?` : `${innerText}?`;
@@ -8569,11 +8822,12 @@ function typeNamedRefs(expr) {
8569
8822
  }
8570
8823
  function typeIsMany(expr) {
8571
8824
  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]);
8825
+ if (expr.form === "optional" || expr.form === "async" || expr.form === "result") return typeIsMany(expr.args[0]);
8573
8826
  return false;
8574
8827
  }
8575
8828
  function contextFor(position, omittable2, looseNumber = false) {
8576
- return { position, top: true, voidAllowed: RETURNS_POSITIONS.has(position), omittable: omittable2, looseNumber };
8829
+ const returns = RETURNS_POSITIONS.has(position);
8830
+ return { position, top: true, voidAllowed: returns, resultAllowed: returns, omittable: omittable2, looseNumber };
8577
8831
  }
8578
8832
  function parseTypePosition(text3, position, omittable2 = false) {
8579
8833
  let raw;
@@ -8764,6 +9018,47 @@ function typeProblemIntProposal(problem) {
8764
9018
  if (!parse.expression || parse.problem) return null;
8765
9019
  return { specId: problem.specId, kind: problem.kind, path: problem.path, written: problem.written, stored: parse.canonical };
8766
9020
  }
9021
+ function stringLiteralValue(text3) {
9022
+ const quote = text3[0];
9023
+ if (quote !== "'" && quote !== '"' && quote !== "`" || text3[text3.length - 1] !== quote) return null;
9024
+ return text3.slice(1, -1).replace(/\\(.)/g, "$1");
9025
+ }
9026
+ function kebabOf(name) {
9027
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[\s_.]+/g, "-").toLowerCase();
9028
+ }
9029
+ function typeProblemEnumProposal(problem) {
9030
+ if (problem.code !== "TYPE_FORM_UNSUPPORTED") return null;
9031
+ if (problem.specId === void 0 || problem.kind === void 0 || problem.path === void 0) return null;
9032
+ let raw;
9033
+ try {
9034
+ raw = readRaw(problem.written);
9035
+ } catch (e) {
9036
+ if (e instanceof TypeSyntaxError) return null;
9037
+ throw e;
9038
+ }
9039
+ const members = raw.k === "union" ? raw.members : [raw];
9040
+ const values = [];
9041
+ let optional = false;
9042
+ for (const member of members) {
9043
+ if (member.k === "name" && member.args === void 0 && NONES.has(member.name)) {
9044
+ optional = true;
9045
+ continue;
9046
+ }
9047
+ const value = member.k === "literal" ? stringLiteralValue(member.text) : null;
9048
+ if (value === null) return null;
9049
+ if (!values.includes(value)) values.push(value);
9050
+ }
9051
+ if (values.length === 0) return null;
9052
+ return {
9053
+ specId: problem.specId,
9054
+ kind: problem.kind,
9055
+ path: problem.path,
9056
+ written: problem.written,
9057
+ enumId: kebabOf(positionNameOf(problem.path)),
9058
+ values,
9059
+ optional
9060
+ };
9061
+ }
8767
9062
  function emptyTypeSpellingFacts() {
8768
9063
  return { respellings: [], problems: [] };
8769
9064
  }
@@ -8914,6 +9209,7 @@ function lastSegment(name) {
8914
9209
  return name.split(/::|\./).filter(Boolean).pop() ?? name;
8915
9210
  }
8916
9211
  function sameType(code, spec, codeNames) {
9212
+ if (spec.form === "result" && code.form !== "result") return sameType(code, spec.args[0], codeNames);
8917
9213
  if (code.form === "primitive" && code.name === "number") return spec.form === "primitive" && (spec.name === "int" || spec.name === "float");
8918
9214
  if (code.form === "named" && (spec.form === "named" || spec.form === "applied")) {
8919
9215
  if (spec.form === "applied") return false;
@@ -8950,6 +9246,8 @@ function writeTypeScript(expr) {
8950
9246
  return expr.args.map(writeTypeScript).join(" | ");
8951
9247
  case "async":
8952
9248
  return `Promise<${writeTypeScript(expr.args[0])}>`;
9249
+ case "result":
9250
+ return writeTypeScript(expr.args[0]);
8953
9251
  case "applied":
8954
9252
  return `${expr.name}<${expr.args.map(writeTypeScript).join(", ")}>`;
8955
9253
  }
@@ -8992,6 +9290,7 @@ var typescriptDialect = {
8992
9290
  "T? \u2192 T | null",
8993
9291
  "A | B \u2192 A | B",
8994
9292
  "async T \u2192 Promise<T>",
9293
+ "result<T, E> \u2192 T, throwing E on failure (result<void, E> \u2192 void)",
8995
9294
  "an enum E \u2192 type E = 'a' | 'b' (a string-literal union alias)",
8996
9295
  "a named scalar E holding P \u2192 type E = P (an alias of the primitive)"
8997
9296
  ];
@@ -9266,7 +9565,7 @@ function jumpFieldsOf(s) {
9266
9565
  }
9267
9566
 
9268
9567
  // src/models/code-model.ts
9269
- var path9 = __toESM(require("path"));
9568
+ var path10 = __toESM(require("path"));
9270
9569
  function pathKey(sourcePath) {
9271
9570
  return sourcePath.replace(/\\/g, "/").replace(/^\.\//, "");
9272
9571
  }
@@ -9340,7 +9639,7 @@ var EXTENSION_LANGUAGE = {
9340
9639
  ".swift": "swift"
9341
9640
  };
9342
9641
  function languageOfSourcePath(sourcePath) {
9343
- return EXTENSION_LANGUAGE[path9.extname(sourcePath).toLowerCase()];
9642
+ return EXTENSION_LANGUAGE[path10.extname(sourcePath).toLowerCase()];
9344
9643
  }
9345
9644
  function dialectOf(facts) {
9346
9645
  return facts.language ? typeDialectFor(facts.language) : null;
@@ -9353,7 +9652,7 @@ function resolveImport(fromFile, specifier, knownPaths, packages) {
9353
9652
  const mapped = ownEntry(packages, specifier);
9354
9653
  return mapped !== void 0 && knownPaths.has(mapped) ? mapped : void 0;
9355
9654
  }
9356
- const joined = pathKey(path9.posix.normalize(path9.posix.join(path9.posix.dirname(fromFile), specifier)));
9655
+ const joined = pathKey(path10.posix.normalize(path10.posix.join(path10.posix.dirname(fromFile), specifier)));
9357
9656
  const candidates = [
9358
9657
  joined,
9359
9658
  joined.replace(/\.js$/, ".ts"),
@@ -9500,7 +9799,7 @@ function exportTargetKey(e) {
9500
9799
  }
9501
9800
 
9502
9801
  // src/models/project-family.ts
9503
- var path10 = __toESM(require("path"));
9802
+ var path11 = __toESM(require("path"));
9504
9803
  function walkMemberPath(byKey3, start, segments) {
9505
9804
  let node = start;
9506
9805
  for (const segment of segments.slice(0, -1)) {
@@ -9562,15 +9861,15 @@ function keyIn(project, localId) {
9562
9861
  return project ? `${project}::${localId}` : localId;
9563
9862
  }
9564
9863
  function approvalKeyIn(file, root, parts) {
9565
- const resolved = path10.resolve(file);
9864
+ const resolved = path11.resolve(file);
9566
9865
  for (const part of parts) {
9567
9866
  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("/")}`;
9867
+ const rel2 = path11.relative(part.directory, resolved);
9868
+ if (rel2 !== "" && !rel2.startsWith("..") && !path11.isAbsolute(rel2)) {
9869
+ return `members/${part.alias}/${rel2.split(path11.sep).join("/")}`;
9571
9870
  }
9572
9871
  }
9573
- return path10.relative(root, resolved).split(path10.sep).join("/");
9872
+ return path11.relative(root, resolved).split(path11.sep).join("/");
9574
9873
  }
9575
9874
  function familyNode(family, namespace) {
9576
9875
  return family.nodes.find((n) => n.namespace === namespace) ?? null;
@@ -9703,7 +10002,7 @@ var DesignDependencyRoleSchema = import_zod9.z.enum(DESIGN_DEPENDENCY_ROLES);
9703
10002
  var ExtSchema = import_zod9.z.record(import_zod9.z.unknown());
9704
10003
  var TypeExpressionSchema = import_zod9.z.lazy(
9705
10004
  () => import_zod9.z.object({
9706
- form: import_zod9.z.enum(["primitive", "named", "list", "set", "map", "optional", "union", "async", "applied"]),
10005
+ form: import_zod9.z.enum(["primitive", "named", "list", "set", "map", "optional", "union", "async", "applied", "result"]),
9707
10006
  name: import_zod9.z.string().optional(),
9708
10007
  args: import_zod9.z.array(TypeExpressionSchema)
9709
10008
  }).passthrough()
@@ -9940,7 +10239,7 @@ function resolveNarrativeLabels(methodName, steps) {
9940
10239
 
9941
10240
  // src/core/canvas.ts
9942
10241
  var fs7 = __toESM(require("fs"));
9943
- var path11 = __toESM(require("path"));
10242
+ var path12 = __toESM(require("path"));
9944
10243
 
9945
10244
  // src/core/canvas-layout.ts
9946
10245
  function computeLayout(model, collapsed) {
@@ -10711,9 +11010,9 @@ function embedJson(value) {
10711
11010
  }
10712
11011
  function loadCytoscapeLib() {
10713
11012
  const candidates = [
10714
- path11.resolve(__dirname, "..", "templates", "canvas", "cytoscape.min.js"),
11013
+ path12.resolve(__dirname, "..", "templates", "canvas", "cytoscape.min.js"),
10715
11014
  // src/core & dist/cli
10716
- path11.resolve(__dirname, "templates", "canvas", "cytoscape.min.js")
11015
+ path12.resolve(__dirname, "templates", "canvas", "cytoscape.min.js")
10717
11016
  // dist (library entry)
10718
11017
  ];
10719
11018
  for (const p of candidates) {
@@ -15096,17 +15395,17 @@ function safeFilenamePart(value) {
15096
15395
 
15097
15396
  // src/core/externals.ts
15098
15397
  var fs8 = __toESM(require("fs"));
15099
- var path12 = __toESM(require("path"));
15398
+ var path13 = __toESM(require("path"));
15100
15399
  function lockPath2(root) {
15101
- return path12.join(root, ".wai", "externals.lock.yaml");
15400
+ return path13.join(root, ".wai", "externals.lock.yaml");
15102
15401
  }
15103
15402
  function snapshotPath(root, alias) {
15104
- return path12.join(root, ".wai", "externals", `${alias}.yaml`);
15403
+ return path13.join(root, ".wai", "externals", `${alias}.yaml`);
15105
15404
  }
15106
15405
  function writeAtomically(file, text3) {
15107
- fs8.mkdirSync(path12.dirname(file), { recursive: true });
15406
+ fs8.mkdirSync(path13.dirname(file), { recursive: true });
15108
15407
  const tmp = `${file}.${process.pid}.tmp`;
15109
- fs8.writeFileSync(tmp, text3);
15408
+ fs8.writeFileSync(tmp, withLineEndings(file, text3));
15110
15409
  fs8.renameSync(tmp, file);
15111
15410
  }
15112
15411
  function sortedLock(lock) {
@@ -15149,7 +15448,7 @@ var externalsFileAdapter = {
15149
15448
  const file = snapshotPath(getProjectRoot(), alias);
15150
15449
  if (!fs8.existsSync(file)) return false;
15151
15450
  fs8.unlinkSync(file);
15152
- const dir = path12.dirname(file);
15451
+ const dir = path13.dirname(file);
15153
15452
  if (fs8.existsSync(dir) && fs8.readdirSync(dir).length === 0) fs8.rmdirSync(dir);
15154
15453
  return true;
15155
15454
  },
@@ -15282,6 +15581,8 @@ function schemaOfExpression(expr, closureIds) {
15282
15581
  return { oneOf: expr.args.map((member) => schemaOfExpression(member, closureIds)) };
15283
15582
  case "async":
15284
15583
  return schemaOfExpression(expr.args[0], closureIds);
15584
+ case "result":
15585
+ return schemaOfExpression(expr.args[0], closureIds);
15285
15586
  }
15286
15587
  }
15287
15588
  function schemaFor(typeRef2, closureIds) {
@@ -15291,7 +15592,8 @@ function schemaFor(typeRef2, closureIds) {
15291
15592
  }
15292
15593
  function returnsNothing(returns) {
15293
15594
  const expr = expressionOf(returns);
15294
- const inner2 = expr?.form === "async" ? expr.args[0] : expr;
15595
+ const awaited = expr?.form === "async" ? expr.args[0] : expr;
15596
+ const inner2 = awaited?.form === "result" ? awaited.args[0] : awaited;
15295
15597
  return inner2?.form === "primitive" && inner2.name === "void";
15296
15598
  }
15297
15599
  function enumDescription(values) {
@@ -15743,61 +16045,6 @@ function fromOpenApi(document, projectName) {
15743
16045
  });
15744
16046
  }
15745
16047
 
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
16048
  // src/core/design-export.ts
15802
16049
  var NoSystemSpecError = class extends Error {
15803
16050
  constructor() {
@@ -17292,7 +17539,7 @@ function approvalFinding(entry, config) {
17292
17539
  return finding(
17293
17540
  config,
17294
17541
  "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\`).`,
17542
+ `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
17543
  entry.key
17297
17544
  );
17298
17545
  }
@@ -18298,6 +18545,16 @@ function verifyIntegrity(selection, packDir, where) {
18298
18545
  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
18546
  };
18300
18547
  }
18548
+ function bundlesSingleFile(bundleDir, storePath) {
18549
+ try {
18550
+ if (!fs12.statSync(storePath).isFile()) return false;
18551
+ const entry = packDirEntry(bundleDir);
18552
+ if (!entry || fs12.readdirSync(bundleDir).length !== 1) return false;
18553
+ return fs12.readFileSync(entry).equals(fs12.readFileSync(storePath));
18554
+ } catch {
18555
+ return false;
18556
+ }
18557
+ }
18301
18558
  function detectBundleStoreDrift(selection, bundleDir) {
18302
18559
  const bundledVersion = path17.basename(bundleDir);
18303
18560
  const installed = resolveInstalledPack(selection.name, bundledVersion);
@@ -18305,6 +18562,7 @@ function detectBundleStoreDrift(selection, bundleDir) {
18305
18562
  const bundleDigest = computePackDigest(bundleDir);
18306
18563
  const storeDigest = computePackDigest(installed.path);
18307
18564
  if (bundleDigest === storeDigest) return void 0;
18565
+ if (bundlesSingleFile(bundleDir, installed.path)) return void 0;
18308
18566
  return {
18309
18567
  code: "PACK_STORE_DRIFT",
18310
18568
  severity: "warning",
@@ -18863,7 +19121,7 @@ var localIdsRule = {
18863
19121
  var projectIdentityRule = {
18864
19122
  name: "project-identity",
18865
19123
  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.",
19124
+ 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
19125
  codes: [
18868
19126
  { 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
19127
  { code: "PROJECT_ID_CHANGED", defaultSeverity: "error", summary: "Project id differs from the id the lock approved" },
@@ -18881,7 +19139,7 @@ function judgeOwnIdentity(ctx, identity2) {
18881
19139
  ctx.addIssue(
18882
19140
  "notice",
18883
19141
  "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).`
19142
+ `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
19143
  );
18886
19144
  }
18887
19145
  const ambiguous = problem("ambiguous");
@@ -19270,10 +19528,10 @@ function problemMessage(problem, specId) {
19270
19528
  var typeExpressionsRule = {
19271
19529
  name: "type-expressions",
19272
19530
  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).",
19531
+ 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
19532
  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??" },
19533
+ { code: "TYPE_EXPRESSION_INVALID", defaultSeverity: "warning", summary: "A structured type position does not parse under the type grammar" },
19534
+ { 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
19535
  { 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
19536
  { 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
19537
  { 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 +19541,8 @@ var typeExpressionsRule = {
19283
19541
  if (!facts) return;
19284
19542
  for (const problem of facts.problems) {
19285
19543
  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
19544
  ctx.addIssue(
19288
- severity,
19545
+ "warning",
19289
19546
  problem.code,
19290
19547
  problemMessage(problem, problem.specId),
19291
19548
  problem.specId,
@@ -19619,7 +19876,7 @@ var publicSurfaceBoundContractRule = {
19619
19876
  var exportTablesRule = {
19620
19877
  name: "export-tables",
19621
19878
  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.",
19879
+ 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
19880
  codes: [
19624
19881
  { code: "EXPORT_ID_DUPLICATE", defaultSeverity: "error", summary: "One public name bound to different targets by two wildcard re-exports or two explicit entries" },
19625
19882
  { 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 +20038,7 @@ function whyNoRewrite(ref) {
19781
20038
  var referenceFormsRule = {
19782
20039
  name: "reference-forms",
19783
20040
  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.",
20041
+ 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
20042
  codes: [
19786
20043
  { 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
20044
  ],
@@ -19812,9 +20069,9 @@ function carriedFields(mount) {
19812
20069
  var memberDeclarationsRule = {
19813
20070
  name: "member-declarations",
19814
20071
  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.",
20072
+ 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
20073
  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`." },
20074
+ { 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
20075
  { 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
20076
  { 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
20077
  ],
@@ -19852,7 +20109,7 @@ var memberDeclarationsRule = {
19852
20109
  ctx.addIssue(
19853
20110
  "notice",
19854
20111
  "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.`
20112
+ `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
20113
  );
19857
20114
  }
19858
20115
  for (const problem of family.problems) {
@@ -19889,7 +20146,7 @@ function declaration(producer) {
19889
20146
  var projectBoundariesRule = {
19890
20147
  name: "project-boundaries",
19891
20148
  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.",
20149
+ 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
20150
  codes: [
19894
20151
  { 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
20152
  { 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 +22692,7 @@ var reproducibilityRule = {
22435
22692
  ctx.addIssue(
22436
22693
  "warning",
22437
22694
  "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}\`).`
22695
+ `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
22696
  );
22440
22697
  }
22441
22698
  }
@@ -23422,10 +23679,22 @@ var invariantBackingRule = {
23422
23679
  };
23423
23680
 
23424
23681
  // src/core/rules/wiring/invariant-references.ts
23682
+ function unknownInvariantMessage(where, ref, types, aliases) {
23683
+ const parts = splitInvariantRef(ref);
23684
+ const qualifier = parts ? parts.typeRef.lastIndexOf("::") : -1;
23685
+ if (parts && qualifier > 0) {
23686
+ const bare = `${parts.typeRef.slice(qualifier + 2)}.${parts.invariantId}`;
23687
+ if (resolveInvariantRef(bare, types, aliases)) {
23688
+ const prefix = parts.typeRef.slice(0, qualifier);
23689
+ 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).`;
23690
+ }
23691
+ }
23692
+ return `${where} asserts invariant "${ref}", but no entity declares it (expected "<type-id>.<invariant-id>" naming a declared entry in that entity's invariants).`;
23693
+ }
23425
23694
  var invariantReferencesRule = {
23426
23695
  name: "invariant-references",
23427
23696
  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.",
23697
+ 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
23698
  codes: [
23430
23699
  { code: "UNKNOWN_INVARIANT_REF", defaultSeverity: "error", summary: "A narrative step asserts an invariant that no entity declares" }
23431
23700
  ],
@@ -23440,7 +23709,7 @@ var invariantReferencesRule = {
23440
23709
  ctx.addIssue(
23441
23710
  "error",
23442
23711
  "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).`,
23712
+ unknownInvariantMessage(`Step ${step.stepNumber} of "${method.name}" in implementation "${impl.id}"`, ref, ctx.types, aliases),
23444
23713
  impl.id,
23445
23714
  isDraftCtx
23446
23715
  );
@@ -29774,8 +30043,8 @@ function generateSequenceDiagram(componentId, methodName, options) {
29774
30043
  const steps = [...methodImpl.narrative].sort((a, b) => a.stepNumber - b.stepNumber);
29775
30044
  for (const step of steps) {
29776
30045
  for (const par of parallelArms) {
29777
- const sep14 = par.sepByStep.get(step.stepNumber);
29778
- if (sep14 !== void 0) lines.push(` and ${escapeLabel(sep14)}`);
30046
+ const sep15 = par.sepByStep.get(step.stepNumber);
30047
+ if (sep15 !== void 0) lines.push(` and ${escapeLabel(sep15)}`);
29779
30048
  }
29780
30049
  switch (step.type) {
29781
30050
  case "local":
@@ -30505,6 +30774,15 @@ function classifySource(world, sourceId, label7, sources) {
30505
30774
  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
30775
  };
30507
30776
  }
30777
+ function duplicateDetail(entries, index) {
30778
+ const e = entries[index];
30779
+ if (e.from !== void 0 || e.component !== void 0 || e.interface !== void 0 || e.typeDef !== void 0) return null;
30780
+ const name = e.as ?? e.id;
30781
+ if (name === void 0) return null;
30782
+ const other = entries.findIndex((x, i) => i !== index && (x.as ?? x.id) === name);
30783
+ if (other === -1) return null;
30784
+ 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`;
30785
+ }
30508
30786
  function bindOwnType(world, owner, e, problems) {
30509
30787
  const type = (world.types.get(e.typeDef) ?? []).find((t) => !t.subsystem);
30510
30788
  if (!type) {
@@ -30576,7 +30854,8 @@ function resolveProjectTable(system, subsystems, components, interfaces, types,
30576
30854
  const owner = sources.namespace ? sources.namespace : system.name;
30577
30855
  const problems = [];
30578
30856
  const candidates = [];
30579
- (system.publicInterfaces ?? []).forEach((e, index) => {
30857
+ const entries = system.publicInterfaces ?? [];
30858
+ entries.forEach((e, index) => {
30580
30859
  const label7 = e.as ?? e.id ?? e.interface ?? e.component ?? e.typeDef ?? `#${index + 1}`;
30581
30860
  const source = classifySource(world, sourceOf(world, e), label7, sources);
30582
30861
  switch (source.kind) {
@@ -30602,7 +30881,7 @@ function resolveProjectTable(system, subsystems, components, interfaces, types,
30602
30881
  return;
30603
30882
  }
30604
30883
  default:
30605
- problems.push({ kind: "invalid", owner, publicName: label7, detail: source.detail });
30884
+ problems.push({ kind: "invalid", owner, publicName: label7, detail: duplicateDetail(entries, index) ?? source.detail });
30606
30885
  }
30607
30886
  });
30608
30887
  return settle(owner, "project", candidates, problems);
@@ -31195,6 +31474,16 @@ function qualifiedTypeNames(typeStr) {
31195
31474
  }
31196
31475
  return out;
31197
31476
  }
31477
+ function invariantTypeRef(ref) {
31478
+ const at = ref.lastIndexOf(".");
31479
+ return at > 0 && at < ref.length - 1 ? ref.slice(0, at) : void 0;
31480
+ }
31481
+ function mapInvariantRef(ref, map) {
31482
+ const typeRef2 = invariantTypeRef(ref);
31483
+ if (typeRef2 === void 0 || !typeRef2.includes("::")) return ref;
31484
+ const next = map("type", typeRef2);
31485
+ return next === typeRef2 ? ref : `${next}${ref.slice(typeRef2.length)}`;
31486
+ }
31198
31487
  function rawReferences(kind, spec) {
31199
31488
  const out = [];
31200
31489
  const add2 = (position, value) => {
@@ -31217,6 +31506,7 @@ function rawReferences(kind, spec) {
31217
31506
  for (const step of m.narrative) {
31218
31507
  const source = step.auth?.from;
31219
31508
  if (typeof source === "string" && source.startsWith(COMPONENT_AUTH_SOURCE)) add2("auth", source.slice(COMPONENT_AUTH_SOURCE.length));
31509
+ for (const ref of step.assertsInvariants ?? []) add2("type", invariantTypeRef(ref));
31220
31510
  }
31221
31511
  }
31222
31512
  break;
@@ -31336,6 +31626,9 @@ function respellStoredReferences(kind, doc, map) {
31336
31626
  for (const m of list(doc.methods)) {
31337
31627
  for (const step of list(m.narrative)) {
31338
31628
  at(step, "targetComponent", "narrative");
31629
+ if (Array.isArray(step.assertsInvariants)) {
31630
+ step.assertsInvariants = step.assertsInvariants.map((ref) => typeof ref === "string" ? mapInvariantRef(ref, map) : ref);
31631
+ }
31339
31632
  const auth = step.auth;
31340
31633
  if (auth && typeof auth.from === "string" && auth.from.startsWith(COMPONENT_AUTH_SOURCE)) {
31341
31634
  auth.from = `${COMPONENT_AUTH_SOURCE}${map("auth", auth.from.slice(COMPONENT_AUTH_SOURCE.length))}`;
@@ -31395,7 +31688,8 @@ function mapRawReferences(kind, spec, map) {
31395
31688
  return call && next !== call.compId ? `${next}.${call.methodName}` : entry;
31396
31689
  })
31397
31690
  } : {},
31398
- narrative: m.narrative.map((step) => {
31691
+ narrative: m.narrative.map((authored) => {
31692
+ const step = authored.assertsInvariants ? { ...authored, assertsInvariants: authored.assertsInvariants.map((ref) => mapInvariantRef(ref, map)) } : authored;
31399
31693
  const source = step.auth?.from;
31400
31694
  if (typeof source !== "string" || !source.startsWith(COMPONENT_AUTH_SOURCE)) return step;
31401
31695
  const auth = step.auth;
@@ -33491,12 +33785,12 @@ var SpecWorkspace = class {
33491
33785
  return this.relativizeSpec("interface", stored);
33492
33786
  }
33493
33787
  prepareImplementationForWrite(spec, carry = true) {
33494
- const relative19 = this.relativizeSpec("implementation", spec, carry);
33788
+ const relative20 = this.relativizeSpec("implementation", spec, carry);
33495
33789
  const member = this.getSubprojectPrefix(spec.id);
33496
33790
  const memberRoot = member ? this.resolveSubprojectForNamespace(member) : null;
33497
- if (memberRoot) return childRelativeFilePaths(relative19, this.rootDir, memberRoot);
33791
+ if (memberRoot) return childRelativeFilePaths(relative20, this.rootDir, memberRoot);
33498
33792
  const partDir = this.partHolding(this.getImplementationPath(spec.id, spec.contract))?.part.directory;
33499
- return partDir ? partRelativeFilePaths(relative19, this.rootDir, partDir) : relative19;
33793
+ return partDir ? partRelativeFilePaths(relative20, this.rootDir, partDir) : relative20;
33500
33794
  }
33501
33795
  /**
33502
33796
  * A type written in its stored form — every type position canonical (one
@@ -36047,7 +36341,7 @@ function resolveGit(external, config) {
36047
36341
  const declared = gitDeclarationOf(config, external);
36048
36342
  if (!declared) return { ...external, problem: "git producer unavailable: its declaration names no repository" };
36049
36343
  try {
36050
- const commit2 = external.role === "member" && declared.commit ? declared.commit : resolve7(declared.url, declared.ref);
36344
+ const commit2 = external.role === "member" && declared.commit ? declared.commit : resolve3(declared.url, declared.ref);
36051
36345
  const directory = fetch(declared.url, commit2, declared.dir);
36052
36346
  return { ...external, directory: path23.resolve(directory), commit: commit2, problem: void 0 };
36053
36347
  } catch (e) {
@@ -36476,7 +36770,7 @@ function scaffoldPart(alias, memberDir, contained) {
36476
36770
  }
36477
36771
  function createGitMember(alias, source, description, as) {
36478
36772
  const url = source.git;
36479
- const commit2 = source.commit ?? resolve7(url, source.ref);
36773
+ const commit2 = source.commit ?? resolve3(url, source.ref);
36480
36774
  const dir = fetch(url, commit2, source.dir);
36481
36775
  const kind = holdsProjectContent(dir) ? "project" : "part";
36482
36776
  if (as !== void 0 && as !== kind) {
@@ -37436,7 +37730,7 @@ function advanceMember(alias, ref, commit2, dryRun) {
37436
37730
  const url = member.source.git;
37437
37731
  const from = member.source.commit ?? "";
37438
37732
  const followed = commit2 === void 0 ? ref ?? member.source.ref : void 0;
37439
- const to = commit2 ?? resolve7(url, followed);
37733
+ const to = commit2 ?? resolve3(url, followed);
37440
37734
  const after = specContents(fetch(url, to, member.source.dir));
37441
37735
  const before = specContents(from !== "" ? fetch(url, from, member.source.dir) : null);
37442
37736
  const added = [...after.keys()].filter((f) => !before.has(f)).sort();
@@ -38977,17 +39271,17 @@ function memberHop(agentId) {
38977
39271
  const alias = agentId.slice(0, at);
38978
39272
  const member = graph().nodes.find((n) => n.parent === "" && n.mountAlias === alias);
38979
39273
  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 };
39274
+ const relative20 = path29.relative(getProjectRoot(), member.directory).split(path29.sep).join("/");
39275
+ return { alias, rest: agentId.slice(at + 2), directory: member.directory, relative: relative20 };
38982
39276
  }
38983
- function throughMount(brief, alias, relative19) {
38984
- const under = (p) => path29.posix.join(relative19, p);
39277
+ function throughMount(brief, alias, relative20) {
39278
+ const under = (p) => path29.posix.join(relative20, p);
38985
39279
  return {
38986
39280
  ...brief,
38987
39281
  agentId: `${alias}::${brief.agentId}`,
38988
39282
  ownedPaths: brief.ownedPaths.map(under),
38989
39283
  ...brief.readPaths ? { readPaths: brief.readPaths.map(under) } : {},
38990
- root: brief.root ? under(brief.root) : relative19
39284
+ root: brief.root ? under(brief.root) : relative20
38991
39285
  };
38992
39286
  }
38993
39287
  function loadRegistry() {
@@ -39266,19 +39560,19 @@ function commitStagedTree(stagingDir, destDir, roots, backupExisting) {
39266
39560
  const backupDir = path34.join(root, BACKUP_DIR, timestampSlug());
39267
39561
  let backedUp = false;
39268
39562
  for (const rootRel of roots) {
39269
- const relative19 = rootRel === "." ? "" : rootRel;
39270
- const stagedSpecDir = path34.join(stagingDir, relative19, ".wai");
39563
+ const relative20 = rootRel === "." ? "" : rootRel;
39564
+ const stagedSpecDir = path34.join(stagingDir, relative20, ".wai");
39271
39565
  if (!fs20.existsSync(stagedSpecDir)) continue;
39272
- const liveSpecDir = liveSpecDirFor(path34.join(root, relative19));
39566
+ const liveSpecDir = liveSpecDirFor(path34.join(root, relative20));
39273
39567
  if (backupExisting && liveSpecDir && fs20.existsSync(liveSpecDir)) {
39274
- const backupTarget = path34.join(backupDir, relative19, path34.basename(liveSpecDir));
39568
+ const backupTarget = path34.join(backupDir, relative20, path34.basename(liveSpecDir));
39275
39569
  fs20.mkdirSync(path34.dirname(backupTarget), { recursive: true });
39276
39570
  fs20.renameSync(liveSpecDir, backupTarget);
39277
39571
  backedUp = true;
39278
39572
  } else if (liveSpecDir && fs20.existsSync(liveSpecDir)) {
39279
39573
  fs20.rmSync(liveSpecDir, { recursive: true, force: true });
39280
39574
  }
39281
- const target = path34.join(root, relative19, ".wai");
39575
+ const target = path34.join(root, relative20, ".wai");
39282
39576
  fs20.mkdirSync(path34.dirname(target), { recursive: true });
39283
39577
  if (fs20.existsSync(target)) fs20.rmSync(target, { recursive: true, force: true });
39284
39578
  fs20.renameSync(stagedSpecDir, target);
@@ -39557,7 +39851,7 @@ function repairTypeSpellings(apply3) {
39557
39851
  const key = `${kind}:${specId}`;
39558
39852
  let repair = repairs.get(key);
39559
39853
  if (!repair) {
39560
- repair = { specId, kind, rewritten: [], proposals: [], authorNeeded: [] };
39854
+ repair = { specId, kind, rewritten: [], proposals: [], enumProposals: [], authorNeeded: [] };
39561
39855
  repairs.set(key, repair);
39562
39856
  }
39563
39857
  return repair;
@@ -39572,7 +39866,9 @@ function repairTypeSpellings(apply3) {
39572
39866
  if (problem.specId === void 0 || problem.kind === void 0 || !own(problem.specId)) continue;
39573
39867
  const repair = repairOf(problem.specId, problem.kind);
39574
39868
  const proposal = typeProblemIntProposal(problem);
39869
+ const enumProposal = proposal ? null : typeProblemEnumProposal(problem);
39575
39870
  if (proposal) repair.proposals.push(proposal);
39871
+ else if (enumProposal) repair.enumProposals.push(enumProposal);
39576
39872
  else repair.authorNeeded.push(problem);
39577
39873
  }
39578
39874
  const planned = [...repairs.values()];
@@ -39719,7 +40015,7 @@ ${headline} \u2014 approved ${approval.lockedAt} by ${by}:
39719
40015
  return quiet;
39720
40016
  }
39721
40017
  }
39722
- var GATE_UPGRADE_NOTE = "the gate identity gained inputs in stage 5 (members' composition subjects, `composition`; code conformance moved beside the claim)";
40018
+ var GATE_UPGRADE_NOTE = "the gate identity gained inputs in this release (members' composition subjects, `composition`; code conformance moved beside the claim)";
39723
40019
  function memberNotes(approvals) {
39724
40020
  const own = approvals.find((a) => a.key === "");
39725
40021
  const direct = approvals.filter((a) => a.parent === "" && a.as !== "part");
@@ -39801,16 +40097,16 @@ var GLOBAL_GUIDE_BODY = `## wairon \u2014 Spec-Driven Development (optional)
39801
40097
  If \`.wai/specs/\` exists, the wairon SDD workflow is active; otherwise ignore it. wairon does not orchestrate sessions \u2014 it equips yours.
39802
40098
 
39803
40099
  ### 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\`).
40100
+ - **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
40101
  - **Validation**: Conformance checks (stereotype rules, cycle checks, reference integrity) are run via the \`sdd_validate_tree\` MCP tool.
39806
40102
  - **Operating Rules**:
39807
40103
  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
40104
  2. **MCP Tools Only**: Author/validate specs *only* via \`sdd_*\` tools (e.g. \`sdd_initialize_system\`, \`sdd_validate_tree\`).
39809
40105
  3. **No CLI Exec**: Do not run the \`wairon\` CLI (human tool). Use MCP tools \`sdd_validate_tree\` and \`sdd_get_status\` instead.
39810
40106
  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.
40107
+ 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
40108
  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.`;
40109
+ 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
40110
  var LOCAL_GUIDE_BODY = `## Wairon \u2014 Spec-Driven Development (you are operating inside it)
39815
40111
 
39816
40112
  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.
@@ -39827,7 +40123,8 @@ This project uses **wairon**. System specs live under \`.wai/specs/\` (L0 System
39827
40123
  - **\`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
40124
  - **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
40125
  - **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.
40126
+ - **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.
40127
+ - **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
40128
  - **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
40129
 
39833
40130
  ### Rules (enforced by \`sdd_validate_tree\`)
@@ -40296,7 +40593,7 @@ function getStatusReport(options = {}, decor) {
40296
40593
  }
40297
40594
  const own = options.approvals?.find((a) => a.key === "");
40298
40595
  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)" : ""}
40596
+ if (own && (own.state !== "never" || hasMembers)) output += `${mark.layer("system", "Approval:")} this project is ${own.state}${own.upgraded ? " (locked under an earlier gate identity \u2014 re-lock once)" : ""}
40300
40597
  `;
40301
40598
  return { text: output, failed: false };
40302
40599
  }
@@ -41478,7 +41775,7 @@ var KINDS_AT = {
41478
41775
  auth: ["implementation"],
41479
41776
  subsystem: ["component", "type"],
41480
41777
  group: ["type"],
41481
- type: ["interface", "type"]
41778
+ type: ["interface", "type", "implementation"]
41482
41779
  };
41483
41780
  function kindOf(ref) {
41484
41781
  const kinds = KINDS_AT[ref.position] ?? ["component", "interface", "implementation", "type", "subsystem"];
@@ -42558,7 +42855,7 @@ function emptyPlan(family, request) {
42558
42855
  const whole = !(reach2 !== null && !reach2.parentReach);
42559
42856
  return { request, familyRoot: familyNode(family, "").directory, whole, edits: [], refusals: [], changes: [], relock: [], notes: [] };
42560
42857
  }
42561
- var posix3 = (p) => p.split(path43.sep).join("/");
42858
+ var posix5 = (p) => p.split(path43.sep).join("/");
42562
42859
  var refuse = (plan6, code, project, detail) => {
42563
42860
  plan6.refusals.push({ code, project, detail });
42564
42861
  };
@@ -42636,7 +42933,7 @@ function attachEdits(plan6, family, bound2, dir, alias, bindingConfig) {
42636
42933
  edit(plan6, key, "id", `id: ${id} (declared${approved !== void 0 ? ", the id its lock approved" : ""})`, { root: dir, call: "setId", args: [id] });
42637
42934
  }
42638
42935
  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));
42936
+ const relPath = posix5(path43.relative(familyNode(family, bound2).directory, dir));
42640
42937
  const declaration2 = { path: relPath, ...plan6.request.description !== void 0 ? { description: plan6.request.description } : {} };
42641
42938
  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
42939
  for (const requirement of requiredPolicies(bindingConfig)) {
@@ -42683,8 +42980,8 @@ function planDetach(family, bound2, request) {
42683
42980
  if (plan6.request.widen) planWidening(plan6, member, narrow);
42684
42981
  if (plan6.refusals.length > 0) return plan6;
42685
42982
  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)) };
42983
+ const held = memberDeclarationOf(configAt3(boundDir)?.members?.[alias] ?? posix5(path43.relative(boundDir, member.directory)));
42984
+ const declared = { ...held, path: memberLocationOf(held) ?? posix5(path43.relative(boundDir, member.directory)) };
42688
42985
  detachEdits(plan6, family, bound2, member, alias, declared, consumers, outward);
42689
42986
  return plan6;
42690
42987
  }
@@ -42724,7 +43021,7 @@ function planWidening(plan6, member, narrow) {
42724
43021
  function detachEdits(plan6, family, bound2, member, alias, declared, consumers, outward) {
42725
43022
  const boundDir = familyNode(family, bound2).directory;
42726
43023
  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)) };
43024
+ const sourceTo = (from, producer) => relocation ? { hosted: producer.id ?? producer.mountAlias ?? producer.namespace } : { path: posix5(path43.relative(from, producer.directory)) };
42728
43025
  const where = (source) => source.hosted !== void 0 ? `hosted record ${source.hosted}` : source.path;
42729
43026
  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
43027
  const boundSource = relocation ? sourceTo(boundDir, member) : { path: declared.path };
@@ -42798,7 +43095,7 @@ function adoptEdits(plan6, family, bound2, alias, external, dir, at = dir) {
42798
43095
  edit(plan6, bound2, "pin", `pin ${alias} removed (the family supplies it now)`, { root: boundDir, call: "unpin", args: [alias] });
42799
43096
  edit(plan6, bound2, "external", `externals: ${alias} removed`, { root: boundDir, call: "removeExternal", args: [alias] });
42800
43097
  const declaration2 = {
42801
- path: posix3(path43.relative(boundDir, at)),
43098
+ path: posix5(path43.relative(boundDir, at)),
42802
43099
  ...external.description !== void 0 ? { description: external.description } : {},
42803
43100
  ...external.use && external.use.length > 0 ? { use: external.use } : {}
42804
43101
  };
@@ -42909,7 +43206,7 @@ var KINDS_AT2 = {
42909
43206
  auth: ["implementation"],
42910
43207
  subsystem: ["component", "type"],
42911
43208
  group: ["type"],
42912
- type: ["interface", "type"]
43209
+ type: ["interface", "type", "implementation"]
42913
43210
  };
42914
43211
  function kindOf2(ref) {
42915
43212
  const kinds = KINDS_AT2[ref.position] ?? ["component", "interface", "implementation", "type", "subsystem"];
@@ -43110,7 +43407,7 @@ function emptyPlan3(family, request) {
43110
43407
  const whole = !(reach2 !== null && !reach2.parentReach);
43111
43408
  return { request, familyRoot: familyNode(family, "").directory, whole, edits: [], refusals: [], changes: [], relock: [], notes: [] };
43112
43409
  }
43113
- var posix4 = (p) => p.split(path45.sep).join("/");
43410
+ var posix6 = (p) => p.split(path45.sep).join("/");
43114
43411
  var label6 = (key) => key === "" ? "the top project" : `"${key}"`;
43115
43412
  var refuse3 = (plan6, code, project, detail) => {
43116
43413
  plan6.refusals.push({ code, project, detail });
@@ -43141,7 +43438,7 @@ var KINDS_AT3 = {
43141
43438
  auth: ["implementation"],
43142
43439
  subsystem: ["component", "type"],
43143
43440
  group: ["type"],
43144
- type: ["interface", "type"]
43441
+ type: ["interface", "type", "implementation"]
43145
43442
  };
43146
43443
  function kindOf3(ref) {
43147
43444
  const kinds = KINDS_AT3[ref.position] ?? ["component", "interface", "implementation", "type", "subsystem"];
@@ -43320,7 +43617,7 @@ function respellConsumer(plan6, family, c, own) {
43320
43617
  function repointConsumer(plan6, c, boundNode2) {
43321
43618
  const declared = configAt5(c.node.directory)?.externals?.[c.alias];
43322
43619
  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;
43620
+ const source = declared?.source ? { path: posix6(path45.relative(c.node.directory, boundNode2.directory)) } : null;
43324
43621
  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
43622
  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
43623
  return c;
@@ -43356,8 +43653,8 @@ function planExternalize(family, bound2, request) {
43356
43653
  plan6,
43357
43654
  bound2,
43358
43655
  "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))] }
43656
+ `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`,
43657
+ { root: node.directory, call: "externalizeSubsystem", args: [id, posix6(path45.relative(node.directory, dir))] }
43361
43658
  );
43362
43659
  plan6.notes.push("Source code is not moved: move the subsystem's code yourself if it should live beside its specs.");
43363
43660
  return plan6;
@@ -43366,7 +43663,7 @@ function planExternalize(family, bound2, request) {
43366
43663
  crossingsBack(plan6, family, bound2, subKey);
43367
43664
  checkSiblings(plan6, family, bound2, id, subKey, table);
43368
43665
  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"] });
43666
+ 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
43667
  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: [[]] });
43371
43668
  plan6.notes.push("Source code is not moved: move the subsystem's code into the member yourself.");
43372
43669
  return plan6;
@@ -44495,7 +44792,7 @@ function errText(message) {
44495
44792
  function errMessage(e) {
44496
44793
  return errText(e instanceof Error ? e.message : String(e));
44497
44794
  }
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.`;
44795
+ 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
44796
  function structured(content, data) {
44500
44797
  return { content: [{ type: "text", text: content }], structuredContent: data };
44501
44798
  }
@@ -44595,6 +44892,28 @@ function renderChangeReport(report3) {
44595
44892
  return lines.join("\n");
44596
44893
  }
44597
44894
  var SPEC_KINDS = ["system", "subsystem", "component", "interface", "implementation", "type"];
44895
+ function specKindsHolding(id) {
44896
+ const system = loadSystemSpec();
44897
+ const held = {
44898
+ system: !!system && (id === "system" || id === system.name),
44899
+ subsystem: !!loadSubsystemSpec(id),
44900
+ component: !!loadComponentSpec(id),
44901
+ interface: !!loadInterfaceSpec(id),
44902
+ implementation: !!loadImplementationSpec(id),
44903
+ type: !!loadTypeSpec(id)
44904
+ };
44905
+ return SPEC_KINDS.filter((k) => held[k]);
44906
+ }
44907
+ var PORTAL_TYPE_TRANSPORT = {
44908
+ HTTP_API: "HTTP",
44909
+ gRPC: "gRPC",
44910
+ GraphQL: "GraphQL",
44911
+ MessageBus: "MessageBus",
44912
+ NamedPipe: "NamedPipe",
44913
+ IPC: "IPC",
44914
+ CLI: "CLI",
44915
+ Custom: void 0
44916
+ };
44598
44917
  var STATUS_VALUES = ["draft", "design", "complete"];
44599
44918
  var testsToRevisitOutput = import_zod12.z.array(import_zod12.z.object({
44600
44919
  method: import_zod12.z.string().describe("The contract method this write changed or deleted."),
@@ -44737,7 +45056,7 @@ var memberCreationOutput = {
44737
45056
  profile: import_zod12.z.string().optional()
44738
45057
  })).describe("Requirements no installed version satisfies: nothing was written for them, and the next family run reports each as POLICY_NOT_ADOPTED."),
44739
45058
  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)."),
45059
+ as: import_zod12.z.string().describe("part | project: what was created (a part by default)."),
44741
45060
  storage: import_zod12.z.string().describe("contained | path | git: where its files live."),
44742
45061
  commit: import_zod12.z.string().optional().describe("git: the commit the member was pinned at."),
44743
45062
  ...staleServerOutput
@@ -45319,7 +45638,7 @@ ${renderChangeReport(report3)}`,
45319
45638
  server,
45320
45639
  "sdd_add_member",
45321
45640
  {
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.",
45641
+ 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
45642
  inputSchema: {
45324
45643
  alias: import_zod12.z.string().describe("The member's alias ([a-z0-9-_]+); a project's new id"),
45325
45644
  source: import_zod12.z.string().describe("A contained path, a `../` sibling path, or a git URL (optionally `#<commit>`)"),
@@ -45389,7 +45708,7 @@ ${renderChangeReport(report3)}`,
45389
45708
  server,
45390
45709
  "sdd_externalize_subsystem",
45391
45710
  {
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.",
45711
+ 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
45712
  inputSchema: {
45394
45713
  subsystem: import_zod12.z.string().describe("The subsystem to move out"),
45395
45714
  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 +45722,7 @@ ${renderChangeReport(report3)}`,
45403
45722
  server,
45404
45723
  "sdd_promote_member",
45405
45724
  {
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.",
45725
+ 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
45726
  inputSchema: {
45408
45727
  alias: import_zod12.z.string().describe("The part's alias"),
45409
45728
  id: import_zod12.z.string().optional().describe("The new project's id (default: the alias)"),
@@ -45416,7 +45735,7 @@ ${renderChangeReport(report3)}`,
45416
45735
  server,
45417
45736
  "sdd_demote_member",
45418
45737
  {
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.",
45738
+ 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
45739
  inputSchema: {
45421
45740
  alias: import_zod12.z.string().describe("The project member's alias"),
45422
45741
  destination: import_zod12.z.object({
@@ -45759,7 +46078,7 @@ ${renderChangeReport(report3)}`,
45759
46078
  interface: import_zod12.z.string().describe('The L3 interface ID (e.g. "ibilling-gateway")'),
45760
46079
  endpoints: import_zod12.z.array(import_zod12.z.object({
45761
46080
  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"),
46081
+ 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
46082
  httpMethod: import_zod12.z.enum(["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]).optional().describe("HTTP: verb"),
45764
46083
  path: import_zod12.z.string().optional().describe('HTTP: route path, e.g. "/v1/checkout"'),
45765
46084
  service: import_zod12.z.string().optional().describe("gRPC: service name"),
@@ -45782,8 +46101,10 @@ ${renderChangeReport(report3)}`,
45782
46101
  try {
45783
46102
  const intf = loadInterfaceSpec(interfaceId);
45784
46103
  if (!intf) return errText(`Interface "${interfaceId}" does not exist.`);
45785
- const buildRaw = (e) => {
45786
- switch (e.transport) {
46104
+ const owner = intf.component ? loadComponentSpec(intf.component) : null;
46105
+ const implied = owner?.componentType === "Portal" && owner.portalType ? PORTAL_TYPE_TRANSPORT[owner.portalType] : void 0;
46106
+ const buildRaw = (e, transport) => {
46107
+ switch (transport) {
45787
46108
  case "HTTP":
45788
46109
  return { transport: "HTTP", method: e.httpMethod, path: e.path };
45789
46110
  case "gRPC":
@@ -45807,13 +46128,19 @@ ${renderChangeReport(report3)}`,
45807
46128
  for (const e of endpoints) {
45808
46129
  const m = intf.methods.find((x) => x.name === e.method);
45809
46130
  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));
46131
+ const transport = e.transport === "HTTP_API" ? "HTTP" : e.transport;
46132
+ if (implied && transport !== implied) {
46133
+ return errText(
46134
+ `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.`
46135
+ );
46136
+ }
46137
+ const parsed = EndpointSchema.safeParse(buildRaw(e, transport));
45811
46138
  if (!parsed.success) {
45812
46139
  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}`);
46140
+ return errText(`Invalid ${transport} endpoint for method "${e.method}": ${detail}`);
45814
46141
  }
45815
46142
  methods.push({ name: e.method, endpoint: parsed.data });
45816
- bound2.push(`${e.method}\u2192${e.transport}`);
46143
+ bound2.push(`${e.method}\u2192${transport}`);
45817
46144
  }
45818
46145
  const report3 = updateSpecGated("interface", interfaceId, { methods });
45819
46146
  return structured(
@@ -46074,14 +46401,24 @@ ${renderChangeReport(report3)}`,
46074
46401
  {
46075
46402
  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
46403
  inputSchema: {
46077
- kind: import_zod12.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).describe("The kind of specification"),
46404
+ 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
46405
  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
46406
  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
46407
  },
46081
46408
  outputSchema: getSpecOutput
46082
46409
  },
46083
- ({ kind, id, methods }) => {
46410
+ ({ kind: askedKind, id, methods }) => {
46084
46411
  try {
46412
+ let kind;
46413
+ if (askedKind) {
46414
+ kind = askedKind;
46415
+ } else {
46416
+ const holders = specKindsHolding(id);
46417
+ if (holders.length !== 1) {
46418
+ 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.`);
46419
+ }
46420
+ kind = holders[0];
46421
+ }
46085
46422
  let result = null;
46086
46423
  let partial2 = null;
46087
46424
  switch (kind) {
@@ -46634,6 +46971,7 @@ function loadProjectConfig3() {
46634
46971
  keyIn,
46635
46972
  landReference,
46636
46973
  languageOfSourcePath,
46974
+ lineEndingFor,
46637
46975
  listDirectChainedSubprojects,
46638
46976
  listExternals,
46639
46977
  listFiles,
@@ -46792,6 +47130,7 @@ function loadProjectConfig3() {
46792
47130
  typeIsMany,
46793
47131
  typeMatchesRef,
46794
47132
  typeNamedRefs,
47133
+ typeProblemEnumProposal,
46795
47134
  typeProblemIntProposal,
46796
47135
  typeSourceFiles,
46797
47136
  typeSpellingFacts,
@@ -46805,6 +47144,7 @@ function loadProjectConfig3() {
46805
47144
  validateProject,
46806
47145
  validateProjectConfig,
46807
47146
  validateRegistry,
47147
+ withLineEndings,
46808
47148
  withPack,
46809
47149
  writeFile,
46810
47150
  writeFileIfChanged,