@wairon/cli 5.1.1-dev.100 → 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/cli/index.js CHANGED
@@ -65,7 +65,7 @@ var init_defaults = __esm({
65
65
  copilot: ".github/prompts",
66
66
  codex: ".codex/agents"
67
67
  };
68
- WAIRON_VERSION = "5.1.1-dev.100";
68
+ WAIRON_VERSION = "5.1.1-dev.102";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  SUPPORTED_ALIASES = ["wai"];
71
71
  SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
@@ -214,6 +214,7 @@ __export(fs_exports, {
214
214
  getRequestProjectRoot: () => getRequestProjectRoot,
215
215
  getWriteReach: () => getWriteReach,
216
216
  isOutsideRoot: () => isOutsideRoot,
217
+ lineEndingFor: () => lineEndingFor,
217
218
  listFiles: () => listFiles,
218
219
  listFilesRecursive: () => listFilesRecursive,
219
220
  pathExists: () => pathExists,
@@ -222,6 +223,7 @@ __export(fs_exports, {
222
223
  runWithProjectBinding: () => runWithProjectBinding,
223
224
  runWithProjectRoot: () => runWithProjectRoot,
224
225
  setProjectRoot: () => setProjectRoot,
226
+ withLineEndings: () => withLineEndings,
225
227
  writeFile: () => writeFile,
226
228
  writeFileIfChanged: () => writeFileIfChanged,
227
229
  writeReachDenials: () => writeReachDenials,
@@ -232,17 +234,118 @@ function ensureDir(dirPath) {
232
234
  }
233
235
  function writeFile(filePath, content) {
234
236
  ensureDir(path2.dirname(filePath));
235
- fs.writeFileSync(filePath, content, "utf-8");
237
+ fs.writeFileSync(filePath, withLineEndings(filePath, content), "utf-8");
236
238
  }
237
239
  function writeFileIfChanged(filePath, content) {
240
+ const text3 = withLineEndings(filePath, content);
238
241
  if (fs.existsSync(filePath)) {
239
242
  const existing = fs.readFileSync(filePath, "utf-8");
240
- if (existing === content) return false;
243
+ if (existing === text3) return false;
241
244
  }
242
245
  ensureDir(path2.dirname(filePath));
243
- fs.writeFileSync(filePath, content, "utf-8");
246
+ fs.writeFileSync(filePath, text3, "utf-8");
244
247
  return true;
245
248
  }
249
+ function endingsOf(text3) {
250
+ const crlf = text3.split("\r\n").length - 1;
251
+ const lf = text3.split("\n").length - 1 - crlf;
252
+ if (crlf === 0 && lf === 0) return null;
253
+ return crlf > lf ? "\r\n" : "\n";
254
+ }
255
+ function sampleOf(file) {
256
+ let fd;
257
+ try {
258
+ fd = fs.openSync(file, "r");
259
+ const buffer = Buffer.alloc(EOL_SAMPLE_BYTES);
260
+ const read2 = fs.readSync(fd, buffer, 0, EOL_SAMPLE_BYTES, 0);
261
+ return buffer.subarray(0, read2).toString("utf-8");
262
+ } catch {
263
+ return null;
264
+ } finally {
265
+ if (fd !== void 0) fs.closeSync(fd);
266
+ }
267
+ }
268
+ function attributeMatches(pattern, relative28) {
269
+ const glob = pattern.replace(/^\//, "");
270
+ const target = glob.includes("/") ? relative28 : path2.posix.basename(relative28);
271
+ const source = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*\//g, "\0").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/\u0000/g, "(?:.*/)?");
272
+ return new RegExp(`^${source}$`).test(target);
273
+ }
274
+ function attributeEol(attributesFile, file) {
275
+ const text3 = readFileOrNull(attributesFile);
276
+ if (text3 === null) return null;
277
+ const relative28 = path2.relative(path2.dirname(attributesFile), file).split(path2.sep).join("/");
278
+ let eol = null;
279
+ for (const raw of text3.split(/\r?\n/)) {
280
+ const line2 = raw.trim();
281
+ if (line2 === "" || line2.startsWith("#")) continue;
282
+ const [pattern, ...attrs] = line2.split(/\s+/);
283
+ if (!attributeMatches(pattern, relative28)) continue;
284
+ for (const attr of attrs) {
285
+ if (attr === "eol=crlf") eol = "\r\n";
286
+ else if (attr === "eol=lf") eol = "\n";
287
+ else if (attr === "-text" || attr === "binary") eol = null;
288
+ }
289
+ }
290
+ return eol;
291
+ }
292
+ function conventionFolders(dir) {
293
+ const folders = [];
294
+ let at = path2.resolve(dir);
295
+ for (let depth = 0; depth < 8; depth++) {
296
+ folders.push(at);
297
+ if (fs.existsSync(path2.join(at, ".git")) || fs.existsSync(path2.join(at, ".wai"))) return folders;
298
+ const parent = path2.dirname(at);
299
+ if (parent === at) break;
300
+ at = parent;
301
+ }
302
+ return folders.slice(0, 1);
303
+ }
304
+ function folderEndings(folder, ext, exclude) {
305
+ let names;
306
+ try {
307
+ names = fs.readdirSync(folder);
308
+ } catch {
309
+ return null;
310
+ }
311
+ const candidates = names.filter((n) => path2.join(folder, n) !== exclude && /\.(ya?ml|json|md|ts|js|txt)$/i.test(n)).sort((a, b) => Number(path2.extname(b) === ext) - Number(path2.extname(a) === ext)).slice(0, 24);
312
+ let crlf = 0;
313
+ let lf = 0;
314
+ for (const name of candidates) {
315
+ const full = path2.join(folder, name);
316
+ try {
317
+ if (!fs.statSync(full).isFile()) continue;
318
+ } catch {
319
+ continue;
320
+ }
321
+ const sample = sampleOf(full);
322
+ const endings = sample === null ? null : endingsOf(sample);
323
+ if (endings === "\r\n") crlf++;
324
+ else if (endings === "\n") lf++;
325
+ }
326
+ if (crlf === lf) return null;
327
+ return crlf > lf ? "\r\n" : "\n";
328
+ }
329
+ function lineEndingFor(filePath) {
330
+ const own = sampleOf(filePath);
331
+ const existing = own === null ? null : endingsOf(own);
332
+ if (existing) return existing;
333
+ const folders = conventionFolders(path2.dirname(filePath));
334
+ for (const folder of folders) {
335
+ const eol = attributeEol(path2.join(folder, ".gitattributes"), filePath);
336
+ if (eol) return eol;
337
+ }
338
+ const ext = path2.extname(filePath);
339
+ for (const folder of folders) {
340
+ const endings = folderEndings(folder, ext, path2.resolve(filePath));
341
+ if (endings) return endings;
342
+ }
343
+ return "\n";
344
+ }
345
+ function withLineEndings(filePath, content) {
346
+ const normalised = content.replace(/\r\n/g, "\n");
347
+ return lineEndingFor(filePath) === "\r\n" ? normalised.replace(/\n/g, "\r\n") : normalised;
348
+ }
246
349
  function readFileOrNull(filePath) {
247
350
  try {
248
351
  return fs.readFileSync(filePath, "utf-8");
@@ -384,19 +487,154 @@ function aiDirAt(root, ...segments) {
384
487
  const base = !fs.existsSync(waiPath) && fs.existsSync(waironPath) ? waironPath : waiPath;
385
488
  return path2.join(base, ...segments);
386
489
  }
387
- var fs, path2, import_node_async_hooks, projectRootOverride, sameRoot, requestRootStore;
490
+ var fs, path2, import_node_async_hooks, EOL_SAMPLE_BYTES, projectRootOverride, sameRoot, requestRootStore;
388
491
  var init_fs = __esm({
389
492
  "src/utils/fs.ts"() {
390
493
  "use strict";
391
494
  fs = __toESM(require("fs"));
392
495
  path2 = __toESM(require("path"));
393
496
  import_node_async_hooks = require("async_hooks");
497
+ EOL_SAMPLE_BYTES = 64 * 1024;
394
498
  projectRootOverride = null;
395
499
  sameRoot = (a, b) => path2.resolve(a) === path2.resolve(b);
396
500
  requestRootStore = new import_node_async_hooks.AsyncLocalStorage();
397
501
  }
398
502
  });
399
503
 
504
+ // src/core/adapters/git-source.ts
505
+ function osUserCache() {
506
+ if (process.platform === "win32") return process.env.LOCALAPPDATA ?? path3.join(os2.homedir(), "AppData", "Local");
507
+ if (process.platform === "darwin") return path3.join(os2.homedir(), "Library", "Caches");
508
+ return process.env.XDG_CACHE_HOME ?? path3.join(os2.homedir(), ".cache");
509
+ }
510
+ function cacheRoot() {
511
+ const setting = process.env.WAIRON_CACHE_DIR ?? "";
512
+ let root = cacheRoots.get(setting);
513
+ if (root === void 0) {
514
+ root = setting !== "" ? path3.resolve(setting) : path3.join(osUserCache(), "wairon");
515
+ cacheRoots.set(setting, root);
516
+ }
517
+ return root;
518
+ }
519
+ function git(args, cwd) {
520
+ return (0, import_child_process.execFileSync)("git", args, {
521
+ cwd,
522
+ encoding: "utf8",
523
+ stdio: ["ignore", "pipe", "pipe"],
524
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "echo" }
525
+ });
526
+ }
527
+ function reasonOf(e) {
528
+ const stderr = e?.stderr;
529
+ const text3 = typeof stderr === "string" && stderr.trim() !== "" ? stderr : e instanceof Error ? e.message : String(e);
530
+ return text3.trim().split(/\r?\n/).filter((l) => l.trim() !== "").slice(-1)[0] ?? "git failed";
531
+ }
532
+ function cacheEntryOf(url, commit3) {
533
+ const repo = crypto.createHash("sha256").update(url).digest("hex");
534
+ return path3.join(cacheRoot(), "git", repo, commit3);
535
+ }
536
+ function within(entry, url, commit3, dir) {
537
+ const at = dir ? path3.join(entry, dir) : entry;
538
+ if (!fs2.existsSync(at)) {
539
+ throw new SourceUnavailable(`the commit ${commit3} of ${url} holds no "${dir}"`);
540
+ }
541
+ return at;
542
+ }
543
+ function fetchInto(tmp, url, commit3) {
544
+ git(["init", "-q"], tmp);
545
+ try {
546
+ git(["fetch", "-q", "--depth", "1", url, commit3], tmp);
547
+ } catch {
548
+ git(["fetch", "-q", url, "+refs/heads/*:refs/remotes/source/*", "+refs/tags/*:refs/tags/*"], tmp);
549
+ }
550
+ git(["-c", "advice.detachedHead=false", "checkout", "-q", commit3], tmp);
551
+ fs2.rmSync(path3.join(tmp, ".git"), { recursive: true, force: true });
552
+ }
553
+ function fetch2(url, commit3, dir) {
554
+ const entry = cacheEntryOf(url, commit3);
555
+ if (fs2.existsSync(entry)) return within(entry, url, commit3, dir);
556
+ fs2.mkdirSync(path3.dirname(entry), { recursive: true });
557
+ const tmp = `${entry}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
558
+ fs2.mkdirSync(tmp, { recursive: true });
559
+ try {
560
+ fetchInto(tmp, url, commit3);
561
+ try {
562
+ fs2.renameSync(tmp, entry);
563
+ } catch (e) {
564
+ if (!fs2.existsSync(entry)) throw e;
565
+ fs2.rmSync(tmp, { recursive: true, force: true });
566
+ }
567
+ } catch (e) {
568
+ fs2.rmSync(tmp, { recursive: true, force: true });
569
+ if (fs2.readdirSync(path3.dirname(entry)).length === 0) fs2.rmdirSync(path3.dirname(entry));
570
+ throw e instanceof SourceUnavailable ? e : new SourceUnavailable(`cannot fetch ${commit3} of ${url}: ${reasonOf(e)}`);
571
+ }
572
+ return within(entry, url, commit3, dir);
573
+ }
574
+ function resolve3(url, ref) {
575
+ let listed;
576
+ try {
577
+ listed = git(["ls-remote", url, ref ?? "HEAD"]);
578
+ } catch (e) {
579
+ throw new SourceUnavailable(`cannot reach ${url}: ${reasonOf(e)}`);
580
+ }
581
+ const wanted = commitListed(listed, ref ?? "HEAD");
582
+ if (!wanted) throw new SourceUnavailable(`${url} lists no ref "${ref ?? "HEAD"}"`);
583
+ return wanted;
584
+ }
585
+ function commitListed(listed, ref) {
586
+ const lines = listed.split(/\r?\n/).filter((l) => l.trim() !== "").map((l) => l.split(/\s+/));
587
+ const names = [ref, `refs/heads/${ref}`, `refs/tags/${ref}^{}`, `refs/tags/${ref}`];
588
+ for (const name of names) {
589
+ const line2 = lines.find(([, listedName]) => listedName === name);
590
+ if (line2) return line2[0];
591
+ }
592
+ return void 0;
593
+ }
594
+ function trackedFiles(directory, pathspecs) {
595
+ if (pathspecs.length === 0) return [];
596
+ try {
597
+ return git(["ls-files", "-z", "--", ...pathspecs], directory).split("\0").filter((p) => p !== "");
598
+ } catch {
599
+ return null;
600
+ }
601
+ }
602
+ function head(directory) {
603
+ try {
604
+ return git(["rev-parse", "HEAD"], directory).trim() || null;
605
+ } catch {
606
+ return null;
607
+ }
608
+ }
609
+ function repositoryRoot(directory) {
610
+ let at = path3.resolve(directory);
611
+ for (; ; ) {
612
+ if (fs2.existsSync(path3.join(at, ".git"))) return at;
613
+ const up = path3.dirname(at);
614
+ if (up === at) return null;
615
+ at = up;
616
+ }
617
+ }
618
+ var import_child_process, crypto, fs2, os2, path3, SourceUnavailable, cacheRoots;
619
+ var init_git_source = __esm({
620
+ "src/core/adapters/git-source.ts"() {
621
+ "use strict";
622
+ import_child_process = require("child_process");
623
+ crypto = __toESM(require("crypto"));
624
+ fs2 = __toESM(require("fs"));
625
+ os2 = __toESM(require("os"));
626
+ path3 = __toESM(require("path"));
627
+ init_errors();
628
+ SourceUnavailable = class extends WaironError {
629
+ constructor(message) {
630
+ super(message);
631
+ this.name = "SourceUnavailable";
632
+ }
633
+ };
634
+ cacheRoots = /* @__PURE__ */ new Map();
635
+ }
636
+ });
637
+
400
638
  // src/utils/yaml.ts
401
639
  function parseYaml(content, sourcePath) {
402
640
  try {
@@ -720,7 +958,7 @@ function activeTargetTypes(config) {
720
958
  function declaredPackNames(config) {
721
959
  const selection = config.profileSelection;
722
960
  return [.../* @__PURE__ */ new Set([
723
- ...(config.extensions?.packs ?? []).map((entry) => typeof entry === "string" ? path3.basename(entry).replace(PACK_EXT_RE, "") : entry.name),
961
+ ...(config.extensions?.packs ?? []).map((entry) => typeof entry === "string" ? path4.basename(entry).replace(PACK_EXT_RE, "") : entry.name),
724
962
  ...selection?.requiredPackNames ?? [],
725
963
  ...selection?.defaultPackNames ?? []
726
964
  ])];
@@ -999,11 +1237,11 @@ function declaredMembers(config) {
999
1237
  function isPart(config) {
1000
1238
  return config?.partOf !== void 0;
1001
1239
  }
1002
- var path3, import_zod3, BuiltinTargetConfigSchema, CustomTargetConfigSchema, TargetConfigSchema, NamingRuleConfigSchema, DocumentationRuleConfigSchema, ComplexityRuleConfigSchema, CarriedDebtKindSchema, CarriedFindingSchema, CarriedDebtSchema, ConformanceRuleConfigSchema, DesignDepthSchema, RulesConfigSchema, PathsConfigSchema, PackSelectionSchema, ProfileSelectionSubjectSchema, ProjectProfileSelectionSchema, ExternalSourceSchema, ExternalDeclarationSchema, MemberDeclarationSchema, PartOfSchema, plainString, PackRequirementSchema, CompositionConfigSchema, ProjectConfigSchema, PACK_EXT_RE, CONCRETE_RE, PARTIAL_RE, OPERATOR_RE, text, cv, PROJECT_ID_RE, EXTERNAL_ALIAS_RE, USE_ENTRY_RE, FULL_COMMIT_RE, GIT_URL_RE;
1240
+ var path4, import_zod3, BuiltinTargetConfigSchema, CustomTargetConfigSchema, TargetConfigSchema, NamingRuleConfigSchema, DocumentationRuleConfigSchema, ComplexityRuleConfigSchema, CarriedDebtKindSchema, CarriedFindingSchema, CarriedDebtSchema, ConformanceRuleConfigSchema, DesignDepthSchema, RulesConfigSchema, PathsConfigSchema, PackSelectionSchema, ProfileSelectionSubjectSchema, ProjectProfileSelectionSchema, ExternalSourceSchema, ExternalDeclarationSchema, MemberDeclarationSchema, PartOfSchema, plainString, PackRequirementSchema, CompositionConfigSchema, ProjectConfigSchema, PACK_EXT_RE, CONCRETE_RE, PARTIAL_RE, OPERATOR_RE, text, cv, PROJECT_ID_RE, EXTERNAL_ALIAS_RE, USE_ENTRY_RE, FULL_COMMIT_RE, GIT_URL_RE;
1003
1241
  var init_project = __esm({
1004
1242
  "src/models/project.ts"() {
1005
1243
  "use strict";
1006
- path3 = __toESM(require("path"));
1244
+ path4 = __toESM(require("path"));
1007
1245
  import_zod3 = require("zod");
1008
1246
  init_agent();
1009
1247
  init_execution();
@@ -1335,7 +1573,7 @@ var init_project = __esm({
1335
1573
  /**
1336
1574
  * The producer's public names this project imports, so its specs may name
1337
1575
  * them bare: `['*']` imports every public name the producer exports to this
1338
- * project, `[waffler-error]` only those. Plain strings: a malformed entry is
1576
+ * project, `[shared-error]` only those. Plain strings: a malformed entry is
1339
1577
  * REPORTED (the declaration's problem), never unreadable.
1340
1578
  */
1341
1579
  use: import_zod3.z.array(import_zod3.z.string()).optional(),
@@ -1546,13 +1784,13 @@ var init_project = __esm({
1546
1784
 
1547
1785
  // src/config/project-config.ts
1548
1786
  function waiDirAt(root) {
1549
- const wai = path4.join(root, ".wai");
1550
- const legacy = path4.join(root, ".wairon");
1551
- return !fs2.existsSync(wai) && fs2.existsSync(legacy) ? legacy : wai;
1787
+ const wai = path5.join(root, ".wai");
1788
+ const legacy = path5.join(root, ".wairon");
1789
+ return !fs3.existsSync(wai) && fs3.existsSync(legacy) ? legacy : wai;
1552
1790
  }
1553
1791
  function projectConfigFsAdapterAt(rootDir) {
1554
- const root = path4.resolve(rootDir);
1555
- const file = () => path4.join(waiDirAt(root), "project.yaml");
1792
+ const root = path5.resolve(rootDir);
1793
+ const file = () => path5.join(waiDirAt(root), "project.yaml");
1556
1794
  return {
1557
1795
  readDocument() {
1558
1796
  return readYamlFile(file());
@@ -1566,7 +1804,7 @@ function projectConfigFsAdapterAt(rootDir) {
1566
1804
  writeFile(file(), editDocumentText(text3, document, root));
1567
1805
  },
1568
1806
  documentExists() {
1569
- return fs2.existsSync(file());
1807
+ return fs3.existsSync(file());
1570
1808
  },
1571
1809
  readText() {
1572
1810
  return readFileOrNull(file());
@@ -1588,8 +1826,9 @@ function storeOver(adapter, root) {
1588
1826
  if (!checked.success) {
1589
1827
  throw new WaironError(`Refusing to write an invalid .wai/project.yaml at ${root}: ${checked.error.message}`);
1590
1828
  }
1591
- const document = config.partOf !== void 0 ? { schemaVersion: config.schemaVersion, partOf: config.partOf } : config;
1592
- adapter.writeDocument(overlayKnownFields(ProjectConfigSchema, document, currentDocument()));
1829
+ const onDisk = currentDocument();
1830
+ const document = config.partOf !== void 0 ? { schemaVersion: config.schemaVersion, partOf: config.partOf } : withoutDefaultedMaterialization(config, onDisk);
1831
+ adapter.writeDocument(overlayKnownFields(ProjectConfigSchema, document, onDisk));
1593
1832
  },
1594
1833
  exists() {
1595
1834
  return adapter.documentExists();
@@ -1846,9 +2085,67 @@ function asReadable(document) {
1846
2085
  if (!isPlainObject(document) || document.partOf === void 0) return document;
1847
2086
  return { name: "", createdAt: PART_EPOCH, updatedAt: PART_EPOCH, ...document };
1848
2087
  }
2088
+ function holdsMaterializedAgentFiles(document, root) {
2089
+ const dirs = new Set(Object.values(DEFAULT_TARGET_DIRS));
2090
+ for (const target of Array.isArray(document.targets) ? document.targets : []) {
2091
+ const outputDir = isPlainObject(target) ? target.outputDir : void 0;
2092
+ if (typeof outputDir === "string") dirs.add(outputDir);
2093
+ }
2094
+ const base = path5.resolve(root);
2095
+ const inside = [];
2096
+ for (const dir of dirs) {
2097
+ const rel2 = path5.relative(base, path5.resolve(base, dir));
2098
+ if (rel2.startsWith("..") || path5.isAbsolute(rel2)) continue;
2099
+ let present = false;
2100
+ try {
2101
+ present = fs3.statSync(path5.join(base, rel2)).isDirectory();
2102
+ } catch {
2103
+ present = false;
2104
+ }
2105
+ if (present) inside.push(rel2 === "" ? "." : rel2.split(path5.sep).join("/"));
2106
+ }
2107
+ if (inside.length === 0) return false;
2108
+ const tracked = trackedFiles(base, inside);
2109
+ const candidates = [];
2110
+ if (tracked === null) {
2111
+ for (const dir of inside) {
2112
+ let names;
2113
+ try {
2114
+ names = fs3.readdirSync(path5.join(base, dir));
2115
+ } catch {
2116
+ continue;
2117
+ }
2118
+ for (const name of names) candidates.push(path5.posix.join(dir, name));
2119
+ }
2120
+ } else {
2121
+ const folders = new Set(inside.map((dir) => path5.posix.normalize(dir)));
2122
+ for (const file of tracked) if (folders.has(path5.posix.dirname(file))) candidates.push(file);
2123
+ }
2124
+ for (const file of candidates) {
2125
+ const head2 = readFileOrNull(path5.join(base, file));
2126
+ if (head2 === null) continue;
2127
+ if (MANAGED_AGENT_NAME.test(path5.posix.basename(file))) return true;
2128
+ if (head2.slice(0, 4096).includes(MANAGED_AGENT_MARKER)) return true;
2129
+ }
2130
+ return false;
2131
+ }
2132
+ function withInferredMaterialization(document, root) {
2133
+ if (!isPlainObject(document) || document.partOf !== void 0) return document;
2134
+ const rules = isPlainObject(document.rules) ? document.rules : void 0;
2135
+ if (rules !== void 0 && hasOwn(rules, "materializeAgentFiles")) return document;
2136
+ if (!holdsMaterializedAgentFiles(document, root)) return document;
2137
+ return { ...document, rules: { ...rules ?? {}, materializeAgentFiles: true } };
2138
+ }
2139
+ function withoutDefaultedMaterialization(config, onDisk) {
2140
+ if (!isPlainObject(onDisk) || config.rules?.materializeAgentFiles !== false) return config;
2141
+ const stated = isPlainObject(onDisk.rules) && hasOwn(onDisk.rules, "materializeAgentFiles");
2142
+ if (stated) return config;
2143
+ const { materializeAgentFiles: _defaulted, ...rules } = config.rules;
2144
+ return { ...config, rules };
2145
+ }
1849
2146
  function parseConfig(document, root) {
1850
2147
  try {
1851
- return ProjectConfigSchema.parse(asReadable(document));
2148
+ return ProjectConfigSchema.parse(asReadable(withInferredMaterialization(document, root)));
1852
2149
  } catch (e) {
1853
2150
  throw new WaironError(
1854
2151
  `Invalid .wai/project.yaml: ${e instanceof Error ? e.message : String(e)}
@@ -2287,15 +2584,15 @@ function indexOver(store, root) {
2287
2584
  specsDir() {
2288
2585
  try {
2289
2586
  const declared = store.readField("paths.specsDir");
2290
- if (declared) return path4.resolve(root, declared);
2587
+ if (declared) return path5.resolve(root, declared);
2291
2588
  } catch {
2292
2589
  }
2293
- return path4.join(waiDirAt(root), "specs");
2590
+ return path5.join(waiDirAt(root), "specs");
2294
2591
  }
2295
2592
  };
2296
2593
  }
2297
2594
  function projectConfigRepositoryOver(adapter, rootDir) {
2298
- const root = path4.resolve(rootDir);
2595
+ const root = path5.resolve(rootDir);
2299
2596
  const store = storeOver(adapter, root);
2300
2597
  const index = indexOver(store, root);
2301
2598
  const registry = registryOver(store, root);
@@ -2389,15 +2686,17 @@ function projectConfigRepositoryOver(adapter, rootDir) {
2389
2686
  function projectConfigRepositoryAt(rootDir) {
2390
2687
  return projectConfigRepositoryOver(projectConfigFsAdapterAt(rootDir), rootDir);
2391
2688
  }
2392
- var fs2, path4, YAML, import_zod4, KEY_RE, FRESH_TEXT_OPTIONS, PARSE_OPTIONS, DocumentReconciler, PART_EPOCH, USE_GLOBAL_PACKS_DEFAULT, PROJECT_FIELDS, bound, projectConfigRepository;
2689
+ var fs3, path5, YAML, import_zod4, KEY_RE, FRESH_TEXT_OPTIONS, PARSE_OPTIONS, DocumentReconciler, PART_EPOCH, MANAGED_AGENT_MARKER, MANAGED_AGENT_NAME, USE_GLOBAL_PACKS_DEFAULT, PROJECT_FIELDS, bound, projectConfigRepository;
2393
2690
  var init_project_config = __esm({
2394
2691
  "src/config/project-config.ts"() {
2395
2692
  "use strict";
2396
- fs2 = __toESM(require("fs"));
2397
- path4 = __toESM(require("path"));
2693
+ fs3 = __toESM(require("fs"));
2694
+ path5 = __toESM(require("path"));
2398
2695
  YAML = __toESM(require("yaml"));
2399
2696
  import_zod4 = require("zod");
2400
2697
  init_fs();
2698
+ init_defaults();
2699
+ init_git_source();
2401
2700
  init_yaml();
2402
2701
  init_fs();
2403
2702
  init_errors();
@@ -2546,6 +2845,8 @@ var init_project_config = __esm({
2546
2845
  }
2547
2846
  };
2548
2847
  PART_EPOCH = "1970-01-01T00:00:00.000Z";
2848
+ MANAGED_AGENT_MARKER = "wairon:managed";
2849
+ MANAGED_AGENT_NAME = /-(owner|implementer|architect)\.(md|ya?ml)$/;
2549
2850
  USE_GLOBAL_PACKS_DEFAULT = ProjectConfigSchema.shape.extensions.unwrap().parse({}).useGlobalPacks;
2550
2851
  PROJECT_FIELDS = ["id", "members", "externals", "composition", "rules", "extensions", "targets"];
2551
2852
  bound = () => {
@@ -2643,12 +2944,12 @@ var init_project_config = __esm({
2643
2944
 
2644
2945
  // src/config/paths.ts
2645
2946
  function aiPathsAt(rootDir) {
2646
- const resolvedRoot = path5.resolve(rootDir);
2947
+ const resolvedRoot = path6.resolve(rootDir);
2647
2948
  const aiDirAt2 = (...segments) => {
2648
- const waiPath = path5.join(resolvedRoot, ".wai");
2649
- const waironPath = path5.join(resolvedRoot, ".wairon");
2650
- const base = !fs3.existsSync(waiPath) && fs3.existsSync(waironPath) ? waironPath : waiPath;
2651
- return path5.join(base, ...segments);
2949
+ const waiPath = path6.join(resolvedRoot, ".wai");
2950
+ const waironPath = path6.join(resolvedRoot, ".wairon");
2951
+ const base = !fs4.existsSync(waiPath) && fs4.existsSync(waironPath) ? waironPath : waiPath;
2952
+ return path6.join(base, ...segments);
2652
2953
  };
2653
2954
  const specsDir = () => projectConfigRepositoryAt(resolvedRoot).specsDir();
2654
2955
  return {
@@ -2665,12 +2966,12 @@ function aiPathsAt(rootDir) {
2665
2966
  contextDomainsMd: () => aiDirAt2("context", "domains.md"),
2666
2967
  contextWaironGuideMd: () => aiDirAt2("context", "wairon-guide.md"),
2667
2968
  specsDir,
2668
- specsSystem: () => path5.join(specsDir(), ".index.yaml"),
2669
- specsSubsystemsDir: () => path5.join(specsDir(), "subsystems"),
2670
- specsComponentsDir: () => path5.join(specsDir(), "components"),
2671
- specsInterfacesDir: () => path5.join(specsDir(), "interfaces"),
2672
- specsImplementationsDir: () => path5.join(specsDir(), "implementations"),
2673
- specsTypesDir: () => path5.join(specsDir(), "types")
2969
+ specsSystem: () => path6.join(specsDir(), ".index.yaml"),
2970
+ specsSubsystemsDir: () => path6.join(specsDir(), "subsystems"),
2971
+ specsComponentsDir: () => path6.join(specsDir(), "components"),
2972
+ specsInterfacesDir: () => path6.join(specsDir(), "interfaces"),
2973
+ specsImplementationsDir: () => path6.join(specsDir(), "implementations"),
2974
+ specsTypesDir: () => path6.join(specsDir(), "types")
2674
2975
  };
2675
2976
  }
2676
2977
  function isProjectInitialized() {
@@ -2681,12 +2982,12 @@ function assertProjectInitialized() {
2681
2982
  throw new ProjectNotInitializedError();
2682
2983
  }
2683
2984
  }
2684
- var fs3, path5, AI_PATHS;
2985
+ var fs4, path6, AI_PATHS;
2685
2986
  var init_paths = __esm({
2686
2987
  "src/config/paths.ts"() {
2687
2988
  "use strict";
2688
- fs3 = __toESM(require("fs"));
2689
- path5 = __toESM(require("path"));
2989
+ fs4 = __toESM(require("fs"));
2990
+ path6 = __toESM(require("path"));
2690
2991
  init_fs();
2691
2992
  init_errors();
2692
2993
  init_project_config();
@@ -2755,7 +3056,7 @@ function digestTree(keep, asStored = (impl) => impl) {
2755
3056
  implementations: kind(loadImplementationSpecs().map(asStored)),
2756
3057
  types: kind(loadTypeSpecs())
2757
3058
  };
2758
- const digest3 = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
3059
+ const digest3 = crypto2.createHash("sha256").update(canonicalize(tree)).digest("hex");
2759
3060
  return { algorithm: CONTENT_ALGORITHM, digest: digest3 };
2760
3061
  }
2761
3062
  function computeStateId() {
@@ -2775,11 +3076,11 @@ function computeOwnStateId() {
2775
3076
  return digestTree((id) => (owners.get(id) ?? "") === "", asStored);
2776
3077
  }
2777
3078
  function isInside(dir, file) {
2778
- const rel2 = path6.relative(dir, file);
2779
- return rel2 !== "" && !rel2.startsWith("..") && !path6.isAbsolute(rel2);
3079
+ const rel2 = path7.relative(dir, file);
3080
+ return rel2 !== "" && !rel2.startsWith("..") && !path7.isAbsolute(rel2);
2780
3081
  }
2781
3082
  function fromPartRoot(impl, root, partDir) {
2782
- const from = (p) => path6.isAbsolute(p) ? p : path6.relative(partDir, path6.resolve(root, p)).split(path6.sep).join("/");
3083
+ const from = (p) => path7.isAbsolute(p) ? p : path7.relative(partDir, path7.resolve(root, p)).split(path7.sep).join("/");
2783
3084
  return {
2784
3085
  ...impl,
2785
3086
  ...impl.sourcePath ? { sourcePath: from(impl.sourcePath) } : {},
@@ -2790,13 +3091,13 @@ function fromPartRoot(impl, root, partDir) {
2790
3091
  function stateIdEquals(a, b) {
2791
3092
  return !!a && !!b && a.algorithm === b.algorithm && a.digest === b.digest;
2792
3093
  }
2793
- var crypto, path6, CONTENT_ALGORITHM;
3094
+ var crypto2, path7, CONTENT_ALGORITHM;
2794
3095
  var init_statehash = __esm({
2795
3096
  "src/core/statehash.ts"() {
2796
3097
  "use strict";
2797
- crypto = __toESM(require("crypto"));
3098
+ crypto2 = __toESM(require("crypto"));
2798
3099
  init_specs2();
2799
- path6 = __toESM(require("path"));
3100
+ path7 = __toESM(require("path"));
2800
3101
  init_fs();
2801
3102
  init_canonical_json();
2802
3103
  CONTENT_ALGORITHM = "sha256";
@@ -2817,14 +3118,14 @@ function normalizeApprover(value) {
2817
3118
  }
2818
3119
  function readLockRecord() {
2819
3120
  try {
2820
- return normalizeRecord(JSON.parse(fs4.readFileSync(lockPath(), "utf8")));
3121
+ return normalizeRecord(JSON.parse(fs5.readFileSync(lockPath(), "utf8")));
2821
3122
  } catch {
2822
3123
  return null;
2823
3124
  }
2824
3125
  }
2825
3126
  function readLockRecordAt(root) {
2826
3127
  try {
2827
- return normalizeRecord(JSON.parse(fs4.readFileSync(aiDirAt(root, "lock.json"), "utf8")));
3128
+ return normalizeRecord(JSON.parse(fs5.readFileSync(aiDirAt(root, "lock.json"), "utf8")));
2828
3129
  } catch {
2829
3130
  return null;
2830
3131
  }
@@ -2835,10 +3136,10 @@ function normalizeRecord(raw) {
2835
3136
  }
2836
3137
  function writeLockRecord(record2) {
2837
3138
  const p = lockPath();
2838
- fs4.mkdirSync(path7.dirname(p), { recursive: true });
3139
+ fs5.mkdirSync(path8.dirname(p), { recursive: true });
2839
3140
  const tmp = `${p}.tmp`;
2840
- fs4.writeFileSync(tmp, JSON.stringify(withSortedMaps(record2), null, 2) + "\n");
2841
- fs4.renameSync(tmp, p);
3141
+ fs5.writeFileSync(tmp, withLineEndings(p, JSON.stringify(withSortedMaps(record2), null, 2) + "\n"));
3142
+ fs5.renameSync(tmp, p);
2842
3143
  }
2843
3144
  function withSortedMaps(record2) {
2844
3145
  const sorted = (m) => {
@@ -2856,12 +3157,12 @@ function withSortedMaps(record2) {
2856
3157
  ...members ? { members } : {}
2857
3158
  };
2858
3159
  }
2859
- var fs4, path7;
3160
+ var fs5, path8;
2860
3161
  var init_lockfile = __esm({
2861
3162
  "src/core/lockfile.ts"() {
2862
3163
  "use strict";
2863
- fs4 = __toESM(require("fs"));
2864
- path7 = __toESM(require("path"));
3164
+ fs5 = __toESM(require("fs"));
3165
+ path8 = __toESM(require("path"));
2865
3166
  init_fs();
2866
3167
  }
2867
3168
  });
@@ -2877,157 +3178,31 @@ function listSpecFiles(specsDir) {
2877
3178
  return listFilesRecursive(specsDir, SPEC_FILE_EXTENSION);
2878
3179
  }
2879
3180
  function removeSpecFile(specPath, specsRoot) {
2880
- if (!fs5.existsSync(specPath)) return false;
2881
- fs5.unlinkSync(specPath);
2882
- pruneEmptyDirs(path8.dirname(specPath), path8.resolve(specsRoot));
3181
+ if (!fs6.existsSync(specPath)) return false;
3182
+ fs6.unlinkSync(specPath);
3183
+ pruneEmptyDirs(path9.dirname(specPath), path9.resolve(specsRoot));
2883
3184
  return true;
2884
3185
  }
2885
3186
  function pruneEmptyDirs(dir, specsRoot) {
2886
3187
  let at = dir;
2887
3188
  while (at !== specsRoot && at.startsWith(specsRoot)) {
2888
- if (!fs5.existsSync(at) || fs5.readdirSync(at).length > 0) return;
2889
- fs5.rmdirSync(at);
2890
- at = path8.dirname(at);
3189
+ if (!fs6.existsSync(at) || fs6.readdirSync(at).length > 0) return;
3190
+ fs6.rmdirSync(at);
3191
+ at = path9.dirname(at);
2891
3192
  }
2892
3193
  }
2893
- var fs5, path8, SPEC_FILE_EXTENSION;
3194
+ var fs6, path9, SPEC_FILE_EXTENSION;
2894
3195
  var init_spec_files = __esm({
2895
3196
  "src/core/spec-files.ts"() {
2896
3197
  "use strict";
2897
- fs5 = __toESM(require("fs"));
2898
- path8 = __toESM(require("path"));
3198
+ fs6 = __toESM(require("fs"));
3199
+ path9 = __toESM(require("path"));
2899
3200
  init_fs();
2900
3201
  init_yaml();
2901
3202
  SPEC_FILE_EXTENSION = ".yaml";
2902
3203
  }
2903
3204
  });
2904
3205
 
2905
- // src/core/adapters/git-source.ts
2906
- function osUserCache() {
2907
- if (process.platform === "win32") return process.env.LOCALAPPDATA ?? path9.join(os2.homedir(), "AppData", "Local");
2908
- if (process.platform === "darwin") return path9.join(os2.homedir(), "Library", "Caches");
2909
- return process.env.XDG_CACHE_HOME ?? path9.join(os2.homedir(), ".cache");
2910
- }
2911
- function cacheRoot() {
2912
- const setting = process.env.WAIRON_CACHE_DIR ?? "";
2913
- let root = cacheRoots.get(setting);
2914
- if (root === void 0) {
2915
- root = setting !== "" ? path9.resolve(setting) : path9.join(osUserCache(), "wairon");
2916
- cacheRoots.set(setting, root);
2917
- }
2918
- return root;
2919
- }
2920
- function git(args, cwd) {
2921
- return (0, import_child_process.execFileSync)("git", args, {
2922
- cwd,
2923
- encoding: "utf8",
2924
- stdio: ["ignore", "pipe", "pipe"],
2925
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "echo" }
2926
- });
2927
- }
2928
- function reasonOf(e) {
2929
- const stderr = e?.stderr;
2930
- const text3 = typeof stderr === "string" && stderr.trim() !== "" ? stderr : e instanceof Error ? e.message : String(e);
2931
- return text3.trim().split(/\r?\n/).filter((l) => l.trim() !== "").slice(-1)[0] ?? "git failed";
2932
- }
2933
- function cacheEntryOf(url, commit3) {
2934
- const repo = crypto2.createHash("sha256").update(url).digest("hex");
2935
- return path9.join(cacheRoot(), "git", repo, commit3);
2936
- }
2937
- function within(entry, url, commit3, dir) {
2938
- const at = dir ? path9.join(entry, dir) : entry;
2939
- if (!fs6.existsSync(at)) {
2940
- throw new SourceUnavailable(`the commit ${commit3} of ${url} holds no "${dir}"`);
2941
- }
2942
- return at;
2943
- }
2944
- function fetchInto(tmp, url, commit3) {
2945
- git(["init", "-q"], tmp);
2946
- try {
2947
- git(["fetch", "-q", "--depth", "1", url, commit3], tmp);
2948
- } catch {
2949
- git(["fetch", "-q", url, "+refs/heads/*:refs/remotes/source/*", "+refs/tags/*:refs/tags/*"], tmp);
2950
- }
2951
- git(["-c", "advice.detachedHead=false", "checkout", "-q", commit3], tmp);
2952
- fs6.rmSync(path9.join(tmp, ".git"), { recursive: true, force: true });
2953
- }
2954
- function fetch2(url, commit3, dir) {
2955
- const entry = cacheEntryOf(url, commit3);
2956
- if (fs6.existsSync(entry)) return within(entry, url, commit3, dir);
2957
- fs6.mkdirSync(path9.dirname(entry), { recursive: true });
2958
- const tmp = `${entry}.tmp-${process.pid}-${crypto2.randomBytes(4).toString("hex")}`;
2959
- fs6.mkdirSync(tmp, { recursive: true });
2960
- try {
2961
- fetchInto(tmp, url, commit3);
2962
- try {
2963
- fs6.renameSync(tmp, entry);
2964
- } catch (e) {
2965
- if (!fs6.existsSync(entry)) throw e;
2966
- fs6.rmSync(tmp, { recursive: true, force: true });
2967
- }
2968
- } catch (e) {
2969
- fs6.rmSync(tmp, { recursive: true, force: true });
2970
- if (fs6.readdirSync(path9.dirname(entry)).length === 0) fs6.rmdirSync(path9.dirname(entry));
2971
- throw e instanceof SourceUnavailable ? e : new SourceUnavailable(`cannot fetch ${commit3} of ${url}: ${reasonOf(e)}`);
2972
- }
2973
- return within(entry, url, commit3, dir);
2974
- }
2975
- function resolve7(url, ref) {
2976
- let listed;
2977
- try {
2978
- listed = git(["ls-remote", url, ref ?? "HEAD"]);
2979
- } catch (e) {
2980
- throw new SourceUnavailable(`cannot reach ${url}: ${reasonOf(e)}`);
2981
- }
2982
- const wanted = commitListed(listed, ref ?? "HEAD");
2983
- if (!wanted) throw new SourceUnavailable(`${url} lists no ref "${ref ?? "HEAD"}"`);
2984
- return wanted;
2985
- }
2986
- function commitListed(listed, ref) {
2987
- const lines = listed.split(/\r?\n/).filter((l) => l.trim() !== "").map((l) => l.split(/\s+/));
2988
- const names = [ref, `refs/heads/${ref}`, `refs/tags/${ref}^{}`, `refs/tags/${ref}`];
2989
- for (const name of names) {
2990
- const line2 = lines.find(([, listedName]) => listedName === name);
2991
- if (line2) return line2[0];
2992
- }
2993
- return void 0;
2994
- }
2995
- function head(directory) {
2996
- try {
2997
- return git(["rev-parse", "HEAD"], directory).trim() || null;
2998
- } catch {
2999
- return null;
3000
- }
3001
- }
3002
- function repositoryRoot(directory) {
3003
- let at = path9.resolve(directory);
3004
- for (; ; ) {
3005
- if (fs6.existsSync(path9.join(at, ".git"))) return at;
3006
- const up = path9.dirname(at);
3007
- if (up === at) return null;
3008
- at = up;
3009
- }
3010
- }
3011
- var import_child_process, crypto2, fs6, os2, path9, SourceUnavailable, cacheRoots;
3012
- var init_git_source = __esm({
3013
- "src/core/adapters/git-source.ts"() {
3014
- "use strict";
3015
- import_child_process = require("child_process");
3016
- crypto2 = __toESM(require("crypto"));
3017
- fs6 = __toESM(require("fs"));
3018
- os2 = __toESM(require("os"));
3019
- path9 = __toESM(require("path"));
3020
- init_errors();
3021
- SourceUnavailable = class extends WaironError {
3022
- constructor(message) {
3023
- super(message);
3024
- this.name = "SourceUnavailable";
3025
- }
3026
- };
3027
- cacheRoots = /* @__PURE__ */ new Map();
3028
- }
3029
- });
3030
-
3031
3206
  // src/models/domain.ts
3032
3207
  function createEmptyTopologyConfig() {
3033
3208
  return { schemaVersion: "1.0.0", domains: [] };
@@ -4459,7 +4634,7 @@ function optionalOf(inner2) {
4459
4634
  return { form: "optional", args: [inner2] };
4460
4635
  }
4461
4636
  function inner(ctx) {
4462
- return { ...ctx, top: false, voidAllowed: false, omittable: false };
4637
+ return { ...ctx, top: false, voidAllowed: false, resultAllowed: false, omittable: false };
4463
4638
  }
4464
4639
  function normalise(node, ctx, notes) {
4465
4640
  switch (node.k) {
@@ -4493,9 +4668,21 @@ function asyncOf(innerNode, ctx, notes, spelled) {
4493
4668
  if (!ctx.top || !RETURNS_POSITIONS.has(ctx.position)) {
4494
4669
  throw new TypeProblemError("TYPE_POSITION_INVALID", `${spelled} may stand only at the top of a returns, not in a ${ctx.position}`);
4495
4670
  }
4496
- const read2 = normalise(innerNode, { ...inner(ctx), voidAllowed: true }, notes);
4671
+ const read2 = normalise(innerNode, { ...inner(ctx), voidAllowed: true, resultAllowed: true }, notes);
4497
4672
  return { form: "async", args: [read2] };
4498
4673
  }
4674
+ function resultOf(args, ctx, notes, spelled) {
4675
+ if (!ctx.resultAllowed) {
4676
+ throw new TypeProblemError(
4677
+ "TYPE_POSITION_INVALID",
4678
+ `${spelled} may stand only as a whole returns (or under its async), not in a ${ctx.top ? ctx.position : "collection, union, optional or argument"}`,
4679
+ "the success type here (a failure belongs to a method's returns)"
4680
+ );
4681
+ }
4682
+ const ok = normalise(args[0], { ...inner(ctx), voidAllowed: true }, notes);
4683
+ const err = normalise(args[1], inner(ctx), notes);
4684
+ return { form: "result", args: [ok, err] };
4685
+ }
4499
4686
  function bareName(name, ctx, notes) {
4500
4687
  const lower = name.toLowerCase();
4501
4688
  if (NONES.has(name)) {
@@ -4533,6 +4720,7 @@ function appliedName(name, args, ctx, notes) {
4533
4720
  throw new TypeProblemError("TYPE_EXPRESSION_INVALID", `"${name}" takes ${alias.arity} type argument${alias.arity === 1 ? "" : "s"}, ${args.length} given`);
4534
4721
  }
4535
4722
  if (alias.form === "async") return asyncOf(args[0], ctx, notes, `${name}<...>`);
4723
+ if (alias.form === "result") return resultOf(args, ctx, notes, `${name}<...>`);
4536
4724
  if (alias.form === "optional") return optionalOf(normalise(args[0], inner(ctx), notes));
4537
4725
  const read2 = args.map((a) => normalise(a, inner(ctx), notes));
4538
4726
  if (alias.form === "map") assertMapKey(read2[0], args[0]);
@@ -4606,6 +4794,8 @@ function canonicalTypeText(expr) {
4606
4794
  return `${expr.form}<${canonicalTypeText(expr.args[0])}>`;
4607
4795
  case "map":
4608
4796
  return `map<${canonicalTypeText(expr.args[0])}, ${canonicalTypeText(expr.args[1])}>`;
4797
+ case "result":
4798
+ return `result<${canonicalTypeText(expr.args[0])}, ${canonicalTypeText(expr.args[1])}>`;
4609
4799
  case "optional": {
4610
4800
  const innerText = canonicalTypeText(expr.args[0]);
4611
4801
  return expr.args[0].form === "union" ? `(${innerText})?` : `${innerText}?`;
@@ -4625,11 +4815,12 @@ function typeNamedRefs(expr) {
4625
4815
  }
4626
4816
  function typeIsMany(expr) {
4627
4817
  if (expr.form === "list" || expr.form === "set" || expr.form === "map") return true;
4628
- if (expr.form === "optional" || expr.form === "async") return typeIsMany(expr.args[0]);
4818
+ if (expr.form === "optional" || expr.form === "async" || expr.form === "result") return typeIsMany(expr.args[0]);
4629
4819
  return false;
4630
4820
  }
4631
4821
  function contextFor(position, omittable2, looseNumber = false) {
4632
- return { position, top: true, voidAllowed: RETURNS_POSITIONS.has(position), omittable: omittable2, looseNumber };
4822
+ const returns = RETURNS_POSITIONS.has(position);
4823
+ return { position, top: true, voidAllowed: returns, resultAllowed: returns, omittable: omittable2, looseNumber };
4633
4824
  }
4634
4825
  function parseTypePosition(text3, position, omittable2 = false) {
4635
4826
  let raw;
@@ -4754,6 +4945,47 @@ function typeProblemIntProposal(problem) {
4754
4945
  if (!parse.expression || parse.problem) return null;
4755
4946
  return { specId: problem.specId, kind: problem.kind, path: problem.path, written: problem.written, stored: parse.canonical };
4756
4947
  }
4948
+ function stringLiteralValue(text3) {
4949
+ const quote = text3[0];
4950
+ if (quote !== "'" && quote !== '"' && quote !== "`" || text3[text3.length - 1] !== quote) return null;
4951
+ return text3.slice(1, -1).replace(/\\(.)/g, "$1");
4952
+ }
4953
+ function kebabOf(name) {
4954
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[\s_.]+/g, "-").toLowerCase();
4955
+ }
4956
+ function typeProblemEnumProposal(problem) {
4957
+ if (problem.code !== "TYPE_FORM_UNSUPPORTED") return null;
4958
+ if (problem.specId === void 0 || problem.kind === void 0 || problem.path === void 0) return null;
4959
+ let raw;
4960
+ try {
4961
+ raw = readRaw(problem.written);
4962
+ } catch (e) {
4963
+ if (e instanceof TypeSyntaxError) return null;
4964
+ throw e;
4965
+ }
4966
+ const members = raw.k === "union" ? raw.members : [raw];
4967
+ const values = [];
4968
+ let optional = false;
4969
+ for (const member of members) {
4970
+ if (member.k === "name" && member.args === void 0 && NONES.has(member.name)) {
4971
+ optional = true;
4972
+ continue;
4973
+ }
4974
+ const value = member.k === "literal" ? stringLiteralValue(member.text) : null;
4975
+ if (value === null) return null;
4976
+ if (!values.includes(value)) values.push(value);
4977
+ }
4978
+ if (values.length === 0) return null;
4979
+ return {
4980
+ specId: problem.specId,
4981
+ kind: problem.kind,
4982
+ path: problem.path,
4983
+ written: problem.written,
4984
+ enumId: kebabOf(positionNameOf(problem.path)),
4985
+ values,
4986
+ optional
4987
+ };
4988
+ }
4757
4989
  function emptyTypeSpellingFacts() {
4758
4990
  return { respellings: [], problems: [] };
4759
4991
  }
@@ -4812,7 +5044,11 @@ var init_type_grammar = __esm({
4812
5044
  ["HashMap", { form: "map", arity: 2 }],
4813
5045
  ["Option", { form: "optional", arity: 1 }],
4814
5046
  ["Optional", { form: "optional", arity: 1 }],
4815
- ["Promise", { form: "async", arity: 1 }]
5047
+ ["Promise", { form: "async", arity: 1 }],
5048
+ // The success-or-failure type the mainstream languages with typed failures
5049
+ // share (Rust, Swift, Kotlin, F#); Rust's spelling is its alias.
5050
+ ["result", { form: "result", arity: 2 }],
5051
+ ["Result", { form: "result", arity: 2 }]
4816
5052
  ]);
4817
5053
  NOT_NEUTRAL = /* @__PURE__ */ new Map([
4818
5054
  ["uuid", "a named value-object holding a string (e.g. order_id)"],
@@ -4821,7 +5057,6 @@ var init_type_grammar = __esm({
4821
5057
  ["byte", "int, or bytes for binary data"],
4822
5058
  ["time", "datetime for an instant, or duration for an elapsed time"],
4823
5059
  ["tuple", "a named value-object"],
4824
- ["result", "the success type as the returns (failures as findings or throws), or a named value-object"],
4825
5060
  ["error", "a named value-object"],
4826
5061
  ["never", "void"],
4827
5062
  ["box", "the inner type (ownership is an implementation detail)"],
@@ -4964,9 +5199,9 @@ var init_type_grammar = __esm({
4964
5199
  }
4965
5200
  name += this.ident();
4966
5201
  while (this.isP("::") || this.isP(".")) {
4967
- const sep18 = this.peek().v;
5202
+ const sep19 = this.peek().v;
4968
5203
  this.at++;
4969
- name += sep18 + this.ident();
5204
+ name += sep19 + this.ident();
4970
5205
  }
4971
5206
  if (!this.isP("<")) return { k: "name", name };
4972
5207
  this.at++;
@@ -4993,6 +5228,10 @@ var init_type_grammar = __esm({
4993
5228
  const close2 = this.closingParen();
4994
5229
  const after = close2 === -1 ? void 0 : this.tokens[close2 + 1];
4995
5230
  if (!after || after.t !== "p" || after.v !== "=>") {
5231
+ if (this.isP(")", 1)) {
5232
+ this.at += 2;
5233
+ return { k: "name", name: "void" };
5234
+ }
4996
5235
  this.expect("(");
4997
5236
  const inner2 = this.type();
4998
5237
  this.expect(")");
@@ -5309,6 +5548,7 @@ function lastSegment(name) {
5309
5548
  return name.split(/::|\./).filter(Boolean).pop() ?? name;
5310
5549
  }
5311
5550
  function sameType(code, spec, codeNames) {
5551
+ if (spec.form === "result" && code.form !== "result") return sameType(code, spec.args[0], codeNames);
5312
5552
  if (code.form === "primitive" && code.name === "number") return spec.form === "primitive" && (spec.name === "int" || spec.name === "float");
5313
5553
  if (code.form === "named" && (spec.form === "named" || spec.form === "applied")) {
5314
5554
  if (spec.form === "applied") return false;
@@ -5345,6 +5585,8 @@ function writeTypeScript(expr) {
5345
5585
  return expr.args.map(writeTypeScript).join(" | ");
5346
5586
  case "async":
5347
5587
  return `Promise<${writeTypeScript(expr.args[0])}>`;
5588
+ case "result":
5589
+ return writeTypeScript(expr.args[0]);
5348
5590
  case "applied":
5349
5591
  return `${expr.name}<${expr.args.map(writeTypeScript).join(", ")}>`;
5350
5592
  }
@@ -5395,6 +5637,7 @@ var init_type_dialects = __esm({
5395
5637
  "T? \u2192 T | null",
5396
5638
  "A | B \u2192 A | B",
5397
5639
  "async T \u2192 Promise<T>",
5640
+ "result<T, E> \u2192 T, throwing E on failure (result<void, E> \u2192 void)",
5398
5641
  "an enum E \u2192 type E = 'a' | 'b' (a string-literal union alias)",
5399
5642
  "a named scalar E holding P \u2192 type E = P (an alias of the primitive)"
5400
5643
  ];
@@ -5725,8 +5968,14 @@ function languageOfSourcePath(sourcePath) {
5725
5968
  function dialectOf(facts) {
5726
5969
  return facts.language ? typeDialectFor(facts.language) : null;
5727
5970
  }
5728
- function resolveImport(fromFile, specifier, knownPaths) {
5729
- if (!specifier.startsWith(".")) return void 0;
5971
+ function returnTypeOf(facts, fn) {
5972
+ return ownEntry(facts.returnTypes, fn);
5973
+ }
5974
+ function resolveImport(fromFile, specifier, knownPaths, packages) {
5975
+ if (!specifier.startsWith(".")) {
5976
+ const mapped = ownEntry(packages, specifier);
5977
+ return mapped !== void 0 && knownPaths.has(mapped) ? mapped : void 0;
5978
+ }
5730
5979
  const joined = pathKey(path10.posix.normalize(path10.posix.join(path10.posix.dirname(fromFile), specifier)));
5731
5980
  const candidates = [
5732
5981
  joined,
@@ -6141,7 +6390,7 @@ var init_design_export = __esm({
6141
6390
  ExtSchema = import_zod9.z.record(import_zod9.z.unknown());
6142
6391
  TypeExpressionSchema = import_zod9.z.lazy(
6143
6392
  () => import_zod9.z.object({
6144
- form: import_zod9.z.enum(["primitive", "named", "list", "set", "map", "optional", "union", "async", "applied"]),
6393
+ form: import_zod9.z.enum(["primitive", "named", "list", "set", "map", "optional", "union", "async", "applied", "result"]),
6145
6394
  name: import_zod9.z.string().optional(),
6146
6395
  args: import_zod9.z.array(TypeExpressionSchema)
6147
6396
  }).passthrough()
@@ -11609,7 +11858,7 @@ function snapshotPath(root, alias) {
11609
11858
  function writeAtomically(file, text3) {
11610
11859
  fs8.mkdirSync(path13.dirname(file), { recursive: true });
11611
11860
  const tmp = `${file}.${process.pid}.tmp`;
11612
- fs8.writeFileSync(tmp, text3);
11861
+ fs8.writeFileSync(tmp, withLineEndings(file, text3));
11613
11862
  fs8.renameSync(tmp, file);
11614
11863
  }
11615
11864
  function sortedLock(lock) {
@@ -11784,6 +12033,8 @@ function schemaOfExpression(expr, closureIds) {
11784
12033
  return { oneOf: expr.args.map((member) => schemaOfExpression(member, closureIds)) };
11785
12034
  case "async":
11786
12035
  return schemaOfExpression(expr.args[0], closureIds);
12036
+ case "result":
12037
+ return schemaOfExpression(expr.args[0], closureIds);
11787
12038
  }
11788
12039
  }
11789
12040
  function schemaFor(typeRef2, closureIds) {
@@ -11793,7 +12044,8 @@ function schemaFor(typeRef2, closureIds) {
11793
12044
  }
11794
12045
  function returnsNothing(returns) {
11795
12046
  const expr = expressionOf(returns);
11796
- const inner2 = expr?.form === "async" ? expr.args[0] : expr;
12047
+ const awaited = expr?.form === "async" ? expr.args[0] : expr;
12048
+ const inner2 = awaited?.form === "result" ? awaited.args[0] : awaited;
11797
12049
  return inner2?.form === "primitive" && inner2.name === "void";
11798
12050
  }
11799
12051
  function enumDescription(values) {
@@ -13789,7 +14041,7 @@ function approvalFinding(entry, config) {
13789
14041
  return finding(
13790
14042
  config,
13791
14043
  "MEMBER_DRIFTED",
13792
- `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\`).`,
14044
+ `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\`).`,
13793
14045
  entry.key
13794
14046
  );
13795
14047
  }
@@ -14185,12 +14437,12 @@ function packFiles(dir, prefix = "") {
14185
14437
  function computePackDigest(packDir) {
14186
14438
  const hash = crypto3.createHash("sha256");
14187
14439
  const abs = path16.resolve(packDir);
14188
- let isFile = false;
14440
+ let isFile2 = false;
14189
14441
  try {
14190
- isFile = fs11.statSync(abs).isFile();
14442
+ isFile2 = fs11.statSync(abs).isFile();
14191
14443
  } catch {
14192
14444
  }
14193
- const entries = isFile ? [[path16.basename(abs), abs]] : packFiles(abs).map((rel2) => [rel2, path16.join(abs, rel2)]);
14445
+ const entries = isFile2 ? [[path16.basename(abs), abs]] : packFiles(abs).map((rel2) => [rel2, path16.join(abs, rel2)]);
14194
14446
  for (const [rel2, full] of entries) {
14195
14447
  hash.update(rel2);
14196
14448
  hash.update("\0");
@@ -14683,6 +14935,16 @@ function verifyIntegrity(selection, packDir, where) {
14683
14935
  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.`
14684
14936
  };
14685
14937
  }
14938
+ function bundlesSingleFile(bundleDir, storePath16) {
14939
+ try {
14940
+ if (!fs12.statSync(storePath16).isFile()) return false;
14941
+ const entry = packDirEntry(bundleDir);
14942
+ if (!entry || fs12.readdirSync(bundleDir).length !== 1) return false;
14943
+ return fs12.readFileSync(entry).equals(fs12.readFileSync(storePath16));
14944
+ } catch {
14945
+ return false;
14946
+ }
14947
+ }
14686
14948
  function detectBundleStoreDrift(selection, bundleDir) {
14687
14949
  const bundledVersion = path17.basename(bundleDir);
14688
14950
  const installed = resolveInstalledPack(selection.name, bundledVersion);
@@ -14690,6 +14952,7 @@ function detectBundleStoreDrift(selection, bundleDir) {
14690
14952
  const bundleDigest = computePackDigest(bundleDir);
14691
14953
  const storeDigest = computePackDigest(installed.path);
14692
14954
  if (bundleDigest === storeDigest) return void 0;
14955
+ if (bundlesSingleFile(bundleDir, installed.path)) return void 0;
14693
14956
  return {
14694
14957
  code: "PACK_STORE_DRIFT",
14695
14958
  severity: "warning",
@@ -14911,6 +15174,7 @@ function appendUnique(map, key, value, same) {
14911
15174
  list2.push(value);
14912
15175
  }
14913
15176
  function buildCodeIndex(model) {
15177
+ const packages = model.packages ?? {};
14914
15178
  const facts = /* @__PURE__ */ new Map();
14915
15179
  const paths = /* @__PURE__ */ new Set();
14916
15180
  const exactPaths = /* @__PURE__ */ new Set();
@@ -14943,7 +15207,7 @@ function buildCodeIndex(model) {
14943
15207
  const f = facts.get(from);
14944
15208
  if (f?.status !== "analyzed") continue;
14945
15209
  for (const specifier of f.reexports) {
14946
- const to = resolveImport(from, specifier, paths);
15210
+ const to = resolveImport(from, specifier, paths, packages);
14947
15211
  if (to && !out.has(to)) {
14948
15212
  out.add(to);
14949
15213
  queue.push(to);
@@ -14955,7 +15219,7 @@ function buildCodeIndex(model) {
14955
15219
  };
14956
15220
  const NO_ORIGIN = /* @__PURE__ */ new Set();
14957
15221
  const landingFrom = (scope, specifier) => {
14958
- const to = resolveImport(scope, specifier, paths);
15222
+ const to = resolveImport(scope, specifier, paths, packages);
14959
15223
  return to ? republishedFrom(to) : NO_ORIGIN;
14960
15224
  };
14961
15225
  const scopeOf3 = (site, from) => {
@@ -14985,7 +15249,7 @@ function buildCodeIndex(model) {
14985
15249
  };
14986
15250
  const possibleOriginsOf = (site, from) => {
14987
15251
  const proven = originOf2(site, from);
14988
- if (!site.field && !site.constructed && !site.via) return proven;
15252
+ if (!site.field && !site.constructed && !site.via && !site.returnedBy && !site.enclosingClass) return proven;
14989
15253
  const resolved = scopeOf3(site, from);
14990
15254
  if (!resolved) return proven;
14991
15255
  const { path: scope, facts: f } = resolved;
@@ -14998,24 +15262,59 @@ function buildCodeIndex(model) {
14998
15262
  if (site.field) for (const typeName of fieldTypesOf(f, site.field)) widenThroughType(typeName);
14999
15263
  if (site.constructed) widenThrough(site.constructed, importBindingOf(f, site.constructed)?.from);
15000
15264
  if (site.via) for (const typeName of localTypesOf(f, site.via)) widenThroughType(typeName);
15265
+ if (site.enclosingClass !== void 0 && holdsMemberBody(f, site.name, site.enclosingClass)) {
15266
+ for (const candidate of republishedFrom(scope)) out.add(candidate);
15267
+ }
15268
+ if (site.returnedBy !== void 0) {
15269
+ for (const fnFile of originOf2({ name: site.returnedBy, member: false }, scope)) {
15270
+ const at = facts.get(fnFile);
15271
+ if (at?.status !== "analyzed" || at.analysisGrade !== "exact") continue;
15272
+ const typeName = returnTypeOf(at, site.returnedBy);
15273
+ if (typeName === void 0) continue;
15274
+ const specifier = typeBindingOf(at, typeName);
15275
+ const landings = specifier !== void 0 ? landingFrom(fnFile, specifier) : declarationsAt(fnFile).has(typeName) ? republishedFrom(fnFile) : NO_ORIGIN;
15276
+ for (const candidate of landings) {
15277
+ const there = facts.get(candidate);
15278
+ if (there && holdsMemberBody(there, site.name, typeName)) out.add(candidate);
15279
+ }
15280
+ }
15281
+ }
15001
15282
  return out;
15002
15283
  };
15284
+ const holdsMemberBody = (f, name, container) => f.status === "analyzed" && f.analysisGrade === "exact" && !!f.functionBodies && Object.prototype.hasOwnProperty.call(f.functionBodies, name) && f.functionBodies[name].some((b) => b.container === container);
15003
15285
  const declarationsAt = (path93) => namesAt(declarations, pathKey(path93), (f) => /* @__PURE__ */ new Set([...f.declaredNames, ...f.exportedNames]));
15004
- const forwardsOf = (start2, name) => {
15286
+ const forwardsOf = (start2, name, container) => {
15005
15287
  const out = [];
15288
+ const listed = /* @__PURE__ */ new Set();
15006
15289
  const seen = /* @__PURE__ */ new Set();
15007
- const queue = [{ file: pathKey(start2), name }];
15290
+ const queue = [{ file: pathKey(start2), name, container }];
15008
15291
  while (queue.length) {
15009
15292
  const at = queue.shift();
15010
- const key = `${at.file}#${at.name}`;
15293
+ const key = `${at.scope ? "s" : "p"}|${at.container ?? ""}|${at.file}#${at.name}`;
15011
15294
  if (seen.has(key)) continue;
15012
15295
  seen.add(key);
15013
- out.push(at);
15014
- for (const binding of facts.get(at.file)?.reexportBindings ?? []) {
15296
+ if (!listed.has(`${at.file}#${at.name}`)) {
15297
+ listed.add(`${at.file}#${at.name}`);
15298
+ out.push({ file: at.file, name: at.name });
15299
+ }
15300
+ const f = facts.get(at.file);
15301
+ if (at.scope) {
15302
+ const binding = f ? importBindingOf(f, at.name) : void 0;
15303
+ if (!binding || binding.namespace) continue;
15304
+ const to = resolveImport(at.file, binding.from, paths, packages);
15305
+ if (to) queue.push({ file: to, name: binding.imported ?? at.name });
15306
+ continue;
15307
+ }
15308
+ for (const alias of f?.exportAliases ?? []) {
15309
+ if (alias.exported !== at.name || alias.container !== at.container) continue;
15310
+ queue.push({ file: at.file, name: alias.local, scope: true });
15311
+ }
15312
+ if (at.container !== void 0) continue;
15313
+ for (const binding of f?.reexportBindings ?? []) {
15015
15314
  const star = binding.exported === "*";
15016
15315
  if (!star && binding.exported !== at.name) continue;
15017
15316
  if (star && at.name === "default") continue;
15018
- const to = resolveImport(at.file, binding.from, paths);
15317
+ const to = resolveImport(at.file, binding.from, paths, packages);
15019
15318
  if (!to) continue;
15020
15319
  if (star && !facts.get(to)?.exportedNames.includes(at.name)) continue;
15021
15320
  queue.push({ file: to, name: star ? at.name : binding.local });
@@ -15026,6 +15325,7 @@ function buildCodeIndex(model) {
15026
15325
  return {
15027
15326
  paths,
15028
15327
  exactPaths,
15328
+ packages,
15029
15329
  factsAt: (path93) => facts.get(pathKey(path93)),
15030
15330
  declarationsAt,
15031
15331
  anchorsAt: (path93) => namesAt(anchors, pathKey(path93), (f) => /* @__PURE__ */ new Set([...f.declaredNames, ...f.exportedNames, ...f.anchoredNames])),
@@ -15079,7 +15379,7 @@ function buildImportGraph(index, universe) {
15079
15379
  const traces = /* @__PURE__ */ new Map();
15080
15380
  const resolveAll = (from, specifiers, into) => {
15081
15381
  for (const specifier of specifiers) {
15082
- const to = resolveImport(from, specifier, universe);
15382
+ const to = resolveImport(from, specifier, universe, index.packages);
15083
15383
  if (to && to !== from) into.add(to);
15084
15384
  }
15085
15385
  };
@@ -15406,7 +15706,7 @@ function judgeOwnIdentity(ctx, identity2) {
15406
15706
  ctx.addIssue(
15407
15707
  "notice",
15408
15708
  "PROJECT_ID_DEFAULTED",
15409
- `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).`
15709
+ `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).`
15410
15710
  );
15411
15711
  }
15412
15712
  const ambiguous = problem("ambiguous");
@@ -15440,7 +15740,7 @@ var init_project_identity = __esm({
15440
15740
  projectIdentityRule = {
15441
15741
  name: "project-identity",
15442
15742
  judges: "design",
15443
- 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.",
15743
+ 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.",
15444
15744
  codes: [
15445
15745
  { code: "PROJECT_ID_AMBIGUOUS", defaultSeverity: "warning", summary: "Project has no usable id: its name yields no slug, or its declared id breaks the grammar" },
15446
15746
  { code: "PROJECT_ID_CHANGED", defaultSeverity: "error", summary: "Project id differs from the id the lock approved" },
@@ -15866,10 +16166,10 @@ var init_type_expressions = __esm({
15866
16166
  typeExpressionsRule = {
15867
16167
  name: "type-expressions",
15868
16168
  judges: "design",
15869
- 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).",
16169
+ 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).",
15870
16170
  codes: [
15871
- { code: "TYPE_EXPRESSION_INVALID", defaultSeverity: "error", summary: "A structured type position does not parse under the type grammar" },
15872
- { 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??" },
16171
+ { code: "TYPE_EXPRESSION_INVALID", defaultSeverity: "warning", summary: "A structured type position does not parse under the type grammar" },
16172
+ { 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??" },
15873
16173
  { 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" },
15874
16174
  { 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" },
15875
16175
  { 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" }
@@ -15879,9 +16179,8 @@ var init_type_expressions = __esm({
15879
16179
  if (!facts) return;
15880
16180
  for (const problem of facts.problems) {
15881
16181
  if (problem.specId === void 0 || !ctx.isSpecInScope(problem.specId)) continue;
15882
- const severity = problem.code === "TYPE_EXPRESSION_INVALID" || problem.code === "TYPE_POSITION_INVALID" ? "error" : "warning";
15883
16182
  ctx.addIssue(
15884
- severity,
16183
+ "warning",
15885
16184
  problem.code,
15886
16185
  problemMessage(problem, problem.specId),
15887
16186
  problem.specId,
@@ -16270,7 +16569,7 @@ var init_export_tables = __esm({
16270
16569
  exportTablesRule = {
16271
16570
  name: "export-tables",
16272
16571
  judges: "design",
16273
- 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.",
16572
+ 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.",
16274
16573
  codes: [
16275
16574
  { code: "EXPORT_ID_DUPLICATE", defaultSeverity: "error", summary: "One public name bound to different targets by two wildcard re-exports or two explicit entries" },
16276
16575
  { 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)" },
@@ -16446,7 +16745,7 @@ var init_reference_forms = __esm({
16446
16745
  referenceFormsRule = {
16447
16746
  name: "reference-forms",
16448
16747
  judges: "design",
16449
- 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.",
16748
+ 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.",
16450
16749
  codes: [
16451
16750
  { 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" }
16452
16751
  ],
@@ -16483,9 +16782,9 @@ var init_member_declarations = __esm({
16483
16782
  memberDeclarationsRule = {
16484
16783
  name: "member-declarations",
16485
16784
  judges: "design",
16486
- 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.",
16785
+ 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.",
16487
16786
  codes: [
16488
- { 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`." },
16787
+ { 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`." },
16489
16788
  { 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." },
16490
16789
  { 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." }
16491
16790
  ],
@@ -16523,7 +16822,7 @@ var init_member_declarations = __esm({
16523
16822
  ctx.addIssue(
16524
16823
  "notice",
16525
16824
  "DEPRECATED_MOUNT_FORM",
16526
- `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.`
16825
+ `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.`
16527
16826
  );
16528
16827
  }
16529
16828
  for (const problem of family.problems) {
@@ -16567,7 +16866,7 @@ var init_project_boundaries = __esm({
16567
16866
  projectBoundariesRule = {
16568
16867
  name: "project-boundaries",
16569
16868
  judges: "design",
16570
- 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.",
16869
+ 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.",
16571
16870
  codes: [
16572
16871
  { 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" },
16573
16872
  { 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" },
@@ -19420,7 +19719,7 @@ var init_pack_reproducibility = __esm({
19420
19719
  ctx.addIssue(
19421
19720
  "warning",
19422
19721
  "PACK_SOURCE_UNFETCHABLE",
19423
- `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}\`).`
19722
+ `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.`
19424
19723
  );
19425
19724
  }
19426
19725
  }
@@ -20522,6 +20821,18 @@ var init_invariant_backing = __esm({
20522
20821
  });
20523
20822
 
20524
20823
  // src/core/rules/wiring/invariant-references.ts
20824
+ function unknownInvariantMessage(where, ref, types, aliases) {
20825
+ const parts = splitInvariantRef(ref);
20826
+ const qualifier = parts ? parts.typeRef.lastIndexOf("::") : -1;
20827
+ if (parts && qualifier > 0) {
20828
+ const bare = `${parts.typeRef.slice(qualifier + 2)}.${parts.invariantId}`;
20829
+ if (resolveInvariantRef(bare, types, aliases)) {
20830
+ const prefix = parts.typeRef.slice(0, qualifier);
20831
+ 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).`;
20832
+ }
20833
+ }
20834
+ return `${where} asserts invariant "${ref}", but no entity declares it (expected "<type-id>.<invariant-id>" naming a declared entry in that entity's invariants).`;
20835
+ }
20525
20836
  var invariantReferencesRule;
20526
20837
  var init_invariant_references = __esm({
20527
20838
  "src/core/rules/wiring/invariant-references.ts"() {
@@ -20530,7 +20841,7 @@ var init_invariant_references = __esm({
20530
20841
  invariantReferencesRule = {
20531
20842
  name: "invariant-references",
20532
20843
  judges: "design",
20533
- 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.",
20844
+ 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.",
20534
20845
  codes: [
20535
20846
  { code: "UNKNOWN_INVARIANT_REF", defaultSeverity: "error", summary: "A narrative step asserts an invariant that no entity declares" }
20536
20847
  ],
@@ -20545,7 +20856,7 @@ var init_invariant_references = __esm({
20545
20856
  ctx.addIssue(
20546
20857
  "error",
20547
20858
  "UNKNOWN_INVARIANT_REF",
20548
- `Step ${step.stepNumber} of "${method2.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).`,
20859
+ unknownInvariantMessage(`Step ${step.stepNumber} of "${method2.name}" in implementation "${impl.id}"`, ref, ctx.types, aliases),
20549
20860
  impl.id,
20550
20861
  isDraftCtx
20551
20862
  );
@@ -20918,13 +21229,13 @@ function invokedAs(code, site, file) {
20918
21229
  if (!site.member) {
20919
21230
  const binding = importBindingOf(facts, site.name);
20920
21231
  if (!binding || binding.namespace) return [];
20921
- const module3 = resolveImport(scope, binding.from, code.paths);
21232
+ const module3 = resolveImport(scope, binding.from, code.paths, code.packages);
20922
21233
  return module3 ? code.forwardsOf(module3, binding.imported ?? site.name) : [];
20923
21234
  }
20924
21235
  if (!site.via) return [];
20925
21236
  const receiver = importBindingOf(facts, site.via);
20926
21237
  if (!receiver?.namespace) return [];
20927
- const module2 = resolveImport(scope, receiver.from, code.paths);
21238
+ const module2 = resolveImport(scope, receiver.from, code.paths, code.packages);
20928
21239
  return module2 ? code.forwardsOf(module2, site.name) : [];
20929
21240
  }
20930
21241
  function claimsOf(implMethod) {
@@ -20944,6 +21255,8 @@ function describeSite(site) {
20944
21255
  if (site.via) return `${site.via}.${site.name}(\u2026)`;
20945
21256
  if (site.field) return `this.${site.field}.${site.name}(\u2026)`;
20946
21257
  if (site.constructed) return `new ${site.constructed}(\u2026).${site.name}(\u2026)`;
21258
+ if (site.returnedBy) return `${site.returnedBy}(\u2026).${site.name}(\u2026)`;
21259
+ if (site.enclosingClass) return `this.${site.name}(\u2026)`;
20947
21260
  return `<receiver>.${site.name}(\u2026)`;
20948
21261
  }
20949
21262
  function describeMiss(m) {
@@ -20964,7 +21277,7 @@ var init_call_conformance = __esm({
20964
21277
  callConformanceRule = {
20965
21278
  name: "call-conformance",
20966
21279
  judges: "code",
20967
- description: "Code\u2194spec Level 3: the CLAIMED call \u2194 realized call relation, judged both ways against the method's own source file (its sourcePath, else the implementation's), at exact analysis grade only. A method claims a call two ways and they face one check: a narrative `call` step, and an entry of the declared `calls` a narrative-less method reaches its collaborators through. They assert the same thing, and the step number is no part of the claim \u2014 this check never reads order, so it is only how a finding points at one. Forward: every claim must be realized by a call whose callee RESOLVES TO one of the target method's own source files \u2014 the target's contract name or a per-method `symbol` override, closed transitively over the named helpers the realized function calls, and resolved against every file the call CAN have reached: a `this.<field>.<method>()` receiver followed through the field's DECLARED TYPE, a `new Class(...).<method>()` receiver through the module its CLASS NAME came from, and a plain `<name>.<method>()` receiver through the type the file ANNOTATES that name with \u2014 a parameter's, or an annotated variable's, which is how a module that wires its collaborators as closures writes every one of its calls. A matching call whose origin a pure model cannot resolve is reported apart as CALL_ORIGIN_UNRESOLVED, which NAMES the shape it could not follow and asks for nothing \u2014 a coverage hole in the reader, reported for the reason CONFORMANCE_DEGRADED is: a silently degraded gate is worse than a degraded one. A finding names a landing only from the PROVEN tier so that widening what a call reached can accept a claim but never accuse one, and a target that names no file of its own falls back to name membership. Converse: a call that resolves to a modelled method of ANOTHER component in the SAME file crosses a component boundary while looking local, so the method must claim it \u2014 in a step or in `calls` (UNDECLARED_COLOCATED_CALL) \u2014 judged on the proven tier alone, and a same-file private helper is no modelled method and is never reported. A method that claims nothing at all is judged in neither direction: what it leaves unsaid is UNUSED_*'s subject. Order, arguments and conditions stay unverified, dispatch steps (runtime-table routed) are no claim about a call site, a malformed declared reference is MALFORMED_DECLARED_CALL's finding and names no target here, the conformance dial (off) skips, and weaker analysis grades never guess.",
21280
+ description: "Code\u2194spec Level 3: the CLAIMED call \u2194 realized call relation, judged both ways against the method's own source file (its sourcePath, else the implementation's), at exact analysis grade only. A method claims a call two ways and they face one check: a narrative `call` step, and an entry of the declared `calls` a narrative-less method reaches its collaborators through. They assert the same thing, and the step number is no part of the claim \u2014 this check never reads order, so it is only how a finding points at one. Forward: every claim must be realized by a call whose callee RESOLVES TO one of the target method's own source files \u2014 the target's contract name or a per-method `symbol` override, closed transitively over the named helpers the realized function calls, and resolved against every file the call CAN have reached: a `this.<field>.<method>()` receiver followed through the field's DECLARED TYPE, a `new Class(...).<method>()` receiver through the module its CLASS NAME came from, a plain `<name>.<method>()` receiver through the type the file ANNOTATES that name with \u2014 a parameter's, or an annotated variable's, which is how a module that wires its collaborators as closures writes every one of its calls \u2014 a `fn(...).<method>()` receiver through the type fn RETURNS where the code settles it, and a `this.<method>()` receiver through the class whose own method it sits in; a package specifier naming a package of this repository resolves like a relative one, and an export alias (`export { a as b }`, an adapter object property `{ b: a }`) forwards by identity like a re-export. A matching call whose origin a pure model cannot resolve is reported apart as CALL_ORIGIN_UNRESOLVED, which NAMES the shape it could not follow and asks for nothing \u2014 a coverage hole in the reader, reported for the reason CONFORMANCE_DEGRADED is: a silently degraded gate is worse than a degraded one. A finding names a landing only from the PROVEN tier so that widening what a call reached can accept a claim but never accuse one, and a target that names no file of its own falls back to name membership. Converse: a call that resolves to a modelled method of ANOTHER component in the SAME file crosses a component boundary while looking local, so the method must claim it \u2014 in a step or in `calls` (UNDECLARED_COLOCATED_CALL) \u2014 judged on the proven tier alone, and a same-file private helper is no modelled method and is never reported; a function forwarding by identity to a modelled method written in another file IS that method, so what it calls is that method's subject. A method that claims nothing at all is judged in neither direction: what it leaves unsaid is UNUSED_*'s subject. Order, arguments and conditions stay unverified, dispatch steps (runtime-table routed) are no claim about a call site, a malformed declared reference is MALFORMED_DECLARED_CALL's finding and names no target here, the conformance dial (off) skips, and weaker analysis grades never guess.",
20968
21281
  codes: [
20969
21282
  { code: "CALL_STEP_UNREALIZED", defaultSeverity: "warning", summary: "Narrative call step or declared call realized by no call that resolves to the target method's own source file \u2014 the call is absent, or it lands in another module", carryable: true },
20970
21283
  { code: "CALL_ORIGIN_UNRESOLVED", defaultSeverity: "warning", summary: "Narrative call step or declared call whose target name IS called, but only from call sites written in a shape this analysis cannot resolve to a file \u2014 the claim was not checked, and is neither proven realized nor accused", carryable: true },
@@ -21007,7 +21320,7 @@ var init_call_conformance = __esm({
21007
21320
  if (!ctx.componentMap.has(claim.component)) continue;
21008
21321
  const target = resolveCallTarget(ctx, claim.component, claim.method);
21009
21322
  if (target.accepted.has(fnSymbol)) continue;
21010
- if (isTarget(code.forwardsOf(file, fnSymbol), target)) continue;
21323
+ if (isTarget(code.forwardsOf(file, fnSymbol, implMethod.exportedVia), target)) continue;
21011
21324
  const matching = sites.filter((s) => target.accepted.has(s.name));
21012
21325
  const ref = `${claim.component}.${claim.method}`;
21013
21326
  if (target.files.size === 0) {
@@ -21050,7 +21363,7 @@ var init_call_conformance = __esm({
21050
21363
  ctx.addIssue(
21051
21364
  "warning",
21052
21365
  "CALL_ORIGIN_UNRESOLVED",
21053
- `Method "${implMethod.name}" in implementation "${impl.id}": ${unresolved.length} ${claimNoun(implMethod)} ARE called by name inside the function "${fnSymbol}" in "${file}", but every site calling them is written in a shape this analysis cannot resolve to a file \u2014 ${unresolved.map(describeMiss).join("; ")}. What resolves is a name bound to a module of THIS project: a bare or namespaced call through such an import binding, a \`this.<field>\` receiver whose declared type names one, a \`new Class(\u2026)\` receiver whose class does, and a receiver NAME the file annotates with a type that does. What does not: an import from a PACKAGE specifier, a receiver holding a value the module assembled, a receiver this model records no name for, and one the file annotates with nothing. This reports what was not checked, not what is wrong \u2014 the claim is neither proven realized nor accused of being missing, and no working call is asked to be rewritten to suit the reader.`,
21366
+ `Method "${implMethod.name}" in implementation "${impl.id}": ${unresolved.length} ${claimNoun(implMethod)} ARE called by name inside the function "${fnSymbol}" in "${file}", but every site calling them is written in a shape this analysis cannot resolve to a file \u2014 ${unresolved.map(describeMiss).join("; ")}. What resolves is a name bound to a module of THIS project: a bare or namespaced call through such an import binding (a package of this repository included), a \`this.<field>\` receiver whose declared type names one, a \`new Class(\u2026)\` receiver whose class does, a receiver NAME the file annotates with a type that does, a \`fn(\u2026)\` receiver whose function's return the code settles on a type with that method's body, and \`this\` inside a class's own method that has it. What does not: an import from a third-party PACKAGE, a receiver holding a value the module assembled, a receiver this model records no name for, and one the file annotates with nothing. This reports what was not checked, not what is wrong \u2014 the claim is neither proven realized nor accused of being missing, and no working call is asked to be rewritten to suit the reader.`,
21054
21367
  impl.id,
21055
21368
  entry.draftContext,
21056
21369
  void 0,
@@ -21072,7 +21385,9 @@ var init_call_conformance = __esm({
21072
21385
  reached.set(site, owner);
21073
21386
  return owner !== void 0;
21074
21387
  };
21075
- for (const site of closedCallSites(code, file, fnSymbol, stopAtModelled, implMethod.exportedVia) ?? []) {
21388
+ const forwardsToModelled = code.forwardsOf(file, fnSymbol, implMethod.exportedVia).some((p) => p.file !== here && modelledAt.get(p.file)?.byName.has(p.name));
21389
+ const conversed = forwardsToModelled ? [] : closedCallSites(code, file, fnSymbol, stopAtModelled, implMethod.exportedVia) ?? [];
21390
+ for (const site of conversed) {
21076
21391
  const owner = reached.get(site);
21077
21392
  if (!owner || owner.component === component.id) continue;
21078
21393
  const ref = `${owner.component}.${owner.method}`;
@@ -22363,7 +22678,7 @@ var init_export_conformance = __esm({
22363
22678
  const facts = code.factsAt(file);
22364
22679
  if (!facts || facts.status !== "analyzed" || facts.analysisGrade !== "exact") continue;
22365
22680
  for (const binding of facts.reexportBindings ?? []) {
22366
- const target = resolveImport(file, binding.from, code.paths);
22681
+ const target = resolveImport(file, binding.from, code.paths, code.packages);
22367
22682
  const methods = target ? typeMethodsAt.get(target) : void 0;
22368
22683
  if (!methods) continue;
22369
22684
  if (binding.local === "*") {
@@ -22375,7 +22690,7 @@ var init_export_conformance = __esm({
22375
22690
  }
22376
22691
  const takenFrom = /* @__PURE__ */ new Map();
22377
22692
  const take = (consumer, writtenIn, specifier, names) => {
22378
- const target = resolveImport(writtenIn, specifier, code.paths);
22693
+ const target = resolveImport(writtenIn, specifier, code.paths, code.packages);
22379
22694
  if (!target) return;
22380
22695
  let byName = takenFrom.get(target);
22381
22696
  if (!byName) takenFrom.set(target, byName = /* @__PURE__ */ new Map());
@@ -24203,6 +24518,9 @@ function walkExact(ts, sourceText, fileName) {
24203
24518
  const reexports = /* @__PURE__ */ new Set();
24204
24519
  const starExports = [];
24205
24520
  const namedReexports = [];
24521
+ const exportAliases = [];
24522
+ const propertyAliases = [];
24523
+ const returnTypes = /* @__PURE__ */ new Map();
24206
24524
  const complexity = /* @__PURE__ */ new Map();
24207
24525
  const calls = /* @__PURE__ */ new Map();
24208
24526
  const bodies = /* @__PURE__ */ new Map();
@@ -24219,7 +24537,7 @@ function walkExact(ts, sourceText, fileName) {
24219
24537
  const mutableBindings = /* @__PURE__ */ new Set();
24220
24538
  const schemaConstants = /* @__PURE__ */ new Map();
24221
24539
  const derivedAliases = [];
24222
- const receiverKey = (site) => site.via ?? (site.field ? `this.${site.field}` : site.constructed ? `new ${site.constructed}` : "");
24540
+ const receiverKey = (site) => site.via ?? (site.field ? `this.${site.field}` : site.constructed ? `new ${site.constructed}` : site.returnedBy ? `${site.returnedBy}()` : site.enclosingClass ? `this:${site.enclosingClass}` : "");
24223
24541
  const siteKey = (site) => `${site.member ? "m" : "b"}:${receiverKey(site)}:${site.name}`;
24224
24542
  const addBindingNames = (name) => {
24225
24543
  if (ts.isIdentifier(name)) declared.add(name.text);
@@ -24327,6 +24645,44 @@ function walkExact(ts, sourceText, fileName) {
24327
24645
  }
24328
24646
  return void 0;
24329
24647
  };
24648
+ const enclosingClassOf = (node) => {
24649
+ let at = node.parent;
24650
+ while (at) {
24651
+ if (ts.isFunctionExpression(at) || ts.isFunctionDeclaration(at) || ts.isClassExpression(at)) return void 0;
24652
+ if ((ts.isMethodDeclaration(at) || ts.isGetAccessorDeclaration(at) || ts.isSetAccessorDeclaration(at)) && at.parent && ts.isObjectLiteralExpression(at.parent)) return void 0;
24653
+ if (ts.isClassDeclaration(at)) {
24654
+ return at.name && at.parent && ts.isSourceFile(at.parent) ? at.name.text : void 0;
24655
+ }
24656
+ at = at.parent;
24657
+ }
24658
+ return void 0;
24659
+ };
24660
+ const returnedTypeOf = (fn) => {
24661
+ const annotated = fn.type;
24662
+ if (annotated) {
24663
+ if (!ts.isTypeReferenceNode(annotated)) return void 0;
24664
+ if (ts.isIdentifier(annotated.typeName) && annotated.typeName.text === "Promise" && annotated.typeArguments?.length === 1) {
24665
+ const inner2 = annotated.typeArguments[0];
24666
+ return ts.isTypeReferenceNode(inner2) && !inner2.typeArguments ? typeReferenceName(inner2) : void 0;
24667
+ }
24668
+ return annotated.typeArguments ? void 0 : typeReferenceName(annotated);
24669
+ }
24670
+ const constructedClass = (expression) => {
24671
+ let node = expression;
24672
+ while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node))) node = node.expression;
24673
+ return node && ts.isNewExpression(node) && ts.isIdentifier(node.expression) ? node.expression.text : void 0;
24674
+ };
24675
+ if (!fn.body) return void 0;
24676
+ if (!ts.isBlock(fn.body)) return constructedClass(fn.body);
24677
+ const returns = [];
24678
+ const collect2 = (node) => {
24679
+ if (ts.isFunctionLike(node)) return;
24680
+ if (ts.isReturnStatement(node)) returns.push(node);
24681
+ ts.forEachChild(node, collect2);
24682
+ };
24683
+ ts.forEachChild(fn.body, collect2);
24684
+ return returns.length === 1 ? constructedClass(returns[0].expression) : void 0;
24685
+ };
24330
24686
  const isTopLevelVariable = (decl) => ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name) && !!decl.parent && ts.isVariableDeclarationList(decl.parent) && !!decl.parent.parent && ts.isVariableStatement(decl.parent.parent) && !!decl.parent.parent.parent && ts.isSourceFile(decl.parent.parent.parent);
24331
24687
  const containerOf = (owner) => {
24332
24688
  if (!owner) return void 0;
@@ -24388,6 +24744,11 @@ function walkExact(ts, sourceText, fileName) {
24388
24744
  addSite({ name, member: true, field: receiver.name.text });
24389
24745
  } else if (ts.isNewExpression(receiver) && ts.isIdentifier(receiver.expression)) {
24390
24746
  addSite({ name, member: true, constructed: receiver.expression.text });
24747
+ } else if (ts.isCallExpression(receiver) && ts.isIdentifier(receiver.expression)) {
24748
+ addSite({ name, member: true, returnedBy: receiver.expression.text });
24749
+ } else if (receiver.kind === ts.SyntaxKind.ThisKeyword) {
24750
+ const owner = enclosingClassOf(node);
24751
+ addSite(owner !== void 0 ? { name, member: true, enclosingClass: owner } : { name, member: true });
24391
24752
  } else addSite({ name, member: true });
24392
24753
  }
24393
24754
  }
@@ -24514,6 +24875,8 @@ function walkExact(ts, sourceText, fileName) {
24514
24875
  signatures.push(declaredParameters(node));
24515
24876
  functionParams.set(fnName, signatures);
24516
24877
  if (completesLater(node)) asyncFunctions.push(fnName);
24878
+ const returned = returnedTypeOf(node) ?? null;
24879
+ returnTypes.set(fnName, returnTypes.has(fnName) && returnTypes.get(fnName) !== returned ? null : returned);
24517
24880
  const routes = collectRoutes(node);
24518
24881
  if (routes.size > 0) {
24519
24882
  const known = functionRoutes.get(fnName) ?? /* @__PURE__ */ new Map();
@@ -24550,6 +24913,23 @@ function walkExact(ts, sourceText, fileName) {
24550
24913
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && !schemaConstants.has(node.name.text)) {
24551
24914
  schemaConstants.set(node.name.text, node.initializer);
24552
24915
  }
24916
+ if (isTopLevelVariable(node) && node.initializer) {
24917
+ let literal = node.initializer;
24918
+ while (ts.isAsExpression(literal) || ts.isSatisfiesExpression(literal) || ts.isParenthesizedExpression(literal)) {
24919
+ literal = literal.expression;
24920
+ }
24921
+ if (ts.isObjectLiteralExpression(literal)) {
24922
+ const container = node.name.text;
24923
+ for (const property of literal.properties) {
24924
+ if (ts.isShorthandPropertyAssignment(property)) {
24925
+ propertyAliases.push({ exported: property.name.text, local: property.name.text, container });
24926
+ } else if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.initializer)) {
24927
+ const key = propertyNameText(property.name);
24928
+ if (key) propertyAliases.push({ exported: key, local: property.initializer.text, container });
24929
+ }
24930
+ }
24931
+ }
24932
+ }
24553
24933
  if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) || ts.isTypeAliasDeclaration(node) || ts.isEnumDeclaration(node) || ts.isModuleDeclaration(node)) {
24554
24934
  const name = node.name && ts.isIdentifier(node.name) ? node.name.text : void 0;
24555
24935
  if (name) {
@@ -24601,9 +24981,10 @@ function walkExact(ts, sourceText, fileName) {
24601
24981
  for (const el of node.exportClause.elements) {
24602
24982
  declared.add(el.name.text);
24603
24983
  exported.add(el.name.text);
24604
- if (spec && !node.isTypeOnly && !el.isTypeOnly) {
24605
- namedReexports.push({ exported: el.name.text, local: (el.propertyName ?? el.name).text, from: spec });
24606
- }
24984
+ if (node.isTypeOnly || el.isTypeOnly) continue;
24985
+ const local = (el.propertyName ?? el.name).text;
24986
+ if (spec) namedReexports.push({ exported: el.name.text, local, from: spec });
24987
+ else exportAliases.push({ exported: el.name.text, local });
24607
24988
  }
24608
24989
  } else if (!node.exportClause && spec) {
24609
24990
  starExports.push(spec);
@@ -24678,7 +25059,12 @@ function walkExact(ts, sourceText, fileName) {
24678
25059
  if (values && schemaConstants.has(alias.constant)) enumValues.set(alias.name, values);
24679
25060
  }
24680
25061
  const reexportOnly = sf.statements.length > 0 && sf.statements.every((st) => ts.isExportDeclaration(st) && !!st.moduleSpecifier);
24681
- return { declared, anchors, exported, imports, reexports, starExports, namedReexports, complexity, calls, bodies, importBindings, typeOnlyBindings, fieldTypes, localTypes, typeShapes, functionParams, functionRoutes, asyncFunctions, enumValues, aliasTypes, mutableBindings, reexportOnly };
25062
+ for (const alias of propertyAliases) if (exported.has(alias.container)) exportAliases.push(alias);
25063
+ return { declared, anchors, exported, imports, reexports, starExports, namedReexports, exportAliases, returnTypes, complexity, calls, bodies, importBindings, typeOnlyBindings, fieldTypes, localTypes, typeShapes, functionParams, functionRoutes, asyncFunctions, enumValues, aliasTypes, mutableBindings, reexportOnly };
25064
+ }
25065
+ function resolveModule(fromFile, specifier, packages) {
25066
+ if (!specifier.startsWith(".")) return packages.get(specifier) ?? null;
25067
+ return resolveRelativeModule(fromFile, specifier);
24682
25068
  }
24683
25069
  function resolveRelativeModule(fromFile, specifier) {
24684
25070
  if (!specifier.startsWith(".")) return null;
@@ -24701,8 +25087,8 @@ function resolveRelativeModule(fromFile, specifier) {
24701
25087
  }
24702
25088
  return null;
24703
25089
  }
24704
- function reexportTarget(ts, filePath, specifier, projectRoot2, visited, exactCache) {
24705
- const target = resolveRelativeModule(filePath, specifier);
25090
+ function reexportTarget(ts, filePath, specifier, projectRoot2, visited, exactCache, packages) {
25091
+ const target = resolveModule(filePath, specifier, packages);
24706
25092
  if (!target || path18.relative(projectRoot2, target).startsWith("..")) return null;
24707
25093
  let targetFacts = exactCache.get(target);
24708
25094
  if (targetFacts === void 0) {
@@ -24716,7 +25102,7 @@ function reexportTarget(ts, filePath, specifier, projectRoot2, visited, exactCac
24716
25102
  if (!targetFacts) return null;
24717
25103
  if (!visited.has(target)) {
24718
25104
  visited.add(target);
24719
- chaseReexports(ts, targetFacts, target, projectRoot2, visited, exactCache);
25105
+ chaseReexports(ts, targetFacts, target, projectRoot2, visited, exactCache, packages);
24720
25106
  }
24721
25107
  return { path: target, facts: targetFacts };
24722
25108
  }
@@ -24739,13 +25125,33 @@ function carryBody(facts, target, targetPath, local, exported, projectRoot2) {
24739
25125
  }
24740
25126
  facts.bodies.set(exported, carried);
24741
25127
  }
24742
- function chaseReexports(ts, facts, filePath, projectRoot2, visited, exactCache) {
25128
+ function carryAliasBody(facts, source, from, local, alias) {
25129
+ const sourceBodies = source.bodies.get(local) ?? [];
25130
+ if (!sourceBodies.length || sourceBodies.some((b) => b.nested)) return;
25131
+ const moduleScope = sourceBodies.filter((b) => b.container === void 0);
25132
+ if (!moduleScope.length) return;
25133
+ const score = source.complexity.get(local);
25134
+ if (score !== void 0) {
25135
+ facts.complexity.set(alias.exported, Math.max(facts.complexity.get(alias.exported) ?? 0, score));
25136
+ }
25137
+ const relocate2 = (site) => from !== void 0 ? { ...site, from: site.from ?? from } : site;
25138
+ const sites = facts.calls.get(alias.exported) ?? /* @__PURE__ */ new Map();
25139
+ const carried = facts.bodies.get(alias.exported) ?? [];
25140
+ for (const body of moduleScope) {
25141
+ const moved = body.sites.map(relocate2);
25142
+ for (const site of moved) sites.set(`${from ?? ""}|${alias.container ?? ""}|${JSON.stringify(site)}`, site);
25143
+ carried.push(alias.container !== void 0 ? { container: alias.container, sites: moved } : { sites: moved });
25144
+ }
25145
+ facts.calls.set(alias.exported, sites);
25146
+ facts.bodies.set(alias.exported, carried);
25147
+ }
25148
+ function chaseReexports(ts, facts, filePath, projectRoot2, visited, exactCache, packages = NO_PACKAGES) {
24743
25149
  for (const re of facts.namedReexports) {
24744
- const target = reexportTarget(ts, filePath, re.from, projectRoot2, visited, exactCache);
25150
+ const target = reexportTarget(ts, filePath, re.from, projectRoot2, visited, exactCache, packages);
24745
25151
  if (target) carryBody(facts, target.facts, target.path, re.local, re.exported, projectRoot2);
24746
25152
  }
24747
25153
  for (const spec of facts.starExports) {
24748
- const target = reexportTarget(ts, filePath, spec, projectRoot2, visited, exactCache);
25154
+ const target = reexportTarget(ts, filePath, spec, projectRoot2, visited, exactCache, packages);
24749
25155
  if (!target) continue;
24750
25156
  for (const name of target.facts.exported) {
24751
25157
  facts.exported.add(name);
@@ -24753,6 +25159,147 @@ function chaseReexports(ts, facts, filePath, projectRoot2, visited, exactCache)
24753
25159
  carryBody(facts, target.facts, target.path, name, name, projectRoot2);
24754
25160
  }
24755
25161
  }
25162
+ for (const alias of facts.exportAliases) {
25163
+ const binding = facts.importBindings.get(alias.local);
25164
+ if (binding) {
25165
+ if (binding.namespace) continue;
25166
+ const target = reexportTarget(ts, filePath, binding.from, projectRoot2, visited, exactCache, packages);
25167
+ if (!target) continue;
25168
+ const from = pathKey(path18.relative(projectRoot2, target.path));
25169
+ carryAliasBody(facts, target.facts, from, binding.imported ?? alias.local, alias);
25170
+ } else if (alias.container !== void 0 || alias.local !== alias.exported) {
25171
+ carryAliasBody(facts, facts, void 0, alias.local, alias);
25172
+ }
25173
+ }
25174
+ }
25175
+ function isOwnFile(projectRoot2, absolute) {
25176
+ const rel2 = path18.relative(projectRoot2, absolute);
25177
+ if (rel2.startsWith("..") || path18.isAbsolute(rel2)) return false;
25178
+ return !pathKey(rel2).split("/").includes("node_modules");
25179
+ }
25180
+ function isFile(absolute) {
25181
+ try {
25182
+ return fs13.statSync(absolute).isFile();
25183
+ } catch {
25184
+ return false;
25185
+ }
25186
+ }
25187
+ function readJsonFile(file, ts) {
25188
+ let text3;
25189
+ try {
25190
+ text3 = fs13.readFileSync(file, "utf8");
25191
+ } catch {
25192
+ return void 0;
25193
+ }
25194
+ try {
25195
+ return JSON.parse(text3);
25196
+ } catch {
25197
+ if (!ts) return void 0;
25198
+ const parsed = ts.parseConfigFileTextToJson(file, text3);
25199
+ return parsed.error ? void 0 : parsed.config;
25200
+ }
25201
+ }
25202
+ function conditionTargets(value) {
25203
+ if (typeof value === "string") return [value];
25204
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [];
25205
+ const record2 = value;
25206
+ const keys = [...ENTRY_CONDITIONS.filter((k) => k in record2), ...Object.keys(record2).filter((k) => !ENTRY_CONDITIONS.includes(k))];
25207
+ return keys.flatMap((k) => conditionTargets(record2[k]));
25208
+ }
25209
+ function packageEntries(pkg) {
25210
+ const entries = /* @__PURE__ */ new Map();
25211
+ const exportsField = pkg.exports;
25212
+ if (exportsField !== void 0 && exportsField !== null) {
25213
+ const isSubpathMap = typeof exportsField === "object" && !Array.isArray(exportsField) && Object.keys(exportsField).some((k) => k.startsWith("."));
25214
+ if (isSubpathMap) {
25215
+ for (const [subpath, value] of Object.entries(exportsField)) {
25216
+ if (!subpath.startsWith(".") || subpath.includes("*")) continue;
25217
+ const targets = conditionTargets(value);
25218
+ if (targets.length) entries.set(subpath, targets);
25219
+ }
25220
+ } else {
25221
+ const targets = conditionTargets(exportsField);
25222
+ if (targets.length) entries.set(".", targets);
25223
+ }
25224
+ return entries;
25225
+ }
25226
+ const fallbacks = ["module", "main", "types"].map((k) => pkg[k]).filter((v) => typeof v === "string");
25227
+ if (fallbacks.length) entries.set(".", fallbacks);
25228
+ return entries;
25229
+ }
25230
+ function entrySource(packageDir, target, compiler, projectRoot2) {
25231
+ const absolute = path18.resolve(packageDir, target);
25232
+ if (!isOwnFile(projectRoot2, absolute)) return null;
25233
+ const isDeclaration = /\.d\.[cm]?ts$/.test(absolute);
25234
+ if (!isDeclaration && languageOfSourcePath(absolute) && isFile(absolute) && !(compiler.outDir && !path18.relative(compiler.outDir, absolute).startsWith(".."))) {
25235
+ return absolute;
25236
+ }
25237
+ if (!compiler.outDir || !compiler.rootDir) return null;
25238
+ const inOut = path18.relative(compiler.outDir, absolute);
25239
+ if (inOut.startsWith("..") || path18.isAbsolute(inOut)) return null;
25240
+ const extension = BUILD_EXTENSIONS.find((ext) => inOut.endsWith(ext));
25241
+ const stem2 = extension ? inOut.slice(0, -extension.length) : inOut;
25242
+ for (const ext of SOURCE_EXTENSIONS) {
25243
+ const candidate = path18.join(compiler.rootDir, stem2 + ext);
25244
+ if (isOwnFile(projectRoot2, candidate) && isFile(candidate)) return candidate;
25245
+ }
25246
+ return null;
25247
+ }
25248
+ function readLocalPackages(projectRoot2) {
25249
+ const out = /* @__PURE__ */ new Map();
25250
+ const ts = resolveTypeScript(projectRoot2);
25251
+ const root = readJsonFile(path18.join(projectRoot2, "package.json"), null);
25252
+ if (!root || typeof root !== "object" || Array.isArray(root)) return out;
25253
+ const rootPkg = root;
25254
+ const declared = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces && typeof rootPkg.workspaces === "object" && Array.isArray(rootPkg.workspaces.packages) ? rootPkg.workspaces.packages : [];
25255
+ const dirs = [projectRoot2];
25256
+ for (const entry of declared) {
25257
+ if (typeof entry !== "string") continue;
25258
+ const key = pathKey(entry).replace(/\/+$/, "");
25259
+ if (!key || path18.isAbsolute(key)) continue;
25260
+ if (key.endsWith("/*")) {
25261
+ const base = path18.resolve(projectRoot2, key.slice(0, -2));
25262
+ if (base.includes("*") || !isOwnFile(projectRoot2, base)) continue;
25263
+ let children;
25264
+ try {
25265
+ children = fs13.readdirSync(base, { withFileTypes: true });
25266
+ } catch {
25267
+ continue;
25268
+ }
25269
+ for (const child of children.slice().sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
25270
+ if (child.isDirectory() && child.name !== "node_modules" && !child.name.startsWith(".")) {
25271
+ dirs.push(path18.join(base, child.name));
25272
+ }
25273
+ }
25274
+ } else if (!key.includes("*")) {
25275
+ const dir = path18.resolve(projectRoot2, key);
25276
+ if (isOwnFile(projectRoot2, dir)) dirs.push(dir);
25277
+ }
25278
+ }
25279
+ for (const dir of dirs) {
25280
+ const pkg = dir === projectRoot2 ? rootPkg : readJsonFile(path18.join(dir, "package.json"), null);
25281
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) continue;
25282
+ const name = pkg.name;
25283
+ if (typeof name !== "string" || !name) continue;
25284
+ const tsconfig = readJsonFile(path18.join(dir, "tsconfig.json"), ts);
25285
+ const options = tsconfig?.compilerOptions ?? {};
25286
+ const compiler = {
25287
+ outDir: typeof options.outDir === "string" ? path18.resolve(dir, options.outDir) : void 0,
25288
+ rootDir: typeof options.rootDir === "string" ? path18.resolve(dir, options.rootDir) : void 0
25289
+ };
25290
+ for (const [subpath, targets] of packageEntries(pkg)) {
25291
+ const specifier = subpath === "." ? name : `${name}/${subpath.replace(/^\.\//, "")}`;
25292
+ if (out.has(specifier)) continue;
25293
+ for (const target of targets) {
25294
+ const source = entrySource(dir, target, compiler, projectRoot2);
25295
+ if (source) {
25296
+ out.set(specifier, source);
25297
+ break;
25298
+ }
25299
+ }
25300
+ }
25301
+ }
25302
+ return out;
24756
25303
  }
24757
25304
  function looksBinary(buffer) {
24758
25305
  const probe2 = buffer.subarray(0, Math.min(buffer.length, 4096));
@@ -24810,6 +25357,9 @@ function buildCodeModel(implementations, types, projectRoot2, sourceRoots = [],
24810
25357
  const seen = /* @__PURE__ */ new Set();
24811
25358
  const exactCache = /* @__PURE__ */ new Map();
24812
25359
  const rootFiles = walkDeclaredRoots(sourceRoots, exclude, projectRoot2);
25360
+ const localPackages = readLocalPackages(projectRoot2);
25361
+ const packages = {};
25362
+ for (const [specifier, absolute] of localPackages) packages[specifier] = pathKey(path18.relative(projectRoot2, absolute));
24813
25363
  const declaredPaths = [];
24814
25364
  for (const impl of implementations) {
24815
25365
  declaredPaths.push(...implementationSourceFiles(impl));
@@ -24855,7 +25405,7 @@ function buildCodeModel(implementations, types, projectRoot2, sourceRoots = [],
24855
25405
  if (ts) {
24856
25406
  try {
24857
25407
  const facts = walkExact(ts, text3, absolute);
24858
- chaseReexports(ts, facts, absolute, projectRoot2, /* @__PURE__ */ new Set([absolute]), exactCache);
25408
+ chaseReexports(ts, facts, absolute, projectRoot2, /* @__PURE__ */ new Set([absolute]), exactCache, localPackages);
24859
25409
  analyzed = {
24860
25410
  analysisGrade: "exact",
24861
25411
  declaredNames: [...facts.declared],
@@ -24867,6 +25417,10 @@ function buildCodeModel(implementations, types, projectRoot2, sourceRoots = [],
24867
25417
  ...facts.namedReexports.map(({ exported, local, from }) => ({ exported, local, from })),
24868
25418
  ...facts.starExports.map((from) => ({ exported: "*", local: "*", from }))
24869
25419
  ],
25420
+ exportAliases: facts.exportAliases.map((alias) => ({ ...alias })),
25421
+ returnTypes: Object.fromEntries(
25422
+ [...facts.returnTypes].filter((entry) => entry[1] !== null)
25423
+ ),
24870
25424
  functionComplexity: Object.fromEntries(facts.complexity),
24871
25425
  functionCallSites: Object.fromEntries([...facts.calls].map(([k, v]) => [k, [...v.values()]])),
24872
25426
  functionBodies: Object.fromEntries(facts.bodies),
@@ -24896,7 +25450,7 @@ function buildCodeModel(implementations, types, projectRoot2, sourceRoots = [],
24896
25450
  }
24897
25451
  files.push({ path: sourcePath, status: "analyzed", language, ...analyzed });
24898
25452
  }
24899
- return { files, projectRoot: projectRoot2, rootFiles };
25453
+ return { files, projectRoot: projectRoot2, rootFiles, packages };
24900
25454
  }
24901
25455
  function clauseBindings(clause, specifier) {
24902
25456
  const out = [];
@@ -25032,7 +25586,7 @@ function findTestsReferencing(methods, projectRoot2, testRoots) {
25032
25586
  }
25033
25587
  return found;
25034
25588
  }
25035
- var fs13, path18, import_module2, C_FAMILY_COMMENTS, NAMED_IMPORT_BINDINGS_RE, JS_PATTERNS, LANGUAGE_PATTERNS, PATTERN_ANALYSIS_MAX_BYTES, IDENTIFIER_RE, STRING_RE, tsResolutionCache, TS_RESOLUTION_RETRY_MS, IMPORT_FROM_RE, DYNAMIC_IMPORT_RE, IDENTIFIER_ONLY_RE, NAMED_REEXPORT_RE, STAR_REEXPORT_RE;
25589
+ var fs13, path18, import_module2, C_FAMILY_COMMENTS, NAMED_IMPORT_BINDINGS_RE, JS_PATTERNS, LANGUAGE_PATTERNS, PATTERN_ANALYSIS_MAX_BYTES, IDENTIFIER_RE, STRING_RE, tsResolutionCache, TS_RESOLUTION_RETRY_MS, NO_PACKAGES, BUILD_EXTENSIONS, SOURCE_EXTENSIONS, ENTRY_CONDITIONS, IMPORT_FROM_RE, DYNAMIC_IMPORT_RE, IDENTIFIER_ONLY_RE, NAMED_REEXPORT_RE, STAR_REEXPORT_RE;
25036
25590
  var init_source_analysis = __esm({
25037
25591
  "src/core/source-analysis.ts"() {
25038
25592
  "use strict";
@@ -25141,6 +25695,10 @@ var init_source_analysis = __esm({
25141
25695
  STRING_RE = /'([^'\\\n]*(?:\\.[^'\\\n]*)*)'|"([^"\\\n]*(?:\\.[^"\\\n]*)*)"|`([^`\\]*(?:\\.[^`\\]*)*)`/g;
25142
25696
  tsResolutionCache = /* @__PURE__ */ new Map();
25143
25697
  TS_RESOLUTION_RETRY_MS = 3e4;
25698
+ NO_PACKAGES = /* @__PURE__ */ new Map();
25699
+ BUILD_EXTENSIONS = [".d.mts", ".d.cts", ".d.ts", ".mjs", ".cjs", ".jsx", ".js"];
25700
+ SOURCE_EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
25701
+ ENTRY_CONDITIONS = ["types", "import", "require", "default"];
25144
25702
  IMPORT_FROM_RE = /(?:^|;)[ \t]*import\s+(?!\()([^'";]*?)\s*from\s*['"]([^'"]+)['"]/gm;
25145
25703
  DYNAMIC_IMPORT_RE = /\{([^{}]*)\}\s*=\s*await\s+import\(\s*['"]([^'"]+)['"]\s*\)/g;
25146
25704
  IDENTIFIER_ONLY_RE = /^[A-Za-z_$][\w$]*$/;
@@ -27006,8 +27564,8 @@ function generateSequenceDiagram(componentId, methodName, options) {
27006
27564
  const steps = [...methodImpl.narrative].sort((a, b) => a.stepNumber - b.stepNumber);
27007
27565
  for (const step of steps) {
27008
27566
  for (const par of parallelArms) {
27009
- const sep18 = par.sepByStep.get(step.stepNumber);
27010
- if (sep18 !== void 0) lines.push(` and ${escapeLabel(sep18)}`);
27567
+ const sep19 = par.sepByStep.get(step.stepNumber);
27568
+ if (sep19 !== void 0) lines.push(` and ${escapeLabel(sep19)}`);
27011
27569
  }
27012
27570
  switch (step.type) {
27013
27571
  case "local":
@@ -27785,6 +28343,15 @@ function classifySource(world, sourceId, label7, sources) {
27785
28343
  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`
27786
28344
  };
27787
28345
  }
28346
+ function duplicateDetail(entries, index) {
28347
+ const e = entries[index];
28348
+ if (e.from !== void 0 || e.component !== void 0 || e.interface !== void 0 || e.typeDef !== void 0) return null;
28349
+ const name = e.as ?? e.id;
28350
+ if (name === void 0) return null;
28351
+ const other = entries.findIndex((x, i) => i !== index && (x.as ?? x.id) === name);
28352
+ if (other === -1) return null;
28353
+ 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`;
28354
+ }
27788
28355
  function bindOwnType(world, owner, e, problems) {
27789
28356
  const type = (world.types.get(e.typeDef) ?? []).find((t) => !t.subsystem);
27790
28357
  if (!type) {
@@ -27856,7 +28423,8 @@ function resolveProjectTable(system, subsystems, components, interfaces, types,
27856
28423
  const owner = sources.namespace ? sources.namespace : system.name;
27857
28424
  const problems = [];
27858
28425
  const candidates = [];
27859
- (system.publicInterfaces ?? []).forEach((e, index) => {
28426
+ const entries = system.publicInterfaces ?? [];
28427
+ entries.forEach((e, index) => {
27860
28428
  const label7 = e.as ?? e.id ?? e.interface ?? e.component ?? e.typeDef ?? `#${index + 1}`;
27861
28429
  const source = classifySource(world, sourceOf(world, e), label7, sources);
27862
28430
  switch (source.kind) {
@@ -27882,7 +28450,7 @@ function resolveProjectTable(system, subsystems, components, interfaces, types,
27882
28450
  return;
27883
28451
  }
27884
28452
  default:
27885
- problems.push({ kind: "invalid", owner, publicName: label7, detail: source.detail });
28453
+ problems.push({ kind: "invalid", owner, publicName: label7, detail: duplicateDetail(entries, index) ?? source.detail });
27886
28454
  }
27887
28455
  });
27888
28456
  return settle(owner, "project", candidates, problems);
@@ -28479,6 +29047,16 @@ function qualifiedTypeNames(typeStr) {
28479
29047
  }
28480
29048
  return out;
28481
29049
  }
29050
+ function invariantTypeRef(ref) {
29051
+ const at = ref.lastIndexOf(".");
29052
+ return at > 0 && at < ref.length - 1 ? ref.slice(0, at) : void 0;
29053
+ }
29054
+ function mapInvariantRef(ref, map) {
29055
+ const typeRef2 = invariantTypeRef(ref);
29056
+ if (typeRef2 === void 0 || !typeRef2.includes("::")) return ref;
29057
+ const next = map("type", typeRef2);
29058
+ return next === typeRef2 ? ref : `${next}${ref.slice(typeRef2.length)}`;
29059
+ }
28482
29060
  function rawReferences(kind, spec) {
28483
29061
  const out = [];
28484
29062
  const add2 = (position, value) => {
@@ -28501,6 +29079,7 @@ function rawReferences(kind, spec) {
28501
29079
  for (const step of m.narrative) {
28502
29080
  const source = step.auth?.from;
28503
29081
  if (typeof source === "string" && source.startsWith(COMPONENT_AUTH_SOURCE)) add2("auth", source.slice(COMPONENT_AUTH_SOURCE.length));
29082
+ for (const ref of step.assertsInvariants ?? []) add2("type", invariantTypeRef(ref));
28504
29083
  }
28505
29084
  }
28506
29085
  break;
@@ -28619,6 +29198,9 @@ function respellStoredReferences(kind, doc, map) {
28619
29198
  for (const m of list2(doc.methods)) {
28620
29199
  for (const step of list2(m.narrative)) {
28621
29200
  at(step, "targetComponent", "narrative");
29201
+ if (Array.isArray(step.assertsInvariants)) {
29202
+ step.assertsInvariants = step.assertsInvariants.map((ref) => typeof ref === "string" ? mapInvariantRef(ref, map) : ref);
29203
+ }
28622
29204
  const auth = step.auth;
28623
29205
  if (auth && typeof auth.from === "string" && auth.from.startsWith(COMPONENT_AUTH_SOURCE)) {
28624
29206
  auth.from = `${COMPONENT_AUTH_SOURCE}${map("auth", auth.from.slice(COMPONENT_AUTH_SOURCE.length))}`;
@@ -28678,7 +29260,8 @@ function mapRawReferences(kind, spec, map) {
28678
29260
  return call && next !== call.compId ? `${next}.${call.methodName}` : entry;
28679
29261
  })
28680
29262
  } : {},
28681
- narrative: m.narrative.map((step) => {
29263
+ narrative: m.narrative.map((authored) => {
29264
+ const step = authored.assertsInvariants ? { ...authored, assertsInvariants: authored.assertsInvariants.map((ref) => mapInvariantRef(ref, map)) } : authored;
28682
29265
  const source = step.auth?.from;
28683
29266
  if (typeof source !== "string" || !source.startsWith(COMPONENT_AUTH_SOURCE)) return step;
28684
29267
  const auth = step.auth;
@@ -28779,7 +29362,7 @@ function resolveFamilyTables(raws) {
28779
29362
  const byKey3 = new Map(raws.map((r) => [r.record.namespace, r]));
28780
29363
  const done = /* @__PURE__ */ new Set();
28781
29364
  const resolving = /* @__PURE__ */ new Set();
28782
- const resolve55 = (raw) => {
29365
+ const resolve56 = (raw) => {
28783
29366
  const key = raw.record.namespace;
28784
29367
  if (done.has(key)) return raw.record.exports;
28785
29368
  if (resolving.has(key)) return null;
@@ -28794,7 +29377,7 @@ function resolveFamilyTables(raws) {
28794
29377
  const producerKey = source !== void 0 ? raw.record.aliases.get(source) : void 0;
28795
29378
  if (producerKey === void 0 || producerTables.has(producerKey)) continue;
28796
29379
  const producer = byKey3.get(producerKey);
28797
- producerTables.set(producerKey, producer ? resolve55(producer) : null);
29380
+ producerTables.set(producerKey, producer ? resolve56(producer) : null);
28798
29381
  audienceOf.set(producerKey, "project");
28799
29382
  }
28800
29383
  raw.record.subsystemExports = subsystemTables;
@@ -28810,7 +29393,7 @@ function resolveFamilyTables(raws) {
28810
29393
  resolving.delete(key);
28811
29394
  }
28812
29395
  };
28813
- raws.forEach((raw) => resolve55(raw));
29396
+ raws.forEach((raw) => resolve56(raw));
28814
29397
  }
28815
29398
  function bindAuthored(raw, raws, readables, authored) {
28816
29399
  const key = raw.record.namespace;
@@ -31245,12 +31828,12 @@ var init_specs2 = __esm({
31245
31828
  return this.relativizeSpec("interface", stored);
31246
31829
  }
31247
31830
  prepareImplementationForWrite(spec, carry = true) {
31248
- const relative26 = this.relativizeSpec("implementation", spec, carry);
31831
+ const relative28 = this.relativizeSpec("implementation", spec, carry);
31249
31832
  const member = this.getSubprojectPrefix(spec.id);
31250
31833
  const memberRoot = member ? this.resolveSubprojectForNamespace(member) : null;
31251
- if (memberRoot) return childRelativeFilePaths(relative26, this.rootDir, memberRoot);
31834
+ if (memberRoot) return childRelativeFilePaths(relative28, this.rootDir, memberRoot);
31252
31835
  const partDir = this.partHolding(this.getImplementationPath(spec.id, spec.contract))?.part.directory;
31253
- return partDir ? partRelativeFilePaths(relative26, this.rootDir, partDir) : relative26;
31836
+ return partDir ? partRelativeFilePaths(relative28, this.rootDir, partDir) : relative28;
31254
31837
  }
31255
31838
  /**
31256
31839
  * A type written in its stored form — every type position canonical (one
@@ -33407,7 +33990,7 @@ function resolveGit(external, config) {
33407
33990
  const declared = gitDeclarationOf(config, external);
33408
33991
  if (!declared) return { ...external, problem: "git producer unavailable: its declaration names no repository" };
33409
33992
  try {
33410
- const commit3 = external.role === "member" && declared.commit ? declared.commit : resolve7(declared.url, declared.ref);
33993
+ const commit3 = external.role === "member" && declared.commit ? declared.commit : resolve3(declared.url, declared.ref);
33411
33994
  const directory = fetch2(declared.url, commit3, declared.dir);
33412
33995
  return { ...external, directory: path23.resolve(directory), commit: commit3, problem: void 0 };
33413
33996
  } catch (e) {
@@ -33858,7 +34441,7 @@ function scaffoldPart(alias, memberDir, contained) {
33858
34441
  }
33859
34442
  function createGitMember(alias, source, description, as) {
33860
34443
  const url = source.git;
33861
- const commit3 = source.commit ?? resolve7(url, source.ref);
34444
+ const commit3 = source.commit ?? resolve3(url, source.ref);
33862
34445
  const dir = fetch2(url, commit3, source.dir);
33863
34446
  const kind = holdsProjectContent(dir) ? "project" : "part";
33864
34447
  if (as !== void 0 && as !== kind) {
@@ -34800,7 +35383,7 @@ function advanceMember(alias, ref, commit3, dryRun) {
34800
35383
  const url = member.source.git;
34801
35384
  const from = member.source.commit ?? "";
34802
35385
  const followed = commit3 === void 0 ? ref ?? member.source.ref : void 0;
34803
- const to = commit3 ?? resolve7(url, followed);
35386
+ const to = commit3 ?? resolve3(url, followed);
34804
35387
  const after = specContents(fetch2(url, to, member.source.dir));
34805
35388
  const before = specContents(from !== "" ? fetch2(url, from, member.source.dir) : null);
34806
35389
  const added = [...after.keys()].filter((f) => !before.has(f)).sort();
@@ -36409,17 +36992,17 @@ function memberHop(agentId) {
36409
36992
  const alias = agentId.slice(0, at);
36410
36993
  const member = graph().nodes.find((n) => n.parent === "" && n.mountAlias === alias);
36411
36994
  if (!member) return null;
36412
- const relative26 = path29.relative(getProjectRoot(), member.directory).split(path29.sep).join("/");
36413
- return { alias, rest: agentId.slice(at + 2), directory: member.directory, relative: relative26 };
36995
+ const relative28 = path29.relative(getProjectRoot(), member.directory).split(path29.sep).join("/");
36996
+ return { alias, rest: agentId.slice(at + 2), directory: member.directory, relative: relative28 };
36414
36997
  }
36415
- function throughMount(brief, alias, relative26) {
36416
- const under = (p) => path29.posix.join(relative26, p);
36998
+ function throughMount(brief, alias, relative28) {
36999
+ const under = (p) => path29.posix.join(relative28, p);
36417
37000
  return {
36418
37001
  ...brief,
36419
37002
  agentId: `${alias}::${brief.agentId}`,
36420
37003
  ownedPaths: brief.ownedPaths.map(under),
36421
37004
  ...brief.readPaths ? { readPaths: brief.readPaths.map(under) } : {},
36422
- root: brief.root ? under(brief.root) : relative26
37005
+ root: brief.root ? under(brief.root) : relative28
36423
37006
  };
36424
37007
  }
36425
37008
  function loadRegistry() {
@@ -39810,8 +40393,8 @@ var require_dist = __commonJS({
39810
40393
  walkPackDir(root, path93.join(current2, entry.name), out);
39811
40394
  } else if (entry.isFile()) {
39812
40395
  const absolute = path93.join(current2, entry.name);
39813
- const relative26 = path93.relative(root, absolute).split(path93.sep).join("/");
39814
- out.push({ path: relative26, contents: fs69.readFileSync(absolute) });
40396
+ const relative28 = path93.relative(root, absolute).split(path93.sep).join("/");
40397
+ out.push({ path: relative28, contents: fs69.readFileSync(absolute) });
39815
40398
  }
39816
40399
  }
39817
40400
  }
@@ -40011,8 +40594,8 @@ var require_dist = __commonJS({
40011
40594
  return normalized === "." ? ".wai/" : `${normalized}/.wai/`;
40012
40595
  }
40013
40596
  function normalizeRootPath(relativePath) {
40014
- const posix6 = relativePath.split(path210.sep).join("/").replace(/^\.\//, "").replace(/\/+$/, "");
40015
- return posix6 === "" || posix6 === "." ? "." : posix6;
40597
+ const posix8 = relativePath.split(path210.sep).join("/").replace(/^\.\//, "").replace(/\/+$/, "");
40598
+ return posix8 === "" || posix8 === "." ? "." : posix8;
40016
40599
  }
40017
40600
  function slugify2(projectName) {
40018
40601
  const slug2 = projectName.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -40118,19 +40701,19 @@ function commitStagedTree(stagingDir, destDir, roots, backupExisting) {
40118
40701
  const backupDir = path34.join(root, BACKUP_DIR, timestampSlug());
40119
40702
  let backedUp = false;
40120
40703
  for (const rootRel of roots) {
40121
- const relative26 = rootRel === "." ? "" : rootRel;
40122
- const stagedSpecDir = path34.join(stagingDir, relative26, ".wai");
40704
+ const relative28 = rootRel === "." ? "" : rootRel;
40705
+ const stagedSpecDir = path34.join(stagingDir, relative28, ".wai");
40123
40706
  if (!fs20.existsSync(stagedSpecDir)) continue;
40124
- const liveSpecDir = liveSpecDirFor(path34.join(root, relative26));
40707
+ const liveSpecDir = liveSpecDirFor(path34.join(root, relative28));
40125
40708
  if (backupExisting && liveSpecDir && fs20.existsSync(liveSpecDir)) {
40126
- const backupTarget = path34.join(backupDir, relative26, path34.basename(liveSpecDir));
40709
+ const backupTarget = path34.join(backupDir, relative28, path34.basename(liveSpecDir));
40127
40710
  fs20.mkdirSync(path34.dirname(backupTarget), { recursive: true });
40128
40711
  fs20.renameSync(liveSpecDir, backupTarget);
40129
40712
  backedUp = true;
40130
40713
  } else if (liveSpecDir && fs20.existsSync(liveSpecDir)) {
40131
40714
  fs20.rmSync(liveSpecDir, { recursive: true, force: true });
40132
40715
  }
40133
- const target = path34.join(root, relative26, ".wai");
40716
+ const target = path34.join(root, relative28, ".wai");
40134
40717
  fs20.mkdirSync(path34.dirname(target), { recursive: true });
40135
40718
  if (fs20.existsSync(target)) fs20.rmSync(target, { recursive: true, force: true });
40136
40719
  fs20.renameSync(stagedSpecDir, target);
@@ -40445,7 +41028,7 @@ function repairTypeSpellings(apply4) {
40445
41028
  const key = `${kind}:${specId}`;
40446
41029
  let repair = repairs.get(key);
40447
41030
  if (!repair) {
40448
- repair = { specId, kind, rewritten: [], proposals: [], authorNeeded: [] };
41031
+ repair = { specId, kind, rewritten: [], proposals: [], enumProposals: [], authorNeeded: [] };
40449
41032
  repairs.set(key, repair);
40450
41033
  }
40451
41034
  return repair;
@@ -40460,7 +41043,9 @@ function repairTypeSpellings(apply4) {
40460
41043
  if (problem.specId === void 0 || problem.kind === void 0 || !own(problem.specId)) continue;
40461
41044
  const repair = repairOf(problem.specId, problem.kind);
40462
41045
  const proposal = typeProblemIntProposal(problem);
41046
+ const enumProposal = proposal ? null : typeProblemEnumProposal(problem);
40463
41047
  if (proposal) repair.proposals.push(proposal);
41048
+ else if (enumProposal) repair.enumProposals.push(enumProposal);
40464
41049
  else repair.authorNeeded.push(problem);
40465
41050
  }
40466
41051
  const planned = [...repairs.values()];
@@ -40670,7 +41255,7 @@ var init_approval = __esm({
40670
41255
  init_lockfile();
40671
41256
  init_lock();
40672
41257
  init_project_family();
40673
- GATE_UPGRADE_NOTE = "the gate identity gained inputs in stage 5 (members' composition subjects, `composition`; code conformance moved beside the claim)";
41258
+ GATE_UPGRADE_NOTE = "the gate identity gained inputs in this release (members' composition subjects, `composition`; code conformance moved beside the claim)";
40674
41259
  }
40675
41260
  });
40676
41261
 
@@ -40849,16 +41434,16 @@ var init_ai_guide = __esm({
40849
41434
  If \`.wai/specs/\` exists, the wairon SDD workflow is active; otherwise ignore it. wairon does not orchestrate sessions \u2014 it equips yours.
40850
41435
 
40851
41436
  ### In SDD Projects:
40852
- - **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\`).
41437
+ - **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.
40853
41438
  - **Validation**: Conformance checks (stereotype rules, cycle checks, reference integrity) are run via the \`sdd_validate_tree\` MCP tool.
40854
41439
  - **Operating Rules**:
40855
41440
  1. **Skills**: Use \`sdd-architect\` to design (and \`sdd-implement\`, \`sdd-narrative\`, \`sdd-auditor\`). Refer to project's local guide file for detailed constraints.
40856
41441
  2. **MCP Tools Only**: Author/validate specs *only* via \`sdd_*\` tools (e.g. \`sdd_initialize_system\`, \`sdd_validate_tree\`).
40857
41442
  3. **No CLI Exec**: Do not run the \`wairon\` CLI (human tool). Use MCP tools \`sdd_validate_tree\` and \`sdd_get_status\` instead.
40858
41443
  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>\`).
40859
- 5. **Design First**: Complete spec and pass \`sdd_validate_tree\` before writing code.
41444
+ 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.
40860
41445
  6. **Consistency**: Code must match L3 interfaces and L5 narratives exactly. If the spec is wrong, stop and update the spec.
40861
- 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.`;
41446
+ 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.`;
40862
41447
  LOCAL_GUIDE_BODY = `## Wairon \u2014 Spec-Driven Development (you are operating inside it)
40863
41448
 
40864
41449
  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.
@@ -40875,7 +41460,8 @@ This project uses **wairon**. System specs live under \`.wai/specs/\` (L0 System
40875
41460
  - **\`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\`).
40876
41461
  - **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\`).
40877
41462
  - **Do not run the \`wairon\` CLI**: Use \`sdd_validate_tree\` and \`sdd_get_status\` instead of CLI commands.
40878
- - **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.
41463
+ - **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.
41464
+ - **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\`.
40879
41465
  - **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.
40880
41466
 
40881
41467
  ### Rules (enforced by \`sdd_validate_tree\`)
@@ -41255,7 +41841,7 @@ function getStatusReport(options = {}, decor) {
41255
41841
  }
41256
41842
  const own = options.approvals?.find((a) => a.key === "");
41257
41843
  const hasMembers = (options.approvals ?? []).some((a) => a.key !== "" && a.as !== "part");
41258
- 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)" : ""}
41844
+ 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)" : ""}
41259
41845
  `;
41260
41846
  return { text: output, failed: false };
41261
41847
  }
@@ -42921,7 +43507,7 @@ var init_position_migration = __esm({
42921
43507
  auth: ["implementation"],
42922
43508
  subsystem: ["component", "type"],
42923
43509
  group: ["type"],
42924
- type: ["interface", "type"]
43510
+ type: ["interface", "type", "implementation"]
42925
43511
  };
42926
43512
  BOUND = /* @__PURE__ */ new Set(["publicInterfaces", "lifecycle", "subsystem", "owns", "dependsOn", "dispatch", "mounts", "contract", "narrative", "group"]);
42927
43513
  compare2 = (a, b) => a < b ? -1 : a > b ? 1 : 0;
@@ -43882,7 +44468,7 @@ function attachEdits(plan7, family, bound2, dir, alias, bindingConfig) {
43882
44468
  edit(plan7, key, "id", `id: ${id} (declared${approved !== void 0 ? ", the id its lock approved" : ""})`, { root: dir, call: "setId", args: [id] });
43883
44469
  }
43884
44470
  if (!attached) plan7.notes.push(`${dir} has no project.yaml of its own \u2014 \`wairon doctor --fix\` writes one declaring "${alias}" as its id`);
43885
- const relPath = posix3(path45.relative(familyNode(family, bound2).directory, dir));
44471
+ const relPath = posix5(path45.relative(familyNode(family, bound2).directory, dir));
43886
44472
  const declaration2 = { path: relPath, ...plan7.request.description !== void 0 ? { description: plan7.request.description } : {} };
43887
44473
  edit(plan7, bound2, "member", `members: ${alias} \u2192 ${relPath} (an existing project, kept as it is)`, { root: familyNode(family, bound2).directory, call: "declareMember", args: [alias, declaration2] });
43888
44474
  for (const requirement of requiredPolicies(bindingConfig)) {
@@ -43929,8 +44515,8 @@ function planDetach(family, bound2, request) {
43929
44515
  if (plan7.request.widen) planWidening(plan7, member, narrow);
43930
44516
  if (plan7.refusals.length > 0) return plan7;
43931
44517
  const boundDir = familyNode(family, bound2).directory;
43932
- const held = memberDeclarationOf(configAt3(boundDir)?.members?.[alias] ?? posix3(path45.relative(boundDir, member.directory)));
43933
- const declared = { ...held, path: memberLocationOf(held) ?? posix3(path45.relative(boundDir, member.directory)) };
44518
+ const held = memberDeclarationOf(configAt3(boundDir)?.members?.[alias] ?? posix5(path45.relative(boundDir, member.directory)));
44519
+ const declared = { ...held, path: memberLocationOf(held) ?? posix5(path45.relative(boundDir, member.directory)) };
43934
44520
  detachEdits(plan7, family, bound2, member, alias, declared, consumers, outward);
43935
44521
  return plan7;
43936
44522
  }
@@ -43970,7 +44556,7 @@ function planWidening(plan7, member, narrow) {
43970
44556
  function detachEdits(plan7, family, bound2, member, alias, declared, consumers, outward) {
43971
44557
  const boundDir = familyNode(family, bound2).directory;
43972
44558
  const relocation = plan7.request.relocation;
43973
- const sourceTo = (from, producer) => relocation ? { hosted: producer.id ?? producer.mountAlias ?? producer.namespace } : { path: posix3(path45.relative(from, producer.directory)) };
44559
+ const sourceTo = (from, producer) => relocation ? { hosted: producer.id ?? producer.mountAlias ?? producer.namespace } : { path: posix5(path45.relative(from, producer.directory)) };
43974
44560
  const where = (source) => source.hosted !== void 0 ? `hosted record ${source.hosted}` : source.path;
43975
44561
  edit(plan7, bound2, "member", `members: ${alias} removed (${declared.path} ${relocation ? `moves to ${relocation.to}` : "stays as it is"})`, { root: boundDir, call: "removeMember", args: [alias] });
43976
44562
  const boundSource = relocation ? sourceTo(boundDir, member) : { path: declared.path };
@@ -44044,7 +44630,7 @@ function adoptEdits(plan7, family, bound2, alias, external, dir, at = dir) {
44044
44630
  edit(plan7, bound2, "pin", `pin ${alias} removed (the family supplies it now)`, { root: boundDir, call: "unpin", args: [alias] });
44045
44631
  edit(plan7, bound2, "external", `externals: ${alias} removed`, { root: boundDir, call: "removeExternal", args: [alias] });
44046
44632
  const declaration2 = {
44047
- path: posix3(path45.relative(boundDir, at)),
44633
+ path: posix5(path45.relative(boundDir, at)),
44048
44634
  ...external.description !== void 0 ? { description: external.description } : {},
44049
44635
  ...external.use && external.use.length > 0 ? { use: external.use } : {}
44050
44636
  };
@@ -44117,7 +44703,7 @@ function writeOne(rehearsal, w) {
44117
44703
  }
44118
44704
  });
44119
44705
  }
44120
- var fs26, path45, posix3, refuse, edit, label4;
44706
+ var fs26, path45, posix5, refuse, edit, label4;
44121
44707
  var init_membership = __esm({
44122
44708
  "src/migrations/membership.ts"() {
44123
44709
  "use strict";
@@ -44132,7 +44718,7 @@ var init_membership = __esm({
44132
44718
  init_project_family();
44133
44719
  init_specs();
44134
44720
  init_types2();
44135
- posix3 = (p) => p.split(path45.sep).join("/");
44721
+ posix5 = (p) => p.split(path45.sep).join("/");
44136
44722
  refuse = (plan7, code, project2, detail) => {
44137
44723
  plan7.refusals.push({ code, project: project2, detail });
44138
44724
  };
@@ -44380,7 +44966,7 @@ var init_identity = __esm({
44380
44966
  auth: ["implementation"],
44381
44967
  subsystem: ["component", "type"],
44382
44968
  group: ["type"],
44383
- type: ["interface", "type"]
44969
+ type: ["interface", "type", "implementation"]
44384
44970
  };
44385
44971
  PHASES = ["rewriteReferences", "renameAlias", "repointExternal", "renameId", "renamePin"];
44386
44972
  }
@@ -44578,7 +45164,7 @@ function respellConsumer(plan7, family, c, own) {
44578
45164
  function repointConsumer(plan7, c, boundNode2) {
44579
45165
  const declared = configAt5(c.node.directory)?.externals?.[c.alias];
44580
45166
  const project2 = boundNode2.id !== void 0 && boundNode2.id !== c.alias ? boundNode2.id : null;
44581
- const source = declared?.source ? { path: posix4(path47.relative(c.node.directory, boundNode2.directory)) } : null;
45167
+ const source = declared?.source ? { path: posix6(path47.relative(c.node.directory, boundNode2.directory)) } : null;
44582
45168
  edit3(plan7, 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, project2, source] });
44583
45169
  plan7.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.`);
44584
45170
  return c;
@@ -44614,8 +45200,8 @@ function planExternalize(family, bound2, request) {
44614
45200
  plan7,
44615
45201
  bound2,
44616
45202
  "move",
44617
- `externalize ${id}: its specs into ${joined ? `the part "${joined.alias}"` : `a part at ${posix4(path47.relative(node.directory, dir))}, declared as "${id}"`} \u2014 a storage move; no reference, export or pin changes`,
44618
- { root: node.directory, call: "externalizeSubsystem", args: [id, posix4(path47.relative(node.directory, dir))] }
45203
+ `externalize ${id}: its specs into ${joined ? `the part "${joined.alias}"` : `a part at ${posix6(path47.relative(node.directory, dir))}, declared as "${id}"`} \u2014 a storage move; no reference, export or pin changes`,
45204
+ { root: node.directory, call: "externalizeSubsystem", args: [id, posix6(path47.relative(node.directory, dir))] }
44619
45205
  );
44620
45206
  plan7.notes.push("Source code is not moved: move the subsystem's code yourself if it should live beside its specs.");
44621
45207
  return plan7;
@@ -44624,7 +45210,7 @@ function planExternalize(family, bound2, request) {
44624
45210
  crossingsBack(plan7, family, bound2, subKey);
44625
45211
  checkSiblings(plan7, family, bound2, id, subKey, table);
44626
45212
  if (plan7.refusals.length > 0) return plan7;
44627
- edit3(plan7, bound2, "move", `externalize ${id} as a project: its specs into a member at ${posix4(path47.relative(node.directory, dir))}, declared as "${id}", then promoted`, { root: node.directory, call: "externalizeSubsystem", args: [id, posix4(path47.relative(node.directory, dir)), "project"] });
45213
+ edit3(plan7, bound2, "move", `externalize ${id} as a project: its specs into a member at ${posix6(path47.relative(node.directory, dir))}, declared as "${id}", then promoted`, { root: node.directory, call: "externalizeSubsystem", args: [id, posix6(path47.relative(node.directory, dir)), "project"] });
44628
45214
  edit3(plan7, 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: [[]] });
44629
45215
  plan7.notes.push("Source code is not moved: move the subsystem's code into the member yourself.");
44630
45216
  return plan7;
@@ -44869,7 +45455,7 @@ function writeOne3(rehearsal, w) {
44869
45455
  }
44870
45456
  });
44871
45457
  }
44872
- var path47, posix4, label6, refuse3, edit3, KINDS_AT3;
45458
+ var path47, posix6, label6, refuse3, edit3, KINDS_AT3;
44873
45459
  var init_boundary = __esm({
44874
45460
  "src/migrations/boundary.ts"() {
44875
45461
  "use strict";
@@ -44881,7 +45467,7 @@ var init_boundary = __esm({
44881
45467
  init_project();
44882
45468
  init_project_family();
44883
45469
  init_types2();
44884
- posix4 = (p) => p.split(path47.sep).join("/");
45470
+ posix6 = (p) => p.split(path47.sep).join("/");
44885
45471
  label6 = (key) => key === "" ? "the top project" : `"${key}"`;
44886
45472
  refuse3 = (plan7, code, project2, detail) => {
44887
45473
  plan7.refusals.push({ code, project: project2, detail });
@@ -44903,7 +45489,7 @@ var init_boundary = __esm({
44903
45489
  auth: ["implementation"],
44904
45490
  subsystem: ["component", "type"],
44905
45491
  group: ["type"],
44906
- type: ["interface", "type"]
45492
+ type: ["interface", "type", "implementation"]
44907
45493
  };
44908
45494
  }
44909
45495
  });
@@ -45932,6 +46518,18 @@ function renderChangeReport(report4) {
45932
46518
  lines.push(...renderTestsToRevisit(report4.testsToRevisit));
45933
46519
  return lines.join("\n");
45934
46520
  }
46521
+ function specKindsHolding(id) {
46522
+ const system = loadSystemSpec();
46523
+ const held = {
46524
+ system: !!system && (id === "system" || id === system.name),
46525
+ subsystem: !!loadSubsystemSpec(id),
46526
+ component: !!loadComponentSpec(id),
46527
+ interface: !!loadInterfaceSpec(id),
46528
+ implementation: !!loadImplementationSpec(id),
46529
+ type: !!loadTypeSpec(id)
46530
+ };
46531
+ return SPEC_KINDS.filter((k) => held[k]);
46532
+ }
45935
46533
  function renderTestsToRevisit(entries) {
45936
46534
  if (!entries.length) return [];
45937
46535
  const lines = ["", "TESTS TO REVISIT:"];
@@ -46516,7 +47114,7 @@ ${renderChangeReport(report4)}`,
46516
47114
  server,
46517
47115
  "sdd_add_member",
46518
47116
  {
46519
- 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.",
47117
+ 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.",
46520
47118
  inputSchema: {
46521
47119
  alias: import_zod12.z.string().describe("The member's alias ([a-z0-9-_]+); a project's new id"),
46522
47120
  source: import_zod12.z.string().describe("A contained path, a `../` sibling path, or a git URL (optionally `#<commit>`)"),
@@ -46586,7 +47184,7 @@ ${renderChangeReport(report4)}`,
46586
47184
  server,
46587
47185
  "sdd_externalize_subsystem",
46588
47186
  {
46589
- 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.",
47187
+ 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.",
46590
47188
  inputSchema: {
46591
47189
  subsystem: import_zod12.z.string().describe("The subsystem to move out"),
46592
47190
  path: import_zod12.z.string().describe("The part's root, relative to the bound project (a new directory, or an existing part's)"),
@@ -46600,7 +47198,7 @@ ${renderChangeReport(report4)}`,
46600
47198
  server,
46601
47199
  "sdd_promote_member",
46602
47200
  {
46603
- 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.",
47201
+ 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.",
46604
47202
  inputSchema: {
46605
47203
  alias: import_zod12.z.string().describe("The part's alias"),
46606
47204
  id: import_zod12.z.string().optional().describe("The new project's id (default: the alias)"),
@@ -46613,7 +47211,7 @@ ${renderChangeReport(report4)}`,
46613
47211
  server,
46614
47212
  "sdd_demote_member",
46615
47213
  {
46616
- 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.",
47214
+ 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.",
46617
47215
  inputSchema: {
46618
47216
  alias: import_zod12.z.string().describe("The project member's alias"),
46619
47217
  destination: import_zod12.z.object({
@@ -46956,7 +47554,7 @@ ${renderChangeReport(report4)}`,
46956
47554
  interface: import_zod12.z.string().describe('The L3 interface ID (e.g. "ibilling-gateway")'),
46957
47555
  endpoints: import_zod12.z.array(import_zod12.z.object({
46958
47556
  method: import_zod12.z.string().describe("The NAME of the interface method to bind (not the HTTP verb)"),
46959
- transport: import_zod12.z.enum(["HTTP", "gRPC", "GraphQL", "MessageBus", "NamedPipe", "IPC", "CLI", "Custom"]).describe("Wire protocol; must match the Portal's portalType"),
47557
+ 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"),
46960
47558
  httpMethod: import_zod12.z.enum(["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]).optional().describe("HTTP: verb"),
46961
47559
  path: import_zod12.z.string().optional().describe('HTTP: route path, e.g. "/v1/checkout"'),
46962
47560
  service: import_zod12.z.string().optional().describe("gRPC: service name"),
@@ -46979,8 +47577,10 @@ ${renderChangeReport(report4)}`,
46979
47577
  try {
46980
47578
  const intf = loadInterfaceSpec(interfaceId);
46981
47579
  if (!intf) return errText(`Interface "${interfaceId}" does not exist.`);
46982
- const buildRaw = (e) => {
46983
- switch (e.transport) {
47580
+ const owner = intf.component ? loadComponentSpec(intf.component) : null;
47581
+ const implied = owner?.componentType === "Portal" && owner.portalType ? PORTAL_TYPE_TRANSPORT[owner.portalType] : void 0;
47582
+ const buildRaw = (e, transport) => {
47583
+ switch (transport) {
46984
47584
  case "HTTP":
46985
47585
  return { transport: "HTTP", method: e.httpMethod, path: e.path };
46986
47586
  case "gRPC":
@@ -47004,13 +47604,19 @@ ${renderChangeReport(report4)}`,
47004
47604
  for (const e of endpoints) {
47005
47605
  const m = intf.methods.find((x) => x.name === e.method);
47006
47606
  if (!m) return errText(`Method "${e.method}" not found on interface "${interfaceId}". Define it via sdd_define_interface first.`);
47007
- const parsed = EndpointSchema.safeParse(buildRaw(e));
47607
+ const transport = e.transport === "HTTP_API" ? "HTTP" : e.transport;
47608
+ if (implied && transport !== implied) {
47609
+ return errText(
47610
+ `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.`
47611
+ );
47612
+ }
47613
+ const parsed = EndpointSchema.safeParse(buildRaw(e, transport));
47008
47614
  if (!parsed.success) {
47009
47615
  const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
47010
- return errText(`Invalid ${e.transport} endpoint for method "${e.method}": ${detail}`);
47616
+ return errText(`Invalid ${transport} endpoint for method "${e.method}": ${detail}`);
47011
47617
  }
47012
47618
  methods.push({ name: e.method, endpoint: parsed.data });
47013
- bound2.push(`${e.method}\u2192${e.transport}`);
47619
+ bound2.push(`${e.method}\u2192${transport}`);
47014
47620
  }
47015
47621
  const report4 = updateSpecGated("interface", interfaceId, { methods });
47016
47622
  return structured(
@@ -47271,14 +47877,24 @@ ${renderChangeReport(report4)}`,
47271
47877
  {
47272
47878
  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.`,
47273
47879
  inputSchema: {
47274
- kind: import_zod12.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).describe("The kind of specification"),
47880
+ 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"),
47275
47881
  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")'),
47276
47882
  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.")
47277
47883
  },
47278
47884
  outputSchema: getSpecOutput
47279
47885
  },
47280
- ({ kind, id, methods }) => {
47886
+ ({ kind: askedKind, id, methods }) => {
47281
47887
  try {
47888
+ let kind;
47889
+ if (askedKind) {
47890
+ kind = askedKind;
47891
+ } else {
47892
+ const holders = specKindsHolding(id);
47893
+ if (holders.length !== 1) {
47894
+ 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.`);
47895
+ }
47896
+ kind = holders[0];
47897
+ }
47282
47898
  let result = null;
47283
47899
  let partial2 = null;
47284
47900
  switch (kind) {
@@ -47637,7 +48253,7 @@ async function startMcpServer() {
47637
48253
  } catch {
47638
48254
  }
47639
48255
  }
47640
- var import_mcp, import_stdio, import_zod12, import_types12, fs29, path50, import_url, crypto10, TYPE_REF_GRAMMAR, staleServerOutput, externalPinsOutput, externalStatusesOutput, SPEC_KINDS, STATUS_VALUES, testsToRevisitOutput, typeRespellingOutput, specWriteReceiptOutput, specDeletionOutput, specChangeOutput, specChangeReportOutput, validationIssueOutput, memberCreationOutput, doctrineChangeOutput, impactTotalsOutput, packImpactOutput, validateTreeOutput, getSpecOutput, FINGERPRINT_VERSION, runningFingerprint, RECONNECT, assessedAsChanged, serverBuildStamps, SERVER_BUILD_STAMP, SPEC_WRITE_TOOLS, listChangedEmitters, statusInput, SKILL_RESOURCE_MIME, AGENT_BRIEF_SCHEME;
48256
+ var import_mcp, import_stdio, import_zod12, import_types12, fs29, path50, import_url, crypto10, TYPE_REF_GRAMMAR, staleServerOutput, externalPinsOutput, externalStatusesOutput, SPEC_KINDS, PORTAL_TYPE_TRANSPORT, STATUS_VALUES, testsToRevisitOutput, typeRespellingOutput, specWriteReceiptOutput, specDeletionOutput, specChangeOutput, specChangeReportOutput, validationIssueOutput, memberCreationOutput, doctrineChangeOutput, impactTotalsOutput, packImpactOutput, validateTreeOutput, getSpecOutput, FINGERPRINT_VERSION, runningFingerprint, RECONNECT, assessedAsChanged, serverBuildStamps, SERVER_BUILD_STAMP, SPEC_WRITE_TOOLS, listChangedEmitters, statusInput, SKILL_RESOURCE_MIME, AGENT_BRIEF_SCHEME;
47641
48257
  var init_server = __esm({
47642
48258
  "src/mcp/server.ts"() {
47643
48259
  "use strict";
@@ -47666,7 +48282,7 @@ var init_server = __esm({
47666
48282
  init_project();
47667
48283
  init_validation_options();
47668
48284
  init_migrations3();
47669
- 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.`;
48285
+ 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.`;
47670
48286
  staleServerOutput = {
47671
48287
  staleServer: import_zod12.z.boolean().optional().describe(
47672
48288
  "True when the wairon build on disk changed after this server started \u2014 the structured twin of the text answer's STALE SERVER banner. When the rebuild left the spec and tool schemas alone, writes still go through; reconnect to run the new code."
@@ -47706,6 +48322,16 @@ var init_server = __esm({
47706
48322
  ...staleServerOutput
47707
48323
  };
47708
48324
  SPEC_KINDS = ["system", "subsystem", "component", "interface", "implementation", "type"];
48325
+ PORTAL_TYPE_TRANSPORT = {
48326
+ HTTP_API: "HTTP",
48327
+ gRPC: "gRPC",
48328
+ GraphQL: "GraphQL",
48329
+ MessageBus: "MessageBus",
48330
+ NamedPipe: "NamedPipe",
48331
+ IPC: "IPC",
48332
+ CLI: "CLI",
48333
+ Custom: void 0
48334
+ };
47709
48335
  STATUS_VALUES = ["draft", "design", "complete"];
47710
48336
  testsToRevisitOutput = import_zod12.z.array(import_zod12.z.object({
47711
48337
  method: import_zod12.z.string().describe("The contract method this write changed or deleted."),
@@ -47820,7 +48446,7 @@ var init_server = __esm({
47820
48446
  profile: import_zod12.z.string().optional()
47821
48447
  })).describe("Requirements no installed version satisfies: nothing was written for them, and the next family run reports each as POLICY_NOT_ADOPTED."),
47822
48448
  projectType: import_zod12.z.string().optional().describe("The profile written as the new member's projectType, when exactly one adopted requirement names one."),
47823
- as: import_zod12.z.string().describe("part | project: what was created (stage 8; a part by default)."),
48449
+ as: import_zod12.z.string().describe("part | project: what was created (a part by default)."),
47824
48450
  storage: import_zod12.z.string().describe("contained | path | git: where its files live."),
47825
48451
  commit: import_zod12.z.string().optional().describe("git: the commit the member was pinned at."),
47826
48452
  ...staleServerOutput
@@ -48149,8 +48775,7 @@ function resolveInstallTargets(options) {
48149
48775
  const isPackaged = typeof process.pkg !== "undefined";
48150
48776
  const scriptPath = process.argv[1] ? path51.resolve(process.argv[1]).replace(/\\/g, "/") : null;
48151
48777
  const useDirectNode = !isPackaged && scriptPath && (scriptPath.endsWith(".js") || scriptPath.endsWith(".ts"));
48152
- const env = useGlobal ? {} : { WAIRON_PROJECT_DIR: process.cwd().replace(/\\/g, "/") };
48153
- const desiredEntry = options.hostedUrl ? hostedMcpEntry(options) : useDirectNode ? { command: "node", args: [scriptPath, "mcp", "serve"], env } : { command: "wairon", args: ["mcp", "serve"], env };
48778
+ const desiredEntry = options.hostedUrl ? hostedMcpEntry(options) : useGlobal ? machineEntry(useDirectNode ? scriptPath : null) : projectEntry(root, useDirectNode ? scriptPath : null);
48154
48779
  const existingEntry = mcpServers["wairon"];
48155
48780
  return {
48156
48781
  backend,
@@ -48165,6 +48790,16 @@ function resolveInstallTargets(options) {
48165
48790
  };
48166
48791
  });
48167
48792
  }
48793
+ function machineEntry(scriptPath) {
48794
+ return scriptPath ? { command: "node", args: [scriptPath, "mcp", "serve"], env: {} } : { command: "wairon", args: ["mcp", "serve"], env: {} };
48795
+ }
48796
+ function projectEntry(root, scriptPath) {
48797
+ if (scriptPath && !isOutsideRoot(root, scriptPath)) {
48798
+ const relative28 = path51.relative(root, scriptPath).replace(/\\/g, "/");
48799
+ return { command: "node", args: [`./${relative28}`, "mcp", "serve"] };
48800
+ }
48801
+ return { command: "wairon", args: ["mcp", "serve"] };
48802
+ }
48168
48803
  function planMcpInstall(options) {
48169
48804
  return resolveInstallTargets(options).map(({ backend, path: file, outsideProject, before, after, upToDate }) => ({
48170
48805
  backend,
@@ -48200,10 +48835,8 @@ function reportInstalled(target, agentLabel) {
48200
48835
  const wasStale = target.before !== void 0;
48201
48836
  logger.success(`wairon MCP server ${wasStale ? "updated (was stale)" : "registered"} for ${agentLabel} in ${import_chalk3.default.cyan(target.path)}.`);
48202
48837
  logger.blank();
48203
- logger.info("AI tools using this config will have access to these wairon tools:");
48204
- logger.info(" listAgents \xB7 getAgent \xB7 listDomains \xB7 validateTopology \xB7 getProjectConfig");
48205
- logger.info(" sdd_initialize_system \xB7 sdd_add_subsystem \xB7 sdd_add_component \xB7 sdd_define_interface");
48206
- logger.info(" sdd_write_narrative \xB7 sdd_validate_tree \xB7 sdd_get_status");
48838
+ logger.info("AI tools using this config get the wairon MCP server: the sdd_* spec-authoring, validation, member and");
48839
+ logger.info('rename tools, plus live agent briefs (sdd_get_agent_brief). The full list is in docs/cli.md ("MCP tools").');
48207
48840
  logger.blank();
48208
48841
  const restartApp = target.backend === "gemini" ? "Antigravity CLI (agy)" : "claude";
48209
48842
  logger.info(`Restart ${import_chalk3.default.bold(restartApp)} (or reload MCP servers) to activate.`);
@@ -48336,7 +48969,8 @@ init_skills_portal();
48336
48969
  init_base();
48337
48970
  init_fs();
48338
48971
  init_project();
48339
- var WAIRON_AGENT_FILE = /-(owner|implementer|architect)\.md$/;
48972
+ var AGENT_FILE_EXTENSION = /\.(md|ya?ml)$/;
48973
+ var WAIRON_AGENT_FILE = /-(owner|implementer|architect)\.(md|ya?ml)$/;
48340
48974
  function pruneStaleAgents(expectedPaths, scanDirs, reach2) {
48341
48975
  const dirs = new Set(scanDirs ?? [...expectedPaths].map((p) => path39.dirname(p)));
48342
48976
  let pruned = 0;
@@ -48350,7 +48984,7 @@ function pruneStaleAgents(expectedPaths, scanDirs, reach2) {
48350
48984
  continue;
48351
48985
  }
48352
48986
  for (const name of entries) {
48353
- if (!name.endsWith(".md")) continue;
48987
+ if (!AGENT_FILE_EXTENSION.test(name)) continue;
48354
48988
  const full = path39.resolve(dir, name);
48355
48989
  if (expectedPaths.has(full)) continue;
48356
48990
  let owned = WAIRON_AGENT_FILE.test(name);
@@ -48392,6 +49026,18 @@ function announceOutsideTargets(projectConfig, reach2) {
48392
49026
  }
48393
49027
  return outside.map((t) => t.type);
48394
49028
  }
49029
+ function configuredTargetTypes(projectConfig) {
49030
+ const targets = projectConfig.targets;
49031
+ return [...new Set(targets.map((t) => typeof t === "string" ? t : t.type))];
49032
+ }
49033
+ var UnknownTargetError = class extends WaironError {
49034
+ constructor(target, known) {
49035
+ super(
49036
+ `Unknown target "${target}". This project configures: ${known.length ? known.join(", ") : "(none)"}. Pass one of those, or add a target to .wai/project.yaml (\`targets\`).`
49037
+ );
49038
+ this.name = "UnknownTargetError";
49039
+ }
49040
+ };
48395
49041
  async function runGenerate(options = {}) {
48396
49042
  await generateLayer(options);
48397
49043
  const scoped = options.root || options.domain || options.domains;
@@ -48410,6 +49056,10 @@ async function generateLayer(options = {}) {
48410
49056
  assertProjectInitialized();
48411
49057
  const projectConfig = loadProjectConfig();
48412
49058
  if (!projectConfig) throw new ProjectNotInitializedError();
49059
+ if (options.target !== void 0) {
49060
+ const known = configuredTargetTypes(projectConfig);
49061
+ if (!known.includes(options.target)) throw new UnknownTargetError(options.target, known);
49062
+ }
48413
49063
  const agents = resolveAgentTopology();
48414
49064
  if (agents.length === 0) {
48415
49065
  logger.warn("No agents resolved from the spec tree. Define subsystems and components first \u2014 see `wairon status`.");
@@ -48426,6 +49076,8 @@ async function generateLayer(options = {}) {
48426
49076
  const removed = pruneStaleAgents(/* @__PURE__ */ new Set(), candidateDirs, reach2);
48427
49077
  if (removed > 0) {
48428
49078
  logger.info(`Removed ${removed} previously materialized agent file(s) \u2014 agents are served as live briefs (sdd_get_agent_brief); opt back in with rules.materializeAgentFiles: true.`);
49079
+ } else {
49080
+ logger.info("Agent files: off (rules.materializeAgentFiles) \u2014 agents are served as live briefs (sdd_get_agent_brief); no agent files written.");
48429
49081
  }
48430
49082
  }
48431
49083
  if (options.dryRun) return;
@@ -48444,6 +49096,7 @@ async function generateLayer(options = {}) {
48444
49096
  try {
48445
49097
  reinjectLocalGuides(getProjectRoot(), activeTargetTypes(projectConfig));
48446
49098
  logger.verbose("Local AI guides re-injected.");
49099
+ logger.info(`Guides, skills and context reconciled for: ${activeTargetTypes(projectConfig).join(", ") || "(no active targets)"}.`);
48447
49100
  } catch (err) {
48448
49101
  logger.warn(`Failed to re-inject AI guides: ${String(err)}`);
48449
49102
  }
@@ -48553,7 +49206,7 @@ function checkApproval(strict) {
48553
49206
  return {
48554
49207
  state: "stale",
48555
49208
  approved: false,
48556
- message: `The design, or something it was approved under (doctrine, declared inputs, \`composition\`, a direct member's approval), changed after it was approved (the approval on record is ${record2.lockedAt} by ${describeApprover(record2.lockedBy)}). Nothing says a human has seen what is about to merge. Fix: run \`wairon lock\` on this branch and commit the updated .wai/lock.json.`
49209
+ message: `The design, or something it was approved under, changed after it was approved (the approval on record is ${record2.lockedAt} by ${describeApprover(record2.lockedBy)}). ${whatMoved(record2)} Nothing says a human has seen what is about to merge. Fix: run \`wairon lock\` on this branch and commit the updated .wai/lock.json.`
48557
49210
  };
48558
49211
  default:
48559
49212
  return {
@@ -48563,13 +49216,34 @@ function checkApproval(strict) {
48563
49216
  };
48564
49217
  }
48565
49218
  }
49219
+ function whatMoved(record2) {
49220
+ const diff3 = diffAgainstApproval();
49221
+ const own = diff3 ? diffSize(diff3) : null;
49222
+ const members = familyApprovals(1).filter((a) => a.parent === "" && a.as !== "part");
49223
+ const named2 = [];
49224
+ for (const m of members) {
49225
+ const name = m.alias ?? m.key;
49226
+ if (m.pinned === "unpinned") named2.push(`${name} (added since the approval)`);
49227
+ else if (m.pinned === "moved") named2.push(`${name} (its approval moved since this one was taken${m.state === "approved" ? "" : `; now ${m.state}`})`);
49228
+ else if (m.state !== "approved") named2.push(`${name} (${m.state} at its own root)`);
49229
+ }
49230
+ const current2 = new Set(members.map((m) => m.alias ?? m.key));
49231
+ for (const alias of Object.keys(record2.members ?? {})) {
49232
+ if (!current2.has(alias)) named2.push(`${alias} (removed since the approval)`);
49233
+ }
49234
+ const parts = [];
49235
+ if (own !== null && own > 0) parts.push(`${own} own spec file(s) changed since the approval`);
49236
+ if (named2.length > 0) parts.push(`direct member(s): ${named2.join(", ")}`);
49237
+ if (parts.length > 0) return `What moved: ${parts.join("; ")}.`;
49238
+ return own === null ? "This approval predates per-spec digests, so which spec moved cannot be said." : "No own spec file and no direct member's approval moved, so what changed is the doctrine, a declared input or `composition`.";
49239
+ }
48566
49240
  function upgradedCheck(record2) {
48567
49241
  const diff3 = diffAgainstApproval();
48568
49242
  const specs = !diff3 ? "This approval predates per-spec digests, so whether a spec file moved since cannot be said." : diffSize(diff3) === 0 ? "No own spec file has changed since the approval." : `${diffSize(diff3)} own spec file(s) changed since the approval.`;
48569
49243
  return {
48570
49244
  state: "stale",
48571
49245
  approved: false,
48572
- message: `The approval on record (${record2.lockedAt} by ${describeApprover(record2.lockedBy)}) was taken under an earlier gate identity \u2014 the gate identity gained inputs in stage 5: members' composition subjects, \`composition\`; code conformance moved beside the claim. ${specs} Fix: re-lock once (\`wairon lock\`) and commit the updated .wai/lock.json.`
49246
+ message: `The approval on record (${record2.lockedAt} by ${describeApprover(record2.lockedBy)}) was taken under an earlier gate identity (written by wairon ${record2.validatorVersion}). Since wairon 6.0.0 the gate identity also covers members' composition subjects and \`composition\`, and code conformance is recorded beside the claim. ${specs} Fix: re-lock once (\`wairon lock\`) and commit the updated .wai/lock.json.`
48573
49247
  };
48574
49248
  }
48575
49249
  var LockRefusedError = class extends WaironError {
@@ -48579,7 +49253,7 @@ var LockRefusedError = class extends WaironError {
48579
49253
  }
48580
49254
  };
48581
49255
  function describeMemberState(entry) {
48582
- return `${entry.alias ?? entry.key} (${entry.state}${entry.upgraded ? " \u2014 approved under the pre-stage-5 identity, re-lock it once" : ""})`;
49256
+ return `${entry.alias ?? entry.key} (${entry.state}${entry.upgraded ? " \u2014 approved under an earlier gate identity, re-lock it once" : ""})`;
48583
49257
  }
48584
49258
  function codeLine(code) {
48585
49259
  if (!code) return "code: no code analysis recorded";
@@ -48588,9 +49262,14 @@ function codeLine(code) {
48588
49262
  function summarize3(gate, members) {
48589
49263
  const diff3 = diffAgainstApproval();
48590
49264
  if (!diff3) {
48591
- logger.info("First approval of this tree \u2014 the whole spec tree becomes the approved baseline.");
49265
+ const previous = readLockRecord();
49266
+ if (previous) {
49267
+ logger.info(`Replacing the approval on record (${previous.lockedAt} by ${describeApprover(previous.lockedBy)}, wairon ${previous.validatorVersion ?? "unknown"}). It records no per-spec digests, so the whole spec tree becomes the approved baseline.`);
49268
+ } else {
49269
+ logger.info("First approval of this tree \u2014 the whole spec tree becomes the approved baseline.");
49270
+ }
48592
49271
  } else if (diffSize(diff3) === 0) {
48593
- logger.info("Nothing has changed since the last approval \u2014 this will re-validate and regenerate the agent topology.");
49272
+ logger.info("Nothing has changed since the last approval \u2014 this re-records the approval and refreshes the generated outputs of this project (guides, skills, and agent files when materialized).");
48594
49273
  } else {
48595
49274
  logger.info(`${diffSize(diff3)} spec(s) changed since the last approval:`);
48596
49275
  for (const p of diff3.changed) logger.info(` ~ ${p}`);
@@ -48604,7 +49283,7 @@ function summarize3(gate, members) {
48604
49283
  continue;
48605
49284
  }
48606
49285
  const pin2 = m.pinned === "moved" ? " \u2014 its pin moved since the last approval" : "";
48607
- logger.info(` member ${m.alias ?? m.key}: ${m.state}${m.upgraded ? " (pre-stage-5 lock, re-lock once)" : ""}${pin2}`);
49286
+ logger.info(` member ${m.alias ?? m.key}: ${m.state}${m.upgraded ? " (approved under an earlier gate identity, re-lock once)" : ""}${pin2}`);
48608
49287
  }
48609
49288
  }
48610
49289
  async function runLock(options, gate, captured) {
@@ -48618,7 +49297,7 @@ async function runLock(options, gate, captured) {
48618
49297
  );
48619
49298
  }
48620
49299
  logger.blank();
48621
- logger.warn("This records the current design as approved and (re)generates the agent topology.");
49300
+ logger.warn("This records the current design as approved in .wai/lock.json (no spec file is rewritten) and refreshes the generated outputs of this project.");
48622
49301
  if (!options.yes) {
48623
49302
  if (!process.stdin.isTTY) {
48624
49303
  logger.error("Non-interactive shell \u2014 re-run with --yes to confirm the lock.");
@@ -48628,7 +49307,7 @@ async function runLock(options, gate, captured) {
48628
49307
  {
48629
49308
  type: "confirm",
48630
49309
  name: "confirmed",
48631
- message: "Approve this design and generate the agent topology?",
49310
+ message: "Approve this design?",
48632
49311
  default: false
48633
49312
  }
48634
49313
  ]);
@@ -48652,7 +49331,7 @@ async function runLock(options, gate, captured) {
48652
49331
  const design = designOnly(gate);
48653
49332
  const count = (severity) => design.issues.filter((i) => i.severity === severity).length;
48654
49333
  const record2 = {
48655
- // The record format: 2 from stage 5 on (members, code, no children).
49334
+ // The record format: 2 since wairon 6.0.0 (members, code, no children).
48656
49335
  format: 2,
48657
49336
  stateId: captured,
48658
49337
  lockedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -48727,12 +49406,12 @@ function projectLabel(key, result) {
48727
49406
  const root = result.projects?.find((p) => p.key === "");
48728
49407
  return `${root?.id ?? "bound project"} (bound)`;
48729
49408
  }
48730
- function renderSpecFindings(result) {
49409
+ function renderSpecFindings(result, all = false) {
48731
49410
  const tally = { errors: false, fatalWarnings: false, waived: 0, notices: 0 };
48732
49411
  if (result.issues.length === 0) {
48733
49412
  logger.success("Spec tree is valid and component type boundaries are enforced.");
48734
49413
  }
48735
- const MAX_PRINT = 100;
49414
+ const MAX_PRINT = all ? Number.POSITIVE_INFINITY : 100;
48736
49415
  const printed = { error: 0, warning: 0, notice: 0 };
48737
49416
  const skipped = { error: 0, warning: 0, notice: 0 };
48738
49417
  for (const issue2 of result.issues) {
@@ -48752,10 +49431,11 @@ function renderSpecFindings(result) {
48752
49431
  else if (issue2.severity === "notice") logger.notice(line2);
48753
49432
  else logger.warn(line2);
48754
49433
  }
48755
- const more = (n, what) => `... and ${n} more ${what}(s) omitted. Use '--subsystem <id>' to validate a specific subsystem.`;
49434
+ const more = (n, what) => `... and ${n} more ${what}(s) not shown (${printed[what]} of ${printed[what] + n} printed). Run with --all to print every finding.`;
48756
49435
  if (skipped.error > 0) logger.error(more(skipped.error, "error"));
48757
49436
  if (skipped.warning > 0) logger.warn(more(skipped.warning, "warning"));
48758
49437
  if (skipped.notice > 0) logger.notice(more(skipped.notice, "notice"));
49438
+ if (skipped.error + skipped.warning + skipped.notice > 0 || all && result.issues.length > 100) renderCodeTotals(result);
48759
49439
  if (result.projects) {
48760
49440
  logger.blank();
48761
49441
  logger.info("Per project (each its own gate):");
@@ -48767,6 +49447,20 @@ function renderSpecFindings(result) {
48767
49447
  if (result.hint) logger.info(result.hint);
48768
49448
  return tally;
48769
49449
  }
49450
+ function renderCodeTotals(result) {
49451
+ const counts = /* @__PURE__ */ new Map();
49452
+ for (const issue2 of result.issues) {
49453
+ const key = `${issue2.severity} ${issue2.code}`;
49454
+ const entry = counts.get(key) ?? { severity: issue2.severity, code: issue2.code, n: 0 };
49455
+ entry.n++;
49456
+ counts.set(key, entry);
49457
+ }
49458
+ const rank = { error: 0, warning: 1, notice: 2 };
49459
+ const rows = [...counts.values()].sort((a, b) => (rank[a.severity] ?? 3) - (rank[b.severity] ?? 3) || b.n - a.n || a.code.localeCompare(b.code));
49460
+ logger.blank();
49461
+ logger.info("Findings by code:");
49462
+ for (const r of rows) logger.info(` ${String(r.n).padStart(6)} ${r.severity.padEnd(7)} ${r.code}`);
49463
+ }
48770
49464
  async function runValidate(options = {}) {
48771
49465
  assertProjectInitialized();
48772
49466
  const projectConfig = loadProjectConfig();
@@ -48838,7 +49532,7 @@ async function runValidate(options = {}) {
48838
49532
  };
48839
49533
  const family = selectsFamily(sddOptions, declaredMembers(projectConfig).length > 0);
48840
49534
  const sddResult = family ? validateFamily2(sddOptions) : validateProject2(sddOptions);
48841
- const tally = renderSpecFindings(sddResult);
49535
+ const tally = renderSpecFindings(sddResult, options.all === true);
48842
49536
  hasErrors ||= tally.errors;
48843
49537
  hasFatalWarnings ||= tally.fatalWarnings;
48844
49538
  waivedWarnings += tally.waived;
@@ -48946,6 +49640,9 @@ async function runStatus(options = {}) {
48946
49640
  logger.blank();
48947
49641
  if (lock.drifted) logger.warn(lock.text.trim());
48948
49642
  else logger.info(lock.text.trim());
49643
+ logger.info(import_chalk4.default.gray(
49644
+ "The percentages measure authoring readiness (each spec's draft/design/complete status), not approval: approval is the lock record above (.wai/lock.json), and `wairon lock` does not change any spec's status."
49645
+ ));
48949
49646
  }
48950
49647
  logger.blank();
48951
49648
  }
@@ -49080,14 +49777,14 @@ var http = __toESM(require("http"));
49080
49777
  var https = __toESM(require("https"));
49081
49778
  init_defaults();
49082
49779
  function downloadFile(url, dest) {
49083
- return new Promise((resolve55, reject) => {
49780
+ return new Promise((resolve56, reject) => {
49084
49781
  const file = fs31.createWriteStream(dest);
49085
49782
  const get4 = url.startsWith("https://") ? https.get : http.get;
49086
49783
  get4(url, { headers: { "User-Agent": `wairon/${WAIRON_VERSION}` }, agent: false }, (res) => {
49087
49784
  if (res.statusCode === 301 || res.statusCode === 302) {
49088
49785
  file.close();
49089
49786
  res.destroy();
49090
- downloadFile(res.headers.location, dest).then(resolve55).catch(reject);
49787
+ downloadFile(res.headers.location, dest).then(resolve56).catch(reject);
49091
49788
  return;
49092
49789
  }
49093
49790
  if (res.statusCode !== 200) {
@@ -49099,7 +49796,7 @@ function downloadFile(url, dest) {
49099
49796
  res.pipe(file);
49100
49797
  file.on("finish", () => {
49101
49798
  res.destroy();
49102
- file.close(() => resolve55());
49799
+ file.close(() => resolve56());
49103
49800
  });
49104
49801
  file.on("error", (err) => {
49105
49802
  res.destroy();
@@ -49593,7 +50290,12 @@ function writeBundle(root, resolved) {
49593
50290
  const dest = path52.join(root, ".wai", "packs", resolved.name, resolved.version);
49594
50291
  fs32.rmSync(dest, { recursive: true, force: true });
49595
50292
  fs32.mkdirSync(path52.dirname(dest), { recursive: true });
49596
- fs32.cpSync(resolved.path, dest, { recursive: true });
50293
+ if (fs32.statSync(resolved.path).isFile()) {
50294
+ fs32.mkdirSync(dest, { recursive: true });
50295
+ fs32.copyFileSync(resolved.path, path52.join(dest, `pack${path52.extname(resolved.path).toLowerCase()}`));
50296
+ } else {
50297
+ fs32.cpSync(resolved.path, dest, { recursive: true });
50298
+ }
49597
50299
  return dest;
49598
50300
  }
49599
50301
  function bundlePack(name, options = {}) {
@@ -50375,7 +51077,7 @@ async function runLock2(options) {
50375
51077
  }
50376
51078
  }
50377
51079
  if (skippedErrors > 0) {
50378
- logger.error(`... and ${skippedErrors} more error(s) omitted.`);
51080
+ logger.error(`... and ${skippedErrors} more error(s) not shown (${errorCount} of ${errors.length} printed). Run \`wairon validate --all\` to see every finding (it judges draft specs leniently; the lock judges them as complete).`);
50379
51081
  }
50380
51082
  logger.blank();
50381
51083
  logger.info("Fix the errors above, then run `wairon lock` again. Nothing was changed.");
@@ -50427,7 +51129,7 @@ async function validateCommand(opts) {
50427
51129
  for (const t of recover3(getProjectRoot(), false)) {
50428
51130
  logger.notice(`[TRANSACTION_PENDING] an unfinished family migration (${t.verb}, transaction ${t.id}, coordinator phase ${t.phase}) \u2014 run \`wairon doctor --fix\` to roll it back`);
50429
51131
  }
50430
- await runValidate({ ci: opts.ci, subsystem: opts.subsystem, recursive: opts.recursive, family: opts.family });
51132
+ await runValidate({ ci: opts.ci, subsystem: opts.subsystem, recursive: opts.recursive, family: opts.family, all: opts.all });
50431
51133
  }
50432
51134
  async function statusCommand(opts) {
50433
51135
  const target = resolveTarget(getProjectRoot(), {});
@@ -50874,6 +51576,10 @@ var SCREENFUL = 20;
50874
51576
  function shownDir(dir) {
50875
51577
  return nodePath3.relative(process.cwd(), dir) || ".";
50876
51578
  }
51579
+ function relockLabel(dir) {
51580
+ const shown2 = shownDir(dir);
51581
+ return shown2 === "." ? "this project (.)" : shown2;
51582
+ }
50877
51583
  function printPlan(planned) {
50878
51584
  logger.header(`wairon family migration: ${planned.request.verb}`);
50879
51585
  logger.info(`Family root: ${shownDir(planned.familyRoot)}${planned.whole ? "" : " (not the whole family: the reach stops below its top)"}`);
@@ -50884,7 +51590,7 @@ function printPlan(planned) {
50884
51590
  for (const r of planned.refusals) logger.error(`[${r.code}]${r.project ? ` ${ownerLabel(r.project)}:` : ""} ${r.detail}`);
50885
51591
  for (const note of planned.notes) logger.warn(`NOTE: ${note}`);
50886
51592
  printFiles(planned);
50887
- if (planned.relock.length > 0) logger.info(`To re-lock once applied: ${planned.relock.map(shownDir).join(", ")}`);
51593
+ if (planned.relock.length > 0) logger.info(`To re-lock once applied: ${planned.relock.map(relockLabel).join(", ")}`);
50888
51594
  }
50889
51595
  function printFiles(planned) {
50890
51596
  if (planned.changes.length === 0) return;
@@ -50913,7 +51619,7 @@ function printOutcome(report4) {
50913
51619
  const verb = report4.plan.request.verb;
50914
51620
  if (report4.applied) {
50915
51621
  logger.success(`Applied the ${verb} migration: ${report4.plan.changes.length} file(s) across ${new Set(report4.plan.changes.map((c) => c.project)).size} project(s).`);
50916
- for (const dir of report4.relock) logger.info(`Re-lock ${shownDir(dir)}: run \`wairon lock\` there.`);
51622
+ for (const dir of report4.relock) logger.info(`Re-lock ${relockLabel(dir)}: run \`wairon lock\` ${shownDir(dir) === "." ? "here" : "there"}.`);
50917
51623
  return;
50918
51624
  }
50919
51625
  process.exitCode = 1;
@@ -50980,9 +51686,14 @@ async function runInit(options = {}) {
50980
51686
  const ancestorRoot = findSystemRoot(cwd);
50981
51687
  if (ancestorRoot && path58.resolve(ancestorRoot) === path58.resolve(cwd)) {
50982
51688
  logger.info("Project already initialized.");
50983
- logger.info("Design your spec tree with the SDD architect skill, then run `wairon generate`.");
51689
+ logger.info("Design your spec tree with the SDD architect skill, validate it, then run `wairon lock` to approve it.");
50984
51690
  return;
50985
51691
  }
51692
+ if (!options.yes && !process.stdin.isTTY) {
51693
+ throw new WaironError(
51694
+ "Non-interactive shell: `wairon init` asks questions and there is no terminal to ask them on. Re-run with --yes (-y) to use the defaults (project name from the directory, profile backend, targets claude and agy). Nothing was written."
51695
+ );
51696
+ }
50986
51697
  if (ancestorRoot) {
50987
51698
  await runInitAsMember(ancestorRoot, cwd, options);
50988
51699
  return;
@@ -51251,13 +51962,17 @@ async function executeInit(projectName, projectConfig, guidePlan) {
51251
51962
  logger.warn(`Failed to automatically register MCP server for Claude: ${err}`);
51252
51963
  }
51253
51964
  }
51254
- if (targetTypes.includes("gemini") || targetTypes.includes("agy")) {
51965
+ if (targetTypes.includes("gemini")) {
51255
51966
  try {
51256
51967
  const { runMcpInstall: runMcpInstall2 } = (init_mcp(), __toCommonJS(mcp_exports));
51257
51968
  runMcpInstall2({ backend: "gemini", global: false });
51258
51969
  } catch (err) {
51259
- logger.warn(`Failed to automatically register MCP server for Antigravity: ${err}`);
51970
+ logger.warn(`Failed to automatically register MCP server for the Gemini CLI: ${err}`);
51260
51971
  }
51972
+ } else if (targetTypes.includes("agy")) {
51973
+ logger.info(
51974
+ `Antigravity (agy) reads MCP servers only from its machine-wide ~/.gemini/antigravity-cli/mcp_config.json, which init does not write. To give it the wairon tools: ${import_chalk11.default.bold("wairon mcp install --backend gemini --global")}`
51975
+ );
51261
51976
  }
51262
51977
  syncContextFiles();
51263
51978
  logger.verbose("Context files seeded in .wai/context/");
@@ -51281,9 +51996,10 @@ async function executeInit(projectName, projectConfig, guidePlan) {
51281
51996
  logger.blank();
51282
51997
  logger.info("Next steps:");
51283
51998
  logger.info(" Edit .wai/context/project.md \u2014 describe the project concept and stack details for the AI");
51284
- logger.info(" wairon status \u2014 view the architecture completeness dashboard");
51285
- logger.info(" wairon validate \u2014 check the spec tree & component boundaries");
51286
51999
  logger.info(" Design your spec tree with the SDD architect skill or the wairon MCP sdd_* tools");
52000
+ logger.info(" wairon status \u2014 view the spec tree, its authoring readiness and approval state");
52001
+ logger.info(" wairon validate \u2014 check the spec tree & component boundaries");
52002
+ logger.info(" wairon lock \u2014 approve the validated design (commit .wai/lock.json); `wairon lock-check` gates CI on it");
51287
52003
  logger.blank();
51288
52004
  }
51289
52005
  function buildProjectConfig(name, targets, now, projectType) {
@@ -51366,11 +52082,13 @@ Define the non-negotiable architectural guardrails here. The AI agent must follo
51366
52082
 
51367
52083
  * [ ] **Primary Language & Runtime:** Node.js (TypeScript) / Python / etc.
51368
52084
  * [ ] **Architectural Style:** Clean Architecture / Hexagonal / Domain-Driven Design (DDD).
51369
- * [ ] **Data Persistence Rules:** E.g. No raw SQL in controllers; all DB operations must use a \`Store\` / \`Repository\`.
51370
- * [ ] **Stereotype Dependencies:**
51371
- * \`Store\` components can only call other \`Stores\` or \`Registries\`.
51372
- * \`Adapter\` components cannot depend on \`Orchestrators\` or \`Stores\` directly.
51373
- * Only \`Portal\` components can accept external traffic.
52085
+ * [ ] **Data Persistence Rules:** E.g. No raw SQL in controllers; held state lives in a data component \u2014 the recommended \`Repository\` (owning its \`Store\`, \`Registry\`, \`Index\`es and \`Query\`s), or a deliberately standalone \`Store\` for genuinely simple state.
52086
+ * [ ] **Stereotype Dependencies** (enforced by \`sdd_validate_tree\`):
52087
+ * Only \`Portal\` components accept external traffic; a gateway is a \`Portal\` with the \`gateway\` variant.
52088
+ * A \`Portal\` never depends on a \`Store\`, \`Registry\` or \`Adapter\`. Its reads may go through a \`Repository\` / \`Index\` facade; every write routes through an \`Orchestrator\`.
52089
+ * State never lives as fields inside an \`Orchestrator\`; logic is an \`Orchestrator\` (\`dependencyClass: pure | read\` bounds what it may depend on).
52090
+ * A \`Registry\` and an \`Index\` depend on the \`Store\` they read; a \`Store\` may use an \`Adapter\` as its backend.
52091
+ * Outbound calls to third parties go through an \`Adapter\`.
51374
52092
 
51375
52093
  ---
51376
52094
 
@@ -51383,15 +52101,16 @@ Define the non-negotiable architectural guardrails here. The AI agent must follo
51383
52101
 
51384
52102
  ---
51385
52103
 
51386
- ## Stage 3: Ingress/Egress Portals (Level 2 & Level 3)
52104
+ ## Stage 3: Ingress Portals & Outbound Adapters (Level 2 & Level 3)
51387
52105
 
51388
- Portals are the boundaries of your subsystems. Define how requests enter and leave.
52106
+ Portals are where requests enter a subsystem; Adapters are where calls leave it.
51389
52107
 
51390
52108
  * [ ] **Define Ingress Portals (REST / gRPC / MessageBus):**
51391
52109
  * *AI Action:* Create L2 Portal components with \`status: draft\` and map their L3 interfaces.
51392
- * *Design check:* Ensure HTTP endpoints (method, path) or gRPC names are correctly declared in the method bindings.
51393
- * [ ] **Define Egress Portals (Clients / Publishers):**
51394
- * *AI Action:* Declare any external event publishing or client communication Portals.
52110
+ * *Design check:* Bind each method's wire endpoint with \`sdd_set_endpoints\`, and give every HTTP Portal its \`mounts\` (or a listener) \u2014 otherwise \`UNMOUNTED_PORTAL\`.
52111
+ * *Design check:* A subsystem another subsystem calls lists those callers in its \`publicInterfaces[].consumers\`.
52112
+ * [ ] **Define Outbound Adapters (third-party clients / publishers):**
52113
+ * *AI Action:* Declare an \`Adapter\` for each external API, database driver or event publisher the subsystem calls.
51395
52114
 
51396
52115
  ---
51397
52116
 
@@ -51416,13 +52135,13 @@ Map the behavior step-by-step.
51416
52135
 
51417
52136
  ---
51418
52137
 
51419
- ## Stage 6: Sandbox Implementation
52138
+ ## Stage 6: Approval & Implementation
51420
52139
 
51421
- Once the specs are clean and compiled, mark the components as \`status: complete\` to lock them, then generate the agents and write code!
52140
+ Once the specs validate cleanly, the human approves the design with \`wairon lock\`; implementation starts from the approved design.
51422
52141
 
51423
52142
  * [ ] **Validation Check (AI):** Call the \`sdd_validate_tree\` MCP tool (must return 0 errors).
51424
- * [ ] **Agent Generation (human):** The developer runs \`wairon generate\` from their terminal to instantiate agent sandboxes \u2014 the AI does not run this.
51425
- * [ ] **Code Implementation:** Let the agent implement the component code matching the narrative.
52143
+ * [ ] **Approval (human):** The developer runs \`wairon lock\` from their terminal and commits \`.wai/lock.json\` \u2014 the AI does not run this. The lock records the approval; it does not rewrite spec files or their \`status\`. \`wairon lock-check\` gates merges on it in CI.
52144
+ * [ ] **Code Implementation (AI):** Confirm \`sdd_get_status\` reports the design approved, then delegate each component via the \`sdd-delegate\` skill (live briefs from \`sdd_get_agent_brief\`); code must match the L3 interfaces and L5 narratives.
51426
52145
  `;
51427
52146
  writeFile(fromProjectRoot(".wai", "phased_design.md"), content);
51428
52147
  }
@@ -51723,7 +52442,7 @@ function releaseChannelLabel(tag) {
51723
52442
  return version.slice(dash + 1).replace(/\.\d+$/, "") || "stable";
51724
52443
  }
51725
52444
  function fetchReleases(repo) {
51726
- return new Promise((resolve55, reject) => {
52445
+ return new Promise((resolve56, reject) => {
51727
52446
  const url = `https://api.github.com/repos/${repo}/releases?per_page=100`;
51728
52447
  const options = {
51729
52448
  headers: {
@@ -51743,7 +52462,7 @@ function fetchReleases(repo) {
51743
52462
  return;
51744
52463
  }
51745
52464
  try {
51746
- resolve55(JSON.parse(data));
52465
+ resolve56(JSON.parse(data));
51747
52466
  } catch {
51748
52467
  reject(new Error("Failed to parse GitHub API response"));
51749
52468
  }
@@ -51915,7 +52634,7 @@ async function filteredCheckbox(config) {
51915
52634
  32,
51916
52635
  Math.max(...items.map((i) => i.label.length))
51917
52636
  );
51918
- return new Promise((resolve55) => {
52637
+ return new Promise((resolve56) => {
51919
52638
  const checked = /* @__PURE__ */ new Set();
51920
52639
  let cursor = 0;
51921
52640
  let filterIdx = 0;
@@ -51982,7 +52701,7 @@ async function filteredCheckbox(config) {
51982
52701
  );
51983
52702
  teardown();
51984
52703
  const result = items.filter((_, idx) => checked.has(idx)).map((i) => i.value);
51985
- resolve55(result);
52704
+ resolve56(result);
51986
52705
  }
51987
52706
  function abort() {
51988
52707
  process.stdout.write("\n");
@@ -52446,7 +53165,7 @@ function mcpEntryHealth(settingsPath) {
52446
53165
  if (!entry) return { mark: "warn", note: "not registered" };
52447
53166
  if (entry.command === "node" && Array.isArray(entry.args) && typeof entry.args[0] === "string") {
52448
53167
  const scriptPath = entry.args[0];
52449
- if (!fs38.existsSync(scriptPath)) {
53168
+ if (!fs38.existsSync(path61.resolve(getProjectRoot(), scriptPath))) {
52450
53169
  return { mark: "error", note: `registered but the server path is missing \u2014 ${scriptPath}` };
52451
53170
  }
52452
53171
  }
@@ -52512,15 +53231,26 @@ async function runDoctor(options = {}) {
52512
53231
  try {
52513
53232
  const cfg = loadProjectConfig();
52514
53233
  if (!cfg) throw new ProjectNotInitializedError();
52515
- const result = validateProject2({ rules: cfg.rules, projectType: cfg.projectType });
52516
- const errs = result.issues.filter((i) => i.severity === "error").length;
52517
- const warns = result.issues.filter((i) => i.severity === "warning").length;
52518
- const notes = result.issues.filter((i) => i.severity === "notice").length;
53234
+ const result = validateFamily2({ rules: cfg.rules, projectType: cfg.projectType });
53235
+ const counts = severityCounts(result.issues);
53236
+ const { errs, warns, notes } = counts;
52519
53237
  const noted = notes > 0 ? `, ${notes} notice(s)` : "";
52520
53238
  if (errs > 0) line(tally, "error", `Conformance: ${errs} error(s), ${warns} warning(s)${noted} \u2014 see \`wairon validate\` (you: \`sdd_validate_tree\`)`);
52521
53239
  else if (warns > 0) line(tally, "warn", `Conformance: ${warns} warning(s)${noted} \u2014 see \`wairon validate\` (you: \`sdd_validate_tree\`)`);
52522
53240
  else if (notes > 0) line(tally, "ok", `Conformance: 0 errors, 0 warnings, ${notes} notice(s) \u2014 see \`wairon validate\` (you: \`sdd_validate_tree\`)`);
52523
53241
  else line(tally, "ok", "Conformance: 0 errors, 0 warnings");
53242
+ const byProject = /* @__PURE__ */ new Map();
53243
+ for (const issue2 of result.issues) {
53244
+ const key = issue2.project ?? "";
53245
+ byProject.set(key, [...byProject.get(key) ?? [], issue2]);
53246
+ }
53247
+ if (byProject.size > 1 || byProject.size === 1 && !byProject.has("")) {
53248
+ for (const [key, issues] of [...byProject].sort(([a], [b]) => a.localeCompare(b))) {
53249
+ const own = severityCounts(issues);
53250
+ if (own.errs === 0 && own.warns === 0 && own.notes === 0) continue;
53251
+ console.log(` ${key === "" ? "this project" : `member ${key}`}: ${own.errs} error(s), ${own.warns} warning(s), ${own.notes} notice(s)`);
53252
+ }
53253
+ }
52524
53254
  } catch (e) {
52525
53255
  line(tally, "warn", `Could not run conformance check: ${e instanceof Error ? e.message : String(e)}`);
52526
53256
  }
@@ -52590,6 +53320,11 @@ async function runDoctor(options = {}) {
52590
53320
  line(tally, "warn", `${proposal.kind} ${proposal.specId} ${proposal.path}: "${proposal.written}" \u2192 ${proposal.stored} (proposed)`);
52591
53321
  }
52592
53322
  }
53323
+ const enumProposals = spellings.flatMap((repair) => repair.enumProposals);
53324
+ if (enumProposals.length > 0) {
53325
+ console.log(` ${import_chalk17.default.gray("enum proposed \u2014 never written: define the enum, then write its id here")}`);
53326
+ for (const proposal of enumProposals) line(tally, "warn", describeEnumProposal(proposal));
53327
+ }
52593
53328
  if (authorNeeded.length > 0) {
52594
53329
  console.log(` ${import_chalk17.default.gray("needs an author \u2014 no rewrite can settle these")}`);
52595
53330
  for (const problem of authorNeeded) {
@@ -52600,6 +53335,11 @@ async function runDoctor(options = {}) {
52600
53335
  } catch (e) {
52601
53336
  line(tally, "warn", `Could not plan the type-spelling repair: ${e instanceof Error ? e.message : String(e)}`);
52602
53337
  }
53338
+ try {
53339
+ reportMemberRepairs(tally);
53340
+ } catch (e) {
53341
+ line(tally, "warn", `Could not plan the members' repairs: ${e instanceof Error ? e.message : String(e)}`);
53342
+ }
52603
53343
  try {
52604
53344
  const planned = plan5({ verb: "chaining", rehearse: false });
52605
53345
  const pending4 = planned.chaining;
@@ -52668,7 +53408,7 @@ async function runDoctor(options = {}) {
52668
53408
  line(
52669
53409
  tally,
52670
53410
  "warn",
52671
- `stale \u2014 the lock of ${lock.record.lockedAt} was taken under an earlier gate identity: the gate identity gained inputs in stage 5 (members' composition subjects, \`composition\`; code conformance moved beside the claim). Re-lock once (\`wairon lock\`).`
53411
+ `stale \u2014 the lock of ${lock.record.lockedAt} was taken under an earlier gate identity: the gate identity gained inputs in this release (members' composition subjects, \`composition\`; code conformance moved beside the claim). Re-lock once (\`wairon lock\`).`
52672
53412
  );
52673
53413
  } else if (lock.state === "stale") {
52674
53414
  console.log(import_chalk17.default.bold("Lock"));
@@ -52733,9 +53473,13 @@ async function runDoctor(options = {}) {
52733
53473
  line(tally, h.mark, `Claude (project .mcp.json): ${h.note}${h.mark === "ok" ? "" : " \u2014 run `wairon mcp install --backend claude`"}`);
52734
53474
  }
52735
53475
  if (wantGemini) {
52736
- const globalCfg = path61.join(os13.homedir(), ".gemini", "antigravity-cli", "mcp_config.json");
52737
- const hg = mcpEntryHealth(globalCfg);
52738
- line(tally, hg.mark, `Antigravity (global mcp_config.json): ${hg.note}${hg.mark === "ok" ? "" : " \u2014 run `wairon mcp install --backend gemini --global`"}`);
53476
+ const agyHome = path61.join(os13.homedir(), ".gemini", "antigravity-cli");
53477
+ if (!fs38.existsSync(agyHome)) {
53478
+ line(tally, "ok", `Antigravity: not installed on this machine (no ${agyHome}) \u2014 nothing to register. Once it is: \`wairon mcp install --backend gemini --global\``);
53479
+ } else {
53480
+ const hg = mcpEntryHealth(path61.join(agyHome, "mcp_config.json"));
53481
+ line(tally, hg.mark, `Antigravity (global mcp_config.json): ${hg.note}${hg.mark === "ok" ? "" : " \u2014 run `wairon mcp install --backend gemini --global`"}`);
53482
+ }
52739
53483
  const projPath = fromProjectRoot(".gemini", "settings.json");
52740
53484
  if (fs38.existsSync(projPath)) {
52741
53485
  const hp = mcpEntryHealth(projPath);
@@ -52771,14 +53515,7 @@ async function applyFixes(options, tally) {
52771
53515
  } catch (e) {
52772
53516
  console.log(` ${icon("error")} Regeneration failed: ${e instanceof Error ? e.message : String(e)}`);
52773
53517
  }
52774
- try {
52775
- const pinned = pinInstalledPacksAsSelections();
52776
- if (pinned.length > 0) {
52777
- console.log(` ${icon("ok")} Recorded ${pinned.length} installed pack(s) as explicit selections: ${pinned.join(", ")}.`);
52778
- }
52779
- } catch (e) {
52780
- console.log(` ${icon("error")} Could not record pack selections: ${e instanceof Error ? e.message : String(e)}`);
52781
- }
53518
+ pinPackSelections("");
52782
53519
  try {
52783
53520
  const legacySpecs = findLegacySpecFiles();
52784
53521
  if (legacySpecs.length > 0) {
@@ -52798,63 +53535,157 @@ async function applyFixes(options, tally) {
52798
53535
  } catch (e) {
52799
53536
  console.log(` ${icon("error")} Chained-subproject backfill failed: ${e instanceof Error ? e.message : String(e)}`);
52800
53537
  }
53538
+ repairProjectSpecs("");
53539
+ await migrateChaining(options, tally);
53540
+ repairMembers();
53541
+ await repairMcpRegistration(options, targets);
53542
+ logger.blank();
53543
+ }
53544
+ function whoPrefix(who) {
53545
+ return who === "" ? "" : `[${who}] `;
53546
+ }
53547
+ function severityCounts(issues) {
53548
+ return {
53549
+ errs: issues.filter((i) => i.severity === "error").length,
53550
+ warns: issues.filter((i) => i.severity === "warning").length,
53551
+ notes: issues.filter((i) => i.severity === "notice").length
53552
+ };
53553
+ }
53554
+ function describeEnumProposal(p) {
53555
+ return `${p.kind} ${p.specId} ${p.path}: "${p.written}" \u2192 enum ${p.enumId} (${p.values.join(", ")}), written as ${p.enumId}${p.optional ? "?" : ""} (proposed)`;
53556
+ }
53557
+ function pinPackSelections(who) {
53558
+ try {
53559
+ const pinned = pinInstalledPacksAsSelections();
53560
+ if (pinned.length === 0) return;
53561
+ console.log(` ${icon("ok")} ${whoPrefix(who)}Recorded ${pinned.length} installed pack(s) as explicit selections: ${pinned.join(", ")}.`);
53562
+ const pinnedNames = new Set(pinned.map((label7) => label7.replace(/@[^@]*$/, "")));
53563
+ const unobtainable = (loadProjectConfig()?.extensions?.packs ?? []).filter((e) => typeof e !== "string").filter((e) => pinnedNames.has(e.name) && !e.source && !e.bundle).map((e) => e.name);
53564
+ if (unobtainable.length > 0) {
53565
+ console.log(` ${icon("warn")} ${whoPrefix(who)}${unobtainable.length} of them record no fetchable source (installed from a local path), so CI and a fresh clone cannot obtain them: ${unobtainable.join(", ")}. Run \`wairon pack bundle --all\` to commit a copy the repository carries, or \`wairon pack use <name> --source <url>\` to record where \`wairon pack sync\` fetches each.`);
53566
+ }
53567
+ } catch (e) {
53568
+ console.log(` ${icon("error")} ${whoPrefix(who)}Could not record pack selections: ${e instanceof Error ? e.message : String(e)}`);
53569
+ }
53570
+ }
53571
+ function repairProjectSpecs(who) {
53572
+ const at = whoPrefix(who);
52801
53573
  try {
52802
53574
  const retirement = retireSpecialists(true);
52803
53575
  if (retirement.retyped.length > 0) {
52804
- console.log(` ${icon("ok")} Retired ${retirement.retyped.length} Specialist(s) to Orchestrator.`);
53576
+ console.log(` ${icon("ok")} ${at}Retired ${retirement.retyped.length} Specialist(s) to Orchestrator.`);
52805
53577
  }
52806
53578
  if (retirement.rebasedVariants.length > 0) {
52807
- console.log(` ${icon("ok")} Rebased ${retirement.rebasedVariants.length} variant(s) from Specialist to Orchestrator: ${retirement.rebasedVariants.join(", ")}.`);
53579
+ console.log(` ${icon("ok")} ${at}Rebased ${retirement.rebasedVariants.length} variant(s) from Specialist to Orchestrator: ${retirement.rebasedVariants.join(", ")}.`);
52808
53580
  }
52809
53581
  } catch (e) {
52810
- console.log(` ${icon("error")} Specialist retirement failed: ${e instanceof Error ? e.message : String(e)}`);
53582
+ console.log(` ${icon("error")} ${at}Specialist retirement failed: ${e instanceof Error ? e.message : String(e)}`);
52811
53583
  }
52812
53584
  try {
52813
53585
  const repairs = repairForeignStepFields(true);
52814
53586
  if (repairs.length > 0) {
52815
- console.log(` ${icon("ok")} Dropped foreign fields from ${repairs.length} narrative step(s):`);
53587
+ console.log(` ${icon("ok")} ${at}Dropped foreign fields from ${repairs.length} narrative step(s):`);
52816
53588
  for (const repair of repairs) {
52817
53589
  console.log(` ${repair.implementation}.${repair.method} step ${repair.stepNumber} (${repair.stepType}): ${repair.fields.join(", ")}`);
52818
53590
  }
52819
53591
  }
52820
53592
  } catch (e) {
52821
- console.log(` ${icon("error")} Narrative-step repair failed: ${e instanceof Error ? e.message : String(e)}`);
53593
+ console.log(` ${icon("error")} ${at}Narrative-step repair failed: ${e instanceof Error ? e.message : String(e)}`);
52822
53594
  }
52823
53595
  try {
52824
53596
  const signatures = repairSignatures(true);
52825
53597
  if (signatures.length > 0) {
52826
- console.log(` ${icon("ok")} Rewrote ${signatures.length} spec(s) into their stored signature form:`);
53598
+ console.log(` ${icon("ok")} ${at}Rewrote ${signatures.length} spec(s) into their stored signature form:`);
52827
53599
  for (const repair of signatures) {
52828
53600
  console.log(` ${repair.kind} ${repair.specId}: ${describeSignatureRepair(repair)}`);
52829
53601
  }
52830
53602
  }
52831
53603
  } catch (e) {
52832
- console.log(` ${icon("error")} Signature repair failed: ${e instanceof Error ? e.message : String(e)}`);
53604
+ console.log(` ${icon("error")} ${at}Signature repair failed: ${e instanceof Error ? e.message : String(e)}`);
52833
53605
  }
52834
53606
  try {
52835
53607
  const spellings = repairTypeSpellings(true);
52836
53608
  const rewritten = spellings.filter((repair) => repair.rewritten.length > 0);
52837
53609
  if (rewritten.length > 0) {
52838
53610
  const positions = rewritten.reduce((n, repair) => n + repair.rewritten.length, 0);
52839
- console.log(` ${icon("ok")} Rewrote ${positions} type position(s) in ${rewritten.length} spec(s) into their canonical spelling.`);
53611
+ console.log(` ${icon("ok")} ${at}Rewrote ${positions} type position(s) in ${rewritten.length} spec(s) into their canonical spelling.`);
52840
53612
  }
52841
53613
  const proposals = spellings.flatMap((repair) => repair.proposals);
52842
53614
  if (proposals.length > 0) {
52843
- console.log(` ${icon("warn")} ${proposals.length} number position(s) with int proposed \u2014 not written; confirm by writing int, or write float:`);
53615
+ console.log(` ${icon("warn")} ${at}${proposals.length} number position(s) with int proposed \u2014 not written; confirm by writing int, or write float:`);
52844
53616
  for (const proposal of proposals) console.log(` ${proposal.kind} ${proposal.specId} ${proposal.path}: "${proposal.written}" \u2192 ${proposal.stored} (proposed)`);
52845
53617
  }
53618
+ const enumProposals = spellings.flatMap((repair) => repair.enumProposals);
53619
+ if (enumProposals.length > 0) {
53620
+ console.log(` ${icon("warn")} ${at}${enumProposals.length} string-literal union(s) with an enum proposed \u2014 not written; define the enum (sdd_add_type kind enum), then write its id:`);
53621
+ for (const proposal of enumProposals) console.log(` ${describeEnumProposal(proposal)}`);
53622
+ }
52846
53623
  const authorNeeded = spellings.flatMap((repair) => repair.authorNeeded);
52847
53624
  if (authorNeeded.length > 0) {
52848
- console.log(` ${icon("warn")} ${authorNeeded.length} type position(s) need an author \u2014 no rewrite can settle them:`);
53625
+ console.log(` ${icon("warn")} ${at}${authorNeeded.length} type position(s) need an author \u2014 no rewrite can settle them:`);
52849
53626
  for (const problem of authorNeeded) {
52850
53627
  console.log(` ${problem.kind} ${problem.specId} ${problem.path ?? ""}: "${problem.written}" \u2014 ${problem.replacement ? `write ${problem.replacement}` : problem.detail}`);
52851
53628
  }
52852
53629
  }
52853
53630
  } catch (e) {
52854
- console.log(` ${icon("error")} Type-spelling repair failed: ${e instanceof Error ? e.message : String(e)}`);
53631
+ console.log(` ${icon("error")} ${at}Type-spelling repair failed: ${e instanceof Error ? e.message : String(e)}`);
52855
53632
  }
52856
- await migrateChaining(options, tally);
52857
- await repairMcpRegistration(options, targets);
53633
+ }
53634
+ function memberProjects() {
53635
+ const root = getProjectRoot();
53636
+ return graph().nodes.filter((node) => node.namespace !== "" && node.directory && pathExists(node.directory)).map((node) => ({ alias: node.mountAlias ?? node.namespace, directory: node.directory, inside: !isOutsideRoot(root, node.directory) }));
53637
+ }
53638
+ function repairMembers() {
53639
+ let members;
53640
+ try {
53641
+ members = memberProjects();
53642
+ } catch (e) {
53643
+ console.log(` ${icon("error")} Could not read the family's members: ${e instanceof Error ? e.message : String(e)}`);
53644
+ return;
53645
+ }
53646
+ for (const member of members) {
53647
+ if (!member.inside) {
53648
+ console.log(` ${icon("warn")} [${member.alias}] lies outside this project (${member.directory}): run \`wairon doctor --fix\` there to upgrade it.`);
53649
+ continue;
53650
+ }
53651
+ runWithProjectRoot(member.directory, () => {
53652
+ if (!projectConfigExists()) return;
53653
+ pinPackSelections(member.alias);
53654
+ repairProjectSpecs(member.alias);
53655
+ });
53656
+ }
53657
+ }
53658
+ function reportMemberRepairs(tally) {
53659
+ const pending4 = [];
53660
+ for (const member of memberProjects()) {
53661
+ const counts = runWithProjectRoot(member.directory, () => {
53662
+ if (!projectConfigExists()) return null;
53663
+ const spellings = repairTypeSpellings(false);
53664
+ return {
53665
+ specialists: retireSpecialists(false).retyped.length,
53666
+ stepFields: repairForeignStepFields(false).length,
53667
+ signatures: repairSignatures(false).length,
53668
+ respell: spellings.reduce((n, r) => n + r.rewritten.length, 0),
53669
+ proposals: spellings.reduce((n, r) => n + r.proposals.length + r.enumProposals.length, 0),
53670
+ authorNeeded: spellings.reduce((n, r) => n + r.authorNeeded.length, 0)
53671
+ };
53672
+ });
53673
+ if (!counts) continue;
53674
+ const parts = [
53675
+ counts.specialists ? `${counts.specialists} Specialist(s)` : "",
53676
+ counts.stepFields ? `${counts.stepFields} step(s) with foreign fields` : "",
53677
+ counts.signatures ? `${counts.signatures} spec(s) with stale signatures` : "",
53678
+ counts.respell ? `${counts.respell} type position(s) to respell` : "",
53679
+ counts.proposals ? `${counts.proposals} proposal(s)` : "",
53680
+ counts.authorNeeded ? `${counts.authorNeeded} position(s) needing an author` : ""
53681
+ ].filter(Boolean);
53682
+ if (parts.length === 0) continue;
53683
+ const where = member.inside ? "`wairon doctor --fix` here cascades into it" : `run \`wairon doctor --fix\` in ${member.directory}`;
53684
+ pending4.push(`member ${member.alias}: ${parts.join(", ")} \u2014 ${where}`);
53685
+ }
53686
+ if (pending4.length === 0) return;
53687
+ console.log(import_chalk17.default.bold("Members"));
53688
+ for (const text3 of pending4) line(tally, "warn", text3);
52858
53689
  logger.blank();
52859
53690
  }
52860
53691
  async function repairMcpRegistration(options, targets) {
@@ -52949,11 +53780,11 @@ function reportLabel(key) {
52949
53780
  return key === "" ? "(this project)" : key;
52950
53781
  }
52951
53782
  function printUpgradeReport(report4) {
52952
- console.log(import_chalk17.default.bold("Composed validation: what stage 4 changed"));
53783
+ console.log(import_chalk17.default.bold("Composed validation: what it changed since each lock"));
52953
53784
  console.log(import_chalk17.default.gray(" A lock records totals only (errors, warnings, notices) and the validator version that took it \u2014 nothing per code \u2014 so this is not a per-code diff. Today's findings are classed by a reason computed now."));
52954
53785
  for (const p of report4.projects) {
52955
53786
  const locked = p.lockedTotals ? `locked by v${p.lockedBy}: ${p.lockedTotals}` : "never locked";
52956
- const attribution = p.lockedTotals ? p.predatesStage4 ? " \u2014 the lock predates stage 4, so a difference is the upgrade's" : " \u2014 the lock was taken by a stage-4 validator, so a difference is not the upgrade's" : "";
53787
+ const attribution = p.lockedTotals ? p.predatesStage4 ? " \u2014 the lock predates composed validation, so a difference is the upgrade's" : " \u2014 the lock was taken under composed validation, so a difference is not the upgrade's" : "";
52957
53788
  console.log(` ${reportLabel(p.key)}: ${locked}; today (as-complete): ${p.currentTotals}${attribution}`);
52958
53789
  }
52959
53790
  const byCode = /* @__PURE__ */ new Map();
@@ -52962,12 +53793,12 @@ function printUpgradeReport(report4) {
52962
53793
  console.log(` ${import_chalk17.default.bold(code)} (${entries.length})`);
52963
53794
  for (const e of entries) {
52964
53795
  const where = `${reportLabel(e.project)}${e.specId ? ` ${e.specId}` : ""}`;
52965
- const reason = e.reason === "escalated" ? "escalated: a stage-2 notice that is an error since stage 4" : e.reason === "pinned" ? "pinned: judged against the project's own pin, no longer through the parent's live tree" : `positional: the family's top matches it${e.resolvedAs ? ` to ${e.resolvedAs}` : ""} by position, which is how it passed before${e.rewrite ? ` \u2014 the migration writes ${e.rewrite}` : ""}`;
53796
+ const reason = e.reason === "escalated" ? "escalated: once a notice, an error under composed validation" : e.reason === "pinned" ? "pinned: judged against the project's own pin, no longer through the parent's live tree" : `positional: the family's top matches it${e.resolvedAs ? ` to ${e.resolvedAs}` : ""} by position, which is how it passed before${e.rewrite ? ` \u2014 the migration writes ${e.rewrite}` : ""}`;
52966
53797
  console.log(` ${e.severity} ${where}: ${reason}`);
52967
53798
  }
52968
53799
  }
52969
- if (report4.entries.length === 0) console.log(" No finding is new for a reason stage 4 introduced.");
52970
- console.log(` ${report4.unclassified} finding(s) could not be attributed to stage 4.`);
53800
+ if (report4.entries.length === 0) console.log(" No finding is new for a reason composed validation introduced.");
53801
+ console.log(` ${report4.unclassified} finding(s) could not be attributed to composed validation.`);
52971
53802
  if (report4.unmatched.length > 0) {
52972
53803
  console.log(` ${import_chalk17.default.bold("Positional matches no rule decides")} (${report4.unmatched.length}) \u2014 counted above; a person picks:`);
52973
53804
  for (const m of report4.unmatched) {
@@ -53131,7 +53962,7 @@ function printProjectMigration(p) {
53131
53962
  console.log(` will be removed from its stored L0 by the first write (the schema does not know them): ${p.droppedKeys.join("; ")}`);
53132
53963
  }
53133
53964
  for (const x of p.externals) {
53134
- const why = x.reason === "legacy-pin" ? "replaces the stage-1 family pin of" : "its references cross into";
53965
+ const why = x.reason === "legacy-pin" ? "replaces the legacy family pin of" : "its references cross into";
53135
53966
  console.log(` external: ${x.alias}: {} \u2014 ${why} ${projectLabel2(x.producer)}`);
53136
53967
  }
53137
53968
  for (const i of p.imports) {
@@ -53147,10 +53978,10 @@ function printProjectMigration(p) {
53147
53978
  if (m.retired.length > 0) console.log(` retires with the mount: ${m.retired.join("; ")}`);
53148
53979
  }
53149
53980
  if (p.supersededPins.length > 0) {
53150
- console.log(` superseded family pins: ${p.supersededPins.length} \u2014 deleted once the externals that replace them are pinned; nothing reads them since stage 3`);
53981
+ console.log(` superseded family pins: ${p.supersededPins.length} \u2014 deleted once the externals that replace them are pinned; nothing reads them any more`);
53151
53982
  }
53152
53983
  for (const alias of p.locations) {
53153
- console.log(` member: ${alias}: the deprecated long-form \`path\` rewritten to \`source\` (stage 8) \u2014 its meaning unchanged`);
53984
+ console.log(` member: ${alias}: the deprecated long-form \`path\` rewritten to \`source\` \u2014 its meaning unchanged`);
53154
53985
  }
53155
53986
  }
53156
53987
  function describeEntry(e) {
@@ -53220,13 +54051,13 @@ function writeArtifact(dest, content) {
53220
54051
  fs39.writeFileSync(dest, content, "utf-8");
53221
54052
  }
53222
54053
  function parseSequenceRef(ref) {
53223
- const sep18 = ref.includes(":") ? ref.lastIndexOf(":") : ref.lastIndexOf(".");
53224
- if (sep18 <= 0 || sep18 === ref.length - 1) {
54054
+ const sep19 = ref.includes(":") ? ref.lastIndexOf(":") : ref.lastIndexOf(".");
54055
+ if (sep19 <= 0 || sep19 === ref.length - 1) {
53225
54056
  throw new WaironError(
53226
54057
  `Invalid --sequence reference "${ref}". Use <componentId>:<methodName> (e.g. billing-portal:authorize).`
53227
54058
  );
53228
54059
  }
53229
- return { component: ref.slice(0, sep18), method: ref.slice(sep18 + 1) };
54060
+ return { component: ref.slice(0, sep19), method: ref.slice(sep19 + 1) };
53230
54061
  }
53231
54062
  function canvasRelations(options) {
53232
54063
  if (options.health === false) return void 0;
@@ -53378,8 +54209,8 @@ function withoutRoot(record2) {
53378
54209
  return rest;
53379
54210
  }
53380
54211
  function normalizeMemberPath(p) {
53381
- const posix6 = path64.posix.normalize(p.replace(/\\/g, "/"));
53382
- return posix6.replace(/^\.\/+/, "").replace(/\/+$/, "");
54212
+ const posix8 = path64.posix.normalize(p.replace(/\\/g, "/"));
54213
+ return posix8.replace(/^\.\/+/, "").replace(/\/+$/, "");
53383
54214
  }
53384
54215
  function projectRoot(dataDir, id) {
53385
54216
  return path64.join(dataDir, "projects", id);
@@ -57084,12 +57915,13 @@ function mintKey(cfg, credential, project2, role) {
57084
57915
  "instance-wide super-admin (*:*) keys cannot be minted \u2014 the built-in admin account (WAIRON_ADMIN_USER) is the only super-admin"
57085
57916
  );
57086
57917
  }
57918
+ const narrowed2 = assertMintableNarrowingEntry(cfg.dataDir, project2);
57087
57919
  const token = "wk_" + crypto17.randomBytes(24).toString("hex");
57088
57920
  const record2 = {
57089
57921
  id: crypto17.randomBytes(6).toString("hex"),
57090
57922
  keyHash: hashToken(token),
57091
57923
  role,
57092
- projects: project2 === "*" ? ["*"] : [project2],
57924
+ projects: [narrowed2],
57093
57925
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
57094
57926
  };
57095
57927
  createCredential(cfg.dataDir, record2);
@@ -57182,7 +58014,7 @@ function refuseDesignErrors(gate) {
57182
58014
  function refuseUnapprovedMembers(required, members) {
57183
58015
  const unapproved = members.filter((m) => m.as !== "part" && m.state !== "approved");
57184
58016
  if (!required || unapproved.length === 0) return;
57185
- const named2 = unapproved.map((m) => `${m.alias ?? m.key} (${m.state}${m.upgraded ? " \u2014 approved under the pre-stage-5 identity, re-lock it once" : ""})`);
58017
+ const named2 = unapproved.map((m) => `${m.alias ?? m.key} (${m.state}${m.upgraded ? " \u2014 approved under an earlier gate identity, re-lock it once" : ""})`);
57186
58018
  throw new LockRefusedError2(
57187
58019
  `composition.requireApprovedMembers: direct member(s) not approved \u2014 ${named2.join(", ")}. Lock each at its own root first; a parent never approves below itself. Nothing was written.`
57188
58020
  );
@@ -61364,7 +62196,7 @@ async function awaitApproval(cfg, credential, requestId, timeoutSeconds) {
61364
62196
  let current2 = req;
61365
62197
  while (current2.status === "pending" && Date.now() < deadline) {
61366
62198
  const remaining = deadline - Date.now();
61367
- await new Promise((resolve55) => setTimeout(resolve55, Math.min(AWAIT_POLL_INTERVAL_MS, remaining)));
62199
+ await new Promise((resolve56) => setTimeout(resolve56, Math.min(AWAIT_POLL_INTERVAL_MS, remaining)));
61368
62200
  expirePendingApprovals(cfg.dataDir, (/* @__PURE__ */ new Date()).toISOString());
61369
62201
  current2 = getApprovalRequestById(cfg.dataDir, requestId) ?? current2;
61370
62202
  }
@@ -66963,8 +67795,9 @@ function stop() {
66963
67795
  var DEV_UNIT_ID = "local";
66964
67796
  function bootstrapInstance(config) {
66965
67797
  ensureInstanceIdentity(config.dataDir);
66966
- if (config.devMode && !getOrganizationUnit(config.dataDir, DEV_UNIT_ID)) {
66967
- const system = { userId: "system", kind: "service", issuer: "local" };
67798
+ if (!config.devMode) return;
67799
+ const system = { userId: "system", kind: "service", issuer: "local" };
67800
+ if (!getOrganizationUnit(config.dataDir, DEV_UNIT_ID)) {
66968
67801
  createUnit(config.dataDir, {
66969
67802
  id: DEV_UNIT_ID,
66970
67803
  name: "Local Development",
@@ -66976,6 +67809,18 @@ function bootstrapInstance(config) {
66976
67809
  createdBy: system
66977
67810
  });
66978
67811
  }
67812
+ const placed = new Set(listProjectPlacements(config.dataDir).map((p) => p.projectId));
67813
+ for (const record2 of listProjectRecords(config.dataDir)) {
67814
+ if (placed.has(record2.id) || record2.parentProjectId !== void 0) continue;
67815
+ placeProject(config.dataDir, {
67816
+ id: "",
67817
+ projectId: record2.id,
67818
+ unitId: DEV_UNIT_ID,
67819
+ role: "owner",
67820
+ createdAt: "",
67821
+ createdBy: system
67822
+ });
67823
+ }
66979
67824
  }
66980
67825
 
66981
67826
  // src/server/identity-portal.ts
@@ -68312,11 +69157,11 @@ async function runServe(options = {}) {
68312
69157
  logger.info(` data dir: ${import_chalk18.default.gray(cfg.dataDir)}`);
68313
69158
  logger.blank();
68314
69159
  logger.info("Press Ctrl+C to stop.");
68315
- await new Promise((resolve55) => {
69160
+ await new Promise((resolve56) => {
68316
69161
  const shutdown = () => {
68317
69162
  logger.info("Shutting down\u2026");
68318
69163
  handle.close();
68319
- resolve55();
69164
+ resolve56();
68320
69165
  };
68321
69166
  process.on("SIGINT", shutdown);
68322
69167
  process.on("SIGTERM", shutdown);
@@ -68381,11 +69226,11 @@ async function runDev(options = {}) {
68381
69226
  logger.blank();
68382
69227
  logger.info("An agent edits specs; refresh the page to see the live graph. Press Ctrl+C to stop.");
68383
69228
  if (options.open) openBrowser(url);
68384
- await new Promise((resolve55) => {
69229
+ await new Promise((resolve56) => {
68385
69230
  const shutdown = () => {
68386
69231
  logger.info("Shutting down\u2026");
68387
69232
  handle.close();
68388
- resolve55();
69233
+ resolve56();
68389
69234
  };
68390
69235
  process.on("SIGINT", shutdown);
68391
69236
  process.on("SIGTERM", shutdown);
@@ -68491,7 +69336,9 @@ async function runHostUnit(action, options = {}) {
68491
69336
  id: qualifiedId2,
68492
69337
  name: options.name ?? options.slug,
68493
69338
  slug: options.slug,
68494
- kind: options.kind ?? "team",
69339
+ // A top-level unit must be a business_entity (the org-unit hierarchy's
69340
+ // root kind), so that is the default without --parent; under a parent, team.
69341
+ kind: options.kind ?? (options.parent ? "team" : "business_entity"),
68495
69342
  ...options.parent ? { parentId: options.parent } : {},
68496
69343
  status: "active",
68497
69344
  createdAt: "",
@@ -68580,6 +69427,21 @@ function resolveScopeOptions(options) {
68580
69427
  if (options.unit) return { kind: "unit", id: options.unit };
68581
69428
  return { kind: "instance" };
68582
69429
  }
69430
+ function warnIfUnreadable(cfg, cred, userId, scope, why) {
69431
+ const target = scope.kind === "subsystem" ? { kind: "project", id: (scope.id ?? "").split("/")[0] } : scope;
69432
+ let read2;
69433
+ try {
69434
+ read2 = explain3(cfg, cred, userId, "project:read", target.kind, target.id ?? "").value;
69435
+ } catch {
69436
+ return;
69437
+ }
69438
+ if (read2 === "yes") return;
69439
+ const where = target.kind === "instance" ? "the instance" : `${target.kind} ${target.id}`;
69440
+ const flag = target.kind === "instance" ? "--instance" : `--${target.kind} ${target.id}`;
69441
+ logger.warn(
69442
+ `project:read for "${userId}" at ${where} resolves to ${read2} \u2014 ${why}. Grant it with \`wairon host permission set --user ${userId} --capability project:read ${flag}\`.`
69443
+ );
69444
+ }
68583
69445
  async function runHostPermission(action, options = {}) {
68584
69446
  const cfg = resolveHostConfig(options);
68585
69447
  const cred = masterCredential();
@@ -68608,6 +69470,9 @@ async function runHostPermission(action, options = {}) {
68608
69470
  logger.success(
68609
69471
  `Assignment ${stored.id}: ${options.user} ${options.capability}=${value} @ ${scope.kind}${scope.id ? ` ${scope.id}` : ""}`
68610
69472
  );
69473
+ if (options.capability === "project:write" && (value === "yes" || value === "approval")) {
69474
+ warnIfUnreadable(cfg, cred, options.user, scope, `${options.user} can write here but cannot read it`);
69475
+ }
68611
69476
  break;
68612
69477
  }
68613
69478
  case "list": {
@@ -68661,7 +69526,16 @@ async function runHostKey(action, options = {}) {
68661
69526
  console.log(` ${import_chalk18.default.bold(minted.token)}`);
68662
69527
  logger.blank();
68663
69528
  for (const line2 of minted.mapped) {
68664
- logger.warn(`Narrowing ${line2}: a member is a record of its own since stage 7, so the token names it by its id (qualified entries are deprecated).`);
69529
+ logger.warn(`Narrowing ${line2}: a member is a record of its own, so the token names it by its id (qualified entries are deprecated).`);
69530
+ }
69531
+ if (options.project !== "*") {
69532
+ warnIfUnreadable(
69533
+ cfg,
69534
+ cred,
69535
+ options.owner,
69536
+ { kind: "project", id: options.project },
69537
+ `the token acts as "${options.owner}"'s live permission, so it cannot read the project either`
69538
+ );
68665
69539
  }
68666
69540
  break;
68667
69541
  }
@@ -69065,14 +69939,23 @@ async function runExport(out) {
69065
69939
  }
69066
69940
  const approval = verdict.state;
69067
69941
  const design = exportDesign3(out, approval);
69942
+ const members = memberNote(design);
69068
69943
  if (out) {
69069
69944
  logger.success(
69070
69945
  `Design export of "${design.project.name}" written to ${path92.resolve(out)} (${design.components.length} component(s), ${design.types.length} type(s); approval: ${design.source.approval}).`
69071
69946
  );
69947
+ if (members) logger.info(members);
69072
69948
  return;
69073
69949
  }
69074
69950
  process.stdout.write(`${JSON.stringify(design, null, 2)}
69075
69951
  `);
69952
+ if (members && design.components.length === 0 && design.types.length === 0) process.stderr.write(`${members}
69953
+ `);
69954
+ }
69955
+ function memberNote(design) {
69956
+ const members = design.dependencies.filter((d) => d.role === "member").map((d) => d.alias);
69957
+ if (members.length === 0) return null;
69958
+ return `${members.length} member project(s) are listed under \`dependencies\`, not inlined (${members.join(", ")}): run \`wairon export\` in a member's own root for its design.`;
69076
69959
  }
69077
69960
 
69078
69961
  // src/commands/externals.ts
@@ -69295,16 +70178,16 @@ program.command("generate").description("Generate agent output files from the sp
69295
70178
  dryRun: opts.dryRun
69296
70179
  });
69297
70180
  });
69298
- program.command("lock-check").description("Merge gate: is the design in this working tree the design that was approved? Compares the tree's gate identity against the committed .wai/lock.json. Exits 1 when the design moved past its approval; passes with a notice when the project never locked, unless --strict.").option("--strict", "also fail when nothing was ever approved, or when there is no spec tree at all").action(async (opts) => {
70181
+ program.command("lock-check").description("Merge gate: is the design in this working tree the design that was approved? Compares the tree's gate identity against .wai/lock.json as it is in the working tree (in CI, the file committed in the checked-out revision; locally, an uncommitted record counts too). Exits 1 when the design moved past its approval; passes with a notice when the project never locked, unless --strict.").option("--strict", "also fail when nothing was ever approved, or when there is no spec tree at all").action(async (opts) => {
69299
70182
  await lockCheckCommand(opts);
69300
70183
  });
69301
- program.command("lock").description("Final check before implementation: validate the spec tree as complete, freeze all specs to complete, and (re)generate the agent topology \u2014 only if it validates. In an attached checkout, locks the hosted project instead.").option("-y, --yes", "skip the confirmation prompt (for scripts / CI)").option("--subsystem <id>", "only lock specs in the specified subsystem").option("--no-recursive", "do not recursively validate subprojects").action(async (opts) => {
70184
+ program.command("lock").description("Record human approval of the design: validate the spec tree as complete, write the approval to .wai/lock.json (one digest per spec; spec files are not rewritten), and refresh this project's generated outputs \u2014 only if the design validates. Commit .wai/lock.json; `wairon lock-check` gates on it. In an attached checkout, locks the hosted project instead.").option("-y, --yes", "skip the confirmation prompt (for scripts / CI)").option("--subsystem <id>", "only lock specs in the specified subsystem").option("--no-recursive", "accepted for one release and ignored: a lock approves this project only; each member locks at its own root").action(async (opts) => {
69302
70185
  await lockCommand(opts);
69303
70186
  });
69304
- program.command("validate").description("Validate the project configuration and the SDD Spec Tree").option("--ci", "treat warnings as errors for CI pipelines (notices are printed and counted, never fatal)").option("--subsystem <id>", "only validate the specified subsystem (granular)").option("--no-recursive", "at a project that declares members: run the owner's gate alone instead of the family run").option("--family", "run the family run from here: every member's own gate, and this project's externals composed against their live producers").action(async (opts) => {
70187
+ program.command("validate").description("Validate the project configuration and the SDD Spec Tree").option("--ci", "treat warnings as errors for CI pipelines (notices are printed and counted, never fatal)").option("--subsystem <id>", "only validate the specified subsystem (granular)").option("--no-recursive", "at a project that declares members: run the owner's gate alone instead of the family run").option("--family", "run the family run from here: every member's own gate, and this project's externals composed against their live producers").option("--all", "print every finding (by default the first 100 per severity are printed, followed by per-code totals)").action(async (opts) => {
69305
70188
  await validateCommand(opts);
69306
70189
  });
69307
- program.command("status").description("Show a hierarchical completeness graph of the SDD Spec Tree").option("--subsystem <id>", "only show status for the specified subsystem").option("--no-recursive", "do not recursively show status for subprojects").action(async (opts) => {
70190
+ program.command("status").description("Show the spec tree with its authoring readiness (how far each spec is written out, from its draft/design/complete status) and, separately, its approval state from .wai/lock.json").option("--subsystem <id>", "only show status for the specified subsystem").option("--no-recursive", "show this project only, without its members").action(async (opts) => {
69308
70191
  await statusCommand(opts);
69309
70192
  });
69310
70193
  program.command("doctor").description("Health check: flags stale generated guides/skills, an unregistered MCP server, spec-tree issues and the chaining migration still pending").option("--fix", "regenerate stale in-project guides/context/skills, register the MCP server, then apply the chaining migration once confirmed").option("--report <section>", "print one section's report and nothing else, writing nothing (chaining | composed-validation); never combines with --fix").option("-y, --yes", "answer the chaining migration's confirmation (for a non-interactive --fix); a write outside the project root also needs --global").option("--global", "consent to the --fix writes outside the project root (a machine-wide MCP config, the legacy global plugin); with --yes it answers their confirmation, and each replaced file is backed up beside itself").action(async (opts) => {
@@ -69484,7 +70367,7 @@ hostCmd.command("project <action>").description("create | list | destroy a hoste
69484
70367
  hostCmd.command("demo").description("provision a project seeded with a rich example spec tree, so the canvas has content to render").option("--id <id>", "project id", "demo").option("--unit <unitId>", "owner unit id (created if absent)", "demo").option("--force", "destroy and reseed the project if it already exists").option("--data-dir <path>", "data root (default WAIRON_DATA_DIR or ~/.wairon/data)").action(async (opts) => {
69485
70368
  await runHostDemo({ id: opts.id, unit: opts.unit, force: opts.force, dataDir: opts.dataDir });
69486
70369
  });
69487
- hostCmd.command("unit <action>").description("create an organization unit (projects are placed into units at creation)").option("--slug <slug>", "unit slug (REQUIRED for create; a root unit's id IS its slug)").option("--name <name>", "display name (defaults to the slug)").option("--kind <kind>", "unit kind (business_entity | department | team | \u2026)", "team").option("--parent <unitId>", "qualified id of the parent unit (omit for a root unit)").option("--data-dir <path>", "data root").action(async (action, opts) => {
70370
+ hostCmd.command("unit <action>").description("create an organization unit (projects are placed into units at creation)").option("--slug <slug>", "unit slug (REQUIRED for create; a root unit's id IS its slug)").option("--name <name>", "display name (defaults to the slug)").option("--kind <kind>", "unit kind (business_entity | department | team | \u2026); default business_entity for a top-level unit, team under --parent").option("--parent <unitId>", "qualified id of the parent unit (omit for a root unit)").option("--data-dir <path>", "data root").action(async (action, opts) => {
69488
70371
  await runHostUnit(action, { slug: opts.slug, name: opts.name, kind: opts.kind, parent: opts.parent, dataDir: opts.dataDir });
69489
70372
  });
69490
70373
  hostCmd.command("doctor").description("migrate a hosted data dir to the permission model (grants \u2192 assignments, owners, unit ids, placements); dry-run without --fix").option("--fix", "apply the migration (REQUIRED at rollout \u2014 legacy users/tokens otherwise resolve to zero permissions)").option("--data-dir <path>", "data root").action(async (opts) => {