@wairon/cli 5.1.1-dev.12 → 5.1.1-dev.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -65,7 +65,7 @@ var init_defaults = __esm({
65
65
  copilot: ".github/prompts",
66
66
  codex: ".codex/agents"
67
67
  };
68
- WAIRON_VERSION = "5.1.1-dev.12";
68
+ WAIRON_VERSION = "5.1.1-dev.13";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  ARCHITECT_AGENT_ID = "agent-architect";
71
71
  ARCHITECT_TEMPLATE_ID = "architect";
@@ -3554,17 +3554,43 @@ function isExternalNamespaceRef(ctx, ref) {
3554
3554
  if (sep9 === -1) return false;
3555
3555
  return !ctx.subsystemIds.has(ref.slice(0, sep9));
3556
3556
  }
3557
+ function isProvidedBy(snapshot, provider) {
3558
+ return snapshot.projectName === provider || snapshot.projectName.split("::").pop() === provider;
3559
+ }
3560
+ function sameContract(a, b) {
3561
+ return a.component.split("::").pop() === b.component.split("::").pop() && JSON.stringify(a.methods) === JSON.stringify(b.methods) && JSON.stringify(a.dispatch ?? []) === JSON.stringify(b.dispatch ?? []);
3562
+ }
3557
3563
  function resolveSurfaceRef(ctx, ref, fromSubsystem) {
3558
- const local = ref.split("::").filter((seg) => seg && seg !== "super").pop();
3559
- if (!local) return null;
3564
+ const segments = ref.split("::").filter((seg) => seg && seg !== "super");
3565
+ const local = segments.pop();
3566
+ if (!local) return { kind: "unresolved" };
3567
+ const provider = segments.pop();
3560
3568
  const mountPools = fromSubsystem ? enclosingMounts(ctx, fromSubsystem).reverse().map((ns) => ctx.mountSurfaceSnapshots.find((m) => m.namespace === ns)?.snapshots ?? []) : [];
3561
3569
  for (const pool of [...mountPools, ctx.surfaceSnapshots]) {
3570
+ const candidates = [];
3562
3571
  for (const snapshot of pool) {
3572
+ if (provider !== void 0 && !isProvidedBy(snapshot, provider)) continue;
3563
3573
  const entry = snapshot.interfaces.find((e) => e.component === local || e.id === local);
3564
- if (entry) return { snapshot, entry };
3574
+ if (entry) candidates.push({ snapshot, entry });
3565
3575
  }
3576
+ if (candidates.length === 0) continue;
3577
+ const [first] = candidates;
3578
+ if (candidates.every((c) => sameContract(c.entry, first.entry))) {
3579
+ return { kind: "resolved", ...first };
3580
+ }
3581
+ return { kind: "ambiguous", providers: [...new Set(candidates.map((c) => c.snapshot.projectName))] };
3566
3582
  }
3567
- return null;
3583
+ return { kind: "unresolved" };
3584
+ }
3585
+ function reportAmbiguousSurfaceRef(ctx, subject, ref, providers, specId, isDraftContext) {
3586
+ const local = ref.split("::").filter((seg) => seg && seg !== "super").pop() ?? ref;
3587
+ ctx.addIssue(
3588
+ "error",
3589
+ "SURFACE_REF_AMBIGUOUS",
3590
+ `${subject} cross-tree component "${ref}", which the surface snapshots of ${providers.map((p) => `"${p}"`).join(", ")} expose with different contracts \u2014 the reference matches more than one declared contract, so none of them can judge it. Name the provider it means (super::<provider>::${local}), or remove the snapshot that no longer applies.`,
3591
+ specId,
3592
+ isDraftContext
3593
+ );
3568
3594
  }
3569
3595
  function enclosingMounts(ctx, subsystemId) {
3570
3596
  const mounts = [];
@@ -3655,6 +3681,7 @@ var init_contracts = __esm({
3655
3681
  { code: "INVALID_TARGET_COMPONENT_REFERENCE", defaultSeverity: "error", summary: "Call/register step targets a non-existent component" },
3656
3682
  { code: "CROSS_TREE_REF_UNRESOLVED", defaultSeverity: "warning", summary: "Cross-tree reference (super::/:: form) with no surface snapshot covering it \u2014 only the parent project can verify it" },
3657
3683
  { code: "SURFACE_REF_NOT_EXPOSED", defaultSeverity: "error", summary: "Cross-tree reference resolves to a surface snapshot that does not expose the called method/capability" },
3684
+ { code: "SURFACE_REF_AMBIGUOUS", defaultSeverity: "error", summary: "Cross-tree call/dispatch/register target matched by surface snapshots of several providers with different contracts" },
3658
3685
  { code: "UNDECLARED_DEPENDENCY_CALL", defaultSeverity: "error", summary: "Call step targets a component the caller does not depend on or own" },
3659
3686
  { code: "INVALID_TARGET_METHOD_REFERENCE", defaultSeverity: "error", summary: "Call step targets a method not on any target interface" },
3660
3687
  { code: "NARRATIVE_SEMANTIC_UNBACKED", defaultSeverity: "warning", summary: "Narrative asserts a guarantee the called contract does not declare" }
@@ -3709,7 +3736,18 @@ var init_contracts = __esm({
3709
3736
  if (!dispatchTarget) {
3710
3737
  if (isCrossTreeForm || isCollapsedForm) {
3711
3738
  const resolved = resolveSurfaceRef(ctx, step.targetComponent, fromSubsystem);
3712
- if (resolved) {
3739
+ if (resolved.kind === "ambiguous") {
3740
+ reportAmbiguousSurfaceRef(
3741
+ ctx,
3742
+ `Method "${implMethod.name}" in implementation "${impl.id}" dispatches (step ${step.stepNumber}) through`,
3743
+ step.targetComponent,
3744
+ resolved.providers,
3745
+ impl.id,
3746
+ isDraftCtx
3747
+ );
3748
+ continue;
3749
+ }
3750
+ if (resolved.kind === "resolved") {
3713
3751
  if (step.capability && !(resolved.entry.dispatch ?? []).some((b) => b.capability === step.capability)) {
3714
3752
  ctx.addIssue(
3715
3753
  "error",
@@ -3769,7 +3807,18 @@ var init_contracts = __esm({
3769
3807
  if (!targetComp) {
3770
3808
  if (isCrossTreeForm || isCollapsedForm) {
3771
3809
  const resolved = resolveSurfaceRef(ctx, step.targetComponent, fromSubsystem);
3772
- if (resolved) {
3810
+ if (resolved.kind === "ambiguous") {
3811
+ reportAmbiguousSurfaceRef(
3812
+ ctx,
3813
+ `Method "${implMethod.name}" in implementation "${impl.id}" ${verb} "${step.targetMethod}" (step ${step.stepNumber}) on`,
3814
+ step.targetComponent,
3815
+ resolved.providers,
3816
+ impl.id,
3817
+ isDraftCtx
3818
+ );
3819
+ continue;
3820
+ }
3821
+ if (resolved.kind === "resolved") {
3773
3822
  const surfaceMethod = resolved.entry.methods.find((m) => m.name === step.targetMethod);
3774
3823
  if (!surfaceMethod) {
3775
3824
  ctx.addIssue(
@@ -5284,6 +5333,7 @@ var init_stereotype_deps = __esm({
5284
5333
  codes: [
5285
5334
  { code: "INVALID_DEPENDENCY_REFERENCE", defaultSeverity: "error", summary: "dependsOn names a non-existent component" },
5286
5335
  { code: "CROSS_TREE_REF_UNRESOLVED", defaultSeverity: "warning", summary: "Cross-tree dependsOn (super::/:: form) with no surface snapshot covering it" },
5336
+ { code: "SURFACE_REF_AMBIGUOUS", defaultSeverity: "error", summary: "Cross-tree dependsOn matched by surface snapshots of several providers with different contracts" },
5287
5337
  { code: "CROSS_SUBSYSTEM_NON_ADAPTER", defaultSeverity: "error", summary: "Non-Adapter component crossing a subsystem boundary" },
5288
5338
  { code: "CROSS_SUBSYSTEM_PRIVATE_ACCESS", defaultSeverity: "error", summary: "Cross-subsystem dependency on an unpublished component" },
5289
5339
  { code: "CROSS_SUBSYSTEM_TARGET_NON_PORTAL", defaultSeverity: "error", summary: "Cross-subsystem hop entering through a non-Portal" },
@@ -5307,7 +5357,11 @@ var init_stereotype_deps = __esm({
5307
5357
  const external = isExternalNamespaceRef(ctx, depId);
5308
5358
  if (external || isCollapsedCrossTreeRef(ctx, depId, comp.subsystem)) {
5309
5359
  const resolved = resolveSurfaceRef(ctx, depId, comp.subsystem);
5310
- if (resolved) {
5360
+ if (resolved.kind === "ambiguous") {
5361
+ reportAmbiguousSurfaceRef(ctx, `Component "${comp.id}" depends on`, depId, resolved.providers, comp.id, isDraftCtx);
5362
+ continue;
5363
+ }
5364
+ if (resolved.kind === "resolved") {
5311
5365
  if (comp.componentType !== "Adapter") {
5312
5366
  ctx.addIssue(
5313
5367
  "error",
@@ -9009,8 +9063,8 @@ function doctrineIdentity(doctrine, gate) {
9009
9063
  )
9010
9064
  };
9011
9065
  }
9012
- function hashGateState(doctrine, gate = {}) {
9013
- const payload = { tree: loadTree(), doctrine: doctrineIdentity(doctrine, gate) };
9066
+ function hashGateState(doctrine, inputs, gate = {}) {
9067
+ const payload = { tree: loadTree(), doctrine: doctrineIdentity(doctrine, gate), inputs: [...inputs].sort() };
9014
9068
  const digest2 = crypto2.createHash("sha256").update(canonicalize(payload)).digest("hex");
9015
9069
  return { algorithm: GATE_ALGORITHM, digest: digest2 };
9016
9070
  }
@@ -9041,7 +9095,7 @@ var init_statehash = __esm({
9041
9095
  init_specs2();
9042
9096
  init_rules();
9043
9097
  CONTENT_ALGORITHM = "sha256";
9044
- GATE_ALGORITHM = "sha256+doctrine";
9098
+ GATE_ALGORITHM = "sha256+doctrine+inputs";
9045
9099
  }
9046
9100
  });
9047
9101
 
@@ -14686,15 +14740,18 @@ function importSurface(sourcePath, origin) {
14686
14740
  saveSnapshot(snapshot);
14687
14741
  return snapshot;
14688
14742
  }
14689
- function pinFamilySurfaces() {
14690
- const parent = resolveChainingParent();
14691
- if (!parent) return null;
14692
- const childRoot = getProjectRoot();
14693
- const projected = runWithProjectRoot(parent.parentRoot, () => {
14743
+ function projectFamilySurfaces(parent) {
14744
+ return runWithProjectRoot(parent.parentRoot, () => {
14694
14745
  invalidateSpecCache();
14695
14746
  const siblings = loadSubsystemSpecs().filter((s) => !s.id.includes("::") && s.id !== parent.subsystemId);
14696
14747
  return [projectChildSurface(), ...siblings.map((s) => projectSubsystemSurface(s.id))];
14697
14748
  });
14749
+ }
14750
+ function pinFamilySurfaces() {
14751
+ const parent = resolveChainingParent();
14752
+ if (!parent) return null;
14753
+ const childRoot = getProjectRoot();
14754
+ const projected = projectFamilySurfaces(parent);
14698
14755
  const before = new Map(listSnapshots(childRoot).map((s) => [s.projectName, surfaceContentKey(s)]));
14699
14756
  const changed = [];
14700
14757
  for (const snapshot of projected) {
@@ -14705,17 +14762,21 @@ function pinFamilySurfaces() {
14705
14762
  }
14706
14763
  return changed;
14707
14764
  }
14708
- function computeParentStateId(parentRoot) {
14709
- return computeStateIdAt(parentRoot);
14765
+ function projectedFamilyContent(parent) {
14766
+ try {
14767
+ return new Map(projectFamilySurfaces(parent).map((s) => [s.projectName, surfaceContentKey(s)]));
14768
+ } catch {
14769
+ return null;
14770
+ }
14710
14771
  }
14711
14772
  function listExternalInterfaces() {
14712
14773
  const snapshots = listSnapshots();
14713
14774
  const chainingParent = resolveChainingParent();
14714
- const parentStateId = chainingParent ? computeParentStateId(chainingParent.parentRoot) : null;
14775
+ const projected = chainingParent ? projectedFamilyContent(chainingParent) : null;
14715
14776
  return snapshots.map((snapshot) => {
14716
14777
  const generated = snapshot.origin === "generated";
14717
14778
  const sourceKind = !generated ? "foreign" : snapshot.projectName.includes("::") ? "sibling" : "parent";
14718
- const freshness = generated && parentStateId ? snapshot.stateId === parentStateId ? "fresh" : "stale" : "unverifiable";
14779
+ const freshness = generated && projected ? projected.get(snapshot.projectName) === surfaceContentKey(snapshot) ? "fresh" : "stale" : "unverifiable";
14719
14780
  return {
14720
14781
  projectName: snapshot.projectName,
14721
14782
  origin: snapshot.origin,
@@ -15075,7 +15136,7 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
15075
15136
  rule.check(ctx);
15076
15137
  }
15077
15138
  const unresolved = (i) => RESOLUTION_FAILURE_CODES.has(i.code) && !i.surfaceResolved;
15078
- const chainingParent = crossTree !== "off" && issues.some(unresolved) ? findChainingParent(getProjectRoot()) : null;
15139
+ const chainingParent = crossTree !== "off" && issues.some(unresolved) ? resolveChainingParent() : null;
15079
15140
  const resolution = chainingParent ? resolveThroughParent(getProjectRoot(), treatAllAsComplete, rules) : null;
15080
15141
  if (resolution) {
15081
15142
  const judgedByParent = worstByKey(resolution.issues);
@@ -15113,7 +15174,7 @@ function resolveThroughParent(boundRoot, treatAllAsComplete, childRules) {
15113
15174
  const chain = [];
15114
15175
  let top = path18.resolve(boundRoot);
15115
15176
  while (top !== ceiling) {
15116
- const hop = findChainingParent(top);
15177
+ const hop = findChainingParent(top, ceiling);
15117
15178
  if (!hop) break;
15118
15179
  const next = path18.resolve(hop.parentRoot);
15119
15180
  if (ceiling && !isWithinOrEqual(ceiling, next)) break;
@@ -15788,6 +15849,7 @@ __export(specs_exports, {
15788
15849
  getSubprojectPrefix: () => getSubprojectPrefix,
15789
15850
  getSubsystemPath: () => getSubsystemPath,
15790
15851
  getTypePath: () => getTypePath,
15852
+ inspectChainedRoots: () => inspectChainedRoots,
15791
15853
  invalidateSpecCache: () => invalidateSpecCache,
15792
15854
  listChainedRoots: () => listChainedRoots,
15793
15855
  loadComponentSpec: () => loadComponentSpec,
@@ -15805,6 +15867,7 @@ __export(specs_exports, {
15805
15867
  loadTypeSpecs: () => loadTypeSpecs,
15806
15868
  normalizeComponentLayout: () => normalizeComponentLayout,
15807
15869
  readLockState: () => readLockState,
15870
+ rebaseReference: () => rebaseReference,
15808
15871
  resolveChainingParent: () => resolveChainingParent,
15809
15872
  resolveSubprojectForNamespace: () => resolveSubprojectForNamespace,
15810
15873
  restoreSpecFiles: () => restoreSpecFiles,
@@ -15904,33 +15967,75 @@ function relativizeId(id, prefix) {
15904
15967
  if (common === idParts.length) common--;
15905
15968
  return `${"super::".repeat(prefixParts.length - common)}${idParts.slice(common).join("::")}`;
15906
15969
  }
15970
+ function rebaseReference(ref, mount, direction, isMoved = () => false) {
15971
+ if (!ref || ref.startsWith("::")) return ref;
15972
+ const segments = ref.split("::");
15973
+ let hops = 0;
15974
+ while (segments[hops] === "super") hops++;
15975
+ if (hops === segments.length) return ref;
15976
+ const outer = Array.from({ length: hops + 1 }, (_, i) => `<n${i}>`).join("::");
15977
+ const inner = `${outer}::${mount}`;
15978
+ const [from, to] = direction === "into" ? [outer, inner] : [inner, outer];
15979
+ const target = qualifyId(ref, from, NO_ROOT_SUBSYSTEMS);
15980
+ const left = relativizeId(target, from);
15981
+ const travels = !left.startsWith("super::") && (direction === "outOf" || isMoved(left));
15982
+ return relativizeId(travels ? qualifyId(left, to, NO_ROOT_SUBSYSTEMS) : target, to);
15983
+ }
15907
15984
  function isWithin(dir, file) {
15908
15985
  const d = path19.resolve(dir);
15909
15986
  const f = path19.resolve(file);
15910
15987
  return f === d || f.startsWith(d + path19.sep);
15911
15988
  }
15989
+ function canonicalPath(target) {
15990
+ let existing = path19.resolve(target);
15991
+ const rest = [];
15992
+ for (; ; ) {
15993
+ try {
15994
+ fs13.lstatSync(existing);
15995
+ break;
15996
+ } catch {
15997
+ const up = path19.dirname(existing);
15998
+ if (up === existing) return path19.resolve(target);
15999
+ rest.unshift(path19.basename(existing));
16000
+ existing = up;
16001
+ }
16002
+ }
16003
+ try {
16004
+ return path19.join(fs13.realpathSync.native(existing), ...rest);
16005
+ } catch {
16006
+ return null;
16007
+ }
16008
+ }
16009
+ function chainDirKey(dir) {
16010
+ return canonicalPath(dir) ?? path19.resolve(dir);
16011
+ }
15912
16012
  function projectPathEscapesRoot(projectRoot2, projectPath, resolvedChildDir) {
15913
- return path19.isAbsolute(projectPath) || !isWithin(projectRoot2, resolvedChildDir);
16013
+ if (path19.isAbsolute(projectPath) || !isWithin(projectRoot2, resolvedChildDir)) return true;
16014
+ const root = canonicalPath(projectRoot2);
16015
+ const child = canonicalPath(resolvedChildDir);
16016
+ return root === null || child === null || !isWithin(root, child);
15914
16017
  }
15915
16018
  function assertContainedProjectPath(projectRoot2, projectPath) {
15916
16019
  const root = path19.resolve(projectRoot2);
15917
16020
  const resolved = path19.resolve(root, projectPath);
15918
16021
  if (projectPathEscapesRoot(root, projectPath, resolved)) {
15919
16022
  throw new Error(
15920
- `projectPath "${projectPath}" must resolve within the project root "${root}", but resolves to "${resolved}"; absolute paths and ../-escaping paths are rejected so a chained subproject is always contained by its parent.`
16023
+ `projectPath "${projectPath}" must resolve within the project root "${root}", but resolves to "${resolved}"; absolute, ../-escaping and link-escaping paths are rejected so a chained subproject is always contained by its parent.`
15921
16024
  );
15922
16025
  }
15923
16026
  return resolved;
15924
16027
  }
15925
- function findChainingParent(childRoot) {
16028
+ function findChainingParent(childRoot, ceiling) {
15926
16029
  let childResolved;
15927
16030
  try {
15928
16031
  childResolved = path19.resolve(childRoot);
15929
16032
  } catch {
15930
16033
  return null;
15931
16034
  }
16035
+ const bound = ceiling ? path19.resolve(ceiling) : void 0;
15932
16036
  let dir = path19.dirname(childResolved);
15933
16037
  for (let hops = 0; hops < 32; hops++) {
16038
+ if (bound && !isWithin(bound, dir)) break;
15934
16039
  const specsDir = aiPathsAt(dir).specsDir();
15935
16040
  if (pathExists(specsDir)) {
15936
16041
  for (const file of listFilesRecursive(specsDir, ".yaml")) {
@@ -15944,7 +16049,8 @@ function findChainingParent(childRoot) {
15944
16049
  const projectPath = raw.projectPath;
15945
16050
  if (typeof projectPath === "string" && projectPath.trim() !== "") {
15946
16051
  try {
15947
- if (path19.resolve(dir, projectPath) === childResolved) {
16052
+ const mountDir = path19.resolve(dir, projectPath);
16053
+ if (mountDir === childResolved && !projectPathEscapesRoot(dir, projectPath, mountDir)) {
15948
16054
  const id = raw.id;
15949
16055
  return { parentRoot: dir, subsystemId: typeof id === "string" ? id : "?" };
15950
16056
  }
@@ -15960,12 +16066,11 @@ function findChainingParent(childRoot) {
15960
16066
  }
15961
16067
  return null;
15962
16068
  }
15963
- function listChainedRoots(rootDir = getProjectRoot()) {
16069
+ function inspectChainedRoots(rootDir = getProjectRoot()) {
15964
16070
  const root = path19.resolve(rootDir);
15965
- const found = [];
15966
- const visited = /* @__PURE__ */ new Set([root]);
15967
- const walk = (projectDir, depth) => {
15968
- if (depth > 32) return;
16071
+ const inspection = { roots: [], skipped: [] };
16072
+ const listed = /* @__PURE__ */ new Set();
16073
+ const walk = (projectDir, prefix, ancestors, depth) => {
15969
16074
  const specsDir = aiPathsAt(projectDir).specsDir();
15970
16075
  if (!pathExists(specsDir)) return;
15971
16076
  for (const file of listFilesRecursive(specsDir, ".yaml")) {
@@ -15976,24 +16081,37 @@ function listChainedRoots(rootDir = getProjectRoot()) {
15976
16081
  continue;
15977
16082
  }
15978
16083
  if (!raw || typeof raw !== "object" || !("parentSystem" in raw)) continue;
15979
- const projectPath = raw.projectPath;
16084
+ const { id, projectPath } = raw;
15980
16085
  if (typeof projectPath !== "string" || projectPath.trim() === "") continue;
16086
+ const localId = typeof id === "string" ? id : "?";
15981
16087
  let childDir;
15982
16088
  try {
15983
16089
  childDir = path19.resolve(projectDir, projectPath);
15984
16090
  } catch {
15985
16091
  continue;
15986
16092
  }
15987
- if (projectPathEscapesRoot(root, projectPath, childDir)) continue;
15988
- if (visited.has(childDir)) continue;
15989
- if (!fs13.existsSync(childDir)) continue;
15990
- visited.add(childDir);
15991
- found.push(path19.relative(root, childDir).split(path19.sep).join("/"));
15992
- walk(childDir, depth + 1);
16093
+ let reason;
16094
+ if (projectPathEscapesRoot(projectDir, projectPath, childDir)) reason = "escapes";
16095
+ else if (ancestors.has(chainDirKey(childDir))) reason = "cyclic";
16096
+ else if (!fs13.existsSync(childDir)) reason = "missing";
16097
+ else if (depth >= 32) reason = "too-deep";
16098
+ if (reason) {
16099
+ const mount = prefix ? qualifyDeclaredId(localId, prefix, true) : localId;
16100
+ inspection.skipped.push({ mount, projectPath, reason });
16101
+ continue;
16102
+ }
16103
+ const key = chainDirKey(childDir);
16104
+ if (listed.has(key)) continue;
16105
+ listed.add(key);
16106
+ inspection.roots.push(path19.relative(root, childDir).split(path19.sep).join("/"));
16107
+ walk(childDir, prefix ? `${prefix}::${localId}` : localId, /* @__PURE__ */ new Set([...ancestors, key]), depth + 1);
15993
16108
  }
15994
16109
  };
15995
- walk(root, 0);
15996
- return found;
16110
+ walk(root, "", /* @__PURE__ */ new Set([chainDirKey(root)]), 0);
16111
+ return inspection;
16112
+ }
16113
+ function listChainedRoots(rootDir = getProjectRoot()) {
16114
+ return inspectChainedRoots(rootDir).roots;
15997
16115
  }
15998
16116
  function mergeMountRealizations(subs) {
15999
16117
  const result = [];
@@ -16276,12 +16394,24 @@ function buildProjectGraph(level) {
16276
16394
  function resolveChainingParent() {
16277
16395
  const reach = getRequestParentReach();
16278
16396
  if (reach && !reach.parentReach) return null;
16279
- const parent = findChainingParent(getProjectRoot());
16280
- if (parent && reach?.topRoot) {
16281
- const fromTop = path19.relative(path19.resolve(reach.topRoot), path19.resolve(parent.parentRoot));
16282
- if (fromTop === ".." || fromTop.startsWith(`..${path19.sep}`) || path19.isAbsolute(fromTop)) return null;
16397
+ return findChainingParent(getProjectRoot(), reach?.topRoot);
16398
+ }
16399
+ function snapshotInputKey(snapshot) {
16400
+ const { stateId, generatedAt, origin, ...content } = snapshot;
16401
+ return canonicalize(content);
16402
+ }
16403
+ function consumedSurfaceInputsAt(rootDir) {
16404
+ const dir = path19.join(rootDir, ".wai", "surfaces");
16405
+ if (!pathExists(dir)) return [];
16406
+ const keys = [];
16407
+ for (const file of fs13.readdirSync(dir)) {
16408
+ if (!file.endsWith(".yaml") && !file.endsWith(".yml")) continue;
16409
+ try {
16410
+ keys.push(snapshotInputKey(SurfaceSnapshotSchema.parse(readYamlFile(path19.join(dir, file)))));
16411
+ } catch {
16412
+ }
16283
16413
  }
16284
- return parent;
16414
+ return keys;
16285
16415
  }
16286
16416
  function computeGateStateId() {
16287
16417
  let gate = {};
@@ -16290,7 +16420,10 @@ function computeGateStateId() {
16290
16420
  gate = { projectType: config.projectType, rules: config.rules };
16291
16421
  } catch {
16292
16422
  }
16293
- return hashGateState(loadProjectExtensions(), gate);
16423
+ const root = getProjectRoot();
16424
+ const roots = [root, ...listChainedRoots(root).map((rel2) => path19.join(root, rel2))];
16425
+ const inputs = roots.flatMap(consumedSurfaceInputsAt);
16426
+ return hashGateState(loadProjectExtensions(), inputs, gate);
16294
16427
  }
16295
16428
  function readLockState() {
16296
16429
  const record2 = readLockRecord();
@@ -16400,7 +16533,7 @@ var init_specs2 = __esm({
16400
16533
  this.rootSubsystems.clear();
16401
16534
  this.cachedRecursive = recursive;
16402
16535
  this.scanVisitedSpecDirs = [];
16403
- const visited = /* @__PURE__ */ new Set([path19.resolve(this.rootDir)]);
16536
+ const visited = /* @__PURE__ */ new Set([chainDirKey(this.rootDir)]);
16404
16537
  const maxDepth = typeof recursive === "number" ? recursive : recursive ? Infinity : 0;
16405
16538
  this.cachedIndex = this.scanSpecsForProject(this.rootDir, "", visited, maxDepth, 0);
16406
16539
  this.cachedSpecDirs = this.scanVisitedSpecDirs;
@@ -16600,21 +16733,22 @@ var init_specs2 = __esm({
16600
16733
  if (currentDepth < maxDepth) {
16601
16734
  for (const subproj of localSubprojects) {
16602
16735
  const childDir = path19.resolve(projectDir, subproj.projectPath);
16603
- if (projectPathEscapesRoot(this.rootDir, subproj.projectPath, childDir)) {
16736
+ const mountId = namespacePrefix ? qualifyDeclaredId(subproj.subsystemId, namespacePrefix, true) : subproj.subsystemId;
16737
+ if (projectPathEscapesRoot(projectDir, subproj.projectPath, childDir)) {
16604
16738
  this.loaderIssues.push({
16605
16739
  severity: "error",
16606
16740
  code: "PROJECTPATH_ESCAPE",
16607
- message: `Subproject path "${subproj.projectPath}" declared by subsystem "${subproj.subsystemId}" escapes the project root "${this.rootDir}" (resolves to "${childDir}"); absolute and ../-escaping projectPaths are rejected. Skipping this subproject.`,
16608
- specId: subproj.subsystemId
16741
+ message: `Subproject path "${subproj.projectPath}" declared by subsystem "${mountId}" escapes the root of the project declaring it, "${projectDir}" (resolves to "${childDir}"); absolute, ../-escaping and link-escaping projectPaths are rejected. Skipping this subproject.`,
16742
+ specId: mountId
16609
16743
  });
16610
16744
  continue;
16611
16745
  }
16612
- if (visitedDirs.has(childDir)) {
16746
+ if (visitedDirs.has(chainDirKey(childDir))) {
16613
16747
  this.loaderIssues.push({
16614
16748
  severity: "error",
16615
16749
  code: "CIRCULAR_SUBPROJECT_REFERENCE",
16616
- message: `Circular reference detected: Subsystem "${subproj.subsystemId}" refers to subproject "${childDir}" which is already loaded.`,
16617
- specId: subproj.subsystemId
16750
+ message: `Circular reference detected: Subsystem "${mountId}" refers to subproject "${childDir}" which is already loaded.`,
16751
+ specId: mountId
16618
16752
  });
16619
16753
  continue;
16620
16754
  }
@@ -16622,14 +16756,14 @@ var init_specs2 = __esm({
16622
16756
  this.loaderIssues.push({
16623
16757
  severity: "error",
16624
16758
  code: "SUBPROJECT_NOT_FOUND",
16625
- message: `Subproject directory "${childDir}" declared by subsystem "${subproj.subsystemId}" does not exist.`,
16626
- specId: subproj.subsystemId
16759
+ message: `Subproject directory "${childDir}" declared by subsystem "${mountId}" does not exist.`,
16760
+ specId: mountId
16627
16761
  });
16628
16762
  continue;
16629
16763
  }
16630
16764
  const childNamespace = namespacePrefix ? `${namespacePrefix}::${subproj.subsystemId}` : subproj.subsystemId;
16631
16765
  const newVisited = new Set(visitedDirs);
16632
- newVisited.add(childDir);
16766
+ newVisited.add(chainDirKey(childDir));
16633
16767
  const childIndex = this.scanSpecsForProject(childDir, childNamespace, newVisited, maxDepth, currentDepth + 1);
16634
16768
  index.subsystems.push(...childIndex.subsystems);
16635
16769
  index.components.push(...childIndex.components);
@@ -16662,11 +16796,11 @@ var init_specs2 = __esm({
16662
16796
  const sub = index.subsystems.find((s) => s.id === currentPrefix);
16663
16797
  if (sub && sub.projectPath) {
16664
16798
  const nextDir = path19.resolve(currentDir, sub.projectPath);
16665
- if (projectPathEscapesRoot(this.rootDir, sub.projectPath, nextDir)) {
16799
+ if (projectPathEscapesRoot(currentDir, sub.projectPath, nextDir)) {
16666
16800
  this.loaderIssues.push({
16667
16801
  severity: "error",
16668
16802
  code: "PROJECTPATH_ESCAPE",
16669
- message: `Subproject path "${sub.projectPath}" declared by subsystem "${currentPrefix}" escapes the project root "${this.rootDir}" (resolves to "${nextDir}"); absolute and ../-escaping projectPaths are rejected. Skipping this subproject.`,
16803
+ message: `Subproject path "${sub.projectPath}" declared by subsystem "${currentPrefix}" escapes the root of the project declaring it, "${currentDir}" (resolves to "${nextDir}"); absolute, ../-escaping and link-escaping projectPaths are rejected. Skipping this subproject.`,
16670
16804
  specId: currentPrefix
16671
16805
  });
16672
16806
  continue;
@@ -18310,6 +18444,7 @@ function externalizeSubsystem(subsystemId, projectPath) {
18310
18444
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
18311
18445
  });
18312
18446
  rewriteRefsInDir(parentSpecsDir, renameMap, fooDir);
18447
+ rebaseMovedRefs(childFooDir, subsystemId, "into");
18313
18448
  invalidateSpecCache();
18314
18449
  }
18315
18450
  function internalizeSubsystem(subsystemId) {
@@ -18349,6 +18484,7 @@ function internalizeSubsystem(subsystemId) {
18349
18484
  });
18350
18485
  fs14.rmSync(childWai, { recursive: true, force: true });
18351
18486
  rewriteRefsInDir(parentSpecsDir, renameMap, fooDir);
18487
+ rebaseMovedRefs(fooDir, subsystemId, "outOf");
18352
18488
  invalidateSpecCache();
18353
18489
  }
18354
18490
  function buildRenameMap(subsystemId, externalize) {
@@ -18375,7 +18511,27 @@ function buildRenameMap(subsystemId, externalize) {
18375
18511
  }
18376
18512
  function rewriteRefsInDir(specsDir, renameMap, excludeDir) {
18377
18513
  if (renameMap.size === 0) return;
18378
- const remap = (id) => id !== void 0 && renameMap.has(id) ? renameMap.get(id) : id;
18514
+ rewriteRefFields(specsDir, (ref) => renameMap.get(ref) ?? ref, excludeDir);
18515
+ }
18516
+ function rebaseMovedRefs(movedDir, mount, direction) {
18517
+ const declared = componentIdsUnder(movedDir);
18518
+ rewriteRefFields(movedDir, (ref, position) => position === "component" ? rebaseReference(ref, mount, direction, (id) => declared.has(id)) : ref);
18519
+ }
18520
+ function componentIdsUnder(dir) {
18521
+ const ids = /* @__PURE__ */ new Set();
18522
+ for (const file of listFilesRecursive(dir, ".yaml")) {
18523
+ let raw;
18524
+ try {
18525
+ raw = readYamlFile(file);
18526
+ } catch {
18527
+ continue;
18528
+ }
18529
+ if (raw && typeof raw === "object" && "componentType" in raw && typeof raw.id === "string") ids.add(raw.id);
18530
+ }
18531
+ return ids;
18532
+ }
18533
+ function rewriteRefFields(specsDir, remap, excludeDir) {
18534
+ const at = (ref, position) => typeof ref === "string" ? remap(ref, position) : ref;
18379
18535
  for (const file of listFilesRecursive(specsDir, ".yaml")) {
18380
18536
  if (excludeDir && isWithinDir(excludeDir, file)) continue;
18381
18537
  let raw;
@@ -18387,14 +18543,14 @@ function rewriteRefsInDir(specsDir, renameMap, excludeDir) {
18387
18543
  if (!raw || typeof raw !== "object") continue;
18388
18544
  let changed = false;
18389
18545
  if ("componentType" in raw && Array.isArray(raw.dependsOn)) {
18390
- const next = raw.dependsOn.map((d) => remap(d));
18546
+ const next = raw.dependsOn.map((d) => at(d, "component"));
18391
18547
  if (next.some((v, i) => v !== raw.dependsOn[i])) {
18392
18548
  raw.dependsOn = next;
18393
18549
  changed = true;
18394
18550
  }
18395
18551
  if (Array.isArray(raw.dispatch)) {
18396
18552
  for (const b of raw.dispatch) {
18397
- const nc = remap(b.component);
18553
+ const nc = at(b.component, "component");
18398
18554
  if (nc !== b.component) {
18399
18555
  b.component = nc;
18400
18556
  changed = true;
@@ -18403,7 +18559,7 @@ function rewriteRefsInDir(specsDir, renameMap, excludeDir) {
18403
18559
  }
18404
18560
  } else if ("parentSystem" in raw && Array.isArray(raw.lifecycle)) {
18405
18561
  for (const le of raw.lifecycle) {
18406
- const nc = remap(le.component);
18562
+ const nc = at(le.component, "component");
18407
18563
  if (nc !== le.component) {
18408
18564
  le.component = nc;
18409
18565
  changed = true;
@@ -18413,7 +18569,7 @@ function rewriteRefsInDir(specsDir, renameMap, excludeDir) {
18413
18569
  for (const m of raw.methods) {
18414
18570
  if (!Array.isArray(m.params)) continue;
18415
18571
  for (const p of m.params) {
18416
- const nt = remap(p.type);
18572
+ const nt = at(p.type, "type");
18417
18573
  if (nt !== p.type) {
18418
18574
  p.type = nt;
18419
18575
  changed = true;
@@ -18424,7 +18580,7 @@ function rewriteRefsInDir(specsDir, renameMap, excludeDir) {
18424
18580
  for (const m of raw.methods) {
18425
18581
  if (!Array.isArray(m.narrative)) continue;
18426
18582
  for (const step of m.narrative) {
18427
- const nt = remap(step.targetComponent);
18583
+ const nt = at(step.targetComponent, "component");
18428
18584
  if (nt !== step.targetComponent) {
18429
18585
  step.targetComponent = nt;
18430
18586
  changed = true;
@@ -18433,7 +18589,7 @@ function rewriteRefsInDir(specsDir, renameMap, excludeDir) {
18433
18589
  }
18434
18590
  } else if ("kind" in raw && Array.isArray(raw.fields)) {
18435
18591
  for (const f of raw.fields) {
18436
- const nt = remap(f.type);
18592
+ const nt = at(f.type, "type");
18437
18593
  if (nt !== f.type) {
18438
18594
  f.type = nt;
18439
18595
  changed = true;
@@ -22822,8 +22978,34 @@ __export(server_exports, {
22822
22978
  captureBuildStamp: () => captureBuildStamp,
22823
22979
  createMcpServer: () => createMcpServer,
22824
22980
  isBuildStale: () => isBuildStale,
22825
- startMcpServer: () => startMcpServer
22981
+ startMcpServer: () => startMcpServer,
22982
+ statusFamilyContext: () => statusFamilyContext
22826
22983
  });
22984
+ function statusFamilyContext() {
22985
+ const lines = [];
22986
+ try {
22987
+ const parent = resolveChainingParent();
22988
+ if (parent) {
22989
+ let parentName;
22990
+ try {
22991
+ const system = readYamlFile(aiPathsAt(parent.parentRoot).specsSystem());
22992
+ if (typeof system?.name === "string") parentName = system.name;
22993
+ } catch {
22994
+ }
22995
+ lines.push(
22996
+ `Family: this project is a chained subproject, mounted as subsystem "${parent.subsystemId}" of ${parentName ? `the parent project "${parentName}"` : "its parent project"} \u2014 the surfaces it can consume are listed by sdd_list_external_interfaces.`
22997
+ );
22998
+ }
22999
+ const mounts = loadSubsystemSpecs().filter((s) => s.projectPath && !s.id.includes("::"));
23000
+ if (mounts.length > 0) {
23001
+ lines.push(`Family: chained subprojects mounted here \u2014 ${mounts.map((s) => `${s.id} (${s.projectPath})`).join(", ")}.`);
23002
+ }
23003
+ } catch {
23004
+ }
23005
+ return lines.length > 0 ? `${lines.join("\n")}
23006
+
23007
+ ` : "";
23008
+ }
22827
23009
  function requireLoader() {
22828
23010
  return loader_exports;
22829
23011
  }
@@ -24035,10 +24217,11 @@ NOTICE:
24035
24217
  },
24036
24218
  ({ subsystem, recursive }) => {
24037
24219
  try {
24038
- return text(getStatusReport({
24220
+ const report2 = getStatusReport({
24039
24221
  subsystem,
24040
24222
  recursive: recursive ?? true
24041
- }));
24223
+ });
24224
+ return text(`${statusFamilyContext()}${report2}`);
24042
24225
  } catch (e) {
24043
24226
  return errText(String(e));
24044
24227
  }
@@ -24166,8 +24349,10 @@ NOTICE:
24166
24349
  }
24167
24350
  }, hostedStub);
24168
24351
  reg(server, "sdd_host_export_tree", {
24169
- description: "Hosted spec-tree transfer: pack the BOUND project's WHOLE spec tree \u2014 its own .wai plus every chained subproject \u2014 into a .waitree archive, returned as base64 with its roots, file count and state id. The migration counterpart of an import: use it to take a hosted project local, or to move it to another instance. Requires project:read over the project. Bounded by the data-plane body cap; a very large tree exports through the web download route instead.",
24170
- inputSchema: {}
24352
+ description: "Hosted spec-tree transfer: pack the BOUND project's WHOLE spec tree \u2014 its own .wai plus every chained subproject \u2014 into a .waitree archive, returned as base64 with its roots, file count and state id. The migration counterpart of an import: use it to take a hosted project local, or to move it to another instance. Requires project:read over the project. Bounded by the data-plane body cap; a very large tree exports through the web download route instead. Refuses when a chained mount cannot be packed (escaping, missing, cyclic, too deep, or holding no .wai), naming every one and why, unless allowPartial is set \u2014 then the archive is built anyway and the result lists what was skipped.",
24353
+ inputSchema: {
24354
+ allowPartial: import_zod10.z.boolean().optional().describe("Build the archive even when some chained mounts cannot be packed, listing them as skipped; default false (refuse and name them)")
24355
+ }
24171
24356
  }, hostedStub);
24172
24357
  reg(server, "sdd_host_import_tree", {
24173
24358
  description: "Hosted spec-tree transfer: REPLACE the BOUND project's spec tree from a base64 .waitree archive. Requires project:admin over the project (strictly above project:write \u2014 this replaces the whole design, not one spec). Refuses an occupied destination unless replaceExisting is set, always refuses executable entries (rule/code packs install only through the trusted filesystem), and moves the previous tree aside to a backup whose path is returned.",
@@ -24230,6 +24415,7 @@ var init_server = __esm({
24230
24415
  path30 = __toESM(require("path"));
24231
24416
  import_url = require("url");
24232
24417
  init_fs();
24418
+ init_yaml();
24233
24419
  init_status();
24234
24420
  init_defaults();
24235
24421
  init_narrative_labels();
@@ -25359,18 +25545,29 @@ function discardStagingDir(stagingDir) {
25359
25545
  } catch {
25360
25546
  }
25361
25547
  }
25362
- function exportSpecTree(includeDerived) {
25548
+ function exportSpecTree(includeDerived, allowPartial) {
25363
25549
  const system = loadSystemSpec();
25364
25550
  if (!system) {
25365
25551
  throw new Error("no spec tree to export at this project root");
25366
25552
  }
25367
- const chained = listChainedRoots();
25553
+ const inspection = inspectChainedRoots();
25368
25554
  const root = getProjectRoot();
25369
- const candidates = [
25370
- { relativePath: ".", waiDir: aiPathsAt(root).root() },
25371
- ...chained.map((rel2) => ({ relativePath: rel2, waiDir: aiPathsAt(path28.resolve(root, rel2)).root() }))
25372
- ];
25373
- const roots = candidates.filter((source) => pathExists(source.waiDir));
25555
+ const skipped = [...inspection.skipped];
25556
+ const roots = [{ relativePath: ".", waiDir: aiPathsAt(root).root() }];
25557
+ for (const rel2 of inspection.roots) {
25558
+ const waiDir = aiPathsAt(path28.resolve(root, rel2)).root();
25559
+ if (pathExists(waiDir)) {
25560
+ roots.push({ relativePath: rel2, waiDir });
25561
+ } else {
25562
+ skipped.push({ mount: rel2, projectPath: rel2, reason: "no-spec-tree" });
25563
+ }
25564
+ }
25565
+ if (skipped.length > 0 && !allowPartial) {
25566
+ const detail = skipped.map((s) => `${s.mount} (${s.reason})`).join(", ");
25567
+ throw new Error(
25568
+ `cannot export the whole spec tree \u2014 skipped: ${detail}. Pass allowPartial to export the rest anyway.`
25569
+ );
25570
+ }
25374
25571
  const stateId = computeStateId();
25375
25572
  const built = buildTreeArchive2(roots, system.name, stateId.digest, includeDerived);
25376
25573
  const result = {
@@ -25379,7 +25576,8 @@ function exportSpecTree(includeDerived) {
25379
25576
  projectName: built.manifest.projectName,
25380
25577
  roots: built.manifest.roots,
25381
25578
  fileCount: built.fileCount,
25382
- stateId: stateId.digest
25579
+ stateId: stateId.digest,
25580
+ skipped
25383
25581
  };
25384
25582
  return result;
25385
25583
  }
@@ -30159,7 +30357,19 @@ function excludeLocalFiles() {
30159
30357
  const excludePath = path48.join(getProjectRoot(), ".git", "info", "exclude");
30160
30358
  try {
30161
30359
  fs36.mkdirSync(path48.dirname(excludePath), { recursive: true });
30162
- fs36.appendFileSync(excludePath, "\n.wai/lock.json\n.wai/git.json\n");
30360
+ const existing = fs36.existsSync(excludePath) ? fs36.readFileSync(excludePath, "utf8") : "";
30361
+ const lines = existing.split(/\r?\n/).filter((line2) => line2.length > 0);
30362
+ const hadLegacyLockExclusion = lines.some((line2) => line2.trim() === ".wai/lock.json");
30363
+ const kept = lines.filter((line2) => line2.trim() !== ".wai/lock.json");
30364
+ if (!kept.some((line2) => line2.trim() === ".wai/git.json")) {
30365
+ kept.push(".wai/git.json");
30366
+ }
30367
+ fs36.writeFileSync(excludePath, kept.join("\n") + "\n");
30368
+ if (hadLegacyLockExclusion) {
30369
+ console.warn(
30370
+ "[wairon git] removed a .wai/lock.json exclusion left by an earlier version \u2014 the approval it records is committed with the rest of .wai/ now, so it reaches the remote."
30371
+ );
30372
+ }
30163
30373
  } catch {
30164
30374
  }
30165
30375
  }
@@ -31131,13 +31341,13 @@ function listSecrets(_cfg, credential) {
31131
31341
  requireAdmin(credential);
31132
31342
  return listSecretKeys();
31133
31343
  }
31134
- function exportProjectTree(cfg, credential, project2, subproject, includeDerived) {
31344
+ function exportProjectTree(cfg, credential, project2, subproject, includeDerived, allowPartial) {
31135
31345
  const principal = requirePrincipal(cfg, credential);
31136
31346
  if (authorize(cfg.dataDir, principal, "project:read", "project", project2).value !== "yes") {
31137
31347
  throw new AdminAuthError("Forbidden \u2014 exporting a project's spec tree requires project:read over it");
31138
31348
  }
31139
31349
  const root = boundLifecycleRoot(cfg, project2, subproject);
31140
- return runWithProjectRoot(root, () => hostCore.exportSpecTree(includeDerived));
31350
+ return runWithProjectRoot(root, () => hostCore.exportSpecTree(includeDerived, allowPartial));
31141
31351
  }
31142
31352
  function importProjectTree(cfg, credential, project2, archive, subproject, replaceExisting) {
31143
31353
  const principal = requirePrincipal(cfg, credential);
@@ -35842,8 +36052,8 @@ function runPeriodicBackingSync(cfg) {
35842
36052
  }
35843
36053
 
35844
36054
  // src/server/projectops.ts
35845
- function exportProjectTree2(cfg, credential, project2, subproject, includeDerived) {
35846
- return exportProjectTree(cfg, credential, project2, subproject, includeDerived);
36055
+ function exportProjectTree2(cfg, credential, project2, subproject, includeDerived, allowPartial) {
36056
+ return exportProjectTree(cfg, credential, project2, subproject, includeDerived, allowPartial);
35847
36057
  }
35848
36058
  function importProjectTree2(cfg, credential, project2, archive, subproject, replaceExisting) {
35849
36059
  return importProjectTree(cfg, credential, project2, archive, subproject, replaceExisting);
@@ -39233,7 +39443,8 @@ function opsExportProjectTree(cfg, sessionId, url, res) {
39233
39443
  sessionId,
39234
39444
  q(url, "projectId") ?? "",
39235
39445
  void 0,
39236
- q(url, "includeDerived") === "1"
39446
+ q(url, "includeDerived") === "1",
39447
+ q(url, "allowPartial") === "1"
39237
39448
  );
39238
39449
  res.writeHead(200, {
39239
39450
  "content-type": "application/zip",
@@ -39999,6 +40210,18 @@ function requiredDataPlaneCapability(toolName) {
39999
40210
  if (WRITE_TOOL_PREFIXES.some((p) => toolName.startsWith(p))) return "project:write";
40000
40211
  return "project:write";
40001
40212
  }
40213
+ var TREE_SCOPED_HOST_TOOLS = /* @__PURE__ */ new Set([
40214
+ "sdd_host_lock_project",
40215
+ ...TREE_TRANSFER_TOOLS
40216
+ ]);
40217
+ function toolScope(toolName) {
40218
+ if (PROJECT_RECORD_TOOLS.has(toolName)) return "record";
40219
+ if (TREE_SCOPED_HOST_TOOLS.has(toolName)) return "tree";
40220
+ if (READ_TOOL_NAMES.has(toolName) || READ_TOOL_PREFIXES.some((p) => toolName.startsWith(p)) || WRITE_TOOL_PREFIXES.some((p) => toolName.startsWith(p))) {
40221
+ return "tree";
40222
+ }
40223
+ return void 0;
40224
+ }
40002
40225
  var MUTATING_HOST_TOOLS = /* @__PURE__ */ new Set([
40003
40226
  "sdd_host_initialize_project",
40004
40227
  "sdd_host_lock_project",
@@ -40039,7 +40262,11 @@ function subprojectConfinementError(projectId, subproject, body) {
40039
40262
  const msg = jsonRpcRequest(body);
40040
40263
  if (!msg || msg.method !== "tools/call") return void 0;
40041
40264
  const name = msg.params?.name;
40042
- if (typeof name !== "string" || !PROJECT_RECORD_TOOLS.has(name)) return void 0;
40265
+ if (typeof name !== "string") return void 0;
40266
+ const scope = toolScope(name);
40267
+ if (scope === "tree") return void 0;
40268
+ const bound = `${projectId}${SUBPROJECT_SEPARATOR}${subproject}`;
40269
+ const why = scope === "record" ? `${name} acts on the whole project "${projectId}", but this credential is bound to subproject "${bound}". ` : `${name} declares no scope, and this credential is bound to subproject "${bound}", which serves only tools that act on the bound tree. `;
40043
40270
  return {
40044
40271
  jsonrpc: "2.0",
40045
40272
  id: msg.id ?? null,
@@ -40047,7 +40274,7 @@ function subprojectConfinementError(projectId, subproject, body) {
40047
40274
  content: [
40048
40275
  {
40049
40276
  type: "text",
40050
- text: `Refused \u2014 ${name} acts on the whole project "${projectId}", but this credential is bound to subproject "${projectId}${SUBPROJECT_SEPARATOR}${subproject}". An unqualified credential for "${projectId}" is required to call ${name}.`
40277
+ text: `Refused \u2014 ${why}An unqualified credential for "${projectId}" is required to call ${name}.`
40051
40278
  }
40052
40279
  ],
40053
40280
  isError: true
@@ -40132,14 +40359,22 @@ async function dispatchProjectLifecycleTool(cfg, credential, projectId, body, su
40132
40359
  );
40133
40360
  break;
40134
40361
  case "sdd_host_export_tree": {
40135
- const exported = exportProjectTree2(cfg, credential, projectId, subproject);
40362
+ const exported = exportProjectTree2(
40363
+ cfg,
40364
+ credential,
40365
+ projectId,
40366
+ subproject,
40367
+ void 0,
40368
+ args.allowPartial === true
40369
+ );
40136
40370
  value = {
40137
40371
  projectName: exported.projectName,
40138
40372
  roots: exported.roots,
40139
40373
  fileCount: exported.fileCount,
40140
40374
  stateId: exported.stateId,
40141
40375
  suggestedFileName: exported.suggestedFileName,
40142
- archiveBase64: Buffer.from(exported.archive).toString("base64")
40376
+ archiveBase64: Buffer.from(exported.archive).toString("base64"),
40377
+ skipped: exported.skipped
40143
40378
  };
40144
40379
  break;
40145
40380
  }
@@ -40727,7 +40962,8 @@ async function routeAdmin(cfg, req, res) {
40727
40962
  return sendJson(res, 200, { ok: true });
40728
40963
  }
40729
40964
  if (req.method === "GET" && parts.length === 4 && parts[3] === "tree") {
40730
- const exported = exportProjectTree(cfg, cred, parts[2]);
40965
+ const allowPartial = url.searchParams.get("allowPartial") === "1";
40966
+ const exported = exportProjectTree(cfg, cred, parts[2], void 0, void 0, allowPartial);
40731
40967
  res.writeHead(200, {
40732
40968
  "content-type": "application/zip",
40733
40969
  "content-disposition": `attachment; filename="${exported.suggestedFileName}"`
@@ -42179,8 +42415,8 @@ init_provision();
42179
42415
  function composeAgentBrief3(agentId) {
42180
42416
  return composeAgentBrief(agentId);
42181
42417
  }
42182
- function exportSpecTree2(includeDerived) {
42183
- return exportSpecTree(includeDerived);
42418
+ function exportSpecTree2(includeDerived, allowPartial) {
42419
+ return exportSpecTree(includeDerived, allowPartial);
42184
42420
  }
42185
42421
  function importSpecTree2(archive, options) {
42186
42422
  return importSpecTree(archive, options);
@@ -42392,8 +42628,8 @@ async function callTool(target, name, args) {
42392
42628
  return raw;
42393
42629
  }
42394
42630
  }
42395
- async function exportRemoteTree(target) {
42396
- const payload = await callTool(target, "sdd_host_export_tree", {});
42631
+ async function exportRemoteTree(target, allowPartial) {
42632
+ const payload = await callTool(target, "sdd_host_export_tree", { allowPartial: allowPartial === true });
42397
42633
  if (!payload || typeof payload.archiveBase64 !== "string" || !payload.archiveBase64) {
42398
42634
  throw new WaironError(`The hosted export returned no archive for project "${target.projectId}".`);
42399
42635
  }
@@ -42402,7 +42638,8 @@ async function exportRemoteTree(target) {
42402
42638
  suggestedFileName: payload.suggestedFileName ?? `${target.projectId}.waitree`,
42403
42639
  projectName: payload.projectName ?? target.projectId,
42404
42640
  roots: payload.roots ?? ["."],
42405
- fileCount: payload.fileCount ?? 0
42641
+ fileCount: payload.fileCount ?? 0,
42642
+ skipped: payload.skipped ?? []
42406
42643
  };
42407
42644
  if (payload.stateId) result.stateId = payload.stateId;
42408
42645
  return result;
@@ -42462,7 +42699,7 @@ async function initializeRemoteProject(target, ownerUnitId) {
42462
42699
  return record2.summary ?? `created project "${record2.id ?? topProjectId(target.projectId)}"`;
42463
42700
  }
42464
42701
  async function pushTree(target, options = {}) {
42465
- const exported = exportSpecTree2(options.includeDerived);
42702
+ const exported = exportSpecTree2(options.includeDerived, options.allowPartial);
42466
42703
  const archivePath = writeArchiveIfAsked(exported.archive, options.archivePath, exported.suggestedFileName);
42467
42704
  let createdProject = false;
42468
42705
  if (options.createUnitId) {
@@ -42482,10 +42719,11 @@ async function pushTree(target, options = {}) {
42482
42719
  };
42483
42720
  if (imported.backupPath) result.backupPath = imported.backupPath;
42484
42721
  if (archivePath) result.archivePath = archivePath;
42722
+ if (exported.skipped.length > 0) result.skipped = exported.skipped;
42485
42723
  return result;
42486
42724
  }
42487
42725
  async function pullTree(target, options = {}) {
42488
- const exported = await exportRemoteTree(target);
42726
+ const exported = await exportRemoteTree(target, options.allowPartial);
42489
42727
  const archivePath = writeArchiveIfAsked(exported.archive, options.archivePath, exported.suggestedFileName);
42490
42728
  const imported = importSpecTree2(exported.archive, {
42491
42729
  replaceExisting: options.replaceExisting === true,
@@ -42502,6 +42740,7 @@ async function pullTree(target, options = {}) {
42502
42740
  };
42503
42741
  if (imported.backupPath) result.backupPath = imported.backupPath;
42504
42742
  if (archivePath) result.archivePath = archivePath;
42743
+ if (exported.skipped.length > 0) result.skipped = exported.skipped;
42505
42744
  return result;
42506
42745
  }
42507
42746
  function resolveTarget(root, overrides = {}) {
@@ -42601,7 +42840,8 @@ async function runRemote(action, options = {}) {
42601
42840
  assertProjectInitialized();
42602
42841
  const transfer = {
42603
42842
  replaceExisting: options.force === true,
42604
- includeDerived: options.includeDerived === true
42843
+ includeDerived: options.includeDerived === true,
42844
+ allowPartial: options.allowPartial === true
42605
42845
  };
42606
42846
  if (options.unit) transfer.createUnitId = options.unit;
42607
42847
  if (options.archive) transfer.archivePath = options.archive;
@@ -42609,7 +42849,10 @@ async function runRemote(action, options = {}) {
42609
42849
  return;
42610
42850
  }
42611
42851
  case "pull": {
42612
- const transfer = { replaceExisting: options.force === true };
42852
+ const transfer = {
42853
+ replaceExisting: options.force === true,
42854
+ allowPartial: options.allowPartial === true
42855
+ };
42613
42856
  if (options.archive) transfer.archivePath = options.archive;
42614
42857
  if (options.dir) transfer.destDir = path68.resolve(options.dir);
42615
42858
  report(await pullTree(target, transfer));
@@ -42718,6 +42961,11 @@ function report(outcome) {
42718
42961
  if (outcome.backupPath) {
42719
42962
  logger.info(`The replaced tree was backed up to ${import_chalk20.default.cyan(outcome.backupPath)} \u2014 restore it by moving it back.`);
42720
42963
  }
42964
+ if (outcome.skipped && outcome.skipped.length > 0) {
42965
+ logger.warn(
42966
+ `Skipped ${outcome.skipped.length} mount(s) \u2014 allowPartial let this through: ` + outcome.skipped.map((s) => `${s.mount} (${s.reason})`).join(", ")
42967
+ );
42968
+ }
42721
42969
  }
42722
42970
 
42723
42971
  // src/cli/index.ts
@@ -43172,7 +43420,10 @@ program.command("login <url>").description("store a bearer credential for a host
43172
43420
  program.command("logout [url]").description("forget a stored credential for a hosted instance (local only \u2014 does NOT revoke it), or list what is stored").action(async (url) => {
43173
43421
  await runLogout(url, {});
43174
43422
  });
43175
- program.command("remote <action>").description("hosted instance: push | pull (migrate a spec tree) \xB7 attach | detach | status (bind this checkout)").option("--url <url>", "hosted instance base URL (else WAIRON_REMOTE_URL)").option("--project <id>", "hosted project id, optionally qualified as project::subsystem (else WAIRON_REMOTE_PROJECT)").option("--token <token>", "bearer credential (else WAIRON_REMOTE_TOKEN) \u2014 mint one in the hosted UI under Tokens").option("--unit <id>", "push: create the destination project first, owned by this organization unit").option("--force", "replace an authored spec tree at the destination (it is backed up first)").option("--include-derived", "push: pack regenerable artifacts (diagrams, generated topology) too").option("--archive <path>", "also write the transferred archive to this path (file or directory)").option("--dir <path>", "pull: the local project root to import into (default: this project)").action(async (action, opts) => {
43423
+ program.command("remote <action>").description("hosted instance: push | pull (migrate a spec tree) \xB7 attach | detach | status (bind this checkout)").option("--url <url>", "hosted instance base URL (else WAIRON_REMOTE_URL)").option("--project <id>", "hosted project id, optionally qualified as project::subsystem (else WAIRON_REMOTE_PROJECT)").option("--token <token>", "bearer credential (else WAIRON_REMOTE_TOKEN) \u2014 mint one in the hosted UI under Tokens").option("--unit <id>", "push: create the destination project first, owned by this organization unit").option("--force", "replace an authored spec tree at the destination (it is backed up first)").option("--include-derived", "push: pack regenerable artifacts (diagrams, generated topology) too").option(
43424
+ "--allow-partial",
43425
+ "build the archive even when some chained mounts cannot be packed, listing them as skipped (default: refuse)"
43426
+ ).option("--archive <path>", "also write the transferred archive to this path (file or directory)").option("--dir <path>", "pull: the local project root to import into (default: this project)").action(async (action, opts) => {
43176
43427
  await runRemote(action, {
43177
43428
  url: opts.url,
43178
43429
  project: opts.project,
@@ -43180,6 +43431,7 @@ program.command("remote <action>").description("hosted instance: push | pull (mi
43180
43431
  unit: opts.unit,
43181
43432
  force: opts.force,
43182
43433
  includeDerived: opts.includeDerived,
43434
+ allowPartial: opts.allowPartial,
43183
43435
  archive: opts.archive,
43184
43436
  dir: opts.dir
43185
43437
  });