@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/index.js CHANGED
@@ -1312,7 +1312,7 @@ var init_defaults = __esm({
1312
1312
  copilot: ".github/prompts",
1313
1313
  codex: ".codex/agents"
1314
1314
  };
1315
- WAIRON_VERSION = "5.1.1-dev.12";
1315
+ WAIRON_VERSION = "5.1.1-dev.13";
1316
1316
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
1317
1317
  ARCHITECT_AGENT_ID = "agent-architect";
1318
1318
  ARCHITECT_TEMPLATE_ID = "architect";
@@ -2884,17 +2884,43 @@ function isExternalNamespaceRef(ctx, ref) {
2884
2884
  if (sep6 === -1) return false;
2885
2885
  return !ctx.subsystemIds.has(ref.slice(0, sep6));
2886
2886
  }
2887
+ function isProvidedBy(snapshot, provider) {
2888
+ return snapshot.projectName === provider || snapshot.projectName.split("::").pop() === provider;
2889
+ }
2890
+ function sameContract(a, b) {
2891
+ 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 ?? []);
2892
+ }
2887
2893
  function resolveSurfaceRef(ctx, ref, fromSubsystem) {
2888
- const local = ref.split("::").filter((seg) => seg && seg !== "super").pop();
2889
- if (!local) return null;
2894
+ const segments = ref.split("::").filter((seg) => seg && seg !== "super");
2895
+ const local = segments.pop();
2896
+ if (!local) return { kind: "unresolved" };
2897
+ const provider = segments.pop();
2890
2898
  const mountPools = fromSubsystem ? enclosingMounts(ctx, fromSubsystem).reverse().map((ns) => ctx.mountSurfaceSnapshots.find((m) => m.namespace === ns)?.snapshots ?? []) : [];
2891
2899
  for (const pool of [...mountPools, ctx.surfaceSnapshots]) {
2900
+ const candidates = [];
2892
2901
  for (const snapshot of pool) {
2902
+ if (provider !== void 0 && !isProvidedBy(snapshot, provider)) continue;
2893
2903
  const entry = snapshot.interfaces.find((e) => e.component === local || e.id === local);
2894
- if (entry) return { snapshot, entry };
2904
+ if (entry) candidates.push({ snapshot, entry });
2905
+ }
2906
+ if (candidates.length === 0) continue;
2907
+ const [first] = candidates;
2908
+ if (candidates.every((c) => sameContract(c.entry, first.entry))) {
2909
+ return { kind: "resolved", ...first };
2895
2910
  }
2911
+ return { kind: "ambiguous", providers: [...new Set(candidates.map((c) => c.snapshot.projectName))] };
2896
2912
  }
2897
- return null;
2913
+ return { kind: "unresolved" };
2914
+ }
2915
+ function reportAmbiguousSurfaceRef(ctx, subject, ref, providers, specId, isDraftContext) {
2916
+ const local = ref.split("::").filter((seg) => seg && seg !== "super").pop() ?? ref;
2917
+ ctx.addIssue(
2918
+ "error",
2919
+ "SURFACE_REF_AMBIGUOUS",
2920
+ `${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.`,
2921
+ specId,
2922
+ isDraftContext
2923
+ );
2898
2924
  }
2899
2925
  function enclosingMounts(ctx, subsystemId) {
2900
2926
  const mounts = [];
@@ -2985,6 +3011,7 @@ var init_contracts = __esm({
2985
3011
  { code: "INVALID_TARGET_COMPONENT_REFERENCE", defaultSeverity: "error", summary: "Call/register step targets a non-existent component" },
2986
3012
  { 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" },
2987
3013
  { code: "SURFACE_REF_NOT_EXPOSED", defaultSeverity: "error", summary: "Cross-tree reference resolves to a surface snapshot that does not expose the called method/capability" },
3014
+ { code: "SURFACE_REF_AMBIGUOUS", defaultSeverity: "error", summary: "Cross-tree call/dispatch/register target matched by surface snapshots of several providers with different contracts" },
2988
3015
  { code: "UNDECLARED_DEPENDENCY_CALL", defaultSeverity: "error", summary: "Call step targets a component the caller does not depend on or own" },
2989
3016
  { code: "INVALID_TARGET_METHOD_REFERENCE", defaultSeverity: "error", summary: "Call step targets a method not on any target interface" },
2990
3017
  { code: "NARRATIVE_SEMANTIC_UNBACKED", defaultSeverity: "warning", summary: "Narrative asserts a guarantee the called contract does not declare" }
@@ -3039,7 +3066,18 @@ var init_contracts = __esm({
3039
3066
  if (!dispatchTarget) {
3040
3067
  if (isCrossTreeForm || isCollapsedForm) {
3041
3068
  const resolved = resolveSurfaceRef(ctx, step.targetComponent, fromSubsystem);
3042
- if (resolved) {
3069
+ if (resolved.kind === "ambiguous") {
3070
+ reportAmbiguousSurfaceRef(
3071
+ ctx,
3072
+ `Method "${implMethod.name}" in implementation "${impl.id}" dispatches (step ${step.stepNumber}) through`,
3073
+ step.targetComponent,
3074
+ resolved.providers,
3075
+ impl.id,
3076
+ isDraftCtx
3077
+ );
3078
+ continue;
3079
+ }
3080
+ if (resolved.kind === "resolved") {
3043
3081
  if (step.capability && !(resolved.entry.dispatch ?? []).some((b) => b.capability === step.capability)) {
3044
3082
  ctx.addIssue(
3045
3083
  "error",
@@ -3099,7 +3137,18 @@ var init_contracts = __esm({
3099
3137
  if (!targetComp) {
3100
3138
  if (isCrossTreeForm || isCollapsedForm) {
3101
3139
  const resolved = resolveSurfaceRef(ctx, step.targetComponent, fromSubsystem);
3102
- if (resolved) {
3140
+ if (resolved.kind === "ambiguous") {
3141
+ reportAmbiguousSurfaceRef(
3142
+ ctx,
3143
+ `Method "${implMethod.name}" in implementation "${impl.id}" ${verb} "${step.targetMethod}" (step ${step.stepNumber}) on`,
3144
+ step.targetComponent,
3145
+ resolved.providers,
3146
+ impl.id,
3147
+ isDraftCtx
3148
+ );
3149
+ continue;
3150
+ }
3151
+ if (resolved.kind === "resolved") {
3103
3152
  const surfaceMethod = resolved.entry.methods.find((m) => m.name === step.targetMethod);
3104
3153
  if (!surfaceMethod) {
3105
3154
  ctx.addIssue(
@@ -4614,6 +4663,7 @@ var init_stereotype_deps = __esm({
4614
4663
  codes: [
4615
4664
  { code: "INVALID_DEPENDENCY_REFERENCE", defaultSeverity: "error", summary: "dependsOn names a non-existent component" },
4616
4665
  { code: "CROSS_TREE_REF_UNRESOLVED", defaultSeverity: "warning", summary: "Cross-tree dependsOn (super::/:: form) with no surface snapshot covering it" },
4666
+ { code: "SURFACE_REF_AMBIGUOUS", defaultSeverity: "error", summary: "Cross-tree dependsOn matched by surface snapshots of several providers with different contracts" },
4617
4667
  { code: "CROSS_SUBSYSTEM_NON_ADAPTER", defaultSeverity: "error", summary: "Non-Adapter component crossing a subsystem boundary" },
4618
4668
  { code: "CROSS_SUBSYSTEM_PRIVATE_ACCESS", defaultSeverity: "error", summary: "Cross-subsystem dependency on an unpublished component" },
4619
4669
  { code: "CROSS_SUBSYSTEM_TARGET_NON_PORTAL", defaultSeverity: "error", summary: "Cross-subsystem hop entering through a non-Portal" },
@@ -4637,7 +4687,11 @@ var init_stereotype_deps = __esm({
4637
4687
  const external = isExternalNamespaceRef(ctx, depId);
4638
4688
  if (external || isCollapsedCrossTreeRef(ctx, depId, comp.subsystem)) {
4639
4689
  const resolved = resolveSurfaceRef(ctx, depId, comp.subsystem);
4640
- if (resolved) {
4690
+ if (resolved.kind === "ambiguous") {
4691
+ reportAmbiguousSurfaceRef(ctx, `Component "${comp.id}" depends on`, depId, resolved.providers, comp.id, isDraftCtx);
4692
+ continue;
4693
+ }
4694
+ if (resolved.kind === "resolved") {
4641
4695
  if (comp.componentType !== "Adapter") {
4642
4696
  ctx.addIssue(
4643
4697
  "error",
@@ -8343,8 +8397,8 @@ function doctrineIdentity(doctrine, gate) {
8343
8397
  )
8344
8398
  };
8345
8399
  }
8346
- function hashGateState(doctrine, gate = {}) {
8347
- const payload = { tree: loadTree(), doctrine: doctrineIdentity(doctrine, gate) };
8400
+ function hashGateState(doctrine, inputs, gate = {}) {
8401
+ const payload = { tree: loadTree(), doctrine: doctrineIdentity(doctrine, gate), inputs: [...inputs].sort() };
8348
8402
  const digest2 = crypto2.createHash("sha256").update(canonicalize(payload)).digest("hex");
8349
8403
  return { algorithm: GATE_ALGORITHM, digest: digest2 };
8350
8404
  }
@@ -8375,7 +8429,7 @@ var init_statehash = __esm({
8375
8429
  init_specs2();
8376
8430
  init_rules();
8377
8431
  CONTENT_ALGORITHM = "sha256";
8378
- GATE_ALGORITHM = "sha256+doctrine";
8432
+ GATE_ALGORITHM = "sha256+doctrine+inputs";
8379
8433
  }
8380
8434
  });
8381
8435
 
@@ -14138,15 +14192,18 @@ function importSurface(sourcePath, origin) {
14138
14192
  saveSnapshot(snapshot);
14139
14193
  return snapshot;
14140
14194
  }
14141
- function pinFamilySurfaces() {
14142
- const parent = resolveChainingParent();
14143
- if (!parent) return null;
14144
- const childRoot = getProjectRoot();
14145
- const projected = runWithProjectRoot(parent.parentRoot, () => {
14195
+ function projectFamilySurfaces(parent) {
14196
+ return runWithProjectRoot(parent.parentRoot, () => {
14146
14197
  invalidateSpecCache();
14147
14198
  const siblings = loadSubsystemSpecs().filter((s) => !s.id.includes("::") && s.id !== parent.subsystemId);
14148
14199
  return [projectChildSurface(), ...siblings.map((s) => projectSubsystemSurface(s.id))];
14149
14200
  });
14201
+ }
14202
+ function pinFamilySurfaces() {
14203
+ const parent = resolveChainingParent();
14204
+ if (!parent) return null;
14205
+ const childRoot = getProjectRoot();
14206
+ const projected = projectFamilySurfaces(parent);
14150
14207
  const before = new Map(listSnapshots(childRoot).map((s) => [s.projectName, surfaceContentKey(s)]));
14151
14208
  const changed = [];
14152
14209
  for (const snapshot of projected) {
@@ -14157,17 +14214,21 @@ function pinFamilySurfaces() {
14157
14214
  }
14158
14215
  return changed;
14159
14216
  }
14160
- function computeParentStateId(parentRoot) {
14161
- return computeStateIdAt(parentRoot);
14217
+ function projectedFamilyContent(parent) {
14218
+ try {
14219
+ return new Map(projectFamilySurfaces(parent).map((s) => [s.projectName, surfaceContentKey(s)]));
14220
+ } catch {
14221
+ return null;
14222
+ }
14162
14223
  }
14163
14224
  function listExternalInterfaces() {
14164
14225
  const snapshots = listSnapshots();
14165
14226
  const chainingParent = resolveChainingParent();
14166
- const parentStateId = chainingParent ? computeParentStateId(chainingParent.parentRoot) : null;
14227
+ const projected = chainingParent ? projectedFamilyContent(chainingParent) : null;
14167
14228
  return snapshots.map((snapshot) => {
14168
14229
  const generated = snapshot.origin === "generated";
14169
14230
  const sourceKind = !generated ? "foreign" : snapshot.projectName.includes("::") ? "sibling" : "parent";
14170
- const freshness = generated && parentStateId ? snapshot.stateId === parentStateId ? "fresh" : "stale" : "unverifiable";
14231
+ const freshness = generated && projected ? projected.get(snapshot.projectName) === surfaceContentKey(snapshot) ? "fresh" : "stale" : "unverifiable";
14171
14232
  return {
14172
14233
  projectName: snapshot.projectName,
14173
14234
  origin: snapshot.origin,
@@ -14505,7 +14566,7 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
14505
14566
  rule.check(ctx);
14506
14567
  }
14507
14568
  const unresolved = (i) => RESOLUTION_FAILURE_CODES.has(i.code) && !i.surfaceResolved;
14508
- const chainingParent = crossTree !== "off" && issues.some(unresolved) ? findChainingParent(getProjectRoot()) : null;
14569
+ const chainingParent = crossTree !== "off" && issues.some(unresolved) ? resolveChainingParent() : null;
14509
14570
  const resolution = chainingParent ? resolveThroughParent(getProjectRoot(), treatAllAsComplete, rules) : null;
14510
14571
  if (resolution) {
14511
14572
  const judgedByParent = worstByKey(resolution.issues);
@@ -14543,7 +14604,7 @@ function resolveThroughParent(boundRoot, treatAllAsComplete, childRules) {
14543
14604
  const chain = [];
14544
14605
  let top = path14.resolve(boundRoot);
14545
14606
  while (top !== ceiling) {
14546
- const hop = findChainingParent(top);
14607
+ const hop = findChainingParent(top, ceiling);
14547
14608
  if (!hop) break;
14548
14609
  const next = path14.resolve(hop.parentRoot);
14549
14610
  if (ceiling && !isWithinOrEqual(ceiling, next)) break;
@@ -15273,33 +15334,75 @@ function relativizeId(id, prefix) {
15273
15334
  if (common === idParts.length) common--;
15274
15335
  return `${"super::".repeat(prefixParts.length - common)}${idParts.slice(common).join("::")}`;
15275
15336
  }
15337
+ function rebaseReference(ref, mount, direction, isMoved = () => false) {
15338
+ if (!ref || ref.startsWith("::")) return ref;
15339
+ const segments = ref.split("::");
15340
+ let hops = 0;
15341
+ while (segments[hops] === "super") hops++;
15342
+ if (hops === segments.length) return ref;
15343
+ const outer = Array.from({ length: hops + 1 }, (_, i) => `<n${i}>`).join("::");
15344
+ const inner = `${outer}::${mount}`;
15345
+ const [from, to] = direction === "into" ? [outer, inner] : [inner, outer];
15346
+ const target = qualifyId(ref, from, NO_ROOT_SUBSYSTEMS);
15347
+ const left = relativizeId(target, from);
15348
+ const travels = !left.startsWith("super::") && (direction === "outOf" || isMoved(left));
15349
+ return relativizeId(travels ? qualifyId(left, to, NO_ROOT_SUBSYSTEMS) : target, to);
15350
+ }
15276
15351
  function isWithin(dir, file) {
15277
15352
  const d = path15.resolve(dir);
15278
15353
  const f = path15.resolve(file);
15279
15354
  return f === d || f.startsWith(d + path15.sep);
15280
15355
  }
15356
+ function canonicalPath(target) {
15357
+ let existing = path15.resolve(target);
15358
+ const rest = [];
15359
+ for (; ; ) {
15360
+ try {
15361
+ fs9.lstatSync(existing);
15362
+ break;
15363
+ } catch {
15364
+ const up = path15.dirname(existing);
15365
+ if (up === existing) return path15.resolve(target);
15366
+ rest.unshift(path15.basename(existing));
15367
+ existing = up;
15368
+ }
15369
+ }
15370
+ try {
15371
+ return path15.join(fs9.realpathSync.native(existing), ...rest);
15372
+ } catch {
15373
+ return null;
15374
+ }
15375
+ }
15376
+ function chainDirKey(dir) {
15377
+ return canonicalPath(dir) ?? path15.resolve(dir);
15378
+ }
15281
15379
  function projectPathEscapesRoot(projectRoot, projectPath, resolvedChildDir) {
15282
- return path15.isAbsolute(projectPath) || !isWithin(projectRoot, resolvedChildDir);
15380
+ if (path15.isAbsolute(projectPath) || !isWithin(projectRoot, resolvedChildDir)) return true;
15381
+ const root = canonicalPath(projectRoot);
15382
+ const child = canonicalPath(resolvedChildDir);
15383
+ return root === null || child === null || !isWithin(root, child);
15283
15384
  }
15284
15385
  function assertContainedProjectPath(projectRoot, projectPath) {
15285
15386
  const root = path15.resolve(projectRoot);
15286
15387
  const resolved = path15.resolve(root, projectPath);
15287
15388
  if (projectPathEscapesRoot(root, projectPath, resolved)) {
15288
15389
  throw new Error(
15289
- `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.`
15390
+ `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.`
15290
15391
  );
15291
15392
  }
15292
15393
  return resolved;
15293
15394
  }
15294
- function findChainingParent(childRoot) {
15395
+ function findChainingParent(childRoot, ceiling) {
15295
15396
  let childResolved;
15296
15397
  try {
15297
15398
  childResolved = path15.resolve(childRoot);
15298
15399
  } catch {
15299
15400
  return null;
15300
15401
  }
15402
+ const bound = ceiling ? path15.resolve(ceiling) : void 0;
15301
15403
  let dir = path15.dirname(childResolved);
15302
15404
  for (let hops = 0; hops < 32; hops++) {
15405
+ if (bound && !isWithin(bound, dir)) break;
15303
15406
  const specsDir = aiPathsAt(dir).specsDir();
15304
15407
  if (pathExists(specsDir)) {
15305
15408
  for (const file of listFilesRecursive(specsDir, ".yaml")) {
@@ -15313,7 +15416,8 @@ function findChainingParent(childRoot) {
15313
15416
  const projectPath = raw.projectPath;
15314
15417
  if (typeof projectPath === "string" && projectPath.trim() !== "") {
15315
15418
  try {
15316
- if (path15.resolve(dir, projectPath) === childResolved) {
15419
+ const mountDir = path15.resolve(dir, projectPath);
15420
+ if (mountDir === childResolved && !projectPathEscapesRoot(dir, projectPath, mountDir)) {
15317
15421
  const id = raw.id;
15318
15422
  return { parentRoot: dir, subsystemId: typeof id === "string" ? id : "?" };
15319
15423
  }
@@ -15329,12 +15433,11 @@ function findChainingParent(childRoot) {
15329
15433
  }
15330
15434
  return null;
15331
15435
  }
15332
- function listChainedRoots(rootDir = getProjectRoot()) {
15436
+ function inspectChainedRoots(rootDir = getProjectRoot()) {
15333
15437
  const root = path15.resolve(rootDir);
15334
- const found = [];
15335
- const visited = /* @__PURE__ */ new Set([root]);
15336
- const walk = (projectDir, depth) => {
15337
- if (depth > 32) return;
15438
+ const inspection = { roots: [], skipped: [] };
15439
+ const listed = /* @__PURE__ */ new Set();
15440
+ const walk = (projectDir, prefix, ancestors, depth) => {
15338
15441
  const specsDir = aiPathsAt(projectDir).specsDir();
15339
15442
  if (!pathExists(specsDir)) return;
15340
15443
  for (const file of listFilesRecursive(specsDir, ".yaml")) {
@@ -15345,24 +15448,37 @@ function listChainedRoots(rootDir = getProjectRoot()) {
15345
15448
  continue;
15346
15449
  }
15347
15450
  if (!raw || typeof raw !== "object" || !("parentSystem" in raw)) continue;
15348
- const projectPath = raw.projectPath;
15451
+ const { id, projectPath } = raw;
15349
15452
  if (typeof projectPath !== "string" || projectPath.trim() === "") continue;
15453
+ const localId = typeof id === "string" ? id : "?";
15350
15454
  let childDir;
15351
15455
  try {
15352
15456
  childDir = path15.resolve(projectDir, projectPath);
15353
15457
  } catch {
15354
15458
  continue;
15355
15459
  }
15356
- if (projectPathEscapesRoot(root, projectPath, childDir)) continue;
15357
- if (visited.has(childDir)) continue;
15358
- if (!fs9.existsSync(childDir)) continue;
15359
- visited.add(childDir);
15360
- found.push(path15.relative(root, childDir).split(path15.sep).join("/"));
15361
- walk(childDir, depth + 1);
15460
+ let reason;
15461
+ if (projectPathEscapesRoot(projectDir, projectPath, childDir)) reason = "escapes";
15462
+ else if (ancestors.has(chainDirKey(childDir))) reason = "cyclic";
15463
+ else if (!fs9.existsSync(childDir)) reason = "missing";
15464
+ else if (depth >= 32) reason = "too-deep";
15465
+ if (reason) {
15466
+ const mount = prefix ? qualifyDeclaredId(localId, prefix, true) : localId;
15467
+ inspection.skipped.push({ mount, projectPath, reason });
15468
+ continue;
15469
+ }
15470
+ const key = chainDirKey(childDir);
15471
+ if (listed.has(key)) continue;
15472
+ listed.add(key);
15473
+ inspection.roots.push(path15.relative(root, childDir).split(path15.sep).join("/"));
15474
+ walk(childDir, prefix ? `${prefix}::${localId}` : localId, /* @__PURE__ */ new Set([...ancestors, key]), depth + 1);
15362
15475
  }
15363
15476
  };
15364
- walk(root, 0);
15365
- return found;
15477
+ walk(root, "", /* @__PURE__ */ new Set([chainDirKey(root)]), 0);
15478
+ return inspection;
15479
+ }
15480
+ function listChainedRoots(rootDir = getProjectRoot()) {
15481
+ return inspectChainedRoots(rootDir).roots;
15366
15482
  }
15367
15483
  function mergeMountRealizations(subs) {
15368
15484
  const result = [];
@@ -15645,12 +15761,24 @@ function buildProjectGraph(level) {
15645
15761
  function resolveChainingParent() {
15646
15762
  const reach = getRequestParentReach();
15647
15763
  if (reach && !reach.parentReach) return null;
15648
- const parent = findChainingParent(getProjectRoot());
15649
- if (parent && reach?.topRoot) {
15650
- const fromTop = path15.relative(path15.resolve(reach.topRoot), path15.resolve(parent.parentRoot));
15651
- if (fromTop === ".." || fromTop.startsWith(`..${path15.sep}`) || path15.isAbsolute(fromTop)) return null;
15764
+ return findChainingParent(getProjectRoot(), reach?.topRoot);
15765
+ }
15766
+ function snapshotInputKey(snapshot) {
15767
+ const { stateId, generatedAt, origin, ...content } = snapshot;
15768
+ return canonicalize(content);
15769
+ }
15770
+ function consumedSurfaceInputsAt(rootDir) {
15771
+ const dir = path15.join(rootDir, ".wai", "surfaces");
15772
+ if (!pathExists(dir)) return [];
15773
+ const keys = [];
15774
+ for (const file of fs9.readdirSync(dir)) {
15775
+ if (!file.endsWith(".yaml") && !file.endsWith(".yml")) continue;
15776
+ try {
15777
+ keys.push(snapshotInputKey(SurfaceSnapshotSchema.parse(readYamlFile(path15.join(dir, file)))));
15778
+ } catch {
15779
+ }
15652
15780
  }
15653
- return parent;
15781
+ return keys;
15654
15782
  }
15655
15783
  function computeGateStateId() {
15656
15784
  let gate = {};
@@ -15659,7 +15787,10 @@ function computeGateStateId() {
15659
15787
  gate = { projectType: config.projectType, rules: config.rules };
15660
15788
  } catch {
15661
15789
  }
15662
- return hashGateState(loadProjectExtensions(), gate);
15790
+ const root = getProjectRoot();
15791
+ const roots = [root, ...listChainedRoots(root).map((rel2) => path15.join(root, rel2))];
15792
+ const inputs = roots.flatMap(consumedSurfaceInputsAt);
15793
+ return hashGateState(loadProjectExtensions(), inputs, gate);
15663
15794
  }
15664
15795
  function readLockState() {
15665
15796
  const record = readLockRecord();
@@ -15769,7 +15900,7 @@ var init_specs2 = __esm({
15769
15900
  this.rootSubsystems.clear();
15770
15901
  this.cachedRecursive = recursive;
15771
15902
  this.scanVisitedSpecDirs = [];
15772
- const visited = /* @__PURE__ */ new Set([path15.resolve(this.rootDir)]);
15903
+ const visited = /* @__PURE__ */ new Set([chainDirKey(this.rootDir)]);
15773
15904
  const maxDepth = typeof recursive === "number" ? recursive : recursive ? Infinity : 0;
15774
15905
  this.cachedIndex = this.scanSpecsForProject(this.rootDir, "", visited, maxDepth, 0);
15775
15906
  this.cachedSpecDirs = this.scanVisitedSpecDirs;
@@ -15969,21 +16100,22 @@ var init_specs2 = __esm({
15969
16100
  if (currentDepth < maxDepth) {
15970
16101
  for (const subproj of localSubprojects) {
15971
16102
  const childDir = path15.resolve(projectDir, subproj.projectPath);
15972
- if (projectPathEscapesRoot(this.rootDir, subproj.projectPath, childDir)) {
16103
+ const mountId = namespacePrefix ? qualifyDeclaredId(subproj.subsystemId, namespacePrefix, true) : subproj.subsystemId;
16104
+ if (projectPathEscapesRoot(projectDir, subproj.projectPath, childDir)) {
15973
16105
  this.loaderIssues.push({
15974
16106
  severity: "error",
15975
16107
  code: "PROJECTPATH_ESCAPE",
15976
- 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.`,
15977
- specId: subproj.subsystemId
16108
+ 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.`,
16109
+ specId: mountId
15978
16110
  });
15979
16111
  continue;
15980
16112
  }
15981
- if (visitedDirs.has(childDir)) {
16113
+ if (visitedDirs.has(chainDirKey(childDir))) {
15982
16114
  this.loaderIssues.push({
15983
16115
  severity: "error",
15984
16116
  code: "CIRCULAR_SUBPROJECT_REFERENCE",
15985
- message: `Circular reference detected: Subsystem "${subproj.subsystemId}" refers to subproject "${childDir}" which is already loaded.`,
15986
- specId: subproj.subsystemId
16117
+ message: `Circular reference detected: Subsystem "${mountId}" refers to subproject "${childDir}" which is already loaded.`,
16118
+ specId: mountId
15987
16119
  });
15988
16120
  continue;
15989
16121
  }
@@ -15991,14 +16123,14 @@ var init_specs2 = __esm({
15991
16123
  this.loaderIssues.push({
15992
16124
  severity: "error",
15993
16125
  code: "SUBPROJECT_NOT_FOUND",
15994
- message: `Subproject directory "${childDir}" declared by subsystem "${subproj.subsystemId}" does not exist.`,
15995
- specId: subproj.subsystemId
16126
+ message: `Subproject directory "${childDir}" declared by subsystem "${mountId}" does not exist.`,
16127
+ specId: mountId
15996
16128
  });
15997
16129
  continue;
15998
16130
  }
15999
16131
  const childNamespace = namespacePrefix ? `${namespacePrefix}::${subproj.subsystemId}` : subproj.subsystemId;
16000
16132
  const newVisited = new Set(visitedDirs);
16001
- newVisited.add(childDir);
16133
+ newVisited.add(chainDirKey(childDir));
16002
16134
  const childIndex = this.scanSpecsForProject(childDir, childNamespace, newVisited, maxDepth, currentDepth + 1);
16003
16135
  index.subsystems.push(...childIndex.subsystems);
16004
16136
  index.components.push(...childIndex.components);
@@ -16031,11 +16163,11 @@ var init_specs2 = __esm({
16031
16163
  const sub = index.subsystems.find((s) => s.id === currentPrefix);
16032
16164
  if (sub && sub.projectPath) {
16033
16165
  const nextDir = path15.resolve(currentDir, sub.projectPath);
16034
- if (projectPathEscapesRoot(this.rootDir, sub.projectPath, nextDir)) {
16166
+ if (projectPathEscapesRoot(currentDir, sub.projectPath, nextDir)) {
16035
16167
  this.loaderIssues.push({
16036
16168
  severity: "error",
16037
16169
  code: "PROJECTPATH_ESCAPE",
16038
- 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.`,
16170
+ 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.`,
16039
16171
  specId: currentPrefix
16040
16172
  });
16041
16173
  continue;
@@ -21508,6 +21640,7 @@ __export(src_exports, {
21508
21640
  buildRuleContext: () => buildRuleContext,
21509
21641
  buildServerInstructions: () => buildServerInstructions2,
21510
21642
  buildTreeArchive: () => buildTreeArchive2,
21643
+ canonicalize: () => canonicalize,
21511
21644
  captureApprovedSpecs: () => captureApprovedSpecs,
21512
21645
  checkSkillFreshness: () => checkSkillFreshness,
21513
21646
  clearLoaderIssues: () => clearLoaderIssues,
@@ -21518,7 +21651,6 @@ __export(src_exports, {
21518
21651
  composeVariantGuidance: () => composeVariantGuidance,
21519
21652
  computeGateStateId: () => computeGateStateId,
21520
21653
  computePackDigest: () => computePackDigest,
21521
- computeParentStateId: () => computeParentStateId,
21522
21654
  computeStateId: () => computeStateId,
21523
21655
  computeStateIdAt: () => computeStateIdAt,
21524
21656
  contextDir: () => contextDir,
@@ -21595,6 +21727,7 @@ __export(src_exports, {
21595
21727
  hashGateState: () => hashGateState,
21596
21728
  importSpecTree: () => importSpecTree,
21597
21729
  importSurface: () => importSurface,
21730
+ inspectChainedRoots: () => inspectChainedRoots,
21598
21731
  inspectTreeArchive: () => inspectTreeArchive2,
21599
21732
  installPackFromDirectory: () => installPackFromDirectory,
21600
21733
  internalizeSubsystem: () => internalizeSubsystem,
@@ -21673,6 +21806,7 @@ __export(src_exports, {
21673
21806
  readResource: () => readResource,
21674
21807
  readSkillResource: () => readSkillResource,
21675
21808
  readYamlFile: () => readYamlFile,
21809
+ rebaseReference: () => rebaseReference,
21676
21810
  registerExporter: () => registerExporter,
21677
21811
  removeFreeStandingDomain: () => removeFreeStandingDomain,
21678
21812
  removeSnapshot: () => removeSnapshot,
@@ -22166,6 +22300,7 @@ function externalizeSubsystem(subsystemId, projectPath) {
22166
22300
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22167
22301
  });
22168
22302
  rewriteRefsInDir(parentSpecsDir, renameMap, fooDir);
22303
+ rebaseMovedRefs(childFooDir, subsystemId, "into");
22169
22304
  invalidateSpecCache();
22170
22305
  }
22171
22306
  function internalizeSubsystem(subsystemId) {
@@ -22205,6 +22340,7 @@ function internalizeSubsystem(subsystemId) {
22205
22340
  });
22206
22341
  fs13.rmSync(childWai, { recursive: true, force: true });
22207
22342
  rewriteRefsInDir(parentSpecsDir, renameMap, fooDir);
22343
+ rebaseMovedRefs(fooDir, subsystemId, "outOf");
22208
22344
  invalidateSpecCache();
22209
22345
  }
22210
22346
  function buildRenameMap(subsystemId, externalize) {
@@ -22231,7 +22367,27 @@ function buildRenameMap(subsystemId, externalize) {
22231
22367
  }
22232
22368
  function rewriteRefsInDir(specsDir, renameMap, excludeDir) {
22233
22369
  if (renameMap.size === 0) return;
22234
- const remap = (id) => id !== void 0 && renameMap.has(id) ? renameMap.get(id) : id;
22370
+ rewriteRefFields(specsDir, (ref) => renameMap.get(ref) ?? ref, excludeDir);
22371
+ }
22372
+ function rebaseMovedRefs(movedDir, mount, direction) {
22373
+ const declared = componentIdsUnder(movedDir);
22374
+ rewriteRefFields(movedDir, (ref, position) => position === "component" ? rebaseReference(ref, mount, direction, (id) => declared.has(id)) : ref);
22375
+ }
22376
+ function componentIdsUnder(dir) {
22377
+ const ids = /* @__PURE__ */ new Set();
22378
+ for (const file of listFilesRecursive(dir, ".yaml")) {
22379
+ let raw;
22380
+ try {
22381
+ raw = readYamlFile(file);
22382
+ } catch {
22383
+ continue;
22384
+ }
22385
+ if (raw && typeof raw === "object" && "componentType" in raw && typeof raw.id === "string") ids.add(raw.id);
22386
+ }
22387
+ return ids;
22388
+ }
22389
+ function rewriteRefFields(specsDir, remap, excludeDir) {
22390
+ const at = (ref, position) => typeof ref === "string" ? remap(ref, position) : ref;
22235
22391
  for (const file of listFilesRecursive(specsDir, ".yaml")) {
22236
22392
  if (excludeDir && isWithinDir(excludeDir, file)) continue;
22237
22393
  let raw;
@@ -22243,14 +22399,14 @@ function rewriteRefsInDir(specsDir, renameMap, excludeDir) {
22243
22399
  if (!raw || typeof raw !== "object") continue;
22244
22400
  let changed = false;
22245
22401
  if ("componentType" in raw && Array.isArray(raw.dependsOn)) {
22246
- const next = raw.dependsOn.map((d) => remap(d));
22402
+ const next = raw.dependsOn.map((d) => at(d, "component"));
22247
22403
  if (next.some((v, i) => v !== raw.dependsOn[i])) {
22248
22404
  raw.dependsOn = next;
22249
22405
  changed = true;
22250
22406
  }
22251
22407
  if (Array.isArray(raw.dispatch)) {
22252
22408
  for (const b of raw.dispatch) {
22253
- const nc = remap(b.component);
22409
+ const nc = at(b.component, "component");
22254
22410
  if (nc !== b.component) {
22255
22411
  b.component = nc;
22256
22412
  changed = true;
@@ -22259,7 +22415,7 @@ function rewriteRefsInDir(specsDir, renameMap, excludeDir) {
22259
22415
  }
22260
22416
  } else if ("parentSystem" in raw && Array.isArray(raw.lifecycle)) {
22261
22417
  for (const le of raw.lifecycle) {
22262
- const nc = remap(le.component);
22418
+ const nc = at(le.component, "component");
22263
22419
  if (nc !== le.component) {
22264
22420
  le.component = nc;
22265
22421
  changed = true;
@@ -22269,7 +22425,7 @@ function rewriteRefsInDir(specsDir, renameMap, excludeDir) {
22269
22425
  for (const m of raw.methods) {
22270
22426
  if (!Array.isArray(m.params)) continue;
22271
22427
  for (const p of m.params) {
22272
- const nt = remap(p.type);
22428
+ const nt = at(p.type, "type");
22273
22429
  if (nt !== p.type) {
22274
22430
  p.type = nt;
22275
22431
  changed = true;
@@ -22280,7 +22436,7 @@ function rewriteRefsInDir(specsDir, renameMap, excludeDir) {
22280
22436
  for (const m of raw.methods) {
22281
22437
  if (!Array.isArray(m.narrative)) continue;
22282
22438
  for (const step of m.narrative) {
22283
- const nt = remap(step.targetComponent);
22439
+ const nt = at(step.targetComponent, "component");
22284
22440
  if (nt !== step.targetComponent) {
22285
22441
  step.targetComponent = nt;
22286
22442
  changed = true;
@@ -22289,7 +22445,7 @@ function rewriteRefsInDir(specsDir, renameMap, excludeDir) {
22289
22445
  }
22290
22446
  } else if ("kind" in raw && Array.isArray(raw.fields)) {
22291
22447
  for (const f of raw.fields) {
22292
- const nt = remap(f.type);
22448
+ const nt = at(f.type, "type");
22293
22449
  if (nt !== f.type) {
22294
22450
  f.type = nt;
22295
22451
  changed = true;
@@ -22885,18 +23041,29 @@ function discardStagingDir(stagingDir) {
22885
23041
  } catch {
22886
23042
  }
22887
23043
  }
22888
- function exportSpecTree(includeDerived) {
23044
+ function exportSpecTree(includeDerived, allowPartial) {
22889
23045
  const system = loadSystemSpec();
22890
23046
  if (!system) {
22891
23047
  throw new Error("no spec tree to export at this project root");
22892
23048
  }
22893
- const chained = listChainedRoots();
23049
+ const inspection = inspectChainedRoots();
22894
23050
  const root = getProjectRoot();
22895
- const candidates = [
22896
- { relativePath: ".", waiDir: aiPathsAt(root).root() },
22897
- ...chained.map((rel2) => ({ relativePath: rel2, waiDir: aiPathsAt(path22.resolve(root, rel2)).root() }))
22898
- ];
22899
- const roots = candidates.filter((source) => pathExists(source.waiDir));
23051
+ const skipped = [...inspection.skipped];
23052
+ const roots = [{ relativePath: ".", waiDir: aiPathsAt(root).root() }];
23053
+ for (const rel2 of inspection.roots) {
23054
+ const waiDir = aiPathsAt(path22.resolve(root, rel2)).root();
23055
+ if (pathExists(waiDir)) {
23056
+ roots.push({ relativePath: rel2, waiDir });
23057
+ } else {
23058
+ skipped.push({ mount: rel2, projectPath: rel2, reason: "no-spec-tree" });
23059
+ }
23060
+ }
23061
+ if (skipped.length > 0 && !allowPartial) {
23062
+ const detail = skipped.map((s) => `${s.mount} (${s.reason})`).join(", ");
23063
+ throw new Error(
23064
+ `cannot export the whole spec tree \u2014 skipped: ${detail}. Pass allowPartial to export the rest anyway.`
23065
+ );
23066
+ }
22900
23067
  const stateId = computeStateId();
22901
23068
  const built = buildTreeArchive2(roots, system.name, stateId.digest, includeDerived);
22902
23069
  const result = {
@@ -22905,7 +23072,8 @@ function exportSpecTree(includeDerived) {
22905
23072
  projectName: built.manifest.projectName,
22906
23073
  roots: built.manifest.roots,
22907
23074
  fileCount: built.fileCount,
22908
- stateId: stateId.digest
23075
+ stateId: stateId.digest,
23076
+ skipped
22909
23077
  };
22910
23078
  return result;
22911
23079
  }
@@ -23426,6 +23594,7 @@ init_yaml();
23426
23594
  buildRuleContext,
23427
23595
  buildServerInstructions,
23428
23596
  buildTreeArchive,
23597
+ canonicalize,
23429
23598
  captureApprovedSpecs,
23430
23599
  checkSkillFreshness,
23431
23600
  clearLoaderIssues,
@@ -23436,7 +23605,6 @@ init_yaml();
23436
23605
  composeVariantGuidance,
23437
23606
  computeGateStateId,
23438
23607
  computePackDigest,
23439
- computeParentStateId,
23440
23608
  computeStateId,
23441
23609
  computeStateIdAt,
23442
23610
  contextDir,
@@ -23513,6 +23681,7 @@ init_yaml();
23513
23681
  hashGateState,
23514
23682
  importSpecTree,
23515
23683
  importSurface,
23684
+ inspectChainedRoots,
23516
23685
  inspectTreeArchive,
23517
23686
  installPackFromDirectory,
23518
23687
  internalizeSubsystem,
@@ -23591,6 +23760,7 @@ init_yaml();
23591
23760
  readResource,
23592
23761
  readSkillResource,
23593
23762
  readYamlFile,
23763
+ rebaseReference,
23594
23764
  registerExporter,
23595
23765
  removeFreeStandingDomain,
23596
23766
  removeSnapshot,