@wairon/cli 5.1.1-dev.91 → 5.1.1-dev.93

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
@@ -3621,6 +3621,7 @@ __export(src_exports, {
3621
3621
  getRequestParentReach: () => getRequestParentReach,
3622
3622
  getRequestProjectRoot: () => getRequestProjectRoot,
3623
3623
  getStatusReport: () => getStatusReport,
3624
+ getWriteReach: () => getWriteReach,
3624
3625
  globalGuideFilePath: () => globalGuideFilePath,
3625
3626
  globalPacksDir: () => globalPacksDir,
3626
3627
  globalPacksEnabled: () => globalPacksEnabled,
@@ -3725,6 +3726,7 @@ __export(src_exports, {
3725
3726
  registerExporter: () => registerExporter,
3726
3727
  registerPackRef: () => registerPackRef,
3727
3728
  reinjectLocalGuides: () => reinjectLocalGuides,
3729
+ relationHealth: () => relationHealth,
3728
3730
  removeDomain: () => removeDomain,
3729
3731
  removeExternal: () => removeExternal,
3730
3732
  removeMember: () => removeMember,
@@ -3791,6 +3793,8 @@ __export(src_exports, {
3791
3793
  writeFileIfChanged: () => writeFileIfChanged,
3792
3794
  writeJsonFile: () => writeJsonFile,
3793
3795
  writeLockRecord: () => writeLockRecord,
3796
+ writeReachDenials: () => writeReachDenials,
3797
+ writeReachPermits: () => writeReachPermits,
3794
3798
  writeRootGuideDelegator: () => writeRootGuideDelegator,
3795
3799
  writeYamlFile: () => writeYamlFile
3796
3800
  });
@@ -3860,6 +3864,18 @@ function listFilesRecursive(dirPath, ext) {
3860
3864
  return files;
3861
3865
  }
3862
3866
  var projectRootOverride = null;
3867
+ var sameRoot = (a, b) => path.resolve(a) === path.resolve(b);
3868
+ function writeReachPermits(reach2, root, subsystemId) {
3869
+ const own = subsystemId === null ? void 0 : reach2.subsystems.find((s) => s.subsystemId === subsystemId && sameRoot(s.root, root));
3870
+ if (own) return own.writable;
3871
+ return !reach2.unwritableRoots.some((r) => sameRoot(r, root));
3872
+ }
3873
+ function writeReachDenials(reach2, root) {
3874
+ return reach2.subsystems.filter((s) => !s.writable && sameRoot(s.root, root));
3875
+ }
3876
+ function getWriteReach() {
3877
+ return requestRootStore.getStore()?.writeReach ?? null;
3878
+ }
3863
3879
  var requestRootStore = new import_node_async_hooks.AsyncLocalStorage();
3864
3880
  function currentRootBinding() {
3865
3881
  return requestRootStore.getStore();
@@ -3875,7 +3891,8 @@ function runWithProjectBinding(dir, reach2, fn) {
3875
3891
  parentReach: reach2.parentReach,
3876
3892
  ...reach2.narrowed ? { narrowed: true } : {},
3877
3893
  ...reach2.unwritableRoots ? { unwritableRoots: reach2.unwritableRoots.map((r) => path.resolve(r)) } : {},
3878
- ...reach2.hostedLookup ? { hostedLookup: reach2.hostedLookup } : {}
3894
+ ...reach2.hostedLookup ? { hostedLookup: reach2.hostedLookup } : {},
3895
+ ...reach2.writeReach ? { writeReach: reach2.writeReach } : {}
3879
3896
  },
3880
3897
  fn
3881
3898
  );
@@ -7727,6 +7744,12 @@ var ParentExcerptSchema = import_zod8.z.object({
7727
7744
  /** sha256:… over the canonical excerpt without its commit. */
7728
7745
  digest: import_zod8.z.string()
7729
7746
  });
7747
+ function relationHealth(status) {
7748
+ if (status.uses.some((u) => u.state === "changed" || u.state === "removed")) return "incompatible";
7749
+ if (!status.reachable || status.outOfReach || status.uses.some((u) => u.state === "unavailable" || u.state === "unlocked")) return "unavailable";
7750
+ if (status.drifted) return "drifted";
7751
+ return "ok";
7752
+ }
7730
7753
  var NamedOpenApiSpecSchema = import_zod8.z.object({
7731
7754
  portalId: import_zod8.z.string(),
7732
7755
  name: import_zod8.z.string(),
@@ -9048,12 +9071,16 @@ function memberProjectNodes() {
9048
9071
  trustedLinks: [],
9049
9072
  project: true
9050
9073
  }));
9051
- const referenced = (family.nodes.find((n) => n.namespace === "")?.externals ?? []).filter((e) => e.role === "member").map((e) => ({ id: e.alias, name: e.alias, description: "", trustedLinks: [], project: true, storage: storageBadge(e.sourceKind === "family" ? "contained" : e.sourceKind, e.directory, e.commit) }));
9052
- return [...contained, ...referenced];
9074
+ const rootExternals = family.nodes.find((n) => n.namespace === "")?.externals ?? [];
9075
+ const referenced = rootExternals.filter((e) => e.role === "member").map((e) => ({ id: e.alias, name: e.alias, description: "", trustedLinks: [], project: true, storage: storageBadge(e.sourceKind === "family" ? "contained" : e.sourceKind, e.directory, e.commit) }));
9076
+ const taken = new Set([...contained, ...referenced].map((n) => n.id));
9077
+ const external = rootExternals.filter((e) => e.role !== "member" && e.sourceKind !== "family" && !taken.has(e.alias)).map((e) => ({ id: e.alias, name: e.alias, description: e.project === e.alias ? "" : `project ${e.project}`, trustedLinks: [], project: true, external: true, storage: storageBadge(e.sourceKind, e.directory, e.commit) }));
9078
+ return [...contained, ...referenced, ...external];
9053
9079
  }
9054
9080
  function storageBadge(storage, directory, commit2) {
9055
9081
  if (storage === "git") return `git ${commit2 ? commit2.slice(0, 7) : ""}`.trim();
9056
9082
  if (storage === "hosted") return "hosted";
9083
+ if (storage === "unresolved") return "unresolved";
9057
9084
  if (storage === "path") return directory ? `../${directory.split(/[\\/]/).pop()}` : "sibling";
9058
9085
  return "contained";
9059
9086
  }
@@ -9065,7 +9092,60 @@ function partBadges() {
9065
9092
  }
9066
9093
  return out;
9067
9094
  }
9068
- function buildCanvasModel(issues = []) {
9095
+ function useLabel(use) {
9096
+ if (use.publicName === void 0) return use.member ?? "(the external)";
9097
+ return use.member === void 0 || use.member === "type" ? use.publicName : `${use.publicName}.${use.member}`;
9098
+ }
9099
+ function relationFrom(id, consumer, producer, status, comparedAt) {
9100
+ const changed = status.uses.filter((u) => u.state !== "unchanged");
9101
+ const health = relationHealth(status);
9102
+ return {
9103
+ id,
9104
+ consumer,
9105
+ producer,
9106
+ health,
9107
+ uses: changed.map((u) => ({ name: useLabel(u), state: u.state, ...u.detail !== void 0 ? { detail: u.detail } : {} })),
9108
+ unchanged: status.uses.length - changed.length,
9109
+ ...status.pinnedCommit !== void 0 ? { pinnedCommit: status.pinnedCommit } : {},
9110
+ ...status.pinnedDigest !== void 0 ? { pinnedDigest: status.pinnedDigest } : {},
9111
+ comparedAt,
9112
+ ...health === "unavailable" && status.detail !== void 0 ? { detail: status.detail } : {}
9113
+ };
9114
+ }
9115
+ function statusFor(entry, consumer, producer) {
9116
+ const { alias, node } = producer;
9117
+ return entry.externals.find((s) => alias !== void 0 && s.alias === alias || node !== void 0 && (node.parent === consumer && s.alias === node.mountAlias || s.sourceKind === "family" && node.id !== void 0 && s.project === node.id));
9118
+ }
9119
+ function nameRelations(edges, relations, outsideIds, ownerOf4) {
9120
+ const family = graph();
9121
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9122
+ const out = /* @__PURE__ */ new Map();
9123
+ for (const edge of edges) {
9124
+ if (!edge.consumption) continue;
9125
+ const consumer = ownerOf4(edge.from);
9126
+ const node = outsideIds.has(edge.to) ? void 0 : familyNode(family, ownerOf4(edge.to)) ?? void 0;
9127
+ const alias = outsideIds.has(edge.to) ? edge.to : void 0;
9128
+ const entry = relations.find((r) => r.project === consumer);
9129
+ const status = entry && !entry.detail ? statusFor(entry, consumer, { alias, node }) : void 0;
9130
+ const producer = status?.alias ?? alias ?? (node?.parent === consumer ? node.mountAlias : void 0) ?? node?.id ?? ownerOf4(edge.to);
9131
+ const id = `${consumer}\u2192${producer}`;
9132
+ if (!out.has(id)) {
9133
+ out.set(id, status ? relationFrom(id, consumer, producer, status, entry.comparedAt) : {
9134
+ id,
9135
+ consumer,
9136
+ producer,
9137
+ health: "unavailable",
9138
+ uses: [],
9139
+ unchanged: 0,
9140
+ comparedAt: entry?.comparedAt ?? now,
9141
+ detail: entry?.detail ?? (entry ? `the consumer's statuses name no producer "${producer}"` : "the consumer's statuses were not read")
9142
+ });
9143
+ }
9144
+ edge.relation = id;
9145
+ }
9146
+ return [...out.values()];
9147
+ }
9148
+ function buildCanvasModel(issues = [], relations) {
9069
9149
  const system = loadSystemSpec();
9070
9150
  const subsystems = loadSubsystemSpecs();
9071
9151
  const components = loadComponentSpecs();
@@ -9173,22 +9253,26 @@ function buildCanvasModel(issues = []) {
9173
9253
  const badges = partBadges();
9174
9254
  const componentSub = new Map(components.map((c) => [c.id, c.subsystem]));
9175
9255
  const edges = [];
9176
- const referencedIds = new Set(projectNodes.filter((p) => p.storage !== void 0).map((p) => p.id));
9256
+ const outsideIds = new Set(projectNodes.filter((p) => p.storage !== void 0).map((p) => p.id));
9257
+ const owners = graph().owners;
9258
+ const ownerOfSpec = (id) => owners.get(id) ?? "";
9177
9259
  for (const comp of components) {
9178
9260
  for (const depId of comp.dependsOn) {
9179
9261
  const alias = depId.includes("::") ? depId.split("::")[0] : "";
9180
- if (referencedIds.has(alias) && !componentIds.has(depId)) {
9181
- edges.push({ from: comp.id, to: alias, cross: true });
9262
+ if (outsideIds.has(alias) && !componentIds.has(depId)) {
9263
+ edges.push({ from: comp.id, to: alias, cross: true, consumption: true });
9182
9264
  continue;
9183
9265
  }
9184
9266
  if (!componentIds.has(depId)) continue;
9185
9267
  edges.push({
9186
9268
  from: comp.id,
9187
9269
  to: depId,
9188
- cross: componentSub.get(depId) !== comp.subsystem
9270
+ cross: componentSub.get(depId) !== comp.subsystem,
9271
+ ...ownerOfSpec(depId) !== ownerOfSpec(comp.id) ? { consumption: true } : {}
9189
9272
  });
9190
9273
  }
9191
9274
  }
9275
+ const canvasRelations = relations === void 0 ? void 0 : nameRelations(edges, relations, outsideIds, ownerOfSpec);
9192
9276
  const typeSpecs = loadTypeSpecs();
9193
9277
  const usedByFor = (t) => {
9194
9278
  const qualified = t.subsystem && !t.id.startsWith(`${t.subsystem}::`) ? `${t.subsystem}::${t.id}` : t.id;
@@ -9310,6 +9394,7 @@ function buildCanvasModel(issues = []) {
9310
9394
  ],
9311
9395
  components: modelComponents,
9312
9396
  edges,
9397
+ ...canvasRelations !== void 0 ? { relations: canvasRelations } : {},
9313
9398
  types: modelTypes,
9314
9399
  typeEdges,
9315
9400
  dataEdges,
@@ -9483,6 +9568,7 @@ body[data-theme="light"] .selectControl { color-scheme:light; }
9483
9568
  #cy { position:absolute; inset:0; }
9484
9569
  .legend { position:absolute; left:12px; bottom:12px; background:var(--chrome); border:1px solid var(--chrome-border); border-radius:10px; padding:8px 12px; font-size:11px; color:var(--dim); z-index:5; pointer-events:none; }
9485
9570
  .legend .sw { display:inline-block; width:10px; height:10px; border-radius:3px; margin-right:4px; vertical-align:-1px; border:1.5px solid; }
9571
+ .reltip { position:absolute; display:none; background:var(--chrome); border:1px solid var(--chrome-border); border-radius:8px; padding:6px 10px; font-size:11px; color:var(--dim); white-space:pre; z-index:6; pointer-events:none; max-width:520px; overflow:hidden; }
9486
9572
  /* Stage overlays clear the floating header (52px + 10px top + 10px gap). The
9487
9573
  header hides in presentation mode, where they return to the top edge. */
9488
9574
  .viewhint { position:absolute; top:72px; left:12px; color:var(--dim); font-size:11px; background:var(--chrome); border:1px solid var(--chrome-border); border-radius:9px; padding:5px 10px; z-index:5; pointer-events:none; }
@@ -9668,6 +9754,7 @@ body.presentation #exitPresent, body.presentation #presentDetails { display:bloc
9668
9754
  <div class="viewhint" id="viewHint"></div>
9669
9755
  <div id="typesWarn"></div>
9670
9756
  <div class="legend" id="legend"></div>
9757
+ <div class="reltip" id="relTip"></div>
9671
9758
  </div>
9672
9759
  <div id="panelResizer" title="Drag to resize details sidebar"></div>
9673
9760
  <div id="panel"></div>
@@ -9995,6 +10082,7 @@ var MODEL = __MODEL_JSON__;
9995
10082
  retired: { fill: '#ebe8e4', stroke: '#857b73', text: '#35302b' },
9996
10083
  },
9997
10084
  issue: '#b3261e', selGlow: '#3465b4', bgLabel: '#f2f5f8', png: '#f2f5f8',
10085
+ relOk: '#2e7d4f', relDrift: '#8a6116', relBad: '#b3261e', relUnknown: '#5f6b78',
9998
10086
  },
9999
10087
  syw: {
10000
10088
  pageEdge: '#7c8ca3', edgeText: '#aebdd2', cross: '#ff6b81', ink: '#eef2f8',
@@ -10016,6 +10104,7 @@ var MODEL = __MODEL_JSON__;
10016
10104
  retired: { fill: '#24211f', stroke: '#a8a29e', text: '#e7e5e4' },
10017
10105
  },
10018
10106
  issue: '#ff6b81', selGlow: '#22ddff', bgLabel: '#0a0a0f', png: '#0a0a0f',
10107
+ relOk: '#34d399', relDrift: '#f59e0b', relBad: '#ff6b81', relUnknown: '#93a1b8',
10019
10108
  },
10020
10109
  };
10021
10110
 
@@ -10078,6 +10167,15 @@ var MODEL = __MODEL_JSON__;
10078
10167
  { selector: 'edge.bundle', style: { width: 4.5, opacity: 0.7 } },
10079
10168
  { selector: 'edge.toghost', style: { 'line-style': 'dashed', opacity: 0.75 } },
10080
10169
  { selector: 'edge.inneredge', style: { width: 1.1, 'arrow-scale': 0.6, opacity: 0.8 } },
10170
+ // Relation health on consumption edges: the health colour replaces the
10171
+ // boundary-hop colour, and unavailable is dotted as well as grey, so the
10172
+ // state never rests on colour alone (the glyph is the edge's label).
10173
+ { selector: 'edge.rel-unchecked', style: { 'line-color': t.pageEdge, 'target-arrow-color': t.pageEdge } },
10174
+ { selector: 'edge.rel-ok', style: { 'line-color': t.relOk, 'target-arrow-color': t.relOk } },
10175
+ { selector: 'edge.rel-drifted', style: { 'line-color': t.relDrift, 'target-arrow-color': t.relDrift } },
10176
+ { selector: 'edge.rel-incompatible', style: { 'line-color': t.relBad, 'target-arrow-color': t.relBad } },
10177
+ { selector: 'edge.rel-unavailable', style: { 'line-color': t.relUnknown, 'target-arrow-color': t.relUnknown, 'line-style': 'dotted' } },
10178
+ { selector: '.extProject', style: { 'border-style': 'dashed' } },
10081
10179
  { selector: 'edge.stubHover', style: { 'line-color': t.selGlow, 'target-arrow-color': t.selGlow, width: 2.6, opacity: 1, 'z-compound-depth': 'top' } },
10082
10180
  { selector: '.dimmed', style: { opacity: 0.13 } },
10083
10181
  { selector: '.hasIssue', style: { 'border-color': t.issue, 'border-style': 'dashed', 'border-width': 3 } },
@@ -10114,6 +10212,83 @@ var MODEL = __MODEL_JSON__;
10114
10212
  ];
10115
10213
  }
10116
10214
 
10215
+ // ---- relation health (consumption edges) ----------------------------------
10216
+ // The model carries relations only when health was computed; without them a
10217
+ // consumption edge is drawn neutral and the legend says it was not checked \u2014
10218
+ // never green for something nobody compared.
10219
+ var HAS_RELATIONS = Array.isArray(MODEL.relations);
10220
+ var REL_BY_ID = {};
10221
+ (MODEL.relations || []).forEach(function (r) { REL_BY_ID[r.id] = r; });
10222
+ var HEALTH_RANK = { ok: 1, drifted: 2, unavailable: 3, incompatible: 4 };
10223
+ var HEALTH_GLYPH = { ok: '\\u2713', drifted: '\\u0394', incompatible: '\\u2715', unavailable: '?' };
10224
+ var HAS_CONSUMPTION = MODEL.edges.some(function (e) { return !!e.consumption; });
10225
+ function edgeHealth(edge) {
10226
+ var r = edge.relation ? REL_BY_ID[edge.relation] : null;
10227
+ return r ? r.health : (HAS_RELATIONS ? 'unavailable' : null);
10228
+ }
10229
+ // Where edges fold into one, the worst health wins.
10230
+ function worseHealth(a, b) {
10231
+ if (!a) return b;
10232
+ if (!b) return a;
10233
+ return HEALTH_RANK[b] > HEALTH_RANK[a] ? b : a;
10234
+ }
10235
+ function relationsComparedAt() {
10236
+ var at = null;
10237
+ (MODEL.relations || []).forEach(function (r) { if (!at || r.comparedAt < at) at = r.comparedAt; });
10238
+ return at;
10239
+ }
10240
+ function shortPin(r) {
10241
+ if (r.pinnedCommit) return r.pinnedCommit.slice(0, 7);
10242
+ return r.pinnedDigest ? r.pinnedDigest.replace(/^[a-z0-9-]+:/, '').slice(0, 12) : '';
10243
+ }
10244
+ function relationName(r) { return (r.consumer || 'this project') + ' \\u2192 ' + r.producer; }
10245
+ function relationText(r) {
10246
+ var lines = [relationName(r) + ': ' + HEALTH_GLYPH[r.health] + ' ' + r.health];
10247
+ r.uses.forEach(function (u) { lines.push(' ' + u.name + ' \\u2014 ' + u.state + (u.detail ? ': ' + u.detail : '')); });
10248
+ if (r.unchanged) lines.push(' ' + r.unchanged + ' use' + (r.unchanged === 1 ? '' : 's') + ' unchanged');
10249
+ if (r.detail) lines.push(' ' + r.detail);
10250
+ var pin = shortPin(r);
10251
+ lines.push((pin ? 'pinned ' + pin + ' \\u00B7 ' : '') + 'compared ' + r.comparedAt);
10252
+ return lines.join('\\n');
10253
+ }
10254
+ function relationHtml(r) {
10255
+ var pin = shortPin(r);
10256
+ return '<div class="method"><div class="mname">' + esc(relationName(r)) + '<span class="grow"></span><span class="chip">' + HEALTH_GLYPH[r.health] + ' ' + esc(r.health) + '</span></div>'
10257
+ + r.uses.map(function (u) { return '<div class="mdesc">' + esc(u.name) + ' \\u2014 ' + esc(u.state) + (u.detail ? ': ' + esc(u.detail) : '') + '</div>'; }).join('')
10258
+ + (r.unchanged ? '<div class="mdesc">' + r.unchanged + ' use' + (r.unchanged === 1 ? '' : 's') + ' unchanged</div>' : '')
10259
+ + (r.detail ? '<div class="mdesc">' + esc(r.detail) + '</div>' : '')
10260
+ + '<div class="mdesc">' + (pin ? 'pinned ' + esc(pin) + ' \\u00B7 ' : '') + 'compared ' + esc(r.comparedAt) + '</div></div>';
10261
+ }
10262
+ // The relations whose consumption edges start or land inside a node.
10263
+ function relationsTouching(sid) {
10264
+ var seen = {}, out = [];
10265
+ var inside = function (x) {
10266
+ var c = compById[x];
10267
+ return x === sid || x.indexOf(sid + '::') === 0 || !!(c && (c.subsystem === sid || c.subsystem.indexOf(sid + '::') === 0));
10268
+ };
10269
+ MODEL.edges.forEach(function (e) {
10270
+ if (!e.relation || seen[e.relation] || !(inside(e.to) || inside(e.from))) return;
10271
+ seen[e.relation] = true;
10272
+ if (REL_BY_ID[e.relation]) out.push(REL_BY_ID[e.relation]);
10273
+ });
10274
+ return out;
10275
+ }
10276
+ // The one legend row, only when the model has a consumption edge.
10277
+ function relationLegend(t) {
10278
+ if (!HAS_CONSUMPTION) return '';
10279
+ if (!HAS_RELATIONS) return '<br>relations: <span style="color:' + t.pageEdge + '">health not checked</span>';
10280
+ var at = relationsComparedAt();
10281
+ return '<br>relations: <span style="color:' + t.relOk + '">\\u2713 ok</span>&nbsp; <span style="color:' + t.relDrift + '">\\u0394 drifted</span>&nbsp; '
10282
+ + '<span style="color:' + t.relBad + '">\\u2715 incompatible</span>&nbsp; <span style="color:' + t.relUnknown + '">? unavailable (dotted)</span>'
10283
+ + (at ? '&nbsp; \\u00B7 as of ' + esc(at) : '');
10284
+ }
10285
+ // A project's own page, only through the host's open-project hook (the web
10286
+ // app): never in a standalone file or on a shared page, which have none.
10287
+ function openProjectButton(s) {
10288
+ if (!s.recordId || typeof opts === 'undefined' || !opts || typeof opts.onOpenProject !== 'function') return '';
10289
+ return '<div class="openbtn"><button class="tbtn" data-openproject-id="' + esc(s.recordId) + '">Open project \\u2197</button></div>';
10290
+ }
10291
+
10117
10292
  function renderLegend() {
10118
10293
  var t = THEMES[state.theme];
10119
10294
  var sw = function (c) { return '<span class="sw" style="background:' + c.fill + ';border-color:' + c.stroke + '"></span>'; };
@@ -10127,7 +10302,8 @@ var MODEL = __MODEL_JSON__;
10127
10302
  sw(t.proxyIn) + '\\u21E0 in-port&nbsp; ' + sw(t.proxyOut) + '\\u21E2 out-port&nbsp; \u2014 bold border = published \xB7 ' +
10128
10303
  '<span style="color:' + t.cross + '">red</span> = boundary hop \xB7 double-click = open<br>' +
10129
10304
  'on select: <span style="color:' + t.selGlow + '">\\u2192 depends on</span>&nbsp; <span style="color:' + t.warn + '">\\u2190 used by</span>' +
10130
- (state.dataCoupling ? '&nbsp; \xB7 &nbsp;<span style="color:' + t.typeV.stroke + '">- - \\u25B8 uses models</span>' : '');
10305
+ (state.dataCoupling ? '&nbsp; \xB7 &nbsp;<span style="color:' + t.typeV.stroke + '">- - \\u25B8 uses models</span>' : '') +
10306
+ relationLegend(t);
10131
10307
  }
10132
10308
 
10133
10309
  // ---- view layout ---------------------------------------------------------------
@@ -10594,6 +10770,14 @@ var MODEL = __MODEL_JSON__;
10594
10770
  if (!agg[key]) agg[key] = { src: src, tgt: tgt, n: 0, cross: false, ghost: ghost };
10595
10771
  agg[key].n++;
10596
10772
  if (edge.cross) agg[key].cross = true;
10773
+ if (edge.consumption) {
10774
+ agg[key].consumption = true;
10775
+ agg[key].health = worseHealth(agg[key].health, edgeHealth(edge));
10776
+ if (edge.relation) {
10777
+ var rels = agg[key].rels || (agg[key].rels = []);
10778
+ if (rels.indexOf(edge.relation) < 0) rels.push(edge.relation);
10779
+ }
10780
+ }
10597
10781
  });
10598
10782
  return { agg: agg, ghosts: ghosts };
10599
10783
  }
@@ -11154,7 +11338,7 @@ var MODEL = __MODEL_JSON__;
11154
11338
  var classes, label;
11155
11339
  var isPub = e.kind === 'component' && compById[e.id] && compById[e.id].public;
11156
11340
  if (e.kind === 'subsystem') {
11157
- classes = 'subsysBox';
11341
+ classes = 'subsysBox' + (subById[e.id] && subById[e.id].external ? ' extProject' : '');
11158
11342
  label = nameOf(e) + (e.hasKids && !inner ? '\\n\\u25B8 open' : '');
11159
11343
  } else {
11160
11344
  var c = compById[e.id];
@@ -11362,9 +11546,13 @@ var MODEL = __MODEL_JSON__;
11362
11546
  var bundle = e.n > 1;
11363
11547
  var dim = state.query && (dimmedAnchors[e.src] || dimmedAnchors[e.tgt]);
11364
11548
  var route = routeData(e.src, e.tgt, key);
11549
+ // A consumption edge carries its relation's health: the glyph at its middle,
11550
+ // its colour (and line, dotted when unavailable) by class.
11551
+ var glyph = e.health ? HEALTH_GLYPH[e.health] : '';
11552
+ var relCls = e.consumption ? 'rel rel-' + (e.health || 'unchecked') + ' ' : '';
11365
11553
  eles.push({
11366
- data: { id: 'e' + (i++), source: e.src, target: e.tgt, lbl: bundle ? e.n + ' links' : '', cpDist: route.cpDist, cpWeight: route.cpWeight, taxiTurn: route.taxiTurn },
11367
- classes: 'routed ' + (e.cross ? 'cross ' : '') + (bundle ? 'bundle ' : '') + (e.ghost ? 'toghost ' : '') + (dim ? 'dimmed' : ''),
11554
+ data: { id: 'e' + (i++), source: e.src, target: e.tgt, lbl: (bundle ? e.n + ' links' : '') + (glyph ? (bundle ? ' ' : '') + glyph : ''), rels: (e.rels || []).join('|'), cpDist: route.cpDist, cpWeight: route.cpWeight, taxiTurn: route.taxiTurn },
11555
+ classes: 'routed ' + (e.cross ? 'cross ' : '') + (bundle ? 'bundle ' : '') + (e.ghost ? 'toghost ' : '') + relCls + (dim ? 'dimmed' : ''),
11368
11556
  });
11369
11557
  });
11370
11558
 
@@ -11864,6 +12052,27 @@ var MODEL = __MODEL_JSON__;
11864
12052
  cy.on('mouseover', 'node', function (ev) { setHover(ev.target); });
11865
12053
  cy.on('mouseout', 'node', function (ev) { if (hoveredNode && hoveredNode.id() === ev.target.id()) setHover(null); });
11866
12054
 
12055
+ // Relation health: hovering a consumption edge shows its relation(s) \u2014 which
12056
+ // uses changed or were removed, why one could not be compared, the pin and
12057
+ // when it was compared; tapping it shows the same in the details panel.
12058
+ function relationsOfEdge(edge) {
12059
+ return (edge.data('rels') || '').split('|').filter(Boolean).map(function (rid) { return REL_BY_ID[rid]; }).filter(Boolean);
12060
+ }
12061
+ cy.on('mouseover', 'edge.rel', function (ev) {
12062
+ var tip = document.getElementById('relTip');
12063
+ var rels = relationsOfEdge(ev.target);
12064
+ tip.textContent = rels.length ? rels.map(relationText).join('\\n\\n') : 'relation health not checked';
12065
+ var at = ev.renderedPosition || { x: 0, y: 0 };
12066
+ tip.style.left = (at.x + 12) + 'px';
12067
+ tip.style.top = (at.y + 12) + 'px';
12068
+ tip.style.display = 'block';
12069
+ });
12070
+ cy.on('mouseout', 'edge.rel', function () { document.getElementById('relTip').style.display = 'none'; });
12071
+ cy.on('tap', 'edge.rel', function (ev) {
12072
+ var ids = ev.target.data('rels') || '';
12073
+ if (ids) select('relation', ids, false);
12074
+ });
12075
+
11867
12076
  cy.on('tap', 'node', function (ev) {
11868
12077
  var t = idOf(ev.target);
11869
12078
  if (t.group) return;
@@ -11921,7 +12130,15 @@ var MODEL = __MODEL_JSON__;
11921
12130
  var hasKids = t.kind === 'subsystem'
11922
12131
  ? (childSubsOf(t.id).length + childCompsOf(t.id).length) > 0
11923
12132
  : memberCompsOf(t.id).length > 0;
11924
- if (hasKids) navigateTo(t.kind, t.id);
12133
+ if (hasKids) { navigateTo(t.kind, t.id); return; }
12134
+ // A project node with nothing to drill into (a referenced or external one):
12135
+ // its own page through the host's open-project hook when it carries a
12136
+ // record id, else its details panel \u2014 its storage and its relations.
12137
+ var ps = t.kind === 'subsystem' ? subById[t.id] : null;
12138
+ if (ps && ps.project) {
12139
+ if (ps.recordId && typeof opts !== 'undefined' && opts && typeof opts.onOpenProject === 'function') { opts.onOpenProject(ps.recordId); return; }
12140
+ select('subsystem', t.id, false);
12141
+ }
11925
12142
  });
11926
12143
  cy.on('dragfree', 'node', function () { harvestPositions(); });
11927
12144
 
@@ -13158,7 +13375,7 @@ var MODEL = __MODEL_JSON__;
13158
13375
  cy.nodes().removeClass('sel');
13159
13376
  cy.edges().removeClass('fieldhl');
13160
13377
  if (id) {
13161
- var node = nodeForRef(kind, id);
13378
+ var node = kind === 'relation' ? cy.collection() : nodeForRef(kind, id);
13162
13379
  if (node.length) {
13163
13380
  node.addClass('sel');
13164
13381
  applyFocus(node);
@@ -13328,6 +13545,10 @@ var MODEL = __MODEL_JSON__;
13328
13545
  via.map(function (v) { return chip(v.label, v.kind, v.id); }).join(''), true);
13329
13546
  }
13330
13547
  }
13548
+ } else if (focusKind === 'relation') {
13549
+ var rl = String(focusId).split('|').map(function (rid) { return REL_BY_ID[rid]; }).filter(Boolean);
13550
+ head = '<h2>' + esc(rl.length === 1 ? relationName(rl[0]) : rl.length + ' relations') + '</h2>' + staticChip('relation');
13551
+ body += rl.map(relationHtml).join('');
13331
13552
  } else if (focusKind === 'type') {
13332
13553
  var ty = null;
13333
13554
  MODEL.types.forEach(function (t2) { if (t2.id === focusId) ty = t2; });
@@ -13411,8 +13632,14 @@ var MODEL = __MODEL_JSON__;
13411
13632
  + (scopeFocus ? staticChip('current view') : '')
13412
13633
  + (scopeFocus ? '' : openViewButton('subsystem', s.id, subKids > 0))
13413
13634
  + openApiButton(subsystemExposesApi(s.id), '')
13414
- + openSpecButton('subsystem', s.id);
13635
+ + openSpecButton('subsystem', s.id)
13636
+ + (s.project ? staticChip(s.external ? 'external project' : 'project') : '')
13637
+ + (s.storage ? staticChip(s.storage) : '')
13638
+ + openProjectButton(s);
13415
13639
  body += '<p class="desc">' + esc(s.description) + '</p>';
13640
+ var nodeRels = relationsTouching(s.id);
13641
+ if (nodeRels.length) body += section('Relations', nodeRels.length, nodeRels.map(relationHtml).join(''), true);
13642
+ else if (s.project && HAS_CONSUMPTION && !HAS_RELATIONS) body += section('Relations', null, '<span class="desc">relation health was not checked</span>', true);
13416
13643
  if (s.trustedLinks.length) {
13417
13644
  body += section('Trusted links (fast lanes)', s.trustedLinks.length, s.trustedLinks.map(function (t2) {
13418
13645
  return '<div class="method">' + chip(t2.subsystem, 'subsystem', t2.subsystem) + '<div class="mdesc">' + esc(t2.reason) + '</div></div>';
@@ -13479,6 +13706,14 @@ var MODEL = __MODEL_JSON__;
13479
13706
  });
13480
13707
  })(ospecs[si]);
13481
13708
  }
13709
+ var oprojs = panel.querySelectorAll('[data-openproject-id]');
13710
+ for (var opi = 0; opi < oprojs.length; opi++) {
13711
+ (function (b) {
13712
+ b.addEventListener('click', function () {
13713
+ if (typeof opts !== 'undefined' && opts && typeof opts.onOpenProject === 'function') opts.onOpenProject(b.getAttribute('data-openproject-id'));
13714
+ });
13715
+ })(oprojs[opi]);
13716
+ }
13482
13717
  var flows = panel.querySelectorAll('[data-flow-comp]');
13483
13718
  for (var j = 0; j < flows.length; j++) {
13484
13719
  (function (b) {
@@ -14074,7 +14309,14 @@ function externalStatus(binding, lock) {
14074
14309
  const { external } = binding;
14075
14310
  const entry = lock?.externals[external.alias];
14076
14311
  const pinned = entry !== void 0 && externalsRepository.readSnapshot(external.alias) !== null;
14077
- const base = { alias: external.alias, project: external.project, sourceKind: external.sourceKind, pinned };
14312
+ const base = {
14313
+ alias: external.alias,
14314
+ project: external.project,
14315
+ sourceKind: external.sourceKind,
14316
+ pinned,
14317
+ ...entry !== void 0 ? { pinnedDigest: entry.digest } : {},
14318
+ ...entry?.commit !== void 0 ? { pinnedCommit: entry.commit } : {}
14319
+ };
14078
14320
  const unreadable = external.sourceKind === "unresolved" ? hostedOnly(external) ? external.problem : `the external does not resolve: ${external.problem}` : producerUnreadable(external);
14079
14321
  let live = null;
14080
14322
  let reason = unreadable;
@@ -14314,7 +14556,8 @@ function reachCeiling(root, family) {
14314
14556
  }
14315
14557
  function within2(dir, ceiling, fn) {
14316
14558
  if (ceiling === null) return runWithProjectRoot(dir, fn);
14317
- return runWithProjectBinding(dir, { topRoot: ceiling.topRoot, parentReach: true, narrowed: ceiling.narrowed }, fn);
14559
+ const lookup2 = getHostedLookup();
14560
+ return runWithProjectBinding(dir, { topRoot: ceiling.topRoot, parentReach: true, narrowed: ceiling.narrowed, ...lookup2 ? { hostedLookup: lookup2 } : {} }, fn);
14318
14561
  }
14319
14562
  function depthOf(family, node) {
14320
14563
  let depth = 0;
@@ -14365,7 +14608,8 @@ function referencedOf(nodes) {
14365
14608
  return out;
14366
14609
  }
14367
14610
  function atReferenced(ref, fn) {
14368
- return runWithProjectBinding(ref.directory, { topRoot: ref.directory, parentReach: true, narrowed: true }, fn);
14611
+ const lookup2 = getHostedLookup();
14612
+ return runWithProjectBinding(ref.directory, { topRoot: ref.directory, parentReach: true, narrowed: true, ...lookup2 ? { hostedLookup: lookup2 } : {} }, fn);
14369
14613
  }
14370
14614
  function narrowed(family, selected) {
14371
14615
  const keys = /* @__PURE__ */ new Set(["", ...selected.map((s) => s.node.namespace)]);
@@ -14853,7 +15097,7 @@ function defaultTargetConfig(type) {
14853
15097
  enabled: true
14854
15098
  };
14855
15099
  }
14856
- var WAIRON_VERSION = "5.1.1-dev.91";
15100
+ var WAIRON_VERSION = "5.1.1-dev.93";
14857
15101
  var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
14858
15102
  var ARCHITECT_AGENT_ID = "agent-architect";
14859
15103
  var ARCHITECT_TEMPLATE_ID = "architect";
@@ -25349,10 +25593,11 @@ function familyApprovals(depth) {
25349
25593
  }
25350
25594
 
25351
25595
  // src/core/diagram.ts
25352
- function renderDiagram(format) {
25596
+ function renderDiagram(format, relations) {
25353
25597
  switch (format) {
25598
+ // Only the canvas carries relation health; the other formats ignore it.
25354
25599
  case "canvas":
25355
- return renderCanvasHtml(buildCanvasModel(diagramIssues()));
25600
+ return renderCanvasHtml(buildCanvasModel(diagramIssues(), relations));
25356
25601
  case "mermaid":
25357
25602
  return generateComponentDiagram();
25358
25603
  case "drawio":
@@ -25363,8 +25608,8 @@ function renderDiagram(format) {
25363
25608
  throw new Error(`Unsupported diagram format "${format}" (canvas | mermaid | drawio | excalidraw).`);
25364
25609
  }
25365
25610
  }
25366
- function buildCanvasDataModel() {
25367
- return buildCanvasModel(diagramIssues());
25611
+ function buildCanvasDataModel(relations) {
25612
+ return buildCanvasModel(diagramIssues(), relations);
25368
25613
  }
25369
25614
  function diagramIssues() {
25370
25615
  try {
@@ -26617,6 +26862,34 @@ var PartReadOnly = class extends WaironError {
26617
26862
  this.name = "PartReadOnly";
26618
26863
  }
26619
26864
  };
26865
+ var SubsystemWriteDenied = class extends WaironError {
26866
+ constructor(message) {
26867
+ super(message);
26868
+ this.name = "SubsystemWriteDenied";
26869
+ }
26870
+ };
26871
+ function deniedOwners(reach2, owners) {
26872
+ const seen = /* @__PURE__ */ new Set();
26873
+ const denied = [];
26874
+ for (const o of owners) {
26875
+ const key = `${path21.resolve(o.root)}|${o.subsystemId ?? ""}`;
26876
+ if (seen.has(key)) continue;
26877
+ seen.add(key);
26878
+ if (writeReachPermits(reach2, o.root, o.subsystemId)) continue;
26879
+ const rule = o.subsystemId === null ? void 0 : reach2.subsystems.find((s) => s.subsystemId === o.subsystemId && path21.resolve(s.root) === path21.resolve(o.root));
26880
+ denied.push(o.subsystemId === null ? `project "${o.project}" itself (a spec of no subsystem, decided at the project rung)` : `subsystem "${o.subsystemId}" of project "${o.project}" (decided by ${rule?.decidedBy ?? "the project rung"})`);
26881
+ }
26882
+ return denied;
26883
+ }
26884
+ function assertSpecWritesPermitted(owners, what) {
26885
+ const reach2 = getWriteReach();
26886
+ if (!reach2) return;
26887
+ const denied = deniedOwners(reach2, owners);
26888
+ if (denied.length === 0) return;
26889
+ throw new SubsystemWriteDenied(
26890
+ `SubsystemWriteDenied: ${what} would change ${denied.length === 1 ? "a spec" : "specs"} this request may not change \u2014 ${denied.join("; ")}. Nothing was written.`
26891
+ );
26892
+ }
26620
26893
  function unknownKeysIn(raw, parsed, at, out) {
26621
26894
  if (Array.isArray(raw)) {
26622
26895
  if (!Array.isArray(parsed) || parsed.length !== raw.length) return;
@@ -28627,6 +28900,92 @@ var SpecWorkspace = class {
28627
28900
  `The spec file ${file} belongs to the part "${held.part.alias}", fetched from git (${held.source?.git ?? "its repository"} at ${held.part.commit ?? "its pinned commit"}): a git part is read-only here. Edit it in its own repository, commit it there, and take it in with \`wairon member update ${held.part.alias}\`. Nothing was written.`
28628
28901
  );
28629
28902
  }
28903
+ /**
28904
+ * spec_registry.save step 5 / delete step 2: inside a hosted request, may the
28905
+ * request change this spec? Judged against the request's write reach with the
28906
+ * home's project root and the spec's owning subsystem — the document written
28907
+ * and, when a stored document is replaced or removed, the one stored (a
28908
+ * component moved out of a subsystem changes that subsystem too). Outside a
28909
+ * hosted request nothing is judged.
28910
+ */
28911
+ assertHomeInReach(file, kind, document) {
28912
+ if (!getWriteReach()) return;
28913
+ const raw = this.homeRootOf(file);
28914
+ const owners = [];
28915
+ if (document !== null) owners.push(this.documentOwner(raw, kind, document));
28916
+ if (pathExists(file)) {
28917
+ try {
28918
+ owners.push(this.documentOwner(raw, kind, readSpecFile(file)));
28919
+ } catch {
28920
+ }
28921
+ }
28922
+ assertSpecWritesPermitted(owners, `writing ${kind} spec ${path21.basename(path21.dirname(file))}/${path21.basename(file)}`);
28923
+ }
28924
+ /** The project root a spec file is homed under: the root whose specs folder, or one of whose parts' folders, holds it (deepest wins). */
28925
+ homeRootOf(file) {
28926
+ this.scanAll();
28927
+ let best;
28928
+ let depth = -1;
28929
+ for (const raw of this.cachedRawRoots) {
28930
+ const homes = [aiPathsAt(raw.dir).specsDir(), ...raw.record.parts.map((p) => p.directory).filter((d) => d !== void 0)];
28931
+ for (const home of homes) {
28932
+ if (isWithin(home, file) && path21.resolve(home).length > depth) {
28933
+ best = raw;
28934
+ depth = path21.resolve(home).length;
28935
+ }
28936
+ }
28937
+ }
28938
+ if (!best) return { dir: this.rootDir, namespace: "", project: path21.basename(this.rootDir) };
28939
+ return { dir: best.dir, namespace: best.record.namespace, project: idOf(best) ?? (best.record.namespace || path21.basename(best.dir)) };
28940
+ }
28941
+ /**
28942
+ * The owner of one spec DOCUMENT as stored in its root (its references local
28943
+ * to that root): a subsystem is its own, a component or type names its
28944
+ * subsystem, an interface or implementation follows its component; the L0, a
28945
+ * system-level type and a type group belong to no subsystem.
28946
+ */
28947
+ documentOwner(root, kind, document) {
28948
+ const doc = document ?? {};
28949
+ const local = (v) => typeof v === "string" && v ? splitNamespace(v).localId : null;
28950
+ const key = (v) => typeof v === "string" && v ? keyIn(root.namespace, splitNamespace(v).localId) : null;
28951
+ let subsystemId = null;
28952
+ if (kind === "subsystem") subsystemId = local(doc.id);
28953
+ else if (kind === "component" || kind === "type") subsystemId = local(doc.subsystem);
28954
+ else if (kind === "interface") subsystemId = this.componentSubsystem(key(doc.component));
28955
+ else if (kind === "implementation") {
28956
+ const contract = key(doc.contract);
28957
+ subsystemId = this.componentSubsystem(contract ? this.loadInterfaceSpec(contract)?.component ?? null : null);
28958
+ }
28959
+ return { root: root.dir, project: root.project, subsystemId };
28960
+ }
28961
+ /** The local id of a component's subsystem, by the component's key; null when no such component is stored. */
28962
+ componentSubsystem(componentKey) {
28963
+ if (!componentKey) return null;
28964
+ const subsystem = this.loadComponentSpec(componentKey)?.subsystem;
28965
+ return subsystem ? splitNamespace(subsystem).localId : null;
28966
+ }
28967
+ /**
28968
+ * The owner of one IN-MEMORY spec (its references keys, as the loader binds
28969
+ * them) — what a multi-spec write (a rename, a method move) judges before its
28970
+ * first write. `staged` answers a spec the write itself creates.
28971
+ */
28972
+ specOwner(kind, spec, staged) {
28973
+ this.scanAll();
28974
+ const id = kind === "system" ? "" : String(spec?.id ?? "");
28975
+ const namespace = (kind === "system" ? "" : this.ownerKeyOf(id)) ?? "";
28976
+ const raw = this.cachedRawRoots.find((r) => r.record.namespace === namespace);
28977
+ const root = raw ? { dir: raw.dir, project: idOf(raw) ?? (namespace || path21.basename(raw.dir)) } : { dir: this.rootDir, project: path21.basename(this.rootDir) };
28978
+ const localOfKey = (v) => v ? splitNamespace(v).localId : null;
28979
+ let subsystemId = null;
28980
+ if (kind === "subsystem") subsystemId = localOfKey(id);
28981
+ else if (kind === "component" || kind === "type") subsystemId = localOfKey(spec?.subsystem);
28982
+ else if (kind === "interface") subsystemId = this.componentSubsystem(spec?.component ?? null);
28983
+ else if (kind === "implementation" && spec?.contract) {
28984
+ const component = this.loadInterfaceSpec(spec.contract)?.component ?? staged?.("interface", spec.contract)?.component ?? null;
28985
+ subsystemId = this.componentSubsystem(component);
28986
+ }
28987
+ return { root: root.dir, project: root.project, subsystemId };
28988
+ }
28630
28989
  /**
28631
28990
  * The key one spec file is approved under (stage 8): its path from the bound
28632
28991
  * root, POSIX — or, for a part's file, `members/<alias>/<path inside the
@@ -28637,13 +28996,15 @@ var SpecWorkspace = class {
28637
28996
  return approvalKeyIn(file, this.rootDir, this.cachedRoots[0]?.parts ?? []);
28638
28997
  }
28639
28998
  /** The file store's write, at a home the writer resolved — a part's folder included, a git part's refused. */
28640
- writeHome(file, document) {
28999
+ writeHome(file, document, kind) {
28641
29000
  this.assertHomeWritable(file);
29001
+ this.assertHomeInReach(file, kind, document);
28642
29002
  writeSpecFile(file, document);
28643
29003
  }
28644
29004
  /** The file store's delete, pruning up to the specs folder the file lives in (its part's, for a part's file). */
28645
- removeHome(file) {
29005
+ removeHome(file, kind) {
28646
29006
  this.assertHomeWritable(file);
29007
+ this.assertHomeInReach(file, kind, null);
28647
29008
  const part = this.partHolding(file)?.part;
28648
29009
  return removeSpecFile(file, part?.directory ? aiPathsAt(part.directory).specsDir() : this.paths.specsDir());
28649
29010
  }
@@ -28994,6 +29355,7 @@ var SpecWorkspace = class {
28994
29355
  }
28995
29356
  saveSystemSpec(spec) {
28996
29357
  const p = this.paths.specsSystem();
29358
+ this.assertHomeInReach(p, "system", spec);
28997
29359
  ensureDir(path21.dirname(p));
28998
29360
  writeSpecFile(p, parseOrThrow(SystemSpecSchema, spec, "system", spec.name));
28999
29361
  invalidateSpecCache();
@@ -29144,12 +29506,12 @@ var SpecWorkspace = class {
29144
29506
  }
29145
29507
  }
29146
29508
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
29147
- this.writeHome(p, parseOrThrow(SubsystemSpecSchema, specToWrite, "subsystem", spec.id));
29509
+ this.writeHome(p, parseOrThrow(SubsystemSpecSchema, specToWrite, "subsystem", spec.id), "subsystem");
29148
29510
  invalidateSpecCache();
29149
29511
  }
29150
29512
  deleteSubsystemSpec(id) {
29151
29513
  const p = this.getSubsystemPath(id);
29152
- if (!this.removeHome(p)) return false;
29514
+ if (!this.removeHome(p, "subsystem")) return false;
29153
29515
  invalidateSpecCache();
29154
29516
  return true;
29155
29517
  }
@@ -29204,14 +29566,14 @@ var SpecWorkspace = class {
29204
29566
  );
29205
29567
  }
29206
29568
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
29207
- this.writeHome(p, parseOrThrow(ComponentSpecSchema, specToWrite, "component", spec.id));
29569
+ this.writeHome(p, parseOrThrow(ComponentSpecSchema, specToWrite, "component", spec.id), "component");
29208
29570
  invalidateSpecCache();
29209
29571
  this.normalizeComponentLayout();
29210
29572
  return notices;
29211
29573
  }
29212
29574
  deleteComponentSpec(id) {
29213
29575
  const p = this.getComponentPath(id);
29214
- if (!this.removeHome(p)) return false;
29576
+ if (!this.removeHome(p, "component")) return false;
29215
29577
  invalidateSpecCache();
29216
29578
  return true;
29217
29579
  }
@@ -29333,13 +29695,13 @@ var SpecWorkspace = class {
29333
29695
  }
29334
29696
  }
29335
29697
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
29336
- this.writeHome(p, parseOrThrow(InterfaceSpecSchema, specToWrite, "interface", spec.id));
29698
+ this.writeHome(p, parseOrThrow(InterfaceSpecSchema, specToWrite, "interface", spec.id), "interface");
29337
29699
  invalidateSpecCache();
29338
29700
  return notices;
29339
29701
  }
29340
29702
  deleteInterfaceSpec(id) {
29341
29703
  const p = this.getInterfacePath(id);
29342
- if (!this.removeHome(p)) return false;
29704
+ if (!this.removeHome(p, "interface")) return false;
29343
29705
  invalidateSpecCache();
29344
29706
  return true;
29345
29707
  }
@@ -29389,13 +29751,13 @@ var SpecWorkspace = class {
29389
29751
  }
29390
29752
  }
29391
29753
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
29392
- this.writeHome(p, parseOrThrow(ImplementationSpecSchema, specToWrite, "implementation", spec.id));
29754
+ this.writeHome(p, parseOrThrow(ImplementationSpecSchema, specToWrite, "implementation", spec.id), "implementation");
29393
29755
  invalidateSpecCache();
29394
29756
  return notices;
29395
29757
  }
29396
29758
  deleteImplementationSpec(id) {
29397
29759
  const p = this.getImplementationPath(id);
29398
- if (!this.removeHome(p)) return false;
29760
+ if (!this.removeHome(p, "implementation")) return false;
29399
29761
  invalidateSpecCache();
29400
29762
  return true;
29401
29763
  }
@@ -29441,14 +29803,14 @@ var SpecWorkspace = class {
29441
29803
  }
29442
29804
  }
29443
29805
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
29444
- this.writeHome(p, parseOrThrow(TypeSpecSchema, specToWrite, "type", spec.id));
29806
+ this.writeHome(p, parseOrThrow(TypeSpecSchema, specToWrite, "type", spec.id), "type");
29445
29807
  invalidateSpecCache();
29446
29808
  return notices;
29447
29809
  }
29448
29810
  deleteTypeSpec(id) {
29449
29811
  const spec = this.loadTypeSpec(id);
29450
29812
  const p = this.getTypePath(id, spec?.subsystem, spec?.group);
29451
- if (!this.removeHome(p)) return false;
29813
+ if (!this.removeHome(p, "type")) return false;
29452
29814
  invalidateSpecCache();
29453
29815
  return true;
29454
29816
  }
@@ -29471,12 +29833,12 @@ var SpecWorkspace = class {
29471
29833
  specToWrite.createdAt = existing.createdAt;
29472
29834
  }
29473
29835
  specToWrite.updatedAt = opts?.preserveUpdatedAt && existing?.updatedAt ? existing.updatedAt : (/* @__PURE__ */ new Date()).toISOString();
29474
- this.writeHome(p, parseOrThrow(GroupSpecSchema, specToWrite, "group", spec.id));
29836
+ this.writeHome(p, parseOrThrow(GroupSpecSchema, specToWrite, "group", spec.id), "group");
29475
29837
  invalidateSpecCache();
29476
29838
  }
29477
29839
  deleteGroupSpec(id) {
29478
29840
  const p = this.getGroupPath(id);
29479
- if (!this.removeHome(p)) return false;
29841
+ if (!this.removeHome(p, "group")) return false;
29480
29842
  invalidateSpecCache();
29481
29843
  return true;
29482
29844
  }
@@ -29674,7 +30036,7 @@ var SpecWorkspace = class {
29674
30036
  );
29675
30037
  }
29676
30038
  if (JSON.stringify(document) === JSON.stringify(stored)) return false;
29677
- this.writeHome(file, document);
30039
+ this.writeHome(file, document, kind);
29678
30040
  invalidateSpecCache();
29679
30041
  return true;
29680
30042
  }
@@ -30588,6 +30950,7 @@ var SpecWorkspace = class {
30588
30950
  }
30589
30951
  }
30590
30952
  const carried = projectConfigRepository.rekeyCarried(rename, true);
30953
+ this.assertMoveInReach(from, to, plan6, carried.length > 0);
30591
30954
  if (dryRun) {
30592
30955
  const edits2 = moveChangeReports([...plan6.edits.values()], true);
30593
30956
  return {
@@ -30627,6 +30990,14 @@ var SpecWorkspace = class {
30627
30990
  carried
30628
30991
  };
30629
30992
  }
30993
+ /** Refuse a method move, naming every subsystem it may not change, before anything is written. */
30994
+ assertMoveInReach(from, to, plan6, rekeysRegister) {
30995
+ if (!getWriteReach()) return;
30996
+ const staged = (kind, id) => plan6.edits.get(`${kind}:${id}`)?.after;
30997
+ const owners = [...plan6.edits.values()].flatMap((e) => [e.before, e.after].filter((s) => s !== null && s !== void 0).map((s) => this.specOwner(e.kind, s, staged)));
30998
+ if (rekeysRegister) owners.push(this.specOwner("system", null));
30999
+ assertSpecWritesPermitted(owners, `moving methods from "${from}" to "${to}"`);
31000
+ }
30630
31001
  /**
30631
31002
  * Rekey every lint allow keyed on a moved method, staging each spec it
30632
31003
  * changes like any other reference the move follows. An allow anchored at a
@@ -31357,6 +31728,14 @@ function normalizeReferences(kind, id) {
31357
31728
  function rewriteReferences(kind, id, edits) {
31358
31729
  return current().respellReferences(kind, id, edits);
31359
31730
  }
31731
+ function assertSpecsInReach(targets, what, projectRung = false) {
31732
+ if (!getWriteReach()) return;
31733
+ const ws = current();
31734
+ const writable = ["system", "subsystem", "component", "interface", "implementation", "type"];
31735
+ const owners = targets.map((t) => ws.specOwner(t.kind, t.kind === "system" ? null : (writable.includes(t.kind) ? ws.load(t.kind, t.id) : null) ?? { id: t.id }));
31736
+ if (projectRung) owners.push(ws.specOwner("system", null));
31737
+ assertSpecWritesPermitted(owners, what);
31738
+ }
31360
31739
  function moveMethods(from, to, methods, hooks, dryRun) {
31361
31740
  return current().moveMethods(from, to, methods, hooks, dryRun);
31362
31741
  }
@@ -32922,7 +33301,7 @@ function restateReExports(from, to) {
32922
33301
  publicInterfaces: entries.map((e) => e.from === from ? { ...e, from: to } : e)
32923
33302
  });
32924
33303
  }
32925
- function rewriteRefFields(specsDir, remap, excludeDir) {
33304
+ function rewriteRefFields(specsDir, remap, excludeDir, dryRun = false) {
32926
33305
  const rewritten = [];
32927
33306
  for (const file of listFilesRecursive(specsDir, ".yaml")) {
32928
33307
  if (excludeDir && isWithinDir2(excludeDir, file)) continue;
@@ -32935,12 +33314,12 @@ function rewriteRefFields(specsDir, remap, excludeDir) {
32935
33314
  if (!raw || typeof raw !== "object") continue;
32936
33315
  const kind = specKind(raw);
32937
33316
  if (!rewriteSpecRefs(raw, remap) || !kind) continue;
32938
- writeYamlFile(file, raw);
33317
+ if (!dryRun) writeYamlFile(file, raw);
32939
33318
  rewritten.push({ kind, id: kind === "system" ? "system" : String(raw.id) });
32940
33319
  }
32941
33320
  return rewritten;
32942
33321
  }
32943
- function rekeyLintAllows(specsDir, rename) {
33322
+ function rekeyLintAllows(specsDir, rename, dryRun = false) {
32944
33323
  const rewritten = [];
32945
33324
  for (const file of listFilesRecursive(specsDir, ".yaml")) {
32946
33325
  let raw;
@@ -32971,7 +33350,7 @@ function rekeyLintAllows(specsDir, rename) {
32971
33350
  }
32972
33351
  }
32973
33352
  if (!changed) continue;
32974
- writeYamlFile(file, raw);
33353
+ if (!dryRun) writeYamlFile(file, raw);
32975
33354
  rewritten.push({ kind, id: specId });
32976
33355
  }
32977
33356
  return rewritten;
@@ -33049,15 +33428,22 @@ function renameComponent(componentId, newId) {
33049
33428
  specs: renamed.map(({ from, to }) => ({ from, to })),
33050
33429
  components: [{ from: componentId, to: newId }]
33051
33430
  };
33052
- projectConfigRepository.rekeyCarried(rename, true);
33053
- const rewrittenRefs = rewriteRefFields(aiPathsAt(getProjectRoot()).specsDir(), (ref, position) => {
33431
+ const registerEdits = projectConfigRepository.rekeyCarried(rename, true);
33432
+ const remap = (ref, position) => {
33054
33433
  if (position === "component" || position === "entity-class" || position === "auth-source") {
33055
33434
  return ref === componentId ? newId : ref;
33056
33435
  }
33057
33436
  if (position === "interface" && ref === movingInterface?.id) return interfaceId;
33058
33437
  return ref;
33059
- });
33060
- const allowsRekeyed = rekeyLintAllows(aiPathsAt(getProjectRoot()).specsDir(), rename);
33438
+ };
33439
+ const specsDir = aiPathsAt(getProjectRoot()).specsDir();
33440
+ assertSpecsInReach(
33441
+ [...renamed.map(({ kind, from }) => ({ kind, id: from })), ...rewriteRefFields(specsDir, remap, void 0, true), ...rekeyLintAllows(specsDir, rename, true)],
33442
+ `renaming component "${componentId}"`,
33443
+ Array.isArray(registerEdits) && registerEdits.length > 0
33444
+ );
33445
+ const rewrittenRefs = rewriteRefFields(specsDir, remap);
33446
+ const allowsRekeyed = rekeyLintAllows(specsDir, rename);
33061
33447
  const rewritten = [...new Set([...rewrittenRefs, ...allowsRekeyed].filter((spec) => !renamed.some((moved) => moved.kind === spec.kind && moved.from === spec.id)).map((spec) => spec.id))];
33062
33448
  invalidateSpecCache();
33063
33449
  moveRenamedSpecs(componentId, newId, component, movingInterface, movingImplementation);
@@ -33147,7 +33533,18 @@ function renameMethod(componentId, methodName, newName, pinSymbol) {
33147
33533
  ]
33148
33534
  }]
33149
33535
  };
33150
- projectConfigRepository.rekeyCarried(rename, true);
33536
+ const registerEdits = projectConfigRepository.rekeyCarried(rename, true);
33537
+ const methodRemap = (ref, position, owner) => position === "method" && owner === componentId && ref === methodName ? newName : ref;
33538
+ const reachDir = aiPathsAt(getProjectRoot()).specsDir();
33539
+ assertSpecsInReach(
33540
+ [
33541
+ ...(rename.methods?.[0]?.specs ?? []).map((id) => ({ kind: moving.some((i) => i.id === id) ? "interface" : "implementation", id })),
33542
+ ...rewriteRefFields(reachDir, methodRemap, void 0, true),
33543
+ ...rekeyLintAllows(reachDir, rename, true)
33544
+ ],
33545
+ `renaming method "${componentId}.${methodName}"`,
33546
+ Array.isArray(registerEdits) && registerEdits.length > 0
33547
+ );
33151
33548
  for (const contract of moving) {
33152
33549
  saveInterfaceSpec({
33153
33550
  ...contract,
@@ -33170,7 +33567,7 @@ function renameMethod(componentId, methodName, newName, pinSymbol) {
33170
33567
  }
33171
33568
  invalidateSpecCache();
33172
33569
  const specsDir = aiPathsAt(getProjectRoot()).specsDir();
33173
- const retargeted = rewriteRefFields(specsDir, (ref, position, owner) => position === "method" && owner === componentId && ref === methodName ? newName : ref);
33570
+ const retargeted = rewriteRefFields(specsDir, methodRemap);
33174
33571
  const allowsRekeyed = rekeyLintAllows(specsDir, rename);
33175
33572
  const rewritten = [...new Set([...retargeted, ...allowsRekeyed].map((spec) => spec.id))];
33176
33573
  const mentions = collectMentions(specsDir, methodName, moving);
@@ -39209,10 +39606,13 @@ function rehearse2(plan6) {
39209
39606
  return { ...plan6, refusals: [...plan6.refusals, { code: `${plan6.request.verb}-refused`, project: "", detail }] };
39210
39607
  }
39211
39608
  const changes = diff(rehearsal);
39212
- const outside = unwritableOwners(changes);
39213
- if (outside.length > 0) {
39609
+ const refused2 = [
39610
+ ...unwritableOwners(changes).map((o) => ({ code: "family-partial", project: o, detail: `the plan must write ${o}, which this request may not write` })),
39611
+ ...deniedSubsystems(changes)
39612
+ ];
39613
+ if (refused2.length > 0) {
39214
39614
  discard(rehearsal);
39215
- return { ...plan6, refusals: [...plan6.refusals, ...outside.map((o) => ({ code: "family-partial", project: o, detail: `the plan must write ${o}, which this request may not write` }))] };
39615
+ return { ...plan6, refusals: [...plan6.refusals, ...refused2] };
39216
39616
  }
39217
39617
  return { ...plan6, rehearsal, changes, relock: relockOf(plan6, changes) };
39218
39618
  }
@@ -39256,6 +39656,16 @@ function unwritableOwners(changes) {
39256
39656
  const owners = [...new Set(changes.map((c) => path47.resolve(c.project)))];
39257
39657
  return owners.filter((o) => denied.includes(o));
39258
39658
  }
39659
+ function deniedSubsystems(changes) {
39660
+ const reach2 = getWriteReach();
39661
+ if (!reach2) return [];
39662
+ const owners = [...new Set(changes.map((c) => path47.resolve(c.project)))];
39663
+ return owners.flatMap((o) => writeReachDenials(reach2, o).map((s) => ({
39664
+ code: "subsystem-denied",
39665
+ project: o,
39666
+ detail: `the plan must change ${o}, which holds subsystem "${s.subsystemId}" this request may not change (decided by ${s.decidedBy}); a family migration reshapes its projects whole, so it needs write on every subsystem of every project it changes`
39667
+ })));
39668
+ }
39259
39669
  function pathProducers(plan6, family) {
39260
39670
  const inside = (dir) => {
39261
39671
  const rel2 = path47.relative(plan6.familyRoot, dir);
@@ -39525,7 +39935,10 @@ var externalStatusesOutput = {
39525
39935
  stale: import_zod11.z.boolean(),
39526
39936
  drifted: import_zod11.z.boolean().optional(),
39527
39937
  uses: import_zod11.z.array(import_zod11.z.record(import_zod11.z.unknown())),
39528
- detail: import_zod11.z.string().optional()
39938
+ detail: import_zod11.z.string().optional(),
39939
+ outOfReach: import_zod11.z.boolean().optional(),
39940
+ pinnedDigest: import_zod11.z.string().optional().describe("The content digest the lock recorded; provenance, never compared here."),
39941
+ pinnedCommit: import_zod11.z.string().optional().describe("The producer commit the lock recorded, for one in another repository; provenance only.")
39529
39942
  })).describe("One status per declared external, in declaration order; EXTERNAL_CHECK_UNAVAILABLE marks what could not be compared."),
39530
39943
  ...staleServerOutput
39531
39944
  };
@@ -41613,6 +42026,7 @@ function loadProjectConfig3() {
41613
42026
  getRequestParentReach,
41614
42027
  getRequestProjectRoot,
41615
42028
  getStatusReport,
42029
+ getWriteReach,
41616
42030
  globalGuideFilePath,
41617
42031
  globalPacksDir,
41618
42032
  globalPacksEnabled,
@@ -41717,6 +42131,7 @@ function loadProjectConfig3() {
41717
42131
  registerExporter,
41718
42132
  registerPackRef,
41719
42133
  reinjectLocalGuides,
42134
+ relationHealth,
41720
42135
  removeDomain,
41721
42136
  removeExternal,
41722
42137
  removeMember,
@@ -41783,6 +42198,8 @@ function loadProjectConfig3() {
41783
42198
  writeFileIfChanged,
41784
42199
  writeJsonFile,
41785
42200
  writeLockRecord,
42201
+ writeReachDenials,
42202
+ writeReachPermits,
41786
42203
  writeRootGuideDelegator,
41787
42204
  writeYamlFile
41788
42205
  });