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

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
@@ -3725,6 +3725,7 @@ __export(src_exports, {
3725
3725
  registerExporter: () => registerExporter,
3726
3726
  registerPackRef: () => registerPackRef,
3727
3727
  reinjectLocalGuides: () => reinjectLocalGuides,
3728
+ relationHealth: () => relationHealth,
3728
3729
  removeDomain: () => removeDomain,
3729
3730
  removeExternal: () => removeExternal,
3730
3731
  removeMember: () => removeMember,
@@ -7727,6 +7728,12 @@ var ParentExcerptSchema = import_zod8.z.object({
7727
7728
  /** sha256:… over the canonical excerpt without its commit. */
7728
7729
  digest: import_zod8.z.string()
7729
7730
  });
7731
+ function relationHealth(status) {
7732
+ if (status.uses.some((u) => u.state === "changed" || u.state === "removed")) return "incompatible";
7733
+ if (!status.reachable || status.outOfReach || status.uses.some((u) => u.state === "unavailable" || u.state === "unlocked")) return "unavailable";
7734
+ if (status.drifted) return "drifted";
7735
+ return "ok";
7736
+ }
7730
7737
  var NamedOpenApiSpecSchema = import_zod8.z.object({
7731
7738
  portalId: import_zod8.z.string(),
7732
7739
  name: import_zod8.z.string(),
@@ -9048,12 +9055,16 @@ function memberProjectNodes() {
9048
9055
  trustedLinks: [],
9049
9056
  project: true
9050
9057
  }));
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];
9058
+ const rootExternals = family.nodes.find((n) => n.namespace === "")?.externals ?? [];
9059
+ 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) }));
9060
+ const taken = new Set([...contained, ...referenced].map((n) => n.id));
9061
+ 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) }));
9062
+ return [...contained, ...referenced, ...external];
9053
9063
  }
9054
9064
  function storageBadge(storage, directory, commit2) {
9055
9065
  if (storage === "git") return `git ${commit2 ? commit2.slice(0, 7) : ""}`.trim();
9056
9066
  if (storage === "hosted") return "hosted";
9067
+ if (storage === "unresolved") return "unresolved";
9057
9068
  if (storage === "path") return directory ? `../${directory.split(/[\\/]/).pop()}` : "sibling";
9058
9069
  return "contained";
9059
9070
  }
@@ -9065,7 +9076,60 @@ function partBadges() {
9065
9076
  }
9066
9077
  return out;
9067
9078
  }
9068
- function buildCanvasModel(issues = []) {
9079
+ function useLabel(use) {
9080
+ if (use.publicName === void 0) return use.member ?? "(the external)";
9081
+ return use.member === void 0 || use.member === "type" ? use.publicName : `${use.publicName}.${use.member}`;
9082
+ }
9083
+ function relationFrom(id, consumer, producer, status, comparedAt) {
9084
+ const changed = status.uses.filter((u) => u.state !== "unchanged");
9085
+ const health = relationHealth(status);
9086
+ return {
9087
+ id,
9088
+ consumer,
9089
+ producer,
9090
+ health,
9091
+ uses: changed.map((u) => ({ name: useLabel(u), state: u.state, ...u.detail !== void 0 ? { detail: u.detail } : {} })),
9092
+ unchanged: status.uses.length - changed.length,
9093
+ ...status.pinnedCommit !== void 0 ? { pinnedCommit: status.pinnedCommit } : {},
9094
+ ...status.pinnedDigest !== void 0 ? { pinnedDigest: status.pinnedDigest } : {},
9095
+ comparedAt,
9096
+ ...health === "unavailable" && status.detail !== void 0 ? { detail: status.detail } : {}
9097
+ };
9098
+ }
9099
+ function statusFor(entry, consumer, producer) {
9100
+ const { alias, node } = producer;
9101
+ 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));
9102
+ }
9103
+ function nameRelations(edges, relations, outsideIds, ownerOf4) {
9104
+ const family = graph();
9105
+ const now = (/* @__PURE__ */ new Date()).toISOString();
9106
+ const out = /* @__PURE__ */ new Map();
9107
+ for (const edge of edges) {
9108
+ if (!edge.consumption) continue;
9109
+ const consumer = ownerOf4(edge.from);
9110
+ const node = outsideIds.has(edge.to) ? void 0 : familyNode(family, ownerOf4(edge.to)) ?? void 0;
9111
+ const alias = outsideIds.has(edge.to) ? edge.to : void 0;
9112
+ const entry = relations.find((r) => r.project === consumer);
9113
+ const status = entry && !entry.detail ? statusFor(entry, consumer, { alias, node }) : void 0;
9114
+ const producer = status?.alias ?? alias ?? (node?.parent === consumer ? node.mountAlias : void 0) ?? node?.id ?? ownerOf4(edge.to);
9115
+ const id = `${consumer}\u2192${producer}`;
9116
+ if (!out.has(id)) {
9117
+ out.set(id, status ? relationFrom(id, consumer, producer, status, entry.comparedAt) : {
9118
+ id,
9119
+ consumer,
9120
+ producer,
9121
+ health: "unavailable",
9122
+ uses: [],
9123
+ unchanged: 0,
9124
+ comparedAt: entry?.comparedAt ?? now,
9125
+ detail: entry?.detail ?? (entry ? `the consumer's statuses name no producer "${producer}"` : "the consumer's statuses were not read")
9126
+ });
9127
+ }
9128
+ edge.relation = id;
9129
+ }
9130
+ return [...out.values()];
9131
+ }
9132
+ function buildCanvasModel(issues = [], relations) {
9069
9133
  const system = loadSystemSpec();
9070
9134
  const subsystems = loadSubsystemSpecs();
9071
9135
  const components = loadComponentSpecs();
@@ -9173,22 +9237,26 @@ function buildCanvasModel(issues = []) {
9173
9237
  const badges = partBadges();
9174
9238
  const componentSub = new Map(components.map((c) => [c.id, c.subsystem]));
9175
9239
  const edges = [];
9176
- const referencedIds = new Set(projectNodes.filter((p) => p.storage !== void 0).map((p) => p.id));
9240
+ const outsideIds = new Set(projectNodes.filter((p) => p.storage !== void 0).map((p) => p.id));
9241
+ const owners = graph().owners;
9242
+ const ownerOfSpec = (id) => owners.get(id) ?? "";
9177
9243
  for (const comp of components) {
9178
9244
  for (const depId of comp.dependsOn) {
9179
9245
  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 });
9246
+ if (outsideIds.has(alias) && !componentIds.has(depId)) {
9247
+ edges.push({ from: comp.id, to: alias, cross: true, consumption: true });
9182
9248
  continue;
9183
9249
  }
9184
9250
  if (!componentIds.has(depId)) continue;
9185
9251
  edges.push({
9186
9252
  from: comp.id,
9187
9253
  to: depId,
9188
- cross: componentSub.get(depId) !== comp.subsystem
9254
+ cross: componentSub.get(depId) !== comp.subsystem,
9255
+ ...ownerOfSpec(depId) !== ownerOfSpec(comp.id) ? { consumption: true } : {}
9189
9256
  });
9190
9257
  }
9191
9258
  }
9259
+ const canvasRelations = relations === void 0 ? void 0 : nameRelations(edges, relations, outsideIds, ownerOfSpec);
9192
9260
  const typeSpecs = loadTypeSpecs();
9193
9261
  const usedByFor = (t) => {
9194
9262
  const qualified = t.subsystem && !t.id.startsWith(`${t.subsystem}::`) ? `${t.subsystem}::${t.id}` : t.id;
@@ -9310,6 +9378,7 @@ function buildCanvasModel(issues = []) {
9310
9378
  ],
9311
9379
  components: modelComponents,
9312
9380
  edges,
9381
+ ...canvasRelations !== void 0 ? { relations: canvasRelations } : {},
9313
9382
  types: modelTypes,
9314
9383
  typeEdges,
9315
9384
  dataEdges,
@@ -9483,6 +9552,7 @@ body[data-theme="light"] .selectControl { color-scheme:light; }
9483
9552
  #cy { position:absolute; inset:0; }
9484
9553
  .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
9554
  .legend .sw { display:inline-block; width:10px; height:10px; border-radius:3px; margin-right:4px; vertical-align:-1px; border:1.5px solid; }
9555
+ .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
9556
  /* Stage overlays clear the floating header (52px + 10px top + 10px gap). The
9487
9557
  header hides in presentation mode, where they return to the top edge. */
9488
9558
  .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 +9738,7 @@ body.presentation #exitPresent, body.presentation #presentDetails { display:bloc
9668
9738
  <div class="viewhint" id="viewHint"></div>
9669
9739
  <div id="typesWarn"></div>
9670
9740
  <div class="legend" id="legend"></div>
9741
+ <div class="reltip" id="relTip"></div>
9671
9742
  </div>
9672
9743
  <div id="panelResizer" title="Drag to resize details sidebar"></div>
9673
9744
  <div id="panel"></div>
@@ -9995,6 +10066,7 @@ var MODEL = __MODEL_JSON__;
9995
10066
  retired: { fill: '#ebe8e4', stroke: '#857b73', text: '#35302b' },
9996
10067
  },
9997
10068
  issue: '#b3261e', selGlow: '#3465b4', bgLabel: '#f2f5f8', png: '#f2f5f8',
10069
+ relOk: '#2e7d4f', relDrift: '#8a6116', relBad: '#b3261e', relUnknown: '#5f6b78',
9998
10070
  },
9999
10071
  syw: {
10000
10072
  pageEdge: '#7c8ca3', edgeText: '#aebdd2', cross: '#ff6b81', ink: '#eef2f8',
@@ -10016,6 +10088,7 @@ var MODEL = __MODEL_JSON__;
10016
10088
  retired: { fill: '#24211f', stroke: '#a8a29e', text: '#e7e5e4' },
10017
10089
  },
10018
10090
  issue: '#ff6b81', selGlow: '#22ddff', bgLabel: '#0a0a0f', png: '#0a0a0f',
10091
+ relOk: '#34d399', relDrift: '#f59e0b', relBad: '#ff6b81', relUnknown: '#93a1b8',
10019
10092
  },
10020
10093
  };
10021
10094
 
@@ -10078,6 +10151,15 @@ var MODEL = __MODEL_JSON__;
10078
10151
  { selector: 'edge.bundle', style: { width: 4.5, opacity: 0.7 } },
10079
10152
  { selector: 'edge.toghost', style: { 'line-style': 'dashed', opacity: 0.75 } },
10080
10153
  { selector: 'edge.inneredge', style: { width: 1.1, 'arrow-scale': 0.6, opacity: 0.8 } },
10154
+ // Relation health on consumption edges: the health colour replaces the
10155
+ // boundary-hop colour, and unavailable is dotted as well as grey, so the
10156
+ // state never rests on colour alone (the glyph is the edge's label).
10157
+ { selector: 'edge.rel-unchecked', style: { 'line-color': t.pageEdge, 'target-arrow-color': t.pageEdge } },
10158
+ { selector: 'edge.rel-ok', style: { 'line-color': t.relOk, 'target-arrow-color': t.relOk } },
10159
+ { selector: 'edge.rel-drifted', style: { 'line-color': t.relDrift, 'target-arrow-color': t.relDrift } },
10160
+ { selector: 'edge.rel-incompatible', style: { 'line-color': t.relBad, 'target-arrow-color': t.relBad } },
10161
+ { selector: 'edge.rel-unavailable', style: { 'line-color': t.relUnknown, 'target-arrow-color': t.relUnknown, 'line-style': 'dotted' } },
10162
+ { selector: '.extProject', style: { 'border-style': 'dashed' } },
10081
10163
  { selector: 'edge.stubHover', style: { 'line-color': t.selGlow, 'target-arrow-color': t.selGlow, width: 2.6, opacity: 1, 'z-compound-depth': 'top' } },
10082
10164
  { selector: '.dimmed', style: { opacity: 0.13 } },
10083
10165
  { selector: '.hasIssue', style: { 'border-color': t.issue, 'border-style': 'dashed', 'border-width': 3 } },
@@ -10114,6 +10196,83 @@ var MODEL = __MODEL_JSON__;
10114
10196
  ];
10115
10197
  }
10116
10198
 
10199
+ // ---- relation health (consumption edges) ----------------------------------
10200
+ // The model carries relations only when health was computed; without them a
10201
+ // consumption edge is drawn neutral and the legend says it was not checked \u2014
10202
+ // never green for something nobody compared.
10203
+ var HAS_RELATIONS = Array.isArray(MODEL.relations);
10204
+ var REL_BY_ID = {};
10205
+ (MODEL.relations || []).forEach(function (r) { REL_BY_ID[r.id] = r; });
10206
+ var HEALTH_RANK = { ok: 1, drifted: 2, unavailable: 3, incompatible: 4 };
10207
+ var HEALTH_GLYPH = { ok: '\\u2713', drifted: '\\u0394', incompatible: '\\u2715', unavailable: '?' };
10208
+ var HAS_CONSUMPTION = MODEL.edges.some(function (e) { return !!e.consumption; });
10209
+ function edgeHealth(edge) {
10210
+ var r = edge.relation ? REL_BY_ID[edge.relation] : null;
10211
+ return r ? r.health : (HAS_RELATIONS ? 'unavailable' : null);
10212
+ }
10213
+ // Where edges fold into one, the worst health wins.
10214
+ function worseHealth(a, b) {
10215
+ if (!a) return b;
10216
+ if (!b) return a;
10217
+ return HEALTH_RANK[b] > HEALTH_RANK[a] ? b : a;
10218
+ }
10219
+ function relationsComparedAt() {
10220
+ var at = null;
10221
+ (MODEL.relations || []).forEach(function (r) { if (!at || r.comparedAt < at) at = r.comparedAt; });
10222
+ return at;
10223
+ }
10224
+ function shortPin(r) {
10225
+ if (r.pinnedCommit) return r.pinnedCommit.slice(0, 7);
10226
+ return r.pinnedDigest ? r.pinnedDigest.replace(/^[a-z0-9-]+:/, '').slice(0, 12) : '';
10227
+ }
10228
+ function relationName(r) { return (r.consumer || 'this project') + ' \\u2192 ' + r.producer; }
10229
+ function relationText(r) {
10230
+ var lines = [relationName(r) + ': ' + HEALTH_GLYPH[r.health] + ' ' + r.health];
10231
+ r.uses.forEach(function (u) { lines.push(' ' + u.name + ' \\u2014 ' + u.state + (u.detail ? ': ' + u.detail : '')); });
10232
+ if (r.unchanged) lines.push(' ' + r.unchanged + ' use' + (r.unchanged === 1 ? '' : 's') + ' unchanged');
10233
+ if (r.detail) lines.push(' ' + r.detail);
10234
+ var pin = shortPin(r);
10235
+ lines.push((pin ? 'pinned ' + pin + ' \\u00B7 ' : '') + 'compared ' + r.comparedAt);
10236
+ return lines.join('\\n');
10237
+ }
10238
+ function relationHtml(r) {
10239
+ var pin = shortPin(r);
10240
+ 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>'
10241
+ + r.uses.map(function (u) { return '<div class="mdesc">' + esc(u.name) + ' \\u2014 ' + esc(u.state) + (u.detail ? ': ' + esc(u.detail) : '') + '</div>'; }).join('')
10242
+ + (r.unchanged ? '<div class="mdesc">' + r.unchanged + ' use' + (r.unchanged === 1 ? '' : 's') + ' unchanged</div>' : '')
10243
+ + (r.detail ? '<div class="mdesc">' + esc(r.detail) + '</div>' : '')
10244
+ + '<div class="mdesc">' + (pin ? 'pinned ' + esc(pin) + ' \\u00B7 ' : '') + 'compared ' + esc(r.comparedAt) + '</div></div>';
10245
+ }
10246
+ // The relations whose consumption edges start or land inside a node.
10247
+ function relationsTouching(sid) {
10248
+ var seen = {}, out = [];
10249
+ var inside = function (x) {
10250
+ var c = compById[x];
10251
+ return x === sid || x.indexOf(sid + '::') === 0 || !!(c && (c.subsystem === sid || c.subsystem.indexOf(sid + '::') === 0));
10252
+ };
10253
+ MODEL.edges.forEach(function (e) {
10254
+ if (!e.relation || seen[e.relation] || !(inside(e.to) || inside(e.from))) return;
10255
+ seen[e.relation] = true;
10256
+ if (REL_BY_ID[e.relation]) out.push(REL_BY_ID[e.relation]);
10257
+ });
10258
+ return out;
10259
+ }
10260
+ // The one legend row, only when the model has a consumption edge.
10261
+ function relationLegend(t) {
10262
+ if (!HAS_CONSUMPTION) return '';
10263
+ if (!HAS_RELATIONS) return '<br>relations: <span style="color:' + t.pageEdge + '">health not checked</span>';
10264
+ var at = relationsComparedAt();
10265
+ return '<br>relations: <span style="color:' + t.relOk + '">\\u2713 ok</span>&nbsp; <span style="color:' + t.relDrift + '">\\u0394 drifted</span>&nbsp; '
10266
+ + '<span style="color:' + t.relBad + '">\\u2715 incompatible</span>&nbsp; <span style="color:' + t.relUnknown + '">? unavailable (dotted)</span>'
10267
+ + (at ? '&nbsp; \\u00B7 as of ' + esc(at) : '');
10268
+ }
10269
+ // A project's own page, only through the host's open-project hook (the web
10270
+ // app): never in a standalone file or on a shared page, which have none.
10271
+ function openProjectButton(s) {
10272
+ if (!s.recordId || typeof opts === 'undefined' || !opts || typeof opts.onOpenProject !== 'function') return '';
10273
+ return '<div class="openbtn"><button class="tbtn" data-openproject-id="' + esc(s.recordId) + '">Open project \\u2197</button></div>';
10274
+ }
10275
+
10117
10276
  function renderLegend() {
10118
10277
  var t = THEMES[state.theme];
10119
10278
  var sw = function (c) { return '<span class="sw" style="background:' + c.fill + ';border-color:' + c.stroke + '"></span>'; };
@@ -10127,7 +10286,8 @@ var MODEL = __MODEL_JSON__;
10127
10286
  sw(t.proxyIn) + '\\u21E0 in-port&nbsp; ' + sw(t.proxyOut) + '\\u21E2 out-port&nbsp; \u2014 bold border = published \xB7 ' +
10128
10287
  '<span style="color:' + t.cross + '">red</span> = boundary hop \xB7 double-click = open<br>' +
10129
10288
  '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>' : '');
10289
+ (state.dataCoupling ? '&nbsp; \xB7 &nbsp;<span style="color:' + t.typeV.stroke + '">- - \\u25B8 uses models</span>' : '') +
10290
+ relationLegend(t);
10131
10291
  }
10132
10292
 
10133
10293
  // ---- view layout ---------------------------------------------------------------
@@ -10594,6 +10754,14 @@ var MODEL = __MODEL_JSON__;
10594
10754
  if (!agg[key]) agg[key] = { src: src, tgt: tgt, n: 0, cross: false, ghost: ghost };
10595
10755
  agg[key].n++;
10596
10756
  if (edge.cross) agg[key].cross = true;
10757
+ if (edge.consumption) {
10758
+ agg[key].consumption = true;
10759
+ agg[key].health = worseHealth(agg[key].health, edgeHealth(edge));
10760
+ if (edge.relation) {
10761
+ var rels = agg[key].rels || (agg[key].rels = []);
10762
+ if (rels.indexOf(edge.relation) < 0) rels.push(edge.relation);
10763
+ }
10764
+ }
10597
10765
  });
10598
10766
  return { agg: agg, ghosts: ghosts };
10599
10767
  }
@@ -11154,7 +11322,7 @@ var MODEL = __MODEL_JSON__;
11154
11322
  var classes, label;
11155
11323
  var isPub = e.kind === 'component' && compById[e.id] && compById[e.id].public;
11156
11324
  if (e.kind === 'subsystem') {
11157
- classes = 'subsysBox';
11325
+ classes = 'subsysBox' + (subById[e.id] && subById[e.id].external ? ' extProject' : '');
11158
11326
  label = nameOf(e) + (e.hasKids && !inner ? '\\n\\u25B8 open' : '');
11159
11327
  } else {
11160
11328
  var c = compById[e.id];
@@ -11362,9 +11530,13 @@ var MODEL = __MODEL_JSON__;
11362
11530
  var bundle = e.n > 1;
11363
11531
  var dim = state.query && (dimmedAnchors[e.src] || dimmedAnchors[e.tgt]);
11364
11532
  var route = routeData(e.src, e.tgt, key);
11533
+ // A consumption edge carries its relation's health: the glyph at its middle,
11534
+ // its colour (and line, dotted when unavailable) by class.
11535
+ var glyph = e.health ? HEALTH_GLYPH[e.health] : '';
11536
+ var relCls = e.consumption ? 'rel rel-' + (e.health || 'unchecked') + ' ' : '';
11365
11537
  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' : ''),
11538
+ 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 },
11539
+ classes: 'routed ' + (e.cross ? 'cross ' : '') + (bundle ? 'bundle ' : '') + (e.ghost ? 'toghost ' : '') + relCls + (dim ? 'dimmed' : ''),
11368
11540
  });
11369
11541
  });
11370
11542
 
@@ -11864,6 +12036,27 @@ var MODEL = __MODEL_JSON__;
11864
12036
  cy.on('mouseover', 'node', function (ev) { setHover(ev.target); });
11865
12037
  cy.on('mouseout', 'node', function (ev) { if (hoveredNode && hoveredNode.id() === ev.target.id()) setHover(null); });
11866
12038
 
12039
+ // Relation health: hovering a consumption edge shows its relation(s) \u2014 which
12040
+ // uses changed or were removed, why one could not be compared, the pin and
12041
+ // when it was compared; tapping it shows the same in the details panel.
12042
+ function relationsOfEdge(edge) {
12043
+ return (edge.data('rels') || '').split('|').filter(Boolean).map(function (rid) { return REL_BY_ID[rid]; }).filter(Boolean);
12044
+ }
12045
+ cy.on('mouseover', 'edge.rel', function (ev) {
12046
+ var tip = document.getElementById('relTip');
12047
+ var rels = relationsOfEdge(ev.target);
12048
+ tip.textContent = rels.length ? rels.map(relationText).join('\\n\\n') : 'relation health not checked';
12049
+ var at = ev.renderedPosition || { x: 0, y: 0 };
12050
+ tip.style.left = (at.x + 12) + 'px';
12051
+ tip.style.top = (at.y + 12) + 'px';
12052
+ tip.style.display = 'block';
12053
+ });
12054
+ cy.on('mouseout', 'edge.rel', function () { document.getElementById('relTip').style.display = 'none'; });
12055
+ cy.on('tap', 'edge.rel', function (ev) {
12056
+ var ids = ev.target.data('rels') || '';
12057
+ if (ids) select('relation', ids, false);
12058
+ });
12059
+
11867
12060
  cy.on('tap', 'node', function (ev) {
11868
12061
  var t = idOf(ev.target);
11869
12062
  if (t.group) return;
@@ -11921,7 +12114,15 @@ var MODEL = __MODEL_JSON__;
11921
12114
  var hasKids = t.kind === 'subsystem'
11922
12115
  ? (childSubsOf(t.id).length + childCompsOf(t.id).length) > 0
11923
12116
  : memberCompsOf(t.id).length > 0;
11924
- if (hasKids) navigateTo(t.kind, t.id);
12117
+ if (hasKids) { navigateTo(t.kind, t.id); return; }
12118
+ // A project node with nothing to drill into (a referenced or external one):
12119
+ // its own page through the host's open-project hook when it carries a
12120
+ // record id, else its details panel \u2014 its storage and its relations.
12121
+ var ps = t.kind === 'subsystem' ? subById[t.id] : null;
12122
+ if (ps && ps.project) {
12123
+ if (ps.recordId && typeof opts !== 'undefined' && opts && typeof opts.onOpenProject === 'function') { opts.onOpenProject(ps.recordId); return; }
12124
+ select('subsystem', t.id, false);
12125
+ }
11925
12126
  });
11926
12127
  cy.on('dragfree', 'node', function () { harvestPositions(); });
11927
12128
 
@@ -13158,7 +13359,7 @@ var MODEL = __MODEL_JSON__;
13158
13359
  cy.nodes().removeClass('sel');
13159
13360
  cy.edges().removeClass('fieldhl');
13160
13361
  if (id) {
13161
- var node = nodeForRef(kind, id);
13362
+ var node = kind === 'relation' ? cy.collection() : nodeForRef(kind, id);
13162
13363
  if (node.length) {
13163
13364
  node.addClass('sel');
13164
13365
  applyFocus(node);
@@ -13328,6 +13529,10 @@ var MODEL = __MODEL_JSON__;
13328
13529
  via.map(function (v) { return chip(v.label, v.kind, v.id); }).join(''), true);
13329
13530
  }
13330
13531
  }
13532
+ } else if (focusKind === 'relation') {
13533
+ var rl = String(focusId).split('|').map(function (rid) { return REL_BY_ID[rid]; }).filter(Boolean);
13534
+ head = '<h2>' + esc(rl.length === 1 ? relationName(rl[0]) : rl.length + ' relations') + '</h2>' + staticChip('relation');
13535
+ body += rl.map(relationHtml).join('');
13331
13536
  } else if (focusKind === 'type') {
13332
13537
  var ty = null;
13333
13538
  MODEL.types.forEach(function (t2) { if (t2.id === focusId) ty = t2; });
@@ -13411,8 +13616,14 @@ var MODEL = __MODEL_JSON__;
13411
13616
  + (scopeFocus ? staticChip('current view') : '')
13412
13617
  + (scopeFocus ? '' : openViewButton('subsystem', s.id, subKids > 0))
13413
13618
  + openApiButton(subsystemExposesApi(s.id), '')
13414
- + openSpecButton('subsystem', s.id);
13619
+ + openSpecButton('subsystem', s.id)
13620
+ + (s.project ? staticChip(s.external ? 'external project' : 'project') : '')
13621
+ + (s.storage ? staticChip(s.storage) : '')
13622
+ + openProjectButton(s);
13415
13623
  body += '<p class="desc">' + esc(s.description) + '</p>';
13624
+ var nodeRels = relationsTouching(s.id);
13625
+ if (nodeRels.length) body += section('Relations', nodeRels.length, nodeRels.map(relationHtml).join(''), true);
13626
+ else if (s.project && HAS_CONSUMPTION && !HAS_RELATIONS) body += section('Relations', null, '<span class="desc">relation health was not checked</span>', true);
13416
13627
  if (s.trustedLinks.length) {
13417
13628
  body += section('Trusted links (fast lanes)', s.trustedLinks.length, s.trustedLinks.map(function (t2) {
13418
13629
  return '<div class="method">' + chip(t2.subsystem, 'subsystem', t2.subsystem) + '<div class="mdesc">' + esc(t2.reason) + '</div></div>';
@@ -13479,6 +13690,14 @@ var MODEL = __MODEL_JSON__;
13479
13690
  });
13480
13691
  })(ospecs[si]);
13481
13692
  }
13693
+ var oprojs = panel.querySelectorAll('[data-openproject-id]');
13694
+ for (var opi = 0; opi < oprojs.length; opi++) {
13695
+ (function (b) {
13696
+ b.addEventListener('click', function () {
13697
+ if (typeof opts !== 'undefined' && opts && typeof opts.onOpenProject === 'function') opts.onOpenProject(b.getAttribute('data-openproject-id'));
13698
+ });
13699
+ })(oprojs[opi]);
13700
+ }
13482
13701
  var flows = panel.querySelectorAll('[data-flow-comp]');
13483
13702
  for (var j = 0; j < flows.length; j++) {
13484
13703
  (function (b) {
@@ -14074,7 +14293,14 @@ function externalStatus(binding, lock) {
14074
14293
  const { external } = binding;
14075
14294
  const entry = lock?.externals[external.alias];
14076
14295
  const pinned = entry !== void 0 && externalsRepository.readSnapshot(external.alias) !== null;
14077
- const base = { alias: external.alias, project: external.project, sourceKind: external.sourceKind, pinned };
14296
+ const base = {
14297
+ alias: external.alias,
14298
+ project: external.project,
14299
+ sourceKind: external.sourceKind,
14300
+ pinned,
14301
+ ...entry !== void 0 ? { pinnedDigest: entry.digest } : {},
14302
+ ...entry?.commit !== void 0 ? { pinnedCommit: entry.commit } : {}
14303
+ };
14078
14304
  const unreadable = external.sourceKind === "unresolved" ? hostedOnly(external) ? external.problem : `the external does not resolve: ${external.problem}` : producerUnreadable(external);
14079
14305
  let live = null;
14080
14306
  let reason = unreadable;
@@ -14314,7 +14540,8 @@ function reachCeiling(root, family) {
14314
14540
  }
14315
14541
  function within2(dir, ceiling, fn) {
14316
14542
  if (ceiling === null) return runWithProjectRoot(dir, fn);
14317
- return runWithProjectBinding(dir, { topRoot: ceiling.topRoot, parentReach: true, narrowed: ceiling.narrowed }, fn);
14543
+ const lookup2 = getHostedLookup();
14544
+ return runWithProjectBinding(dir, { topRoot: ceiling.topRoot, parentReach: true, narrowed: ceiling.narrowed, ...lookup2 ? { hostedLookup: lookup2 } : {} }, fn);
14318
14545
  }
14319
14546
  function depthOf(family, node) {
14320
14547
  let depth = 0;
@@ -14365,7 +14592,8 @@ function referencedOf(nodes) {
14365
14592
  return out;
14366
14593
  }
14367
14594
  function atReferenced(ref, fn) {
14368
- return runWithProjectBinding(ref.directory, { topRoot: ref.directory, parentReach: true, narrowed: true }, fn);
14595
+ const lookup2 = getHostedLookup();
14596
+ return runWithProjectBinding(ref.directory, { topRoot: ref.directory, parentReach: true, narrowed: true, ...lookup2 ? { hostedLookup: lookup2 } : {} }, fn);
14369
14597
  }
14370
14598
  function narrowed(family, selected) {
14371
14599
  const keys = /* @__PURE__ */ new Set(["", ...selected.map((s) => s.node.namespace)]);
@@ -14853,7 +15081,7 @@ function defaultTargetConfig(type) {
14853
15081
  enabled: true
14854
15082
  };
14855
15083
  }
14856
- var WAIRON_VERSION = "5.1.1-dev.91";
15084
+ var WAIRON_VERSION = "5.1.1-dev.92";
14857
15085
  var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
14858
15086
  var ARCHITECT_AGENT_ID = "agent-architect";
14859
15087
  var ARCHITECT_TEMPLATE_ID = "architect";
@@ -25349,10 +25577,11 @@ function familyApprovals(depth) {
25349
25577
  }
25350
25578
 
25351
25579
  // src/core/diagram.ts
25352
- function renderDiagram(format) {
25580
+ function renderDiagram(format, relations) {
25353
25581
  switch (format) {
25582
+ // Only the canvas carries relation health; the other formats ignore it.
25354
25583
  case "canvas":
25355
- return renderCanvasHtml(buildCanvasModel(diagramIssues()));
25584
+ return renderCanvasHtml(buildCanvasModel(diagramIssues(), relations));
25356
25585
  case "mermaid":
25357
25586
  return generateComponentDiagram();
25358
25587
  case "drawio":
@@ -25363,8 +25592,8 @@ function renderDiagram(format) {
25363
25592
  throw new Error(`Unsupported diagram format "${format}" (canvas | mermaid | drawio | excalidraw).`);
25364
25593
  }
25365
25594
  }
25366
- function buildCanvasDataModel() {
25367
- return buildCanvasModel(diagramIssues());
25595
+ function buildCanvasDataModel(relations) {
25596
+ return buildCanvasModel(diagramIssues(), relations);
25368
25597
  }
25369
25598
  function diagramIssues() {
25370
25599
  try {
@@ -39525,7 +39754,10 @@ var externalStatusesOutput = {
39525
39754
  stale: import_zod11.z.boolean(),
39526
39755
  drifted: import_zod11.z.boolean().optional(),
39527
39756
  uses: import_zod11.z.array(import_zod11.z.record(import_zod11.z.unknown())),
39528
- detail: import_zod11.z.string().optional()
39757
+ detail: import_zod11.z.string().optional(),
39758
+ outOfReach: import_zod11.z.boolean().optional(),
39759
+ pinnedDigest: import_zod11.z.string().optional().describe("The content digest the lock recorded; provenance, never compared here."),
39760
+ pinnedCommit: import_zod11.z.string().optional().describe("The producer commit the lock recorded, for one in another repository; provenance only.")
39529
39761
  })).describe("One status per declared external, in declaration order; EXTERNAL_CHECK_UNAVAILABLE marks what could not be compared."),
39530
39762
  ...staleServerOutput
39531
39763
  };
@@ -41717,6 +41949,7 @@ function loadProjectConfig3() {
41717
41949
  registerExporter,
41718
41950
  registerPackRef,
41719
41951
  reinjectLocalGuides,
41952
+ relationHealth,
41720
41953
  removeDomain,
41721
41954
  removeExternal,
41722
41955
  removeMember,