@wairon/cli 5.0.2-dev.12 → 5.0.2-dev.14

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
@@ -1260,6 +1260,47 @@ var init_errors = __esm({
1260
1260
  }
1261
1261
  });
1262
1262
 
1263
+ // src/core/statehash.ts
1264
+ function computeStateId() {
1265
+ const tree = {
1266
+ system: loadSystemSpec(),
1267
+ subsystems: loadSubsystemSpecs(),
1268
+ components: loadComponentSpecs(),
1269
+ interfaces: loadInterfaceSpecs(),
1270
+ implementations: loadImplementationSpecs(),
1271
+ types: loadTypeSpecs()
1272
+ };
1273
+ const digest = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
1274
+ return { algorithm: "sha256", digest };
1275
+ }
1276
+ function stateIdEquals(a, b) {
1277
+ return !!a && !!b && a.algorithm === b.algorithm && a.digest === b.digest;
1278
+ }
1279
+ function canonicalize(value) {
1280
+ return JSON.stringify(sortKeys(value));
1281
+ }
1282
+ function sortKeys(v) {
1283
+ if (Array.isArray(v)) return v.map(sortKeys);
1284
+ if (v && typeof v === "object") {
1285
+ const src = v;
1286
+ const out = {};
1287
+ for (const k of Object.keys(src).sort()) {
1288
+ if (k === "createdAt" || k === "updatedAt") continue;
1289
+ out[k] = sortKeys(src[k]);
1290
+ }
1291
+ return out;
1292
+ }
1293
+ return v;
1294
+ }
1295
+ var crypto;
1296
+ var init_statehash = __esm({
1297
+ "src/core/statehash.ts"() {
1298
+ "use strict";
1299
+ crypto = __toESM(require("crypto"));
1300
+ init_specs2();
1301
+ }
1302
+ });
1303
+
1263
1304
  // src/core/narrative-labels.ts
1264
1305
  function resolveNarrativeLabels(methodName, steps) {
1265
1306
  const errors = [];
@@ -2345,6 +2386,16 @@ header input[type="search"]::placeholder { color:var(--dim); }
2345
2386
  #moreMenu .dropdown { display:block; width:100%; }
2346
2387
  #moreMenu .dropdown > .tbtn, #moreMenu > .tbtn { display:block; width:100%; text-align:left; border:none; background:transparent; margin:2px 0; }
2347
2388
  #moreMenu .dropdown > .tbtn:hover, #moreMenu > .tbtn:hover { background:var(--hover-bg); }
2389
+ /* Staged header compaction (responsive only, never persisted): compact
2390
+ stand-ins for the search input, the mode tabs, and the ancestor crumbs.
2391
+ All hidden at full width, so a roomy header renders exactly as before. */
2392
+ #searchBtn { position:relative; }
2393
+ #searchBtn.hasq::after { content:''; position:absolute; top:3px; right:3px; width:7px; height:7px; border-radius:50%; background:var(--accent); }
2394
+ .search-menu { min-width:210px; padding:8px; }
2395
+ .search-menu input[type="search"] { width:100%; }
2396
+ #modeMenu button.active { background:var(--accent); color:#fff; font-weight:700; }
2397
+ #crumbs .dropdown { display:inline-flex; }
2398
+ #crumbs .crumbmore { font-weight:700; }
2348
2399
 
2349
2400
  /* Settings panel \u2014 toggle switches */
2350
2401
  .settings-menu { min-width:266px; }
@@ -2468,9 +2519,17 @@ body.presentation #exitPresent, body.presentation #presentDetails { display:bloc
2468
2519
  <button data-vm="types">Types</button>
2469
2520
  <button data-vm="databases">Databases</button>
2470
2521
  </div>
2522
+ <div class="dropdown" id="modeDd" style="display:none">
2523
+ <button class="tbtn" id="modeBtn" title="Switch between the component architecture, the type ERD, or the database schemas">Components \u25BE</button>
2524
+ <div class="menu" id="modeMenu"></div>
2525
+ </div>
2471
2526
  <nav id="crumbs"></nav>
2472
2527
  <span class="divider"></span>
2473
2528
  <input id="search" type="search" placeholder="Search this view\u2026">
2529
+ <div class="dropdown" id="searchDd" style="display:none">
2530
+ <button class="tbtn" id="searchBtn" title="Search this view">\u{1F50D}</button>
2531
+ <div class="menu search-menu" id="searchMenu"></div>
2532
+ </div>
2474
2533
  <div class="seg" id="typesDetailSeg" style="display:none" title="ERD detail level">
2475
2534
  <button data-td="full">Full</button>
2476
2535
  <button data-td="fields">Fields</button>
@@ -2987,13 +3046,300 @@ var MODEL = __MODEL_JSON__;
2987
3046
  var BOX_W = 200, BOX_H = 56, SUBBOX_W = 230, SUBBOX_H = 84, GAP_X = 110, GAP_Y = 34;
2988
3047
  var INNER_W = 130, INNER_H = 36, INNER_GAPX = 26, INNER_GAPY = 12, HEAD_H = 34, PADI = 14;
2989
3048
 
3049
+ // ---- deep expansion (opt-in via subsystem.deepInternals) -------------------
3050
+ // When Internals is on and a subsystem record carries deepInternals:true, its
3051
+ // box renders its WHOLE subtree instead of one layer: deep-flagged child
3052
+ // subsystems become NESTED boundary boxes (recursing, defensively capped),
3053
+ // every other child renders as a fixed leaf tile that is never expanded
3054
+ // further. Sizes are computed bottom-up (a nested container tile takes its
3055
+ // recursive {w,h}); positions are emitted top-down by accumulating parent
3056
+ // top-left offsets. Relations between concrete visible endpoints are drawn
3057
+ // as ONE direct line each by buildDeepContext \u2014 crossing nested boundaries
3058
+ // on purpose (the full org overview of project relations); only relations
3059
+ // that cannot resolve to two concrete endpoints keep today's port machinery,
3060
+ // and ONLY at the outermost box. Without the flag this whole path is inert
3061
+ // and the classic one-layer innerLayout runs unchanged.
3062
+ var DEEP_MAX_DEPTH = 6;
3063
+ function isDeepId(subId) {
3064
+ var s = subById[subId];
3065
+ return !!(s && s.deepInternals);
3066
+ }
3067
+ // The DEEPEST visible tile representing compId inside the deep-expanded box
3068
+ // rooted at rootSubId: descend deep-flagged containers (the same expansion
3069
+ // rule as deepContainerLayout) until the containing child is a leaf tile.
3070
+ // Returns the child entry { kind, id } (its node id is IN(kind, id)).
3071
+ function deepLeafFor(compId, rootSubId) {
3072
+ var subId = rootSubId, depth = 1;
3073
+ for (;;) {
3074
+ var child = childOfScopeContaining(compId, { kind: 'subsystem', id: subId });
3075
+ if (!child) return null;
3076
+ if (child.kind === 'subsystem' && isDeepId(child.id) && depth < DEEP_MAX_DEPTH) {
3077
+ subId = child.id; depth += 1;
3078
+ continue;
3079
+ }
3080
+ return child;
3081
+ }
3082
+ }
3083
+ // One deep container's DIRECT children, placed with PER-TILE sizes (nested
3084
+ // containers take their recursive size; leaves stay INNER_W x INNER_H). The
3085
+ // placement mirrors innerLayout's strategy switch, generalised to variable
3086
+ // tile sizes. Tiles are centres relative to THIS container's top-left corner
3087
+ // (content sits right of PADI, below the HEAD_H label band); nested tiles
3088
+ // are relative to their own container, so emission accumulates offsets.
3089
+ function deepContainerLayout(subId, depth) {
3090
+ var kids = childrenOf({ kind: 'subsystem', id: subId });
3091
+ if (!kids.length) {
3092
+ // An EMPTY deep subsystem still shows as a (min-size) boundary box.
3093
+ return { tiles: [], w: INNER_W + 2 * PADI, h: HEAD_H + PADI };
3094
+ }
3095
+ var scope = { kind: 'subsystem', id: subId };
3096
+ var kidKey = function (k) { return k.kind + ':' + k.id; };
3097
+ var size = {};
3098
+ kids.forEach(function (k) {
3099
+ if (k.kind === 'subsystem' && isDeepId(k.id) && depth < DEEP_MAX_DEPTH) {
3100
+ var nested = deepContainerLayout(k.id, depth + 1);
3101
+ size[kidKey(k)] = { w: nested.w, h: nested.h, sub: nested };
3102
+ } else {
3103
+ size[kidKey(k)] = { w: INNER_W, h: INNER_H, sub: null };
3104
+ }
3105
+ });
3106
+ // Intra-container edges lifted to DIRECT children \u2014 for LAYOUT ONLY (the
3107
+ // drawn lines come from buildDeepContext's direct pass, never per level).
3108
+ var intra = {};
3109
+ MODEL.edges.forEach(function (edge) {
3110
+ var a = childOfScopeContaining(edge.from, scope);
3111
+ var b = childOfScopeContaining(edge.to, scope);
3112
+ if (!a || !b) return;
3113
+ var ak = a.kind + ':' + a.id, bk = b.kind + ':' + b.id;
3114
+ if (!size[ak] || !size[bk] || ak === bk) return;
3115
+ intra[ak + '=>' + bk] = 1;
3116
+ });
3117
+ var layer = {};
3118
+ function calc(k, stack) {
3119
+ var key = kidKey(k);
3120
+ if (layer[key] !== undefined) return layer[key];
3121
+ if (stack[key]) return 0;
3122
+ stack[key] = 1;
3123
+ var l = 0;
3124
+ if (k.kind === 'component') {
3125
+ var c = compById[k.id];
3126
+ if (c && (c.componentType === 'Portal' || c.componentType === 'Observer')) { layer[key] = 0; delete stack[key]; return 0; }
3127
+ }
3128
+ Object.keys(intra).forEach(function (ek) {
3129
+ var cut = ek.indexOf('=>');
3130
+ if (ek.slice(cut + 2) !== key) return;
3131
+ var srcKid = kids.filter(function (x) { return kidKey(x) === ek.slice(0, cut); })[0];
3132
+ if (srcKid) l = Math.max(l, calc(srcKid, stack) + 1);
3133
+ });
3134
+ delete stack[key];
3135
+ layer[key] = l;
3136
+ return l;
3137
+ }
3138
+ kids.forEach(function (k) { calc(k, {}); });
3139
+ var tiles = [], contentW = 0, contentH = 0;
3140
+ if (state.layout === 'grid') {
3141
+ // Row packing by ACTUAL tile size (the fixed per-column grid assumed
3142
+ // uniform tiles); the target row width follows the tile count, widened
3143
+ // to at least the widest single tile.
3144
+ var gsorted = kids.slice().sort(function (a, b) { return a.id < b.id ? -1 : 1; });
3145
+ var target = Math.max(1, Math.ceil(Math.sqrt(kids.length))) * (INNER_W + INNER_GAPX);
3146
+ kids.forEach(function (k) { var s0 = size[kidKey(k)]; if (s0.w > target) target = s0.w; });
3147
+ var gx = 0, gy = 0, rowH = 0;
3148
+ gsorted.forEach(function (k) {
3149
+ var s = size[kidKey(k)];
3150
+ if (gx > 0 && gx + s.w > target) { gx = 0; gy += rowH + INNER_GAPY; rowH = 0; }
3151
+ tiles.push({ kid: k, x: gx + s.w / 2, y: gy + s.h / 2, w: s.w, h: s.h, sub: s.sub });
3152
+ gx += s.w + INNER_GAPX;
3153
+ if (s.h > rowH) rowH = s.h;
3154
+ if (gx - INNER_GAPX > contentW) contentW = gx - INNER_GAPX;
3155
+ if (gy + rowH > contentH) contentH = gy + rowH;
3156
+ });
3157
+ } else if (state.layout === 'concentric' || state.layout === 'force') {
3158
+ var ideg = {};
3159
+ kids.forEach(function (k) { ideg[kidKey(k)] = 0; });
3160
+ Object.keys(intra).forEach(function (ek) {
3161
+ var cut2 = ek.indexOf('=>');
3162
+ var sk = ek.slice(0, cut2), tk = ek.slice(cut2 + 2);
3163
+ if (ideg[sk] !== undefined) ideg[sk]++;
3164
+ if (ideg[tk] !== undefined) ideg[tk]++;
3165
+ });
3166
+ var rel = concentricPositions(
3167
+ kids.map(kidKey),
3168
+ function (key) { return ideg[key] || 0; },
3169
+ function (key) { return { w: size[key].w, h: size[key].h }; }
3170
+ );
3171
+ // Normalise by the tiles' BOUNDING BOX (not just the centres) so a wide
3172
+ // nested container on the rim still clears the container's left/top pad.
3173
+ var minL = Infinity, minT = Infinity;
3174
+ kids.forEach(function (k) {
3175
+ var s1 = size[kidKey(k)], p1 = rel[kidKey(k)] || { x: 0, y: 0 };
3176
+ if (p1.x - s1.w / 2 < minL) minL = p1.x - s1.w / 2;
3177
+ if (p1.y - s1.h / 2 < minT) minT = p1.y - s1.h / 2;
3178
+ });
3179
+ if (minL === Infinity) { minL = 0; minT = 0; }
3180
+ kids.forEach(function (k) {
3181
+ var s2 = size[kidKey(k)], p2 = rel[kidKey(k)] || { x: 0, y: 0 };
3182
+ var cx = p2.x - minL, cyy = p2.y - minT;
3183
+ tiles.push({ kid: k, x: cx, y: cyy, w: s2.w, h: s2.h, sub: s2.sub });
3184
+ if (cx + s2.w / 2 > contentW) contentW = cx + s2.w / 2;
3185
+ if (cyy + s2.h / 2 > contentH) contentH = cyy + s2.h / 2;
3186
+ });
3187
+ } else {
3188
+ // Layered dependency columns: the column is as wide as its widest tile,
3189
+ // and each tile advances by ITS OWN height.
3190
+ var cols = {};
3191
+ kids.forEach(function (k) { var l = layer[kidKey(k)] || 0; (cols[l] = cols[l] || []).push(k); });
3192
+ var colKeys = Object.keys(cols).map(Number).sort(function (a, b) { return a - b; });
3193
+ var x = 0;
3194
+ colKeys.forEach(function (ck) {
3195
+ var col = cols[ck].sort(function (a, b) { return a.id < b.id ? -1 : 1; });
3196
+ var colW = 0, y = 0;
3197
+ col.forEach(function (k) { var s3 = size[kidKey(k)]; if (s3.w > colW) colW = s3.w; });
3198
+ col.forEach(function (k) {
3199
+ var s = size[kidKey(k)];
3200
+ tiles.push({ kid: k, x: x + colW / 2, y: y + s.h / 2, w: s.w, h: s.h, sub: s.sub });
3201
+ y += s.h + INNER_GAPY;
3202
+ });
3203
+ if (y - INNER_GAPY > contentH) contentH = y - INNER_GAPY;
3204
+ x += colW + INNER_GAPX;
3205
+ });
3206
+ contentW = x - INNER_GAPX;
3207
+ }
3208
+ tiles.forEach(function (t) { t.x += PADI; t.y += HEAD_H; });
3209
+ return { tiles: tiles, w: contentW + 2 * PADI, h: HEAD_H + contentH + PADI };
3210
+ }
3211
+ // Top-level deep box: the recursive interior plus today's port machinery at
3212
+ // the OUTERMOST box only (buildDeepContext supplies which relations still
3213
+ // need ports; stubs run port <-> the DEEPEST visible leaf tile). Returns the
3214
+ // same shape as innerLayout, plus deep:true so emission recurses.
3215
+ function deepLayout(entry, portRec) {
3216
+ var box = deepContainerLayout(entry.id, 1);
3217
+ var parentId = anchorNodeId(entry);
3218
+ var pBaseIn = 'p~in~' + parentId + '~', pBaseOut = 'p~out~' + parentId + '~';
3219
+ var extIn = portRec ? portRec.extIn : {}, extOut = portRec ? portRec.extOut : {};
3220
+ var inIds = Object.keys(extIn).sort(), outIds = Object.keys(extOut).sort();
3221
+ var hasIn = inIds.length > 0, hasOut = outIds.length > 0;
3222
+ var PROXY_W = 22, PROXY_H = 22, PROXY_GAP = 8;
3223
+ var shift = hasIn ? PROXY_W + INNER_GAPX : 0;
3224
+ var tiles = box.tiles;
3225
+ if (shift) tiles.forEach(function (t) { t.x += shift; });
3226
+ var w = box.w + shift + (hasOut ? PROXY_W + INNER_GAPX : 0);
3227
+ if (w < SUBBOX_W) w = SUBBOX_W;
3228
+ var stackMax = Math.max(inIds.length, outIds.length);
3229
+ var h = Math.max(box.h, HEAD_H + stackMax * PROXY_H + Math.max(0, stackMax - 1) * PROXY_GAP + PADI);
3230
+ var midY = HEAD_H + Math.max(0, (h - HEAD_H - PADI) / 2);
3231
+ function stackPorts(ids, recs, base, cx, dir) {
3232
+ var total = ids.length * PROXY_H + Math.max(0, ids.length - 1) * PROXY_GAP;
3233
+ var y0 = Math.max(HEAD_H + PROXY_H / 2, midY - total / 2 + PROXY_H / 2);
3234
+ return ids.map(function (eid, i) {
3235
+ var km = recs[eid].kids, klist = [];
3236
+ Object.keys(km).forEach(function (key) { klist.push(km[key]); });
3237
+ return { id: base + eid, extId: eid, dir: dir, kids: klist, raws: Object.keys(recs[eid].raws), x: cx, y: y0 + i * (PROXY_H + PROXY_GAP), w: PROXY_W, h: PROXY_H };
3238
+ });
3239
+ }
3240
+ return {
3241
+ deep: true,
3242
+ tiles: tiles,
3243
+ edges: portRec ? Object.keys(portRec.stubs).map(function (k) { return portRec.stubs[k]; }) : [],
3244
+ proxies: stackPorts(inIds, extIn, pBaseIn, PADI + PROXY_W / 2, 'in')
3245
+ .concat(stackPorts(outIds, extOut, pBaseOut, w - PADI - PROXY_W / 2, 'out')),
3246
+ w: w,
3247
+ h: h,
3248
+ };
3249
+ }
3250
+ // View-level deep context (null unless Internals is on AND at least one
3251
+ // in-view entry is deep-flagged \u2014 the classic path never sees it): which
3252
+ // top-level entries are deep-expanded; every relation drawn as a DIRECT
3253
+ // concrete line (deduped per src=>tgt pair); the per-top-pair count used to
3254
+ // suppress aggregated edges whose constituents are ALL drawn directly; and
3255
+ // the port records for relations that keep today's port semantics.
3256
+ function buildDeepContext(scope, entries) {
3257
+ if (!state.internals) return null;
3258
+ var deepByAnchor = {}, any = false;
3259
+ entries.forEach(function (e) {
3260
+ if (e.kind === 'subsystem' && isDeepId(e.id)) { deepByAnchor[anchorNodeId(e)] = e; any = true; }
3261
+ });
3262
+ if (!any) return null;
3263
+ var entryByAnchor = {};
3264
+ entries.forEach(function (e) { entryByAnchor[e.kind + ':' + e.id] = e; });
3265
+ // A relation endpoint's DIRECT-line node in this view: the deepest leaf
3266
+ // tile inside a deep-expanded entry, or a top-level component box ITSELF.
3267
+ // null = this endpoint keeps aggregated/port semantics (interiors of
3268
+ // non-deep entries, out-of-scope counterparts).
3269
+ function directEnd(compId) {
3270
+ var child = childOfScopeContaining(compId, scope);
3271
+ var entry = child && entryByAnchor[child.kind + ':' + child.id];
3272
+ if (!entry) return null;
3273
+ var aid = anchorNodeId(entry);
3274
+ if (deepByAnchor[aid]) {
3275
+ var leaf = deepLeafFor(compId, entry.id);
3276
+ return leaf ? { node: IN(leaf.kind, leaf.id), top: aid, deep: true } : null;
3277
+ }
3278
+ if (entry.kind === 'component' && entry.id === compId) return { node: aid, top: aid, deep: false };
3279
+ return null;
3280
+ }
3281
+ var direct = {}, directTopCount = {}, ports = {};
3282
+ function portRec(aid) { return ports[aid] = ports[aid] || { extIn: {}, extOut: {}, stubs: {} }; }
3283
+ MODEL.edges.forEach(function (edge) {
3284
+ var a = directEnd(edge.from), b = directEnd(edge.to);
3285
+ if (a && b && (a.deep || b.deep) && a.node !== b.node) {
3286
+ // Drawn as ONE direct line \u2014 never ALSO as ports/stubs (dedupe rule).
3287
+ var key = a.node + '=>' + b.node;
3288
+ if (!direct[key]) direct[key] = { src: a.node, tgt: b.node, cross: false, aTop: a.top, bTop: b.top };
3289
+ if (edge.cross) direct[key].cross = true;
3290
+ if (a.top !== b.top) {
3291
+ var tk = a.top + '=>' + b.top;
3292
+ directTopCount[tk] = (directTopCount[tk] || 0) + 1;
3293
+ }
3294
+ return;
3295
+ }
3296
+ // Not a direct line: keep today's port semantics on any deep box with
3297
+ // exactly one endpoint inside its subtree, stubbed to the deepest leaf.
3298
+ var ac = childOfScopeContaining(edge.from, scope);
3299
+ var bc = childOfScopeContaining(edge.to, scope);
3300
+ var aEnt = ac && entryByAnchor[ac.kind + ':' + ac.id];
3301
+ var bEnt = bc && entryByAnchor[bc.kind + ':' + bc.id];
3302
+ var aAid = aEnt ? anchorNodeId(aEnt) : null;
3303
+ var bAid = bEnt ? anchorNodeId(bEnt) : null;
3304
+ if (aAid === bAid) return; // internal to one entry, or neither in scope
3305
+ if (aAid && deepByAnchor[aAid]) {
3306
+ var leafA = deepLeafFor(edge.from, aEnt.id);
3307
+ if (leafA) {
3308
+ var recA = portRec(aAid);
3309
+ var ro = recA.extOut[edge.to] = recA.extOut[edge.to] || { kids: {}, raws: {} };
3310
+ ro.kids[leafA.kind + ':' + leafA.id] = leafA;
3311
+ ro.raws[edge.from] = 1;
3312
+ var poId = 'p~out~' + aAid + '~' + edge.to;
3313
+ recA.stubs[IN(leafA.kind, leafA.id) + '=>' + poId] = { src: IN(leafA.kind, leafA.id), tgt: poId, stub: true };
3314
+ }
3315
+ }
3316
+ if (bAid && deepByAnchor[bAid]) {
3317
+ var leafB = deepLeafFor(edge.to, bEnt.id);
3318
+ if (leafB) {
3319
+ var recB = portRec(bAid);
3320
+ var ri = recB.extIn[edge.from] = recB.extIn[edge.from] || { kids: {}, raws: {} };
3321
+ ri.kids[leafB.kind + ':' + leafB.id] = leafB;
3322
+ ri.raws[edge.to] = 1;
3323
+ var piId = 'p~in~' + bAid + '~' + edge.from;
3324
+ recB.stubs[piId + '=>' + IN(leafB.kind, leafB.id)] = { src: piId, tgt: IN(leafB.kind, leafB.id), stub: true };
3325
+ }
3326
+ }
3327
+ });
3328
+ return { deepByAnchor: deepByAnchor, direct: direct, directTopCount: directTopCount, ports: ports };
3329
+ }
3330
+
2990
3331
  // Micro-layout for a container's direct children when Internals is on:
2991
3332
  // layered mini columns + intra-container edges. Each external relation gets
2992
3333
  // its own small PORT node INSIDE the container (one per external
2993
3334
  // counterpart; incoming left, outgoing right). Children connect to ports
2994
3335
  // with short edges that never leave the box \u2014 the real cross-boundary line
2995
3336
  // is only revealed on hover, or pinned while the port is selected.
2996
- function innerLayout(entry) {
3337
+ function innerLayout(entry, deepCtx) {
3338
+ // Deep-flagged subsystems take the recursive path (deepCtx exists only
3339
+ // when Internals is on and the view has deep entries \u2014 see buildElements).
3340
+ if (deepCtx && entry.kind === 'subsystem' && deepCtx.deepByAnchor[anchorNodeId(entry)]) {
3341
+ return deepLayout(entry, deepCtx.ports[anchorNodeId(entry)]);
3342
+ }
2997
3343
  var kids = state.internals && entry.hasKids ? childrenOf({ kind: entry.kind, id: entry.id }) : [];
2998
3344
  if (!kids.length) return null;
2999
3345
  var scope = { kind: entry.kind, id: entry.id };
@@ -3565,12 +3911,42 @@ var MODEL = __MODEL_JSON__;
3565
3911
  if (scope.kind === 'types' || scope.kind === 'databases') return buildTypeElements();
3566
3912
  var entries = childrenOf(scope);
3567
3913
  var eles = [];
3914
+ var deepCtx = buildDeepContext(scope, entries);
3568
3915
  var ve = viewEdges(scope, entries);
3569
3916
  // Data-coupling overlay: same scoping pipeline, a different edge source.
3570
3917
  var vd = state.dataCoupling ? viewEdges(scope, entries, MODEL.dataEdges) : { agg: {}, ghosts: {} };
3571
3918
  Object.keys(vd.ghosts).forEach(function (g) { if (!ve.ghosts[g]) ve.ghosts[g] = vd.ghosts[g]; });
3572
3919
  var inners = {};
3573
- entries.forEach(function (e) { inners[anchorNodeId(e)] = innerLayout(e); });
3920
+ entries.forEach(function (e) { inners[anchorNodeId(e)] = innerLayout(e, deepCtx); });
3921
+
3922
+ // Deep-mode recursive tile emission: a nested container becomes a cytoscape
3923
+ // compound parent (no explicit position \u2014 a compound derives its bounds
3924
+ // from its children); leaves and EMPTY containers are plain positioned
3925
+ // nodes. ox/oy = the emitting container's absolute top-left; tile.x/y are
3926
+ // centres relative to it, so offsets accumulate top-down. Search dimming
3927
+ // propagates the TOP entry's dim to the whole subtree.
3928
+ function emitDeepTiles(tiles, parentNodeId, ox, oy, dimCls) {
3929
+ tiles.forEach(function (tile) {
3930
+ var ax = ox + tile.x, ay = oy + tile.y;
3931
+ if (tile.sub) {
3932
+ var nid = SN(tile.kid.id);
3933
+ var selCls = state.selectedKind === 'subsystem' && state.selected === tile.kid.id ? ' sel' : '';
3934
+ var nested = {
3935
+ data: { id: nid, parent: parentNodeId, label: nameOf(tile.kid), w: tile.w, h: tile.h, tw: tile.w - 16 },
3936
+ classes: 'subsysBox' + (tile.sub.tiles.length ? ' drillable' : '') + dimCls + selCls,
3937
+ };
3938
+ if (!tile.sub.tiles.length) nested.position = { x: ax, y: ay };
3939
+ eles.push(nested);
3940
+ emitDeepTiles(tile.sub.tiles, nid, ax - tile.w / 2, ay - tile.h / 2, dimCls);
3941
+ } else {
3942
+ eles.push({
3943
+ data: { id: IN(tile.kid.kind, tile.kid.id), parent: parentNodeId, label: nameOf(tile.kid), w: INNER_W, h: INNER_H, tw: INNER_W - 10 },
3944
+ position: { x: ax, y: ay },
3945
+ classes: 'inner' + dimCls,
3946
+ });
3947
+ }
3948
+ });
3949
+ }
3574
3950
 
3575
3951
  // Resolve a port's reveal target(s) in THIS view. Preference order: the
3576
3952
  // MATCHING PORT inside the counterpart's container (a port-to-port line
@@ -3702,17 +4078,25 @@ var MODEL = __MODEL_JSON__;
3702
4078
  + (state.showIssues && issuesBySpec[e.id] ? ' hasIssue' : '')
3703
4079
  + (state.selectedKind === e.kind && state.selected === e.id ? ' sel' : '');
3704
4080
  if (inner) {
3705
- eles.push({ data: { id: aid, label: e.kind === 'subsystem' ? nameOf(e) : nameOf(e), w: p.w, h: p.h, tw: p.w - 16 }, classes: classes });
3706
- inner.tiles.forEach(function (tile) {
3707
- eles.push({
3708
- data: {
3709
- id: IN(tile.kid.kind, tile.kid.id), parent: aid,
3710
- label: nameOf(tile.kid), w: INNER_W, h: INNER_H, tw: INNER_W - 10,
3711
- },
3712
- position: { x: p.x - p.w / 2 + tile.x, y: p.y - p.h / 2 + tile.y },
3713
- classes: 'inner' + (dim ? ' dimmed' : ''),
4081
+ var boxNode = { data: { id: aid, label: e.kind === 'subsystem' ? nameOf(e) : nameOf(e), w: p.w, h: p.h, tw: p.w - 16 }, classes: classes };
4082
+ // An EMPTY deep boundary box has no children, so it is NOT a compound
4083
+ // parent \u2014 it needs (and honours) an explicit position and size.
4084
+ if (inner.deep && !inner.tiles.length && !(inner.proxies && inner.proxies.length)) boxNode.position = { x: p.x, y: p.y };
4085
+ eles.push(boxNode);
4086
+ if (inner.deep) {
4087
+ emitDeepTiles(inner.tiles, aid, p.x - p.w / 2, p.y - p.h / 2, dim ? ' dimmed' : '');
4088
+ } else {
4089
+ inner.tiles.forEach(function (tile) {
4090
+ eles.push({
4091
+ data: {
4092
+ id: IN(tile.kid.kind, tile.kid.id), parent: aid,
4093
+ label: nameOf(tile.kid), w: INNER_W, h: INNER_H, tw: INNER_W - 10,
4094
+ },
4095
+ position: { x: p.x - p.w / 2 + tile.x, y: p.y - p.h / 2 + tile.y },
4096
+ classes: 'inner' + (dim ? ' dimmed' : ''),
4097
+ });
3714
4098
  });
3715
- });
4099
+ }
3716
4100
  (inner.proxies || []).forEach(function (px) {
3717
4101
  eles.push({
3718
4102
  data: {
@@ -3865,9 +4249,28 @@ var MODEL = __MODEL_JSON__;
3865
4249
 
3866
4250
  var dimmedAnchors = {};
3867
4251
  entries.forEach(function (e) { if (state.query && !matches(e)) dimmedAnchors[anchorNodeId(e)] = true; });
4252
+
4253
+ // Deep mode: ONE direct line per related pair of concrete visible nodes \u2014
4254
+ // leaf tiles at any depth and/or top-level component boxes (deduped by
4255
+ // buildDeepContext). These lines cross nested boundaries on purpose.
4256
+ if (deepCtx) {
4257
+ var ddi = 0;
4258
+ Object.keys(deepCtx.direct).sort().forEach(function (key) {
4259
+ var d = deepCtx.direct[key];
4260
+ var ddim = state.query && (dimmedAnchors[d.aTop] || dimmedAnchors[d.bTop]);
4261
+ eles.push({
4262
+ data: { id: 'dd' + (ddi++), source: d.src, target: d.tgt, lbl: '' },
4263
+ classes: 'inneredge' + (d.cross ? ' cross' : '') + (ddim ? ' dimmed' : ''),
4264
+ });
4265
+ });
4266
+ }
4267
+
3868
4268
  var i = 0;
3869
4269
  Object.keys(ve.agg).forEach(function (key) {
3870
4270
  var e = ve.agg[key];
4271
+ // Prefer the leaf lines: drop an aggregated container edge whose
4272
+ // constituent relations were ALL drawn as direct deep lines above.
4273
+ if (deepCtx && deepCtx.directTopCount[key] >= e.n) return;
3871
4274
  var bundle = e.n > 1;
3872
4275
  var dim = state.query && (dimmedAnchors[e.src] || dimmedAnchors[e.tgt]);
3873
4276
  var route = routeData(e.src, e.tgt, key);
@@ -4126,22 +4529,58 @@ var MODEL = __MODEL_JSON__;
4126
4529
  }
4127
4530
  return path;
4128
4531
  }
4532
+ // Crumb compaction (compaction stage 5) is a RENDER MODE, not marker-based
4533
+ // reparenting: renderCrumbs rebuilds #crumbs' innerHTML on every navigation,
4534
+ // so nodes physically moved elsewhere would be destroyed by the next render.
4535
+ // The header-compaction stage toggles crumbsCompact and re-renders; compact
4536
+ // keeps the CURRENT scope visible and folds the ancestors into an ordered
4537
+ // "\\u2026" dropdown (document order, root first) whose entries navigate
4538
+ // exactly like the crumbs they replace.
4539
+ var crumbsCompact = false;
4540
+ var lastCrumbsHtml; // no initializer: the boot render at cy-init time precedes this line
4541
+ // Set by the header-compaction IIFE: crumb re-renders change the header's
4542
+ // CONTENT width without resizing #hdr itself (it is edge-anchored), so the
4543
+ // ResizeObserver never fires for them \u2014 renderCrumbs nudges a reflow here.
4544
+ var headerReflowHook = null;
4545
+ function crumbBtnHtml(p, cur) {
4546
+ return '<button class="crumb' + (cur ? ' cur' : '') + '" data-ck="' + p.kind + '" data-ci="' + (p.id || '') + '">' + p.label + '</button>';
4547
+ }
4129
4548
  function renderCrumbs() {
4130
4549
  var el = document.getElementById('crumbs');
4131
4550
  var path = crumbPath();
4132
- el.innerHTML = path.map(function (p, i) {
4133
- var cur = i === path.length - 1;
4134
- return '<button class="crumb' + (cur ? ' cur' : '') + '" data-ck="' + p.kind + '" data-ci="' + (p.id || '') + '">' + p.label + '</button>'
4135
- + (cur ? '' : '<span class="sep">\\u203A</span>');
4136
- }).join('');
4551
+ var html;
4552
+ if (crumbsCompact && path.length > 1) {
4553
+ html = '<span class="dropdown" id="crumbDd">'
4554
+ + '<button class="crumb crumbmore" id="crumbMoreBtn" title="Show the collapsed ancestor path">\\u2026</button>'
4555
+ + '<span class="menu" id="crumbMenu">'
4556
+ + path.slice(0, path.length - 1).map(function (p) {
4557
+ return '<button data-ck="' + p.kind + '" data-ci="' + (p.id || '') + '">' + p.label + '</button>';
4558
+ }).join('')
4559
+ + '</span></span>'
4560
+ + '<span class="sep">\\u203A</span>'
4561
+ + crumbBtnHtml(path[path.length - 1], true);
4562
+ } else {
4563
+ html = path.map(function (p, i) {
4564
+ var cur = i === path.length - 1;
4565
+ return crumbBtnHtml(p, cur) + (cur ? '' : '<span class="sep">\\u203A</span>');
4566
+ }).join('');
4567
+ }
4568
+ // No-op renders keep the already-wired nodes (and an open "\\u2026" menu)
4569
+ // intact \u2014 and don't churn the reflow scheduler while a search query types.
4570
+ if (html === lastCrumbsHtml) return;
4571
+ lastCrumbsHtml = html;
4572
+ el.innerHTML = html;
4137
4573
  var btns = el.querySelectorAll('button');
4138
4574
  for (var i = 0; i < btns.length; i++) {
4139
4575
  (function (b) {
4576
+ if (!b.getAttribute('data-ck')) return; // the "\\u2026" trigger toggles, never navigates
4140
4577
  b.addEventListener('click', function () {
4141
4578
  navigateTo(b.getAttribute('data-ck'), b.getAttribute('data-ci') || null);
4142
4579
  });
4143
4580
  })(btns[i]);
4144
4581
  }
4582
+ if (crumbsCompact && path.length > 1) wireDropdown('crumbDd', 'crumbMoreBtn');
4583
+ if (headerReflowHook) headerReflowHook();
4145
4584
  }
4146
4585
  function renderViewHint() {
4147
4586
  if (state.view.kind === 'types' || state.view.kind === 'databases') {
@@ -4408,7 +4847,16 @@ var MODEL = __MODEL_JSON__;
4408
4847
  return c ? { kind: 'subsystem', id: c.subsystem } : { kind: 'system', id: null };
4409
4848
  }
4410
4849
 
4411
- document.getElementById('search').addEventListener('input', function (ev) { state.query = ev.target.value.trim(); rebuild(false); });
4850
+ // While the search input is compacted behind the magnifier icon (compaction
4851
+ // stage 3), a non-empty query must stay discoverable \u2014 mark the icon with an
4852
+ // accent dot. The class is kept in sync on every query edit; the dot is only
4853
+ // ever visible while the compact icon itself is.
4854
+ function updateSearchBadge() {
4855
+ var b = document.getElementById('searchBtn');
4856
+ if (b && b.classList) b.classList[state.query ? 'add' : 'remove']('hasq');
4857
+ }
4858
+ document.getElementById('search').addEventListener('input', function (ev) { state.query = ev.target.value.trim(); updateSearchBadge(); rebuild(false); });
4859
+ updateSearchBadge();
4412
4860
  // Sync each View toggle's checkbox from the (possibly persisted) state, then
4413
4861
  // persist on change so the choices survive a refresh (see persist()/saved).
4414
4862
  document.getElementById('internalsToggle').checked = state.internals;
@@ -4462,9 +4910,24 @@ var MODEL = __MODEL_JSON__;
4462
4910
  })(btns[i]);
4463
4911
  }
4464
4912
  })();
4913
+ // Compaction stage 4 replaces the mode tabs with one dropdown trigger; its
4914
+ // label must follow the CURRENT mode. updateHeaderSegs runs on every rebuild,
4915
+ // so a mode change made while compact re-labels the trigger immediately.
4916
+ function updateModeBtn() {
4917
+ var b = document.getElementById('modeBtn');
4918
+ if (!b) return;
4919
+ var lbl = state.view.kind === 'types' ? 'Types' : state.view.kind === 'databases' ? 'Databases' : 'Components';
4920
+ b.textContent = lbl + ' \\u25BE';
4921
+ }
4465
4922
  function updateHeaderSegs() {
4466
4923
  var seg = document.getElementById('modeSeg');
4467
4924
  var btns = seg.querySelectorAll('button');
4925
+ if (!btns.length) {
4926
+ // Compaction stage 4 moved the real tab buttons into the mode dropdown \u2014
4927
+ // keep driving THEIR active classes there (they move back node-identical).
4928
+ var mm = document.getElementById('modeMenu');
4929
+ if (mm && mm.querySelectorAll) btns = mm.querySelectorAll('button');
4930
+ }
4468
4931
  for (var i = 0; i < btns.length; i++) {
4469
4932
  var vm = btns[i].getAttribute('data-vm');
4470
4933
  var active = vm === 'components'
@@ -4472,6 +4935,7 @@ var MODEL = __MODEL_JSON__;
4472
4935
  : vm === state.view.kind;
4473
4936
  if (btns[i].classList) btns[i].classList[active ? 'add' : 'remove']('active');
4474
4937
  }
4938
+ updateModeBtn();
4475
4939
  var td = document.getElementById('typesDetailSeg');
4476
4940
  td.style.display = (state.view.kind === 'types' || state.view.kind === 'databases') ? '' : 'none';
4477
4941
  var tbs = td.querySelectorAll('button');
@@ -4535,7 +4999,13 @@ var MODEL = __MODEL_JSON__;
4535
4999
  var r = btn.getBoundingClientRect();
4536
5000
  menu.style.top = (r.bottom + 6) + 'px';
4537
5001
  menu.style.left = 'auto';
4538
- menu.style.right = Math.max(6, window.innerWidth - r.right) + 'px';
5002
+ // Right-aligned to the trigger, but never pushed off the LEFT edge \u2014 the
5003
+ // compact search / crumb triggers (header compaction) sit on the header's
5004
+ // left side, where a 200px menu right-aligned to a narrow button would clip.
5005
+ var right = Math.max(6, window.innerWidth - r.right);
5006
+ var mw = menu.getBoundingClientRect ? menu.getBoundingClientRect().width : 0;
5007
+ if (mw && window.innerWidth - right - mw < 6) right = Math.max(6, window.innerWidth - mw - 6);
5008
+ menu.style.right = right + 'px';
4539
5009
  }
4540
5010
  function wireDropdown(ddId, btnId) {
4541
5011
  var dd = document.getElementById(ddId);
@@ -4554,11 +5024,29 @@ var MODEL = __MODEL_JSON__;
4554
5024
  var ldd = wireDropdown('layoutDd', 'layoutBtn');
4555
5025
  var sdd = wireDropdown('settingsDd', 'settingsBtn');
4556
5026
  var mdd = wireDropdown('moreDd', 'moreBtn');
5027
+ // Compact stand-ins (header compaction stages 3-4): the search panel and the
5028
+ // mode-tab dropdown are ordinary dropdowns; their triggers stay hidden until
5029
+ // their compaction stage shows them, so wiring them here is inert at full width.
5030
+ var qdd = wireDropdown('searchDd', 'searchBtn');
5031
+ var vdd = wireDropdown('modeDd', 'modeBtn');
5032
+ // Opening the compact search panel focuses the REAL input (stage 3 moves the
5033
+ // node, never clones it, so its input listener keeps driving state.query).
5034
+ // Registered after wireDropdown's toggle, so 'open' reflects the new state.
5035
+ document.getElementById('searchBtn').addEventListener('click', function () {
5036
+ if (String(qdd.className || '').indexOf('open') >= 0) {
5037
+ var inp = document.getElementById('search');
5038
+ if (inp && inp.focus) inp.focus();
5039
+ }
5040
+ });
4557
5041
  // Keep the settings panel open while flipping switches (clicks inside it don't
4558
5042
  // bubble to the document-level close handler).
4559
5043
  (function () {
4560
5044
  var m = document.getElementById('settingsMenu');
4561
5045
  if (m && m.addEventListener) m.addEventListener('click', function (ev) { if (ev && ev.stopPropagation) ev.stopPropagation(); });
5046
+ // Same for the floating search panel: clicking into the input must not
5047
+ // bubble to the document-level close handler and shut the panel mid-typing.
5048
+ var sm = document.getElementById('searchMenu');
5049
+ if (sm && sm.addEventListener) sm.addEventListener('click', function (ev) { if (ev && ev.stopPropagation) ev.stopPropagation(); });
4562
5050
  })();
4563
5051
 
4564
5052
  // Layout picker: choose the auto-layout algorithm. Components use cytoscape's
@@ -4582,6 +5070,12 @@ var MODEL = __MODEL_JSON__;
4582
5070
  });
4583
5071
  updateLayoutBtn();
4584
5072
 
5073
+ // The compact crumb dropdown (compaction stage 5) is re-created by every
5074
+ // compact crumb render, so it is looked up per close instead of captured.
5075
+ function closeCrumbDd() {
5076
+ var cdd = document.getElementById('crumbDd');
5077
+ if (cdd && cdd.classList) cdd.classList.remove('open');
5078
+ }
4585
5079
  if (document.addEventListener) {
4586
5080
  document.addEventListener('click', function () {
4587
5081
  if (dd.classList) dd.classList.remove('open');
@@ -4589,6 +5083,9 @@ var MODEL = __MODEL_JSON__;
4589
5083
  if (ldd.classList) ldd.classList.remove('open');
4590
5084
  if (sdd.classList) sdd.classList.remove('open');
4591
5085
  if (mdd && mdd.classList) mdd.classList.remove('open');
5086
+ if (qdd && qdd.classList) qdd.classList.remove('open');
5087
+ if (vdd && vdd.classList) vdd.classList.remove('open');
5088
+ closeCrumbDd();
4592
5089
  });
4593
5090
  document.addEventListener('keydown', function (ev) {
4594
5091
  if (ev.key === 'Escape') {
@@ -4596,16 +5093,27 @@ var MODEL = __MODEL_JSON__;
4596
5093
  if (modal.classList && String(modal.className).indexOf('open') >= 0) { closeFlow(); return; }
4597
5094
  setPresentation(false);
4598
5095
  if (dd.classList) dd.classList.remove('open');
5096
+ if (qdd && qdd.classList) qdd.classList.remove('open');
5097
+ if (vdd && vdd.classList) vdd.classList.remove('open');
5098
+ closeCrumbDd();
4599
5099
  }
4600
5100
  });
4601
5101
  }
4602
5102
 
4603
- // \u2500\u2500 Responsive header overflow \u2192 "\u22EF" dropdown \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4604
- // When the floating header no longer fits its controls, trailing items
4605
- // COLLAPSE into the More menu instead of relying on horizontal scroll \u2014
4606
- // every control stays one click away. Whole items move (listeners survive
4607
- // reparenting); a hidden placeholder pins each item's original position so
4608
- // restoring keeps the exact order. Collapse order = least-used first.
5103
+ // \u2500\u2500 Responsive header compaction \u2192 ordered stages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
5104
+ // When the floating header no longer fits its controls, standard reusable
5105
+ // COMPACTION BEHAVIORS apply progressively \u2014 each stage only while the row
5106
+ // still overflows \u2014 and restore in REVERSE order when space returns:
5107
+ // 1. trailing buttons fold into the "\u22EF" menu (least-used first, one by one)
5108
+ // 2. the View dropdown folds in after them
5109
+ // 3. the search input compacts to a \u{1F50D} icon + floating panel
5110
+ // 4. the mode tabs compact to one current-mode dropdown
5111
+ // 5. ancestor crumbs compact into an ordered "\u2026" dropdown
5112
+ // Stages 1-2 move whole items (listeners survive reparenting) with a hidden
5113
+ // placeholder pinning each item's original spot for restore; stage 5 is a
5114
+ // render mode (renderCrumbs rebuilds its innerHTML, so reparenting would not
5115
+ // survive navigation). Nothing here is persisted \u2014 compaction is purely
5116
+ // responsive to the available width.
4609
5117
  (function () {
4610
5118
  if (typeof window === 'undefined') return;
4611
5119
  var hdr = document.getElementById('hdr');
@@ -4633,7 +5141,122 @@ var MODEL = __MODEL_JSON__;
4633
5141
  }
4634
5142
  return markers[id];
4635
5143
  }
4636
- var collapsed = [];
5144
+ // Fold stage (the classic behavior): move items into the "\u22EF" menu ONE per
5145
+ // apply() call \u2014 the reflow loop keeps a stage active until it reports no
5146
+ // further progress, preserving the original per-button granularity.
5147
+ function foldStage(ids) {
5148
+ var folded = [];
5149
+ return {
5150
+ apply: function () {
5151
+ while (folded.length < ids.length) {
5152
+ var id = ids[folded.length];
5153
+ var el = movableFor(id);
5154
+ if (!el || el === moreDd || el.parentNode === moreMenu) { folded.push({ el: null, marker: null }); continue; }
5155
+ var m = markerFor(id, el);
5156
+ // A dropdown moved while open would strand its fixed-positioned menu.
5157
+ if (el.classList) el.classList.remove('open');
5158
+ moreDd.style.display = '';
5159
+ moreMenu.appendChild(el);
5160
+ folded.push({ el: el, marker: m });
5161
+ return true;
5162
+ }
5163
+ return false;
5164
+ },
5165
+ restore: function () {
5166
+ for (var i = folded.length - 1; i >= 0; i--) {
5167
+ var it = folded[i];
5168
+ if (it.el && it.marker && it.marker.parentNode) it.marker.parentNode.insertBefore(it.el, it.marker);
5169
+ }
5170
+ folded = [];
5171
+ },
5172
+ };
5173
+ }
5174
+ // Stage 3: the search input compacts behind a \u{1F50D} icon; the REAL input node
5175
+ // MOVES into the floating panel (fixed-positioned by the same helper as
5176
+ // every dropdown menu), so its input listener keeps driving state.query.
5177
+ function searchStage() {
5178
+ var on = false;
5179
+ return {
5180
+ apply: function () {
5181
+ if (on) return false;
5182
+ var inp = document.getElementById('search');
5183
+ var ddw = document.getElementById('searchDd');
5184
+ var menu = document.getElementById('searchMenu');
5185
+ if (!inp || !ddw || !menu) return false;
5186
+ menu.appendChild(inp);
5187
+ ddw.style.display = '';
5188
+ on = true;
5189
+ return true;
5190
+ },
5191
+ restore: function () {
5192
+ if (!on) return;
5193
+ on = false;
5194
+ var inp = document.getElementById('search');
5195
+ var ddw = document.getElementById('searchDd');
5196
+ if (inp && ddw && ddw.parentNode) ddw.parentNode.insertBefore(inp, ddw);
5197
+ if (ddw) { ddw.style.display = 'none'; if (ddw.classList) ddw.classList.remove('open'); }
5198
+ },
5199
+ };
5200
+ }
5201
+ // Stage 4: the mode tabs collapse into ONE dropdown labelled with the
5202
+ // current mode. The REAL tab buttons move into its menu (listeners and
5203
+ // active styling survive); updateHeaderSegs keeps the trigger label in
5204
+ // sync when the mode changes while compact.
5205
+ function modeStage() {
5206
+ var moved = [];
5207
+ return {
5208
+ apply: function () {
5209
+ if (moved.length) return false;
5210
+ var seg = document.getElementById('modeSeg');
5211
+ var ddw = document.getElementById('modeDd');
5212
+ var menu = document.getElementById('modeMenu');
5213
+ if (!seg || !ddw || !menu) return false;
5214
+ var btns = seg.querySelectorAll('button');
5215
+ if (!btns.length) return false;
5216
+ for (var i = 0; i < btns.length; i++) moved.push(btns[i]);
5217
+ for (var j = 0; j < moved.length; j++) menu.appendChild(moved[j]);
5218
+ seg.style.display = 'none';
5219
+ ddw.style.display = '';
5220
+ updateModeBtn();
5221
+ return true;
5222
+ },
5223
+ restore: function () {
5224
+ if (!moved.length) return;
5225
+ var seg = document.getElementById('modeSeg');
5226
+ var ddw = document.getElementById('modeDd');
5227
+ for (var i = 0; i < moved.length; i++) seg.appendChild(moved[i]);
5228
+ moved = [];
5229
+ seg.style.display = '';
5230
+ if (ddw) { ddw.style.display = 'none'; if (ddw.classList) ddw.classList.remove('open'); }
5231
+ },
5232
+ };
5233
+ }
5234
+ // Stage 5: crumb compaction is a render-mode toggle consulted by
5235
+ // renderCrumbs itself \u2014 see crumbsCompact there. Never reparenting.
5236
+ function crumbStage() {
5237
+ return {
5238
+ apply: function () {
5239
+ if (crumbsCompact) return false;
5240
+ crumbsCompact = true;
5241
+ renderCrumbs();
5242
+ return true;
5243
+ },
5244
+ restore: function () {
5245
+ if (!crumbsCompact) return;
5246
+ crumbsCompact = false;
5247
+ renderCrumbs();
5248
+ },
5249
+ };
5250
+ }
5251
+ // Ordered compaction stages: applied first-to-last only while the header
5252
+ // overflows, restored last-to-first when space returns.
5253
+ var STAGES = [
5254
+ foldStage(COLLAPSE), // 1: trailing buttons \u2192 "\u22EF" menu
5255
+ foldStage(['settingsBtn']), // 2: the View dropdown folds in too
5256
+ searchStage(), // 3: search input \u2192 \u{1F50D} + floating panel
5257
+ modeStage(), // 4: mode tabs \u2192 current-mode dropdown
5258
+ crumbStage(), // 5: ancestor crumbs \u2192 "\u2026" dropdown
5259
+ ];
4637
5260
  // Signed fit measure in px: positive = overflowing, negative = headroom.
4638
5261
  // The header is a flex row whose ONLY flex:1 child is the .spacer, so the
4639
5262
  // spacer's rendered width IS the free space -- it grows to absorb all slack
@@ -4649,33 +5272,54 @@ var MODEL = __MODEL_JSON__;
4649
5272
  var slack = spacer ? spacer.getBoundingClientRect().width : 0;
4650
5273
  return (hdr.scrollWidth - hdr.clientWidth) - slack;
4651
5274
  }
5275
+ var inReflow = false;
4652
5276
  function reflow() {
4653
5277
  // Not laid out (hidden tab, non-browser DOM) \u2014 measuring would misfire.
4654
5278
  var box = hdr.getBoundingClientRect();
4655
5279
  if (!box || box.width <= 0) return;
4656
- // Restore everything, then collapse until the row fits (idempotent).
4657
- for (var i = collapsed.length - 1; i >= 0; i--) {
4658
- var it = collapsed[i];
4659
- if (it.el && it.marker && it.marker.parentNode) it.marker.parentNode.insertBefore(it.el, it.marker);
4660
- }
4661
- collapsed = [];
4662
- moreDd.style.display = 'none';
4663
- hdr.scrollLeft = 0;
4664
- var guard = 0;
4665
- // Demand a few px of headroom, not a bare fit \u2014 the marginal-fit widths
4666
- // are exactly where the phantom scrollbar appeared.
4667
- while (overflowPx() > -8 && guard < COLLAPSE.length) {
4668
- var id = COLLAPSE[guard++];
4669
- var el = movableFor(id);
4670
- if (!el || el === moreDd || el.parentNode === moreMenu) continue;
4671
- var m = markerFor(id, el);
4672
- // A dropdown moved while open would strand its fixed-positioned menu.
4673
- if (el.classList) el.classList.remove('open');
4674
- moreDd.style.display = '';
4675
- moreMenu.appendChild(el);
4676
- collapsed.push({ el: el, marker: m });
5280
+ inReflow = true;
5281
+ try {
5282
+ // The floating search panel must survive a reflow cycle: restore-all
5283
+ // would close it (and reparenting blurs the input), so capture its
5284
+ // open/focus state up front and reinstate it after the stage walk.
5285
+ var ddw = document.getElementById('searchDd');
5286
+ var inp = document.getElementById('search');
5287
+ var searchOpen = !!(ddw && String(ddw.className || '').indexOf('open') >= 0);
5288
+ var searchFocus = false;
5289
+ try {
5290
+ var ae = (typeof ROOT !== 'undefined' && ROOT ? ROOT : document).activeElement;
5291
+ searchFocus = !!(ae && inp && ae === inp);
5292
+ } catch (e) { /* stubbed DOM */ }
5293
+ // Restore every stage in REVERSE order, then re-apply progressively
5294
+ // while the row still overflows (idempotent).
5295
+ for (var i = STAGES.length - 1; i >= 0; i--) STAGES[i].restore();
5296
+ moreDd.style.display = 'none';
5297
+ hdr.scrollLeft = 0;
5298
+ // Demand a few px of headroom, not a bare fit \u2014 the marginal-fit widths
5299
+ // are exactly where the phantom scrollbar appeared. Every apply()
5300
+ // changes the very widths being measured, but restore-all + a strictly
5301
+ // forward stage walk make the outcome a pure function of the current
5302
+ // width, and reflow never reschedules itself (renderCrumbs' nudge is
5303
+ // suppressed via inReflow, and #hdr's own box never changes here), so
5304
+ // boundary widths settle in ONE pass instead of oscillating.
5305
+ var si = 0;
5306
+ var guard = 0;
5307
+ var bound = COLLAPSE.length + STAGES.length + 8;
5308
+ while (overflowPx() > -8 && si < STAGES.length && guard < bound) {
5309
+ guard++;
5310
+ if (!STAGES[si].apply()) si++;
5311
+ }
5312
+ if (searchOpen || searchFocus) {
5313
+ var compactNow = ddw && ddw.style && ddw.style.display !== 'none';
5314
+ if (compactNow && searchOpen) {
5315
+ if (ddw.classList) ddw.classList.add('open');
5316
+ positionDropdownMenu(ddw, document.getElementById('searchBtn'));
5317
+ }
5318
+ if (searchFocus && inp && inp.focus) inp.focus();
5319
+ }
5320
+ } finally {
5321
+ inReflow = false;
4677
5322
  }
4678
- if (collapsed.length === 0) moreDd.style.display = 'none';
4679
5323
  }
4680
5324
  var raf = null;
4681
5325
  var defer = window.requestAnimationFrame
@@ -4685,6 +5329,9 @@ var MODEL = __MODEL_JSON__;
4685
5329
  if (raf !== null) return;
4686
5330
  raf = defer(function () { raf = null; reflow(); });
4687
5331
  }
5332
+ // Crumb re-renders change the header's content width without resizing #hdr
5333
+ // itself \u2014 renderCrumbs nudges a reflow through this hook (no-op mid-reflow).
5334
+ headerReflowHook = function () { if (!inReflow) schedule(); };
4688
5335
  if (typeof ResizeObserver !== 'undefined') {
4689
5336
  new ResizeObserver(schedule).observe(hdr);
4690
5337
  } else if (window.addEventListener) {
@@ -6218,47 +6865,6 @@ var init_filenames = __esm({
6218
6865
  }
6219
6866
  });
6220
6867
 
6221
- // src/core/statehash.ts
6222
- function computeStateId() {
6223
- const tree = {
6224
- system: loadSystemSpec(),
6225
- subsystems: loadSubsystemSpecs(),
6226
- components: loadComponentSpecs(),
6227
- interfaces: loadInterfaceSpecs(),
6228
- implementations: loadImplementationSpecs(),
6229
- types: loadTypeSpecs()
6230
- };
6231
- const digest = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
6232
- return { algorithm: "sha256", digest };
6233
- }
6234
- function stateIdEquals(a, b) {
6235
- return !!a && !!b && a.algorithm === b.algorithm && a.digest === b.digest;
6236
- }
6237
- function canonicalize(value) {
6238
- return JSON.stringify(sortKeys(value));
6239
- }
6240
- function sortKeys(v) {
6241
- if (Array.isArray(v)) return v.map(sortKeys);
6242
- if (v && typeof v === "object") {
6243
- const src = v;
6244
- const out = {};
6245
- for (const k of Object.keys(src).sort()) {
6246
- if (k === "createdAt" || k === "updatedAt") continue;
6247
- out[k] = sortKeys(src[k]);
6248
- }
6249
- return out;
6250
- }
6251
- return v;
6252
- }
6253
- var crypto;
6254
- var init_statehash = __esm({
6255
- "src/core/statehash.ts"() {
6256
- "use strict";
6257
- crypto = __toESM(require("crypto"));
6258
- init_specs2();
6259
- }
6260
- });
6261
-
6262
6868
  // src/core/openapi.ts
6263
6869
  function schemaFor(typeRef, closureIds) {
6264
6870
  const trimmed = typeRef.trim().replace(/^promise\s*<(.+)>$/i, "$1").trim();
@@ -6716,6 +7322,57 @@ function projectOwnSurface(maxAudience) {
6716
7322
  function projectChildSurface() {
6717
7323
  return projectOwnSurface("project");
6718
7324
  }
7325
+ function localName(id) {
7326
+ return id.split("::").pop();
7327
+ }
7328
+ function projectSubsystemSurface(subsystemId) {
7329
+ const system = loadSystemSpec();
7330
+ if (!system) {
7331
+ throw new Error("Cannot project a subsystem surface: the L0 system spec is missing.");
7332
+ }
7333
+ const subsystems = loadSubsystemSpecs();
7334
+ const target = subsystems.find((s) => s.id === subsystemId);
7335
+ if (!target) {
7336
+ throw new Error(`Cannot project a subsystem surface: subsystem "${subsystemId}" does not exist.`);
7337
+ }
7338
+ const components = loadComponentSpecs();
7339
+ const interfaces = loadInterfaceSpecs();
7340
+ const types = loadTypeSpecs();
7341
+ const entries = [];
7342
+ for (const pub of target.publicInterfaces ?? []) {
7343
+ if (!pub.component) continue;
7344
+ const comp = components.find((c) => c.id === pub.component || c.id === `${subsystemId}::${pub.component}`);
7345
+ if (!comp) continue;
7346
+ if (comp.componentType !== "Portal") continue;
7347
+ const compInterfaces = interfaces.filter((i) => i.component === comp.id && (!pub.interface || i.id === pub.interface || i.id === `${subsystemId}::${pub.interface}`));
7348
+ const methods = compInterfaces.flatMap((i) => i.methods);
7349
+ entries.push({
7350
+ id: localName(pub.interface ?? comp.id),
7351
+ name: comp.name,
7352
+ // Family ceiling: a sibling surface is consumable by the system family only.
7353
+ audience: "project",
7354
+ type: pub.type ?? "Custom",
7355
+ // The snapshot carries the LOCAL portal name — consumers resolve cross-tree
7356
+ // refs by their final segment.
7357
+ component: localName(comp.id),
7358
+ methods,
7359
+ ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
7360
+ // Project the backing Portal's auth + basePath so the codec can emit
7361
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
7362
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
7363
+ ...comp.basePath ? { basePath: comp.basePath } : {},
7364
+ details: pub.details ?? ""
7365
+ });
7366
+ }
7367
+ return SurfaceSnapshotSchema.parse({
7368
+ projectName: `${system.name}::${subsystemId}`,
7369
+ origin: "generated",
7370
+ stateId: stateIdString(),
7371
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
7372
+ interfaces: entries,
7373
+ types: computeTypeClosure(entries, types)
7374
+ });
7375
+ }
6719
7376
  function listSnapshots(rootDir = getProjectRoot()) {
6720
7377
  const dir = surfacesDir(rootDir);
6721
7378
  if (!fs4.existsSync(dir)) return [];
@@ -6732,18 +7389,37 @@ function listSnapshots(rootDir = getProjectRoot()) {
6732
7389
  function getSnapshot(projectName, rootDir = getProjectRoot()) {
6733
7390
  return listSnapshots(rootDir).find((s) => s.projectName === projectName) ?? null;
6734
7391
  }
7392
+ function snapshotFilename(projectName) {
7393
+ return `${safeFilenamePart(projectName)}.yaml`;
7394
+ }
6735
7395
  function saveSnapshot(snapshot, rootDir = getProjectRoot()) {
6736
7396
  const dir = surfacesDir(rootDir);
6737
7397
  fs4.mkdirSync(dir, { recursive: true });
6738
- const p = path5.join(dir, `${snapshot.projectName}.yaml`);
7398
+ const p = path5.join(dir, snapshotFilename(snapshot.projectName));
6739
7399
  writeYamlFile(p, SurfaceSnapshotSchema.parse(snapshot));
6740
7400
  return p;
6741
7401
  }
6742
7402
  function removeSnapshot(projectName, rootDir = getProjectRoot()) {
6743
- const p = path5.join(surfacesDir(rootDir), `${projectName}.yaml`);
6744
- if (!fs4.existsSync(p)) return false;
6745
- fs4.unlinkSync(p);
6746
- return true;
7403
+ const dir = surfacesDir(rootDir);
7404
+ const direct = path5.join(dir, snapshotFilename(projectName));
7405
+ if (fs4.existsSync(direct)) {
7406
+ fs4.unlinkSync(direct);
7407
+ return true;
7408
+ }
7409
+ if (!fs4.existsSync(dir)) return false;
7410
+ for (const file of fs4.readdirSync(dir)) {
7411
+ if (!file.endsWith(".yaml") && !file.endsWith(".yml")) continue;
7412
+ const p = path5.join(dir, file);
7413
+ try {
7414
+ const snap = SurfaceSnapshotSchema.parse(readYamlFile(p));
7415
+ if (snap.projectName === projectName) {
7416
+ fs4.unlinkSync(p);
7417
+ return true;
7418
+ }
7419
+ } catch {
7420
+ }
7421
+ }
7422
+ return false;
6747
7423
  }
6748
7424
  function loadSurfaceSnapshots() {
6749
7425
  return listSnapshots();
@@ -6814,17 +7490,54 @@ function importSurface(sourcePath, origin) {
6814
7490
  return snapshot;
6815
7491
  }
6816
7492
  function generateChildSnapshots(rootDir = getProjectRoot()) {
6817
- const children = loadSubsystemSpecs().filter((s) => s.projectPath && !s.id.includes("::"));
7493
+ const topLevel = loadSubsystemSpecs().filter((s) => !s.id.includes("::"));
7494
+ const children = topLevel.filter((s) => s.projectPath);
6818
7495
  if (!children.length) return [];
6819
- const snapshot = projectChildSurface();
7496
+ const familySnapshot = projectChildSurface();
7497
+ const siblingSnapshots = /* @__PURE__ */ new Map();
7498
+ const siblingSurface = (subsystemId) => {
7499
+ let snap = siblingSnapshots.get(subsystemId);
7500
+ if (!snap) {
7501
+ snap = projectSubsystemSurface(subsystemId);
7502
+ siblingSnapshots.set(subsystemId, snap);
7503
+ }
7504
+ return snap;
7505
+ };
6820
7506
  const written = [];
6821
7507
  for (const child of children) {
6822
7508
  const childDir = path5.resolve(rootDir, child.projectPath);
6823
7509
  if (!fs4.existsSync(childDir)) continue;
6824
- written.push(saveSnapshot(snapshot, childDir));
7510
+ written.push(saveSnapshot(familySnapshot, childDir));
7511
+ for (const sibling of topLevel) {
7512
+ if (sibling.id === child.id) continue;
7513
+ written.push(saveSnapshot(siblingSurface(sibling.id), childDir));
7514
+ }
6825
7515
  }
6826
7516
  return written;
6827
7517
  }
7518
+ function computeParentStateId(parentRoot) {
7519
+ return computeStateIdAt(parentRoot);
7520
+ }
7521
+ function listExternalInterfaces() {
7522
+ const snapshots = listSnapshots();
7523
+ const chainingParent = resolveChainingParent();
7524
+ const parentStateId = chainingParent ? computeParentStateId(chainingParent.parentRoot) : null;
7525
+ return snapshots.map((snapshot) => {
7526
+ const generated = snapshot.origin === "generated";
7527
+ const sourceKind = !generated ? "foreign" : snapshot.projectName.includes("::") ? "sibling" : "parent";
7528
+ const freshness = generated && parentStateId ? snapshot.stateId === parentStateId ? "fresh" : "stale" : "unverifiable";
7529
+ return {
7530
+ projectName: snapshot.projectName,
7531
+ origin: snapshot.origin,
7532
+ sourceKind,
7533
+ generatedAt: snapshot.generatedAt,
7534
+ ...snapshot.stateId ? { stateId: snapshot.stateId } : {},
7535
+ ...snapshot.version ? { version: snapshot.version } : {},
7536
+ freshness,
7537
+ interfaceIds: snapshot.interfaces.map((e) => e.id)
7538
+ };
7539
+ });
7540
+ }
6828
7541
  function surfaceContentKey(snapshot) {
6829
7542
  const { stateId, generatedAt, origin, ...content } = snapshot;
6830
7543
  return JSON.stringify(content);
@@ -7033,7 +7746,8 @@ var init_contracts = __esm({
7033
7746
  "SURFACE_REF_NOT_EXPOSED",
7034
7747
  `Method "${implMethod.name}" in implementation "${impl.id}" dispatches capability "${step.capability}" through cross-tree portal "${step.targetComponent}" (step ${step.stepNumber}), but the surface snapshot of "${resolved.snapshot.projectName}" does not serve that capability on "${resolved.entry.id}".`,
7035
7748
  impl.id,
7036
- isDraftCtx
7749
+ isDraftCtx,
7750
+ true
7037
7751
  );
7038
7752
  }
7039
7753
  continue;
@@ -7090,7 +7804,8 @@ var init_contracts = __esm({
7090
7804
  "SURFACE_REF_NOT_EXPOSED",
7091
7805
  `Method "${implMethod.name}" in implementation "${impl.id}" calls "${step.targetMethod}" on cross-tree component "${step.targetComponent}" (step ${step.stepNumber}), but the surface snapshot of "${resolved.snapshot.projectName}" does not expose that method on "${resolved.entry.id}".`,
7092
7806
  impl.id,
7093
- isDraftCtx
7807
+ isDraftCtx,
7808
+ true
7094
7809
  );
7095
7810
  } else if (step.assertsGuarantees) {
7096
7811
  const declared = new Set(surfaceMethod.guarantees ?? []);
@@ -7101,7 +7816,8 @@ var init_contracts = __esm({
7101
7816
  "NARRATIVE_SEMANTIC_UNBACKED",
7102
7817
  `Step ${step.stepNumber} of "${implMethod.name}" in implementation "${impl.id}" asserts guarantee "${g}", but the surface snapshot of "${resolved.snapshot.projectName}" does not declare it on "${resolved.entry.id}.${step.targetMethod}".`,
7103
7818
  impl.id,
7104
- isDraftCtx
7819
+ isDraftCtx,
7820
+ true
7105
7821
  );
7106
7822
  }
7107
7823
  }
@@ -8589,7 +9305,8 @@ var init_stereotype_deps = __esm({
8589
9305
  "CROSS_SUBSYSTEM_NON_ADAPTER",
8590
9306
  `Boundary violation: ${comp.componentType} "${comp.id}" depends directly on "${depId}", a surface of project "${resolved.snapshot.projectName}". Only a local client Adapter may cross a project boundary \u2014 route this hop through an Adapter.`,
8591
9307
  comp.id,
8592
- isDraftCtx
9308
+ isDraftCtx,
9309
+ true
8593
9310
  );
8594
9311
  }
8595
9312
  continue;
@@ -11847,9 +12564,15 @@ function buildRuleContext(opts) {
11847
12564
  ...[...SDD_RULES, ...extensions.rules].flatMap((r) => r.codes.map((c) => c.code)),
11848
12565
  // Declarative assertions bring their own namespaced codes — lint.allow
11849
12566
  // and severity overrides treat them exactly like builtins.
11850
- ...extensions.assertions.map((a) => a.fullCode)
12567
+ ...extensions.assertions.map((a) => a.fullCode),
12568
+ // Entry-point emitted codes: validateSddTree's chained-subproject pass
12569
+ // raises these AFTER the rule run (it post-processes the aggregated issue
12570
+ // list), so no registered rule declares them — but lint.allow validation
12571
+ // must still recognize them as real codes.
12572
+ "CHAINED_SUBPROJECT_CONTEXT",
12573
+ "UNVERIFIED_EXTERNAL_REF"
11851
12574
  ]);
11852
- const addIssue = (defaultSeverity, code, message, specId, isDraftContext) => {
12575
+ const addIssue = (defaultSeverity, code, message, specId, isDraftContext, surfaceResolved) => {
11853
12576
  if (scopeSubsystem && specId && !isSpecInScope(specId)) {
11854
12577
  return;
11855
12578
  }
@@ -11867,7 +12590,14 @@ function buildRuleContext(opts) {
11867
12590
  if (severity === "warning") return;
11868
12591
  }
11869
12592
  }
11870
- issues.push({ severity, code, message, specId, ...isDraftContext ? { draftContext: true } : {} });
12593
+ issues.push({
12594
+ severity,
12595
+ code,
12596
+ message,
12597
+ specId,
12598
+ ...isDraftContext ? { draftContext: true } : {},
12599
+ ...surfaceResolved ? { surfaceResolved: true } : {}
12600
+ });
11871
12601
  };
11872
12602
  return {
11873
12603
  system,
@@ -12345,24 +13075,54 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
12345
13075
  for (const rule of ruleSequence()) {
12346
13076
  rule.check(ctx);
12347
13077
  }
12348
- const hasCrossTreeSuspects = issues.some((i) => SUBPROJECT_LENIENT_CODES.has(i.code));
13078
+ const hasCrossTreeSuspects = issues.some(
13079
+ (i) => SUBPROJECT_REFERENCE_CODES.has(i.code) || SUBPROJECT_CONFORMANCE_CODES.has(i.code)
13080
+ );
12349
13081
  const chainingParent = hasCrossTreeSuspects ? findChainingParent(getProjectRoot()) : null;
12350
13082
  if (chainingParent) {
13083
+ let unverified = 0;
12351
13084
  let downgraded = 0;
12352
- for (const iss of issues) {
12353
- if (!SUBPROJECT_LENIENT_CODES.has(iss.code)) continue;
12354
- if (iss.severity === "error") {
12355
- iss.severity = "warning";
12356
- downgraded++;
13085
+ for (let at = 0; at < issues.length; at++) {
13086
+ const iss = issues[at];
13087
+ if (SUBPROJECT_REFERENCE_CODES.has(iss.code) && !iss.surfaceResolved) {
13088
+ issues[at] = {
13089
+ severity: "warning",
13090
+ code: "UNVERIFIED_EXTERNAL_REF",
13091
+ crossTreeContext: true,
13092
+ // --ci waives it (parent root is authoritative)
13093
+ specId: iss.specId,
13094
+ ...iss.agentId ? { agentId: iss.agentId } : {},
13095
+ ...iss.draftContext ? { draftContext: true } : {},
13096
+ message: `Unverified external reference (${iss.code}): ${iss.message} No vendored surface snapshot covers this reference, so it cannot be verified from this chained subproject standalone \u2014 re-lock the parent so fresh family/sibling snapshots ship, or inspect what this project can consume via \`wairon surface externals\` / sdd_list_external_interfaces.`
13097
+ };
13098
+ unverified++;
13099
+ continue;
13100
+ }
13101
+ if (SUBPROJECT_CONFORMANCE_CODES.has(iss.code)) {
13102
+ if (iss.severity === "error") {
13103
+ iss.severity = "warning";
13104
+ downgraded++;
13105
+ }
13106
+ iss.crossTreeContext = true;
12357
13107
  }
12358
- iss.crossTreeContext = true;
12359
13108
  }
12360
- if (downgraded > 0) {
13109
+ if (unverified > 0 || downgraded > 0) {
13110
+ const notes = [];
13111
+ if (unverified > 0) {
13112
+ notes.push(
13113
+ `${unverified} cross-tree reference(s) have no vendored surface snapshot covering them and were reported as UNVERIFIED_EXTERNAL_REF warnings \u2014 re-lock the parent so fresh family/sibling snapshots ship, or inspect via \`wairon surface externals\` / sdd_list_external_interfaces.`
13114
+ );
13115
+ }
13116
+ if (downgraded > 0) {
13117
+ notes.push(
13118
+ `${downgraded} code\u2194spec conformance finding(s) (parent-root-relative source paths) were downgraded to warnings.`
13119
+ );
13120
+ }
12361
13121
  issues.unshift({
12362
13122
  severity: "warning",
12363
13123
  code: "CHAINED_SUBPROJECT_CONTEXT",
12364
13124
  crossTreeContext: true,
12365
- message: `This project is a chained subproject ("${chainingParent.subsystemId}") of the parent project at "${chainingParent.parentRoot}". ${downgraded} reference(s) resolve only in the parent tree (shared types, sibling subsystems, or cross-tree components that live above this root) and were downgraded to warnings \u2014 validating a subproject standalone cannot verify them. Run validation from the parent root for full cross-tree verification.`
13125
+ message: `This project is a chained subproject ("${chainingParent.subsystemId}") of the parent project at "${chainingParent.parentRoot}". ${notes.join(" ")} Full cross-tree verification runs from the parent root.`
12366
13126
  });
12367
13127
  }
12368
13128
  }
@@ -12379,7 +13139,7 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
12379
13139
  function validateAsComplete(options) {
12380
13140
  return validateSddTree({ ...options ?? {}, treatAllAsComplete: true });
12381
13141
  }
12382
- var SUBPROJECT_LENIENT_CODES;
13142
+ var SUBPROJECT_REFERENCE_CODES, SUBPROJECT_CONFORMANCE_CODES;
12383
13143
  var init_validation = __esm({
12384
13144
  "src/core/validation.ts"() {
12385
13145
  "use strict";
@@ -12392,8 +13152,7 @@ var init_validation = __esm({
12392
13152
  init_source_analysis();
12393
13153
  init_specs2();
12394
13154
  init_fs();
12395
- SUBPROJECT_LENIENT_CODES = /* @__PURE__ */ new Set([
12396
- // reference resolution
13155
+ SUBPROJECT_REFERENCE_CODES = /* @__PURE__ */ new Set([
12397
13156
  "UNDEFINED_TYPE_REFERENCE",
12398
13157
  "INVALID_DEPENDENCY_REFERENCE",
12399
13158
  "INVALID_TARGET_COMPONENT_REFERENCE",
@@ -12401,8 +13160,9 @@ var init_validation = __esm({
12401
13160
  "UNDECLARED_DEPENDENCY_CALL",
12402
13161
  "INVALID_TRUSTED_LINK",
12403
13162
  "CROSS_SUBSYSTEM_NON_ADAPTER",
12404
- "CROSS_TREE_REF_UNRESOLVED",
12405
- // code↔spec conformance (root-relative sourcePaths / import graph)
13163
+ "CROSS_TREE_REF_UNRESOLVED"
13164
+ ]);
13165
+ SUBPROJECT_CONFORMANCE_CODES = /* @__PURE__ */ new Set([
12406
13166
  "MISSING_SOURCE_FILE",
12407
13167
  "SOURCE_PATH_ESCAPES_ROOT",
12408
13168
  "MISSING_SOURCE_PATH",
@@ -13325,6 +14085,19 @@ function dryRunSerializeSpecs(include) {
13325
14085
  function buildProjectGraph(level) {
13326
14086
  return buildGraphModel(level);
13327
14087
  }
14088
+ function resolveChainingParent() {
14089
+ return findChainingParent(getProjectRoot());
14090
+ }
14091
+ function computeStateIdAt(root) {
14092
+ const resolved = path10.resolve(root);
14093
+ return runWithProjectRoot(resolved, () => {
14094
+ workspaceFor(resolved).invalidate();
14095
+ const system = loadSystemSpec();
14096
+ if (!system) return null;
14097
+ const s = computeStateId();
14098
+ return `${s.algorithm}:${s.digest}`;
14099
+ });
14100
+ }
13328
14101
  function deleteTypeSpec(id) {
13329
14102
  return current().deleteTypeSpec(id);
13330
14103
  }
@@ -13368,6 +14141,7 @@ var init_specs2 = __esm({
13368
14141
  path10 = __toESM(require("path"));
13369
14142
  init_loader();
13370
14143
  init_fs();
14144
+ init_statehash();
13371
14145
  init_yaml();
13372
14146
  init_models();
13373
14147
  init_narrative_labels();
@@ -15532,7 +16306,9 @@ __export(src_exports, {
15532
16306
  clearLoaderIssues: () => clearLoaderIssues,
15533
16307
  collectPromotableSpecs: () => collectPromotableSpecs,
15534
16308
  composeRuleSequence: () => composeRuleSequence,
16309
+ computeParentStateId: () => computeParentStateId,
15535
16310
  computeStateId: () => computeStateId,
16311
+ computeStateIdAt: () => computeStateIdAt,
15536
16312
  contextDir: () => contextDir,
15537
16313
  createAgentRecord: () => createAgentRecord,
15538
16314
  createChainedSubsystem: () => createChainedSubsystem,
@@ -15598,6 +16374,7 @@ __export(src_exports, {
15598
16374
  isOpenApiDocument: () => isOpenApiDocument,
15599
16375
  isProjectInitialized: () => isProjectInitialized,
15600
16376
  listDirectChainedSubprojects: () => listDirectChainedSubprojects,
16377
+ listExternalInterfaces: () => listExternalInterfaces,
15601
16378
  listFiles: () => listFiles,
15602
16379
  listFilesRecursive: () => listFilesRecursive,
15603
16380
  listFreeStandingDomains: () => listFreeStandingDomains,
@@ -15643,6 +16420,7 @@ __export(src_exports, {
15643
16420
  pathExists: () => pathExists,
15644
16421
  projectChildSurface: () => projectChildSurface,
15645
16422
  projectOwnSurface: () => projectOwnSurface,
16423
+ projectSubsystemSurface: () => projectSubsystemSurface,
15646
16424
  promoteAllComplete: () => promoteAllComplete,
15647
16425
  provisionProject: () => provisionProject,
15648
16426
  readArchitectureContext: () => readArchitectureContext,
@@ -15662,6 +16440,7 @@ __export(src_exports, {
15662
16440
  renderTemplateInstructions: () => renderTemplateInstructions,
15663
16441
  renderWaironGuide: () => renderWaironGuide,
15664
16442
  resolveAgentTopology: () => resolveAgentTopology,
16443
+ resolveChainingParent: () => resolveChainingParent,
15665
16444
  resolveDomains: () => resolveDomains,
15666
16445
  resolvePackRef: () => resolvePackRef,
15667
16446
  resolveSubprojectForNamespace: () => resolveSubprojectForNamespace,
@@ -15725,7 +16504,7 @@ function defaultTargetConfig(type) {
15725
16504
  enabled: true
15726
16505
  };
15727
16506
  }
15728
- var WAIRON_VERSION = "5.0.2-dev.12";
16507
+ var WAIRON_VERSION = "5.0.2-dev.14";
15729
16508
  var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
15730
16509
  var ARCHITECT_AGENT_ID = "agent-architect";
15731
16510
  var ARCHITECT_TEMPLATE_ID = "architect";
@@ -17119,7 +17898,9 @@ init_yaml();
17119
17898
  clearLoaderIssues,
17120
17899
  collectPromotableSpecs,
17121
17900
  composeRuleSequence,
17901
+ computeParentStateId,
17122
17902
  computeStateId,
17903
+ computeStateIdAt,
17123
17904
  contextDir,
17124
17905
  createAgentRecord,
17125
17906
  createChainedSubsystem,
@@ -17185,6 +17966,7 @@ init_yaml();
17185
17966
  isOpenApiDocument,
17186
17967
  isProjectInitialized,
17187
17968
  listDirectChainedSubprojects,
17969
+ listExternalInterfaces,
17188
17970
  listFiles,
17189
17971
  listFilesRecursive,
17190
17972
  listFreeStandingDomains,
@@ -17230,6 +18012,7 @@ init_yaml();
17230
18012
  pathExists,
17231
18013
  projectChildSurface,
17232
18014
  projectOwnSurface,
18015
+ projectSubsystemSurface,
17233
18016
  promoteAllComplete,
17234
18017
  provisionProject,
17235
18018
  readArchitectureContext,
@@ -17249,6 +18032,7 @@ init_yaml();
17249
18032
  renderTemplateInstructions,
17250
18033
  renderWaironGuide,
17251
18034
  resolveAgentTopology,
18035
+ resolveChainingParent,
17252
18036
  resolveDomains,
17253
18037
  resolvePackRef,
17254
18038
  resolveSubprojectForNamespace,