@wairon/cli 5.0.2-dev.8 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.8";
68
+ WAIRON_VERSION = "5.1.0";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  ARCHITECT_AGENT_ID = "agent-architect";
71
71
  ARCHITECT_TEMPLATE_ID = "architect";
@@ -458,7 +458,7 @@ var init_domain = __esm({
458
458
  });
459
459
 
460
460
  // src/models/project.ts
461
- var import_zod3, BuiltinTargetConfigSchema, CustomTargetConfigSchema, TargetConfigSchema, NamingRuleConfigSchema, DocumentationRuleConfigSchema, ComplexityRuleConfigSchema, DesignDepthSchema, RulesConfigSchema, PathsConfigSchema, ProjectConfigSchema;
461
+ var import_zod3, BuiltinTargetConfigSchema, CustomTargetConfigSchema, TargetConfigSchema, NamingRuleConfigSchema, DocumentationRuleConfigSchema, ComplexityRuleConfigSchema, DesignDepthSchema, RulesConfigSchema, PathsConfigSchema, ProfileSelectionSubjectSchema, ProjectProfileSelectionSchema, ProjectConfigSchema;
462
462
  var init_project = __esm({
463
463
  "src/models/project.ts"() {
464
464
  "use strict";
@@ -582,6 +582,24 @@ var init_project = __esm({
582
582
  /** Base directory containing SDD specification files, relative to project root */
583
583
  specsDir: import_zod3.z.string().default(".wai/specs")
584
584
  });
585
+ ProfileSelectionSubjectSchema = import_zod3.z.object({
586
+ userId: import_zod3.z.string(),
587
+ kind: import_zod3.z.string(),
588
+ issuer: import_zod3.z.string(),
589
+ externalSubject: import_zod3.z.string().optional(),
590
+ displayName: import_zod3.z.string().optional(),
591
+ email: import_zod3.z.string().optional()
592
+ });
593
+ ProjectProfileSelectionSchema = import_zod3.z.object({
594
+ /** Selected architectural profile ids. The first resolvable one is applied as projectType. */
595
+ profileIds: import_zod3.z.array(import_zod3.z.string()).default([]),
596
+ /** Pack names the governing policy requires for this project. */
597
+ requiredPackNames: import_zod3.z.array(import_zod3.z.string()).default([]),
598
+ /** Pack names applied by default unless explicitly overridden. */
599
+ defaultPackNames: import_zod3.z.array(import_zod3.z.string()).optional(),
600
+ selectedBy: ProfileSelectionSubjectSchema.optional(),
601
+ selectedAt: import_zod3.z.string()
602
+ });
585
603
  ProjectConfigSchema = import_zod3.z.object({
586
604
  /**
587
605
  * Schema version — used to detect incompatible config formats in future
@@ -622,6 +640,13 @@ var init_project = __esm({
622
640
  useGlobalPacks: import_zod3.z.boolean().default(true)
623
641
  }).optional(),
624
642
  paths: PathsConfigSchema.default({}),
643
+ /**
644
+ * The profile/pack selection a hosted policy workflow applied to this project.
645
+ * The RECORD of what was chosen; `projectType` above is what actually governs
646
+ * validation. Modeled so the parse/write round trip preserves it (see
647
+ * ProjectProfileSelectionSchema).
648
+ */
649
+ profileSelection: ProjectProfileSelectionSchema.optional(),
625
650
  /**
626
651
  * Path to a directory containing org/user-level default templates.
627
652
  * Resolved before built-in templates but after project-local templates.
@@ -1095,6 +1120,18 @@ var init_specs = __esm({
1095
1120
  // Required if type is 'call', references Method name on target interface
1096
1121
  capability: import_zod6.z.string().optional(),
1097
1122
  // Required if type is 'dispatch': the capability routed through the target Portal's dispatch table
1123
+ /**
1124
+ * call/dispatch only: the credential this step presents to an authed callee
1125
+ * Portal, and WHERE it is loaded from (`from`). Two forms: an OPAQUE source
1126
+ * (`env:API_KEY`, a config key, `vault:path`, a free note) — a design note
1127
+ * wairon never resolves; or a MODELED reference `component:<id>` pointing at
1128
+ * the Adapter/Store that provides the secret — validated to resolve, be an
1129
+ * Adapter/Store, and be wired to the presenter (a checked graph edge). The
1130
+ * actual secret is never stored here. Absence on a call into a Portal whose
1131
+ * `auth ≠ none` warns (PORTAL_AUTH_UNMET), so credential loading is never
1132
+ * overlooked.
1133
+ */
1134
+ auth: import_zod6.z.object({ from: import_zod6.z.string(), note: import_zod6.z.string().optional() }).optional(),
1098
1135
  assertsGuarantees: import_zod6.z.array(GuaranteeSchema).optional(),
1099
1136
  /**
1100
1137
  * Declared entity invariants this step upholds, as "<type-id>.<invariant-id>"
@@ -1902,6 +1939,47 @@ var init_loader = __esm({
1902
1939
  }
1903
1940
  });
1904
1941
 
1942
+ // src/core/statehash.ts
1943
+ function computeStateId() {
1944
+ const tree = {
1945
+ system: loadSystemSpec(),
1946
+ subsystems: loadSubsystemSpecs(),
1947
+ components: loadComponentSpecs(),
1948
+ interfaces: loadInterfaceSpecs(),
1949
+ implementations: loadImplementationSpecs(),
1950
+ types: loadTypeSpecs()
1951
+ };
1952
+ const digest = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
1953
+ return { algorithm: "sha256", digest };
1954
+ }
1955
+ function stateIdEquals(a, b) {
1956
+ return !!a && !!b && a.algorithm === b.algorithm && a.digest === b.digest;
1957
+ }
1958
+ function canonicalize(value) {
1959
+ return JSON.stringify(sortKeys(value));
1960
+ }
1961
+ function sortKeys(v) {
1962
+ if (Array.isArray(v)) return v.map(sortKeys);
1963
+ if (v && typeof v === "object") {
1964
+ const src = v;
1965
+ const out = {};
1966
+ for (const k of Object.keys(src).sort()) {
1967
+ if (k === "createdAt" || k === "updatedAt") continue;
1968
+ out[k] = sortKeys(src[k]);
1969
+ }
1970
+ return out;
1971
+ }
1972
+ return v;
1973
+ }
1974
+ var crypto;
1975
+ var init_statehash = __esm({
1976
+ "src/core/statehash.ts"() {
1977
+ "use strict";
1978
+ crypto = __toESM(require("crypto"));
1979
+ init_specs2();
1980
+ }
1981
+ });
1982
+
1905
1983
  // src/core/narrative-labels.ts
1906
1984
  function resolveNarrativeLabels(methodName, steps) {
1907
1985
  const errors = [];
@@ -2987,6 +3065,16 @@ header input[type="search"]::placeholder { color:var(--dim); }
2987
3065
  #moreMenu .dropdown { display:block; width:100%; }
2988
3066
  #moreMenu .dropdown > .tbtn, #moreMenu > .tbtn { display:block; width:100%; text-align:left; border:none; background:transparent; margin:2px 0; }
2989
3067
  #moreMenu .dropdown > .tbtn:hover, #moreMenu > .tbtn:hover { background:var(--hover-bg); }
3068
+ /* Staged header compaction (responsive only, never persisted): compact
3069
+ stand-ins for the search input, the mode tabs, and the ancestor crumbs.
3070
+ All hidden at full width, so a roomy header renders exactly as before. */
3071
+ #searchBtn { position:relative; }
3072
+ #searchBtn.hasq::after { content:''; position:absolute; top:3px; right:3px; width:7px; height:7px; border-radius:50%; background:var(--accent); }
3073
+ .search-menu { min-width:210px; padding:8px; }
3074
+ .search-menu input[type="search"] { width:100%; }
3075
+ #modeMenu button.active { background:var(--accent); color:#fff; font-weight:700; }
3076
+ #crumbs .dropdown { display:inline-flex; }
3077
+ #crumbs .crumbmore { font-weight:700; }
2990
3078
 
2991
3079
  /* Settings panel \u2014 toggle switches */
2992
3080
  .settings-menu { min-width:266px; }
@@ -3110,9 +3198,17 @@ body.presentation #exitPresent, body.presentation #presentDetails { display:bloc
3110
3198
  <button data-vm="types">Types</button>
3111
3199
  <button data-vm="databases">Databases</button>
3112
3200
  </div>
3201
+ <div class="dropdown" id="modeDd" style="display:none">
3202
+ <button class="tbtn" id="modeBtn" title="Switch between the component architecture, the type ERD, or the database schemas">Components \u25BE</button>
3203
+ <div class="menu" id="modeMenu"></div>
3204
+ </div>
3113
3205
  <nav id="crumbs"></nav>
3114
3206
  <span class="divider"></span>
3115
3207
  <input id="search" type="search" placeholder="Search this view\u2026">
3208
+ <div class="dropdown" id="searchDd" style="display:none">
3209
+ <button class="tbtn" id="searchBtn" title="Search this view">\u{1F50D}</button>
3210
+ <div class="menu search-menu" id="searchMenu"></div>
3211
+ </div>
3116
3212
  <div class="seg" id="typesDetailSeg" style="display:none" title="ERD detail level">
3117
3213
  <button data-td="full">Full</button>
3118
3214
  <button data-td="fields">Fields</button>
@@ -3629,13 +3725,300 @@ var MODEL = __MODEL_JSON__;
3629
3725
  var BOX_W = 200, BOX_H = 56, SUBBOX_W = 230, SUBBOX_H = 84, GAP_X = 110, GAP_Y = 34;
3630
3726
  var INNER_W = 130, INNER_H = 36, INNER_GAPX = 26, INNER_GAPY = 12, HEAD_H = 34, PADI = 14;
3631
3727
 
3728
+ // ---- deep expansion (opt-in via subsystem.deepInternals) -------------------
3729
+ // When Internals is on and a subsystem record carries deepInternals:true, its
3730
+ // box renders its WHOLE subtree instead of one layer: deep-flagged child
3731
+ // subsystems become NESTED boundary boxes (recursing, defensively capped),
3732
+ // every other child renders as a fixed leaf tile that is never expanded
3733
+ // further. Sizes are computed bottom-up (a nested container tile takes its
3734
+ // recursive {w,h}); positions are emitted top-down by accumulating parent
3735
+ // top-left offsets. Relations between concrete visible endpoints are drawn
3736
+ // as ONE direct line each by buildDeepContext \u2014 crossing nested boundaries
3737
+ // on purpose (the full org overview of project relations); only relations
3738
+ // that cannot resolve to two concrete endpoints keep today's port machinery,
3739
+ // and ONLY at the outermost box. Without the flag this whole path is inert
3740
+ // and the classic one-layer innerLayout runs unchanged.
3741
+ var DEEP_MAX_DEPTH = 6;
3742
+ function isDeepId(subId) {
3743
+ var s = subById[subId];
3744
+ return !!(s && s.deepInternals);
3745
+ }
3746
+ // The DEEPEST visible tile representing compId inside the deep-expanded box
3747
+ // rooted at rootSubId: descend deep-flagged containers (the same expansion
3748
+ // rule as deepContainerLayout) until the containing child is a leaf tile.
3749
+ // Returns the child entry { kind, id } (its node id is IN(kind, id)).
3750
+ function deepLeafFor(compId, rootSubId) {
3751
+ var subId = rootSubId, depth = 1;
3752
+ for (;;) {
3753
+ var child = childOfScopeContaining(compId, { kind: 'subsystem', id: subId });
3754
+ if (!child) return null;
3755
+ if (child.kind === 'subsystem' && isDeepId(child.id) && depth < DEEP_MAX_DEPTH) {
3756
+ subId = child.id; depth += 1;
3757
+ continue;
3758
+ }
3759
+ return child;
3760
+ }
3761
+ }
3762
+ // One deep container's DIRECT children, placed with PER-TILE sizes (nested
3763
+ // containers take their recursive size; leaves stay INNER_W x INNER_H). The
3764
+ // placement mirrors innerLayout's strategy switch, generalised to variable
3765
+ // tile sizes. Tiles are centres relative to THIS container's top-left corner
3766
+ // (content sits right of PADI, below the HEAD_H label band); nested tiles
3767
+ // are relative to their own container, so emission accumulates offsets.
3768
+ function deepContainerLayout(subId, depth) {
3769
+ var kids = childrenOf({ kind: 'subsystem', id: subId });
3770
+ if (!kids.length) {
3771
+ // An EMPTY deep subsystem still shows as a (min-size) boundary box.
3772
+ return { tiles: [], w: INNER_W + 2 * PADI, h: HEAD_H + PADI };
3773
+ }
3774
+ var scope = { kind: 'subsystem', id: subId };
3775
+ var kidKey = function (k) { return k.kind + ':' + k.id; };
3776
+ var size = {};
3777
+ kids.forEach(function (k) {
3778
+ if (k.kind === 'subsystem' && isDeepId(k.id) && depth < DEEP_MAX_DEPTH) {
3779
+ var nested = deepContainerLayout(k.id, depth + 1);
3780
+ size[kidKey(k)] = { w: nested.w, h: nested.h, sub: nested };
3781
+ } else {
3782
+ size[kidKey(k)] = { w: INNER_W, h: INNER_H, sub: null };
3783
+ }
3784
+ });
3785
+ // Intra-container edges lifted to DIRECT children \u2014 for LAYOUT ONLY (the
3786
+ // drawn lines come from buildDeepContext's direct pass, never per level).
3787
+ var intra = {};
3788
+ MODEL.edges.forEach(function (edge) {
3789
+ var a = childOfScopeContaining(edge.from, scope);
3790
+ var b = childOfScopeContaining(edge.to, scope);
3791
+ if (!a || !b) return;
3792
+ var ak = a.kind + ':' + a.id, bk = b.kind + ':' + b.id;
3793
+ if (!size[ak] || !size[bk] || ak === bk) return;
3794
+ intra[ak + '=>' + bk] = 1;
3795
+ });
3796
+ var layer = {};
3797
+ function calc(k, stack) {
3798
+ var key = kidKey(k);
3799
+ if (layer[key] !== undefined) return layer[key];
3800
+ if (stack[key]) return 0;
3801
+ stack[key] = 1;
3802
+ var l = 0;
3803
+ if (k.kind === 'component') {
3804
+ var c = compById[k.id];
3805
+ if (c && (c.componentType === 'Portal' || c.componentType === 'Observer')) { layer[key] = 0; delete stack[key]; return 0; }
3806
+ }
3807
+ Object.keys(intra).forEach(function (ek) {
3808
+ var cut = ek.indexOf('=>');
3809
+ if (ek.slice(cut + 2) !== key) return;
3810
+ var srcKid = kids.filter(function (x) { return kidKey(x) === ek.slice(0, cut); })[0];
3811
+ if (srcKid) l = Math.max(l, calc(srcKid, stack) + 1);
3812
+ });
3813
+ delete stack[key];
3814
+ layer[key] = l;
3815
+ return l;
3816
+ }
3817
+ kids.forEach(function (k) { calc(k, {}); });
3818
+ var tiles = [], contentW = 0, contentH = 0;
3819
+ if (state.layout === 'grid') {
3820
+ // Row packing by ACTUAL tile size (the fixed per-column grid assumed
3821
+ // uniform tiles); the target row width follows the tile count, widened
3822
+ // to at least the widest single tile.
3823
+ var gsorted = kids.slice().sort(function (a, b) { return a.id < b.id ? -1 : 1; });
3824
+ var target = Math.max(1, Math.ceil(Math.sqrt(kids.length))) * (INNER_W + INNER_GAPX);
3825
+ kids.forEach(function (k) { var s0 = size[kidKey(k)]; if (s0.w > target) target = s0.w; });
3826
+ var gx = 0, gy = 0, rowH = 0;
3827
+ gsorted.forEach(function (k) {
3828
+ var s = size[kidKey(k)];
3829
+ if (gx > 0 && gx + s.w > target) { gx = 0; gy += rowH + INNER_GAPY; rowH = 0; }
3830
+ tiles.push({ kid: k, x: gx + s.w / 2, y: gy + s.h / 2, w: s.w, h: s.h, sub: s.sub });
3831
+ gx += s.w + INNER_GAPX;
3832
+ if (s.h > rowH) rowH = s.h;
3833
+ if (gx - INNER_GAPX > contentW) contentW = gx - INNER_GAPX;
3834
+ if (gy + rowH > contentH) contentH = gy + rowH;
3835
+ });
3836
+ } else if (state.layout === 'concentric' || state.layout === 'force') {
3837
+ var ideg = {};
3838
+ kids.forEach(function (k) { ideg[kidKey(k)] = 0; });
3839
+ Object.keys(intra).forEach(function (ek) {
3840
+ var cut2 = ek.indexOf('=>');
3841
+ var sk = ek.slice(0, cut2), tk = ek.slice(cut2 + 2);
3842
+ if (ideg[sk] !== undefined) ideg[sk]++;
3843
+ if (ideg[tk] !== undefined) ideg[tk]++;
3844
+ });
3845
+ var rel = concentricPositions(
3846
+ kids.map(kidKey),
3847
+ function (key) { return ideg[key] || 0; },
3848
+ function (key) { return { w: size[key].w, h: size[key].h }; }
3849
+ );
3850
+ // Normalise by the tiles' BOUNDING BOX (not just the centres) so a wide
3851
+ // nested container on the rim still clears the container's left/top pad.
3852
+ var minL = Infinity, minT = Infinity;
3853
+ kids.forEach(function (k) {
3854
+ var s1 = size[kidKey(k)], p1 = rel[kidKey(k)] || { x: 0, y: 0 };
3855
+ if (p1.x - s1.w / 2 < minL) minL = p1.x - s1.w / 2;
3856
+ if (p1.y - s1.h / 2 < minT) minT = p1.y - s1.h / 2;
3857
+ });
3858
+ if (minL === Infinity) { minL = 0; minT = 0; }
3859
+ kids.forEach(function (k) {
3860
+ var s2 = size[kidKey(k)], p2 = rel[kidKey(k)] || { x: 0, y: 0 };
3861
+ var cx = p2.x - minL, cyy = p2.y - minT;
3862
+ tiles.push({ kid: k, x: cx, y: cyy, w: s2.w, h: s2.h, sub: s2.sub });
3863
+ if (cx + s2.w / 2 > contentW) contentW = cx + s2.w / 2;
3864
+ if (cyy + s2.h / 2 > contentH) contentH = cyy + s2.h / 2;
3865
+ });
3866
+ } else {
3867
+ // Layered dependency columns: the column is as wide as its widest tile,
3868
+ // and each tile advances by ITS OWN height.
3869
+ var cols = {};
3870
+ kids.forEach(function (k) { var l = layer[kidKey(k)] || 0; (cols[l] = cols[l] || []).push(k); });
3871
+ var colKeys = Object.keys(cols).map(Number).sort(function (a, b) { return a - b; });
3872
+ var x = 0;
3873
+ colKeys.forEach(function (ck) {
3874
+ var col = cols[ck].sort(function (a, b) { return a.id < b.id ? -1 : 1; });
3875
+ var colW = 0, y = 0;
3876
+ col.forEach(function (k) { var s3 = size[kidKey(k)]; if (s3.w > colW) colW = s3.w; });
3877
+ col.forEach(function (k) {
3878
+ var s = size[kidKey(k)];
3879
+ tiles.push({ kid: k, x: x + colW / 2, y: y + s.h / 2, w: s.w, h: s.h, sub: s.sub });
3880
+ y += s.h + INNER_GAPY;
3881
+ });
3882
+ if (y - INNER_GAPY > contentH) contentH = y - INNER_GAPY;
3883
+ x += colW + INNER_GAPX;
3884
+ });
3885
+ contentW = x - INNER_GAPX;
3886
+ }
3887
+ tiles.forEach(function (t) { t.x += PADI; t.y += HEAD_H; });
3888
+ return { tiles: tiles, w: contentW + 2 * PADI, h: HEAD_H + contentH + PADI };
3889
+ }
3890
+ // Top-level deep box: the recursive interior plus today's port machinery at
3891
+ // the OUTERMOST box only (buildDeepContext supplies which relations still
3892
+ // need ports; stubs run port <-> the DEEPEST visible leaf tile). Returns the
3893
+ // same shape as innerLayout, plus deep:true so emission recurses.
3894
+ function deepLayout(entry, portRec) {
3895
+ var box = deepContainerLayout(entry.id, 1);
3896
+ var parentId = anchorNodeId(entry);
3897
+ var pBaseIn = 'p~in~' + parentId + '~', pBaseOut = 'p~out~' + parentId + '~';
3898
+ var extIn = portRec ? portRec.extIn : {}, extOut = portRec ? portRec.extOut : {};
3899
+ var inIds = Object.keys(extIn).sort(), outIds = Object.keys(extOut).sort();
3900
+ var hasIn = inIds.length > 0, hasOut = outIds.length > 0;
3901
+ var PROXY_W = 22, PROXY_H = 22, PROXY_GAP = 8;
3902
+ var shift = hasIn ? PROXY_W + INNER_GAPX : 0;
3903
+ var tiles = box.tiles;
3904
+ if (shift) tiles.forEach(function (t) { t.x += shift; });
3905
+ var w = box.w + shift + (hasOut ? PROXY_W + INNER_GAPX : 0);
3906
+ if (w < SUBBOX_W) w = SUBBOX_W;
3907
+ var stackMax = Math.max(inIds.length, outIds.length);
3908
+ var h = Math.max(box.h, HEAD_H + stackMax * PROXY_H + Math.max(0, stackMax - 1) * PROXY_GAP + PADI);
3909
+ var midY = HEAD_H + Math.max(0, (h - HEAD_H - PADI) / 2);
3910
+ function stackPorts(ids, recs, base, cx, dir) {
3911
+ var total = ids.length * PROXY_H + Math.max(0, ids.length - 1) * PROXY_GAP;
3912
+ var y0 = Math.max(HEAD_H + PROXY_H / 2, midY - total / 2 + PROXY_H / 2);
3913
+ return ids.map(function (eid, i) {
3914
+ var km = recs[eid].kids, klist = [];
3915
+ Object.keys(km).forEach(function (key) { klist.push(km[key]); });
3916
+ 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 };
3917
+ });
3918
+ }
3919
+ return {
3920
+ deep: true,
3921
+ tiles: tiles,
3922
+ edges: portRec ? Object.keys(portRec.stubs).map(function (k) { return portRec.stubs[k]; }) : [],
3923
+ proxies: stackPorts(inIds, extIn, pBaseIn, PADI + PROXY_W / 2, 'in')
3924
+ .concat(stackPorts(outIds, extOut, pBaseOut, w - PADI - PROXY_W / 2, 'out')),
3925
+ w: w,
3926
+ h: h,
3927
+ };
3928
+ }
3929
+ // View-level deep context (null unless Internals is on AND at least one
3930
+ // in-view entry is deep-flagged \u2014 the classic path never sees it): which
3931
+ // top-level entries are deep-expanded; every relation drawn as a DIRECT
3932
+ // concrete line (deduped per src=>tgt pair); the per-top-pair count used to
3933
+ // suppress aggregated edges whose constituents are ALL drawn directly; and
3934
+ // the port records for relations that keep today's port semantics.
3935
+ function buildDeepContext(scope, entries) {
3936
+ if (!state.internals) return null;
3937
+ var deepByAnchor = {}, any = false;
3938
+ entries.forEach(function (e) {
3939
+ if (e.kind === 'subsystem' && isDeepId(e.id)) { deepByAnchor[anchorNodeId(e)] = e; any = true; }
3940
+ });
3941
+ if (!any) return null;
3942
+ var entryByAnchor = {};
3943
+ entries.forEach(function (e) { entryByAnchor[e.kind + ':' + e.id] = e; });
3944
+ // A relation endpoint's DIRECT-line node in this view: the deepest leaf
3945
+ // tile inside a deep-expanded entry, or a top-level component box ITSELF.
3946
+ // null = this endpoint keeps aggregated/port semantics (interiors of
3947
+ // non-deep entries, out-of-scope counterparts).
3948
+ function directEnd(compId) {
3949
+ var child = childOfScopeContaining(compId, scope);
3950
+ var entry = child && entryByAnchor[child.kind + ':' + child.id];
3951
+ if (!entry) return null;
3952
+ var aid = anchorNodeId(entry);
3953
+ if (deepByAnchor[aid]) {
3954
+ var leaf = deepLeafFor(compId, entry.id);
3955
+ return leaf ? { node: IN(leaf.kind, leaf.id), top: aid, deep: true } : null;
3956
+ }
3957
+ if (entry.kind === 'component' && entry.id === compId) return { node: aid, top: aid, deep: false };
3958
+ return null;
3959
+ }
3960
+ var direct = {}, directTopCount = {}, ports = {};
3961
+ function portRec(aid) { return ports[aid] = ports[aid] || { extIn: {}, extOut: {}, stubs: {} }; }
3962
+ MODEL.edges.forEach(function (edge) {
3963
+ var a = directEnd(edge.from), b = directEnd(edge.to);
3964
+ if (a && b && (a.deep || b.deep) && a.node !== b.node) {
3965
+ // Drawn as ONE direct line \u2014 never ALSO as ports/stubs (dedupe rule).
3966
+ var key = a.node + '=>' + b.node;
3967
+ if (!direct[key]) direct[key] = { src: a.node, tgt: b.node, cross: false, aTop: a.top, bTop: b.top };
3968
+ if (edge.cross) direct[key].cross = true;
3969
+ if (a.top !== b.top) {
3970
+ var tk = a.top + '=>' + b.top;
3971
+ directTopCount[tk] = (directTopCount[tk] || 0) + 1;
3972
+ }
3973
+ return;
3974
+ }
3975
+ // Not a direct line: keep today's port semantics on any deep box with
3976
+ // exactly one endpoint inside its subtree, stubbed to the deepest leaf.
3977
+ var ac = childOfScopeContaining(edge.from, scope);
3978
+ var bc = childOfScopeContaining(edge.to, scope);
3979
+ var aEnt = ac && entryByAnchor[ac.kind + ':' + ac.id];
3980
+ var bEnt = bc && entryByAnchor[bc.kind + ':' + bc.id];
3981
+ var aAid = aEnt ? anchorNodeId(aEnt) : null;
3982
+ var bAid = bEnt ? anchorNodeId(bEnt) : null;
3983
+ if (aAid === bAid) return; // internal to one entry, or neither in scope
3984
+ if (aAid && deepByAnchor[aAid]) {
3985
+ var leafA = deepLeafFor(edge.from, aEnt.id);
3986
+ if (leafA) {
3987
+ var recA = portRec(aAid);
3988
+ var ro = recA.extOut[edge.to] = recA.extOut[edge.to] || { kids: {}, raws: {} };
3989
+ ro.kids[leafA.kind + ':' + leafA.id] = leafA;
3990
+ ro.raws[edge.from] = 1;
3991
+ var poId = 'p~out~' + aAid + '~' + edge.to;
3992
+ recA.stubs[IN(leafA.kind, leafA.id) + '=>' + poId] = { src: IN(leafA.kind, leafA.id), tgt: poId, stub: true };
3993
+ }
3994
+ }
3995
+ if (bAid && deepByAnchor[bAid]) {
3996
+ var leafB = deepLeafFor(edge.to, bEnt.id);
3997
+ if (leafB) {
3998
+ var recB = portRec(bAid);
3999
+ var ri = recB.extIn[edge.from] = recB.extIn[edge.from] || { kids: {}, raws: {} };
4000
+ ri.kids[leafB.kind + ':' + leafB.id] = leafB;
4001
+ ri.raws[edge.to] = 1;
4002
+ var piId = 'p~in~' + bAid + '~' + edge.from;
4003
+ recB.stubs[piId + '=>' + IN(leafB.kind, leafB.id)] = { src: piId, tgt: IN(leafB.kind, leafB.id), stub: true };
4004
+ }
4005
+ }
4006
+ });
4007
+ return { deepByAnchor: deepByAnchor, direct: direct, directTopCount: directTopCount, ports: ports };
4008
+ }
4009
+
3632
4010
  // Micro-layout for a container's direct children when Internals is on:
3633
4011
  // layered mini columns + intra-container edges. Each external relation gets
3634
4012
  // its own small PORT node INSIDE the container (one per external
3635
4013
  // counterpart; incoming left, outgoing right). Children connect to ports
3636
4014
  // with short edges that never leave the box \u2014 the real cross-boundary line
3637
4015
  // is only revealed on hover, or pinned while the port is selected.
3638
- function innerLayout(entry) {
4016
+ function innerLayout(entry, deepCtx) {
4017
+ // Deep-flagged subsystems take the recursive path (deepCtx exists only
4018
+ // when Internals is on and the view has deep entries \u2014 see buildElements).
4019
+ if (deepCtx && entry.kind === 'subsystem' && deepCtx.deepByAnchor[anchorNodeId(entry)]) {
4020
+ return deepLayout(entry, deepCtx.ports[anchorNodeId(entry)]);
4021
+ }
3639
4022
  var kids = state.internals && entry.hasKids ? childrenOf({ kind: entry.kind, id: entry.id }) : [];
3640
4023
  if (!kids.length) return null;
3641
4024
  var scope = { kind: entry.kind, id: entry.id };
@@ -4207,12 +4590,42 @@ var MODEL = __MODEL_JSON__;
4207
4590
  if (scope.kind === 'types' || scope.kind === 'databases') return buildTypeElements();
4208
4591
  var entries = childrenOf(scope);
4209
4592
  var eles = [];
4593
+ var deepCtx = buildDeepContext(scope, entries);
4210
4594
  var ve = viewEdges(scope, entries);
4211
4595
  // Data-coupling overlay: same scoping pipeline, a different edge source.
4212
4596
  var vd = state.dataCoupling ? viewEdges(scope, entries, MODEL.dataEdges) : { agg: {}, ghosts: {} };
4213
4597
  Object.keys(vd.ghosts).forEach(function (g) { if (!ve.ghosts[g]) ve.ghosts[g] = vd.ghosts[g]; });
4214
4598
  var inners = {};
4215
- entries.forEach(function (e) { inners[anchorNodeId(e)] = innerLayout(e); });
4599
+ entries.forEach(function (e) { inners[anchorNodeId(e)] = innerLayout(e, deepCtx); });
4600
+
4601
+ // Deep-mode recursive tile emission: a nested container becomes a cytoscape
4602
+ // compound parent (no explicit position \u2014 a compound derives its bounds
4603
+ // from its children); leaves and EMPTY containers are plain positioned
4604
+ // nodes. ox/oy = the emitting container's absolute top-left; tile.x/y are
4605
+ // centres relative to it, so offsets accumulate top-down. Search dimming
4606
+ // propagates the TOP entry's dim to the whole subtree.
4607
+ function emitDeepTiles(tiles, parentNodeId, ox, oy, dimCls) {
4608
+ tiles.forEach(function (tile) {
4609
+ var ax = ox + tile.x, ay = oy + tile.y;
4610
+ if (tile.sub) {
4611
+ var nid = SN(tile.kid.id);
4612
+ var selCls = state.selectedKind === 'subsystem' && state.selected === tile.kid.id ? ' sel' : '';
4613
+ var nested = {
4614
+ data: { id: nid, parent: parentNodeId, label: nameOf(tile.kid), w: tile.w, h: tile.h, tw: tile.w - 16 },
4615
+ classes: 'subsysBox' + (tile.sub.tiles.length ? ' drillable' : '') + dimCls + selCls,
4616
+ };
4617
+ if (!tile.sub.tiles.length) nested.position = { x: ax, y: ay };
4618
+ eles.push(nested);
4619
+ emitDeepTiles(tile.sub.tiles, nid, ax - tile.w / 2, ay - tile.h / 2, dimCls);
4620
+ } else {
4621
+ eles.push({
4622
+ 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 },
4623
+ position: { x: ax, y: ay },
4624
+ classes: 'inner' + dimCls,
4625
+ });
4626
+ }
4627
+ });
4628
+ }
4216
4629
 
4217
4630
  // Resolve a port's reveal target(s) in THIS view. Preference order: the
4218
4631
  // MATCHING PORT inside the counterpart's container (a port-to-port line
@@ -4344,17 +4757,25 @@ var MODEL = __MODEL_JSON__;
4344
4757
  + (state.showIssues && issuesBySpec[e.id] ? ' hasIssue' : '')
4345
4758
  + (state.selectedKind === e.kind && state.selected === e.id ? ' sel' : '');
4346
4759
  if (inner) {
4347
- 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 });
4348
- inner.tiles.forEach(function (tile) {
4349
- eles.push({
4350
- data: {
4351
- id: IN(tile.kid.kind, tile.kid.id), parent: aid,
4352
- label: nameOf(tile.kid), w: INNER_W, h: INNER_H, tw: INNER_W - 10,
4353
- },
4354
- position: { x: p.x - p.w / 2 + tile.x, y: p.y - p.h / 2 + tile.y },
4355
- classes: 'inner' + (dim ? ' dimmed' : ''),
4760
+ 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 };
4761
+ // An EMPTY deep boundary box has no children, so it is NOT a compound
4762
+ // parent \u2014 it needs (and honours) an explicit position and size.
4763
+ if (inner.deep && !inner.tiles.length && !(inner.proxies && inner.proxies.length)) boxNode.position = { x: p.x, y: p.y };
4764
+ eles.push(boxNode);
4765
+ if (inner.deep) {
4766
+ emitDeepTiles(inner.tiles, aid, p.x - p.w / 2, p.y - p.h / 2, dim ? ' dimmed' : '');
4767
+ } else {
4768
+ inner.tiles.forEach(function (tile) {
4769
+ eles.push({
4770
+ data: {
4771
+ id: IN(tile.kid.kind, tile.kid.id), parent: aid,
4772
+ label: nameOf(tile.kid), w: INNER_W, h: INNER_H, tw: INNER_W - 10,
4773
+ },
4774
+ position: { x: p.x - p.w / 2 + tile.x, y: p.y - p.h / 2 + tile.y },
4775
+ classes: 'inner' + (dim ? ' dimmed' : ''),
4776
+ });
4356
4777
  });
4357
- });
4778
+ }
4358
4779
  (inner.proxies || []).forEach(function (px) {
4359
4780
  eles.push({
4360
4781
  data: {
@@ -4507,9 +4928,28 @@ var MODEL = __MODEL_JSON__;
4507
4928
 
4508
4929
  var dimmedAnchors = {};
4509
4930
  entries.forEach(function (e) { if (state.query && !matches(e)) dimmedAnchors[anchorNodeId(e)] = true; });
4931
+
4932
+ // Deep mode: ONE direct line per related pair of concrete visible nodes \u2014
4933
+ // leaf tiles at any depth and/or top-level component boxes (deduped by
4934
+ // buildDeepContext). These lines cross nested boundaries on purpose.
4935
+ if (deepCtx) {
4936
+ var ddi = 0;
4937
+ Object.keys(deepCtx.direct).sort().forEach(function (key) {
4938
+ var d = deepCtx.direct[key];
4939
+ var ddim = state.query && (dimmedAnchors[d.aTop] || dimmedAnchors[d.bTop]);
4940
+ eles.push({
4941
+ data: { id: 'dd' + (ddi++), source: d.src, target: d.tgt, lbl: '' },
4942
+ classes: 'inneredge' + (d.cross ? ' cross' : '') + (ddim ? ' dimmed' : ''),
4943
+ });
4944
+ });
4945
+ }
4946
+
4510
4947
  var i = 0;
4511
4948
  Object.keys(ve.agg).forEach(function (key) {
4512
4949
  var e = ve.agg[key];
4950
+ // Prefer the leaf lines: drop an aggregated container edge whose
4951
+ // constituent relations were ALL drawn as direct deep lines above.
4952
+ if (deepCtx && deepCtx.directTopCount[key] >= e.n) return;
4513
4953
  var bundle = e.n > 1;
4514
4954
  var dim = state.query && (dimmedAnchors[e.src] || dimmedAnchors[e.tgt]);
4515
4955
  var route = routeData(e.src, e.tgt, key);
@@ -4768,22 +5208,58 @@ var MODEL = __MODEL_JSON__;
4768
5208
  }
4769
5209
  return path;
4770
5210
  }
5211
+ // Crumb compaction (compaction stage 5) is a RENDER MODE, not marker-based
5212
+ // reparenting: renderCrumbs rebuilds #crumbs' innerHTML on every navigation,
5213
+ // so nodes physically moved elsewhere would be destroyed by the next render.
5214
+ // The header-compaction stage toggles crumbsCompact and re-renders; compact
5215
+ // keeps the CURRENT scope visible and folds the ancestors into an ordered
5216
+ // "\\u2026" dropdown (document order, root first) whose entries navigate
5217
+ // exactly like the crumbs they replace.
5218
+ var crumbsCompact = false;
5219
+ var lastCrumbsHtml; // no initializer: the boot render at cy-init time precedes this line
5220
+ // Set by the header-compaction IIFE: crumb re-renders change the header's
5221
+ // CONTENT width without resizing #hdr itself (it is edge-anchored), so the
5222
+ // ResizeObserver never fires for them \u2014 renderCrumbs nudges a reflow here.
5223
+ var headerReflowHook = null;
5224
+ function crumbBtnHtml(p, cur) {
5225
+ return '<button class="crumb' + (cur ? ' cur' : '') + '" data-ck="' + p.kind + '" data-ci="' + (p.id || '') + '">' + p.label + '</button>';
5226
+ }
4771
5227
  function renderCrumbs() {
4772
5228
  var el = document.getElementById('crumbs');
4773
5229
  var path = crumbPath();
4774
- el.innerHTML = path.map(function (p, i) {
4775
- var cur = i === path.length - 1;
4776
- return '<button class="crumb' + (cur ? ' cur' : '') + '" data-ck="' + p.kind + '" data-ci="' + (p.id || '') + '">' + p.label + '</button>'
4777
- + (cur ? '' : '<span class="sep">\\u203A</span>');
4778
- }).join('');
5230
+ var html;
5231
+ if (crumbsCompact && path.length > 1) {
5232
+ html = '<span class="dropdown" id="crumbDd">'
5233
+ + '<button class="crumb crumbmore" id="crumbMoreBtn" title="Show the collapsed ancestor path">\\u2026</button>'
5234
+ + '<span class="menu" id="crumbMenu">'
5235
+ + path.slice(0, path.length - 1).map(function (p) {
5236
+ return '<button data-ck="' + p.kind + '" data-ci="' + (p.id || '') + '">' + p.label + '</button>';
5237
+ }).join('')
5238
+ + '</span></span>'
5239
+ + '<span class="sep">\\u203A</span>'
5240
+ + crumbBtnHtml(path[path.length - 1], true);
5241
+ } else {
5242
+ html = path.map(function (p, i) {
5243
+ var cur = i === path.length - 1;
5244
+ return crumbBtnHtml(p, cur) + (cur ? '' : '<span class="sep">\\u203A</span>');
5245
+ }).join('');
5246
+ }
5247
+ // No-op renders keep the already-wired nodes (and an open "\\u2026" menu)
5248
+ // intact \u2014 and don't churn the reflow scheduler while a search query types.
5249
+ if (html === lastCrumbsHtml) return;
5250
+ lastCrumbsHtml = html;
5251
+ el.innerHTML = html;
4779
5252
  var btns = el.querySelectorAll('button');
4780
5253
  for (var i = 0; i < btns.length; i++) {
4781
5254
  (function (b) {
5255
+ if (!b.getAttribute('data-ck')) return; // the "\\u2026" trigger toggles, never navigates
4782
5256
  b.addEventListener('click', function () {
4783
5257
  navigateTo(b.getAttribute('data-ck'), b.getAttribute('data-ci') || null);
4784
5258
  });
4785
5259
  })(btns[i]);
4786
5260
  }
5261
+ if (crumbsCompact && path.length > 1) wireDropdown('crumbDd', 'crumbMoreBtn');
5262
+ if (headerReflowHook) headerReflowHook();
4787
5263
  }
4788
5264
  function renderViewHint() {
4789
5265
  if (state.view.kind === 'types' || state.view.kind === 'databases') {
@@ -5050,7 +5526,16 @@ var MODEL = __MODEL_JSON__;
5050
5526
  return c ? { kind: 'subsystem', id: c.subsystem } : { kind: 'system', id: null };
5051
5527
  }
5052
5528
 
5053
- document.getElementById('search').addEventListener('input', function (ev) { state.query = ev.target.value.trim(); rebuild(false); });
5529
+ // While the search input is compacted behind the magnifier icon (compaction
5530
+ // stage 3), a non-empty query must stay discoverable \u2014 mark the icon with an
5531
+ // accent dot. The class is kept in sync on every query edit; the dot is only
5532
+ // ever visible while the compact icon itself is.
5533
+ function updateSearchBadge() {
5534
+ var b = document.getElementById('searchBtn');
5535
+ if (b && b.classList) b.classList[state.query ? 'add' : 'remove']('hasq');
5536
+ }
5537
+ document.getElementById('search').addEventListener('input', function (ev) { state.query = ev.target.value.trim(); updateSearchBadge(); rebuild(false); });
5538
+ updateSearchBadge();
5054
5539
  // Sync each View toggle's checkbox from the (possibly persisted) state, then
5055
5540
  // persist on change so the choices survive a refresh (see persist()/saved).
5056
5541
  document.getElementById('internalsToggle').checked = state.internals;
@@ -5104,9 +5589,24 @@ var MODEL = __MODEL_JSON__;
5104
5589
  })(btns[i]);
5105
5590
  }
5106
5591
  })();
5592
+ // Compaction stage 4 replaces the mode tabs with one dropdown trigger; its
5593
+ // label must follow the CURRENT mode. updateHeaderSegs runs on every rebuild,
5594
+ // so a mode change made while compact re-labels the trigger immediately.
5595
+ function updateModeBtn() {
5596
+ var b = document.getElementById('modeBtn');
5597
+ if (!b) return;
5598
+ var lbl = state.view.kind === 'types' ? 'Types' : state.view.kind === 'databases' ? 'Databases' : 'Components';
5599
+ b.textContent = lbl + ' \\u25BE';
5600
+ }
5107
5601
  function updateHeaderSegs() {
5108
5602
  var seg = document.getElementById('modeSeg');
5109
5603
  var btns = seg.querySelectorAll('button');
5604
+ if (!btns.length) {
5605
+ // Compaction stage 4 moved the real tab buttons into the mode dropdown \u2014
5606
+ // keep driving THEIR active classes there (they move back node-identical).
5607
+ var mm = document.getElementById('modeMenu');
5608
+ if (mm && mm.querySelectorAll) btns = mm.querySelectorAll('button');
5609
+ }
5110
5610
  for (var i = 0; i < btns.length; i++) {
5111
5611
  var vm = btns[i].getAttribute('data-vm');
5112
5612
  var active = vm === 'components'
@@ -5114,6 +5614,7 @@ var MODEL = __MODEL_JSON__;
5114
5614
  : vm === state.view.kind;
5115
5615
  if (btns[i].classList) btns[i].classList[active ? 'add' : 'remove']('active');
5116
5616
  }
5617
+ updateModeBtn();
5117
5618
  var td = document.getElementById('typesDetailSeg');
5118
5619
  td.style.display = (state.view.kind === 'types' || state.view.kind === 'databases') ? '' : 'none';
5119
5620
  var tbs = td.querySelectorAll('button');
@@ -5177,7 +5678,13 @@ var MODEL = __MODEL_JSON__;
5177
5678
  var r = btn.getBoundingClientRect();
5178
5679
  menu.style.top = (r.bottom + 6) + 'px';
5179
5680
  menu.style.left = 'auto';
5180
- menu.style.right = Math.max(6, window.innerWidth - r.right) + 'px';
5681
+ // Right-aligned to the trigger, but never pushed off the LEFT edge \u2014 the
5682
+ // compact search / crumb triggers (header compaction) sit on the header's
5683
+ // left side, where a 200px menu right-aligned to a narrow button would clip.
5684
+ var right = Math.max(6, window.innerWidth - r.right);
5685
+ var mw = menu.getBoundingClientRect ? menu.getBoundingClientRect().width : 0;
5686
+ if (mw && window.innerWidth - right - mw < 6) right = Math.max(6, window.innerWidth - mw - 6);
5687
+ menu.style.right = right + 'px';
5181
5688
  }
5182
5689
  function wireDropdown(ddId, btnId) {
5183
5690
  var dd = document.getElementById(ddId);
@@ -5196,11 +5703,29 @@ var MODEL = __MODEL_JSON__;
5196
5703
  var ldd = wireDropdown('layoutDd', 'layoutBtn');
5197
5704
  var sdd = wireDropdown('settingsDd', 'settingsBtn');
5198
5705
  var mdd = wireDropdown('moreDd', 'moreBtn');
5706
+ // Compact stand-ins (header compaction stages 3-4): the search panel and the
5707
+ // mode-tab dropdown are ordinary dropdowns; their triggers stay hidden until
5708
+ // their compaction stage shows them, so wiring them here is inert at full width.
5709
+ var qdd = wireDropdown('searchDd', 'searchBtn');
5710
+ var vdd = wireDropdown('modeDd', 'modeBtn');
5711
+ // Opening the compact search panel focuses the REAL input (stage 3 moves the
5712
+ // node, never clones it, so its input listener keeps driving state.query).
5713
+ // Registered after wireDropdown's toggle, so 'open' reflects the new state.
5714
+ document.getElementById('searchBtn').addEventListener('click', function () {
5715
+ if (String(qdd.className || '').indexOf('open') >= 0) {
5716
+ var inp = document.getElementById('search');
5717
+ if (inp && inp.focus) inp.focus();
5718
+ }
5719
+ });
5199
5720
  // Keep the settings panel open while flipping switches (clicks inside it don't
5200
5721
  // bubble to the document-level close handler).
5201
5722
  (function () {
5202
5723
  var m = document.getElementById('settingsMenu');
5203
5724
  if (m && m.addEventListener) m.addEventListener('click', function (ev) { if (ev && ev.stopPropagation) ev.stopPropagation(); });
5725
+ // Same for the floating search panel: clicking into the input must not
5726
+ // bubble to the document-level close handler and shut the panel mid-typing.
5727
+ var sm = document.getElementById('searchMenu');
5728
+ if (sm && sm.addEventListener) sm.addEventListener('click', function (ev) { if (ev && ev.stopPropagation) ev.stopPropagation(); });
5204
5729
  })();
5205
5730
 
5206
5731
  // Layout picker: choose the auto-layout algorithm. Components use cytoscape's
@@ -5224,6 +5749,12 @@ var MODEL = __MODEL_JSON__;
5224
5749
  });
5225
5750
  updateLayoutBtn();
5226
5751
 
5752
+ // The compact crumb dropdown (compaction stage 5) is re-created by every
5753
+ // compact crumb render, so it is looked up per close instead of captured.
5754
+ function closeCrumbDd() {
5755
+ var cdd = document.getElementById('crumbDd');
5756
+ if (cdd && cdd.classList) cdd.classList.remove('open');
5757
+ }
5227
5758
  if (document.addEventListener) {
5228
5759
  document.addEventListener('click', function () {
5229
5760
  if (dd.classList) dd.classList.remove('open');
@@ -5231,6 +5762,9 @@ var MODEL = __MODEL_JSON__;
5231
5762
  if (ldd.classList) ldd.classList.remove('open');
5232
5763
  if (sdd.classList) sdd.classList.remove('open');
5233
5764
  if (mdd && mdd.classList) mdd.classList.remove('open');
5765
+ if (qdd && qdd.classList) qdd.classList.remove('open');
5766
+ if (vdd && vdd.classList) vdd.classList.remove('open');
5767
+ closeCrumbDd();
5234
5768
  });
5235
5769
  document.addEventListener('keydown', function (ev) {
5236
5770
  if (ev.key === 'Escape') {
@@ -5238,16 +5772,27 @@ var MODEL = __MODEL_JSON__;
5238
5772
  if (modal.classList && String(modal.className).indexOf('open') >= 0) { closeFlow(); return; }
5239
5773
  setPresentation(false);
5240
5774
  if (dd.classList) dd.classList.remove('open');
5775
+ if (qdd && qdd.classList) qdd.classList.remove('open');
5776
+ if (vdd && vdd.classList) vdd.classList.remove('open');
5777
+ closeCrumbDd();
5241
5778
  }
5242
5779
  });
5243
5780
  }
5244
5781
 
5245
- // \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
5246
- // When the floating header no longer fits its controls, trailing items
5247
- // COLLAPSE into the More menu instead of relying on horizontal scroll \u2014
5248
- // every control stays one click away. Whole items move (listeners survive
5249
- // reparenting); a hidden placeholder pins each item's original position so
5250
- // restoring keeps the exact order. Collapse order = least-used first.
5782
+ // \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
5783
+ // When the floating header no longer fits its controls, standard reusable
5784
+ // COMPACTION BEHAVIORS apply progressively \u2014 each stage only while the row
5785
+ // still overflows \u2014 and restore in REVERSE order when space returns:
5786
+ // 1. trailing buttons fold into the "\u22EF" menu (least-used first, one by one)
5787
+ // 2. the View dropdown folds in after them
5788
+ // 3. the search input compacts to a \u{1F50D} icon + floating panel
5789
+ // 4. the mode tabs compact to one current-mode dropdown
5790
+ // 5. ancestor crumbs compact into an ordered "\u2026" dropdown
5791
+ // Stages 1-2 move whole items (listeners survive reparenting) with a hidden
5792
+ // placeholder pinning each item's original spot for restore; stage 5 is a
5793
+ // render mode (renderCrumbs rebuilds its innerHTML, so reparenting would not
5794
+ // survive navigation). Nothing here is persisted \u2014 compaction is purely
5795
+ // responsive to the available width.
5251
5796
  (function () {
5252
5797
  if (typeof window === 'undefined') return;
5253
5798
  var hdr = document.getElementById('hdr');
@@ -5275,7 +5820,122 @@ var MODEL = __MODEL_JSON__;
5275
5820
  }
5276
5821
  return markers[id];
5277
5822
  }
5278
- var collapsed = [];
5823
+ // Fold stage (the classic behavior): move items into the "\u22EF" menu ONE per
5824
+ // apply() call \u2014 the reflow loop keeps a stage active until it reports no
5825
+ // further progress, preserving the original per-button granularity.
5826
+ function foldStage(ids) {
5827
+ var folded = [];
5828
+ return {
5829
+ apply: function () {
5830
+ while (folded.length < ids.length) {
5831
+ var id = ids[folded.length];
5832
+ var el = movableFor(id);
5833
+ if (!el || el === moreDd || el.parentNode === moreMenu) { folded.push({ el: null, marker: null }); continue; }
5834
+ var m = markerFor(id, el);
5835
+ // A dropdown moved while open would strand its fixed-positioned menu.
5836
+ if (el.classList) el.classList.remove('open');
5837
+ moreDd.style.display = '';
5838
+ moreMenu.appendChild(el);
5839
+ folded.push({ el: el, marker: m });
5840
+ return true;
5841
+ }
5842
+ return false;
5843
+ },
5844
+ restore: function () {
5845
+ for (var i = folded.length - 1; i >= 0; i--) {
5846
+ var it = folded[i];
5847
+ if (it.el && it.marker && it.marker.parentNode) it.marker.parentNode.insertBefore(it.el, it.marker);
5848
+ }
5849
+ folded = [];
5850
+ },
5851
+ };
5852
+ }
5853
+ // Stage 3: the search input compacts behind a \u{1F50D} icon; the REAL input node
5854
+ // MOVES into the floating panel (fixed-positioned by the same helper as
5855
+ // every dropdown menu), so its input listener keeps driving state.query.
5856
+ function searchStage() {
5857
+ var on = false;
5858
+ return {
5859
+ apply: function () {
5860
+ if (on) return false;
5861
+ var inp = document.getElementById('search');
5862
+ var ddw = document.getElementById('searchDd');
5863
+ var menu = document.getElementById('searchMenu');
5864
+ if (!inp || !ddw || !menu) return false;
5865
+ menu.appendChild(inp);
5866
+ ddw.style.display = '';
5867
+ on = true;
5868
+ return true;
5869
+ },
5870
+ restore: function () {
5871
+ if (!on) return;
5872
+ on = false;
5873
+ var inp = document.getElementById('search');
5874
+ var ddw = document.getElementById('searchDd');
5875
+ if (inp && ddw && ddw.parentNode) ddw.parentNode.insertBefore(inp, ddw);
5876
+ if (ddw) { ddw.style.display = 'none'; if (ddw.classList) ddw.classList.remove('open'); }
5877
+ },
5878
+ };
5879
+ }
5880
+ // Stage 4: the mode tabs collapse into ONE dropdown labelled with the
5881
+ // current mode. The REAL tab buttons move into its menu (listeners and
5882
+ // active styling survive); updateHeaderSegs keeps the trigger label in
5883
+ // sync when the mode changes while compact.
5884
+ function modeStage() {
5885
+ var moved = [];
5886
+ return {
5887
+ apply: function () {
5888
+ if (moved.length) return false;
5889
+ var seg = document.getElementById('modeSeg');
5890
+ var ddw = document.getElementById('modeDd');
5891
+ var menu = document.getElementById('modeMenu');
5892
+ if (!seg || !ddw || !menu) return false;
5893
+ var btns = seg.querySelectorAll('button');
5894
+ if (!btns.length) return false;
5895
+ for (var i = 0; i < btns.length; i++) moved.push(btns[i]);
5896
+ for (var j = 0; j < moved.length; j++) menu.appendChild(moved[j]);
5897
+ seg.style.display = 'none';
5898
+ ddw.style.display = '';
5899
+ updateModeBtn();
5900
+ return true;
5901
+ },
5902
+ restore: function () {
5903
+ if (!moved.length) return;
5904
+ var seg = document.getElementById('modeSeg');
5905
+ var ddw = document.getElementById('modeDd');
5906
+ for (var i = 0; i < moved.length; i++) seg.appendChild(moved[i]);
5907
+ moved = [];
5908
+ seg.style.display = '';
5909
+ if (ddw) { ddw.style.display = 'none'; if (ddw.classList) ddw.classList.remove('open'); }
5910
+ },
5911
+ };
5912
+ }
5913
+ // Stage 5: crumb compaction is a render-mode toggle consulted by
5914
+ // renderCrumbs itself \u2014 see crumbsCompact there. Never reparenting.
5915
+ function crumbStage() {
5916
+ return {
5917
+ apply: function () {
5918
+ if (crumbsCompact) return false;
5919
+ crumbsCompact = true;
5920
+ renderCrumbs();
5921
+ return true;
5922
+ },
5923
+ restore: function () {
5924
+ if (!crumbsCompact) return;
5925
+ crumbsCompact = false;
5926
+ renderCrumbs();
5927
+ },
5928
+ };
5929
+ }
5930
+ // Ordered compaction stages: applied first-to-last only while the header
5931
+ // overflows, restored last-to-first when space returns.
5932
+ var STAGES = [
5933
+ foldStage(COLLAPSE), // 1: trailing buttons \u2192 "\u22EF" menu
5934
+ foldStage(['settingsBtn']), // 2: the View dropdown folds in too
5935
+ searchStage(), // 3: search input \u2192 \u{1F50D} + floating panel
5936
+ modeStage(), // 4: mode tabs \u2192 current-mode dropdown
5937
+ crumbStage(), // 5: ancestor crumbs \u2192 "\u2026" dropdown
5938
+ ];
5279
5939
  // Signed fit measure in px: positive = overflowing, negative = headroom.
5280
5940
  // The header is a flex row whose ONLY flex:1 child is the .spacer, so the
5281
5941
  // spacer's rendered width IS the free space -- it grows to absorb all slack
@@ -5291,33 +5951,54 @@ var MODEL = __MODEL_JSON__;
5291
5951
  var slack = spacer ? spacer.getBoundingClientRect().width : 0;
5292
5952
  return (hdr.scrollWidth - hdr.clientWidth) - slack;
5293
5953
  }
5954
+ var inReflow = false;
5294
5955
  function reflow() {
5295
5956
  // Not laid out (hidden tab, non-browser DOM) \u2014 measuring would misfire.
5296
5957
  var box = hdr.getBoundingClientRect();
5297
5958
  if (!box || box.width <= 0) return;
5298
- // Restore everything, then collapse until the row fits (idempotent).
5299
- for (var i = collapsed.length - 1; i >= 0; i--) {
5300
- var it = collapsed[i];
5301
- if (it.el && it.marker && it.marker.parentNode) it.marker.parentNode.insertBefore(it.el, it.marker);
5302
- }
5303
- collapsed = [];
5304
- moreDd.style.display = 'none';
5305
- hdr.scrollLeft = 0;
5306
- var guard = 0;
5307
- // Demand a few px of headroom, not a bare fit \u2014 the marginal-fit widths
5308
- // are exactly where the phantom scrollbar appeared.
5309
- while (overflowPx() > -8 && guard < COLLAPSE.length) {
5310
- var id = COLLAPSE[guard++];
5311
- var el = movableFor(id);
5312
- if (!el || el === moreDd || el.parentNode === moreMenu) continue;
5313
- var m = markerFor(id, el);
5314
- // A dropdown moved while open would strand its fixed-positioned menu.
5315
- if (el.classList) el.classList.remove('open');
5316
- moreDd.style.display = '';
5317
- moreMenu.appendChild(el);
5318
- collapsed.push({ el: el, marker: m });
5319
- }
5320
- if (collapsed.length === 0) moreDd.style.display = 'none';
5959
+ inReflow = true;
5960
+ try {
5961
+ // The floating search panel must survive a reflow cycle: restore-all
5962
+ // would close it (and reparenting blurs the input), so capture its
5963
+ // open/focus state up front and reinstate it after the stage walk.
5964
+ var ddw = document.getElementById('searchDd');
5965
+ var inp = document.getElementById('search');
5966
+ var searchOpen = !!(ddw && String(ddw.className || '').indexOf('open') >= 0);
5967
+ var searchFocus = false;
5968
+ try {
5969
+ var ae = (typeof ROOT !== 'undefined' && ROOT ? ROOT : document).activeElement;
5970
+ searchFocus = !!(ae && inp && ae === inp);
5971
+ } catch (e) { /* stubbed DOM */ }
5972
+ // Restore every stage in REVERSE order, then re-apply progressively
5973
+ // while the row still overflows (idempotent).
5974
+ for (var i = STAGES.length - 1; i >= 0; i--) STAGES[i].restore();
5975
+ moreDd.style.display = 'none';
5976
+ hdr.scrollLeft = 0;
5977
+ // Demand a few px of headroom, not a bare fit \u2014 the marginal-fit widths
5978
+ // are exactly where the phantom scrollbar appeared. Every apply()
5979
+ // changes the very widths being measured, but restore-all + a strictly
5980
+ // forward stage walk make the outcome a pure function of the current
5981
+ // width, and reflow never reschedules itself (renderCrumbs' nudge is
5982
+ // suppressed via inReflow, and #hdr's own box never changes here), so
5983
+ // boundary widths settle in ONE pass instead of oscillating.
5984
+ var si = 0;
5985
+ var guard = 0;
5986
+ var bound = COLLAPSE.length + STAGES.length + 8;
5987
+ while (overflowPx() > -8 && si < STAGES.length && guard < bound) {
5988
+ guard++;
5989
+ if (!STAGES[si].apply()) si++;
5990
+ }
5991
+ if (searchOpen || searchFocus) {
5992
+ var compactNow = ddw && ddw.style && ddw.style.display !== 'none';
5993
+ if (compactNow && searchOpen) {
5994
+ if (ddw.classList) ddw.classList.add('open');
5995
+ positionDropdownMenu(ddw, document.getElementById('searchBtn'));
5996
+ }
5997
+ if (searchFocus && inp && inp.focus) inp.focus();
5998
+ }
5999
+ } finally {
6000
+ inReflow = false;
6001
+ }
5321
6002
  }
5322
6003
  var raf = null;
5323
6004
  var defer = window.requestAnimationFrame
@@ -5327,6 +6008,9 @@ var MODEL = __MODEL_JSON__;
5327
6008
  if (raf !== null) return;
5328
6009
  raf = defer(function () { raf = null; reflow(); });
5329
6010
  }
6011
+ // Crumb re-renders change the header's content width without resizing #hdr
6012
+ // itself \u2014 renderCrumbs nudges a reflow through this hook (no-op mid-reflow).
6013
+ headerReflowHook = function () { if (!inReflow) schedule(); };
5330
6014
  if (typeof ResizeObserver !== 'undefined') {
5331
6015
  new ResizeObserver(schedule).observe(hdr);
5332
6016
  } else if (window.addEventListener) {
@@ -6654,7 +7338,7 @@ var init_extensions = __esm({
6654
7338
  });
6655
7339
 
6656
7340
  // src/core/rules/types.ts
6657
- var BUILTIN_PROFILES;
7341
+ var BUILTIN_PROFILES, PROJECT_KINDS;
6658
7342
  var init_types = __esm({
6659
7343
  "src/core/rules/types.ts"() {
6660
7344
  "use strict";
@@ -6667,6 +7351,7 @@ var init_types = __esm({
6667
7351
  "realtime-embedded",
6668
7352
  "plc-cyclic"
6669
7353
  ];
7354
+ PROJECT_KINDS = ["fullstack", "system-of-systems", "monorepo"];
6670
7355
  }
6671
7356
  });
6672
7357
 
@@ -6847,44 +7532,13 @@ var init_type_references = __esm({
6847
7532
  }
6848
7533
  });
6849
7534
 
6850
- // src/core/statehash.ts
6851
- function computeStateId() {
6852
- const tree = {
6853
- system: loadSystemSpec(),
6854
- subsystems: loadSubsystemSpecs(),
6855
- components: loadComponentSpecs(),
6856
- interfaces: loadInterfaceSpecs(),
6857
- implementations: loadImplementationSpecs(),
6858
- types: loadTypeSpecs()
6859
- };
6860
- const digest = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
6861
- return { algorithm: "sha256", digest };
6862
- }
6863
- function stateIdEquals(a, b) {
6864
- return !!a && !!b && a.algorithm === b.algorithm && a.digest === b.digest;
6865
- }
6866
- function canonicalize(value) {
6867
- return JSON.stringify(sortKeys(value));
7535
+ // src/utils/filenames.ts
7536
+ function safeFilenamePart(value) {
7537
+ return value.replace(/[^a-zA-Z0-9._-]/g, "-");
6868
7538
  }
6869
- function sortKeys(v) {
6870
- if (Array.isArray(v)) return v.map(sortKeys);
6871
- if (v && typeof v === "object") {
6872
- const src = v;
6873
- const out = {};
6874
- for (const k of Object.keys(src).sort()) {
6875
- if (k === "createdAt" || k === "updatedAt") continue;
6876
- out[k] = sortKeys(src[k]);
6877
- }
6878
- return out;
6879
- }
6880
- return v;
6881
- }
6882
- var crypto;
6883
- var init_statehash = __esm({
6884
- "src/core/statehash.ts"() {
7539
+ var init_filenames = __esm({
7540
+ "src/utils/filenames.ts"() {
6885
7541
  "use strict";
6886
- crypto = __toESM(require("crypto"));
6887
- init_specs2();
6888
7542
  }
6889
7543
  });
6890
7544
 
@@ -7341,6 +7995,66 @@ function projectOwnSurface(maxAudience) {
7341
7995
  function projectChildSurface() {
7342
7996
  return projectOwnSurface("project");
7343
7997
  }
7998
+ function localName(id) {
7999
+ return id.split("::").pop();
8000
+ }
8001
+ function projectSubsystemSurface(subsystemId) {
8002
+ const system = loadSystemSpec();
8003
+ if (!system) {
8004
+ throw new Error("Cannot project a subsystem surface: the L0 system spec is missing.");
8005
+ }
8006
+ const subsystems = loadSubsystemSpecs();
8007
+ const target = subsystems.find((s) => s.id === subsystemId);
8008
+ if (!target) {
8009
+ throw new Error(`Cannot project a subsystem surface: subsystem "${subsystemId}" does not exist.`);
8010
+ }
8011
+ const components = loadComponentSpecs();
8012
+ const interfaces = loadInterfaceSpecs();
8013
+ const types = loadTypeSpecs();
8014
+ const entries = [];
8015
+ const unprojectable = [];
8016
+ for (const pub of target.publicInterfaces ?? []) {
8017
+ if (!pub.component) continue;
8018
+ const comp = components.find((c) => c.id === pub.component || c.id === `${subsystemId}::${pub.component}`);
8019
+ if (!comp) continue;
8020
+ if (!CROSS_BOUNDARY_TARGETS.has(comp.componentType)) {
8021
+ unprojectable.push({ component: pub.component, componentType: comp.componentType });
8022
+ continue;
8023
+ }
8024
+ const compInterfaces = interfaces.filter((i) => i.component === comp.id && (!pub.interface || i.id === pub.interface || i.id === `${subsystemId}::${pub.interface}`));
8025
+ const methods = compInterfaces.flatMap((i) => i.methods);
8026
+ entries.push({
8027
+ id: localName(pub.interface ?? comp.id),
8028
+ name: comp.name,
8029
+ // Family ceiling: a sibling surface is consumable by the system family only.
8030
+ audience: "project",
8031
+ type: pub.type ?? "Custom",
8032
+ // The snapshot carries the LOCAL portal name — consumers resolve cross-tree
8033
+ // refs by their final segment.
8034
+ component: localName(comp.id),
8035
+ methods,
8036
+ ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
8037
+ // Project the backing component's auth + basePath so the codec can emit
8038
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
8039
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
8040
+ ...comp.basePath ? { basePath: comp.basePath } : {},
8041
+ details: pub.details ?? ""
8042
+ });
8043
+ }
8044
+ for (const skipped of unprojectable) {
8045
+ console.error(
8046
+ `[surfaces] skipped "${subsystemId}::${skipped.component}": a published ${skipped.componentType} can never serve a cross-boundary caller, so it stays out of every chained child's sibling surface \u2014 publish this surface through a Portal, a Gateway, or an Observer (for events).`
8047
+ );
8048
+ }
8049
+ return SurfaceSnapshotSchema.parse({
8050
+ projectName: `${system.name}::${subsystemId}`,
8051
+ origin: "generated",
8052
+ stateId: stateIdString(),
8053
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
8054
+ interfaces: entries,
8055
+ types: computeTypeClosure(entries, types)
8056
+ });
8057
+ }
7344
8058
  function listSnapshots(rootDir = getProjectRoot()) {
7345
8059
  const dir = surfacesDir(rootDir);
7346
8060
  if (!fs9.existsSync(dir)) return [];
@@ -7357,39 +8071,66 @@ function listSnapshots(rootDir = getProjectRoot()) {
7357
8071
  function getSnapshot(projectName, rootDir = getProjectRoot()) {
7358
8072
  return listSnapshots(rootDir).find((s) => s.projectName === projectName) ?? null;
7359
8073
  }
8074
+ function snapshotFilename(projectName) {
8075
+ return `${safeFilenamePart(projectName)}.yaml`;
8076
+ }
7360
8077
  function saveSnapshot(snapshot, rootDir = getProjectRoot()) {
7361
8078
  const dir = surfacesDir(rootDir);
7362
8079
  fs9.mkdirSync(dir, { recursive: true });
7363
- const p = path10.join(dir, `${snapshot.projectName}.yaml`);
8080
+ const p = path10.join(dir, snapshotFilename(snapshot.projectName));
7364
8081
  writeYamlFile(p, SurfaceSnapshotSchema.parse(snapshot));
7365
8082
  return p;
7366
8083
  }
7367
8084
  function loadSurfaceSnapshots() {
7368
8085
  return listSnapshots();
7369
8086
  }
7370
- function renderOpenApiForms(snapshot) {
7371
- const renderedSet = toOpenApiSet(snapshot);
7372
- return { renderedSet, ...renderedSet.length === 1 ? { rendered: renderedSet[0].document } : {} };
8087
+ function selectPortalSpec(renderedSet, portalId) {
8088
+ const hit = renderedSet.find((spec) => spec.portalId === portalId);
8089
+ if (!hit) {
8090
+ const known = renderedSet.map((s) => s.portalId).join(", ");
8091
+ throw new Error(`Unknown portal "${portalId}" \u2014 this surface renders: ${known || "(no portals)"}.`);
8092
+ }
8093
+ return [hit];
8094
+ }
8095
+ function perPortalPath(resolvedOut, portalId) {
8096
+ const ext = path10.extname(resolvedOut);
8097
+ const stem2 = ext ? resolvedOut.slice(0, -ext.length) : resolvedOut;
8098
+ return `${stem2}.${safeFilenamePart(portalId)}${ext}`;
7373
8099
  }
7374
- function writeSurfaceFile(outPath, body, snapshot) {
8100
+ function writeSurfaceFile(outPath, snapshot, renderedSet) {
7375
8101
  const resolved = path10.resolve(outPath);
7376
8102
  fs9.mkdirSync(path10.dirname(resolved), { recursive: true });
7377
- if (body !== void 0) fs9.writeFileSync(resolved, body);
7378
- else writeYamlFile(resolved, snapshot);
7379
- return resolved;
8103
+ if (!renderedSet || renderedSet.length === 0) {
8104
+ writeYamlFile(resolved, snapshot);
8105
+ return [resolved];
8106
+ }
8107
+ if (renderedSet.length === 1) {
8108
+ fs9.writeFileSync(resolved, renderedSet[0].document);
8109
+ return [resolved];
8110
+ }
8111
+ return renderedSet.map((spec) => {
8112
+ const target = perPortalPath(resolved, spec.portalId);
8113
+ fs9.writeFileSync(target, spec.document);
8114
+ return target;
8115
+ });
7380
8116
  }
7381
- function exportSurface(maxAudience, format, outPath) {
7382
- const snapshot = projectOwnSurface(maxAudience);
7383
- const openapi = format === "openapi" ? renderOpenApiForms(snapshot) : void 0;
7384
- const body = openapi?.rendered ?? openapi?.renderedSet[0]?.document;
7385
- const writtenTo = outPath ? writeSurfaceFile(outPath, body, snapshot) : void 0;
8117
+ function exportResult(snapshot, renderedSet, writtenPaths) {
8118
+ const rendered = renderedSet?.length === 1 ? renderedSet[0].document : void 0;
7386
8119
  return {
7387
8120
  snapshot,
7388
- ...openapi?.rendered !== void 0 ? { rendered: openapi.rendered } : {},
7389
- ...openapi ? { renderedSet: openapi.renderedSet } : {},
7390
- ...writtenTo ? { writtenTo } : {}
8121
+ ...rendered !== void 0 ? { rendered } : {},
8122
+ ...renderedSet ? { renderedSet } : {},
8123
+ ...writtenPaths.length === 1 ? { writtenTo: writtenPaths[0] } : {},
8124
+ ...writtenPaths.length ? { writtenPaths } : {}
7391
8125
  };
7392
8126
  }
8127
+ function exportSurface(maxAudience, format, outPath, portalId) {
8128
+ const snapshot = projectOwnSurface(maxAudience);
8129
+ let renderedSet = format === "openapi" ? toOpenApiSet(snapshot) : void 0;
8130
+ if (renderedSet && portalId) renderedSet = selectPortalSpec(renderedSet, portalId);
8131
+ const writtenPaths = outPath ? writeSurfaceFile(outPath, snapshot, renderedSet) : [];
8132
+ return exportResult(snapshot, renderedSet, writtenPaths);
8133
+ }
7393
8134
  function importSurface(sourcePath, origin) {
7394
8135
  const resolved = path10.resolve(sourcePath);
7395
8136
  if (!fs9.existsSync(resolved)) {
@@ -7409,17 +8150,54 @@ function importSurface(sourcePath, origin) {
7409
8150
  return snapshot;
7410
8151
  }
7411
8152
  function generateChildSnapshots(rootDir = getProjectRoot()) {
7412
- const children = loadSubsystemSpecs().filter((s) => s.projectPath && !s.id.includes("::"));
8153
+ const topLevel = loadSubsystemSpecs().filter((s) => !s.id.includes("::"));
8154
+ const children = topLevel.filter((s) => s.projectPath);
7413
8155
  if (!children.length) return [];
7414
- const snapshot = projectChildSurface();
8156
+ const familySnapshot = projectChildSurface();
8157
+ const siblingSnapshots = /* @__PURE__ */ new Map();
8158
+ const siblingSurface = (subsystemId) => {
8159
+ let snap = siblingSnapshots.get(subsystemId);
8160
+ if (!snap) {
8161
+ snap = projectSubsystemSurface(subsystemId);
8162
+ siblingSnapshots.set(subsystemId, snap);
8163
+ }
8164
+ return snap;
8165
+ };
7415
8166
  const written = [];
7416
8167
  for (const child of children) {
7417
8168
  const childDir = path10.resolve(rootDir, child.projectPath);
7418
8169
  if (!fs9.existsSync(childDir)) continue;
7419
- written.push(saveSnapshot(snapshot, childDir));
8170
+ written.push(saveSnapshot(familySnapshot, childDir));
8171
+ for (const sibling of topLevel) {
8172
+ if (sibling.id === child.id) continue;
8173
+ written.push(saveSnapshot(siblingSurface(sibling.id), childDir));
8174
+ }
7420
8175
  }
7421
8176
  return written;
7422
8177
  }
8178
+ function computeParentStateId(parentRoot) {
8179
+ return computeStateIdAt(parentRoot);
8180
+ }
8181
+ function listExternalInterfaces() {
8182
+ const snapshots = listSnapshots();
8183
+ const chainingParent = resolveChainingParent();
8184
+ const parentStateId = chainingParent ? computeParentStateId(chainingParent.parentRoot) : null;
8185
+ return snapshots.map((snapshot) => {
8186
+ const generated = snapshot.origin === "generated";
8187
+ const sourceKind = !generated ? "foreign" : snapshot.projectName.includes("::") ? "sibling" : "parent";
8188
+ const freshness = generated && parentStateId ? snapshot.stateId === parentStateId ? "fresh" : "stale" : "unverifiable";
8189
+ return {
8190
+ projectName: snapshot.projectName,
8191
+ origin: snapshot.origin,
8192
+ sourceKind,
8193
+ generatedAt: snapshot.generatedAt,
8194
+ ...snapshot.stateId ? { stateId: snapshot.stateId } : {},
8195
+ ...snapshot.version ? { version: snapshot.version } : {},
8196
+ freshness,
8197
+ interfaceIds: snapshot.interfaces.map((e) => e.id)
8198
+ };
8199
+ });
8200
+ }
7423
8201
  function surfaceContentKey(snapshot) {
7424
8202
  const { stateId, generatedAt, origin, ...content } = snapshot;
7425
8203
  return JSON.stringify(content);
@@ -7446,7 +8224,7 @@ function checkChildSurfaceFreshness(rootDir = getProjectRoot()) {
7446
8224
  }
7447
8225
  return issues;
7448
8226
  }
7449
- var fs9, path10, SURFACES_DIRNAME;
8227
+ var fs9, path10, SURFACES_DIRNAME, CROSS_BOUNDARY_TARGETS;
7450
8228
  var init_surfaces = __esm({
7451
8229
  "src/core/surfaces.ts"() {
7452
8230
  "use strict";
@@ -7454,12 +8232,14 @@ var init_surfaces = __esm({
7454
8232
  path10 = __toESM(require("path"));
7455
8233
  init_fs();
7456
8234
  init_yaml();
8235
+ init_filenames();
7457
8236
  init_models();
7458
8237
  init_specs2();
7459
8238
  init_statehash();
7460
8239
  init_type_analysis();
7461
8240
  init_openapi();
7462
8241
  SURFACES_DIRNAME = "surfaces";
8242
+ CROSS_BOUNDARY_TARGETS = /* @__PURE__ */ new Set(["Portal", "Gateway", "Observer"]);
7463
8243
  }
7464
8244
  });
7465
8245
 
@@ -7627,7 +8407,8 @@ var init_contracts = __esm({
7627
8407
  "SURFACE_REF_NOT_EXPOSED",
7628
8408
  `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}".`,
7629
8409
  impl.id,
7630
- isDraftCtx
8410
+ isDraftCtx,
8411
+ true
7631
8412
  );
7632
8413
  }
7633
8414
  continue;
@@ -7684,7 +8465,8 @@ var init_contracts = __esm({
7684
8465
  "SURFACE_REF_NOT_EXPOSED",
7685
8466
  `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}".`,
7686
8467
  impl.id,
7687
- isDraftCtx
8468
+ isDraftCtx,
8469
+ true
7688
8470
  );
7689
8471
  } else if (step.assertsGuarantees) {
7690
8472
  const declared = new Set(surfaceMethod.guarantees ?? []);
@@ -7695,7 +8477,8 @@ var init_contracts = __esm({
7695
8477
  "NARRATIVE_SEMANTIC_UNBACKED",
7696
8478
  `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}".`,
7697
8479
  impl.id,
7698
- isDraftCtx
8480
+ isDraftCtx,
8481
+ true
7699
8482
  );
7700
8483
  }
7701
8484
  }
@@ -9046,13 +9829,14 @@ var init_portals = __esm({
9046
9829
  };
9047
9830
  portalsRule = {
9048
9831
  name: "portal-endpoints",
9049
- description: "A Portal declares its portalType and binds every interface method to a concrete endpoint of the matching transport. Non-Portal components carry no portalType, basePath, or endpoints.",
9832
+ description: "A Portal declares its portalType and binds every interface method to a concrete endpoint of the matching transport. Non-Portal components carry no portalType, basePath, endpoints, or auth (auth is inbound transport auth \u2014 a Gateway carries it on the Portal it owns).",
9050
9833
  codes: [
9051
9834
  { code: "MISSING_PORTAL_TYPE", defaultSeverity: "error", summary: "Portal without a portalType" },
9052
9835
  { code: "MISSING_ENDPOINT", defaultSeverity: "error", summary: "Portal method without a wire endpoint binding" },
9053
9836
  { code: "ENDPOINT_TRANSPORT_MISMATCH", defaultSeverity: "error", summary: "Endpoint transport does not match the Portal portalType" },
9054
9837
  { code: "UNEXPECTED_PORTAL_FIELD", defaultSeverity: "error", summary: "Non-Portal component with portalType/basePath" },
9055
- { code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" }
9838
+ { code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" },
9839
+ { code: "AUTH_ON_NON_PORTAL", defaultSeverity: "warning", summary: "Non-Portal component declaring auth (auth is inbound transport auth, only meaningful on a Portal)" }
9056
9840
  ],
9057
9841
  check(ctx) {
9058
9842
  for (const comp of ctx.components) {
@@ -9104,6 +9888,15 @@ var init_portals = __esm({
9104
9888
  isDraftCtx
9105
9889
  );
9106
9890
  }
9891
+ if (comp.auth !== void 0) {
9892
+ ctx.addIssue(
9893
+ "warning",
9894
+ "AUTH_ON_NON_PORTAL",
9895
+ `Component "${comp.id}" is a ${comp.componentType}, not a Portal, but declares "auth". Auth is inbound transport auth and is only meaningful on a Portal (a Gateway carries it on the Portal it owns). Move it to the exposed Portal, or remove it.`,
9896
+ comp.id,
9897
+ isDraftCtx
9898
+ );
9899
+ }
9107
9900
  const compInterfaces = ctx.interfaces.filter((i) => i.component === comp.id);
9108
9901
  for (const intf of compInterfaces) {
9109
9902
  for (const m of intf.methods) {
@@ -9173,7 +9966,8 @@ var init_stereotype_deps = __esm({
9173
9966
  "CROSS_SUBSYSTEM_NON_ADAPTER",
9174
9967
  `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.`,
9175
9968
  comp.id,
9176
- isDraftCtx
9969
+ isDraftCtx,
9970
+ true
9177
9971
  );
9178
9972
  }
9179
9973
  continue;
@@ -9751,14 +10545,14 @@ var init_declarative_assertions = __esm({
9751
10545
  });
9752
10546
 
9753
10547
  // src/core/rules/profiles.ts
9754
- var BACKEND_LIKE, FRONTEND_LIKE, PROJECT_KINDS, profilesRule;
10548
+ var BACKEND_LIKE, FRONTEND_LIKE, PROJECT_KINDS2, profilesRule;
9755
10549
  var init_profiles = __esm({
9756
10550
  "src/core/rules/profiles.ts"() {
9757
10551
  "use strict";
9758
10552
  init_types();
9759
10553
  BACKEND_LIKE = /* @__PURE__ */ new Set(["backend", "lowlevel-os", "game-ecs", "realtime-embedded", "plc-cyclic"]);
9760
10554
  FRONTEND_LIKE = /* @__PURE__ */ new Set(["frontend-reactive", "frontend-controller"]);
9761
- PROJECT_KINDS = /* @__PURE__ */ new Set(["fullstack", "system-of-systems", "monorepo"]);
10555
+ PROJECT_KINDS2 = new Set(PROJECT_KINDS);
9762
10556
  profilesRule = {
9763
10557
  name: "architectural-profiles",
9764
10558
  description: "Per-profile stereotype constraints: View/FeatureComponent/RouterComponent only in frontend profiles; Actor/Supervisor forbidden in plc-cyclic (single scan cycle); Actor/Supervisor in frontend profiles warned. Extension packs may register custom profiles (family + forbidden/discouraged stereotype lists); unknown profile names are flagged.",
@@ -9782,7 +10576,7 @@ var init_profiles = __esm({
9782
10576
  );
9783
10577
  }
9784
10578
  }
9785
- if (!registered.has(ctx.projectType) && !PROJECT_KINDS.has(ctx.projectType)) {
10579
+ if (!registered.has(ctx.projectType) && !PROJECT_KINDS2.has(ctx.projectType)) {
9786
10580
  ctx.addIssue(
9787
10581
  "warning",
9788
10582
  "UNKNOWN_PROFILE",
@@ -11036,11 +11830,11 @@ var init_narrative_antipatterns = __esm({
11036
11830
  const memberEdges = keys.flatMap((k) => (adjacency.get(k) ?? []).filter((e) => inScc.has(e.toKey)));
11037
11831
  if (memberEdges.length === 0) continue;
11038
11832
  const anchor = [...memberEdges].sort((a, b) => a.fromKey.localeCompare(b.fromKey))[0];
11039
- const path62 = [...keys].sort().join(" \u2192 ");
11833
+ const path61 = [...keys].sort().join(" \u2192 ");
11040
11834
  ctx.addIssue(
11041
11835
  "warning",
11042
11836
  "UNCONDITIONAL_CALL_CYCLE",
11043
- `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.`,
11837
+ `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.`,
11044
11838
  anchor.impl.id,
11045
11839
  memberEdges.some((e) => ctx.isImplementationDraft(e.impl))
11046
11840
  );
@@ -12187,6 +12981,95 @@ var init_lint_allows = __esm({
12187
12981
  }
12188
12982
  });
12189
12983
 
12984
+ // src/core/rules/portal-call-auth.ts
12985
+ var COMPONENT_REF_PREFIX, portalCallAuthRule;
12986
+ var init_portal_call_auth = __esm({
12987
+ "src/core/rules/portal-call-auth.ts"() {
12988
+ "use strict";
12989
+ COMPONENT_REF_PREFIX = "component:";
12990
+ portalCallAuthRule = {
12991
+ name: "portal-call-auth",
12992
+ description: "Hardens authenticated cross-service calls. An OUTBOUND narrative `call` into ANOTHER component's Portal whose auth is not `none` must (a) be made by an Adapter \u2014 the only block that does external I/O \u2014 and (b) declare the credential source it presents via the step's `auth.from`. Absence of a source warns PORTAL_AUTH_UNMET; a non-Adapter presenter warns AUTH_PRESENTER_NOT_ADAPTER. When `auth.from` is a modeled reference (`component:<id>`) it must resolve to an Adapter/Store the presenter is wired to (UNKNOWN_AUTH_SOURCE / AUTH_SOURCE_NOT_PROVIDER / AUTH_SOURCE_UNWIRED). The actual secret is never stored in the spec. Dispatch steps (a portal's OWN inbound routing) and self-calls are not cross-service calls and are excluded.",
12993
+ codes: [
12994
+ { code: "PORTAL_AUTH_UNMET", defaultSeverity: "warning", summary: "A narrative call into another component's authed Portal does not declare where its credential loads from" },
12995
+ { code: "AUTH_PRESENTER_NOT_ADAPTER", defaultSeverity: "warning", summary: "A non-Adapter component authenticates an outbound call to a portal (external I/O must go through an Adapter)" },
12996
+ { code: "UNKNOWN_AUTH_SOURCE", defaultSeverity: "warning", summary: "auth.from references a component: source that does not exist" },
12997
+ { code: "AUTH_SOURCE_NOT_PROVIDER", defaultSeverity: "warning", summary: "auth.from references a component that is not an Adapter or Store" },
12998
+ { code: "AUTH_SOURCE_UNWIRED", defaultSeverity: "warning", summary: "The presenter declares a component: credential source it does not depend on or own" }
12999
+ ],
13000
+ check(ctx) {
13001
+ for (const impl of ctx.implementations) {
13002
+ const ownComponent = ctx.interfaceMap.get(impl.contract)?.component;
13003
+ const presenter = ownComponent ? ctx.componentMap.get(ownComponent) : void 0;
13004
+ const draft = ctx.isImplementationDraft(impl);
13005
+ for (const method2 of impl.methods ?? []) {
13006
+ for (const step of method2.narrative ?? []) {
13007
+ if (step.type !== "call") continue;
13008
+ if (!step.targetComponent || step.targetComponent === ownComponent) continue;
13009
+ const target = ctx.componentMap.get(step.targetComponent);
13010
+ if (!target || target.componentType !== "Portal") continue;
13011
+ if (!target.auth || target.auth.scheme === "none") continue;
13012
+ if (presenter && presenter.componentType !== "Adapter") {
13013
+ ctx.addIssue(
13014
+ "warning",
13015
+ "AUTH_PRESENTER_NOT_ADAPTER",
13016
+ `Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}": "${presenter.id}" (${presenter.componentType}) authenticates an outbound call to portal "${target.id}". An authenticated cross-service call is external I/O and must be made by an Adapter (the only block that does external I/O) \u2014 route it through a client Adapter.`,
13017
+ impl.id,
13018
+ draft
13019
+ );
13020
+ }
13021
+ const from = step.auth?.from;
13022
+ if (!from) {
13023
+ ctx.addIssue(
13024
+ "warning",
13025
+ "PORTAL_AUTH_UNMET",
13026
+ `Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}" calls the authenticated portal "${target.id}" (auth scheme: ${target.auth.scheme}) but does not declare where its credential is loaded from. Add the credential source (the step's auth.from \u2014 a secret-store component, env var, config key, or vault ref), or drop the portal's auth if it needs none.`,
13027
+ impl.id,
13028
+ draft
13029
+ );
13030
+ continue;
13031
+ }
13032
+ if (from.startsWith(COMPONENT_REF_PREFIX)) {
13033
+ const srcId = from.slice(COMPONENT_REF_PREFIX.length);
13034
+ const src = ctx.componentMap.get(srcId);
13035
+ if (!src) {
13036
+ ctx.addIssue(
13037
+ "warning",
13038
+ "UNKNOWN_AUTH_SOURCE",
13039
+ `Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}": auth.from references component "${srcId}", which does not exist.`,
13040
+ impl.id,
13041
+ draft
13042
+ );
13043
+ } else {
13044
+ if (src.componentType !== "Adapter" && src.componentType !== "Store") {
13045
+ ctx.addIssue(
13046
+ "warning",
13047
+ "AUTH_SOURCE_NOT_PROVIDER",
13048
+ `Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}": auth.from references "${srcId}" (${src.componentType}); a credential source must be an Adapter (loads the secret via external I/O) or a Store (holds it).`,
13049
+ impl.id,
13050
+ draft
13051
+ );
13052
+ }
13053
+ const wired = (presenter?.dependsOn ?? []).includes(srcId) || (presenter?.owns ?? []).includes(srcId);
13054
+ if (presenter && !wired) {
13055
+ ctx.addIssue(
13056
+ "warning",
13057
+ "AUTH_SOURCE_UNWIRED",
13058
+ `Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}": "${presenter.id}" loads its credential from "${srcId}" but neither depends on nor owns it \u2014 declare the dependsOn edge so the credential wiring is real.`,
13059
+ impl.id,
13060
+ draft
13061
+ );
13062
+ }
13063
+ }
13064
+ }
13065
+ }
13066
+ }
13067
+ }
13068
+ }
13069
+ };
13070
+ }
13071
+ });
13072
+
12190
13073
  // src/core/rules/index.ts
12191
13074
  function makeScopeFilter(opts) {
12192
13075
  const { components, interfaces, implementations, types, scopeSubsystem } = opts;
@@ -12338,9 +13221,15 @@ function buildRuleContext(opts) {
12338
13221
  ...[...SDD_RULES, ...extensions.rules].flatMap((r) => r.codes.map((c) => c.code)),
12339
13222
  // Declarative assertions bring their own namespaced codes — lint.allow
12340
13223
  // and severity overrides treat them exactly like builtins.
12341
- ...extensions.assertions.map((a) => a.fullCode)
13224
+ ...extensions.assertions.map((a) => a.fullCode),
13225
+ // Entry-point emitted codes: validateSddTree's chained-subproject pass
13226
+ // raises these AFTER the rule run (it post-processes the aggregated issue
13227
+ // list), so no registered rule declares them — but lint.allow validation
13228
+ // must still recognize them as real codes.
13229
+ "CHAINED_SUBPROJECT_CONTEXT",
13230
+ "UNVERIFIED_EXTERNAL_REF"
12342
13231
  ]);
12343
- const addIssue = (defaultSeverity, code, message, specId, isDraftContext) => {
13232
+ const addIssue = (defaultSeverity, code, message, specId, isDraftContext, surfaceResolved) => {
12344
13233
  if (scopeSubsystem && specId && !isSpecInScope(specId)) {
12345
13234
  return;
12346
13235
  }
@@ -12358,7 +13247,14 @@ function buildRuleContext(opts) {
12358
13247
  if (severity === "warning") return;
12359
13248
  }
12360
13249
  }
12361
- issues.push({ severity, code, message, specId, ...isDraftContext ? { draftContext: true } : {} });
13250
+ issues.push({
13251
+ severity,
13252
+ code,
13253
+ message,
13254
+ specId,
13255
+ ...isDraftContext ? { draftContext: true } : {},
13256
+ ...surfaceResolved ? { surfaceResolved: true } : {}
13257
+ });
12362
13258
  };
12363
13259
  return {
12364
13260
  system,
@@ -12432,6 +13328,7 @@ var init_rules = __esm({
12432
13328
  init_hidden_state();
12433
13329
  init_dependency_conformance();
12434
13330
  init_lint_allows();
13331
+ init_portal_call_auth();
12435
13332
  init_source_analysis();
12436
13333
  init_types();
12437
13334
  init_type_analysis();
@@ -12453,6 +13350,9 @@ var init_rules = __esm({
12453
13350
  narrativeAntipatternsRule,
12454
13351
  narrativeDetailRule,
12455
13352
  portalsRule,
13353
+ // Cross-call auth: a narrative call into an authed Portal must name its
13354
+ // credential source (rides with the portal family).
13355
+ portalCallAuthRule,
12456
13356
  stereotypeDepsRule,
12457
13357
  patternsRule,
12458
13358
  // Facade shape rides with pattern ownership: same §7 doctrine, narrative side.
@@ -12515,6 +13415,7 @@ var init_rules = __esm({
12515
13415
  // L4 expectations: implementations and their code linkage.
12516
13416
  MISSING_IMPLEMENTATION_METHOD: "implementations",
12517
13417
  MISSING_SOURCE_PATH: "implementations",
13418
+ PORTAL_AUTH_UNMET: "implementations",
12518
13419
  MISSING_SOURCE_FILE: "implementations",
12519
13420
  SOURCE_PATH_ESCAPES_ROOT: "implementations",
12520
13421
  UNREALIZED_METHOD: "implementations",
@@ -12778,24 +13679,54 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
12778
13679
  for (const rule of ruleSequence()) {
12779
13680
  rule.check(ctx);
12780
13681
  }
12781
- const hasCrossTreeSuspects = issues.some((i) => SUBPROJECT_LENIENT_CODES.has(i.code));
13682
+ const hasCrossTreeSuspects = issues.some(
13683
+ (i) => SUBPROJECT_REFERENCE_CODES.has(i.code) || SUBPROJECT_CONFORMANCE_CODES.has(i.code)
13684
+ );
12782
13685
  const chainingParent = hasCrossTreeSuspects ? findChainingParent(getProjectRoot()) : null;
12783
13686
  if (chainingParent) {
13687
+ let unverified = 0;
12784
13688
  let downgraded = 0;
12785
- for (const iss of issues) {
12786
- if (!SUBPROJECT_LENIENT_CODES.has(iss.code)) continue;
12787
- if (iss.severity === "error") {
12788
- iss.severity = "warning";
12789
- downgraded++;
13689
+ for (let at = 0; at < issues.length; at++) {
13690
+ const iss = issues[at];
13691
+ if (SUBPROJECT_REFERENCE_CODES.has(iss.code) && !iss.surfaceResolved) {
13692
+ issues[at] = {
13693
+ severity: "warning",
13694
+ code: "UNVERIFIED_EXTERNAL_REF",
13695
+ crossTreeContext: true,
13696
+ // --ci waives it (parent root is authoritative)
13697
+ specId: iss.specId,
13698
+ ...iss.agentId ? { agentId: iss.agentId } : {},
13699
+ ...iss.draftContext ? { draftContext: true } : {},
13700
+ 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.`
13701
+ };
13702
+ unverified++;
13703
+ continue;
13704
+ }
13705
+ if (SUBPROJECT_CONFORMANCE_CODES.has(iss.code)) {
13706
+ if (iss.severity === "error") {
13707
+ iss.severity = "warning";
13708
+ downgraded++;
13709
+ }
13710
+ iss.crossTreeContext = true;
12790
13711
  }
12791
- iss.crossTreeContext = true;
12792
13712
  }
12793
- if (downgraded > 0) {
13713
+ if (unverified > 0 || downgraded > 0) {
13714
+ const notes = [];
13715
+ if (unverified > 0) {
13716
+ notes.push(
13717
+ `${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.`
13718
+ );
13719
+ }
13720
+ if (downgraded > 0) {
13721
+ notes.push(
13722
+ `${downgraded} code\u2194spec conformance finding(s) (parent-root-relative source paths) were downgraded to warnings.`
13723
+ );
13724
+ }
12794
13725
  issues.unshift({
12795
13726
  severity: "warning",
12796
13727
  code: "CHAINED_SUBPROJECT_CONTEXT",
12797
13728
  crossTreeContext: true,
12798
- 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.`
13729
+ 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.`
12799
13730
  });
12800
13731
  }
12801
13732
  }
@@ -12812,7 +13743,7 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
12812
13743
  function validateAsComplete(options) {
12813
13744
  return validateSddTree({ ...options ?? {}, treatAllAsComplete: true });
12814
13745
  }
12815
- var SUBPROJECT_LENIENT_CODES;
13746
+ var SUBPROJECT_REFERENCE_CODES, SUBPROJECT_CONFORMANCE_CODES;
12816
13747
  var init_validation = __esm({
12817
13748
  "src/core/validation.ts"() {
12818
13749
  "use strict";
@@ -12825,8 +13756,7 @@ var init_validation = __esm({
12825
13756
  init_source_analysis();
12826
13757
  init_specs2();
12827
13758
  init_fs();
12828
- SUBPROJECT_LENIENT_CODES = /* @__PURE__ */ new Set([
12829
- // reference resolution
13759
+ SUBPROJECT_REFERENCE_CODES = /* @__PURE__ */ new Set([
12830
13760
  "UNDEFINED_TYPE_REFERENCE",
12831
13761
  "INVALID_DEPENDENCY_REFERENCE",
12832
13762
  "INVALID_TARGET_COMPONENT_REFERENCE",
@@ -12834,8 +13764,9 @@ var init_validation = __esm({
12834
13764
  "UNDECLARED_DEPENDENCY_CALL",
12835
13765
  "INVALID_TRUSTED_LINK",
12836
13766
  "CROSS_SUBSYSTEM_NON_ADAPTER",
12837
- "CROSS_TREE_REF_UNRESOLVED",
12838
- // code↔spec conformance (root-relative sourcePaths / import graph)
13767
+ "CROSS_TREE_REF_UNRESOLVED"
13768
+ ]);
13769
+ SUBPROJECT_CONFORMANCE_CODES = /* @__PURE__ */ new Set([
12839
13770
  "MISSING_SOURCE_FILE",
12840
13771
  "SOURCE_PATH_ESCAPES_ROOT",
12841
13772
  "MISSING_SOURCE_PATH",
@@ -13377,6 +14308,7 @@ __export(specs_exports, {
13377
14308
  buildProjectGraph: () => buildProjectGraph,
13378
14309
  clearLoaderIssues: () => clearLoaderIssues,
13379
14310
  collectPromotableSpecs: () => collectPromotableSpecs,
14311
+ computeStateIdAt: () => computeStateIdAt,
13380
14312
  deleteComponentSpec: () => deleteComponentSpec,
13381
14313
  deleteGroupSpec: () => deleteGroupSpec,
13382
14314
  deleteImplementationSpec: () => deleteImplementationSpec,
@@ -13409,6 +14341,7 @@ __export(specs_exports, {
13409
14341
  loadTypeSpec: () => loadTypeSpec,
13410
14342
  loadTypeSpecs: () => loadTypeSpecs,
13411
14343
  normalizeComponentLayout: () => normalizeComponentLayout,
14344
+ resolveChainingParent: () => resolveChainingParent,
13412
14345
  resolveSubprojectForNamespace: () => resolveSubprojectForNamespace,
13413
14346
  restoreSpecFiles: () => restoreSpecFiles,
13414
14347
  saveComponentSpec: () => saveComponentSpec,
@@ -13813,6 +14746,19 @@ function dryRunSerializeSpecs(include) {
13813
14746
  function buildProjectGraph(level) {
13814
14747
  return buildGraphModel(level);
13815
14748
  }
14749
+ function resolveChainingParent() {
14750
+ return findChainingParent(getProjectRoot());
14751
+ }
14752
+ function computeStateIdAt(root) {
14753
+ const resolved = path14.resolve(root);
14754
+ return runWithProjectRoot(resolved, () => {
14755
+ workspaceFor(resolved).invalidate();
14756
+ const system = loadSystemSpec();
14757
+ if (!system) return null;
14758
+ const s = computeStateId();
14759
+ return `${s.algorithm}:${s.digest}`;
14760
+ });
14761
+ }
13816
14762
  function deleteTypeSpec(id) {
13817
14763
  return current().deleteTypeSpec(id);
13818
14764
  }
@@ -13856,6 +14802,7 @@ var init_specs2 = __esm({
13856
14802
  path14 = __toESM(require("path"));
13857
14803
  init_loader();
13858
14804
  init_fs();
14805
+ init_statehash();
13859
14806
  init_yaml();
13860
14807
  init_models();
13861
14808
  init_narrative_labels();
@@ -16449,6 +17396,9 @@ function requireSpecs() {
16449
17396
  function requireProvision() {
16450
17397
  return init_provision(), __toCommonJS(provision_exports);
16451
17398
  }
17399
+ function listExternalInterfaces2() {
17400
+ return listExternalInterfaces();
17401
+ }
16452
17402
  function text(content) {
16453
17403
  return { content: [{ type: "text", text: content }] };
16454
17404
  }
@@ -17078,6 +18028,7 @@ NOTICE:
17078
18028
  type: import_zod9.z.enum(["local", "call", "dispatch", "branch", "switch", "loop", "try", "parallel", "jump", "return", "throw"]),
17079
18029
  targetComponent: import_zod9.z.string().optional().describe("call/dispatch: L2 component id (for dispatch, the Portal routed through)"),
17080
18030
  targetMethod: import_zod9.z.string().optional().describe("call: method name on the target"),
18031
+ auth: import_zod9.z.object({ from: import_zod9.z.string(), note: import_zod9.z.string().optional() }).optional().describe("call/dispatch: the credential this step presents to an AUTHED callee Portal and WHERE it loads from (`from`). Opaque form (env:API_KEY, a config key, vault:path) = a design note wairon never resolves; modeled form `component:<id>` references the Adapter/Store that provides the secret and is validated (must resolve, be an Adapter/Store, and be wired to the presenter). Absence on a call into a Portal whose auth \u2260 none warns (PORTAL_AUTH_UNMET). The authenticated call itself should be made by an Adapter (AUTH_PRESENTER_NOT_ADAPTER)."),
17081
18032
  detach: import_zod9.z.boolean().optional().describe("call/dispatch: fire-and-forget \u2014 issue the call and continue without awaiting the result (no later step consumes it)"),
17082
18033
  capability: import_zod9.z.string().optional().describe("dispatch: the capability routed through the target Portal's dispatch table (validated against it \u2014 UNSERVED_CAPABILITY)"),
17083
18034
  assertsGuarantees: import_zod9.z.array(import_zod9.z.string().min(1)).optional().describe("Semantic guarantees this step relies on \u2014 each must be declared in the called method's L3 guarantees (NARRATIVE_SEMANTIC_UNBACKED otherwise). Builtin tokens: idempotent | atomic | transactional | exactly-once; extension packs may declare more (any other token is UNKNOWN_GUARANTEE)"),
@@ -17414,7 +18365,36 @@ NOTICE:
17414
18365
  }
17415
18366
  }
17416
18367
  );
18368
+ reg(
18369
+ server,
18370
+ "sdd_list_external_interfaces",
18371
+ {
18372
+ 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."
18373
+ },
18374
+ () => {
18375
+ try {
18376
+ return json(listExternalInterfaces2());
18377
+ } catch (e) {
18378
+ return errText(String(e));
18379
+ }
18380
+ }
18381
+ );
17417
18382
  registerSkillResources(server);
18383
+ try {
18384
+ const chainingParent = resolveChainingParent();
18385
+ if (chainingParent) {
18386
+ let externalSurfaceCount = 0;
18387
+ try {
18388
+ externalSurfaceCount = listExternalInterfaces2().length;
18389
+ } catch {
18390
+ }
18391
+ process.stderr.write(
18392
+ `[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
18393
+ `
18394
+ );
18395
+ }
18396
+ } catch {
18397
+ }
17418
18398
  if (options.hostedTools) {
17419
18399
  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.");
17420
18400
  reg(server, "sdd_host_lock_project", {
@@ -17550,6 +18530,8 @@ var init_server = __esm({
17550
18530
  init_narrative_labels();
17551
18531
  init_specs();
17552
18532
  init_skills();
18533
+ init_specs2();
18534
+ init_surfaces();
17553
18535
  SERVER_BUILD_STAMP = captureBuildStamp(__filename);
17554
18536
  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.";
17555
18537
  SKILL_RESOURCE_MIME = "text/markdown";
@@ -20623,7 +21605,7 @@ var require_dist = __commonJS({
20623
21605
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
20624
21606
  }
20625
21607
  var fs51 = __toESM2(require("fs"));
20626
- var path62 = __toESM2(require("path"));
21608
+ var path61 = __toESM2(require("path"));
20627
21609
  var import_fflate = require_node();
20628
21610
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".hg", ".svn"]);
20629
21611
  function listEntries(archive) {
@@ -20661,8 +21643,8 @@ var require_dist = __commonJS({
20661
21643
  }
20662
21644
  function writeTree(destDir, files) {
20663
21645
  for (const file of files) {
20664
- const absolute = path62.join(destDir, file.path);
20665
- fs51.mkdirSync(path62.dirname(absolute), { recursive: true });
21646
+ const absolute = path61.join(destDir, file.path);
21647
+ fs51.mkdirSync(path61.dirname(absolute), { recursive: true });
20666
21648
  fs51.writeFileSync(absolute, file.contents);
20667
21649
  }
20668
21650
  }
@@ -20675,10 +21657,10 @@ var require_dist = __commonJS({
20675
21657
  for (const entry of fs51.readdirSync(current2, { withFileTypes: true })) {
20676
21658
  if (entry.isDirectory()) {
20677
21659
  if (SKIP_DIRS.has(entry.name)) continue;
20678
- walkPackDir(root, path62.join(current2, entry.name), out);
21660
+ walkPackDir(root, path61.join(current2, entry.name), out);
20679
21661
  } else if (entry.isFile()) {
20680
- const absolute = path62.join(current2, entry.name);
20681
- const relative22 = path62.relative(root, absolute).split(path62.sep).join("/");
21662
+ const absolute = path61.join(current2, entry.name);
21663
+ const relative22 = path61.relative(root, absolute).split(path61.sep).join("/");
20682
21664
  out.push({ path: relative22, contents: fs51.readFileSync(absolute) });
20683
21665
  }
20684
21666
  }
@@ -22089,86 +23071,216 @@ async function generateLayer(options = {}) {
22089
23071
  }
22090
23072
 
22091
23073
  // src/commands/lock.ts
23074
+ var os7 = __toESM(require("os"));
22092
23075
  var import_inquirer2 = __toESM(require("inquirer"));
22093
23076
  init_logger();
22094
- init_loader();
22095
- init_fs();
22096
- init_validation();
22097
- init_specs2();
22098
- async function runLock(options = {}) {
22099
- assertProjectInitialized();
22100
- if (!pathExists(AI_PATHS.specsSystem())) {
22101
- logger.error("No SDD spec tree found (.wai/specs). Nothing to lock.");
22102
- process.exit(1);
23077
+ init_defaults();
23078
+
23079
+ // src/core/detection.ts
23080
+ var fs18 = __toESM(require("fs"));
23081
+ var path27 = __toESM(require("path"));
23082
+ init_defaults();
23083
+ var PACKAGE_MARKERS = [
23084
+ "package.json",
23085
+ "pyproject.toml",
23086
+ "Cargo.toml",
23087
+ "go.mod",
23088
+ "build.gradle",
23089
+ "build.gradle.kts",
23090
+ "pom.xml"
23091
+ ];
23092
+ var MAX_SCAN_DEPTH = 5;
23093
+ function detectDomainCandidates(projectRoot2, alreadyTrackedPaths = /* @__PURE__ */ new Set(), alreadyTrackedIds = /* @__PURE__ */ new Set()) {
23094
+ const candidates = /* @__PURE__ */ new Map();
23095
+ for (const c of detectGitSubmodules(projectRoot2)) {
23096
+ candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
22103
23097
  }
22104
- const projectConfig = loadProjectConfig();
22105
- logger.info("Analyzing and validating specifications in-memory...");
22106
- const index = scanAllSpecs({ recursive: options.recursive ?? true });
22107
- const promotable = collectPromotableSpecs(options.subsystem);
22108
- const originalStatuses = /* @__PURE__ */ new Map();
22109
- const isSpecInSubsystemScope = (specSubsystem) => {
22110
- if (!options.subsystem) return true;
22111
- if (!specSubsystem) return false;
22112
- return specSubsystem === options.subsystem || specSubsystem.startsWith(`${options.subsystem}::`);
22113
- };
22114
- for (const s of index.subsystems) {
22115
- if (!options.subsystem || s.id === options.subsystem || s.id.startsWith(`${options.subsystem}::`)) {
22116
- originalStatuses.set(s, s.status);
22117
- s.status = "complete";
23098
+ for (const c of detectNestedGitRepos(projectRoot2)) {
23099
+ if (!candidates.has(c.path)) {
23100
+ candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
22118
23101
  }
22119
23102
  }
22120
- for (const c of index.components) {
22121
- if (isSpecInSubsystemScope(c.subsystem)) {
22122
- originalStatuses.set(c, c.status);
22123
- c.status = "complete";
22124
- }
23103
+ const gitPaths = new Set(
23104
+ Array.from(candidates.values()).filter((c) => c.type === "git-submodule" || c.type === "git-repo").map((c) => c.path)
23105
+ );
23106
+ for (const c of detectPackageRoots(projectRoot2)) {
23107
+ if (candidates.has(c.path)) continue;
23108
+ const insideGit = Array.from(gitPaths).some(
23109
+ (gp) => c.path === gp || c.path.startsWith(gp + "/")
23110
+ );
23111
+ if (insideGit) continue;
23112
+ candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
22125
23113
  }
22126
- for (const i of index.interfaces) {
22127
- const comp = index.components.find((c) => c.id === i.component);
22128
- if (comp && isSpecInSubsystemScope(comp.subsystem)) {
22129
- originalStatuses.set(i, i.status);
22130
- i.status = "complete";
22131
- }
23114
+ const sorted = Array.from(candidates.values()).sort((a, b) => a.path.localeCompare(b.path));
23115
+ return deduplicateIds(sorted, alreadyTrackedIds);
23116
+ }
23117
+ function deduplicateIds(candidates, existingIds = /* @__PURE__ */ new Set()) {
23118
+ const idCount = /* @__PURE__ */ new Map();
23119
+ for (const id of existingIds) {
23120
+ idCount.set(id, (idCount.get(id) ?? 0) + 1);
22132
23121
  }
22133
- for (const m of index.implementations) {
22134
- const intf = index.interfaces.find((i) => i.id === m.contract);
22135
- const comp = intf ? index.components.find((c) => c.id === intf.component) : null;
22136
- if (comp && isSpecInSubsystemScope(comp.subsystem)) {
22137
- originalStatuses.set(m, m.status);
22138
- m.status = "complete";
22139
- }
23122
+ for (const c of candidates) {
23123
+ idCount.set(c.suggestedId, (idCount.get(c.suggestedId) ?? 0) + 1);
22140
23124
  }
22141
- const dry = validateSddTree({
22142
- rules: projectConfig.rules,
22143
- projectType: projectConfig.projectType,
22144
- scopeSubsystem: options.subsystem,
22145
- recursive: options.recursive ?? true
23125
+ return candidates.map((c) => {
23126
+ if ((idCount.get(c.suggestedId) ?? 0) <= 1) return c;
23127
+ const parts = c.path.split("/");
23128
+ const qualifiedId2 = parts.length >= 2 ? pathToId(`${parts[parts.length - 2]}-${parts[parts.length - 1]}`) : c.suggestedId;
23129
+ return { ...c, suggestedId: qualifiedId2 };
22146
23130
  });
22147
- for (const [spec, status2] of originalStatuses.entries()) {
22148
- spec.status = status2;
23131
+ }
23132
+ function parseGitmodules(filePath) {
23133
+ const content = fs18.readFileSync(filePath, "utf-8");
23134
+ const entries = [];
23135
+ let current2 = {};
23136
+ for (const line2 of content.split("\n")) {
23137
+ const trimmed = line2.trim();
23138
+ const headerMatch = trimmed.match(/^\[submodule "(.+)"\]$/);
23139
+ if (headerMatch) {
23140
+ if (current2.path) entries.push(current2);
23141
+ current2 = { name: headerMatch[1] };
23142
+ continue;
23143
+ }
23144
+ const keyVal = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
23145
+ if (keyVal) {
23146
+ const [, key, value] = keyVal;
23147
+ if (key === "path") current2.path = value.trim();
23148
+ if (key === "url") current2.url = value.trim();
23149
+ }
22149
23150
  }
22150
- const errors = dry.issues.filter((i) => i.severity === "error");
22151
- if (errors.length > 0) {
22152
- logger.header("Cannot lock \u2014 the spec tree does not validate as complete");
22153
- let errorCount = 0;
22154
- const MAX_PRINT = 100;
22155
- let skippedErrors = 0;
22156
- for (const i of errors) {
22157
- if (errorCount < MAX_PRINT) {
22158
- logger.error(`${i.specId ? `[${i.specId}] ` : ""}[${i.code}] ${i.message}`);
22159
- errorCount++;
22160
- } else {
22161
- skippedErrors++;
22162
- }
23151
+ if (current2.path) entries.push(current2);
23152
+ return entries;
23153
+ }
23154
+ function detectGitSubmodules(projectRoot2) {
23155
+ const gitmodulesPath = path27.join(projectRoot2, ".gitmodules");
23156
+ if (!fs18.existsSync(gitmodulesPath)) return [];
23157
+ return parseGitmodules(gitmodulesPath).map((entry) => ({
23158
+ suggestedId: pathToId(entry.path),
23159
+ suggestedName: pathToName(entry.path),
23160
+ path: normalizePath3(entry.path),
23161
+ type: "git-submodule",
23162
+ alreadyTracked: false
23163
+ }));
23164
+ }
23165
+ function detectNestedGitRepos(projectRoot2) {
23166
+ const results = [];
23167
+ walkForGit(projectRoot2, projectRoot2, 0, results);
23168
+ return results;
23169
+ }
23170
+ function walkForGit(projectRoot2, currentDir, depth, results) {
23171
+ if (depth > MAX_SCAN_DEPTH) return;
23172
+ let entries;
23173
+ try {
23174
+ entries = fs18.readdirSync(currentDir, { withFileTypes: true });
23175
+ } catch {
23176
+ return;
23177
+ }
23178
+ for (const entry of entries) {
23179
+ if (!entry.isDirectory()) continue;
23180
+ if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
23181
+ const fullPath = path27.join(currentDir, entry.name);
23182
+ const relPath = normalizePath3(path27.relative(projectRoot2, fullPath));
23183
+ if (relPath === "" || relPath === ".") continue;
23184
+ const gitPath = path27.join(fullPath, ".git");
23185
+ if (fs18.existsSync(gitPath)) {
23186
+ results.push({
23187
+ suggestedId: pathToId(relPath),
23188
+ suggestedName: pathToName(relPath),
23189
+ path: relPath,
23190
+ type: "git-repo",
23191
+ alreadyTracked: false
23192
+ });
23193
+ continue;
22163
23194
  }
22164
- if (skippedErrors > 0) {
22165
- logger.error(`... and ${skippedErrors} more error(s) omitted.`);
23195
+ walkForGit(projectRoot2, fullPath, depth + 1, results);
23196
+ }
23197
+ }
23198
+ function detectPackageRoots(projectRoot2) {
23199
+ const results = [];
23200
+ walkForPackages(projectRoot2, projectRoot2, 0, results);
23201
+ return results;
23202
+ }
23203
+ function walkForPackages(projectRoot2, currentDir, depth, results) {
23204
+ if (depth > MAX_SCAN_DEPTH) return;
23205
+ let entries;
23206
+ try {
23207
+ entries = fs18.readdirSync(currentDir, { withFileTypes: true });
23208
+ } catch {
23209
+ return;
23210
+ }
23211
+ for (const entry of entries) {
23212
+ if (!entry.isDirectory()) continue;
23213
+ if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
23214
+ const fullPath = path27.join(currentDir, entry.name);
23215
+ const relPath = normalizePath3(path27.relative(projectRoot2, fullPath));
23216
+ if (relPath === "" || relPath === ".") continue;
23217
+ const hasMarker = PACKAGE_MARKERS.some((m) => fs18.existsSync(path27.join(fullPath, m)));
23218
+ if (hasMarker) {
23219
+ results.push({
23220
+ suggestedId: pathToId(relPath),
23221
+ suggestedName: pathToName(relPath),
23222
+ path: relPath,
23223
+ type: "package-root",
23224
+ alreadyTracked: false
23225
+ });
22166
23226
  }
22167
- logger.blank();
22168
- logger.info("Fix the errors above, then run `wairon lock` again. Nothing was changed.");
22169
- process.exit(1);
23227
+ walkForPackages(projectRoot2, fullPath, depth + 1, results);
22170
23228
  }
22171
- logger.header("Lock SDD specs");
23229
+ }
23230
+ function pathToId(relPath) {
23231
+ const basename11 = path27.basename(relPath);
23232
+ return basename11.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
23233
+ }
23234
+ function pathToName(relPath) {
23235
+ const id = pathToId(relPath);
23236
+ return id.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
23237
+ }
23238
+ function normalizePath3(p) {
23239
+ return p.replace(/\\/g, "/");
23240
+ }
23241
+
23242
+ // src/core/index.ts
23243
+ init_domains();
23244
+ init_validation();
23245
+ init_extensions();
23246
+ init_variants();
23247
+ init_rules();
23248
+ init_specs2();
23249
+ init_provision();
23250
+ init_diagram();
23251
+
23252
+ // src/core/lockfile.ts
23253
+ var fs19 = __toESM(require("fs"));
23254
+ var path28 = __toESM(require("path"));
23255
+ init_fs();
23256
+ function lockPath() {
23257
+ return aiDir("lock.json");
23258
+ }
23259
+ function readLockRecord() {
23260
+ try {
23261
+ return JSON.parse(fs19.readFileSync(lockPath(), "utf8"));
23262
+ } catch {
23263
+ return null;
23264
+ }
23265
+ }
23266
+ function writeLockRecord(record2) {
23267
+ const p = lockPath();
23268
+ fs19.mkdirSync(path28.dirname(p), { recursive: true });
23269
+ const tmp = `${p}.tmp`;
23270
+ fs19.writeFileSync(tmp, JSON.stringify(record2, null, 2) + "\n");
23271
+ fs19.renameSync(tmp, p);
23272
+ }
23273
+
23274
+ // src/core/index.ts
23275
+ init_statehash();
23276
+ init_agent_resolver();
23277
+ init_skills();
23278
+ init_surfaces();
23279
+ init_openapi();
23280
+
23281
+ // src/commands/lock.ts
23282
+ async function runLock(options = {}, gate) {
23283
+ const promotable = collectPromotableSpecs(options.subsystem);
22172
23284
  if (promotable.length === 0) {
22173
23285
  logger.info("All specs are already complete \u2014 this will re-validate and regenerate the agent topology.");
22174
23286
  } else {
@@ -22192,23 +23304,36 @@ async function runLock(options = {}) {
22192
23304
  default: false
22193
23305
  }
22194
23306
  ]);
22195
- if (!confirmed) {
22196
- logger.info("Cancelled. Nothing was changed.");
22197
- return;
22198
- }
23307
+ if (!confirmed) return null;
23308
+ }
23309
+ if (options.subsystem) {
23310
+ for (const p of promotable) applySpecStatus(p.kind, p.id, "complete");
23311
+ invalidateSpecCache();
23312
+ } else {
23313
+ promoteAllComplete();
22199
23314
  }
22200
- for (const p of promotable) applySpecStatus(p.kind, p.id, "complete");
22201
- invalidateSpecCache();
22202
23315
  if (promotable.length > 0) {
22203
23316
  logger.success(`Locked ${promotable.length} spec(s) as complete.`);
22204
23317
  }
22205
- logger.blank();
22206
- await runGenerate({ domain: options.subsystem });
22207
- logger.blank();
22208
- logger.success("Specs locked and agent topology generated.");
22209
- logger.warn(
22210
- "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."
22211
- );
23318
+ let lockedBy = "local";
23319
+ try {
23320
+ lockedBy = `local:${os7.userInfo().username}`;
23321
+ } catch {
23322
+ }
23323
+ const record2 = {
23324
+ stateId: computeStateId(),
23325
+ lockedAt: (/* @__PURE__ */ new Date()).toISOString(),
23326
+ lockedBy,
23327
+ validatorVersion: WAIRON_VERSION,
23328
+ validationResult: {
23329
+ valid: true,
23330
+ errors: 0,
23331
+ warnings: gate ? gate.issues.filter((i) => i.severity === "warning").length : 0
23332
+ },
23333
+ status: "ready"
23334
+ };
23335
+ writeLockRecord(record2);
23336
+ return record2;
22212
23337
  }
22213
23338
 
22214
23339
  // src/commands/validate.ts
@@ -22347,6 +23472,10 @@ async function runValidate(options = {}) {
22347
23472
  }
22348
23473
  }
22349
23474
 
23475
+ // src/cli/index.ts
23476
+ init_loader();
23477
+ init_fs();
23478
+
22350
23479
  // src/commands/list.ts
22351
23480
  var import_chalk7 = __toESM(require("chalk"));
22352
23481
  init_logger();
@@ -22461,9 +23590,9 @@ init_mcp();
22461
23590
  // src/commands/update.ts
22462
23591
  var https = __toESM(require("https"));
22463
23592
  var http = __toESM(require("http"));
22464
- var fs18 = __toESM(require("fs"));
22465
- var path27 = __toESM(require("path"));
22466
- var os7 = __toESM(require("os"));
23593
+ var fs20 = __toESM(require("fs"));
23594
+ var path29 = __toESM(require("path"));
23595
+ var os8 = __toESM(require("os"));
22467
23596
  var crypto2 = __toESM(require("crypto"));
22468
23597
  var import_child_process2 = require("child_process");
22469
23598
  init_logger();
@@ -22526,8 +23655,8 @@ async function runUpdate(options = {}) {
22526
23655
  logger.info(`Download manually from: ${release.html_url}`);
22527
23656
  process.exit(1);
22528
23657
  }
22529
- const tmpDir = os7.tmpdir();
22530
- const tmpFile = path27.join(tmpDir, assetName);
23658
+ const tmpDir = os8.tmpdir();
23659
+ const tmpFile = path29.join(tmpDir, assetName);
22531
23660
  logger.info(`Downloading ${assetName}...`);
22532
23661
  try {
22533
23662
  await downloadFile(asset.browser_download_url, tmpFile);
@@ -22544,16 +23673,16 @@ async function runUpdate(options = {}) {
22544
23673
  const checksumAssetName = assetName + ".sha256";
22545
23674
  const checksumAsset = release.assets.find((a) => a.name === checksumAssetName);
22546
23675
  if (checksumAsset) {
22547
- const tmpChecksum = path27.join(tmpDir, checksumAssetName);
23676
+ const tmpChecksum = path29.join(tmpDir, checksumAssetName);
22548
23677
  logger.info(`Verifying checksum...`);
22549
23678
  try {
22550
23679
  await downloadFile(checksumAsset.browser_download_url, tmpChecksum);
22551
23680
  verifyChecksum(tmpFile, tmpChecksum, assetName);
22552
- fs18.unlinkSync(tmpChecksum);
23681
+ fs20.unlinkSync(tmpChecksum);
22553
23682
  } catch (err) {
22554
23683
  logger.error(`Checksum verification failed: ${err.message}`);
22555
23684
  try {
22556
- fs18.unlinkSync(tmpFile);
23685
+ fs20.unlinkSync(tmpFile);
22557
23686
  } catch {
22558
23687
  }
22559
23688
  process.exit(1);
@@ -22619,7 +23748,7 @@ function fetchReleases(repo) {
22619
23748
  }
22620
23749
  function downloadFile(url, dest) {
22621
23750
  return new Promise((resolve24, reject) => {
22622
- const file = fs18.createWriteStream(dest);
23751
+ const file = fs20.createWriteStream(dest);
22623
23752
  const get3 = url.startsWith("https://") ? https.get : http.get;
22624
23753
  get3(url, { headers: { "User-Agent": `wairon/${WAIRON_VERSION}` }, agent: false }, (res) => {
22625
23754
  if (res.statusCode === 301 || res.statusCode === 302) {
@@ -22641,21 +23770,21 @@ function downloadFile(url, dest) {
22641
23770
  });
22642
23771
  file.on("error", (err) => {
22643
23772
  res.destroy();
22644
- fs18.unlink(dest, () => {
23773
+ fs20.unlink(dest, () => {
22645
23774
  });
22646
23775
  reject(err);
22647
23776
  });
22648
23777
  }).on("error", (err) => {
22649
- fs18.unlink(dest, () => {
23778
+ fs20.unlink(dest, () => {
22650
23779
  });
22651
23780
  reject(err);
22652
23781
  });
22653
23782
  });
22654
23783
  }
22655
23784
  function verifyChecksum(filePath, checksumFile, expectedFilename) {
22656
- const checksumContent = fs18.readFileSync(checksumFile, "utf-8").trim();
23785
+ const checksumContent = fs20.readFileSync(checksumFile, "utf-8").trim();
22657
23786
  const expectedHash = checksumContent.split(/\s+/)[0].toLowerCase();
22658
- const fileBuffer = fs18.readFileSync(filePath);
23787
+ const fileBuffer = fs20.readFileSync(filePath);
22659
23788
  const actualHash = crypto2.createHash("sha256").update(fileBuffer).digest("hex").toLowerCase();
22660
23789
  if (actualHash !== expectedHash) {
22661
23790
  throw new Error(
@@ -22686,9 +23815,9 @@ function isPkgBinary2() {
22686
23815
  function installBinary(tmpFile, destPath) {
22687
23816
  const platform = process.platform;
22688
23817
  const isZip = tmpFile.endsWith(".zip");
22689
- const extractDir = path27.join(os7.tmpdir(), "wairon-extract");
22690
- if (fs18.existsSync(extractDir)) fs18.rmSync(extractDir, { recursive: true });
22691
- fs18.mkdirSync(extractDir, { recursive: true });
23818
+ const extractDir = path29.join(os8.tmpdir(), "wairon-extract");
23819
+ if (fs20.existsSync(extractDir)) fs20.rmSync(extractDir, { recursive: true });
23820
+ fs20.mkdirSync(extractDir, { recursive: true });
22692
23821
  if (isZip) {
22693
23822
  (0, import_child_process2.execSync)(
22694
23823
  `powershell -NoProfile -NonInteractive -Command "Expand-Archive -Path '${tmpFile}' -DestinationPath '${extractDir}' -Force"`,
@@ -22698,18 +23827,18 @@ function installBinary(tmpFile, destPath) {
22698
23827
  (0, import_child_process2.execSync)(`tar -xzf "${tmpFile}" -C "${extractDir}"`, { stdio: ["ignore", "pipe", "pipe"] });
22699
23828
  }
22700
23829
  const binaryName = platform === "win32" ? "wairon.exe" : "wairon";
22701
- const extractedBinary = path27.join(extractDir, binaryName);
22702
- if (!fs18.existsSync(extractedBinary)) {
23830
+ const extractedBinary = path29.join(extractDir, binaryName);
23831
+ if (!fs20.existsSync(extractedBinary)) {
22703
23832
  throw new Error(`Extracted binary not found at ${extractedBinary}`);
22704
23833
  }
22705
23834
  if (platform === "win32") {
22706
23835
  const oldPath = destPath + ".old";
22707
23836
  try {
22708
23837
  cleanStaleBinary(oldPath);
22709
- fs18.renameSync(destPath, oldPath);
22710
- fs18.copyFileSync(extractedBinary, destPath);
23838
+ fs20.renameSync(destPath, oldPath);
23839
+ fs20.copyFileSync(extractedBinary, destPath);
22711
23840
  try {
22712
- fs18.unlinkSync(oldPath);
23841
+ fs20.unlinkSync(oldPath);
22713
23842
  } catch {
22714
23843
  }
22715
23844
  } catch (err) {
@@ -22723,25 +23852,25 @@ function installBinary(tmpFile, destPath) {
22723
23852
  }
22724
23853
  } else {
22725
23854
  const tmpDest = destPath + ".new";
22726
- fs18.copyFileSync(extractedBinary, tmpDest);
22727
- fs18.chmodSync(tmpDest, 493);
22728
- fs18.renameSync(tmpDest, destPath);
23855
+ fs20.copyFileSync(extractedBinary, tmpDest);
23856
+ fs20.chmodSync(tmpDest, 493);
23857
+ fs20.renameSync(tmpDest, destPath);
22729
23858
  }
22730
23859
  try {
22731
- fs18.unlinkSync(tmpFile);
23860
+ fs20.unlinkSync(tmpFile);
22732
23861
  } catch {
22733
23862
  }
22734
23863
  try {
22735
- fs18.rmSync(extractDir, { recursive: true });
23864
+ fs20.rmSync(extractDir, { recursive: true });
22736
23865
  } catch {
22737
23866
  }
22738
23867
  }
22739
23868
  function cleanStaleBinary(oldPath) {
22740
23869
  const target = oldPath ?? (isPkgBinary2() ? process.execPath + ".old" : null);
22741
23870
  if (!target) return;
22742
- if (fs18.existsSync(target)) {
23871
+ if (fs20.existsSync(target)) {
22743
23872
  try {
22744
- fs18.unlinkSync(target);
23873
+ fs20.unlinkSync(target);
22745
23874
  } catch {
22746
23875
  }
22747
23876
  }
@@ -22987,171 +24116,6 @@ async function filteredCheckbox(config) {
22987
24116
 
22988
24117
  // src/commands/domains.ts
22989
24118
  init_loader();
22990
-
22991
- // src/core/detection.ts
22992
- var fs19 = __toESM(require("fs"));
22993
- var path28 = __toESM(require("path"));
22994
- init_defaults();
22995
- var PACKAGE_MARKERS = [
22996
- "package.json",
22997
- "pyproject.toml",
22998
- "Cargo.toml",
22999
- "go.mod",
23000
- "build.gradle",
23001
- "build.gradle.kts",
23002
- "pom.xml"
23003
- ];
23004
- var MAX_SCAN_DEPTH = 5;
23005
- function detectDomainCandidates(projectRoot2, alreadyTrackedPaths = /* @__PURE__ */ new Set(), alreadyTrackedIds = /* @__PURE__ */ new Set()) {
23006
- const candidates = /* @__PURE__ */ new Map();
23007
- for (const c of detectGitSubmodules(projectRoot2)) {
23008
- candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
23009
- }
23010
- for (const c of detectNestedGitRepos(projectRoot2)) {
23011
- if (!candidates.has(c.path)) {
23012
- candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
23013
- }
23014
- }
23015
- const gitPaths = new Set(
23016
- Array.from(candidates.values()).filter((c) => c.type === "git-submodule" || c.type === "git-repo").map((c) => c.path)
23017
- );
23018
- for (const c of detectPackageRoots(projectRoot2)) {
23019
- if (candidates.has(c.path)) continue;
23020
- const insideGit = Array.from(gitPaths).some(
23021
- (gp) => c.path === gp || c.path.startsWith(gp + "/")
23022
- );
23023
- if (insideGit) continue;
23024
- candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
23025
- }
23026
- const sorted = Array.from(candidates.values()).sort((a, b) => a.path.localeCompare(b.path));
23027
- return deduplicateIds(sorted, alreadyTrackedIds);
23028
- }
23029
- function deduplicateIds(candidates, existingIds = /* @__PURE__ */ new Set()) {
23030
- const idCount = /* @__PURE__ */ new Map();
23031
- for (const id of existingIds) {
23032
- idCount.set(id, (idCount.get(id) ?? 0) + 1);
23033
- }
23034
- for (const c of candidates) {
23035
- idCount.set(c.suggestedId, (idCount.get(c.suggestedId) ?? 0) + 1);
23036
- }
23037
- return candidates.map((c) => {
23038
- if ((idCount.get(c.suggestedId) ?? 0) <= 1) return c;
23039
- const parts = c.path.split("/");
23040
- const qualifiedId2 = parts.length >= 2 ? pathToId(`${parts[parts.length - 2]}-${parts[parts.length - 1]}`) : c.suggestedId;
23041
- return { ...c, suggestedId: qualifiedId2 };
23042
- });
23043
- }
23044
- function parseGitmodules(filePath) {
23045
- const content = fs19.readFileSync(filePath, "utf-8");
23046
- const entries = [];
23047
- let current2 = {};
23048
- for (const line2 of content.split("\n")) {
23049
- const trimmed = line2.trim();
23050
- const headerMatch = trimmed.match(/^\[submodule "(.+)"\]$/);
23051
- if (headerMatch) {
23052
- if (current2.path) entries.push(current2);
23053
- current2 = { name: headerMatch[1] };
23054
- continue;
23055
- }
23056
- const keyVal = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
23057
- if (keyVal) {
23058
- const [, key, value] = keyVal;
23059
- if (key === "path") current2.path = value.trim();
23060
- if (key === "url") current2.url = value.trim();
23061
- }
23062
- }
23063
- if (current2.path) entries.push(current2);
23064
- return entries;
23065
- }
23066
- function detectGitSubmodules(projectRoot2) {
23067
- const gitmodulesPath = path28.join(projectRoot2, ".gitmodules");
23068
- if (!fs19.existsSync(gitmodulesPath)) return [];
23069
- return parseGitmodules(gitmodulesPath).map((entry) => ({
23070
- suggestedId: pathToId(entry.path),
23071
- suggestedName: pathToName(entry.path),
23072
- path: normalizePath3(entry.path),
23073
- type: "git-submodule",
23074
- alreadyTracked: false
23075
- }));
23076
- }
23077
- function detectNestedGitRepos(projectRoot2) {
23078
- const results = [];
23079
- walkForGit(projectRoot2, projectRoot2, 0, results);
23080
- return results;
23081
- }
23082
- function walkForGit(projectRoot2, currentDir, depth, results) {
23083
- if (depth > MAX_SCAN_DEPTH) return;
23084
- let entries;
23085
- try {
23086
- entries = fs19.readdirSync(currentDir, { withFileTypes: true });
23087
- } catch {
23088
- return;
23089
- }
23090
- for (const entry of entries) {
23091
- if (!entry.isDirectory()) continue;
23092
- if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
23093
- const fullPath = path28.join(currentDir, entry.name);
23094
- const relPath = normalizePath3(path28.relative(projectRoot2, fullPath));
23095
- if (relPath === "" || relPath === ".") continue;
23096
- const gitPath = path28.join(fullPath, ".git");
23097
- if (fs19.existsSync(gitPath)) {
23098
- results.push({
23099
- suggestedId: pathToId(relPath),
23100
- suggestedName: pathToName(relPath),
23101
- path: relPath,
23102
- type: "git-repo",
23103
- alreadyTracked: false
23104
- });
23105
- continue;
23106
- }
23107
- walkForGit(projectRoot2, fullPath, depth + 1, results);
23108
- }
23109
- }
23110
- function detectPackageRoots(projectRoot2) {
23111
- const results = [];
23112
- walkForPackages(projectRoot2, projectRoot2, 0, results);
23113
- return results;
23114
- }
23115
- function walkForPackages(projectRoot2, currentDir, depth, results) {
23116
- if (depth > MAX_SCAN_DEPTH) return;
23117
- let entries;
23118
- try {
23119
- entries = fs19.readdirSync(currentDir, { withFileTypes: true });
23120
- } catch {
23121
- return;
23122
- }
23123
- for (const entry of entries) {
23124
- if (!entry.isDirectory()) continue;
23125
- if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
23126
- const fullPath = path28.join(currentDir, entry.name);
23127
- const relPath = normalizePath3(path28.relative(projectRoot2, fullPath));
23128
- if (relPath === "" || relPath === ".") continue;
23129
- const hasMarker = PACKAGE_MARKERS.some((m) => fs19.existsSync(path28.join(fullPath, m)));
23130
- if (hasMarker) {
23131
- results.push({
23132
- suggestedId: pathToId(relPath),
23133
- suggestedName: pathToName(relPath),
23134
- path: relPath,
23135
- type: "package-root",
23136
- alreadyTracked: false
23137
- });
23138
- }
23139
- walkForPackages(projectRoot2, fullPath, depth + 1, results);
23140
- }
23141
- }
23142
- function pathToId(relPath) {
23143
- const basename12 = path28.basename(relPath);
23144
- return basename12.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
23145
- }
23146
- function pathToName(relPath) {
23147
- const id = pathToId(relPath);
23148
- return id.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
23149
- }
23150
- function normalizePath3(p) {
23151
- return p.replace(/\\/g, "/");
23152
- }
23153
-
23154
- // src/commands/domains.ts
23155
24119
  init_domains();
23156
24120
  init_domain();
23157
24121
  async function runDomainsList() {
@@ -23341,9 +24305,9 @@ async function runSkillsInstall() {
23341
24305
  }
23342
24306
 
23343
24307
  // src/commands/doctor.ts
23344
- var fs20 = __toESM(require("fs"));
23345
- var os8 = __toESM(require("os"));
23346
- var path29 = __toESM(require("path"));
24308
+ var fs21 = __toESM(require("fs"));
24309
+ var os9 = __toESM(require("os"));
24310
+ var path30 = __toESM(require("path"));
23347
24311
  var import_chalk12 = __toESM(require("chalk"));
23348
24312
  init_logger();
23349
24313
  init_defaults();
@@ -23373,10 +24337,10 @@ function stampVerdict(content) {
23373
24337
  return { mark: "warn", note: `v${v} \u2014 stale, installed is v${WAIRON_VERSION}` };
23374
24338
  }
23375
24339
  function mcpEntryHealth(settingsPath) {
23376
- if (!fs20.existsSync(settingsPath)) return { mark: "warn", note: "not registered" };
24340
+ if (!fs21.existsSync(settingsPath)) return { mark: "warn", note: "not registered" };
23377
24341
  let entry;
23378
24342
  try {
23379
- const s = JSON.parse(fs20.readFileSync(settingsPath, "utf8"));
24343
+ const s = JSON.parse(fs21.readFileSync(settingsPath, "utf8"));
23380
24344
  entry = s.mcpServers?.["wairon"];
23381
24345
  } catch {
23382
24346
  return { mark: "error", note: "parse error" };
@@ -23384,7 +24348,7 @@ function mcpEntryHealth(settingsPath) {
23384
24348
  if (!entry) return { mark: "warn", note: "not registered" };
23385
24349
  if (entry.command === "node" && Array.isArray(entry.args) && typeof entry.args[0] === "string") {
23386
24350
  const scriptPath = entry.args[0];
23387
- if (!fs20.existsSync(scriptPath)) {
24351
+ if (!fs21.existsSync(scriptPath)) {
23388
24352
  return { mark: "error", note: `registered but the server path is missing \u2014 ${scriptPath}` };
23389
24353
  }
23390
24354
  }
@@ -23436,7 +24400,7 @@ async function runDoctor(options = {}) {
23436
24400
  const { findChainingSubprojectsMissingConfig: findChainingSubprojectsMissingConfig2 } = (init_provision(), __toCommonJS(provision_exports));
23437
24401
  const missing = findChainingSubprojectsMissingConfig2(getProjectRoot());
23438
24402
  if (missing.length > 0) {
23439
- 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.`);
24403
+ 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.`);
23440
24404
  }
23441
24405
  } catch {
23442
24406
  }
@@ -23474,7 +24438,7 @@ async function runDoctor(options = {}) {
23474
24438
  const gp = localGuideFilePath(process.cwd(), t);
23475
24439
  if (!gp || seenGuides.has(gp)) continue;
23476
24440
  seenGuides.add(gp);
23477
- const rel2 = path29.relative(process.cwd(), gp).replace(/\\/g, "/");
24441
+ const rel2 = path30.relative(process.cwd(), gp).replace(/\\/g, "/");
23478
24442
  if (!pathExists(gp)) {
23479
24443
  line(tally, "warn", `${rel2} guide \u2014 not injected (run \`wairon generate\`)`);
23480
24444
  continue;
@@ -23508,17 +24472,17 @@ async function runDoctor(options = {}) {
23508
24472
  line(tally, h.mark, `Claude (project .mcp.json): ${h.note}${h.mark === "ok" ? "" : " \u2014 run `wairon mcp install --backend claude`"}`);
23509
24473
  }
23510
24474
  if (wantGemini) {
23511
- const globalCfg = path29.join(os8.homedir(), ".gemini", "antigravity-cli", "mcp_config.json");
24475
+ const globalCfg = path30.join(os9.homedir(), ".gemini", "antigravity-cli", "mcp_config.json");
23512
24476
  const hg = mcpEntryHealth(globalCfg);
23513
24477
  line(tally, hg.mark, `Antigravity (global mcp_config.json): ${hg.note}${hg.mark === "ok" ? "" : " \u2014 run `wairon mcp install --backend gemini --global`"}`);
23514
24478
  const projPath = fromProjectRoot(".gemini", "settings.json");
23515
- if (fs20.existsSync(projPath)) {
24479
+ if (fs21.existsSync(projPath)) {
23516
24480
  const hp = mcpEntryHealth(projPath);
23517
24481
  line(tally, hp.mark === "error" ? "error" : "ok", `Gemini CLI (project): ${hp.note} ${import_chalk12.default.gray("(Antigravity ignores this file)")}`);
23518
24482
  }
23519
24483
  }
23520
- const pluginDir = path29.join(os8.homedir(), ".gemini", "config", "plugins", "wairon");
23521
- if (fs20.existsSync(pluginDir)) {
24484
+ const pluginDir = path30.join(os9.homedir(), ".gemini", "config", "plugins", "wairon");
24485
+ if (fs21.existsSync(pluginDir)) {
23522
24486
  line(tally, "warn", `Legacy Antigravity plugin present (${pluginDir}) \u2014 it collides with the wairon MCP server. Remove it with \`wairon doctor --fix\`.`);
23523
24487
  }
23524
24488
  logger.blank();
@@ -23549,7 +24513,7 @@ async function applyFixes() {
23549
24513
  const legacySpecs = findLegacySpecFiles();
23550
24514
  if (legacySpecs.length > 0) {
23551
24515
  for (const { path: oldPath, expected: newPath } of legacySpecs) {
23552
- fs20.renameSync(oldPath, newPath);
24516
+ fs21.renameSync(oldPath, newPath);
23553
24517
  }
23554
24518
  console.log(` ${icon("ok")} Migrated ${legacySpecs.length} legacy spec file(s) to the new dot-prefixed unified schema.`);
23555
24519
  }
@@ -23603,8 +24567,8 @@ function printSummary(tally) {
23603
24567
  }
23604
24568
 
23605
24569
  // src/commands/diagram.ts
23606
- var fs21 = __toESM(require("fs"));
23607
- var path30 = __toESM(require("path"));
24570
+ var fs22 = __toESM(require("fs"));
24571
+ var path31 = __toESM(require("path"));
23608
24572
  init_logger();
23609
24573
  init_loader();
23610
24574
  init_fs();
@@ -23639,8 +24603,8 @@ function collectIssues() {
23639
24603
  }
23640
24604
  function writeCanvas(dest) {
23641
24605
  const model = buildCanvasModel(collectIssues());
23642
- ensureDir(path30.dirname(path30.resolve(dest)));
23643
- fs21.writeFileSync(dest, renderCanvasHtml(model), "utf-8");
24606
+ ensureDir(path31.dirname(path31.resolve(dest)));
24607
+ fs22.writeFileSync(dest, renderCanvasHtml(model), "utf-8");
23644
24608
  }
23645
24609
  function parseSequenceRef(ref) {
23646
24610
  const sep6 = ref.includes(":") ? ref.lastIndexOf(":") : ref.lastIndexOf(".");
@@ -23655,55 +24619,55 @@ async function runDiagram(rawOptions = {}) {
23655
24619
  assertProjectInitialized();
23656
24620
  const options = applyFormat(rawOptions);
23657
24621
  if (options.canvas && !options.all) {
23658
- const dest2 = options.out ?? path30.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
24622
+ const dest2 = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
23659
24623
  writeCanvas(dest2);
23660
24624
  logger.success(`Interactive canvas written to ${dest2}`);
23661
24625
  logger.info("Open it in a browser \u2014 fully self-contained (works offline).");
23662
24626
  return;
23663
24627
  }
23664
24628
  if (options.drawio && !options.all) {
23665
- const dest2 = options.out ?? path30.join(AI_PATHS.docsDir(), "diagrams", "architecture.drawio");
23666
- ensureDir(path30.dirname(path30.resolve(dest2)));
23667
- fs21.writeFileSync(dest2, generateDrawioXml(buildCanvasModel()), "utf-8");
24629
+ const dest2 = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams", "architecture.drawio");
24630
+ ensureDir(path31.dirname(path31.resolve(dest2)));
24631
+ fs22.writeFileSync(dest2, generateDrawioXml(buildCanvasModel()), "utf-8");
23668
24632
  logger.success(`draw.io diagram written to ${dest2}`);
23669
24633
  logger.info("Open with draw.io / diagrams.net (or import into tools that accept the format).");
23670
24634
  return;
23671
24635
  }
23672
24636
  if (options.excalidraw && !options.all) {
23673
- const dest2 = options.out ?? path30.join(AI_PATHS.docsDir(), "diagrams", "architecture.excalidraw");
23674
- ensureDir(path30.dirname(path30.resolve(dest2)));
23675
- fs21.writeFileSync(dest2, generateExcalidrawScene(buildCanvasModel()), "utf-8");
24637
+ const dest2 = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams", "architecture.excalidraw");
24638
+ ensureDir(path31.dirname(path31.resolve(dest2)));
24639
+ fs22.writeFileSync(dest2, generateExcalidrawScene(buildCanvasModel()), "utf-8");
23676
24640
  logger.success(`Excalidraw scene written to ${dest2}`);
23677
24641
  logger.info("Open with excalidraw.com or the VS Code extension.");
23678
24642
  return;
23679
24643
  }
23680
24644
  const wantsMermaid = options.format?.toLowerCase().startsWith("mermaid") || !!options.subsystem || !!options.sequence;
23681
24645
  if (!options.all && !options.sequence && !wantsMermaid) {
23682
- const dest2 = options.out ?? path30.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
24646
+ const dest2 = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams", "canvas.html");
23683
24647
  writeCanvas(dest2);
23684
24648
  logger.success(`Interactive canvas written to ${dest2}`);
23685
24649
  logger.info("Open it in a browser \u2014 fully self-contained (works offline). Other formats: --format mermaid|drawio|excalidraw.");
23686
24650
  return;
23687
24651
  }
23688
24652
  if (options.all) {
23689
- const outDir = options.out ?? path30.join(AI_PATHS.docsDir(), "diagrams");
24653
+ const outDir = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams");
23690
24654
  const files = generateDiagramSet();
23691
24655
  if (files.length === 0) {
23692
24656
  logger.warn("No diagrams to generate \u2014 the spec tree has no components yet.");
23693
24657
  return;
23694
24658
  }
23695
24659
  for (const file of files) {
23696
- const dest2 = path30.join(outDir, file.relPath);
23697
- ensureDir(path30.dirname(dest2));
23698
- fs21.writeFileSync(dest2, toMarkdown(file), "utf-8");
24660
+ const dest2 = path31.join(outDir, file.relPath);
24661
+ ensureDir(path31.dirname(dest2));
24662
+ fs22.writeFileSync(dest2, toMarkdown(file), "utf-8");
23699
24663
  }
23700
- writeCanvas(path30.join(outDir, "canvas.html"));
24664
+ writeCanvas(path31.join(outDir, "canvas.html"));
23701
24665
  const exportModel = buildCanvasModel();
23702
- fs21.writeFileSync(path30.join(outDir, "architecture.drawio"), generateDrawioXml(exportModel), "utf-8");
23703
- fs21.writeFileSync(path30.join(outDir, "architecture.excalidraw"), generateExcalidrawScene(exportModel), "utf-8");
24666
+ fs22.writeFileSync(path31.join(outDir, "architecture.drawio"), generateDrawioXml(exportModel), "utf-8");
24667
+ fs22.writeFileSync(path31.join(outDir, "architecture.excalidraw"), generateExcalidrawScene(exportModel), "utf-8");
23704
24668
  const graph = loadSpecGraph();
23705
- const indexPath = path30.join(outDir, "README.md");
23706
- fs21.writeFileSync(indexPath, diagramSetIndex(files, graph.systemName), "utf-8");
24669
+ const indexPath = path31.join(outDir, "README.md");
24670
+ fs22.writeFileSync(indexPath, diagramSetIndex(files, graph.systemName), "utf-8");
23707
24671
  logger.success(`Generated ${files.length} diagram(s) + interactive canvas.html + index into ${outDir}`);
23708
24672
  for (const file of files.slice(0, 12)) {
23709
24673
  logger.info(` ${file.relPath}`);
@@ -23714,26 +24678,26 @@ async function runDiagram(rawOptions = {}) {
23714
24678
  let mermaid;
23715
24679
  let title;
23716
24680
  let defaultDest;
23717
- const diagramsDir = path30.join(AI_PATHS.docsDir(), "diagrams");
24681
+ const diagramsDir = path31.join(AI_PATHS.docsDir(), "diagrams");
23718
24682
  if (options.sequence) {
23719
24683
  const { component, method: method2 } = parseSequenceRef(options.sequence);
23720
24684
  mermaid = generateSequenceDiagram(component, method2, { depth: options.depth });
23721
24685
  title = `${component}.${method2} \u2014 narrative sequence`;
23722
- defaultDest = path30.join(diagramsDir, "sequences", `${component.replace(/::/g, "--")}.${method2}.md`);
24686
+ defaultDest = path31.join(diagramsDir, "sequences", `${component.replace(/::/g, "--")}.${method2}.md`);
23723
24687
  } else if (options.subsystem) {
23724
24688
  mermaid = generateComponentDiagram({ subsystem: options.subsystem });
23725
24689
  title = `${options.subsystem} \u2014 components`;
23726
- defaultDest = path30.join(diagramsDir, "subsystems", `${options.subsystem.replace(/::/g, "--")}.md`);
24690
+ defaultDest = path31.join(diagramsDir, "subsystems", `${options.subsystem.replace(/::/g, "--")}.md`);
23727
24691
  } else {
23728
24692
  mermaid = generateComponentDiagram();
23729
24693
  title = "Component architecture";
23730
- defaultDest = path30.join(diagramsDir, "system.md");
24694
+ defaultDest = path31.join(diagramsDir, "system.md");
23731
24695
  }
23732
24696
  const dest = options.out ?? defaultDest;
23733
- ensureDir(path30.dirname(path30.resolve(dest)));
24697
+ ensureDir(path31.dirname(path31.resolve(dest)));
23734
24698
  const content = dest.endsWith(".mmd") ? `${mermaid}
23735
24699
  ` : toMarkdown({ relPath: dest, title, mermaid });
23736
- fs21.writeFileSync(dest, content, "utf-8");
24700
+ fs22.writeFileSync(dest, content, "utf-8");
23737
24701
  logger.success(`Mermaid diagram written to ${dest}`);
23738
24702
  logger.info("Renders on GitHub/IDE previews; use a .mmd --out path for raw Mermaid.");
23739
24703
  }
@@ -23842,8 +24806,8 @@ Component variants (${variants.length})
23842
24806
  }
23843
24807
 
23844
24808
  // src/commands/packs.ts
23845
- var fs22 = __toESM(require("fs"));
23846
- var path31 = __toESM(require("path"));
24809
+ var fs23 = __toESM(require("fs"));
24810
+ var path32 = __toESM(require("path"));
23847
24811
  var import_chalk16 = __toESM(require("chalk"));
23848
24812
  var import_sdk = __toESM(require_dist());
23849
24813
  init_logger();
@@ -23869,11 +24833,11 @@ function describe(probe2) {
23869
24833
  return parts.join(", ");
23870
24834
  }
23871
24835
  function resolveSourceUnit(source) {
23872
- const abs = path31.resolve(source);
23873
- if (!fs22.existsSync(abs)) {
24836
+ const abs = path32.resolve(source);
24837
+ if (!fs23.existsSync(abs)) {
23874
24838
  throw new Error(`Pack source "${source}" does not exist.`);
23875
24839
  }
23876
- const isDir = fs22.statSync(abs).isDirectory();
24840
+ const isDir = fs23.statSync(abs).isDirectory();
23877
24841
  if (isDir && !packDirEntry(abs)) {
23878
24842
  throw new Error(`"${source}" is a directory without a pack entry file (pack.yaml | pack.cjs | index.cjs | ...).`);
23879
24843
  }
@@ -23889,7 +24853,7 @@ async function addPack(source, options = {}) {
23889
24853
  }
23890
24854
  const { abs } = resolveSourceUnit(source);
23891
24855
  const scope = options.global ? "global" : "project";
23892
- const probe2 = probePack(abs, path31.dirname(abs), scope);
24856
+ const probe2 = probePack(abs, path32.dirname(abs), scope);
23893
24857
  if (probe2.error) {
23894
24858
  logger.error(probe2.error);
23895
24859
  process.exitCode = 1;
@@ -23897,10 +24861,10 @@ async function addPack(source, options = {}) {
23897
24861
  }
23898
24862
  if (options.global) {
23899
24863
  const destDir = globalPacksDir();
23900
- const dest2 = path31.join(destDir, path31.basename(abs));
23901
- if (path31.resolve(dest2) !== abs) {
23902
- fs22.mkdirSync(destDir, { recursive: true });
23903
- fs22.cpSync(abs, dest2, { recursive: true, force: true });
24864
+ const dest2 = path32.join(destDir, path32.basename(abs));
24865
+ if (path32.resolve(dest2) !== abs) {
24866
+ fs23.mkdirSync(destDir, { recursive: true });
24867
+ fs23.cpSync(abs, dest2, { recursive: true, force: true });
23904
24868
  }
23905
24869
  logger.success(`Installed pack "${probe2.name}" globally: ${dest2}`);
23906
24870
  logger.info(`${describe(probe2)} \u2014 auto-loaded for every project on this machine (WAIRON_PACKS_DIR / ~/.wairon/packs).`);
@@ -23913,11 +24877,11 @@ async function addPack(source, options = {}) {
23913
24877
  return;
23914
24878
  }
23915
24879
  const root = getProjectRoot();
23916
- const relRef = `.wai/packs/${path31.basename(abs)}`;
23917
- const dest = path31.join(root, ".wai", "packs", path31.basename(abs));
23918
- if (path31.resolve(dest) !== abs) {
23919
- fs22.mkdirSync(path31.dirname(dest), { recursive: true });
23920
- fs22.cpSync(abs, dest, { recursive: true, force: true });
24880
+ const relRef = `.wai/packs/${path32.basename(abs)}`;
24881
+ const dest = path32.join(root, ".wai", "packs", path32.basename(abs));
24882
+ if (path32.resolve(dest) !== abs) {
24883
+ fs23.mkdirSync(path32.dirname(dest), { recursive: true });
24884
+ fs23.cpSync(abs, dest, { recursive: true, force: true });
23921
24885
  }
23922
24886
  const config = loadProjectConfig();
23923
24887
  const packs = config.extensions?.packs ?? [];
@@ -23926,14 +24890,14 @@ async function addPack(source, options = {}) {
23926
24890
  saveProjectConfig(config);
23927
24891
  logger.success(`Vendored pack "${probe2.name}" into ${relRef} and registered it in .wai/project.yaml.`);
23928
24892
  } else {
23929
- fs22.cpSync(abs, dest, { recursive: true, force: true });
24893
+ fs23.cpSync(abs, dest, { recursive: true, force: true });
23930
24894
  logger.success(`Pack "${probe2.name}" already registered \u2014 refreshed ${relRef} from the source.`);
23931
24895
  }
23932
24896
  logger.info(`${describe(probe2)} \u2014 commit .wai/ so CI and every clone enforce it.`);
23933
24897
  }
23934
24898
  async function addPackFromArchive(source, options) {
23935
- const abs = path31.resolve(source);
23936
- if (!fs22.existsSync(abs) || !fs22.statSync(abs).isFile()) {
24899
+ const abs = path32.resolve(source);
24900
+ if (!fs23.existsSync(abs) || !fs23.statSync(abs).isFile()) {
23937
24901
  logger.error(`Pack archive "${source}" does not exist.`);
23938
24902
  process.exitCode = 1;
23939
24903
  return;
@@ -23948,27 +24912,27 @@ async function addPackFromArchive(source, options) {
23948
24912
  process.exitCode = 1;
23949
24913
  return;
23950
24914
  }
23951
- baseDir = path31.join(getProjectRoot(), ".wai", "packs");
24915
+ baseDir = path32.join(getProjectRoot(), ".wai", "packs");
23952
24916
  }
23953
- const bytes = fs22.readFileSync(abs);
23954
- fs22.mkdirSync(baseDir, { recursive: true });
23955
- const staging = fs22.mkdtempSync(path31.join(baseDir, ".wpack-staging-"));
24917
+ const bytes = fs23.readFileSync(abs);
24918
+ fs23.mkdirSync(baseDir, { recursive: true });
24919
+ const staging = fs23.mkdtempSync(path32.join(baseDir, ".wpack-staging-"));
23956
24920
  let result;
23957
24921
  try {
23958
24922
  result = (0, import_sdk.extractPack)(bytes, staging);
23959
24923
  } catch (err) {
23960
- fs22.rmSync(staging, { recursive: true, force: true });
23961
- logger.error(`Failed to extract pack archive "${path31.basename(abs)}": ${err instanceof Error ? err.message : String(err)}`);
24924
+ fs23.rmSync(staging, { recursive: true, force: true });
24925
+ logger.error(`Failed to extract pack archive "${path32.basename(abs)}": ${err instanceof Error ? err.message : String(err)}`);
23962
24926
  process.exitCode = 1;
23963
24927
  return;
23964
24928
  }
23965
24929
  const name = result.name;
23966
- const destDir = path31.join(baseDir, name);
23967
- if (fs22.existsSync(destDir)) fs22.rmSync(destDir, { recursive: true, force: true });
23968
- fs22.renameSync(staging, destDir);
23969
- const probe2 = probePack(destDir, path31.dirname(destDir), scope);
24930
+ const destDir = path32.join(baseDir, name);
24931
+ if (fs23.existsSync(destDir)) fs23.rmSync(destDir, { recursive: true, force: true });
24932
+ fs23.renameSync(staging, destDir);
24933
+ const probe2 = probePack(destDir, path32.dirname(destDir), scope);
23970
24934
  if (probe2.error) {
23971
- fs22.rmSync(destDir, { recursive: true, force: true });
24935
+ fs23.rmSync(destDir, { recursive: true, force: true });
23972
24936
  logger.error(probe2.error);
23973
24937
  process.exitCode = 1;
23974
24938
  return;
@@ -23986,7 +24950,7 @@ async function addPackFromArchive(source, options) {
23986
24950
  saveProjectConfig(config);
23987
24951
  logger.success(`Installed pack "${probe2.name ?? name}" into ${relRef} and registered it in .wai/project.yaml.`);
23988
24952
  } else {
23989
- logger.success(`Pack "${probe2.name ?? name}" already registered \u2014 refreshed ${relRef} from ${path31.basename(abs)}.`);
24953
+ logger.success(`Pack "${probe2.name ?? name}" already registered \u2014 refreshed ${relRef} from ${path32.basename(abs)}.`);
23990
24954
  }
23991
24955
  logger.info(`${describe(probe2)} \u2014 commit .wai/ so CI and every clone enforce it.`);
23992
24956
  }
@@ -24007,7 +24971,7 @@ async function buildPack(source, options = {}) {
24007
24971
  const sourceDir = source && source.length > 0 ? source : ".";
24008
24972
  const result = (0, import_sdk.buildPack)(sourceDir);
24009
24973
  const outPath = options.out ?? result.suggestedFileName;
24010
- fs22.writeFileSync(outPath, result.archive);
24974
+ fs23.writeFileSync(outPath, result.archive);
24011
24975
  logger.success(`Built pack "${result.info.name}" v${result.info.version} \u2192 ${outPath} (${result.archive.byteLength} bytes)`);
24012
24976
  logger.info(`Install it with \`wairon pack add ${outPath}\`, or upload it to a hosted instance.`);
24013
24977
  }
@@ -24021,9 +24985,9 @@ async function listPacks() {
24021
24985
  console.log(import_chalk16.default.bold.cyan(`\u25A0 Global (${globalPacksDir()})${useGlobal ? "" : import_chalk16.default.yellow(" [disabled: extensions.useGlobalPacks: false]")}`));
24022
24986
  if (globalRefs.length === 0) console.log(import_chalk16.default.dim(" (none)"));
24023
24987
  for (const ref of globalRefs) {
24024
- const probe2 = probePack(ref, path31.dirname(ref), "global");
24025
- if (probe2.error) console.log(` ${import_chalk16.default.red("\u2716")} ${path31.basename(ref)} \u2014 ${import_chalk16.default.red(probe2.error)}`);
24026
- else console.log(` ${import_chalk16.default.green("\u25CF")} ${import_chalk16.default.bold(probe2.name ?? path31.basename(ref))} ${import_chalk16.default.dim(describe(probe2))}`);
24988
+ const probe2 = probePack(ref, path32.dirname(ref), "global");
24989
+ if (probe2.error) console.log(` ${import_chalk16.default.red("\u2716")} ${path32.basename(ref)} \u2014 ${import_chalk16.default.red(probe2.error)}`);
24990
+ else console.log(` ${import_chalk16.default.green("\u25CF")} ${import_chalk16.default.bold(probe2.name ?? path32.basename(ref))} ${import_chalk16.default.dim(describe(probe2))}`);
24027
24991
  }
24028
24992
  console.log("");
24029
24993
  if (!inProject) {
@@ -24044,9 +25008,9 @@ async function listPacks() {
24044
25008
  async function removePack(name, options = {}) {
24045
25009
  if (options.global) {
24046
25010
  for (const ref of discoverPacks(globalPacksDir())) {
24047
- const probe2 = probePack(ref, path31.dirname(ref), "global");
24048
- if (probe2.name === name || path31.basename(ref) === name) {
24049
- fs22.rmSync(ref, { recursive: true, force: true });
25011
+ const probe2 = probePack(ref, path32.dirname(ref), "global");
25012
+ if (probe2.name === name || path32.basename(ref) === name) {
25013
+ fs23.rmSync(ref, { recursive: true, force: true });
24050
25014
  logger.success(`Removed global pack "${probe2.name ?? name}" (${ref}).`);
24051
25015
  return;
24052
25016
  }
@@ -24065,16 +25029,16 @@ async function removePack(name, options = {}) {
24065
25029
  const packs = config.extensions?.packs ?? [];
24066
25030
  for (const ref of packs) {
24067
25031
  const probe2 = probePack(ref, root, "project");
24068
- if (probe2.name === name || ref === name || path31.basename(ref) === name) {
25032
+ if (probe2.name === name || ref === name || path32.basename(ref) === name) {
24069
25033
  config.extensions = {
24070
25034
  packs: packs.filter((p) => p !== ref),
24071
25035
  useGlobalPacks: config.extensions?.useGlobalPacks ?? true
24072
25036
  };
24073
25037
  saveProjectConfig(config);
24074
- const resolved = path31.resolve(root, ref);
24075
- const vendorDir = path31.resolve(root, ".wai", "packs");
24076
- if (resolved.startsWith(vendorDir + path31.sep)) {
24077
- fs22.rmSync(resolved, { recursive: true, force: true });
25038
+ const resolved = path32.resolve(root, ref);
25039
+ const vendorDir = path32.resolve(root, ".wai", "packs");
25040
+ if (resolved.startsWith(vendorDir + path32.sep)) {
25041
+ fs23.rmSync(resolved, { recursive: true, force: true });
24078
25042
  logger.success(`Deregistered pack "${probe2.name ?? name}" and deleted ${ref}.`);
24079
25043
  } else {
24080
25044
  logger.success(`Deregistered pack "${probe2.name ?? name}" (files at ${ref} left in place).`);
@@ -24088,8 +25052,8 @@ async function removePack(name, options = {}) {
24088
25052
 
24089
25053
  // src/commands/host.ts
24090
25054
  var fs50 = __toESM(require("fs"));
24091
- var path60 = __toESM(require("path"));
24092
- var os9 = __toESM(require("os"));
25055
+ var path59 = __toESM(require("path"));
25056
+ var os10 = __toESM(require("os"));
24093
25057
  var crypto20 = __toESM(require("crypto"));
24094
25058
  var import_child_process5 = require("child_process");
24095
25059
  var import_chalk17 = __toESM(require("chalk"));
@@ -24116,29 +25080,29 @@ var UNAUTHENTICATED = {
24116
25080
  var WEB_SESSION_PREFIX = "ws_";
24117
25081
 
24118
25082
  // src/server/credentials.ts
24119
- var fs23 = __toESM(require("fs"));
24120
- var path32 = __toESM(require("path"));
25083
+ var fs24 = __toESM(require("fs"));
25084
+ var path33 = __toESM(require("path"));
24121
25085
  var crypto3 = __toESM(require("crypto"));
24122
25086
  var HASH_NS = "wairon:token:v1";
24123
25087
  function hashToken(token) {
24124
25088
  return crypto3.createHash("sha256").update(`${HASH_NS}:${token}`).digest("hex");
24125
25089
  }
24126
25090
  function storePath(dataDir) {
24127
- return path32.join(dataDir, "auth", "credentials.json");
25091
+ return path33.join(dataDir, "auth", "credentials.json");
24128
25092
  }
24129
25093
  function load3(dataDir) {
24130
25094
  try {
24131
- return JSON.parse(fs23.readFileSync(storePath(dataDir), "utf8"));
25095
+ return JSON.parse(fs24.readFileSync(storePath(dataDir), "utf8"));
24132
25096
  } catch {
24133
25097
  return [];
24134
25098
  }
24135
25099
  }
24136
25100
  function save(dataDir, records) {
24137
25101
  const p = storePath(dataDir);
24138
- fs23.mkdirSync(path32.dirname(p), { recursive: true });
25102
+ fs24.mkdirSync(path33.dirname(p), { recursive: true });
24139
25103
  const tmp = `${p}.tmp`;
24140
- fs23.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
24141
- fs23.renameSync(tmp, p);
25104
+ fs24.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
25105
+ fs24.renameSync(tmp, p);
24142
25106
  }
24143
25107
  function digestEquals(a, b) {
24144
25108
  const ab = Buffer.from(a, "hex");
@@ -24183,17 +25147,17 @@ function listByOwner(dataDir, ownerUserId) {
24183
25147
  }
24184
25148
 
24185
25149
  // src/server/websessions.ts
24186
- var fs24 = __toESM(require("fs"));
24187
- var path33 = __toESM(require("path"));
25150
+ var fs25 = __toESM(require("fs"));
25151
+ var path34 = __toESM(require("path"));
24188
25152
  var crypto4 = __toESM(require("crypto"));
24189
25153
  function storePath2(dataDir) {
24190
- return path33.join(dataDir, "web-sessions.json");
25154
+ return path34.join(dataDir, "web-sessions.json");
24191
25155
  }
24192
25156
  function readSessions(dataDir) {
24193
25157
  const p = storePath2(dataDir);
24194
25158
  let raw;
24195
25159
  try {
24196
- raw = fs24.readFileSync(p, "utf8");
25160
+ raw = fs25.readFileSync(p, "utf8");
24197
25161
  } catch (e) {
24198
25162
  if (e.code === "ENOENT") return [];
24199
25163
  throw new Error(`Failed to read web session store at ${p}: ${e.message}`);
@@ -24208,10 +25172,10 @@ function readSessions(dataDir) {
24208
25172
  }
24209
25173
  function persistSessions(dataDir, sessions) {
24210
25174
  const p = storePath2(dataDir);
24211
- fs24.mkdirSync(path33.dirname(p), { recursive: true });
25175
+ fs25.mkdirSync(path34.dirname(p), { recursive: true });
24212
25176
  const tmp = `${p}.tmp`;
24213
- fs24.writeFileSync(tmp, JSON.stringify(sessions, null, 2) + "\n");
24214
- fs24.renameSync(tmp, p);
25177
+ fs25.writeFileSync(tmp, JSON.stringify(sessions, null, 2) + "\n");
25178
+ fs25.renameSync(tmp, p);
24215
25179
  }
24216
25180
  function mintSessionId() {
24217
25181
  return `${WEB_SESSION_PREFIX}${crypto4.randomBytes(24).toString("hex")}`;
@@ -24372,17 +25336,17 @@ function listWebSessionsBySubject(dataDir, userId) {
24372
25336
  }
24373
25337
 
24374
25338
  // src/server/users.ts
24375
- var fs25 = __toESM(require("fs"));
24376
- var path34 = __toESM(require("path"));
25339
+ var fs26 = __toESM(require("fs"));
25340
+ var path35 = __toESM(require("path"));
24377
25341
  var VALID_STATUSES = ["active", "inactive", "suspended", "deactivated", "disabled"];
24378
25342
  function storePath3(dataDir) {
24379
- return path34.join(dataDir, "users.json");
25343
+ return path35.join(dataDir, "users.json");
24380
25344
  }
24381
25345
  function loadStore(dataDir) {
24382
25346
  const p = storePath3(dataDir);
24383
25347
  let raw;
24384
25348
  try {
24385
- raw = fs25.readFileSync(p, "utf8");
25349
+ raw = fs26.readFileSync(p, "utf8");
24386
25350
  } catch (err) {
24387
25351
  if (err.code === "ENOENT") return [];
24388
25352
  throw new Error(`Cannot read hosted-user store at ${p}: ${err.message}`);
@@ -24400,10 +25364,10 @@ function loadStore(dataDir) {
24400
25364
  }
24401
25365
  function replaceAll(dataDir, records) {
24402
25366
  const p = storePath3(dataDir);
24403
- fs25.mkdirSync(path34.dirname(p), { recursive: true });
25367
+ fs26.mkdirSync(path35.dirname(p), { recursive: true });
24404
25368
  const tmp = `${p}.tmp`;
24405
- fs25.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
24406
- fs25.renameSync(tmp, p);
25369
+ fs26.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
25370
+ fs26.renameSync(tmp, p);
24407
25371
  }
24408
25372
  function registryUpsert(dataDir, record2) {
24409
25373
  const records = loadStore(dataDir);
@@ -24505,11 +25469,11 @@ function remapUnitReferences(dataDir, remap, removedScopeIds) {
24505
25469
  }
24506
25470
 
24507
25471
  // src/server/instance.ts
24508
- var fs26 = __toESM(require("fs"));
24509
- var path35 = __toESM(require("path"));
25472
+ var fs27 = __toESM(require("fs"));
25473
+ var path36 = __toESM(require("path"));
24510
25474
  var import_crypto = require("crypto");
24511
25475
  function storePath4(dataDir) {
24512
- return path35.join(dataDir, "instance.json");
25476
+ return path36.join(dataDir, "instance.json");
24513
25477
  }
24514
25478
  var InstanceIdentityStore = class {
24515
25479
  constructor(dataDir) {
@@ -24526,7 +25490,7 @@ var InstanceIdentityStore = class {
24526
25490
  const p = storePath4(this.dataDir);
24527
25491
  let raw;
24528
25492
  try {
24529
- raw = fs26.readFileSync(p, "utf8");
25493
+ raw = fs27.readFileSync(p, "utf8");
24530
25494
  } catch (err) {
24531
25495
  if (err.code === "ENOENT") return null;
24532
25496
  throw new Error(`Cannot read instance identity at ${p}: ${err.message}`);
@@ -24549,10 +25513,10 @@ var InstanceIdentityStore = class {
24549
25513
  * never truncates the file. Only called by the registry's create-once seed. */
24550
25514
  replace(identity) {
24551
25515
  const p = storePath4(this.dataDir);
24552
- fs26.mkdirSync(path35.dirname(p), { recursive: true });
25516
+ fs27.mkdirSync(path36.dirname(p), { recursive: true });
24553
25517
  const tmp = `${p}.tmp`;
24554
- fs26.writeFileSync(tmp, JSON.stringify(identity, null, 2) + "\n");
24555
- fs26.renameSync(tmp, p);
25518
+ fs27.writeFileSync(tmp, JSON.stringify(identity, null, 2) + "\n");
25519
+ fs27.renameSync(tmp, p);
24556
25520
  }
24557
25521
  };
24558
25522
  var InstanceIdentityRegistry = class {
@@ -24608,8 +25572,8 @@ function getInstanceIdentity(dataDir) {
24608
25572
  }
24609
25573
 
24610
25574
  // src/utils/secrets.ts
24611
- var fs27 = __toESM(require("fs"));
24612
- var path36 = __toESM(require("path"));
25575
+ var fs28 = __toESM(require("fs"));
25576
+ var path37 = __toESM(require("path"));
24613
25577
  var ENV_FALLBACK = {
24614
25578
  "git-token": ["WAIRON_GIT_TOKEN"],
24615
25579
  "notion-token": ["WAIRON_NOTION_TOKEN"],
@@ -24618,13 +25582,13 @@ var ENV_FALLBACK = {
24618
25582
  };
24619
25583
  function storePath5() {
24620
25584
  const dataDir = process.env["WAIRON_DATA_DIR"];
24621
- return dataDir ? path36.join(dataDir, "auth", "secrets.json") : null;
25585
+ return dataDir ? path37.join(dataDir, "auth", "secrets.json") : null;
24622
25586
  }
24623
25587
  function readStore() {
24624
25588
  const p = storePath5();
24625
25589
  if (!p) return {};
24626
25590
  try {
24627
- return JSON.parse(fs27.readFileSync(p, "utf8"));
25591
+ return JSON.parse(fs28.readFileSync(p, "utf8"));
24628
25592
  } catch {
24629
25593
  return {};
24630
25594
  }
@@ -24649,10 +25613,10 @@ function setSecret(key, value) {
24649
25613
  if (!p) throw new Error("WAIRON_DATA_DIR is not set \u2014 a running server needs it to store secrets.");
24650
25614
  const store = readStore();
24651
25615
  store[key] = value;
24652
- fs27.mkdirSync(path36.dirname(p), { recursive: true });
25616
+ fs28.mkdirSync(path37.dirname(p), { recursive: true });
24653
25617
  const tmp = `${p}.tmp`;
24654
- fs27.writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n");
24655
- fs27.renameSync(tmp, p);
25618
+ fs28.writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n");
25619
+ fs28.renameSync(tmp, p);
24656
25620
  }
24657
25621
  function listSecretKeys() {
24658
25622
  return Object.keys(readStore());
@@ -24860,17 +25824,17 @@ function verifySsoState(state) {
24860
25824
  }
24861
25825
 
24862
25826
  // src/server/organization.ts
24863
- var fs28 = __toESM(require("fs"));
24864
- var path37 = __toESM(require("path"));
25827
+ var fs29 = __toESM(require("fs"));
25828
+ var path38 = __toESM(require("path"));
24865
25829
  var crypto6 = __toESM(require("crypto"));
24866
25830
  function storePath6(dataDir) {
24867
- return path37.join(dataDir, "organization.json");
25831
+ return path38.join(dataDir, "organization.json");
24868
25832
  }
24869
25833
  function readState(dataDir) {
24870
25834
  const p = storePath6(dataDir);
24871
25835
  let raw;
24872
25836
  try {
24873
- raw = fs28.readFileSync(p, "utf8");
25837
+ raw = fs29.readFileSync(p, "utf8");
24874
25838
  } catch (e) {
24875
25839
  if (e.code === "ENOENT") return { units: [], placements: [] };
24876
25840
  throw new Error(`Failed to read organization store at ${p}: ${e.message}`);
@@ -24887,10 +25851,10 @@ function readState(dataDir) {
24887
25851
  }
24888
25852
  function persistState(dataDir, state) {
24889
25853
  const p = storePath6(dataDir);
24890
- fs28.mkdirSync(path37.dirname(p), { recursive: true });
25854
+ fs29.mkdirSync(path38.dirname(p), { recursive: true });
24891
25855
  const tmp = `${p}.tmp`;
24892
- fs28.writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
24893
- fs28.renameSync(tmp, p);
25856
+ fs29.writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
25857
+ fs29.renameSync(tmp, p);
24894
25858
  }
24895
25859
  var SLUG_PATTERN = /^[a-z0-9-]+$/;
24896
25860
  var UNIT_KINDS = ["business_entity", "department", "team", "group"];
@@ -25259,11 +26223,11 @@ function getOrganizationUnit(dataDir, id) {
25259
26223
  }
25260
26224
 
25261
26225
  // src/server/permissions.ts
25262
- var fs29 = __toESM(require("fs"));
25263
- var path38 = __toESM(require("path"));
26226
+ var fs30 = __toESM(require("fs"));
26227
+ var path39 = __toESM(require("path"));
25264
26228
  var import_crypto2 = require("crypto");
25265
26229
  function storePath7(dataDir) {
25266
- return path38.join(dataDir, "permissions.json");
26230
+ return path39.join(dataDir, "permissions.json");
25267
26231
  }
25268
26232
  function assignmentKey(a) {
25269
26233
  return [a.subjectKind, a.subjectId ?? "", a.scopeKind, a.scopeId ?? "", a.capability].join("|");
@@ -25272,7 +26236,7 @@ function load4(dataDir) {
25272
26236
  const p = storePath7(dataDir);
25273
26237
  let raw;
25274
26238
  try {
25275
- raw = fs29.readFileSync(p, "utf8");
26239
+ raw = fs30.readFileSync(p, "utf8");
25276
26240
  } catch (err) {
25277
26241
  if (err.code === "ENOENT") return [];
25278
26242
  throw new Error(`Cannot read permission store at ${p}: ${err.message}`);
@@ -25290,10 +26254,10 @@ function load4(dataDir) {
25290
26254
  }
25291
26255
  function replaceAll2(dataDir, assignments) {
25292
26256
  const p = storePath7(dataDir);
25293
- fs29.mkdirSync(path38.dirname(p), { recursive: true });
26257
+ fs30.mkdirSync(path39.dirname(p), { recursive: true });
25294
26258
  const tmp = `${p}.tmp`;
25295
- fs29.writeFileSync(tmp, JSON.stringify(assignments, null, 2) + "\n");
25296
- fs29.renameSync(tmp, p);
26259
+ fs30.writeFileSync(tmp, JSON.stringify(assignments, null, 2) + "\n");
26260
+ fs30.renameSync(tmp, p);
25297
26261
  }
25298
26262
  function registrySet(dataDir, assignment) {
25299
26263
  const assignments = load4(dataDir);
@@ -25379,8 +26343,8 @@ function getAssignment(dataDir, assignmentId) {
25379
26343
  }
25380
26344
 
25381
26345
  // src/server/roles.ts
25382
- var fs30 = __toESM(require("fs"));
25383
- var path39 = __toESM(require("path"));
26346
+ var fs31 = __toESM(require("fs"));
26347
+ var path40 = __toESM(require("path"));
25384
26348
  var BUILTIN_ROLES = [
25385
26349
  {
25386
26350
  id: SSO_ADMIN_ROLE_ID,
@@ -25398,13 +26362,13 @@ function isBuiltinRoleId(roleId) {
25398
26362
  return BUILTIN_ROLE_IDS.has(roleId);
25399
26363
  }
25400
26364
  function storePath8(dataDir) {
25401
- return path39.join(dataDir, "roles.json");
26365
+ return path40.join(dataDir, "roles.json");
25402
26366
  }
25403
26367
  function load5(dataDir) {
25404
26368
  const p = storePath8(dataDir);
25405
26369
  let raw;
25406
26370
  try {
25407
- raw = fs30.readFileSync(p, "utf8");
26371
+ raw = fs31.readFileSync(p, "utf8");
25408
26372
  } catch (err) {
25409
26373
  if (err.code === "ENOENT") return [];
25410
26374
  throw new Error(`Cannot read role store at ${p}: ${err.message}`);
@@ -25422,10 +26386,10 @@ function load5(dataDir) {
25422
26386
  }
25423
26387
  function replaceAll3(dataDir, roles) {
25424
26388
  const p = storePath8(dataDir);
25425
- fs30.mkdirSync(path39.dirname(p), { recursive: true });
26389
+ fs31.mkdirSync(path40.dirname(p), { recursive: true });
25426
26390
  const tmp = `${p}.tmp`;
25427
- fs30.writeFileSync(tmp, JSON.stringify(roles, null, 2) + "\n");
25428
- fs30.renameSync(tmp, p);
26391
+ fs31.writeFileSync(tmp, JSON.stringify(roles, null, 2) + "\n");
26392
+ fs31.renameSync(tmp, p);
25429
26393
  }
25430
26394
  function registryCreate(dataDir, role) {
25431
26395
  if (isBuiltinRoleId(role.id)) {
@@ -25659,131 +26623,14 @@ function actionableUnitIds(scopes) {
25659
26623
  }
25660
26624
 
25661
26625
  // src/server/projects.ts
25662
- var fs31 = __toESM(require("fs"));
25663
- var path40 = __toESM(require("path"));
25664
- var ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
25665
- function isValidProjectId(id) {
25666
- return typeof id === "string" && ID_RE.test(id);
25667
- }
25668
- function registryPath(dataDir) {
25669
- return path40.join(dataDir, "projects.json");
25670
- }
25671
- function load6(dataDir) {
25672
- try {
25673
- return JSON.parse(fs31.readFileSync(registryPath(dataDir), "utf8"));
25674
- } catch {
25675
- return [];
25676
- }
25677
- }
25678
- function save2(dataDir, records) {
25679
- const p = registryPath(dataDir);
25680
- fs31.mkdirSync(path40.dirname(p), { recursive: true });
25681
- const tmp = `${p}.tmp`;
25682
- fs31.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
25683
- fs31.renameSync(tmp, p);
25684
- }
25685
- function projectRoot(dataDir, id) {
25686
- return path40.join(dataDir, "projects", id);
25687
- }
25688
- function existingProjectRoot(dataDir, id) {
25689
- if (!isValidProjectId(id)) return null;
25690
- const rec = load6(dataDir).find((r) => r.id === id);
25691
- return rec ? rec.rootPath : null;
25692
- }
25693
- function createProjectRecord(dataDir, id) {
25694
- if (!isValidProjectId(id)) {
25695
- throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
25696
- }
25697
- const records = load6(dataDir);
25698
- if (records.some((r) => r.id === id)) {
25699
- throw new Error(`Project "${id}" already exists.`);
25700
- }
25701
- const root = projectRoot(dataDir, id);
25702
- fs31.mkdirSync(root, { recursive: true });
25703
- const record2 = {
25704
- id,
25705
- rootPath: root,
25706
- status: "active",
25707
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
25708
- };
25709
- records.push(record2);
25710
- save2(dataDir, records);
25711
- return record2;
25712
- }
25713
- function registerLocalDevProject(dataDir, id, rootPath) {
25714
- if (!isValidProjectId(id)) {
25715
- throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
25716
- }
25717
- const records = load6(dataDir);
25718
- const existing = records.find((r) => r.id === id);
25719
- const record2 = {
25720
- id,
25721
- rootPath,
25722
- status: "active",
25723
- createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
25724
- };
25725
- const next = existing ? records.map((r) => r.id === id ? record2 : r) : [...records, record2];
25726
- save2(dataDir, next);
25727
- return record2;
25728
- }
25729
- function listProjectRecords(dataDir) {
25730
- return load6(dataDir);
25731
- }
25732
- function removeProjectRecord(dataDir, id) {
25733
- const records = load6(dataDir);
25734
- const rec = records.find((r) => r.id === id);
25735
- if (rec) {
25736
- try {
25737
- fs31.rmSync(rec.rootPath, { recursive: true, force: true });
25738
- } catch {
25739
- }
25740
- }
25741
- save2(dataDir, records.filter((r) => r.id !== id));
25742
- }
25743
- function resolveProjectRoot(dataDir, principal, selector) {
25744
- const authorized = principal.projects;
25745
- const wildcard = authorized.includes("*");
25746
- let target;
25747
- if (selector) {
25748
- if (!wildcard && !authorized.includes(selector)) return null;
25749
- target = selector;
25750
- } else if (!wildcard && authorized.length === 1) {
25751
- target = authorized[0];
25752
- } else {
25753
- return null;
25754
- }
25755
- if (!isValidProjectId(target)) return null;
25756
- const rec = load6(dataDir).find((r) => r.id === target);
25757
- if (!rec || rec.status !== "active") return null;
25758
- return rec.rootPath;
25759
- }
25760
-
25761
- // src/server/adapters.ts
25762
- init_statehash();
25763
-
25764
- // src/core/lockfile.ts
25765
- var fs32 = __toESM(require("fs"));
25766
- var path41 = __toESM(require("path"));
26626
+ var fs35 = __toESM(require("fs"));
26627
+ var path44 = __toESM(require("path"));
26628
+ init_loader();
26629
+ init_yaml();
25767
26630
  init_fs();
25768
- function lockPath() {
25769
- return aiDir("lock.json");
25770
- }
25771
- function readLockRecord() {
25772
- try {
25773
- return JSON.parse(fs32.readFileSync(lockPath(), "utf8"));
25774
- } catch {
25775
- return null;
25776
- }
25777
- }
25778
- function writeLockRecord(record2) {
25779
- const p = lockPath();
25780
- fs32.mkdirSync(path41.dirname(p), { recursive: true });
25781
- const tmp = `${p}.tmp`;
25782
- fs32.writeFileSync(tmp, JSON.stringify(record2, null, 2) + "\n");
25783
- fs32.renameSync(tmp, p);
25784
- }
25785
26631
 
25786
26632
  // src/server/adapters.ts
26633
+ init_statehash();
25787
26634
  init_specs2();
25788
26635
  init_provision();
25789
26636
  init_validation();
@@ -25794,37 +26641,37 @@ init_types();
25794
26641
  init_server();
25795
26642
 
25796
26643
  // src/git/config.ts
25797
- var fs33 = __toESM(require("fs"));
25798
- var path42 = __toESM(require("path"));
26644
+ var fs32 = __toESM(require("fs"));
26645
+ var path41 = __toESM(require("path"));
25799
26646
  init_fs();
25800
26647
  function configPath() {
25801
26648
  return aiDir("git.json");
25802
26649
  }
25803
26650
  function readGitConfig() {
25804
26651
  try {
25805
- return JSON.parse(fs33.readFileSync(configPath(), "utf8"));
26652
+ return JSON.parse(fs32.readFileSync(configPath(), "utf8"));
25806
26653
  } catch {
25807
26654
  return null;
25808
26655
  }
25809
26656
  }
25810
26657
  function writeGitConfig(config) {
25811
26658
  const p = configPath();
25812
- fs33.mkdirSync(path42.dirname(p), { recursive: true });
26659
+ fs32.mkdirSync(path41.dirname(p), { recursive: true });
25813
26660
  const tmp = `${p}.tmp`;
25814
- fs33.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n");
25815
- fs33.renameSync(tmp, p);
26661
+ fs32.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n");
26662
+ fs32.renameSync(tmp, p);
25816
26663
  }
25817
26664
  function clearGitConfig() {
25818
26665
  try {
25819
- fs33.rmSync(configPath(), { force: true });
26666
+ fs32.rmSync(configPath(), { force: true });
25820
26667
  } catch {
25821
26668
  }
25822
26669
  }
25823
26670
 
25824
26671
  // src/git/adapter.ts
25825
26672
  var import_child_process3 = require("child_process");
25826
- var fs34 = __toESM(require("fs"));
25827
- var path43 = __toESM(require("path"));
26673
+ var fs33 = __toESM(require("fs"));
26674
+ var path42 = __toESM(require("path"));
25828
26675
  init_fs();
25829
26676
  function git(args, cwd) {
25830
26677
  return (0, import_child_process3.execFileSync)("git", args, {
@@ -25876,10 +26723,10 @@ function compareUrl(remote, defaultBranch, workingBranch) {
25876
26723
  return `${web}/compare/${encodeURIComponent(defaultBranch)}...${encodeURIComponent(workingBranch)}`;
25877
26724
  }
25878
26725
  function excludeLocalFiles() {
25879
- const excludePath = path43.join(getProjectRoot(), ".git", "info", "exclude");
26726
+ const excludePath = path42.join(getProjectRoot(), ".git", "info", "exclude");
25880
26727
  try {
25881
- fs34.mkdirSync(path43.dirname(excludePath), { recursive: true });
25882
- fs34.appendFileSync(excludePath, "\n.wai/lock.json\n.wai/git.json\n");
26728
+ fs33.mkdirSync(path42.dirname(excludePath), { recursive: true });
26729
+ fs33.appendFileSync(excludePath, "\n.wai/lock.json\n.wai/git.json\n");
25883
26730
  } catch {
25884
26731
  }
25885
26732
  }
@@ -25944,39 +26791,39 @@ function configureSync(periodicSyncMinutes, skipIfClean) {
25944
26791
  }
25945
26792
 
25946
26793
  // src/producers/config.ts
25947
- var fs35 = __toESM(require("fs"));
25948
- var path44 = __toESM(require("path"));
26794
+ var fs34 = __toESM(require("fs"));
26795
+ var path43 = __toESM(require("path"));
25949
26796
  init_fs();
25950
26797
  function configPath2() {
25951
26798
  return aiDir("producers.json");
25952
26799
  }
25953
- function load7() {
26800
+ function load6() {
25954
26801
  try {
25955
- return JSON.parse(fs35.readFileSync(configPath2(), "utf8"));
26802
+ return JSON.parse(fs34.readFileSync(configPath2(), "utf8"));
25956
26803
  } catch {
25957
26804
  return [];
25958
26805
  }
25959
26806
  }
25960
- function save3(configs) {
26807
+ function save2(configs) {
25961
26808
  const p = configPath2();
25962
- fs35.mkdirSync(path44.dirname(p), { recursive: true });
26809
+ fs34.mkdirSync(path43.dirname(p), { recursive: true });
25963
26810
  const tmp = `${p}.tmp`;
25964
- fs35.writeFileSync(tmp, JSON.stringify(configs, null, 2) + "\n");
25965
- fs35.renameSync(tmp, p);
26811
+ fs34.writeFileSync(tmp, JSON.stringify(configs, null, 2) + "\n");
26812
+ fs34.renameSync(tmp, p);
25966
26813
  }
25967
26814
  function readProducerConfig(target) {
25968
- return load7().find((c) => c.target === target) ?? null;
26815
+ return load6().find((c) => c.target === target) ?? null;
25969
26816
  }
25970
26817
  function writeProducerConfig(config) {
25971
- const configs = load7().filter((c) => c.target !== config.target);
26818
+ const configs = load6().filter((c) => c.target !== config.target);
25972
26819
  configs.push(config);
25973
- save3(configs);
26820
+ save2(configs);
25974
26821
  }
25975
26822
  function clearProducerConfig(target) {
25976
- save3(load7().filter((c) => c.target !== target));
26823
+ save2(load6().filter((c) => c.target !== target));
25977
26824
  }
25978
26825
  function listProducerConfigs() {
25979
- return load7();
26826
+ return load6();
25980
26827
  }
25981
26828
 
25982
26829
  // src/producers/core-adapter.ts
@@ -26315,8 +27162,19 @@ var hostCore = {
26315
27162
  /** The ids of wairon's built-in architectural profiles, read from the core rules
26316
27163
  * registry's built-in profile set (BUILTIN_PROFILES) — a pure, side-effect-free
26317
27164
  * read of a bundled constant. */
26318
- builtinProfileIds: () => [...BUILTIN_PROFILES]
27165
+ builtinProfileIds: () => [...BUILTIN_PROFILES],
27166
+ /** The ids of wairon's built-in COMPOSITE PROJECT KINDS, read from the core
27167
+ * rules registry's bundled constant (PROJECT_KINDS) — a pure, side-effect-free
27168
+ * read. The counterpart of builtinProfileIds: legal projectType values that
27169
+ * are not architectural profiles and carry no profile doctrine of their own,
27170
+ * so the hosted profile-application path recognizes a project kind as
27171
+ * resolvable-as-is (no contributing pack to adopt) instead of refusing it as
27172
+ * an unknown profile. */
27173
+ builtinProjectKinds: () => [...PROJECT_KINDS]
26319
27174
  };
27175
+ function resolveContainedProjectPath(projectRoot2, projectPath) {
27176
+ return assertContainedProjectPath(projectRoot2, projectPath);
27177
+ }
26320
27178
  function validateProjectAsComplete() {
26321
27179
  const config = loadProjectConfig();
26322
27180
  return validateAsComplete({ rules: config.rules, projectType: config.projectType });
@@ -26348,6 +27206,180 @@ var hostSdk = {
26348
27206
  extractArchive: (archive, destDir, limits) => sdkPortal.extractPack(archive, destDir, limits)
26349
27207
  };
26350
27208
 
27209
+ // src/server/projects.ts
27210
+ var ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
27211
+ function isValidProjectId(id) {
27212
+ return typeof id === "string" && ID_RE.test(id);
27213
+ }
27214
+ function registryPath(dataDir) {
27215
+ return path44.join(dataDir, "projects.json");
27216
+ }
27217
+ function load7(dataDir) {
27218
+ try {
27219
+ return JSON.parse(fs35.readFileSync(registryPath(dataDir), "utf8"));
27220
+ } catch {
27221
+ return [];
27222
+ }
27223
+ }
27224
+ function save3(dataDir, records) {
27225
+ const p = registryPath(dataDir);
27226
+ fs35.mkdirSync(path44.dirname(p), { recursive: true });
27227
+ const tmp = `${p}.tmp`;
27228
+ fs35.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
27229
+ fs35.renameSync(tmp, p);
27230
+ }
27231
+ function projectRoot(dataDir, id) {
27232
+ return path44.join(dataDir, "projects", id);
27233
+ }
27234
+ function existingProjectRoot(dataDir, id) {
27235
+ if (!isValidProjectId(id)) return null;
27236
+ const rec = load7(dataDir).find((r) => r.id === id);
27237
+ return rec ? rec.rootPath : null;
27238
+ }
27239
+ function createProjectRecord(dataDir, id) {
27240
+ if (!isValidProjectId(id)) {
27241
+ throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
27242
+ }
27243
+ const records = load7(dataDir);
27244
+ if (records.some((r) => r.id === id)) {
27245
+ throw new Error(`Project "${id}" already exists.`);
27246
+ }
27247
+ const root = projectRoot(dataDir, id);
27248
+ fs35.mkdirSync(root, { recursive: true });
27249
+ const record2 = {
27250
+ id,
27251
+ rootPath: root,
27252
+ status: "active",
27253
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
27254
+ };
27255
+ records.push(record2);
27256
+ save3(dataDir, records);
27257
+ return record2;
27258
+ }
27259
+ function registerLocalDevProject(dataDir, id, rootPath) {
27260
+ if (!isValidProjectId(id)) {
27261
+ throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
27262
+ }
27263
+ const records = load7(dataDir);
27264
+ const existing = records.find((r) => r.id === id);
27265
+ const record2 = {
27266
+ id,
27267
+ rootPath,
27268
+ status: "active",
27269
+ createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
27270
+ };
27271
+ const next = existing ? records.map((r) => r.id === id ? record2 : r) : [...records, record2];
27272
+ save3(dataDir, next);
27273
+ return record2;
27274
+ }
27275
+ function listProjectRecords(dataDir) {
27276
+ return load7(dataDir);
27277
+ }
27278
+ function removeProjectRecord(dataDir, id) {
27279
+ const records = load7(dataDir);
27280
+ const rec = records.find((r) => r.id === id);
27281
+ if (rec) {
27282
+ try {
27283
+ fs35.rmSync(rec.rootPath, { recursive: true, force: true });
27284
+ } catch {
27285
+ }
27286
+ }
27287
+ save3(dataDir, records.filter((r) => r.id !== id));
27288
+ }
27289
+ var SUBPROJECT_SEPARATOR = "::";
27290
+ function parseQualifiedSelector(value) {
27291
+ if (typeof value !== "string" || value.length === 0) return null;
27292
+ const [projectId, ...mounts] = value.split(SUBPROJECT_SEPARATOR);
27293
+ if (!isValidProjectId(projectId)) return null;
27294
+ if (mounts.some((m) => m.trim() === "")) return null;
27295
+ return { projectId, mounts };
27296
+ }
27297
+ function findSubsystemSpec(root, subsystemId) {
27298
+ const specsDir = aiPathsAt(root).specsDir();
27299
+ if (!fs35.existsSync(specsDir)) return null;
27300
+ for (const file of listFilesRecursive(specsDir, ".yaml")) {
27301
+ let raw;
27302
+ try {
27303
+ raw = readYamlFile(file);
27304
+ } catch {
27305
+ continue;
27306
+ }
27307
+ if (!raw || typeof raw !== "object" || !("parentSystem" in raw)) continue;
27308
+ if (raw.id !== subsystemId) continue;
27309
+ const pp = raw.projectPath;
27310
+ return typeof pp === "string" && pp.trim() !== "" ? { projectPath: pp } : {};
27311
+ }
27312
+ return null;
27313
+ }
27314
+ function resolveSubprojectMounts(projectId, projectRoot2, mounts) {
27315
+ let root = projectRoot2;
27316
+ let at = projectId;
27317
+ for (const mount of mounts) {
27318
+ const sub = findSubsystemSpec(root, mount);
27319
+ if (!sub) {
27320
+ throw new Error(
27321
+ `unknown subproject mount "${mount}" on "${at}" \u2014 no subsystem with that id exists in its spec tree`
27322
+ );
27323
+ }
27324
+ if (!sub.projectPath) {
27325
+ throw new Error(
27326
+ `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`
27327
+ );
27328
+ }
27329
+ root = resolveContainedProjectPath(root, sub.projectPath);
27330
+ at = `${at}${SUBPROJECT_SEPARATOR}${mount}`;
27331
+ }
27332
+ return root;
27333
+ }
27334
+ function assertMintableNarrowingEntry(dataDir, entry) {
27335
+ if (entry === "*") return;
27336
+ const parsed = parseQualifiedSelector(entry);
27337
+ if (!parsed) {
27338
+ throw new Error(
27339
+ `invalid project narrowing entry "${entry}" (expected a project id, optionally subproject-qualified as projectId::subsystemId)`
27340
+ );
27341
+ }
27342
+ const rec = load7(dataDir).find((r) => r.id === parsed.projectId);
27343
+ if (!rec) throw new Error(`unknown project "${parsed.projectId}"`);
27344
+ if (parsed.mounts.length > 0) {
27345
+ resolveSubprojectMounts(parsed.projectId, rec.rootPath, parsed.mounts);
27346
+ }
27347
+ }
27348
+ function narrowingCovers(entry, target) {
27349
+ return target === entry || target.startsWith(entry + SUBPROJECT_SEPARATOR);
27350
+ }
27351
+ function resolveProjectBinding(dataDir, principal, selector) {
27352
+ const authorized = principal.projects;
27353
+ const wildcard = authorized.includes("*");
27354
+ let target;
27355
+ if (selector) {
27356
+ if (!wildcard && !authorized.some((e) => e !== "*" && narrowingCovers(e, selector))) return null;
27357
+ target = selector;
27358
+ } else if (!wildcard && authorized.length === 1) {
27359
+ target = authorized[0];
27360
+ } else {
27361
+ return null;
27362
+ }
27363
+ const parsed = parseQualifiedSelector(target);
27364
+ if (!parsed) return null;
27365
+ const rec = load7(dataDir).find((r) => r.id === parsed.projectId);
27366
+ if (!rec || rec.status !== "active") return null;
27367
+ let rootPath = rec.rootPath;
27368
+ if (parsed.mounts.length > 0) {
27369
+ try {
27370
+ rootPath = resolveSubprojectMounts(parsed.projectId, rec.rootPath, parsed.mounts);
27371
+ } catch {
27372
+ return null;
27373
+ }
27374
+ }
27375
+ const binding = { rootPath, projectId: parsed.projectId };
27376
+ if (parsed.mounts.length > 0) binding.subproject = parsed.mounts.join(SUBPROJECT_SEPARATOR);
27377
+ return binding;
27378
+ }
27379
+ function resolveProjectRoot(dataDir, principal, selector) {
27380
+ return resolveProjectBinding(dataDir, principal, selector)?.rootPath ?? null;
27381
+ }
27382
+
26351
27383
  // src/server/errors.ts
26352
27384
  var UnauthenticatedError = class extends Error {
26353
27385
  constructor() {
@@ -26468,9 +27500,14 @@ function lockProject(cfg, credential, project2) {
26468
27500
  }
26469
27501
  return executeApprovedLock(cfg, project2);
26470
27502
  }
26471
- function executeApprovedLock(cfg, projectId) {
27503
+ function boundLifecycleRoot(cfg, projectId, subproject) {
26472
27504
  const root = existingProjectRoot(cfg.dataDir, projectId);
26473
27505
  if (!root) throw new Error(`Unknown project "${projectId}".`);
27506
+ if (!subproject) return root;
27507
+ return resolveSubprojectMounts(projectId, root, subproject.split(SUBPROJECT_SEPARATOR));
27508
+ }
27509
+ function executeApprovedLock(cfg, projectId, subproject) {
27510
+ const root = boundLifecycleRoot(cfg, projectId, subproject);
26474
27511
  return runWithProjectRoot(root, () => {
26475
27512
  hostGit.sync();
26476
27513
  const result = validateProjectAsComplete();
@@ -26642,9 +27679,8 @@ function promoteProject(cfg, credential, project2) {
26642
27679
  }
26643
27680
  return executeApprovedPromote(cfg, project2);
26644
27681
  }
26645
- function executeApprovedPromote(cfg, projectId) {
26646
- const root = existingProjectRoot(cfg.dataDir, projectId);
26647
- if (!root) throw new Error(`Unknown project "${projectId}".`);
27682
+ function executeApprovedPromote(cfg, projectId, subproject) {
27683
+ const root = boundLifecycleRoot(cfg, projectId, subproject);
26648
27684
  return runWithProjectRoot(root, () => {
26649
27685
  const lock = hostCore.readLockRecord();
26650
27686
  if (!lock) {
@@ -26759,6 +27795,35 @@ function storeListGlobalPacks() {
26759
27795
  );
26760
27796
  return [...instance, ...image];
26761
27797
  }
27798
+ function scanGlobalPackProfiles() {
27799
+ const out = [];
27800
+ for (const dir of [hostCore.globalPacksDir(), imagePacksDir()]) {
27801
+ for (const full of hostCore.discoverPacks(dir)) {
27802
+ try {
27803
+ const loaded = hostCore.loadExtensionPacks([{ ref: full, scope: "global" }], path45.dirname(full));
27804
+ if (loaded.errors.length) continue;
27805
+ const source = loaded.packNames[0] ?? path45.basename(full);
27806
+ for (const [id, def] of Object.entries(loaded.profiles)) out.push({ id, source, family: def.family });
27807
+ } catch {
27808
+ }
27809
+ }
27810
+ }
27811
+ return out;
27812
+ }
27813
+ function scanProjectPackProfiles() {
27814
+ const root = getProjectRoot();
27815
+ const out = [];
27816
+ for (const ref of loadProjectConfig().extensions?.packs ?? []) {
27817
+ try {
27818
+ const loaded = hostCore.loadExtensionPacks([{ ref, scope: "project" }], root);
27819
+ if (loaded.errors.length) continue;
27820
+ const source = loaded.packNames[0] ?? stem(ref);
27821
+ for (const [id, def] of Object.entries(loaded.profiles)) out.push({ id, source, family: def.family });
27822
+ } catch {
27823
+ }
27824
+ }
27825
+ return out;
27826
+ }
26762
27827
  function storeListAvailableProfiles() {
26763
27828
  const out = [];
26764
27829
  const seen = /* @__PURE__ */ new Set();
@@ -26769,19 +27834,21 @@ function storeListAvailableProfiles() {
26769
27834
  out.push(family ? { id, source, family } : { id, source });
26770
27835
  };
26771
27836
  for (const id of hostCore.builtinProfileIds()) emit(id, "builtin");
26772
- const scanTier = (dir) => {
26773
- for (const full of hostCore.discoverPacks(dir)) {
26774
- try {
26775
- const loaded = hostCore.loadExtensionPacks([{ ref: full, scope: "global" }], path45.dirname(full));
26776
- if (loaded.errors.length) continue;
26777
- const source = loaded.packNames[0] ?? path45.basename(full);
26778
- for (const [id, def] of Object.entries(loaded.profiles)) emit(id, source, def.family);
26779
- } catch {
26780
- }
26781
- }
27837
+ for (const c of scanGlobalPackProfiles()) emit(c.id, c.source, c.family);
27838
+ return out;
27839
+ }
27840
+ function storeListProjectProfiles() {
27841
+ const out = [];
27842
+ const seen = /* @__PURE__ */ new Set();
27843
+ const emit = (id, source, installed, family) => {
27844
+ const key = JSON.stringify([id, source]);
27845
+ if (seen.has(key)) return;
27846
+ seen.add(key);
27847
+ out.push({ id, source, ...family ? { family } : {}, installed });
26782
27848
  };
26783
- scanTier(hostCore.globalPacksDir());
26784
- scanTier(imagePacksDir());
27849
+ for (const id of hostCore.builtinProfileIds()) emit(id, "builtin", true);
27850
+ for (const c of scanProjectPackProfiles()) emit(c.id, c.source, true, c.family);
27851
+ for (const c of scanGlobalPackProfiles()) emit(c.id, c.source, false, c.family);
26785
27852
  return out;
26786
27853
  }
26787
27854
  function readPackContent(full) {
@@ -26974,6 +28041,10 @@ function listAvailableProfiles(cfg, credential) {
26974
28041
  requirePrincipal2(cfg, credential);
26975
28042
  return storeListAvailableProfiles();
26976
28043
  }
28044
+ function listProjectProfiles(cfg, credential, project2) {
28045
+ requireCap(cfg, credential, "project:read", "project", project2, "Forbidden \u2014 listing a project's selectable profiles requires project:read over the project");
28046
+ return executeApprovedListProjectProfiles(cfg, project2);
28047
+ }
26977
28048
  function listAdoptableProjectPacks(cfg, credential, project2) {
26978
28049
  requireCap(cfg, credential, "project:read", "project", project2, "Forbidden \u2014 listing adoptable packs requires project:read over the project");
26979
28050
  return storeListGlobalPacks();
@@ -26996,6 +28067,26 @@ function executeApprovedInstallProjectPack(cfg, project2, name, content) {
26996
28067
  function executeApprovedResolveGlobalPacks(names) {
26997
28068
  return storeResolveGlobalPacks(names);
26998
28069
  }
28070
+ function executeApprovedListProjectProfiles(cfg, project2) {
28071
+ return runWithProjectRoot(boundProject2(cfg, project2), () => storeListProjectProfiles());
28072
+ }
28073
+ function executeApprovedEnsureProfileInstalled(cfg, project2, profileId) {
28074
+ if (hostCore.builtinProjectKinds().includes(profileId) || hostCore.builtinProfileIds().includes(profileId)) {
28075
+ return { profileId, source: "builtin" };
28076
+ }
28077
+ const contributors = executeApprovedListProjectProfiles(cfg, project2).filter((p) => p.id === profileId);
28078
+ const installed = contributors.find((p) => p.installed);
28079
+ if (installed) return { profileId, source: installed.source };
28080
+ const adoptable = contributors[0];
28081
+ const resolved = adoptable ? executeApprovedResolveGlobalPacks([adoptable.source]).resolved[0] : void 0;
28082
+ if (!resolved) {
28083
+ throw new Error(
28084
+ `Unknown profile "${profileId}" \u2014 no built-in profile or project kind carries it, no pack registered in project "${project2}" contributes it, and no server-global pack (mutable instance tier or immutable image tier) contributes it. Writing an unresolvable id as the projectType would silently disable the whole profile doctrine (UNKNOWN_PROFILE), so it is refused instead of applied.`
28085
+ );
28086
+ }
28087
+ executeApprovedInstallProjectPack(cfg, project2, resolved.name, resolved.content);
28088
+ return { profileId, source: resolved.name, adoptedPackName: resolved.name };
28089
+ }
26999
28090
  function removeProjectPack(cfg, credential, project2, name) {
27000
28091
  requireCap(cfg, credential, "project:admin", "project", project2, "Forbidden \u2014 removing a project pack requires project:admin over the project");
27001
28092
  runWithProjectRoot(boundProject2(cfg, project2), () => storeRemoveProjectPack(name));
@@ -27990,6 +29081,40 @@ function requestPackNames(request) {
27990
29081
  if (!sel) return [];
27991
29082
  return [.../* @__PURE__ */ new Set([...sel.requiredPackNames ?? [], ...sel.defaultPackNames ?? []])];
27992
29083
  }
29084
+ function classifyProfile(projectType, catalog) {
29085
+ if (hostCore.builtinProfileIds().includes(projectType) || hostCore.builtinProjectKinds().includes(projectType)) {
29086
+ return { source: "builtin", resolvable: true };
29087
+ }
29088
+ const installed = catalog.find((p) => p.id === projectType && p.installed);
29089
+ if (installed) return { source: installed.source, resolvable: true };
29090
+ return { resolvable: false };
29091
+ }
29092
+ function firstApplicableProfileId(candidates, catalog) {
29093
+ const kinds = hostCore.builtinProjectKinds();
29094
+ const catalogIds = new Set(catalog.map((p) => p.id));
29095
+ return candidates.find((id) => kinds.includes(id) || catalogIds.has(id));
29096
+ }
29097
+ function unappliedIds(selectedProfileIds, governingProfileId) {
29098
+ return selectedProfileIds.filter((id) => id !== governingProfileId);
29099
+ }
29100
+ function foldAppliedProfile(root, profileId, actor) {
29101
+ const recorded = readProjectProfileSelection(root);
29102
+ const folded = {
29103
+ profileIds: [profileId, ...unappliedIds(recorded?.profileIds ?? [], profileId)],
29104
+ requiredPackNames: recorded?.requiredPackNames ?? [],
29105
+ selectedBy: actor,
29106
+ selectedAt: (/* @__PURE__ */ new Date()).toISOString()
29107
+ };
29108
+ if (recorded?.defaultPackNames) folded.defaultPackNames = recorded.defaultPackNames;
29109
+ recordProjectProfileSelection(root, folded);
29110
+ return folded;
29111
+ }
29112
+ function overridingSubsystemIds(root) {
29113
+ return runWithProjectRoot(
29114
+ root,
29115
+ () => hostCore.loadSubsystemSpecs().filter((s) => !!s.profile).map((s) => s.id)
29116
+ );
29117
+ }
27993
29118
  function resolvedSelection(request, policy, selectedBy) {
27994
29119
  const sel = request.profileSelection;
27995
29120
  const selection = {
@@ -28008,7 +29133,8 @@ function buildEvaluation(input) {
28008
29133
  selectedProfileIds,
28009
29134
  hasSelection,
28010
29135
  countMissingPacksAsViolation,
28011
- requiredDefaultResolution
29136
+ requiredDefaultResolution,
29137
+ governingProfileId
28012
29138
  } = input;
28013
29139
  const present = new Set(presentPackNames);
28014
29140
  let missingPackNames;
@@ -28029,6 +29155,7 @@ function buildEvaluation(input) {
28029
29155
  const allowed = policy.allowedProfileIds;
28030
29156
  const disallowedProfileIds = allowed && allowed.length > 0 ? selectedProfileIds.filter((id) => !allowed.includes(id)) : [];
28031
29157
  const selectionRequiredUnmet = policy.requireProfileSelection && !hasSelection;
29158
+ const unappliedProfileIds = governingProfileId ? unappliedIds(selectedProfileIds, governingProfileId) : [];
28032
29159
  const messages = [];
28033
29160
  if (selectionRequiredUnmet) {
28034
29161
  messages.push("Profile selection is required by policy but none was provided.");
@@ -28040,16 +29167,24 @@ function buildEvaluation(input) {
28040
29167
  for (const n of blockedPackNames) messages.push(`Pack "${n}" is blocked by policy.`);
28041
29168
  for (const id of missingProfileIds) messages.push(`Required profile "${id}" is not selected.`);
28042
29169
  for (const id of disallowedProfileIds) messages.push(`Profile "${id}" is not permitted by policy.`);
29170
+ for (const id of unappliedProfileIds) {
29171
+ messages.push(
29172
+ `Selected profile "${id}" is recorded but does not govern the project (projectType is "${governingProfileId}") \u2014 a project has exactly one governing profile.`
29173
+ );
29174
+ }
28043
29175
  const violation = selectionRequiredUnmet || blockedPackNames.length > 0 || missingProfileIds.length > 0 || disallowedProfileIds.length > 0 || countMissingPacksAsViolation && (missingPackNames.length > 0 || unresolvedPacks.length > 0);
28044
- return {
29176
+ const result = {
28045
29177
  compliant: !violation,
28046
29178
  mode: policy.enforcementMode,
28047
29179
  missingPackNames,
28048
29180
  blockedPackNames,
28049
29181
  missingProfileIds,
28050
29182
  unresolvedPacks,
29183
+ unappliedProfileIds,
28051
29184
  messages
28052
29185
  };
29186
+ if (governingProfileId) result.governingProfileId = governingProfileId;
29187
+ return result;
28053
29188
  }
28054
29189
  function performInit(cfg, request, principal) {
28055
29190
  const policy = effectivePolicy(cfg.dataDir);
@@ -28069,17 +29204,24 @@ function performInit(cfg, request, principal) {
28069
29204
  request.ownerUnitId,
28070
29205
  principal ? principalSubject3(principal) : void 0
28071
29206
  );
28072
- installResolvedPacks(
28073
- cfg,
28074
- record2.id,
28075
- executeApprovedResolveGlobalPacks([
28076
- ...policy.requiredGlobalPacks,
28077
- ...policy.defaultProjectPacks,
28078
- ...requestPackNames(request)
28079
- ])
28080
- );
29207
+ const packResolution = executeApprovedResolveGlobalPacks([
29208
+ ...policy.requiredGlobalPacks,
29209
+ ...policy.defaultProjectPacks,
29210
+ ...requestPackNames(request)
29211
+ ]);
29212
+ installResolvedPacks(cfg, record2.id, packResolution);
28081
29213
  const selectedBy = principal ? principalSubject3(principal) : void 0;
28082
- recordProjectProfileSelection(record2.rootPath, resolvedSelection(request, policy, selectedBy));
29214
+ const selection = resolvedSelection(request, policy, selectedBy);
29215
+ recordProjectProfileSelection(record2.rootPath, selection);
29216
+ const catalog = executeApprovedListProjectProfiles(cfg, record2.id);
29217
+ const appliedProfileId = firstApplicableProfileId(selection.profileIds, catalog);
29218
+ let appliedSource;
29219
+ if (appliedProfileId) {
29220
+ const application = executeApprovedEnsureProfileInstalled(cfg, record2.id, appliedProfileId);
29221
+ writeProjectType(record2.rootPath, application.profileId);
29222
+ appliedSource = application.source;
29223
+ }
29224
+ const unapplied = appliedProfileId ? unappliedIds(selection.profileIds, appliedProfileId) : [...selection.profileIds];
28083
29225
  const actor = principal ? principalSubject3(principal) : SYSTEM_SUBJECT;
28084
29226
  tryAppendAudit2(
28085
29227
  cfg,
@@ -28088,7 +29230,18 @@ function performInit(cfg, request, principal) {
28088
29230
  "project.init.policy",
28089
29231
  "info",
28090
29232
  "project",
28091
- { target: record2.id, projectId: record2.id },
29233
+ {
29234
+ target: record2.id,
29235
+ projectId: record2.id,
29236
+ metadata: JSON.stringify({
29237
+ ...appliedProfileId ? { appliedProfileId, profileSource: appliedSource } : {
29238
+ appliedProfileId: null,
29239
+ profileNotApplied: selection.profileIds.length === 0 ? "no profile was selected \u2014 the default projectType stands" : "no selected profile is resolvable on this instance \u2014 the default projectType stands"
29240
+ },
29241
+ ...unapplied.length > 0 ? { unappliedProfileIds: unapplied } : {},
29242
+ ...packResolution.unresolved.length > 0 ? { unresolvedPacks: packResolution.unresolved } : {}
29243
+ })
29244
+ },
28092
29245
  principal?.tokenId
28093
29246
  )
28094
29247
  );
@@ -28129,6 +29282,7 @@ function evaluateProjectPolicy(cfg, credential, projectId) {
28129
29282
  const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
28130
29283
  if (!root) throw new Error(`Unknown project "${projectId}".`);
28131
29284
  const policy = effectivePolicy(cfg.dataDir);
29285
+ const governingProfileId = readProjectType(root);
28132
29286
  const selection = readProjectProfileSelection(root);
28133
29287
  return buildEvaluation({
28134
29288
  policy,
@@ -28136,7 +29290,8 @@ function evaluateProjectPolicy(cfg, credential, projectId) {
28136
29290
  selectedProfileIds: selection?.profileIds ?? [],
28137
29291
  hasSelection: !!selection,
28138
29292
  countMissingPacksAsViolation: true,
28139
- requiredDefaultResolution: executeApprovedResolveGlobalPacks(requiredDefaultNames(policy))
29293
+ requiredDefaultResolution: executeApprovedResolveGlobalPacks(requiredDefaultNames(policy)),
29294
+ governingProfileId
28140
29295
  });
28141
29296
  }
28142
29297
  function reconcileProjectPolicy(cfg, credential, projectId) {
@@ -28151,20 +29306,41 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
28151
29306
  const policy = effectivePolicy(cfg.dataDir);
28152
29307
  const selection = readProjectProfileSelection(root);
28153
29308
  const resolution = executeApprovedResolveGlobalPacks(requiredDefaultNames(policy));
28154
- let installed = installedPackNames(cfg, projectId);
28155
- const installedSet = new Set(installed);
29309
+ const installedSet = new Set(installedPackNames(cfg, projectId));
28156
29310
  const toApply = resolution.resolved.filter((p) => !installedSet.has(p.name));
29311
+ const appliedPackNames = toApply.map((p) => p.name);
28157
29312
  if (toApply.length > 0) {
28158
29313
  installResolvedPacks(cfg, projectId, { resolved: toApply, unresolved: [] });
28159
- installed = installedPackNames(cfg, projectId);
29314
+ }
29315
+ const catalog = executeApprovedListProjectProfiles(cfg, projectId);
29316
+ const previousProfileId = readProjectType(root);
29317
+ let governingProfileId = previousProfileId;
29318
+ const requiredProfileIds = policy.requiredProfileIds ?? [];
29319
+ const policyUnsatisfied = requiredProfileIds.length > 0 && !requiredProfileIds.includes(governingProfileId);
29320
+ const governingUnresolvable = !classifyProfile(governingProfileId, catalog).resolvable;
29321
+ let repairedProfileId;
29322
+ let selectedProfileIds = selection?.profileIds ?? [];
29323
+ if (policyUnsatisfied || governingUnresolvable) {
29324
+ const target = firstApplicableProfileId(
29325
+ [...requiredProfileIds, ...selection?.profileIds ?? []],
29326
+ catalog
29327
+ );
29328
+ if (target) {
29329
+ const application = executeApprovedEnsureProfileInstalled(cfg, projectId, target);
29330
+ writeProjectType(root, application.profileId);
29331
+ governingProfileId = application.profileId;
29332
+ repairedProfileId = application.profileId;
29333
+ selectedProfileIds = foldAppliedProfile(root, application.profileId, principalSubject3(principal)).profileIds;
29334
+ }
28160
29335
  }
28161
29336
  const result = buildEvaluation({
28162
29337
  policy,
28163
- presentPackNames: installed,
28164
- selectedProfileIds: selection?.profileIds ?? [],
29338
+ presentPackNames: installedPackNames(cfg, projectId),
29339
+ selectedProfileIds,
28165
29340
  hasSelection: !!selection,
28166
29341
  countMissingPacksAsViolation: true,
28167
- requiredDefaultResolution: resolution
29342
+ requiredDefaultResolution: resolution,
29343
+ governingProfileId
28168
29344
  });
28169
29345
  tryAppendAudit2(
28170
29346
  cfg,
@@ -28173,7 +29349,15 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
28173
29349
  "policy.reconcile",
28174
29350
  "info",
28175
29351
  "policy",
28176
- { target: projectId, projectId },
29352
+ {
29353
+ target: projectId,
29354
+ projectId,
29355
+ metadata: JSON.stringify({
29356
+ ...appliedPackNames.length > 0 ? { appliedPackNames } : {},
29357
+ ...repairedProfileId ? { repairedProfileId, previousProfileId } : {},
29358
+ ...resolution.unresolved.length > 0 ? { unresolvedPacks: resolution.unresolved } : {}
29359
+ })
29360
+ },
28177
29361
  principal.tokenId
28178
29362
  )
28179
29363
  );
@@ -28189,8 +29373,19 @@ function getProjectConfig(cfg, credential, projectId) {
28189
29373
  const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
28190
29374
  if (!root) throw new Error(`Unknown project "${projectId}".`);
28191
29375
  const projectType = readProjectType(root);
29376
+ const selection = readProjectProfileSelection(root);
29377
+ const classified = classifyProfile(projectType, executeApprovedListProjectProfiles(cfg, projectId));
28192
29378
  const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
28193
- return { projectType, locked };
29379
+ const overriding = overridingSubsystemIds(root);
29380
+ const view = {
29381
+ projectType,
29382
+ locked,
29383
+ profileResolvable: classified.resolvable,
29384
+ unappliedProfileIds: unappliedIds(selection?.profileIds ?? [], projectType),
29385
+ overridingSubsystemIds: overriding
29386
+ };
29387
+ if (classified.source) view.profileSource = classified.source;
29388
+ return view;
28194
29389
  }
28195
29390
  function setProjectType(cfg, credential, projectId, projectType) {
28196
29391
  const principal = requirePrincipal4(cfg, credential);
@@ -28201,9 +29396,23 @@ function setProjectType(cfg, credential, projectId, projectType) {
28201
29396
  }
28202
29397
  const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
28203
29398
  if (!root) throw new Error(`Unknown project "${projectId}".`);
28204
- writeProjectType(root, projectType);
29399
+ const application = executeApprovedEnsureProfileInstalled(cfg, projectId, projectType);
29400
+ writeProjectType(root, application.profileId);
29401
+ const folded = foldAppliedProfile(root, application.profileId, principalSubject3(principal));
29402
+ const remainder = folded.profileIds.slice(1);
28205
29403
  const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
28206
- return { projectType, locked };
29404
+ const overriding = overridingSubsystemIds(root);
29405
+ const view = {
29406
+ projectType: application.profileId,
29407
+ locked,
29408
+ profileSource: application.source,
29409
+ // The write path guarantees resolvability — the ensure seam refused anything else.
29410
+ profileResolvable: true,
29411
+ unappliedProfileIds: remainder,
29412
+ overridingSubsystemIds: overriding
29413
+ };
29414
+ if (application.adoptedPackName) view.adoptedPackName = application.adoptedPackName;
29415
+ return view;
28207
29416
  }
28208
29417
  function getPackPolicy(cfg, credential) {
28209
29418
  requirePrincipal4(cfg, credential);
@@ -28373,11 +29582,8 @@ function mintToken(cfg, credential, request) {
28373
29582
  }
28374
29583
  assertNotReservedSubjectId(cfg, [request.ownerUserId]);
28375
29584
  const projects = request.projects?.length ? request.projects : ["*"];
28376
- const knownProjects = new Set(listProjectRecords(cfg.dataDir).map((p) => p.id));
28377
29585
  for (const p of projects) {
28378
- if (p !== "*" && !knownProjects.has(p)) {
28379
- throw new Error(`unknown project "${p}"`);
28380
- }
29586
+ assertMintableNarrowingEntry(cfg.dataDir, p);
28381
29587
  }
28382
29588
  const owner = findUserByRecordOrSubjectId(cfg.dataDir, request.ownerUserId);
28383
29589
  if (owner && owner.status !== "active") {
@@ -28414,12 +29620,21 @@ function revokeToken(cfg, credential, tokenId) {
28414
29620
  }
28415
29621
  function mintSelfToken(cfg, credential, projectId, write) {
28416
29622
  const principal = requirePrincipal5(cfg, credential);
28417
- if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY2, "project", projectId).value !== "yes") {
29623
+ const parsed = parseQualifiedSelector(projectId);
29624
+ if (!parsed) {
29625
+ throw new Error(
29626
+ `invalid project id "${projectId}" (expected a project id, optionally subproject-qualified as projectId::subsystemId)`
29627
+ );
29628
+ }
29629
+ if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY2, "project", parsed.projectId).value !== "yes") {
28418
29630
  throw new ForbiddenError("caller lacks project:read on the requested project");
28419
29631
  }
28420
- if (write && authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY2, "project", projectId).value !== "yes") {
29632
+ if (write && authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY2, "project", parsed.projectId).value !== "yes") {
28421
29633
  throw new ForbiddenError("caller lacks project:write on the requested project");
28422
29634
  }
29635
+ if (parsed.mounts.length > 0) {
29636
+ assertMintableNarrowingEntry(cfg.dataDir, projectId);
29637
+ }
28423
29638
  const token = "wk_" + crypto10.randomBytes(24).toString("hex");
28424
29639
  const owner = auditActor(principal);
28425
29640
  const record2 = {
@@ -29031,12 +30246,12 @@ function resolveVisibility(observerProjectId, units, placements) {
29031
30246
  const best = /* @__PURE__ */ new Map();
29032
30247
  for (const placement of placements) {
29033
30248
  if (placement.projectId === observerProjectId) continue;
29034
- const path62 = chainOf(placement.unitId, unitById);
29035
- if (!path62.length) continue;
29036
- const closedOk = path62.every((u) => effectivePosture(u, unitById) !== "closed" || observerInside(u.id) || grantedTo(u));
30249
+ const path61 = chainOf(placement.unitId, unitById);
30250
+ if (!path61.length) continue;
30251
+ const closedOk = path61.every((u) => effectivePosture(u, unitById) !== "closed" || observerInside(u.id) || grantedTo(u));
29037
30252
  if (!closedOk) continue;
29038
- const crossTenant = !tenantRoots.has(path62[path62.length - 1].id);
29039
- if (crossTenant && !path62.some(grantedTo)) continue;
30253
+ const crossTenant = !tenantRoots.has(path61[path61.length - 1].id);
30254
+ if (crossTenant && !path61.some(grantedTo)) continue;
29040
30255
  if (crossTenant && directUnits.length === 0) continue;
29041
30256
  const distance = crossTenant ? "partner" : sameBranch(placement.unitId) ? "department" : "instance";
29042
30257
  const existing = best.get(placement.projectId);
@@ -29059,6 +30274,17 @@ function audienceDistance(resolution, targetProjectId) {
29059
30274
 
29060
30275
  // src/server/landscape.ts
29061
30276
  var yamlLib = __toESM(require("js-yaml"));
30277
+ init_filenames();
30278
+
30279
+ // src/server/openapiindex.ts
30280
+ function buildOpenApiSpecIndex(specs) {
30281
+ return { openapiIndex: true, specs: specs.map((s) => ({ portalId: s.portalId, name: s.name })) };
30282
+ }
30283
+ function openApiIndexDocument(specs) {
30284
+ return JSON.stringify(buildOpenApiSpecIndex(specs), null, 2);
30285
+ }
30286
+
30287
+ // src/server/landscape.ts
29062
30288
  var PROJECT_ADMIN_CAPABILITY3 = "project:admin";
29063
30289
  var PROJECT_READ_CAPABILITY3 = "project:read";
29064
30290
  var PROJECT_WRITE_CAPABILITY3 = "project:write";
@@ -29545,7 +30771,7 @@ function getProjectSurfaceForMcp(cfg, credential, currentProjectId, targetProjec
29545
30771
  const result = runWithProjectRoot(record2.rootPath, () => hostSurfaces.exportBoundSurface(maxAudience, "native"));
29546
30772
  return { ...result.snapshot, origin: "exchanged" };
29547
30773
  }
29548
- function exportProjectSurface(cfg, credential, projectId, format, maxAudience) {
30774
+ function exportProjectSurface(cfg, credential, projectId, format, maxAudience, portalId) {
29549
30775
  const principal = requirePrincipal6(cfg, credential);
29550
30776
  if (!permitsCap(cfg, principal, PROJECT_ADMIN_CAPABILITY3, "project", projectId)) {
29551
30777
  throw new ForbiddenError(
@@ -29559,16 +30785,37 @@ function exportProjectSurface(cfg, credential, projectId, format, maxAudience) {
29559
30785
  if (!root) throw new Error(`Unknown project "${projectId}".`);
29560
30786
  const result = runWithProjectRoot(root, () => hostSurfaces.exportBoundSurface(maxAudience, format));
29561
30787
  if (format === "openapi") {
30788
+ const specs = result.renderedSet ?? [];
30789
+ if (portalId) {
30790
+ const hit = specs.find((s) => s.portalId === portalId);
30791
+ if (!hit) {
30792
+ throw new Error(
30793
+ `Unknown portal "${portalId}" in project "${projectId}" (published: ${specs.map((s) => s.portalId).join(", ") || "none"}).`
30794
+ );
30795
+ }
30796
+ return {
30797
+ body: hit.document,
30798
+ contentType: "application/json",
30799
+ filename: `${safeFilenamePart(projectId)}-${safeFilenamePart(portalId)}-surface.openapi.json`
30800
+ };
30801
+ }
30802
+ if (specs.length === 1) {
30803
+ return {
30804
+ body: specs[0].document,
30805
+ contentType: "application/json",
30806
+ filename: `${safeFilenamePart(projectId)}-surface.openapi.json`
30807
+ };
30808
+ }
29562
30809
  return {
29563
- body: result.rendered ?? "{}",
30810
+ body: openApiIndexDocument(specs),
29564
30811
  contentType: "application/json",
29565
- filename: `${projectId}-surface.openapi.json`
30812
+ filename: `${safeFilenamePart(projectId)}-surface.openapi.index.json`
29566
30813
  };
29567
30814
  }
29568
30815
  return {
29569
30816
  body: yamlLib.dump(result.snapshot),
29570
30817
  contentType: "application/yaml",
29571
- filename: `${projectId}-surface.yaml`
30818
+ filename: `${safeFilenamePart(projectId)}-surface.yaml`
29572
30819
  };
29573
30820
  }
29574
30821
  function removeRelation(cfg, credential, id) {
@@ -29612,7 +30859,8 @@ function handleLandscapeRequest(cfg, credential, req, res, body, url) {
29612
30859
  credential,
29613
30860
  parts[2],
29614
30861
  url.searchParams.get("format") ?? "native",
29615
- url.searchParams.get("audience") ?? "instance"
30862
+ url.searchParams.get("audience") ?? "instance",
30863
+ url.searchParams.get("spec") ?? void 0
29616
30864
  );
29617
30865
  res.writeHead(200, {
29618
30866
  "content-type": artifact.contentType,
@@ -29942,10 +31190,9 @@ function migratePermissionModel(dataDir, apply) {
29942
31190
  // src/server/http.ts
29943
31191
  var http2 = __toESM(require("http"));
29944
31192
  var fs49 = __toESM(require("fs"));
29945
- var path59 = __toESM(require("path"));
31193
+ var path58 = __toESM(require("path"));
29946
31194
 
29947
31195
  // src/server/request.ts
29948
- var path58 = __toESM(require("path"));
29949
31196
  var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
29950
31197
  init_fs();
29951
31198
 
@@ -30318,38 +31565,58 @@ function initializeProject(cfg, credential, request) {
30318
31565
  }
30319
31566
  }
30320
31567
  }
30321
- function lockProject2(cfg, credential, projectId) {
31568
+ function lockProject2(cfg, credential, projectId, subproject) {
30322
31569
  return lifecycleAction(cfg, credential, projectId, {
30323
31570
  action: "project:lock",
30324
31571
  verb: "Lock",
30325
31572
  noun: "lock",
31573
+ subproject,
30326
31574
  execute: () => {
30327
- const lock = executeApprovedLock(cfg, projectId);
31575
+ const lock = executeApprovedLock(cfg, projectId, subproject);
30328
31576
  return {
30329
31577
  status: "completed",
30330
31578
  action: "project:lock",
30331
- summary: `Locked project "${projectId}" (status: ${lock.status}).`,
31579
+ summary: `Locked project "${projectId}"${subprojectSuffix(subproject)} (status: ${lock.status}).`,
30332
31580
  lock
30333
31581
  };
30334
31582
  }
30335
31583
  });
30336
31584
  }
30337
- function promoteProject2(cfg, credential, projectId) {
31585
+ function promoteProject2(cfg, credential, projectId, subproject) {
30338
31586
  return lifecycleAction(cfg, credential, projectId, {
30339
31587
  action: "project:promote",
30340
31588
  verb: "Promote",
30341
31589
  noun: "promotion",
31590
+ subproject,
30342
31591
  execute: () => {
30343
- const promo = executeApprovedPromote(cfg, projectId);
31592
+ const promo = executeApprovedPromote(cfg, projectId, subproject);
30344
31593
  return {
30345
31594
  status: "completed",
30346
31595
  action: "project:promote",
30347
- summary: `Promotion of project "${projectId}": ${promo.status} \u2014 ${promo.message}`,
31596
+ summary: `Promotion of project "${projectId}"${subprojectSuffix(subproject)}: ${promo.status} \u2014 ${promo.message}`,
30348
31597
  promote: promo
30349
31598
  };
30350
31599
  }
30351
31600
  });
30352
31601
  }
31602
+ function subprojectSuffix(subproject) {
31603
+ return subproject ? ` subproject "${subproject}"` : "";
31604
+ }
31605
+ var SUBPROJECT_SCOPE_PAYLOAD = "SubprojectScope";
31606
+ function subprojectScopePayload(subproject) {
31607
+ if (!subproject) return {};
31608
+ return { payloadType: SUBPROJECT_SCOPE_PAYLOAD, payload: JSON.stringify({ subproject }) };
31609
+ }
31610
+ function readSubprojectScope(req) {
31611
+ if (req.payloadType !== SUBPROJECT_SCOPE_PAYLOAD || !req.payload) return void 0;
31612
+ const parsed = JSON.parse(req.payload);
31613
+ if (typeof parsed.subproject !== "string" || !parsed.subproject) {
31614
+ throw new Error(
31615
+ `Approved ${req.kind} request "${req.id}" carries a ${SUBPROJECT_SCOPE_PAYLOAD} payload with no usable subproject qualifier \u2014 refusing to execute, because falling back to the whole project would widen the scope the requester was confined to.`
31616
+ );
31617
+ }
31618
+ return parsed.subproject;
31619
+ }
30353
31620
  function lifecycleAction(cfg, credential, projectId, opts) {
30354
31621
  const principal = requirePrincipal7(cfg, credential);
30355
31622
  const effective = authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY4, "project", projectId);
@@ -30375,8 +31642,9 @@ function lifecycleAction(cfg, credential, projectId, opts) {
30375
31642
  const pending = buildPendingRequest(
30376
31643
  principal,
30377
31644
  opts.action,
30378
- `${opts.verb} project ${projectId}`,
30379
- projectId
31645
+ `${opts.verb} project ${projectId}${subprojectSuffix(opts.subproject)}`,
31646
+ projectId,
31647
+ subprojectScopePayload(opts.subproject)
30380
31648
  );
30381
31649
  return createPendingOutcome(cfg, principal, pending, opts.action);
30382
31650
  }
@@ -30450,12 +31718,14 @@ function executeApproved(cfg, req) {
30450
31718
  return `Initialized project "${rec.id}" under the active pack policy.`;
30451
31719
  }
30452
31720
  case "project:lock": {
30453
- const lock = executeApprovedLock(cfg, req.projectId ?? "");
30454
- return `Locked project "${req.projectId}" (status: ${lock.status}).`;
31721
+ const scope = readSubprojectScope(req);
31722
+ const lock = executeApprovedLock(cfg, req.projectId ?? "", scope);
31723
+ return `Locked project "${req.projectId}"${subprojectSuffix(scope)} (status: ${lock.status}).`;
30455
31724
  }
30456
31725
  case "project:promote": {
30457
- const promo = executeApprovedPromote(cfg, req.projectId ?? "");
30458
- return `Promotion of project "${req.projectId}": ${promo.status} \u2014 ${promo.message}`;
31726
+ const scope = readSubprojectScope(req);
31727
+ const promo = executeApprovedPromote(cfg, req.projectId ?? "", scope);
31728
+ return `Promotion of project "${req.projectId}"${subprojectSuffix(scope)}: ${promo.status} \u2014 ${promo.message}`;
30459
31729
  }
30460
31730
  default:
30461
31731
  throw new Error(`Unsupported approval kind "${req.kind}".`);
@@ -31159,6 +32429,9 @@ function getProjectConfig2(cfg, credential, projectId) {
31159
32429
  function setProjectType2(cfg, credential, projectId, projectType) {
31160
32430
  return setProjectType(cfg, credential, projectId, projectType);
31161
32431
  }
32432
+ function listProjectProfiles2(cfg, credential, project2) {
32433
+ return listProjectProfiles(cfg, credential, project2);
32434
+ }
31162
32435
  function listProducers2(cfg, credential, project2) {
31163
32436
  return listProducers(cfg, credential, project2);
31164
32437
  }
@@ -31687,6 +32960,19 @@ var ShareSnapshotRegistry = class {
31687
32960
  return stored;
31688
32961
  }
31689
32962
  };
32963
+ function selectCapturedOpenApi(snap, portalId) {
32964
+ const specs = snap.openapiSet;
32965
+ if (!specs || specs.length === 0) {
32966
+ if (portalId) return null;
32967
+ return snap.openapi ?? null;
32968
+ }
32969
+ if (portalId) {
32970
+ const hit = specs.find((s) => s.portalId === portalId);
32971
+ return hit ? hit.document : null;
32972
+ }
32973
+ if (specs.length === 1) return specs[0].document;
32974
+ return openApiIndexDocument(specs);
32975
+ }
31690
32976
  var ShareSnapshotIndex = class {
31691
32977
  constructor(store) {
31692
32978
  this.store = store;
@@ -31694,12 +32980,12 @@ var ShareSnapshotIndex = class {
31694
32980
  get(snapshotId) {
31695
32981
  return this.store.read(snapshotId);
31696
32982
  }
31697
- getArtifact(snapshotId, kind) {
32983
+ getArtifact(snapshotId, kind, portalId) {
31698
32984
  const snap = this.store.read(snapshotId);
31699
32985
  if (!snap) return null;
31700
32986
  if (kind === "canvas") return snap.canvasModel ?? null;
31701
32987
  if (kind === "html") return snap.html ?? null;
31702
- if (kind === "openapi") return snap.openapi ?? null;
32988
+ if (kind === "openapi") return selectCapturedOpenApi(snap, portalId);
31703
32989
  return null;
31704
32990
  }
31705
32991
  };
@@ -31709,8 +32995,8 @@ function putSnapshot(dataDir, snapshot) {
31709
32995
  function getSnapshot2(dataDir, snapshotId) {
31710
32996
  return new ShareSnapshotIndex(new ShareSnapshotStore(dataDir)).get(snapshotId);
31711
32997
  }
31712
- function getSnapshotArtifact(dataDir, snapshotId, kind) {
31713
- return new ShareSnapshotIndex(new ShareSnapshotStore(dataDir)).getArtifact(snapshotId, kind);
32998
+ function getSnapshotArtifact(dataDir, snapshotId, kind, portalId) {
32999
+ return new ShareSnapshotIndex(new ShareSnapshotStore(dataDir)).getArtifact(snapshotId, kind, portalId);
31714
33000
  }
31715
33001
  function captureSnapshot(dataDir, principal, projectId, view, artifacts) {
31716
33002
  const root = resolveProjectRoot(dataDir, principal, projectId);
@@ -31729,7 +33015,11 @@ function captureSnapshot(dataDir, principal, projectId, view, artifacts) {
31729
33015
  }
31730
33016
  if (artifacts.includes("openapi")) {
31731
33017
  const result = hostSurfaces.exportBoundSurface("project", "openapi");
31732
- snapshot.openapi = result.rendered ?? "{}";
33018
+ const specs = result.renderedSet ?? [];
33019
+ if (specs.length) {
33020
+ snapshot.openapiSet = specs;
33021
+ if (specs.length === 1) snapshot.openapi = specs[0].document;
33022
+ }
31733
33023
  }
31734
33024
  return snapshot;
31735
33025
  });
@@ -32290,10 +33580,10 @@ function getWebProjectOpenApi(cfg, sessionId, projectId, portalId) {
32290
33580
  if (!root) throw new ForbiddenError("project not authorized or unknown");
32291
33581
  const result = runWithProjectRoot(root, () => hostSurfaces.exportBoundSurface("project", "openapi"));
32292
33582
  const specs = result.renderedSet ?? (result.rendered ? [{ portalId: "", name: projectId, document: result.rendered }] : []);
32293
- if (specs.length === 0) return swaggerUiPage("{}", projectId);
33583
+ if (specs.length === 0) return openApiIndexPage(projectId, []);
32294
33584
  if (portalId) {
32295
- const sel = specs.find((s) => s.portalId === portalId) ?? specs[0];
32296
- return swaggerUiPage(sel.document, sel.name);
33585
+ const sel = specs.find((s) => s.portalId === portalId);
33586
+ return sel ? swaggerUiPage(sel.document, sel.name) : openApiIndexPage(projectId, specs);
32297
33587
  }
32298
33588
  if (specs.length === 1) return swaggerUiPage(specs[0].document, specs[0].name);
32299
33589
  return openApiIndexPage(projectId, specs);
@@ -32301,7 +33591,7 @@ function getWebProjectOpenApi(cfg, sessionId, projectId, portalId) {
32301
33591
  function openApiIndexPage(projectId, specs) {
32302
33592
  const esc2 = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
32303
33593
  const items = specs.map((s) => `<li><a href="/web/openapi?projectId=${encodeURIComponent(projectId)}&spec=${encodeURIComponent(s.portalId)}">${esc2(s.name)}</a> <code>${esc2(s.portalId)}</code></li>`).join("");
32304
- return `<!doctype html><html><head><meta charset="utf-8"><title>${esc2(projectId)} \u2014 API specs</title><style>body{font-family:system-ui,sans-serif;max-width:680px;margin:48px auto;padding:0 20px;color:#e6e6e6;background:#161616}h1{font-size:20px}a{color:#6ea8fe;text-decoration:none}a:hover{text-decoration:underline}li{margin:10px 0}code{color:#8a94a6;font-size:12px;margin-left:8px}</style></head><body><h1>${esc2(projectId)} \u2014 API specs</h1><p>This project exposes ${specs.length} separate public APIs, each with its own OpenAPI document and auth:</p><ul>${items}</ul></body></html>`;
33594
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${esc2(projectId)} \u2014 API specs</title><style>body{font-family:system-ui,sans-serif;max-width:680px;margin:48px auto;padding:0 20px;color:#e6e6e6;background:#161616}h1{font-size:20px}a{color:#6ea8fe;text-decoration:none}a:hover{text-decoration:underline}li{margin:10px 0}code{color:#8a94a6;font-size:12px;margin-left:8px}</style></head><body><h1>${esc2(projectId)} \u2014 API specs</h1>` + (specs.length ? `<p>This project exposes ${specs.length} separate public APIs, each with its own OpenAPI document and auth:</p><ul>${items}</ul>` : `<p>This project publishes no HTTP API \u2014 no public portal exposes one.</p>`) + `</body></html>`;
32305
33595
  }
32306
33596
  function reshapeLandscapeGraph(model, level) {
32307
33597
  const unitNodeIds = new Set(model.nodes.filter((n) => n.nodeKind === "orgUnit").map((n) => n.id));
@@ -32335,7 +33625,31 @@ function reshapeLandscapeGraph(model, level) {
32335
33625
  }
32336
33626
  const kept = nodes.filter((n) => n.level <= level);
32337
33627
  const keptIds = new Set(kept.map((n) => n.id));
32338
- const edges = model.edges.filter((e) => keptIds.has(e.from) && keptIds.has(e.to));
33628
+ const projectNodeIdByProject = /* @__PURE__ */ new Map();
33629
+ const interfaceOwnerProject = /* @__PURE__ */ new Map();
33630
+ for (const n of model.nodes) {
33631
+ if (n.projectId === void 0) continue;
33632
+ if (n.nodeKind === "project") projectNodeIdByProject.set(n.projectId, n.id);
33633
+ else if (n.nodeKind === "publicInterface") interfaceOwnerProject.set(n.id, n.projectId);
33634
+ }
33635
+ const retarget = (endpoint) => {
33636
+ if (keptIds.has(endpoint)) return endpoint;
33637
+ const owner = interfaceOwnerProject.get(endpoint);
33638
+ const projectNode = owner === void 0 ? void 0 : projectNodeIdByProject.get(owner);
33639
+ return projectNode !== void 0 && keptIds.has(projectNode) ? projectNode : void 0;
33640
+ };
33641
+ const edges = [];
33642
+ for (const e of model.edges) {
33643
+ if (keptIds.has(e.from) && keptIds.has(e.to)) {
33644
+ edges.push(e);
33645
+ continue;
33646
+ }
33647
+ if (e.relationId === void 0) continue;
33648
+ const from = retarget(e.from);
33649
+ const to = retarget(e.to);
33650
+ if (from === void 0 || to === void 0 || from === to) continue;
33651
+ edges.push({ ...e, from, to });
33652
+ }
32339
33653
  return {
32340
33654
  tier: "landscape",
32341
33655
  nodes: kept,
@@ -34479,6 +35793,9 @@ function opsGetProjectConfig(cfg, sessionId, url, res) {
34479
35793
  function opsSetProjectConfig(cfg, sessionId, body, res) {
34480
35794
  sendJson(res, 200, setProjectType2(cfg, sessionId, String(body?.projectId ?? ""), String(body?.projectType ?? "")));
34481
35795
  }
35796
+ function opsListProjectProfiles(cfg, sessionId, url, res) {
35797
+ sendJson(res, 200, { profiles: listProjectProfiles2(cfg, sessionId, q(url, "projectId") ?? "") });
35798
+ }
34482
35799
  function opsListProducers(cfg, sessionId, url, res) {
34483
35800
  sendJson(res, 200, { producers: listProducers2(cfg, sessionId, q(url, "projectId") ?? "") });
34484
35801
  }
@@ -34718,6 +36035,9 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
34718
36035
  if (req.method === "POST" && parts.length === 3 && parts[2] === "config") {
34719
36036
  return opsSetProjectConfig(cfg, sessionId, body, res);
34720
36037
  }
36038
+ if (req.method === "GET" && parts.length === 3 && parts[2] === "profiles") {
36039
+ return opsListProjectProfiles(cfg, sessionId, url, res);
36040
+ }
34721
36041
  if (req.method === "GET" && parts.length === 3 && parts[2] === "policy") {
34722
36042
  return opsPolicyEvaluate(cfg, sessionId, url, res);
34723
36043
  }
@@ -34942,8 +36262,8 @@ var RealtimeHub = class {
34942
36262
  * complete the handshake, and register the connection. A bad path or session
34943
36263
  * destroys the socket. */
34944
36264
  handleUpgrade(cfg, req, socket) {
34945
- const path62 = (req.url ?? "/").split("?")[0];
34946
- if (path62 !== REALTIME_PATH || !isWebSocketUpgrade(req)) {
36265
+ const path61 = (req.url ?? "/").split("?")[0];
36266
+ if (path61 !== REALTIME_PATH || !isWebSocketUpgrade(req)) {
34947
36267
  socket.destroy();
34948
36268
  return;
34949
36269
  }
@@ -35088,7 +36408,7 @@ function deriveMcpOutcome(response) {
35088
36408
  if (r.result && typeof r.result === "object" && r.result.isError) return "failed";
35089
36409
  return "success";
35090
36410
  }
35091
- function auditToolCall(dataDir, principal, projectId, body, outcome) {
36411
+ function auditToolCall(dataDir, principal, projectId, body, outcome, subproject) {
35092
36412
  const target = mcpToolTarget(body);
35093
36413
  if (!target) return;
35094
36414
  const actor = principal.subject ?? {
@@ -35106,7 +36426,8 @@ function auditToolCall(dataDir, principal, projectId, body, outcome) {
35106
36426
  actor,
35107
36427
  tokenId: principal.tokenId,
35108
36428
  projectId,
35109
- target
36429
+ target,
36430
+ ...subproject ? { metadata: JSON.stringify({ subproject }) } : {}
35110
36431
  };
35111
36432
  try {
35112
36433
  appendAuditEvent(dataDir, event, DEFAULT_AUDIT_POLICY);
@@ -35137,6 +36458,12 @@ var PROJECT_OPS_TOOLS = /* @__PURE__ */ new Set([
35137
36458
  "sdd_host_produce",
35138
36459
  "sdd_host_commit_project"
35139
36460
  ]);
36461
+ var PROJECT_RECORD_TOOLS = /* @__PURE__ */ new Set([
36462
+ "sdd_host_initialize_project",
36463
+ "sdd_host_get_approval_status",
36464
+ "sdd_host_await_approval",
36465
+ ...PROJECT_OPS_TOOLS
36466
+ ]);
35140
36467
  function jsonRpcRequests(body) {
35141
36468
  const arr = Array.isArray(body) ? body : [body];
35142
36469
  return arr.filter((m) => !!m && typeof m === "object" && "method" in m);
@@ -35202,7 +36529,27 @@ function dataPlanePermissionError(cfg, principal, projectId, body) {
35202
36529
  }
35203
36530
  };
35204
36531
  }
35205
- async function dispatchProjectLifecycleTool(cfg, credential, projectId, body) {
36532
+ function subprojectConfinementError(projectId, subproject, body) {
36533
+ if (!subproject) return void 0;
36534
+ const msg = jsonRpcRequest(body);
36535
+ if (!msg || msg.method !== "tools/call") return void 0;
36536
+ const name = msg.params?.name;
36537
+ if (typeof name !== "string" || !PROJECT_RECORD_TOOLS.has(name)) return void 0;
36538
+ return {
36539
+ jsonrpc: "2.0",
36540
+ id: msg.id ?? null,
36541
+ result: {
36542
+ content: [
36543
+ {
36544
+ type: "text",
36545
+ text: `Refused \u2014 ${name} acts on the whole project "${projectId}", but this credential is bound to subproject "${projectId}${SUBPROJECT_SEPARATOR}${subproject}". An unqualified credential for "${projectId}" is required to call ${name}.`
36546
+ }
36547
+ ],
36548
+ isError: true
36549
+ }
36550
+ };
36551
+ }
36552
+ async function dispatchProjectLifecycleTool(cfg, credential, projectId, body, subproject) {
35206
36553
  const msg = jsonRpcRequest(body);
35207
36554
  if (!msg || msg.method !== "tools/call") return void 0;
35208
36555
  const name = msg.params?.name;
@@ -35219,10 +36566,10 @@ async function dispatchProjectLifecycleTool(cfg, credential, projectId, body) {
35219
36566
  value = initializeProject(cfg, credential, args);
35220
36567
  break;
35221
36568
  case "sdd_host_lock_project":
35222
- value = lockProject2(cfg, credential, projectId);
36569
+ value = lockProject2(cfg, credential, projectId, subproject);
35223
36570
  break;
35224
36571
  case "sdd_host_promote_project":
35225
- value = promoteProject2(cfg, credential, projectId);
36572
+ value = promoteProject2(cfg, credential, projectId, subproject);
35226
36573
  break;
35227
36574
  case "sdd_host_await_approval":
35228
36575
  value = await awaitApproval(
@@ -35310,8 +36657,8 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
35310
36657
  permissionSubject: { subjectId: "anonymous", roleBindings: [], instanceAdmin: true }
35311
36658
  };
35312
36659
  }
35313
- const root = resolveProjectRoot(cfg.dataDir, principal, projectSelector(req));
35314
- if (!root) {
36660
+ const binding = resolveProjectBinding(cfg.dataDir, principal, projectSelector(req));
36661
+ if (!binding) {
35315
36662
  sendJson(res, 403, { error: "project not authorized, unknown, or not specified" });
35316
36663
  return;
35317
36664
  }
@@ -35326,19 +36673,26 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
35326
36673
  });
35327
36674
  return;
35328
36675
  }
35329
- await runWithProjectRoot(root, async () => {
35330
- const projectId = path58.basename(root);
35331
- const dispatchedResponse = await dispatchProjectLifecycleTool(cfg, cred, projectId, body);
36676
+ await runWithProjectRoot(binding.rootPath, async () => {
36677
+ const projectId = binding.projectId;
36678
+ const subproject = binding.subproject;
36679
+ const confinementError = subprojectConfinementError(projectId, subproject, body);
36680
+ if (confinementError !== void 0) {
36681
+ sendJson(res, 200, confinementError);
36682
+ auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(confinementError), subproject);
36683
+ return;
36684
+ }
36685
+ const dispatchedResponse = await dispatchProjectLifecycleTool(cfg, cred, projectId, body, subproject);
35332
36686
  if (dispatchedResponse !== void 0) {
35333
36687
  sendJson(res, 200, dispatchedResponse);
35334
- auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(dispatchedResponse));
36688
+ auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(dispatchedResponse), subproject);
35335
36689
  for (const ch of mcpChangeChannels(body, projectId, dispatchedResponse)) publishChange(ch);
35336
36690
  return;
35337
36691
  }
35338
36692
  const permissionError = dataPlanePermissionError(cfg, principal, projectId, body);
35339
36693
  if (permissionError !== void 0) {
35340
36694
  sendJson(res, 200, permissionError);
35341
- auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(permissionError));
36695
+ auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(permissionError), subproject);
35342
36696
  return;
35343
36697
  }
35344
36698
  const server = createScopedServer();
@@ -35359,7 +36713,7 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
35359
36713
  };
35360
36714
  await server.connect(transport);
35361
36715
  await transport.handleRequest(req, res, body);
35362
- auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(response));
36716
+ auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(response), subproject);
35363
36717
  for (const ch of mcpChangeChannels(body, projectId, response)) publishChange(ch);
35364
36718
  });
35365
36719
  }
@@ -35427,7 +36781,7 @@ var CONTENT_TYPE = {
35427
36781
  openapi: "application/json",
35428
36782
  canvas: "application/json"
35429
36783
  };
35430
- function downloadArtifact(cfg, token, kind, meta) {
36784
+ function downloadArtifact(cfg, token, kind, meta, portalId) {
35431
36785
  const link = linkByTokenHash(cfg.dataDir, hashToken(token));
35432
36786
  const check = usable(link);
35433
36787
  if ("outcome" in check) {
@@ -35439,7 +36793,7 @@ function downloadArtifact(cfg, token, kind, meta) {
35439
36793
  record(cfg.dataDir, check.link.id, meta, "denied-download");
35440
36794
  return { found: false, outcome: "denied-download" };
35441
36795
  }
35442
- const content = getSnapshotArtifact(cfg.dataDir, check.link.snapshotId, kind);
36796
+ const content = getSnapshotArtifact(cfg.dataDir, check.link.snapshotId, kind, portalId);
35443
36797
  if (content === null) {
35444
36798
  record(cfg.dataDir, check.link.id, meta, "not-found");
35445
36799
  return { found: false, outcome: "not-found" };
@@ -35449,6 +36803,35 @@ function downloadArtifact(cfg, token, kind, meta) {
35449
36803
  }
35450
36804
 
35451
36805
  // src/server/sharehttp.ts
36806
+ init_filenames();
36807
+ function specParam(req) {
36808
+ const url = req.url ?? "";
36809
+ const q2 = url.indexOf("?");
36810
+ if (q2 < 0) return void 0;
36811
+ return new URLSearchParams(url.slice(q2 + 1)).get("spec") || void 0;
36812
+ }
36813
+ function asOpenApiIndex(payload) {
36814
+ try {
36815
+ const parsed = JSON.parse(payload);
36816
+ return parsed && parsed.openapiIndex === true && Array.isArray(parsed.specs) ? parsed : null;
36817
+ } catch {
36818
+ return null;
36819
+ }
36820
+ }
36821
+ function escapeHtml2(s) {
36822
+ return s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
36823
+ }
36824
+ function sharedOpenApiIndexPage(index) {
36825
+ const items = index.specs.map(
36826
+ (s) => `<li><a href="?spec=${encodeURIComponent(s.portalId)}">${escapeHtml2(s.name)}</a><code>${escapeHtml2(s.portalId)}</code></li>`
36827
+ ).join("");
36828
+ return `<!doctype html><html><head><meta charset="utf-8"><title>Shared APIs</title><meta name="viewport" content="width=device-width, initial-scale=1"><style>body{font:15px/1.6 system-ui,sans-serif;max-width:680px;margin:48px auto;padding:0 20px;background:#0b1120;color:#e8e8f0}h1{font-size:20px}a{color:#6ea8fe;text-decoration:none}a:hover{text-decoration:underline}li{margin:10px 0}code{color:#9a9aab;font-size:12px;margin-left:8px}p{color:#9a9aab}</style></head><body><h1>Shared APIs</h1><p>This share exposes ${index.specs.length} separate APIs, each with its own OpenAPI document and auth:</p><ul>${items}</ul></body></html>`;
36829
+ }
36830
+ function downloadFilename(kind, portalId, payload) {
36831
+ if (kind !== "openapi") return `shared-canvas.${safeFilenamePart(kind)}`;
36832
+ if (portalId) return `shared-canvas.${safeFilenamePart(portalId)}.openapi.json`;
36833
+ return asOpenApiIndex(payload) ? "shared-canvas.openapi.index.json" : "shared-canvas.openapi.json";
36834
+ }
35452
36835
  function shareRequestMeta(req) {
35453
36836
  const fwd = req.headers["x-forwarded-for"];
35454
36837
  const ip = (Array.isArray(fwd) ? fwd[0] : fwd)?.split(",")[0].trim() || req.socket?.remoteAddress || "unknown";
@@ -35500,14 +36883,16 @@ function serveSharedModel(cfg, token, req, res) {
35500
36883
  );
35501
36884
  }
35502
36885
  function serveSharedOpenApi(cfg, token, req, res) {
35503
- const result = downloadArtifact(cfg, token, "openapi", shareRequestMeta(req));
36886
+ const result = downloadArtifact(cfg, token, "openapi", shareRequestMeta(req), specParam(req));
35504
36887
  if (!result.found || result.content === void 0) return notFoundPage(res);
36888
+ const index = asOpenApiIndex(result.content);
35505
36889
  harden(res, void 0, "text/html; charset=utf-8");
35506
36890
  res.statusCode = 200;
35507
- res.end(swaggerUiPage(result.content, "Shared API"));
36891
+ res.end(index ? sharedOpenApiIndexPage(index) : swaggerUiPage(result.content, "Shared API"));
35508
36892
  }
35509
36893
  function serveSharedDownload(cfg, token, kind, req, res) {
35510
- const result = downloadArtifact(cfg, token, kind, shareRequestMeta(req));
36894
+ const portalId = specParam(req);
36895
+ const result = downloadArtifact(cfg, token, kind, shareRequestMeta(req), portalId);
35511
36896
  if (!result.found || result.content === void 0) {
35512
36897
  if (result.outcome === "denied-download") {
35513
36898
  harden(res, void 0, "text/plain");
@@ -35517,9 +36902,11 @@ function serveSharedDownload(cfg, token, kind, req, res) {
35517
36902
  }
35518
36903
  return notFoundPage(res);
35519
36904
  }
35520
- const ext = kind === "html" ? "html" : kind === "openapi" ? "openapi.json" : kind;
35521
36905
  harden(res, void 0, result.contentType ?? "application/octet-stream");
35522
- res.setHeader("content-disposition", `attachment; filename="shared-canvas.${ext}"`);
36906
+ res.setHeader(
36907
+ "content-disposition",
36908
+ `attachment; filename="${downloadFilename(kind, portalId, result.content)}"`
36909
+ );
35523
36910
  res.statusCode = 200;
35524
36911
  res.end(result.content);
35525
36912
  }
@@ -35761,7 +37148,7 @@ function routeData(cfg, req, res) {
35761
37148
  }
35762
37149
  function readExposurePolicyFile(dataDir) {
35763
37150
  try {
35764
- const raw = fs49.readFileSync(path59.join(dataDir, "exposure-policy.json"), "utf8");
37151
+ const raw = fs49.readFileSync(path58.join(dataDir, "exposure-policy.json"), "utf8");
35765
37152
  const parsed = JSON.parse(raw);
35766
37153
  return parsed && typeof parsed === "object" ? parsed : void 0;
35767
37154
  } catch {
@@ -36446,9 +37833,9 @@ function seedDemoTree() {
36446
37833
 
36447
37834
  // src/commands/host.ts
36448
37835
  function resolveHostConfig(options) {
36449
- const dataDir = options.dataDir || process.env["WAIRON_DATA_DIR"] || path60.join(os9.homedir(), ".wairon", "data");
37836
+ const dataDir = options.dataDir || process.env["WAIRON_DATA_DIR"] || path59.join(os10.homedir(), ".wairon", "data");
36450
37837
  if (!process.env["WAIRON_PACKS_DIR"]) {
36451
- process.env["WAIRON_PACKS_DIR"] = path60.join(dataDir, "packs");
37838
+ process.env["WAIRON_PACKS_DIR"] = path59.join(dataDir, "packs");
36452
37839
  }
36453
37840
  const cfg = {
36454
37841
  host: options.host || "0.0.0.0",
@@ -36573,13 +37960,13 @@ function openBrowser(url) {
36573
37960
  }
36574
37961
  async function runDev(options = {}) {
36575
37962
  const cwd = process.cwd();
36576
- if (!fs50.existsSync(path60.join(cwd, ".wai"))) {
37963
+ if (!fs50.existsSync(path59.join(cwd, ".wai"))) {
36577
37964
  throw new WaironError(
36578
37965
  "No .wai/ found in the current directory. Run `wairon dev` from a wairon project root (or run `wairon init` first)."
36579
37966
  );
36580
37967
  }
36581
37968
  const hash = crypto20.createHash("sha256").update(cwd).digest("hex").slice(0, 16);
36582
- const dataDir = path60.join(os9.tmpdir(), "wairon-dev", hash);
37969
+ const dataDir = path59.join(os10.tmpdir(), "wairon-dev", hash);
36583
37970
  fs50.mkdirSync(dataDir, { recursive: true });
36584
37971
  registerLocalDevProject(dataDir, "local", cwd);
36585
37972
  const port = options.port ? Number(options.port) : 8080;
@@ -36988,8 +38375,8 @@ async function runHostPacks(action, options = {}) {
36988
38375
  }
36989
38376
  case "install": {
36990
38377
  if (!options.file) throw new WaironError("`--file <path>` (a declarative pack YAML) is required for install.");
36991
- const name = options.name ?? path60.basename(options.file).replace(/\.(ya?ml)$/i, "");
36992
- const content = fs50.readFileSync(path60.resolve(options.file), "utf8");
38378
+ const name = options.name ?? path59.basename(options.file).replace(/\.(ya?ml)$/i, "");
38379
+ const content = fs50.readFileSync(path59.resolve(options.file), "utf8");
36993
38380
  const desc = project2 ? installProjectPack(cfg, cred, project2, name, content) : installGlobalPack(cfg, cred, name, content);
36994
38381
  logger.success(`Installed ${scope} pack "${desc.name}" (${desc.profiles} profile(s), ${desc.languages} language(s)).`);
36995
38382
  if (project2) logger.info("Committed with the project \u2014 every clone and CI will enforce it.");
@@ -37129,15 +38516,29 @@ async function runSurface(action, options = {}) {
37129
38516
  if (format !== "native" && format !== "openapi") {
37130
38517
  throw new WaironError(`Unknown format "${format}" (supported: native, openapi).`);
37131
38518
  }
37132
- const result = exportSurface(audience, format, options.out);
38519
+ if (options.portal && format !== "openapi") {
38520
+ throw new WaironError("`--portal` selects one OpenAPI document and only applies to `--format openapi`.");
38521
+ }
38522
+ const result = exportSurface(audience, format, options.out, options.portal);
37133
38523
  logger.success(
37134
38524
  `Projected surface of "${result.snapshot.projectName}": ${result.snapshot.interfaces.length} interface(s), ${result.snapshot.types.length} type(s) at audience \u2265 ${audience}.`
37135
38525
  );
37136
- if (result.writtenTo) {
37137
- logger.info(`Written to ${result.writtenTo}`);
38526
+ const written = result.writtenPaths ?? (result.writtenTo ? [result.writtenTo] : []);
38527
+ if (written.length === 1) {
38528
+ logger.info(`Written to ${written[0]}`);
38529
+ } else if (written.length > 1) {
38530
+ logger.info(`Written ${written.length} document(s) \u2014 one per portal:`);
38531
+ for (const p of written) logger.info(` ${p}`);
37138
38532
  } else if (result.rendered) {
37139
38533
  process.stdout.write(`${result.rendered}
37140
38534
  `);
38535
+ } else if (result.renderedSet && result.renderedSet.length > 1) {
38536
+ logger.info(
38537
+ `This project publishes ${result.renderedSet.length} portals \u2014 pick one with \`--portal <id>\` (or use --out to write them all):`
38538
+ );
38539
+ for (const spec of result.renderedSet) {
38540
+ logger.info(` ${import_chalk19.default.cyan(spec.portalId)} \u2014 ${spec.name}`);
38541
+ }
37141
38542
  } else {
37142
38543
  for (const entry of result.snapshot.interfaces) {
37143
38544
  logger.info(` ${import_chalk19.default.cyan(entry.id)} (${entry.type}, ${entry.audience}) \u2014 ${entry.methods.length} method(s)`);
@@ -37181,13 +38582,27 @@ async function runSurface(action, options = {}) {
37181
38582
  for (const p of written) logger.info(` ${p}`);
37182
38583
  return;
37183
38584
  }
38585
+ case "externals": {
38586
+ const entries = listExternalInterfaces();
38587
+ if (!entries.length) {
38588
+ logger.info("No external surfaces available (.wai/surfaces/ holds no snapshots).");
38589
+ return;
38590
+ }
38591
+ const freshness = (f) => f === "fresh" ? import_chalk19.default.green(f) : f === "stale" ? import_chalk19.default.yellow(f) : import_chalk19.default.gray(f);
38592
+ for (const e of entries) {
38593
+ logger.info(
38594
+ `${e.sourceKind.padEnd(8)} ${import_chalk19.default.cyan(e.projectName)} [${e.origin}] ${freshness(e.freshness)} \u2014 ${e.interfaceIds.length ? e.interfaceIds.join(", ") : "(no interfaces)"}`
38595
+ );
38596
+ }
38597
+ return;
38598
+ }
37184
38599
  default:
37185
- throw new WaironError(`Unknown surface action "${action}" (supported: export, import, list, generate-children).`);
38600
+ throw new WaironError(`Unknown surface action "${action}" (supported: export, import, list, generate-children, externals).`);
37186
38601
  }
37187
38602
  }
37188
38603
 
37189
38604
  // src/commands/subsystem.ts
37190
- var path61 = __toESM(require("path"));
38605
+ var path60 = __toESM(require("path"));
37191
38606
  init_logger();
37192
38607
  init_errors();
37193
38608
  init_fs();
@@ -37223,9 +38638,9 @@ async function runSubsystemAdd(id, options = {}) {
37223
38638
  updatedAt: now
37224
38639
  };
37225
38640
  createChainedSubsystem(subsystem, displayName);
37226
- const childDir = path61.resolve(getProjectRoot(), options.projectPath);
38641
+ const childDir = path60.resolve(getProjectRoot(), options.projectPath);
37227
38642
  logger.success(`Added external subsystem "${id}" \u2192 ${options.projectPath}`);
37228
- logger.info(`Scaffolded child project at ${path61.relative(process.cwd(), childDir) || "."}`);
38643
+ logger.info(`Scaffolded child project at ${path60.relative(process.cwd(), childDir) || "."}`);
37229
38644
  logger.info(`Design its spec tree from this parent using namespaced ids (e.g. ${id}::<component>).`);
37230
38645
  }
37231
38646
  async function runSubsystemMove(id, options = {}) {
@@ -37248,9 +38663,9 @@ async function runSubsystemExternalize(id, options = {}) {
37248
38663
  throw new WaironError("--project-path (the subproject destination) is required.");
37249
38664
  }
37250
38665
  externalizeSubsystem(id, options.projectPath);
37251
- const childDir = path61.resolve(getProjectRoot(), options.projectPath);
38666
+ const childDir = path60.resolve(getProjectRoot(), options.projectPath);
37252
38667
  logger.success(`Externalized subsystem "${id}" \u2192 ${options.projectPath}`);
37253
- logger.info(`Moved its specs into ${path61.relative(process.cwd(), childDir) || "."} (now a standalone subproject).`);
38668
+ logger.info(`Moved its specs into ${path60.relative(process.cwd(), childDir) || "."} (now a standalone subproject).`);
37254
38669
  logger.info("Move the source code there yourself, then run `wairon validate` to confirm the tree.");
37255
38670
  }
37256
38671
  async function runSubsystemInternalize(id) {
@@ -37289,8 +38704,64 @@ program.command("generate").description("Generate agent output files from the sp
37289
38704
  dryRun: opts.dryRun
37290
38705
  });
37291
38706
  });
38707
+ async function runLock2(options) {
38708
+ assertProjectInitialized();
38709
+ if (!pathExists(AI_PATHS.specsSystem())) {
38710
+ logger.error("No SDD spec tree found (.wai/specs). Nothing to lock.");
38711
+ process.exit(1);
38712
+ }
38713
+ const projectConfig = loadProjectConfig();
38714
+ logger.info("Analyzing and validating specifications in-memory...");
38715
+ const dry = validateAsComplete({
38716
+ rules: projectConfig.rules,
38717
+ projectType: projectConfig.projectType,
38718
+ scopeSubsystem: options.subsystem,
38719
+ recursive: options.recursive ?? true
38720
+ });
38721
+ const errors = dry.issues.filter((i) => i.severity === "error");
38722
+ if (errors.length > 0) {
38723
+ logger.header("Cannot lock \u2014 the spec tree does not validate as complete");
38724
+ let errorCount = 0;
38725
+ const MAX_PRINT = 100;
38726
+ let skippedErrors = 0;
38727
+ for (const i of errors) {
38728
+ if (errorCount < MAX_PRINT) {
38729
+ logger.error(`${i.specId ? `[${i.specId}] ` : ""}[${i.code}] ${i.message}`);
38730
+ errorCount++;
38731
+ } else {
38732
+ skippedErrors++;
38733
+ }
38734
+ }
38735
+ if (skippedErrors > 0) {
38736
+ logger.error(`... and ${skippedErrors} more error(s) omitted.`);
38737
+ }
38738
+ logger.blank();
38739
+ logger.info("Fix the errors above, then run `wairon lock` again. Nothing was changed.");
38740
+ process.exit(1);
38741
+ }
38742
+ logger.header("Lock SDD specs");
38743
+ const record2 = await runLock(options, dry);
38744
+ if (!record2) {
38745
+ logger.info("Cancelled. Nothing was changed.");
38746
+ return;
38747
+ }
38748
+ const childPaths = generateChildSnapshots();
38749
+ if (childPaths.length > 0) {
38750
+ logger.blank();
38751
+ logger.success(`Regenerated the family/sibling surfaces into ${childPaths.length} chained child snapshot(s):`);
38752
+ for (const p of childPaths) logger.info(` ${p}`);
38753
+ }
38754
+ logger.blank();
38755
+ await runGenerate({ domain: options.subsystem });
38756
+ logger.blank();
38757
+ logger.success("Specs locked and agent topology generated.");
38758
+ logger.info(`Lock record written (.wai/lock.json): stateId ${record2.stateId.algorithm}:${record2.stateId.digest} \u2014 status ${record2.status}.`);
38759
+ logger.warn(
38760
+ "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."
38761
+ );
38762
+ }
37292
38763
  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) => {
37293
- await runLock({ yes: opts.yes, subsystem: opts.subsystem, recursive: opts.recursive });
38764
+ await runLock2({ yes: opts.yes, subsystem: opts.subsystem, recursive: opts.recursive });
37294
38765
  });
37295
38766
  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) => {
37296
38767
  await runValidate({ ci: opts.ci, subsystem: opts.subsystem, recursive: opts.recursive });
@@ -37406,13 +38877,14 @@ mcpCmd.command("status").description("Show whether the wairon MCP server is regi
37406
38877
  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) => {
37407
38878
  await runProduce(target, { page: opts.page, token: opts.token });
37408
38879
  });
37409
- 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("--source <path>", "import: the surface document (native snapshot YAML or OpenAPI)").option("--origin <origin>", "import provenance: exchanged | authored (default authored)").action(async (action, opts) => {
38880
+ 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) => {
37410
38881
  await runSurface(action, {
37411
38882
  audience: opts.audience,
37412
38883
  format: opts.format,
37413
38884
  out: opts.out,
37414
38885
  source: opts.source,
37415
- origin: opts.origin
38886
+ origin: opts.origin,
38887
+ portal: opts.portal
37416
38888
  });
37417
38889
  });
37418
38890
  program.command("serve").description("Run the wairon hosting server: HTTP MCP for many isolated projects + admin API").option("--host <host>", "data-plane bind host (default 0.0.0.0)").option("--port <port>", "data-plane port (default 8080)").option("--admin-host <host>", "admin-plane bind host (default 127.0.0.1)").option("--admin-port <port>", "admin-plane port (default 8081)").option("--data-dir <path>", "data root holding projects/ and auth/ (default WAIRON_DATA_DIR or ~/.wairon/data)").option("--no-auth", "disable data-plane auth (trusted networks only)").action(async (opts) => {