@wairon/cli 5.0.2-dev.9 → 5.1.0

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
@@ -148,7 +148,7 @@ var init_domain = __esm({
148
148
  });
149
149
 
150
150
  // src/models/project.ts
151
- var import_zod3, BuiltinTargetConfigSchema, CustomTargetConfigSchema, TargetConfigSchema, NamingRuleConfigSchema, DocumentationRuleConfigSchema, ComplexityRuleConfigSchema, DesignDepthSchema, RulesConfigSchema, PathsConfigSchema, ProjectConfigSchema;
151
+ var import_zod3, BuiltinTargetConfigSchema, CustomTargetConfigSchema, TargetConfigSchema, NamingRuleConfigSchema, DocumentationRuleConfigSchema, ComplexityRuleConfigSchema, DesignDepthSchema, RulesConfigSchema, PathsConfigSchema, ProfileSelectionSubjectSchema, ProjectProfileSelectionSchema, ProjectConfigSchema;
152
152
  var init_project = __esm({
153
153
  "src/models/project.ts"() {
154
154
  "use strict";
@@ -272,6 +272,24 @@ var init_project = __esm({
272
272
  /** Base directory containing SDD specification files, relative to project root */
273
273
  specsDir: import_zod3.z.string().default(".wai/specs")
274
274
  });
275
+ ProfileSelectionSubjectSchema = import_zod3.z.object({
276
+ userId: import_zod3.z.string(),
277
+ kind: import_zod3.z.string(),
278
+ issuer: import_zod3.z.string(),
279
+ externalSubject: import_zod3.z.string().optional(),
280
+ displayName: import_zod3.z.string().optional(),
281
+ email: import_zod3.z.string().optional()
282
+ });
283
+ ProjectProfileSelectionSchema = import_zod3.z.object({
284
+ /** Selected architectural profile ids. The first resolvable one is applied as projectType. */
285
+ profileIds: import_zod3.z.array(import_zod3.z.string()).default([]),
286
+ /** Pack names the governing policy requires for this project. */
287
+ requiredPackNames: import_zod3.z.array(import_zod3.z.string()).default([]),
288
+ /** Pack names applied by default unless explicitly overridden. */
289
+ defaultPackNames: import_zod3.z.array(import_zod3.z.string()).optional(),
290
+ selectedBy: ProfileSelectionSubjectSchema.optional(),
291
+ selectedAt: import_zod3.z.string()
292
+ });
275
293
  ProjectConfigSchema = import_zod3.z.object({
276
294
  /**
277
295
  * Schema version — used to detect incompatible config formats in future
@@ -312,6 +330,13 @@ var init_project = __esm({
312
330
  useGlobalPacks: import_zod3.z.boolean().default(true)
313
331
  }).optional(),
314
332
  paths: PathsConfigSchema.default({}),
333
+ /**
334
+ * The profile/pack selection a hosted policy workflow applied to this project.
335
+ * The RECORD of what was chosen; `projectType` above is what actually governs
336
+ * validation. Modeled so the parse/write round trip preserves it (see
337
+ * ProjectProfileSelectionSchema).
338
+ */
339
+ profileSelection: ProjectProfileSelectionSchema.optional(),
315
340
  /**
316
341
  * Path to a directory containing org/user-level default templates.
317
342
  * Resolved before built-in templates but after project-local templates.
@@ -787,10 +812,14 @@ var init_specs = __esm({
787
812
  // Required if type is 'dispatch': the capability routed through the target Portal's dispatch table
788
813
  /**
789
814
  * call/dispatch only: the credential this step presents to an authed callee
790
- * Portal, and WHERE it is loaded from (`from` a secret-store component id,
791
- * `env:API_KEY`, a config key, a vault ref, …). A declared DESIGN NOTE — wairon
792
- * never fetches it — but its absence on a call into a Portal whose `auth none`
793
- * warns (PORTAL_AUTH_UNMET), so credential loading is never overlooked.
815
+ * Portal, and WHERE it is loaded from (`from`). Two forms: an OPAQUE source
816
+ * (`env:API_KEY`, a config key, `vault:path`, a free note) a design note
817
+ * wairon never resolves; or a MODELED reference `component:<id>` pointing at
818
+ * the Adapter/Store that provides the secret validated to resolve, be an
819
+ * Adapter/Store, and be wired to the presenter (a checked graph edge). The
820
+ * actual secret is never stored here. Absence on a call into a Portal whose
821
+ * `auth ≠ none` warns (PORTAL_AUTH_UNMET), so credential loading is never
822
+ * overlooked.
794
823
  */
795
824
  auth: import_zod6.z.object({ from: import_zod6.z.string(), note: import_zod6.z.string().optional() }).optional(),
796
825
  assertsGuarantees: import_zod6.z.array(GuaranteeSchema).optional(),
@@ -1256,6 +1285,47 @@ var init_errors = __esm({
1256
1285
  }
1257
1286
  });
1258
1287
 
1288
+ // src/core/statehash.ts
1289
+ function computeStateId() {
1290
+ const tree = {
1291
+ system: loadSystemSpec(),
1292
+ subsystems: loadSubsystemSpecs(),
1293
+ components: loadComponentSpecs(),
1294
+ interfaces: loadInterfaceSpecs(),
1295
+ implementations: loadImplementationSpecs(),
1296
+ types: loadTypeSpecs()
1297
+ };
1298
+ const digest = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
1299
+ return { algorithm: "sha256", digest };
1300
+ }
1301
+ function stateIdEquals(a, b) {
1302
+ return !!a && !!b && a.algorithm === b.algorithm && a.digest === b.digest;
1303
+ }
1304
+ function canonicalize(value) {
1305
+ return JSON.stringify(sortKeys(value));
1306
+ }
1307
+ function sortKeys(v) {
1308
+ if (Array.isArray(v)) return v.map(sortKeys);
1309
+ if (v && typeof v === "object") {
1310
+ const src = v;
1311
+ const out = {};
1312
+ for (const k of Object.keys(src).sort()) {
1313
+ if (k === "createdAt" || k === "updatedAt") continue;
1314
+ out[k] = sortKeys(src[k]);
1315
+ }
1316
+ return out;
1317
+ }
1318
+ return v;
1319
+ }
1320
+ var crypto;
1321
+ var init_statehash = __esm({
1322
+ "src/core/statehash.ts"() {
1323
+ "use strict";
1324
+ crypto = __toESM(require("crypto"));
1325
+ init_specs2();
1326
+ }
1327
+ });
1328
+
1259
1329
  // src/core/narrative-labels.ts
1260
1330
  function resolveNarrativeLabels(methodName, steps) {
1261
1331
  const errors = [];
@@ -2341,6 +2411,16 @@ header input[type="search"]::placeholder { color:var(--dim); }
2341
2411
  #moreMenu .dropdown { display:block; width:100%; }
2342
2412
  #moreMenu .dropdown > .tbtn, #moreMenu > .tbtn { display:block; width:100%; text-align:left; border:none; background:transparent; margin:2px 0; }
2343
2413
  #moreMenu .dropdown > .tbtn:hover, #moreMenu > .tbtn:hover { background:var(--hover-bg); }
2414
+ /* Staged header compaction (responsive only, never persisted): compact
2415
+ stand-ins for the search input, the mode tabs, and the ancestor crumbs.
2416
+ All hidden at full width, so a roomy header renders exactly as before. */
2417
+ #searchBtn { position:relative; }
2418
+ #searchBtn.hasq::after { content:''; position:absolute; top:3px; right:3px; width:7px; height:7px; border-radius:50%; background:var(--accent); }
2419
+ .search-menu { min-width:210px; padding:8px; }
2420
+ .search-menu input[type="search"] { width:100%; }
2421
+ #modeMenu button.active { background:var(--accent); color:#fff; font-weight:700; }
2422
+ #crumbs .dropdown { display:inline-flex; }
2423
+ #crumbs .crumbmore { font-weight:700; }
2344
2424
 
2345
2425
  /* Settings panel \u2014 toggle switches */
2346
2426
  .settings-menu { min-width:266px; }
@@ -2464,9 +2544,17 @@ body.presentation #exitPresent, body.presentation #presentDetails { display:bloc
2464
2544
  <button data-vm="types">Types</button>
2465
2545
  <button data-vm="databases">Databases</button>
2466
2546
  </div>
2547
+ <div class="dropdown" id="modeDd" style="display:none">
2548
+ <button class="tbtn" id="modeBtn" title="Switch between the component architecture, the type ERD, or the database schemas">Components \u25BE</button>
2549
+ <div class="menu" id="modeMenu"></div>
2550
+ </div>
2467
2551
  <nav id="crumbs"></nav>
2468
2552
  <span class="divider"></span>
2469
2553
  <input id="search" type="search" placeholder="Search this view\u2026">
2554
+ <div class="dropdown" id="searchDd" style="display:none">
2555
+ <button class="tbtn" id="searchBtn" title="Search this view">\u{1F50D}</button>
2556
+ <div class="menu search-menu" id="searchMenu"></div>
2557
+ </div>
2470
2558
  <div class="seg" id="typesDetailSeg" style="display:none" title="ERD detail level">
2471
2559
  <button data-td="full">Full</button>
2472
2560
  <button data-td="fields">Fields</button>
@@ -2983,13 +3071,300 @@ var MODEL = __MODEL_JSON__;
2983
3071
  var BOX_W = 200, BOX_H = 56, SUBBOX_W = 230, SUBBOX_H = 84, GAP_X = 110, GAP_Y = 34;
2984
3072
  var INNER_W = 130, INNER_H = 36, INNER_GAPX = 26, INNER_GAPY = 12, HEAD_H = 34, PADI = 14;
2985
3073
 
3074
+ // ---- deep expansion (opt-in via subsystem.deepInternals) -------------------
3075
+ // When Internals is on and a subsystem record carries deepInternals:true, its
3076
+ // box renders its WHOLE subtree instead of one layer: deep-flagged child
3077
+ // subsystems become NESTED boundary boxes (recursing, defensively capped),
3078
+ // every other child renders as a fixed leaf tile that is never expanded
3079
+ // further. Sizes are computed bottom-up (a nested container tile takes its
3080
+ // recursive {w,h}); positions are emitted top-down by accumulating parent
3081
+ // top-left offsets. Relations between concrete visible endpoints are drawn
3082
+ // as ONE direct line each by buildDeepContext \u2014 crossing nested boundaries
3083
+ // on purpose (the full org overview of project relations); only relations
3084
+ // that cannot resolve to two concrete endpoints keep today's port machinery,
3085
+ // and ONLY at the outermost box. Without the flag this whole path is inert
3086
+ // and the classic one-layer innerLayout runs unchanged.
3087
+ var DEEP_MAX_DEPTH = 6;
3088
+ function isDeepId(subId) {
3089
+ var s = subById[subId];
3090
+ return !!(s && s.deepInternals);
3091
+ }
3092
+ // The DEEPEST visible tile representing compId inside the deep-expanded box
3093
+ // rooted at rootSubId: descend deep-flagged containers (the same expansion
3094
+ // rule as deepContainerLayout) until the containing child is a leaf tile.
3095
+ // Returns the child entry { kind, id } (its node id is IN(kind, id)).
3096
+ function deepLeafFor(compId, rootSubId) {
3097
+ var subId = rootSubId, depth = 1;
3098
+ for (;;) {
3099
+ var child = childOfScopeContaining(compId, { kind: 'subsystem', id: subId });
3100
+ if (!child) return null;
3101
+ if (child.kind === 'subsystem' && isDeepId(child.id) && depth < DEEP_MAX_DEPTH) {
3102
+ subId = child.id; depth += 1;
3103
+ continue;
3104
+ }
3105
+ return child;
3106
+ }
3107
+ }
3108
+ // One deep container's DIRECT children, placed with PER-TILE sizes (nested
3109
+ // containers take their recursive size; leaves stay INNER_W x INNER_H). The
3110
+ // placement mirrors innerLayout's strategy switch, generalised to variable
3111
+ // tile sizes. Tiles are centres relative to THIS container's top-left corner
3112
+ // (content sits right of PADI, below the HEAD_H label band); nested tiles
3113
+ // are relative to their own container, so emission accumulates offsets.
3114
+ function deepContainerLayout(subId, depth) {
3115
+ var kids = childrenOf({ kind: 'subsystem', id: subId });
3116
+ if (!kids.length) {
3117
+ // An EMPTY deep subsystem still shows as a (min-size) boundary box.
3118
+ return { tiles: [], w: INNER_W + 2 * PADI, h: HEAD_H + PADI };
3119
+ }
3120
+ var scope = { kind: 'subsystem', id: subId };
3121
+ var kidKey = function (k) { return k.kind + ':' + k.id; };
3122
+ var size = {};
3123
+ kids.forEach(function (k) {
3124
+ if (k.kind === 'subsystem' && isDeepId(k.id) && depth < DEEP_MAX_DEPTH) {
3125
+ var nested = deepContainerLayout(k.id, depth + 1);
3126
+ size[kidKey(k)] = { w: nested.w, h: nested.h, sub: nested };
3127
+ } else {
3128
+ size[kidKey(k)] = { w: INNER_W, h: INNER_H, sub: null };
3129
+ }
3130
+ });
3131
+ // Intra-container edges lifted to DIRECT children \u2014 for LAYOUT ONLY (the
3132
+ // drawn lines come from buildDeepContext's direct pass, never per level).
3133
+ var intra = {};
3134
+ MODEL.edges.forEach(function (edge) {
3135
+ var a = childOfScopeContaining(edge.from, scope);
3136
+ var b = childOfScopeContaining(edge.to, scope);
3137
+ if (!a || !b) return;
3138
+ var ak = a.kind + ':' + a.id, bk = b.kind + ':' + b.id;
3139
+ if (!size[ak] || !size[bk] || ak === bk) return;
3140
+ intra[ak + '=>' + bk] = 1;
3141
+ });
3142
+ var layer = {};
3143
+ function calc(k, stack) {
3144
+ var key = kidKey(k);
3145
+ if (layer[key] !== undefined) return layer[key];
3146
+ if (stack[key]) return 0;
3147
+ stack[key] = 1;
3148
+ var l = 0;
3149
+ if (k.kind === 'component') {
3150
+ var c = compById[k.id];
3151
+ if (c && (c.componentType === 'Portal' || c.componentType === 'Observer')) { layer[key] = 0; delete stack[key]; return 0; }
3152
+ }
3153
+ Object.keys(intra).forEach(function (ek) {
3154
+ var cut = ek.indexOf('=>');
3155
+ if (ek.slice(cut + 2) !== key) return;
3156
+ var srcKid = kids.filter(function (x) { return kidKey(x) === ek.slice(0, cut); })[0];
3157
+ if (srcKid) l = Math.max(l, calc(srcKid, stack) + 1);
3158
+ });
3159
+ delete stack[key];
3160
+ layer[key] = l;
3161
+ return l;
3162
+ }
3163
+ kids.forEach(function (k) { calc(k, {}); });
3164
+ var tiles = [], contentW = 0, contentH = 0;
3165
+ if (state.layout === 'grid') {
3166
+ // Row packing by ACTUAL tile size (the fixed per-column grid assumed
3167
+ // uniform tiles); the target row width follows the tile count, widened
3168
+ // to at least the widest single tile.
3169
+ var gsorted = kids.slice().sort(function (a, b) { return a.id < b.id ? -1 : 1; });
3170
+ var target = Math.max(1, Math.ceil(Math.sqrt(kids.length))) * (INNER_W + INNER_GAPX);
3171
+ kids.forEach(function (k) { var s0 = size[kidKey(k)]; if (s0.w > target) target = s0.w; });
3172
+ var gx = 0, gy = 0, rowH = 0;
3173
+ gsorted.forEach(function (k) {
3174
+ var s = size[kidKey(k)];
3175
+ if (gx > 0 && gx + s.w > target) { gx = 0; gy += rowH + INNER_GAPY; rowH = 0; }
3176
+ tiles.push({ kid: k, x: gx + s.w / 2, y: gy + s.h / 2, w: s.w, h: s.h, sub: s.sub });
3177
+ gx += s.w + INNER_GAPX;
3178
+ if (s.h > rowH) rowH = s.h;
3179
+ if (gx - INNER_GAPX > contentW) contentW = gx - INNER_GAPX;
3180
+ if (gy + rowH > contentH) contentH = gy + rowH;
3181
+ });
3182
+ } else if (state.layout === 'concentric' || state.layout === 'force') {
3183
+ var ideg = {};
3184
+ kids.forEach(function (k) { ideg[kidKey(k)] = 0; });
3185
+ Object.keys(intra).forEach(function (ek) {
3186
+ var cut2 = ek.indexOf('=>');
3187
+ var sk = ek.slice(0, cut2), tk = ek.slice(cut2 + 2);
3188
+ if (ideg[sk] !== undefined) ideg[sk]++;
3189
+ if (ideg[tk] !== undefined) ideg[tk]++;
3190
+ });
3191
+ var rel = concentricPositions(
3192
+ kids.map(kidKey),
3193
+ function (key) { return ideg[key] || 0; },
3194
+ function (key) { return { w: size[key].w, h: size[key].h }; }
3195
+ );
3196
+ // Normalise by the tiles' BOUNDING BOX (not just the centres) so a wide
3197
+ // nested container on the rim still clears the container's left/top pad.
3198
+ var minL = Infinity, minT = Infinity;
3199
+ kids.forEach(function (k) {
3200
+ var s1 = size[kidKey(k)], p1 = rel[kidKey(k)] || { x: 0, y: 0 };
3201
+ if (p1.x - s1.w / 2 < minL) minL = p1.x - s1.w / 2;
3202
+ if (p1.y - s1.h / 2 < minT) minT = p1.y - s1.h / 2;
3203
+ });
3204
+ if (minL === Infinity) { minL = 0; minT = 0; }
3205
+ kids.forEach(function (k) {
3206
+ var s2 = size[kidKey(k)], p2 = rel[kidKey(k)] || { x: 0, y: 0 };
3207
+ var cx = p2.x - minL, cyy = p2.y - minT;
3208
+ tiles.push({ kid: k, x: cx, y: cyy, w: s2.w, h: s2.h, sub: s2.sub });
3209
+ if (cx + s2.w / 2 > contentW) contentW = cx + s2.w / 2;
3210
+ if (cyy + s2.h / 2 > contentH) contentH = cyy + s2.h / 2;
3211
+ });
3212
+ } else {
3213
+ // Layered dependency columns: the column is as wide as its widest tile,
3214
+ // and each tile advances by ITS OWN height.
3215
+ var cols = {};
3216
+ kids.forEach(function (k) { var l = layer[kidKey(k)] || 0; (cols[l] = cols[l] || []).push(k); });
3217
+ var colKeys = Object.keys(cols).map(Number).sort(function (a, b) { return a - b; });
3218
+ var x = 0;
3219
+ colKeys.forEach(function (ck) {
3220
+ var col = cols[ck].sort(function (a, b) { return a.id < b.id ? -1 : 1; });
3221
+ var colW = 0, y = 0;
3222
+ col.forEach(function (k) { var s3 = size[kidKey(k)]; if (s3.w > colW) colW = s3.w; });
3223
+ col.forEach(function (k) {
3224
+ var s = size[kidKey(k)];
3225
+ tiles.push({ kid: k, x: x + colW / 2, y: y + s.h / 2, w: s.w, h: s.h, sub: s.sub });
3226
+ y += s.h + INNER_GAPY;
3227
+ });
3228
+ if (y - INNER_GAPY > contentH) contentH = y - INNER_GAPY;
3229
+ x += colW + INNER_GAPX;
3230
+ });
3231
+ contentW = x - INNER_GAPX;
3232
+ }
3233
+ tiles.forEach(function (t) { t.x += PADI; t.y += HEAD_H; });
3234
+ return { tiles: tiles, w: contentW + 2 * PADI, h: HEAD_H + contentH + PADI };
3235
+ }
3236
+ // Top-level deep box: the recursive interior plus today's port machinery at
3237
+ // the OUTERMOST box only (buildDeepContext supplies which relations still
3238
+ // need ports; stubs run port <-> the DEEPEST visible leaf tile). Returns the
3239
+ // same shape as innerLayout, plus deep:true so emission recurses.
3240
+ function deepLayout(entry, portRec) {
3241
+ var box = deepContainerLayout(entry.id, 1);
3242
+ var parentId = anchorNodeId(entry);
3243
+ var pBaseIn = 'p~in~' + parentId + '~', pBaseOut = 'p~out~' + parentId + '~';
3244
+ var extIn = portRec ? portRec.extIn : {}, extOut = portRec ? portRec.extOut : {};
3245
+ var inIds = Object.keys(extIn).sort(), outIds = Object.keys(extOut).sort();
3246
+ var hasIn = inIds.length > 0, hasOut = outIds.length > 0;
3247
+ var PROXY_W = 22, PROXY_H = 22, PROXY_GAP = 8;
3248
+ var shift = hasIn ? PROXY_W + INNER_GAPX : 0;
3249
+ var tiles = box.tiles;
3250
+ if (shift) tiles.forEach(function (t) { t.x += shift; });
3251
+ var w = box.w + shift + (hasOut ? PROXY_W + INNER_GAPX : 0);
3252
+ if (w < SUBBOX_W) w = SUBBOX_W;
3253
+ var stackMax = Math.max(inIds.length, outIds.length);
3254
+ var h = Math.max(box.h, HEAD_H + stackMax * PROXY_H + Math.max(0, stackMax - 1) * PROXY_GAP + PADI);
3255
+ var midY = HEAD_H + Math.max(0, (h - HEAD_H - PADI) / 2);
3256
+ function stackPorts(ids, recs, base, cx, dir) {
3257
+ var total = ids.length * PROXY_H + Math.max(0, ids.length - 1) * PROXY_GAP;
3258
+ var y0 = Math.max(HEAD_H + PROXY_H / 2, midY - total / 2 + PROXY_H / 2);
3259
+ return ids.map(function (eid, i) {
3260
+ var km = recs[eid].kids, klist = [];
3261
+ Object.keys(km).forEach(function (key) { klist.push(km[key]); });
3262
+ 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 };
3263
+ });
3264
+ }
3265
+ return {
3266
+ deep: true,
3267
+ tiles: tiles,
3268
+ edges: portRec ? Object.keys(portRec.stubs).map(function (k) { return portRec.stubs[k]; }) : [],
3269
+ proxies: stackPorts(inIds, extIn, pBaseIn, PADI + PROXY_W / 2, 'in')
3270
+ .concat(stackPorts(outIds, extOut, pBaseOut, w - PADI - PROXY_W / 2, 'out')),
3271
+ w: w,
3272
+ h: h,
3273
+ };
3274
+ }
3275
+ // View-level deep context (null unless Internals is on AND at least one
3276
+ // in-view entry is deep-flagged \u2014 the classic path never sees it): which
3277
+ // top-level entries are deep-expanded; every relation drawn as a DIRECT
3278
+ // concrete line (deduped per src=>tgt pair); the per-top-pair count used to
3279
+ // suppress aggregated edges whose constituents are ALL drawn directly; and
3280
+ // the port records for relations that keep today's port semantics.
3281
+ function buildDeepContext(scope, entries) {
3282
+ if (!state.internals) return null;
3283
+ var deepByAnchor = {}, any = false;
3284
+ entries.forEach(function (e) {
3285
+ if (e.kind === 'subsystem' && isDeepId(e.id)) { deepByAnchor[anchorNodeId(e)] = e; any = true; }
3286
+ });
3287
+ if (!any) return null;
3288
+ var entryByAnchor = {};
3289
+ entries.forEach(function (e) { entryByAnchor[e.kind + ':' + e.id] = e; });
3290
+ // A relation endpoint's DIRECT-line node in this view: the deepest leaf
3291
+ // tile inside a deep-expanded entry, or a top-level component box ITSELF.
3292
+ // null = this endpoint keeps aggregated/port semantics (interiors of
3293
+ // non-deep entries, out-of-scope counterparts).
3294
+ function directEnd(compId) {
3295
+ var child = childOfScopeContaining(compId, scope);
3296
+ var entry = child && entryByAnchor[child.kind + ':' + child.id];
3297
+ if (!entry) return null;
3298
+ var aid = anchorNodeId(entry);
3299
+ if (deepByAnchor[aid]) {
3300
+ var leaf = deepLeafFor(compId, entry.id);
3301
+ return leaf ? { node: IN(leaf.kind, leaf.id), top: aid, deep: true } : null;
3302
+ }
3303
+ if (entry.kind === 'component' && entry.id === compId) return { node: aid, top: aid, deep: false };
3304
+ return null;
3305
+ }
3306
+ var direct = {}, directTopCount = {}, ports = {};
3307
+ function portRec(aid) { return ports[aid] = ports[aid] || { extIn: {}, extOut: {}, stubs: {} }; }
3308
+ MODEL.edges.forEach(function (edge) {
3309
+ var a = directEnd(edge.from), b = directEnd(edge.to);
3310
+ if (a && b && (a.deep || b.deep) && a.node !== b.node) {
3311
+ // Drawn as ONE direct line \u2014 never ALSO as ports/stubs (dedupe rule).
3312
+ var key = a.node + '=>' + b.node;
3313
+ if (!direct[key]) direct[key] = { src: a.node, tgt: b.node, cross: false, aTop: a.top, bTop: b.top };
3314
+ if (edge.cross) direct[key].cross = true;
3315
+ if (a.top !== b.top) {
3316
+ var tk = a.top + '=>' + b.top;
3317
+ directTopCount[tk] = (directTopCount[tk] || 0) + 1;
3318
+ }
3319
+ return;
3320
+ }
3321
+ // Not a direct line: keep today's port semantics on any deep box with
3322
+ // exactly one endpoint inside its subtree, stubbed to the deepest leaf.
3323
+ var ac = childOfScopeContaining(edge.from, scope);
3324
+ var bc = childOfScopeContaining(edge.to, scope);
3325
+ var aEnt = ac && entryByAnchor[ac.kind + ':' + ac.id];
3326
+ var bEnt = bc && entryByAnchor[bc.kind + ':' + bc.id];
3327
+ var aAid = aEnt ? anchorNodeId(aEnt) : null;
3328
+ var bAid = bEnt ? anchorNodeId(bEnt) : null;
3329
+ if (aAid === bAid) return; // internal to one entry, or neither in scope
3330
+ if (aAid && deepByAnchor[aAid]) {
3331
+ var leafA = deepLeafFor(edge.from, aEnt.id);
3332
+ if (leafA) {
3333
+ var recA = portRec(aAid);
3334
+ var ro = recA.extOut[edge.to] = recA.extOut[edge.to] || { kids: {}, raws: {} };
3335
+ ro.kids[leafA.kind + ':' + leafA.id] = leafA;
3336
+ ro.raws[edge.from] = 1;
3337
+ var poId = 'p~out~' + aAid + '~' + edge.to;
3338
+ recA.stubs[IN(leafA.kind, leafA.id) + '=>' + poId] = { src: IN(leafA.kind, leafA.id), tgt: poId, stub: true };
3339
+ }
3340
+ }
3341
+ if (bAid && deepByAnchor[bAid]) {
3342
+ var leafB = deepLeafFor(edge.to, bEnt.id);
3343
+ if (leafB) {
3344
+ var recB = portRec(bAid);
3345
+ var ri = recB.extIn[edge.from] = recB.extIn[edge.from] || { kids: {}, raws: {} };
3346
+ ri.kids[leafB.kind + ':' + leafB.id] = leafB;
3347
+ ri.raws[edge.to] = 1;
3348
+ var piId = 'p~in~' + bAid + '~' + edge.from;
3349
+ recB.stubs[piId + '=>' + IN(leafB.kind, leafB.id)] = { src: piId, tgt: IN(leafB.kind, leafB.id), stub: true };
3350
+ }
3351
+ }
3352
+ });
3353
+ return { deepByAnchor: deepByAnchor, direct: direct, directTopCount: directTopCount, ports: ports };
3354
+ }
3355
+
2986
3356
  // Micro-layout for a container's direct children when Internals is on:
2987
3357
  // layered mini columns + intra-container edges. Each external relation gets
2988
3358
  // its own small PORT node INSIDE the container (one per external
2989
3359
  // counterpart; incoming left, outgoing right). Children connect to ports
2990
3360
  // with short edges that never leave the box \u2014 the real cross-boundary line
2991
3361
  // is only revealed on hover, or pinned while the port is selected.
2992
- function innerLayout(entry) {
3362
+ function innerLayout(entry, deepCtx) {
3363
+ // Deep-flagged subsystems take the recursive path (deepCtx exists only
3364
+ // when Internals is on and the view has deep entries \u2014 see buildElements).
3365
+ if (deepCtx && entry.kind === 'subsystem' && deepCtx.deepByAnchor[anchorNodeId(entry)]) {
3366
+ return deepLayout(entry, deepCtx.ports[anchorNodeId(entry)]);
3367
+ }
2993
3368
  var kids = state.internals && entry.hasKids ? childrenOf({ kind: entry.kind, id: entry.id }) : [];
2994
3369
  if (!kids.length) return null;
2995
3370
  var scope = { kind: entry.kind, id: entry.id };
@@ -3561,12 +3936,42 @@ var MODEL = __MODEL_JSON__;
3561
3936
  if (scope.kind === 'types' || scope.kind === 'databases') return buildTypeElements();
3562
3937
  var entries = childrenOf(scope);
3563
3938
  var eles = [];
3939
+ var deepCtx = buildDeepContext(scope, entries);
3564
3940
  var ve = viewEdges(scope, entries);
3565
3941
  // Data-coupling overlay: same scoping pipeline, a different edge source.
3566
3942
  var vd = state.dataCoupling ? viewEdges(scope, entries, MODEL.dataEdges) : { agg: {}, ghosts: {} };
3567
3943
  Object.keys(vd.ghosts).forEach(function (g) { if (!ve.ghosts[g]) ve.ghosts[g] = vd.ghosts[g]; });
3568
3944
  var inners = {};
3569
- entries.forEach(function (e) { inners[anchorNodeId(e)] = innerLayout(e); });
3945
+ entries.forEach(function (e) { inners[anchorNodeId(e)] = innerLayout(e, deepCtx); });
3946
+
3947
+ // Deep-mode recursive tile emission: a nested container becomes a cytoscape
3948
+ // compound parent (no explicit position \u2014 a compound derives its bounds
3949
+ // from its children); leaves and EMPTY containers are plain positioned
3950
+ // nodes. ox/oy = the emitting container's absolute top-left; tile.x/y are
3951
+ // centres relative to it, so offsets accumulate top-down. Search dimming
3952
+ // propagates the TOP entry's dim to the whole subtree.
3953
+ function emitDeepTiles(tiles, parentNodeId, ox, oy, dimCls) {
3954
+ tiles.forEach(function (tile) {
3955
+ var ax = ox + tile.x, ay = oy + tile.y;
3956
+ if (tile.sub) {
3957
+ var nid = SN(tile.kid.id);
3958
+ var selCls = state.selectedKind === 'subsystem' && state.selected === tile.kid.id ? ' sel' : '';
3959
+ var nested = {
3960
+ data: { id: nid, parent: parentNodeId, label: nameOf(tile.kid), w: tile.w, h: tile.h, tw: tile.w - 16 },
3961
+ classes: 'subsysBox' + (tile.sub.tiles.length ? ' drillable' : '') + dimCls + selCls,
3962
+ };
3963
+ if (!tile.sub.tiles.length) nested.position = { x: ax, y: ay };
3964
+ eles.push(nested);
3965
+ emitDeepTiles(tile.sub.tiles, nid, ax - tile.w / 2, ay - tile.h / 2, dimCls);
3966
+ } else {
3967
+ eles.push({
3968
+ 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 },
3969
+ position: { x: ax, y: ay },
3970
+ classes: 'inner' + dimCls,
3971
+ });
3972
+ }
3973
+ });
3974
+ }
3570
3975
 
3571
3976
  // Resolve a port's reveal target(s) in THIS view. Preference order: the
3572
3977
  // MATCHING PORT inside the counterpart's container (a port-to-port line
@@ -3698,17 +4103,25 @@ var MODEL = __MODEL_JSON__;
3698
4103
  + (state.showIssues && issuesBySpec[e.id] ? ' hasIssue' : '')
3699
4104
  + (state.selectedKind === e.kind && state.selected === e.id ? ' sel' : '');
3700
4105
  if (inner) {
3701
- 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 });
3702
- inner.tiles.forEach(function (tile) {
3703
- eles.push({
3704
- data: {
3705
- id: IN(tile.kid.kind, tile.kid.id), parent: aid,
3706
- label: nameOf(tile.kid), w: INNER_W, h: INNER_H, tw: INNER_W - 10,
3707
- },
3708
- position: { x: p.x - p.w / 2 + tile.x, y: p.y - p.h / 2 + tile.y },
3709
- classes: 'inner' + (dim ? ' dimmed' : ''),
4106
+ 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 };
4107
+ // An EMPTY deep boundary box has no children, so it is NOT a compound
4108
+ // parent \u2014 it needs (and honours) an explicit position and size.
4109
+ if (inner.deep && !inner.tiles.length && !(inner.proxies && inner.proxies.length)) boxNode.position = { x: p.x, y: p.y };
4110
+ eles.push(boxNode);
4111
+ if (inner.deep) {
4112
+ emitDeepTiles(inner.tiles, aid, p.x - p.w / 2, p.y - p.h / 2, dim ? ' dimmed' : '');
4113
+ } else {
4114
+ inner.tiles.forEach(function (tile) {
4115
+ eles.push({
4116
+ data: {
4117
+ id: IN(tile.kid.kind, tile.kid.id), parent: aid,
4118
+ label: nameOf(tile.kid), w: INNER_W, h: INNER_H, tw: INNER_W - 10,
4119
+ },
4120
+ position: { x: p.x - p.w / 2 + tile.x, y: p.y - p.h / 2 + tile.y },
4121
+ classes: 'inner' + (dim ? ' dimmed' : ''),
4122
+ });
3710
4123
  });
3711
- });
4124
+ }
3712
4125
  (inner.proxies || []).forEach(function (px) {
3713
4126
  eles.push({
3714
4127
  data: {
@@ -3861,9 +4274,28 @@ var MODEL = __MODEL_JSON__;
3861
4274
 
3862
4275
  var dimmedAnchors = {};
3863
4276
  entries.forEach(function (e) { if (state.query && !matches(e)) dimmedAnchors[anchorNodeId(e)] = true; });
4277
+
4278
+ // Deep mode: ONE direct line per related pair of concrete visible nodes \u2014
4279
+ // leaf tiles at any depth and/or top-level component boxes (deduped by
4280
+ // buildDeepContext). These lines cross nested boundaries on purpose.
4281
+ if (deepCtx) {
4282
+ var ddi = 0;
4283
+ Object.keys(deepCtx.direct).sort().forEach(function (key) {
4284
+ var d = deepCtx.direct[key];
4285
+ var ddim = state.query && (dimmedAnchors[d.aTop] || dimmedAnchors[d.bTop]);
4286
+ eles.push({
4287
+ data: { id: 'dd' + (ddi++), source: d.src, target: d.tgt, lbl: '' },
4288
+ classes: 'inneredge' + (d.cross ? ' cross' : '') + (ddim ? ' dimmed' : ''),
4289
+ });
4290
+ });
4291
+ }
4292
+
3864
4293
  var i = 0;
3865
4294
  Object.keys(ve.agg).forEach(function (key) {
3866
4295
  var e = ve.agg[key];
4296
+ // Prefer the leaf lines: drop an aggregated container edge whose
4297
+ // constituent relations were ALL drawn as direct deep lines above.
4298
+ if (deepCtx && deepCtx.directTopCount[key] >= e.n) return;
3867
4299
  var bundle = e.n > 1;
3868
4300
  var dim = state.query && (dimmedAnchors[e.src] || dimmedAnchors[e.tgt]);
3869
4301
  var route = routeData(e.src, e.tgt, key);
@@ -4122,22 +4554,58 @@ var MODEL = __MODEL_JSON__;
4122
4554
  }
4123
4555
  return path;
4124
4556
  }
4557
+ // Crumb compaction (compaction stage 5) is a RENDER MODE, not marker-based
4558
+ // reparenting: renderCrumbs rebuilds #crumbs' innerHTML on every navigation,
4559
+ // so nodes physically moved elsewhere would be destroyed by the next render.
4560
+ // The header-compaction stage toggles crumbsCompact and re-renders; compact
4561
+ // keeps the CURRENT scope visible and folds the ancestors into an ordered
4562
+ // "\\u2026" dropdown (document order, root first) whose entries navigate
4563
+ // exactly like the crumbs they replace.
4564
+ var crumbsCompact = false;
4565
+ var lastCrumbsHtml; // no initializer: the boot render at cy-init time precedes this line
4566
+ // Set by the header-compaction IIFE: crumb re-renders change the header's
4567
+ // CONTENT width without resizing #hdr itself (it is edge-anchored), so the
4568
+ // ResizeObserver never fires for them \u2014 renderCrumbs nudges a reflow here.
4569
+ var headerReflowHook = null;
4570
+ function crumbBtnHtml(p, cur) {
4571
+ return '<button class="crumb' + (cur ? ' cur' : '') + '" data-ck="' + p.kind + '" data-ci="' + (p.id || '') + '">' + p.label + '</button>';
4572
+ }
4125
4573
  function renderCrumbs() {
4126
4574
  var el = document.getElementById('crumbs');
4127
4575
  var path = crumbPath();
4128
- el.innerHTML = path.map(function (p, i) {
4129
- var cur = i === path.length - 1;
4130
- return '<button class="crumb' + (cur ? ' cur' : '') + '" data-ck="' + p.kind + '" data-ci="' + (p.id || '') + '">' + p.label + '</button>'
4131
- + (cur ? '' : '<span class="sep">\\u203A</span>');
4132
- }).join('');
4576
+ var html;
4577
+ if (crumbsCompact && path.length > 1) {
4578
+ html = '<span class="dropdown" id="crumbDd">'
4579
+ + '<button class="crumb crumbmore" id="crumbMoreBtn" title="Show the collapsed ancestor path">\\u2026</button>'
4580
+ + '<span class="menu" id="crumbMenu">'
4581
+ + path.slice(0, path.length - 1).map(function (p) {
4582
+ return '<button data-ck="' + p.kind + '" data-ci="' + (p.id || '') + '">' + p.label + '</button>';
4583
+ }).join('')
4584
+ + '</span></span>'
4585
+ + '<span class="sep">\\u203A</span>'
4586
+ + crumbBtnHtml(path[path.length - 1], true);
4587
+ } else {
4588
+ html = path.map(function (p, i) {
4589
+ var cur = i === path.length - 1;
4590
+ return crumbBtnHtml(p, cur) + (cur ? '' : '<span class="sep">\\u203A</span>');
4591
+ }).join('');
4592
+ }
4593
+ // No-op renders keep the already-wired nodes (and an open "\\u2026" menu)
4594
+ // intact \u2014 and don't churn the reflow scheduler while a search query types.
4595
+ if (html === lastCrumbsHtml) return;
4596
+ lastCrumbsHtml = html;
4597
+ el.innerHTML = html;
4133
4598
  var btns = el.querySelectorAll('button');
4134
4599
  for (var i = 0; i < btns.length; i++) {
4135
4600
  (function (b) {
4601
+ if (!b.getAttribute('data-ck')) return; // the "\\u2026" trigger toggles, never navigates
4136
4602
  b.addEventListener('click', function () {
4137
4603
  navigateTo(b.getAttribute('data-ck'), b.getAttribute('data-ci') || null);
4138
4604
  });
4139
4605
  })(btns[i]);
4140
4606
  }
4607
+ if (crumbsCompact && path.length > 1) wireDropdown('crumbDd', 'crumbMoreBtn');
4608
+ if (headerReflowHook) headerReflowHook();
4141
4609
  }
4142
4610
  function renderViewHint() {
4143
4611
  if (state.view.kind === 'types' || state.view.kind === 'databases') {
@@ -4404,7 +4872,16 @@ var MODEL = __MODEL_JSON__;
4404
4872
  return c ? { kind: 'subsystem', id: c.subsystem } : { kind: 'system', id: null };
4405
4873
  }
4406
4874
 
4407
- document.getElementById('search').addEventListener('input', function (ev) { state.query = ev.target.value.trim(); rebuild(false); });
4875
+ // While the search input is compacted behind the magnifier icon (compaction
4876
+ // stage 3), a non-empty query must stay discoverable \u2014 mark the icon with an
4877
+ // accent dot. The class is kept in sync on every query edit; the dot is only
4878
+ // ever visible while the compact icon itself is.
4879
+ function updateSearchBadge() {
4880
+ var b = document.getElementById('searchBtn');
4881
+ if (b && b.classList) b.classList[state.query ? 'add' : 'remove']('hasq');
4882
+ }
4883
+ document.getElementById('search').addEventListener('input', function (ev) { state.query = ev.target.value.trim(); updateSearchBadge(); rebuild(false); });
4884
+ updateSearchBadge();
4408
4885
  // Sync each View toggle's checkbox from the (possibly persisted) state, then
4409
4886
  // persist on change so the choices survive a refresh (see persist()/saved).
4410
4887
  document.getElementById('internalsToggle').checked = state.internals;
@@ -4458,9 +4935,24 @@ var MODEL = __MODEL_JSON__;
4458
4935
  })(btns[i]);
4459
4936
  }
4460
4937
  })();
4938
+ // Compaction stage 4 replaces the mode tabs with one dropdown trigger; its
4939
+ // label must follow the CURRENT mode. updateHeaderSegs runs on every rebuild,
4940
+ // so a mode change made while compact re-labels the trigger immediately.
4941
+ function updateModeBtn() {
4942
+ var b = document.getElementById('modeBtn');
4943
+ if (!b) return;
4944
+ var lbl = state.view.kind === 'types' ? 'Types' : state.view.kind === 'databases' ? 'Databases' : 'Components';
4945
+ b.textContent = lbl + ' \\u25BE';
4946
+ }
4461
4947
  function updateHeaderSegs() {
4462
4948
  var seg = document.getElementById('modeSeg');
4463
4949
  var btns = seg.querySelectorAll('button');
4950
+ if (!btns.length) {
4951
+ // Compaction stage 4 moved the real tab buttons into the mode dropdown \u2014
4952
+ // keep driving THEIR active classes there (they move back node-identical).
4953
+ var mm = document.getElementById('modeMenu');
4954
+ if (mm && mm.querySelectorAll) btns = mm.querySelectorAll('button');
4955
+ }
4464
4956
  for (var i = 0; i < btns.length; i++) {
4465
4957
  var vm = btns[i].getAttribute('data-vm');
4466
4958
  var active = vm === 'components'
@@ -4468,6 +4960,7 @@ var MODEL = __MODEL_JSON__;
4468
4960
  : vm === state.view.kind;
4469
4961
  if (btns[i].classList) btns[i].classList[active ? 'add' : 'remove']('active');
4470
4962
  }
4963
+ updateModeBtn();
4471
4964
  var td = document.getElementById('typesDetailSeg');
4472
4965
  td.style.display = (state.view.kind === 'types' || state.view.kind === 'databases') ? '' : 'none';
4473
4966
  var tbs = td.querySelectorAll('button');
@@ -4531,7 +5024,13 @@ var MODEL = __MODEL_JSON__;
4531
5024
  var r = btn.getBoundingClientRect();
4532
5025
  menu.style.top = (r.bottom + 6) + 'px';
4533
5026
  menu.style.left = 'auto';
4534
- menu.style.right = Math.max(6, window.innerWidth - r.right) + 'px';
5027
+ // Right-aligned to the trigger, but never pushed off the LEFT edge \u2014 the
5028
+ // compact search / crumb triggers (header compaction) sit on the header's
5029
+ // left side, where a 200px menu right-aligned to a narrow button would clip.
5030
+ var right = Math.max(6, window.innerWidth - r.right);
5031
+ var mw = menu.getBoundingClientRect ? menu.getBoundingClientRect().width : 0;
5032
+ if (mw && window.innerWidth - right - mw < 6) right = Math.max(6, window.innerWidth - mw - 6);
5033
+ menu.style.right = right + 'px';
4535
5034
  }
4536
5035
  function wireDropdown(ddId, btnId) {
4537
5036
  var dd = document.getElementById(ddId);
@@ -4550,11 +5049,29 @@ var MODEL = __MODEL_JSON__;
4550
5049
  var ldd = wireDropdown('layoutDd', 'layoutBtn');
4551
5050
  var sdd = wireDropdown('settingsDd', 'settingsBtn');
4552
5051
  var mdd = wireDropdown('moreDd', 'moreBtn');
5052
+ // Compact stand-ins (header compaction stages 3-4): the search panel and the
5053
+ // mode-tab dropdown are ordinary dropdowns; their triggers stay hidden until
5054
+ // their compaction stage shows them, so wiring them here is inert at full width.
5055
+ var qdd = wireDropdown('searchDd', 'searchBtn');
5056
+ var vdd = wireDropdown('modeDd', 'modeBtn');
5057
+ // Opening the compact search panel focuses the REAL input (stage 3 moves the
5058
+ // node, never clones it, so its input listener keeps driving state.query).
5059
+ // Registered after wireDropdown's toggle, so 'open' reflects the new state.
5060
+ document.getElementById('searchBtn').addEventListener('click', function () {
5061
+ if (String(qdd.className || '').indexOf('open') >= 0) {
5062
+ var inp = document.getElementById('search');
5063
+ if (inp && inp.focus) inp.focus();
5064
+ }
5065
+ });
4553
5066
  // Keep the settings panel open while flipping switches (clicks inside it don't
4554
5067
  // bubble to the document-level close handler).
4555
5068
  (function () {
4556
5069
  var m = document.getElementById('settingsMenu');
4557
5070
  if (m && m.addEventListener) m.addEventListener('click', function (ev) { if (ev && ev.stopPropagation) ev.stopPropagation(); });
5071
+ // Same for the floating search panel: clicking into the input must not
5072
+ // bubble to the document-level close handler and shut the panel mid-typing.
5073
+ var sm = document.getElementById('searchMenu');
5074
+ if (sm && sm.addEventListener) sm.addEventListener('click', function (ev) { if (ev && ev.stopPropagation) ev.stopPropagation(); });
4558
5075
  })();
4559
5076
 
4560
5077
  // Layout picker: choose the auto-layout algorithm. Components use cytoscape's
@@ -4578,6 +5095,12 @@ var MODEL = __MODEL_JSON__;
4578
5095
  });
4579
5096
  updateLayoutBtn();
4580
5097
 
5098
+ // The compact crumb dropdown (compaction stage 5) is re-created by every
5099
+ // compact crumb render, so it is looked up per close instead of captured.
5100
+ function closeCrumbDd() {
5101
+ var cdd = document.getElementById('crumbDd');
5102
+ if (cdd && cdd.classList) cdd.classList.remove('open');
5103
+ }
4581
5104
  if (document.addEventListener) {
4582
5105
  document.addEventListener('click', function () {
4583
5106
  if (dd.classList) dd.classList.remove('open');
@@ -4585,6 +5108,9 @@ var MODEL = __MODEL_JSON__;
4585
5108
  if (ldd.classList) ldd.classList.remove('open');
4586
5109
  if (sdd.classList) sdd.classList.remove('open');
4587
5110
  if (mdd && mdd.classList) mdd.classList.remove('open');
5111
+ if (qdd && qdd.classList) qdd.classList.remove('open');
5112
+ if (vdd && vdd.classList) vdd.classList.remove('open');
5113
+ closeCrumbDd();
4588
5114
  });
4589
5115
  document.addEventListener('keydown', function (ev) {
4590
5116
  if (ev.key === 'Escape') {
@@ -4592,16 +5118,27 @@ var MODEL = __MODEL_JSON__;
4592
5118
  if (modal.classList && String(modal.className).indexOf('open') >= 0) { closeFlow(); return; }
4593
5119
  setPresentation(false);
4594
5120
  if (dd.classList) dd.classList.remove('open');
5121
+ if (qdd && qdd.classList) qdd.classList.remove('open');
5122
+ if (vdd && vdd.classList) vdd.classList.remove('open');
5123
+ closeCrumbDd();
4595
5124
  }
4596
5125
  });
4597
5126
  }
4598
5127
 
4599
- // \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
4600
- // When the floating header no longer fits its controls, trailing items
4601
- // COLLAPSE into the More menu instead of relying on horizontal scroll \u2014
4602
- // every control stays one click away. Whole items move (listeners survive
4603
- // reparenting); a hidden placeholder pins each item's original position so
4604
- // restoring keeps the exact order. Collapse order = least-used first.
5128
+ // \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
5129
+ // When the floating header no longer fits its controls, standard reusable
5130
+ // COMPACTION BEHAVIORS apply progressively \u2014 each stage only while the row
5131
+ // still overflows \u2014 and restore in REVERSE order when space returns:
5132
+ // 1. trailing buttons fold into the "\u22EF" menu (least-used first, one by one)
5133
+ // 2. the View dropdown folds in after them
5134
+ // 3. the search input compacts to a \u{1F50D} icon + floating panel
5135
+ // 4. the mode tabs compact to one current-mode dropdown
5136
+ // 5. ancestor crumbs compact into an ordered "\u2026" dropdown
5137
+ // Stages 1-2 move whole items (listeners survive reparenting) with a hidden
5138
+ // placeholder pinning each item's original spot for restore; stage 5 is a
5139
+ // render mode (renderCrumbs rebuilds its innerHTML, so reparenting would not
5140
+ // survive navigation). Nothing here is persisted \u2014 compaction is purely
5141
+ // responsive to the available width.
4605
5142
  (function () {
4606
5143
  if (typeof window === 'undefined') return;
4607
5144
  var hdr = document.getElementById('hdr');
@@ -4629,7 +5166,122 @@ var MODEL = __MODEL_JSON__;
4629
5166
  }
4630
5167
  return markers[id];
4631
5168
  }
4632
- var collapsed = [];
5169
+ // Fold stage (the classic behavior): move items into the "\u22EF" menu ONE per
5170
+ // apply() call \u2014 the reflow loop keeps a stage active until it reports no
5171
+ // further progress, preserving the original per-button granularity.
5172
+ function foldStage(ids) {
5173
+ var folded = [];
5174
+ return {
5175
+ apply: function () {
5176
+ while (folded.length < ids.length) {
5177
+ var id = ids[folded.length];
5178
+ var el = movableFor(id);
5179
+ if (!el || el === moreDd || el.parentNode === moreMenu) { folded.push({ el: null, marker: null }); continue; }
5180
+ var m = markerFor(id, el);
5181
+ // A dropdown moved while open would strand its fixed-positioned menu.
5182
+ if (el.classList) el.classList.remove('open');
5183
+ moreDd.style.display = '';
5184
+ moreMenu.appendChild(el);
5185
+ folded.push({ el: el, marker: m });
5186
+ return true;
5187
+ }
5188
+ return false;
5189
+ },
5190
+ restore: function () {
5191
+ for (var i = folded.length - 1; i >= 0; i--) {
5192
+ var it = folded[i];
5193
+ if (it.el && it.marker && it.marker.parentNode) it.marker.parentNode.insertBefore(it.el, it.marker);
5194
+ }
5195
+ folded = [];
5196
+ },
5197
+ };
5198
+ }
5199
+ // Stage 3: the search input compacts behind a \u{1F50D} icon; the REAL input node
5200
+ // MOVES into the floating panel (fixed-positioned by the same helper as
5201
+ // every dropdown menu), so its input listener keeps driving state.query.
5202
+ function searchStage() {
5203
+ var on = false;
5204
+ return {
5205
+ apply: function () {
5206
+ if (on) return false;
5207
+ var inp = document.getElementById('search');
5208
+ var ddw = document.getElementById('searchDd');
5209
+ var menu = document.getElementById('searchMenu');
5210
+ if (!inp || !ddw || !menu) return false;
5211
+ menu.appendChild(inp);
5212
+ ddw.style.display = '';
5213
+ on = true;
5214
+ return true;
5215
+ },
5216
+ restore: function () {
5217
+ if (!on) return;
5218
+ on = false;
5219
+ var inp = document.getElementById('search');
5220
+ var ddw = document.getElementById('searchDd');
5221
+ if (inp && ddw && ddw.parentNode) ddw.parentNode.insertBefore(inp, ddw);
5222
+ if (ddw) { ddw.style.display = 'none'; if (ddw.classList) ddw.classList.remove('open'); }
5223
+ },
5224
+ };
5225
+ }
5226
+ // Stage 4: the mode tabs collapse into ONE dropdown labelled with the
5227
+ // current mode. The REAL tab buttons move into its menu (listeners and
5228
+ // active styling survive); updateHeaderSegs keeps the trigger label in
5229
+ // sync when the mode changes while compact.
5230
+ function modeStage() {
5231
+ var moved = [];
5232
+ return {
5233
+ apply: function () {
5234
+ if (moved.length) return false;
5235
+ var seg = document.getElementById('modeSeg');
5236
+ var ddw = document.getElementById('modeDd');
5237
+ var menu = document.getElementById('modeMenu');
5238
+ if (!seg || !ddw || !menu) return false;
5239
+ var btns = seg.querySelectorAll('button');
5240
+ if (!btns.length) return false;
5241
+ for (var i = 0; i < btns.length; i++) moved.push(btns[i]);
5242
+ for (var j = 0; j < moved.length; j++) menu.appendChild(moved[j]);
5243
+ seg.style.display = 'none';
5244
+ ddw.style.display = '';
5245
+ updateModeBtn();
5246
+ return true;
5247
+ },
5248
+ restore: function () {
5249
+ if (!moved.length) return;
5250
+ var seg = document.getElementById('modeSeg');
5251
+ var ddw = document.getElementById('modeDd');
5252
+ for (var i = 0; i < moved.length; i++) seg.appendChild(moved[i]);
5253
+ moved = [];
5254
+ seg.style.display = '';
5255
+ if (ddw) { ddw.style.display = 'none'; if (ddw.classList) ddw.classList.remove('open'); }
5256
+ },
5257
+ };
5258
+ }
5259
+ // Stage 5: crumb compaction is a render-mode toggle consulted by
5260
+ // renderCrumbs itself \u2014 see crumbsCompact there. Never reparenting.
5261
+ function crumbStage() {
5262
+ return {
5263
+ apply: function () {
5264
+ if (crumbsCompact) return false;
5265
+ crumbsCompact = true;
5266
+ renderCrumbs();
5267
+ return true;
5268
+ },
5269
+ restore: function () {
5270
+ if (!crumbsCompact) return;
5271
+ crumbsCompact = false;
5272
+ renderCrumbs();
5273
+ },
5274
+ };
5275
+ }
5276
+ // Ordered compaction stages: applied first-to-last only while the header
5277
+ // overflows, restored last-to-first when space returns.
5278
+ var STAGES = [
5279
+ foldStage(COLLAPSE), // 1: trailing buttons \u2192 "\u22EF" menu
5280
+ foldStage(['settingsBtn']), // 2: the View dropdown folds in too
5281
+ searchStage(), // 3: search input \u2192 \u{1F50D} + floating panel
5282
+ modeStage(), // 4: mode tabs \u2192 current-mode dropdown
5283
+ crumbStage(), // 5: ancestor crumbs \u2192 "\u2026" dropdown
5284
+ ];
4633
5285
  // Signed fit measure in px: positive = overflowing, negative = headroom.
4634
5286
  // The header is a flex row whose ONLY flex:1 child is the .spacer, so the
4635
5287
  // spacer's rendered width IS the free space -- it grows to absorb all slack
@@ -4645,33 +5297,54 @@ var MODEL = __MODEL_JSON__;
4645
5297
  var slack = spacer ? spacer.getBoundingClientRect().width : 0;
4646
5298
  return (hdr.scrollWidth - hdr.clientWidth) - slack;
4647
5299
  }
5300
+ var inReflow = false;
4648
5301
  function reflow() {
4649
5302
  // Not laid out (hidden tab, non-browser DOM) \u2014 measuring would misfire.
4650
5303
  var box = hdr.getBoundingClientRect();
4651
5304
  if (!box || box.width <= 0) return;
4652
- // Restore everything, then collapse until the row fits (idempotent).
4653
- for (var i = collapsed.length - 1; i >= 0; i--) {
4654
- var it = collapsed[i];
4655
- if (it.el && it.marker && it.marker.parentNode) it.marker.parentNode.insertBefore(it.el, it.marker);
4656
- }
4657
- collapsed = [];
4658
- moreDd.style.display = 'none';
4659
- hdr.scrollLeft = 0;
4660
- var guard = 0;
4661
- // Demand a few px of headroom, not a bare fit \u2014 the marginal-fit widths
4662
- // are exactly where the phantom scrollbar appeared.
4663
- while (overflowPx() > -8 && guard < COLLAPSE.length) {
4664
- var id = COLLAPSE[guard++];
4665
- var el = movableFor(id);
4666
- if (!el || el === moreDd || el.parentNode === moreMenu) continue;
4667
- var m = markerFor(id, el);
4668
- // A dropdown moved while open would strand its fixed-positioned menu.
4669
- if (el.classList) el.classList.remove('open');
4670
- moreDd.style.display = '';
4671
- moreMenu.appendChild(el);
4672
- collapsed.push({ el: el, marker: m });
5305
+ inReflow = true;
5306
+ try {
5307
+ // The floating search panel must survive a reflow cycle: restore-all
5308
+ // would close it (and reparenting blurs the input), so capture its
5309
+ // open/focus state up front and reinstate it after the stage walk.
5310
+ var ddw = document.getElementById('searchDd');
5311
+ var inp = document.getElementById('search');
5312
+ var searchOpen = !!(ddw && String(ddw.className || '').indexOf('open') >= 0);
5313
+ var searchFocus = false;
5314
+ try {
5315
+ var ae = (typeof ROOT !== 'undefined' && ROOT ? ROOT : document).activeElement;
5316
+ searchFocus = !!(ae && inp && ae === inp);
5317
+ } catch (e) { /* stubbed DOM */ }
5318
+ // Restore every stage in REVERSE order, then re-apply progressively
5319
+ // while the row still overflows (idempotent).
5320
+ for (var i = STAGES.length - 1; i >= 0; i--) STAGES[i].restore();
5321
+ moreDd.style.display = 'none';
5322
+ hdr.scrollLeft = 0;
5323
+ // Demand a few px of headroom, not a bare fit \u2014 the marginal-fit widths
5324
+ // are exactly where the phantom scrollbar appeared. Every apply()
5325
+ // changes the very widths being measured, but restore-all + a strictly
5326
+ // forward stage walk make the outcome a pure function of the current
5327
+ // width, and reflow never reschedules itself (renderCrumbs' nudge is
5328
+ // suppressed via inReflow, and #hdr's own box never changes here), so
5329
+ // boundary widths settle in ONE pass instead of oscillating.
5330
+ var si = 0;
5331
+ var guard = 0;
5332
+ var bound = COLLAPSE.length + STAGES.length + 8;
5333
+ while (overflowPx() > -8 && si < STAGES.length && guard < bound) {
5334
+ guard++;
5335
+ if (!STAGES[si].apply()) si++;
5336
+ }
5337
+ if (searchOpen || searchFocus) {
5338
+ var compactNow = ddw && ddw.style && ddw.style.display !== 'none';
5339
+ if (compactNow && searchOpen) {
5340
+ if (ddw.classList) ddw.classList.add('open');
5341
+ positionDropdownMenu(ddw, document.getElementById('searchBtn'));
5342
+ }
5343
+ if (searchFocus && inp && inp.focus) inp.focus();
5344
+ }
5345
+ } finally {
5346
+ inReflow = false;
4673
5347
  }
4674
- if (collapsed.length === 0) moreDd.style.display = 'none';
4675
5348
  }
4676
5349
  var raf = null;
4677
5350
  var defer = window.requestAnimationFrame
@@ -4681,6 +5354,9 @@ var MODEL = __MODEL_JSON__;
4681
5354
  if (raf !== null) return;
4682
5355
  raf = defer(function () { raf = null; reflow(); });
4683
5356
  }
5357
+ // Crumb re-renders change the header's content width without resizing #hdr
5358
+ // itself \u2014 renderCrumbs nudges a reflow through this hook (no-op mid-reflow).
5359
+ headerReflowHook = function () { if (!inReflow) schedule(); };
4684
5360
  if (typeof ResizeObserver !== 'undefined') {
4685
5361
  new ResizeObserver(schedule).observe(hdr);
4686
5362
  } else if (window.addEventListener) {
@@ -6011,7 +6687,7 @@ var init_extensions = __esm({
6011
6687
  });
6012
6688
 
6013
6689
  // src/core/rules/types.ts
6014
- var BUILTIN_PROFILES;
6690
+ var BUILTIN_PROFILES, PROJECT_KINDS;
6015
6691
  var init_types = __esm({
6016
6692
  "src/core/rules/types.ts"() {
6017
6693
  "use strict";
@@ -6024,6 +6700,7 @@ var init_types = __esm({
6024
6700
  "realtime-embedded",
6025
6701
  "plc-cyclic"
6026
6702
  ];
6703
+ PROJECT_KINDS = ["fullstack", "system-of-systems", "monorepo"];
6027
6704
  }
6028
6705
  });
6029
6706
 
@@ -6204,44 +6881,13 @@ var init_type_references = __esm({
6204
6881
  }
6205
6882
  });
6206
6883
 
6207
- // src/core/statehash.ts
6208
- function computeStateId() {
6209
- const tree = {
6210
- system: loadSystemSpec(),
6211
- subsystems: loadSubsystemSpecs(),
6212
- components: loadComponentSpecs(),
6213
- interfaces: loadInterfaceSpecs(),
6214
- implementations: loadImplementationSpecs(),
6215
- types: loadTypeSpecs()
6216
- };
6217
- const digest = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
6218
- return { algorithm: "sha256", digest };
6884
+ // src/utils/filenames.ts
6885
+ function safeFilenamePart(value) {
6886
+ return value.replace(/[^a-zA-Z0-9._-]/g, "-");
6219
6887
  }
6220
- function stateIdEquals(a, b) {
6221
- return !!a && !!b && a.algorithm === b.algorithm && a.digest === b.digest;
6222
- }
6223
- function canonicalize(value) {
6224
- return JSON.stringify(sortKeys(value));
6225
- }
6226
- function sortKeys(v) {
6227
- if (Array.isArray(v)) return v.map(sortKeys);
6228
- if (v && typeof v === "object") {
6229
- const src = v;
6230
- const out = {};
6231
- for (const k of Object.keys(src).sort()) {
6232
- if (k === "createdAt" || k === "updatedAt") continue;
6233
- out[k] = sortKeys(src[k]);
6234
- }
6235
- return out;
6236
- }
6237
- return v;
6238
- }
6239
- var crypto;
6240
- var init_statehash = __esm({
6241
- "src/core/statehash.ts"() {
6888
+ var init_filenames = __esm({
6889
+ "src/utils/filenames.ts"() {
6242
6890
  "use strict";
6243
- crypto = __toESM(require("crypto"));
6244
- init_specs2();
6245
6891
  }
6246
6892
  });
6247
6893
 
@@ -6702,6 +7348,66 @@ function projectOwnSurface(maxAudience) {
6702
7348
  function projectChildSurface() {
6703
7349
  return projectOwnSurface("project");
6704
7350
  }
7351
+ function localName(id) {
7352
+ return id.split("::").pop();
7353
+ }
7354
+ function projectSubsystemSurface(subsystemId) {
7355
+ const system = loadSystemSpec();
7356
+ if (!system) {
7357
+ throw new Error("Cannot project a subsystem surface: the L0 system spec is missing.");
7358
+ }
7359
+ const subsystems = loadSubsystemSpecs();
7360
+ const target = subsystems.find((s) => s.id === subsystemId);
7361
+ if (!target) {
7362
+ throw new Error(`Cannot project a subsystem surface: subsystem "${subsystemId}" does not exist.`);
7363
+ }
7364
+ const components = loadComponentSpecs();
7365
+ const interfaces = loadInterfaceSpecs();
7366
+ const types = loadTypeSpecs();
7367
+ const entries = [];
7368
+ const unprojectable = [];
7369
+ for (const pub of target.publicInterfaces ?? []) {
7370
+ if (!pub.component) continue;
7371
+ const comp = components.find((c) => c.id === pub.component || c.id === `${subsystemId}::${pub.component}`);
7372
+ if (!comp) continue;
7373
+ if (!CROSS_BOUNDARY_TARGETS.has(comp.componentType)) {
7374
+ unprojectable.push({ component: pub.component, componentType: comp.componentType });
7375
+ continue;
7376
+ }
7377
+ const compInterfaces = interfaces.filter((i) => i.component === comp.id && (!pub.interface || i.id === pub.interface || i.id === `${subsystemId}::${pub.interface}`));
7378
+ const methods = compInterfaces.flatMap((i) => i.methods);
7379
+ entries.push({
7380
+ id: localName(pub.interface ?? comp.id),
7381
+ name: comp.name,
7382
+ // Family ceiling: a sibling surface is consumable by the system family only.
7383
+ audience: "project",
7384
+ type: pub.type ?? "Custom",
7385
+ // The snapshot carries the LOCAL portal name — consumers resolve cross-tree
7386
+ // refs by their final segment.
7387
+ component: localName(comp.id),
7388
+ methods,
7389
+ ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
7390
+ // Project the backing component's auth + basePath so the codec can emit
7391
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
7392
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
7393
+ ...comp.basePath ? { basePath: comp.basePath } : {},
7394
+ details: pub.details ?? ""
7395
+ });
7396
+ }
7397
+ for (const skipped of unprojectable) {
7398
+ console.error(
7399
+ `[surfaces] skipped "${subsystemId}::${skipped.component}": a published ${skipped.componentType} can never serve a cross-boundary caller, so it stays out of every chained child's sibling surface \u2014 publish this surface through a Portal, a Gateway, or an Observer (for events).`
7400
+ );
7401
+ }
7402
+ return SurfaceSnapshotSchema.parse({
7403
+ projectName: `${system.name}::${subsystemId}`,
7404
+ origin: "generated",
7405
+ stateId: stateIdString(),
7406
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
7407
+ interfaces: entries,
7408
+ types: computeTypeClosure(entries, types)
7409
+ });
7410
+ }
6705
7411
  function listSnapshots(rootDir = getProjectRoot()) {
6706
7412
  const dir = surfacesDir(rootDir);
6707
7413
  if (!fs4.existsSync(dir)) return [];
@@ -6718,45 +7424,88 @@ function listSnapshots(rootDir = getProjectRoot()) {
6718
7424
  function getSnapshot(projectName, rootDir = getProjectRoot()) {
6719
7425
  return listSnapshots(rootDir).find((s) => s.projectName === projectName) ?? null;
6720
7426
  }
7427
+ function snapshotFilename(projectName) {
7428
+ return `${safeFilenamePart(projectName)}.yaml`;
7429
+ }
6721
7430
  function saveSnapshot(snapshot, rootDir = getProjectRoot()) {
6722
7431
  const dir = surfacesDir(rootDir);
6723
7432
  fs4.mkdirSync(dir, { recursive: true });
6724
- const p = path5.join(dir, `${snapshot.projectName}.yaml`);
7433
+ const p = path5.join(dir, snapshotFilename(snapshot.projectName));
6725
7434
  writeYamlFile(p, SurfaceSnapshotSchema.parse(snapshot));
6726
7435
  return p;
6727
7436
  }
6728
7437
  function removeSnapshot(projectName, rootDir = getProjectRoot()) {
6729
- const p = path5.join(surfacesDir(rootDir), `${projectName}.yaml`);
6730
- if (!fs4.existsSync(p)) return false;
6731
- fs4.unlinkSync(p);
6732
- return true;
7438
+ const dir = surfacesDir(rootDir);
7439
+ const direct = path5.join(dir, snapshotFilename(projectName));
7440
+ if (fs4.existsSync(direct)) {
7441
+ fs4.unlinkSync(direct);
7442
+ return true;
7443
+ }
7444
+ if (!fs4.existsSync(dir)) return false;
7445
+ for (const file of fs4.readdirSync(dir)) {
7446
+ if (!file.endsWith(".yaml") && !file.endsWith(".yml")) continue;
7447
+ const p = path5.join(dir, file);
7448
+ try {
7449
+ const snap = SurfaceSnapshotSchema.parse(readYamlFile(p));
7450
+ if (snap.projectName === projectName) {
7451
+ fs4.unlinkSync(p);
7452
+ return true;
7453
+ }
7454
+ } catch {
7455
+ }
7456
+ }
7457
+ return false;
6733
7458
  }
6734
7459
  function loadSurfaceSnapshots() {
6735
7460
  return listSnapshots();
6736
7461
  }
6737
- function renderOpenApiForms(snapshot) {
6738
- const renderedSet = toOpenApiSet(snapshot);
6739
- return { renderedSet, ...renderedSet.length === 1 ? { rendered: renderedSet[0].document } : {} };
7462
+ function selectPortalSpec(renderedSet, portalId) {
7463
+ const hit = renderedSet.find((spec) => spec.portalId === portalId);
7464
+ if (!hit) {
7465
+ const known = renderedSet.map((s) => s.portalId).join(", ");
7466
+ throw new Error(`Unknown portal "${portalId}" \u2014 this surface renders: ${known || "(no portals)"}.`);
7467
+ }
7468
+ return [hit];
7469
+ }
7470
+ function perPortalPath(resolvedOut, portalId) {
7471
+ const ext = path5.extname(resolvedOut);
7472
+ const stem = ext ? resolvedOut.slice(0, -ext.length) : resolvedOut;
7473
+ return `${stem}.${safeFilenamePart(portalId)}${ext}`;
6740
7474
  }
6741
- function writeSurfaceFile(outPath, body, snapshot) {
7475
+ function writeSurfaceFile(outPath, snapshot, renderedSet) {
6742
7476
  const resolved = path5.resolve(outPath);
6743
7477
  fs4.mkdirSync(path5.dirname(resolved), { recursive: true });
6744
- if (body !== void 0) fs4.writeFileSync(resolved, body);
6745
- else writeYamlFile(resolved, snapshot);
6746
- return resolved;
7478
+ if (!renderedSet || renderedSet.length === 0) {
7479
+ writeYamlFile(resolved, snapshot);
7480
+ return [resolved];
7481
+ }
7482
+ if (renderedSet.length === 1) {
7483
+ fs4.writeFileSync(resolved, renderedSet[0].document);
7484
+ return [resolved];
7485
+ }
7486
+ return renderedSet.map((spec) => {
7487
+ const target = perPortalPath(resolved, spec.portalId);
7488
+ fs4.writeFileSync(target, spec.document);
7489
+ return target;
7490
+ });
6747
7491
  }
6748
- function exportSurface(maxAudience, format, outPath) {
6749
- const snapshot = projectOwnSurface(maxAudience);
6750
- const openapi = format === "openapi" ? renderOpenApiForms(snapshot) : void 0;
6751
- const body = openapi?.rendered ?? openapi?.renderedSet[0]?.document;
6752
- const writtenTo = outPath ? writeSurfaceFile(outPath, body, snapshot) : void 0;
7492
+ function exportResult(snapshot, renderedSet, writtenPaths) {
7493
+ const rendered = renderedSet?.length === 1 ? renderedSet[0].document : void 0;
6753
7494
  return {
6754
7495
  snapshot,
6755
- ...openapi?.rendered !== void 0 ? { rendered: openapi.rendered } : {},
6756
- ...openapi ? { renderedSet: openapi.renderedSet } : {},
6757
- ...writtenTo ? { writtenTo } : {}
7496
+ ...rendered !== void 0 ? { rendered } : {},
7497
+ ...renderedSet ? { renderedSet } : {},
7498
+ ...writtenPaths.length === 1 ? { writtenTo: writtenPaths[0] } : {},
7499
+ ...writtenPaths.length ? { writtenPaths } : {}
6758
7500
  };
6759
7501
  }
7502
+ function exportSurface(maxAudience, format, outPath, portalId) {
7503
+ const snapshot = projectOwnSurface(maxAudience);
7504
+ let renderedSet = format === "openapi" ? toOpenApiSet(snapshot) : void 0;
7505
+ if (renderedSet && portalId) renderedSet = selectPortalSpec(renderedSet, portalId);
7506
+ const writtenPaths = outPath ? writeSurfaceFile(outPath, snapshot, renderedSet) : [];
7507
+ return exportResult(snapshot, renderedSet, writtenPaths);
7508
+ }
6760
7509
  function importSurface(sourcePath, origin) {
6761
7510
  const resolved = path5.resolve(sourcePath);
6762
7511
  if (!fs4.existsSync(resolved)) {
@@ -6776,17 +7525,54 @@ function importSurface(sourcePath, origin) {
6776
7525
  return snapshot;
6777
7526
  }
6778
7527
  function generateChildSnapshots(rootDir = getProjectRoot()) {
6779
- const children = loadSubsystemSpecs().filter((s) => s.projectPath && !s.id.includes("::"));
7528
+ const topLevel = loadSubsystemSpecs().filter((s) => !s.id.includes("::"));
7529
+ const children = topLevel.filter((s) => s.projectPath);
6780
7530
  if (!children.length) return [];
6781
- const snapshot = projectChildSurface();
7531
+ const familySnapshot = projectChildSurface();
7532
+ const siblingSnapshots = /* @__PURE__ */ new Map();
7533
+ const siblingSurface = (subsystemId) => {
7534
+ let snap = siblingSnapshots.get(subsystemId);
7535
+ if (!snap) {
7536
+ snap = projectSubsystemSurface(subsystemId);
7537
+ siblingSnapshots.set(subsystemId, snap);
7538
+ }
7539
+ return snap;
7540
+ };
6782
7541
  const written = [];
6783
7542
  for (const child of children) {
6784
7543
  const childDir = path5.resolve(rootDir, child.projectPath);
6785
7544
  if (!fs4.existsSync(childDir)) continue;
6786
- written.push(saveSnapshot(snapshot, childDir));
7545
+ written.push(saveSnapshot(familySnapshot, childDir));
7546
+ for (const sibling of topLevel) {
7547
+ if (sibling.id === child.id) continue;
7548
+ written.push(saveSnapshot(siblingSurface(sibling.id), childDir));
7549
+ }
6787
7550
  }
6788
7551
  return written;
6789
7552
  }
7553
+ function computeParentStateId(parentRoot) {
7554
+ return computeStateIdAt(parentRoot);
7555
+ }
7556
+ function listExternalInterfaces() {
7557
+ const snapshots = listSnapshots();
7558
+ const chainingParent = resolveChainingParent();
7559
+ const parentStateId = chainingParent ? computeParentStateId(chainingParent.parentRoot) : null;
7560
+ return snapshots.map((snapshot) => {
7561
+ const generated = snapshot.origin === "generated";
7562
+ const sourceKind = !generated ? "foreign" : snapshot.projectName.includes("::") ? "sibling" : "parent";
7563
+ const freshness = generated && parentStateId ? snapshot.stateId === parentStateId ? "fresh" : "stale" : "unverifiable";
7564
+ return {
7565
+ projectName: snapshot.projectName,
7566
+ origin: snapshot.origin,
7567
+ sourceKind,
7568
+ generatedAt: snapshot.generatedAt,
7569
+ ...snapshot.stateId ? { stateId: snapshot.stateId } : {},
7570
+ ...snapshot.version ? { version: snapshot.version } : {},
7571
+ freshness,
7572
+ interfaceIds: snapshot.interfaces.map((e) => e.id)
7573
+ };
7574
+ });
7575
+ }
6790
7576
  function surfaceContentKey(snapshot) {
6791
7577
  const { stateId, generatedAt, origin, ...content } = snapshot;
6792
7578
  return JSON.stringify(content);
@@ -6813,7 +7599,7 @@ function checkChildSurfaceFreshness(rootDir = getProjectRoot()) {
6813
7599
  }
6814
7600
  return issues;
6815
7601
  }
6816
- var fs4, path5, SURFACES_DIRNAME;
7602
+ var fs4, path5, SURFACES_DIRNAME, CROSS_BOUNDARY_TARGETS;
6817
7603
  var init_surfaces = __esm({
6818
7604
  "src/core/surfaces.ts"() {
6819
7605
  "use strict";
@@ -6821,12 +7607,14 @@ var init_surfaces = __esm({
6821
7607
  path5 = __toESM(require("path"));
6822
7608
  init_fs();
6823
7609
  init_yaml();
7610
+ init_filenames();
6824
7611
  init_models();
6825
7612
  init_specs2();
6826
7613
  init_statehash();
6827
7614
  init_type_analysis();
6828
7615
  init_openapi();
6829
7616
  SURFACES_DIRNAME = "surfaces";
7617
+ CROSS_BOUNDARY_TARGETS = /* @__PURE__ */ new Set(["Portal", "Gateway", "Observer"]);
6830
7618
  }
6831
7619
  });
6832
7620
 
@@ -6994,7 +7782,8 @@ var init_contracts = __esm({
6994
7782
  "SURFACE_REF_NOT_EXPOSED",
6995
7783
  `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}".`,
6996
7784
  impl.id,
6997
- isDraftCtx
7785
+ isDraftCtx,
7786
+ true
6998
7787
  );
6999
7788
  }
7000
7789
  continue;
@@ -7051,7 +7840,8 @@ var init_contracts = __esm({
7051
7840
  "SURFACE_REF_NOT_EXPOSED",
7052
7841
  `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}".`,
7053
7842
  impl.id,
7054
- isDraftCtx
7843
+ isDraftCtx,
7844
+ true
7055
7845
  );
7056
7846
  } else if (step.assertsGuarantees) {
7057
7847
  const declared = new Set(surfaceMethod.guarantees ?? []);
@@ -7062,7 +7852,8 @@ var init_contracts = __esm({
7062
7852
  "NARRATIVE_SEMANTIC_UNBACKED",
7063
7853
  `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}".`,
7064
7854
  impl.id,
7065
- isDraftCtx
7855
+ isDraftCtx,
7856
+ true
7066
7857
  );
7067
7858
  }
7068
7859
  }
@@ -8413,13 +9204,14 @@ var init_portals = __esm({
8413
9204
  };
8414
9205
  portalsRule = {
8415
9206
  name: "portal-endpoints",
8416
- description: "A Portal declares its portalType and binds every interface method to a concrete endpoint of the matching transport. Non-Portal components carry no portalType, basePath, or endpoints.",
9207
+ description: "A Portal declares its portalType and binds every interface method to a concrete endpoint of the matching transport. Non-Portal components carry no portalType, basePath, endpoints, or auth (auth is inbound transport auth \u2014 a Gateway carries it on the Portal it owns).",
8417
9208
  codes: [
8418
9209
  { code: "MISSING_PORTAL_TYPE", defaultSeverity: "error", summary: "Portal without a portalType" },
8419
9210
  { code: "MISSING_ENDPOINT", defaultSeverity: "error", summary: "Portal method without a wire endpoint binding" },
8420
9211
  { code: "ENDPOINT_TRANSPORT_MISMATCH", defaultSeverity: "error", summary: "Endpoint transport does not match the Portal portalType" },
8421
9212
  { code: "UNEXPECTED_PORTAL_FIELD", defaultSeverity: "error", summary: "Non-Portal component with portalType/basePath" },
8422
- { code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" }
9213
+ { code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" },
9214
+ { code: "AUTH_ON_NON_PORTAL", defaultSeverity: "warning", summary: "Non-Portal component declaring auth (auth is inbound transport auth, only meaningful on a Portal)" }
8423
9215
  ],
8424
9216
  check(ctx) {
8425
9217
  for (const comp of ctx.components) {
@@ -8471,6 +9263,15 @@ var init_portals = __esm({
8471
9263
  isDraftCtx
8472
9264
  );
8473
9265
  }
9266
+ if (comp.auth !== void 0) {
9267
+ ctx.addIssue(
9268
+ "warning",
9269
+ "AUTH_ON_NON_PORTAL",
9270
+ `Component "${comp.id}" is a ${comp.componentType}, not a Portal, but declares "auth". Auth is inbound transport auth and is only meaningful on a Portal (a Gateway carries it on the Portal it owns). Move it to the exposed Portal, or remove it.`,
9271
+ comp.id,
9272
+ isDraftCtx
9273
+ );
9274
+ }
8474
9275
  const compInterfaces = ctx.interfaces.filter((i) => i.component === comp.id);
8475
9276
  for (const intf of compInterfaces) {
8476
9277
  for (const m of intf.methods) {
@@ -8540,7 +9341,8 @@ var init_stereotype_deps = __esm({
8540
9341
  "CROSS_SUBSYSTEM_NON_ADAPTER",
8541
9342
  `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.`,
8542
9343
  comp.id,
8543
- isDraftCtx
9344
+ isDraftCtx,
9345
+ true
8544
9346
  );
8545
9347
  }
8546
9348
  continue;
@@ -9118,14 +9920,14 @@ var init_declarative_assertions = __esm({
9118
9920
  });
9119
9921
 
9120
9922
  // src/core/rules/profiles.ts
9121
- var BACKEND_LIKE, FRONTEND_LIKE, PROJECT_KINDS, profilesRule;
9923
+ var BACKEND_LIKE, FRONTEND_LIKE, PROJECT_KINDS2, profilesRule;
9122
9924
  var init_profiles = __esm({
9123
9925
  "src/core/rules/profiles.ts"() {
9124
9926
  "use strict";
9125
9927
  init_types();
9126
9928
  BACKEND_LIKE = /* @__PURE__ */ new Set(["backend", "lowlevel-os", "game-ecs", "realtime-embedded", "plc-cyclic"]);
9127
9929
  FRONTEND_LIKE = /* @__PURE__ */ new Set(["frontend-reactive", "frontend-controller"]);
9128
- PROJECT_KINDS = /* @__PURE__ */ new Set(["fullstack", "system-of-systems", "monorepo"]);
9930
+ PROJECT_KINDS2 = new Set(PROJECT_KINDS);
9129
9931
  profilesRule = {
9130
9932
  name: "architectural-profiles",
9131
9933
  description: "Per-profile stereotype constraints: View/FeatureComponent/RouterComponent only in frontend profiles; Actor/Supervisor forbidden in plc-cyclic (single scan cycle); Actor/Supervisor in frontend profiles warned. Extension packs may register custom profiles (family + forbidden/discouraged stereotype lists); unknown profile names are flagged.",
@@ -9149,7 +9951,7 @@ var init_profiles = __esm({
9149
9951
  );
9150
9952
  }
9151
9953
  }
9152
- if (!registered.has(ctx.projectType) && !PROJECT_KINDS.has(ctx.projectType)) {
9954
+ if (!registered.has(ctx.projectType) && !PROJECT_KINDS2.has(ctx.projectType)) {
9153
9955
  ctx.addIssue(
9154
9956
  "warning",
9155
9957
  "UNKNOWN_PROFILE",
@@ -11555,19 +12357,25 @@ var init_lint_allows = __esm({
11555
12357
  });
11556
12358
 
11557
12359
  // src/core/rules/portal-call-auth.ts
11558
- var portalCallAuthRule;
12360
+ var COMPONENT_REF_PREFIX, portalCallAuthRule;
11559
12361
  var init_portal_call_auth = __esm({
11560
12362
  "src/core/rules/portal-call-auth.ts"() {
11561
12363
  "use strict";
12364
+ COMPONENT_REF_PREFIX = "component:";
11562
12365
  portalCallAuthRule = {
11563
12366
  name: "portal-call-auth",
11564
- description: "An OUTBOUND narrative `call` into ANOTHER component's Portal whose auth is not `none` must declare the credential source it presents \u2014 the step's `auth.from`, a design note naming where the secret loads from (a secret-store component, env var, config key, vault ref). Its absence warns (PORTAL_AUTH_UNMET) so credential loading is never overlooked; the actual secret is never stored in the spec. Dispatch steps (a portal's OWN inbound routing) and self-calls are not cross-service calls and are excluded.",
12367
+ description: "Hardens authenticated cross-service calls. An OUTBOUND narrative `call` into ANOTHER component's Portal whose auth is not `none` must (a) be made by an Adapter \u2014 the only block that does external I/O \u2014 and (b) declare the credential source it presents via the step's `auth.from`. Absence of a source warns PORTAL_AUTH_UNMET; a non-Adapter presenter warns AUTH_PRESENTER_NOT_ADAPTER. When `auth.from` is a modeled reference (`component:<id>`) it must resolve to an Adapter/Store the presenter is wired to (UNKNOWN_AUTH_SOURCE / AUTH_SOURCE_NOT_PROVIDER / AUTH_SOURCE_UNWIRED). The actual secret is never stored in the spec. Dispatch steps (a portal's OWN inbound routing) and self-calls are not cross-service calls and are excluded.",
11565
12368
  codes: [
11566
- { code: "PORTAL_AUTH_UNMET", defaultSeverity: "warning", summary: "A narrative call into another component's authed Portal does not declare where its credential loads from" }
12369
+ { code: "PORTAL_AUTH_UNMET", defaultSeverity: "warning", summary: "A narrative call into another component's authed Portal does not declare where its credential loads from" },
12370
+ { code: "AUTH_PRESENTER_NOT_ADAPTER", defaultSeverity: "warning", summary: "A non-Adapter component authenticates an outbound call to a portal (external I/O must go through an Adapter)" },
12371
+ { code: "UNKNOWN_AUTH_SOURCE", defaultSeverity: "warning", summary: "auth.from references a component: source that does not exist" },
12372
+ { code: "AUTH_SOURCE_NOT_PROVIDER", defaultSeverity: "warning", summary: "auth.from references a component that is not an Adapter or Store" },
12373
+ { code: "AUTH_SOURCE_UNWIRED", defaultSeverity: "warning", summary: "The presenter declares a component: credential source it does not depend on or own" }
11567
12374
  ],
11568
12375
  check(ctx) {
11569
12376
  for (const impl of ctx.implementations) {
11570
12377
  const ownComponent = ctx.interfaceMap.get(impl.contract)?.component;
12378
+ const presenter = ownComponent ? ctx.componentMap.get(ownComponent) : void 0;
11571
12379
  const draft = ctx.isImplementationDraft(impl);
11572
12380
  for (const method of impl.methods ?? []) {
11573
12381
  for (const step of method.narrative ?? []) {
@@ -11576,14 +12384,59 @@ var init_portal_call_auth = __esm({
11576
12384
  const target = ctx.componentMap.get(step.targetComponent);
11577
12385
  if (!target || target.componentType !== "Portal") continue;
11578
12386
  if (!target.auth || target.auth.scheme === "none") continue;
11579
- if (step.auth?.from) continue;
11580
- ctx.addIssue(
11581
- "warning",
11582
- "PORTAL_AUTH_UNMET",
11583
- `Narrative step ${step.stepNumber} of "${method.name}" in "${impl.id}" calls the authenticated portal "${target.id}" (auth scheme: ${target.auth.scheme}) but does not declare where its credential is loaded from. Add the credential source (the step's auth.from \u2014 a secret-store component, env var, config key, or vault ref), or drop the portal's auth if it needs none.`,
11584
- impl.id,
11585
- draft
11586
- );
12387
+ if (presenter && presenter.componentType !== "Adapter") {
12388
+ ctx.addIssue(
12389
+ "warning",
12390
+ "AUTH_PRESENTER_NOT_ADAPTER",
12391
+ `Narrative step ${step.stepNumber} of "${method.name}" in "${impl.id}": "${presenter.id}" (${presenter.componentType}) authenticates an outbound call to portal "${target.id}". An authenticated cross-service call is external I/O and must be made by an Adapter (the only block that does external I/O) \u2014 route it through a client Adapter.`,
12392
+ impl.id,
12393
+ draft
12394
+ );
12395
+ }
12396
+ const from = step.auth?.from;
12397
+ if (!from) {
12398
+ ctx.addIssue(
12399
+ "warning",
12400
+ "PORTAL_AUTH_UNMET",
12401
+ `Narrative step ${step.stepNumber} of "${method.name}" in "${impl.id}" calls the authenticated portal "${target.id}" (auth scheme: ${target.auth.scheme}) but does not declare where its credential is loaded from. Add the credential source (the step's auth.from \u2014 a secret-store component, env var, config key, or vault ref), or drop the portal's auth if it needs none.`,
12402
+ impl.id,
12403
+ draft
12404
+ );
12405
+ continue;
12406
+ }
12407
+ if (from.startsWith(COMPONENT_REF_PREFIX)) {
12408
+ const srcId = from.slice(COMPONENT_REF_PREFIX.length);
12409
+ const src = ctx.componentMap.get(srcId);
12410
+ if (!src) {
12411
+ ctx.addIssue(
12412
+ "warning",
12413
+ "UNKNOWN_AUTH_SOURCE",
12414
+ `Narrative step ${step.stepNumber} of "${method.name}" in "${impl.id}": auth.from references component "${srcId}", which does not exist.`,
12415
+ impl.id,
12416
+ draft
12417
+ );
12418
+ } else {
12419
+ if (src.componentType !== "Adapter" && src.componentType !== "Store") {
12420
+ ctx.addIssue(
12421
+ "warning",
12422
+ "AUTH_SOURCE_NOT_PROVIDER",
12423
+ `Narrative step ${step.stepNumber} of "${method.name}" in "${impl.id}": auth.from references "${srcId}" (${src.componentType}); a credential source must be an Adapter (loads the secret via external I/O) or a Store (holds it).`,
12424
+ impl.id,
12425
+ draft
12426
+ );
12427
+ }
12428
+ const wired = (presenter?.dependsOn ?? []).includes(srcId) || (presenter?.owns ?? []).includes(srcId);
12429
+ if (presenter && !wired) {
12430
+ ctx.addIssue(
12431
+ "warning",
12432
+ "AUTH_SOURCE_UNWIRED",
12433
+ `Narrative step ${step.stepNumber} of "${method.name}" in "${impl.id}": "${presenter.id}" loads its credential from "${srcId}" but neither depends on nor owns it \u2014 declare the dependsOn edge so the credential wiring is real.`,
12434
+ impl.id,
12435
+ draft
12436
+ );
12437
+ }
12438
+ }
12439
+ }
11587
12440
  }
11588
12441
  }
11589
12442
  }
@@ -11747,9 +12600,15 @@ function buildRuleContext(opts) {
11747
12600
  ...[...SDD_RULES, ...extensions.rules].flatMap((r) => r.codes.map((c) => c.code)),
11748
12601
  // Declarative assertions bring their own namespaced codes — lint.allow
11749
12602
  // and severity overrides treat them exactly like builtins.
11750
- ...extensions.assertions.map((a) => a.fullCode)
12603
+ ...extensions.assertions.map((a) => a.fullCode),
12604
+ // Entry-point emitted codes: validateSddTree's chained-subproject pass
12605
+ // raises these AFTER the rule run (it post-processes the aggregated issue
12606
+ // list), so no registered rule declares them — but lint.allow validation
12607
+ // must still recognize them as real codes.
12608
+ "CHAINED_SUBPROJECT_CONTEXT",
12609
+ "UNVERIFIED_EXTERNAL_REF"
11751
12610
  ]);
11752
- const addIssue = (defaultSeverity, code, message, specId, isDraftContext) => {
12611
+ const addIssue = (defaultSeverity, code, message, specId, isDraftContext, surfaceResolved) => {
11753
12612
  if (scopeSubsystem && specId && !isSpecInScope(specId)) {
11754
12613
  return;
11755
12614
  }
@@ -11767,7 +12626,14 @@ function buildRuleContext(opts) {
11767
12626
  if (severity === "warning") return;
11768
12627
  }
11769
12628
  }
11770
- issues.push({ severity, code, message, specId, ...isDraftContext ? { draftContext: true } : {} });
12629
+ issues.push({
12630
+ severity,
12631
+ code,
12632
+ message,
12633
+ specId,
12634
+ ...isDraftContext ? { draftContext: true } : {},
12635
+ ...surfaceResolved ? { surfaceResolved: true } : {}
12636
+ });
11771
12637
  };
11772
12638
  return {
11773
12639
  system,
@@ -12245,24 +13111,54 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
12245
13111
  for (const rule of ruleSequence()) {
12246
13112
  rule.check(ctx);
12247
13113
  }
12248
- const hasCrossTreeSuspects = issues.some((i) => SUBPROJECT_LENIENT_CODES.has(i.code));
13114
+ const hasCrossTreeSuspects = issues.some(
13115
+ (i) => SUBPROJECT_REFERENCE_CODES.has(i.code) || SUBPROJECT_CONFORMANCE_CODES.has(i.code)
13116
+ );
12249
13117
  const chainingParent = hasCrossTreeSuspects ? findChainingParent(getProjectRoot()) : null;
12250
13118
  if (chainingParent) {
13119
+ let unverified = 0;
12251
13120
  let downgraded = 0;
12252
- for (const iss of issues) {
12253
- if (!SUBPROJECT_LENIENT_CODES.has(iss.code)) continue;
12254
- if (iss.severity === "error") {
12255
- iss.severity = "warning";
12256
- downgraded++;
13121
+ for (let at = 0; at < issues.length; at++) {
13122
+ const iss = issues[at];
13123
+ if (SUBPROJECT_REFERENCE_CODES.has(iss.code) && !iss.surfaceResolved) {
13124
+ issues[at] = {
13125
+ severity: "warning",
13126
+ code: "UNVERIFIED_EXTERNAL_REF",
13127
+ crossTreeContext: true,
13128
+ // --ci waives it (parent root is authoritative)
13129
+ specId: iss.specId,
13130
+ ...iss.agentId ? { agentId: iss.agentId } : {},
13131
+ ...iss.draftContext ? { draftContext: true } : {},
13132
+ 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.`
13133
+ };
13134
+ unverified++;
13135
+ continue;
13136
+ }
13137
+ if (SUBPROJECT_CONFORMANCE_CODES.has(iss.code)) {
13138
+ if (iss.severity === "error") {
13139
+ iss.severity = "warning";
13140
+ downgraded++;
13141
+ }
13142
+ iss.crossTreeContext = true;
12257
13143
  }
12258
- iss.crossTreeContext = true;
12259
13144
  }
12260
- if (downgraded > 0) {
13145
+ if (unverified > 0 || downgraded > 0) {
13146
+ const notes = [];
13147
+ if (unverified > 0) {
13148
+ notes.push(
13149
+ `${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.`
13150
+ );
13151
+ }
13152
+ if (downgraded > 0) {
13153
+ notes.push(
13154
+ `${downgraded} code\u2194spec conformance finding(s) (parent-root-relative source paths) were downgraded to warnings.`
13155
+ );
13156
+ }
12261
13157
  issues.unshift({
12262
13158
  severity: "warning",
12263
13159
  code: "CHAINED_SUBPROJECT_CONTEXT",
12264
13160
  crossTreeContext: true,
12265
- 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.`
13161
+ 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.`
12266
13162
  });
12267
13163
  }
12268
13164
  }
@@ -12279,7 +13175,7 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
12279
13175
  function validateAsComplete(options) {
12280
13176
  return validateSddTree({ ...options ?? {}, treatAllAsComplete: true });
12281
13177
  }
12282
- var SUBPROJECT_LENIENT_CODES;
13178
+ var SUBPROJECT_REFERENCE_CODES, SUBPROJECT_CONFORMANCE_CODES;
12283
13179
  var init_validation = __esm({
12284
13180
  "src/core/validation.ts"() {
12285
13181
  "use strict";
@@ -12292,8 +13188,7 @@ var init_validation = __esm({
12292
13188
  init_source_analysis();
12293
13189
  init_specs2();
12294
13190
  init_fs();
12295
- SUBPROJECT_LENIENT_CODES = /* @__PURE__ */ new Set([
12296
- // reference resolution
13191
+ SUBPROJECT_REFERENCE_CODES = /* @__PURE__ */ new Set([
12297
13192
  "UNDEFINED_TYPE_REFERENCE",
12298
13193
  "INVALID_DEPENDENCY_REFERENCE",
12299
13194
  "INVALID_TARGET_COMPONENT_REFERENCE",
@@ -12301,8 +13196,9 @@ var init_validation = __esm({
12301
13196
  "UNDECLARED_DEPENDENCY_CALL",
12302
13197
  "INVALID_TRUSTED_LINK",
12303
13198
  "CROSS_SUBSYSTEM_NON_ADAPTER",
12304
- "CROSS_TREE_REF_UNRESOLVED",
12305
- // code↔spec conformance (root-relative sourcePaths / import graph)
13199
+ "CROSS_TREE_REF_UNRESOLVED"
13200
+ ]);
13201
+ SUBPROJECT_CONFORMANCE_CODES = /* @__PURE__ */ new Set([
12306
13202
  "MISSING_SOURCE_FILE",
12307
13203
  "SOURCE_PATH_ESCAPES_ROOT",
12308
13204
  "MISSING_SOURCE_PATH",
@@ -13225,6 +14121,19 @@ function dryRunSerializeSpecs(include) {
13225
14121
  function buildProjectGraph(level) {
13226
14122
  return buildGraphModel(level);
13227
14123
  }
14124
+ function resolveChainingParent() {
14125
+ return findChainingParent(getProjectRoot());
14126
+ }
14127
+ function computeStateIdAt(root) {
14128
+ const resolved = path10.resolve(root);
14129
+ return runWithProjectRoot(resolved, () => {
14130
+ workspaceFor(resolved).invalidate();
14131
+ const system = loadSystemSpec();
14132
+ if (!system) return null;
14133
+ const s = computeStateId();
14134
+ return `${s.algorithm}:${s.digest}`;
14135
+ });
14136
+ }
13228
14137
  function deleteTypeSpec(id) {
13229
14138
  return current().deleteTypeSpec(id);
13230
14139
  }
@@ -13268,6 +14177,7 @@ var init_specs2 = __esm({
13268
14177
  path10 = __toESM(require("path"));
13269
14178
  init_loader();
13270
14179
  init_fs();
14180
+ init_statehash();
13271
14181
  init_yaml();
13272
14182
  init_models();
13273
14183
  init_narrative_labels();
@@ -15362,6 +16272,7 @@ __export(src_exports, {
15362
16272
  OutputTargetSchema: () => OutputTargetSchema,
15363
16273
  PACK_DIR_ENTRIES: () => PACK_DIR_ENTRIES,
15364
16274
  PATTERN_TYPES: () => PATTERN_TYPES,
16275
+ PROJECT_KINDS: () => PROJECT_KINDS,
15365
16276
  PackAssertionSchema: () => PackAssertionSchema,
15366
16277
  PackSkillSchema: () => PackSkillSchema,
15367
16278
  ParallelBranchSchema: () => ParallelBranchSchema,
@@ -15372,8 +16283,10 @@ __export(src_exports, {
15372
16283
  PortalAuthSchemeSchema: () => PortalAuthSchemeSchema,
15373
16284
  PortalTypeSchema: () => PortalTypeSchema,
15374
16285
  ProfileDefSchema: () => ProfileDefSchema,
16286
+ ProfileSelectionSubjectSchema: () => ProfileSelectionSubjectSchema,
15375
16287
  ProjectConfigSchema: () => ProjectConfigSchema,
15376
16288
  ProjectNotInitializedError: () => ProjectNotInitializedError,
16289
+ ProjectProfileSelectionSchema: () => ProjectProfileSelectionSchema,
15377
16290
  PublicInterfaceSchema: () => PublicInterfaceSchema,
15378
16291
  PublicInterfaceTypeSchema: () => PublicInterfaceTypeSchema,
15379
16292
  RegistrySchema: () => RegistrySchema,
@@ -15432,7 +16345,9 @@ __export(src_exports, {
15432
16345
  clearLoaderIssues: () => clearLoaderIssues,
15433
16346
  collectPromotableSpecs: () => collectPromotableSpecs,
15434
16347
  composeRuleSequence: () => composeRuleSequence,
16348
+ computeParentStateId: () => computeParentStateId,
15435
16349
  computeStateId: () => computeStateId,
16350
+ computeStateIdAt: () => computeStateIdAt,
15436
16351
  contextDir: () => contextDir,
15437
16352
  createAgentRecord: () => createAgentRecord,
15438
16353
  createChainedSubsystem: () => createChainedSubsystem,
@@ -15498,6 +16413,7 @@ __export(src_exports, {
15498
16413
  isOpenApiDocument: () => isOpenApiDocument,
15499
16414
  isProjectInitialized: () => isProjectInitialized,
15500
16415
  listDirectChainedSubprojects: () => listDirectChainedSubprojects,
16416
+ listExternalInterfaces: () => listExternalInterfaces,
15501
16417
  listFiles: () => listFiles,
15502
16418
  listFilesRecursive: () => listFilesRecursive,
15503
16419
  listFreeStandingDomains: () => listFreeStandingDomains,
@@ -15543,6 +16459,7 @@ __export(src_exports, {
15543
16459
  pathExists: () => pathExists,
15544
16460
  projectChildSurface: () => projectChildSurface,
15545
16461
  projectOwnSurface: () => projectOwnSurface,
16462
+ projectSubsystemSurface: () => projectSubsystemSurface,
15546
16463
  promoteAllComplete: () => promoteAllComplete,
15547
16464
  provisionProject: () => provisionProject,
15548
16465
  readArchitectureContext: () => readArchitectureContext,
@@ -15562,6 +16479,7 @@ __export(src_exports, {
15562
16479
  renderTemplateInstructions: () => renderTemplateInstructions,
15563
16480
  renderWaironGuide: () => renderWaironGuide,
15564
16481
  resolveAgentTopology: () => resolveAgentTopology,
16482
+ resolveChainingParent: () => resolveChainingParent,
15565
16483
  resolveDomains: () => resolveDomains,
15566
16484
  resolvePackRef: () => resolvePackRef,
15567
16485
  resolveSubprojectForNamespace: () => resolveSubprojectForNamespace,
@@ -15625,7 +16543,7 @@ function defaultTargetConfig(type) {
15625
16543
  enabled: true
15626
16544
  };
15627
16545
  }
15628
- var WAIRON_VERSION = "5.0.2-dev.9";
16546
+ var WAIRON_VERSION = "5.1.0";
15629
16547
  var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
15630
16548
  var ARCHITECT_AGENT_ID = "agent-architect";
15631
16549
  var ARCHITECT_TEMPLATE_ID = "architect";
@@ -16949,6 +17867,7 @@ init_yaml();
16949
17867
  OutputTargetSchema,
16950
17868
  PACK_DIR_ENTRIES,
16951
17869
  PATTERN_TYPES,
17870
+ PROJECT_KINDS,
16952
17871
  PackAssertionSchema,
16953
17872
  PackSkillSchema,
16954
17873
  ParallelBranchSchema,
@@ -16959,8 +17878,10 @@ init_yaml();
16959
17878
  PortalAuthSchemeSchema,
16960
17879
  PortalTypeSchema,
16961
17880
  ProfileDefSchema,
17881
+ ProfileSelectionSubjectSchema,
16962
17882
  ProjectConfigSchema,
16963
17883
  ProjectNotInitializedError,
17884
+ ProjectProfileSelectionSchema,
16964
17885
  PublicInterfaceSchema,
16965
17886
  PublicInterfaceTypeSchema,
16966
17887
  RegistrySchema,
@@ -17019,7 +17940,9 @@ init_yaml();
17019
17940
  clearLoaderIssues,
17020
17941
  collectPromotableSpecs,
17021
17942
  composeRuleSequence,
17943
+ computeParentStateId,
17022
17944
  computeStateId,
17945
+ computeStateIdAt,
17023
17946
  contextDir,
17024
17947
  createAgentRecord,
17025
17948
  createChainedSubsystem,
@@ -17085,6 +18008,7 @@ init_yaml();
17085
18008
  isOpenApiDocument,
17086
18009
  isProjectInitialized,
17087
18010
  listDirectChainedSubprojects,
18011
+ listExternalInterfaces,
17088
18012
  listFiles,
17089
18013
  listFilesRecursive,
17090
18014
  listFreeStandingDomains,
@@ -17130,6 +18054,7 @@ init_yaml();
17130
18054
  pathExists,
17131
18055
  projectChildSurface,
17132
18056
  projectOwnSurface,
18057
+ projectSubsystemSurface,
17133
18058
  promoteAllComplete,
17134
18059
  provisionProject,
17135
18060
  readArchitectureContext,
@@ -17149,6 +18074,7 @@ init_yaml();
17149
18074
  renderTemplateInstructions,
17150
18075
  renderWaironGuide,
17151
18076
  resolveAgentTopology,
18077
+ resolveChainingParent,
17152
18078
  resolveDomains,
17153
18079
  resolvePackRef,
17154
18080
  resolveSubprojectForNamespace,