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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.102";
68
+ WAIRON_VERSION = "5.1.1-dev.103";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  SUPPORTED_ALIASES = ["wai"];
71
71
  SCAN_EXCLUDE_DIRS = /* @__PURE__ */ new Set([
@@ -219,6 +219,7 @@ __export(fs_exports, {
219
219
  listFilesRecursive: () => listFilesRecursive,
220
220
  pathExists: () => pathExists,
221
221
  readFileOrNull: () => readFileOrNull,
222
+ resolveProjectBinding: () => resolveProjectBinding,
222
223
  runWithHostedLookup: () => runWithHostedLookup,
223
224
  runWithProjectBinding: () => runWithProjectBinding,
224
225
  runWithProjectRoot: () => runWithProjectRoot,
@@ -441,22 +442,74 @@ function setProjectRoot(dir) {
441
442
  function getProjectRootOverride() {
442
443
  return projectRootOverride;
443
444
  }
444
- function findSystemRoot(startDir) {
445
+ function holdsSystemSpec(dir) {
446
+ for (const base of [".wai", ".wairon"]) {
447
+ const specs = path2.join(dir, base, "specs");
448
+ if (fs.existsSync(path2.join(specs, ".index.yaml")) || fs.existsSync(path2.join(specs, "system.yaml"))) return true;
449
+ if (base === ".wai" && fs.existsSync(path2.join(dir, ".wai"))) break;
450
+ }
451
+ return false;
452
+ }
453
+ function findRepositoryRoot(startDir) {
445
454
  let dir = path2.resolve(startDir);
446
455
  while (true) {
447
- const isWai = fs.existsSync(path2.join(dir, ".wai"));
448
- const isWairon = !isWai && fs.existsSync(path2.join(dir, ".wairon"));
449
- const base = isWai ? ".wai" : isWairon ? ".wairon" : null;
450
- if (base) {
451
- if (fs.existsSync(path2.join(dir, base, "specs", ".index.yaml")) || fs.existsSync(path2.join(dir, base, "specs", "system.yaml"))) {
452
- return dir;
453
- }
456
+ if (fs.existsSync(path2.join(dir, ".git"))) return dir;
457
+ const parent = path2.dirname(dir);
458
+ if (parent === dir) return null;
459
+ dir = parent;
460
+ }
461
+ }
462
+ function isAtOrBelow(outer, inner2) {
463
+ const rel2 = path2.relative(outer, inner2);
464
+ return rel2 === "" || !rel2.startsWith("..") && !path2.isAbsolute(rel2);
465
+ }
466
+ function declaredMemberDirs(projectRoot2) {
467
+ let parsed;
468
+ try {
469
+ const text3 = fs.readFileSync(path2.join(projectRoot2, ".wai", "project.yaml"), "utf-8");
470
+ parsed = yaml.load(text3);
471
+ } catch {
472
+ return [];
473
+ }
474
+ const members = parsed?.members;
475
+ if (!members || typeof members !== "object") return [];
476
+ const dirs = [];
477
+ for (const value of Object.values(members)) {
478
+ const declaration2 = value;
479
+ const source = typeof declaration2 === "string" ? declaration2 : typeof declaration2?.source === "string" ? declaration2.source : typeof declaration2?.path === "string" ? declaration2.path : void 0;
480
+ if (!source) continue;
481
+ const text3 = source.trim();
482
+ if (text3.startsWith("hosted:") || /^[a-z][a-z0-9+.-]*:\/\//i.test(text3) || /^[\w.-]+@[\w.-]+:/.test(text3) || path2.isAbsolute(text3)) continue;
483
+ dirs.push(path2.resolve(projectRoot2, text3));
484
+ }
485
+ return dirs;
486
+ }
487
+ function resolveProjectBinding(startDir) {
488
+ const start2 = path2.resolve(startDir);
489
+ const repositoryRoot2 = findRepositoryRoot(start2) ?? void 0;
490
+ const withRepo = (b) => repositoryRoot2 ? { ...b, repositoryRoot: repositoryRoot2 } : b;
491
+ let dir = start2;
492
+ while (true) {
493
+ if (holdsSystemSpec(dir)) return withRepo({ root: dir, via: dir === start2 ? "here" : "ancestor" });
494
+ if (repositoryRoot2 && dir === repositoryRoot2) break;
495
+ const parent = path2.dirname(dir);
496
+ if (parent === dir) return null;
497
+ dir = parent;
498
+ }
499
+ dir = path2.dirname(repositoryRoot2);
500
+ while (true) {
501
+ if (holdsSystemSpec(dir)) {
502
+ const declares2 = declaredMemberDirs(dir).some((member) => isAtOrBelow(repositoryRoot2, member) && isAtOrBelow(member, start2));
503
+ if (declares2) return withRepo({ root: dir, via: "declared-member" });
454
504
  }
455
505
  const parent = path2.dirname(dir);
456
506
  if (parent === dir) return null;
457
507
  dir = parent;
458
508
  }
459
509
  }
510
+ function findSystemRoot(startDir) {
511
+ return resolveProjectBinding(startDir)?.root ?? null;
512
+ }
460
513
  function getProjectRoot() {
461
514
  const scoped = requestRootStore.getStore()?.root;
462
515
  if (scoped) return scoped;
@@ -487,13 +540,14 @@ function aiDirAt(root, ...segments) {
487
540
  const base = !fs.existsSync(waiPath) && fs.existsSync(waironPath) ? waironPath : waiPath;
488
541
  return path2.join(base, ...segments);
489
542
  }
490
- var fs, path2, import_node_async_hooks, EOL_SAMPLE_BYTES, projectRootOverride, sameRoot, requestRootStore;
543
+ var fs, path2, import_node_async_hooks, yaml, EOL_SAMPLE_BYTES, projectRootOverride, sameRoot, requestRootStore;
491
544
  var init_fs = __esm({
492
545
  "src/utils/fs.ts"() {
493
546
  "use strict";
494
547
  fs = __toESM(require("fs"));
495
548
  path2 = __toESM(require("path"));
496
549
  import_node_async_hooks = require("async_hooks");
550
+ yaml = __toESM(require("js-yaml"));
497
551
  EOL_SAMPLE_BYTES = 64 * 1024;
498
552
  projectRootOverride = null;
499
553
  sameRoot = (a, b) => path2.resolve(a) === path2.resolve(b);
@@ -638,14 +692,14 @@ var init_git_source = __esm({
638
692
  // src/utils/yaml.ts
639
693
  function parseYaml(content, sourcePath) {
640
694
  try {
641
- return yaml.load(content);
695
+ return yaml2.load(content);
642
696
  } catch (err) {
643
697
  const loc = sourcePath ? ` (${sourcePath})` : "";
644
698
  throw new Error(`Failed to parse YAML${loc}: ${String(err)}`);
645
699
  }
646
700
  }
647
701
  function serializeYaml(value) {
648
- return yaml.dump(value, {
702
+ return yaml2.dump(value, {
649
703
  indent: 2,
650
704
  lineWidth: 100,
651
705
  noRefs: true,
@@ -660,11 +714,11 @@ function readYamlFile(filePath) {
660
714
  function writeYamlFile(filePath, value) {
661
715
  writeFile(filePath, serializeYaml(value));
662
716
  }
663
- var yaml;
717
+ var yaml2;
664
718
  var init_yaml = __esm({
665
719
  "src/utils/yaml.ts"() {
666
720
  "use strict";
667
- yaml = __toESM(require("js-yaml"));
721
+ yaml2 = __toESM(require("js-yaml"));
668
722
  init_fs();
669
723
  }
670
724
  });
@@ -897,6 +951,14 @@ var init_agent = __esm({
897
951
  * agent implements nothing.
898
952
  */
899
953
  typeMapping: import_zod2.z.array(import_zod2.z.string()).optional(),
954
+ /**
955
+ * Where an implementing agent may write CODE: each source file its
956
+ * implementations name, plus the folder they share below the project root as
957
+ * `<folder>/**`. Empty when nothing names a file yet (the instructions then
958
+ * ask the spawning session to name the planned files); absent for an agent
959
+ * that implements nothing.
960
+ */
961
+ codeFence: import_zod2.z.array(import_zod2.z.string()).optional(),
900
962
  /**
901
963
  * Set only on a brief for a member's agent (an id qualified `<alias>::`): the
902
964
  * member's directory relative to the root the brief was asked from. The
@@ -5442,7 +5504,7 @@ function extractTypesFromSignature(signature, returns) {
5442
5504
  let sigCleaned = signature.replace(/\/\/.*$/gm, "").replace(/#.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
5443
5505
  sigCleaned = sigCleaned.replace(/(?<=\S)\s*\([^)]*\)\s*$/, "");
5444
5506
  sigCleaned = sigCleaned.replace(/(?<=\S)\s+[-—:]\s+[a-z\s_-]+$/, "");
5445
- const returnsCleaned = returns.replace(/\/\/.*$/gm, "").replace(/#.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/[a-zA-Z0-9_-]+\s*\??\s*:/g, "");
5507
+ const returnsCleaned = returns.replace(/\/\/.*$/gm, "").replace(/#.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(?<![:\w-])[a-zA-Z0-9_-]+\s*\??\s*:(?!:)/g, "");
5446
5508
  types.push(...extractTypeIdentifiers(returnsCleaned));
5447
5509
  const openParen = sigCleaned.indexOf("(");
5448
5510
  const closeParen = sigCleaned.lastIndexOf(")");
@@ -5473,7 +5535,7 @@ function extractTypesFromSignature(signature, returns) {
5473
5535
  const colonIndex = param.indexOf(":");
5474
5536
  if (colonIndex !== -1) {
5475
5537
  const paramType = param.slice(colonIndex + 1).trim();
5476
- const paramTypeCleaned = paramType.replace(/[a-zA-Z0-9_-]+\s*\??\s*:/g, "");
5538
+ const paramTypeCleaned = paramType.replace(/(?<![:\w-])[a-zA-Z0-9_-]+\s*\??\s*:(?!:)/g, "");
5477
5539
  types.push(...extractTypeIdentifiers(paramTypeCleaned));
5478
5540
  }
5479
5541
  }
@@ -5671,10 +5733,10 @@ function stepGraph(method2) {
5671
5733
  const s = byNum.get(n);
5672
5734
  if (s.type !== "parallel" || s.endStep === void 0 || !s.branches?.length) continue;
5673
5735
  const entries = s.branches.map((b) => b.step).sort((a, b) => a - b);
5674
- const join64 = fallNext(s.endStep);
5736
+ const join66 = fallNext(s.endStep);
5675
5737
  for (let i = 0; i < entries.length; i++) {
5676
5738
  const armEnd = i + 1 < entries.length ? prevOf(entries[i + 1]) : s.endStep;
5677
- if (armEnd !== void 0 && armEnd >= entries[i]) armEndJoin.set(armEnd, join64);
5739
+ if (armEnd !== void 0 && armEnd >= entries[i]) armEndJoin.set(armEnd, join66);
5678
5740
  }
5679
5741
  }
5680
5742
  const successorsOf = (n) => {
@@ -5937,6 +5999,12 @@ function fieldTypesOf(facts, field) {
5937
5999
  function localTypesOf(facts, name) {
5938
6000
  return ownEntry(facts.localTypes, name) ?? NO_DECLARED_TYPES;
5939
6001
  }
6002
+ function memberTypesOf(facts, shape, property) {
6003
+ return ownEntry(facts.memberTypes, `${shape}.${property}`) ?? NO_DECLARED_TYPES;
6004
+ }
6005
+ function aliasTargetOf(facts, name) {
6006
+ return ownEntry(facts.aliasTargets, name);
6007
+ }
5940
6008
  function typeBindingOf(facts, name) {
5941
6009
  return ownEntry(facts.typeOnlyBindings, name) ?? ownEntry(facts.importBindings, name)?.from;
5942
6010
  }
@@ -12268,7 +12336,7 @@ function toOpenApiSet(snapshot) {
12268
12336
  }
12269
12337
  function isOpenApiDocument(body) {
12270
12338
  try {
12271
- const parsed = yaml2.load(body);
12339
+ const parsed = yaml3.load(body);
12272
12340
  return !!parsed && typeof parsed === "object" && typeof parsed.openapi === "string";
12273
12341
  } catch {
12274
12342
  return false;
@@ -12378,7 +12446,7 @@ function authFromSecurityScheme(scheme) {
12378
12446
  function fromOpenApi(document, projectName) {
12379
12447
  let parsed;
12380
12448
  try {
12381
- parsed = yaml2.load(document);
12449
+ parsed = yaml3.load(document);
12382
12450
  } catch (e) {
12383
12451
  throw new Error(`Invalid surface document: not parseable as JSON/YAML (${e instanceof Error ? e.message : String(e)})`);
12384
12452
  }
@@ -12494,11 +12562,11 @@ function fromOpenApi(document, projectName) {
12494
12562
  types
12495
12563
  });
12496
12564
  }
12497
- var yaml2, PRIMITIVE_SCHEMAS, SIGNATURE_EXTENSION, SCALAR_SCHEMA_TYPES;
12565
+ var yaml3, PRIMITIVE_SCHEMAS, SIGNATURE_EXTENSION, SCALAR_SCHEMA_TYPES;
12498
12566
  var init_openapi = __esm({
12499
12567
  "src/core/openapi.ts"() {
12500
12568
  "use strict";
12501
- yaml2 = __toESM(require("js-yaml"));
12569
+ yaml3 = __toESM(require("js-yaml"));
12502
12570
  init_models();
12503
12571
  PRIMITIVE_SCHEMAS = {
12504
12572
  string: { type: "string" },
@@ -13169,7 +13237,9 @@ function writeSurfaceFile(targetPath, snapshot, renderedSet) {
13169
13237
  const resolved = path14.resolve(targetPath);
13170
13238
  fs9.mkdirSync(path14.dirname(resolved), { recursive: true });
13171
13239
  if (!renderedSet || renderedSet.length === 0) {
13172
- writeYamlFile(resolved, snapshot);
13240
+ if (path14.extname(resolved).toLowerCase() === ".json") fs9.writeFileSync(resolved, `${JSON.stringify(snapshot, null, 2)}
13241
+ `);
13242
+ else writeYamlFile(resolved, snapshot);
13173
13243
  return [resolved];
13174
13244
  }
13175
13245
  if (renderedSet.length === 1) {
@@ -15249,17 +15319,47 @@ function buildCodeIndex(model) {
15249
15319
  };
15250
15320
  const possibleOriginsOf = (site, from) => {
15251
15321
  const proven = originOf2(site, from);
15252
- if (!site.field && !site.constructed && !site.via && !site.returnedBy && !site.enclosingClass) return proven;
15322
+ if (!site.field && !site.constructed && !site.via && !site.returnedBy && !site.enclosingClass && !site.receiverPath) {
15323
+ return proven;
15324
+ }
15253
15325
  const resolved = scopeOf3(site, from);
15254
15326
  if (!resolved) return proven;
15255
15327
  const { path: scope, facts: f } = resolved;
15256
15328
  const out = new Set(proven);
15257
- const widenThrough = (name, specifier) => {
15258
- const landings = specifier !== void 0 ? landingFrom(scope, specifier) : declarationsAt(scope).has(name) ? republishedFrom(scope) : NO_ORIGIN;
15329
+ const widenThroughAt = (at, name, specifier) => {
15330
+ const landings = specifier !== void 0 ? landingFrom(at, specifier) : declarationsAt(at).has(name) ? republishedFrom(at) : NO_ORIGIN;
15259
15331
  for (const candidate of landings) out.add(candidate);
15260
15332
  };
15261
- const widenThroughType = (typeName) => widenThrough(typeName, typeBindingOf(f, typeName));
15333
+ const widenThrough = (name, specifier) => widenThroughAt(scope, name, specifier);
15334
+ const widenThroughType = (typeName, at = scope, atFacts = f) => {
15335
+ widenThroughAt(at, typeName, typeBindingOf(atFacts, typeName));
15336
+ for (const decl of typeDeclarationsOf(at, atFacts, typeName)) {
15337
+ const target = aliasTargetOf(decl.facts, decl.name);
15338
+ if (target !== void 0) widenThroughAt(decl.path, target, typeBindingOf(decl.facts, target));
15339
+ }
15340
+ };
15262
15341
  if (site.field) for (const typeName of fieldTypesOf(f, site.field)) widenThroughType(typeName);
15342
+ if (site.receiverPath && site.receiverPath.length >= 2) {
15343
+ const [root, ...rest] = site.receiverPath;
15344
+ const links = [...rest];
15345
+ let current2 = [];
15346
+ if (root === "this") {
15347
+ const field = links.shift();
15348
+ if (field !== void 0) current2 = fieldTypesOf(f, field).map((type) => ({ at: scope, facts: f, type }));
15349
+ } else {
15350
+ current2 = localTypesOf(f, root).map((type) => ({ at: scope, facts: f, type }));
15351
+ }
15352
+ for (const link of links) {
15353
+ const next = [];
15354
+ for (const c of current2) {
15355
+ for (const decl of typeDeclarationsOf(c.at, c.facts, c.type, true)) {
15356
+ for (const type of memberTypesOf(decl.facts, decl.name, link)) next.push({ at: decl.path, facts: decl.facts, type });
15357
+ }
15358
+ }
15359
+ current2 = next;
15360
+ }
15361
+ for (const c of current2) widenThroughType(c.type, c.at, c.facts);
15362
+ }
15263
15363
  if (site.constructed) widenThrough(site.constructed, importBindingOf(f, site.constructed)?.from);
15264
15364
  if (site.via) for (const typeName of localTypesOf(f, site.via)) widenThroughType(typeName);
15265
15365
  if (site.enclosingClass !== void 0 && holdsMemberBody(f, site.name, site.enclosingClass)) {
@@ -15281,6 +15381,34 @@ function buildCodeIndex(model) {
15281
15381
  }
15282
15382
  return out;
15283
15383
  };
15384
+ const typeDeclarationsOf = (at, atFacts, typeName, throughAlias2 = false) => {
15385
+ const found = [];
15386
+ const exactFacts = (p) => {
15387
+ const x = facts.get(p);
15388
+ return x?.status === "analyzed" && x.analysisGrade === "exact" ? x : void 0;
15389
+ };
15390
+ const direct = (where, whereFacts, name) => {
15391
+ if (whereFacts.declaredNames.includes(name) && typeBindingOf(whereFacts, name) === void 0) {
15392
+ return [{ path: where, facts: whereFacts, name }];
15393
+ }
15394
+ const specifier = typeBindingOf(whereFacts, name);
15395
+ if (specifier === void 0) return [];
15396
+ const published = importBindingOf(whereFacts, name)?.imported ?? name;
15397
+ const out = [];
15398
+ for (const candidate of landingFrom(where, specifier)) {
15399
+ const there = exactFacts(candidate);
15400
+ if (there && there.declaredNames.includes(published)) out.push({ path: candidate, facts: there, name: published });
15401
+ }
15402
+ return out;
15403
+ };
15404
+ for (const decl of direct(at, atFacts, typeName)) {
15405
+ found.push(decl);
15406
+ if (!throughAlias2) continue;
15407
+ const target = aliasTargetOf(decl.facts, decl.name);
15408
+ if (target !== void 0) found.push(...direct(decl.path, decl.facts, target));
15409
+ }
15410
+ return found;
15411
+ };
15284
15412
  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);
15285
15413
  const declarationsAt = (path93) => namesAt(declarations, pathKey(path93), (f) => /* @__PURE__ */ new Set([...f.declaredNames, ...f.exportedNames]));
15286
15414
  const forwardsOf = (start2, name, container) => {
@@ -15841,10 +15969,11 @@ var init_field_type_references = __esm({
15841
15969
  for (const ref of refs) {
15842
15970
  if (!ctx.isTypeResolved(ref, /* @__PURE__ */ new Set())) {
15843
15971
  const hint = ctx.importHint(ref);
15972
+ const spelled = ctx.aliasSpelling(ref);
15844
15973
  ctx.addIssue(
15845
15974
  "error",
15846
15975
  "UNDEFINED_TYPE_REFERENCE",
15847
- `Type "${t.id}" field "${field.name}" references undefined type "${ref}" in "${field.type}".${hint ? ` A declared dependency exports it without this project importing it \u2014 add \`${hint}\`.` : ""}`,
15976
+ `Type "${t.id}" field "${field.name}" references undefined type "${ref}" in "${field.type}".${hint ? ` A declared dependency exports it without this project importing it \u2014 add \`${hint}\`.` : ""}${spelled ? ` "${ref.split(".")[0]}" is another project here, not a subsystem of this one \u2014 write \`${spelled}\`.` : ""}`,
15848
15977
  t.id,
15849
15978
  isDraftCtx
15850
15979
  );
@@ -15886,10 +16015,11 @@ var init_signature_type_references = __esm({
15886
16015
  for (const ref of refs) {
15887
16016
  if (!ctx.isTypeResolved(ref, allGenerics)) {
15888
16017
  const hint = ctx.importHint(ref);
16018
+ const spelled = ctx.aliasSpelling(ref);
15889
16019
  ctx.addIssue(
15890
16020
  "error",
15891
16021
  "UNDEFINED_TYPE_REFERENCE",
15892
- `Method "${m.name}" on interface "${intf.id}" references undefined type "${ref}" in signature.${hint ? ` A declared dependency exports it without this project importing it \u2014 add \`${hint}\`.` : ""}`,
16022
+ `Method "${m.name}" on interface "${intf.id}" references undefined type "${ref}" in signature.${hint ? ` A declared dependency exports it without this project importing it \u2014 add \`${hint}\`.` : ""}${spelled ? ` "${ref.split(".")[0]}" is another project here, not a subsystem of this one \u2014 write \`${spelled}\`.` : ""}`,
15893
16023
  intf.id,
15894
16024
  isDraftCtx
15895
16025
  );
@@ -15908,10 +16038,11 @@ var init_signature_type_references = __esm({
15908
16038
  for (const { ref, by } of named2) {
15909
16039
  if (ctx.isTypeResolved(ref, typeGenerics)) continue;
15910
16040
  const hint = ctx.importHint(ref);
16041
+ const spelled = ctx.aliasSpelling(ref);
15911
16042
  ctx.addIssue(
15912
16043
  "error",
15913
16044
  "UNDEFINED_TYPE_REFERENCE",
15914
- `${by} references undefined type "${ref}" in signature.${hint ? ` A declared dependency exports it without this project importing it \u2014 add \`${hint}\`.` : ""}`,
16045
+ `${by} references undefined type "${ref}" in signature.${hint ? ` A declared dependency exports it without this project importing it \u2014 add \`${hint}\`.` : ""}${spelled ? ` "${ref.split(".")[0]}" is another project here, not a subsystem of this one \u2014 write \`${spelled}\`.` : ""}`,
15915
16046
  t.id,
15916
16047
  isDraftCtx
15917
16048
  );
@@ -21208,16 +21339,36 @@ function closedCallSites(code, file, fn, stopAt, container) {
21208
21339
  function resolveCallTarget(ctx, componentId, methodName) {
21209
21340
  const accepted = /* @__PURE__ */ new Set([methodName]);
21210
21341
  const files = /* @__PURE__ */ new Set();
21342
+ const bodies = /* @__PURE__ */ new Set();
21343
+ const anchors = [];
21211
21344
  for (const intf of ctx.interfacesByComponent.get(componentId) ?? []) {
21212
21345
  for (const impl of ctx.implementationsByContract.get(intf.id) ?? []) {
21213
21346
  const method2 = impl.methods.find((m) => m.name === methodName);
21214
21347
  if (!method2 && !intf.methods.some((m) => m.name === methodName)) continue;
21215
21348
  if (method2?.symbol) accepted.add(method2.symbol);
21216
21349
  const file = methodSourceFile(method2 ?? {}, impl.sourcePath);
21217
- if (file) files.add(pathKey(file));
21350
+ if (!file) continue;
21351
+ files.add(pathKey(file));
21352
+ bodies.add(bodyKey(pathKey(file), method2?.symbol ?? methodName, method2?.exportedVia));
21353
+ anchors.push({ file: pathKey(file), symbol: method2?.symbol ?? methodName, exportedVia: method2?.exportedVia });
21218
21354
  }
21219
21355
  }
21220
- return { accepted, files };
21356
+ return { accepted, files, bodies, anchors };
21357
+ }
21358
+ function targetForwards(code, target) {
21359
+ const out = /* @__PURE__ */ new Set();
21360
+ for (const anchor of target.anchors) {
21361
+ for (const p of code.forwardsOf(anchor.file, anchor.symbol, anchor.exportedVia)) out.add(`${p.file}#${p.name}`);
21362
+ if (anchor.exportedVia !== void 0) continue;
21363
+ const facts = code.factsAt(anchor.file);
21364
+ if (!facts || facts.status !== "analyzed" || facts.analysisGrade !== "exact") continue;
21365
+ const binding = importBindingOf(facts, anchor.symbol);
21366
+ if (!binding || binding.namespace) continue;
21367
+ const module2 = resolveImport(anchor.file, binding.from, code.paths, code.packages);
21368
+ if (!module2) continue;
21369
+ for (const p of code.forwardsOf(module2, binding.imported ?? anchor.symbol)) out.add(`${p.file}#${p.name}`);
21370
+ }
21371
+ return out;
21221
21372
  }
21222
21373
  function isTarget(pairs, target) {
21223
21374
  return pairs.some((p) => target.files.has(p.file) && target.accepted.has(p.name));
@@ -21256,6 +21407,7 @@ function describeSite(site) {
21256
21407
  if (site.field) return `this.${site.field}.${site.name}(\u2026)`;
21257
21408
  if (site.constructed) return `new ${site.constructed}(\u2026).${site.name}(\u2026)`;
21258
21409
  if (site.returnedBy) return `${site.returnedBy}(\u2026).${site.name}(\u2026)`;
21410
+ if (site.receiverPath) return `${site.receiverPath.join(".")}.${site.name}(\u2026)`;
21259
21411
  if (site.enclosingClass) return `this.${site.name}(\u2026)`;
21260
21412
  return `<receiver>.${site.name}(\u2026)`;
21261
21413
  }
@@ -21266,21 +21418,22 @@ function describeMiss(m) {
21266
21418
  if (m.miss.kind === "unresolved") return `${head2}, written as ${m.miss.shapes.map((s) => `\`${s}\``).join(" / ")})`;
21267
21419
  return `${head2})`;
21268
21420
  }
21269
- var anchorKey, unitOf, claimNoun, callConformanceRule;
21421
+ var anchorKey, bodyKey, unitOf, claimNoun, callConformanceRule;
21270
21422
  var init_call_conformance = __esm({
21271
21423
  "src/core/rules/conformance/call-conformance.ts"() {
21272
21424
  "use strict";
21273
21425
  init_models();
21274
21426
  anchorKey = (symbol, exportedVia) => exportedVia !== void 0 ? `${exportedVia}.${symbol}` : symbol;
21427
+ bodyKey = (file, symbol, exportedVia) => `${file}|${anchorKey(symbol, exportedVia)}`;
21275
21428
  unitOf = (m) => m.step === void 0 ? m.target : `${m.step}:${m.target}`;
21276
21429
  claimNoun = (implMethod) => implMethod.narrative.length ? "narrative call step(s)" : "declared call(s)";
21277
21430
  callConformanceRule = {
21278
21431
  name: "call-conformance",
21279
21432
  judges: "code",
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.",
21433
+ description: "Code\u2194spec Level 3: the CLAIMED call \u2194 realized call relation, judged both ways against the method's own source file (its sourcePath, else the implementation's), at exact analysis grade only. A method claims a call two ways and they face one check: a narrative `call` step, and an entry of the declared `calls` a narrative-less method reaches its collaborators through. They assert the same thing, and the step number is no part of the claim \u2014 this check never reads order, so it is only how a finding points at one. Forward: every claim must be realized by a call whose callee RESOLVES TO one of the target method's own source files \u2014 the target's contract name or a per-method `symbol` override, closed transitively over the named helpers the realized function calls, and resolved against every file the call CAN have reached: a `this.<field>.<method>()` receiver followed through the field's DECLARED TYPE, a `new Class(...).<method>()` receiver through the module its CLASS NAME came from, a plain `<name>.<method>()` receiver through the type the file ANNOTATES that name with \u2014 a parameter's, or an annotated variable's, which is how a module that wires its collaborators as closures writes every one of its calls \u2014 a `fn(...).<method>()` receiver through the type fn RETURNS where the code settles it, and a `this.<method>()` receiver through the class whose own method it sits in, and a receiver CHAIN (`this.<field>.<property>.<method>()`, `<name>.<property>.<method>()`) link by link through each link's declared type \u2014 a declared type read through a member-preserving utility (Pick, Omit, Partial, Required, Readonly) and one alias hop; a package specifier naming a package of this repository resolves like a relative one, and an export alias (`export { a as b }`, an adapter object property `{ b: a }`) forwards by identity like a re-export. A matching call whose origin a pure model cannot resolve is reported apart as CALL_ORIGIN_UNRESOLVED, which NAMES the shape it could not follow and asks for nothing \u2014 a coverage hole in the reader, reported for the reason CONFORMANCE_DEGRADED is: a silently degraded gate is worse than a degraded one. A finding names a landing only from the PROVEN tier so that widening what a call reached can accept a claim but never accuse one, and a target that names no file of its own falls back to name membership. N:1 identity \u2014 a caller that IS the target \u2014 is judged on the body, the same anchor in the same file, never on a shared name: a Portal method forwarding to a same-named Orchestrator method elsewhere must make that call. Converse: a call that resolves to a modelled method of ANOTHER component in the SAME file crosses a component boundary while looking local, so the method must claim it \u2014 in a step or in `calls` (UNDECLARED_COLOCATED_CALL) \u2014 judged on the proven tier alone, and a same-file private helper is no modelled method and is never reported; a function forwarding by identity to a modelled method written in another file IS that method, so what it calls is that method's subject. A method that claims nothing at all is judged in neither direction: what it leaves unsaid is UNUSED_*'s subject. Order, arguments and conditions stay unverified, dispatch steps (runtime-table routed) are no claim about a call site, a malformed declared reference is MALFORMED_DECLARED_CALL's finding and names no target here, the conformance dial (off) skips, and weaker analysis grades never guess.",
21281
21434
  codes: [
21282
21435
  { 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 },
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 },
21436
+ { code: "CALL_ORIGIN_UNRESOLVED", defaultSeverity: "warning", summary: "Narrative call step or declared call whose target name IS called, but only from call sites written in a shape this analysis cannot resolve to a file \u2014 the claim was not checked, and is neither proven realized nor accused. What resolves is a name bound to a module of THIS project: a bare or namespaced call through such an import binding, a this.<field> receiver whose declared type names one, a receiver chain (this.<field>.<property>, <name>.<property>) each of whose links the code declares a type for, a new Class(\u2026) receiver whose class does, a receiver name the file annotates with such a type, a fn(\u2026) receiver whose return the code settles, and this inside a class's own method \u2014 a declared type read through Pick/Omit/Partial/Required/Readonly and one alias hop. What does not: a third-party package import, a value the module assembled, and a receiver the file declares nothing for", carryable: true },
21284
21437
  { code: "UNDECLARED_COLOCATED_CALL", defaultSeverity: "warning", summary: "The realized function calls a modelled method of another component living in the same source file, and neither a narrative step nor a declared call names it", carryable: true }
21285
21438
  ],
21286
21439
  check(ctx) {
@@ -21308,9 +21461,9 @@ var init_call_conformance = __esm({
21308
21461
  const here = pathKey(file);
21309
21462
  const fnSymbol = implMethod.symbol ?? implMethod.name;
21310
21463
  const colocated = modelledAt.get(here) ?? { byKey: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() };
21311
- const ownerOf4 = (site, bodyKey) => {
21464
+ const ownerOf4 = (site, bodyKey2) => {
21312
21465
  const inHere = pathKey(site.from ?? here) === here;
21313
- if (inHere && bodyKey !== void 0 && !bodyKey.startsWith("*")) return colocated.byKey.get(bodyKey);
21466
+ if (inHere && bodyKey2 !== void 0 && !bodyKey2.startsWith("*")) return colocated.byKey.get(bodyKey2);
21314
21467
  return colocated.byName.get(site.name);
21315
21468
  };
21316
21469
  const sites = closedCallSites(code, file, fnSymbol, void 0, implMethod.exportedVia);
@@ -21319,7 +21472,9 @@ var init_call_conformance = __esm({
21319
21472
  for (const claim of claimsOf(implMethod)) {
21320
21473
  if (!ctx.componentMap.has(claim.component)) continue;
21321
21474
  const target = resolveCallTarget(ctx, claim.component, claim.method);
21322
- if (target.accepted.has(fnSymbol)) continue;
21475
+ if (target.bodies.has(bodyKey(here, fnSymbol, implMethod.exportedVia))) continue;
21476
+ const sharedBody = targetForwards(code, target);
21477
+ if (code.forwardsOf(file, fnSymbol, implMethod.exportedVia).some((p) => p.file !== here && sharedBody.has(`${p.file}#${p.name}`))) continue;
21323
21478
  if (isTarget(code.forwardsOf(file, fnSymbol, implMethod.exportedVia), target)) continue;
21324
21479
  const matching = sites.filter((s) => target.accepted.has(s.name));
21325
21480
  const ref = `${claim.component}.${claim.method}`;
@@ -21363,7 +21518,10 @@ var init_call_conformance = __esm({
21363
21518
  ctx.addIssue(
21364
21519
  "warning",
21365
21520
  "CALL_ORIGIN_UNRESOLVED",
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.`,
21521
+ // The sites, and what the answer means — once. Which receiver
21522
+ // forms resolve is the code's summary and the rule's description
21523
+ // (`wairon rules list`), not prose every finding repeats.
21524
+ `Method "${implMethod.name}" in implementation "${impl.id}": ${unresolved.length} ${claimNoun(implMethod)} not checked \u2014 called by name inside "${fnSymbol}" in "${file}", but only through receivers this analysis cannot resolve to a file: ${unresolved.map(describeMiss).join("; ")}. Neither proven realized nor accused; the receiver forms that do resolve are listed under CALL_ORIGIN_UNRESOLVED in \`wairon rules list\`.`,
21367
21525
  impl.id,
21368
21526
  entry.draftContext,
21369
21527
  void 0,
@@ -21380,8 +21538,8 @@ var init_call_conformance = __esm({
21380
21538
  }
21381
21539
  const crossings = /* @__PURE__ */ new Map();
21382
21540
  const reached = /* @__PURE__ */ new Map();
21383
- const stopAtModelled = (site, bodyKey) => {
21384
- const owner = ownerOf4(site, bodyKey);
21541
+ const stopAtModelled = (site, bodyKey2) => {
21542
+ const owner = ownerOf4(site, bodyKey2);
21385
21543
  reached.set(site, owner);
21386
21544
  return owner !== void 0;
21387
21545
  };
@@ -21484,7 +21642,7 @@ var init_dependency_conformance = __esm({
21484
21642
  dependencyConformanceRule = {
21485
21643
  name: "dependency-conformance",
21486
21644
  judges: "code",
21487
- description: "Code\u2194spec Level 2: runtime import edges between component-mapped source files (a component maps to every file its implementations and their methods name) must be justified by declared relations \u2014 a direct dependsOn/owns pair, a shared component, membership in a depended-on pattern, or (across subsystems) a declared edge to the target subsystem's published surface (UNDECLARED_DEPENDENCY). Conversely, a declared dependsOn/owns edge between components realized in different files should be visible as an import between any file of the source and any file of the target (UNREALIZED_DEPENDENCY \u2014 DI indirection can defeat this, hence warning). Only exact-grade analyzed files participate; type-only imports are exempt; chained subprojects validate standalone.",
21645
+ description: "Code\u2194spec Level 2: runtime import edges between component-mapped source files (a component maps to every file its implementations and their methods name) must be justified by declared relations \u2014 a direct dependsOn/owns pair, a shared component, membership in a depended-on pattern, or (across subsystems) a declared edge to the target subsystem's published surface (UNDECLARED_DEPENDENCY). Conversely, a declared dependsOn/owns edge between components realized in different files should be visible as an import between any file of the source and any file of the target (UNREALIZED_DEPENDENCY \u2014 DI indirection can defeat this, hence warning). A type-only import never accuses, but it does realize a declared edge: a constructor-injected collaborator is reached through its type, and `import type` is what dependency injection writes down. Only exact-grade analyzed files participate; chained subprojects validate standalone.",
21488
21646
  codes: [
21489
21647
  { code: "UNDECLARED_DEPENDENCY", defaultSeverity: "warning", summary: "A runtime import between component-mapped files has no declared dependsOn/owns (or published-surface) justification" },
21490
21648
  { code: "UNREALIZED_DEPENDENCY", defaultSeverity: "warning", summary: "A declared dependsOn/owns edge between components in different files is realized by no import between any of their files" }
@@ -21552,6 +21710,23 @@ var init_dependency_conformance = __esm({
21552
21710
  );
21553
21711
  }
21554
21712
  }
21713
+ const typeTraces = /* @__PURE__ */ new Map();
21714
+ const typeTracesOf = (path93) => {
21715
+ let out = typeTraces.get(path93);
21716
+ if (out) return out;
21717
+ out = /* @__PURE__ */ new Set();
21718
+ const facts = code.factsAt(path93);
21719
+ for (const specifier of new Set(Object.values(facts?.typeOnlyBindings ?? {}))) {
21720
+ const to = resolveImport(path93, specifier, mappedPaths, code.packages);
21721
+ if (to && to !== path93) out.add(to);
21722
+ }
21723
+ typeTraces.set(path93, out);
21724
+ return out;
21725
+ };
21726
+ const typeConnects = (from, to) => from.some((f) => {
21727
+ const traces = typeTracesOf(f);
21728
+ return to.some((t) => traces.has(t));
21729
+ });
21555
21730
  for (const component of ctx.components) {
21556
21731
  const fromFiles = filesOf(component.id);
21557
21732
  if (fromFiles.length === 0) continue;
@@ -21565,6 +21740,7 @@ var init_dependency_conformance = __esm({
21565
21740
  if (toFiles.length === 0) continue;
21566
21741
  if (fromFiles.some((f) => toFiles.includes(f))) continue;
21567
21742
  if (graph2.connects(fromFiles, toFiles, isMountingDeclarer)) continue;
21743
+ if (typeConnects(fromFiles, toFiles)) continue;
21568
21744
  const impls = mappedImplsOf(component.id);
21569
21745
  const declaredDependsOn = component.dependsOn.includes(targetId);
21570
21746
  const declaredOwns = (component.owns ?? []).includes(targetId);
@@ -21573,7 +21749,7 @@ var init_dependency_conformance = __esm({
21573
21749
  ctx.addIssue(
21574
21750
  "warning",
21575
21751
  "UNREALIZED_DEPENDENCY",
21576
- `Component "${component.id}" declares ${relation} "${targetId}", but no runtime import connects their source files (${fromFiles.join(", ")} \u219B ${crossSubsystem ? `subsystem ${target.subsystem}` : filesOf(targetId).join(", ")}) \u2014 either the collaboration is wired indirectly (DI) or the declared edge is stale.`,
21752
+ `Component "${component.id}" declares ${relation} "${targetId}", but no import \u2014 runtime or type-only \u2014 connects their source files (${fromFiles.join(", ")} \u219B ${crossSubsystem ? `subsystem ${target.subsystem}` : filesOf(targetId).join(", ")}) \u2014 either the collaboration is wired indirectly (DI) or the declared edge is stale.`,
21577
21753
  impls[0]?.id ?? component.id,
21578
21754
  draft,
21579
21755
  void 0,
@@ -21655,14 +21831,17 @@ var integrationSimDeclarationRule;
21655
21831
  var init_integration_sim_declaration = __esm({
21656
21832
  "src/core/rules/conformance/integration-sim-declaration.ts"() {
21657
21833
  "use strict";
21834
+ init_models();
21658
21835
  integrationSimDeclarationRule = {
21659
21836
  name: "integration-sim-declaration",
21660
21837
  judges: "code",
21661
- description: "Once a subsystem has adopted integration sims (its first declared L4 simPath), every OTHER complete, non-leaf implementation of that subsystem must declare one too: its unit suite proves contract shape against mocks, not that the wired components run together. A leaf component's unit suite IS its sim, and a subsystem that has adopted nothing is never flooded.",
21838
+ description: "Once a subsystem has adopted integration sims (its first declared L4 simPath), every OTHER complete, non-leaf implementation of that subsystem must declare one too: its unit suite proves contract shape against mocks, not that the wired components run together. A leaf component's unit suite IS its sim, a subsystem that has adopted nothing is never flooded, and an implementation with no code yet \u2014 no source file named, or only files that are missing \u2014 has nothing to wire and is not asked for a harness.",
21662
21839
  codes: [
21663
21840
  { code: "MISSING_INTEGRATION_SIM", defaultSeverity: "warning", summary: "Complete non-leaf implementation in a sim-adopting subsystem declares no simPath" }
21664
21841
  ],
21665
21842
  check(ctx) {
21843
+ const code = ctx.codeIndex();
21844
+ const hasCode = (files) => files.some((file) => code.factsAt(pathKey(file))?.status !== "missing");
21666
21845
  const adopted = /* @__PURE__ */ new Set();
21667
21846
  for (const impl of ctx.implementations) {
21668
21847
  if (!impl.simPath) continue;
@@ -21681,6 +21860,7 @@ var init_integration_sim_declaration = __esm({
21681
21860
  if (ctx.isImplementationDraft(impl)) continue;
21682
21861
  const deps = [.../* @__PURE__ */ new Set([...comp.dependsOn, ...comp.owns])].filter((d) => ctx.componentMap.has(d));
21683
21862
  if (deps.length === 0) continue;
21863
+ if (!hasCode(implementationSourceFiles(impl))) continue;
21684
21864
  ctx.addIssue(
21685
21865
  "warning",
21686
21866
  "MISSING_INTEGRATION_SIM",
@@ -22578,7 +22758,7 @@ var init_export_conformance = __esm({
22578
22758
  exportConformanceRule = {
22579
22759
  name: "export-conformance",
22580
22760
  judges: "code",
22581
- description: "Code-to-contract for the SURFACE: does this file publish anything the components it realizes never promised, that another component then takes? The rest of the conformance set reads contract-to-code \u2014 it asks whether the code holds what a spec claims \u2014 so a file could export whatever it liked under a component's name and nothing looked. That is one half of the same hole as a contract promising a parameter it never passes. A method's `exportedVia` names the export a consumer imports to REACH it \u2014 the value that composes it, which `symbol` cannot name because `symbol` names the function inside \u2014 and a listener's mount names the router entry it calls to hand a portal its requests; both are declared publication, and a declared handle the file does not actually export is itself a finding, so naming one can never become a free-text suppression. Both questions are asked only of a file read at exact grade, and a taker counts only when its own import specifier resolves to the file it is accused of taking from: below that nothing separates an export from a mention, or a shared word from a shared module.",
22761
+ description: "Code-to-contract for the SURFACE: does this file publish anything the components it realizes never promised, that another component then takes? The rest of the conformance set reads contract-to-code \u2014 it asks whether the code holds what a spec claims \u2014 so a file could export whatever it liked under a component's name and nothing looked. That is one half of the same hole as a contract promising a parameter it never passes. A method's `exportedVia` names the export a consumer imports to REACH it \u2014 the value that composes it, which `symbol` cannot name because `symbol` names the function inside \u2014 and a listener's mount names the router entry it calls to hand a portal its requests; both are declared publication, and a declared handle the file does not actually export is itself a finding, so naming one can never become a free-text suppression. Both questions are asked only of a file read at exact grade, and a taker counts only when its own import specifier resolves to the file it is accused of taking from: below that nothing separates an export from a mention, or a shared word from a shared module. A taker inside the owns fence is no crossing: a pattern constructs the blocks it owns, so the owner's file, or another member of the same pattern, importing a member's class is the owns relation made real.",
22582
22762
  codes: [
22583
22763
  {
22584
22764
  code: "UNDECLARED_EXPORT",
@@ -22600,6 +22780,7 @@ var init_export_conformance = __esm({
22600
22780
  check(ctx) {
22601
22781
  const code = ctx.codeIndex();
22602
22782
  const realization = ctx.realizationIndex();
22783
+ const ownership = ctx.ownershipIndex();
22603
22784
  const promised = /* @__PURE__ */ new Map();
22604
22785
  const promise = (file, name) => {
22605
22786
  if (!name) return;
@@ -22726,13 +22907,20 @@ var init_export_conformance = __esm({
22726
22907
  const declared = promised.get(file) ?? NOTHING_PROMISED;
22727
22908
  const taken = takenFrom.get(file);
22728
22909
  const owners = new Set(realization.componentsAt(file).map((c) => c.id));
22910
+ const fence = new Set(owners);
22911
+ for (const id of owners) {
22912
+ const pattern = ownership.ownerOf(id);
22913
+ if (pattern === void 0) continue;
22914
+ fence.add(pattern);
22915
+ for (const member of ownership.ownedMembers) if (ownership.ownerOf(member) === pattern) fence.add(member);
22916
+ }
22729
22917
  const crossed = [];
22730
22918
  for (const name of [...new Set(facts.exportedNames)].sort()) {
22731
22919
  if (declared.has(name)) continue;
22732
22920
  const takers = taken?.get(name);
22733
22921
  if (!takers) continue;
22734
22922
  const crossing = [...takers].some(
22735
- (taker) => taker !== file && !realization.componentsAt(taker).some((c) => owners.has(c.id))
22923
+ (taker) => taker !== file && !realization.componentsAt(taker).some((c) => fence.has(c.id))
22736
22924
  );
22737
22925
  if (crossing) crossed.push(name);
22738
22926
  }
@@ -24528,6 +24716,8 @@ function walkExact(ts, sourceText, fileName) {
24528
24716
  const typeOnlyBindings = /* @__PURE__ */ new Map();
24529
24717
  const fieldTypes = /* @__PURE__ */ new Map();
24530
24718
  const localTypes = /* @__PURE__ */ new Map();
24719
+ const memberTypes = /* @__PURE__ */ new Map();
24720
+ const aliasTargets = /* @__PURE__ */ new Map();
24531
24721
  const typeShapes = /* @__PURE__ */ new Map();
24532
24722
  const functionParams = /* @__PURE__ */ new Map();
24533
24723
  const functionRoutes = /* @__PURE__ */ new Map();
@@ -24537,7 +24727,7 @@ function walkExact(ts, sourceText, fileName) {
24537
24727
  const mutableBindings = /* @__PURE__ */ new Set();
24538
24728
  const schemaConstants = /* @__PURE__ */ new Map();
24539
24729
  const derivedAliases = [];
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}` : "");
24730
+ const receiverKey = (site) => site.via ?? (site.field ? `this.${site.field}` : site.constructed ? `new ${site.constructed}` : site.returnedBy ? `${site.returnedBy}()` : site.enclosingClass ? `this:${site.enclosingClass}` : site.receiverPath ? site.receiverPath.join(".") : "");
24541
24731
  const siteKey = (site) => `${site.member ? "m" : "b"}:${receiverKey(site)}:${site.name}`;
24542
24732
  const addBindingNames = (name) => {
24543
24733
  if (ts.isIdentifier(name)) declared.add(name.text);
@@ -24552,7 +24742,34 @@ function walkExact(ts, sourceText, fileName) {
24552
24742
  if (ts.isPrivateIdentifier(name)) return name.text;
24553
24743
  return void 0;
24554
24744
  };
24555
- const typeReferenceName = (type) => type && ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) ? type.typeName.text : void 0;
24745
+ const MEMBER_PRESERVING = /* @__PURE__ */ new Set(["Pick", "Omit", "Partial", "Required", "Readonly"]);
24746
+ const typeReferenceName = (type) => {
24747
+ let at = type;
24748
+ for (let depth = 0; at && depth < 4; depth++) {
24749
+ if (!ts.isTypeReferenceNode(at) || !ts.isIdentifier(at.typeName)) return void 0;
24750
+ const name = at.typeName.text;
24751
+ if (!MEMBER_PRESERVING.has(name) || !at.typeArguments?.length) return name;
24752
+ at = at.typeArguments[0];
24753
+ }
24754
+ return void 0;
24755
+ };
24756
+ const recordMemberType = (shape, name, type) => {
24757
+ if (!shape) return;
24758
+ const typeName = typeReferenceName(type);
24759
+ if (!typeName) return;
24760
+ const property = propertyNameText(name);
24761
+ if (!property) return;
24762
+ const key = `${shape}.${property}`;
24763
+ const named2 = memberTypes.get(key) ?? /* @__PURE__ */ new Set();
24764
+ named2.add(typeName);
24765
+ memberTypes.set(key, named2);
24766
+ };
24767
+ const shapeNameOf = (holder) => {
24768
+ if (!holder) return void 0;
24769
+ if ((ts.isClassDeclaration(holder) || ts.isInterfaceDeclaration(holder)) && holder.name) return holder.name.text;
24770
+ if (ts.isTypeLiteralNode(holder) && holder.parent && ts.isTypeAliasDeclaration(holder.parent)) return holder.parent.name.text;
24771
+ return void 0;
24772
+ };
24556
24773
  const isParameterProperty = (node) => !!node.modifiers?.some((m) => m.kind === ts.SyntaxKind.PublicKeyword || m.kind === ts.SyntaxKind.PrivateKeyword || m.kind === ts.SyntaxKind.ProtectedKeyword || m.kind === ts.SyntaxKind.ReadonlyKeyword);
24557
24774
  const recordFieldType = (name, type) => {
24558
24775
  const typeName = typeReferenceName(type);
@@ -24749,7 +24966,18 @@ function walkExact(ts, sourceText, fileName) {
24749
24966
  } else if (receiver.kind === ts.SyntaxKind.ThisKeyword) {
24750
24967
  const owner = enclosingClassOf(node);
24751
24968
  addSite(owner !== void 0 ? { name, member: true, enclosingClass: owner } : { name, member: true });
24752
- } else addSite({ name, member: true });
24969
+ } else {
24970
+ const chain = [];
24971
+ let at = receiver;
24972
+ while (ts.isPropertyAccessExpression(at) && !at.questionDotToken && ts.isIdentifier(at.name)) {
24973
+ chain.unshift(at.name.text);
24974
+ at = at.expression;
24975
+ }
24976
+ if (ts.isIdentifier(at)) chain.unshift(at.text);
24977
+ else if (at.kind === ts.SyntaxKind.ThisKeyword) chain.unshift("this");
24978
+ else chain.length = 0;
24979
+ addSite(chain.length >= 2 ? { name, member: true, receiverPath: chain } : { name, member: true });
24980
+ }
24753
24981
  }
24754
24982
  }
24755
24983
  ts.forEachChild(node, count);
@@ -24884,9 +25112,14 @@ function walkExact(ts, sourceText, fileName) {
24884
25112
  functionRoutes.set(fnName, known);
24885
25113
  }
24886
25114
  }
24887
- if (ts.isPropertyDeclaration(node)) recordFieldType(node.name, node.type);
24888
- else if (ts.isParameter(node) && node.parent && ts.isConstructorDeclaration(node.parent) && isParameterProperty(node)) {
25115
+ if (ts.isPropertyDeclaration(node)) {
25116
+ recordFieldType(node.name, node.type);
25117
+ recordMemberType(shapeNameOf(node.parent), node.name, node.type);
25118
+ } else if (ts.isParameter(node) && node.parent && ts.isConstructorDeclaration(node.parent) && isParameterProperty(node)) {
24889
25119
  recordFieldType(node.name, node.type);
25120
+ recordMemberType(shapeNameOf(node.parent.parent), node.name, node.type);
25121
+ } else if (ts.isPropertySignature(node)) {
25122
+ recordMemberType(shapeNameOf(node.parent), node.name, node.type);
24890
25123
  }
24891
25124
  if (ts.isParameter(node) || ts.isVariableDeclaration(node)) recordLocalType(node.name, node.type);
24892
25125
  if (ts.isInterfaceDeclaration(node)) {
@@ -24900,6 +25133,10 @@ function walkExact(ts, sourceText, fileName) {
24900
25133
  if (ts.isTypeLiteralNode(node.type)) recordDeclaredShape(node.name.text, node.type.members, false);
24901
25134
  else {
24902
25135
  if (!aliasTypes.has(node.name.text)) aliasTypes.set(node.name.text, node.type.getText(sf));
25136
+ const named2 = typeReferenceName(node.type);
25137
+ if (named2 !== void 0 && named2 !== node.name.text && !aliasTargets.has(node.name.text)) {
25138
+ aliasTargets.set(node.name.text, named2);
25139
+ }
24903
25140
  const constant = inferredSchemaConstant(node.type);
24904
25141
  if (constant) derivedAliases.push({ name: node.name.text, constant });
24905
25142
  const literals = stringLiteralUnion(node.type);
@@ -25060,7 +25297,7 @@ function walkExact(ts, sourceText, fileName) {
25060
25297
  }
25061
25298
  const reexportOnly = sf.statements.length > 0 && sf.statements.every((st) => ts.isExportDeclaration(st) && !!st.moduleSpecifier);
25062
25299
  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 };
25300
+ return { declared, anchors, exported, imports, reexports, starExports, namedReexports, exportAliases, returnTypes, complexity, calls, bodies, importBindings, typeOnlyBindings, fieldTypes, localTypes, memberTypes, aliasTargets, typeShapes, functionParams, functionRoutes, asyncFunctions, enumValues, aliasTypes, mutableBindings, reexportOnly };
25064
25301
  }
25065
25302
  function resolveModule(fromFile, specifier, packages) {
25066
25303
  if (!specifier.startsWith(".")) return packages.get(specifier) ?? null;
@@ -25428,6 +25665,8 @@ function buildCodeModel(implementations, types, projectRoot2, sourceRoots = [],
25428
25665
  typeOnlyBindings: Object.fromEntries(facts.typeOnlyBindings),
25429
25666
  fieldTypes: Object.fromEntries([...facts.fieldTypes].map(([k, v]) => [k, [...v]])),
25430
25667
  localTypes: Object.fromEntries([...facts.localTypes].map(([k, v]) => [k, [...v]])),
25668
+ memberTypes: Object.fromEntries([...facts.memberTypes].map(([k, v]) => [k, [...v]])),
25669
+ aliasTargets: Object.fromEntries(facts.aliasTargets),
25431
25670
  typeShapes: Object.fromEntries(facts.typeShapes),
25432
25671
  functionParams: Object.fromEntries(facts.functionParams),
25433
25672
  functionRoutes: Object.fromEntries([...facts.functionRoutes].map(([k, v]) => [k, [...v.values()]])),
@@ -25910,8 +26149,16 @@ function buildRuleContext(opts) {
25910
26149
  }
25911
26150
  if (ownerId && nameKey(first) === nameKey(ownerId)) return ownTypes.some((spec) => typeMatchesRef(spec, rest.join("::")));
25912
26151
  if (ref.includes("::") && authoredTypeRefs.has(ref)) return true;
26152
+ return false;
26153
+ };
26154
+ const aliasSpelling = (ref) => {
26155
+ const segments = ref.split(".");
26156
+ if (segments.length !== 2 || ref.includes("::")) return null;
26157
+ const [first, name] = segments;
26158
+ if (subsystems.some((s) => nameKey(s.id.split("::").pop()) === nameKey(first) && memberOf(s.id) === void 0)) return null;
25913
26159
  const via = aliasOf(first);
25914
- return via !== void 0 && rest.length === 1 && exportedBy(via, nameKey(rest[0]), "type") !== void 0;
26160
+ const publicName = via ? exportedBy(via, nameKey(name), "type") : void 0;
26161
+ return via && publicName ? `${via.alias}::${publicName}` : null;
25915
26162
  };
25916
26163
  const foreignSurfaceRef = (ref) => {
25917
26164
  const segments = ref.split("::").filter((seg) => seg && seg !== "super");
@@ -26285,6 +26532,7 @@ function buildRuleContext(opts) {
26285
26532
  isImplementationDraft,
26286
26533
  getComponentProfile,
26287
26534
  isTypeResolved,
26535
+ aliasSpelling,
26288
26536
  targetLanguageFor,
26289
26537
  isSpecInScope,
26290
26538
  isInChainedSubproject,
@@ -28977,6 +29225,24 @@ function mapSignatureSource(value, map) {
28977
29225
  const head2 = map("signatureFrom", parts.head);
28978
29226
  return head2 === parts.head ? value : `${head2}.${parts.tail}`;
28979
29227
  }
29228
+ function typeExpressionSlots(kind, doc) {
29229
+ const out = [];
29230
+ const paths = kind === "interface" || kind === "type" ? TYPE_EXPRESSION_PATHS[kind] : [];
29231
+ const walk2 = (node, segments, at, method2) => {
29232
+ if (!node || typeof node !== "object") return;
29233
+ const holder = node;
29234
+ const [head2, ...rest] = segments;
29235
+ if (rest.length === 0) {
29236
+ if (typeof holder[head2] === "string") out.push({ holder, key: head2, path: at, ...method2 ? { method: method2 } : {} });
29237
+ return;
29238
+ }
29239
+ const next = holder[head2];
29240
+ const items = Array.isArray(next) ? next : next && typeof next === "object" ? [next] : [];
29241
+ for (const item of items) walk2(item, rest, at, head2 === "methods" ? item : method2);
29242
+ };
29243
+ for (const at of paths) walk2(doc, at.split("."), at, void 0);
29244
+ return out;
29245
+ }
28980
29246
  function mapSpecReferences(kind, spec, map) {
28981
29247
  switch (kind) {
28982
29248
  case "subsystem": {
@@ -29067,11 +29333,8 @@ function rawReferences(kind, spec) {
29067
29333
  for (const link of spec.trustedLinks ?? []) add2("trustedLinks", link.subsystem);
29068
29334
  break;
29069
29335
  case "interface":
29070
- for (const m of spec.methods) {
29071
- qualifiedTypeNames(m.returns).forEach((t) => add2("type", t));
29072
- m.params?.forEach((p) => qualifiedTypeNames(p.type).forEach((t) => add2("type", t)));
29073
- if (m.signatureFrom !== void 0 && !signatureSourceParts(m.signatureFrom)) add2("type", m.signatureFrom);
29074
- }
29336
+ case "type":
29337
+ typeSlotReferences(kind, spec).forEach((t) => add2("type", t));
29075
29338
  break;
29076
29339
  case "implementation":
29077
29340
  for (const m of spec.methods) {
@@ -29083,19 +29346,36 @@ function rawReferences(kind, spec) {
29083
29346
  }
29084
29347
  }
29085
29348
  break;
29086
- case "type": {
29087
- const t = spec;
29088
- for (const f of t.fields) qualifiedTypeNames(f.type).forEach((n) => add2("type", n));
29089
- t.params?.forEach((p) => qualifiedTypeNames(p.type).forEach((n) => add2("type", n)));
29090
- qualifiedTypeNames(t.returns).forEach((n) => add2("type", n));
29091
- for (const m of t.methods ?? []) m.params?.forEach((p) => qualifiedTypeNames(p.type).forEach((n) => add2("type", n)));
29092
- break;
29093
- }
29094
29349
  default:
29095
29350
  break;
29096
29351
  }
29097
29352
  return out;
29098
29353
  }
29354
+ function typeSlotReferences(kind, spec) {
29355
+ const out = [];
29356
+ const structured2 = /* @__PURE__ */ new Set();
29357
+ const slots = typeExpressionSlots(kind, spec);
29358
+ for (const slot of slots) {
29359
+ const value = slot.holder[slot.key];
29360
+ if (slot.path === "methods.signature") continue;
29361
+ if (slot.path === "methods.signatureFrom") {
29362
+ if (!signatureSourceParts(value)) out.push(value);
29363
+ continue;
29364
+ }
29365
+ for (const name of qualifiedTypeNames(value)) {
29366
+ out.push(name);
29367
+ structured2.add(name);
29368
+ }
29369
+ }
29370
+ for (const slot of slots.filter((s) => s.path === "methods.signature")) {
29371
+ for (const name of qualifiedTypeNames(slot.holder[slot.key])) {
29372
+ if (structured2.has(name)) continue;
29373
+ structured2.add(name);
29374
+ out.push(name);
29375
+ }
29376
+ }
29377
+ return out;
29378
+ }
29099
29379
  function bareRawReferences(kind, spec) {
29100
29380
  const out = [];
29101
29381
  const seen = /* @__PURE__ */ new Set();
@@ -29121,7 +29401,11 @@ function bareRawReferences(kind, spec) {
29121
29401
  const t = spec;
29122
29402
  for (const f of t.fields) for (const ref of fieldTypeRefs(t, f.type)) add2("type", ref);
29123
29403
  for (const ref of signatureTypeRefs(t)) add2("type", ref);
29124
- for (const m of t.methods ?? []) for (const p of m.params ?? []) for (const ref of fieldTypeRefs(t, p.type)) add2("type", ref);
29404
+ const own = lower(typeGenericParameters(t));
29405
+ for (const m of t.methods ?? []) {
29406
+ const generics = /* @__PURE__ */ new Set([...own, ...lower(methodGenericParameters(m))]);
29407
+ for (const ref of methodTypeRefs(m)) add2("type", ref, generics);
29408
+ }
29125
29409
  break;
29126
29410
  }
29127
29411
  case "implementation":
@@ -29134,6 +29418,9 @@ function bareRawReferences(kind, spec) {
29134
29418
  }
29135
29419
  return out;
29136
29420
  }
29421
+ function respellTypeNames(typeStr, map) {
29422
+ return mapTypeNames(typeStr, (_position, name) => map(name));
29423
+ }
29137
29424
  function mapTypeNames(typeStr, map) {
29138
29425
  if (typeStr === void 0) return void 0;
29139
29426
  return typeStr.replace(
@@ -29157,12 +29444,9 @@ function respellStoredReferences(kind, doc, map) {
29157
29444
  const values = holder[key];
29158
29445
  if (Array.isArray(values)) holder[key] = values.map((v) => typeof v === "string" ? map(position, v) : v);
29159
29446
  };
29160
- const typed = (holder, key) => {
29161
- if (typeof holder[key] === "string") holder[key] = mapTypeNames(holder[key], map);
29162
- };
29163
29447
  const published = (entries) => {
29164
29448
  for (const e of list2(entries)) {
29165
- for (const key of ["from", "component", "interface", "typeDef"]) at(e, key, "publicInterfaces");
29449
+ for (const key of ["from", "subsystem", "component", "interface", "typeDef"]) at(e, key, "publicInterfaces");
29166
29450
  each(e, "consumers", "publicInterfaces");
29167
29451
  }
29168
29452
  };
@@ -29184,14 +29468,7 @@ function respellStoredReferences(kind, doc, map) {
29184
29468
  break;
29185
29469
  case "interface":
29186
29470
  at(doc, "component", "contract");
29187
- for (const m of list2(doc.methods)) {
29188
- typed(m, "signature");
29189
- typed(m, "returns");
29190
- for (const p of list2(m.params)) typed(p, "type");
29191
- if (typeof m.signatureFrom === "string") {
29192
- m.signatureFrom = signatureSourceParts(m.signatureFrom) ? mapSignatureSource(m.signatureFrom, map) : mapTypeNames(m.signatureFrom, map);
29193
- }
29194
- }
29471
+ typedSlots("interface", doc, map);
29195
29472
  break;
29196
29473
  case "implementation":
29197
29474
  at(doc, "contract", "contract");
@@ -29218,34 +29495,33 @@ function respellStoredReferences(kind, doc, map) {
29218
29495
  case "type":
29219
29496
  at(doc, "subsystem", "subsystem");
29220
29497
  at(doc, "group", "group");
29221
- for (const f of list2(doc.fields)) typed(f, "type");
29222
- for (const p of list2(doc.params)) typed(p, "type");
29223
- typed(doc, "returns");
29224
- for (const m of list2(doc.methods)) for (const p of list2(m.params)) typed(p, "type");
29498
+ typedSlots("type", doc, map);
29225
29499
  break;
29226
29500
  default:
29227
29501
  break;
29228
29502
  }
29229
29503
  }
29504
+ function typedSlots(kind, doc, map) {
29505
+ for (const slot of typeExpressionSlots(kind, doc)) {
29506
+ const value = slot.holder[slot.key];
29507
+ slot.holder[slot.key] = slot.path === "methods.signatureFrom" && signatureSourceParts(value) ? mapSignatureSource(value, map) : mapTypeNames(value, map);
29508
+ }
29509
+ }
29230
29510
  function mapRawReferences(kind, spec, map) {
29231
29511
  switch (kind) {
29232
29512
  case "subsystem": {
29233
29513
  const s = spec;
29234
29514
  return { ...s, trustedLinks: (s.trustedLinks ?? []).map((l) => ({ ...l, subsystem: map("trustedLinks", l.subsystem) })) };
29235
29515
  }
29236
- case "interface": {
29237
- const i = spec;
29238
- return {
29239
- ...i,
29240
- methods: i.methods.map((m) => ({
29241
- ...m,
29242
- ...m.signature !== void 0 ? { signature: mapTypeNames(m.signature, map) } : {},
29243
- ...m.returns !== void 0 ? { returns: mapTypeNames(m.returns, map) } : {},
29244
- ...m.params ? { params: m.params.map((p) => ({ ...p, type: mapTypeNames(p.type, map) })) } : {},
29245
- // A type source is a raw type position; a method source's head is bound, never raw.
29246
- ...m.signatureFrom !== void 0 && !signatureSourceParts(m.signatureFrom) ? { signatureFrom: mapTypeNames(m.signatureFrom, map) } : {}
29247
- }))
29248
- };
29516
+ case "interface":
29517
+ case "type": {
29518
+ const copy = JSON.parse(JSON.stringify(spec));
29519
+ for (const slot of typeExpressionSlots(kind, copy)) {
29520
+ const value = slot.holder[slot.key];
29521
+ if (slot.path === "methods.signatureFrom" && signatureSourceParts(value)) continue;
29522
+ slot.holder[slot.key] = mapTypeNames(value, map);
29523
+ }
29524
+ return copy;
29249
29525
  }
29250
29526
  case "implementation": {
29251
29527
  const impl = spec;
@@ -29270,17 +29546,6 @@ function mapRawReferences(kind, spec, map) {
29270
29546
  }))
29271
29547
  };
29272
29548
  }
29273
- case "type": {
29274
- const t = spec;
29275
- const typedParams = (params) => params ? { params: params.map((p) => ({ ...p, type: mapTypeNames(p.type, map) })) } : {};
29276
- return {
29277
- ...t,
29278
- fields: t.fields.map((f) => ({ ...f, type: mapTypeNames(f.type, map) })),
29279
- ...typedParams(t.params),
29280
- ...t.returns !== void 0 ? { returns: mapTypeNames(t.returns, map) } : {},
29281
- methods: (t.methods ?? []).map((m) => ({ ...m, ...typedParams(m.params) }))
29282
- };
29283
- }
29284
29549
  default:
29285
29550
  return spec;
29286
29551
  }
@@ -29362,7 +29627,7 @@ function resolveFamilyTables(raws) {
29362
29627
  const byKey3 = new Map(raws.map((r) => [r.record.namespace, r]));
29363
29628
  const done = /* @__PURE__ */ new Set();
29364
29629
  const resolving = /* @__PURE__ */ new Set();
29365
- const resolve56 = (raw) => {
29630
+ const resolve57 = (raw) => {
29366
29631
  const key = raw.record.namespace;
29367
29632
  if (done.has(key)) return raw.record.exports;
29368
29633
  if (resolving.has(key)) return null;
@@ -29377,7 +29642,7 @@ function resolveFamilyTables(raws) {
29377
29642
  const producerKey = source !== void 0 ? raw.record.aliases.get(source) : void 0;
29378
29643
  if (producerKey === void 0 || producerTables.has(producerKey)) continue;
29379
29644
  const producer = byKey3.get(producerKey);
29380
- producerTables.set(producerKey, producer ? resolve56(producer) : null);
29645
+ producerTables.set(producerKey, producer ? resolve57(producer) : null);
29381
29646
  audienceOf.set(producerKey, "project");
29382
29647
  }
29383
29648
  raw.record.subsystemExports = subsystemTables;
@@ -29393,7 +29658,7 @@ function resolveFamilyTables(raws) {
29393
29658
  resolving.delete(key);
29394
29659
  }
29395
29660
  };
29396
- raws.forEach((raw) => resolve56(raw));
29661
+ raws.forEach((raw) => resolve57(raw));
29397
29662
  }
29398
29663
  function bindAuthored(raw, raws, readables, authored) {
29399
29664
  const key = raw.record.namespace;
@@ -30479,6 +30744,12 @@ function readLockState(current2) {
30479
30744
  if (!record2) return { state: "unlocked", record: null, current: current2 };
30480
30745
  return { state: stateIdEquals(record2.stateId, current2) ? "locked" : "stale", record: record2, current: current2 };
30481
30746
  }
30747
+ function writeLockRecord2(record2) {
30748
+ writeLockRecord(record2);
30749
+ }
30750
+ function renderDiagram2(format, relations) {
30751
+ return renderDiagram(format, relations);
30752
+ }
30482
30753
  function createProjectConfig(config) {
30483
30754
  projectConfigRepository.create(config);
30484
30755
  }
@@ -30583,7 +30854,7 @@ function assertSpecsInReach(targets, what, projectRung = false) {
30583
30854
  function moveMethods(from, to, methods, hooks, dryRun) {
30584
30855
  return current().moveMethods(from, to, methods, hooks, dryRun);
30585
30856
  }
30586
- var fs14, path21, crypto5, PartReadOnly, SubsystemWriteDenied, PART_FORBIDDEN_FIELDS, IMPORTABLE_POSITIONS, SIGNATURE_TTL_MS, COMPONENT_AUTH_SOURCE, STEP_JUMP_FIELDS, STEP_JUMP_LISTS, DELTA_VERBS, JUMP_LABEL_TWINS, LABEL_TWIN_CONTAINERS, SpecWorkspace, DEFAULT_DEPENDENCY_LIMIT, PROSE_FIELDS, DELTA_JUMP_PINS, workspaces, lastServedBinding;
30857
+ var fs14, path21, crypto5, PartReadOnly, SubsystemWriteDenied, PART_FORBIDDEN_FIELDS, TYPE_EXPRESSION_PATHS, IMPORTABLE_POSITIONS, SIGNATURE_TTL_MS, COMPONENT_AUTH_SOURCE, STEP_JUMP_FIELDS, STEP_JUMP_LISTS, DELTA_VERBS, JUMP_LABEL_TWINS, LABEL_TWIN_CONTAINERS, SpecWorkspace, DEFAULT_DEPENDENCY_LIMIT, PROSE_FIELDS, DELTA_JUMP_PINS, workspaces, lastServedBinding;
30587
30858
  var init_specs2 = __esm({
30588
30859
  "src/core/specs.ts"() {
30589
30860
  "use strict";
@@ -30625,6 +30896,10 @@ var init_specs2 = __esm({
30625
30896
  }
30626
30897
  };
30627
30898
  PART_FORBIDDEN_FIELDS = ["members", "externals", "rules", "extensions", "composition", "targets"];
30899
+ TYPE_EXPRESSION_PATHS = {
30900
+ interface: ["methods.params.type", "methods.returns", "methods.signature", "methods.signatureFrom"],
30901
+ type: ["fields.type", "params.type", "returns", "methods.params.type", "methods.returns", "methods.signature"]
30902
+ };
30628
30903
  IMPORTABLE_POSITIONS = /* @__PURE__ */ new Set(["dependsOn", "dispatch", "mounts", "narrative"]);
30629
30904
  SIGNATURE_TTL_MS = 2e3;
30630
30905
  COMPONENT_AUTH_SOURCE = "component:";
@@ -34597,7 +34872,7 @@ function isWithinDir2(dir, file) {
34597
34872
  return f === d || f.startsWith(d + path25.sep);
34598
34873
  }
34599
34874
  function externalizeSubsystem(subsystemId, path93, as) {
34600
- externalizeInto(subsystemId, path93, as);
34875
+ return externalizeInto(subsystemId, path93, as);
34601
34876
  }
34602
34877
  function externalizeInto(subsystemId, partPath, as) {
34603
34878
  if (subsystemId.includes("::")) {
@@ -34616,7 +34891,8 @@ function externalizeInto(subsystemId, partPath, as) {
34616
34891
  const parentRoot = getProjectRoot();
34617
34892
  const parentSpecsDir = aiPathsAt(parentRoot).specsDir();
34618
34893
  const fooDir = path25.join(parentSpecsDir, subsystemId);
34619
- if (!fs16.existsSync(fooDir)) throw new WaironError(`subsystem specs directory not found: ${fooDir}`);
34894
+ const owned = subsystemFiles(subsystemId, parentSpecsDir);
34895
+ if (owned.files.length === 0) throw new WaironError(`subsystem specs not found for "${subsystemId}" under ${parentSpecsDir}`);
34620
34896
  const relPath = toPosixPath(partPath);
34621
34897
  const partDir = assertContainedProjectPath(parentRoot, relPath);
34622
34898
  if (path25.resolve(partDir) === path25.resolve(parentRoot)) throw new WaironError(`cannot externalize "${subsystemId}": the path "${relPath}" is the project root itself.`);
@@ -34629,19 +34905,74 @@ function externalizeInto(subsystemId, partPath, as) {
34629
34905
  if (!joined && (declaredMembers(projectConfigRepository.load() ?? {}).some((m) => m.alias === alias) || projectConfigRepository.load()?.externals?.[alias] !== void 0)) {
34630
34906
  throw new WaironError(`cannot externalize "${subsystemId}": this project already declares the alias "${alias}".`);
34631
34907
  }
34632
- const targetDir = path25.join(aiPathsAt(partDir).specsDir(), subsystemId);
34633
- if (fs16.existsSync(targetDir)) throw new WaironError(`target already contains a "${subsystemId}" subsystem: ${targetDir}`);
34908
+ const partSpecsDir = aiPathsAt(partDir).specsDir();
34909
+ const targetDir = path25.join(partSpecsDir, subsystemId);
34910
+ if (owned.home !== void 0 && fs16.existsSync(targetDir)) throw new WaironError(`target already contains a "${subsystemId}" subsystem: ${targetDir}`);
34911
+ const destinationOf = (file) => path25.join(partSpecsDir, path25.relative(parentSpecsDir, file));
34912
+ const strays = owned.files.filter((f) => !(owned.home !== void 0 && isWithinDir2(owned.home, f)));
34913
+ const taken = strays.filter((f) => fs16.existsSync(destinationOf(f)));
34914
+ if (taken.length > 0) throw new WaironError(`cannot externalize "${subsystemId}": the part already holds ${taken.map((f) => toPosixPath(path25.relative(partDir, destinationOf(f)))).join(", ")}.`);
34634
34915
  if (as === "project") {
34635
34916
  if (joined) throw new WaironError(`cannot externalize "${subsystemId}" as a project into the existing part "${joined.alias}": externalize it into the part, then promote the part (\`wairon member promote ${joined.alias}\`).`);
34636
- const refusals = promoteRefusals(family, specFilesUnder(fooDir), ownSpecFiles(family, fooDir), alias);
34917
+ const inside = new Set(owned.files.map((f) => path25.resolve(f)));
34918
+ const refusals = promoteRefusals(family, owned.files, ownSpecFiles(family, fooDir).filter((f) => !inside.has(path25.resolve(f))), alias);
34637
34919
  if (refusals.length > 0) throw new WaironError(`cannot externalize "${subsystemId}" as a project: ${refusals.join("; ")}.`);
34638
34920
  }
34639
- ensureDir(path25.dirname(targetDir));
34640
- fs16.renameSync(fooDir, targetDir);
34641
- rebaseImplementationPaths(targetDir, parentRoot, partDir);
34921
+ const moved = [];
34922
+ if (owned.home !== void 0) {
34923
+ ensureDir(path25.dirname(targetDir));
34924
+ fs16.renameSync(owned.home, targetDir);
34925
+ moved.push(...specFilesUnder(targetDir));
34926
+ }
34927
+ for (const file of strays) {
34928
+ const to = destinationOf(file);
34929
+ ensureDir(path25.dirname(to));
34930
+ fs16.renameSync(file, to);
34931
+ moved.push(to);
34932
+ removeEmptyDirsUpTo(path25.dirname(file), parentSpecsDir);
34933
+ }
34934
+ rebaseImplementationPaths(partSpecsDir, parentRoot, partDir, moved);
34642
34935
  if (!joined) projectConfigRepository.declareMember(alias, { source: relPath });
34643
34936
  invalidateSpecCache();
34644
- if (as === "project") promoteMember(alias);
34937
+ return as === "project" ? promoteMember(alias) : void 0;
34938
+ }
34939
+ function subsystemFiles(subsystemId, specsDir) {
34940
+ const subPath = getSubsystemPath(subsystemId);
34941
+ const nested = subPath.endsWith(".index.yaml") && path25.resolve(path25.dirname(subPath)) === path25.resolve(path25.join(specsDir, subsystemId)) && fs16.existsSync(subPath);
34942
+ const home = nested ? path25.dirname(subPath) : void 0;
34943
+ const files = new Set(home !== void 0 ? specFilesUnder(home).map((f) => path25.resolve(f)) : []);
34944
+ if (!nested && fs16.existsSync(subPath)) files.add(path25.resolve(subPath));
34945
+ const l0 = path25.resolve(aiPathsAt(getProjectRoot()).specsSystem());
34946
+ const others = () => specFilesUnder(specsDir).filter((f) => path25.resolve(f) !== l0 && !files.has(path25.resolve(f)));
34947
+ const add2 = (file) => {
34948
+ if (path25.basename(file) === ".index.yaml") for (const f of specFilesUnder(path25.dirname(file))) files.add(path25.resolve(f));
34949
+ else files.add(path25.resolve(file));
34950
+ };
34951
+ for (const file of others()) {
34952
+ let raw;
34953
+ try {
34954
+ raw = readYamlFile(file);
34955
+ } catch {
34956
+ continue;
34957
+ }
34958
+ const kind = raw && typeof raw === "object" ? specKind(raw) : void 0;
34959
+ if (kind === "subsystem" || kind === "system" || raw?.subsystem !== subsystemId) continue;
34960
+ add2(file);
34961
+ }
34962
+ const owned = () => rawSpecs([...files]);
34963
+ const components = new Set(owned().filter((x) => x.kind === "component").map((x) => x.id));
34964
+ for (const spec of rawSpecs(others())) if (spec.kind === "interface" && components.has(spec.raw?.component)) add2(spec.file);
34965
+ const contracts = new Set(owned().filter((x) => x.kind === "interface").map((x) => x.id));
34966
+ for (const spec of rawSpecs(others())) if (spec.kind === "implementation" && contracts.has(spec.raw?.contract)) add2(spec.file);
34967
+ return { home, files: [...files] };
34968
+ }
34969
+ function removeEmptyDirsUpTo(dir, stop2) {
34970
+ let at = path25.resolve(dir);
34971
+ const end = path25.resolve(stop2);
34972
+ while (at !== end && isWithinDir2(end, at) && fs16.existsSync(at) && fs16.readdirSync(at).length === 0) {
34973
+ fs16.rmdirSync(at);
34974
+ at = path25.dirname(at);
34975
+ }
34645
34976
  }
34646
34977
  function specFilesUnder(dir) {
34647
34978
  return fs16.existsSync(dir) ? listFilesRecursive(dir, ".yaml") : [];
@@ -34667,7 +34998,8 @@ function rawSpecs(files) {
34667
34998
  return out;
34668
34999
  }
34669
35000
  function typeTokens(expression) {
34670
- return typeof expression === "string" ? [...expression.matchAll(/[A-Za-z_][A-Za-z0-9_-]*/g)].map((m) => m[0]) : [];
35001
+ if (typeof expression !== "string") return [];
35002
+ return [...expression.matchAll(/(?:::)?[A-Za-z_][A-Za-z0-9_-]*(?:(?:::|\.)[A-Za-z_][A-Za-z0-9_-]*)*/g)].map((m) => m[0]).filter((t) => !t.includes(".") && !t.includes("::"));
34671
35003
  }
34672
35004
  function referencesOf(spec, wholeParams) {
34673
35005
  const out = [];
@@ -34676,13 +35008,13 @@ function referencesOf(spec, wholeParams) {
34676
35008
  return ref;
34677
35009
  });
34678
35010
  const expressions = [];
34679
- if (spec.kind === "interface") {
34680
- for (const m of spec.raw.methods ?? []) {
34681
- expressions.push(m?.returns);
34682
- for (const p of m?.params ?? []) if (wholeParams || !(typeof p?.type === "string" && /^[A-Za-z_][A-Za-z0-9_-]*$/.test(p.type))) expressions.push(p?.type);
34683
- }
35011
+ for (const slot of typeExpressionSlots(spec.kind, spec.raw)) {
35012
+ const value = slot.holder[slot.key];
35013
+ if (slot.path === "methods.signatureFrom") continue;
35014
+ if (slot.path === "methods.signature" && Array.isArray(slot.method?.params)) continue;
35015
+ if (slot.path === "methods.params.type" && spec.kind === "interface" && !wholeParams && /^[A-Za-z_][A-Za-z0-9_-]*$/.test(String(value))) continue;
35016
+ expressions.push(slot.path === "methods.signature" ? String(value).replace(/^\s*[A-Za-z_][\w-]*\s*(?:<[^>]*>)?\s*\(/, "(").replace(/(?<![:\w-])[A-Za-z_][\w-]*\s*\??\s*:(?!:)/g, "") : value);
34684
35017
  }
34685
- if (spec.kind === "type") for (const f of spec.raw.fields ?? []) expressions.push(f?.type);
34686
35018
  for (const token of expressions.flatMap(typeTokens)) out.push({ ref: token, position: "bare-type" });
34687
35019
  return out;
34688
35020
  }
@@ -34690,6 +35022,9 @@ function crossingsOf(inside, outside) {
34690
35022
  const idsOf = (specs) => new Set(specs.filter((s) => s.kind !== "system").map((s) => s.id));
34691
35023
  const insideIds = idsOf(inside);
34692
35024
  const outsideIds = idsOf(outside);
35025
+ const targetsOf2 = (specs) => new Set(specs.filter((s) => s.kind === "component" || s.kind === "interface" || s.kind === "type").map((s) => s.id));
35026
+ const insideTargets = targetsOf2(inside);
35027
+ const outsideTargets = targetsOf2(outside);
34693
35028
  const typeIds = (specs) => new Map(specs.filter((s) => s.kind === "type").map((s) => [nameKey(s.id), s.id]));
34694
35029
  const insideTypes = typeIds(inside);
34695
35030
  const outsideTypes = typeIds(outside);
@@ -34707,13 +35042,14 @@ function crossingsOf(inside, outside) {
34707
35042
  }
34708
35043
  return found;
34709
35044
  };
34710
- return { out: collect2(inside, outsideIds, outsideTypes, true), in: collect2(outside, insideIds, insideTypes, false), insideIds, outsideIds };
35045
+ return { out: collect2(inside, outsideTargets, outsideTypes, true), in: collect2(outside, insideTargets, insideTypes, false), insideIds, outsideIds, insideTargets, outsideTargets };
34711
35046
  }
34712
35047
  function promoteRefusals(family, insideFiles, outsideFiles, id) {
34713
35048
  const out = [];
34714
35049
  const inside = rawSpecs(insideFiles);
34715
35050
  const outside = rawSpecs(outsideFiles);
34716
35051
  const crossing = crossingsOf(inside, outside);
35052
+ out.push(...unreachableUpward(inside, outside));
34717
35053
  const subsystemsIn = new Set(inside.filter((s) => s.kind === "subsystem").map((s) => s.id));
34718
35054
  for (const s of [...inside, ...outside].filter((x) => x.kind === "subsystem")) {
34719
35055
  for (const link of s.raw.trustedLinks ?? []) {
@@ -34754,13 +35090,19 @@ function climbsOf(family, ids, parentId) {
34754
35090
  }
34755
35091
  return { map, refused: [...new Set(refused2)] };
34756
35092
  }
34757
- function rewriteRefsIn(specs, remap) {
35093
+ function rewriteRefsIn(specs, remap, record2) {
34758
35094
  const rewritten = [];
34759
35095
  for (const spec of specs) {
34760
35096
  const raw = readYamlFile(spec.file);
34761
- if (!rewriteSpecRefs(raw, (ref, position) => remap(ref, position))) continue;
35097
+ const edits = [];
35098
+ if (!rewriteSpecRefs(raw, (ref, position) => {
35099
+ const to = remap(ref, position);
35100
+ if (to !== ref) edits.push({ kind: spec.kind, specId: spec.id, position, from: ref, to });
35101
+ return to;
35102
+ })) continue;
34762
35103
  writeYamlFile(spec.file, raw);
34763
35104
  rewritten.push({ kind: spec.kind, id: spec.id });
35105
+ if (record2) for (const e of edits) recordRespelling(record2.result, e, record2.member);
34764
35106
  }
34765
35107
  return rewritten;
34766
35108
  }
@@ -34810,15 +35152,19 @@ function promoteMember(alias, id) {
34810
35152
  patchKeepingStamp(s.file, (raw) => raw.parentSystem === projectName ? false : (raw.parentSystem = projectName, true));
34811
35153
  }
34812
35154
  if (declaration2?.as === "part") projectConfigRepository.updateMember(alias, { as: "" });
34813
- const parentSide = rewriteRefsIn(crossing.in.map((c) => c.spec).filter(unique), (ref, position) => INTO_MEMBER_POSITIONS.has(position) && crossing.insideIds.has(ref) ? `${alias}::${ref}` : ref);
35155
+ const result = { respelled: [], memberRespelled: [], exported: [], imported: [], consumersDropped: [] };
35156
+ const parentSide = rewriteRefsIn(crossing.in.map((c) => c.spec).filter(unique), (ref, position) => INTO_MEMBER_POSITIONS.has(position) && crossing.insideTargets.has(ref) ? `${alias}::${ref}` : ref, { result, member: false });
34814
35157
  const climbs = climbsOf(family, crossing.insideIds, parentId).map;
34815
35158
  const climbing = inside.filter((s) => family.authoredReferences.some((r) => r.form === "super" && r.specId === s.id));
34816
- const memberSide = rewriteRefsIn([...crossing.out.map((c) => c.spec), ...climbing].filter(unique), (ref, position) => climbs.get(ref) ?? (MOVED_REF_POSITIONS.has(position) && crossing.outsideIds.has(ref) ? `${parentId}::${ref}` : ref));
35159
+ const memberSide = rewriteRefsIn([...crossing.out.map((c) => c.spec), ...climbing].filter(unique), (ref, position) => climbs.get(ref) ?? (MOVED_REF_POSITIONS.has(position) && crossing.outsideTargets.has(ref) ? `${parentId}::${ref}` : ref), { result, member: true });
35160
+ const qualified = respellQualified(inside, outside, { alias, parentId, newId }, result);
34817
35161
  invalidateSpecCache();
34818
- if (crossing.out.length > 0 || [...climbs.values()].some((t) => t.startsWith(`${parentId}::`))) runWithProjectRoot(dir, () => projectConfigRepository.declareExternal(parentId, {}));
35162
+ dropCrossingConsumers(inside, outside, result);
35163
+ if (crossing.out.length > 0 || qualified.out.length > 0 || [...climbs.values()].some((t) => t.startsWith(`${parentId}::`))) runWithProjectRoot(dir, () => projectConfigRepository.declareExternal(parentId, {}));
34819
35164
  for (const s of inside.filter((x) => x.kind === "subsystem")) restateReExports(s.id, alias);
34820
- exportCrossings(crossing.out, dir, alias, parentId, "out");
34821
- exportCrossings(crossing.in, dir, alias, parentId, "in");
35165
+ exportCrossings([...crossing.out, ...qualified.out], dir, alias, parentId, "out", result);
35166
+ exportCrossings([...crossing.in, ...qualified.in], dir, alias, parentId, "in", result);
35167
+ exportReExported(inside, dir, alias, result);
34822
35168
  invalidateSpecCache();
34823
35169
  for (const spec of parentSide) if (spec.kind !== "system") normalizeReferences(spec.kind, spec.id);
34824
35170
  const memberKey = graph().nodes.find((n) => n.parent === "" && n.mountAlias === alias)?.namespace;
@@ -34837,6 +35183,165 @@ function promoteMember(alias, id) {
34837
35183
  });
34838
35184
  }
34839
35185
  invalidateSpecCache();
35186
+ return result;
35187
+ }
35188
+ function recordRespelling(result, edit4, member) {
35189
+ if (edit4.from === edit4.to) return;
35190
+ const list2 = member ? result.memberRespelled : result.respelled;
35191
+ if (list2.some((e) => e.kind === edit4.kind && e.specId === edit4.specId && e.position === edit4.position && e.from === edit4.from)) return;
35192
+ list2.push(edit4);
35193
+ }
35194
+ function assertedTypeRef(ref) {
35195
+ const at = ref.lastIndexOf(".");
35196
+ return at > 0 && at < ref.length - 1 ? ref.slice(0, at) : void 0;
35197
+ }
35198
+ function sidedTypes(inside, outside) {
35199
+ const types = [
35200
+ ...inside.filter((s) => s.kind === "type").map((s) => ({ spec: s, side: "inside" })),
35201
+ ...outside.filter((s) => s.kind === "type").map((s) => ({ spec: s, side: "outside" }))
35202
+ ];
35203
+ const subsystems = new Set([...inside, ...outside].filter((s) => s.kind === "subsystem").map((s) => nameKey(s.id)));
35204
+ return {
35205
+ qualified: (token) => {
35206
+ const segments = token.split(/::|\./);
35207
+ if (segments.length !== 2 || segments.some((x) => x === "") || !subsystems.has(nameKey(segments[0]))) return void 0;
35208
+ const found = types.filter((t) => typeof t.spec.raw.subsystem === "string" && nameKey(t.spec.raw.subsystem) === nameKey(segments[0]) && nameKey(t.spec.id) === nameKey(segments[1]));
35209
+ return found.length === 1 ? found[0] : void 0;
35210
+ },
35211
+ bare: (token) => {
35212
+ const found = types.filter((t) => nameKey(t.spec.id) === nameKey(token));
35213
+ return found.length === 1 ? found[0] : void 0;
35214
+ }
35215
+ };
35216
+ }
35217
+ function unreachableUpward(inside, outside) {
35218
+ const out = [];
35219
+ const types = sidedTypes(inside, outside);
35220
+ const outsideComponents = new Set(outside.filter((x) => x.kind === "component").map((x) => x.id));
35221
+ const target = (ref) => ref.includes(".") || ref.includes("::") ? types.qualified(ref) : types.bare(ref);
35222
+ for (const spec of inside) {
35223
+ if (spec.kind === "interface") {
35224
+ for (const m of spec.raw.methods ?? []) {
35225
+ const source = m?.signatureFrom;
35226
+ if (typeof source !== "string") continue;
35227
+ const head2 = source.includes(".") ? source.slice(0, source.lastIndexOf(".")) : void 0;
35228
+ if (target(source)?.side === "outside" || head2 !== void 0 && outsideComponents.has(head2)) {
35229
+ out.push(`"${spec.id}" method "${m.name}" takes its signature from "${source}", which stays in this project: a signatureFrom is not read across a project boundary \u2014 state its params and returns first`);
35230
+ }
35231
+ }
35232
+ }
35233
+ if (spec.kind === "implementation") {
35234
+ for (const m of spec.raw.methods ?? []) {
35235
+ for (const step of m?.narrative ?? []) {
35236
+ for (const ref of step?.assertsInvariants ?? []) {
35237
+ const typeRef2 = typeof ref === "string" ? assertedTypeRef(ref) : void 0;
35238
+ if (typeRef2 !== void 0 && !typeRef2.includes("::") && target(typeRef2)?.side === "outside") {
35239
+ out.push(`"${spec.id}" asserts the invariant "${ref}" of an entity that stays in this project: an invariant is asserted by its own project's write paths \u2014 move the assertion first`);
35240
+ }
35241
+ }
35242
+ }
35243
+ }
35244
+ }
35245
+ }
35246
+ return out;
35247
+ }
35248
+ function respellQualified(inside, outside, names, result) {
35249
+ const crossings = { out: [], in: [] };
35250
+ const { qualified: qualifiedTarget, bare: bareTarget } = sidedTypes(inside, outside);
35251
+ const respell = (spec, side, token, target, selfPrefix2) => {
35252
+ if (!target) return token;
35253
+ if (target.side !== side) {
35254
+ const to = `${side === "outside" ? names.alias : names.parentId}::${target.spec.id}`;
35255
+ (side === "inside" ? crossings.out : crossings.in).push({ spec, target: target.spec.id, position: "type" });
35256
+ return to;
35257
+ }
35258
+ return side === "inside" && selfPrefix2 ? target.spec.id : token;
35259
+ };
35260
+ for (const [specs, side] of [[inside, "inside"], [outside, "outside"]]) {
35261
+ for (const spec of specs) {
35262
+ if (!fs16.existsSync(spec.file)) continue;
35263
+ const raw = readYamlFile(spec.file);
35264
+ if (!raw || typeof raw !== "object") continue;
35265
+ const edits = [];
35266
+ const record2 = (from, to) => {
35267
+ if (from !== to) edits.push({ kind: spec.kind, specId: spec.id, position: "type", from, to });
35268
+ };
35269
+ const mapToken = (token) => {
35270
+ const target = qualifiedTarget(token);
35271
+ const to = respell(spec, side, token, target, nameKey(token.split(/::|\./)[0]) === nameKey(names.newId));
35272
+ record2(token, to);
35273
+ return to;
35274
+ };
35275
+ for (const slot of typeExpressionSlots(spec.kind, raw)) {
35276
+ const value = slot.holder[slot.key];
35277
+ if (slot.path === "methods.signatureFrom" && !qualifiedTarget(value)) continue;
35278
+ slot.holder[slot.key] = respellTypeNames(value, mapToken);
35279
+ }
35280
+ if (spec.kind === "implementation") {
35281
+ for (const m of raw.methods ?? []) {
35282
+ for (const step of m?.narrative ?? []) {
35283
+ if (!Array.isArray(step?.assertsInvariants)) continue;
35284
+ step.assertsInvariants = step.assertsInvariants.map((ref) => {
35285
+ const typeRef2 = typeof ref === "string" ? assertedTypeRef(ref) : void 0;
35286
+ if (typeRef2 === void 0 || typeRef2.includes("::") && !qualifiedTarget(typeRef2)) return ref;
35287
+ const target = typeRef2.includes(".") || typeRef2.includes("::") ? qualifiedTarget(typeRef2) : bareTarget(typeRef2);
35288
+ const to = respell(spec, side, typeRef2, target, typeRef2.includes(".") && nameKey(typeRef2.split(".")[0]) === nameKey(names.newId));
35289
+ if (to === typeRef2) return ref;
35290
+ record2(ref, `${to}${ref.slice(typeRef2.length)}`);
35291
+ return `${to}${ref.slice(typeRef2.length)}`;
35292
+ });
35293
+ }
35294
+ }
35295
+ }
35296
+ if (edits.length === 0) continue;
35297
+ writeYamlFile(spec.file, raw);
35298
+ for (const e of edits) recordRespelling(result, e, side === "inside");
35299
+ }
35300
+ }
35301
+ return crossings;
35302
+ }
35303
+ function dropCrossingConsumers(inside, outside, result) {
35304
+ const idsOf = (specs) => new Set(specs.filter((s) => s.kind === "subsystem").map((s) => s.id));
35305
+ const sides = [[inside, idsOf(outside)], [outside, idsOf(inside)]];
35306
+ for (const [specs, across] of sides) {
35307
+ for (const spec of specs.filter((s) => s.kind === "subsystem")) {
35308
+ patchKeepingStamp(spec.file, (raw) => {
35309
+ let changed = false;
35310
+ for (const entry of raw.publicInterfaces ?? []) {
35311
+ if (!Array.isArray(entry?.consumers)) continue;
35312
+ const dropped = entry.consumers.filter((c) => typeof c === "string" && across.has(c));
35313
+ if (dropped.length === 0) continue;
35314
+ const kept = entry.consumers.filter((c) => !dropped.includes(c));
35315
+ if (kept.length > 0) entry.consumers = kept;
35316
+ else delete entry.consumers;
35317
+ changed = true;
35318
+ const what = entry.component ?? entry.typeDef ?? entry.interface ?? entry.from ?? "(entry)";
35319
+ result.consumersDropped.push(`${spec.id} ${what}: ${dropped.join(", ")}${kept.length === 0 ? " (the list is gone: the L0 export table governs who outside may use it)" : ""}`);
35320
+ }
35321
+ return changed;
35322
+ });
35323
+ }
35324
+ }
35325
+ }
35326
+ function exportReExported(inside, memberDir, alias, result) {
35327
+ invalidateSpecCache();
35328
+ const entries = (loadSystemSpec()?.publicInterfaces ?? []).filter((e) => e.from === alias);
35329
+ if (entries.length === 0) return;
35330
+ const targets = entries.map((e) => e.component ?? e.typeDef).filter((t) => t !== void 0);
35331
+ const whole = entries.some((e) => e.component === void 0 && e.typeDef === void 0);
35332
+ runWithProjectRoot(memberDir, () => {
35333
+ for (const name of exportItems(targets)) result.exported.push(`${alias} L0: ${name} (re-exported by the parent)`);
35334
+ if (!whole) return;
35335
+ invalidateSpecCache();
35336
+ const system = loadSystemSpec();
35337
+ if (!system) return;
35338
+ const own = system.publicInterfaces ?? [];
35339
+ const added = inside.filter((s) => s.kind === "subsystem" && !own.some((e) => e.from === s.id && e.component === void 0 && e.typeDef === void 0)).map((s) => ({ from: s.id, audience: "project" }));
35340
+ if (added.length === 0) return;
35341
+ saveSystemSpec({ ...system, publicInterfaces: [...own, ...added] });
35342
+ for (const e of added) result.exported.push(`${alias} L0: everything ${e.from} exports (re-exported whole by the parent)`);
35343
+ });
35344
+ invalidateSpecCache();
34840
35345
  }
34841
35346
  function promotedVision(id, parentName) {
34842
35347
  return `Project ${id}, promoted from a part of ${parentName}`;
@@ -34844,43 +35349,57 @@ function promotedVision(id, parentName) {
34844
35349
  function unique(value, index, all) {
34845
35350
  return all.indexOf(value) === index;
34846
35351
  }
34847
- function exportCrossings(crossings, memberDir, alias, parentId, direction) {
35352
+ function exportCrossings(crossings, memberDir, alias, parentId, direction, result) {
34848
35353
  const targets = [...new Set(crossings.map((c) => c.target))];
34849
35354
  const bare = [...new Set(crossings.filter((c) => c.position === "bare-type").map((c) => c.target))];
34850
35355
  if (direction === "out") {
34851
- exportItems(targets);
34852
- if (bare.length > 0) projectConfigRepositoryAt(memberDir).importNames(parentId, bare);
35356
+ for (const name of exportItems(targets)) result.exported.push(`${parentId} L0: ${name}`);
35357
+ if (bare.length > 0) {
35358
+ projectConfigRepositoryAt(memberDir).importNames(parentId, bare);
35359
+ result.imported.push(`${alias}: externals.${parentId}.use gains ${bare.join(", ")}`);
35360
+ }
34853
35361
  } else {
34854
- runWithProjectRoot(memberDir, () => exportItems(targets));
34855
- if (bare.length > 0) projectConfigRepository.importNames(alias, bare);
35362
+ for (const name of runWithProjectRoot(memberDir, () => exportItems(targets))) result.exported.push(`${alias} L0: ${name}`);
35363
+ if (bare.length > 0) {
35364
+ projectConfigRepository.importNames(alias, bare);
35365
+ result.imported.push(`${parentId}: members.${alias}.use gains ${bare.join(", ")}`);
35366
+ }
34856
35367
  }
34857
35368
  }
34858
35369
  function localIn(key, project2) {
34859
35370
  return project2 && key.startsWith(`${project2}::`) ? key.slice(project2.length + 2) : key;
34860
35371
  }
34861
35372
  function exportItems(ids) {
34862
- if (ids.length === 0) return;
35373
+ if (ids.length === 0) return [];
34863
35374
  invalidateSpecCache();
34864
35375
  const system = loadSystemSpec();
34865
- if (!system) return;
35376
+ if (!system) return [];
34866
35377
  const entries = [...system.publicInterfaces ?? []];
34867
35378
  const types = loadTypeSpecs();
35379
+ const added = [];
34868
35380
  for (const id of ids) {
34869
35381
  if (!PUBLIC_NAME_RE.test(id)) continue;
34870
35382
  const type = types.find((t) => t.id === id);
34871
35383
  if (type) {
34872
35384
  if (type.subsystem) exportTypeFromSubsystem(type.subsystem, id);
34873
- if (!entries.some((e) => e.typeDef === id && e.from === void 0)) entries.push({ typeDef: id, audience: "project" });
35385
+ if (!entries.some((e) => e.typeDef === id && (e.from === void 0 || e.from === type.subsystem))) {
35386
+ entries.push({ ...type.subsystem ? { from: type.subsystem } : {}, typeDef: id, audience: "project" });
35387
+ added.push(type.subsystem ? `${id} (from ${type.subsystem})` : id);
35388
+ }
34874
35389
  continue;
34875
35390
  }
34876
35391
  const component = loadComponentSpec(id);
34877
35392
  if (!component) continue;
34878
35393
  const exported = resolveSubsystemExports(component.subsystem).entries.some((e) => e.kind === "component" && e.component === id);
34879
- if (exported && !entries.some((e) => e.component === id)) entries.push({ from: component.subsystem, component: id, audience: "project" });
35394
+ if (exported && !entries.some((e) => e.component === id)) {
35395
+ entries.push({ from: component.subsystem, component: id, audience: "project" });
35396
+ added.push(`${id} (from ${component.subsystem})`);
35397
+ }
34880
35398
  }
34881
35399
  if (entries.length !== (system.publicInterfaces ?? []).length) {
34882
35400
  saveSystemSpec({ ...system, publicInterfaces: entries });
34883
35401
  }
35402
+ return added;
34884
35403
  }
34885
35404
  function exportTypeFromSubsystem(subsystemId, typeId) {
34886
35405
  const sub = loadSubsystemSpec(subsystemId);
@@ -34910,7 +35429,7 @@ function internalizeMember(alias, destination) {
34910
35429
  if (refusals.length > 0) {
34911
35430
  throw new WaironError(`cannot internalize "${alias}": the member cannot be taken in whole \u2014 ${refusals.join("; ")}.`);
34912
35431
  }
34913
- const result = { subsystems: scan.subsystems, types: scan.types, placed: [], externals: [], deleted: [], notCarried: [] };
35432
+ const result = { subsystems: scan.subsystems, types: scan.types, placed: [], externals: [], deleted: [], notCarried: [], consumersRestored: [] };
34914
35433
  const intoMember = crossingReferences(family, "", member.namespace);
34915
35434
  const backOut = crossingReferences(family, member.namespace, "");
34916
35435
  deleteMount(alias);
@@ -34940,7 +35459,7 @@ function internalizePart(alias, part) {
34940
35459
  projectConfigRepository.removeMember(alias);
34941
35460
  const deleted = deleteMemberWai(partDir);
34942
35461
  invalidateSpecCache();
34943
- return { subsystems: part.subsystems, types, placed: [], externals: [], deleted, notCarried: [] };
35462
+ return { subsystems: part.subsystems, types, placed: [], externals: [], deleted, notCarried: [], consumersRestored: [] };
34944
35463
  }
34945
35464
  function scanMember(family, alias, member, destination) {
34946
35465
  const root = getProjectRoot();
@@ -35288,7 +35807,7 @@ function demoteMember(alias, destination) {
35288
35807
  refusals.push(`its metadata needs a home subsystem (--home) \u2014 ${home === "" ? "none was named" : `"${home}" is no subsystem of the member or of this project`}; its subsystems are ${subsystems.join(", ") || "none"}`);
35289
35808
  }
35290
35809
  if (refusals.length > 0) throw new WaironError(`demote refused: "${alias}" cannot become a part \u2014 ${refusals.join("; ")}.`);
35291
- const result = { subsystems, types, placed: [], externals: [], deleted: [], notCarried: [] };
35810
+ const result = { subsystems, types, placed: [], externals: [], deleted: [], notCarried: [], consumersRestored: [] };
35292
35811
  const intoMember = localsOf(dir);
35293
35812
  const backOut = localsOf(root);
35294
35813
  invalidateSpecCache();
@@ -35305,6 +35824,10 @@ function demoteMember(alias, destination) {
35305
35824
  const parentSide = rewriteRefsIn(ownSpecs, across(/* @__PURE__ */ new Set([alias]), intoMember, false));
35306
35825
  const memberSide = rewriteRefsIn(memberSpecs, across(parentAliases, backOut, true));
35307
35826
  for (const pa of parentAliases) for (const name of config?.externals?.[pa]?.use ?? []) if (name !== "*") crossed.add(backOut.get(name) ?? name);
35827
+ const memberNow = rawSpecs(specFilesUnder(aiPathsAt(dir).specsDir()).filter((f) => path25.resolve(f) !== path25.resolve(aiPathsAt(dir).specsSystem())));
35828
+ const parentNow = rawSpecs(ownSpecFiles(family, aiPathsAt(dir).specsDir()));
35829
+ respellQualifiedBack(parentNow, memberNow, { intoMember: (a) => a === alias, backOut: (a) => parentAliases.has(a) }, { intoMember, backOut }, crossed);
35830
+ restoreCrossingConsumers(memberNow, parentNow, result);
35308
35831
  const parentName = loadSystemSpec()?.name;
35309
35832
  if (parentName) {
35310
35833
  for (const s of memberSpecs.filter((x) => x.kind === "subsystem")) {
@@ -35329,6 +35852,79 @@ function demoteMember(alias, destination) {
35329
35852
  invalidateSpecCache();
35330
35853
  return result;
35331
35854
  }
35855
+ function respellQualifiedBack(parentSide, memberSide, aliases, tables, crossed) {
35856
+ const types = [...parentSide, ...memberSide].filter((s) => s.kind === "type");
35857
+ const localForm = (id) => {
35858
+ const found = types.filter((t) => t.id === id);
35859
+ if (found.length !== 1) return void 0;
35860
+ const sub = found[0].raw?.subsystem;
35861
+ return typeof sub === "string" && sub !== "" ? `${sub}.${id}` : id;
35862
+ };
35863
+ for (const [specs, across, table, record2] of [
35864
+ [parentSide, aliases.intoMember, tables.intoMember, false],
35865
+ [memberSide, aliases.backOut, tables.backOut, true]
35866
+ ]) {
35867
+ const local = (token) => {
35868
+ const segments = token.split("::");
35869
+ if (segments.length !== 2 || !across(segments[0])) return token;
35870
+ const id = table.get(segments[1]) ?? segments[1];
35871
+ const to = localForm(id);
35872
+ if (to === void 0) return token;
35873
+ if (record2) crossed.add(id);
35874
+ return to;
35875
+ };
35876
+ for (const spec of specs) {
35877
+ if (!fs16.existsSync(spec.file)) continue;
35878
+ const raw = readYamlFile(spec.file);
35879
+ if (!raw || typeof raw !== "object") continue;
35880
+ let changed = false;
35881
+ for (const slot of typeExpressionSlots(spec.kind, raw)) {
35882
+ const value = slot.holder[slot.key];
35883
+ const next = respellTypeNames(value, local);
35884
+ if (next !== value) {
35885
+ slot.holder[slot.key] = next;
35886
+ changed = true;
35887
+ }
35888
+ }
35889
+ if (spec.kind === "implementation") {
35890
+ for (const m of raw.methods ?? []) {
35891
+ for (const step of m?.narrative ?? []) {
35892
+ if (!Array.isArray(step?.assertsInvariants)) continue;
35893
+ step.assertsInvariants = step.assertsInvariants.map((ref) => {
35894
+ const typeRef2 = typeof ref === "string" ? assertedTypeRef(ref) : void 0;
35895
+ if (typeRef2 === void 0 || !typeRef2.includes("::")) return ref;
35896
+ const to = local(typeRef2);
35897
+ if (to === typeRef2) return ref;
35898
+ changed = true;
35899
+ return `${to}${ref.slice(typeRef2.length)}`;
35900
+ });
35901
+ }
35902
+ }
35903
+ }
35904
+ if (changed) writeYamlFile(spec.file, raw);
35905
+ }
35906
+ }
35907
+ }
35908
+ function restoreCrossingConsumers(memberSide, parentSide, result) {
35909
+ const sides = [[memberSide, parentSide], [parentSide, memberSide]];
35910
+ for (const [providers, consumers] of sides) {
35911
+ const dependents = consumers.filter((c) => c.kind === "component" && typeof c.raw?.subsystem === "string" && Array.isArray(c.raw?.dependsOn));
35912
+ for (const spec of providers.filter((x) => x.kind === "subsystem")) {
35913
+ patchKeepingStamp(spec.file, (raw) => {
35914
+ let changed = false;
35915
+ for (const entry of raw.publicInterfaces ?? []) {
35916
+ if (!Array.isArray(entry?.consumers) || entry.consumers.length === 0 || typeof entry.component !== "string") continue;
35917
+ const users = [...new Set(dependents.filter((c) => c.raw.dependsOn.includes(entry.component)).map((c) => c.raw.subsystem))].filter((sub) => sub !== spec.id && !entry.consumers.includes(sub));
35918
+ if (users.length === 0) continue;
35919
+ entry.consumers = [...entry.consumers, ...users];
35920
+ changed = true;
35921
+ result.consumersRestored.push(`${spec.id} ${entry.component}: ${users.join(", ")}`);
35922
+ }
35923
+ return changed;
35924
+ });
35925
+ }
35926
+ }
35927
+ }
35332
35928
  function deleteProjectContent(memberDir) {
35333
35929
  const wai = path25.join(memberDir, ".wai");
35334
35930
  const deleted = [];
@@ -35358,9 +35954,9 @@ function retireBoundaryExports(family, used, memberKey) {
35358
35954
  const { publicInterfaces: _dropped, ...rest } = system;
35359
35955
  saveSystemSpec(kept.length > 0 ? { ...rest, publicInterfaces: kept } : rest);
35360
35956
  invalidateSpecCache();
35361
- for (const e of retired.filter((x) => x.typeDef !== void 0 && x.from === void 0)) {
35957
+ for (const e of retired.filter((x) => x.typeDef !== void 0)) {
35362
35958
  const type = loadTypeSpecs().find((t) => t.id === e.typeDef);
35363
- if (!type?.subsystem) continue;
35959
+ if (!type?.subsystem || e.from !== void 0 && e.from !== type.subsystem) continue;
35364
35960
  patchKeepingStamp(getSubsystemPath(type.subsystem), (raw) => {
35365
35961
  const before = (raw.publicInterfaces ?? []).length;
35366
35962
  raw.publicInterfaces = (raw.publicInterfaces ?? []).filter((pi) => !(pi?.typeDef === e.typeDef && pi?.from === void 0 && Object.keys(pi).length === 1));
@@ -35529,7 +36125,7 @@ function renameComponent(componentId, newId) {
35529
36125
  }
35530
36126
  if (componentId.includes("::")) {
35531
36127
  throw new WaironError(
35532
- `chained-component: "${componentId}" lives in a chained subproject; rename it from that project's own root.`
36128
+ `chained-component: "${componentId}" lives in another project; rename it from that project's own root. ${memberRootWay(componentId, `sdd_rename_component ${componentId.slice(componentId.lastIndexOf("::") + 2)} <new id>`)}`
35533
36129
  );
35534
36130
  }
35535
36131
  if (!SpecIdSchema.safeParse(newId).success) {
@@ -35680,7 +36276,7 @@ function renameMethod(componentId, methodName, newName, pinSymbol) {
35680
36276
  }
35681
36277
  if (componentId.includes("::")) {
35682
36278
  throw new WaironError(
35683
- `chained-component: "${componentId}" lives in a chained subproject; rename its method from that project's own root.`
36279
+ `chained-component: "${componentId}" lives in another project; rename its method from that project's own root. ${memberRootWay(componentId, `sdd_rename_method ${componentId.slice(componentId.lastIndexOf("::") + 2)} <method> <new name>`)}`
35684
36280
  );
35685
36281
  }
35686
36282
  if (!METHOD_NAME.test(newName)) {
@@ -35838,18 +36434,29 @@ function respellLike(written, oldId, newId) {
35838
36434
  if (written.includes("-")) return kebab(newId);
35839
36435
  return /^[A-Z]/.test(written) ? pascal(newId) : newId;
35840
36436
  }
36437
+ function memberRootWay(qualifiedId2, call) {
36438
+ const alias = qualifiedId2.slice(0, qualifiedId2.indexOf("::"));
36439
+ let folder;
36440
+ try {
36441
+ const node = graph().nodes.find((n) => n.namespace === alias || n.parent === "" && n.mountAlias === alias);
36442
+ if (node) folder = path25.relative(getProjectRoot(), node.directory).split(path25.sep).join("/") || ".";
36443
+ } catch {
36444
+ }
36445
+ const where = folder ? `the member project "${alias}" at ${folder}` : `the member project "${alias}"`;
36446
+ return `It belongs to ${where}: open a session in that folder (its own guide and .mcp.json \u2014 run \`wairon generate\` and \`wairon mcp install --backend claude\` there first if it has none) and call ${call} there.`;
36447
+ }
35841
36448
  function renameType(typeId, newId) {
35842
36449
  const all = loadTypeSpecs();
35843
36450
  const cut = typeId.lastIndexOf("::");
35844
36451
  const qualifier = cut >= 0 ? typeId.slice(0, cut) : void 0;
35845
36452
  const ownSubsystem = qualifier !== void 0 && loadSubsystemSpecs().some((s) => s.id === qualifier);
35846
36453
  if (qualifier !== void 0 && !ownSubsystem) {
35847
- throw new WaironError(`chained-type: "${typeId}" lives in another project; rename it from that project's own root.`);
36454
+ throw new WaironError(`chained-type: "${typeId}" lives in another project; rename it from that project's own root. ${memberRootWay(typeId, `sdd_rename_type ${typeId.slice(cut + 2)} <new id>`)} An exported type keeps its public name through \`as\`, so this project's references do not break.`);
35848
36455
  }
35849
36456
  const type = qualifier !== void 0 ? all.find((t) => t.id === typeId.slice(cut + 2) && t.subsystem === qualifier) ?? null : loadTypeSpec(typeId);
35850
36457
  if (!type) throw new WaironError(`type-missing: no type has the id "${typeId}".`);
35851
36458
  if (type.id.includes("::")) {
35852
- throw new WaironError(`chained-type: "${type.id}" lives in another project; rename it from that project's own root.`);
36459
+ throw new WaironError(`chained-type: "${type.id}" lives in another project; rename it from that project's own root. ${memberRootWay(type.id, `sdd_rename_type ${type.id.slice(type.id.lastIndexOf("::") + 2)} <new id>`)}`);
35853
36460
  }
35854
36461
  const oldId = type.id;
35855
36462
  if (!SpecIdSchema.safeParse(newId).success) {
@@ -36177,8 +36784,8 @@ function walkForPackages(projectRoot2, currentDir, depth, results) {
36177
36784
  }
36178
36785
  }
36179
36786
  function pathToId(relPath) {
36180
- const basename16 = path26.basename(relPath);
36181
- return basename16.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
36787
+ const basename19 = path26.basename(relPath);
36788
+ return basename19.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
36182
36789
  }
36183
36790
  function pathToName(relPath) {
36184
36791
  const id = pathToId(relPath);
@@ -36708,7 +37315,7 @@ function isWithinDir3(dir, target) {
36708
37315
  const rel2 = path29.relative(path29.resolve(dir), path29.resolve(target));
36709
37316
  return rel2 === "" || !rel2.startsWith("..") && !path29.isAbsolute(rel2);
36710
37317
  }
36711
- function resolveLayer(delegate) {
37318
+ function resolveLayer(delegate, implementers = false) {
36712
37319
  projectFilesCache.clear();
36713
37320
  const system = loadSystemSpec();
36714
37321
  if (!system) return [];
@@ -36839,7 +37446,7 @@ function resolveLayer(delegate) {
36839
37446
  updatedAt: sub.updatedAt
36840
37447
  });
36841
37448
  }
36842
- if (config.rules.generateComponentImplementers) {
37449
+ if (config.rules.generateComponentImplementers || implementers) {
36843
37450
  for (const comp of components) {
36844
37451
  const compInterfaces = interfaces.filter((i) => i.component === comp.id);
36845
37452
  const compInterfaceIds = compInterfaces.map((i) => i.id);
@@ -36912,7 +37519,7 @@ function composeAgentBrief(agentId) {
36912
37519
  return throughMount(brief, hop.alias, hop.relative);
36913
37520
  }
36914
37521
  const records = resolveAgentTopology();
36915
- const record2 = records.find((r) => r.id === agentId);
37522
+ const record2 = records.find((r) => r.id === agentId) ?? componentImplementer(agentId);
36916
37523
  if (!record2) {
36917
37524
  throw new UnknownAgentError(agentId, records.map((r) => r.id));
36918
37525
  }
@@ -36931,6 +37538,17 @@ function composeAgentBrief(agentId) {
36931
37538
  domainName: "",
36932
37539
  variantGuidance: record2.variantGuidance ?? ""
36933
37540
  });
37541
+ const codeFence = codeFenceOf(record2);
37542
+ if (codeFence) {
37543
+ instructions = `${instructions.trimEnd()}
37544
+
37545
+ ## Code write fence
37546
+
37547
+ ${codeFence.length > 0 ? `Write code only here (and tests beside it, as the project lays tests out):
37548
+
37549
+ ${codeFence.map((p) => `- \`${p}\``).join("\n")}
37550
+ ` : "No implementation names a source file yet, so no code location is declared. The spawning session must name the file(s) this agent may create (the planned sourcePath); set `sourcePath` on the implementation once the file exists.\n"}`;
37551
+ }
36934
37552
  const guidance = loadAgentOverride(agentId);
36935
37553
  if (guidance !== null) {
36936
37554
  instructions = `${instructions.trimEnd()}
@@ -36965,10 +37583,34 @@ ${typeMapping.map((line2) => `- ${line2}`).join("\n")}
36965
37583
  instructions,
36966
37584
  variantGuidance: record2.variantGuidance || void 0,
36967
37585
  ...typeMapping ? { typeMapping } : {},
37586
+ ...codeFence ? { codeFence } : {},
36968
37587
  profile: budget ? profile : void 0,
36969
37588
  budget
36970
37589
  };
36971
37590
  }
37591
+ function componentImplementer(agentId) {
37592
+ const componentId = agentId.endsWith("-implementer") ? agentId.slice(0, -"-implementer".length) : agentId;
37593
+ if (componentId === "" || componentId.includes("::")) return null;
37594
+ return resolveLayer(true, true).find((r) => r.id === `${componentId}-implementer`) ?? null;
37595
+ }
37596
+ function codeFenceOf(record2) {
37597
+ const implementsComponents = record2.template === "implementer" || record2.template === "domain-owner" && record2.creationReason.startsWith("Automatically inferred from L1");
37598
+ if (!implementsComponents) return null;
37599
+ const files = record2.ownedPaths.filter((p) => !p.startsWith(".wai/") && !p.includes("/.wai/") && !/[*?]/.test(p));
37600
+ if (files.length === 0) return [];
37601
+ const dirs = files.map((f) => path29.posix.dirname(f.replace(/\\/g, "/")));
37602
+ let shared = dirs[0].split("/");
37603
+ for (const dir of dirs.slice(1)) {
37604
+ const parts = dir.split("/");
37605
+ let i = 0;
37606
+ while (i < shared.length && i < parts.length && shared[i] === parts[i]) i++;
37607
+ shared = shared.slice(0, i);
37608
+ }
37609
+ const folder = shared.join("/");
37610
+ const fence = [...files];
37611
+ if (folder !== "" && folder !== ".") fence.push(`${folder}/**`);
37612
+ return fence;
37613
+ }
36972
37614
  function implementationLanguage(record2) {
36973
37615
  const owned = new Set(record2.ownedPaths);
36974
37616
  const implementations = loadImplementationSpecs().filter((impl) => implementationSourceFiles(impl).some((file) => owned.has(file)));
@@ -37001,6 +37643,7 @@ function throughMount(brief, alias, relative28) {
37001
37643
  ...brief,
37002
37644
  agentId: `${alias}::${brief.agentId}`,
37003
37645
  ownedPaths: brief.ownedPaths.map(under),
37646
+ ...brief.codeFence ? { codeFence: brief.codeFence.map(under) } : {},
37004
37647
  ...brief.readPaths ? { readPaths: brief.readPaths.map(under) } : {},
37005
37648
  root: brief.root ? under(brief.root) : relative28
37006
37649
  };
@@ -37037,7 +37680,7 @@ var init_agent_resolver = __esm({
37037
37680
  projectFilesCache = /* @__PURE__ */ new Map();
37038
37681
  UnknownAgentError = class extends WaironError {
37039
37682
  constructor(agentId, knownIds) {
37040
- super(`Unknown agent id: "${agentId}". Known agent ids: ${knownIds.join(", ")}`);
37683
+ super(`Unknown agent id: "${agentId}". Known agent ids: ${knownIds.join(", ")} \u2014 or any component id of this project, which composes that component's implementer brief.`);
37041
37684
  this.name = "UnknownAgentError";
37042
37685
  }
37043
37686
  };
@@ -41217,7 +41860,8 @@ Approved: ${approval.lockedAt} by ${by} \u2014 no spec has changed since.${child
41217
41860
  ${headline} \u2014 approved ${approval.lockedAt} by ${by}:
41218
41861
  ` + named2.map((p) => ` ${p}`).join("\n") + (rest > 0 ? `
41219
41862
  \u2026 and ${rest} more` : "") + childNote + "\n",
41220
- drifted: true
41863
+ drifted: true,
41864
+ moved: [...diff3.changed.map((p) => `~ ${p}`), ...diff3.added.map((p) => `+ ${p}`), ...diff3.removed.map((p) => `- ${p}`)]
41221
41865
  };
41222
41866
  } catch {
41223
41867
  return quiet;
@@ -41345,7 +41989,7 @@ ${GUIDE_MARKER_END}
41345
41989
  const stripped = stripGuideSection(existing);
41346
41990
  const newContent = stripped.trimEnd() + section;
41347
41991
  fs21.mkdirSync(path36.dirname(filePath), { recursive: true });
41348
- fs21.writeFileSync(filePath, newContent, "utf-8");
41992
+ writeFile(filePath, newContent);
41349
41993
  }
41350
41994
  function stripGuideSection(content) {
41351
41995
  const start2 = content.indexOf(GUIDE_MARKER_START);
@@ -41368,7 +42012,7 @@ To design or modify the system, invoke the **\`sdd-architect\`** skill
41368
42012
  (in \`.claude/skills/\`). Author and validate specs with the \`sdd_*\` MCP tools;
41369
42013
  the \`wairon\` CLI is the human developer's tool, not yours.
41370
42014
  `;
41371
- fs21.writeFileSync(filePath, content, "utf-8");
42015
+ writeFile(filePath, content);
41372
42016
  } else if (targetType === "gemini" || targetType === "agy") {
41373
42017
  const filePath = path36.join(projectRoot2, "GEMINI.md");
41374
42018
  const content = `# Wairon SDD Project
@@ -41377,7 +42021,7 @@ ${versionStamp()}
41377
42021
  ${LOCAL_GUIDE_BODY}
41378
42022
  ${GUIDE_MARKER_END}
41379
42023
  `;
41380
- fs21.writeFileSync(filePath, content, "utf-8");
42024
+ writeFile(filePath, content);
41381
42025
  } else if (targetType === "cursor") {
41382
42026
  const filePath = path36.join(projectRoot2, ".cursorrules");
41383
42027
  const content = `# Wairon SDD Project
@@ -41386,7 +42030,7 @@ This project uses the Wairon Spec-Driven Development (SDD) framework.
41386
42030
 
41387
42031
  Refer to the rules in [.cursor/rules/](.cursor/rules/) for full instructions.
41388
42032
  `;
41389
- fs21.writeFileSync(filePath, content, "utf-8");
42033
+ writeFile(filePath, content);
41390
42034
  } else if (targetType === "copilot") {
41391
42035
  const filePath = path36.join(projectRoot2, ".github", "copilot-instructions.md");
41392
42036
  fs21.mkdirSync(path36.dirname(filePath), { recursive: true });
@@ -41396,14 +42040,14 @@ This project uses the Wairon Spec-Driven Development (SDD) framework.
41396
42040
 
41397
42041
  Refer to the prompts in [.github/prompts/](.github/prompts/) for instructions.
41398
42042
  `;
41399
- fs21.writeFileSync(filePath, content, "utf-8");
42043
+ writeFile(filePath, content);
41400
42044
  } else if (targetType === "codex") {
41401
42045
  const filePath = path36.join(projectRoot2, ".codexrules");
41402
42046
  const content = `# Wairon SDD Project
41403
42047
 
41404
42048
  Refer to [.codex/agents/](.codex/agents/) for full instructions.
41405
42049
  `;
41406
- fs21.writeFileSync(filePath, content, "utf-8");
42050
+ writeFile(filePath, content);
41407
42051
  }
41408
42052
  }
41409
42053
  function reinjectLocalGuides(projectRoot2, targetTypes) {
@@ -41427,6 +42071,7 @@ var init_ai_guide = __esm({
41427
42071
  os6 = __toESM(require("os"));
41428
42072
  path36 = __toESM(require("path"));
41429
42073
  init_stamp();
42074
+ init_fs();
41430
42075
  GUIDE_MARKER_START = "<!-- wairon-guide-start -->";
41431
42076
  GUIDE_MARKER_END = "<!-- wairon-guide-end -->";
41432
42077
  GLOBAL_GUIDE_BODY = `## wairon \u2014 Spec-Driven Development (optional)
@@ -41450,13 +42095,13 @@ This project uses **wairon**. System specs live under \`.wai/specs/\` (L0 System
41450
42095
 
41451
42096
  **Do NOT search files or read agent configs to learn about wairon or SDD. Use the context here and the \`sdd-architect\` skill to start.**
41452
42097
 
41453
- **Your first move: call the \`sdd_get_status\` MCP tool** (or \`wairon/sdd_get_status\`) to see the spec tree. Do not parse files or run CLI commands manually.
42098
+ **Your first move: call the \`sdd_get_status\` MCP tool** (or \`wairon/sdd_get_status\`) to see the spec tree. Do not parse files or run CLI commands manually. If no \`sdd_*\` tools are available, this folder is not the root of the project this guide came from \u2014 say so instead of guessing.
41454
42099
 
41455
42100
  ### How you operate
41456
42101
  - **To design/modify specs**: Use **\`sdd-architect\`** skill (in \`.claude/skills/\` or \`.gemini/skills/\`).
41457
42102
  - **Manage specs via MCP tools only**: Use \`sdd_initialize_system\`, \`sdd_add_subsystem\`, \`sdd_add_component\`, \`sdd_define_interface\`, \`sdd_write_narrative\`, \`sdd_add_type\`, \`sdd_get_spec\`, \`sdd_delete_spec\`, \`sdd_validate_tree\`, and \`sdd_get_status\` (namespaced if needed). Do not edit specs manually.
41458
42103
  - **Growing a system \u2014 subsystems \u2192 parts \u2192 projects**: boundaries are earned. Start with subsystems in one folder. A piece that needs its own folder or repository but stays the same system is a **part**: declared in \`.wai/project.yaml\` \`members\` by one location key (\`scheduler: services/scheduler\`, \`admin: ../admin\`, \`payments: git@host:acme/payments.git#<commit>\`; long form \`{ source, as, description }\`), its subsystems this project's own \u2014 local ids, this project's lock. \`sdd_add_member\` creates a part by default (\`as: project\` for a project) and \`sdd_externalize_subsystem\` moves a subsystem into one. A **project** is an independent boundary \u2014 its own id, exports, lock \u2014 reached as \`alias::name\`; make one only when it needs its own team, release, approval or public surface: \`sdd_promote_member\` (\`sdd_demote_member\` back). What a member is follows from its content (an id, an L0 or a lock make a project).
41459
- - **Members & cross-project references**: relocate a member with \`sdd_move_member\`. Every other change of the family's shape is a **family migration**, each with \`dryRun\`: promote/demote, make an existing project a member with \`sdd_attach_member\`, take one out and back with \`sdd_detach_member\` / \`sdd_adopt_member\`, rename a project's id with \`sdd_rename_project\` or an alias with \`sdd_rename_member_alias\`, move a subsystem into a part with \`sdd_externalize_subsystem\` and fold a member back in with \`sdd_internalize_member\`. Run it with \`dryRun: true\` first and show the plan; applied, it writes every project it touches or none, and never locks \u2014 it names the projects to re-lock. A project member is never a subsystem of its parent: it has its own \`.wai/\` tree and is designed from its own root.
42104
+ - **Members & cross-project references**: relocate a member with \`sdd_move_member\`. Every other change of the family's shape is a **family migration**, each with \`dryRun\`: promote/demote, make an existing project a member with \`sdd_attach_member\`, take one out and back with \`sdd_detach_member\` / \`sdd_adopt_member\`, rename a project's id with \`sdd_rename_project\` or an alias with \`sdd_rename_member_alias\`, move a subsystem into a part with \`sdd_externalize_subsystem\` and fold a member back in with \`sdd_internalize_member\`. Run it with \`dryRun: true\` first and show the plan (the human's CLI calls the same plan \`--report\`); applied, it writes every project it touches or none, and never locks \u2014 it names the projects to re-lock. A project member is never a subsystem of its parent: it has its own \`.wai/\` tree and is designed from its own root \u2014 its specs, its L0 export table and its \`project.yaml\` are written by a session opened in that member's folder (its own guide and \`.mcp.json\`), and the tools here refuse such a write naming that folder.
41460
42105
  - **\`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\`).
41461
42106
  - **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\`).
41462
42107
  - **Do not run the \`wairon\` CLI**: Use \`sdd_validate_tree\` and \`sdd_get_status\` instead of CLI commands.
@@ -41626,8 +42271,9 @@ var init_context = __esm({
41626
42271
  // src/core/status.ts
41627
42272
  function approvalTag(entry) {
41628
42273
  if (!entry) return "";
41629
- const state = entry.upgraded ? `${entry.state}, re-lock once` : entry.state;
41630
- return entry.pinned ? ` [${state} \xB7 pin ${entry.pinned}]` : ` [${state}]`;
42274
+ const words2 = APPROVAL_WORDS[entry.state];
42275
+ const state = entry.upgraded ? `${words2}, re-lock once` : words2;
42276
+ return entry.pinned ? ` [${state} \xB7 ${PIN_WORDS[entry.pinned]}]` : ` [${state}]`;
41631
42277
  }
41632
42278
  function fillDecor(decor) {
41633
42279
  return {
@@ -41713,11 +42359,15 @@ function getStatusReport(options = {}, decor) {
41713
42359
  }
41714
42360
  componentScores.set(comp.id, score);
41715
42361
  }
42362
+ const typeOwners = new Set(loadTypeSpecs().map((t) => t.subsystem).filter((id) => typeof id === "string"));
41716
42363
  const getSubsystemScore = (subId) => {
41717
42364
  const sub = subsystems.find((s) => s.id === subId);
41718
42365
  if (!sub) return 0;
41719
42366
  const subComps = components.filter((c) => c.subsystem === subId);
41720
- if (subComps.length === 0) return 0;
42367
+ if (subComps.length === 0) {
42368
+ if (!typeOwners.has(subId)) return 0;
42369
+ return sub.status === "complete" ? 100 : 50;
42370
+ }
41721
42371
  const totalScore = subComps.reduce((acc, c) => acc + (componentScores.get(c.id) ?? 0), 0);
41722
42372
  let avg = Math.round(totalScore / subComps.length);
41723
42373
  if (sub.status === "draft" || sub.status === "design") {
@@ -41840,12 +42490,11 @@ function getStatusReport(options = {}, decor) {
41840
42490
  `;
41841
42491
  }
41842
42492
  const own = options.approvals?.find((a) => a.key === "");
41843
- const hasMembers = (options.approvals ?? []).some((a) => a.key !== "" && a.as !== "part");
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)" : ""}
42493
+ if (own) output += `${mark.layer("system", "Approval:")} this project is ${OWN_APPROVAL_WORDS[own.state]}${own.upgraded ? " (locked under an earlier gate identity \u2014 re-lock once)" : ""}
41845
42494
  `;
41846
42495
  return { text: output, failed: false };
41847
42496
  }
41848
- var path37, asWritten;
42497
+ var path37, APPROVAL_WORDS, PIN_WORDS, OWN_APPROVAL_WORDS, asWritten;
41849
42498
  var init_status = __esm({
41850
42499
  "src/core/status.ts"() {
41851
42500
  "use strict";
@@ -41854,6 +42503,13 @@ var init_status = __esm({
41854
42503
  init_specs();
41855
42504
  init_fs();
41856
42505
  init_yaml();
42506
+ APPROVAL_WORDS = { approved: "approved", drifted: "drifted", never: "never approved" };
42507
+ PIN_WORDS = { matches: "pinned", moved: "pin moved", unpinned: "not pinned" };
42508
+ OWN_APPROVAL_WORDS = {
42509
+ approved: "approved",
42510
+ drifted: "drifted \u2014 its approval no longer covers this tree (`wairon lock` re-approves)",
42511
+ never: "never approved (`wairon lock` approves the design)"
42512
+ };
41857
42513
  asWritten = (text3) => text3;
41858
42514
  }
41859
42515
  });
@@ -41894,7 +42550,8 @@ var init_core = __esm({
41894
42550
  init_provision();
41895
42551
  init_statehash();
41896
42552
  init_lockfile();
41897
- init_diagram();
42553
+ init_specs2();
42554
+ init_specs2();
41898
42555
  init_diagram();
41899
42556
  init_diagram();
41900
42557
  init_detection();
@@ -42150,7 +42807,7 @@ function exportSddSkills(targetTypes) {
42150
42807
  const content = composeBuiltinSkill(name, packSkills.filter((s) => s.targets.includes(type)));
42151
42808
  const destPath = skillDestPath(type, destDir, name);
42152
42809
  ensureDir(path38.dirname(destPath));
42153
- fs22.writeFileSync(destPath, content, "utf-8");
42810
+ writeFileIfChanged(destPath, content);
42154
42811
  fileCount++;
42155
42812
  }
42156
42813
  for (const skill of packSkills) {
@@ -42161,7 +42818,7 @@ function exportSddSkills(targetTypes) {
42161
42818
  const content = fs22.readFileSync(skill.sourcePath, "utf-8");
42162
42819
  const destPath = skillDestPath(type, destDir, id);
42163
42820
  ensureDir(path38.dirname(destPath));
42164
- fs22.writeFileSync(destPath, content, "utf-8");
42821
+ writeFileIfChanged(destPath, content);
42165
42822
  fileCount++;
42166
42823
  }
42167
42824
  }
@@ -42180,7 +42837,7 @@ function checkSkillFreshness(type) {
42180
42837
  }
42181
42838
  const want = composeBuiltinSkill(name, packSkills);
42182
42839
  const have = fs22.readFileSync(destPath, "utf-8");
42183
- if (have === want) result.ok.push(name);
42840
+ if (have.replace(/\r\n/g, "\n") === want.replace(/\r\n/g, "\n")) result.ok.push(name);
42184
42841
  else result.stale.push(name);
42185
42842
  }
42186
42843
  return result;
@@ -44890,8 +45547,32 @@ function planAliasRename(family, bound2, request) {
44890
45547
  if ([...family.owners].some(([key, owner]) => owner === bound2 && key.split("::").pop() === next)) {
44891
45548
  plan7.notes.push(`${label5(bound2)} has a spec whose id is "${next}" \u2014 a local id equal to an alias (LOCAL_ID_SHADOWS_PROJECT reports it)`);
44892
45549
  }
45550
+ repinIntactConsumers(plan7, family, bound2);
44893
45551
  return plan7;
44894
45552
  }
45553
+ function repinIntactConsumers(plan7, family, bound2) {
45554
+ const boundNode2 = familyNode(family, bound2);
45555
+ for (const node of family.nodes) {
45556
+ if (path46.resolve(node.directory) === path46.resolve(boundNode2.directory)) continue;
45557
+ let statuses = [];
45558
+ try {
45559
+ statuses = runWithProjectRoot(node.directory, () => getExternalsStatus2());
45560
+ } catch {
45561
+ continue;
45562
+ }
45563
+ for (const status2 of statuses) {
45564
+ const producer = node.aliases.get(status2.alias);
45565
+ const pinsBound = producer !== void 0 ? producer === bound2 : boundNode2.id !== void 0 && status2.project === boundNode2.id;
45566
+ if (!status2.pinned || status2.outOfReach || !pinsBound) continue;
45567
+ const broken = status2.uses.filter((u) => u.state === "changed" || u.state === "removed");
45568
+ if (broken.length > 0) {
45569
+ plan7.notes.push(`${label5(node.namespace)}'s pin of "${status2.alias}" is already incompatible (${broken.map((u) => u.publicName ?? u.member ?? "?").join(", ")}): not re-pinned \u2014 adapt its uses, then run \`wairon externals pin ${status2.alias}\` there.`);
45570
+ continue;
45571
+ }
45572
+ edit2(plan7, node.namespace, "pin", `pin ${status2.alias} taken again: the names it uses of ${label5(bound2)} are unchanged`, { root: node.directory, call: "pinExternals", args: [[status2.alias]] });
45573
+ }
45574
+ }
45575
+ }
44895
45576
  function write4(plan7, rehearsal) {
44896
45577
  const planned = plan7.edits.filter((e) => e.write !== void 0);
44897
45578
  const unknown = planned.find((x) => !PHASES.includes(x.write.call));
@@ -44906,6 +45587,7 @@ function write4(plan7, rehearsal) {
44906
45587
  for (const e of planned.filter((x) => x.write.call === "renameId")) writeOne2(rehearsal, e.write);
44907
45588
  }
44908
45589
  for (const e of planned.filter((x) => x.write.call === "renamePin")) writeOne2(rehearsal, e.write);
45590
+ for (const e of planned.filter((x) => x.write.call === "pinExternals")) writeOne2(rehearsal, e.write);
44909
45591
  }
44910
45592
  function referencesBySpec(planned) {
44911
45593
  const out = /* @__PURE__ */ new Map();
@@ -44929,6 +45611,8 @@ function writeOne2(rehearsal, w) {
44929
45611
  return renameProjectId(args[0], args[1]);
44930
45612
  case "renamePin":
44931
45613
  return renamePin2(args[0], args[1], args[2]);
45614
+ case "pinExternals":
45615
+ return pinExternals2(args[0]);
44932
45616
  default:
44933
45617
  throw new Error(`identity_migration makes no "${w.call}" write`);
44934
45618
  }
@@ -44968,7 +45652,7 @@ var init_identity = __esm({
44968
45652
  group: ["type"],
44969
45653
  type: ["interface", "type", "implementation"]
44970
45654
  };
44971
- PHASES = ["rewriteReferences", "renameAlias", "repointExternal", "renameId", "renamePin"];
45655
+ PHASES = ["rewriteReferences", "renameAlias", "repointExternal", "renameId", "renamePin", "pinExternals"];
44972
45656
  }
44973
45657
  });
44974
45658
 
@@ -45212,6 +45896,7 @@ function planExternalize(family, bound2, request) {
45212
45896
  if (plan7.refusals.length > 0) return plan7;
45213
45897
  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"] });
45214
45898
  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: [[]] });
45899
+ repinIntactConsumers2(plan7, family, bound2, [dir]);
45215
45900
  plan7.notes.push("Source code is not moved: move the subsystem's code into the member yourself.");
45216
45901
  return plan7;
45217
45902
  }
@@ -45340,6 +46025,7 @@ function planPromote(family, bound2, request) {
45340
46025
  );
45341
46026
  edit3(plan7, newId, "pin", `pin the new project's external for ${node.id ?? "this project"} (when its specs reference it)`, { root: part.directory, call: "pinExternals", args: [[]] });
45342
46027
  if (part.storage !== "contained") edit3(plan7, bound2, "pin", `pin ${alias} (a project stored outside ${label6(bound2)} is judged against its pin)`, { root: node.directory, call: "pinExternals", args: [[alias]] });
46028
+ repinIntactConsumers2(plan7, family, bound2, [part.directory]);
45343
46029
  plan7.notes.push(`"${newId}" has no lock yet: lock it at its own root (\`wairon lock\`) once it validates.`);
45344
46030
  return plan7;
45345
46031
  }
@@ -45406,16 +46092,19 @@ function write5(plan7, rehearsal) {
45406
46092
  return;
45407
46093
  }
45408
46094
  if (plan7.request.verb === "externalize") {
45409
- for (const e of planned.filter((x) => x.write.call === "externalizeSubsystem")) writeOne3(rehearsal, e.write);
46095
+ for (const e of planned.filter((x) => x.write.call === "externalizeSubsystem")) recordCrossing(plan7, e.project, e.write.args[0], writeOne3(rehearsal, e.write));
45410
46096
  for (const e of planned.filter((x) => x.write.call === "pinExternals")) writeOne3(rehearsal, e.write);
45411
46097
  return;
45412
46098
  }
45413
46099
  if (plan7.request.verb === "promote") {
45414
- for (const e of planned.filter((x) => x.write.call === "promoteMember")) writeOne3(rehearsal, e.write);
46100
+ for (const e of planned.filter((x) => x.write.call === "promoteMember")) recordCrossing(plan7, e.project, e.write.args[1] ?? e.write.args[0], writeOne3(rehearsal, e.write));
45415
46101
  for (const e of planned.filter((x) => x.write.call === "pinExternals")) writeOne3(rehearsal, e.write);
45416
46102
  return;
45417
46103
  }
45418
- for (const e of planned.filter((x) => x.write.call === "demoteMember")) writeOne3(rehearsal, e.write);
46104
+ for (const e of planned.filter((x) => x.write.call === "demoteMember")) {
46105
+ const demoted = writeOne3(rehearsal, e.write);
46106
+ for (const line2 of demoted?.consumersRestored ?? []) plan7.edits.push({ project: e.project, kind: "reference", detail: `consumers across the old boundary restored: ${line2}` });
46107
+ }
45419
46108
  for (const e of planned.filter((x) => x.write.call === "unpin")) writeOne3(rehearsal, e.write);
45420
46109
  }
45421
46110
  function referencesBySpec2(planned) {
@@ -45428,9 +46117,46 @@ ${id}`) ?? [], ...edits]);
45428
46117
  }
45429
46118
  return out;
45430
46119
  }
46120
+ function repinIntactConsumers2(plan7, family, bound2, created) {
46121
+ const boundNode2 = familyNode(family, bound2);
46122
+ const skip = new Set([boundNode2.directory, ...created].map((d) => path47.resolve(d)));
46123
+ for (const node of family.nodes) {
46124
+ if (skip.has(path47.resolve(node.directory))) continue;
46125
+ let statuses = [];
46126
+ try {
46127
+ statuses = runWithProjectRoot(node.directory, () => getExternalsStatus2());
46128
+ } catch {
46129
+ continue;
46130
+ }
46131
+ for (const status2 of statuses) {
46132
+ const producer = node.aliases.get(status2.alias);
46133
+ const pinsBound = producer !== void 0 ? producer === bound2 : boundNode2.id !== void 0 && status2.project === boundNode2.id;
46134
+ if (!status2.pinned || status2.outOfReach || !pinsBound) continue;
46135
+ const broken = status2.uses.filter((u) => u.state === "changed" || u.state === "removed");
46136
+ if (broken.length > 0) {
46137
+ plan7.notes.push(`${label6(node.namespace)}'s pin of "${status2.alias}" is already incompatible (${broken.map((u) => u.publicName ?? u.member ?? "?").join(", ")}): not re-pinned \u2014 adapt its uses, then run \`wairon externals pin ${status2.alias}\` there.`);
46138
+ continue;
46139
+ }
46140
+ edit3(plan7, node.namespace, "pin", `pin ${status2.alias} taken again: the names it uses of ${label6(bound2)} only move with the boundary`, { root: node.directory, call: "pinExternals", args: [[status2.alias]] });
46141
+ }
46142
+ }
46143
+ }
46144
+ function recordCrossing(plan7, bound2, member, result) {
46145
+ const crossed = result;
46146
+ if (!crossed) return;
46147
+ for (const r of crossed.respelled) {
46148
+ plan7.edits.push({ project: bound2, kind: "reference", detail: `${r.specId} (${r.position}): ${r.from} \u2192 ${r.to}`, reference: r });
46149
+ }
46150
+ for (const r of crossed.memberRespelled) {
46151
+ plan7.edits.push({ project: member, kind: "reference", detail: `${r.specId} (${r.position}): ${r.from} \u2192 ${r.to}`, reference: r });
46152
+ }
46153
+ for (const line2 of crossed.exported) plan7.edits.push({ project: bound2, kind: "export", detail: `export ${line2}` });
46154
+ for (const line2 of crossed.imported) plan7.edits.push({ project: bound2, kind: "external", detail: `import: ${line2}` });
46155
+ for (const line2 of crossed.consumersDropped) plan7.edits.push({ project: bound2, kind: "reference", detail: `consumers across the boundary to ${member} dropped: ${line2}` });
46156
+ }
45431
46157
  function writeOne3(rehearsal, w) {
45432
46158
  const args = w.args;
45433
- runWithProjectRoot(rehearsalRoot(rehearsal, w.root), () => {
46159
+ return runWithProjectRoot(rehearsalRoot(rehearsal, w.root), () => {
45434
46160
  switch (w.call) {
45435
46161
  case "internalizeMember":
45436
46162
  return internalizeMember(args[0], args[1]);
@@ -45557,7 +46283,7 @@ function dropRehearsal(id) {
45557
46283
  function saveJournal(journal) {
45558
46284
  const ignore = nodePath2.join(transactionsDir(journal.owner), ".gitignore");
45559
46285
  if (!fs27.existsSync(ignore)) writeDurably(ignore, "*\n");
45560
- writeDurably(nodePath2.join(transactionDir(journal.owner, journal.id), "journal.yaml"), yaml3.dump(journal, { noRefs: true, lineWidth: 200 }));
46286
+ writeDurably(nodePath2.join(transactionDir(journal.owner, journal.id), "journal.yaml"), yaml4.dump(journal, { noRefs: true, lineWidth: 200 }));
45561
46287
  }
45562
46288
  function readJournal(owner, id) {
45563
46289
  const file = nodePath2.join(transactionDir(owner, id), "journal.yaml");
@@ -45569,7 +46295,7 @@ function readJournal(owner, id) {
45569
46295
  throw e;
45570
46296
  }
45571
46297
  try {
45572
- return yaml3.load(text3);
46298
+ return yaml4.load(text3);
45573
46299
  } catch (e) {
45574
46300
  throw new Error(`${file} is not a readable transaction journal: ${e instanceof Error ? e.message : String(e)}`);
45575
46301
  }
@@ -45630,14 +46356,14 @@ function close(owner, id) {
45630
46356
  } catch {
45631
46357
  }
45632
46358
  }
45633
- var fs27, os7, nodePath2, yaml3, TRANSACTIONS2, transactionsDir, transactionDir, rehearsalDir;
46359
+ var fs27, os7, nodePath2, yaml4, TRANSACTIONS2, transactionsDir, transactionDir, rehearsalDir;
45634
46360
  var init_transaction_files = __esm({
45635
46361
  "src/migrations/transaction-files.ts"() {
45636
46362
  "use strict";
45637
46363
  fs27 = __toESM(require("fs"));
45638
46364
  os7 = __toESM(require("os"));
45639
46365
  nodePath2 = __toESM(require("path"));
45640
- yaml3 = __toESM(require("js-yaml"));
46366
+ yaml4 = __toESM(require("js-yaml"));
45641
46367
  TRANSACTIONS2 = nodePath2.join(".wai", "transactions");
45642
46368
  transactionsDir = (owner) => nodePath2.join(owner, TRANSACTIONS2);
45643
46369
  transactionDir = (owner, id) => nodePath2.join(transactionsDir(owner), id);
@@ -46481,10 +47207,19 @@ ${body}`);
46481
47207
  }
46482
47208
  function migrationView(report4, dryRun) {
46483
47209
  const plan7 = report4.plan;
47210
+ const request = plan7.request;
47211
+ const madeProject = request.verb === "promote" || request.verb === "externalize" && request.as === "project";
47212
+ const madeAt = madeProject && report4.applied ? plan7.changes.find((c) => c.action === "create" && c.path.replace(/\\/g, "/") === ".wai/project.yaml")?.project : void 0;
46484
47213
  return {
46485
47214
  dryRun,
46486
47215
  applied: report4.applied,
46487
47216
  relock: report4.applied ? report4.relock : plan7.relock,
47217
+ ...madeAt ? {
47218
+ newProject: {
47219
+ root: madeAt,
47220
+ next: `The new project at ${madeAt} has no guide, skills or MCP registration of its own yet. The human runs, in that folder: \`wairon generate\` and \`wairon mcp install --backend claude\` (the CLI's \`wairon member promote\` / \`subsystem externalize --as project\` do this themselves). Its own session then designs it \u2014 its specs, L0 export table and project.yaml are written from its own root.`
47221
+ }
47222
+ } : {},
46488
47223
  ...report4.outcome ? { outcome: { committed: report4.outcome.committed, restored: report4.outcome.restored, unrestored: report4.outcome.unrestored, ...report4.outcome.failure ? { failure: report4.outcome.failure } : {} } } : {},
46489
47224
  plan: {
46490
47225
  verb: plan7.request.verb,
@@ -48088,9 +48823,9 @@ ${testsBlock}` : ""}`, deletion);
48088
48823
  server,
48089
48824
  "sdd_get_agent_brief",
48090
48825
  {
48091
- description: "Compose the LIVE delegation brief for one resolved agent: the fully rendered instruction body plus the ownedPaths/readPaths scope fence \u2014 the dynamic replacement for generated per-component agent files. Fetch a brief and spawn a generic subagent with it: always fresh after a re-lock, no session restart needed. List agent ids via listAgents or resources/list (the wairon-agent:// entries).",
48826
+ description: "Compose the LIVE delegation brief for one resolved agent: the fully rendered instruction body plus the ownedPaths/readPaths scope fence and the codeFence (where the agent may write CODE: its source files and their shared folder; empty when no file is named yet \u2014 then name the planned files when spawning) \u2014 the dynamic replacement for generated per-component agent files. A COMPONENT id (or `<component>-implementer`) composes that one component's implementer brief, with no agent files and no setting needed. Fetch a brief and spawn a generic subagent with it: always fresh after a re-lock, no session restart needed. List agent ids via listAgents or resources/list (the wairon-agent:// entries).",
48092
48827
  inputSchema: {
48093
- agentId: import_zod12.z.string().describe("The resolved agent id (list via listAgents or resources/list)")
48828
+ agentId: import_zod12.z.string().describe("The resolved agent id (list via listAgents or resources/list), or a component id for that component's implementer brief")
48094
48829
  }
48095
48830
  },
48096
48831
  ({ agentId }) => {
@@ -48606,7 +49341,7 @@ function geminiGlobalDir(override) {
48606
49341
  return override || process.env["GEMINI_CONFIG_DIR"] || path51.join(os8.homedir(), ".gemini");
48607
49342
  }
48608
49343
  function claudeMcpConfigPath(useGlobal, override) {
48609
- if (!useGlobal) return path51.join(process.cwd(), ".mcp.json");
49344
+ if (!useGlobal) return path51.join(getProjectRoot(), ".mcp.json");
48610
49345
  const dir = override || process.env["CLAUDE_CONFIG_DIR"];
48611
49346
  return dir ? path51.join(dir, ".claude.json") : path51.join(os8.homedir(), ".claude.json");
48612
49347
  }
@@ -48636,32 +49371,33 @@ function validateConfigDir(dir, backend) {
48636
49371
  );
48637
49372
  }
48638
49373
  }
49374
+ function bindingLine(root) {
49375
+ let id = null;
49376
+ try {
49377
+ const config = loadProjectConfig();
49378
+ if (config) id = effectiveProjectId(config);
49379
+ } catch {
49380
+ }
49381
+ return `project ${id ?? path51.basename(root)} at ${root}`;
49382
+ }
49383
+ function serveRoot(cwd, envDir) {
49384
+ if (envDir && fs30.existsSync(path51.join(envDir, ".wai"))) return { root: path51.resolve(envDir), how: "WAIRON_PROJECT_DIR (pinned at install)" };
49385
+ const binding = resolveProjectBinding(cwd);
49386
+ if (binding?.via === "here") return { root: binding.root, how: "cwd" };
49387
+ if (binding?.via === "ancestor") return { root: binding.root, how: `walked up to ancestor inside the repository \u2014 cwd (${cwd}) holds no project` };
49388
+ if (binding) return { root: binding.root, how: `walked up past the repository root to the project that declares it as a member \u2014 cwd (${cwd})` };
49389
+ if (fs30.existsSync(path51.join(cwd, ".wai"))) return { root: cwd, how: "cwd (its .wai holds no L0 yet)" };
49390
+ return { root: cwd, how: "cwd (no project binds this folder)" };
49391
+ }
48639
49392
  async function runMcpServe() {
48640
- const { setProjectRoot: setProjectRoot2, getProjectRoot: getProjectRoot2, findProjectRoot: findProjectRoot2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
49393
+ const { setProjectRoot: setProjectRoot2, getProjectRoot: getProjectRoot2 } = await Promise.resolve().then(() => (init_fs(), fs_exports));
48641
49394
  const cwd = path51.resolve(process.cwd());
48642
49395
  const envDir = process.env["WAIRON_PROJECT_DIR"];
48643
- let resolved;
48644
- let how;
48645
- if (envDir && fs30.existsSync(path51.join(envDir, ".wai"))) {
48646
- resolved = path51.resolve(envDir);
48647
- how = "WAIRON_PROJECT_DIR (pinned at install)";
48648
- } else {
48649
- const found = findProjectRoot2(cwd);
48650
- if (found && found === cwd) {
48651
- resolved = found;
48652
- how = "cwd";
48653
- } else if (found) {
48654
- resolved = found;
48655
- how = `walked up to ancestor \u2014 cwd (${cwd}) has no .wai`;
48656
- } else {
48657
- resolved = cwd;
48658
- how = "cwd (no .wai found anywhere above)";
48659
- }
48660
- }
49396
+ const { root: resolved, how } = serveRoot(cwd, envDir);
48661
49397
  setProjectRoot2(resolved);
48662
49398
  const initialized = isProjectInitialized();
48663
49399
  process.stderr.write(
48664
- `[wairon mcp] v${WAIRON_VERSION} \u2014 project root: ${getProjectRoot2()} [via ${how}] (.wai ${initialized ? "found" : "NOT found \u2014 sdd_* tools will report no project"})
49400
+ `[wairon mcp] v${WAIRON_VERSION} \u2014 ${bindingLine(getProjectRoot2())} [via ${how}] (.wai ${initialized ? "found" : "NOT found \u2014 sdd_* tools will report no project"})
48665
49401
  `
48666
49402
  );
48667
49403
  const { startMcpServer: startMcpServer2 } = (init_server(), __toCommonJS(server_exports));
@@ -48756,7 +49492,7 @@ function resolveInstallTargets(options) {
48756
49492
  configBase = path51.join(geminiGlobalDir(options.configDir), "antigravity-cli");
48757
49493
  settingsPath = path51.join(configBase, "mcp_config.json");
48758
49494
  } else {
48759
- configBase = path51.join(process.cwd(), ".gemini");
49495
+ configBase = path51.join(root, ".gemini");
48760
49496
  settingsPath = path51.join(configBase, "settings.json");
48761
49497
  }
48762
49498
  } else {
@@ -48829,14 +49565,14 @@ function writeInstallTarget(target) {
48829
49565
  if (target.outsideProject && fs30.existsSync(target.path)) {
48830
49566
  logger.info(`Backed up ${import_chalk3.default.gray(target.path)} to ${import_chalk3.default.cyan(backupBeside(target.path))} before replacing it (outside the project root).`);
48831
49567
  }
48832
- fs30.writeFileSync(target.path, JSON.stringify(target.settings, null, 2) + "\n");
49568
+ fs30.writeFileSync(target.path, withLineEndings(target.path, JSON.stringify(target.settings, null, 2) + "\n"));
48833
49569
  }
48834
49570
  function reportInstalled(target, agentLabel) {
48835
49571
  const wasStale = target.before !== void 0;
48836
49572
  logger.success(`wairon MCP server ${wasStale ? "updated (was stale)" : "registered"} for ${agentLabel} in ${import_chalk3.default.cyan(target.path)}.`);
48837
49573
  logger.blank();
48838
49574
  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").');
49575
+ logger.info('rename tools, plus live agent briefs (sdd_get_agent_brief). The full list is in docs/cli.md ("MCP tools"), shipped with the package (node_modules/@wairon/cli/docs/cli.md) and at https://github.com/SYW-Apps/Waffle-AIron/blob/main/docs/cli.md');
48840
49576
  logger.blank();
48841
49577
  const restartApp = target.backend === "gemini" ? "Antigravity CLI (agy)" : "claude";
48842
49578
  logger.info(`Restart ${import_chalk3.default.bold(restartApp)} (or reload MCP servers) to activate.`);
@@ -48855,7 +49591,7 @@ function runMcpStatus() {
48855
49591
  if (!projectConfig) throw new ProjectNotInitializedError();
48856
49592
  const claudeProject = claudeMcpConfigPath(false);
48857
49593
  const claudeGlobal = claudeMcpConfigPath(true);
48858
- const geminiProject = path51.join(process.cwd(), ".gemini", "settings.json");
49594
+ const geminiProject = path51.join(getProjectRoot(), ".gemini", "settings.json");
48859
49595
  const geminiGlobal = path51.join(geminiGlobalDir(), "antigravity-cli", "mcp_config.json");
48860
49596
  logger.blank();
48861
49597
  logger.info(`${import_chalk3.default.bold("wairon MCP Server")}`);
@@ -48938,6 +49674,7 @@ var init_mcp = __esm({
48938
49674
  init_fs();
48939
49675
  init_errors();
48940
49676
  init_defaults();
49677
+ init_project();
48941
49678
  }
48942
49679
  });
48943
49680
 
@@ -49194,32 +49931,99 @@ function checkApproval(strict) {
49194
49931
  }
49195
49932
  const lock = readLockState(computeGateStateId());
49196
49933
  const record2 = lock.record;
49934
+ const members = familyApprovals().filter((a) => a.key !== "" && a.as !== "part");
49935
+ const drifted = members.filter((a) => a.state === "drifted");
49936
+ const never = members.filter((a) => a.state === "never");
49937
+ if (drifted.length > 0) {
49938
+ const where = memberFolders();
49939
+ const own = lock.state === "locked" ? "This project's own approval still covers its design." : lock.state === "stale" ? "This project's own approval is stale too \u2014 run `wairon lock` here afterwards." : "This project itself has no approval on record.";
49940
+ return {
49941
+ state: lock.state === "unlocked" ? "unlocked" : "stale",
49942
+ approved: false,
49943
+ message: `Member project(s) carry spec changes nobody approved: ${drifted.map((m) => describeMemberAt(m, where)).join("; ")}. Nothing says a human has seen what is about to merge in them. Fix: run \`wairon lock\` in each member's own folder and commit its .wai/lock.json. ${own}`
49944
+ };
49945
+ }
49197
49946
  if (lock.state === "stale" && record2.stateId.algorithm !== lock.current.algorithm) return upgradedCheck(record2);
49198
49947
  switch (lock.state) {
49199
- case "locked":
49948
+ case "locked": {
49949
+ if (strict && never.length > 0) {
49950
+ const where = memberFolders();
49951
+ return {
49952
+ state: "locked",
49953
+ approved: false,
49954
+ message: `This project's own design is approved, but --strict asks for an approved family and member project(s) were never approved by anyone: ${never.map((m) => describeMemberAt(m, where)).join("; ")}. Fix: run \`wairon lock\` in each member's own folder and commit its .wai/lock.json.`
49955
+ };
49956
+ }
49957
+ const approvedMembers = members.length - never.length;
49958
+ const family = members.length === 0 ? "" : ` ${approvedMembers} member project(s) approved at their own roots.` + (never.length > 0 ? ` NOT gated: ${never.map((m) => m.alias ?? m.key).join(", ")} \u2014 never approved by anyone (\`--strict\` fails here).` : "");
49200
49959
  return {
49201
49960
  state: "locked",
49202
49961
  approved: true,
49203
- message: `The design in this tree is the approved design \u2014 approved ${record2.lockedAt} by ${describeApprover(record2.lockedBy)}.`
49962
+ message: (never.length > 0 ? "This project's own design is the approved design \u2014 " : "The design in this tree is the approved design \u2014 ") + `approved ${record2.lockedAt} by ${describeApprover(record2.lockedBy)}.${family}`
49204
49963
  };
49205
- case "stale":
49964
+ }
49965
+ case "stale": {
49966
+ const repinned = repinOnly(record2);
49967
+ if (repinned) {
49968
+ return {
49969
+ state: "stale",
49970
+ approved: false,
49971
+ message: `Member project(s) were re-approved at their own roots since this project's approval was taken: ${repinned.join(", ")}. That is how a member's approval is meant to move \u2014 its own approver signed its new design off \u2014 so the member is not in question. What is stale is this project's pin of it: the approval on record (${record2.lockedAt} by ${describeApprover(record2.lockedBy)}) still names the member's earlier approval, so it does not cover the member design about to merge. No own spec file changed. Fix: run \`wairon lock\` here to pin the member(s)' new approval (the same lock records anything else the gate identity covers that moved alongside), and commit the updated .wai/lock.json.`
49972
+ };
49973
+ }
49206
49974
  return {
49207
49975
  state: "stale",
49208
49976
  approved: false,
49209
49977
  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.`
49210
49978
  };
49979
+ }
49211
49980
  default:
49212
49981
  return {
49213
49982
  state: "unlocked",
49214
49983
  approved: !strict,
49215
- message: strict ? "No approval on record (.wai/lock.json is absent), and --strict asks for one. Run `wairon lock` and commit the record." : "No approval on record (.wai/lock.json is absent) \u2014 this project has not opted into the approval gate, so nothing is gated. Run `wairon lock` and commit the record to turn it on, or pass --strict to fail here instead."
49984
+ message: strict ? "No approval on record (.wai/lock.json is absent), and --strict asks for one. Run `wairon lock` and commit the record." : "No approval on record (.wai/lock.json is absent), so nothing is gated: plain `wairon lock-check` fails only a committed approval that no longer matches the design, and a project with no .wai/lock.json \u2014 never locked, or the record deleted \u2014 reads the same. CI should run `wairon lock-check --strict`, which fails here. Run `wairon lock` and commit the record to approve the design."
49216
49985
  };
49217
49986
  }
49218
49987
  }
49988
+ function memberFolders() {
49989
+ const folders = /* @__PURE__ */ new Map();
49990
+ try {
49991
+ const family = graph();
49992
+ for (const node of family.nodes) if (node.namespace !== "") folders.set(node.namespace, node.directory);
49993
+ for (const external of family.nodes.find((n) => n.namespace === "")?.externals ?? []) {
49994
+ if (external.role === "member" && external.directory) folders.set(external.alias, external.directory);
49995
+ }
49996
+ } catch {
49997
+ }
49998
+ return folders;
49999
+ }
50000
+ function describeMemberAt(member, folders) {
50001
+ const dir = folders.get(member.key);
50002
+ const shown2 = dir ? path40.relative(process.cwd(), dir) || "." : void 0;
50003
+ const state = member.state === "drifted" ? member.upgraded ? "approved under an earlier gate identity \u2014 re-lock it once" : "changed since its own approval" : "never approved";
50004
+ return `${member.alias ?? member.key} (${state}${shown2 ? `, in ${shown2}` : ""})`;
50005
+ }
50006
+ function repinOnly(record2) {
50007
+ const diff3 = diffAgainstApproval();
50008
+ if (!diff3 || diffSize(diff3) > 0) return null;
50009
+ const direct = familyApprovals(1).filter((a) => a.parent === "");
50010
+ const members = direct.filter((a) => a.as !== "part");
50011
+ const current2 = new Set(direct.map((m) => m.alias ?? m.key));
50012
+ if (Object.keys(record2.members ?? {}).some((alias) => !current2.has(alias))) return null;
50013
+ const moved = [];
50014
+ for (const m of members) {
50015
+ if (m.pinned === "unpinned") return null;
50016
+ if (m.pinned !== "moved") continue;
50017
+ if (m.state !== "approved") return null;
50018
+ moved.push(m.alias ?? m.key);
50019
+ }
50020
+ return moved.length > 0 ? moved : null;
50021
+ }
49219
50022
  function whatMoved(record2) {
49220
50023
  const diff3 = diffAgainstApproval();
49221
50024
  const own = diff3 ? diffSize(diff3) : null;
49222
- const members = familyApprovals(1).filter((a) => a.parent === "" && a.as !== "part");
50025
+ const direct = familyApprovals(1).filter((a) => a.parent === "");
50026
+ const members = direct.filter((a) => a.as !== "part");
49223
50027
  const named2 = [];
49224
50028
  for (const m of members) {
49225
50029
  const name = m.alias ?? m.key;
@@ -49227,7 +50031,7 @@ function whatMoved(record2) {
49227
50031
  else if (m.pinned === "moved") named2.push(`${name} (its approval moved since this one was taken${m.state === "approved" ? "" : `; now ${m.state}`})`);
49228
50032
  else if (m.state !== "approved") named2.push(`${name} (${m.state} at its own root)`);
49229
50033
  }
49230
- const current2 = new Set(members.map((m) => m.alias ?? m.key));
50034
+ const current2 = new Set(direct.map((m) => m.alias ?? m.key));
49231
50035
  for (const alias of Object.keys(record2.members ?? {})) {
49232
50036
  if (!current2.has(alias)) named2.push(`${alias} (removed since the approval)`);
49233
50037
  }
@@ -49344,7 +50148,7 @@ async function runLock(options, gate, captured) {
49344
50148
  members: memberPins(members),
49345
50149
  ...gate.analysis ? { code: withoutCodes(gate.analysis) } : {}
49346
50150
  };
49347
- writeLockRecord(record2);
50151
+ writeLockRecord2(record2);
49348
50152
  return record2;
49349
50153
  }
49350
50154
  function memberPins(members) {
@@ -49573,6 +50377,7 @@ async function runValidate(options = {}) {
49573
50377
  init_lock();
49574
50378
  init_paths();
49575
50379
  init_fs();
50380
+ init_project();
49576
50381
  init_mcp();
49577
50382
 
49578
50383
  // src/commands/status.ts
@@ -49619,7 +50424,7 @@ function refuseUnreadableTree(explanation) {
49619
50424
  function withoutTrailingNewline(text3) {
49620
50425
  return text3.endsWith("\n") ? text3.slice(0, -1) : text3;
49621
50426
  }
49622
- async function runStatus(options = {}) {
50427
+ async function runStatus(options = {}, listAll = false) {
49623
50428
  assertProjectInitialized();
49624
50429
  printPendingTransactions();
49625
50430
  let approvals;
@@ -49638,12 +50443,15 @@ async function runStatus(options = {}) {
49638
50443
  const lock = approvalVerdict(approvals);
49639
50444
  if (lock.text.trim()) {
49640
50445
  logger.blank();
49641
- if (lock.drifted) logger.warn(lock.text.trim());
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
- ));
49646
- }
50446
+ const lines = lock.text.trim().split("\n");
50447
+ const text3 = listAll && lock.moved && lock.moved.length > 0 ? [lines[0], ...lock.moved.map((p) => ` ${p}`), ...lines.slice(1).filter((l) => !l.startsWith(" "))].join("\n") : lock.text.trim();
50448
+ if (lock.drifted) console.log(import_chalk4.default.yellow(`\u26A0 ${text3}`));
50449
+ else logger.info(text3);
50450
+ if (!listAll && lock.moved && lock.moved.length > 5) logger.info(import_chalk4.default.gray("`wairon status --all` lists every spec that moved."));
50451
+ }
50452
+ logger.info(import_chalk4.default.gray(
50453
+ "The percentages measure authoring progress, not approval: 80% once a component's component, contract and implementation specs are written, 100% once its implementation names source files that exist (capped at 50% while any of them is draft or design). Approval is the lock record (.wai/lock.json); `wairon lock` changes no spec."
50454
+ ));
49647
50455
  logger.blank();
49648
50456
  }
49649
50457
  function printPendingTransactions() {
@@ -49777,14 +50585,14 @@ var http = __toESM(require("http"));
49777
50585
  var https = __toESM(require("https"));
49778
50586
  init_defaults();
49779
50587
  function downloadFile(url, dest) {
49780
- return new Promise((resolve56, reject) => {
50588
+ return new Promise((resolve57, reject) => {
49781
50589
  const file = fs31.createWriteStream(dest);
49782
50590
  const get4 = url.startsWith("https://") ? https.get : http.get;
49783
50591
  get4(url, { headers: { "User-Agent": `wairon/${WAIRON_VERSION}` }, agent: false }, (res) => {
49784
50592
  if (res.statusCode === 301 || res.statusCode === 302) {
49785
50593
  file.close();
49786
50594
  res.destroy();
49787
- downloadFile(res.headers.location, dest).then(resolve56).catch(reject);
50595
+ downloadFile(res.headers.location, dest).then(resolve57).catch(reject);
49788
50596
  return;
49789
50597
  }
49790
50598
  if (res.statusCode !== 200) {
@@ -49796,7 +50604,7 @@ function downloadFile(url, dest) {
49796
50604
  res.pipe(file);
49797
50605
  file.on("finish", () => {
49798
50606
  res.destroy();
49799
- file.close(() => resolve56());
50607
+ file.close(() => resolve57());
49800
50608
  });
49801
50609
  file.on("error", (err) => {
49802
50610
  res.destroy();
@@ -50138,6 +50946,13 @@ function uninstallStorePack(spec) {
50138
50946
  function whichPack(spec) {
50139
50947
  const { name, version } = parseNameAtVersion(spec);
50140
50948
  const resolved = resolveInstalledPack(name, version);
50949
+ const bundled = bundledSelectionRef(name, version);
50950
+ if (bundled) {
50951
+ console.log(`${import_chalk8.default.bold(name)} ${import_chalk8.default.cyan(`v${bundled.version ?? "?"}`)} ${import_chalk8.default.dim("(bundled in this project \u2014 .wai/packs/)")}`);
50952
+ console.log(` path ${import_chalk8.default.dim(bundled.ref)}`);
50953
+ console.log(` store ${resolved ? import_chalk8.default.dim(`also installed: v${resolved.version} at ${resolved.path} (the bundle applies here)`) : import_chalk8.default.dim("not installed in the store \u2014 the bundle is all this project needs")}`);
50954
+ return;
50955
+ }
50141
50956
  if (!resolved) {
50142
50957
  logger.error(`No pack "${spec}" is installed in the store (${packStoreDir()}).`);
50143
50958
  const names = [...new Set(listInstalledPacks().map((p) => p.name))];
@@ -50147,6 +50962,24 @@ function whichPack(spec) {
50147
50962
  }
50148
50963
  printInstalled(resolved, version === void 0);
50149
50964
  }
50965
+ function bundledSelectionRef(name, version) {
50966
+ try {
50967
+ if (!projectConfigExists()) return null;
50968
+ const root = getProjectRoot();
50969
+ for (const entry of loadProjectConfig()?.extensions?.packs ?? []) {
50970
+ if (typeof entry === "string" || entry.name !== name) continue;
50971
+ if (version !== void 0 && entry.version !== void 0 && entry.version !== version) continue;
50972
+ const ref = packEntryRef(entry, root);
50973
+ if (!ref) continue;
50974
+ const rel2 = path52.relative(path52.join(root, ".wai", "packs"), path52.resolve(root, ref));
50975
+ if (rel2.startsWith("..") || path52.isAbsolute(rel2)) continue;
50976
+ const segments = rel2.split(path52.sep);
50977
+ return { ref, version: entry.version ?? (segments.length > 1 ? segments[1] : void 0) };
50978
+ }
50979
+ } catch {
50980
+ }
50981
+ return null;
50982
+ }
50150
50983
  function printInstalled(pack, latestByDefault) {
50151
50984
  console.log(`${import_chalk8.default.bold(pack.name)} ${import_chalk8.default.cyan(`v${pack.version}`)}${latestByDefault ? import_chalk8.default.dim(" (latest installed)") : ""}`);
50152
50985
  console.log(` path ${import_chalk8.default.dim(pack.path)}`);
@@ -51139,7 +51972,22 @@ async function statusCommand(opts) {
51139
51972
  `);
51140
51973
  return;
51141
51974
  }
51142
- await runStatus({ subsystem: opts.subsystem, ...opts.recursive === false ? { memberDepth: 0 } : {} });
51975
+ await runStatus({ subsystem: opts.subsystem, ...opts.recursive === false ? { memberDepth: 0 } : {} }, opts.all === true);
51976
+ }
51977
+ var ANNOUNCE_SILENT = /* @__PURE__ */ new Set(["init", "host", "remote", "login", "logout", "serve", "dev", "update", "aliases", "help", "mcp serve"]);
51978
+ function announceBinding(commandPath) {
51979
+ const first = commandPath.split(" ")[0];
51980
+ if (ANNOUNCE_SILENT.has(first) || ANNOUNCE_SILENT.has(commandPath)) return;
51981
+ const root = getProjectRootOverride() ?? resolveProjectBinding(process.cwd())?.root;
51982
+ if (!root) return;
51983
+ let id = null;
51984
+ try {
51985
+ const config = runWithProjectRoot(root, () => loadProjectConfig());
51986
+ if (config) id = effectiveProjectId(config);
51987
+ } catch {
51988
+ }
51989
+ process.stderr.write(`project ${id ?? path55.basename(root)} at ${root}
51990
+ `);
51143
51991
  }
51144
51992
  async function lockCheckCommand(options) {
51145
51993
  const verdict = checkApproval(options.strict === true);
@@ -51196,6 +52044,10 @@ async function runAgent(action, id) {
51196
52044
  logger.info("Owned paths:");
51197
52045
  for (const p of brief.ownedPaths) logger.info(` ${p}`);
51198
52046
  }
52047
+ if (brief.codeFence) {
52048
+ logger.info(brief.codeFence.length > 0 ? "Code write fence:" : "Code write fence: none declared yet (name the planned files when spawning)");
52049
+ for (const p of brief.codeFence) logger.info(` ${p}`);
52050
+ }
51199
52051
  if (brief.readPaths && brief.readPaths.length > 0) {
51200
52052
  logger.info("Read paths:");
51201
52053
  for (const p of brief.readPaths) logger.info(` ${p}`);
@@ -51478,6 +52330,8 @@ var nodePath3 = __toESM(require("path"));
51478
52330
  init_logger();
51479
52331
  init_errors();
51480
52332
  var import_inquirer3 = __toESM(require("inquirer"));
52333
+ init_mcp();
52334
+ init_fs();
51481
52335
  init_core();
51482
52336
  function requireProject() {
51483
52337
  if (!projectConfigExists()) {
@@ -51570,6 +52424,28 @@ async function runMigration(request, options) {
51570
52424
  }
51571
52425
  const report4 = apply2(planned);
51572
52426
  printOutcome(report4);
52427
+ if (report4.applied) await provisionMadeProjects(request);
52428
+ }
52429
+ async function provisionMadeProjects(request) {
52430
+ const madeProject = request.verb === "promote" || request.verb === "externalize" && request.as === "project";
52431
+ if (!madeProject) return;
52432
+ const root = nodePath3.resolve(getProjectRoot());
52433
+ let made;
52434
+ if (request.verb === "externalize") made = request.path ? nodePath3.resolve(root, request.path) : void 0;
52435
+ else made = graph().nodes.find((n) => n.parent === "" && n.mountAlias === request.alias)?.directory;
52436
+ for (const dir of made ? [nodePath3.resolve(made)] : []) {
52437
+ if (dir === root || !pathExists(nodePath3.join(dir, ".wai", "project.yaml"))) continue;
52438
+ const shown2 = shownDir(dir);
52439
+ try {
52440
+ await runWithProjectRoot(dir, async () => {
52441
+ await runGenerate({});
52442
+ runMcpInstall({ global: false, backend: "claude" });
52443
+ });
52444
+ logger.success(`Set ${shown2} up for its own sessions (guide, skills, .mcp.json): a teammate opens that folder in their AI tool.`);
52445
+ } catch (e) {
52446
+ logger.warn(`Could not set ${shown2} up for its own sessions (${e instanceof Error ? e.message : String(e)}). Run there: \`wairon generate\` and \`wairon mcp install --backend claude\`.`);
52447
+ }
52448
+ }
51573
52449
  }
51574
52450
  var ownerLabel = (key) => key === "" ? "the top project" : key;
51575
52451
  var SCREENFUL = 20;
@@ -51691,10 +52567,11 @@ async function runInit(options = {}) {
51691
52567
  }
51692
52568
  if (!options.yes && !process.stdin.isTTY) {
51693
52569
  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."
52570
+ "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, target claude). Nothing was written."
51695
52571
  );
51696
52572
  }
51697
52573
  if (ancestorRoot) {
52574
+ if (options.yes) throw new WaironError(boundByParentMessage(ancestorRoot, cwd));
51698
52575
  await runInitAsMember(ancestorRoot, cwd, options);
51699
52576
  return;
51700
52577
  }
@@ -51707,23 +52584,35 @@ async function runInit(options = {}) {
51707
52584
  function memberAliasOf(name) {
51708
52585
  return name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "member";
51709
52586
  }
52587
+ function boundByParentCommands(parentRoot, cwd) {
52588
+ const relPath = (path58.relative(parentRoot, cwd) || ".").split(path58.sep).join("/");
52589
+ const alias = memberAliasOf(path58.basename(cwd));
52590
+ return [
52591
+ `make it a member project of that parent: cd "${parentRoot}" && wairon member add ${alias} ${relPath} --project`,
52592
+ "or start an independent project here: make this folder its own repository (`git init`), then re-run `wairon init`"
52593
+ ];
52594
+ }
52595
+ function boundByParentMessage(parentRoot, cwd) {
52596
+ const [member, independent] = boundByParentCommands(parentRoot, cwd);
52597
+ return `This folder is bound by the wairon project at ${parentRoot}, and --yes cannot answer whether it should join it \u2014 init never edits a project.yaml outside this folder without asking. Nothing was written. To ${member}; ${independent}.`;
52598
+ }
51710
52599
  async function runInitAsMember(parentRoot, cwd, options) {
51711
52600
  const relPath = (path58.relative(parentRoot, cwd) || ".").split(path58.sep).join("/");
51712
52601
  const defaultAlias = memberAliasOf(path58.basename(cwd));
51713
- logger.info(`Detected a parent wairon project at ${parentRoot}`);
52602
+ logger.info(`Detected a parent wairon project at ${parentRoot} \u2014 it binds this folder.`);
51714
52603
  logger.info(`This directory ("${relPath}") is not yet a wairon project.`);
51715
52604
  if (!options.yes) {
51716
52605
  const { proceed } = await import_inquirer4.default.prompt([
51717
52606
  {
51718
52607
  type: "confirm",
51719
52608
  name: "proceed",
51720
- message: `Create "${relPath}" as a member project of the parent project?`,
51721
- default: true
52609
+ message: `Create "${relPath}" as a member project of the parent project? (edits ${path58.join(parentRoot, ".wai", "project.yaml")})`,
52610
+ default: false
51722
52611
  }
51723
52612
  ]);
51724
52613
  if (!proceed) {
51725
52614
  logger.info("Cancelled \u2014 no changes made.");
51726
- logger.info("To add this directory later, run `wairon member add <alias> <path>` from the parent project.");
52615
+ for (const line2 of boundByParentCommands(parentRoot, cwd)) logger.info(`To ${line2}.`);
51727
52616
  return;
51728
52617
  }
51729
52618
  }
@@ -51763,13 +52652,13 @@ async function runInitNonInteractive() {
51763
52652
  const cwd = process.cwd();
51764
52653
  const projectName = path58.basename(cwd);
51765
52654
  const now = (/* @__PURE__ */ new Date()).toISOString();
51766
- const targets = [defaultTargetConfig("claude"), defaultTargetConfig("agy")];
52655
+ const targets = [defaultTargetConfig("claude")];
51767
52656
  const projectConfig = buildProjectConfig(projectName, targets, now, "backend");
51768
52657
  const guidePlan = {
51769
52658
  claudeGlobal: false,
51770
52659
  claudeLocal: true,
51771
52660
  geminiGlobal: false,
51772
- geminiLocal: true
52661
+ geminiLocal: false
51773
52662
  };
51774
52663
  await executeInit(
51775
52664
  projectName,
@@ -52442,7 +53331,7 @@ function releaseChannelLabel(tag) {
52442
53331
  return version.slice(dash + 1).replace(/\.\d+$/, "") || "stable";
52443
53332
  }
52444
53333
  function fetchReleases(repo) {
52445
- return new Promise((resolve56, reject) => {
53334
+ return new Promise((resolve57, reject) => {
52446
53335
  const url = `https://api.github.com/repos/${repo}/releases?per_page=100`;
52447
53336
  const options = {
52448
53337
  headers: {
@@ -52462,7 +53351,7 @@ function fetchReleases(repo) {
52462
53351
  return;
52463
53352
  }
52464
53353
  try {
52465
- resolve56(JSON.parse(data));
53354
+ resolve57(JSON.parse(data));
52466
53355
  } catch {
52467
53356
  reject(new Error("Failed to parse GitHub API response"));
52468
53357
  }
@@ -52634,7 +53523,7 @@ async function filteredCheckbox(config) {
52634
53523
  32,
52635
53524
  Math.max(...items.map((i) => i.label.length))
52636
53525
  );
52637
- return new Promise((resolve56) => {
53526
+ return new Promise((resolve57) => {
52638
53527
  const checked = /* @__PURE__ */ new Set();
52639
53528
  let cursor = 0;
52640
53529
  let filterIdx = 0;
@@ -52701,7 +53590,7 @@ async function filteredCheckbox(config) {
52701
53590
  );
52702
53591
  teardown();
52703
53592
  const result = items.filter((_, idx) => checked.has(idx)).map((i) => i.value);
52704
- resolve56(result);
53593
+ resolve57(result);
52705
53594
  }
52706
53595
  function abort() {
52707
53596
  process.stdout.write("\n");
@@ -53398,7 +54287,7 @@ async function runDoctor(options = {}) {
53398
54287
  const lock = readLockState(computeGateStateId());
53399
54288
  if (lock.state === "locked") {
53400
54289
  console.log(import_chalk17.default.bold("Lock"));
53401
- line(tally, "ok", `frozen at ${lock.record.lockedAt} by ${describeApprover(lock.record.lockedBy)}`);
54290
+ line(tally, "ok", `approved at ${lock.record.lockedAt} by ${describeApprover(lock.record.lockedBy)}`);
53402
54291
  if (!lock.record.specs) {
53403
54292
  line(tally, "warn", "this lock predates per-spec approval \u2014 re-lock so `wairon status` can name what drifts");
53404
54293
  }
@@ -53415,7 +54304,7 @@ async function runDoctor(options = {}) {
53415
54304
  line(
53416
54305
  tally,
53417
54306
  "warn",
53418
- `stale \u2014 the design or something it was approved under changed since ${lock.record.lockedAt}, so this lock no longer holds. Re-run \`wairon lock\` to freeze the current state.`
54307
+ `stale \u2014 the design or something it was approved under changed since ${lock.record.lockedAt}, so the approval no longer covers it. Run \`wairon lock\` to approve the current design (\`wairon status\` names the specs that moved).`
53419
54308
  );
53420
54309
  logger.blank();
53421
54310
  }
@@ -54068,21 +54957,21 @@ async function runDiagram(options = {}) {
54068
54957
  const formatted = applyFormat(options);
54069
54958
  if (formatted.canvas && !formatted.all) {
54070
54959
  const dest2 = formatted.out ?? path62.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
54071
- writeArtifact(dest2, renderDiagram("canvas", canvasRelations(formatted)));
54960
+ writeArtifact(dest2, renderDiagram2("canvas", canvasRelations(formatted)));
54072
54961
  logger.success(`Interactive canvas written to ${dest2}`);
54073
54962
  logger.info("Open it in a browser \u2014 fully self-contained (works offline).");
54074
54963
  return;
54075
54964
  }
54076
54965
  if (formatted.drawio && !formatted.all) {
54077
54966
  const dest2 = formatted.out ?? path62.join(AI_PATHS.docsDir(), "diagrams", "architecture.drawio");
54078
- writeArtifact(dest2, renderDiagram("drawio"));
54967
+ writeArtifact(dest2, renderDiagram2("drawio"));
54079
54968
  logger.success(`draw.io diagram written to ${dest2}`);
54080
54969
  logger.info("Open with draw.io / diagrams.net (or import into tools that accept the format).");
54081
54970
  return;
54082
54971
  }
54083
54972
  if (formatted.excalidraw && !formatted.all) {
54084
54973
  const dest2 = formatted.out ?? path62.join(AI_PATHS.docsDir(), "diagrams", "architecture.excalidraw");
54085
- writeArtifact(dest2, renderDiagram("excalidraw"));
54974
+ writeArtifact(dest2, renderDiagram2("excalidraw"));
54086
54975
  logger.success(`Excalidraw scene written to ${dest2}`);
54087
54976
  logger.info("Open with excalidraw.com or the VS Code extension.");
54088
54977
  return;
@@ -54090,7 +54979,7 @@ async function runDiagram(options = {}) {
54090
54979
  const wantsMermaid = formatted.format?.toLowerCase().startsWith("mermaid") || !!formatted.subsystem || !!formatted.sequence;
54091
54980
  if (!formatted.all && !formatted.sequence && !wantsMermaid) {
54092
54981
  const dest2 = formatted.out ?? path62.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
54093
- writeArtifact(dest2, renderDiagram("canvas", canvasRelations(formatted)));
54982
+ writeArtifact(dest2, renderDiagram2("canvas", canvasRelations(formatted)));
54094
54983
  logger.success(`Interactive canvas written to ${dest2}`);
54095
54984
  logger.info("Open it in a browser \u2014 fully self-contained (works offline). Other formats: --format mermaid|drawio|excalidraw.");
54096
54985
  return;
@@ -54105,9 +54994,9 @@ async function runDiagram(options = {}) {
54105
54994
  for (const file of files) {
54106
54995
  writeArtifact(path62.join(outDir, file.relPath), toMarkdown(file));
54107
54996
  }
54108
- writeArtifact(path62.join(outDir, "canvas.html"), renderDiagram("canvas", canvasRelations(formatted)));
54109
- writeArtifact(path62.join(outDir, "architecture.drawio"), renderDiagram("drawio"));
54110
- writeArtifact(path62.join(outDir, "architecture.excalidraw"), renderDiagram("excalidraw"));
54997
+ writeArtifact(path62.join(outDir, "canvas.html"), renderDiagram2("canvas", canvasRelations(formatted)));
54998
+ writeArtifact(path62.join(outDir, "architecture.drawio"), renderDiagram2("drawio"));
54999
+ writeArtifact(path62.join(outDir, "architecture.excalidraw"), renderDiagram2("excalidraw"));
54111
55000
  const graph2 = loadSpecGraph();
54112
55001
  const indexPath = path62.join(outDir, "README.md");
54113
55002
  writeArtifact(indexPath, diagramSetIndex(files, graph2.systemName));
@@ -54132,7 +55021,7 @@ async function runDiagram(options = {}) {
54132
55021
  title = `${formatted.subsystem} \u2014 components`;
54133
55022
  defaultDest = path62.join(diagramsDir, "subsystems", `${formatted.subsystem.replace(/::/g, "--")}.md`);
54134
55023
  } else {
54135
- mermaid = renderDiagram("mermaid");
55024
+ mermaid = renderDiagram2("mermaid");
54136
55025
  title = "Component architecture";
54137
55026
  defaultDest = path62.join(diagramsDir, "system.md");
54138
55027
  }
@@ -54178,7 +55067,7 @@ var path63 = __toESM(require("path"));
54178
55067
  function registryPath(dataDir) {
54179
55068
  return path63.join(dataDir, "projects.json");
54180
55069
  }
54181
- function load4(dataDir) {
55070
+ function load5(dataDir) {
54182
55071
  try {
54183
55072
  return JSON.parse(fs40.readFileSync(registryPath(dataDir), "utf8"));
54184
55073
  } catch {
@@ -54198,8 +55087,8 @@ var ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
54198
55087
  function isValidProjectId(id) {
54199
55088
  return typeof id === "string" && ID_RE.test(id);
54200
55089
  }
54201
- function load5(dataDir) {
54202
- return load4(dataDir);
55090
+ function load6(dataDir) {
55091
+ return load5(dataDir);
54203
55092
  }
54204
55093
  function save2(dataDir, records) {
54205
55094
  save(dataDir, records.map((r) => r.parentProjectId ? withoutRoot(r) : r));
@@ -54246,7 +55135,7 @@ function familyRootOf(records, record2) {
54246
55135
  }
54247
55136
  function existingProjectRoot(dataDir, id) {
54248
55137
  if (!isValidProjectId(id)) return null;
54249
- const records = load5(dataDir);
55138
+ const records = load6(dataDir);
54250
55139
  const rec = records.find((r) => r.id === id);
54251
55140
  return rec ? derivedRoot(records, rec) : null;
54252
55141
  }
@@ -54256,7 +55145,7 @@ function declaredSubsystemIds(dataDir, id) {
54256
55145
  }
54257
55146
  function rootOnDisk(dataDir, id) {
54258
55147
  if (!isValidProjectId(id)) return null;
54259
- const records = load5(dataDir);
55148
+ const records = load6(dataDir);
54260
55149
  const rec = records.find((r) => r.id === id);
54261
55150
  const root = rec ? derivedRoot(records, rec) : null;
54262
55151
  return root && fs41.existsSync(root) ? root : null;
@@ -54275,7 +55164,7 @@ function createProjectRecord(dataDir, id) {
54275
55164
  if (!isValidProjectId(id)) {
54276
55165
  throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
54277
55166
  }
54278
- const records = load5(dataDir);
55167
+ const records = load6(dataDir);
54279
55168
  if (records.some((r) => r.id === id)) {
54280
55169
  throw new Error(`Project "${id}" already exists.`);
54281
55170
  }
@@ -54295,7 +55184,7 @@ function registerProjectRecord(dataDir, id, rootPath) {
54295
55184
  if (!isValidProjectId(id)) {
54296
55185
  throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
54297
55186
  }
54298
- const records = load5(dataDir);
55187
+ const records = load6(dataDir);
54299
55188
  const existing = records.find((r) => r.id === id);
54300
55189
  const record2 = {
54301
55190
  id,
@@ -54309,7 +55198,7 @@ function registerProjectRecord(dataDir, id, rootPath) {
54309
55198
  }
54310
55199
  function registerMemberRecord(dataDir, record2) {
54311
55200
  const memberPath = validMemberShape(record2);
54312
- const records = load5(dataDir);
55201
+ const records = load6(dataDir);
54313
55202
  refuseParent(records, record2);
54314
55203
  const existing = records.find((r) => r.id === record2.id);
54315
55204
  const member = {
@@ -54363,7 +55252,7 @@ function setProjectRecordStatus(dataDir, id, status2, reason) {
54363
55252
  if (status2 !== "active" && status2 !== "disabled") {
54364
55253
  throw new Error(`Invalid project status "${status2}" (allowed: active, disabled).`);
54365
55254
  }
54366
- const records = load5(dataDir);
55255
+ const records = load6(dataDir);
54367
55256
  const existing = records.find((r) => r.id === id);
54368
55257
  if (!existing) throw new Error(`Unknown project "${id}".`);
54369
55258
  const { disabledReason: _was, ...rest } = existing;
@@ -54373,11 +55262,11 @@ function setProjectRecordStatus(dataDir, id, status2, reason) {
54373
55262
  return withDerivedRoot(next, record2);
54374
55263
  }
54375
55264
  function listProjectRecords(dataDir) {
54376
- const records = load5(dataDir);
55265
+ const records = load6(dataDir);
54377
55266
  return records.map((r) => withDerivedRoot(records, r));
54378
55267
  }
54379
55268
  function removeProjectRecord(dataDir, id) {
54380
- const records = load5(dataDir);
55269
+ const records = load6(dataDir);
54381
55270
  const rec = records.find((r) => r.id === id);
54382
55271
  if (!rec) return;
54383
55272
  if (!rec.parentProjectId) {
@@ -54393,7 +55282,7 @@ function removeProjectRecord(dataDir, id) {
54393
55282
  save2(dataDir, records.filter((r) => r.id !== id));
54394
55283
  }
54395
55284
  function listFamilyRecords(dataDir, id) {
54396
- const records = load5(dataDir);
55285
+ const records = load6(dataDir);
54397
55286
  const rec = records.find((r) => r.id === id);
54398
55287
  if (!rec) return [];
54399
55288
  const root = familyRootOf(records, rec);
@@ -54408,7 +55297,7 @@ function listFamilyRecords(dataDir, id) {
54408
55297
  return out;
54409
55298
  }
54410
55299
  function projectRepositoryScope(dataDir, id) {
54411
- const records = load5(dataDir);
55300
+ const records = load6(dataDir);
54412
55301
  const rec = records.find((r) => r.id === id);
54413
55302
  if (!rec) return null;
54414
55303
  const segments = [];
@@ -54495,7 +55384,7 @@ function resolveQualifiedMember(dataDir, qualifier) {
54495
55384
  if (!parsed || parsed.mounts.length === 0) {
54496
55385
  throw new Error(`invalid member qualifier "${qualifier}" (expected projectId::alias, one alias per hop)`);
54497
55386
  }
54498
- const records = load5(dataDir);
55387
+ const records = load6(dataDir);
54499
55388
  const top = records.find((r) => r.id === parsed.projectId);
54500
55389
  const topRoot = top ? derivedRoot(records, top) : null;
54501
55390
  if (!top || top.status !== "active" || topRoot === null) throw new Error(`unknown project "${parsed.projectId}"`);
@@ -54536,7 +55425,7 @@ function memberIdAt2(dir) {
54536
55425
  return { id };
54537
55426
  }
54538
55427
  function planMemberRecords(dataDir, id) {
54539
- const records = load5(dataDir);
55428
+ const records = load6(dataDir);
54540
55429
  const rec = records.find((r) => r.id === id);
54541
55430
  if (!rec) return [];
54542
55431
  const root = familyRootOf(records, rec);
@@ -54610,7 +55499,7 @@ function assertMintableNarrowingEntry(dataDir, entry) {
54610
55499
  );
54611
55500
  }
54612
55501
  if (parsed.mounts.length > 0) return resolveQualifiedMember(dataDir, entry).id;
54613
- if (!load5(dataDir).some((r) => r.id === parsed.projectId)) throw new Error(`unknown project "${parsed.projectId}"`);
55502
+ if (!load6(dataDir).some((r) => r.id === parsed.projectId)) throw new Error(`unknown project "${parsed.projectId}"`);
54614
55503
  return parsed.projectId;
54615
55504
  }
54616
55505
  function entryRecordId(dataDir, entry) {
@@ -54638,7 +55527,7 @@ function indexResolveBinding(dataDir, principal, selector) {
54638
55527
  if (targetId === null) return null;
54639
55528
  via = target;
54640
55529
  }
54641
- const records = load5(dataDir);
55530
+ const records = load6(dataDir);
54642
55531
  const rec = records.find((r) => r.id === targetId);
54643
55532
  if (!rec || !isValidProjectId(rec.id)) return null;
54644
55533
  if (!wildcard && !authorized.some((e) => {
@@ -54656,7 +55545,7 @@ function indexResolveBinding(dataDir, principal, selector) {
54656
55545
  function indexResolveRoot(dataDir, principal, selector) {
54657
55546
  return indexResolveBinding(dataDir, principal, selector)?.rootPath ?? null;
54658
55547
  }
54659
- function resolveProjectBinding(dataDir, principal, selector) {
55548
+ function resolveProjectBinding2(dataDir, principal, selector) {
54660
55549
  return indexResolveBinding(dataDir, principal, selector);
54661
55550
  }
54662
55551
  function resolveProjectRoot(dataDir, principal, selector) {
@@ -55079,7 +55968,7 @@ function storePath2(dataDir) {
55079
55968
  function assignmentKey(a) {
55080
55969
  return [a.subjectKind, a.subjectId ?? "", a.scopeKind, a.scopeId ?? "", a.capability].join("|");
55081
55970
  }
55082
- function load6(dataDir) {
55971
+ function load7(dataDir) {
55083
55972
  const p = storePath2(dataDir);
55084
55973
  let raw;
55085
55974
  try {
@@ -55107,7 +55996,7 @@ function replaceAll(dataDir, assignments) {
55107
55996
  fs43.renameSync(tmp, p);
55108
55997
  }
55109
55998
  function registrySet(dataDir, assignment) {
55110
- const assignments = load6(dataDir);
55999
+ const assignments = load7(dataDir);
55111
56000
  const key = assignmentKey(assignment);
55112
56001
  const idx = assignments.findIndex((a) => assignmentKey(a) === key);
55113
56002
  if (assignment.value === "inherit") {
@@ -55129,7 +56018,7 @@ function registrySet(dataDir, assignment) {
55129
56018
  return stored;
55130
56019
  }
55131
56020
  function registryRemove(dataDir, assignmentId) {
55132
- const assignments = load6(dataDir);
56021
+ const assignments = load7(dataDir);
55133
56022
  const remaining = assignments.filter((a) => a.id !== assignmentId);
55134
56023
  if (remaining.length === assignments.length) return;
55135
56024
  replaceAll(dataDir, remaining);
@@ -55137,7 +56026,7 @@ function registryRemove(dataDir, assignmentId) {
55137
56026
  function registryRemapScope(dataDir, remap) {
55138
56027
  if (remap.length === 0) return;
55139
56028
  const lookup2 = new Map(remap.map((r) => [r.oldId, r.newId]));
55140
- const assignments = load6(dataDir);
56029
+ const assignments = load7(dataDir);
55141
56030
  let changed = false;
55142
56031
  const rewritten = assignments.map((a) => {
55143
56032
  const next = a.scopeId ? lookup2.get(a.scopeId) : void 0;
@@ -55151,13 +56040,13 @@ function registryRemapScope(dataDir, remap) {
55151
56040
  function registryRemoveForScopes(dataDir, scopeIds) {
55152
56041
  if (scopeIds.length === 0) return;
55153
56042
  const doomed = new Set(scopeIds);
55154
- const assignments = load6(dataDir);
56043
+ const assignments = load7(dataDir);
55155
56044
  const remaining = assignments.filter((a) => !(a.scopeId && doomed.has(a.scopeId)));
55156
56045
  if (remaining.length === assignments.length) return;
55157
56046
  replaceAll(dataDir, remaining);
55158
56047
  }
55159
56048
  function indexList(dataDir, scopeIds, subjectKind, subjectId, capability) {
55160
- let assignments = load6(dataDir);
56049
+ let assignments = load7(dataDir);
55161
56050
  if (scopeIds && scopeIds.length > 0) {
55162
56051
  const wanted = new Set(scopeIds);
55163
56052
  assignments = assignments.filter((a) => a.scopeId !== void 0 && wanted.has(a.scopeId));
@@ -55168,7 +56057,7 @@ function indexList(dataDir, scopeIds, subjectKind, subjectId, capability) {
55168
56057
  return assignments;
55169
56058
  }
55170
56059
  function indexGet(dataDir, assignmentId) {
55171
- return load6(dataDir).find((a) => a.id === assignmentId) ?? null;
56060
+ return load7(dataDir).find((a) => a.id === assignmentId) ?? null;
55172
56061
  }
55173
56062
  function setAssignment(dataDir, assignment) {
55174
56063
  return registrySet(dataDir, assignment);
@@ -55240,7 +56129,7 @@ function isBuiltinRoleId(roleId) {
55240
56129
  function storePath3(dataDir) {
55241
56130
  return path67.join(dataDir, "roles.json");
55242
56131
  }
55243
- function load7(dataDir) {
56132
+ function load8(dataDir) {
55244
56133
  const p = storePath3(dataDir);
55245
56134
  let raw;
55246
56135
  try {
@@ -55271,7 +56160,7 @@ function registryCreate(dataDir, role) {
55271
56160
  if (isBuiltinRoleId(role.id)) {
55272
56161
  throw new Error(`Role "${role.id}" is a reserved built-in role and cannot be created.`);
55273
56162
  }
55274
- const roles = load7(dataDir);
56163
+ const roles = load8(dataDir);
55275
56164
  if (roles.some((r) => r.id === role.id)) {
55276
56165
  throw new Error(`Role "${role.id}" already exists.`);
55277
56166
  }
@@ -55284,7 +56173,7 @@ function registryUpdate(dataDir, role) {
55284
56173
  if (isBuiltinRoleId(role.id)) {
55285
56174
  throw new Error(`Role "${role.id}" is a reserved built-in role and cannot be modified.`);
55286
56175
  }
55287
- const roles = load7(dataDir);
56176
+ const roles = load8(dataDir);
55288
56177
  const idx = roles.findIndex((r) => r.id === role.id);
55289
56178
  if (idx === -1) {
55290
56179
  throw new Error(`Role "${role.id}" not found.`);
@@ -55298,7 +56187,7 @@ function registryDelete(dataDir, roleId) {
55298
56187
  if (isBuiltinRoleId(roleId)) {
55299
56188
  throw new Error(`Role "${roleId}" is a reserved built-in role and cannot be deleted.`);
55300
56189
  }
55301
- const roles = load7(dataDir);
56190
+ const roles = load8(dataDir);
55302
56191
  const remaining = roles.filter((r) => r.id !== roleId);
55303
56192
  if (remaining.length === roles.length) return;
55304
56193
  replaceAll2(dataDir, remaining);
@@ -55307,7 +56196,7 @@ function indexListEffectiveRoles(dataDir) {
55307
56196
  return [...indexListRoles(dataDir).filter((r) => !isBuiltinRoleId(r.id)), ...BUILTIN_ROLES];
55308
56197
  }
55309
56198
  function indexListRoles(dataDir) {
55310
- return load7(dataDir).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
56199
+ return load8(dataDir).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
55311
56200
  }
55312
56201
  function createRole(dataDir, role) {
55313
56202
  return registryCreate(dataDir, role);
@@ -55735,7 +56624,7 @@ var path69 = __toESM(require("path"));
55735
56624
  function storePath5(dataDir) {
55736
56625
  return path69.join(dataDir, "auth", "credentials.json");
55737
56626
  }
55738
- function load8(dataDir) {
56627
+ function load9(dataDir) {
55739
56628
  try {
55740
56629
  return JSON.parse(fs46.readFileSync(storePath5(dataDir), "utf8"));
55741
56630
  } catch {
@@ -55762,10 +56651,10 @@ function digestEquals(a, b) {
55762
56651
  return crypto13.timingSafeEqual(ab, bb);
55763
56652
  }
55764
56653
  function findByTokenHash(dataDir, tokenHash) {
55765
- return load8(dataDir).find((r) => digestEquals(r.keyHash, tokenHash)) ?? null;
56654
+ return load9(dataDir).find((r) => digestEquals(r.keyHash, tokenHash)) ?? null;
55766
56655
  }
55767
56656
  function createCredential(dataDir, record2) {
55768
- const records = load8(dataDir);
56657
+ const records = load9(dataDir);
55769
56658
  if (records.some((r) => r.id === record2.id)) {
55770
56659
  throw new Error(`Credential "${record2.id}" already exists.`);
55771
56660
  }
@@ -55773,10 +56662,10 @@ function createCredential(dataDir, record2) {
55773
56662
  save3(dataDir, records);
55774
56663
  }
55775
56664
  function revokeCredential(dataDir, id) {
55776
- save3(dataDir, load8(dataDir).filter((r) => r.id !== id));
56665
+ save3(dataDir, load9(dataDir).filter((r) => r.id !== id));
55777
56666
  }
55778
56667
  function revokeAllForOwner(dataDir, ownerUserId) {
55779
- const records = load8(dataDir);
56668
+ const records = load9(dataDir);
55780
56669
  const now = (/* @__PURE__ */ new Date()).toISOString();
55781
56670
  let revoked = 0;
55782
56671
  for (const rec of records) {
@@ -55789,16 +56678,16 @@ function revokeAllForOwner(dataDir, ownerUserId) {
55789
56678
  return revoked;
55790
56679
  }
55791
56680
  function listCredentials(dataDir, project2) {
55792
- return load8(dataDir).filter(
56681
+ return load9(dataDir).filter(
55793
56682
  (r) => project2 === "*" || r.projects.includes("*") || r.projects.includes(project2)
55794
56683
  );
55795
56684
  }
55796
56685
  function listByOwner(dataDir, ownerUserId) {
55797
- return load8(dataDir).filter((r) => r.ownerSubject?.userId === ownerUserId);
56686
+ return load9(dataDir).filter((r) => r.ownerSubject?.userId === ownerUserId);
55798
56687
  }
55799
56688
  function renarrowCredential(dataDir, id, projects) {
55800
56689
  if (projects.length === 0) throw new Error(`Credential "${id}": a narrowing cannot be empty.`);
55801
- const records = load8(dataDir);
56690
+ const records = load9(dataDir);
55802
56691
  const rec = records.find((r) => r.id === id);
55803
56692
  if (!rec) throw new Error(`Unknown credential "${id}".`);
55804
56693
  if (rec.revokedAt) throw new Error(`Credential "${id}" is revoked.`);
@@ -57451,7 +58340,7 @@ init_fs();
57451
58340
  function configPath2() {
57452
58341
  return aiDir("producers.json");
57453
58342
  }
57454
- function load9() {
58343
+ function load10() {
57455
58344
  try {
57456
58345
  return JSON.parse(fs54.readFileSync(configPath2(), "utf8"));
57457
58346
  } catch {
@@ -57466,18 +58355,18 @@ function save4(configs) {
57466
58355
  fs54.renameSync(tmp, p);
57467
58356
  }
57468
58357
  function readProducerConfig(target) {
57469
- return load9().find((c) => c.target === target) ?? null;
58358
+ return load10().find((c) => c.target === target) ?? null;
57470
58359
  }
57471
58360
  function writeProducerConfig(config) {
57472
- const configs = load9().filter((c) => c.target !== config.target);
58361
+ const configs = load10().filter((c) => c.target !== config.target);
57473
58362
  configs.push(config);
57474
58363
  save4(configs);
57475
58364
  }
57476
58365
  function clearProducerConfig(target) {
57477
- save4(load9().filter((c) => c.target !== target));
58366
+ save4(load10().filter((c) => c.target !== target));
57478
58367
  }
57479
58368
  function listProducerConfigs() {
57480
- return load9();
58369
+ return load10();
57481
58370
  }
57482
58371
 
57483
58372
  // src/producers/core-adapter.ts
@@ -57493,7 +58382,7 @@ function project(diagramUrl) {
57493
58382
  const types = loadTypeSpecs();
57494
58383
  let mermaid = "";
57495
58384
  try {
57496
- mermaid = renderDiagram("mermaid");
58385
+ mermaid = renderDiagram2("mermaid");
57497
58386
  } catch {
57498
58387
  }
57499
58388
  return {
@@ -57995,7 +58884,7 @@ function executeApprovedLock(cfg, projectId, approver, expected) {
57995
58884
  const specs = captureApprovedSpecs();
57996
58885
  refuseMovedInputs(captured, computeGateStateId());
57997
58886
  const record2 = lockRecordOf(captured, approver, design, gate, config, specs, members);
57998
- writeLockRecord(record2);
58887
+ writeLockRecord2(record2);
57999
58888
  return publishLock(record2, scope);
58000
58889
  });
58001
58890
  }
@@ -58056,7 +58945,7 @@ function publishLock(record2, scope) {
58056
58945
  const publish2 = runWithProjectRoot(scope.repositoryRoot, () => publish(message, scope.pathspecs));
58057
58946
  if (!publish2.published) return record2;
58058
58947
  const published = { ...record2, commitSha: publish2.commitSha, compareUrl: publish2.compareUrl };
58059
- writeLockRecord(published);
58948
+ writeLockRecord2(published);
58060
58949
  return published;
58061
58950
  }
58062
58951
  function projectGitCredentialKey(project2) {
@@ -58221,7 +59110,7 @@ function generateDiagram(cfg, credential, project2, format) {
58221
59110
  const root = existingProjectRoot(cfg.dataDir, project2);
58222
59111
  if (!root) throw new Error(`Unknown project "${project2}".`);
58223
59112
  const relations = format === "canvas" ? instanceRelations(cfg, project2, root) : void 0;
58224
- return runWithProjectRoot(root, () => renderDiagram(format, relations));
59113
+ return runWithProjectRoot(root, () => renderDiagram2(format, relations));
58225
59114
  }
58226
59115
  function instanceRelations(cfg, project2, root) {
58227
59116
  const familyRoot = listFamilyRecords(cfg.dataDir, project2)[0];
@@ -62006,7 +62895,7 @@ function lockTargetOf(cfg, req) {
62006
62895
  `Approved ${req.kind} request "${req.id}" carries a ${SUBPROJECT_SCOPE_PAYLOAD} payload with no usable member qualifier \u2014 refusing to execute, because falling back to the whole project would widen the scope the requester was confined to.`
62007
62896
  );
62008
62897
  }
62009
- const member = resolveProjectBinding(cfg.dataDir, RECORD_LOOKUP, `${projectId}::${parsed.subproject}`);
62898
+ const member = resolveProjectBinding2(cfg.dataDir, RECORD_LOOKUP, `${projectId}::${parsed.subproject}`);
62010
62899
  if (!member) {
62011
62900
  throw new Error(
62012
62901
  `Approved ${req.kind} request "${req.id}" names member "${parsed.subproject}" of "${projectId}", which holds no hosted record \u2014 refusing to execute rather than widen to the whole project; run \`wairon host doctor --fix\`.`
@@ -62196,7 +63085,7 @@ async function awaitApproval(cfg, credential, requestId, timeoutSeconds) {
62196
63085
  let current2 = req;
62197
63086
  while (current2.status === "pending" && Date.now() < deadline) {
62198
63087
  const remaining = deadline - Date.now();
62199
- await new Promise((resolve56) => setTimeout(resolve56, Math.min(AWAIT_POLL_INTERVAL_MS, remaining)));
63088
+ await new Promise((resolve57) => setTimeout(resolve57, Math.min(AWAIT_POLL_INTERVAL_MS, remaining)));
62200
63089
  expirePendingApprovals(cfg.dataDir, (/* @__PURE__ */ new Date()).toISOString());
62201
63090
  current2 = getApprovalRequestById(cfg.dataDir, requestId) ?? current2;
62202
63091
  }
@@ -63608,7 +64497,7 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
63608
64497
  permissionSubject: { subjectId: "anonymous", roleBindings: [], instanceAdmin: true }
63609
64498
  };
63610
64499
  }
63611
- const binding = resolveProjectBinding(cfg.dataDir, principal, projectSelector(req));
64500
+ const binding = resolveProjectBinding2(cfg.dataDir, principal, projectSelector(req));
63612
64501
  if (!binding) {
63613
64502
  sendJson(res, 403, { error: "project not authorized, unknown, or not specified" });
63614
64503
  return;
@@ -63800,7 +64689,7 @@ function writtenOwners(cfg, binding, response) {
63800
64689
  function familyReachOf(cfg, principal, binding) {
63801
64690
  const family = listFamilyRecords(cfg.dataDir, binding.projectId);
63802
64691
  const reach2 = resolveFamilyReach(cfg.dataDir, principal, family.map((r) => r.id));
63803
- const covered = (id) => resolveProjectBinding(cfg.dataDir, principal, id) !== null;
64692
+ const covered = (id) => resolveProjectBinding2(cfg.dataDir, principal, id) !== null;
63804
64693
  const topRoot = existingProjectRoot(cfg.dataDir, binding.familyRootId) ?? binding.rootPath;
63805
64694
  const parentReach = reach2.readable.includes(binding.familyRootId) && covered(binding.familyRootId);
63806
64695
  const unwritableRoots = family.filter((r) => r.rootPath && !isRetiredPart(r) && !(reach2.writable.includes(r.id) && covered(r.id))).map((r) => r.rootPath);
@@ -63815,7 +64704,7 @@ function familyReachOf(cfg, principal, binding) {
63815
64704
  }
63816
64705
  function hostedRecordLookup(cfg, principal) {
63817
64706
  return (recordId) => {
63818
- const bound2 = resolveProjectBinding(cfg.dataDir, principal, recordId);
64707
+ const bound2 = resolveProjectBinding2(cfg.dataDir, principal, recordId);
63819
64708
  if (!bound2 || recordId.includes(SUBPROJECT_SEPARATOR)) return null;
63820
64709
  return authorize(cfg.dataDir, principal, "project:read", "project", bound2.projectId).value === "yes" ? bound2.rootPath : null;
63821
64710
  };
@@ -63841,7 +64730,7 @@ function handleViewDiagram(cfg, req, res) {
63841
64730
  sendJson(res, 404, { error: "project not found" });
63842
64731
  return;
63843
64732
  }
63844
- const html = runWithProjectRoot(root, () => renderDiagram(grant.format));
64733
+ const html = runWithProjectRoot(root, () => renderDiagram2(grant.format));
63845
64734
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
63846
64735
  res.end(html);
63847
64736
  }
@@ -64224,7 +65113,7 @@ function captureSnapshot(dataDir, principal, projectId, view, artifacts) {
64224
65113
  canvasModel: JSON.stringify(buildCanvasDataModel(relations))
64225
65114
  };
64226
65115
  if (artifacts.includes("html")) {
64227
- snapshot.html = renderDiagram("canvas", relations);
65116
+ snapshot.html = renderDiagram2("canvas", relations);
64228
65117
  }
64229
65118
  if (artifacts.includes("openapi")) {
64230
65119
  const result = exportSurface2("project", "openapi");
@@ -64780,7 +65669,7 @@ function getWebProjectCanvas(cfg, sessionId, projectId) {
64780
65669
  }
64781
65670
  const listed = listProjects2(cfg, sessionId);
64782
65671
  const relations = runWithProjectBinding(root, readReach(cfg, projectId, root, listed), () => familyRelations2());
64783
- return runWithProjectRoot(root, () => renderDiagram("canvas", relations));
65672
+ return runWithProjectRoot(root, () => renderDiagram2("canvas", relations));
64784
65673
  }
64785
65674
  function getWebProjectCanvasModel(cfg, sessionId, projectId) {
64786
65675
  const principal = authenticateSession(cfg.dataDir, sessionId);
@@ -68551,6 +69440,7 @@ function upgradeMemberRecords(cfg, apply4) {
68551
69440
 
68552
69441
  // src/commands/host.ts
68553
69442
  init_fs();
69443
+ init_project();
68554
69444
 
68555
69445
  // src/core/demo-seed.ts
68556
69446
  init_specs2();
@@ -69157,11 +70047,11 @@ async function runServe(options = {}) {
69157
70047
  logger.info(` data dir: ${import_chalk18.default.gray(cfg.dataDir)}`);
69158
70048
  logger.blank();
69159
70049
  logger.info("Press Ctrl+C to stop.");
69160
- await new Promise((resolve56) => {
70050
+ await new Promise((resolve57) => {
69161
70051
  const shutdown = () => {
69162
70052
  logger.info("Shutting down\u2026");
69163
70053
  handle.close();
69164
- resolve56();
70054
+ resolve57();
69165
70055
  };
69166
70056
  process.on("SIGINT", shutdown);
69167
70057
  process.on("SIGTERM", shutdown);
@@ -69180,12 +70070,19 @@ function openBrowser(url) {
69180
70070
  }
69181
70071
  }
69182
70072
  async function runDev(options = {}) {
69183
- const cwd = process.cwd();
69184
- if (!fs68.existsSync(path91.join(cwd, ".wai"))) {
70073
+ const binding = resolveProjectBinding(process.cwd());
70074
+ if (!binding) {
69185
70075
  throw new WaironError(
69186
- "No .wai/ found in the current directory. Run `wairon dev` from a wairon project root (or run `wairon init` first)."
70076
+ "No wairon project binds this folder. Run `wairon dev` inside a wairon project (or run `wairon init` first)."
69187
70077
  );
69188
70078
  }
70079
+ const cwd = binding.root;
70080
+ let projectId = null;
70081
+ try {
70082
+ const config = runWithProjectRoot(cwd, () => loadProjectConfig());
70083
+ if (config) projectId = effectiveProjectId(config);
70084
+ } catch {
70085
+ }
69189
70086
  const hash = crypto29.createHash("sha256").update(cwd).digest("hex").slice(0, 16);
69190
70087
  const dataDir = path91.join(os14.tmpdir(), "wairon-dev", hash);
69191
70088
  fs68.mkdirSync(dataDir, { recursive: true });
@@ -69221,16 +70118,16 @@ async function runDev(options = {}) {
69221
70118
  const url = `http://127.0.0.1:${port}`;
69222
70119
  logger.success("wairon dev server started (local, single-project, no login).");
69223
70120
  logger.info(` open: ${import_chalk18.default.cyan(url)}`);
69224
- logger.info(` project: ${import_chalk18.default.gray(cwd)} ${import_chalk18.default.gray('(served as "local")')}`);
70121
+ logger.info(` project: ${projectId ?? path91.basename(cwd)} at ${import_chalk18.default.gray(cwd)} ${import_chalk18.default.gray('(served as "local")')}`);
69225
70122
  logger.info(` data dir: ${import_chalk18.default.gray(dataDir)} ${import_chalk18.default.gray("(ephemeral)")}`);
69226
70123
  logger.blank();
69227
70124
  logger.info("An agent edits specs; refresh the page to see the live graph. Press Ctrl+C to stop.");
69228
70125
  if (options.open) openBrowser(url);
69229
- await new Promise((resolve56) => {
70126
+ await new Promise((resolve57) => {
69230
70127
  const shutdown = () => {
69231
70128
  logger.info("Shutting down\u2026");
69232
70129
  handle.close();
69233
- resolve56();
70130
+ resolve57();
69234
70131
  };
69235
70132
  process.on("SIGINT", shutdown);
69236
70133
  process.on("SIGTERM", shutdown);
@@ -70002,7 +70899,8 @@ function printStatuses(statuses) {
70002
70899
  s.sourceKind,
70003
70900
  s.pinned ? "pinned" : import_chalk21.default.yellow("not pinned"),
70004
70901
  s.reachable ? "reachable" : import_chalk21.default.yellow("unreachable"),
70005
- s.stale ? import_chalk21.default.red("stale") : "current",
70902
+ // Never "current" beside "unreachable": nothing was compared.
70903
+ s.stale ? import_chalk21.default.red("stale") : s.reachable ? "current" : import_chalk21.default.yellow("not compared"),
70006
70904
  ...s.drifted ? [import_chalk21.default.yellow("drifted")] : []
70007
70905
  ];
70008
70906
  logger.info(`${import_chalk21.default.cyan(s.alias)} \u2192 ${s.project}: ${flags.join(", ")}${s.detail ? ` \u2014 ${s.detail}` : ""}`);
@@ -70011,6 +70909,9 @@ function printStatuses(statuses) {
70011
70909
  logger.info(` ${name}: ${u.state}${u.code ? ` ${u.code}` : ""}${u.detail ? ` \u2014 ${u.detail}` : ""}`);
70012
70910
  }
70013
70911
  }
70912
+ logger.info(import_chalk21.default.gray(
70913
+ "Words: stale \u2014 a name this project uses changed or was removed since the pin (adapt the design, then re-pin); current \u2014 every used name matches the pin; not compared \u2014 the producer could not be read; drifted \u2014 the producer moved since the pin (re-pin to record it; alone it means nothing used changed). Per name: unchanged; changed (its signature moved); removed (gone from the producer's export table); unlocked (used but not in the pin yet \u2014 re-pin); unavailable (not compared \u2014 never a pass)."
70914
+ ));
70014
70915
  }
70015
70916
  function printListings(rows) {
70016
70917
  if (!rows.length) {
@@ -70153,10 +71054,15 @@ async function setExecutionTier2(tier) {
70153
71054
  // src/cli/index.ts
70154
71055
  cleanStaleBinary();
70155
71056
  var program = new import_commander.Command();
70156
- program.name("wairon").description("SYW Waffle AIron \u2014 Spec-Driven Development (SDD) & Agent Topology Orchestration").version(WAIRON_VERSION, "-v, --version").option("--verbose", "enable verbose output").option("--silent", "suppress all output except errors").hook("preAction", (thisCommand) => {
71057
+ program.name("wairon").description("SYW Waffle AIron \u2014 Spec-Driven Development (SDD) & Agent Topology Orchestration").version(WAIRON_VERSION, "-v, --version").option("--verbose", "enable verbose output").option("--silent", "suppress all output except errors").hook("preAction", (thisCommand, actionCommand) => {
70157
71058
  const opts = thisCommand.opts();
70158
71059
  if (opts.verbose) setLogLevel("verbose");
70159
71060
  else if (opts.silent) setLogLevel("silent");
71061
+ if (!opts.silent) {
71062
+ const names = [];
71063
+ for (let c = actionCommand; c && c.parent; c = c.parent) names.unshift(c.name());
71064
+ announceBinding(names.join(" "));
71065
+ }
70160
71066
  });
70161
71067
  program.command("init").description("Initialize wairon and bootstrap the SDD Spec Tree (.wai/specs/) in the current project").option("-y, --yes", "use defaults without interactive prompts").option("--pack <source>", "vendor + register an extension pack right after init (repeatable)", (v, all) => [...all, v], []).action(async (opts) => {
70162
71068
  await runInit({ yes: opts.yes });
@@ -70187,7 +71093,7 @@ program.command("lock").description("Record human approval of the design: valida
70187
71093
  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) => {
70188
71094
  await validateCommand(opts);
70189
71095
  });
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) => {
71096
+ 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").option("--all", "list every spec that moved since the approval, not the first few").action(async (opts) => {
70191
71097
  await statusCommand(opts);
70192
71098
  });
70193
71099
  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) => {
@@ -70265,7 +71171,7 @@ var variantsCmd = program.command("variants").description("Component variants \u
70265
71171
  variantsCmd.command("list").alias("ls").description("List the component variants in the registry (global + project) with their base and guidance").action(async () => {
70266
71172
  await runVariants();
70267
71173
  });
70268
- program.command("diagram").description("Generate architecture diagrams from the spec tree \u2014 interactive canvas by default; Mermaid / draw.io / Excalidraw via --format").option("--subsystem <id>", "scope the component diagram to one subsystem (plus its external neighbors)").option("--sequence <component:method>", "emit a sequence diagram derived from the method's L5 narrative").option("--depth <n>", "max call-expansion depth for sequence diagrams (default 3)", (v) => parseInt(v, 10)).option("--all", "write the full diagram set: system, per-subsystem, entrypoint sequences, and the interactive canvas").option("--canvas", "emit the interactive self-contained HTML canvas (pan/zoom, collapse boundaries, detail panel, issue overlay)").option("--drawio", "emit an editable draw.io / diagrams.net file (same layout as the canvas)").option("--excalidraw", "emit an editable Excalidraw scene (same layout as the canvas)").option("--format <fmt>", "alias for the flags above: mermaid | canvas | drawio | excalidraw").option("--out <path>", "write to a file (or directory with --all; default .wai/docs/diagrams) instead of stdout").option("--no-health", "skip comparing each consumption relation with its live producer: the canvas draws those edges as not checked").action(async (opts) => {
71174
+ program.command("diagram").description("Generate architecture diagrams from the spec tree \u2014 interactive canvas by default; Mermaid / draw.io / Excalidraw via --format").option("--subsystem <id>", "scope the component diagram to one subsystem (plus its external neighbors)").option("--sequence <component:method>", "emit a sequence diagram derived from the method's L5 narrative").option("--depth <n>", "max call-expansion depth for sequence diagrams (default 3)", (v) => parseInt(v, 10)).option("--all", "write the full diagram set: system, per-subsystem, entrypoint sequences, and the interactive canvas").option("--canvas", "emit the interactive self-contained HTML canvas (pan/zoom, collapse boundaries, detail panel, issue overlay)").option("--drawio", "emit an editable draw.io / diagrams.net file (same layout as the canvas)").option("--excalidraw", "emit an editable Excalidraw scene (same layout as the canvas)").option("--format <fmt>", "alias for the flags above: mermaid | canvas | drawio | excalidraw").option("--out <path>", "the file to write (a directory with --all); every format writes a file \u2014 default under .wai/docs/diagrams/ (system.md for Mermaid, canvas.html for the canvas); a .mmd path writes raw Mermaid").option("--no-health", "skip comparing each consumption relation with its live producer: the canvas draws those edges as not checked").action(async (opts) => {
70269
71175
  await runDiagram({
70270
71176
  subsystem: opts.subsystem,
70271
71177
  sequence: opts.sequence,