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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -65,7 +65,7 @@ var init_defaults = __esm({
65
65
  copilot: ".github/prompts",
66
66
  codex: ".codex/agents"
67
67
  };
68
- WAIRON_VERSION = "5.0.2-dev.12";
68
+ WAIRON_VERSION = "5.0.2-dev.14";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  ARCHITECT_AGENT_ID = "agent-architect";
71
71
  ARCHITECT_TEMPLATE_ID = "architect";
@@ -1914,6 +1914,47 @@ var init_loader = __esm({
1914
1914
  }
1915
1915
  });
1916
1916
 
1917
+ // src/core/statehash.ts
1918
+ function computeStateId() {
1919
+ const tree = {
1920
+ system: loadSystemSpec(),
1921
+ subsystems: loadSubsystemSpecs(),
1922
+ components: loadComponentSpecs(),
1923
+ interfaces: loadInterfaceSpecs(),
1924
+ implementations: loadImplementationSpecs(),
1925
+ types: loadTypeSpecs()
1926
+ };
1927
+ const digest = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
1928
+ return { algorithm: "sha256", digest };
1929
+ }
1930
+ function stateIdEquals(a, b) {
1931
+ return !!a && !!b && a.algorithm === b.algorithm && a.digest === b.digest;
1932
+ }
1933
+ function canonicalize(value) {
1934
+ return JSON.stringify(sortKeys(value));
1935
+ }
1936
+ function sortKeys(v) {
1937
+ if (Array.isArray(v)) return v.map(sortKeys);
1938
+ if (v && typeof v === "object") {
1939
+ const src = v;
1940
+ const out = {};
1941
+ for (const k of Object.keys(src).sort()) {
1942
+ if (k === "createdAt" || k === "updatedAt") continue;
1943
+ out[k] = sortKeys(src[k]);
1944
+ }
1945
+ return out;
1946
+ }
1947
+ return v;
1948
+ }
1949
+ var crypto;
1950
+ var init_statehash = __esm({
1951
+ "src/core/statehash.ts"() {
1952
+ "use strict";
1953
+ crypto = __toESM(require("crypto"));
1954
+ init_specs2();
1955
+ }
1956
+ });
1957
+
1917
1958
  // src/core/narrative-labels.ts
1918
1959
  function resolveNarrativeLabels(methodName, steps) {
1919
1960
  const errors = [];
@@ -2999,6 +3040,16 @@ header input[type="search"]::placeholder { color:var(--dim); }
2999
3040
  #moreMenu .dropdown { display:block; width:100%; }
3000
3041
  #moreMenu .dropdown > .tbtn, #moreMenu > .tbtn { display:block; width:100%; text-align:left; border:none; background:transparent; margin:2px 0; }
3001
3042
  #moreMenu .dropdown > .tbtn:hover, #moreMenu > .tbtn:hover { background:var(--hover-bg); }
3043
+ /* Staged header compaction (responsive only, never persisted): compact
3044
+ stand-ins for the search input, the mode tabs, and the ancestor crumbs.
3045
+ All hidden at full width, so a roomy header renders exactly as before. */
3046
+ #searchBtn { position:relative; }
3047
+ #searchBtn.hasq::after { content:''; position:absolute; top:3px; right:3px; width:7px; height:7px; border-radius:50%; background:var(--accent); }
3048
+ .search-menu { min-width:210px; padding:8px; }
3049
+ .search-menu input[type="search"] { width:100%; }
3050
+ #modeMenu button.active { background:var(--accent); color:#fff; font-weight:700; }
3051
+ #crumbs .dropdown { display:inline-flex; }
3052
+ #crumbs .crumbmore { font-weight:700; }
3002
3053
 
3003
3054
  /* Settings panel \u2014 toggle switches */
3004
3055
  .settings-menu { min-width:266px; }
@@ -3122,9 +3173,17 @@ body.presentation #exitPresent, body.presentation #presentDetails { display:bloc
3122
3173
  <button data-vm="types">Types</button>
3123
3174
  <button data-vm="databases">Databases</button>
3124
3175
  </div>
3176
+ <div class="dropdown" id="modeDd" style="display:none">
3177
+ <button class="tbtn" id="modeBtn" title="Switch between the component architecture, the type ERD, or the database schemas">Components \u25BE</button>
3178
+ <div class="menu" id="modeMenu"></div>
3179
+ </div>
3125
3180
  <nav id="crumbs"></nav>
3126
3181
  <span class="divider"></span>
3127
3182
  <input id="search" type="search" placeholder="Search this view\u2026">
3183
+ <div class="dropdown" id="searchDd" style="display:none">
3184
+ <button class="tbtn" id="searchBtn" title="Search this view">\u{1F50D}</button>
3185
+ <div class="menu search-menu" id="searchMenu"></div>
3186
+ </div>
3128
3187
  <div class="seg" id="typesDetailSeg" style="display:none" title="ERD detail level">
3129
3188
  <button data-td="full">Full</button>
3130
3189
  <button data-td="fields">Fields</button>
@@ -3641,13 +3700,300 @@ var MODEL = __MODEL_JSON__;
3641
3700
  var BOX_W = 200, BOX_H = 56, SUBBOX_W = 230, SUBBOX_H = 84, GAP_X = 110, GAP_Y = 34;
3642
3701
  var INNER_W = 130, INNER_H = 36, INNER_GAPX = 26, INNER_GAPY = 12, HEAD_H = 34, PADI = 14;
3643
3702
 
3703
+ // ---- deep expansion (opt-in via subsystem.deepInternals) -------------------
3704
+ // When Internals is on and a subsystem record carries deepInternals:true, its
3705
+ // box renders its WHOLE subtree instead of one layer: deep-flagged child
3706
+ // subsystems become NESTED boundary boxes (recursing, defensively capped),
3707
+ // every other child renders as a fixed leaf tile that is never expanded
3708
+ // further. Sizes are computed bottom-up (a nested container tile takes its
3709
+ // recursive {w,h}); positions are emitted top-down by accumulating parent
3710
+ // top-left offsets. Relations between concrete visible endpoints are drawn
3711
+ // as ONE direct line each by buildDeepContext \u2014 crossing nested boundaries
3712
+ // on purpose (the full org overview of project relations); only relations
3713
+ // that cannot resolve to two concrete endpoints keep today's port machinery,
3714
+ // and ONLY at the outermost box. Without the flag this whole path is inert
3715
+ // and the classic one-layer innerLayout runs unchanged.
3716
+ var DEEP_MAX_DEPTH = 6;
3717
+ function isDeepId(subId) {
3718
+ var s = subById[subId];
3719
+ return !!(s && s.deepInternals);
3720
+ }
3721
+ // The DEEPEST visible tile representing compId inside the deep-expanded box
3722
+ // rooted at rootSubId: descend deep-flagged containers (the same expansion
3723
+ // rule as deepContainerLayout) until the containing child is a leaf tile.
3724
+ // Returns the child entry { kind, id } (its node id is IN(kind, id)).
3725
+ function deepLeafFor(compId, rootSubId) {
3726
+ var subId = rootSubId, depth = 1;
3727
+ for (;;) {
3728
+ var child = childOfScopeContaining(compId, { kind: 'subsystem', id: subId });
3729
+ if (!child) return null;
3730
+ if (child.kind === 'subsystem' && isDeepId(child.id) && depth < DEEP_MAX_DEPTH) {
3731
+ subId = child.id; depth += 1;
3732
+ continue;
3733
+ }
3734
+ return child;
3735
+ }
3736
+ }
3737
+ // One deep container's DIRECT children, placed with PER-TILE sizes (nested
3738
+ // containers take their recursive size; leaves stay INNER_W x INNER_H). The
3739
+ // placement mirrors innerLayout's strategy switch, generalised to variable
3740
+ // tile sizes. Tiles are centres relative to THIS container's top-left corner
3741
+ // (content sits right of PADI, below the HEAD_H label band); nested tiles
3742
+ // are relative to their own container, so emission accumulates offsets.
3743
+ function deepContainerLayout(subId, depth) {
3744
+ var kids = childrenOf({ kind: 'subsystem', id: subId });
3745
+ if (!kids.length) {
3746
+ // An EMPTY deep subsystem still shows as a (min-size) boundary box.
3747
+ return { tiles: [], w: INNER_W + 2 * PADI, h: HEAD_H + PADI };
3748
+ }
3749
+ var scope = { kind: 'subsystem', id: subId };
3750
+ var kidKey = function (k) { return k.kind + ':' + k.id; };
3751
+ var size = {};
3752
+ kids.forEach(function (k) {
3753
+ if (k.kind === 'subsystem' && isDeepId(k.id) && depth < DEEP_MAX_DEPTH) {
3754
+ var nested = deepContainerLayout(k.id, depth + 1);
3755
+ size[kidKey(k)] = { w: nested.w, h: nested.h, sub: nested };
3756
+ } else {
3757
+ size[kidKey(k)] = { w: INNER_W, h: INNER_H, sub: null };
3758
+ }
3759
+ });
3760
+ // Intra-container edges lifted to DIRECT children \u2014 for LAYOUT ONLY (the
3761
+ // drawn lines come from buildDeepContext's direct pass, never per level).
3762
+ var intra = {};
3763
+ MODEL.edges.forEach(function (edge) {
3764
+ var a = childOfScopeContaining(edge.from, scope);
3765
+ var b = childOfScopeContaining(edge.to, scope);
3766
+ if (!a || !b) return;
3767
+ var ak = a.kind + ':' + a.id, bk = b.kind + ':' + b.id;
3768
+ if (!size[ak] || !size[bk] || ak === bk) return;
3769
+ intra[ak + '=>' + bk] = 1;
3770
+ });
3771
+ var layer = {};
3772
+ function calc(k, stack) {
3773
+ var key = kidKey(k);
3774
+ if (layer[key] !== undefined) return layer[key];
3775
+ if (stack[key]) return 0;
3776
+ stack[key] = 1;
3777
+ var l = 0;
3778
+ if (k.kind === 'component') {
3779
+ var c = compById[k.id];
3780
+ if (c && (c.componentType === 'Portal' || c.componentType === 'Observer')) { layer[key] = 0; delete stack[key]; return 0; }
3781
+ }
3782
+ Object.keys(intra).forEach(function (ek) {
3783
+ var cut = ek.indexOf('=>');
3784
+ if (ek.slice(cut + 2) !== key) return;
3785
+ var srcKid = kids.filter(function (x) { return kidKey(x) === ek.slice(0, cut); })[0];
3786
+ if (srcKid) l = Math.max(l, calc(srcKid, stack) + 1);
3787
+ });
3788
+ delete stack[key];
3789
+ layer[key] = l;
3790
+ return l;
3791
+ }
3792
+ kids.forEach(function (k) { calc(k, {}); });
3793
+ var tiles = [], contentW = 0, contentH = 0;
3794
+ if (state.layout === 'grid') {
3795
+ // Row packing by ACTUAL tile size (the fixed per-column grid assumed
3796
+ // uniform tiles); the target row width follows the tile count, widened
3797
+ // to at least the widest single tile.
3798
+ var gsorted = kids.slice().sort(function (a, b) { return a.id < b.id ? -1 : 1; });
3799
+ var target = Math.max(1, Math.ceil(Math.sqrt(kids.length))) * (INNER_W + INNER_GAPX);
3800
+ kids.forEach(function (k) { var s0 = size[kidKey(k)]; if (s0.w > target) target = s0.w; });
3801
+ var gx = 0, gy = 0, rowH = 0;
3802
+ gsorted.forEach(function (k) {
3803
+ var s = size[kidKey(k)];
3804
+ if (gx > 0 && gx + s.w > target) { gx = 0; gy += rowH + INNER_GAPY; rowH = 0; }
3805
+ tiles.push({ kid: k, x: gx + s.w / 2, y: gy + s.h / 2, w: s.w, h: s.h, sub: s.sub });
3806
+ gx += s.w + INNER_GAPX;
3807
+ if (s.h > rowH) rowH = s.h;
3808
+ if (gx - INNER_GAPX > contentW) contentW = gx - INNER_GAPX;
3809
+ if (gy + rowH > contentH) contentH = gy + rowH;
3810
+ });
3811
+ } else if (state.layout === 'concentric' || state.layout === 'force') {
3812
+ var ideg = {};
3813
+ kids.forEach(function (k) { ideg[kidKey(k)] = 0; });
3814
+ Object.keys(intra).forEach(function (ek) {
3815
+ var cut2 = ek.indexOf('=>');
3816
+ var sk = ek.slice(0, cut2), tk = ek.slice(cut2 + 2);
3817
+ if (ideg[sk] !== undefined) ideg[sk]++;
3818
+ if (ideg[tk] !== undefined) ideg[tk]++;
3819
+ });
3820
+ var rel = concentricPositions(
3821
+ kids.map(kidKey),
3822
+ function (key) { return ideg[key] || 0; },
3823
+ function (key) { return { w: size[key].w, h: size[key].h }; }
3824
+ );
3825
+ // Normalise by the tiles' BOUNDING BOX (not just the centres) so a wide
3826
+ // nested container on the rim still clears the container's left/top pad.
3827
+ var minL = Infinity, minT = Infinity;
3828
+ kids.forEach(function (k) {
3829
+ var s1 = size[kidKey(k)], p1 = rel[kidKey(k)] || { x: 0, y: 0 };
3830
+ if (p1.x - s1.w / 2 < minL) minL = p1.x - s1.w / 2;
3831
+ if (p1.y - s1.h / 2 < minT) minT = p1.y - s1.h / 2;
3832
+ });
3833
+ if (minL === Infinity) { minL = 0; minT = 0; }
3834
+ kids.forEach(function (k) {
3835
+ var s2 = size[kidKey(k)], p2 = rel[kidKey(k)] || { x: 0, y: 0 };
3836
+ var cx = p2.x - minL, cyy = p2.y - minT;
3837
+ tiles.push({ kid: k, x: cx, y: cyy, w: s2.w, h: s2.h, sub: s2.sub });
3838
+ if (cx + s2.w / 2 > contentW) contentW = cx + s2.w / 2;
3839
+ if (cyy + s2.h / 2 > contentH) contentH = cyy + s2.h / 2;
3840
+ });
3841
+ } else {
3842
+ // Layered dependency columns: the column is as wide as its widest tile,
3843
+ // and each tile advances by ITS OWN height.
3844
+ var cols = {};
3845
+ kids.forEach(function (k) { var l = layer[kidKey(k)] || 0; (cols[l] = cols[l] || []).push(k); });
3846
+ var colKeys = Object.keys(cols).map(Number).sort(function (a, b) { return a - b; });
3847
+ var x = 0;
3848
+ colKeys.forEach(function (ck) {
3849
+ var col = cols[ck].sort(function (a, b) { return a.id < b.id ? -1 : 1; });
3850
+ var colW = 0, y = 0;
3851
+ col.forEach(function (k) { var s3 = size[kidKey(k)]; if (s3.w > colW) colW = s3.w; });
3852
+ col.forEach(function (k) {
3853
+ var s = size[kidKey(k)];
3854
+ tiles.push({ kid: k, x: x + colW / 2, y: y + s.h / 2, w: s.w, h: s.h, sub: s.sub });
3855
+ y += s.h + INNER_GAPY;
3856
+ });
3857
+ if (y - INNER_GAPY > contentH) contentH = y - INNER_GAPY;
3858
+ x += colW + INNER_GAPX;
3859
+ });
3860
+ contentW = x - INNER_GAPX;
3861
+ }
3862
+ tiles.forEach(function (t) { t.x += PADI; t.y += HEAD_H; });
3863
+ return { tiles: tiles, w: contentW + 2 * PADI, h: HEAD_H + contentH + PADI };
3864
+ }
3865
+ // Top-level deep box: the recursive interior plus today's port machinery at
3866
+ // the OUTERMOST box only (buildDeepContext supplies which relations still
3867
+ // need ports; stubs run port <-> the DEEPEST visible leaf tile). Returns the
3868
+ // same shape as innerLayout, plus deep:true so emission recurses.
3869
+ function deepLayout(entry, portRec) {
3870
+ var box = deepContainerLayout(entry.id, 1);
3871
+ var parentId = anchorNodeId(entry);
3872
+ var pBaseIn = 'p~in~' + parentId + '~', pBaseOut = 'p~out~' + parentId + '~';
3873
+ var extIn = portRec ? portRec.extIn : {}, extOut = portRec ? portRec.extOut : {};
3874
+ var inIds = Object.keys(extIn).sort(), outIds = Object.keys(extOut).sort();
3875
+ var hasIn = inIds.length > 0, hasOut = outIds.length > 0;
3876
+ var PROXY_W = 22, PROXY_H = 22, PROXY_GAP = 8;
3877
+ var shift = hasIn ? PROXY_W + INNER_GAPX : 0;
3878
+ var tiles = box.tiles;
3879
+ if (shift) tiles.forEach(function (t) { t.x += shift; });
3880
+ var w = box.w + shift + (hasOut ? PROXY_W + INNER_GAPX : 0);
3881
+ if (w < SUBBOX_W) w = SUBBOX_W;
3882
+ var stackMax = Math.max(inIds.length, outIds.length);
3883
+ var h = Math.max(box.h, HEAD_H + stackMax * PROXY_H + Math.max(0, stackMax - 1) * PROXY_GAP + PADI);
3884
+ var midY = HEAD_H + Math.max(0, (h - HEAD_H - PADI) / 2);
3885
+ function stackPorts(ids, recs, base, cx, dir) {
3886
+ var total = ids.length * PROXY_H + Math.max(0, ids.length - 1) * PROXY_GAP;
3887
+ var y0 = Math.max(HEAD_H + PROXY_H / 2, midY - total / 2 + PROXY_H / 2);
3888
+ return ids.map(function (eid, i) {
3889
+ var km = recs[eid].kids, klist = [];
3890
+ Object.keys(km).forEach(function (key) { klist.push(km[key]); });
3891
+ 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 };
3892
+ });
3893
+ }
3894
+ return {
3895
+ deep: true,
3896
+ tiles: tiles,
3897
+ edges: portRec ? Object.keys(portRec.stubs).map(function (k) { return portRec.stubs[k]; }) : [],
3898
+ proxies: stackPorts(inIds, extIn, pBaseIn, PADI + PROXY_W / 2, 'in')
3899
+ .concat(stackPorts(outIds, extOut, pBaseOut, w - PADI - PROXY_W / 2, 'out')),
3900
+ w: w,
3901
+ h: h,
3902
+ };
3903
+ }
3904
+ // View-level deep context (null unless Internals is on AND at least one
3905
+ // in-view entry is deep-flagged \u2014 the classic path never sees it): which
3906
+ // top-level entries are deep-expanded; every relation drawn as a DIRECT
3907
+ // concrete line (deduped per src=>tgt pair); the per-top-pair count used to
3908
+ // suppress aggregated edges whose constituents are ALL drawn directly; and
3909
+ // the port records for relations that keep today's port semantics.
3910
+ function buildDeepContext(scope, entries) {
3911
+ if (!state.internals) return null;
3912
+ var deepByAnchor = {}, any = false;
3913
+ entries.forEach(function (e) {
3914
+ if (e.kind === 'subsystem' && isDeepId(e.id)) { deepByAnchor[anchorNodeId(e)] = e; any = true; }
3915
+ });
3916
+ if (!any) return null;
3917
+ var entryByAnchor = {};
3918
+ entries.forEach(function (e) { entryByAnchor[e.kind + ':' + e.id] = e; });
3919
+ // A relation endpoint's DIRECT-line node in this view: the deepest leaf
3920
+ // tile inside a deep-expanded entry, or a top-level component box ITSELF.
3921
+ // null = this endpoint keeps aggregated/port semantics (interiors of
3922
+ // non-deep entries, out-of-scope counterparts).
3923
+ function directEnd(compId) {
3924
+ var child = childOfScopeContaining(compId, scope);
3925
+ var entry = child && entryByAnchor[child.kind + ':' + child.id];
3926
+ if (!entry) return null;
3927
+ var aid = anchorNodeId(entry);
3928
+ if (deepByAnchor[aid]) {
3929
+ var leaf = deepLeafFor(compId, entry.id);
3930
+ return leaf ? { node: IN(leaf.kind, leaf.id), top: aid, deep: true } : null;
3931
+ }
3932
+ if (entry.kind === 'component' && entry.id === compId) return { node: aid, top: aid, deep: false };
3933
+ return null;
3934
+ }
3935
+ var direct = {}, directTopCount = {}, ports = {};
3936
+ function portRec(aid) { return ports[aid] = ports[aid] || { extIn: {}, extOut: {}, stubs: {} }; }
3937
+ MODEL.edges.forEach(function (edge) {
3938
+ var a = directEnd(edge.from), b = directEnd(edge.to);
3939
+ if (a && b && (a.deep || b.deep) && a.node !== b.node) {
3940
+ // Drawn as ONE direct line \u2014 never ALSO as ports/stubs (dedupe rule).
3941
+ var key = a.node + '=>' + b.node;
3942
+ if (!direct[key]) direct[key] = { src: a.node, tgt: b.node, cross: false, aTop: a.top, bTop: b.top };
3943
+ if (edge.cross) direct[key].cross = true;
3944
+ if (a.top !== b.top) {
3945
+ var tk = a.top + '=>' + b.top;
3946
+ directTopCount[tk] = (directTopCount[tk] || 0) + 1;
3947
+ }
3948
+ return;
3949
+ }
3950
+ // Not a direct line: keep today's port semantics on any deep box with
3951
+ // exactly one endpoint inside its subtree, stubbed to the deepest leaf.
3952
+ var ac = childOfScopeContaining(edge.from, scope);
3953
+ var bc = childOfScopeContaining(edge.to, scope);
3954
+ var aEnt = ac && entryByAnchor[ac.kind + ':' + ac.id];
3955
+ var bEnt = bc && entryByAnchor[bc.kind + ':' + bc.id];
3956
+ var aAid = aEnt ? anchorNodeId(aEnt) : null;
3957
+ var bAid = bEnt ? anchorNodeId(bEnt) : null;
3958
+ if (aAid === bAid) return; // internal to one entry, or neither in scope
3959
+ if (aAid && deepByAnchor[aAid]) {
3960
+ var leafA = deepLeafFor(edge.from, aEnt.id);
3961
+ if (leafA) {
3962
+ var recA = portRec(aAid);
3963
+ var ro = recA.extOut[edge.to] = recA.extOut[edge.to] || { kids: {}, raws: {} };
3964
+ ro.kids[leafA.kind + ':' + leafA.id] = leafA;
3965
+ ro.raws[edge.from] = 1;
3966
+ var poId = 'p~out~' + aAid + '~' + edge.to;
3967
+ recA.stubs[IN(leafA.kind, leafA.id) + '=>' + poId] = { src: IN(leafA.kind, leafA.id), tgt: poId, stub: true };
3968
+ }
3969
+ }
3970
+ if (bAid && deepByAnchor[bAid]) {
3971
+ var leafB = deepLeafFor(edge.to, bEnt.id);
3972
+ if (leafB) {
3973
+ var recB = portRec(bAid);
3974
+ var ri = recB.extIn[edge.from] = recB.extIn[edge.from] || { kids: {}, raws: {} };
3975
+ ri.kids[leafB.kind + ':' + leafB.id] = leafB;
3976
+ ri.raws[edge.to] = 1;
3977
+ var piId = 'p~in~' + bAid + '~' + edge.from;
3978
+ recB.stubs[piId + '=>' + IN(leafB.kind, leafB.id)] = { src: piId, tgt: IN(leafB.kind, leafB.id), stub: true };
3979
+ }
3980
+ }
3981
+ });
3982
+ return { deepByAnchor: deepByAnchor, direct: direct, directTopCount: directTopCount, ports: ports };
3983
+ }
3984
+
3644
3985
  // Micro-layout for a container's direct children when Internals is on:
3645
3986
  // layered mini columns + intra-container edges. Each external relation gets
3646
3987
  // its own small PORT node INSIDE the container (one per external
3647
3988
  // counterpart; incoming left, outgoing right). Children connect to ports
3648
3989
  // with short edges that never leave the box \u2014 the real cross-boundary line
3649
3990
  // is only revealed on hover, or pinned while the port is selected.
3650
- function innerLayout(entry) {
3991
+ function innerLayout(entry, deepCtx) {
3992
+ // Deep-flagged subsystems take the recursive path (deepCtx exists only
3993
+ // when Internals is on and the view has deep entries \u2014 see buildElements).
3994
+ if (deepCtx && entry.kind === 'subsystem' && deepCtx.deepByAnchor[anchorNodeId(entry)]) {
3995
+ return deepLayout(entry, deepCtx.ports[anchorNodeId(entry)]);
3996
+ }
3651
3997
  var kids = state.internals && entry.hasKids ? childrenOf({ kind: entry.kind, id: entry.id }) : [];
3652
3998
  if (!kids.length) return null;
3653
3999
  var scope = { kind: entry.kind, id: entry.id };
@@ -4219,12 +4565,42 @@ var MODEL = __MODEL_JSON__;
4219
4565
  if (scope.kind === 'types' || scope.kind === 'databases') return buildTypeElements();
4220
4566
  var entries = childrenOf(scope);
4221
4567
  var eles = [];
4568
+ var deepCtx = buildDeepContext(scope, entries);
4222
4569
  var ve = viewEdges(scope, entries);
4223
4570
  // Data-coupling overlay: same scoping pipeline, a different edge source.
4224
4571
  var vd = state.dataCoupling ? viewEdges(scope, entries, MODEL.dataEdges) : { agg: {}, ghosts: {} };
4225
4572
  Object.keys(vd.ghosts).forEach(function (g) { if (!ve.ghosts[g]) ve.ghosts[g] = vd.ghosts[g]; });
4226
4573
  var inners = {};
4227
- entries.forEach(function (e) { inners[anchorNodeId(e)] = innerLayout(e); });
4574
+ entries.forEach(function (e) { inners[anchorNodeId(e)] = innerLayout(e, deepCtx); });
4575
+
4576
+ // Deep-mode recursive tile emission: a nested container becomes a cytoscape
4577
+ // compound parent (no explicit position \u2014 a compound derives its bounds
4578
+ // from its children); leaves and EMPTY containers are plain positioned
4579
+ // nodes. ox/oy = the emitting container's absolute top-left; tile.x/y are
4580
+ // centres relative to it, so offsets accumulate top-down. Search dimming
4581
+ // propagates the TOP entry's dim to the whole subtree.
4582
+ function emitDeepTiles(tiles, parentNodeId, ox, oy, dimCls) {
4583
+ tiles.forEach(function (tile) {
4584
+ var ax = ox + tile.x, ay = oy + tile.y;
4585
+ if (tile.sub) {
4586
+ var nid = SN(tile.kid.id);
4587
+ var selCls = state.selectedKind === 'subsystem' && state.selected === tile.kid.id ? ' sel' : '';
4588
+ var nested = {
4589
+ data: { id: nid, parent: parentNodeId, label: nameOf(tile.kid), w: tile.w, h: tile.h, tw: tile.w - 16 },
4590
+ classes: 'subsysBox' + (tile.sub.tiles.length ? ' drillable' : '') + dimCls + selCls,
4591
+ };
4592
+ if (!tile.sub.tiles.length) nested.position = { x: ax, y: ay };
4593
+ eles.push(nested);
4594
+ emitDeepTiles(tile.sub.tiles, nid, ax - tile.w / 2, ay - tile.h / 2, dimCls);
4595
+ } else {
4596
+ eles.push({
4597
+ 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 },
4598
+ position: { x: ax, y: ay },
4599
+ classes: 'inner' + dimCls,
4600
+ });
4601
+ }
4602
+ });
4603
+ }
4228
4604
 
4229
4605
  // Resolve a port's reveal target(s) in THIS view. Preference order: the
4230
4606
  // MATCHING PORT inside the counterpart's container (a port-to-port line
@@ -4356,17 +4732,25 @@ var MODEL = __MODEL_JSON__;
4356
4732
  + (state.showIssues && issuesBySpec[e.id] ? ' hasIssue' : '')
4357
4733
  + (state.selectedKind === e.kind && state.selected === e.id ? ' sel' : '');
4358
4734
  if (inner) {
4359
- 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 });
4360
- inner.tiles.forEach(function (tile) {
4361
- eles.push({
4362
- data: {
4363
- id: IN(tile.kid.kind, tile.kid.id), parent: aid,
4364
- label: nameOf(tile.kid), w: INNER_W, h: INNER_H, tw: INNER_W - 10,
4365
- },
4366
- position: { x: p.x - p.w / 2 + tile.x, y: p.y - p.h / 2 + tile.y },
4367
- classes: 'inner' + (dim ? ' dimmed' : ''),
4735
+ 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 };
4736
+ // An EMPTY deep boundary box has no children, so it is NOT a compound
4737
+ // parent \u2014 it needs (and honours) an explicit position and size.
4738
+ if (inner.deep && !inner.tiles.length && !(inner.proxies && inner.proxies.length)) boxNode.position = { x: p.x, y: p.y };
4739
+ eles.push(boxNode);
4740
+ if (inner.deep) {
4741
+ emitDeepTiles(inner.tiles, aid, p.x - p.w / 2, p.y - p.h / 2, dim ? ' dimmed' : '');
4742
+ } else {
4743
+ inner.tiles.forEach(function (tile) {
4744
+ eles.push({
4745
+ data: {
4746
+ id: IN(tile.kid.kind, tile.kid.id), parent: aid,
4747
+ label: nameOf(tile.kid), w: INNER_W, h: INNER_H, tw: INNER_W - 10,
4748
+ },
4749
+ position: { x: p.x - p.w / 2 + tile.x, y: p.y - p.h / 2 + tile.y },
4750
+ classes: 'inner' + (dim ? ' dimmed' : ''),
4751
+ });
4368
4752
  });
4369
- });
4753
+ }
4370
4754
  (inner.proxies || []).forEach(function (px) {
4371
4755
  eles.push({
4372
4756
  data: {
@@ -4519,9 +4903,28 @@ var MODEL = __MODEL_JSON__;
4519
4903
 
4520
4904
  var dimmedAnchors = {};
4521
4905
  entries.forEach(function (e) { if (state.query && !matches(e)) dimmedAnchors[anchorNodeId(e)] = true; });
4906
+
4907
+ // Deep mode: ONE direct line per related pair of concrete visible nodes \u2014
4908
+ // leaf tiles at any depth and/or top-level component boxes (deduped by
4909
+ // buildDeepContext). These lines cross nested boundaries on purpose.
4910
+ if (deepCtx) {
4911
+ var ddi = 0;
4912
+ Object.keys(deepCtx.direct).sort().forEach(function (key) {
4913
+ var d = deepCtx.direct[key];
4914
+ var ddim = state.query && (dimmedAnchors[d.aTop] || dimmedAnchors[d.bTop]);
4915
+ eles.push({
4916
+ data: { id: 'dd' + (ddi++), source: d.src, target: d.tgt, lbl: '' },
4917
+ classes: 'inneredge' + (d.cross ? ' cross' : '') + (ddim ? ' dimmed' : ''),
4918
+ });
4919
+ });
4920
+ }
4921
+
4522
4922
  var i = 0;
4523
4923
  Object.keys(ve.agg).forEach(function (key) {
4524
4924
  var e = ve.agg[key];
4925
+ // Prefer the leaf lines: drop an aggregated container edge whose
4926
+ // constituent relations were ALL drawn as direct deep lines above.
4927
+ if (deepCtx && deepCtx.directTopCount[key] >= e.n) return;
4525
4928
  var bundle = e.n > 1;
4526
4929
  var dim = state.query && (dimmedAnchors[e.src] || dimmedAnchors[e.tgt]);
4527
4930
  var route = routeData(e.src, e.tgt, key);
@@ -4780,22 +5183,58 @@ var MODEL = __MODEL_JSON__;
4780
5183
  }
4781
5184
  return path;
4782
5185
  }
5186
+ // Crumb compaction (compaction stage 5) is a RENDER MODE, not marker-based
5187
+ // reparenting: renderCrumbs rebuilds #crumbs' innerHTML on every navigation,
5188
+ // so nodes physically moved elsewhere would be destroyed by the next render.
5189
+ // The header-compaction stage toggles crumbsCompact and re-renders; compact
5190
+ // keeps the CURRENT scope visible and folds the ancestors into an ordered
5191
+ // "\\u2026" dropdown (document order, root first) whose entries navigate
5192
+ // exactly like the crumbs they replace.
5193
+ var crumbsCompact = false;
5194
+ var lastCrumbsHtml; // no initializer: the boot render at cy-init time precedes this line
5195
+ // Set by the header-compaction IIFE: crumb re-renders change the header's
5196
+ // CONTENT width without resizing #hdr itself (it is edge-anchored), so the
5197
+ // ResizeObserver never fires for them \u2014 renderCrumbs nudges a reflow here.
5198
+ var headerReflowHook = null;
5199
+ function crumbBtnHtml(p, cur) {
5200
+ return '<button class="crumb' + (cur ? ' cur' : '') + '" data-ck="' + p.kind + '" data-ci="' + (p.id || '') + '">' + p.label + '</button>';
5201
+ }
4783
5202
  function renderCrumbs() {
4784
5203
  var el = document.getElementById('crumbs');
4785
5204
  var path = crumbPath();
4786
- el.innerHTML = path.map(function (p, i) {
4787
- var cur = i === path.length - 1;
4788
- return '<button class="crumb' + (cur ? ' cur' : '') + '" data-ck="' + p.kind + '" data-ci="' + (p.id || '') + '">' + p.label + '</button>'
4789
- + (cur ? '' : '<span class="sep">\\u203A</span>');
4790
- }).join('');
5205
+ var html;
5206
+ if (crumbsCompact && path.length > 1) {
5207
+ html = '<span class="dropdown" id="crumbDd">'
5208
+ + '<button class="crumb crumbmore" id="crumbMoreBtn" title="Show the collapsed ancestor path">\\u2026</button>'
5209
+ + '<span class="menu" id="crumbMenu">'
5210
+ + path.slice(0, path.length - 1).map(function (p) {
5211
+ return '<button data-ck="' + p.kind + '" data-ci="' + (p.id || '') + '">' + p.label + '</button>';
5212
+ }).join('')
5213
+ + '</span></span>'
5214
+ + '<span class="sep">\\u203A</span>'
5215
+ + crumbBtnHtml(path[path.length - 1], true);
5216
+ } else {
5217
+ html = path.map(function (p, i) {
5218
+ var cur = i === path.length - 1;
5219
+ return crumbBtnHtml(p, cur) + (cur ? '' : '<span class="sep">\\u203A</span>');
5220
+ }).join('');
5221
+ }
5222
+ // No-op renders keep the already-wired nodes (and an open "\\u2026" menu)
5223
+ // intact \u2014 and don't churn the reflow scheduler while a search query types.
5224
+ if (html === lastCrumbsHtml) return;
5225
+ lastCrumbsHtml = html;
5226
+ el.innerHTML = html;
4791
5227
  var btns = el.querySelectorAll('button');
4792
5228
  for (var i = 0; i < btns.length; i++) {
4793
5229
  (function (b) {
5230
+ if (!b.getAttribute('data-ck')) return; // the "\\u2026" trigger toggles, never navigates
4794
5231
  b.addEventListener('click', function () {
4795
5232
  navigateTo(b.getAttribute('data-ck'), b.getAttribute('data-ci') || null);
4796
5233
  });
4797
5234
  })(btns[i]);
4798
5235
  }
5236
+ if (crumbsCompact && path.length > 1) wireDropdown('crumbDd', 'crumbMoreBtn');
5237
+ if (headerReflowHook) headerReflowHook();
4799
5238
  }
4800
5239
  function renderViewHint() {
4801
5240
  if (state.view.kind === 'types' || state.view.kind === 'databases') {
@@ -5062,7 +5501,16 @@ var MODEL = __MODEL_JSON__;
5062
5501
  return c ? { kind: 'subsystem', id: c.subsystem } : { kind: 'system', id: null };
5063
5502
  }
5064
5503
 
5065
- document.getElementById('search').addEventListener('input', function (ev) { state.query = ev.target.value.trim(); rebuild(false); });
5504
+ // While the search input is compacted behind the magnifier icon (compaction
5505
+ // stage 3), a non-empty query must stay discoverable \u2014 mark the icon with an
5506
+ // accent dot. The class is kept in sync on every query edit; the dot is only
5507
+ // ever visible while the compact icon itself is.
5508
+ function updateSearchBadge() {
5509
+ var b = document.getElementById('searchBtn');
5510
+ if (b && b.classList) b.classList[state.query ? 'add' : 'remove']('hasq');
5511
+ }
5512
+ document.getElementById('search').addEventListener('input', function (ev) { state.query = ev.target.value.trim(); updateSearchBadge(); rebuild(false); });
5513
+ updateSearchBadge();
5066
5514
  // Sync each View toggle's checkbox from the (possibly persisted) state, then
5067
5515
  // persist on change so the choices survive a refresh (see persist()/saved).
5068
5516
  document.getElementById('internalsToggle').checked = state.internals;
@@ -5116,9 +5564,24 @@ var MODEL = __MODEL_JSON__;
5116
5564
  })(btns[i]);
5117
5565
  }
5118
5566
  })();
5567
+ // Compaction stage 4 replaces the mode tabs with one dropdown trigger; its
5568
+ // label must follow the CURRENT mode. updateHeaderSegs runs on every rebuild,
5569
+ // so a mode change made while compact re-labels the trigger immediately.
5570
+ function updateModeBtn() {
5571
+ var b = document.getElementById('modeBtn');
5572
+ if (!b) return;
5573
+ var lbl = state.view.kind === 'types' ? 'Types' : state.view.kind === 'databases' ? 'Databases' : 'Components';
5574
+ b.textContent = lbl + ' \\u25BE';
5575
+ }
5119
5576
  function updateHeaderSegs() {
5120
5577
  var seg = document.getElementById('modeSeg');
5121
5578
  var btns = seg.querySelectorAll('button');
5579
+ if (!btns.length) {
5580
+ // Compaction stage 4 moved the real tab buttons into the mode dropdown \u2014
5581
+ // keep driving THEIR active classes there (they move back node-identical).
5582
+ var mm = document.getElementById('modeMenu');
5583
+ if (mm && mm.querySelectorAll) btns = mm.querySelectorAll('button');
5584
+ }
5122
5585
  for (var i = 0; i < btns.length; i++) {
5123
5586
  var vm = btns[i].getAttribute('data-vm');
5124
5587
  var active = vm === 'components'
@@ -5126,6 +5589,7 @@ var MODEL = __MODEL_JSON__;
5126
5589
  : vm === state.view.kind;
5127
5590
  if (btns[i].classList) btns[i].classList[active ? 'add' : 'remove']('active');
5128
5591
  }
5592
+ updateModeBtn();
5129
5593
  var td = document.getElementById('typesDetailSeg');
5130
5594
  td.style.display = (state.view.kind === 'types' || state.view.kind === 'databases') ? '' : 'none';
5131
5595
  var tbs = td.querySelectorAll('button');
@@ -5189,7 +5653,13 @@ var MODEL = __MODEL_JSON__;
5189
5653
  var r = btn.getBoundingClientRect();
5190
5654
  menu.style.top = (r.bottom + 6) + 'px';
5191
5655
  menu.style.left = 'auto';
5192
- menu.style.right = Math.max(6, window.innerWidth - r.right) + 'px';
5656
+ // Right-aligned to the trigger, but never pushed off the LEFT edge \u2014 the
5657
+ // compact search / crumb triggers (header compaction) sit on the header's
5658
+ // left side, where a 200px menu right-aligned to a narrow button would clip.
5659
+ var right = Math.max(6, window.innerWidth - r.right);
5660
+ var mw = menu.getBoundingClientRect ? menu.getBoundingClientRect().width : 0;
5661
+ if (mw && window.innerWidth - right - mw < 6) right = Math.max(6, window.innerWidth - mw - 6);
5662
+ menu.style.right = right + 'px';
5193
5663
  }
5194
5664
  function wireDropdown(ddId, btnId) {
5195
5665
  var dd = document.getElementById(ddId);
@@ -5208,11 +5678,29 @@ var MODEL = __MODEL_JSON__;
5208
5678
  var ldd = wireDropdown('layoutDd', 'layoutBtn');
5209
5679
  var sdd = wireDropdown('settingsDd', 'settingsBtn');
5210
5680
  var mdd = wireDropdown('moreDd', 'moreBtn');
5681
+ // Compact stand-ins (header compaction stages 3-4): the search panel and the
5682
+ // mode-tab dropdown are ordinary dropdowns; their triggers stay hidden until
5683
+ // their compaction stage shows them, so wiring them here is inert at full width.
5684
+ var qdd = wireDropdown('searchDd', 'searchBtn');
5685
+ var vdd = wireDropdown('modeDd', 'modeBtn');
5686
+ // Opening the compact search panel focuses the REAL input (stage 3 moves the
5687
+ // node, never clones it, so its input listener keeps driving state.query).
5688
+ // Registered after wireDropdown's toggle, so 'open' reflects the new state.
5689
+ document.getElementById('searchBtn').addEventListener('click', function () {
5690
+ if (String(qdd.className || '').indexOf('open') >= 0) {
5691
+ var inp = document.getElementById('search');
5692
+ if (inp && inp.focus) inp.focus();
5693
+ }
5694
+ });
5211
5695
  // Keep the settings panel open while flipping switches (clicks inside it don't
5212
5696
  // bubble to the document-level close handler).
5213
5697
  (function () {
5214
5698
  var m = document.getElementById('settingsMenu');
5215
5699
  if (m && m.addEventListener) m.addEventListener('click', function (ev) { if (ev && ev.stopPropagation) ev.stopPropagation(); });
5700
+ // Same for the floating search panel: clicking into the input must not
5701
+ // bubble to the document-level close handler and shut the panel mid-typing.
5702
+ var sm = document.getElementById('searchMenu');
5703
+ if (sm && sm.addEventListener) sm.addEventListener('click', function (ev) { if (ev && ev.stopPropagation) ev.stopPropagation(); });
5216
5704
  })();
5217
5705
 
5218
5706
  // Layout picker: choose the auto-layout algorithm. Components use cytoscape's
@@ -5236,6 +5724,12 @@ var MODEL = __MODEL_JSON__;
5236
5724
  });
5237
5725
  updateLayoutBtn();
5238
5726
 
5727
+ // The compact crumb dropdown (compaction stage 5) is re-created by every
5728
+ // compact crumb render, so it is looked up per close instead of captured.
5729
+ function closeCrumbDd() {
5730
+ var cdd = document.getElementById('crumbDd');
5731
+ if (cdd && cdd.classList) cdd.classList.remove('open');
5732
+ }
5239
5733
  if (document.addEventListener) {
5240
5734
  document.addEventListener('click', function () {
5241
5735
  if (dd.classList) dd.classList.remove('open');
@@ -5243,6 +5737,9 @@ var MODEL = __MODEL_JSON__;
5243
5737
  if (ldd.classList) ldd.classList.remove('open');
5244
5738
  if (sdd.classList) sdd.classList.remove('open');
5245
5739
  if (mdd && mdd.classList) mdd.classList.remove('open');
5740
+ if (qdd && qdd.classList) qdd.classList.remove('open');
5741
+ if (vdd && vdd.classList) vdd.classList.remove('open');
5742
+ closeCrumbDd();
5246
5743
  });
5247
5744
  document.addEventListener('keydown', function (ev) {
5248
5745
  if (ev.key === 'Escape') {
@@ -5250,16 +5747,27 @@ var MODEL = __MODEL_JSON__;
5250
5747
  if (modal.classList && String(modal.className).indexOf('open') >= 0) { closeFlow(); return; }
5251
5748
  setPresentation(false);
5252
5749
  if (dd.classList) dd.classList.remove('open');
5750
+ if (qdd && qdd.classList) qdd.classList.remove('open');
5751
+ if (vdd && vdd.classList) vdd.classList.remove('open');
5752
+ closeCrumbDd();
5253
5753
  }
5254
5754
  });
5255
5755
  }
5256
5756
 
5257
- // \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
5258
- // When the floating header no longer fits its controls, trailing items
5259
- // COLLAPSE into the More menu instead of relying on horizontal scroll \u2014
5260
- // every control stays one click away. Whole items move (listeners survive
5261
- // reparenting); a hidden placeholder pins each item's original position so
5262
- // restoring keeps the exact order. Collapse order = least-used first.
5757
+ // \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
5758
+ // When the floating header no longer fits its controls, standard reusable
5759
+ // COMPACTION BEHAVIORS apply progressively \u2014 each stage only while the row
5760
+ // still overflows \u2014 and restore in REVERSE order when space returns:
5761
+ // 1. trailing buttons fold into the "\u22EF" menu (least-used first, one by one)
5762
+ // 2. the View dropdown folds in after them
5763
+ // 3. the search input compacts to a \u{1F50D} icon + floating panel
5764
+ // 4. the mode tabs compact to one current-mode dropdown
5765
+ // 5. ancestor crumbs compact into an ordered "\u2026" dropdown
5766
+ // Stages 1-2 move whole items (listeners survive reparenting) with a hidden
5767
+ // placeholder pinning each item's original spot for restore; stage 5 is a
5768
+ // render mode (renderCrumbs rebuilds its innerHTML, so reparenting would not
5769
+ // survive navigation). Nothing here is persisted \u2014 compaction is purely
5770
+ // responsive to the available width.
5263
5771
  (function () {
5264
5772
  if (typeof window === 'undefined') return;
5265
5773
  var hdr = document.getElementById('hdr');
@@ -5287,7 +5795,122 @@ var MODEL = __MODEL_JSON__;
5287
5795
  }
5288
5796
  return markers[id];
5289
5797
  }
5290
- var collapsed = [];
5798
+ // Fold stage (the classic behavior): move items into the "\u22EF" menu ONE per
5799
+ // apply() call \u2014 the reflow loop keeps a stage active until it reports no
5800
+ // further progress, preserving the original per-button granularity.
5801
+ function foldStage(ids) {
5802
+ var folded = [];
5803
+ return {
5804
+ apply: function () {
5805
+ while (folded.length < ids.length) {
5806
+ var id = ids[folded.length];
5807
+ var el = movableFor(id);
5808
+ if (!el || el === moreDd || el.parentNode === moreMenu) { folded.push({ el: null, marker: null }); continue; }
5809
+ var m = markerFor(id, el);
5810
+ // A dropdown moved while open would strand its fixed-positioned menu.
5811
+ if (el.classList) el.classList.remove('open');
5812
+ moreDd.style.display = '';
5813
+ moreMenu.appendChild(el);
5814
+ folded.push({ el: el, marker: m });
5815
+ return true;
5816
+ }
5817
+ return false;
5818
+ },
5819
+ restore: function () {
5820
+ for (var i = folded.length - 1; i >= 0; i--) {
5821
+ var it = folded[i];
5822
+ if (it.el && it.marker && it.marker.parentNode) it.marker.parentNode.insertBefore(it.el, it.marker);
5823
+ }
5824
+ folded = [];
5825
+ },
5826
+ };
5827
+ }
5828
+ // Stage 3: the search input compacts behind a \u{1F50D} icon; the REAL input node
5829
+ // MOVES into the floating panel (fixed-positioned by the same helper as
5830
+ // every dropdown menu), so its input listener keeps driving state.query.
5831
+ function searchStage() {
5832
+ var on = false;
5833
+ return {
5834
+ apply: function () {
5835
+ if (on) return false;
5836
+ var inp = document.getElementById('search');
5837
+ var ddw = document.getElementById('searchDd');
5838
+ var menu = document.getElementById('searchMenu');
5839
+ if (!inp || !ddw || !menu) return false;
5840
+ menu.appendChild(inp);
5841
+ ddw.style.display = '';
5842
+ on = true;
5843
+ return true;
5844
+ },
5845
+ restore: function () {
5846
+ if (!on) return;
5847
+ on = false;
5848
+ var inp = document.getElementById('search');
5849
+ var ddw = document.getElementById('searchDd');
5850
+ if (inp && ddw && ddw.parentNode) ddw.parentNode.insertBefore(inp, ddw);
5851
+ if (ddw) { ddw.style.display = 'none'; if (ddw.classList) ddw.classList.remove('open'); }
5852
+ },
5853
+ };
5854
+ }
5855
+ // Stage 4: the mode tabs collapse into ONE dropdown labelled with the
5856
+ // current mode. The REAL tab buttons move into its menu (listeners and
5857
+ // active styling survive); updateHeaderSegs keeps the trigger label in
5858
+ // sync when the mode changes while compact.
5859
+ function modeStage() {
5860
+ var moved = [];
5861
+ return {
5862
+ apply: function () {
5863
+ if (moved.length) return false;
5864
+ var seg = document.getElementById('modeSeg');
5865
+ var ddw = document.getElementById('modeDd');
5866
+ var menu = document.getElementById('modeMenu');
5867
+ if (!seg || !ddw || !menu) return false;
5868
+ var btns = seg.querySelectorAll('button');
5869
+ if (!btns.length) return false;
5870
+ for (var i = 0; i < btns.length; i++) moved.push(btns[i]);
5871
+ for (var j = 0; j < moved.length; j++) menu.appendChild(moved[j]);
5872
+ seg.style.display = 'none';
5873
+ ddw.style.display = '';
5874
+ updateModeBtn();
5875
+ return true;
5876
+ },
5877
+ restore: function () {
5878
+ if (!moved.length) return;
5879
+ var seg = document.getElementById('modeSeg');
5880
+ var ddw = document.getElementById('modeDd');
5881
+ for (var i = 0; i < moved.length; i++) seg.appendChild(moved[i]);
5882
+ moved = [];
5883
+ seg.style.display = '';
5884
+ if (ddw) { ddw.style.display = 'none'; if (ddw.classList) ddw.classList.remove('open'); }
5885
+ },
5886
+ };
5887
+ }
5888
+ // Stage 5: crumb compaction is a render-mode toggle consulted by
5889
+ // renderCrumbs itself \u2014 see crumbsCompact there. Never reparenting.
5890
+ function crumbStage() {
5891
+ return {
5892
+ apply: function () {
5893
+ if (crumbsCompact) return false;
5894
+ crumbsCompact = true;
5895
+ renderCrumbs();
5896
+ return true;
5897
+ },
5898
+ restore: function () {
5899
+ if (!crumbsCompact) return;
5900
+ crumbsCompact = false;
5901
+ renderCrumbs();
5902
+ },
5903
+ };
5904
+ }
5905
+ // Ordered compaction stages: applied first-to-last only while the header
5906
+ // overflows, restored last-to-first when space returns.
5907
+ var STAGES = [
5908
+ foldStage(COLLAPSE), // 1: trailing buttons \u2192 "\u22EF" menu
5909
+ foldStage(['settingsBtn']), // 2: the View dropdown folds in too
5910
+ searchStage(), // 3: search input \u2192 \u{1F50D} + floating panel
5911
+ modeStage(), // 4: mode tabs \u2192 current-mode dropdown
5912
+ crumbStage(), // 5: ancestor crumbs \u2192 "\u2026" dropdown
5913
+ ];
5291
5914
  // Signed fit measure in px: positive = overflowing, negative = headroom.
5292
5915
  // The header is a flex row whose ONLY flex:1 child is the .spacer, so the
5293
5916
  // spacer's rendered width IS the free space -- it grows to absorb all slack
@@ -5303,33 +5926,54 @@ var MODEL = __MODEL_JSON__;
5303
5926
  var slack = spacer ? spacer.getBoundingClientRect().width : 0;
5304
5927
  return (hdr.scrollWidth - hdr.clientWidth) - slack;
5305
5928
  }
5929
+ var inReflow = false;
5306
5930
  function reflow() {
5307
5931
  // Not laid out (hidden tab, non-browser DOM) \u2014 measuring would misfire.
5308
5932
  var box = hdr.getBoundingClientRect();
5309
5933
  if (!box || box.width <= 0) return;
5310
- // Restore everything, then collapse until the row fits (idempotent).
5311
- for (var i = collapsed.length - 1; i >= 0; i--) {
5312
- var it = collapsed[i];
5313
- if (it.el && it.marker && it.marker.parentNode) it.marker.parentNode.insertBefore(it.el, it.marker);
5314
- }
5315
- collapsed = [];
5316
- moreDd.style.display = 'none';
5317
- hdr.scrollLeft = 0;
5318
- var guard = 0;
5319
- // Demand a few px of headroom, not a bare fit \u2014 the marginal-fit widths
5320
- // are exactly where the phantom scrollbar appeared.
5321
- while (overflowPx() > -8 && guard < COLLAPSE.length) {
5322
- var id = COLLAPSE[guard++];
5323
- var el = movableFor(id);
5324
- if (!el || el === moreDd || el.parentNode === moreMenu) continue;
5325
- var m = markerFor(id, el);
5326
- // A dropdown moved while open would strand its fixed-positioned menu.
5327
- if (el.classList) el.classList.remove('open');
5328
- moreDd.style.display = '';
5329
- moreMenu.appendChild(el);
5330
- collapsed.push({ el: el, marker: m });
5331
- }
5332
- if (collapsed.length === 0) moreDd.style.display = 'none';
5934
+ inReflow = true;
5935
+ try {
5936
+ // The floating search panel must survive a reflow cycle: restore-all
5937
+ // would close it (and reparenting blurs the input), so capture its
5938
+ // open/focus state up front and reinstate it after the stage walk.
5939
+ var ddw = document.getElementById('searchDd');
5940
+ var inp = document.getElementById('search');
5941
+ var searchOpen = !!(ddw && String(ddw.className || '').indexOf('open') >= 0);
5942
+ var searchFocus = false;
5943
+ try {
5944
+ var ae = (typeof ROOT !== 'undefined' && ROOT ? ROOT : document).activeElement;
5945
+ searchFocus = !!(ae && inp && ae === inp);
5946
+ } catch (e) { /* stubbed DOM */ }
5947
+ // Restore every stage in REVERSE order, then re-apply progressively
5948
+ // while the row still overflows (idempotent).
5949
+ for (var i = STAGES.length - 1; i >= 0; i--) STAGES[i].restore();
5950
+ moreDd.style.display = 'none';
5951
+ hdr.scrollLeft = 0;
5952
+ // Demand a few px of headroom, not a bare fit \u2014 the marginal-fit widths
5953
+ // are exactly where the phantom scrollbar appeared. Every apply()
5954
+ // changes the very widths being measured, but restore-all + a strictly
5955
+ // forward stage walk make the outcome a pure function of the current
5956
+ // width, and reflow never reschedules itself (renderCrumbs' nudge is
5957
+ // suppressed via inReflow, and #hdr's own box never changes here), so
5958
+ // boundary widths settle in ONE pass instead of oscillating.
5959
+ var si = 0;
5960
+ var guard = 0;
5961
+ var bound = COLLAPSE.length + STAGES.length + 8;
5962
+ while (overflowPx() > -8 && si < STAGES.length && guard < bound) {
5963
+ guard++;
5964
+ if (!STAGES[si].apply()) si++;
5965
+ }
5966
+ if (searchOpen || searchFocus) {
5967
+ var compactNow = ddw && ddw.style && ddw.style.display !== 'none';
5968
+ if (compactNow && searchOpen) {
5969
+ if (ddw.classList) ddw.classList.add('open');
5970
+ positionDropdownMenu(ddw, document.getElementById('searchBtn'));
5971
+ }
5972
+ if (searchFocus && inp && inp.focus) inp.focus();
5973
+ }
5974
+ } finally {
5975
+ inReflow = false;
5976
+ }
5333
5977
  }
5334
5978
  var raf = null;
5335
5979
  var defer = window.requestAnimationFrame
@@ -5339,6 +5983,9 @@ var MODEL = __MODEL_JSON__;
5339
5983
  if (raf !== null) return;
5340
5984
  raf = defer(function () { raf = null; reflow(); });
5341
5985
  }
5986
+ // Crumb re-renders change the header's content width without resizing #hdr
5987
+ // itself \u2014 renderCrumbs nudges a reflow through this hook (no-op mid-reflow).
5988
+ headerReflowHook = function () { if (!inReflow) schedule(); };
5342
5989
  if (typeof ResizeObserver !== 'undefined') {
5343
5990
  new ResizeObserver(schedule).observe(hdr);
5344
5991
  } else if (window.addEventListener) {
@@ -6869,47 +7516,6 @@ var init_filenames = __esm({
6869
7516
  }
6870
7517
  });
6871
7518
 
6872
- // src/core/statehash.ts
6873
- function computeStateId() {
6874
- const tree = {
6875
- system: loadSystemSpec(),
6876
- subsystems: loadSubsystemSpecs(),
6877
- components: loadComponentSpecs(),
6878
- interfaces: loadInterfaceSpecs(),
6879
- implementations: loadImplementationSpecs(),
6880
- types: loadTypeSpecs()
6881
- };
6882
- const digest = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
6883
- return { algorithm: "sha256", digest };
6884
- }
6885
- function stateIdEquals(a, b) {
6886
- return !!a && !!b && a.algorithm === b.algorithm && a.digest === b.digest;
6887
- }
6888
- function canonicalize(value) {
6889
- return JSON.stringify(sortKeys(value));
6890
- }
6891
- function sortKeys(v) {
6892
- if (Array.isArray(v)) return v.map(sortKeys);
6893
- if (v && typeof v === "object") {
6894
- const src = v;
6895
- const out = {};
6896
- for (const k of Object.keys(src).sort()) {
6897
- if (k === "createdAt" || k === "updatedAt") continue;
6898
- out[k] = sortKeys(src[k]);
6899
- }
6900
- return out;
6901
- }
6902
- return v;
6903
- }
6904
- var crypto;
6905
- var init_statehash = __esm({
6906
- "src/core/statehash.ts"() {
6907
- "use strict";
6908
- crypto = __toESM(require("crypto"));
6909
- init_specs2();
6910
- }
6911
- });
6912
-
6913
7519
  // src/core/openapi.ts
6914
7520
  function schemaFor(typeRef, closureIds) {
6915
7521
  const trimmed = typeRef.trim().replace(/^promise\s*<(.+)>$/i, "$1").trim();
@@ -7363,6 +7969,57 @@ function projectOwnSurface(maxAudience) {
7363
7969
  function projectChildSurface() {
7364
7970
  return projectOwnSurface("project");
7365
7971
  }
7972
+ function localName(id) {
7973
+ return id.split("::").pop();
7974
+ }
7975
+ function projectSubsystemSurface(subsystemId) {
7976
+ const system = loadSystemSpec();
7977
+ if (!system) {
7978
+ throw new Error("Cannot project a subsystem surface: the L0 system spec is missing.");
7979
+ }
7980
+ const subsystems = loadSubsystemSpecs();
7981
+ const target = subsystems.find((s) => s.id === subsystemId);
7982
+ if (!target) {
7983
+ throw new Error(`Cannot project a subsystem surface: subsystem "${subsystemId}" does not exist.`);
7984
+ }
7985
+ const components = loadComponentSpecs();
7986
+ const interfaces = loadInterfaceSpecs();
7987
+ const types = loadTypeSpecs();
7988
+ const entries = [];
7989
+ for (const pub of target.publicInterfaces ?? []) {
7990
+ if (!pub.component) continue;
7991
+ const comp = components.find((c) => c.id === pub.component || c.id === `${subsystemId}::${pub.component}`);
7992
+ if (!comp) continue;
7993
+ if (comp.componentType !== "Portal") continue;
7994
+ const compInterfaces = interfaces.filter((i) => i.component === comp.id && (!pub.interface || i.id === pub.interface || i.id === `${subsystemId}::${pub.interface}`));
7995
+ const methods = compInterfaces.flatMap((i) => i.methods);
7996
+ entries.push({
7997
+ id: localName(pub.interface ?? comp.id),
7998
+ name: comp.name,
7999
+ // Family ceiling: a sibling surface is consumable by the system family only.
8000
+ audience: "project",
8001
+ type: pub.type ?? "Custom",
8002
+ // The snapshot carries the LOCAL portal name — consumers resolve cross-tree
8003
+ // refs by their final segment.
8004
+ component: localName(comp.id),
8005
+ methods,
8006
+ ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
8007
+ // Project the backing Portal's auth + basePath so the codec can emit
8008
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
8009
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
8010
+ ...comp.basePath ? { basePath: comp.basePath } : {},
8011
+ details: pub.details ?? ""
8012
+ });
8013
+ }
8014
+ return SurfaceSnapshotSchema.parse({
8015
+ projectName: `${system.name}::${subsystemId}`,
8016
+ origin: "generated",
8017
+ stateId: stateIdString(),
8018
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
8019
+ interfaces: entries,
8020
+ types: computeTypeClosure(entries, types)
8021
+ });
8022
+ }
7366
8023
  function listSnapshots(rootDir = getProjectRoot()) {
7367
8024
  const dir = surfacesDir(rootDir);
7368
8025
  if (!fs9.existsSync(dir)) return [];
@@ -7379,10 +8036,13 @@ function listSnapshots(rootDir = getProjectRoot()) {
7379
8036
  function getSnapshot(projectName, rootDir = getProjectRoot()) {
7380
8037
  return listSnapshots(rootDir).find((s) => s.projectName === projectName) ?? null;
7381
8038
  }
8039
+ function snapshotFilename(projectName) {
8040
+ return `${safeFilenamePart(projectName)}.yaml`;
8041
+ }
7382
8042
  function saveSnapshot(snapshot, rootDir = getProjectRoot()) {
7383
8043
  const dir = surfacesDir(rootDir);
7384
8044
  fs9.mkdirSync(dir, { recursive: true });
7385
- const p = path10.join(dir, `${snapshot.projectName}.yaml`);
8045
+ const p = path10.join(dir, snapshotFilename(snapshot.projectName));
7386
8046
  writeYamlFile(p, SurfaceSnapshotSchema.parse(snapshot));
7387
8047
  return p;
7388
8048
  }
@@ -7455,17 +8115,54 @@ function importSurface(sourcePath, origin) {
7455
8115
  return snapshot;
7456
8116
  }
7457
8117
  function generateChildSnapshots(rootDir = getProjectRoot()) {
7458
- const children = loadSubsystemSpecs().filter((s) => s.projectPath && !s.id.includes("::"));
8118
+ const topLevel = loadSubsystemSpecs().filter((s) => !s.id.includes("::"));
8119
+ const children = topLevel.filter((s) => s.projectPath);
7459
8120
  if (!children.length) return [];
7460
- const snapshot = projectChildSurface();
8121
+ const familySnapshot = projectChildSurface();
8122
+ const siblingSnapshots = /* @__PURE__ */ new Map();
8123
+ const siblingSurface = (subsystemId) => {
8124
+ let snap = siblingSnapshots.get(subsystemId);
8125
+ if (!snap) {
8126
+ snap = projectSubsystemSurface(subsystemId);
8127
+ siblingSnapshots.set(subsystemId, snap);
8128
+ }
8129
+ return snap;
8130
+ };
7461
8131
  const written = [];
7462
8132
  for (const child of children) {
7463
8133
  const childDir = path10.resolve(rootDir, child.projectPath);
7464
8134
  if (!fs9.existsSync(childDir)) continue;
7465
- written.push(saveSnapshot(snapshot, childDir));
8135
+ written.push(saveSnapshot(familySnapshot, childDir));
8136
+ for (const sibling of topLevel) {
8137
+ if (sibling.id === child.id) continue;
8138
+ written.push(saveSnapshot(siblingSurface(sibling.id), childDir));
8139
+ }
7466
8140
  }
7467
8141
  return written;
7468
8142
  }
8143
+ function computeParentStateId(parentRoot) {
8144
+ return computeStateIdAt(parentRoot);
8145
+ }
8146
+ function listExternalInterfaces() {
8147
+ const snapshots = listSnapshots();
8148
+ const chainingParent = resolveChainingParent();
8149
+ const parentStateId = chainingParent ? computeParentStateId(chainingParent.parentRoot) : null;
8150
+ return snapshots.map((snapshot) => {
8151
+ const generated = snapshot.origin === "generated";
8152
+ const sourceKind = !generated ? "foreign" : snapshot.projectName.includes("::") ? "sibling" : "parent";
8153
+ const freshness = generated && parentStateId ? snapshot.stateId === parentStateId ? "fresh" : "stale" : "unverifiable";
8154
+ return {
8155
+ projectName: snapshot.projectName,
8156
+ origin: snapshot.origin,
8157
+ sourceKind,
8158
+ generatedAt: snapshot.generatedAt,
8159
+ ...snapshot.stateId ? { stateId: snapshot.stateId } : {},
8160
+ ...snapshot.version ? { version: snapshot.version } : {},
8161
+ freshness,
8162
+ interfaceIds: snapshot.interfaces.map((e) => e.id)
8163
+ };
8164
+ });
8165
+ }
7469
8166
  function surfaceContentKey(snapshot) {
7470
8167
  const { stateId, generatedAt, origin, ...content } = snapshot;
7471
8168
  return JSON.stringify(content);
@@ -7674,7 +8371,8 @@ var init_contracts = __esm({
7674
8371
  "SURFACE_REF_NOT_EXPOSED",
7675
8372
  `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}".`,
7676
8373
  impl.id,
7677
- isDraftCtx
8374
+ isDraftCtx,
8375
+ true
7678
8376
  );
7679
8377
  }
7680
8378
  continue;
@@ -7731,7 +8429,8 @@ var init_contracts = __esm({
7731
8429
  "SURFACE_REF_NOT_EXPOSED",
7732
8430
  `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}".`,
7733
8431
  impl.id,
7734
- isDraftCtx
8432
+ isDraftCtx,
8433
+ true
7735
8434
  );
7736
8435
  } else if (step.assertsGuarantees) {
7737
8436
  const declared = new Set(surfaceMethod.guarantees ?? []);
@@ -7742,7 +8441,8 @@ var init_contracts = __esm({
7742
8441
  "NARRATIVE_SEMANTIC_UNBACKED",
7743
8442
  `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}".`,
7744
8443
  impl.id,
7745
- isDraftCtx
8444
+ isDraftCtx,
8445
+ true
7746
8446
  );
7747
8447
  }
7748
8448
  }
@@ -9230,7 +9930,8 @@ var init_stereotype_deps = __esm({
9230
9930
  "CROSS_SUBSYSTEM_NON_ADAPTER",
9231
9931
  `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.`,
9232
9932
  comp.id,
9233
- isDraftCtx
9933
+ isDraftCtx,
9934
+ true
9234
9935
  );
9235
9936
  }
9236
9937
  continue;
@@ -11093,11 +11794,11 @@ var init_narrative_antipatterns = __esm({
11093
11794
  const memberEdges = keys.flatMap((k) => (adjacency.get(k) ?? []).filter((e) => inScc.has(e.toKey)));
11094
11795
  if (memberEdges.length === 0) continue;
11095
11796
  const anchor = [...memberEdges].sort((a, b) => a.fromKey.localeCompare(b.fromKey))[0];
11096
- const path62 = [...keys].sort().join(" \u2192 ");
11797
+ const path61 = [...keys].sort().join(" \u2192 ");
11097
11798
  ctx.addIssue(
11098
11799
  "warning",
11099
11800
  "UNCONDITIONAL_CALL_CYCLE",
11100
- `Call cycle with no guard: ${path62} \u2014 every call edge in this cycle is unavoidable on all paths of its narrative (e.g. step ${anchor.stepNumber} of "${anchor.methodName}" in "${anchor.impl.id}" always calls ${anchor.toLabel}). This recurses without a base case, by construction. Guard at least one edge with a branch/return before the call, or lint.allow with the termination argument.`,
11801
+ `Call cycle with no guard: ${path61} \u2014 every call edge in this cycle is unavoidable on all paths of its narrative (e.g. step ${anchor.stepNumber} of "${anchor.methodName}" in "${anchor.impl.id}" always calls ${anchor.toLabel}). This recurses without a base case, by construction. Guard at least one edge with a branch/return before the call, or lint.allow with the termination argument.`,
11101
11802
  anchor.impl.id,
11102
11803
  memberEdges.some((e) => ctx.isImplementationDraft(e.impl))
11103
11804
  );
@@ -12484,9 +13185,15 @@ function buildRuleContext(opts) {
12484
13185
  ...[...SDD_RULES, ...extensions.rules].flatMap((r) => r.codes.map((c) => c.code)),
12485
13186
  // Declarative assertions bring their own namespaced codes — lint.allow
12486
13187
  // and severity overrides treat them exactly like builtins.
12487
- ...extensions.assertions.map((a) => a.fullCode)
13188
+ ...extensions.assertions.map((a) => a.fullCode),
13189
+ // Entry-point emitted codes: validateSddTree's chained-subproject pass
13190
+ // raises these AFTER the rule run (it post-processes the aggregated issue
13191
+ // list), so no registered rule declares them — but lint.allow validation
13192
+ // must still recognize them as real codes.
13193
+ "CHAINED_SUBPROJECT_CONTEXT",
13194
+ "UNVERIFIED_EXTERNAL_REF"
12488
13195
  ]);
12489
- const addIssue = (defaultSeverity, code, message, specId, isDraftContext) => {
13196
+ const addIssue = (defaultSeverity, code, message, specId, isDraftContext, surfaceResolved) => {
12490
13197
  if (scopeSubsystem && specId && !isSpecInScope(specId)) {
12491
13198
  return;
12492
13199
  }
@@ -12504,7 +13211,14 @@ function buildRuleContext(opts) {
12504
13211
  if (severity === "warning") return;
12505
13212
  }
12506
13213
  }
12507
- issues.push({ severity, code, message, specId, ...isDraftContext ? { draftContext: true } : {} });
13214
+ issues.push({
13215
+ severity,
13216
+ code,
13217
+ message,
13218
+ specId,
13219
+ ...isDraftContext ? { draftContext: true } : {},
13220
+ ...surfaceResolved ? { surfaceResolved: true } : {}
13221
+ });
12508
13222
  };
12509
13223
  return {
12510
13224
  system,
@@ -12929,24 +13643,54 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
12929
13643
  for (const rule of ruleSequence()) {
12930
13644
  rule.check(ctx);
12931
13645
  }
12932
- const hasCrossTreeSuspects = issues.some((i) => SUBPROJECT_LENIENT_CODES.has(i.code));
13646
+ const hasCrossTreeSuspects = issues.some(
13647
+ (i) => SUBPROJECT_REFERENCE_CODES.has(i.code) || SUBPROJECT_CONFORMANCE_CODES.has(i.code)
13648
+ );
12933
13649
  const chainingParent = hasCrossTreeSuspects ? findChainingParent(getProjectRoot()) : null;
12934
13650
  if (chainingParent) {
13651
+ let unverified = 0;
12935
13652
  let downgraded = 0;
12936
- for (const iss of issues) {
12937
- if (!SUBPROJECT_LENIENT_CODES.has(iss.code)) continue;
12938
- if (iss.severity === "error") {
12939
- iss.severity = "warning";
12940
- downgraded++;
13653
+ for (let at = 0; at < issues.length; at++) {
13654
+ const iss = issues[at];
13655
+ if (SUBPROJECT_REFERENCE_CODES.has(iss.code) && !iss.surfaceResolved) {
13656
+ issues[at] = {
13657
+ severity: "warning",
13658
+ code: "UNVERIFIED_EXTERNAL_REF",
13659
+ crossTreeContext: true,
13660
+ // --ci waives it (parent root is authoritative)
13661
+ specId: iss.specId,
13662
+ ...iss.agentId ? { agentId: iss.agentId } : {},
13663
+ ...iss.draftContext ? { draftContext: true } : {},
13664
+ 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.`
13665
+ };
13666
+ unverified++;
13667
+ continue;
13668
+ }
13669
+ if (SUBPROJECT_CONFORMANCE_CODES.has(iss.code)) {
13670
+ if (iss.severity === "error") {
13671
+ iss.severity = "warning";
13672
+ downgraded++;
13673
+ }
13674
+ iss.crossTreeContext = true;
12941
13675
  }
12942
- iss.crossTreeContext = true;
12943
13676
  }
12944
- if (downgraded > 0) {
13677
+ if (unverified > 0 || downgraded > 0) {
13678
+ const notes = [];
13679
+ if (unverified > 0) {
13680
+ notes.push(
13681
+ `${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.`
13682
+ );
13683
+ }
13684
+ if (downgraded > 0) {
13685
+ notes.push(
13686
+ `${downgraded} code\u2194spec conformance finding(s) (parent-root-relative source paths) were downgraded to warnings.`
13687
+ );
13688
+ }
12945
13689
  issues.unshift({
12946
13690
  severity: "warning",
12947
13691
  code: "CHAINED_SUBPROJECT_CONTEXT",
12948
13692
  crossTreeContext: true,
12949
- 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.`
13693
+ 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.`
12950
13694
  });
12951
13695
  }
12952
13696
  }
@@ -12963,7 +13707,7 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
12963
13707
  function validateAsComplete(options) {
12964
13708
  return validateSddTree({ ...options ?? {}, treatAllAsComplete: true });
12965
13709
  }
12966
- var SUBPROJECT_LENIENT_CODES;
13710
+ var SUBPROJECT_REFERENCE_CODES, SUBPROJECT_CONFORMANCE_CODES;
12967
13711
  var init_validation = __esm({
12968
13712
  "src/core/validation.ts"() {
12969
13713
  "use strict";
@@ -12976,8 +13720,7 @@ var init_validation = __esm({
12976
13720
  init_source_analysis();
12977
13721
  init_specs2();
12978
13722
  init_fs();
12979
- SUBPROJECT_LENIENT_CODES = /* @__PURE__ */ new Set([
12980
- // reference resolution
13723
+ SUBPROJECT_REFERENCE_CODES = /* @__PURE__ */ new Set([
12981
13724
  "UNDEFINED_TYPE_REFERENCE",
12982
13725
  "INVALID_DEPENDENCY_REFERENCE",
12983
13726
  "INVALID_TARGET_COMPONENT_REFERENCE",
@@ -12985,8 +13728,9 @@ var init_validation = __esm({
12985
13728
  "UNDECLARED_DEPENDENCY_CALL",
12986
13729
  "INVALID_TRUSTED_LINK",
12987
13730
  "CROSS_SUBSYSTEM_NON_ADAPTER",
12988
- "CROSS_TREE_REF_UNRESOLVED",
12989
- // code↔spec conformance (root-relative sourcePaths / import graph)
13731
+ "CROSS_TREE_REF_UNRESOLVED"
13732
+ ]);
13733
+ SUBPROJECT_CONFORMANCE_CODES = /* @__PURE__ */ new Set([
12990
13734
  "MISSING_SOURCE_FILE",
12991
13735
  "SOURCE_PATH_ESCAPES_ROOT",
12992
13736
  "MISSING_SOURCE_PATH",
@@ -13528,6 +14272,7 @@ __export(specs_exports, {
13528
14272
  buildProjectGraph: () => buildProjectGraph,
13529
14273
  clearLoaderIssues: () => clearLoaderIssues,
13530
14274
  collectPromotableSpecs: () => collectPromotableSpecs,
14275
+ computeStateIdAt: () => computeStateIdAt,
13531
14276
  deleteComponentSpec: () => deleteComponentSpec,
13532
14277
  deleteGroupSpec: () => deleteGroupSpec,
13533
14278
  deleteImplementationSpec: () => deleteImplementationSpec,
@@ -13560,6 +14305,7 @@ __export(specs_exports, {
13560
14305
  loadTypeSpec: () => loadTypeSpec,
13561
14306
  loadTypeSpecs: () => loadTypeSpecs,
13562
14307
  normalizeComponentLayout: () => normalizeComponentLayout,
14308
+ resolveChainingParent: () => resolveChainingParent,
13563
14309
  resolveSubprojectForNamespace: () => resolveSubprojectForNamespace,
13564
14310
  restoreSpecFiles: () => restoreSpecFiles,
13565
14311
  saveComponentSpec: () => saveComponentSpec,
@@ -13964,6 +14710,19 @@ function dryRunSerializeSpecs(include) {
13964
14710
  function buildProjectGraph(level) {
13965
14711
  return buildGraphModel(level);
13966
14712
  }
14713
+ function resolveChainingParent() {
14714
+ return findChainingParent(getProjectRoot());
14715
+ }
14716
+ function computeStateIdAt(root) {
14717
+ const resolved = path14.resolve(root);
14718
+ return runWithProjectRoot(resolved, () => {
14719
+ workspaceFor(resolved).invalidate();
14720
+ const system = loadSystemSpec();
14721
+ if (!system) return null;
14722
+ const s = computeStateId();
14723
+ return `${s.algorithm}:${s.digest}`;
14724
+ });
14725
+ }
13967
14726
  function deleteTypeSpec(id) {
13968
14727
  return current().deleteTypeSpec(id);
13969
14728
  }
@@ -14007,6 +14766,7 @@ var init_specs2 = __esm({
14007
14766
  path14 = __toESM(require("path"));
14008
14767
  init_loader();
14009
14768
  init_fs();
14769
+ init_statehash();
14010
14770
  init_yaml();
14011
14771
  init_models();
14012
14772
  init_narrative_labels();
@@ -16600,6 +17360,9 @@ function requireSpecs() {
16600
17360
  function requireProvision() {
16601
17361
  return init_provision(), __toCommonJS(provision_exports);
16602
17362
  }
17363
+ function listExternalInterfaces2() {
17364
+ return listExternalInterfaces();
17365
+ }
16603
17366
  function text(content) {
16604
17367
  return { content: [{ type: "text", text: content }] };
16605
17368
  }
@@ -17566,7 +18329,36 @@ NOTICE:
17566
18329
  }
17567
18330
  }
17568
18331
  );
18332
+ reg(
18333
+ server,
18334
+ "sdd_list_external_interfaces",
18335
+ {
18336
+ description: "List the bound project's consumable external surfaces (parent family, siblings, foreign imports) as discovery entries with origin, provenance, and freshness \u2014 the tool an agent inside a subproject uses to SEE its outward world instead of discovering it by failed reference resolution. Full contracts stay in the vendored snapshots (.wai/surfaces/); each entry summarizes the interface ids it exposes."
18337
+ },
18338
+ () => {
18339
+ try {
18340
+ return json(listExternalInterfaces2());
18341
+ } catch (e) {
18342
+ return errText(String(e));
18343
+ }
18344
+ }
18345
+ );
17569
18346
  registerSkillResources(server);
18347
+ try {
18348
+ const chainingParent = resolveChainingParent();
18349
+ if (chainingParent) {
18350
+ let externalSurfaceCount = 0;
18351
+ try {
18352
+ externalSurfaceCount = listExternalInterfaces2().length;
18353
+ } catch {
18354
+ }
18355
+ process.stderr.write(
18356
+ `[wairon mcp] chained subproject: this root is mounted as subsystem "${chainingParent.subsystemId}" of the parent project at ${chainingParent.parentRoot} \u2014 ${externalSurfaceCount} vendored external surface(s) discoverable via sdd_list_external_interfaces
18357
+ `
18358
+ );
18359
+ }
18360
+ } catch {
18361
+ }
17570
18362
  if (options.hostedTools) {
17571
18363
  const hostedStub = () => errText("This hosted tool is dispatched by the hosting data plane before reaching the MCP server; it is unavailable outside a hosted request.");
17572
18364
  reg(server, "sdd_host_lock_project", {
@@ -17702,6 +18494,8 @@ var init_server = __esm({
17702
18494
  init_narrative_labels();
17703
18495
  init_specs();
17704
18496
  init_skills();
18497
+ init_specs2();
18498
+ init_surfaces();
17705
18499
  SERVER_BUILD_STAMP = captureBuildStamp(__filename);
17706
18500
  STALE_SERVER_WARNING = "\n\n\u26A0 STALE SERVER: the wairon build on disk changed after this MCP server started. Restart the MCP session (e.g. /mcp reconnect) before further spec edits \u2014 writes through a stale server can silently drop fields introduced by newer schemas.";
17707
18501
  SKILL_RESOURCE_MIME = "text/markdown";
@@ -20775,7 +21569,7 @@ var require_dist = __commonJS({
20775
21569
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
20776
21570
  }
20777
21571
  var fs51 = __toESM2(require("fs"));
20778
- var path62 = __toESM2(require("path"));
21572
+ var path61 = __toESM2(require("path"));
20779
21573
  var import_fflate = require_node();
20780
21574
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".hg", ".svn"]);
20781
21575
  function listEntries(archive) {
@@ -20813,8 +21607,8 @@ var require_dist = __commonJS({
20813
21607
  }
20814
21608
  function writeTree(destDir, files) {
20815
21609
  for (const file of files) {
20816
- const absolute = path62.join(destDir, file.path);
20817
- fs51.mkdirSync(path62.dirname(absolute), { recursive: true });
21610
+ const absolute = path61.join(destDir, file.path);
21611
+ fs51.mkdirSync(path61.dirname(absolute), { recursive: true });
20818
21612
  fs51.writeFileSync(absolute, file.contents);
20819
21613
  }
20820
21614
  }
@@ -20827,10 +21621,10 @@ var require_dist = __commonJS({
20827
21621
  for (const entry of fs51.readdirSync(current2, { withFileTypes: true })) {
20828
21622
  if (entry.isDirectory()) {
20829
21623
  if (SKIP_DIRS.has(entry.name)) continue;
20830
- walkPackDir(root, path62.join(current2, entry.name), out);
21624
+ walkPackDir(root, path61.join(current2, entry.name), out);
20831
21625
  } else if (entry.isFile()) {
20832
- const absolute = path62.join(current2, entry.name);
20833
- const relative22 = path62.relative(root, absolute).split(path62.sep).join("/");
21626
+ const absolute = path61.join(current2, entry.name);
21627
+ const relative22 = path61.relative(root, absolute).split(path61.sep).join("/");
20834
21628
  out.push({ path: relative22, contents: fs51.readFileSync(absolute) });
20835
21629
  }
20836
21630
  }
@@ -22241,86 +23035,216 @@ async function generateLayer(options = {}) {
22241
23035
  }
22242
23036
 
22243
23037
  // src/commands/lock.ts
23038
+ var os7 = __toESM(require("os"));
22244
23039
  var import_inquirer2 = __toESM(require("inquirer"));
22245
23040
  init_logger();
22246
- init_loader();
22247
- init_fs();
22248
- init_validation();
22249
- init_specs2();
22250
- async function runLock(options = {}) {
22251
- assertProjectInitialized();
22252
- if (!pathExists(AI_PATHS.specsSystem())) {
22253
- logger.error("No SDD spec tree found (.wai/specs). Nothing to lock.");
22254
- process.exit(1);
23041
+ init_defaults();
23042
+
23043
+ // src/core/detection.ts
23044
+ var fs18 = __toESM(require("fs"));
23045
+ var path27 = __toESM(require("path"));
23046
+ init_defaults();
23047
+ var PACKAGE_MARKERS = [
23048
+ "package.json",
23049
+ "pyproject.toml",
23050
+ "Cargo.toml",
23051
+ "go.mod",
23052
+ "build.gradle",
23053
+ "build.gradle.kts",
23054
+ "pom.xml"
23055
+ ];
23056
+ var MAX_SCAN_DEPTH = 5;
23057
+ function detectDomainCandidates(projectRoot2, alreadyTrackedPaths = /* @__PURE__ */ new Set(), alreadyTrackedIds = /* @__PURE__ */ new Set()) {
23058
+ const candidates = /* @__PURE__ */ new Map();
23059
+ for (const c of detectGitSubmodules(projectRoot2)) {
23060
+ candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
22255
23061
  }
22256
- const projectConfig = loadProjectConfig();
22257
- logger.info("Analyzing and validating specifications in-memory...");
22258
- const index = scanAllSpecs({ recursive: options.recursive ?? true });
22259
- const promotable = collectPromotableSpecs(options.subsystem);
22260
- const originalStatuses = /* @__PURE__ */ new Map();
22261
- const isSpecInSubsystemScope = (specSubsystem) => {
22262
- if (!options.subsystem) return true;
22263
- if (!specSubsystem) return false;
22264
- return specSubsystem === options.subsystem || specSubsystem.startsWith(`${options.subsystem}::`);
22265
- };
22266
- for (const s of index.subsystems) {
22267
- if (!options.subsystem || s.id === options.subsystem || s.id.startsWith(`${options.subsystem}::`)) {
22268
- originalStatuses.set(s, s.status);
22269
- s.status = "complete";
23062
+ for (const c of detectNestedGitRepos(projectRoot2)) {
23063
+ if (!candidates.has(c.path)) {
23064
+ candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
22270
23065
  }
22271
23066
  }
22272
- for (const c of index.components) {
22273
- if (isSpecInSubsystemScope(c.subsystem)) {
22274
- originalStatuses.set(c, c.status);
22275
- c.status = "complete";
22276
- }
23067
+ const gitPaths = new Set(
23068
+ Array.from(candidates.values()).filter((c) => c.type === "git-submodule" || c.type === "git-repo").map((c) => c.path)
23069
+ );
23070
+ for (const c of detectPackageRoots(projectRoot2)) {
23071
+ if (candidates.has(c.path)) continue;
23072
+ const insideGit = Array.from(gitPaths).some(
23073
+ (gp) => c.path === gp || c.path.startsWith(gp + "/")
23074
+ );
23075
+ if (insideGit) continue;
23076
+ candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
22277
23077
  }
22278
- for (const i of index.interfaces) {
22279
- const comp = index.components.find((c) => c.id === i.component);
22280
- if (comp && isSpecInSubsystemScope(comp.subsystem)) {
22281
- originalStatuses.set(i, i.status);
22282
- i.status = "complete";
22283
- }
23078
+ const sorted = Array.from(candidates.values()).sort((a, b) => a.path.localeCompare(b.path));
23079
+ return deduplicateIds(sorted, alreadyTrackedIds);
23080
+ }
23081
+ function deduplicateIds(candidates, existingIds = /* @__PURE__ */ new Set()) {
23082
+ const idCount = /* @__PURE__ */ new Map();
23083
+ for (const id of existingIds) {
23084
+ idCount.set(id, (idCount.get(id) ?? 0) + 1);
22284
23085
  }
22285
- for (const m of index.implementations) {
22286
- const intf = index.interfaces.find((i) => i.id === m.contract);
22287
- const comp = intf ? index.components.find((c) => c.id === intf.component) : null;
22288
- if (comp && isSpecInSubsystemScope(comp.subsystem)) {
22289
- originalStatuses.set(m, m.status);
22290
- m.status = "complete";
22291
- }
23086
+ for (const c of candidates) {
23087
+ idCount.set(c.suggestedId, (idCount.get(c.suggestedId) ?? 0) + 1);
22292
23088
  }
22293
- const dry = validateSddTree({
22294
- rules: projectConfig.rules,
22295
- projectType: projectConfig.projectType,
22296
- scopeSubsystem: options.subsystem,
22297
- recursive: options.recursive ?? true
23089
+ return candidates.map((c) => {
23090
+ if ((idCount.get(c.suggestedId) ?? 0) <= 1) return c;
23091
+ const parts = c.path.split("/");
23092
+ const qualifiedId2 = parts.length >= 2 ? pathToId(`${parts[parts.length - 2]}-${parts[parts.length - 1]}`) : c.suggestedId;
23093
+ return { ...c, suggestedId: qualifiedId2 };
22298
23094
  });
22299
- for (const [spec, status2] of originalStatuses.entries()) {
22300
- spec.status = status2;
23095
+ }
23096
+ function parseGitmodules(filePath) {
23097
+ const content = fs18.readFileSync(filePath, "utf-8");
23098
+ const entries = [];
23099
+ let current2 = {};
23100
+ for (const line2 of content.split("\n")) {
23101
+ const trimmed = line2.trim();
23102
+ const headerMatch = trimmed.match(/^\[submodule "(.+)"\]$/);
23103
+ if (headerMatch) {
23104
+ if (current2.path) entries.push(current2);
23105
+ current2 = { name: headerMatch[1] };
23106
+ continue;
23107
+ }
23108
+ const keyVal = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
23109
+ if (keyVal) {
23110
+ const [, key, value] = keyVal;
23111
+ if (key === "path") current2.path = value.trim();
23112
+ if (key === "url") current2.url = value.trim();
23113
+ }
22301
23114
  }
22302
- const errors = dry.issues.filter((i) => i.severity === "error");
22303
- if (errors.length > 0) {
22304
- logger.header("Cannot lock \u2014 the spec tree does not validate as complete");
22305
- let errorCount = 0;
22306
- const MAX_PRINT = 100;
22307
- let skippedErrors = 0;
22308
- for (const i of errors) {
22309
- if (errorCount < MAX_PRINT) {
22310
- logger.error(`${i.specId ? `[${i.specId}] ` : ""}[${i.code}] ${i.message}`);
22311
- errorCount++;
22312
- } else {
22313
- skippedErrors++;
22314
- }
23115
+ if (current2.path) entries.push(current2);
23116
+ return entries;
23117
+ }
23118
+ function detectGitSubmodules(projectRoot2) {
23119
+ const gitmodulesPath = path27.join(projectRoot2, ".gitmodules");
23120
+ if (!fs18.existsSync(gitmodulesPath)) return [];
23121
+ return parseGitmodules(gitmodulesPath).map((entry) => ({
23122
+ suggestedId: pathToId(entry.path),
23123
+ suggestedName: pathToName(entry.path),
23124
+ path: normalizePath3(entry.path),
23125
+ type: "git-submodule",
23126
+ alreadyTracked: false
23127
+ }));
23128
+ }
23129
+ function detectNestedGitRepos(projectRoot2) {
23130
+ const results = [];
23131
+ walkForGit(projectRoot2, projectRoot2, 0, results);
23132
+ return results;
23133
+ }
23134
+ function walkForGit(projectRoot2, currentDir, depth, results) {
23135
+ if (depth > MAX_SCAN_DEPTH) return;
23136
+ let entries;
23137
+ try {
23138
+ entries = fs18.readdirSync(currentDir, { withFileTypes: true });
23139
+ } catch {
23140
+ return;
23141
+ }
23142
+ for (const entry of entries) {
23143
+ if (!entry.isDirectory()) continue;
23144
+ if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
23145
+ const fullPath = path27.join(currentDir, entry.name);
23146
+ const relPath = normalizePath3(path27.relative(projectRoot2, fullPath));
23147
+ if (relPath === "" || relPath === ".") continue;
23148
+ const gitPath = path27.join(fullPath, ".git");
23149
+ if (fs18.existsSync(gitPath)) {
23150
+ results.push({
23151
+ suggestedId: pathToId(relPath),
23152
+ suggestedName: pathToName(relPath),
23153
+ path: relPath,
23154
+ type: "git-repo",
23155
+ alreadyTracked: false
23156
+ });
23157
+ continue;
22315
23158
  }
22316
- if (skippedErrors > 0) {
22317
- logger.error(`... and ${skippedErrors} more error(s) omitted.`);
23159
+ walkForGit(projectRoot2, fullPath, depth + 1, results);
23160
+ }
23161
+ }
23162
+ function detectPackageRoots(projectRoot2) {
23163
+ const results = [];
23164
+ walkForPackages(projectRoot2, projectRoot2, 0, results);
23165
+ return results;
23166
+ }
23167
+ function walkForPackages(projectRoot2, currentDir, depth, results) {
23168
+ if (depth > MAX_SCAN_DEPTH) return;
23169
+ let entries;
23170
+ try {
23171
+ entries = fs18.readdirSync(currentDir, { withFileTypes: true });
23172
+ } catch {
23173
+ return;
23174
+ }
23175
+ for (const entry of entries) {
23176
+ if (!entry.isDirectory()) continue;
23177
+ if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
23178
+ const fullPath = path27.join(currentDir, entry.name);
23179
+ const relPath = normalizePath3(path27.relative(projectRoot2, fullPath));
23180
+ if (relPath === "" || relPath === ".") continue;
23181
+ const hasMarker = PACKAGE_MARKERS.some((m) => fs18.existsSync(path27.join(fullPath, m)));
23182
+ if (hasMarker) {
23183
+ results.push({
23184
+ suggestedId: pathToId(relPath),
23185
+ suggestedName: pathToName(relPath),
23186
+ path: relPath,
23187
+ type: "package-root",
23188
+ alreadyTracked: false
23189
+ });
22318
23190
  }
22319
- logger.blank();
22320
- logger.info("Fix the errors above, then run `wairon lock` again. Nothing was changed.");
22321
- process.exit(1);
23191
+ walkForPackages(projectRoot2, fullPath, depth + 1, results);
22322
23192
  }
22323
- logger.header("Lock SDD specs");
23193
+ }
23194
+ function pathToId(relPath) {
23195
+ const basename11 = path27.basename(relPath);
23196
+ return basename11.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
23197
+ }
23198
+ function pathToName(relPath) {
23199
+ const id = pathToId(relPath);
23200
+ return id.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
23201
+ }
23202
+ function normalizePath3(p) {
23203
+ return p.replace(/\\/g, "/");
23204
+ }
23205
+
23206
+ // src/core/index.ts
23207
+ init_domains();
23208
+ init_validation();
23209
+ init_extensions();
23210
+ init_variants();
23211
+ init_rules();
23212
+ init_specs2();
23213
+ init_provision();
23214
+ init_diagram();
23215
+
23216
+ // src/core/lockfile.ts
23217
+ var fs19 = __toESM(require("fs"));
23218
+ var path28 = __toESM(require("path"));
23219
+ init_fs();
23220
+ function lockPath() {
23221
+ return aiDir("lock.json");
23222
+ }
23223
+ function readLockRecord() {
23224
+ try {
23225
+ return JSON.parse(fs19.readFileSync(lockPath(), "utf8"));
23226
+ } catch {
23227
+ return null;
23228
+ }
23229
+ }
23230
+ function writeLockRecord(record2) {
23231
+ const p = lockPath();
23232
+ fs19.mkdirSync(path28.dirname(p), { recursive: true });
23233
+ const tmp = `${p}.tmp`;
23234
+ fs19.writeFileSync(tmp, JSON.stringify(record2, null, 2) + "\n");
23235
+ fs19.renameSync(tmp, p);
23236
+ }
23237
+
23238
+ // src/core/index.ts
23239
+ init_statehash();
23240
+ init_agent_resolver();
23241
+ init_skills();
23242
+ init_surfaces();
23243
+ init_openapi();
23244
+
23245
+ // src/commands/lock.ts
23246
+ async function runLock(options = {}, gate) {
23247
+ const promotable = collectPromotableSpecs(options.subsystem);
22324
23248
  if (promotable.length === 0) {
22325
23249
  logger.info("All specs are already complete \u2014 this will re-validate and regenerate the agent topology.");
22326
23250
  } else {
@@ -22344,23 +23268,36 @@ async function runLock(options = {}) {
22344
23268
  default: false
22345
23269
  }
22346
23270
  ]);
22347
- if (!confirmed) {
22348
- logger.info("Cancelled. Nothing was changed.");
22349
- return;
22350
- }
23271
+ if (!confirmed) return null;
23272
+ }
23273
+ if (options.subsystem) {
23274
+ for (const p of promotable) applySpecStatus(p.kind, p.id, "complete");
23275
+ invalidateSpecCache();
23276
+ } else {
23277
+ promoteAllComplete();
22351
23278
  }
22352
- for (const p of promotable) applySpecStatus(p.kind, p.id, "complete");
22353
- invalidateSpecCache();
22354
23279
  if (promotable.length > 0) {
22355
23280
  logger.success(`Locked ${promotable.length} spec(s) as complete.`);
22356
23281
  }
22357
- logger.blank();
22358
- await runGenerate({ domain: options.subsystem });
22359
- logger.blank();
22360
- logger.success("Specs locked and agent topology generated.");
22361
- logger.warn(
22362
- "Restart any running AI agent sessions (Claude Code / Antigravity / Codex) so the newly generated implementer agents load \u2014 they are not picked up mid-session."
22363
- );
23282
+ let lockedBy = "local";
23283
+ try {
23284
+ lockedBy = `local:${os7.userInfo().username}`;
23285
+ } catch {
23286
+ }
23287
+ const record2 = {
23288
+ stateId: computeStateId(),
23289
+ lockedAt: (/* @__PURE__ */ new Date()).toISOString(),
23290
+ lockedBy,
23291
+ validatorVersion: WAIRON_VERSION,
23292
+ validationResult: {
23293
+ valid: true,
23294
+ errors: 0,
23295
+ warnings: gate ? gate.issues.filter((i) => i.severity === "warning").length : 0
23296
+ },
23297
+ status: "ready"
23298
+ };
23299
+ writeLockRecord(record2);
23300
+ return record2;
22364
23301
  }
22365
23302
 
22366
23303
  // src/commands/validate.ts
@@ -22499,6 +23436,10 @@ async function runValidate(options = {}) {
22499
23436
  }
22500
23437
  }
22501
23438
 
23439
+ // src/cli/index.ts
23440
+ init_loader();
23441
+ init_fs();
23442
+
22502
23443
  // src/commands/list.ts
22503
23444
  var import_chalk7 = __toESM(require("chalk"));
22504
23445
  init_logger();
@@ -22613,9 +23554,9 @@ init_mcp();
22613
23554
  // src/commands/update.ts
22614
23555
  var https = __toESM(require("https"));
22615
23556
  var http = __toESM(require("http"));
22616
- var fs18 = __toESM(require("fs"));
22617
- var path27 = __toESM(require("path"));
22618
- var os7 = __toESM(require("os"));
23557
+ var fs20 = __toESM(require("fs"));
23558
+ var path29 = __toESM(require("path"));
23559
+ var os8 = __toESM(require("os"));
22619
23560
  var crypto2 = __toESM(require("crypto"));
22620
23561
  var import_child_process2 = require("child_process");
22621
23562
  init_logger();
@@ -22678,8 +23619,8 @@ async function runUpdate(options = {}) {
22678
23619
  logger.info(`Download manually from: ${release.html_url}`);
22679
23620
  process.exit(1);
22680
23621
  }
22681
- const tmpDir = os7.tmpdir();
22682
- const tmpFile = path27.join(tmpDir, assetName);
23622
+ const tmpDir = os8.tmpdir();
23623
+ const tmpFile = path29.join(tmpDir, assetName);
22683
23624
  logger.info(`Downloading ${assetName}...`);
22684
23625
  try {
22685
23626
  await downloadFile(asset.browser_download_url, tmpFile);
@@ -22696,16 +23637,16 @@ async function runUpdate(options = {}) {
22696
23637
  const checksumAssetName = assetName + ".sha256";
22697
23638
  const checksumAsset = release.assets.find((a) => a.name === checksumAssetName);
22698
23639
  if (checksumAsset) {
22699
- const tmpChecksum = path27.join(tmpDir, checksumAssetName);
23640
+ const tmpChecksum = path29.join(tmpDir, checksumAssetName);
22700
23641
  logger.info(`Verifying checksum...`);
22701
23642
  try {
22702
23643
  await downloadFile(checksumAsset.browser_download_url, tmpChecksum);
22703
23644
  verifyChecksum(tmpFile, tmpChecksum, assetName);
22704
- fs18.unlinkSync(tmpChecksum);
23645
+ fs20.unlinkSync(tmpChecksum);
22705
23646
  } catch (err) {
22706
23647
  logger.error(`Checksum verification failed: ${err.message}`);
22707
23648
  try {
22708
- fs18.unlinkSync(tmpFile);
23649
+ fs20.unlinkSync(tmpFile);
22709
23650
  } catch {
22710
23651
  }
22711
23652
  process.exit(1);
@@ -22771,7 +23712,7 @@ function fetchReleases(repo) {
22771
23712
  }
22772
23713
  function downloadFile(url, dest) {
22773
23714
  return new Promise((resolve24, reject) => {
22774
- const file = fs18.createWriteStream(dest);
23715
+ const file = fs20.createWriteStream(dest);
22775
23716
  const get3 = url.startsWith("https://") ? https.get : http.get;
22776
23717
  get3(url, { headers: { "User-Agent": `wairon/${WAIRON_VERSION}` }, agent: false }, (res) => {
22777
23718
  if (res.statusCode === 301 || res.statusCode === 302) {
@@ -22793,21 +23734,21 @@ function downloadFile(url, dest) {
22793
23734
  });
22794
23735
  file.on("error", (err) => {
22795
23736
  res.destroy();
22796
- fs18.unlink(dest, () => {
23737
+ fs20.unlink(dest, () => {
22797
23738
  });
22798
23739
  reject(err);
22799
23740
  });
22800
23741
  }).on("error", (err) => {
22801
- fs18.unlink(dest, () => {
23742
+ fs20.unlink(dest, () => {
22802
23743
  });
22803
23744
  reject(err);
22804
23745
  });
22805
23746
  });
22806
23747
  }
22807
23748
  function verifyChecksum(filePath, checksumFile, expectedFilename) {
22808
- const checksumContent = fs18.readFileSync(checksumFile, "utf-8").trim();
23749
+ const checksumContent = fs20.readFileSync(checksumFile, "utf-8").trim();
22809
23750
  const expectedHash = checksumContent.split(/\s+/)[0].toLowerCase();
22810
- const fileBuffer = fs18.readFileSync(filePath);
23751
+ const fileBuffer = fs20.readFileSync(filePath);
22811
23752
  const actualHash = crypto2.createHash("sha256").update(fileBuffer).digest("hex").toLowerCase();
22812
23753
  if (actualHash !== expectedHash) {
22813
23754
  throw new Error(
@@ -22838,9 +23779,9 @@ function isPkgBinary2() {
22838
23779
  function installBinary(tmpFile, destPath) {
22839
23780
  const platform = process.platform;
22840
23781
  const isZip = tmpFile.endsWith(".zip");
22841
- const extractDir = path27.join(os7.tmpdir(), "wairon-extract");
22842
- if (fs18.existsSync(extractDir)) fs18.rmSync(extractDir, { recursive: true });
22843
- fs18.mkdirSync(extractDir, { recursive: true });
23782
+ const extractDir = path29.join(os8.tmpdir(), "wairon-extract");
23783
+ if (fs20.existsSync(extractDir)) fs20.rmSync(extractDir, { recursive: true });
23784
+ fs20.mkdirSync(extractDir, { recursive: true });
22844
23785
  if (isZip) {
22845
23786
  (0, import_child_process2.execSync)(
22846
23787
  `powershell -NoProfile -NonInteractive -Command "Expand-Archive -Path '${tmpFile}' -DestinationPath '${extractDir}' -Force"`,
@@ -22850,18 +23791,18 @@ function installBinary(tmpFile, destPath) {
22850
23791
  (0, import_child_process2.execSync)(`tar -xzf "${tmpFile}" -C "${extractDir}"`, { stdio: ["ignore", "pipe", "pipe"] });
22851
23792
  }
22852
23793
  const binaryName = platform === "win32" ? "wairon.exe" : "wairon";
22853
- const extractedBinary = path27.join(extractDir, binaryName);
22854
- if (!fs18.existsSync(extractedBinary)) {
23794
+ const extractedBinary = path29.join(extractDir, binaryName);
23795
+ if (!fs20.existsSync(extractedBinary)) {
22855
23796
  throw new Error(`Extracted binary not found at ${extractedBinary}`);
22856
23797
  }
22857
23798
  if (platform === "win32") {
22858
23799
  const oldPath = destPath + ".old";
22859
23800
  try {
22860
23801
  cleanStaleBinary(oldPath);
22861
- fs18.renameSync(destPath, oldPath);
22862
- fs18.copyFileSync(extractedBinary, destPath);
23802
+ fs20.renameSync(destPath, oldPath);
23803
+ fs20.copyFileSync(extractedBinary, destPath);
22863
23804
  try {
22864
- fs18.unlinkSync(oldPath);
23805
+ fs20.unlinkSync(oldPath);
22865
23806
  } catch {
22866
23807
  }
22867
23808
  } catch (err) {
@@ -22875,25 +23816,25 @@ function installBinary(tmpFile, destPath) {
22875
23816
  }
22876
23817
  } else {
22877
23818
  const tmpDest = destPath + ".new";
22878
- fs18.copyFileSync(extractedBinary, tmpDest);
22879
- fs18.chmodSync(tmpDest, 493);
22880
- fs18.renameSync(tmpDest, destPath);
23819
+ fs20.copyFileSync(extractedBinary, tmpDest);
23820
+ fs20.chmodSync(tmpDest, 493);
23821
+ fs20.renameSync(tmpDest, destPath);
22881
23822
  }
22882
23823
  try {
22883
- fs18.unlinkSync(tmpFile);
23824
+ fs20.unlinkSync(tmpFile);
22884
23825
  } catch {
22885
23826
  }
22886
23827
  try {
22887
- fs18.rmSync(extractDir, { recursive: true });
23828
+ fs20.rmSync(extractDir, { recursive: true });
22888
23829
  } catch {
22889
23830
  }
22890
23831
  }
22891
23832
  function cleanStaleBinary(oldPath) {
22892
23833
  const target = oldPath ?? (isPkgBinary2() ? process.execPath + ".old" : null);
22893
23834
  if (!target) return;
22894
- if (fs18.existsSync(target)) {
23835
+ if (fs20.existsSync(target)) {
22895
23836
  try {
22896
- fs18.unlinkSync(target);
23837
+ fs20.unlinkSync(target);
22897
23838
  } catch {
22898
23839
  }
22899
23840
  }
@@ -23139,171 +24080,6 @@ async function filteredCheckbox(config) {
23139
24080
 
23140
24081
  // src/commands/domains.ts
23141
24082
  init_loader();
23142
-
23143
- // src/core/detection.ts
23144
- var fs19 = __toESM(require("fs"));
23145
- var path28 = __toESM(require("path"));
23146
- init_defaults();
23147
- var PACKAGE_MARKERS = [
23148
- "package.json",
23149
- "pyproject.toml",
23150
- "Cargo.toml",
23151
- "go.mod",
23152
- "build.gradle",
23153
- "build.gradle.kts",
23154
- "pom.xml"
23155
- ];
23156
- var MAX_SCAN_DEPTH = 5;
23157
- function detectDomainCandidates(projectRoot2, alreadyTrackedPaths = /* @__PURE__ */ new Set(), alreadyTrackedIds = /* @__PURE__ */ new Set()) {
23158
- const candidates = /* @__PURE__ */ new Map();
23159
- for (const c of detectGitSubmodules(projectRoot2)) {
23160
- candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
23161
- }
23162
- for (const c of detectNestedGitRepos(projectRoot2)) {
23163
- if (!candidates.has(c.path)) {
23164
- candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
23165
- }
23166
- }
23167
- const gitPaths = new Set(
23168
- Array.from(candidates.values()).filter((c) => c.type === "git-submodule" || c.type === "git-repo").map((c) => c.path)
23169
- );
23170
- for (const c of detectPackageRoots(projectRoot2)) {
23171
- if (candidates.has(c.path)) continue;
23172
- const insideGit = Array.from(gitPaths).some(
23173
- (gp) => c.path === gp || c.path.startsWith(gp + "/")
23174
- );
23175
- if (insideGit) continue;
23176
- candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
23177
- }
23178
- const sorted = Array.from(candidates.values()).sort((a, b) => a.path.localeCompare(b.path));
23179
- return deduplicateIds(sorted, alreadyTrackedIds);
23180
- }
23181
- function deduplicateIds(candidates, existingIds = /* @__PURE__ */ new Set()) {
23182
- const idCount = /* @__PURE__ */ new Map();
23183
- for (const id of existingIds) {
23184
- idCount.set(id, (idCount.get(id) ?? 0) + 1);
23185
- }
23186
- for (const c of candidates) {
23187
- idCount.set(c.suggestedId, (idCount.get(c.suggestedId) ?? 0) + 1);
23188
- }
23189
- return candidates.map((c) => {
23190
- if ((idCount.get(c.suggestedId) ?? 0) <= 1) return c;
23191
- const parts = c.path.split("/");
23192
- const qualifiedId2 = parts.length >= 2 ? pathToId(`${parts[parts.length - 2]}-${parts[parts.length - 1]}`) : c.suggestedId;
23193
- return { ...c, suggestedId: qualifiedId2 };
23194
- });
23195
- }
23196
- function parseGitmodules(filePath) {
23197
- const content = fs19.readFileSync(filePath, "utf-8");
23198
- const entries = [];
23199
- let current2 = {};
23200
- for (const line2 of content.split("\n")) {
23201
- const trimmed = line2.trim();
23202
- const headerMatch = trimmed.match(/^\[submodule "(.+)"\]$/);
23203
- if (headerMatch) {
23204
- if (current2.path) entries.push(current2);
23205
- current2 = { name: headerMatch[1] };
23206
- continue;
23207
- }
23208
- const keyVal = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
23209
- if (keyVal) {
23210
- const [, key, value] = keyVal;
23211
- if (key === "path") current2.path = value.trim();
23212
- if (key === "url") current2.url = value.trim();
23213
- }
23214
- }
23215
- if (current2.path) entries.push(current2);
23216
- return entries;
23217
- }
23218
- function detectGitSubmodules(projectRoot2) {
23219
- const gitmodulesPath = path28.join(projectRoot2, ".gitmodules");
23220
- if (!fs19.existsSync(gitmodulesPath)) return [];
23221
- return parseGitmodules(gitmodulesPath).map((entry) => ({
23222
- suggestedId: pathToId(entry.path),
23223
- suggestedName: pathToName(entry.path),
23224
- path: normalizePath3(entry.path),
23225
- type: "git-submodule",
23226
- alreadyTracked: false
23227
- }));
23228
- }
23229
- function detectNestedGitRepos(projectRoot2) {
23230
- const results = [];
23231
- walkForGit(projectRoot2, projectRoot2, 0, results);
23232
- return results;
23233
- }
23234
- function walkForGit(projectRoot2, currentDir, depth, results) {
23235
- if (depth > MAX_SCAN_DEPTH) return;
23236
- let entries;
23237
- try {
23238
- entries = fs19.readdirSync(currentDir, { withFileTypes: true });
23239
- } catch {
23240
- return;
23241
- }
23242
- for (const entry of entries) {
23243
- if (!entry.isDirectory()) continue;
23244
- if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
23245
- const fullPath = path28.join(currentDir, entry.name);
23246
- const relPath = normalizePath3(path28.relative(projectRoot2, fullPath));
23247
- if (relPath === "" || relPath === ".") continue;
23248
- const gitPath = path28.join(fullPath, ".git");
23249
- if (fs19.existsSync(gitPath)) {
23250
- results.push({
23251
- suggestedId: pathToId(relPath),
23252
- suggestedName: pathToName(relPath),
23253
- path: relPath,
23254
- type: "git-repo",
23255
- alreadyTracked: false
23256
- });
23257
- continue;
23258
- }
23259
- walkForGit(projectRoot2, fullPath, depth + 1, results);
23260
- }
23261
- }
23262
- function detectPackageRoots(projectRoot2) {
23263
- const results = [];
23264
- walkForPackages(projectRoot2, projectRoot2, 0, results);
23265
- return results;
23266
- }
23267
- function walkForPackages(projectRoot2, currentDir, depth, results) {
23268
- if (depth > MAX_SCAN_DEPTH) return;
23269
- let entries;
23270
- try {
23271
- entries = fs19.readdirSync(currentDir, { withFileTypes: true });
23272
- } catch {
23273
- return;
23274
- }
23275
- for (const entry of entries) {
23276
- if (!entry.isDirectory()) continue;
23277
- if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
23278
- const fullPath = path28.join(currentDir, entry.name);
23279
- const relPath = normalizePath3(path28.relative(projectRoot2, fullPath));
23280
- if (relPath === "" || relPath === ".") continue;
23281
- const hasMarker = PACKAGE_MARKERS.some((m) => fs19.existsSync(path28.join(fullPath, m)));
23282
- if (hasMarker) {
23283
- results.push({
23284
- suggestedId: pathToId(relPath),
23285
- suggestedName: pathToName(relPath),
23286
- path: relPath,
23287
- type: "package-root",
23288
- alreadyTracked: false
23289
- });
23290
- }
23291
- walkForPackages(projectRoot2, fullPath, depth + 1, results);
23292
- }
23293
- }
23294
- function pathToId(relPath) {
23295
- const basename12 = path28.basename(relPath);
23296
- return basename12.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
23297
- }
23298
- function pathToName(relPath) {
23299
- const id = pathToId(relPath);
23300
- return id.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
23301
- }
23302
- function normalizePath3(p) {
23303
- return p.replace(/\\/g, "/");
23304
- }
23305
-
23306
- // src/commands/domains.ts
23307
24083
  init_domains();
23308
24084
  init_domain();
23309
24085
  async function runDomainsList() {
@@ -23493,9 +24269,9 @@ async function runSkillsInstall() {
23493
24269
  }
23494
24270
 
23495
24271
  // src/commands/doctor.ts
23496
- var fs20 = __toESM(require("fs"));
23497
- var os8 = __toESM(require("os"));
23498
- var path29 = __toESM(require("path"));
24272
+ var fs21 = __toESM(require("fs"));
24273
+ var os9 = __toESM(require("os"));
24274
+ var path30 = __toESM(require("path"));
23499
24275
  var import_chalk12 = __toESM(require("chalk"));
23500
24276
  init_logger();
23501
24277
  init_defaults();
@@ -23525,10 +24301,10 @@ function stampVerdict(content) {
23525
24301
  return { mark: "warn", note: `v${v} \u2014 stale, installed is v${WAIRON_VERSION}` };
23526
24302
  }
23527
24303
  function mcpEntryHealth(settingsPath) {
23528
- if (!fs20.existsSync(settingsPath)) return { mark: "warn", note: "not registered" };
24304
+ if (!fs21.existsSync(settingsPath)) return { mark: "warn", note: "not registered" };
23529
24305
  let entry;
23530
24306
  try {
23531
- const s = JSON.parse(fs20.readFileSync(settingsPath, "utf8"));
24307
+ const s = JSON.parse(fs21.readFileSync(settingsPath, "utf8"));
23532
24308
  entry = s.mcpServers?.["wairon"];
23533
24309
  } catch {
23534
24310
  return { mark: "error", note: "parse error" };
@@ -23536,7 +24312,7 @@ function mcpEntryHealth(settingsPath) {
23536
24312
  if (!entry) return { mark: "warn", note: "not registered" };
23537
24313
  if (entry.command === "node" && Array.isArray(entry.args) && typeof entry.args[0] === "string") {
23538
24314
  const scriptPath = entry.args[0];
23539
- if (!fs20.existsSync(scriptPath)) {
24315
+ if (!fs21.existsSync(scriptPath)) {
23540
24316
  return { mark: "error", note: `registered but the server path is missing \u2014 ${scriptPath}` };
23541
24317
  }
23542
24318
  }
@@ -23588,7 +24364,7 @@ async function runDoctor(options = {}) {
23588
24364
  const { findChainingSubprojectsMissingConfig: findChainingSubprojectsMissingConfig2 } = (init_provision(), __toCommonJS(provision_exports));
23589
24365
  const missing = findChainingSubprojectsMissingConfig2(getProjectRoot());
23590
24366
  if (missing.length > 0) {
23591
- line(tally, "warn", `${missing.length} chained subproject(s) have specs but no project.yaml (un-runnable standalone): ${missing.map((d) => path29.relative(getProjectRoot(), d) || ".").join(", ")}. Run \`wairon doctor --fix\` to initialize them.`);
24367
+ line(tally, "warn", `${missing.length} chained subproject(s) have specs but no project.yaml (un-runnable standalone): ${missing.map((d) => path30.relative(getProjectRoot(), d) || ".").join(", ")}. Run \`wairon doctor --fix\` to initialize them.`);
23592
24368
  }
23593
24369
  } catch {
23594
24370
  }
@@ -23626,7 +24402,7 @@ async function runDoctor(options = {}) {
23626
24402
  const gp = localGuideFilePath(process.cwd(), t);
23627
24403
  if (!gp || seenGuides.has(gp)) continue;
23628
24404
  seenGuides.add(gp);
23629
- const rel2 = path29.relative(process.cwd(), gp).replace(/\\/g, "/");
24405
+ const rel2 = path30.relative(process.cwd(), gp).replace(/\\/g, "/");
23630
24406
  if (!pathExists(gp)) {
23631
24407
  line(tally, "warn", `${rel2} guide \u2014 not injected (run \`wairon generate\`)`);
23632
24408
  continue;
@@ -23660,17 +24436,17 @@ async function runDoctor(options = {}) {
23660
24436
  line(tally, h.mark, `Claude (project .mcp.json): ${h.note}${h.mark === "ok" ? "" : " \u2014 run `wairon mcp install --backend claude`"}`);
23661
24437
  }
23662
24438
  if (wantGemini) {
23663
- const globalCfg = path29.join(os8.homedir(), ".gemini", "antigravity-cli", "mcp_config.json");
24439
+ const globalCfg = path30.join(os9.homedir(), ".gemini", "antigravity-cli", "mcp_config.json");
23664
24440
  const hg = mcpEntryHealth(globalCfg);
23665
24441
  line(tally, hg.mark, `Antigravity (global mcp_config.json): ${hg.note}${hg.mark === "ok" ? "" : " \u2014 run `wairon mcp install --backend gemini --global`"}`);
23666
24442
  const projPath = fromProjectRoot(".gemini", "settings.json");
23667
- if (fs20.existsSync(projPath)) {
24443
+ if (fs21.existsSync(projPath)) {
23668
24444
  const hp = mcpEntryHealth(projPath);
23669
24445
  line(tally, hp.mark === "error" ? "error" : "ok", `Gemini CLI (project): ${hp.note} ${import_chalk12.default.gray("(Antigravity ignores this file)")}`);
23670
24446
  }
23671
24447
  }
23672
- const pluginDir = path29.join(os8.homedir(), ".gemini", "config", "plugins", "wairon");
23673
- if (fs20.existsSync(pluginDir)) {
24448
+ const pluginDir = path30.join(os9.homedir(), ".gemini", "config", "plugins", "wairon");
24449
+ if (fs21.existsSync(pluginDir)) {
23674
24450
  line(tally, "warn", `Legacy Antigravity plugin present (${pluginDir}) \u2014 it collides with the wairon MCP server. Remove it with \`wairon doctor --fix\`.`);
23675
24451
  }
23676
24452
  logger.blank();
@@ -23701,7 +24477,7 @@ async function applyFixes() {
23701
24477
  const legacySpecs = findLegacySpecFiles();
23702
24478
  if (legacySpecs.length > 0) {
23703
24479
  for (const { path: oldPath, expected: newPath } of legacySpecs) {
23704
- fs20.renameSync(oldPath, newPath);
24480
+ fs21.renameSync(oldPath, newPath);
23705
24481
  }
23706
24482
  console.log(` ${icon("ok")} Migrated ${legacySpecs.length} legacy spec file(s) to the new dot-prefixed unified schema.`);
23707
24483
  }
@@ -23755,8 +24531,8 @@ function printSummary(tally) {
23755
24531
  }
23756
24532
 
23757
24533
  // src/commands/diagram.ts
23758
- var fs21 = __toESM(require("fs"));
23759
- var path30 = __toESM(require("path"));
24534
+ var fs22 = __toESM(require("fs"));
24535
+ var path31 = __toESM(require("path"));
23760
24536
  init_logger();
23761
24537
  init_loader();
23762
24538
  init_fs();
@@ -23791,8 +24567,8 @@ function collectIssues() {
23791
24567
  }
23792
24568
  function writeCanvas(dest) {
23793
24569
  const model = buildCanvasModel(collectIssues());
23794
- ensureDir(path30.dirname(path30.resolve(dest)));
23795
- fs21.writeFileSync(dest, renderCanvasHtml(model), "utf-8");
24570
+ ensureDir(path31.dirname(path31.resolve(dest)));
24571
+ fs22.writeFileSync(dest, renderCanvasHtml(model), "utf-8");
23796
24572
  }
23797
24573
  function parseSequenceRef(ref) {
23798
24574
  const sep6 = ref.includes(":") ? ref.lastIndexOf(":") : ref.lastIndexOf(".");
@@ -23807,55 +24583,55 @@ async function runDiagram(rawOptions = {}) {
23807
24583
  assertProjectInitialized();
23808
24584
  const options = applyFormat(rawOptions);
23809
24585
  if (options.canvas && !options.all) {
23810
- const dest2 = options.out ?? path30.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
24586
+ const dest2 = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
23811
24587
  writeCanvas(dest2);
23812
24588
  logger.success(`Interactive canvas written to ${dest2}`);
23813
24589
  logger.info("Open it in a browser \u2014 fully self-contained (works offline).");
23814
24590
  return;
23815
24591
  }
23816
24592
  if (options.drawio && !options.all) {
23817
- const dest2 = options.out ?? path30.join(AI_PATHS.docsDir(), "diagrams", "architecture.drawio");
23818
- ensureDir(path30.dirname(path30.resolve(dest2)));
23819
- fs21.writeFileSync(dest2, generateDrawioXml(buildCanvasModel()), "utf-8");
24593
+ const dest2 = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams", "architecture.drawio");
24594
+ ensureDir(path31.dirname(path31.resolve(dest2)));
24595
+ fs22.writeFileSync(dest2, generateDrawioXml(buildCanvasModel()), "utf-8");
23820
24596
  logger.success(`draw.io diagram written to ${dest2}`);
23821
24597
  logger.info("Open with draw.io / diagrams.net (or import into tools that accept the format).");
23822
24598
  return;
23823
24599
  }
23824
24600
  if (options.excalidraw && !options.all) {
23825
- const dest2 = options.out ?? path30.join(AI_PATHS.docsDir(), "diagrams", "architecture.excalidraw");
23826
- ensureDir(path30.dirname(path30.resolve(dest2)));
23827
- fs21.writeFileSync(dest2, generateExcalidrawScene(buildCanvasModel()), "utf-8");
24601
+ const dest2 = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams", "architecture.excalidraw");
24602
+ ensureDir(path31.dirname(path31.resolve(dest2)));
24603
+ fs22.writeFileSync(dest2, generateExcalidrawScene(buildCanvasModel()), "utf-8");
23828
24604
  logger.success(`Excalidraw scene written to ${dest2}`);
23829
24605
  logger.info("Open with excalidraw.com or the VS Code extension.");
23830
24606
  return;
23831
24607
  }
23832
24608
  const wantsMermaid = options.format?.toLowerCase().startsWith("mermaid") || !!options.subsystem || !!options.sequence;
23833
24609
  if (!options.all && !options.sequence && !wantsMermaid) {
23834
- const dest2 = options.out ?? path30.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
24610
+ const dest2 = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
23835
24611
  writeCanvas(dest2);
23836
24612
  logger.success(`Interactive canvas written to ${dest2}`);
23837
24613
  logger.info("Open it in a browser \u2014 fully self-contained (works offline). Other formats: --format mermaid|drawio|excalidraw.");
23838
24614
  return;
23839
24615
  }
23840
24616
  if (options.all) {
23841
- const outDir = options.out ?? path30.join(AI_PATHS.docsDir(), "diagrams");
24617
+ const outDir = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams");
23842
24618
  const files = generateDiagramSet();
23843
24619
  if (files.length === 0) {
23844
24620
  logger.warn("No diagrams to generate \u2014 the spec tree has no components yet.");
23845
24621
  return;
23846
24622
  }
23847
24623
  for (const file of files) {
23848
- const dest2 = path30.join(outDir, file.relPath);
23849
- ensureDir(path30.dirname(dest2));
23850
- fs21.writeFileSync(dest2, toMarkdown(file), "utf-8");
24624
+ const dest2 = path31.join(outDir, file.relPath);
24625
+ ensureDir(path31.dirname(dest2));
24626
+ fs22.writeFileSync(dest2, toMarkdown(file), "utf-8");
23851
24627
  }
23852
- writeCanvas(path30.join(outDir, "canvas.html"));
24628
+ writeCanvas(path31.join(outDir, "canvas.html"));
23853
24629
  const exportModel = buildCanvasModel();
23854
- fs21.writeFileSync(path30.join(outDir, "architecture.drawio"), generateDrawioXml(exportModel), "utf-8");
23855
- fs21.writeFileSync(path30.join(outDir, "architecture.excalidraw"), generateExcalidrawScene(exportModel), "utf-8");
24630
+ fs22.writeFileSync(path31.join(outDir, "architecture.drawio"), generateDrawioXml(exportModel), "utf-8");
24631
+ fs22.writeFileSync(path31.join(outDir, "architecture.excalidraw"), generateExcalidrawScene(exportModel), "utf-8");
23856
24632
  const graph = loadSpecGraph();
23857
- const indexPath = path30.join(outDir, "README.md");
23858
- fs21.writeFileSync(indexPath, diagramSetIndex(files, graph.systemName), "utf-8");
24633
+ const indexPath = path31.join(outDir, "README.md");
24634
+ fs22.writeFileSync(indexPath, diagramSetIndex(files, graph.systemName), "utf-8");
23859
24635
  logger.success(`Generated ${files.length} diagram(s) + interactive canvas.html + index into ${outDir}`);
23860
24636
  for (const file of files.slice(0, 12)) {
23861
24637
  logger.info(` ${file.relPath}`);
@@ -23866,26 +24642,26 @@ async function runDiagram(rawOptions = {}) {
23866
24642
  let mermaid;
23867
24643
  let title;
23868
24644
  let defaultDest;
23869
- const diagramsDir = path30.join(AI_PATHS.docsDir(), "diagrams");
24645
+ const diagramsDir = path31.join(AI_PATHS.docsDir(), "diagrams");
23870
24646
  if (options.sequence) {
23871
24647
  const { component, method: method2 } = parseSequenceRef(options.sequence);
23872
24648
  mermaid = generateSequenceDiagram(component, method2, { depth: options.depth });
23873
24649
  title = `${component}.${method2} \u2014 narrative sequence`;
23874
- defaultDest = path30.join(diagramsDir, "sequences", `${component.replace(/::/g, "--")}.${method2}.md`);
24650
+ defaultDest = path31.join(diagramsDir, "sequences", `${component.replace(/::/g, "--")}.${method2}.md`);
23875
24651
  } else if (options.subsystem) {
23876
24652
  mermaid = generateComponentDiagram({ subsystem: options.subsystem });
23877
24653
  title = `${options.subsystem} \u2014 components`;
23878
- defaultDest = path30.join(diagramsDir, "subsystems", `${options.subsystem.replace(/::/g, "--")}.md`);
24654
+ defaultDest = path31.join(diagramsDir, "subsystems", `${options.subsystem.replace(/::/g, "--")}.md`);
23879
24655
  } else {
23880
24656
  mermaid = generateComponentDiagram();
23881
24657
  title = "Component architecture";
23882
- defaultDest = path30.join(diagramsDir, "system.md");
24658
+ defaultDest = path31.join(diagramsDir, "system.md");
23883
24659
  }
23884
24660
  const dest = options.out ?? defaultDest;
23885
- ensureDir(path30.dirname(path30.resolve(dest)));
24661
+ ensureDir(path31.dirname(path31.resolve(dest)));
23886
24662
  const content = dest.endsWith(".mmd") ? `${mermaid}
23887
24663
  ` : toMarkdown({ relPath: dest, title, mermaid });
23888
- fs21.writeFileSync(dest, content, "utf-8");
24664
+ fs22.writeFileSync(dest, content, "utf-8");
23889
24665
  logger.success(`Mermaid diagram written to ${dest}`);
23890
24666
  logger.info("Renders on GitHub/IDE previews; use a .mmd --out path for raw Mermaid.");
23891
24667
  }
@@ -23994,8 +24770,8 @@ Component variants (${variants.length})
23994
24770
  }
23995
24771
 
23996
24772
  // src/commands/packs.ts
23997
- var fs22 = __toESM(require("fs"));
23998
- var path31 = __toESM(require("path"));
24773
+ var fs23 = __toESM(require("fs"));
24774
+ var path32 = __toESM(require("path"));
23999
24775
  var import_chalk16 = __toESM(require("chalk"));
24000
24776
  var import_sdk = __toESM(require_dist());
24001
24777
  init_logger();
@@ -24021,11 +24797,11 @@ function describe(probe2) {
24021
24797
  return parts.join(", ");
24022
24798
  }
24023
24799
  function resolveSourceUnit(source) {
24024
- const abs = path31.resolve(source);
24025
- if (!fs22.existsSync(abs)) {
24800
+ const abs = path32.resolve(source);
24801
+ if (!fs23.existsSync(abs)) {
24026
24802
  throw new Error(`Pack source "${source}" does not exist.`);
24027
24803
  }
24028
- const isDir = fs22.statSync(abs).isDirectory();
24804
+ const isDir = fs23.statSync(abs).isDirectory();
24029
24805
  if (isDir && !packDirEntry(abs)) {
24030
24806
  throw new Error(`"${source}" is a directory without a pack entry file (pack.yaml | pack.cjs | index.cjs | ...).`);
24031
24807
  }
@@ -24041,7 +24817,7 @@ async function addPack(source, options = {}) {
24041
24817
  }
24042
24818
  const { abs } = resolveSourceUnit(source);
24043
24819
  const scope = options.global ? "global" : "project";
24044
- const probe2 = probePack(abs, path31.dirname(abs), scope);
24820
+ const probe2 = probePack(abs, path32.dirname(abs), scope);
24045
24821
  if (probe2.error) {
24046
24822
  logger.error(probe2.error);
24047
24823
  process.exitCode = 1;
@@ -24049,10 +24825,10 @@ async function addPack(source, options = {}) {
24049
24825
  }
24050
24826
  if (options.global) {
24051
24827
  const destDir = globalPacksDir();
24052
- const dest2 = path31.join(destDir, path31.basename(abs));
24053
- if (path31.resolve(dest2) !== abs) {
24054
- fs22.mkdirSync(destDir, { recursive: true });
24055
- fs22.cpSync(abs, dest2, { recursive: true, force: true });
24828
+ const dest2 = path32.join(destDir, path32.basename(abs));
24829
+ if (path32.resolve(dest2) !== abs) {
24830
+ fs23.mkdirSync(destDir, { recursive: true });
24831
+ fs23.cpSync(abs, dest2, { recursive: true, force: true });
24056
24832
  }
24057
24833
  logger.success(`Installed pack "${probe2.name}" globally: ${dest2}`);
24058
24834
  logger.info(`${describe(probe2)} \u2014 auto-loaded for every project on this machine (WAIRON_PACKS_DIR / ~/.wairon/packs).`);
@@ -24065,11 +24841,11 @@ async function addPack(source, options = {}) {
24065
24841
  return;
24066
24842
  }
24067
24843
  const root = getProjectRoot();
24068
- const relRef = `.wai/packs/${path31.basename(abs)}`;
24069
- const dest = path31.join(root, ".wai", "packs", path31.basename(abs));
24070
- if (path31.resolve(dest) !== abs) {
24071
- fs22.mkdirSync(path31.dirname(dest), { recursive: true });
24072
- fs22.cpSync(abs, dest, { recursive: true, force: true });
24844
+ const relRef = `.wai/packs/${path32.basename(abs)}`;
24845
+ const dest = path32.join(root, ".wai", "packs", path32.basename(abs));
24846
+ if (path32.resolve(dest) !== abs) {
24847
+ fs23.mkdirSync(path32.dirname(dest), { recursive: true });
24848
+ fs23.cpSync(abs, dest, { recursive: true, force: true });
24073
24849
  }
24074
24850
  const config = loadProjectConfig();
24075
24851
  const packs = config.extensions?.packs ?? [];
@@ -24078,14 +24854,14 @@ async function addPack(source, options = {}) {
24078
24854
  saveProjectConfig(config);
24079
24855
  logger.success(`Vendored pack "${probe2.name}" into ${relRef} and registered it in .wai/project.yaml.`);
24080
24856
  } else {
24081
- fs22.cpSync(abs, dest, { recursive: true, force: true });
24857
+ fs23.cpSync(abs, dest, { recursive: true, force: true });
24082
24858
  logger.success(`Pack "${probe2.name}" already registered \u2014 refreshed ${relRef} from the source.`);
24083
24859
  }
24084
24860
  logger.info(`${describe(probe2)} \u2014 commit .wai/ so CI and every clone enforce it.`);
24085
24861
  }
24086
24862
  async function addPackFromArchive(source, options) {
24087
- const abs = path31.resolve(source);
24088
- if (!fs22.existsSync(abs) || !fs22.statSync(abs).isFile()) {
24863
+ const abs = path32.resolve(source);
24864
+ if (!fs23.existsSync(abs) || !fs23.statSync(abs).isFile()) {
24089
24865
  logger.error(`Pack archive "${source}" does not exist.`);
24090
24866
  process.exitCode = 1;
24091
24867
  return;
@@ -24100,27 +24876,27 @@ async function addPackFromArchive(source, options) {
24100
24876
  process.exitCode = 1;
24101
24877
  return;
24102
24878
  }
24103
- baseDir = path31.join(getProjectRoot(), ".wai", "packs");
24879
+ baseDir = path32.join(getProjectRoot(), ".wai", "packs");
24104
24880
  }
24105
- const bytes = fs22.readFileSync(abs);
24106
- fs22.mkdirSync(baseDir, { recursive: true });
24107
- const staging = fs22.mkdtempSync(path31.join(baseDir, ".wpack-staging-"));
24881
+ const bytes = fs23.readFileSync(abs);
24882
+ fs23.mkdirSync(baseDir, { recursive: true });
24883
+ const staging = fs23.mkdtempSync(path32.join(baseDir, ".wpack-staging-"));
24108
24884
  let result;
24109
24885
  try {
24110
24886
  result = (0, import_sdk.extractPack)(bytes, staging);
24111
24887
  } catch (err) {
24112
- fs22.rmSync(staging, { recursive: true, force: true });
24113
- logger.error(`Failed to extract pack archive "${path31.basename(abs)}": ${err instanceof Error ? err.message : String(err)}`);
24888
+ fs23.rmSync(staging, { recursive: true, force: true });
24889
+ logger.error(`Failed to extract pack archive "${path32.basename(abs)}": ${err instanceof Error ? err.message : String(err)}`);
24114
24890
  process.exitCode = 1;
24115
24891
  return;
24116
24892
  }
24117
24893
  const name = result.name;
24118
- const destDir = path31.join(baseDir, name);
24119
- if (fs22.existsSync(destDir)) fs22.rmSync(destDir, { recursive: true, force: true });
24120
- fs22.renameSync(staging, destDir);
24121
- const probe2 = probePack(destDir, path31.dirname(destDir), scope);
24894
+ const destDir = path32.join(baseDir, name);
24895
+ if (fs23.existsSync(destDir)) fs23.rmSync(destDir, { recursive: true, force: true });
24896
+ fs23.renameSync(staging, destDir);
24897
+ const probe2 = probePack(destDir, path32.dirname(destDir), scope);
24122
24898
  if (probe2.error) {
24123
- fs22.rmSync(destDir, { recursive: true, force: true });
24899
+ fs23.rmSync(destDir, { recursive: true, force: true });
24124
24900
  logger.error(probe2.error);
24125
24901
  process.exitCode = 1;
24126
24902
  return;
@@ -24138,7 +24914,7 @@ async function addPackFromArchive(source, options) {
24138
24914
  saveProjectConfig(config);
24139
24915
  logger.success(`Installed pack "${probe2.name ?? name}" into ${relRef} and registered it in .wai/project.yaml.`);
24140
24916
  } else {
24141
- logger.success(`Pack "${probe2.name ?? name}" already registered \u2014 refreshed ${relRef} from ${path31.basename(abs)}.`);
24917
+ logger.success(`Pack "${probe2.name ?? name}" already registered \u2014 refreshed ${relRef} from ${path32.basename(abs)}.`);
24142
24918
  }
24143
24919
  logger.info(`${describe(probe2)} \u2014 commit .wai/ so CI and every clone enforce it.`);
24144
24920
  }
@@ -24159,7 +24935,7 @@ async function buildPack(source, options = {}) {
24159
24935
  const sourceDir = source && source.length > 0 ? source : ".";
24160
24936
  const result = (0, import_sdk.buildPack)(sourceDir);
24161
24937
  const outPath = options.out ?? result.suggestedFileName;
24162
- fs22.writeFileSync(outPath, result.archive);
24938
+ fs23.writeFileSync(outPath, result.archive);
24163
24939
  logger.success(`Built pack "${result.info.name}" v${result.info.version} \u2192 ${outPath} (${result.archive.byteLength} bytes)`);
24164
24940
  logger.info(`Install it with \`wairon pack add ${outPath}\`, or upload it to a hosted instance.`);
24165
24941
  }
@@ -24173,9 +24949,9 @@ async function listPacks() {
24173
24949
  console.log(import_chalk16.default.bold.cyan(`\u25A0 Global (${globalPacksDir()})${useGlobal ? "" : import_chalk16.default.yellow(" [disabled: extensions.useGlobalPacks: false]")}`));
24174
24950
  if (globalRefs.length === 0) console.log(import_chalk16.default.dim(" (none)"));
24175
24951
  for (const ref of globalRefs) {
24176
- const probe2 = probePack(ref, path31.dirname(ref), "global");
24177
- if (probe2.error) console.log(` ${import_chalk16.default.red("\u2716")} ${path31.basename(ref)} \u2014 ${import_chalk16.default.red(probe2.error)}`);
24178
- else console.log(` ${import_chalk16.default.green("\u25CF")} ${import_chalk16.default.bold(probe2.name ?? path31.basename(ref))} ${import_chalk16.default.dim(describe(probe2))}`);
24952
+ const probe2 = probePack(ref, path32.dirname(ref), "global");
24953
+ if (probe2.error) console.log(` ${import_chalk16.default.red("\u2716")} ${path32.basename(ref)} \u2014 ${import_chalk16.default.red(probe2.error)}`);
24954
+ else console.log(` ${import_chalk16.default.green("\u25CF")} ${import_chalk16.default.bold(probe2.name ?? path32.basename(ref))} ${import_chalk16.default.dim(describe(probe2))}`);
24179
24955
  }
24180
24956
  console.log("");
24181
24957
  if (!inProject) {
@@ -24196,9 +24972,9 @@ async function listPacks() {
24196
24972
  async function removePack(name, options = {}) {
24197
24973
  if (options.global) {
24198
24974
  for (const ref of discoverPacks(globalPacksDir())) {
24199
- const probe2 = probePack(ref, path31.dirname(ref), "global");
24200
- if (probe2.name === name || path31.basename(ref) === name) {
24201
- fs22.rmSync(ref, { recursive: true, force: true });
24975
+ const probe2 = probePack(ref, path32.dirname(ref), "global");
24976
+ if (probe2.name === name || path32.basename(ref) === name) {
24977
+ fs23.rmSync(ref, { recursive: true, force: true });
24202
24978
  logger.success(`Removed global pack "${probe2.name ?? name}" (${ref}).`);
24203
24979
  return;
24204
24980
  }
@@ -24217,16 +24993,16 @@ async function removePack(name, options = {}) {
24217
24993
  const packs = config.extensions?.packs ?? [];
24218
24994
  for (const ref of packs) {
24219
24995
  const probe2 = probePack(ref, root, "project");
24220
- if (probe2.name === name || ref === name || path31.basename(ref) === name) {
24996
+ if (probe2.name === name || ref === name || path32.basename(ref) === name) {
24221
24997
  config.extensions = {
24222
24998
  packs: packs.filter((p) => p !== ref),
24223
24999
  useGlobalPacks: config.extensions?.useGlobalPacks ?? true
24224
25000
  };
24225
25001
  saveProjectConfig(config);
24226
- const resolved = path31.resolve(root, ref);
24227
- const vendorDir = path31.resolve(root, ".wai", "packs");
24228
- if (resolved.startsWith(vendorDir + path31.sep)) {
24229
- fs22.rmSync(resolved, { recursive: true, force: true });
25002
+ const resolved = path32.resolve(root, ref);
25003
+ const vendorDir = path32.resolve(root, ".wai", "packs");
25004
+ if (resolved.startsWith(vendorDir + path32.sep)) {
25005
+ fs23.rmSync(resolved, { recursive: true, force: true });
24230
25006
  logger.success(`Deregistered pack "${probe2.name ?? name}" and deleted ${ref}.`);
24231
25007
  } else {
24232
25008
  logger.success(`Deregistered pack "${probe2.name ?? name}" (files at ${ref} left in place).`);
@@ -24240,8 +25016,8 @@ async function removePack(name, options = {}) {
24240
25016
 
24241
25017
  // src/commands/host.ts
24242
25018
  var fs50 = __toESM(require("fs"));
24243
- var path60 = __toESM(require("path"));
24244
- var os9 = __toESM(require("os"));
25019
+ var path59 = __toESM(require("path"));
25020
+ var os10 = __toESM(require("os"));
24245
25021
  var crypto20 = __toESM(require("crypto"));
24246
25022
  var import_child_process5 = require("child_process");
24247
25023
  var import_chalk17 = __toESM(require("chalk"));
@@ -24268,29 +25044,29 @@ var UNAUTHENTICATED = {
24268
25044
  var WEB_SESSION_PREFIX = "ws_";
24269
25045
 
24270
25046
  // src/server/credentials.ts
24271
- var fs23 = __toESM(require("fs"));
24272
- var path32 = __toESM(require("path"));
25047
+ var fs24 = __toESM(require("fs"));
25048
+ var path33 = __toESM(require("path"));
24273
25049
  var crypto3 = __toESM(require("crypto"));
24274
25050
  var HASH_NS = "wairon:token:v1";
24275
25051
  function hashToken(token) {
24276
25052
  return crypto3.createHash("sha256").update(`${HASH_NS}:${token}`).digest("hex");
24277
25053
  }
24278
25054
  function storePath(dataDir) {
24279
- return path32.join(dataDir, "auth", "credentials.json");
25055
+ return path33.join(dataDir, "auth", "credentials.json");
24280
25056
  }
24281
25057
  function load3(dataDir) {
24282
25058
  try {
24283
- return JSON.parse(fs23.readFileSync(storePath(dataDir), "utf8"));
25059
+ return JSON.parse(fs24.readFileSync(storePath(dataDir), "utf8"));
24284
25060
  } catch {
24285
25061
  return [];
24286
25062
  }
24287
25063
  }
24288
25064
  function save(dataDir, records) {
24289
25065
  const p = storePath(dataDir);
24290
- fs23.mkdirSync(path32.dirname(p), { recursive: true });
25066
+ fs24.mkdirSync(path33.dirname(p), { recursive: true });
24291
25067
  const tmp = `${p}.tmp`;
24292
- fs23.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
24293
- fs23.renameSync(tmp, p);
25068
+ fs24.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
25069
+ fs24.renameSync(tmp, p);
24294
25070
  }
24295
25071
  function digestEquals(a, b) {
24296
25072
  const ab = Buffer.from(a, "hex");
@@ -24335,17 +25111,17 @@ function listByOwner(dataDir, ownerUserId) {
24335
25111
  }
24336
25112
 
24337
25113
  // src/server/websessions.ts
24338
- var fs24 = __toESM(require("fs"));
24339
- var path33 = __toESM(require("path"));
25114
+ var fs25 = __toESM(require("fs"));
25115
+ var path34 = __toESM(require("path"));
24340
25116
  var crypto4 = __toESM(require("crypto"));
24341
25117
  function storePath2(dataDir) {
24342
- return path33.join(dataDir, "web-sessions.json");
25118
+ return path34.join(dataDir, "web-sessions.json");
24343
25119
  }
24344
25120
  function readSessions(dataDir) {
24345
25121
  const p = storePath2(dataDir);
24346
25122
  let raw;
24347
25123
  try {
24348
- raw = fs24.readFileSync(p, "utf8");
25124
+ raw = fs25.readFileSync(p, "utf8");
24349
25125
  } catch (e) {
24350
25126
  if (e.code === "ENOENT") return [];
24351
25127
  throw new Error(`Failed to read web session store at ${p}: ${e.message}`);
@@ -24360,10 +25136,10 @@ function readSessions(dataDir) {
24360
25136
  }
24361
25137
  function persistSessions(dataDir, sessions) {
24362
25138
  const p = storePath2(dataDir);
24363
- fs24.mkdirSync(path33.dirname(p), { recursive: true });
25139
+ fs25.mkdirSync(path34.dirname(p), { recursive: true });
24364
25140
  const tmp = `${p}.tmp`;
24365
- fs24.writeFileSync(tmp, JSON.stringify(sessions, null, 2) + "\n");
24366
- fs24.renameSync(tmp, p);
25141
+ fs25.writeFileSync(tmp, JSON.stringify(sessions, null, 2) + "\n");
25142
+ fs25.renameSync(tmp, p);
24367
25143
  }
24368
25144
  function mintSessionId() {
24369
25145
  return `${WEB_SESSION_PREFIX}${crypto4.randomBytes(24).toString("hex")}`;
@@ -24524,17 +25300,17 @@ function listWebSessionsBySubject(dataDir, userId) {
24524
25300
  }
24525
25301
 
24526
25302
  // src/server/users.ts
24527
- var fs25 = __toESM(require("fs"));
24528
- var path34 = __toESM(require("path"));
25303
+ var fs26 = __toESM(require("fs"));
25304
+ var path35 = __toESM(require("path"));
24529
25305
  var VALID_STATUSES = ["active", "inactive", "suspended", "deactivated", "disabled"];
24530
25306
  function storePath3(dataDir) {
24531
- return path34.join(dataDir, "users.json");
25307
+ return path35.join(dataDir, "users.json");
24532
25308
  }
24533
25309
  function loadStore(dataDir) {
24534
25310
  const p = storePath3(dataDir);
24535
25311
  let raw;
24536
25312
  try {
24537
- raw = fs25.readFileSync(p, "utf8");
25313
+ raw = fs26.readFileSync(p, "utf8");
24538
25314
  } catch (err) {
24539
25315
  if (err.code === "ENOENT") return [];
24540
25316
  throw new Error(`Cannot read hosted-user store at ${p}: ${err.message}`);
@@ -24552,10 +25328,10 @@ function loadStore(dataDir) {
24552
25328
  }
24553
25329
  function replaceAll(dataDir, records) {
24554
25330
  const p = storePath3(dataDir);
24555
- fs25.mkdirSync(path34.dirname(p), { recursive: true });
25331
+ fs26.mkdirSync(path35.dirname(p), { recursive: true });
24556
25332
  const tmp = `${p}.tmp`;
24557
- fs25.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
24558
- fs25.renameSync(tmp, p);
25333
+ fs26.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
25334
+ fs26.renameSync(tmp, p);
24559
25335
  }
24560
25336
  function registryUpsert(dataDir, record2) {
24561
25337
  const records = loadStore(dataDir);
@@ -24657,11 +25433,11 @@ function remapUnitReferences(dataDir, remap, removedScopeIds) {
24657
25433
  }
24658
25434
 
24659
25435
  // src/server/instance.ts
24660
- var fs26 = __toESM(require("fs"));
24661
- var path35 = __toESM(require("path"));
25436
+ var fs27 = __toESM(require("fs"));
25437
+ var path36 = __toESM(require("path"));
24662
25438
  var import_crypto = require("crypto");
24663
25439
  function storePath4(dataDir) {
24664
- return path35.join(dataDir, "instance.json");
25440
+ return path36.join(dataDir, "instance.json");
24665
25441
  }
24666
25442
  var InstanceIdentityStore = class {
24667
25443
  constructor(dataDir) {
@@ -24678,7 +25454,7 @@ var InstanceIdentityStore = class {
24678
25454
  const p = storePath4(this.dataDir);
24679
25455
  let raw;
24680
25456
  try {
24681
- raw = fs26.readFileSync(p, "utf8");
25457
+ raw = fs27.readFileSync(p, "utf8");
24682
25458
  } catch (err) {
24683
25459
  if (err.code === "ENOENT") return null;
24684
25460
  throw new Error(`Cannot read instance identity at ${p}: ${err.message}`);
@@ -24701,10 +25477,10 @@ var InstanceIdentityStore = class {
24701
25477
  * never truncates the file. Only called by the registry's create-once seed. */
24702
25478
  replace(identity) {
24703
25479
  const p = storePath4(this.dataDir);
24704
- fs26.mkdirSync(path35.dirname(p), { recursive: true });
25480
+ fs27.mkdirSync(path36.dirname(p), { recursive: true });
24705
25481
  const tmp = `${p}.tmp`;
24706
- fs26.writeFileSync(tmp, JSON.stringify(identity, null, 2) + "\n");
24707
- fs26.renameSync(tmp, p);
25482
+ fs27.writeFileSync(tmp, JSON.stringify(identity, null, 2) + "\n");
25483
+ fs27.renameSync(tmp, p);
24708
25484
  }
24709
25485
  };
24710
25486
  var InstanceIdentityRegistry = class {
@@ -24760,8 +25536,8 @@ function getInstanceIdentity(dataDir) {
24760
25536
  }
24761
25537
 
24762
25538
  // src/utils/secrets.ts
24763
- var fs27 = __toESM(require("fs"));
24764
- var path36 = __toESM(require("path"));
25539
+ var fs28 = __toESM(require("fs"));
25540
+ var path37 = __toESM(require("path"));
24765
25541
  var ENV_FALLBACK = {
24766
25542
  "git-token": ["WAIRON_GIT_TOKEN"],
24767
25543
  "notion-token": ["WAIRON_NOTION_TOKEN"],
@@ -24770,13 +25546,13 @@ var ENV_FALLBACK = {
24770
25546
  };
24771
25547
  function storePath5() {
24772
25548
  const dataDir = process.env["WAIRON_DATA_DIR"];
24773
- return dataDir ? path36.join(dataDir, "auth", "secrets.json") : null;
25549
+ return dataDir ? path37.join(dataDir, "auth", "secrets.json") : null;
24774
25550
  }
24775
25551
  function readStore() {
24776
25552
  const p = storePath5();
24777
25553
  if (!p) return {};
24778
25554
  try {
24779
- return JSON.parse(fs27.readFileSync(p, "utf8"));
25555
+ return JSON.parse(fs28.readFileSync(p, "utf8"));
24780
25556
  } catch {
24781
25557
  return {};
24782
25558
  }
@@ -24801,10 +25577,10 @@ function setSecret(key, value) {
24801
25577
  if (!p) throw new Error("WAIRON_DATA_DIR is not set \u2014 a running server needs it to store secrets.");
24802
25578
  const store = readStore();
24803
25579
  store[key] = value;
24804
- fs27.mkdirSync(path36.dirname(p), { recursive: true });
25580
+ fs28.mkdirSync(path37.dirname(p), { recursive: true });
24805
25581
  const tmp = `${p}.tmp`;
24806
- fs27.writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n");
24807
- fs27.renameSync(tmp, p);
25582
+ fs28.writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n");
25583
+ fs28.renameSync(tmp, p);
24808
25584
  }
24809
25585
  function listSecretKeys() {
24810
25586
  return Object.keys(readStore());
@@ -25012,17 +25788,17 @@ function verifySsoState(state) {
25012
25788
  }
25013
25789
 
25014
25790
  // src/server/organization.ts
25015
- var fs28 = __toESM(require("fs"));
25016
- var path37 = __toESM(require("path"));
25791
+ var fs29 = __toESM(require("fs"));
25792
+ var path38 = __toESM(require("path"));
25017
25793
  var crypto6 = __toESM(require("crypto"));
25018
25794
  function storePath6(dataDir) {
25019
- return path37.join(dataDir, "organization.json");
25795
+ return path38.join(dataDir, "organization.json");
25020
25796
  }
25021
25797
  function readState(dataDir) {
25022
25798
  const p = storePath6(dataDir);
25023
25799
  let raw;
25024
25800
  try {
25025
- raw = fs28.readFileSync(p, "utf8");
25801
+ raw = fs29.readFileSync(p, "utf8");
25026
25802
  } catch (e) {
25027
25803
  if (e.code === "ENOENT") return { units: [], placements: [] };
25028
25804
  throw new Error(`Failed to read organization store at ${p}: ${e.message}`);
@@ -25039,10 +25815,10 @@ function readState(dataDir) {
25039
25815
  }
25040
25816
  function persistState(dataDir, state) {
25041
25817
  const p = storePath6(dataDir);
25042
- fs28.mkdirSync(path37.dirname(p), { recursive: true });
25818
+ fs29.mkdirSync(path38.dirname(p), { recursive: true });
25043
25819
  const tmp = `${p}.tmp`;
25044
- fs28.writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
25045
- fs28.renameSync(tmp, p);
25820
+ fs29.writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
25821
+ fs29.renameSync(tmp, p);
25046
25822
  }
25047
25823
  var SLUG_PATTERN = /^[a-z0-9-]+$/;
25048
25824
  var UNIT_KINDS = ["business_entity", "department", "team", "group"];
@@ -25411,11 +26187,11 @@ function getOrganizationUnit(dataDir, id) {
25411
26187
  }
25412
26188
 
25413
26189
  // src/server/permissions.ts
25414
- var fs29 = __toESM(require("fs"));
25415
- var path38 = __toESM(require("path"));
26190
+ var fs30 = __toESM(require("fs"));
26191
+ var path39 = __toESM(require("path"));
25416
26192
  var import_crypto2 = require("crypto");
25417
26193
  function storePath7(dataDir) {
25418
- return path38.join(dataDir, "permissions.json");
26194
+ return path39.join(dataDir, "permissions.json");
25419
26195
  }
25420
26196
  function assignmentKey(a) {
25421
26197
  return [a.subjectKind, a.subjectId ?? "", a.scopeKind, a.scopeId ?? "", a.capability].join("|");
@@ -25424,7 +26200,7 @@ function load4(dataDir) {
25424
26200
  const p = storePath7(dataDir);
25425
26201
  let raw;
25426
26202
  try {
25427
- raw = fs29.readFileSync(p, "utf8");
26203
+ raw = fs30.readFileSync(p, "utf8");
25428
26204
  } catch (err) {
25429
26205
  if (err.code === "ENOENT") return [];
25430
26206
  throw new Error(`Cannot read permission store at ${p}: ${err.message}`);
@@ -25442,10 +26218,10 @@ function load4(dataDir) {
25442
26218
  }
25443
26219
  function replaceAll2(dataDir, assignments) {
25444
26220
  const p = storePath7(dataDir);
25445
- fs29.mkdirSync(path38.dirname(p), { recursive: true });
26221
+ fs30.mkdirSync(path39.dirname(p), { recursive: true });
25446
26222
  const tmp = `${p}.tmp`;
25447
- fs29.writeFileSync(tmp, JSON.stringify(assignments, null, 2) + "\n");
25448
- fs29.renameSync(tmp, p);
26223
+ fs30.writeFileSync(tmp, JSON.stringify(assignments, null, 2) + "\n");
26224
+ fs30.renameSync(tmp, p);
25449
26225
  }
25450
26226
  function registrySet(dataDir, assignment) {
25451
26227
  const assignments = load4(dataDir);
@@ -25531,8 +26307,8 @@ function getAssignment(dataDir, assignmentId) {
25531
26307
  }
25532
26308
 
25533
26309
  // src/server/roles.ts
25534
- var fs30 = __toESM(require("fs"));
25535
- var path39 = __toESM(require("path"));
26310
+ var fs31 = __toESM(require("fs"));
26311
+ var path40 = __toESM(require("path"));
25536
26312
  var BUILTIN_ROLES = [
25537
26313
  {
25538
26314
  id: SSO_ADMIN_ROLE_ID,
@@ -25550,13 +26326,13 @@ function isBuiltinRoleId(roleId) {
25550
26326
  return BUILTIN_ROLE_IDS.has(roleId);
25551
26327
  }
25552
26328
  function storePath8(dataDir) {
25553
- return path39.join(dataDir, "roles.json");
26329
+ return path40.join(dataDir, "roles.json");
25554
26330
  }
25555
26331
  function load5(dataDir) {
25556
26332
  const p = storePath8(dataDir);
25557
26333
  let raw;
25558
26334
  try {
25559
- raw = fs30.readFileSync(p, "utf8");
26335
+ raw = fs31.readFileSync(p, "utf8");
25560
26336
  } catch (err) {
25561
26337
  if (err.code === "ENOENT") return [];
25562
26338
  throw new Error(`Cannot read role store at ${p}: ${err.message}`);
@@ -25574,10 +26350,10 @@ function load5(dataDir) {
25574
26350
  }
25575
26351
  function replaceAll3(dataDir, roles) {
25576
26352
  const p = storePath8(dataDir);
25577
- fs30.mkdirSync(path39.dirname(p), { recursive: true });
26353
+ fs31.mkdirSync(path40.dirname(p), { recursive: true });
25578
26354
  const tmp = `${p}.tmp`;
25579
- fs30.writeFileSync(tmp, JSON.stringify(roles, null, 2) + "\n");
25580
- fs30.renameSync(tmp, p);
26355
+ fs31.writeFileSync(tmp, JSON.stringify(roles, null, 2) + "\n");
26356
+ fs31.renameSync(tmp, p);
25581
26357
  }
25582
26358
  function registryCreate(dataDir, role) {
25583
26359
  if (isBuiltinRoleId(role.id)) {
@@ -25811,131 +26587,14 @@ function actionableUnitIds(scopes) {
25811
26587
  }
25812
26588
 
25813
26589
  // src/server/projects.ts
25814
- var fs31 = __toESM(require("fs"));
25815
- var path40 = __toESM(require("path"));
25816
- var ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
25817
- function isValidProjectId(id) {
25818
- return typeof id === "string" && ID_RE.test(id);
25819
- }
25820
- function registryPath(dataDir) {
25821
- return path40.join(dataDir, "projects.json");
25822
- }
25823
- function load6(dataDir) {
25824
- try {
25825
- return JSON.parse(fs31.readFileSync(registryPath(dataDir), "utf8"));
25826
- } catch {
25827
- return [];
25828
- }
25829
- }
25830
- function save2(dataDir, records) {
25831
- const p = registryPath(dataDir);
25832
- fs31.mkdirSync(path40.dirname(p), { recursive: true });
25833
- const tmp = `${p}.tmp`;
25834
- fs31.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
25835
- fs31.renameSync(tmp, p);
25836
- }
25837
- function projectRoot(dataDir, id) {
25838
- return path40.join(dataDir, "projects", id);
25839
- }
25840
- function existingProjectRoot(dataDir, id) {
25841
- if (!isValidProjectId(id)) return null;
25842
- const rec = load6(dataDir).find((r) => r.id === id);
25843
- return rec ? rec.rootPath : null;
25844
- }
25845
- function createProjectRecord(dataDir, id) {
25846
- if (!isValidProjectId(id)) {
25847
- throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
25848
- }
25849
- const records = load6(dataDir);
25850
- if (records.some((r) => r.id === id)) {
25851
- throw new Error(`Project "${id}" already exists.`);
25852
- }
25853
- const root = projectRoot(dataDir, id);
25854
- fs31.mkdirSync(root, { recursive: true });
25855
- const record2 = {
25856
- id,
25857
- rootPath: root,
25858
- status: "active",
25859
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
25860
- };
25861
- records.push(record2);
25862
- save2(dataDir, records);
25863
- return record2;
25864
- }
25865
- function registerLocalDevProject(dataDir, id, rootPath) {
25866
- if (!isValidProjectId(id)) {
25867
- throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
25868
- }
25869
- const records = load6(dataDir);
25870
- const existing = records.find((r) => r.id === id);
25871
- const record2 = {
25872
- id,
25873
- rootPath,
25874
- status: "active",
25875
- createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
25876
- };
25877
- const next = existing ? records.map((r) => r.id === id ? record2 : r) : [...records, record2];
25878
- save2(dataDir, next);
25879
- return record2;
25880
- }
25881
- function listProjectRecords(dataDir) {
25882
- return load6(dataDir);
25883
- }
25884
- function removeProjectRecord(dataDir, id) {
25885
- const records = load6(dataDir);
25886
- const rec = records.find((r) => r.id === id);
25887
- if (rec) {
25888
- try {
25889
- fs31.rmSync(rec.rootPath, { recursive: true, force: true });
25890
- } catch {
25891
- }
25892
- }
25893
- save2(dataDir, records.filter((r) => r.id !== id));
25894
- }
25895
- function resolveProjectRoot(dataDir, principal, selector) {
25896
- const authorized = principal.projects;
25897
- const wildcard = authorized.includes("*");
25898
- let target;
25899
- if (selector) {
25900
- if (!wildcard && !authorized.includes(selector)) return null;
25901
- target = selector;
25902
- } else if (!wildcard && authorized.length === 1) {
25903
- target = authorized[0];
25904
- } else {
25905
- return null;
25906
- }
25907
- if (!isValidProjectId(target)) return null;
25908
- const rec = load6(dataDir).find((r) => r.id === target);
25909
- if (!rec || rec.status !== "active") return null;
25910
- return rec.rootPath;
25911
- }
25912
-
25913
- // src/server/adapters.ts
25914
- init_statehash();
25915
-
25916
- // src/core/lockfile.ts
25917
- var fs32 = __toESM(require("fs"));
25918
- var path41 = __toESM(require("path"));
26590
+ var fs35 = __toESM(require("fs"));
26591
+ var path44 = __toESM(require("path"));
26592
+ init_loader();
26593
+ init_yaml();
25919
26594
  init_fs();
25920
- function lockPath() {
25921
- return aiDir("lock.json");
25922
- }
25923
- function readLockRecord() {
25924
- try {
25925
- return JSON.parse(fs32.readFileSync(lockPath(), "utf8"));
25926
- } catch {
25927
- return null;
25928
- }
25929
- }
25930
- function writeLockRecord(record2) {
25931
- const p = lockPath();
25932
- fs32.mkdirSync(path41.dirname(p), { recursive: true });
25933
- const tmp = `${p}.tmp`;
25934
- fs32.writeFileSync(tmp, JSON.stringify(record2, null, 2) + "\n");
25935
- fs32.renameSync(tmp, p);
25936
- }
25937
26595
 
25938
26596
  // src/server/adapters.ts
26597
+ init_statehash();
25939
26598
  init_specs2();
25940
26599
  init_provision();
25941
26600
  init_validation();
@@ -25946,37 +26605,37 @@ init_types();
25946
26605
  init_server();
25947
26606
 
25948
26607
  // src/git/config.ts
25949
- var fs33 = __toESM(require("fs"));
25950
- var path42 = __toESM(require("path"));
26608
+ var fs32 = __toESM(require("fs"));
26609
+ var path41 = __toESM(require("path"));
25951
26610
  init_fs();
25952
26611
  function configPath() {
25953
26612
  return aiDir("git.json");
25954
26613
  }
25955
26614
  function readGitConfig() {
25956
26615
  try {
25957
- return JSON.parse(fs33.readFileSync(configPath(), "utf8"));
26616
+ return JSON.parse(fs32.readFileSync(configPath(), "utf8"));
25958
26617
  } catch {
25959
26618
  return null;
25960
26619
  }
25961
26620
  }
25962
26621
  function writeGitConfig(config) {
25963
26622
  const p = configPath();
25964
- fs33.mkdirSync(path42.dirname(p), { recursive: true });
26623
+ fs32.mkdirSync(path41.dirname(p), { recursive: true });
25965
26624
  const tmp = `${p}.tmp`;
25966
- fs33.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n");
25967
- fs33.renameSync(tmp, p);
26625
+ fs32.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n");
26626
+ fs32.renameSync(tmp, p);
25968
26627
  }
25969
26628
  function clearGitConfig() {
25970
26629
  try {
25971
- fs33.rmSync(configPath(), { force: true });
26630
+ fs32.rmSync(configPath(), { force: true });
25972
26631
  } catch {
25973
26632
  }
25974
26633
  }
25975
26634
 
25976
26635
  // src/git/adapter.ts
25977
26636
  var import_child_process3 = require("child_process");
25978
- var fs34 = __toESM(require("fs"));
25979
- var path43 = __toESM(require("path"));
26637
+ var fs33 = __toESM(require("fs"));
26638
+ var path42 = __toESM(require("path"));
25980
26639
  init_fs();
25981
26640
  function git(args, cwd) {
25982
26641
  return (0, import_child_process3.execFileSync)("git", args, {
@@ -26028,10 +26687,10 @@ function compareUrl(remote, defaultBranch, workingBranch) {
26028
26687
  return `${web}/compare/${encodeURIComponent(defaultBranch)}...${encodeURIComponent(workingBranch)}`;
26029
26688
  }
26030
26689
  function excludeLocalFiles() {
26031
- const excludePath = path43.join(getProjectRoot(), ".git", "info", "exclude");
26690
+ const excludePath = path42.join(getProjectRoot(), ".git", "info", "exclude");
26032
26691
  try {
26033
- fs34.mkdirSync(path43.dirname(excludePath), { recursive: true });
26034
- fs34.appendFileSync(excludePath, "\n.wai/lock.json\n.wai/git.json\n");
26692
+ fs33.mkdirSync(path42.dirname(excludePath), { recursive: true });
26693
+ fs33.appendFileSync(excludePath, "\n.wai/lock.json\n.wai/git.json\n");
26035
26694
  } catch {
26036
26695
  }
26037
26696
  }
@@ -26096,39 +26755,39 @@ function configureSync(periodicSyncMinutes, skipIfClean) {
26096
26755
  }
26097
26756
 
26098
26757
  // src/producers/config.ts
26099
- var fs35 = __toESM(require("fs"));
26100
- var path44 = __toESM(require("path"));
26758
+ var fs34 = __toESM(require("fs"));
26759
+ var path43 = __toESM(require("path"));
26101
26760
  init_fs();
26102
26761
  function configPath2() {
26103
26762
  return aiDir("producers.json");
26104
26763
  }
26105
- function load7() {
26764
+ function load6() {
26106
26765
  try {
26107
- return JSON.parse(fs35.readFileSync(configPath2(), "utf8"));
26766
+ return JSON.parse(fs34.readFileSync(configPath2(), "utf8"));
26108
26767
  } catch {
26109
26768
  return [];
26110
26769
  }
26111
26770
  }
26112
- function save3(configs) {
26771
+ function save2(configs) {
26113
26772
  const p = configPath2();
26114
- fs35.mkdirSync(path44.dirname(p), { recursive: true });
26773
+ fs34.mkdirSync(path43.dirname(p), { recursive: true });
26115
26774
  const tmp = `${p}.tmp`;
26116
- fs35.writeFileSync(tmp, JSON.stringify(configs, null, 2) + "\n");
26117
- fs35.renameSync(tmp, p);
26775
+ fs34.writeFileSync(tmp, JSON.stringify(configs, null, 2) + "\n");
26776
+ fs34.renameSync(tmp, p);
26118
26777
  }
26119
26778
  function readProducerConfig(target) {
26120
- return load7().find((c) => c.target === target) ?? null;
26779
+ return load6().find((c) => c.target === target) ?? null;
26121
26780
  }
26122
26781
  function writeProducerConfig(config) {
26123
- const configs = load7().filter((c) => c.target !== config.target);
26782
+ const configs = load6().filter((c) => c.target !== config.target);
26124
26783
  configs.push(config);
26125
- save3(configs);
26784
+ save2(configs);
26126
26785
  }
26127
26786
  function clearProducerConfig(target) {
26128
- save3(load7().filter((c) => c.target !== target));
26787
+ save2(load6().filter((c) => c.target !== target));
26129
26788
  }
26130
26789
  function listProducerConfigs() {
26131
- return load7();
26790
+ return load6();
26132
26791
  }
26133
26792
 
26134
26793
  // src/producers/core-adapter.ts
@@ -26469,6 +27128,9 @@ var hostCore = {
26469
27128
  * read of a bundled constant. */
26470
27129
  builtinProfileIds: () => [...BUILTIN_PROFILES]
26471
27130
  };
27131
+ function resolveContainedProjectPath(projectRoot2, projectPath) {
27132
+ return assertContainedProjectPath(projectRoot2, projectPath);
27133
+ }
26472
27134
  function validateProjectAsComplete() {
26473
27135
  const config = loadProjectConfig();
26474
27136
  return validateAsComplete({ rules: config.rules, projectType: config.projectType });
@@ -26500,6 +27162,180 @@ var hostSdk = {
26500
27162
  extractArchive: (archive, destDir, limits) => sdkPortal.extractPack(archive, destDir, limits)
26501
27163
  };
26502
27164
 
27165
+ // src/server/projects.ts
27166
+ var ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
27167
+ function isValidProjectId(id) {
27168
+ return typeof id === "string" && ID_RE.test(id);
27169
+ }
27170
+ function registryPath(dataDir) {
27171
+ return path44.join(dataDir, "projects.json");
27172
+ }
27173
+ function load7(dataDir) {
27174
+ try {
27175
+ return JSON.parse(fs35.readFileSync(registryPath(dataDir), "utf8"));
27176
+ } catch {
27177
+ return [];
27178
+ }
27179
+ }
27180
+ function save3(dataDir, records) {
27181
+ const p = registryPath(dataDir);
27182
+ fs35.mkdirSync(path44.dirname(p), { recursive: true });
27183
+ const tmp = `${p}.tmp`;
27184
+ fs35.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
27185
+ fs35.renameSync(tmp, p);
27186
+ }
27187
+ function projectRoot(dataDir, id) {
27188
+ return path44.join(dataDir, "projects", id);
27189
+ }
27190
+ function existingProjectRoot(dataDir, id) {
27191
+ if (!isValidProjectId(id)) return null;
27192
+ const rec = load7(dataDir).find((r) => r.id === id);
27193
+ return rec ? rec.rootPath : null;
27194
+ }
27195
+ function createProjectRecord(dataDir, id) {
27196
+ if (!isValidProjectId(id)) {
27197
+ throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
27198
+ }
27199
+ const records = load7(dataDir);
27200
+ if (records.some((r) => r.id === id)) {
27201
+ throw new Error(`Project "${id}" already exists.`);
27202
+ }
27203
+ const root = projectRoot(dataDir, id);
27204
+ fs35.mkdirSync(root, { recursive: true });
27205
+ const record2 = {
27206
+ id,
27207
+ rootPath: root,
27208
+ status: "active",
27209
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
27210
+ };
27211
+ records.push(record2);
27212
+ save3(dataDir, records);
27213
+ return record2;
27214
+ }
27215
+ function registerLocalDevProject(dataDir, id, rootPath) {
27216
+ if (!isValidProjectId(id)) {
27217
+ throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
27218
+ }
27219
+ const records = load7(dataDir);
27220
+ const existing = records.find((r) => r.id === id);
27221
+ const record2 = {
27222
+ id,
27223
+ rootPath,
27224
+ status: "active",
27225
+ createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
27226
+ };
27227
+ const next = existing ? records.map((r) => r.id === id ? record2 : r) : [...records, record2];
27228
+ save3(dataDir, next);
27229
+ return record2;
27230
+ }
27231
+ function listProjectRecords(dataDir) {
27232
+ return load7(dataDir);
27233
+ }
27234
+ function removeProjectRecord(dataDir, id) {
27235
+ const records = load7(dataDir);
27236
+ const rec = records.find((r) => r.id === id);
27237
+ if (rec) {
27238
+ try {
27239
+ fs35.rmSync(rec.rootPath, { recursive: true, force: true });
27240
+ } catch {
27241
+ }
27242
+ }
27243
+ save3(dataDir, records.filter((r) => r.id !== id));
27244
+ }
27245
+ var SUBPROJECT_SEPARATOR = "::";
27246
+ function parseQualifiedSelector(value) {
27247
+ if (typeof value !== "string" || value.length === 0) return null;
27248
+ const [projectId, ...mounts] = value.split(SUBPROJECT_SEPARATOR);
27249
+ if (!isValidProjectId(projectId)) return null;
27250
+ if (mounts.some((m) => m.trim() === "")) return null;
27251
+ return { projectId, mounts };
27252
+ }
27253
+ function findSubsystemSpec(root, subsystemId) {
27254
+ const specsDir = aiPathsAt(root).specsDir();
27255
+ if (!fs35.existsSync(specsDir)) return null;
27256
+ for (const file of listFilesRecursive(specsDir, ".yaml")) {
27257
+ let raw;
27258
+ try {
27259
+ raw = readYamlFile(file);
27260
+ } catch {
27261
+ continue;
27262
+ }
27263
+ if (!raw || typeof raw !== "object" || !("parentSystem" in raw)) continue;
27264
+ if (raw.id !== subsystemId) continue;
27265
+ const pp = raw.projectPath;
27266
+ return typeof pp === "string" && pp.trim() !== "" ? { projectPath: pp } : {};
27267
+ }
27268
+ return null;
27269
+ }
27270
+ function resolveSubprojectMounts(projectId, projectRoot2, mounts) {
27271
+ let root = projectRoot2;
27272
+ let at = projectId;
27273
+ for (const mount of mounts) {
27274
+ const sub = findSubsystemSpec(root, mount);
27275
+ if (!sub) {
27276
+ throw new Error(
27277
+ `unknown subproject mount "${mount}" on "${at}" \u2014 no subsystem with that id exists in its spec tree`
27278
+ );
27279
+ }
27280
+ if (!sub.projectPath) {
27281
+ throw new Error(
27282
+ `subsystem "${mount}" on "${at}" is not a chained subproject (it carries no projectPath) \u2014 only a subsystem mounted via projectPath can be bound as a subproject`
27283
+ );
27284
+ }
27285
+ root = resolveContainedProjectPath(root, sub.projectPath);
27286
+ at = `${at}${SUBPROJECT_SEPARATOR}${mount}`;
27287
+ }
27288
+ return root;
27289
+ }
27290
+ function assertMintableNarrowingEntry(dataDir, entry) {
27291
+ if (entry === "*") return;
27292
+ const parsed = parseQualifiedSelector(entry);
27293
+ if (!parsed) {
27294
+ throw new Error(
27295
+ `invalid project narrowing entry "${entry}" (expected a project id, optionally subproject-qualified as projectId::subsystemId)`
27296
+ );
27297
+ }
27298
+ const rec = load7(dataDir).find((r) => r.id === parsed.projectId);
27299
+ if (!rec) throw new Error(`unknown project "${parsed.projectId}"`);
27300
+ if (parsed.mounts.length > 0) {
27301
+ resolveSubprojectMounts(parsed.projectId, rec.rootPath, parsed.mounts);
27302
+ }
27303
+ }
27304
+ function narrowingCovers(entry, target) {
27305
+ return target === entry || target.startsWith(entry + SUBPROJECT_SEPARATOR);
27306
+ }
27307
+ function resolveProjectBinding(dataDir, principal, selector) {
27308
+ const authorized = principal.projects;
27309
+ const wildcard = authorized.includes("*");
27310
+ let target;
27311
+ if (selector) {
27312
+ if (!wildcard && !authorized.some((e) => e !== "*" && narrowingCovers(e, selector))) return null;
27313
+ target = selector;
27314
+ } else if (!wildcard && authorized.length === 1) {
27315
+ target = authorized[0];
27316
+ } else {
27317
+ return null;
27318
+ }
27319
+ const parsed = parseQualifiedSelector(target);
27320
+ if (!parsed) return null;
27321
+ const rec = load7(dataDir).find((r) => r.id === parsed.projectId);
27322
+ if (!rec || rec.status !== "active") return null;
27323
+ let rootPath = rec.rootPath;
27324
+ if (parsed.mounts.length > 0) {
27325
+ try {
27326
+ rootPath = resolveSubprojectMounts(parsed.projectId, rec.rootPath, parsed.mounts);
27327
+ } catch {
27328
+ return null;
27329
+ }
27330
+ }
27331
+ const binding = { rootPath, projectId: parsed.projectId };
27332
+ if (parsed.mounts.length > 0) binding.subproject = parsed.mounts.join(SUBPROJECT_SEPARATOR);
27333
+ return binding;
27334
+ }
27335
+ function resolveProjectRoot(dataDir, principal, selector) {
27336
+ return resolveProjectBinding(dataDir, principal, selector)?.rootPath ?? null;
27337
+ }
27338
+
26503
27339
  // src/server/errors.ts
26504
27340
  var UnauthenticatedError = class extends Error {
26505
27341
  constructor() {
@@ -28525,11 +29361,8 @@ function mintToken(cfg, credential, request) {
28525
29361
  }
28526
29362
  assertNotReservedSubjectId(cfg, [request.ownerUserId]);
28527
29363
  const projects = request.projects?.length ? request.projects : ["*"];
28528
- const knownProjects = new Set(listProjectRecords(cfg.dataDir).map((p) => p.id));
28529
29364
  for (const p of projects) {
28530
- if (p !== "*" && !knownProjects.has(p)) {
28531
- throw new Error(`unknown project "${p}"`);
28532
- }
29365
+ assertMintableNarrowingEntry(cfg.dataDir, p);
28533
29366
  }
28534
29367
  const owner = findUserByRecordOrSubjectId(cfg.dataDir, request.ownerUserId);
28535
29368
  if (owner && owner.status !== "active") {
@@ -28566,12 +29399,21 @@ function revokeToken(cfg, credential, tokenId) {
28566
29399
  }
28567
29400
  function mintSelfToken(cfg, credential, projectId, write) {
28568
29401
  const principal = requirePrincipal5(cfg, credential);
28569
- if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY2, "project", projectId).value !== "yes") {
29402
+ const parsed = parseQualifiedSelector(projectId);
29403
+ if (!parsed) {
29404
+ throw new Error(
29405
+ `invalid project id "${projectId}" (expected a project id, optionally subproject-qualified as projectId::subsystemId)`
29406
+ );
29407
+ }
29408
+ if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY2, "project", parsed.projectId).value !== "yes") {
28570
29409
  throw new ForbiddenError("caller lacks project:read on the requested project");
28571
29410
  }
28572
- if (write && authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY2, "project", projectId).value !== "yes") {
29411
+ if (write && authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY2, "project", parsed.projectId).value !== "yes") {
28573
29412
  throw new ForbiddenError("caller lacks project:write on the requested project");
28574
29413
  }
29414
+ if (parsed.mounts.length > 0) {
29415
+ assertMintableNarrowingEntry(cfg.dataDir, projectId);
29416
+ }
28575
29417
  const token = "wk_" + crypto10.randomBytes(24).toString("hex");
28576
29418
  const owner = auditActor(principal);
28577
29419
  const record2 = {
@@ -29183,12 +30025,12 @@ function resolveVisibility(observerProjectId, units, placements) {
29183
30025
  const best = /* @__PURE__ */ new Map();
29184
30026
  for (const placement of placements) {
29185
30027
  if (placement.projectId === observerProjectId) continue;
29186
- const path62 = chainOf(placement.unitId, unitById);
29187
- if (!path62.length) continue;
29188
- const closedOk = path62.every((u) => effectivePosture(u, unitById) !== "closed" || observerInside(u.id) || grantedTo(u));
30028
+ const path61 = chainOf(placement.unitId, unitById);
30029
+ if (!path61.length) continue;
30030
+ const closedOk = path61.every((u) => effectivePosture(u, unitById) !== "closed" || observerInside(u.id) || grantedTo(u));
29189
30031
  if (!closedOk) continue;
29190
- const crossTenant = !tenantRoots.has(path62[path62.length - 1].id);
29191
- if (crossTenant && !path62.some(grantedTo)) continue;
30032
+ const crossTenant = !tenantRoots.has(path61[path61.length - 1].id);
30033
+ if (crossTenant && !path61.some(grantedTo)) continue;
29192
30034
  if (crossTenant && directUnits.length === 0) continue;
29193
30035
  const distance = crossTenant ? "partner" : sameBranch(placement.unitId) ? "department" : "instance";
29194
30036
  const existing = best.get(placement.projectId);
@@ -30127,10 +30969,9 @@ function migratePermissionModel(dataDir, apply) {
30127
30969
  // src/server/http.ts
30128
30970
  var http2 = __toESM(require("http"));
30129
30971
  var fs49 = __toESM(require("fs"));
30130
- var path59 = __toESM(require("path"));
30972
+ var path58 = __toESM(require("path"));
30131
30973
 
30132
30974
  // src/server/request.ts
30133
- var path58 = __toESM(require("path"));
30134
30975
  var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
30135
30976
  init_fs();
30136
30977
 
@@ -32537,7 +33378,31 @@ function reshapeLandscapeGraph(model, level) {
32537
33378
  }
32538
33379
  const kept = nodes.filter((n) => n.level <= level);
32539
33380
  const keptIds = new Set(kept.map((n) => n.id));
32540
- const edges = model.edges.filter((e) => keptIds.has(e.from) && keptIds.has(e.to));
33381
+ const projectNodeIdByProject = /* @__PURE__ */ new Map();
33382
+ const interfaceOwnerProject = /* @__PURE__ */ new Map();
33383
+ for (const n of model.nodes) {
33384
+ if (n.projectId === void 0) continue;
33385
+ if (n.nodeKind === "project") projectNodeIdByProject.set(n.projectId, n.id);
33386
+ else if (n.nodeKind === "publicInterface") interfaceOwnerProject.set(n.id, n.projectId);
33387
+ }
33388
+ const retarget = (endpoint) => {
33389
+ if (keptIds.has(endpoint)) return endpoint;
33390
+ const owner = interfaceOwnerProject.get(endpoint);
33391
+ const projectNode = owner === void 0 ? void 0 : projectNodeIdByProject.get(owner);
33392
+ return projectNode !== void 0 && keptIds.has(projectNode) ? projectNode : void 0;
33393
+ };
33394
+ const edges = [];
33395
+ for (const e of model.edges) {
33396
+ if (keptIds.has(e.from) && keptIds.has(e.to)) {
33397
+ edges.push(e);
33398
+ continue;
33399
+ }
33400
+ if (e.relationId === void 0) continue;
33401
+ const from = retarget(e.from);
33402
+ const to = retarget(e.to);
33403
+ if (from === void 0 || to === void 0 || from === to) continue;
33404
+ edges.push({ ...e, from, to });
33405
+ }
32541
33406
  return {
32542
33407
  tier: "landscape",
32543
33408
  nodes: kept,
@@ -35144,8 +36009,8 @@ var RealtimeHub = class {
35144
36009
  * complete the handshake, and register the connection. A bad path or session
35145
36010
  * destroys the socket. */
35146
36011
  handleUpgrade(cfg, req, socket) {
35147
- const path62 = (req.url ?? "/").split("?")[0];
35148
- if (path62 !== REALTIME_PATH || !isWebSocketUpgrade(req)) {
36012
+ const path61 = (req.url ?? "/").split("?")[0];
36013
+ if (path61 !== REALTIME_PATH || !isWebSocketUpgrade(req)) {
35149
36014
  socket.destroy();
35150
36015
  return;
35151
36016
  }
@@ -35290,7 +36155,7 @@ function deriveMcpOutcome(response) {
35290
36155
  if (r.result && typeof r.result === "object" && r.result.isError) return "failed";
35291
36156
  return "success";
35292
36157
  }
35293
- function auditToolCall(dataDir, principal, projectId, body, outcome) {
36158
+ function auditToolCall(dataDir, principal, projectId, body, outcome, subproject) {
35294
36159
  const target = mcpToolTarget(body);
35295
36160
  if (!target) return;
35296
36161
  const actor = principal.subject ?? {
@@ -35308,7 +36173,8 @@ function auditToolCall(dataDir, principal, projectId, body, outcome) {
35308
36173
  actor,
35309
36174
  tokenId: principal.tokenId,
35310
36175
  projectId,
35311
- target
36176
+ target,
36177
+ ...subproject ? { metadata: JSON.stringify({ subproject }) } : {}
35312
36178
  };
35313
36179
  try {
35314
36180
  appendAuditEvent(dataDir, event, DEFAULT_AUDIT_POLICY);
@@ -35512,8 +36378,8 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
35512
36378
  permissionSubject: { subjectId: "anonymous", roleBindings: [], instanceAdmin: true }
35513
36379
  };
35514
36380
  }
35515
- const root = resolveProjectRoot(cfg.dataDir, principal, projectSelector(req));
35516
- if (!root) {
36381
+ const binding = resolveProjectBinding(cfg.dataDir, principal, projectSelector(req));
36382
+ if (!binding) {
35517
36383
  sendJson(res, 403, { error: "project not authorized, unknown, or not specified" });
35518
36384
  return;
35519
36385
  }
@@ -35528,19 +36394,20 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
35528
36394
  });
35529
36395
  return;
35530
36396
  }
35531
- await runWithProjectRoot(root, async () => {
35532
- const projectId = path58.basename(root);
36397
+ await runWithProjectRoot(binding.rootPath, async () => {
36398
+ const projectId = binding.projectId;
36399
+ const subproject = binding.subproject;
35533
36400
  const dispatchedResponse = await dispatchProjectLifecycleTool(cfg, cred, projectId, body);
35534
36401
  if (dispatchedResponse !== void 0) {
35535
36402
  sendJson(res, 200, dispatchedResponse);
35536
- auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(dispatchedResponse));
36403
+ auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(dispatchedResponse), subproject);
35537
36404
  for (const ch of mcpChangeChannels(body, projectId, dispatchedResponse)) publishChange(ch);
35538
36405
  return;
35539
36406
  }
35540
36407
  const permissionError = dataPlanePermissionError(cfg, principal, projectId, body);
35541
36408
  if (permissionError !== void 0) {
35542
36409
  sendJson(res, 200, permissionError);
35543
- auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(permissionError));
36410
+ auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(permissionError), subproject);
35544
36411
  return;
35545
36412
  }
35546
36413
  const server = createScopedServer();
@@ -35561,7 +36428,7 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
35561
36428
  };
35562
36429
  await server.connect(transport);
35563
36430
  await transport.handleRequest(req, res, body);
35564
- auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(response));
36431
+ auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(response), subproject);
35565
36432
  for (const ch of mcpChangeChannels(body, projectId, response)) publishChange(ch);
35566
36433
  });
35567
36434
  }
@@ -35996,7 +36863,7 @@ function routeData(cfg, req, res) {
35996
36863
  }
35997
36864
  function readExposurePolicyFile(dataDir) {
35998
36865
  try {
35999
- const raw = fs49.readFileSync(path59.join(dataDir, "exposure-policy.json"), "utf8");
36866
+ const raw = fs49.readFileSync(path58.join(dataDir, "exposure-policy.json"), "utf8");
36000
36867
  const parsed = JSON.parse(raw);
36001
36868
  return parsed && typeof parsed === "object" ? parsed : void 0;
36002
36869
  } catch {
@@ -36681,9 +37548,9 @@ function seedDemoTree() {
36681
37548
 
36682
37549
  // src/commands/host.ts
36683
37550
  function resolveHostConfig(options) {
36684
- const dataDir = options.dataDir || process.env["WAIRON_DATA_DIR"] || path60.join(os9.homedir(), ".wairon", "data");
37551
+ const dataDir = options.dataDir || process.env["WAIRON_DATA_DIR"] || path59.join(os10.homedir(), ".wairon", "data");
36685
37552
  if (!process.env["WAIRON_PACKS_DIR"]) {
36686
- process.env["WAIRON_PACKS_DIR"] = path60.join(dataDir, "packs");
37553
+ process.env["WAIRON_PACKS_DIR"] = path59.join(dataDir, "packs");
36687
37554
  }
36688
37555
  const cfg = {
36689
37556
  host: options.host || "0.0.0.0",
@@ -36808,13 +37675,13 @@ function openBrowser(url) {
36808
37675
  }
36809
37676
  async function runDev(options = {}) {
36810
37677
  const cwd = process.cwd();
36811
- if (!fs50.existsSync(path60.join(cwd, ".wai"))) {
37678
+ if (!fs50.existsSync(path59.join(cwd, ".wai"))) {
36812
37679
  throw new WaironError(
36813
37680
  "No .wai/ found in the current directory. Run `wairon dev` from a wairon project root (or run `wairon init` first)."
36814
37681
  );
36815
37682
  }
36816
37683
  const hash = crypto20.createHash("sha256").update(cwd).digest("hex").slice(0, 16);
36817
- const dataDir = path60.join(os9.tmpdir(), "wairon-dev", hash);
37684
+ const dataDir = path59.join(os10.tmpdir(), "wairon-dev", hash);
36818
37685
  fs50.mkdirSync(dataDir, { recursive: true });
36819
37686
  registerLocalDevProject(dataDir, "local", cwd);
36820
37687
  const port = options.port ? Number(options.port) : 8080;
@@ -37223,8 +38090,8 @@ async function runHostPacks(action, options = {}) {
37223
38090
  }
37224
38091
  case "install": {
37225
38092
  if (!options.file) throw new WaironError("`--file <path>` (a declarative pack YAML) is required for install.");
37226
- const name = options.name ?? path60.basename(options.file).replace(/\.(ya?ml)$/i, "");
37227
- const content = fs50.readFileSync(path60.resolve(options.file), "utf8");
38093
+ const name = options.name ?? path59.basename(options.file).replace(/\.(ya?ml)$/i, "");
38094
+ const content = fs50.readFileSync(path59.resolve(options.file), "utf8");
37228
38095
  const desc = project2 ? installProjectPack(cfg, cred, project2, name, content) : installGlobalPack(cfg, cred, name, content);
37229
38096
  logger.success(`Installed ${scope} pack "${desc.name}" (${desc.profiles} profile(s), ${desc.languages} language(s)).`);
37230
38097
  if (project2) logger.info("Committed with the project \u2014 every clone and CI will enforce it.");
@@ -37430,13 +38297,27 @@ async function runSurface(action, options = {}) {
37430
38297
  for (const p of written) logger.info(` ${p}`);
37431
38298
  return;
37432
38299
  }
38300
+ case "externals": {
38301
+ const entries = listExternalInterfaces();
38302
+ if (!entries.length) {
38303
+ logger.info("No external surfaces available (.wai/surfaces/ holds no snapshots).");
38304
+ return;
38305
+ }
38306
+ const freshness = (f) => f === "fresh" ? import_chalk19.default.green(f) : f === "stale" ? import_chalk19.default.yellow(f) : import_chalk19.default.gray(f);
38307
+ for (const e of entries) {
38308
+ logger.info(
38309
+ `${e.sourceKind.padEnd(8)} ${import_chalk19.default.cyan(e.projectName)} [${e.origin}] ${freshness(e.freshness)} \u2014 ${e.interfaceIds.length ? e.interfaceIds.join(", ") : "(no interfaces)"}`
38310
+ );
38311
+ }
38312
+ return;
38313
+ }
37433
38314
  default:
37434
- throw new WaironError(`Unknown surface action "${action}" (supported: export, import, list, generate-children).`);
38315
+ throw new WaironError(`Unknown surface action "${action}" (supported: export, import, list, generate-children, externals).`);
37435
38316
  }
37436
38317
  }
37437
38318
 
37438
38319
  // src/commands/subsystem.ts
37439
- var path61 = __toESM(require("path"));
38320
+ var path60 = __toESM(require("path"));
37440
38321
  init_logger();
37441
38322
  init_errors();
37442
38323
  init_fs();
@@ -37472,9 +38353,9 @@ async function runSubsystemAdd(id, options = {}) {
37472
38353
  updatedAt: now
37473
38354
  };
37474
38355
  createChainedSubsystem(subsystem, displayName);
37475
- const childDir = path61.resolve(getProjectRoot(), options.projectPath);
38356
+ const childDir = path60.resolve(getProjectRoot(), options.projectPath);
37476
38357
  logger.success(`Added external subsystem "${id}" \u2192 ${options.projectPath}`);
37477
- logger.info(`Scaffolded child project at ${path61.relative(process.cwd(), childDir) || "."}`);
38358
+ logger.info(`Scaffolded child project at ${path60.relative(process.cwd(), childDir) || "."}`);
37478
38359
  logger.info(`Design its spec tree from this parent using namespaced ids (e.g. ${id}::<component>).`);
37479
38360
  }
37480
38361
  async function runSubsystemMove(id, options = {}) {
@@ -37497,9 +38378,9 @@ async function runSubsystemExternalize(id, options = {}) {
37497
38378
  throw new WaironError("--project-path (the subproject destination) is required.");
37498
38379
  }
37499
38380
  externalizeSubsystem(id, options.projectPath);
37500
- const childDir = path61.resolve(getProjectRoot(), options.projectPath);
38381
+ const childDir = path60.resolve(getProjectRoot(), options.projectPath);
37501
38382
  logger.success(`Externalized subsystem "${id}" \u2192 ${options.projectPath}`);
37502
- logger.info(`Moved its specs into ${path61.relative(process.cwd(), childDir) || "."} (now a standalone subproject).`);
38383
+ logger.info(`Moved its specs into ${path60.relative(process.cwd(), childDir) || "."} (now a standalone subproject).`);
37503
38384
  logger.info("Move the source code there yourself, then run `wairon validate` to confirm the tree.");
37504
38385
  }
37505
38386
  async function runSubsystemInternalize(id) {
@@ -37538,8 +38419,64 @@ program.command("generate").description("Generate agent output files from the sp
37538
38419
  dryRun: opts.dryRun
37539
38420
  });
37540
38421
  });
38422
+ async function runLock2(options) {
38423
+ assertProjectInitialized();
38424
+ if (!pathExists(AI_PATHS.specsSystem())) {
38425
+ logger.error("No SDD spec tree found (.wai/specs). Nothing to lock.");
38426
+ process.exit(1);
38427
+ }
38428
+ const projectConfig = loadProjectConfig();
38429
+ logger.info("Analyzing and validating specifications in-memory...");
38430
+ const dry = validateAsComplete({
38431
+ rules: projectConfig.rules,
38432
+ projectType: projectConfig.projectType,
38433
+ scopeSubsystem: options.subsystem,
38434
+ recursive: options.recursive ?? true
38435
+ });
38436
+ const errors = dry.issues.filter((i) => i.severity === "error");
38437
+ if (errors.length > 0) {
38438
+ logger.header("Cannot lock \u2014 the spec tree does not validate as complete");
38439
+ let errorCount = 0;
38440
+ const MAX_PRINT = 100;
38441
+ let skippedErrors = 0;
38442
+ for (const i of errors) {
38443
+ if (errorCount < MAX_PRINT) {
38444
+ logger.error(`${i.specId ? `[${i.specId}] ` : ""}[${i.code}] ${i.message}`);
38445
+ errorCount++;
38446
+ } else {
38447
+ skippedErrors++;
38448
+ }
38449
+ }
38450
+ if (skippedErrors > 0) {
38451
+ logger.error(`... and ${skippedErrors} more error(s) omitted.`);
38452
+ }
38453
+ logger.blank();
38454
+ logger.info("Fix the errors above, then run `wairon lock` again. Nothing was changed.");
38455
+ process.exit(1);
38456
+ }
38457
+ logger.header("Lock SDD specs");
38458
+ const record2 = await runLock(options, dry);
38459
+ if (!record2) {
38460
+ logger.info("Cancelled. Nothing was changed.");
38461
+ return;
38462
+ }
38463
+ const childPaths = generateChildSnapshots();
38464
+ if (childPaths.length > 0) {
38465
+ logger.blank();
38466
+ logger.success(`Regenerated the family/sibling surfaces into ${childPaths.length} chained child snapshot(s):`);
38467
+ for (const p of childPaths) logger.info(` ${p}`);
38468
+ }
38469
+ logger.blank();
38470
+ await runGenerate({ domain: options.subsystem });
38471
+ logger.blank();
38472
+ logger.success("Specs locked and agent topology generated.");
38473
+ logger.info(`Lock record written (.wai/lock.json): stateId ${record2.stateId.algorithm}:${record2.stateId.digest} \u2014 status ${record2.status}.`);
38474
+ logger.warn(
38475
+ "Restart any running AI agent sessions (Claude Code / Antigravity / Codex) so the newly generated implementer agents load \u2014 they are not picked up mid-session."
38476
+ );
38477
+ }
37541
38478
  program.command("lock").description("Final check before implementation: validate the spec tree as complete, freeze all specs to complete, and (re)generate the agent topology \u2014 only if it validates").option("-y, --yes", "skip the confirmation prompt (for scripts / CI)").option("--subsystem <id>", "only lock specs in the specified subsystem").option("--no-recursive", "do not recursively validate subprojects").action(async (opts) => {
37542
- await runLock({ yes: opts.yes, subsystem: opts.subsystem, recursive: opts.recursive });
38479
+ await runLock2({ yes: opts.yes, subsystem: opts.subsystem, recursive: opts.recursive });
37543
38480
  });
37544
38481
  program.command("validate").description("Validate the project configuration and the SDD Spec Tree").option("--ci", "treat warnings as errors for CI pipelines").option("--subsystem <id>", "only validate the specified subsystem (granular)").option("--no-recursive", "do not recursively validate subprojects").action(async (opts) => {
37545
38482
  await runValidate({ ci: opts.ci, subsystem: opts.subsystem, recursive: opts.recursive });
@@ -37655,7 +38592,7 @@ mcpCmd.command("status").description("Show whether the wairon MCP server is regi
37655
38592
  program.command("produce <target>").description("project the local project's specs to a producer target (notion | miro)").option("--page <id>", "parent page/board id in the target").option("--token <token>", "integration token (else env, else interactive prompt)").action(async (target, opts) => {
37656
38593
  await runProduce(target, { page: opts.page, token: opts.token });
37657
38594
  });
37658
- program.command("surface <action>").description("public surface exchange: export | import | list | generate-children").option("--audience <level>", "export ceiling: project | department | instance | partner | external (default instance)").option("--format <fmt>", "export format: native | openapi (default native)").option("--out <path>", "export output path (else print)").option("--portal <id>", "export: select one portal's OpenAPI spec (a multi-portal project renders one document per portal)").option("--source <path>", "import: the surface document (native snapshot YAML or OpenAPI)").option("--origin <origin>", "import provenance: exchanged | authored (default authored)").action(async (action, opts) => {
38595
+ program.command("surface <action>").description("public surface exchange: export | import | list | generate-children | externals").option("--audience <level>", "export ceiling: project | department | instance | partner | external (default instance)").option("--format <fmt>", "export format: native | openapi (default native)").option("--out <path>", "export output path (else print)").option("--portal <id>", "export: select one portal's OpenAPI spec (a multi-portal project renders one document per portal)").option("--source <path>", "import: the surface document (native snapshot YAML or OpenAPI)").option("--origin <origin>", "import provenance: exchanged | authored (default authored)").action(async (action, opts) => {
37659
38596
  await runSurface(action, {
37660
38597
  audience: opts.audience,
37661
38598
  format: opts.format,