@wairon/cli 5.1.1-dev.87 → 5.1.1-dev.89

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3060,7 +3060,7 @@ var require_dist = __commonJS({
3060
3060
  function slug(name) {
3061
3061
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3062
3062
  }
3063
- var fs24 = __toESM2(require("fs"));
3063
+ var fs25 = __toESM2(require("fs"));
3064
3064
  var path46 = __toESM2(require("path"));
3065
3065
  var import_fflate = require_node();
3066
3066
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".hg", ".svn"]);
@@ -3100,8 +3100,8 @@ var require_dist = __commonJS({
3100
3100
  function writeTree(destDir, files) {
3101
3101
  for (const file of files) {
3102
3102
  const absolute = path46.join(destDir, file.path);
3103
- fs24.mkdirSync(path46.dirname(absolute), { recursive: true });
3104
- fs24.writeFileSync(absolute, file.contents);
3103
+ fs25.mkdirSync(path46.dirname(absolute), { recursive: true });
3104
+ fs25.writeFileSync(absolute, file.contents);
3105
3105
  }
3106
3106
  }
3107
3107
  function classifyKind(name, symlinks) {
@@ -3110,14 +3110,14 @@ var require_dist = __commonJS({
3110
3110
  return "file";
3111
3111
  }
3112
3112
  function walkPackDir(root, current2, out) {
3113
- for (const entry of fs24.readdirSync(current2, { withFileTypes: true })) {
3113
+ for (const entry of fs25.readdirSync(current2, { withFileTypes: true })) {
3114
3114
  if (entry.isDirectory()) {
3115
3115
  if (SKIP_DIRS.has(entry.name)) continue;
3116
3116
  walkPackDir(root, path46.join(current2, entry.name), out);
3117
3117
  } else if (entry.isFile()) {
3118
3118
  const absolute = path46.join(current2, entry.name);
3119
3119
  const relative22 = path46.relative(root, absolute).split(path46.sep).join("/");
3120
- out.push({ path: relative22, contents: fs24.readFileSync(absolute) });
3120
+ out.push({ path: relative22, contents: fs25.readFileSync(absolute) });
3121
3121
  }
3122
3122
  }
3123
3123
  }
@@ -3503,6 +3503,7 @@ __export(src_exports, {
3503
3503
  SystemPublicInterfaceSchema: () => SystemPublicInterfaceSchema,
3504
3504
  SystemSpecSchema: () => SystemSpecSchema,
3505
3505
  TargetConfigSchema: () => TargetConfigSchema,
3506
+ TechnologySchema: () => TechnologySchema,
3506
3507
  TemplateNotFoundError: () => TemplateNotFoundError,
3507
3508
  TemplateSchema: () => TemplateSchema,
3508
3509
  TopologyConfigSchema: () => TopologyConfigSchema,
@@ -3530,6 +3531,7 @@ __export(src_exports, {
3530
3531
  assertProjectInitialized: () => assertProjectInitialized,
3531
3532
  backendCommand: () => backendCommand,
3532
3533
  backfillChainedSubprojectConfigs: () => backfillChainedSubprojectConfigs,
3534
+ backupBeside: () => backupBeside,
3533
3535
  buildCanvasDataModel: () => buildCanvasDataModel,
3534
3536
  buildProjectGraph: () => buildProjectGraph,
3535
3537
  buildRuleContext: () => buildRuleContext,
@@ -3604,6 +3606,7 @@ __export(src_exports, {
3604
3606
  generateDiagramSet: () => generateDiagramSet,
3605
3607
  generateSequenceDiagram: () => generateSequenceDiagram,
3606
3608
  getExporter: () => getExporter,
3609
+ getHostedLookup: () => getHostedLookup,
3607
3610
  getLoaderIssues: () => getLoaderIssues,
3608
3611
  getProjectRoot: () => getProjectRoot,
3609
3612
  getProjectRootOverride: () => getProjectRootOverride,
@@ -3628,6 +3631,7 @@ __export(src_exports, {
3628
3631
  internalizeMember: () => internalizeMember,
3629
3632
  isDraftSubsystem: () => isDraftSubsystem,
3630
3633
  isLogic: () => isLogic,
3634
+ isOutsideRoot: () => isOutsideRoot,
3631
3635
  isOwnComponentEntry: () => isOwnComponentEntry,
3632
3636
  isPattern: () => isPattern,
3633
3637
  isProjectInitialized: () => isProjectInitialized,
@@ -3735,6 +3739,7 @@ __export(src_exports, {
3735
3739
  resolveVariantGuidance: () => resolveVariantGuidance,
3736
3740
  retireSpecialists: () => retireSpecialists,
3737
3741
  rewriteReferences: () => rewriteReferences2,
3742
+ runWithHostedLookup: () => runWithHostedLookup,
3738
3743
  runWithProjectBinding: () => runWithProjectBinding,
3739
3744
  runWithProjectRoot: () => runWithProjectRoot,
3740
3745
  sameContract: () => sameContract,
@@ -3756,6 +3761,8 @@ __export(src_exports, {
3756
3761
  stepFieldsFor: () => stepFieldsFor,
3757
3762
  stepGraph: () => stepGraph,
3758
3763
  syncContextFiles: () => syncContextFiles,
3764
+ technologyName: () => technologyName,
3765
+ technologyTokens: () => technologyTokens,
3759
3766
  toMarkdown: () => toMarkdown,
3760
3767
  typeBindingOf: () => typeBindingOf,
3761
3768
  typeGenericParameters: () => typeGenericParameters,
@@ -3812,6 +3819,15 @@ function readFileOrNull(filePath) {
3812
3819
  function pathExists(targetPath) {
3813
3820
  return fs.existsSync(targetPath);
3814
3821
  }
3822
+ function isOutsideRoot(root, target) {
3823
+ const rel2 = path.relative(path.resolve(root), path.resolve(target));
3824
+ return rel2 === ".." || rel2.startsWith(`..${path.sep}`) || path.isAbsolute(rel2);
3825
+ }
3826
+ function backupBeside(file, at = /* @__PURE__ */ new Date()) {
3827
+ const backup = `${file}.wairon-backup-${at.toISOString().replace(/[:.]/g, "-")}`;
3828
+ fs.copyFileSync(file, backup);
3829
+ return backup;
3830
+ }
3815
3831
  function listFiles(dirPath, ext) {
3816
3832
  if (!fs.existsSync(dirPath)) return [];
3817
3833
  return fs.readdirSync(dirPath).filter((f) => f.endsWith(ext)).map((f) => path.join(dirPath, f));
@@ -3844,18 +3860,33 @@ function runWithProjectBinding(dir, reach2, fn) {
3844
3860
  root: path.resolve(dir),
3845
3861
  topRoot: path.resolve(reach2.topRoot),
3846
3862
  parentReach: reach2.parentReach,
3847
- ...reach2.narrowed ? { narrowed: true } : {}
3863
+ ...reach2.narrowed ? { narrowed: true } : {},
3864
+ ...reach2.unwritableRoots ? { unwritableRoots: reach2.unwritableRoots.map((r) => path.resolve(r)) } : {},
3865
+ ...reach2.hostedLookup ? { hostedLookup: reach2.hostedLookup } : {}
3848
3866
  },
3849
3867
  fn
3850
3868
  );
3851
3869
  }
3870
+ function getHostedLookup() {
3871
+ return requestRootStore.getStore()?.hostedLookup ?? null;
3872
+ }
3873
+ function runWithHostedLookup(lookup2, fn) {
3874
+ const scope = requestRootStore.getStore();
3875
+ if (!scope?.hostedLookup) return fn();
3876
+ return requestRootStore.run({ ...scope, hostedLookup: lookup2 }, fn);
3877
+ }
3852
3878
  function getRequestProjectRoot() {
3853
3879
  return requestRootStore.getStore()?.root ?? null;
3854
3880
  }
3855
3881
  function getRequestParentReach() {
3856
3882
  const scope = requestRootStore.getStore();
3857
3883
  if (!scope || scope.parentReach === void 0) return null;
3858
- return { topRoot: scope.topRoot, parentReach: scope.parentReach, ...scope.narrowed ? { narrowed: true } : {} };
3884
+ return {
3885
+ topRoot: scope.topRoot,
3886
+ parentReach: scope.parentReach,
3887
+ ...scope.narrowed ? { narrowed: true } : {},
3888
+ ...scope.unwritableRoots ? { unwritableRoots: scope.unwritableRoots } : {}
3889
+ };
3859
3890
  }
3860
3891
  function setProjectRoot(dir) {
3861
3892
  projectRootOverride = dir === null ? null : path.resolve(dir);
@@ -4571,7 +4602,8 @@ var ProjectProfileSelectionSchema = import_zod3.z.object({
4571
4602
  selectedAt: import_zod3.z.string()
4572
4603
  });
4573
4604
  var ExternalSourceSchema = import_zod3.z.object({
4574
- path: import_zod3.z.string()
4605
+ path: import_zod3.z.string().optional(),
4606
+ hosted: import_zod3.z.string().optional()
4575
4607
  });
4576
4608
  var ExternalDeclarationSchema = import_zod3.z.object({
4577
4609
  /** The producer's project id; defaults to the alias. Plain string: a malformed id is REPORTED, never unreadable. */
@@ -4942,6 +4974,12 @@ function projectIdentity(config, lockedProjectId) {
4942
4974
  };
4943
4975
  }
4944
4976
  var EXTERNAL_ALIAS_RE = /^[a-z0-9_-]+$/;
4977
+ function sourceProblem(source) {
4978
+ if (source === void 0) return void 0;
4979
+ const named2 = (source.path !== void 0 ? 1 : 0) + (source.hosted !== void 0 ? 1 : 0);
4980
+ if (named2 === 1) return void 0;
4981
+ return named2 === 0 ? "its `source` names neither `path` nor `hosted` \u2014 give exactly one" : "its `source` names both `path` and `hosted` \u2014 give exactly one";
4982
+ }
4945
4983
  var USE_ENTRY_RE = /^(\*|[a-z0-9_-]+)$/;
4946
4984
  function readUse(alias, use) {
4947
4985
  const list = Array.isArray(use) ? use.map((u) => String(u)) : [];
@@ -4956,11 +4994,12 @@ function declaredExternals(config) {
4956
4994
  return Object.entries(config.externals ?? {}).map(([alias, declaration2]) => {
4957
4995
  const project = declaration2?.project ?? alias;
4958
4996
  const imports = readUse(alias, declaration2?.use);
4959
- const problem = !EXTERNAL_ALIAS_RE.test(alias) ? PROJECT_ID_RE.test(alias) ? `the alias "${alias}" is not a reference name ([a-z0-9-_]+) \u2014 a dotted producer id needs an explicit alias, e.g. \`${alias.replace(/\./g, "-")}: { project: ${alias} }\`` : `the alias "${alias}" breaks [a-z0-9-_]+` : config.members?.[alias] !== void 0 ? `the alias "${alias}" is also declared under \`members\` \u2014 one alias names one project` : !PROJECT_ID_RE.test(project) ? `the producer id "${project}" breaks the project-id grammar` : imports.problem;
4997
+ const problem = !EXTERNAL_ALIAS_RE.test(alias) ? PROJECT_ID_RE.test(alias) ? `the alias "${alias}" is not a reference name ([a-z0-9-_]+) \u2014 a dotted producer id needs an explicit alias, e.g. \`${alias.replace(/\./g, "-")}: { project: ${alias} }\`` : `the alias "${alias}" breaks [a-z0-9-_]+` : config.members?.[alias] !== void 0 ? `the alias "${alias}" is also declared under \`members\` \u2014 one alias names one project` : !PROJECT_ID_RE.test(project) ? `the producer id "${project}" breaks the project-id grammar` : sourceProblem(declaration2?.source) ?? imports.problem;
4960
4998
  return {
4961
4999
  alias,
4962
5000
  project,
4963
5001
  ...declaration2?.source?.path !== void 0 ? { sourcePath: declaration2.source.path } : {},
5002
+ ...declaration2?.source?.hosted !== void 0 ? { sourceHosted: declaration2.source.hosted } : {},
4964
5003
  ...problem ? { problem } : {},
4965
5004
  use: imports.use
4966
5005
  };
@@ -7003,6 +7042,19 @@ function parseDeclaredCall(ref) {
7003
7042
  if (at <= 0 || at === ref.length - 1) return null;
7004
7043
  return { compId: ref.slice(0, at), methodName: ref.slice(at + 1) };
7005
7044
  }
7045
+ var TechnologySchema = import_zod8.z.union([
7046
+ import_zod8.z.string().min(1),
7047
+ import_zod8.z.object({
7048
+ name: import_zod8.z.string().min(1),
7049
+ matches: import_zod8.z.array(import_zod8.z.string().min(1)).min(1)
7050
+ }).strict()
7051
+ ]);
7052
+ function technologyName(tech) {
7053
+ return typeof tech === "string" ? tech : tech.name;
7054
+ }
7055
+ function technologyTokens(tech) {
7056
+ return typeof tech === "string" ? [tech] : [...tech.matches];
7057
+ }
7006
7058
  var ImplementationSpecSchema = import_zod8.z.object({
7007
7059
  id: SpecIdSchema,
7008
7060
  name: import_zod8.z.string(),
@@ -7027,9 +7079,10 @@ var ImplementationSpecSchema = import_zod8.z.object({
7027
7079
  * component's ownership tree the technology's home: references anywhere
7028
7080
  * outside it are flagged (TECH_LEAKAGE), contract identifiers must stay
7029
7081
  * intent-language (VENDOR_NAME_IN_CONTRACT), and only data-layer
7030
- * stereotypes should bind tech directly (TECH_ON_LOGIC_COMPONENT).
7082
+ * stereotypes should bind tech directly (TECH_ON_LOGIC_COMPONENT). An entry
7083
+ * is a bare name or `{ name, matches }` (see TechnologySchema).
7031
7084
  */
7032
- technologies: import_zod8.z.array(import_zod8.z.string()).optional(),
7085
+ technologies: import_zod8.z.array(TechnologySchema).optional(),
7033
7086
  /**
7034
7087
  * The parameter names THIS realization takes BEFORE the ones its contract
7035
7088
  * declares — a config object, a data root, the transport handles a portal is
@@ -7556,10 +7609,10 @@ function stepGraph(method) {
7556
7609
  const s = byNum.get(n);
7557
7610
  if (s.type !== "parallel" || s.endStep === void 0 || !s.branches?.length) continue;
7558
7611
  const entries = s.branches.map((b) => b.step).sort((a, b) => a - b);
7559
- const join27 = fallNext(s.endStep);
7612
+ const join28 = fallNext(s.endStep);
7560
7613
  for (let i = 0; i < entries.length; i++) {
7561
7614
  const armEnd = i + 1 < entries.length ? prevOf(entries[i + 1]) : s.endStep;
7562
- if (armEnd !== void 0 && armEnd >= entries[i]) armEndJoin.set(armEnd, join27);
7615
+ if (armEnd !== void 0 && armEnd >= entries[i]) armEndJoin.set(armEnd, join28);
7563
7616
  }
7564
7617
  }
7565
7618
  const successorsOf = (n) => {
@@ -13418,10 +13471,11 @@ function isWithinDir(dir, target) {
13418
13471
  const rel2 = path10.relative(path10.resolve(dir), path10.resolve(target));
13419
13472
  return rel2 === "" || !rel2.startsWith("..") && !path10.isAbsolute(rel2);
13420
13473
  }
13421
- function producerUnreadable(directory) {
13474
+ function producerUnreadable(external) {
13475
+ const directory = external.directory;
13422
13476
  if (!directory) return "the producer has no root directory";
13423
13477
  const reach2 = getRequestParentReach();
13424
- if (reach2) {
13478
+ if (reach2 && external.sourceKind !== "hosted") {
13425
13479
  const bound2 = getProjectRoot();
13426
13480
  const ceiling = reach2.parentReach ? reach2.topRoot ?? bound2 : bound2;
13427
13481
  if (!isWithinDir(ceiling, directory)) return OUT_OF_REACH;
@@ -13450,7 +13504,7 @@ function usedDigests(usage, snapshot) {
13450
13504
  function pinBinding(binding, lock) {
13451
13505
  const { external } = binding;
13452
13506
  const unexported = binding.usage?.unexported ?? [];
13453
- const unreadable = producerUnreadable(external.directory);
13507
+ const unreadable = producerUnreadable(external);
13454
13508
  let snapshot;
13455
13509
  try {
13456
13510
  if (unreadable) throw new Error(unreadable);
@@ -13553,7 +13607,7 @@ function externalStatus(binding, lock) {
13553
13607
  const entry = lock?.externals[external.alias];
13554
13608
  const pinned = entry !== void 0 && externalsRepository.readSnapshot(external.alias) !== null;
13555
13609
  const base = { alias: external.alias, project: external.project, sourceKind: external.sourceKind, pinned };
13556
- const unreadable = external.sourceKind === "unresolved" ? `the external does not resolve: ${external.problem}` : producerUnreadable(external.directory);
13610
+ const unreadable = external.sourceKind === "unresolved" ? hostedOnly(external) ? external.problem : `the external does not resolve: ${external.problem}` : producerUnreadable(external);
13557
13611
  let live = null;
13558
13612
  let reason = unreadable;
13559
13613
  if (!reason) {
@@ -13564,7 +13618,7 @@ function externalStatus(binding, lock) {
13564
13618
  }
13565
13619
  }
13566
13620
  if (!live) {
13567
- const outOfReach = reason === OUT_OF_REACH || !binding.reachable && external.sourceKind !== "family";
13621
+ const outOfReach = reason === OUT_OF_REACH || hostedOutOfReach(external) || !binding.reachable && external.sourceKind !== "family";
13568
13622
  return {
13569
13623
  ...base,
13570
13624
  reachable: false,
@@ -13575,7 +13629,7 @@ function externalStatus(binding, lock) {
13575
13629
  };
13576
13630
  }
13577
13631
  const drifted = entry !== void 0 ? contentDigest(live) !== entry.digest : void 0;
13578
- if (external.sourceKind === "path") {
13632
+ if (external.sourceKind === "path" || external.sourceKind === "hosted") {
13579
13633
  return { ...base, reachable: true, stale: false, ...drifted !== void 0 ? { drifted } : {}, uses: [{ state: "unavailable", code: CHECK_UNAVAILABLE, detail: OUTSIDE_FAMILY }], detail: OUTSIDE_FAMILY };
13580
13634
  }
13581
13635
  const uses = compareUses(entry, binding.usage, live);
@@ -13587,6 +13641,12 @@ function externalStatus(binding, lock) {
13587
13641
  uses
13588
13642
  };
13589
13643
  }
13644
+ function hostedOnly(external) {
13645
+ return external.hosted !== void 0 && external.sourceKind === "unresolved" && getHostedLookup() === null;
13646
+ }
13647
+ function hostedOutOfReach(external) {
13648
+ return external.hosted !== void 0 && external.sourceKind === "unresolved" && getHostedLookup() !== null;
13649
+ }
13590
13650
  function getExternalsStatus() {
13591
13651
  const bindings = resolveDeclared();
13592
13652
  const lock = externalsRepository.readLock();
@@ -14250,7 +14310,7 @@ function defaultTargetConfig(type) {
14250
14310
  enabled: true
14251
14311
  };
14252
14312
  }
14253
- var WAIRON_VERSION = "5.1.1-dev.87";
14313
+ var WAIRON_VERSION = "5.1.1-dev.89";
14254
14314
  var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
14255
14315
  var ARCHITECT_AGENT_ID = "agent-architect";
14256
14316
  var ARCHITECT_TEMPLATE_ID = "architect";
@@ -15047,13 +15107,13 @@ function buildCodeIndex(model) {
15047
15107
  const to = resolveImport(scope, specifier, paths);
15048
15108
  return to ? republishedFrom(to) : NO_ORIGIN;
15049
15109
  };
15050
- const scopeOf = (site, from) => {
15110
+ const scopeOf2 = (site, from) => {
15051
15111
  const path46 = pathKey(site.from ?? from);
15052
15112
  const f = facts.get(path46);
15053
15113
  return f?.status === "analyzed" && f.analysisGrade === "exact" ? { path: path46, facts: f } : void 0;
15054
15114
  };
15055
15115
  const originOf2 = (site, from) => {
15056
- const resolved = scopeOf(site, from);
15116
+ const resolved = scopeOf2(site, from);
15057
15117
  if (!resolved) return NO_ORIGIN;
15058
15118
  const { path: scope, facts: f } = resolved;
15059
15119
  const landing = (specifier) => landingFrom(scope, specifier);
@@ -15075,7 +15135,7 @@ function buildCodeIndex(model) {
15075
15135
  const possibleOriginsOf = (site, from) => {
15076
15136
  const proven = originOf2(site, from);
15077
15137
  if (!site.field && !site.constructed && !site.via) return proven;
15078
- const resolved = scopeOf(site, from);
15138
+ const resolved = scopeOf2(site, from);
15079
15139
  if (!resolved) return proven;
15080
15140
  const { path: scope, facts: f } = resolved;
15081
15141
  const out = new Set(proven);
@@ -21381,7 +21441,7 @@ var technologyBindingRule = {
21381
21441
  ctx.addIssue(
21382
21442
  "warning",
21383
21443
  "TECH_ON_LOGIC_COMPONENT",
21384
- `Implementation "${impl.id}" binds technology (${impl.technologies.join(", ")}) on component "${comp.id}" (${comp.componentType}). Technology belongs behind a data-layer seam \u2014 extract an Adapter (or Store/Registry/Index) behind an intent interface and let this component depend on that.`,
21444
+ `Implementation "${impl.id}" binds technology (${impl.technologies.map(technologyName).join(", ")}) on component "${comp.id}" (${comp.componentType}). Technology belongs behind a data-layer seam \u2014 extract an Adapter (or Store/Registry/Index) behind an intent interface and let this component depend on that.`,
21385
21445
  impl.id,
21386
21446
  ctx.isImplementationDraft(impl)
21387
21447
  );
@@ -21406,6 +21466,16 @@ function makeMatcher(tech) {
21406
21466
  return false;
21407
21467
  };
21408
21468
  }
21469
+ function newHome(label7) {
21470
+ const home = {
21471
+ label: label7,
21472
+ matchers: [],
21473
+ match: (text3) => home.matchers.some((m) => m(text3)),
21474
+ ownerComponents: /* @__PURE__ */ new Set(),
21475
+ scope: /* @__PURE__ */ new Set()
21476
+ };
21477
+ return home;
21478
+ }
21409
21479
  function contractIdentifiers(m) {
21410
21480
  const parts = [m.name, m.signature, m.returns];
21411
21481
  for (const p of m.params ?? []) parts.push(p.name, p.type);
@@ -21415,7 +21485,7 @@ function contractIdentifiers(m) {
21415
21485
  var technologyRule = {
21416
21486
  name: "technology-boundaries",
21417
21487
  judges: "design",
21418
- description: "Technology stays behind its owning boundary: an L4 that declares `technologies` (e.g. [mysql]) makes its component's ownership tree the technology's home. References outside that tree are leakage, and L3 contract identifiers must stay intent-language \u2014 the contract is the swap seam, so the vendor name is wrong even on the owning component's own interface.",
21488
+ description: "Technology stays behind its owning boundary: an L4 that declares `technologies` (e.g. [mysql]) makes its component's ownership tree the technology's home. References outside that tree are leakage, and L3 contract identifiers must stay intent-language \u2014 the contract is the swap seam, so the vendor name is wrong even on the owning component's own interface. A technology is matched by its name, or \u2014 when it declares `matches` because its name is also an ordinary word of the tree (a package named after the file format it reads) \u2014 by those tokens alone.",
21419
21489
  codes: [
21420
21490
  { code: "TECH_LEAKAGE", defaultSeverity: "warning", summary: "Technology referenced outside its owning boundary" },
21421
21491
  { code: "VENDOR_NAME_IN_CONTRACT", defaultSeverity: "warning", summary: "Technology name in L3 contract identifiers" }
@@ -21452,10 +21522,12 @@ var technologyRule = {
21452
21522
  const intf = ctx.interfaceMap.get(impl.contract);
21453
21523
  const comp = intf ? ctx.componentMap.get(intf.component) : void 0;
21454
21524
  if (!comp) continue;
21455
- for (const tech of impl.technologies) declarations.push({ comp, tech });
21525
+ for (const tech of impl.technologies) {
21526
+ declarations.push({ comp, name: technologyName(tech), tokens: technologyTokens(tech) });
21527
+ }
21456
21528
  }
21457
21529
  const scopes = /* @__PURE__ */ new Map();
21458
- const scopeOf = (comp) => {
21530
+ const scopeOf2 = (comp) => {
21459
21531
  const cached = scopes.get(comp.id);
21460
21532
  if (cached) return cached;
21461
21533
  const compSet = ownsClosure(ownershipRoot(comp.id));
@@ -21476,13 +21548,14 @@ var technologyRule = {
21476
21548
  return scope;
21477
21549
  };
21478
21550
  const homes = /* @__PURE__ */ new Map();
21479
- for (const { comp, tech } of declarations) {
21480
- const match2 = makeMatcher(tech);
21481
- if (!match2) continue;
21482
- const key = tech.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).join("");
21483
- const home = homes.get(key) ?? { label: tech, match: match2, ownerComponents: /* @__PURE__ */ new Set(), scope: /* @__PURE__ */ new Set() };
21551
+ for (const { comp, name, tokens } of declarations) {
21552
+ const matchers = tokens.map(makeMatcher).filter((m) => m !== null);
21553
+ if (!matchers.length) continue;
21554
+ const key = name.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).join("");
21555
+ const home = homes.get(key) ?? newHome(name);
21556
+ home.matchers.push(...matchers);
21484
21557
  home.ownerComponents.add(comp.id);
21485
- for (const id of scopeOf(comp)) home.scope.add(id);
21558
+ for (const id of scopeOf2(comp)) home.scope.add(id);
21486
21559
  homes.set(key, home);
21487
21560
  }
21488
21561
  if (homes.size === 0) return;
@@ -24904,8 +24977,8 @@ function generateSequenceDiagram(componentId, methodName, options) {
24904
24977
  const steps = [...methodImpl.narrative].sort((a, b) => a.stepNumber - b.stepNumber);
24905
24978
  for (const step of steps) {
24906
24979
  for (const par of parallelArms) {
24907
- const sep12 = par.sepByStep.get(step.stepNumber);
24908
- if (sep12 !== void 0) lines.push(` and ${escapeLabel(sep12)}`);
24980
+ const sep13 = par.sepByStep.get(step.stepNumber);
24981
+ if (sep13 !== void 0) lines.push(` and ${escapeLabel(sep13)}`);
24909
24982
  }
24910
24983
  switch (step.type) {
24911
24984
  case "local":
@@ -25167,6 +25240,7 @@ function bindExternals(node, root, nodes) {
25167
25240
  return declaredExternals(root.config).map((decl) => {
25168
25241
  const unresolved = (problem) => ({ alias: decl.alias, project: decl.project, sourceKind: "unresolved", audience: "instance", problem });
25169
25242
  if (decl.problem) return unresolved(decl.problem);
25243
+ if (decl.sourceHosted !== void 0) return { alias: decl.alias, project: decl.project, sourceKind: "hosted", hosted: decl.sourceHosted, audience: "instance" };
25170
25244
  if (decl.sourcePath !== void 0) {
25171
25245
  const directory = path18.resolve(node.directory, decl.sourcePath);
25172
25246
  const found = nodes.find((n) => dirKey(n.directory) === dirKey(directory));
@@ -25954,7 +26028,7 @@ function mapSpecReferences(kind, spec, map) {
25954
26028
  const s = spec;
25955
26029
  return {
25956
26030
  ...s,
25957
- publicInterfaces: s.publicInterfaces.map((p) => ({
26031
+ publicInterfaces: (s.publicInterfaces ?? []).map((p) => ({
25958
26032
  ...p,
25959
26033
  ...p.component !== void 0 ? { component: map("publicInterfaces", p.component) } : {},
25960
26034
  ...p.interface !== void 0 ? { interface: map("publicInterfaces", p.interface) } : {},
@@ -25970,8 +26044,8 @@ function mapSpecReferences(kind, spec, map) {
25970
26044
  return {
25971
26045
  ...c,
25972
26046
  subsystem: map("subsystem", c.subsystem),
25973
- owns: c.owns.map((o) => map("owns", o)),
25974
- dependsOn: c.dependsOn.map((d) => map("dependsOn", d)),
26047
+ owns: (c.owns ?? []).map((o) => map("owns", o)),
26048
+ dependsOn: (c.dependsOn ?? []).map((d) => map("dependsOn", d)),
25975
26049
  ...c.dispatch ? { dispatch: c.dispatch.map((b) => ({ ...b, component: map("dispatch", b.component) })) } : {},
25976
26050
  ...c.mounts ? { mounts: c.mounts.map((m) => ({ ...m, portal: map("mounts", m.portal) })) } : {}
25977
26051
  };
@@ -25985,9 +26059,9 @@ function mapSpecReferences(kind, spec, map) {
25985
26059
  return {
25986
26060
  ...impl,
25987
26061
  contract: map("contract", impl.contract),
25988
- methods: impl.methods.map((m) => ({
26062
+ methods: (impl.methods ?? []).map((m) => ({
25989
26063
  ...m,
25990
- narrative: m.narrative.map((step) => step.targetComponent !== void 0 ? { ...step, targetComponent: map("narrative", step.targetComponent) } : step)
26064
+ narrative: (m.narrative ?? []).map((step) => step.targetComponent !== void 0 ? { ...step, targetComponent: map("narrative", step.targetComponent) } : step)
25991
26065
  }))
25992
26066
  };
25993
26067
  }
@@ -27835,10 +27909,10 @@ var SpecWorkspace = class {
27835
27909
  return this.relativizeSpec("interface", spec);
27836
27910
  }
27837
27911
  prepareImplementationForWrite(spec, carry = true) {
27838
- const relative18 = this.relativizeSpec("implementation", spec, carry);
27912
+ const relative19 = this.relativizeSpec("implementation", spec, carry);
27839
27913
  const member = this.getSubprojectPrefix(spec.id);
27840
27914
  const memberRoot = member ? this.resolveSubprojectForNamespace(member) : null;
27841
- return memberRoot ? childRelativeFilePaths(relative18, this.rootDir, memberRoot) : relative18;
27915
+ return memberRoot ? childRelativeFilePaths(relative19, this.rootDir, memberRoot) : relative19;
27842
27916
  }
27843
27917
  prepareTypeForWrite(spec) {
27844
27918
  return this.relativizeSpec("type", spec);
@@ -28677,7 +28751,10 @@ var SpecWorkspace = class {
28677
28751
  * second delete written as 7 addresses what used to be step 8. That is why
28678
28752
  * a delete may restate the step's `label` or `description` as a guard, and
28679
28753
  * why labels are the reliable way to name a jump target. Inserts and
28680
- * deletes relocate every jump field in the same narrative around them; a
28754
+ * deletes relocate every STORED jump field in the same narrative around
28755
+ * them; a jump the delta itself writes is already in the numbering the
28756
+ * whole delta leaves behind, so none of its later steps moves it. A new
28757
+ * implementation method with no narrative is intent-level ([]); a
28681
28758
  * step whose `type` changes is REBUILT for its new type, keeping its
28682
28759
  * description and label.
28683
28760
  * 5. UNSET the fields the delta names, at the level each `unset` sits in.
@@ -28735,16 +28812,19 @@ var SpecWorkspace = class {
28735
28812
  }
28736
28813
  const JUMP_FIELDS = ["onTrueStep", "onFalseStep", "defaultStep", "endStep", "finallyStep", "toStep"];
28737
28814
  const JUMP_LIST_FIELDS = ["cases", "catches", "branches"];
28815
+ const isPinned = (step, field) => step?.[DELTA_JUMP_PINS] instanceof Set && step[DELTA_JUMP_PINS].has(field);
28816
+ const isPinnedEntry = (entry) => entry?.[DELTA_JUMP_PINS] === true;
28738
28817
  const relocateJumps = (step, shiftFrom, deltaN, captureInsertTarget = false) => {
28739
28818
  const hit = (v, isRegionTail = false) => deltaN > 0 ? captureInsertTarget && !isRegionTail ? v > shiftFrom : v >= shiftFrom : v > shiftFrom;
28740
28819
  const out = { ...step };
28741
28820
  for (const f of JUMP_FIELDS) {
28821
+ if (isPinned(step, f)) continue;
28742
28822
  if (typeof out[f] === "number" && hit(out[f], f === "endStep")) out[f] = out[f] + deltaN;
28743
28823
  }
28744
28824
  for (const lf of JUMP_LIST_FIELDS) {
28745
28825
  if (Array.isArray(out[lf])) {
28746
28826
  out[lf] = out[lf].map(
28747
- (c) => typeof c?.step === "number" && hit(c.step) ? { ...c, step: c.step + deltaN } : c
28827
+ (c) => !isPinnedEntry(c) && typeof c?.step === "number" && hit(c.step) ? { ...c, step: c.step + deltaN } : c
28748
28828
  );
28749
28829
  }
28750
28830
  }
@@ -28752,14 +28832,39 @@ var SpecWorkspace = class {
28752
28832
  };
28753
28833
  const jumpRefsTo = (step, target) => {
28754
28834
  const refs = [];
28755
- for (const f of JUMP_FIELDS) if (step[f] === target) refs.push(f);
28835
+ for (const f of JUMP_FIELDS) if (step[f] === target && !isPinned(step, f)) refs.push(f);
28756
28836
  for (const lf of JUMP_LIST_FIELDS) {
28757
28837
  (Array.isArray(step[lf]) ? step[lf] : []).forEach((c, i) => {
28758
- if (c?.step === target) refs.push(`${lf}[${i}].step`);
28838
+ if (c?.step === target && !isPinnedEntry(c)) refs.push(`${lf}[${i}].step`);
28759
28839
  });
28760
28840
  }
28761
28841
  return refs;
28762
28842
  };
28843
+ const pinDeltaJumps = (deltaStep, carried) => {
28844
+ const pins = new Set(carried ?? []);
28845
+ for (const f of JUMP_FIELDS) if (typeof deltaStep[f] === "number") pins.add(f);
28846
+ const out = { ...deltaStep };
28847
+ for (const lf of JUMP_LIST_FIELDS) {
28848
+ if (!Array.isArray(out[lf])) continue;
28849
+ out[lf] = out[lf].map((c) => c && typeof c === "object" && typeof c.step === "number" ? { ...c, [DELTA_JUMP_PINS]: true } : c);
28850
+ }
28851
+ out[DELTA_JUMP_PINS] = pins;
28852
+ return out;
28853
+ };
28854
+ const unpinned = (step) => {
28855
+ const out = { ...step };
28856
+ delete out[DELTA_JUMP_PINS];
28857
+ for (const lf of JUMP_LIST_FIELDS) {
28858
+ if (!Array.isArray(out[lf])) continue;
28859
+ out[lf] = out[lf].map((c) => {
28860
+ if (!c || typeof c !== "object" || !(DELTA_JUMP_PINS in c)) return c;
28861
+ const entry = { ...c };
28862
+ delete entry[DELTA_JUMP_PINS];
28863
+ return entry;
28864
+ });
28865
+ }
28866
+ return out;
28867
+ };
28763
28868
  const normalizeIdentity = (k) => String(k ?? "").toLowerCase().replace(/[_\-\s]/g, "");
28764
28869
  const unsetFieldsOf = (item) => Array.isArray(item?.unset) ? item.unset.filter((f) => typeof f === "string") : [];
28765
28870
  const withUnset = (merged, deltaItem) => {
@@ -28939,21 +29044,24 @@ var SpecWorkspace = class {
28939
29044
  capture
28940
29045
  ));
28941
29046
  const { action, remove: remove2, captureJumps, ...cleanStep } = deltaStep;
28942
- steps.push(withUnset(cleanStep, deltaStep));
29047
+ steps.push(withUnset(pinDeltaJumps(cleanStep), deltaStep));
28943
29048
  } else {
28944
29049
  const idx = steps.findIndex((s) => s.stepNumber === stepNum);
28945
29050
  if (idx !== -1) {
28946
- const { action, remove: remove2, ...cleanStep } = deltaStep;
29051
+ const { action, remove: remove2, ...rawStep } = deltaStep;
28947
29052
  const stored = steps[idx];
29053
+ const cleanStep = pinDeltaJumps(rawStep, stored[DELTA_JUMP_PINS]);
28948
29054
  const merged = withUnset(retargetByLabel(mergeElement(stored, cleanStep), cleanStep), deltaStep);
28949
- steps[idx] = rebuildOnRetype(stored, cleanStep, merged, methodName);
29055
+ const rebuilt = rebuildOnRetype(stored, rawStep, merged, methodName);
29056
+ rebuilt[DELTA_JUMP_PINS] = cleanStep[DELTA_JUMP_PINS];
29057
+ steps[idx] = rebuilt;
28950
29058
  } else {
28951
29059
  const { action, remove: remove2, ...cleanStep } = deltaStep;
28952
- steps.push(withUnset(cleanStep, deltaStep));
29060
+ steps.push(withUnset(pinDeltaJumps(cleanStep), deltaStep));
28953
29061
  }
28954
29062
  }
28955
29063
  }
28956
- return steps.sort((a, b) => a.stepNumber - b.stepNumber);
29064
+ return steps.map(unpinned).sort((a, b) => a.stepNumber - b.stepNumber);
28957
29065
  };
28958
29066
  const isPlainObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
28959
29067
  const mergeExt = (existing, delta2) => {
@@ -28976,6 +29084,11 @@ var SpecWorkspace = class {
28976
29084
  } else {
28977
29085
  const existingMethod = merged[idx];
28978
29086
  let narrative = existingMethod.narrative ? [...existingMethod.narrative] : [];
29087
+ if (deltaMethod.narrative !== void 0 && deltaMethod.narrative !== null && !Array.isArray(deltaMethod.narrative)) {
29088
+ throw new Error(
29089
+ `Refusing to update implementation method "${String(deltaMethod.name)}": its "narrative" must be a list of step deltas, got ${JSON.stringify(deltaMethod.narrative)}.`
29090
+ );
29091
+ }
28979
29092
  if (Array.isArray(deltaMethod.narrative)) {
28980
29093
  narrative = deltaMethod.narrative.length === 0 ? [] : mergeNarrative(String(deltaMethod.name), narrative, deltaMethod.narrative);
28981
29094
  }
@@ -28988,11 +29101,21 @@ var SpecWorkspace = class {
28988
29101
  }
28989
29102
  } else {
28990
29103
  assertUnmatchedIsAnAddition(deltaMethod, deltaMethod.name, merged.map((m) => m.name), "method");
28991
- merged.push(withUnset(deltaMethod, deltaMethod));
29104
+ merged.push(withUnset(newMethodOf(deltaMethod), deltaMethod));
28992
29105
  }
28993
29106
  }
28994
29107
  return merged;
28995
29108
  };
29109
+ const newMethodOf = (deltaMethod) => {
29110
+ if (kind !== "implementation") return deltaMethod;
29111
+ if (deltaMethod.narrative === void 0 || deltaMethod.narrative === null) return { ...deltaMethod, narrative: [] };
29112
+ if (!Array.isArray(deltaMethod.narrative)) {
29113
+ throw new Error(
29114
+ `Refusing to add implementation method "${String(deltaMethod.name)}": its "narrative" must be a list of steps, got ${JSON.stringify(deltaMethod.narrative)}. Leave it out (or pass []) for an intent-level method.`
29115
+ );
29116
+ }
29117
+ return deltaMethod;
29118
+ };
28996
29119
  const mergeNamedArray = (existing, delta2) => {
28997
29120
  const merged = [...existing];
28998
29121
  for (const deltaItem of delta2) {
@@ -29170,6 +29293,12 @@ var SpecWorkspace = class {
29170
29293
  const qualifiedDelta = qualifyDeltaRefs(mergeableDelta);
29171
29294
  const mergedResult = mergeDelta(result, qualifiedDelta);
29172
29295
  for (const field of unsetFields) delete mergedResult[field];
29296
+ const withDefaults = deltaSchema.safeParse(mergedResult);
29297
+ if (withDefaults.success) {
29298
+ for (const [key, value] of Object.entries(withDefaults.data)) {
29299
+ if (!(key in mergedResult)) mergedResult[key] = value;
29300
+ }
29301
+ }
29173
29302
  if (kind === "implementation" && Array.isArray(mergedResult.methods)) {
29174
29303
  const labelErrors = [];
29175
29304
  for (const m of mergedResult.methods) {
@@ -29690,6 +29819,7 @@ var PROSE_FIELDS = /* @__PURE__ */ new Set([
29690
29819
  "note",
29691
29820
  "caller"
29692
29821
  ]);
29822
+ var DELTA_JUMP_PINS = /* @__PURE__ */ Symbol("jumps written by this delta");
29693
29823
  function cloneSpec(spec) {
29694
29824
  return JSON.parse(JSON.stringify(spec));
29695
29825
  }
@@ -30169,6 +30299,17 @@ function unregisterDomain(id) {
30169
30299
 
30170
30300
  // src/core/external-producers.ts
30171
30301
  var path21 = __toESM(require("path"));
30302
+ function hostedOnlyProducer(id) {
30303
+ return `hosted-only producer \`${id}\`: available only through the hosted server`;
30304
+ }
30305
+ function resolveHosted(external) {
30306
+ if (external.sourceKind !== "hosted" || external.hosted === void 0) return external;
30307
+ const lookup2 = getHostedLookup();
30308
+ const unresolved = (problem) => ({ ...external, sourceKind: "unresolved", problem });
30309
+ if (!lookup2) return unresolved(hostedOnlyProducer(external.hosted));
30310
+ const root = lookup2(external.hosted);
30311
+ return root === null ? unresolved(`the hosted producer \`${external.hosted}\` is unknown or outside this request's reach`) : { ...external, directory: path21.resolve(root) };
30312
+ }
30172
30313
  function dirKey2(dir) {
30173
30314
  const resolved = path21.resolve(dir);
30174
30315
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
@@ -30194,11 +30335,14 @@ function resolveDeclared() {
30194
30335
  const family = graph();
30195
30336
  const node = consumerNode(family, bound2);
30196
30337
  if (!node) return null;
30197
- return node.externals.map((external) => ({
30198
- external,
30199
- ...external.sourceKind === "family" && external.producer !== void 0 ? { usage: exportUsage(node.namespace, external.producer) } : {},
30200
- reachable: whole
30201
- }));
30338
+ return node.externals.map((declared) => {
30339
+ const external = resolveHosted(declared);
30340
+ return {
30341
+ external,
30342
+ ...external.sourceKind === "family" && external.producer !== void 0 ? { usage: exportUsage(node.namespace, external.producer) } : {},
30343
+ reachable: whole
30344
+ };
30345
+ });
30202
30346
  });
30203
30347
  return answer(top) ?? answer(bound2) ?? [];
30204
30348
  }
@@ -30881,7 +31025,7 @@ function carryDeclarations(scan, result) {
30881
31025
  const bound2 = projectConfigRepository.load() ?? {};
30882
31026
  for (const [alias, decl] of Object.entries(scan.config?.externals ?? {})) {
30883
31027
  if (carriedExternal(scan, alias, decl) === "parent") continue;
30884
- const source = decl.source && !path22.isAbsolute(decl.source.path) ? { path: toPosixPath(path22.relative(root, path22.resolve(scan.memberDir, decl.source.path))) } : decl.source;
31028
+ const source = decl.source?.path !== void 0 && !path22.isAbsolute(decl.source.path) ? { path: toPosixPath(path22.relative(root, path22.resolve(scan.memberDir, decl.source.path))) } : decl.source;
30885
31029
  if (bound2.members?.[alias] === void 0) {
30886
31030
  projectConfigRepository.declareExternal(alias, {
30887
31031
  ...decl.project !== void 0 ? { project: decl.project } : {},
@@ -32224,17 +32368,17 @@ function memberHop(agentId) {
32224
32368
  const alias = agentId.slice(0, at);
32225
32369
  const member = graph().nodes.find((n) => n.parent === "" && n.mountAlias === alias);
32226
32370
  if (!member) return null;
32227
- const relative18 = path26.relative(getProjectRoot(), member.directory).split(path26.sep).join("/");
32228
- return { alias, rest: agentId.slice(at + 2), directory: member.directory, relative: relative18 };
32371
+ const relative19 = path26.relative(getProjectRoot(), member.directory).split(path26.sep).join("/");
32372
+ return { alias, rest: agentId.slice(at + 2), directory: member.directory, relative: relative19 };
32229
32373
  }
32230
- function throughMount(brief, alias, relative18) {
32231
- const under = (p) => path26.posix.join(relative18, p);
32374
+ function throughMount(brief, alias, relative19) {
32375
+ const under = (p) => path26.posix.join(relative19, p);
32232
32376
  return {
32233
32377
  ...brief,
32234
32378
  agentId: `${alias}::${brief.agentId}`,
32235
32379
  ownedPaths: brief.ownedPaths.map(under),
32236
32380
  ...brief.readPaths ? { readPaths: brief.readPaths.map(under) } : {},
32237
- root: brief.root ? under(brief.root) : relative18
32381
+ root: brief.root ? under(brief.root) : relative19
32238
32382
  };
32239
32383
  }
32240
32384
  function loadRegistry() {
@@ -32513,19 +32657,19 @@ function commitStagedTree(stagingDir, destDir, roots, backupExisting) {
32513
32657
  const backupDir = path31.join(root, BACKUP_DIR, timestampSlug());
32514
32658
  let backedUp = false;
32515
32659
  for (const rootRel of roots) {
32516
- const relative18 = rootRel === "." ? "" : rootRel;
32517
- const stagedSpecDir = path31.join(stagingDir, relative18, ".wai");
32660
+ const relative19 = rootRel === "." ? "" : rootRel;
32661
+ const stagedSpecDir = path31.join(stagingDir, relative19, ".wai");
32518
32662
  if (!fs17.existsSync(stagedSpecDir)) continue;
32519
- const liveSpecDir = liveSpecDirFor(path31.join(root, relative18));
32663
+ const liveSpecDir = liveSpecDirFor(path31.join(root, relative19));
32520
32664
  if (backupExisting && liveSpecDir && fs17.existsSync(liveSpecDir)) {
32521
- const backupTarget = path31.join(backupDir, relative18, path31.basename(liveSpecDir));
32665
+ const backupTarget = path31.join(backupDir, relative19, path31.basename(liveSpecDir));
32522
32666
  fs17.mkdirSync(path31.dirname(backupTarget), { recursive: true });
32523
32667
  fs17.renameSync(liveSpecDir, backupTarget);
32524
32668
  backedUp = true;
32525
32669
  } else if (liveSpecDir && fs17.existsSync(liveSpecDir)) {
32526
32670
  fs17.rmSync(liveSpecDir, { recursive: true, force: true });
32527
32671
  }
32528
- const target = path31.join(root, relative18, ".wai");
32672
+ const target = path31.join(root, relative19, ".wai");
32529
32673
  fs17.mkdirSync(path31.dirname(target), { recursive: true });
32530
32674
  if (fs17.existsSync(target)) fs17.rmSync(target, { recursive: true, force: true });
32531
32675
  fs17.renameSync(stagedSpecDir, target);
@@ -32583,7 +32727,7 @@ function importSpecTree(archive, options) {
32583
32727
  throw new Error("incompatible .waitree archive (format or minimum wairon version)");
32584
32728
  }
32585
32729
  const destDir = path31.resolve(options.destDir ?? getProjectRoot());
32586
- const occupied = hasSpecTree(destDir);
32730
+ const occupied2 = hasSpecTree(destDir);
32587
32731
  const authored = hasAuthoredSpecs(destDir);
32588
32732
  if (authored && options.replaceExisting !== true) {
32589
32733
  throw new Error(
@@ -32598,7 +32742,7 @@ function importSpecTree(archive, options) {
32598
32742
  discardStagingDir(stagingDir);
32599
32743
  throw err;
32600
32744
  }
32601
- const backupPath = commitStagedTree(stagingDir, destDir, extraction.roots, occupied);
32745
+ const backupPath = commitStagedTree(stagingDir, destDir, extraction.roots, occupied2);
32602
32746
  discardStagingDir(stagingDir);
32603
32747
  invalidateSpecCache();
32604
32748
  const result = {
@@ -32606,7 +32750,7 @@ function importSpecTree(archive, options) {
32606
32750
  projectName: extraction.manifest.projectName,
32607
32751
  roots: extraction.roots,
32608
32752
  fileCount: extraction.writtenPaths.length,
32609
- replaced: occupied
32753
+ replaced: occupied2
32610
32754
  };
32611
32755
  if (backupPath) result.backupPath = backupPath;
32612
32756
  if (extraction.manifest.stateId) result.stateId = extraction.manifest.stateId;
@@ -33108,7 +33252,7 @@ var CONTEXT_PATHS = {
33108
33252
  waironGuideMd: () => contextDir("wairon-guide.md")
33109
33253
  };
33110
33254
  function hasContext() {
33111
- return pathExists(CONTEXT_PATHS.projectMd());
33255
+ return pathExists(CONTEXT_PATHS.dir());
33112
33256
  }
33113
33257
  function readProjectContext() {
33114
33258
  return readFileOrNull(CONTEXT_PATHS.projectMd());
@@ -35205,6 +35349,7 @@ function pin(p, log) {
35205
35349
  }
35206
35350
 
35207
35351
  // src/migrations/membership.ts
35352
+ var fs22 = __toESM(require("fs"));
35208
35353
  var path40 = __toESM(require("path"));
35209
35354
 
35210
35355
  // src/migrations/family-files.ts
@@ -35285,39 +35430,93 @@ function within2(root, candidate) {
35285
35430
  const rel2 = nodePath.relative(root, candidate);
35286
35431
  return rel2 === "" || !rel2.startsWith("..") && !nodePath.isAbsolute(rel2);
35287
35432
  }
35433
+ function areaFiles(owner, area) {
35434
+ const out = [];
35435
+ const walk2 = (rel2) => {
35436
+ if (rel2 === `${WAI}/${TRANSACTIONS}`) return;
35437
+ const abs = nodePath.join(owner, ...rel2.split("/"));
35438
+ let stat;
35439
+ try {
35440
+ stat = fs21.lstatSync(abs);
35441
+ } catch {
35442
+ return;
35443
+ }
35444
+ if (stat.isSymbolicLink()) return;
35445
+ if (stat.isFile()) {
35446
+ out.push(rel2);
35447
+ return;
35448
+ }
35449
+ if (!stat.isDirectory()) return;
35450
+ for (const name of fs21.readdirSync(abs)) walk2(`${rel2}/${name}`);
35451
+ };
35452
+ walk2(area.split(nodePath.sep).join("/").replace(/\/+$/, ""));
35453
+ return out.sort();
35454
+ }
35455
+ function everyFile(owner) {
35456
+ const out = [];
35457
+ const walk2 = (dir, rel2) => {
35458
+ let entries;
35459
+ try {
35460
+ entries = fs21.readdirSync(dir, { withFileTypes: true });
35461
+ } catch {
35462
+ return;
35463
+ }
35464
+ for (const entry of entries) {
35465
+ const relPath = rel2 ? `${rel2}/${entry.name}` : entry.name;
35466
+ if (relPath === `${WAI}/${TRANSACTIONS}`) continue;
35467
+ if (entry.isSymbolicLink()) continue;
35468
+ if (entry.isDirectory()) walk2(nodePath.join(dir, entry.name), relPath);
35469
+ else if (entry.isFile()) out.push(relPath);
35470
+ }
35471
+ };
35472
+ walk2(owner, "");
35473
+ return out.sort();
35474
+ }
35475
+ function insideWhole(whole, dir) {
35476
+ return whole.some((w) => within2(w, dir));
35477
+ }
35478
+ function scopedFiles(owner, areas) {
35479
+ return areas ? [...new Set(areas.flatMap((a) => areaFiles(owner, a)))].sort() : waiFiles(owner);
35480
+ }
35288
35481
  function mirror(scope, target) {
35289
35482
  const digests = /* @__PURE__ */ new Map();
35290
- for (const project of scope.projects) {
35291
- if (!within2(scope.familyRoot, project)) {
35292
- throw new Error(`${project} does not lie within the family root ${scope.familyRoot}; a rehearsal copies only the family's own trees`);
35293
- }
35294
- const at = nodePath.join(target, nodePath.relative(scope.familyRoot, project));
35295
- for (const rel2 of waiFiles(project)) {
35296
- const live = nodePath.join(project, ...rel2.split("/"));
35297
- let bytes;
35298
- try {
35299
- bytes = fs21.readFileSync(live);
35300
- } catch (e) {
35301
- throw new Error(`could not copy ${live} into the rehearsal: ${e instanceof Error ? e.message : String(e)}`);
35302
- }
35303
- const copy = nodePath.join(at, ...rel2.split("/"));
35304
- fs21.mkdirSync(nodePath.dirname(copy), { recursive: true });
35305
- fs21.writeFileSync(copy, bytes);
35306
- digests.set(live, sha2562(bytes));
35483
+ const whole = scope.whole ?? [];
35484
+ for (const owner of [...scope.projects, ...whole]) {
35485
+ if (!within2(scope.familyRoot, owner)) {
35486
+ throw new Error(`${owner} does not lie within the family root ${scope.familyRoot}; a rehearsal copies only the family's own trees`);
35307
35487
  }
35308
35488
  }
35489
+ for (const project of scope.projects.filter((p) => !insideWhole(whole, p))) {
35490
+ copyFiles(project, scopedFiles(project, scope.areas), nodePath.join(target, nodePath.relative(scope.familyRoot, project)), digests);
35491
+ }
35492
+ for (const owner of whole) {
35493
+ copyFiles(owner, everyFile(owner), nodePath.join(target, nodePath.relative(scope.familyRoot, owner)), digests);
35494
+ }
35309
35495
  return digests;
35310
35496
  }
35497
+ function copyFiles(owner, rels, at, digests) {
35498
+ fs21.mkdirSync(at, { recursive: true });
35499
+ for (const rel2 of rels) {
35500
+ const live = nodePath.join(owner, ...rel2.split("/"));
35501
+ let bytes;
35502
+ try {
35503
+ bytes = fs21.readFileSync(live);
35504
+ } catch (e) {
35505
+ throw new Error(`could not copy ${live} into the rehearsal: ${e instanceof Error ? e.message : String(e)}`);
35506
+ }
35507
+ const copy = nodePath.join(at, ...rel2.split("/"));
35508
+ fs21.mkdirSync(nodePath.dirname(copy), { recursive: true });
35509
+ fs21.writeFileSync(copy, bytes);
35510
+ digests.set(live, sha2562(bytes));
35511
+ }
35512
+ }
35311
35513
  function compare4(rehearsal) {
35312
35514
  const changes = [];
35313
35515
  const seen = /* @__PURE__ */ new Set();
35314
- for (const owner of waiOwners(rehearsal.directory)) {
35315
- const live = nodePath.join(rehearsal.familyRoot, nodePath.relative(rehearsal.directory, owner));
35316
- for (const rel2 of waiFiles(owner)) {
35317
- seen.add(nodePath.join(live, ...rel2.split("/")));
35318
- const change = changeOf(rehearsal, live, rel2, sha2562(fs21.readFileSync(nodePath.join(owner, ...rel2.split("/")))));
35319
- if (change) changes.push(change);
35320
- }
35516
+ for (const [live, copyRoot, rel2] of comparedFiles(rehearsal)) {
35517
+ seen.add(nodePath.join(live, ...rel2.split("/")));
35518
+ const change = changeOf(rehearsal, live, rel2, sha2562(fs21.readFileSync(nodePath.join(copyRoot, ...rel2.split("/")))));
35519
+ if (change) changes.push(change);
35321
35520
  }
35322
35521
  for (const [liveFile, base] of rehearsal.baseDigests) {
35323
35522
  if (seen.has(liveFile)) continue;
@@ -35326,6 +35525,16 @@ function compare4(rehearsal) {
35326
35525
  }
35327
35526
  return changes.sort(byOwnerThenPath);
35328
35527
  }
35528
+ function comparedFiles(rehearsal) {
35529
+ const whole = rehearsal.whole ?? [];
35530
+ const owners = rehearsal.areas ? [...rehearsal.roots].map(([live, copy]) => [live, copy, scopedFiles(copy, rehearsal.areas)]) : waiOwners(rehearsal.directory).map((copy) => [nodePath.join(rehearsal.familyRoot, nodePath.relative(rehearsal.directory, copy)), copy, waiFiles(copy)]);
35531
+ const imageOf = (live) => nodePath.join(rehearsal.directory, nodePath.relative(rehearsal.familyRoot, live));
35532
+ const all = [
35533
+ ...owners.filter(([live]) => !insideWhole(whole, live)),
35534
+ ...whole.map((live) => [live, imageOf(live), everyFile(imageOf(live))])
35535
+ ];
35536
+ return all.flatMap(([live, copy, rels]) => rels.map((rel2) => [live, copy, rel2]));
35537
+ }
35329
35538
  function changeOf(rehearsal, live, rel2, staged) {
35330
35539
  const base = rehearsal.baseDigests.get(nodePath.join(live, ...rel2.split("/")));
35331
35540
  if (base === staged) return null;
@@ -35334,9 +35543,23 @@ function changeOf(rehearsal, live, rel2, staged) {
35334
35543
  var order = (a, b) => a < b ? -1 : a > b ? 1 : 0;
35335
35544
  var byOwnerThenPath = (a, b) => order(a.project, b.project) || order(a.path, b.path);
35336
35545
  function ownerOf3(rehearsal, liveFile) {
35337
- const roots = [...rehearsal.roots.keys()].filter((r) => within2(nodePath.join(r, WAI), liveFile));
35546
+ const whole = (rehearsal.whole ?? []).find((w) => within2(w, liveFile));
35547
+ if (whole !== void 0) return whole;
35548
+ const roots = [...rehearsal.roots.keys()].filter((r) => within2(rehearsal.areas ? r : nodePath.join(r, WAI), liveFile));
35338
35549
  return roots.sort((a, b) => b.length - a.length)[0] ?? nodePath.dirname(nodePath.dirname(liveFile));
35339
35550
  }
35551
+ function move(from, to) {
35552
+ const rels = everyFile(from);
35553
+ const taken = rels.find((rel2) => fs21.existsSync(nodePath.join(to, ...rel2.split("/"))));
35554
+ if (taken !== void 0) throw new Error(`cannot move ${from} to ${to}: ${nodePath.join(to, ...taken.split("/"))} already exists`);
35555
+ for (const rel2 of rels) {
35556
+ const target = nodePath.join(to, ...rel2.split("/"));
35557
+ makeDirs(nodePath.dirname(target));
35558
+ replace(nodePath.join(from, ...rel2.split("/")), target);
35559
+ }
35560
+ for (const rel2 of rels) prune(nodePath.dirname(nodePath.join(from, ...rel2.split("/"))), nodePath.dirname(from));
35561
+ prune(from, nodePath.dirname(from));
35562
+ }
35340
35563
  function read(path46) {
35341
35564
  try {
35342
35565
  return fs21.readFileSync(path46);
@@ -35478,6 +35701,13 @@ function hasSystemAt(dir) {
35478
35701
  }
35479
35702
  });
35480
35703
  }
35704
+ function occupied(dir) {
35705
+ try {
35706
+ return !fs22.statSync(dir).isDirectory() || fs22.readdirSync(dir).length > 0;
35707
+ } catch {
35708
+ return false;
35709
+ }
35710
+ }
35481
35711
  function boundNode(family, bound2) {
35482
35712
  return familyNode(family, bound2);
35483
35713
  }
@@ -35551,8 +35781,11 @@ function planDetach(family, bound2, request) {
35551
35781
  const plan5 = emptyPlan(family, request);
35552
35782
  const alias = request.alias ?? "";
35553
35783
  const done = configAt3(familyNode(family, bound2).directory)?.externals?.[alias];
35554
- if (done?.source?.path && !family.nodes.some((n) => n.parent === bound2 && n.mountAlias === alias)) return plan5;
35784
+ if ((done?.source?.path || done?.source?.hosted) && !family.nodes.some((n) => n.parent === bound2 && n.mountAlias === alias)) return plan5;
35555
35785
  const member = memberUnder(family, bound2, alias, plan5);
35786
+ if (member && request.relocation && occupied(request.relocation.to)) {
35787
+ refuse(plan5, "relocation-target-exists", bound2, `the member "${alias}" cannot move to ${request.relocation.to}: it already exists`);
35788
+ }
35556
35789
  if (!member) return plan5;
35557
35790
  const table = resolveProjectExports(member.namespace || void 0);
35558
35791
  const leaving = (key) => key !== void 0 && withinMember(family, member, key);
@@ -35605,27 +35838,34 @@ function planWidening(plan5, member, narrow) {
35605
35838
  }
35606
35839
  function detachEdits(plan5, family, bound2, member, alias, declared, consumers, outward) {
35607
35840
  const boundDir = familyNode(family, bound2).directory;
35608
- edit(plan5, bound2, "member", `members: ${alias} removed (${declared.path} stays as it is)`, { root: boundDir, call: "removeMember", args: [alias] });
35841
+ const relocation = plan5.request.relocation;
35842
+ const sourceTo = (from, producer) => relocation ? { hosted: producer.id ?? producer.mountAlias ?? producer.namespace } : { path: posix3(path40.relative(from, producer.directory)) };
35843
+ const where = (source) => source.hosted !== void 0 ? `hosted record ${source.hosted}` : source.path;
35844
+ edit(plan5, bound2, "member", `members: ${alias} removed (${declared.path} ${relocation ? `moves to ${relocation.to}` : "stays as it is"})`, { root: boundDir, call: "removeMember", args: [alias] });
35845
+ const boundSource = relocation ? sourceTo(boundDir, member) : { path: declared.path };
35609
35846
  const external = {
35610
35847
  ...member.id !== void 0 && member.id !== alias ? { project: member.id } : {},
35611
- source: { path: declared.path },
35848
+ source: boundSource,
35612
35849
  ...declared.use && declared.use.length > 0 ? { use: declared.use } : {},
35613
35850
  ...declared.description !== void 0 ? { description: declared.description } : {}
35614
35851
  };
35615
- edit(plan5, bound2, "external", `externals: ${alias} \u2192 ${member.id ?? alias} at ${declared.path}`, { root: boundDir, call: "declareExternal", args: [alias, external] });
35852
+ edit(plan5, bound2, "external", `externals: ${alias} \u2192 ${member.id ?? alias} at ${where(boundSource)}`, { root: boundDir, call: "declareExternal", args: [alias, external] });
35616
35853
  for (const c of consumers) {
35617
35854
  const held = configAt3(c.node.directory)?.externals?.[c.alias];
35618
- const source = { path: posix3(path40.relative(c.node.directory, member.directory)) };
35619
- edit(plan5, c.node.namespace, "external", `externals: ${c.alias} found at ${source.path} (it leaves the family)`, { root: c.node.directory, call: "repointExternal", args: [c.alias, held?.project ?? null, source] });
35855
+ const source = sourceTo(c.node.directory, member);
35856
+ edit(plan5, c.node.namespace, "external", `externals: ${c.alias} found at ${where(source)} (it leaves the family)`, { root: c.node.directory, call: "repointExternal", args: [c.alias, held?.project ?? null, source] });
35620
35857
  }
35621
35858
  const own = configAt3(member.directory)?.externals ?? {};
35622
35859
  for (const e of outward) {
35623
35860
  const producer = familyNode(family, e.producer);
35624
- const source = { path: posix3(path40.relative(member.directory, producer.directory)) };
35625
- edit(plan5, member.namespace, "external", `externals: ${e.alias} found at ${source.path} (the member leaves the family)`, { root: member.directory, call: "repointExternal", args: [e.alias, own[e.alias]?.project ?? null, source] });
35861
+ const source = sourceTo(member.directory, producer);
35862
+ edit(plan5, member.namespace, "external", `externals: ${e.alias} found at ${where(source)} (the member leaves the family)`, { root: member.directory, call: "repointExternal", args: [e.alias, own[e.alias]?.project ?? null, source] });
35863
+ }
35864
+ if (relocation) {
35865
+ edit(plan5, member.namespace, "move", `${member.directory} moved to ${relocation.to} (its own isolated root), its own members with it`, { root: member.directory, call: "move", args: [relocation.to] });
35626
35866
  }
35627
35867
  edit(plan5, bound2, "pin", `pin ${alias} (the detached project's surface, read last)`, { root: boundDir, call: "pinExternals", args: [[alias]] });
35628
- plan5.notes.push(`Consumers outside the family are not affected: they already name ${member.id ?? alias} by path, or not at all.`);
35868
+ plan5.notes.push(`Consumers outside the family are not affected: they already name ${member.id ?? alias} by ${relocation ? "hosted record id" : "path"}, or not at all.`);
35629
35869
  }
35630
35870
  function planAdopt(family, bound2, request) {
35631
35871
  const plan5 = emptyPlan(family, request);
@@ -35636,6 +35876,7 @@ function planAdopt(family, bound2, request) {
35636
35876
  if (!external && boundConfig?.members?.[alias] !== void 0) return plan5;
35637
35877
  let dir = null;
35638
35878
  if (!external) refuse(plan5, "not-an-external", bound2, `${label4(bound2)} declares no external "${alias}"`);
35879
+ else if (request.relocation) return planRelocatedAdopt(plan5, family, bound2, alias, external, request.relocation);
35639
35880
  else {
35640
35881
  dir = resolve30(boundDir, external.source?.path ?? "");
35641
35882
  if (dir === null) refuse(plan5, "not-contained", bound2, `the external "${alias}" ${external.source?.path ? `is found at "${external.source.path}", which is not strictly within ${label4(bound2)}` : "names no source.path"}`);
@@ -35644,7 +35885,18 @@ function planAdopt(family, bound2, request) {
35644
35885
  if (plan5.refusals.length > 0) return plan5;
35645
35886
  return adoptEdits(plan5, family, bound2, alias, external, dir);
35646
35887
  }
35647
- function adoptEdits(plan5, family, bound2, alias, external, dir) {
35888
+ function planRelocatedAdopt(plan5, family, bound2, alias, external, relocation) {
35889
+ const boundDir = familyNode(family, bound2).directory;
35890
+ const target = resolve30(boundDir, plan5.request.path ?? "");
35891
+ if (!external.source?.hosted) refuse(plan5, "not-contained", bound2, `the external "${alias}" names no source.hosted \u2014 a hosted adopt moves a project named by its hosted record id`);
35892
+ else if (target === null || path40.resolve(target) !== path40.resolve(relocation.to)) refuse(plan5, "not-contained", bound2, `the member path "${plan5.request.path ?? ""}" does not resolve strictly within ${label4(bound2)} to ${relocation.to}`);
35893
+ else if (occupied(relocation.to)) refuse(plan5, "relocation-target-exists", bound2, `the external "${alias}" cannot be adopted at ${relocation.to}: it already exists and is not an empty directory`);
35894
+ const from = relocation.from ?? (external.source?.hosted !== void 0 ? getHostedLookup()?.(external.source.hosted) ?? void 0 : void 0);
35895
+ if (plan5.refusals.length === 0 && !from) refuse(plan5, "not-a-project", bound2, `the hosted record "${external.source?.hosted}" is unknown or outside this request's reach \u2014 there is no root to adopt from`);
35896
+ if (plan5.refusals.length > 0) return plan5;
35897
+ return adoptEdits(plan5, family, bound2, alias, external, from, relocation.to);
35898
+ }
35899
+ function adoptEdits(plan5, family, bound2, alias, external, dir, at = dir) {
35648
35900
  const boundDir = familyNode(family, bound2).directory;
35649
35901
  const adoptee = configAt3(dir);
35650
35902
  if (!adoptee && !hasSystemAt(dir)) {
@@ -35661,22 +35913,28 @@ function adoptEdits(plan5, family, bound2, alias, external, dir) {
35661
35913
  edit(plan5, bound2, "pin", `pin ${alias} removed (the family supplies it now)`, { root: boundDir, call: "unpin", args: [alias] });
35662
35914
  edit(plan5, bound2, "external", `externals: ${alias} removed`, { root: boundDir, call: "removeExternal", args: [alias] });
35663
35915
  const declaration2 = {
35664
- path: posix3(path40.relative(boundDir, dir)),
35916
+ path: posix3(path40.relative(boundDir, at)),
35665
35917
  ...external.description !== void 0 ? { description: external.description } : {},
35666
35918
  ...external.use && external.use.length > 0 ? { use: external.use } : {}
35667
35919
  };
35668
35920
  edit(plan5, bound2, "member", `members: ${alias} \u2192 ${declaration2.path}`, { root: boundDir, call: "declareMember", args: [alias, declaration2] });
35921
+ const namesAdoptee = (from, e) => e.source?.path !== void 0 && path40.resolve(from, e.source.path) === path40.resolve(dir) || plan5.request.relocation !== void 0 && id !== void 0 && e.source?.hosted === id;
35922
+ const sourceText = (e) => e.source?.hosted !== void 0 ? `source.hosted ${e.source.hosted}` : `source.path ${e.source?.path}`;
35669
35923
  for (const n of family.nodes) {
35670
35924
  for (const [a, e] of Object.entries(configAt3(n.directory)?.externals ?? {})) {
35671
35925
  if (n.namespace === bound2 && a === alias) continue;
35672
- if (!e.source?.path || path40.resolve(n.directory, e.source.path) !== path40.resolve(dir)) continue;
35673
- edit(plan5, n.namespace, "external", `externals: ${a} found through the family (its source.path ${e.source.path} removed)`, { root: n.directory, call: "repointExternal", args: [a, e.project ?? null, null] });
35674
- plan5.notes.push(`${label4(n.namespace)}'s external "${a}" named the adoptee by path; adopt removes it \u2014 detach-then-adopt is exact only when detach wrote that path`);
35926
+ if (!namesAdoptee(n.directory, e)) continue;
35927
+ edit(plan5, n.namespace, "external", `externals: ${a} found through the family (its ${sourceText(e)} removed)`, { root: n.directory, call: "repointExternal", args: [a, e.project ?? null, null] });
35928
+ plan5.notes.push(`${label4(n.namespace)}'s external "${a}" named the adoptee by ${e.source?.hosted !== void 0 ? "hosted record id" : "path"}; adopt removes it \u2014 detach-then-adopt is exact only when detach wrote that source`);
35675
35929
  }
35676
35930
  }
35931
+ const namesFamily = (e) => e.source?.path !== void 0 && nodeAt(family, path40.resolve(dir, e.source.path)) !== void 0 || plan5.request.relocation !== void 0 && e.source?.hosted !== void 0 && family.nodes.some((n) => n.id === e.source.hosted);
35677
35932
  for (const [a, e] of Object.entries(adoptee?.externals ?? {})) {
35678
- if (!e.source?.path || !nodeAt(family, path40.resolve(dir, e.source.path))) continue;
35679
- edit(plan5, key, "external", `externals: ${a} found through the family (its source.path ${e.source.path} removed)`, { root: dir, call: "repointExternal", args: [a, e.project ?? null, null] });
35933
+ if (!namesFamily(e)) continue;
35934
+ edit(plan5, key, "external", `externals: ${a} found through the family (its ${sourceText(e)} removed)`, { root: dir, call: "repointExternal", args: [a, e.project ?? null, null] });
35935
+ }
35936
+ if (plan5.request.relocation) {
35937
+ edit(plan5, key, "move", `${dir} moved to ${at} (it joins the family's tree), its own members with it`, { root: dir, call: "move", args: [at] });
35680
35938
  }
35681
35939
  return plan5;
35682
35940
  }
@@ -35688,11 +35946,13 @@ function write3(plan5, rehearsal) {
35688
35946
  break;
35689
35947
  case "detach":
35690
35948
  for (const e of planned.filter((x) => x.write.call === "updateSpecGated")) writeOne(rehearsal, e.write);
35691
- for (const e of planned.filter((x) => x.write.call !== "pinExternals" && x.write.call !== "updateSpecGated")) writeOne(rehearsal, e.write);
35949
+ for (const e of planned.filter((x) => !["pinExternals", "updateSpecGated", "move"].includes(x.write.call))) writeOne(rehearsal, e.write);
35950
+ for (const e of planned.filter((x) => x.write.call === "move")) writeOne(rehearsal, e.write);
35692
35951
  for (const e of planned.filter((x) => x.write.call === "pinExternals")) writeOne(rehearsal, e.write);
35693
35952
  break;
35694
35953
  case "adopt":
35695
- for (const e of planned) writeOne(rehearsal, e.write);
35954
+ for (const e of planned.filter((x) => x.write.call !== "move")) writeOne(rehearsal, e.write);
35955
+ for (const e of planned.filter((x) => x.write.call === "move")) writeOne(rehearsal, e.write);
35696
35956
  break;
35697
35957
  default:
35698
35958
  throw new Error(`membership_migration writes attach, detach and adopt, not "${plan5.request.verb}"`);
@@ -35700,6 +35960,7 @@ function write3(plan5, rehearsal) {
35700
35960
  }
35701
35961
  function writeOne(rehearsal, w) {
35702
35962
  const args = w.args;
35963
+ if (w.call === "move") return move(rehearsalRoot(rehearsal, w.root), rehearsalRoot(rehearsal, String(args[0])));
35703
35964
  runWithProjectRoot(rehearsalRoot(rehearsal, w.root), () => {
35704
35965
  switch (w.call) {
35705
35966
  case "setId":
@@ -36124,11 +36385,11 @@ function planExternalize(family, bound2, request) {
36124
36385
  const donePath = typeof done === "string" ? done : done?.path;
36125
36386
  if (donePath !== void 0 && request.path !== void 0 && path42.resolve(node.directory, donePath) === path42.resolve(node.directory, request.path)) return plan5;
36126
36387
  const dir = resolve30(node.directory, request.path ?? "");
36127
- const occupied = dir !== null && holdsProject(dir);
36388
+ const occupied2 = dir !== null && holdsProject(dir);
36128
36389
  const sub = id.includes("::") ? null : loadSpec("subsystem", subKey);
36129
36390
  if (!sub || sub.projectPath || (family.owners.get(subKey) ?? "") !== bound2) refuse3(plan5, "externalize-refused", bound2, `${label6(bound2)} has no subsystem "${id}" to externalize (it is missing, or already a member)`);
36130
36391
  if (dir === null) refuse3(plan5, "not-contained", bound2, `the path "${request.path ?? ""}" does not resolve strictly within ${label6(bound2)}`);
36131
- else if (family.nodes.some((n) => path42.resolve(n.directory) === dir) || occupied) {
36392
+ else if (family.nodes.some((n) => path42.resolve(n.directory) === dir) || occupied2) {
36132
36393
  refuse3(plan5, "not-contained", bound2, `${dir} already holds a project`);
36133
36394
  }
36134
36395
  const config = configAt5(node.directory);
@@ -36243,11 +36504,11 @@ function writeOne3(rehearsal, w) {
36243
36504
 
36244
36505
  // src/migrations/transaction.ts
36245
36506
  var crypto6 = __toESM(require("crypto"));
36246
- var fs23 = __toESM(require("fs"));
36507
+ var fs24 = __toESM(require("fs"));
36247
36508
  var path43 = __toESM(require("path"));
36248
36509
 
36249
36510
  // src/migrations/transaction-files.ts
36250
- var fs22 = __toESM(require("fs"));
36511
+ var fs23 = __toESM(require("fs"));
36251
36512
  var os6 = __toESM(require("os"));
36252
36513
  var nodePath2 = __toESM(require("path"));
36253
36514
  var yaml3 = __toESM(require("js-yaml"));
@@ -36261,7 +36522,7 @@ function pause2(ms) {
36261
36522
  function removeTree(dir) {
36262
36523
  for (let attempt = 1; ; attempt++) {
36263
36524
  try {
36264
- fs22.rmSync(dir, { recursive: true, force: true });
36525
+ fs23.rmSync(dir, { recursive: true, force: true });
36265
36526
  return;
36266
36527
  } catch (e) {
36267
36528
  const code = e.code;
@@ -36271,20 +36532,20 @@ function removeTree(dir) {
36271
36532
  }
36272
36533
  }
36273
36534
  function writeDurably(file, bytes) {
36274
- fs22.mkdirSync(nodePath2.dirname(file), { recursive: true });
36535
+ fs23.mkdirSync(nodePath2.dirname(file), { recursive: true });
36275
36536
  const tmp = `${file}.tmp`;
36276
36537
  try {
36277
- const fd = fs22.openSync(tmp, "w");
36538
+ const fd = fs23.openSync(tmp, "w");
36278
36539
  try {
36279
- fs22.writeSync(fd, typeof bytes === "string" ? Buffer.from(bytes, "utf8") : bytes);
36280
- fs22.fsyncSync(fd);
36540
+ fs23.writeSync(fd, typeof bytes === "string" ? Buffer.from(bytes, "utf8") : bytes);
36541
+ fs23.fsyncSync(fd);
36281
36542
  } finally {
36282
- fs22.closeSync(fd);
36543
+ fs23.closeSync(fd);
36283
36544
  }
36284
- fs22.renameSync(tmp, file);
36545
+ fs23.renameSync(tmp, file);
36285
36546
  } catch (e) {
36286
36547
  try {
36287
- fs22.unlinkSync(tmp);
36548
+ fs23.unlinkSync(tmp);
36288
36549
  } catch {
36289
36550
  }
36290
36551
  throw e;
@@ -36294,18 +36555,18 @@ function writeDurably(file, bytes) {
36294
36555
  function syncDirectory(dir) {
36295
36556
  if (process.platform === "win32") return;
36296
36557
  try {
36297
- const fd = fs22.openSync(dir, "r");
36558
+ const fd = fs23.openSync(dir, "r");
36298
36559
  try {
36299
- fs22.fsyncSync(fd);
36560
+ fs23.fsyncSync(fd);
36300
36561
  } finally {
36301
- fs22.closeSync(fd);
36562
+ fs23.closeSync(fd);
36302
36563
  }
36303
36564
  } catch {
36304
36565
  }
36305
36566
  }
36306
36567
  function openRehearsal(id) {
36307
36568
  const dir = rehearsalDir(id);
36308
- fs22.mkdirSync(dir);
36569
+ fs23.mkdirSync(dir);
36309
36570
  return dir;
36310
36571
  }
36311
36572
  function dropRehearsal(id) {
@@ -36316,14 +36577,14 @@ function dropRehearsal(id) {
36316
36577
  }
36317
36578
  function saveJournal(journal) {
36318
36579
  const ignore = nodePath2.join(transactionsDir(journal.owner), ".gitignore");
36319
- if (!fs22.existsSync(ignore)) writeDurably(ignore, "*\n");
36580
+ if (!fs23.existsSync(ignore)) writeDurably(ignore, "*\n");
36320
36581
  writeDurably(nodePath2.join(transactionDir(journal.owner, journal.id), "journal.yaml"), yaml3.dump(journal, { noRefs: true, lineWidth: 200 }));
36321
36582
  }
36322
36583
  function readJournal(owner, id) {
36323
36584
  const file = nodePath2.join(transactionDir(owner, id), "journal.yaml");
36324
36585
  let text3;
36325
36586
  try {
36326
- text3 = fs22.readFileSync(file, "utf8");
36587
+ text3 = fs23.readFileSync(file, "utf8");
36327
36588
  } catch (e) {
36328
36589
  if (e.code === "ENOENT") return null;
36329
36590
  throw e;
@@ -36338,7 +36599,7 @@ function listJournals(owner) {
36338
36599
  const dir = transactionsDir(owner);
36339
36600
  let names;
36340
36601
  try {
36341
- names = fs22.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
36602
+ names = fs23.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
36342
36603
  } catch {
36343
36604
  return [];
36344
36605
  }
@@ -36360,7 +36621,7 @@ function putFile(owner, id, area, path46, bytes) {
36360
36621
  }
36361
36622
  function readFile(owner, id, area, path46) {
36362
36623
  try {
36363
- return fs22.readFileSync(locate(owner, id, area, path46));
36624
+ return fs23.readFileSync(locate(owner, id, area, path46));
36364
36625
  } catch (e) {
36365
36626
  if (e.code === "ENOENT") return null;
36366
36627
  throw e;
@@ -36374,7 +36635,7 @@ function close(owner, id) {
36374
36635
  const dir = transactionsDir(owner);
36375
36636
  let left;
36376
36637
  try {
36377
- left = fs22.readdirSync(dir);
36638
+ left = fs23.readdirSync(dir);
36378
36639
  } catch {
36379
36640
  return;
36380
36641
  }
@@ -36382,7 +36643,11 @@ function close(owner, id) {
36382
36643
  removeTree(dir);
36383
36644
  const wai = nodePath2.dirname(dir);
36384
36645
  try {
36385
- if (fs22.readdirSync(wai).length === 0) fs22.rmdirSync(wai);
36646
+ if (fs23.readdirSync(wai).length === 0) fs23.rmdirSync(wai);
36647
+ } catch {
36648
+ }
36649
+ try {
36650
+ if (fs23.readdirSync(owner).length === 0) fs23.rmdirSync(owner);
36386
36651
  } catch {
36387
36652
  }
36388
36653
  }
@@ -36469,15 +36734,16 @@ function rehearse(scope) {
36469
36734
  const directory = transactionRepository.openRehearsal(id);
36470
36735
  const familyRoot = path43.resolve(scope.familyRoot);
36471
36736
  const projects = [...new Set(scope.projects.map((p) => path43.resolve(p)))];
36737
+ const whole = scope.whole ? [...new Set(scope.whole.map((p) => path43.resolve(p)))] : void 0;
36472
36738
  let baseDigests;
36473
36739
  try {
36474
- baseDigests = mirror({ familyRoot, projects }, directory);
36740
+ baseDigests = mirror({ familyRoot, projects, ...scope.areas ? { areas: scope.areas } : {}, ...whole ? { whole } : {} }, directory);
36475
36741
  } catch (e) {
36476
36742
  transactionRepository.dropRehearsal(id);
36477
36743
  throw e;
36478
36744
  }
36479
36745
  const roots = new Map(projects.map((p) => [p, path43.join(directory, path43.relative(familyRoot, p))]));
36480
- return { id, directory, familyRoot, roots, baseDigests };
36746
+ return { id, directory, familyRoot, roots, baseDigests, ...scope.areas ? { areas: [...scope.areas] } : {}, ...whole ? { whole } : {} };
36481
36747
  }
36482
36748
  function diff(rehearsal) {
36483
36749
  return compare4(rehearsal);
@@ -36564,7 +36830,7 @@ function missingDirs(owner, entries) {
36564
36830
  for (const entry of entries.filter((e) => e.action === "create")) {
36565
36831
  const missing = [];
36566
36832
  let at = path43.dirname(liveOf(owner, entry.path));
36567
- while (!fs23.existsSync(at) && at !== path43.dirname(at)) {
36833
+ while (!fs24.existsSync(at) && at !== path43.dirname(at)) {
36568
36834
  missing.unshift(at);
36569
36835
  at = path43.dirname(at);
36570
36836
  }
@@ -36598,7 +36864,7 @@ function stageBytes(rehearsal, journal, entry) {
36598
36864
  function checkVolume(journal, entry, target) {
36599
36865
  const staged = transactionRepository.locate(journal.owner, journal.id, "staged", entry.path);
36600
36866
  let nearest = path43.dirname(target);
36601
- while (!fs23.existsSync(nearest) && nearest !== path43.dirname(nearest)) nearest = path43.dirname(nearest);
36867
+ while (!fs24.existsSync(nearest) && nearest !== path43.dirname(nearest)) nearest = path43.dirname(nearest);
36602
36868
  if (!sameVolume(staged, nearest)) {
36603
36869
  throw new StagingError(`cross-volume: ${target} lies on another volume than its staged copy; a rename across volumes is not atomic`);
36604
36870
  }
@@ -36682,7 +36948,7 @@ function recover(root, fix) {
36682
36948
  var coordinatorRoot = (journal) => path43.resolve(journal.owner, journal.coordinator);
36683
36949
  function journalAt(owner, id) {
36684
36950
  try {
36685
- const journal = fs23.existsSync(owner) ? transactionRepository.readJournal(owner, id) : null;
36951
+ const journal = fs24.existsSync(owner) ? transactionRepository.readJournal(owner, id) : null;
36686
36952
  return journal ? { ...journal, owner } : null;
36687
36953
  } catch {
36688
36954
  return null;
@@ -36714,7 +36980,7 @@ function resolveOne(journal) {
36714
36980
  for (const owner of [...owners.filter((o) => o !== coordinator.owner), coordinator.owner]) transactionRepository.close(owner, journal.id);
36715
36981
  return { ...base, owners, phase, action: "cleaned", detail: "it had committed; its transaction directories were removed" };
36716
36982
  }
36717
- const absent = owners.filter((o) => !fs23.existsSync(o));
36983
+ const absent = phase === "staging" ? [] : owners.filter((o) => !fs24.existsSync(o));
36718
36984
  if (absent.length > 0) {
36719
36985
  return { ...base, owners, phase, action: "refused", detail: `owner(s) out of reach: ${absent.join(", ")}` };
36720
36986
  }
@@ -36745,14 +37011,15 @@ function plan3(request) {
36745
37011
  const { top, whole } = climb3(bound2);
36746
37012
  const family = runWithProjectRoot(top, () => graph());
36747
37013
  const boundKey = family.nodes.find((n) => path44.resolve(n.directory) === path44.resolve(bound2))?.namespace ?? "";
36748
- const pending3 = unfinished(top, bound2);
37014
+ const pending3 = unfinished(top, bound2, request.relocation?.coordinator);
36749
37015
  if (pending3.length > 0) return refusedForPending(request, top, whole, pending3);
36750
37016
  const planned = request.verb === "chaining" ? route(request, family, boundKey) : runWithProjectRoot(top, () => route(request, family, boundKey));
36751
37017
  return rehearse2(request.verb === "chaining" ? planned : { ...planned, familyRoot: top, whole });
36752
37018
  }
36753
- function unfinished(top, bound2) {
37019
+ function unfinished(top, bound2, coordinator) {
36754
37020
  const found = recover(top, false);
36755
37021
  if (path44.resolve(bound2) !== path44.resolve(top)) found.push(...recover(bound2, false));
37022
+ if (coordinator !== void 0) found.push(...recover(coordinator, false));
36756
37023
  return found.filter((t, i) => found.findIndex((u) => u.id === t.id) === i);
36757
37024
  }
36758
37025
  function refusedForPending(request, top, whole, pending3) {
@@ -36830,17 +37097,62 @@ function rehearse2(plan5) {
36830
37097
  if (blocked(plan5) || plan5.request.rehearse === false) return plan5;
36831
37098
  const family = runWithProjectRoot(plan5.familyRoot, () => graph());
36832
37099
  const projects = [...family.nodes.map((n) => n.directory), ...broughtIn(plan5, family), ...pathProducers(plan5, family)];
36833
- const rehearsal = rehearse({ familyRoot: plan5.familyRoot, projects });
37100
+ const rehearsal = rehearse(scopeOf(plan5, projects));
36834
37101
  try {
36835
- onCopy(rehearsal, () => writeVerb(plan5, rehearsal));
37102
+ onCopy(rehearsal, () => relocatedLookup(plan5, rehearsal, () => writeVerb(plan5, rehearsal)));
36836
37103
  } catch (e) {
36837
37104
  discard(rehearsal);
36838
37105
  const detail = e instanceof Error ? e.message : String(e);
36839
37106
  return { ...plan5, refusals: [...plan5.refusals, { code: `${plan5.request.verb}-refused`, project: "", detail }] };
36840
37107
  }
36841
37108
  const changes = diff(rehearsal);
37109
+ const outside = unwritableOwners(changes);
37110
+ if (outside.length > 0) {
37111
+ discard(rehearsal);
37112
+ return { ...plan5, refusals: [...plan5.refusals, ...outside.map((o) => ({ code: "family-partial", project: o, detail: `the plan must write ${o}, which this request may not write` }))] };
37113
+ }
36842
37114
  return { ...plan5, rehearsal, changes, relock: relockOf(plan5, changes) };
36843
37115
  }
37116
+ function movedOf(plan5) {
37117
+ const relocation = plan5.request.relocation;
37118
+ const move2 = plan5.edits.find((e) => e.write?.call === "move");
37119
+ if (!relocation || !move2?.write) return null;
37120
+ return { from: path44.resolve(move2.write.root), to: path44.resolve(relocation.to) };
37121
+ }
37122
+ function scopeOf(plan5, projects) {
37123
+ const relocation = plan5.request.relocation;
37124
+ const moved = movedOf(plan5);
37125
+ if (!relocation || !moved) return { familyRoot: plan5.familyRoot, projects };
37126
+ return {
37127
+ familyRoot: relocation.coordinator,
37128
+ projects: [relocation.coordinator, ...projects],
37129
+ areas: [".wai", ...relocation.areas],
37130
+ whole: [moved.from, moved.to]
37131
+ };
37132
+ }
37133
+ function relocatedLookup(plan5, rehearsal, run3) {
37134
+ const moved = movedOf(plan5);
37135
+ const live = getHostedLookup();
37136
+ if (!moved || !live) return run3();
37137
+ const copied = [...rehearsal.roots.keys(), ...rehearsal.whole ?? []].filter((r) => r !== path44.resolve(rehearsal.familyRoot));
37138
+ const inside = (root, dir) => {
37139
+ const rel2 = path44.relative(root, dir);
37140
+ return rel2 === "" || !rel2.startsWith("..") && !path44.isAbsolute(rel2);
37141
+ };
37142
+ return runWithHostedLookup((id) => {
37143
+ const root = live(id);
37144
+ if (root === null) return null;
37145
+ const at = path44.resolve(root);
37146
+ if (inside(moved.from, at)) return path44.join(rehearsalRoot(rehearsal, moved.to), path44.relative(moved.from, at));
37147
+ return copied.some((r) => inside(r, at)) ? rehearsalRoot(rehearsal, at) : at;
37148
+ }, run3);
37149
+ }
37150
+ function unwritableOwners(changes) {
37151
+ const denied = getRequestParentReach()?.unwritableRoots ?? [];
37152
+ if (denied.length === 0) return [];
37153
+ const owners = [...new Set(changes.map((c) => path44.resolve(c.project)))];
37154
+ return owners.filter((o) => denied.includes(o));
37155
+ }
36844
37156
  function pathProducers(plan5, family) {
36845
37157
  const inside = (dir) => {
36846
37158
  const rel2 = path44.relative(plan5.familyRoot, dir);
@@ -36883,7 +37195,9 @@ function onCopy(rehearsal, run3) {
36883
37195
  function relockOf(plan5, changes) {
36884
37196
  const ended = new Set(changes.filter((c) => c.action === "delete" && c.path === ".wai/project.yaml").map((c) => path44.resolve(c.project)));
36885
37197
  const owners = [...new Set(changes.map((c) => path44.resolve(c.project)))].filter((o) => !ended.has(o));
36886
- if (plan5.request.verb !== "rename") return owners.filter((owner) => approvalRecord(owner) !== null);
37198
+ const moved = movedOf(plan5);
37199
+ const relocated = moved !== null && approvalRecord(moved.from) !== null ? [moved.to] : [];
37200
+ if (plan5.request.verb !== "rename") return [...owners.filter((owner) => approvalRecord(owner) !== null), ...relocated];
36887
37201
  const renamed = plan5.edits.find((e) => e.write?.call === "renameId")?.write?.root;
36888
37202
  const first = renamed !== void 0 ? path44.resolve(renamed) : void 0;
36889
37203
  return [...owners.filter((o) => o === first), ...owners.filter((o) => o !== first)];
@@ -38060,9 +38374,10 @@ ${renderChangeReport(report2)}`,
38060
38374
  server,
38061
38375
  "sdd_adopt_member",
38062
38376
  {
38063
- description: "Make the bound project's external found by a path inside it a member \u2014 sdd_detach_member's inverse. All-or-nothing; dryRun answers the plan and writes nothing. Never locks.",
38377
+ description: "Make the bound project's external a member \u2014 sdd_detach_member's inverse. Locally the external is found by a path inside the bound project and adopted where it is; on a hosted instance it names a project by source.hosted and `path` says where to adopt it (the hosting server relocates it there). All-or-nothing; dryRun answers the plan and writes nothing. Never locks.",
38064
38378
  inputSchema: {
38065
38379
  alias: import_zod11.z.string().describe("The external's alias"),
38380
+ path: import_zod11.z.string().optional().describe("Hosted: the member path to adopt a source.hosted external at, relative to the bound project; a source.path external is adopted where it is"),
38066
38381
  dryRun: dryRunArg
38067
38382
  }
38068
38383
  },
@@ -38408,7 +38723,7 @@ ${renderChangeReport(report2)}`,
38408
38723
  contract: import_zod11.z.string().describe("The L3 Interface contract ID this implements"),
38409
38724
  sourcePath: import_zod11.z.string().optional().describe("Optional: target source code file path relative to project root \u2014 for a chained subproject's implementation (qualified id), relative to that subproject's root; a path given relative to this root that lands inside the subproject is re-expressed for you"),
38410
38725
  simPath: import_zod11.z.string().optional().describe("Optional: the committed integration-sim harness file (project-relative; N:1 sharing allowed). The validator proves it exists and its import graph wires the REAL modules (this component + each direct dependency; technology adapters may stay faked) \u2014 running it is CI's job. Declaring the first simPath in a subsystem activates MISSING_INTEGRATION_SIM for its other complete non-leaf implementations"),
38411
- technologies: import_zod11.z.array(import_zod11.z.string()).optional().describe(`External technologies this implementation binds to (e.g. ["mysql"]) \u2014 declares this component's ownership tree as the technology's home; references outside it are flagged (TECH_LEAKAGE) and contract identifiers must stay intent-language. Only for Adapter/Store/Registry/Index components.`),
38726
+ technologies: import_zod11.z.array(import_zod11.z.union([import_zod11.z.string(), import_zod11.z.object({ name: import_zod11.z.string(), matches: import_zod11.z.array(import_zod11.z.string()).min(1).describe("The tokens the leakage and contract checks match INSTEAD of the name \u2014 for a technology whose name is also an ordinary word of the tree (the yaml package, the YAML format)") }).strict()])).optional().describe(`External technologies this implementation binds to (e.g. ["mysql"], or [{ "name": "yaml", "matches": ["yaml package", "parseDocument"] }] when the bare name would match a common word) \u2014 declares this component's ownership tree as the technology's home; references outside it are flagged (TECH_LEAKAGE) and contract identifiers must stay intent-language. Only for Adapter/Store/Registry/Index components.`),
38412
38727
  injectedParams: import_zod11.z.array(import_zod11.z.string()).optional().describe("The parameter names this realization takes BEFORE the ones its contract declares \u2014 a config object, a data root, the transport handles a portal is handed. Supplied by whatever wires the component up, never by the caller the contract describes, which is why they belong here and not to the contract: another realization may hold them as fields instead. Declared rather than guessed, because a leading parameter the contract does not name cannot be told from one it named under a different name (`seed(config)` realized as `bootstrapInstance(cfg)`); a leading parameter this list does not name is reported (UNDECLARED_PARAM). Only a LEADING run is dropped \u2014 one of these names appearing after the contract's own parameters is an argument in the middle of the caller's list, not wiring"),
38413
38728
  detail: detailEnum.optional().describe("Spec-level narrative detail default for all methods"),
38414
38729
  conformance: conformanceEnum.optional().describe("Spec-level conformance tier default: declared | anchored | off (omitted = stereotype default: Portal \u2192 anchored, else declared)"),
@@ -38687,7 +39002,7 @@ ${testsBlock}` : ""}`, deletion);
38687
39002
  inputSchema: {
38688
39003
  kind: import_zod11.z.enum(["system", "subsystem", "component", "interface", "implementation", "type"]).describe("The spec kind to update (system = the singleton L0 \u2014 vision, boundaries, globalRequirements, databases, and publicInterfaces: the project export table, each entry a re-export {from, component, interface, typeDef, as, name, type, details, audience: project|department|instance|partner|external} \u2014 from alone re-exports everything a subsystem exports, and a legacy {id, subsystem} reads as {as, from})"),
38689
39004
  id: import_zod11.z.string().describe("The ID of the spec to update (namespaced if needed)"),
38690
- delta: import_zod11.z.record(import_zod11.z.any()).describe(`The partial fields to merge into the spec. ARRAYS UPSERT, they do not replace: an array whose elements carry an identity is merged element-by-element, so a delta naming ONE element leaves the others intact. Identity is "name" or "id" by default, and per field: dispatch by "capability", a listener's mounts by "portal", lifecycle by phase+component+method, emits/subscribesTo by topic+event, trustedLinks by "subsystem", a subsystem's publicInterfaces by component+interface (or type+details for an entry not yet bound to a component, and by from+item+interface+as for a re-export or a type export), invariants and patterns by "id", lint.allow by "code" AND "at" (an allow covers one occurrence, so several may share a code on one spec), an interface method's findings by "code", boundaries by "name", globalRequirements by "description", switch cases by "value", try catches by "error". Identity merging applies at EVERY depth, including an array INSIDE an element (a method's params, a step's catches). Add "action: 'delete'" (or "remove: true") alongside that identity to REMOVE an element \u2014 including a stale lint allow. Arrays of plain STRINGS (owns, dependsOn, guarantees) carry no per-element identity and are replaced wholesale; pass [] to clear any array outright. To REMOVE an optional field entirely, list it in "unset": e.g. {"unset": ["basePath", "variant"]} \u2014 passing null/undefined means "no change" (they are skipped), and writing "" would leave the field present but empty, which is a different and usually wrong spec. Unsetting a required field is refused by schema validation, which names it. For narrative steps, match by "stepNumber" and use "action: 'insert'" (shifts subsequent steps up) or "action: 'delete'" (shifts subsequent steps down and removes it). Step entries apply in ASCENDING stepNumber order, each against the numbering the earlier entries of the SAME delta left behind \u2014 delete step 3 and step 7 becomes step 6 \u2014 so prefer labels, and restate the step's "label" or "description" on a delete to have it checked against the step actually addressed. Renumbering RELOCATES every flow jump field (onTrueStep/onFalseStep/cases.step/defaultStep/endStep/catches.step/finallyStep/toStep) in the same narrative. A delete is REJECTED when the narrative has no such step, when a jump still targets it (retarget the referrers first), when a restated label/description does not match, or when it is a loop/try/parallel header whose body would be left standing (retype the header first to dissolve the region, then delete it). Changing a step's "type" REBUILDS it for the new type: its description and label are kept and every field the new type cannot carry is dropped (returned as a NOTICE); a delta that retypes AND sets such a field is refused. A step delta is also refused when it carries a marker the merge does not recognise: a non-boolean "remove", an "action" that is neither "insert" nor "delete", a "captureJumps" outside an insert, or no "stepNumber" to address. Inserting AT a jump target relocates those jumps past the inserted step by default (a NOTICE is returned) \u2014 add "captureJumps": true on the inserted step to retarget entry jumps onto it (loop/try endStep region tails always relocate with the body and are never captured). Every jump field has a "*Label" twin (toLabel, onFalseLabel, endLabel, \u2026, and "label" on a cases/catches entry) resolved against step labels AFTER the merge, so a delta may anchor on a label only pre-existing steps carry; a label the delta supplies REPLACES the stored number it twins, while setting the number and its label together in one delta is refused as a contradiction. Reference ids in deltas may use LOCAL names \u2014 they are qualified against the spec's namespace exactly as the loader would. Per-spec lint suppression: set "lint: { allow: [{ code, at, covers, reason }] }" to silence a WARNING or NOTICE code on this spec only (errors always surface; stale allows are flagged). An allow covers EXACTLY the occurrence it names: "at" is the site the finding names (a contract method, an import edge "from -> to", a declared edge "component -> target") and is REQUIRED for a code whose findings report one, while a finding that reports no site is covered only by an allow that names none; "covers" lists the units an aggregating finding reports, and the allow silences it only when every one is listed. This delta is deliberately OPEN below its top level \u2014 the shapes nest further than a schema here should restate \u2014 so a key that is not a field at its depth is not refused, it is NAMED BACK under NO EFFECT in the answer, together with any value the spec already held and any "unset" that removed nothing. Read that list: it is where a nested typo shows up.`),
39005
+ delta: import_zod11.z.record(import_zod11.z.any()).describe(`The partial fields to merge into the spec. ARRAYS UPSERT, they do not replace: an array whose elements carry an identity is merged element-by-element, so a delta naming ONE element leaves the others intact. Identity is "name" or "id" by default, and per field: dispatch by "capability", a listener's mounts by "portal", lifecycle by phase+component+method, emits/subscribesTo by topic+event, trustedLinks by "subsystem", a subsystem's publicInterfaces by component+interface (or type+details for an entry not yet bound to a component, and by from+item+interface+as for a re-export or a type export), invariants and patterns by "id", lint.allow by "code" AND "at" (an allow covers one occurrence, so several may share a code on one spec), an interface method's findings by "code", boundaries by "name", globalRequirements by "description", switch cases by "value", try catches by "error". Identity merging applies at EVERY depth, including an array INSIDE an element (a method's params, a step's catches). Add "action: 'delete'" (or "remove: true") alongside that identity to REMOVE an element \u2014 including a stale lint allow. Arrays of plain STRINGS (owns, dependsOn, guarantees) carry no per-element identity and are replaced wholesale; pass [] to clear any array outright. To REMOVE an optional field entirely, list it in "unset": e.g. {"unset": ["basePath", "variant"]} \u2014 passing null/undefined means "no change" (they are skipped), and writing "" would leave the field present but empty, which is a different and usually wrong spec. Unsetting a required field is refused by schema validation, which names it. For narrative steps, match by "stepNumber" and use "action: 'insert'" (shifts subsequent steps up) or "action: 'delete'" (shifts subsequent steps down and removes it). Step entries apply in ASCENDING stepNumber order, each against the numbering the earlier entries of the SAME delta left behind \u2014 delete step 3 and step 7 becomes step 6 \u2014 so prefer labels, and restate the step's "label" or "description" on a delete to have it checked against the step actually addressed. Renumbering RELOCATES every flow jump field (onTrueStep/onFalseStep/cases.step/defaultStep/endStep/catches.step/finallyStep/toStep) in the same narrative. Only STORED jumps relocate: a jump the delta itself writes (on an inserted, appended or edited step) names its target in the numbering the WHOLE delta leaves behind, after all of its inserts and deletes, and no later step of the same delta moves it. A method the delta ADDS without a narrative is an intent-level method with none ([]). A delete is REJECTED when the narrative has no such step, when a jump still targets it (retarget the referrers first), when a restated label/description does not match, or when it is a loop/try/parallel header whose body would be left standing (retype the header first to dissolve the region, then delete it). Changing a step's "type" REBUILDS it for the new type: its description and label are kept and every field the new type cannot carry is dropped (returned as a NOTICE); a delta that retypes AND sets such a field is refused. A step delta is also refused when it carries a marker the merge does not recognise: a non-boolean "remove", an "action" that is neither "insert" nor "delete", a "captureJumps" outside an insert, or no "stepNumber" to address. Inserting AT a jump target relocates those jumps past the inserted step by default (a NOTICE is returned) \u2014 add "captureJumps": true on the inserted step to retarget entry jumps onto it (loop/try endStep region tails always relocate with the body and are never captured). Every jump field has a "*Label" twin (toLabel, onFalseLabel, endLabel, \u2026, and "label" on a cases/catches entry) resolved against step labels AFTER the merge, so a delta may anchor on a label only pre-existing steps carry; a label the delta supplies REPLACES the stored number it twins, while setting the number and its label together in one delta is refused as a contradiction. Reference ids in deltas may use LOCAL names \u2014 they are qualified against the spec's namespace exactly as the loader would. Per-spec lint suppression: set "lint: { allow: [{ code, at, covers, reason }] }" to silence a WARNING or NOTICE code on this spec only (errors always surface; stale allows are flagged). An allow covers EXACTLY the occurrence it names: "at" is the site the finding names (a contract method, an import edge "from -> to", a declared edge "component -> target") and is REQUIRED for a code whose findings report one, while a finding that reports no site is covered only by an allow that names none; "covers" lists the units an aggregating finding reports, and the allow silences it only when every one is listed. This delta is deliberately OPEN below its top level \u2014 the shapes nest further than a schema here should restate \u2014 so a key that is not a field at its depth is not refused, it is NAMED BACK under NO EFFECT in the answer, together with any value the spec already held and any "unset" that removed nothing. Read that list: it is where a nested typo shows up.`),
38691
39006
  dryRun: import_zod11.z.boolean().optional().describe("Ask what this delta WOULD do instead of doing it. The whole write runs, the candidate gate included, and the answer is the change report it would have produced \u2014 marked DRY RUN, with nothing stamped and not one byte of the stored file moved. Use it before a delta that renumbers a long narrative.")
38692
39007
  },
38693
39008
  outputSchema: specChangeReportOutput
@@ -39026,6 +39341,7 @@ function loadProjectConfig3() {
39026
39341
  SystemPublicInterfaceSchema,
39027
39342
  SystemSpecSchema,
39028
39343
  TargetConfigSchema,
39344
+ TechnologySchema,
39029
39345
  TemplateNotFoundError,
39030
39346
  TemplateSchema,
39031
39347
  TopologyConfigSchema,
@@ -39053,6 +39369,7 @@ function loadProjectConfig3() {
39053
39369
  assertProjectInitialized,
39054
39370
  backendCommand,
39055
39371
  backfillChainedSubprojectConfigs,
39372
+ backupBeside,
39056
39373
  buildCanvasDataModel,
39057
39374
  buildProjectGraph,
39058
39375
  buildRuleContext,
@@ -39127,6 +39444,7 @@ function loadProjectConfig3() {
39127
39444
  generateDiagramSet,
39128
39445
  generateSequenceDiagram,
39129
39446
  getExporter,
39447
+ getHostedLookup,
39130
39448
  getLoaderIssues,
39131
39449
  getProjectRoot,
39132
39450
  getProjectRootOverride,
@@ -39151,6 +39469,7 @@ function loadProjectConfig3() {
39151
39469
  internalizeMember,
39152
39470
  isDraftSubsystem,
39153
39471
  isLogic,
39472
+ isOutsideRoot,
39154
39473
  isOwnComponentEntry,
39155
39474
  isPattern,
39156
39475
  isProjectInitialized,
@@ -39258,6 +39577,7 @@ function loadProjectConfig3() {
39258
39577
  resolveVariantGuidance,
39259
39578
  retireSpecialists,
39260
39579
  rewriteReferences,
39580
+ runWithHostedLookup,
39261
39581
  runWithProjectBinding,
39262
39582
  runWithProjectRoot,
39263
39583
  sameContract,
@@ -39279,6 +39599,8 @@ function loadProjectConfig3() {
39279
39599
  stepFieldsFor,
39280
39600
  stepGraph,
39281
39601
  syncContextFiles,
39602
+ technologyName,
39603
+ technologyTokens,
39282
39604
  toMarkdown,
39283
39605
  typeBindingOf,
39284
39606
  typeGenericParameters,