@wairon/cli 5.0.2-dev.9 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.9";
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.
@@ -1097,10 +1122,14 @@ var init_specs = __esm({
1097
1122
  // Required if type is 'dispatch': the capability routed through the target Portal's dispatch table
1098
1123
  /**
1099
1124
  * call/dispatch only: the credential this step presents to an authed callee
1100
- * Portal, and WHERE it is loaded from (`from` a secret-store component id,
1101
- * `env:API_KEY`, a config key, a vault ref, …). A declared DESIGN NOTE — wairon
1102
- * never fetches it — but its absence on a call into a Portal whose `auth none`
1103
- * warns (PORTAL_AUTH_UNMET), so credential loading is never overlooked.
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.
1104
1133
  */
1105
1134
  auth: import_zod6.z.object({ from: import_zod6.z.string(), note: import_zod6.z.string().optional() }).optional(),
1106
1135
  assertsGuarantees: import_zod6.z.array(GuaranteeSchema).optional(),
@@ -1910,6 +1939,47 @@ var init_loader = __esm({
1910
1939
  }
1911
1940
  });
1912
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
+
1913
1983
  // src/core/narrative-labels.ts
1914
1984
  function resolveNarrativeLabels(methodName, steps) {
1915
1985
  const errors = [];
@@ -2995,6 +3065,16 @@ header input[type="search"]::placeholder { color:var(--dim); }
2995
3065
  #moreMenu .dropdown { display:block; width:100%; }
2996
3066
  #moreMenu .dropdown > .tbtn, #moreMenu > .tbtn { display:block; width:100%; text-align:left; border:none; background:transparent; margin:2px 0; }
2997
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; }
2998
3078
 
2999
3079
  /* Settings panel \u2014 toggle switches */
3000
3080
  .settings-menu { min-width:266px; }
@@ -3118,9 +3198,17 @@ body.presentation #exitPresent, body.presentation #presentDetails { display:bloc
3118
3198
  <button data-vm="types">Types</button>
3119
3199
  <button data-vm="databases">Databases</button>
3120
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>
3121
3205
  <nav id="crumbs"></nav>
3122
3206
  <span class="divider"></span>
3123
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>
3124
3212
  <div class="seg" id="typesDetailSeg" style="display:none" title="ERD detail level">
3125
3213
  <button data-td="full">Full</button>
3126
3214
  <button data-td="fields">Fields</button>
@@ -3637,13 +3725,300 @@ var MODEL = __MODEL_JSON__;
3637
3725
  var BOX_W = 200, BOX_H = 56, SUBBOX_W = 230, SUBBOX_H = 84, GAP_X = 110, GAP_Y = 34;
3638
3726
  var INNER_W = 130, INNER_H = 36, INNER_GAPX = 26, INNER_GAPY = 12, HEAD_H = 34, PADI = 14;
3639
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
+
3640
4010
  // Micro-layout for a container's direct children when Internals is on:
3641
4011
  // layered mini columns + intra-container edges. Each external relation gets
3642
4012
  // its own small PORT node INSIDE the container (one per external
3643
4013
  // counterpart; incoming left, outgoing right). Children connect to ports
3644
4014
  // with short edges that never leave the box \u2014 the real cross-boundary line
3645
4015
  // is only revealed on hover, or pinned while the port is selected.
3646
- 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
+ }
3647
4022
  var kids = state.internals && entry.hasKids ? childrenOf({ kind: entry.kind, id: entry.id }) : [];
3648
4023
  if (!kids.length) return null;
3649
4024
  var scope = { kind: entry.kind, id: entry.id };
@@ -4215,12 +4590,42 @@ var MODEL = __MODEL_JSON__;
4215
4590
  if (scope.kind === 'types' || scope.kind === 'databases') return buildTypeElements();
4216
4591
  var entries = childrenOf(scope);
4217
4592
  var eles = [];
4593
+ var deepCtx = buildDeepContext(scope, entries);
4218
4594
  var ve = viewEdges(scope, entries);
4219
4595
  // Data-coupling overlay: same scoping pipeline, a different edge source.
4220
4596
  var vd = state.dataCoupling ? viewEdges(scope, entries, MODEL.dataEdges) : { agg: {}, ghosts: {} };
4221
4597
  Object.keys(vd.ghosts).forEach(function (g) { if (!ve.ghosts[g]) ve.ghosts[g] = vd.ghosts[g]; });
4222
4598
  var inners = {};
4223
- 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
+ }
4224
4629
 
4225
4630
  // Resolve a port's reveal target(s) in THIS view. Preference order: the
4226
4631
  // MATCHING PORT inside the counterpart's container (a port-to-port line
@@ -4352,17 +4757,25 @@ var MODEL = __MODEL_JSON__;
4352
4757
  + (state.showIssues && issuesBySpec[e.id] ? ' hasIssue' : '')
4353
4758
  + (state.selectedKind === e.kind && state.selected === e.id ? ' sel' : '');
4354
4759
  if (inner) {
4355
- 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 });
4356
- inner.tiles.forEach(function (tile) {
4357
- eles.push({
4358
- data: {
4359
- id: IN(tile.kid.kind, tile.kid.id), parent: aid,
4360
- label: nameOf(tile.kid), w: INNER_W, h: INNER_H, tw: INNER_W - 10,
4361
- },
4362
- position: { x: p.x - p.w / 2 + tile.x, y: p.y - p.h / 2 + tile.y },
4363
- 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
+ });
4364
4777
  });
4365
- });
4778
+ }
4366
4779
  (inner.proxies || []).forEach(function (px) {
4367
4780
  eles.push({
4368
4781
  data: {
@@ -4515,9 +4928,28 @@ var MODEL = __MODEL_JSON__;
4515
4928
 
4516
4929
  var dimmedAnchors = {};
4517
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
+
4518
4947
  var i = 0;
4519
4948
  Object.keys(ve.agg).forEach(function (key) {
4520
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;
4521
4953
  var bundle = e.n > 1;
4522
4954
  var dim = state.query && (dimmedAnchors[e.src] || dimmedAnchors[e.tgt]);
4523
4955
  var route = routeData(e.src, e.tgt, key);
@@ -4776,22 +5208,58 @@ var MODEL = __MODEL_JSON__;
4776
5208
  }
4777
5209
  return path;
4778
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
+ }
4779
5227
  function renderCrumbs() {
4780
5228
  var el = document.getElementById('crumbs');
4781
5229
  var path = crumbPath();
4782
- el.innerHTML = path.map(function (p, i) {
4783
- var cur = i === path.length - 1;
4784
- return '<button class="crumb' + (cur ? ' cur' : '') + '" data-ck="' + p.kind + '" data-ci="' + (p.id || '') + '">' + p.label + '</button>'
4785
- + (cur ? '' : '<span class="sep">\\u203A</span>');
4786
- }).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;
4787
5252
  var btns = el.querySelectorAll('button');
4788
5253
  for (var i = 0; i < btns.length; i++) {
4789
5254
  (function (b) {
5255
+ if (!b.getAttribute('data-ck')) return; // the "\\u2026" trigger toggles, never navigates
4790
5256
  b.addEventListener('click', function () {
4791
5257
  navigateTo(b.getAttribute('data-ck'), b.getAttribute('data-ci') || null);
4792
5258
  });
4793
5259
  })(btns[i]);
4794
5260
  }
5261
+ if (crumbsCompact && path.length > 1) wireDropdown('crumbDd', 'crumbMoreBtn');
5262
+ if (headerReflowHook) headerReflowHook();
4795
5263
  }
4796
5264
  function renderViewHint() {
4797
5265
  if (state.view.kind === 'types' || state.view.kind === 'databases') {
@@ -5058,7 +5526,16 @@ var MODEL = __MODEL_JSON__;
5058
5526
  return c ? { kind: 'subsystem', id: c.subsystem } : { kind: 'system', id: null };
5059
5527
  }
5060
5528
 
5061
- 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();
5062
5539
  // Sync each View toggle's checkbox from the (possibly persisted) state, then
5063
5540
  // persist on change so the choices survive a refresh (see persist()/saved).
5064
5541
  document.getElementById('internalsToggle').checked = state.internals;
@@ -5112,9 +5589,24 @@ var MODEL = __MODEL_JSON__;
5112
5589
  })(btns[i]);
5113
5590
  }
5114
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
+ }
5115
5601
  function updateHeaderSegs() {
5116
5602
  var seg = document.getElementById('modeSeg');
5117
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
+ }
5118
5610
  for (var i = 0; i < btns.length; i++) {
5119
5611
  var vm = btns[i].getAttribute('data-vm');
5120
5612
  var active = vm === 'components'
@@ -5122,6 +5614,7 @@ var MODEL = __MODEL_JSON__;
5122
5614
  : vm === state.view.kind;
5123
5615
  if (btns[i].classList) btns[i].classList[active ? 'add' : 'remove']('active');
5124
5616
  }
5617
+ updateModeBtn();
5125
5618
  var td = document.getElementById('typesDetailSeg');
5126
5619
  td.style.display = (state.view.kind === 'types' || state.view.kind === 'databases') ? '' : 'none';
5127
5620
  var tbs = td.querySelectorAll('button');
@@ -5185,7 +5678,13 @@ var MODEL = __MODEL_JSON__;
5185
5678
  var r = btn.getBoundingClientRect();
5186
5679
  menu.style.top = (r.bottom + 6) + 'px';
5187
5680
  menu.style.left = 'auto';
5188
- 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';
5189
5688
  }
5190
5689
  function wireDropdown(ddId, btnId) {
5191
5690
  var dd = document.getElementById(ddId);
@@ -5204,11 +5703,29 @@ var MODEL = __MODEL_JSON__;
5204
5703
  var ldd = wireDropdown('layoutDd', 'layoutBtn');
5205
5704
  var sdd = wireDropdown('settingsDd', 'settingsBtn');
5206
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
+ });
5207
5720
  // Keep the settings panel open while flipping switches (clicks inside it don't
5208
5721
  // bubble to the document-level close handler).
5209
5722
  (function () {
5210
5723
  var m = document.getElementById('settingsMenu');
5211
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(); });
5212
5729
  })();
5213
5730
 
5214
5731
  // Layout picker: choose the auto-layout algorithm. Components use cytoscape's
@@ -5232,6 +5749,12 @@ var MODEL = __MODEL_JSON__;
5232
5749
  });
5233
5750
  updateLayoutBtn();
5234
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
+ }
5235
5758
  if (document.addEventListener) {
5236
5759
  document.addEventListener('click', function () {
5237
5760
  if (dd.classList) dd.classList.remove('open');
@@ -5239,6 +5762,9 @@ var MODEL = __MODEL_JSON__;
5239
5762
  if (ldd.classList) ldd.classList.remove('open');
5240
5763
  if (sdd.classList) sdd.classList.remove('open');
5241
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();
5242
5768
  });
5243
5769
  document.addEventListener('keydown', function (ev) {
5244
5770
  if (ev.key === 'Escape') {
@@ -5246,16 +5772,27 @@ var MODEL = __MODEL_JSON__;
5246
5772
  if (modal.classList && String(modal.className).indexOf('open') >= 0) { closeFlow(); return; }
5247
5773
  setPresentation(false);
5248
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();
5249
5778
  }
5250
5779
  });
5251
5780
  }
5252
5781
 
5253
- // \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
5254
- // When the floating header no longer fits its controls, trailing items
5255
- // COLLAPSE into the More menu instead of relying on horizontal scroll \u2014
5256
- // every control stays one click away. Whole items move (listeners survive
5257
- // reparenting); a hidden placeholder pins each item's original position so
5258
- // 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.
5259
5796
  (function () {
5260
5797
  if (typeof window === 'undefined') return;
5261
5798
  var hdr = document.getElementById('hdr');
@@ -5283,7 +5820,122 @@ var MODEL = __MODEL_JSON__;
5283
5820
  }
5284
5821
  return markers[id];
5285
5822
  }
5286
- 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
+ ];
5287
5939
  // Signed fit measure in px: positive = overflowing, negative = headroom.
5288
5940
  // The header is a flex row whose ONLY flex:1 child is the .spacer, so the
5289
5941
  // spacer's rendered width IS the free space -- it grows to absorb all slack
@@ -5299,33 +5951,54 @@ var MODEL = __MODEL_JSON__;
5299
5951
  var slack = spacer ? spacer.getBoundingClientRect().width : 0;
5300
5952
  return (hdr.scrollWidth - hdr.clientWidth) - slack;
5301
5953
  }
5954
+ var inReflow = false;
5302
5955
  function reflow() {
5303
5956
  // Not laid out (hidden tab, non-browser DOM) \u2014 measuring would misfire.
5304
5957
  var box = hdr.getBoundingClientRect();
5305
5958
  if (!box || box.width <= 0) return;
5306
- // Restore everything, then collapse until the row fits (idempotent).
5307
- for (var i = collapsed.length - 1; i >= 0; i--) {
5308
- var it = collapsed[i];
5309
- if (it.el && it.marker && it.marker.parentNode) it.marker.parentNode.insertBefore(it.el, it.marker);
5310
- }
5311
- collapsed = [];
5312
- moreDd.style.display = 'none';
5313
- hdr.scrollLeft = 0;
5314
- var guard = 0;
5315
- // Demand a few px of headroom, not a bare fit \u2014 the marginal-fit widths
5316
- // are exactly where the phantom scrollbar appeared.
5317
- while (overflowPx() > -8 && guard < COLLAPSE.length) {
5318
- var id = COLLAPSE[guard++];
5319
- var el = movableFor(id);
5320
- if (!el || el === moreDd || el.parentNode === moreMenu) continue;
5321
- var m = markerFor(id, el);
5322
- // A dropdown moved while open would strand its fixed-positioned menu.
5323
- if (el.classList) el.classList.remove('open');
5324
- moreDd.style.display = '';
5325
- moreMenu.appendChild(el);
5326
- collapsed.push({ el: el, marker: m });
5327
- }
5328
- 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
+ }
5329
6002
  }
5330
6003
  var raf = null;
5331
6004
  var defer = window.requestAnimationFrame
@@ -5335,6 +6008,9 @@ var MODEL = __MODEL_JSON__;
5335
6008
  if (raf !== null) return;
5336
6009
  raf = defer(function () { raf = null; reflow(); });
5337
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(); };
5338
6014
  if (typeof ResizeObserver !== 'undefined') {
5339
6015
  new ResizeObserver(schedule).observe(hdr);
5340
6016
  } else if (window.addEventListener) {
@@ -6662,7 +7338,7 @@ var init_extensions = __esm({
6662
7338
  });
6663
7339
 
6664
7340
  // src/core/rules/types.ts
6665
- var BUILTIN_PROFILES;
7341
+ var BUILTIN_PROFILES, PROJECT_KINDS;
6666
7342
  var init_types = __esm({
6667
7343
  "src/core/rules/types.ts"() {
6668
7344
  "use strict";
@@ -6675,6 +7351,7 @@ var init_types = __esm({
6675
7351
  "realtime-embedded",
6676
7352
  "plc-cyclic"
6677
7353
  ];
7354
+ PROJECT_KINDS = ["fullstack", "system-of-systems", "monorepo"];
6678
7355
  }
6679
7356
  });
6680
7357
 
@@ -6855,44 +7532,13 @@ var init_type_references = __esm({
6855
7532
  }
6856
7533
  });
6857
7534
 
6858
- // src/core/statehash.ts
6859
- function computeStateId() {
6860
- const tree = {
6861
- system: loadSystemSpec(),
6862
- subsystems: loadSubsystemSpecs(),
6863
- components: loadComponentSpecs(),
6864
- interfaces: loadInterfaceSpecs(),
6865
- implementations: loadImplementationSpecs(),
6866
- types: loadTypeSpecs()
6867
- };
6868
- const digest = crypto.createHash("sha256").update(canonicalize(tree)).digest("hex");
6869
- return { algorithm: "sha256", digest };
6870
- }
6871
- function stateIdEquals(a, b) {
6872
- return !!a && !!b && a.algorithm === b.algorithm && a.digest === b.digest;
7535
+ // src/utils/filenames.ts
7536
+ function safeFilenamePart(value) {
7537
+ return value.replace(/[^a-zA-Z0-9._-]/g, "-");
6873
7538
  }
6874
- function canonicalize(value) {
6875
- return JSON.stringify(sortKeys(value));
6876
- }
6877
- function sortKeys(v) {
6878
- if (Array.isArray(v)) return v.map(sortKeys);
6879
- if (v && typeof v === "object") {
6880
- const src = v;
6881
- const out = {};
6882
- for (const k of Object.keys(src).sort()) {
6883
- if (k === "createdAt" || k === "updatedAt") continue;
6884
- out[k] = sortKeys(src[k]);
6885
- }
6886
- return out;
6887
- }
6888
- return v;
6889
- }
6890
- var crypto;
6891
- var init_statehash = __esm({
6892
- "src/core/statehash.ts"() {
7539
+ var init_filenames = __esm({
7540
+ "src/utils/filenames.ts"() {
6893
7541
  "use strict";
6894
- crypto = __toESM(require("crypto"));
6895
- init_specs2();
6896
7542
  }
6897
7543
  });
6898
7544
 
@@ -7349,6 +7995,66 @@ function projectOwnSurface(maxAudience) {
7349
7995
  function projectChildSurface() {
7350
7996
  return projectOwnSurface("project");
7351
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
+ }
7352
8058
  function listSnapshots(rootDir = getProjectRoot()) {
7353
8059
  const dir = surfacesDir(rootDir);
7354
8060
  if (!fs9.existsSync(dir)) return [];
@@ -7365,39 +8071,66 @@ function listSnapshots(rootDir = getProjectRoot()) {
7365
8071
  function getSnapshot(projectName, rootDir = getProjectRoot()) {
7366
8072
  return listSnapshots(rootDir).find((s) => s.projectName === projectName) ?? null;
7367
8073
  }
8074
+ function snapshotFilename(projectName) {
8075
+ return `${safeFilenamePart(projectName)}.yaml`;
8076
+ }
7368
8077
  function saveSnapshot(snapshot, rootDir = getProjectRoot()) {
7369
8078
  const dir = surfacesDir(rootDir);
7370
8079
  fs9.mkdirSync(dir, { recursive: true });
7371
- const p = path10.join(dir, `${snapshot.projectName}.yaml`);
8080
+ const p = path10.join(dir, snapshotFilename(snapshot.projectName));
7372
8081
  writeYamlFile(p, SurfaceSnapshotSchema.parse(snapshot));
7373
8082
  return p;
7374
8083
  }
7375
8084
  function loadSurfaceSnapshots() {
7376
8085
  return listSnapshots();
7377
8086
  }
7378
- function renderOpenApiForms(snapshot) {
7379
- const renderedSet = toOpenApiSet(snapshot);
7380
- 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}`;
7381
8099
  }
7382
- function writeSurfaceFile(outPath, body, snapshot) {
8100
+ function writeSurfaceFile(outPath, snapshot, renderedSet) {
7383
8101
  const resolved = path10.resolve(outPath);
7384
8102
  fs9.mkdirSync(path10.dirname(resolved), { recursive: true });
7385
- if (body !== void 0) fs9.writeFileSync(resolved, body);
7386
- else writeYamlFile(resolved, snapshot);
7387
- 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
+ });
7388
8116
  }
7389
- function exportSurface(maxAudience, format, outPath) {
7390
- const snapshot = projectOwnSurface(maxAudience);
7391
- const openapi = format === "openapi" ? renderOpenApiForms(snapshot) : void 0;
7392
- const body = openapi?.rendered ?? openapi?.renderedSet[0]?.document;
7393
- 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;
7394
8119
  return {
7395
8120
  snapshot,
7396
- ...openapi?.rendered !== void 0 ? { rendered: openapi.rendered } : {},
7397
- ...openapi ? { renderedSet: openapi.renderedSet } : {},
7398
- ...writtenTo ? { writtenTo } : {}
8121
+ ...rendered !== void 0 ? { rendered } : {},
8122
+ ...renderedSet ? { renderedSet } : {},
8123
+ ...writtenPaths.length === 1 ? { writtenTo: writtenPaths[0] } : {},
8124
+ ...writtenPaths.length ? { writtenPaths } : {}
7399
8125
  };
7400
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
+ }
7401
8134
  function importSurface(sourcePath, origin) {
7402
8135
  const resolved = path10.resolve(sourcePath);
7403
8136
  if (!fs9.existsSync(resolved)) {
@@ -7417,17 +8150,54 @@ function importSurface(sourcePath, origin) {
7417
8150
  return snapshot;
7418
8151
  }
7419
8152
  function generateChildSnapshots(rootDir = getProjectRoot()) {
7420
- 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);
7421
8155
  if (!children.length) return [];
7422
- 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
+ };
7423
8166
  const written = [];
7424
8167
  for (const child of children) {
7425
8168
  const childDir = path10.resolve(rootDir, child.projectPath);
7426
8169
  if (!fs9.existsSync(childDir)) continue;
7427
- 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
+ }
7428
8175
  }
7429
8176
  return written;
7430
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
+ }
7431
8201
  function surfaceContentKey(snapshot) {
7432
8202
  const { stateId, generatedAt, origin, ...content } = snapshot;
7433
8203
  return JSON.stringify(content);
@@ -7454,7 +8224,7 @@ function checkChildSurfaceFreshness(rootDir = getProjectRoot()) {
7454
8224
  }
7455
8225
  return issues;
7456
8226
  }
7457
- var fs9, path10, SURFACES_DIRNAME;
8227
+ var fs9, path10, SURFACES_DIRNAME, CROSS_BOUNDARY_TARGETS;
7458
8228
  var init_surfaces = __esm({
7459
8229
  "src/core/surfaces.ts"() {
7460
8230
  "use strict";
@@ -7462,12 +8232,14 @@ var init_surfaces = __esm({
7462
8232
  path10 = __toESM(require("path"));
7463
8233
  init_fs();
7464
8234
  init_yaml();
8235
+ init_filenames();
7465
8236
  init_models();
7466
8237
  init_specs2();
7467
8238
  init_statehash();
7468
8239
  init_type_analysis();
7469
8240
  init_openapi();
7470
8241
  SURFACES_DIRNAME = "surfaces";
8242
+ CROSS_BOUNDARY_TARGETS = /* @__PURE__ */ new Set(["Portal", "Gateway", "Observer"]);
7471
8243
  }
7472
8244
  });
7473
8245
 
@@ -7635,7 +8407,8 @@ var init_contracts = __esm({
7635
8407
  "SURFACE_REF_NOT_EXPOSED",
7636
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}".`,
7637
8409
  impl.id,
7638
- isDraftCtx
8410
+ isDraftCtx,
8411
+ true
7639
8412
  );
7640
8413
  }
7641
8414
  continue;
@@ -7692,7 +8465,8 @@ var init_contracts = __esm({
7692
8465
  "SURFACE_REF_NOT_EXPOSED",
7693
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}".`,
7694
8467
  impl.id,
7695
- isDraftCtx
8468
+ isDraftCtx,
8469
+ true
7696
8470
  );
7697
8471
  } else if (step.assertsGuarantees) {
7698
8472
  const declared = new Set(surfaceMethod.guarantees ?? []);
@@ -7703,7 +8477,8 @@ var init_contracts = __esm({
7703
8477
  "NARRATIVE_SEMANTIC_UNBACKED",
7704
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}".`,
7705
8479
  impl.id,
7706
- isDraftCtx
8480
+ isDraftCtx,
8481
+ true
7707
8482
  );
7708
8483
  }
7709
8484
  }
@@ -9054,13 +9829,14 @@ var init_portals = __esm({
9054
9829
  };
9055
9830
  portalsRule = {
9056
9831
  name: "portal-endpoints",
9057
- 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).",
9058
9833
  codes: [
9059
9834
  { code: "MISSING_PORTAL_TYPE", defaultSeverity: "error", summary: "Portal without a portalType" },
9060
9835
  { code: "MISSING_ENDPOINT", defaultSeverity: "error", summary: "Portal method without a wire endpoint binding" },
9061
9836
  { code: "ENDPOINT_TRANSPORT_MISMATCH", defaultSeverity: "error", summary: "Endpoint transport does not match the Portal portalType" },
9062
9837
  { code: "UNEXPECTED_PORTAL_FIELD", defaultSeverity: "error", summary: "Non-Portal component with portalType/basePath" },
9063
- { 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)" }
9064
9840
  ],
9065
9841
  check(ctx) {
9066
9842
  for (const comp of ctx.components) {
@@ -9112,6 +9888,15 @@ var init_portals = __esm({
9112
9888
  isDraftCtx
9113
9889
  );
9114
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
+ }
9115
9900
  const compInterfaces = ctx.interfaces.filter((i) => i.component === comp.id);
9116
9901
  for (const intf of compInterfaces) {
9117
9902
  for (const m of intf.methods) {
@@ -9181,7 +9966,8 @@ var init_stereotype_deps = __esm({
9181
9966
  "CROSS_SUBSYSTEM_NON_ADAPTER",
9182
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.`,
9183
9968
  comp.id,
9184
- isDraftCtx
9969
+ isDraftCtx,
9970
+ true
9185
9971
  );
9186
9972
  }
9187
9973
  continue;
@@ -9759,14 +10545,14 @@ var init_declarative_assertions = __esm({
9759
10545
  });
9760
10546
 
9761
10547
  // src/core/rules/profiles.ts
9762
- var BACKEND_LIKE, FRONTEND_LIKE, PROJECT_KINDS, profilesRule;
10548
+ var BACKEND_LIKE, FRONTEND_LIKE, PROJECT_KINDS2, profilesRule;
9763
10549
  var init_profiles = __esm({
9764
10550
  "src/core/rules/profiles.ts"() {
9765
10551
  "use strict";
9766
10552
  init_types();
9767
10553
  BACKEND_LIKE = /* @__PURE__ */ new Set(["backend", "lowlevel-os", "game-ecs", "realtime-embedded", "plc-cyclic"]);
9768
10554
  FRONTEND_LIKE = /* @__PURE__ */ new Set(["frontend-reactive", "frontend-controller"]);
9769
- PROJECT_KINDS = /* @__PURE__ */ new Set(["fullstack", "system-of-systems", "monorepo"]);
10555
+ PROJECT_KINDS2 = new Set(PROJECT_KINDS);
9770
10556
  profilesRule = {
9771
10557
  name: "architectural-profiles",
9772
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.",
@@ -9790,7 +10576,7 @@ var init_profiles = __esm({
9790
10576
  );
9791
10577
  }
9792
10578
  }
9793
- if (!registered.has(ctx.projectType) && !PROJECT_KINDS.has(ctx.projectType)) {
10579
+ if (!registered.has(ctx.projectType) && !PROJECT_KINDS2.has(ctx.projectType)) {
9794
10580
  ctx.addIssue(
9795
10581
  "warning",
9796
10582
  "UNKNOWN_PROFILE",
@@ -11044,11 +11830,11 @@ var init_narrative_antipatterns = __esm({
11044
11830
  const memberEdges = keys.flatMap((k) => (adjacency.get(k) ?? []).filter((e) => inScc.has(e.toKey)));
11045
11831
  if (memberEdges.length === 0) continue;
11046
11832
  const anchor = [...memberEdges].sort((a, b) => a.fromKey.localeCompare(b.fromKey))[0];
11047
- const path62 = [...keys].sort().join(" \u2192 ");
11833
+ const path61 = [...keys].sort().join(" \u2192 ");
11048
11834
  ctx.addIssue(
11049
11835
  "warning",
11050
11836
  "UNCONDITIONAL_CALL_CYCLE",
11051
- `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.`,
11052
11838
  anchor.impl.id,
11053
11839
  memberEdges.some((e) => ctx.isImplementationDraft(e.impl))
11054
11840
  );
@@ -12196,19 +12982,25 @@ var init_lint_allows = __esm({
12196
12982
  });
12197
12983
 
12198
12984
  // src/core/rules/portal-call-auth.ts
12199
- var portalCallAuthRule;
12985
+ var COMPONENT_REF_PREFIX, portalCallAuthRule;
12200
12986
  var init_portal_call_auth = __esm({
12201
12987
  "src/core/rules/portal-call-auth.ts"() {
12202
12988
  "use strict";
12989
+ COMPONENT_REF_PREFIX = "component:";
12203
12990
  portalCallAuthRule = {
12204
12991
  name: "portal-call-auth",
12205
- description: "An OUTBOUND narrative `call` into ANOTHER component's Portal whose auth is not `none` must declare the credential source it presents \u2014 the step's `auth.from`, a design note naming where the secret loads from (a secret-store component, env var, config key, vault ref). Its absence warns (PORTAL_AUTH_UNMET) so credential loading is never overlooked; the actual secret is never stored in the spec. Dispatch steps (a portal's OWN inbound routing) and self-calls are not cross-service calls and are excluded.",
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.",
12206
12993
  codes: [
12207
- { code: "PORTAL_AUTH_UNMET", defaultSeverity: "warning", summary: "A narrative call into another component's authed Portal does not declare where its credential loads from" }
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" }
12208
12999
  ],
12209
13000
  check(ctx) {
12210
13001
  for (const impl of ctx.implementations) {
12211
13002
  const ownComponent = ctx.interfaceMap.get(impl.contract)?.component;
13003
+ const presenter = ownComponent ? ctx.componentMap.get(ownComponent) : void 0;
12212
13004
  const draft = ctx.isImplementationDraft(impl);
12213
13005
  for (const method2 of impl.methods ?? []) {
12214
13006
  for (const step of method2.narrative ?? []) {
@@ -12217,14 +13009,59 @@ var init_portal_call_auth = __esm({
12217
13009
  const target = ctx.componentMap.get(step.targetComponent);
12218
13010
  if (!target || target.componentType !== "Portal") continue;
12219
13011
  if (!target.auth || target.auth.scheme === "none") continue;
12220
- if (step.auth?.from) continue;
12221
- ctx.addIssue(
12222
- "warning",
12223
- "PORTAL_AUTH_UNMET",
12224
- `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.`,
12225
- impl.id,
12226
- draft
12227
- );
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
+ }
12228
13065
  }
12229
13066
  }
12230
13067
  }
@@ -12384,9 +13221,15 @@ function buildRuleContext(opts) {
12384
13221
  ...[...SDD_RULES, ...extensions.rules].flatMap((r) => r.codes.map((c) => c.code)),
12385
13222
  // Declarative assertions bring their own namespaced codes — lint.allow
12386
13223
  // and severity overrides treat them exactly like builtins.
12387
- ...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"
12388
13231
  ]);
12389
- const addIssue = (defaultSeverity, code, message, specId, isDraftContext) => {
13232
+ const addIssue = (defaultSeverity, code, message, specId, isDraftContext, surfaceResolved) => {
12390
13233
  if (scopeSubsystem && specId && !isSpecInScope(specId)) {
12391
13234
  return;
12392
13235
  }
@@ -12404,7 +13247,14 @@ function buildRuleContext(opts) {
12404
13247
  if (severity === "warning") return;
12405
13248
  }
12406
13249
  }
12407
- 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
+ });
12408
13258
  };
12409
13259
  return {
12410
13260
  system,
@@ -12829,24 +13679,54 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
12829
13679
  for (const rule of ruleSequence()) {
12830
13680
  rule.check(ctx);
12831
13681
  }
12832
- 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
+ );
12833
13685
  const chainingParent = hasCrossTreeSuspects ? findChainingParent(getProjectRoot()) : null;
12834
13686
  if (chainingParent) {
13687
+ let unverified = 0;
12835
13688
  let downgraded = 0;
12836
- for (const iss of issues) {
12837
- if (!SUBPROJECT_LENIENT_CODES.has(iss.code)) continue;
12838
- if (iss.severity === "error") {
12839
- iss.severity = "warning";
12840
- 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;
12841
13711
  }
12842
- iss.crossTreeContext = true;
12843
13712
  }
12844
- 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
+ }
12845
13725
  issues.unshift({
12846
13726
  severity: "warning",
12847
13727
  code: "CHAINED_SUBPROJECT_CONTEXT",
12848
13728
  crossTreeContext: true,
12849
- 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.`
12850
13730
  });
12851
13731
  }
12852
13732
  }
@@ -12863,7 +13743,7 @@ function validateSddTree(rulesOrOptions, projectType = "backend") {
12863
13743
  function validateAsComplete(options) {
12864
13744
  return validateSddTree({ ...options ?? {}, treatAllAsComplete: true });
12865
13745
  }
12866
- var SUBPROJECT_LENIENT_CODES;
13746
+ var SUBPROJECT_REFERENCE_CODES, SUBPROJECT_CONFORMANCE_CODES;
12867
13747
  var init_validation = __esm({
12868
13748
  "src/core/validation.ts"() {
12869
13749
  "use strict";
@@ -12876,8 +13756,7 @@ var init_validation = __esm({
12876
13756
  init_source_analysis();
12877
13757
  init_specs2();
12878
13758
  init_fs();
12879
- SUBPROJECT_LENIENT_CODES = /* @__PURE__ */ new Set([
12880
- // reference resolution
13759
+ SUBPROJECT_REFERENCE_CODES = /* @__PURE__ */ new Set([
12881
13760
  "UNDEFINED_TYPE_REFERENCE",
12882
13761
  "INVALID_DEPENDENCY_REFERENCE",
12883
13762
  "INVALID_TARGET_COMPONENT_REFERENCE",
@@ -12885,8 +13764,9 @@ var init_validation = __esm({
12885
13764
  "UNDECLARED_DEPENDENCY_CALL",
12886
13765
  "INVALID_TRUSTED_LINK",
12887
13766
  "CROSS_SUBSYSTEM_NON_ADAPTER",
12888
- "CROSS_TREE_REF_UNRESOLVED",
12889
- // code↔spec conformance (root-relative sourcePaths / import graph)
13767
+ "CROSS_TREE_REF_UNRESOLVED"
13768
+ ]);
13769
+ SUBPROJECT_CONFORMANCE_CODES = /* @__PURE__ */ new Set([
12890
13770
  "MISSING_SOURCE_FILE",
12891
13771
  "SOURCE_PATH_ESCAPES_ROOT",
12892
13772
  "MISSING_SOURCE_PATH",
@@ -13428,6 +14308,7 @@ __export(specs_exports, {
13428
14308
  buildProjectGraph: () => buildProjectGraph,
13429
14309
  clearLoaderIssues: () => clearLoaderIssues,
13430
14310
  collectPromotableSpecs: () => collectPromotableSpecs,
14311
+ computeStateIdAt: () => computeStateIdAt,
13431
14312
  deleteComponentSpec: () => deleteComponentSpec,
13432
14313
  deleteGroupSpec: () => deleteGroupSpec,
13433
14314
  deleteImplementationSpec: () => deleteImplementationSpec,
@@ -13460,6 +14341,7 @@ __export(specs_exports, {
13460
14341
  loadTypeSpec: () => loadTypeSpec,
13461
14342
  loadTypeSpecs: () => loadTypeSpecs,
13462
14343
  normalizeComponentLayout: () => normalizeComponentLayout,
14344
+ resolveChainingParent: () => resolveChainingParent,
13463
14345
  resolveSubprojectForNamespace: () => resolveSubprojectForNamespace,
13464
14346
  restoreSpecFiles: () => restoreSpecFiles,
13465
14347
  saveComponentSpec: () => saveComponentSpec,
@@ -13864,6 +14746,19 @@ function dryRunSerializeSpecs(include) {
13864
14746
  function buildProjectGraph(level) {
13865
14747
  return buildGraphModel(level);
13866
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
+ }
13867
14762
  function deleteTypeSpec(id) {
13868
14763
  return current().deleteTypeSpec(id);
13869
14764
  }
@@ -13907,6 +14802,7 @@ var init_specs2 = __esm({
13907
14802
  path14 = __toESM(require("path"));
13908
14803
  init_loader();
13909
14804
  init_fs();
14805
+ init_statehash();
13910
14806
  init_yaml();
13911
14807
  init_models();
13912
14808
  init_narrative_labels();
@@ -16500,6 +17396,9 @@ function requireSpecs() {
16500
17396
  function requireProvision() {
16501
17397
  return init_provision(), __toCommonJS(provision_exports);
16502
17398
  }
17399
+ function listExternalInterfaces2() {
17400
+ return listExternalInterfaces();
17401
+ }
16503
17402
  function text(content) {
16504
17403
  return { content: [{ type: "text", text: content }] };
16505
17404
  }
@@ -17129,7 +18028,7 @@ NOTICE:
17129
18028
  type: import_zod9.z.enum(["local", "call", "dispatch", "branch", "switch", "loop", "try", "parallel", "jump", "return", "throw"]),
17130
18029
  targetComponent: import_zod9.z.string().optional().describe("call/dispatch: L2 component id (for dispatch, the Portal routed through)"),
17131
18030
  targetMethod: import_zod9.z.string().optional().describe("call: method name on the target"),
17132
- 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` = a secret-store component id, env:API_KEY, a config key, a vault ref). A design note \u2014 wairon never fetches it. Its absence on a call into a Portal whose auth \u2260 none warns (PORTAL_AUTH_UNMET), so credential loading is never overlooked."),
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)."),
17133
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)"),
17134
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)"),
17135
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)"),
@@ -17466,7 +18365,36 @@ NOTICE:
17466
18365
  }
17467
18366
  }
17468
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
+ );
17469
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
+ }
17470
18398
  if (options.hostedTools) {
17471
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.");
17472
18400
  reg(server, "sdd_host_lock_project", {
@@ -17602,6 +18530,8 @@ var init_server = __esm({
17602
18530
  init_narrative_labels();
17603
18531
  init_specs();
17604
18532
  init_skills();
18533
+ init_specs2();
18534
+ init_surfaces();
17605
18535
  SERVER_BUILD_STAMP = captureBuildStamp(__filename);
17606
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.";
17607
18537
  SKILL_RESOURCE_MIME = "text/markdown";
@@ -20675,7 +21605,7 @@ var require_dist = __commonJS({
20675
21605
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
20676
21606
  }
20677
21607
  var fs51 = __toESM2(require("fs"));
20678
- var path62 = __toESM2(require("path"));
21608
+ var path61 = __toESM2(require("path"));
20679
21609
  var import_fflate = require_node();
20680
21610
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".hg", ".svn"]);
20681
21611
  function listEntries(archive) {
@@ -20713,8 +21643,8 @@ var require_dist = __commonJS({
20713
21643
  }
20714
21644
  function writeTree(destDir, files) {
20715
21645
  for (const file of files) {
20716
- const absolute = path62.join(destDir, file.path);
20717
- fs51.mkdirSync(path62.dirname(absolute), { recursive: true });
21646
+ const absolute = path61.join(destDir, file.path);
21647
+ fs51.mkdirSync(path61.dirname(absolute), { recursive: true });
20718
21648
  fs51.writeFileSync(absolute, file.contents);
20719
21649
  }
20720
21650
  }
@@ -20727,10 +21657,10 @@ var require_dist = __commonJS({
20727
21657
  for (const entry of fs51.readdirSync(current2, { withFileTypes: true })) {
20728
21658
  if (entry.isDirectory()) {
20729
21659
  if (SKIP_DIRS.has(entry.name)) continue;
20730
- walkPackDir(root, path62.join(current2, entry.name), out);
21660
+ walkPackDir(root, path61.join(current2, entry.name), out);
20731
21661
  } else if (entry.isFile()) {
20732
- const absolute = path62.join(current2, entry.name);
20733
- 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("/");
20734
21664
  out.push({ path: relative22, contents: fs51.readFileSync(absolute) });
20735
21665
  }
20736
21666
  }
@@ -22141,86 +23071,216 @@ async function generateLayer(options = {}) {
22141
23071
  }
22142
23072
 
22143
23073
  // src/commands/lock.ts
23074
+ var os7 = __toESM(require("os"));
22144
23075
  var import_inquirer2 = __toESM(require("inquirer"));
22145
23076
  init_logger();
22146
- init_loader();
22147
- init_fs();
22148
- init_validation();
22149
- init_specs2();
22150
- async function runLock(options = {}) {
22151
- assertProjectInitialized();
22152
- if (!pathExists(AI_PATHS.specsSystem())) {
22153
- logger.error("No SDD spec tree found (.wai/specs). Nothing to lock.");
22154
- 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) });
22155
23097
  }
22156
- const projectConfig = loadProjectConfig();
22157
- logger.info("Analyzing and validating specifications in-memory...");
22158
- const index = scanAllSpecs({ recursive: options.recursive ?? true });
22159
- const promotable = collectPromotableSpecs(options.subsystem);
22160
- const originalStatuses = /* @__PURE__ */ new Map();
22161
- const isSpecInSubsystemScope = (specSubsystem) => {
22162
- if (!options.subsystem) return true;
22163
- if (!specSubsystem) return false;
22164
- return specSubsystem === options.subsystem || specSubsystem.startsWith(`${options.subsystem}::`);
22165
- };
22166
- for (const s of index.subsystems) {
22167
- if (!options.subsystem || s.id === options.subsystem || s.id.startsWith(`${options.subsystem}::`)) {
22168
- originalStatuses.set(s, s.status);
22169
- 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) });
22170
23101
  }
22171
23102
  }
22172
- for (const c of index.components) {
22173
- if (isSpecInSubsystemScope(c.subsystem)) {
22174
- originalStatuses.set(c, c.status);
22175
- c.status = "complete";
22176
- }
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) });
22177
23113
  }
22178
- for (const i of index.interfaces) {
22179
- const comp = index.components.find((c) => c.id === i.component);
22180
- if (comp && isSpecInSubsystemScope(comp.subsystem)) {
22181
- originalStatuses.set(i, i.status);
22182
- i.status = "complete";
22183
- }
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);
22184
23121
  }
22185
- for (const m of index.implementations) {
22186
- const intf = index.interfaces.find((i) => i.id === m.contract);
22187
- const comp = intf ? index.components.find((c) => c.id === intf.component) : null;
22188
- if (comp && isSpecInSubsystemScope(comp.subsystem)) {
22189
- originalStatuses.set(m, m.status);
22190
- m.status = "complete";
22191
- }
23122
+ for (const c of candidates) {
23123
+ idCount.set(c.suggestedId, (idCount.get(c.suggestedId) ?? 0) + 1);
22192
23124
  }
22193
- const dry = validateSddTree({
22194
- rules: projectConfig.rules,
22195
- projectType: projectConfig.projectType,
22196
- scopeSubsystem: options.subsystem,
22197
- 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 };
22198
23130
  });
22199
- for (const [spec, status2] of originalStatuses.entries()) {
22200
- 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
+ }
22201
23150
  }
22202
- const errors = dry.issues.filter((i) => i.severity === "error");
22203
- if (errors.length > 0) {
22204
- logger.header("Cannot lock \u2014 the spec tree does not validate as complete");
22205
- let errorCount = 0;
22206
- const MAX_PRINT = 100;
22207
- let skippedErrors = 0;
22208
- for (const i of errors) {
22209
- if (errorCount < MAX_PRINT) {
22210
- logger.error(`${i.specId ? `[${i.specId}] ` : ""}[${i.code}] ${i.message}`);
22211
- errorCount++;
22212
- } else {
22213
- skippedErrors++;
22214
- }
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;
22215
23194
  }
22216
- if (skippedErrors > 0) {
22217
- 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
+ });
22218
23226
  }
22219
- logger.blank();
22220
- logger.info("Fix the errors above, then run `wairon lock` again. Nothing was changed.");
22221
- process.exit(1);
23227
+ walkForPackages(projectRoot2, fullPath, depth + 1, results);
22222
23228
  }
22223
- 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);
22224
23284
  if (promotable.length === 0) {
22225
23285
  logger.info("All specs are already complete \u2014 this will re-validate and regenerate the agent topology.");
22226
23286
  } else {
@@ -22244,23 +23304,36 @@ async function runLock(options = {}) {
22244
23304
  default: false
22245
23305
  }
22246
23306
  ]);
22247
- if (!confirmed) {
22248
- logger.info("Cancelled. Nothing was changed.");
22249
- return;
22250
- }
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();
22251
23314
  }
22252
- for (const p of promotable) applySpecStatus(p.kind, p.id, "complete");
22253
- invalidateSpecCache();
22254
23315
  if (promotable.length > 0) {
22255
23316
  logger.success(`Locked ${promotable.length} spec(s) as complete.`);
22256
23317
  }
22257
- logger.blank();
22258
- await runGenerate({ domain: options.subsystem });
22259
- logger.blank();
22260
- logger.success("Specs locked and agent topology generated.");
22261
- logger.warn(
22262
- "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."
22263
- );
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;
22264
23337
  }
22265
23338
 
22266
23339
  // src/commands/validate.ts
@@ -22399,6 +23472,10 @@ async function runValidate(options = {}) {
22399
23472
  }
22400
23473
  }
22401
23474
 
23475
+ // src/cli/index.ts
23476
+ init_loader();
23477
+ init_fs();
23478
+
22402
23479
  // src/commands/list.ts
22403
23480
  var import_chalk7 = __toESM(require("chalk"));
22404
23481
  init_logger();
@@ -22513,9 +23590,9 @@ init_mcp();
22513
23590
  // src/commands/update.ts
22514
23591
  var https = __toESM(require("https"));
22515
23592
  var http = __toESM(require("http"));
22516
- var fs18 = __toESM(require("fs"));
22517
- var path27 = __toESM(require("path"));
22518
- var os7 = __toESM(require("os"));
23593
+ var fs20 = __toESM(require("fs"));
23594
+ var path29 = __toESM(require("path"));
23595
+ var os8 = __toESM(require("os"));
22519
23596
  var crypto2 = __toESM(require("crypto"));
22520
23597
  var import_child_process2 = require("child_process");
22521
23598
  init_logger();
@@ -22578,8 +23655,8 @@ async function runUpdate(options = {}) {
22578
23655
  logger.info(`Download manually from: ${release.html_url}`);
22579
23656
  process.exit(1);
22580
23657
  }
22581
- const tmpDir = os7.tmpdir();
22582
- const tmpFile = path27.join(tmpDir, assetName);
23658
+ const tmpDir = os8.tmpdir();
23659
+ const tmpFile = path29.join(tmpDir, assetName);
22583
23660
  logger.info(`Downloading ${assetName}...`);
22584
23661
  try {
22585
23662
  await downloadFile(asset.browser_download_url, tmpFile);
@@ -22596,16 +23673,16 @@ async function runUpdate(options = {}) {
22596
23673
  const checksumAssetName = assetName + ".sha256";
22597
23674
  const checksumAsset = release.assets.find((a) => a.name === checksumAssetName);
22598
23675
  if (checksumAsset) {
22599
- const tmpChecksum = path27.join(tmpDir, checksumAssetName);
23676
+ const tmpChecksum = path29.join(tmpDir, checksumAssetName);
22600
23677
  logger.info(`Verifying checksum...`);
22601
23678
  try {
22602
23679
  await downloadFile(checksumAsset.browser_download_url, tmpChecksum);
22603
23680
  verifyChecksum(tmpFile, tmpChecksum, assetName);
22604
- fs18.unlinkSync(tmpChecksum);
23681
+ fs20.unlinkSync(tmpChecksum);
22605
23682
  } catch (err) {
22606
23683
  logger.error(`Checksum verification failed: ${err.message}`);
22607
23684
  try {
22608
- fs18.unlinkSync(tmpFile);
23685
+ fs20.unlinkSync(tmpFile);
22609
23686
  } catch {
22610
23687
  }
22611
23688
  process.exit(1);
@@ -22671,7 +23748,7 @@ function fetchReleases(repo) {
22671
23748
  }
22672
23749
  function downloadFile(url, dest) {
22673
23750
  return new Promise((resolve24, reject) => {
22674
- const file = fs18.createWriteStream(dest);
23751
+ const file = fs20.createWriteStream(dest);
22675
23752
  const get3 = url.startsWith("https://") ? https.get : http.get;
22676
23753
  get3(url, { headers: { "User-Agent": `wairon/${WAIRON_VERSION}` }, agent: false }, (res) => {
22677
23754
  if (res.statusCode === 301 || res.statusCode === 302) {
@@ -22693,21 +23770,21 @@ function downloadFile(url, dest) {
22693
23770
  });
22694
23771
  file.on("error", (err) => {
22695
23772
  res.destroy();
22696
- fs18.unlink(dest, () => {
23773
+ fs20.unlink(dest, () => {
22697
23774
  });
22698
23775
  reject(err);
22699
23776
  });
22700
23777
  }).on("error", (err) => {
22701
- fs18.unlink(dest, () => {
23778
+ fs20.unlink(dest, () => {
22702
23779
  });
22703
23780
  reject(err);
22704
23781
  });
22705
23782
  });
22706
23783
  }
22707
23784
  function verifyChecksum(filePath, checksumFile, expectedFilename) {
22708
- const checksumContent = fs18.readFileSync(checksumFile, "utf-8").trim();
23785
+ const checksumContent = fs20.readFileSync(checksumFile, "utf-8").trim();
22709
23786
  const expectedHash = checksumContent.split(/\s+/)[0].toLowerCase();
22710
- const fileBuffer = fs18.readFileSync(filePath);
23787
+ const fileBuffer = fs20.readFileSync(filePath);
22711
23788
  const actualHash = crypto2.createHash("sha256").update(fileBuffer).digest("hex").toLowerCase();
22712
23789
  if (actualHash !== expectedHash) {
22713
23790
  throw new Error(
@@ -22738,9 +23815,9 @@ function isPkgBinary2() {
22738
23815
  function installBinary(tmpFile, destPath) {
22739
23816
  const platform = process.platform;
22740
23817
  const isZip = tmpFile.endsWith(".zip");
22741
- const extractDir = path27.join(os7.tmpdir(), "wairon-extract");
22742
- if (fs18.existsSync(extractDir)) fs18.rmSync(extractDir, { recursive: true });
22743
- 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 });
22744
23821
  if (isZip) {
22745
23822
  (0, import_child_process2.execSync)(
22746
23823
  `powershell -NoProfile -NonInteractive -Command "Expand-Archive -Path '${tmpFile}' -DestinationPath '${extractDir}' -Force"`,
@@ -22750,18 +23827,18 @@ function installBinary(tmpFile, destPath) {
22750
23827
  (0, import_child_process2.execSync)(`tar -xzf "${tmpFile}" -C "${extractDir}"`, { stdio: ["ignore", "pipe", "pipe"] });
22751
23828
  }
22752
23829
  const binaryName = platform === "win32" ? "wairon.exe" : "wairon";
22753
- const extractedBinary = path27.join(extractDir, binaryName);
22754
- if (!fs18.existsSync(extractedBinary)) {
23830
+ const extractedBinary = path29.join(extractDir, binaryName);
23831
+ if (!fs20.existsSync(extractedBinary)) {
22755
23832
  throw new Error(`Extracted binary not found at ${extractedBinary}`);
22756
23833
  }
22757
23834
  if (platform === "win32") {
22758
23835
  const oldPath = destPath + ".old";
22759
23836
  try {
22760
23837
  cleanStaleBinary(oldPath);
22761
- fs18.renameSync(destPath, oldPath);
22762
- fs18.copyFileSync(extractedBinary, destPath);
23838
+ fs20.renameSync(destPath, oldPath);
23839
+ fs20.copyFileSync(extractedBinary, destPath);
22763
23840
  try {
22764
- fs18.unlinkSync(oldPath);
23841
+ fs20.unlinkSync(oldPath);
22765
23842
  } catch {
22766
23843
  }
22767
23844
  } catch (err) {
@@ -22775,25 +23852,25 @@ function installBinary(tmpFile, destPath) {
22775
23852
  }
22776
23853
  } else {
22777
23854
  const tmpDest = destPath + ".new";
22778
- fs18.copyFileSync(extractedBinary, tmpDest);
22779
- fs18.chmodSync(tmpDest, 493);
22780
- fs18.renameSync(tmpDest, destPath);
23855
+ fs20.copyFileSync(extractedBinary, tmpDest);
23856
+ fs20.chmodSync(tmpDest, 493);
23857
+ fs20.renameSync(tmpDest, destPath);
22781
23858
  }
22782
23859
  try {
22783
- fs18.unlinkSync(tmpFile);
23860
+ fs20.unlinkSync(tmpFile);
22784
23861
  } catch {
22785
23862
  }
22786
23863
  try {
22787
- fs18.rmSync(extractDir, { recursive: true });
23864
+ fs20.rmSync(extractDir, { recursive: true });
22788
23865
  } catch {
22789
23866
  }
22790
23867
  }
22791
23868
  function cleanStaleBinary(oldPath) {
22792
23869
  const target = oldPath ?? (isPkgBinary2() ? process.execPath + ".old" : null);
22793
23870
  if (!target) return;
22794
- if (fs18.existsSync(target)) {
23871
+ if (fs20.existsSync(target)) {
22795
23872
  try {
22796
- fs18.unlinkSync(target);
23873
+ fs20.unlinkSync(target);
22797
23874
  } catch {
22798
23875
  }
22799
23876
  }
@@ -23039,171 +24116,6 @@ async function filteredCheckbox(config) {
23039
24116
 
23040
24117
  // src/commands/domains.ts
23041
24118
  init_loader();
23042
-
23043
- // src/core/detection.ts
23044
- var fs19 = __toESM(require("fs"));
23045
- var path28 = __toESM(require("path"));
23046
- init_defaults();
23047
- var PACKAGE_MARKERS = [
23048
- "package.json",
23049
- "pyproject.toml",
23050
- "Cargo.toml",
23051
- "go.mod",
23052
- "build.gradle",
23053
- "build.gradle.kts",
23054
- "pom.xml"
23055
- ];
23056
- var MAX_SCAN_DEPTH = 5;
23057
- function detectDomainCandidates(projectRoot2, alreadyTrackedPaths = /* @__PURE__ */ new Set(), alreadyTrackedIds = /* @__PURE__ */ new Set()) {
23058
- const candidates = /* @__PURE__ */ new Map();
23059
- for (const c of detectGitSubmodules(projectRoot2)) {
23060
- candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
23061
- }
23062
- for (const c of detectNestedGitRepos(projectRoot2)) {
23063
- if (!candidates.has(c.path)) {
23064
- candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
23065
- }
23066
- }
23067
- const gitPaths = new Set(
23068
- Array.from(candidates.values()).filter((c) => c.type === "git-submodule" || c.type === "git-repo").map((c) => c.path)
23069
- );
23070
- for (const c of detectPackageRoots(projectRoot2)) {
23071
- if (candidates.has(c.path)) continue;
23072
- const insideGit = Array.from(gitPaths).some(
23073
- (gp) => c.path === gp || c.path.startsWith(gp + "/")
23074
- );
23075
- if (insideGit) continue;
23076
- candidates.set(c.path, { ...c, alreadyTracked: alreadyTrackedPaths.has(c.path) });
23077
- }
23078
- const sorted = Array.from(candidates.values()).sort((a, b) => a.path.localeCompare(b.path));
23079
- return deduplicateIds(sorted, alreadyTrackedIds);
23080
- }
23081
- function deduplicateIds(candidates, existingIds = /* @__PURE__ */ new Set()) {
23082
- const idCount = /* @__PURE__ */ new Map();
23083
- for (const id of existingIds) {
23084
- idCount.set(id, (idCount.get(id) ?? 0) + 1);
23085
- }
23086
- for (const c of candidates) {
23087
- idCount.set(c.suggestedId, (idCount.get(c.suggestedId) ?? 0) + 1);
23088
- }
23089
- return candidates.map((c) => {
23090
- if ((idCount.get(c.suggestedId) ?? 0) <= 1) return c;
23091
- const parts = c.path.split("/");
23092
- const qualifiedId2 = parts.length >= 2 ? pathToId(`${parts[parts.length - 2]}-${parts[parts.length - 1]}`) : c.suggestedId;
23093
- return { ...c, suggestedId: qualifiedId2 };
23094
- });
23095
- }
23096
- function parseGitmodules(filePath) {
23097
- const content = fs19.readFileSync(filePath, "utf-8");
23098
- const entries = [];
23099
- let current2 = {};
23100
- for (const line2 of content.split("\n")) {
23101
- const trimmed = line2.trim();
23102
- const headerMatch = trimmed.match(/^\[submodule "(.+)"\]$/);
23103
- if (headerMatch) {
23104
- if (current2.path) entries.push(current2);
23105
- current2 = { name: headerMatch[1] };
23106
- continue;
23107
- }
23108
- const keyVal = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
23109
- if (keyVal) {
23110
- const [, key, value] = keyVal;
23111
- if (key === "path") current2.path = value.trim();
23112
- if (key === "url") current2.url = value.trim();
23113
- }
23114
- }
23115
- if (current2.path) entries.push(current2);
23116
- return entries;
23117
- }
23118
- function detectGitSubmodules(projectRoot2) {
23119
- const gitmodulesPath = path28.join(projectRoot2, ".gitmodules");
23120
- if (!fs19.existsSync(gitmodulesPath)) return [];
23121
- return parseGitmodules(gitmodulesPath).map((entry) => ({
23122
- suggestedId: pathToId(entry.path),
23123
- suggestedName: pathToName(entry.path),
23124
- path: normalizePath3(entry.path),
23125
- type: "git-submodule",
23126
- alreadyTracked: false
23127
- }));
23128
- }
23129
- function detectNestedGitRepos(projectRoot2) {
23130
- const results = [];
23131
- walkForGit(projectRoot2, projectRoot2, 0, results);
23132
- return results;
23133
- }
23134
- function walkForGit(projectRoot2, currentDir, depth, results) {
23135
- if (depth > MAX_SCAN_DEPTH) return;
23136
- let entries;
23137
- try {
23138
- entries = fs19.readdirSync(currentDir, { withFileTypes: true });
23139
- } catch {
23140
- return;
23141
- }
23142
- for (const entry of entries) {
23143
- if (!entry.isDirectory()) continue;
23144
- if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
23145
- const fullPath = path28.join(currentDir, entry.name);
23146
- const relPath = normalizePath3(path28.relative(projectRoot2, fullPath));
23147
- if (relPath === "" || relPath === ".") continue;
23148
- const gitPath = path28.join(fullPath, ".git");
23149
- if (fs19.existsSync(gitPath)) {
23150
- results.push({
23151
- suggestedId: pathToId(relPath),
23152
- suggestedName: pathToName(relPath),
23153
- path: relPath,
23154
- type: "git-repo",
23155
- alreadyTracked: false
23156
- });
23157
- continue;
23158
- }
23159
- walkForGit(projectRoot2, fullPath, depth + 1, results);
23160
- }
23161
- }
23162
- function detectPackageRoots(projectRoot2) {
23163
- const results = [];
23164
- walkForPackages(projectRoot2, projectRoot2, 0, results);
23165
- return results;
23166
- }
23167
- function walkForPackages(projectRoot2, currentDir, depth, results) {
23168
- if (depth > MAX_SCAN_DEPTH) return;
23169
- let entries;
23170
- try {
23171
- entries = fs19.readdirSync(currentDir, { withFileTypes: true });
23172
- } catch {
23173
- return;
23174
- }
23175
- for (const entry of entries) {
23176
- if (!entry.isDirectory()) continue;
23177
- if (SCAN_EXCLUDE_DIRS.has(entry.name)) continue;
23178
- const fullPath = path28.join(currentDir, entry.name);
23179
- const relPath = normalizePath3(path28.relative(projectRoot2, fullPath));
23180
- if (relPath === "" || relPath === ".") continue;
23181
- const hasMarker = PACKAGE_MARKERS.some((m) => fs19.existsSync(path28.join(fullPath, m)));
23182
- if (hasMarker) {
23183
- results.push({
23184
- suggestedId: pathToId(relPath),
23185
- suggestedName: pathToName(relPath),
23186
- path: relPath,
23187
- type: "package-root",
23188
- alreadyTracked: false
23189
- });
23190
- }
23191
- walkForPackages(projectRoot2, fullPath, depth + 1, results);
23192
- }
23193
- }
23194
- function pathToId(relPath) {
23195
- const basename12 = path28.basename(relPath);
23196
- return basename12.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
23197
- }
23198
- function pathToName(relPath) {
23199
- const id = pathToId(relPath);
23200
- return id.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
23201
- }
23202
- function normalizePath3(p) {
23203
- return p.replace(/\\/g, "/");
23204
- }
23205
-
23206
- // src/commands/domains.ts
23207
24119
  init_domains();
23208
24120
  init_domain();
23209
24121
  async function runDomainsList() {
@@ -23393,9 +24305,9 @@ async function runSkillsInstall() {
23393
24305
  }
23394
24306
 
23395
24307
  // src/commands/doctor.ts
23396
- var fs20 = __toESM(require("fs"));
23397
- var os8 = __toESM(require("os"));
23398
- var path29 = __toESM(require("path"));
24308
+ var fs21 = __toESM(require("fs"));
24309
+ var os9 = __toESM(require("os"));
24310
+ var path30 = __toESM(require("path"));
23399
24311
  var import_chalk12 = __toESM(require("chalk"));
23400
24312
  init_logger();
23401
24313
  init_defaults();
@@ -23425,10 +24337,10 @@ function stampVerdict(content) {
23425
24337
  return { mark: "warn", note: `v${v} \u2014 stale, installed is v${WAIRON_VERSION}` };
23426
24338
  }
23427
24339
  function mcpEntryHealth(settingsPath) {
23428
- if (!fs20.existsSync(settingsPath)) return { mark: "warn", note: "not registered" };
24340
+ if (!fs21.existsSync(settingsPath)) return { mark: "warn", note: "not registered" };
23429
24341
  let entry;
23430
24342
  try {
23431
- const s = JSON.parse(fs20.readFileSync(settingsPath, "utf8"));
24343
+ const s = JSON.parse(fs21.readFileSync(settingsPath, "utf8"));
23432
24344
  entry = s.mcpServers?.["wairon"];
23433
24345
  } catch {
23434
24346
  return { mark: "error", note: "parse error" };
@@ -23436,7 +24348,7 @@ function mcpEntryHealth(settingsPath) {
23436
24348
  if (!entry) return { mark: "warn", note: "not registered" };
23437
24349
  if (entry.command === "node" && Array.isArray(entry.args) && typeof entry.args[0] === "string") {
23438
24350
  const scriptPath = entry.args[0];
23439
- if (!fs20.existsSync(scriptPath)) {
24351
+ if (!fs21.existsSync(scriptPath)) {
23440
24352
  return { mark: "error", note: `registered but the server path is missing \u2014 ${scriptPath}` };
23441
24353
  }
23442
24354
  }
@@ -23488,7 +24400,7 @@ async function runDoctor(options = {}) {
23488
24400
  const { findChainingSubprojectsMissingConfig: findChainingSubprojectsMissingConfig2 } = (init_provision(), __toCommonJS(provision_exports));
23489
24401
  const missing = findChainingSubprojectsMissingConfig2(getProjectRoot());
23490
24402
  if (missing.length > 0) {
23491
- 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.`);
23492
24404
  }
23493
24405
  } catch {
23494
24406
  }
@@ -23526,7 +24438,7 @@ async function runDoctor(options = {}) {
23526
24438
  const gp = localGuideFilePath(process.cwd(), t);
23527
24439
  if (!gp || seenGuides.has(gp)) continue;
23528
24440
  seenGuides.add(gp);
23529
- const rel2 = path29.relative(process.cwd(), gp).replace(/\\/g, "/");
24441
+ const rel2 = path30.relative(process.cwd(), gp).replace(/\\/g, "/");
23530
24442
  if (!pathExists(gp)) {
23531
24443
  line(tally, "warn", `${rel2} guide \u2014 not injected (run \`wairon generate\`)`);
23532
24444
  continue;
@@ -23560,17 +24472,17 @@ async function runDoctor(options = {}) {
23560
24472
  line(tally, h.mark, `Claude (project .mcp.json): ${h.note}${h.mark === "ok" ? "" : " \u2014 run `wairon mcp install --backend claude`"}`);
23561
24473
  }
23562
24474
  if (wantGemini) {
23563
- 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");
23564
24476
  const hg = mcpEntryHealth(globalCfg);
23565
24477
  line(tally, hg.mark, `Antigravity (global mcp_config.json): ${hg.note}${hg.mark === "ok" ? "" : " \u2014 run `wairon mcp install --backend gemini --global`"}`);
23566
24478
  const projPath = fromProjectRoot(".gemini", "settings.json");
23567
- if (fs20.existsSync(projPath)) {
24479
+ if (fs21.existsSync(projPath)) {
23568
24480
  const hp = mcpEntryHealth(projPath);
23569
24481
  line(tally, hp.mark === "error" ? "error" : "ok", `Gemini CLI (project): ${hp.note} ${import_chalk12.default.gray("(Antigravity ignores this file)")}`);
23570
24482
  }
23571
24483
  }
23572
- const pluginDir = path29.join(os8.homedir(), ".gemini", "config", "plugins", "wairon");
23573
- if (fs20.existsSync(pluginDir)) {
24484
+ const pluginDir = path30.join(os9.homedir(), ".gemini", "config", "plugins", "wairon");
24485
+ if (fs21.existsSync(pluginDir)) {
23574
24486
  line(tally, "warn", `Legacy Antigravity plugin present (${pluginDir}) \u2014 it collides with the wairon MCP server. Remove it with \`wairon doctor --fix\`.`);
23575
24487
  }
23576
24488
  logger.blank();
@@ -23601,7 +24513,7 @@ async function applyFixes() {
23601
24513
  const legacySpecs = findLegacySpecFiles();
23602
24514
  if (legacySpecs.length > 0) {
23603
24515
  for (const { path: oldPath, expected: newPath } of legacySpecs) {
23604
- fs20.renameSync(oldPath, newPath);
24516
+ fs21.renameSync(oldPath, newPath);
23605
24517
  }
23606
24518
  console.log(` ${icon("ok")} Migrated ${legacySpecs.length} legacy spec file(s) to the new dot-prefixed unified schema.`);
23607
24519
  }
@@ -23655,8 +24567,8 @@ function printSummary(tally) {
23655
24567
  }
23656
24568
 
23657
24569
  // src/commands/diagram.ts
23658
- var fs21 = __toESM(require("fs"));
23659
- var path30 = __toESM(require("path"));
24570
+ var fs22 = __toESM(require("fs"));
24571
+ var path31 = __toESM(require("path"));
23660
24572
  init_logger();
23661
24573
  init_loader();
23662
24574
  init_fs();
@@ -23691,8 +24603,8 @@ function collectIssues() {
23691
24603
  }
23692
24604
  function writeCanvas(dest) {
23693
24605
  const model = buildCanvasModel(collectIssues());
23694
- ensureDir(path30.dirname(path30.resolve(dest)));
23695
- fs21.writeFileSync(dest, renderCanvasHtml(model), "utf-8");
24606
+ ensureDir(path31.dirname(path31.resolve(dest)));
24607
+ fs22.writeFileSync(dest, renderCanvasHtml(model), "utf-8");
23696
24608
  }
23697
24609
  function parseSequenceRef(ref) {
23698
24610
  const sep6 = ref.includes(":") ? ref.lastIndexOf(":") : ref.lastIndexOf(".");
@@ -23707,55 +24619,55 @@ async function runDiagram(rawOptions = {}) {
23707
24619
  assertProjectInitialized();
23708
24620
  const options = applyFormat(rawOptions);
23709
24621
  if (options.canvas && !options.all) {
23710
- 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");
23711
24623
  writeCanvas(dest2);
23712
24624
  logger.success(`Interactive canvas written to ${dest2}`);
23713
24625
  logger.info("Open it in a browser \u2014 fully self-contained (works offline).");
23714
24626
  return;
23715
24627
  }
23716
24628
  if (options.drawio && !options.all) {
23717
- const dest2 = options.out ?? path30.join(AI_PATHS.docsDir(), "diagrams", "architecture.drawio");
23718
- ensureDir(path30.dirname(path30.resolve(dest2)));
23719
- 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");
23720
24632
  logger.success(`draw.io diagram written to ${dest2}`);
23721
24633
  logger.info("Open with draw.io / diagrams.net (or import into tools that accept the format).");
23722
24634
  return;
23723
24635
  }
23724
24636
  if (options.excalidraw && !options.all) {
23725
- const dest2 = options.out ?? path30.join(AI_PATHS.docsDir(), "diagrams", "architecture.excalidraw");
23726
- ensureDir(path30.dirname(path30.resolve(dest2)));
23727
- 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");
23728
24640
  logger.success(`Excalidraw scene written to ${dest2}`);
23729
24641
  logger.info("Open with excalidraw.com or the VS Code extension.");
23730
24642
  return;
23731
24643
  }
23732
24644
  const wantsMermaid = options.format?.toLowerCase().startsWith("mermaid") || !!options.subsystem || !!options.sequence;
23733
24645
  if (!options.all && !options.sequence && !wantsMermaid) {
23734
- 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");
23735
24647
  writeCanvas(dest2);
23736
24648
  logger.success(`Interactive canvas written to ${dest2}`);
23737
24649
  logger.info("Open it in a browser \u2014 fully self-contained (works offline). Other formats: --format mermaid|drawio|excalidraw.");
23738
24650
  return;
23739
24651
  }
23740
24652
  if (options.all) {
23741
- const outDir = options.out ?? path30.join(AI_PATHS.docsDir(), "diagrams");
24653
+ const outDir = options.out ?? path31.join(AI_PATHS.docsDir(), "diagrams");
23742
24654
  const files = generateDiagramSet();
23743
24655
  if (files.length === 0) {
23744
24656
  logger.warn("No diagrams to generate \u2014 the spec tree has no components yet.");
23745
24657
  return;
23746
24658
  }
23747
24659
  for (const file of files) {
23748
- const dest2 = path30.join(outDir, file.relPath);
23749
- ensureDir(path30.dirname(dest2));
23750
- 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");
23751
24663
  }
23752
- writeCanvas(path30.join(outDir, "canvas.html"));
24664
+ writeCanvas(path31.join(outDir, "canvas.html"));
23753
24665
  const exportModel = buildCanvasModel();
23754
- fs21.writeFileSync(path30.join(outDir, "architecture.drawio"), generateDrawioXml(exportModel), "utf-8");
23755
- 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");
23756
24668
  const graph = loadSpecGraph();
23757
- const indexPath = path30.join(outDir, "README.md");
23758
- 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");
23759
24671
  logger.success(`Generated ${files.length} diagram(s) + interactive canvas.html + index into ${outDir}`);
23760
24672
  for (const file of files.slice(0, 12)) {
23761
24673
  logger.info(` ${file.relPath}`);
@@ -23766,26 +24678,26 @@ async function runDiagram(rawOptions = {}) {
23766
24678
  let mermaid;
23767
24679
  let title;
23768
24680
  let defaultDest;
23769
- const diagramsDir = path30.join(AI_PATHS.docsDir(), "diagrams");
24681
+ const diagramsDir = path31.join(AI_PATHS.docsDir(), "diagrams");
23770
24682
  if (options.sequence) {
23771
24683
  const { component, method: method2 } = parseSequenceRef(options.sequence);
23772
24684
  mermaid = generateSequenceDiagram(component, method2, { depth: options.depth });
23773
24685
  title = `${component}.${method2} \u2014 narrative sequence`;
23774
- defaultDest = path30.join(diagramsDir, "sequences", `${component.replace(/::/g, "--")}.${method2}.md`);
24686
+ defaultDest = path31.join(diagramsDir, "sequences", `${component.replace(/::/g, "--")}.${method2}.md`);
23775
24687
  } else if (options.subsystem) {
23776
24688
  mermaid = generateComponentDiagram({ subsystem: options.subsystem });
23777
24689
  title = `${options.subsystem} \u2014 components`;
23778
- defaultDest = path30.join(diagramsDir, "subsystems", `${options.subsystem.replace(/::/g, "--")}.md`);
24690
+ defaultDest = path31.join(diagramsDir, "subsystems", `${options.subsystem.replace(/::/g, "--")}.md`);
23779
24691
  } else {
23780
24692
  mermaid = generateComponentDiagram();
23781
24693
  title = "Component architecture";
23782
- defaultDest = path30.join(diagramsDir, "system.md");
24694
+ defaultDest = path31.join(diagramsDir, "system.md");
23783
24695
  }
23784
24696
  const dest = options.out ?? defaultDest;
23785
- ensureDir(path30.dirname(path30.resolve(dest)));
24697
+ ensureDir(path31.dirname(path31.resolve(dest)));
23786
24698
  const content = dest.endsWith(".mmd") ? `${mermaid}
23787
24699
  ` : toMarkdown({ relPath: dest, title, mermaid });
23788
- fs21.writeFileSync(dest, content, "utf-8");
24700
+ fs22.writeFileSync(dest, content, "utf-8");
23789
24701
  logger.success(`Mermaid diagram written to ${dest}`);
23790
24702
  logger.info("Renders on GitHub/IDE previews; use a .mmd --out path for raw Mermaid.");
23791
24703
  }
@@ -23894,8 +24806,8 @@ Component variants (${variants.length})
23894
24806
  }
23895
24807
 
23896
24808
  // src/commands/packs.ts
23897
- var fs22 = __toESM(require("fs"));
23898
- var path31 = __toESM(require("path"));
24809
+ var fs23 = __toESM(require("fs"));
24810
+ var path32 = __toESM(require("path"));
23899
24811
  var import_chalk16 = __toESM(require("chalk"));
23900
24812
  var import_sdk = __toESM(require_dist());
23901
24813
  init_logger();
@@ -23921,11 +24833,11 @@ function describe(probe2) {
23921
24833
  return parts.join(", ");
23922
24834
  }
23923
24835
  function resolveSourceUnit(source) {
23924
- const abs = path31.resolve(source);
23925
- if (!fs22.existsSync(abs)) {
24836
+ const abs = path32.resolve(source);
24837
+ if (!fs23.existsSync(abs)) {
23926
24838
  throw new Error(`Pack source "${source}" does not exist.`);
23927
24839
  }
23928
- const isDir = fs22.statSync(abs).isDirectory();
24840
+ const isDir = fs23.statSync(abs).isDirectory();
23929
24841
  if (isDir && !packDirEntry(abs)) {
23930
24842
  throw new Error(`"${source}" is a directory without a pack entry file (pack.yaml | pack.cjs | index.cjs | ...).`);
23931
24843
  }
@@ -23941,7 +24853,7 @@ async function addPack(source, options = {}) {
23941
24853
  }
23942
24854
  const { abs } = resolveSourceUnit(source);
23943
24855
  const scope = options.global ? "global" : "project";
23944
- const probe2 = probePack(abs, path31.dirname(abs), scope);
24856
+ const probe2 = probePack(abs, path32.dirname(abs), scope);
23945
24857
  if (probe2.error) {
23946
24858
  logger.error(probe2.error);
23947
24859
  process.exitCode = 1;
@@ -23949,10 +24861,10 @@ async function addPack(source, options = {}) {
23949
24861
  }
23950
24862
  if (options.global) {
23951
24863
  const destDir = globalPacksDir();
23952
- const dest2 = path31.join(destDir, path31.basename(abs));
23953
- if (path31.resolve(dest2) !== abs) {
23954
- fs22.mkdirSync(destDir, { recursive: true });
23955
- 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 });
23956
24868
  }
23957
24869
  logger.success(`Installed pack "${probe2.name}" globally: ${dest2}`);
23958
24870
  logger.info(`${describe(probe2)} \u2014 auto-loaded for every project on this machine (WAIRON_PACKS_DIR / ~/.wairon/packs).`);
@@ -23965,11 +24877,11 @@ async function addPack(source, options = {}) {
23965
24877
  return;
23966
24878
  }
23967
24879
  const root = getProjectRoot();
23968
- const relRef = `.wai/packs/${path31.basename(abs)}`;
23969
- const dest = path31.join(root, ".wai", "packs", path31.basename(abs));
23970
- if (path31.resolve(dest) !== abs) {
23971
- fs22.mkdirSync(path31.dirname(dest), { recursive: true });
23972
- 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 });
23973
24885
  }
23974
24886
  const config = loadProjectConfig();
23975
24887
  const packs = config.extensions?.packs ?? [];
@@ -23978,14 +24890,14 @@ async function addPack(source, options = {}) {
23978
24890
  saveProjectConfig(config);
23979
24891
  logger.success(`Vendored pack "${probe2.name}" into ${relRef} and registered it in .wai/project.yaml.`);
23980
24892
  } else {
23981
- fs22.cpSync(abs, dest, { recursive: true, force: true });
24893
+ fs23.cpSync(abs, dest, { recursive: true, force: true });
23982
24894
  logger.success(`Pack "${probe2.name}" already registered \u2014 refreshed ${relRef} from the source.`);
23983
24895
  }
23984
24896
  logger.info(`${describe(probe2)} \u2014 commit .wai/ so CI and every clone enforce it.`);
23985
24897
  }
23986
24898
  async function addPackFromArchive(source, options) {
23987
- const abs = path31.resolve(source);
23988
- if (!fs22.existsSync(abs) || !fs22.statSync(abs).isFile()) {
24899
+ const abs = path32.resolve(source);
24900
+ if (!fs23.existsSync(abs) || !fs23.statSync(abs).isFile()) {
23989
24901
  logger.error(`Pack archive "${source}" does not exist.`);
23990
24902
  process.exitCode = 1;
23991
24903
  return;
@@ -24000,27 +24912,27 @@ async function addPackFromArchive(source, options) {
24000
24912
  process.exitCode = 1;
24001
24913
  return;
24002
24914
  }
24003
- baseDir = path31.join(getProjectRoot(), ".wai", "packs");
24915
+ baseDir = path32.join(getProjectRoot(), ".wai", "packs");
24004
24916
  }
24005
- const bytes = fs22.readFileSync(abs);
24006
- fs22.mkdirSync(baseDir, { recursive: true });
24007
- 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-"));
24008
24920
  let result;
24009
24921
  try {
24010
24922
  result = (0, import_sdk.extractPack)(bytes, staging);
24011
24923
  } catch (err) {
24012
- fs22.rmSync(staging, { recursive: true, force: true });
24013
- 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)}`);
24014
24926
  process.exitCode = 1;
24015
24927
  return;
24016
24928
  }
24017
24929
  const name = result.name;
24018
- const destDir = path31.join(baseDir, name);
24019
- if (fs22.existsSync(destDir)) fs22.rmSync(destDir, { recursive: true, force: true });
24020
- fs22.renameSync(staging, destDir);
24021
- 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);
24022
24934
  if (probe2.error) {
24023
- fs22.rmSync(destDir, { recursive: true, force: true });
24935
+ fs23.rmSync(destDir, { recursive: true, force: true });
24024
24936
  logger.error(probe2.error);
24025
24937
  process.exitCode = 1;
24026
24938
  return;
@@ -24038,7 +24950,7 @@ async function addPackFromArchive(source, options) {
24038
24950
  saveProjectConfig(config);
24039
24951
  logger.success(`Installed pack "${probe2.name ?? name}" into ${relRef} and registered it in .wai/project.yaml.`);
24040
24952
  } else {
24041
- 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)}.`);
24042
24954
  }
24043
24955
  logger.info(`${describe(probe2)} \u2014 commit .wai/ so CI and every clone enforce it.`);
24044
24956
  }
@@ -24059,7 +24971,7 @@ async function buildPack(source, options = {}) {
24059
24971
  const sourceDir = source && source.length > 0 ? source : ".";
24060
24972
  const result = (0, import_sdk.buildPack)(sourceDir);
24061
24973
  const outPath = options.out ?? result.suggestedFileName;
24062
- fs22.writeFileSync(outPath, result.archive);
24974
+ fs23.writeFileSync(outPath, result.archive);
24063
24975
  logger.success(`Built pack "${result.info.name}" v${result.info.version} \u2192 ${outPath} (${result.archive.byteLength} bytes)`);
24064
24976
  logger.info(`Install it with \`wairon pack add ${outPath}\`, or upload it to a hosted instance.`);
24065
24977
  }
@@ -24073,9 +24985,9 @@ async function listPacks() {
24073
24985
  console.log(import_chalk16.default.bold.cyan(`\u25A0 Global (${globalPacksDir()})${useGlobal ? "" : import_chalk16.default.yellow(" [disabled: extensions.useGlobalPacks: false]")}`));
24074
24986
  if (globalRefs.length === 0) console.log(import_chalk16.default.dim(" (none)"));
24075
24987
  for (const ref of globalRefs) {
24076
- const probe2 = probePack(ref, path31.dirname(ref), "global");
24077
- if (probe2.error) console.log(` ${import_chalk16.default.red("\u2716")} ${path31.basename(ref)} \u2014 ${import_chalk16.default.red(probe2.error)}`);
24078
- 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))}`);
24079
24991
  }
24080
24992
  console.log("");
24081
24993
  if (!inProject) {
@@ -24096,9 +25008,9 @@ async function listPacks() {
24096
25008
  async function removePack(name, options = {}) {
24097
25009
  if (options.global) {
24098
25010
  for (const ref of discoverPacks(globalPacksDir())) {
24099
- const probe2 = probePack(ref, path31.dirname(ref), "global");
24100
- if (probe2.name === name || path31.basename(ref) === name) {
24101
- 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 });
24102
25014
  logger.success(`Removed global pack "${probe2.name ?? name}" (${ref}).`);
24103
25015
  return;
24104
25016
  }
@@ -24117,16 +25029,16 @@ async function removePack(name, options = {}) {
24117
25029
  const packs = config.extensions?.packs ?? [];
24118
25030
  for (const ref of packs) {
24119
25031
  const probe2 = probePack(ref, root, "project");
24120
- if (probe2.name === name || ref === name || path31.basename(ref) === name) {
25032
+ if (probe2.name === name || ref === name || path32.basename(ref) === name) {
24121
25033
  config.extensions = {
24122
25034
  packs: packs.filter((p) => p !== ref),
24123
25035
  useGlobalPacks: config.extensions?.useGlobalPacks ?? true
24124
25036
  };
24125
25037
  saveProjectConfig(config);
24126
- const resolved = path31.resolve(root, ref);
24127
- const vendorDir = path31.resolve(root, ".wai", "packs");
24128
- if (resolved.startsWith(vendorDir + path31.sep)) {
24129
- 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 });
24130
25042
  logger.success(`Deregistered pack "${probe2.name ?? name}" and deleted ${ref}.`);
24131
25043
  } else {
24132
25044
  logger.success(`Deregistered pack "${probe2.name ?? name}" (files at ${ref} left in place).`);
@@ -24140,8 +25052,8 @@ async function removePack(name, options = {}) {
24140
25052
 
24141
25053
  // src/commands/host.ts
24142
25054
  var fs50 = __toESM(require("fs"));
24143
- var path60 = __toESM(require("path"));
24144
- var os9 = __toESM(require("os"));
25055
+ var path59 = __toESM(require("path"));
25056
+ var os10 = __toESM(require("os"));
24145
25057
  var crypto20 = __toESM(require("crypto"));
24146
25058
  var import_child_process5 = require("child_process");
24147
25059
  var import_chalk17 = __toESM(require("chalk"));
@@ -24168,29 +25080,29 @@ var UNAUTHENTICATED = {
24168
25080
  var WEB_SESSION_PREFIX = "ws_";
24169
25081
 
24170
25082
  // src/server/credentials.ts
24171
- var fs23 = __toESM(require("fs"));
24172
- var path32 = __toESM(require("path"));
25083
+ var fs24 = __toESM(require("fs"));
25084
+ var path33 = __toESM(require("path"));
24173
25085
  var crypto3 = __toESM(require("crypto"));
24174
25086
  var HASH_NS = "wairon:token:v1";
24175
25087
  function hashToken(token) {
24176
25088
  return crypto3.createHash("sha256").update(`${HASH_NS}:${token}`).digest("hex");
24177
25089
  }
24178
25090
  function storePath(dataDir) {
24179
- return path32.join(dataDir, "auth", "credentials.json");
25091
+ return path33.join(dataDir, "auth", "credentials.json");
24180
25092
  }
24181
25093
  function load3(dataDir) {
24182
25094
  try {
24183
- return JSON.parse(fs23.readFileSync(storePath(dataDir), "utf8"));
25095
+ return JSON.parse(fs24.readFileSync(storePath(dataDir), "utf8"));
24184
25096
  } catch {
24185
25097
  return [];
24186
25098
  }
24187
25099
  }
24188
25100
  function save(dataDir, records) {
24189
25101
  const p = storePath(dataDir);
24190
- fs23.mkdirSync(path32.dirname(p), { recursive: true });
25102
+ fs24.mkdirSync(path33.dirname(p), { recursive: true });
24191
25103
  const tmp = `${p}.tmp`;
24192
- fs23.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
24193
- fs23.renameSync(tmp, p);
25104
+ fs24.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
25105
+ fs24.renameSync(tmp, p);
24194
25106
  }
24195
25107
  function digestEquals(a, b) {
24196
25108
  const ab = Buffer.from(a, "hex");
@@ -24235,17 +25147,17 @@ function listByOwner(dataDir, ownerUserId) {
24235
25147
  }
24236
25148
 
24237
25149
  // src/server/websessions.ts
24238
- var fs24 = __toESM(require("fs"));
24239
- var path33 = __toESM(require("path"));
25150
+ var fs25 = __toESM(require("fs"));
25151
+ var path34 = __toESM(require("path"));
24240
25152
  var crypto4 = __toESM(require("crypto"));
24241
25153
  function storePath2(dataDir) {
24242
- return path33.join(dataDir, "web-sessions.json");
25154
+ return path34.join(dataDir, "web-sessions.json");
24243
25155
  }
24244
25156
  function readSessions(dataDir) {
24245
25157
  const p = storePath2(dataDir);
24246
25158
  let raw;
24247
25159
  try {
24248
- raw = fs24.readFileSync(p, "utf8");
25160
+ raw = fs25.readFileSync(p, "utf8");
24249
25161
  } catch (e) {
24250
25162
  if (e.code === "ENOENT") return [];
24251
25163
  throw new Error(`Failed to read web session store at ${p}: ${e.message}`);
@@ -24260,10 +25172,10 @@ function readSessions(dataDir) {
24260
25172
  }
24261
25173
  function persistSessions(dataDir, sessions) {
24262
25174
  const p = storePath2(dataDir);
24263
- fs24.mkdirSync(path33.dirname(p), { recursive: true });
25175
+ fs25.mkdirSync(path34.dirname(p), { recursive: true });
24264
25176
  const tmp = `${p}.tmp`;
24265
- fs24.writeFileSync(tmp, JSON.stringify(sessions, null, 2) + "\n");
24266
- fs24.renameSync(tmp, p);
25177
+ fs25.writeFileSync(tmp, JSON.stringify(sessions, null, 2) + "\n");
25178
+ fs25.renameSync(tmp, p);
24267
25179
  }
24268
25180
  function mintSessionId() {
24269
25181
  return `${WEB_SESSION_PREFIX}${crypto4.randomBytes(24).toString("hex")}`;
@@ -24424,17 +25336,17 @@ function listWebSessionsBySubject(dataDir, userId) {
24424
25336
  }
24425
25337
 
24426
25338
  // src/server/users.ts
24427
- var fs25 = __toESM(require("fs"));
24428
- var path34 = __toESM(require("path"));
25339
+ var fs26 = __toESM(require("fs"));
25340
+ var path35 = __toESM(require("path"));
24429
25341
  var VALID_STATUSES = ["active", "inactive", "suspended", "deactivated", "disabled"];
24430
25342
  function storePath3(dataDir) {
24431
- return path34.join(dataDir, "users.json");
25343
+ return path35.join(dataDir, "users.json");
24432
25344
  }
24433
25345
  function loadStore(dataDir) {
24434
25346
  const p = storePath3(dataDir);
24435
25347
  let raw;
24436
25348
  try {
24437
- raw = fs25.readFileSync(p, "utf8");
25349
+ raw = fs26.readFileSync(p, "utf8");
24438
25350
  } catch (err) {
24439
25351
  if (err.code === "ENOENT") return [];
24440
25352
  throw new Error(`Cannot read hosted-user store at ${p}: ${err.message}`);
@@ -24452,10 +25364,10 @@ function loadStore(dataDir) {
24452
25364
  }
24453
25365
  function replaceAll(dataDir, records) {
24454
25366
  const p = storePath3(dataDir);
24455
- fs25.mkdirSync(path34.dirname(p), { recursive: true });
25367
+ fs26.mkdirSync(path35.dirname(p), { recursive: true });
24456
25368
  const tmp = `${p}.tmp`;
24457
- fs25.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
24458
- fs25.renameSync(tmp, p);
25369
+ fs26.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
25370
+ fs26.renameSync(tmp, p);
24459
25371
  }
24460
25372
  function registryUpsert(dataDir, record2) {
24461
25373
  const records = loadStore(dataDir);
@@ -24557,11 +25469,11 @@ function remapUnitReferences(dataDir, remap, removedScopeIds) {
24557
25469
  }
24558
25470
 
24559
25471
  // src/server/instance.ts
24560
- var fs26 = __toESM(require("fs"));
24561
- var path35 = __toESM(require("path"));
25472
+ var fs27 = __toESM(require("fs"));
25473
+ var path36 = __toESM(require("path"));
24562
25474
  var import_crypto = require("crypto");
24563
25475
  function storePath4(dataDir) {
24564
- return path35.join(dataDir, "instance.json");
25476
+ return path36.join(dataDir, "instance.json");
24565
25477
  }
24566
25478
  var InstanceIdentityStore = class {
24567
25479
  constructor(dataDir) {
@@ -24578,7 +25490,7 @@ var InstanceIdentityStore = class {
24578
25490
  const p = storePath4(this.dataDir);
24579
25491
  let raw;
24580
25492
  try {
24581
- raw = fs26.readFileSync(p, "utf8");
25493
+ raw = fs27.readFileSync(p, "utf8");
24582
25494
  } catch (err) {
24583
25495
  if (err.code === "ENOENT") return null;
24584
25496
  throw new Error(`Cannot read instance identity at ${p}: ${err.message}`);
@@ -24601,10 +25513,10 @@ var InstanceIdentityStore = class {
24601
25513
  * never truncates the file. Only called by the registry's create-once seed. */
24602
25514
  replace(identity) {
24603
25515
  const p = storePath4(this.dataDir);
24604
- fs26.mkdirSync(path35.dirname(p), { recursive: true });
25516
+ fs27.mkdirSync(path36.dirname(p), { recursive: true });
24605
25517
  const tmp = `${p}.tmp`;
24606
- fs26.writeFileSync(tmp, JSON.stringify(identity, null, 2) + "\n");
24607
- fs26.renameSync(tmp, p);
25518
+ fs27.writeFileSync(tmp, JSON.stringify(identity, null, 2) + "\n");
25519
+ fs27.renameSync(tmp, p);
24608
25520
  }
24609
25521
  };
24610
25522
  var InstanceIdentityRegistry = class {
@@ -24660,8 +25572,8 @@ function getInstanceIdentity(dataDir) {
24660
25572
  }
24661
25573
 
24662
25574
  // src/utils/secrets.ts
24663
- var fs27 = __toESM(require("fs"));
24664
- var path36 = __toESM(require("path"));
25575
+ var fs28 = __toESM(require("fs"));
25576
+ var path37 = __toESM(require("path"));
24665
25577
  var ENV_FALLBACK = {
24666
25578
  "git-token": ["WAIRON_GIT_TOKEN"],
24667
25579
  "notion-token": ["WAIRON_NOTION_TOKEN"],
@@ -24670,13 +25582,13 @@ var ENV_FALLBACK = {
24670
25582
  };
24671
25583
  function storePath5() {
24672
25584
  const dataDir = process.env["WAIRON_DATA_DIR"];
24673
- return dataDir ? path36.join(dataDir, "auth", "secrets.json") : null;
25585
+ return dataDir ? path37.join(dataDir, "auth", "secrets.json") : null;
24674
25586
  }
24675
25587
  function readStore() {
24676
25588
  const p = storePath5();
24677
25589
  if (!p) return {};
24678
25590
  try {
24679
- return JSON.parse(fs27.readFileSync(p, "utf8"));
25591
+ return JSON.parse(fs28.readFileSync(p, "utf8"));
24680
25592
  } catch {
24681
25593
  return {};
24682
25594
  }
@@ -24701,10 +25613,10 @@ function setSecret(key, value) {
24701
25613
  if (!p) throw new Error("WAIRON_DATA_DIR is not set \u2014 a running server needs it to store secrets.");
24702
25614
  const store = readStore();
24703
25615
  store[key] = value;
24704
- fs27.mkdirSync(path36.dirname(p), { recursive: true });
25616
+ fs28.mkdirSync(path37.dirname(p), { recursive: true });
24705
25617
  const tmp = `${p}.tmp`;
24706
- fs27.writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n");
24707
- fs27.renameSync(tmp, p);
25618
+ fs28.writeFileSync(tmp, JSON.stringify(store, null, 2) + "\n");
25619
+ fs28.renameSync(tmp, p);
24708
25620
  }
24709
25621
  function listSecretKeys() {
24710
25622
  return Object.keys(readStore());
@@ -24912,17 +25824,17 @@ function verifySsoState(state) {
24912
25824
  }
24913
25825
 
24914
25826
  // src/server/organization.ts
24915
- var fs28 = __toESM(require("fs"));
24916
- var path37 = __toESM(require("path"));
25827
+ var fs29 = __toESM(require("fs"));
25828
+ var path38 = __toESM(require("path"));
24917
25829
  var crypto6 = __toESM(require("crypto"));
24918
25830
  function storePath6(dataDir) {
24919
- return path37.join(dataDir, "organization.json");
25831
+ return path38.join(dataDir, "organization.json");
24920
25832
  }
24921
25833
  function readState(dataDir) {
24922
25834
  const p = storePath6(dataDir);
24923
25835
  let raw;
24924
25836
  try {
24925
- raw = fs28.readFileSync(p, "utf8");
25837
+ raw = fs29.readFileSync(p, "utf8");
24926
25838
  } catch (e) {
24927
25839
  if (e.code === "ENOENT") return { units: [], placements: [] };
24928
25840
  throw new Error(`Failed to read organization store at ${p}: ${e.message}`);
@@ -24939,10 +25851,10 @@ function readState(dataDir) {
24939
25851
  }
24940
25852
  function persistState(dataDir, state) {
24941
25853
  const p = storePath6(dataDir);
24942
- fs28.mkdirSync(path37.dirname(p), { recursive: true });
25854
+ fs29.mkdirSync(path38.dirname(p), { recursive: true });
24943
25855
  const tmp = `${p}.tmp`;
24944
- fs28.writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
24945
- fs28.renameSync(tmp, p);
25856
+ fs29.writeFileSync(tmp, JSON.stringify(state, null, 2) + "\n");
25857
+ fs29.renameSync(tmp, p);
24946
25858
  }
24947
25859
  var SLUG_PATTERN = /^[a-z0-9-]+$/;
24948
25860
  var UNIT_KINDS = ["business_entity", "department", "team", "group"];
@@ -25311,11 +26223,11 @@ function getOrganizationUnit(dataDir, id) {
25311
26223
  }
25312
26224
 
25313
26225
  // src/server/permissions.ts
25314
- var fs29 = __toESM(require("fs"));
25315
- var path38 = __toESM(require("path"));
26226
+ var fs30 = __toESM(require("fs"));
26227
+ var path39 = __toESM(require("path"));
25316
26228
  var import_crypto2 = require("crypto");
25317
26229
  function storePath7(dataDir) {
25318
- return path38.join(dataDir, "permissions.json");
26230
+ return path39.join(dataDir, "permissions.json");
25319
26231
  }
25320
26232
  function assignmentKey(a) {
25321
26233
  return [a.subjectKind, a.subjectId ?? "", a.scopeKind, a.scopeId ?? "", a.capability].join("|");
@@ -25324,7 +26236,7 @@ function load4(dataDir) {
25324
26236
  const p = storePath7(dataDir);
25325
26237
  let raw;
25326
26238
  try {
25327
- raw = fs29.readFileSync(p, "utf8");
26239
+ raw = fs30.readFileSync(p, "utf8");
25328
26240
  } catch (err) {
25329
26241
  if (err.code === "ENOENT") return [];
25330
26242
  throw new Error(`Cannot read permission store at ${p}: ${err.message}`);
@@ -25342,10 +26254,10 @@ function load4(dataDir) {
25342
26254
  }
25343
26255
  function replaceAll2(dataDir, assignments) {
25344
26256
  const p = storePath7(dataDir);
25345
- fs29.mkdirSync(path38.dirname(p), { recursive: true });
26257
+ fs30.mkdirSync(path39.dirname(p), { recursive: true });
25346
26258
  const tmp = `${p}.tmp`;
25347
- fs29.writeFileSync(tmp, JSON.stringify(assignments, null, 2) + "\n");
25348
- fs29.renameSync(tmp, p);
26259
+ fs30.writeFileSync(tmp, JSON.stringify(assignments, null, 2) + "\n");
26260
+ fs30.renameSync(tmp, p);
25349
26261
  }
25350
26262
  function registrySet(dataDir, assignment) {
25351
26263
  const assignments = load4(dataDir);
@@ -25431,8 +26343,8 @@ function getAssignment(dataDir, assignmentId) {
25431
26343
  }
25432
26344
 
25433
26345
  // src/server/roles.ts
25434
- var fs30 = __toESM(require("fs"));
25435
- var path39 = __toESM(require("path"));
26346
+ var fs31 = __toESM(require("fs"));
26347
+ var path40 = __toESM(require("path"));
25436
26348
  var BUILTIN_ROLES = [
25437
26349
  {
25438
26350
  id: SSO_ADMIN_ROLE_ID,
@@ -25450,13 +26362,13 @@ function isBuiltinRoleId(roleId) {
25450
26362
  return BUILTIN_ROLE_IDS.has(roleId);
25451
26363
  }
25452
26364
  function storePath8(dataDir) {
25453
- return path39.join(dataDir, "roles.json");
26365
+ return path40.join(dataDir, "roles.json");
25454
26366
  }
25455
26367
  function load5(dataDir) {
25456
26368
  const p = storePath8(dataDir);
25457
26369
  let raw;
25458
26370
  try {
25459
- raw = fs30.readFileSync(p, "utf8");
26371
+ raw = fs31.readFileSync(p, "utf8");
25460
26372
  } catch (err) {
25461
26373
  if (err.code === "ENOENT") return [];
25462
26374
  throw new Error(`Cannot read role store at ${p}: ${err.message}`);
@@ -25474,10 +26386,10 @@ function load5(dataDir) {
25474
26386
  }
25475
26387
  function replaceAll3(dataDir, roles) {
25476
26388
  const p = storePath8(dataDir);
25477
- fs30.mkdirSync(path39.dirname(p), { recursive: true });
26389
+ fs31.mkdirSync(path40.dirname(p), { recursive: true });
25478
26390
  const tmp = `${p}.tmp`;
25479
- fs30.writeFileSync(tmp, JSON.stringify(roles, null, 2) + "\n");
25480
- fs30.renameSync(tmp, p);
26391
+ fs31.writeFileSync(tmp, JSON.stringify(roles, null, 2) + "\n");
26392
+ fs31.renameSync(tmp, p);
25481
26393
  }
25482
26394
  function registryCreate(dataDir, role) {
25483
26395
  if (isBuiltinRoleId(role.id)) {
@@ -25711,131 +26623,14 @@ function actionableUnitIds(scopes) {
25711
26623
  }
25712
26624
 
25713
26625
  // src/server/projects.ts
25714
- var fs31 = __toESM(require("fs"));
25715
- var path40 = __toESM(require("path"));
25716
- var ID_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
25717
- function isValidProjectId(id) {
25718
- return typeof id === "string" && ID_RE.test(id);
25719
- }
25720
- function registryPath(dataDir) {
25721
- return path40.join(dataDir, "projects.json");
25722
- }
25723
- function load6(dataDir) {
25724
- try {
25725
- return JSON.parse(fs31.readFileSync(registryPath(dataDir), "utf8"));
25726
- } catch {
25727
- return [];
25728
- }
25729
- }
25730
- function save2(dataDir, records) {
25731
- const p = registryPath(dataDir);
25732
- fs31.mkdirSync(path40.dirname(p), { recursive: true });
25733
- const tmp = `${p}.tmp`;
25734
- fs31.writeFileSync(tmp, JSON.stringify(records, null, 2) + "\n");
25735
- fs31.renameSync(tmp, p);
25736
- }
25737
- function projectRoot(dataDir, id) {
25738
- return path40.join(dataDir, "projects", id);
25739
- }
25740
- function existingProjectRoot(dataDir, id) {
25741
- if (!isValidProjectId(id)) return null;
25742
- const rec = load6(dataDir).find((r) => r.id === id);
25743
- return rec ? rec.rootPath : null;
25744
- }
25745
- function createProjectRecord(dataDir, id) {
25746
- if (!isValidProjectId(id)) {
25747
- throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
25748
- }
25749
- const records = load6(dataDir);
25750
- if (records.some((r) => r.id === id)) {
25751
- throw new Error(`Project "${id}" already exists.`);
25752
- }
25753
- const root = projectRoot(dataDir, id);
25754
- fs31.mkdirSync(root, { recursive: true });
25755
- const record2 = {
25756
- id,
25757
- rootPath: root,
25758
- status: "active",
25759
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
25760
- };
25761
- records.push(record2);
25762
- save2(dataDir, records);
25763
- return record2;
25764
- }
25765
- function registerLocalDevProject(dataDir, id, rootPath) {
25766
- if (!isValidProjectId(id)) {
25767
- throw new Error(`Invalid project id "${id}" (allowed: lowercase letters, digits, hyphen).`);
25768
- }
25769
- const records = load6(dataDir);
25770
- const existing = records.find((r) => r.id === id);
25771
- const record2 = {
25772
- id,
25773
- rootPath,
25774
- status: "active",
25775
- createdAt: existing?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()
25776
- };
25777
- const next = existing ? records.map((r) => r.id === id ? record2 : r) : [...records, record2];
25778
- save2(dataDir, next);
25779
- return record2;
25780
- }
25781
- function listProjectRecords(dataDir) {
25782
- return load6(dataDir);
25783
- }
25784
- function removeProjectRecord(dataDir, id) {
25785
- const records = load6(dataDir);
25786
- const rec = records.find((r) => r.id === id);
25787
- if (rec) {
25788
- try {
25789
- fs31.rmSync(rec.rootPath, { recursive: true, force: true });
25790
- } catch {
25791
- }
25792
- }
25793
- save2(dataDir, records.filter((r) => r.id !== id));
25794
- }
25795
- function resolveProjectRoot(dataDir, principal, selector) {
25796
- const authorized = principal.projects;
25797
- const wildcard = authorized.includes("*");
25798
- let target;
25799
- if (selector) {
25800
- if (!wildcard && !authorized.includes(selector)) return null;
25801
- target = selector;
25802
- } else if (!wildcard && authorized.length === 1) {
25803
- target = authorized[0];
25804
- } else {
25805
- return null;
25806
- }
25807
- if (!isValidProjectId(target)) return null;
25808
- const rec = load6(dataDir).find((r) => r.id === target);
25809
- if (!rec || rec.status !== "active") return null;
25810
- return rec.rootPath;
25811
- }
25812
-
25813
- // src/server/adapters.ts
25814
- init_statehash();
25815
-
25816
- // src/core/lockfile.ts
25817
- var fs32 = __toESM(require("fs"));
25818
- var path41 = __toESM(require("path"));
26626
+ var fs35 = __toESM(require("fs"));
26627
+ var path44 = __toESM(require("path"));
26628
+ init_loader();
26629
+ init_yaml();
25819
26630
  init_fs();
25820
- function lockPath() {
25821
- return aiDir("lock.json");
25822
- }
25823
- function readLockRecord() {
25824
- try {
25825
- return JSON.parse(fs32.readFileSync(lockPath(), "utf8"));
25826
- } catch {
25827
- return null;
25828
- }
25829
- }
25830
- function writeLockRecord(record2) {
25831
- const p = lockPath();
25832
- fs32.mkdirSync(path41.dirname(p), { recursive: true });
25833
- const tmp = `${p}.tmp`;
25834
- fs32.writeFileSync(tmp, JSON.stringify(record2, null, 2) + "\n");
25835
- fs32.renameSync(tmp, p);
25836
- }
25837
26631
 
25838
26632
  // src/server/adapters.ts
26633
+ init_statehash();
25839
26634
  init_specs2();
25840
26635
  init_provision();
25841
26636
  init_validation();
@@ -25846,37 +26641,37 @@ init_types();
25846
26641
  init_server();
25847
26642
 
25848
26643
  // src/git/config.ts
25849
- var fs33 = __toESM(require("fs"));
25850
- var path42 = __toESM(require("path"));
26644
+ var fs32 = __toESM(require("fs"));
26645
+ var path41 = __toESM(require("path"));
25851
26646
  init_fs();
25852
26647
  function configPath() {
25853
26648
  return aiDir("git.json");
25854
26649
  }
25855
26650
  function readGitConfig() {
25856
26651
  try {
25857
- return JSON.parse(fs33.readFileSync(configPath(), "utf8"));
26652
+ return JSON.parse(fs32.readFileSync(configPath(), "utf8"));
25858
26653
  } catch {
25859
26654
  return null;
25860
26655
  }
25861
26656
  }
25862
26657
  function writeGitConfig(config) {
25863
26658
  const p = configPath();
25864
- fs33.mkdirSync(path42.dirname(p), { recursive: true });
26659
+ fs32.mkdirSync(path41.dirname(p), { recursive: true });
25865
26660
  const tmp = `${p}.tmp`;
25866
- fs33.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n");
25867
- fs33.renameSync(tmp, p);
26661
+ fs32.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n");
26662
+ fs32.renameSync(tmp, p);
25868
26663
  }
25869
26664
  function clearGitConfig() {
25870
26665
  try {
25871
- fs33.rmSync(configPath(), { force: true });
26666
+ fs32.rmSync(configPath(), { force: true });
25872
26667
  } catch {
25873
26668
  }
25874
26669
  }
25875
26670
 
25876
26671
  // src/git/adapter.ts
25877
26672
  var import_child_process3 = require("child_process");
25878
- var fs34 = __toESM(require("fs"));
25879
- var path43 = __toESM(require("path"));
26673
+ var fs33 = __toESM(require("fs"));
26674
+ var path42 = __toESM(require("path"));
25880
26675
  init_fs();
25881
26676
  function git(args, cwd) {
25882
26677
  return (0, import_child_process3.execFileSync)("git", args, {
@@ -25928,10 +26723,10 @@ function compareUrl(remote, defaultBranch, workingBranch) {
25928
26723
  return `${web}/compare/${encodeURIComponent(defaultBranch)}...${encodeURIComponent(workingBranch)}`;
25929
26724
  }
25930
26725
  function excludeLocalFiles() {
25931
- const excludePath = path43.join(getProjectRoot(), ".git", "info", "exclude");
26726
+ const excludePath = path42.join(getProjectRoot(), ".git", "info", "exclude");
25932
26727
  try {
25933
- fs34.mkdirSync(path43.dirname(excludePath), { recursive: true });
25934
- 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");
25935
26730
  } catch {
25936
26731
  }
25937
26732
  }
@@ -25996,39 +26791,39 @@ function configureSync(periodicSyncMinutes, skipIfClean) {
25996
26791
  }
25997
26792
 
25998
26793
  // src/producers/config.ts
25999
- var fs35 = __toESM(require("fs"));
26000
- var path44 = __toESM(require("path"));
26794
+ var fs34 = __toESM(require("fs"));
26795
+ var path43 = __toESM(require("path"));
26001
26796
  init_fs();
26002
26797
  function configPath2() {
26003
26798
  return aiDir("producers.json");
26004
26799
  }
26005
- function load7() {
26800
+ function load6() {
26006
26801
  try {
26007
- return JSON.parse(fs35.readFileSync(configPath2(), "utf8"));
26802
+ return JSON.parse(fs34.readFileSync(configPath2(), "utf8"));
26008
26803
  } catch {
26009
26804
  return [];
26010
26805
  }
26011
26806
  }
26012
- function save3(configs) {
26807
+ function save2(configs) {
26013
26808
  const p = configPath2();
26014
- fs35.mkdirSync(path44.dirname(p), { recursive: true });
26809
+ fs34.mkdirSync(path43.dirname(p), { recursive: true });
26015
26810
  const tmp = `${p}.tmp`;
26016
- fs35.writeFileSync(tmp, JSON.stringify(configs, null, 2) + "\n");
26017
- fs35.renameSync(tmp, p);
26811
+ fs34.writeFileSync(tmp, JSON.stringify(configs, null, 2) + "\n");
26812
+ fs34.renameSync(tmp, p);
26018
26813
  }
26019
26814
  function readProducerConfig(target) {
26020
- return load7().find((c) => c.target === target) ?? null;
26815
+ return load6().find((c) => c.target === target) ?? null;
26021
26816
  }
26022
26817
  function writeProducerConfig(config) {
26023
- const configs = load7().filter((c) => c.target !== config.target);
26818
+ const configs = load6().filter((c) => c.target !== config.target);
26024
26819
  configs.push(config);
26025
- save3(configs);
26820
+ save2(configs);
26026
26821
  }
26027
26822
  function clearProducerConfig(target) {
26028
- save3(load7().filter((c) => c.target !== target));
26823
+ save2(load6().filter((c) => c.target !== target));
26029
26824
  }
26030
26825
  function listProducerConfigs() {
26031
- return load7();
26826
+ return load6();
26032
26827
  }
26033
26828
 
26034
26829
  // src/producers/core-adapter.ts
@@ -26367,8 +27162,19 @@ var hostCore = {
26367
27162
  /** The ids of wairon's built-in architectural profiles, read from the core rules
26368
27163
  * registry's built-in profile set (BUILTIN_PROFILES) — a pure, side-effect-free
26369
27164
  * read of a bundled constant. */
26370
- 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]
26371
27174
  };
27175
+ function resolveContainedProjectPath(projectRoot2, projectPath) {
27176
+ return assertContainedProjectPath(projectRoot2, projectPath);
27177
+ }
26372
27178
  function validateProjectAsComplete() {
26373
27179
  const config = loadProjectConfig();
26374
27180
  return validateAsComplete({ rules: config.rules, projectType: config.projectType });
@@ -26400,6 +27206,180 @@ var hostSdk = {
26400
27206
  extractArchive: (archive, destDir, limits) => sdkPortal.extractPack(archive, destDir, limits)
26401
27207
  };
26402
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
+
26403
27383
  // src/server/errors.ts
26404
27384
  var UnauthenticatedError = class extends Error {
26405
27385
  constructor() {
@@ -26520,9 +27500,14 @@ function lockProject(cfg, credential, project2) {
26520
27500
  }
26521
27501
  return executeApprovedLock(cfg, project2);
26522
27502
  }
26523
- function executeApprovedLock(cfg, projectId) {
27503
+ function boundLifecycleRoot(cfg, projectId, subproject) {
26524
27504
  const root = existingProjectRoot(cfg.dataDir, projectId);
26525
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);
26526
27511
  return runWithProjectRoot(root, () => {
26527
27512
  hostGit.sync();
26528
27513
  const result = validateProjectAsComplete();
@@ -26694,9 +27679,8 @@ function promoteProject(cfg, credential, project2) {
26694
27679
  }
26695
27680
  return executeApprovedPromote(cfg, project2);
26696
27681
  }
26697
- function executeApprovedPromote(cfg, projectId) {
26698
- const root = existingProjectRoot(cfg.dataDir, projectId);
26699
- if (!root) throw new Error(`Unknown project "${projectId}".`);
27682
+ function executeApprovedPromote(cfg, projectId, subproject) {
27683
+ const root = boundLifecycleRoot(cfg, projectId, subproject);
26700
27684
  return runWithProjectRoot(root, () => {
26701
27685
  const lock = hostCore.readLockRecord();
26702
27686
  if (!lock) {
@@ -26811,6 +27795,35 @@ function storeListGlobalPacks() {
26811
27795
  );
26812
27796
  return [...instance, ...image];
26813
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
+ }
26814
27827
  function storeListAvailableProfiles() {
26815
27828
  const out = [];
26816
27829
  const seen = /* @__PURE__ */ new Set();
@@ -26821,19 +27834,21 @@ function storeListAvailableProfiles() {
26821
27834
  out.push(family ? { id, source, family } : { id, source });
26822
27835
  };
26823
27836
  for (const id of hostCore.builtinProfileIds()) emit(id, "builtin");
26824
- const scanTier = (dir) => {
26825
- for (const full of hostCore.discoverPacks(dir)) {
26826
- try {
26827
- const loaded = hostCore.loadExtensionPacks([{ ref: full, scope: "global" }], path45.dirname(full));
26828
- if (loaded.errors.length) continue;
26829
- const source = loaded.packNames[0] ?? path45.basename(full);
26830
- for (const [id, def] of Object.entries(loaded.profiles)) emit(id, source, def.family);
26831
- } catch {
26832
- }
26833
- }
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 });
26834
27848
  };
26835
- scanTier(hostCore.globalPacksDir());
26836
- 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);
26837
27852
  return out;
26838
27853
  }
26839
27854
  function readPackContent(full) {
@@ -27026,6 +28041,10 @@ function listAvailableProfiles(cfg, credential) {
27026
28041
  requirePrincipal2(cfg, credential);
27027
28042
  return storeListAvailableProfiles();
27028
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
+ }
27029
28048
  function listAdoptableProjectPacks(cfg, credential, project2) {
27030
28049
  requireCap(cfg, credential, "project:read", "project", project2, "Forbidden \u2014 listing adoptable packs requires project:read over the project");
27031
28050
  return storeListGlobalPacks();
@@ -27048,6 +28067,26 @@ function executeApprovedInstallProjectPack(cfg, project2, name, content) {
27048
28067
  function executeApprovedResolveGlobalPacks(names) {
27049
28068
  return storeResolveGlobalPacks(names);
27050
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
+ }
27051
28090
  function removeProjectPack(cfg, credential, project2, name) {
27052
28091
  requireCap(cfg, credential, "project:admin", "project", project2, "Forbidden \u2014 removing a project pack requires project:admin over the project");
27053
28092
  runWithProjectRoot(boundProject2(cfg, project2), () => storeRemoveProjectPack(name));
@@ -28042,6 +29081,40 @@ function requestPackNames(request) {
28042
29081
  if (!sel) return [];
28043
29082
  return [.../* @__PURE__ */ new Set([...sel.requiredPackNames ?? [], ...sel.defaultPackNames ?? []])];
28044
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
+ }
28045
29118
  function resolvedSelection(request, policy, selectedBy) {
28046
29119
  const sel = request.profileSelection;
28047
29120
  const selection = {
@@ -28060,7 +29133,8 @@ function buildEvaluation(input) {
28060
29133
  selectedProfileIds,
28061
29134
  hasSelection,
28062
29135
  countMissingPacksAsViolation,
28063
- requiredDefaultResolution
29136
+ requiredDefaultResolution,
29137
+ governingProfileId
28064
29138
  } = input;
28065
29139
  const present = new Set(presentPackNames);
28066
29140
  let missingPackNames;
@@ -28081,6 +29155,7 @@ function buildEvaluation(input) {
28081
29155
  const allowed = policy.allowedProfileIds;
28082
29156
  const disallowedProfileIds = allowed && allowed.length > 0 ? selectedProfileIds.filter((id) => !allowed.includes(id)) : [];
28083
29157
  const selectionRequiredUnmet = policy.requireProfileSelection && !hasSelection;
29158
+ const unappliedProfileIds = governingProfileId ? unappliedIds(selectedProfileIds, governingProfileId) : [];
28084
29159
  const messages = [];
28085
29160
  if (selectionRequiredUnmet) {
28086
29161
  messages.push("Profile selection is required by policy but none was provided.");
@@ -28092,16 +29167,24 @@ function buildEvaluation(input) {
28092
29167
  for (const n of blockedPackNames) messages.push(`Pack "${n}" is blocked by policy.`);
28093
29168
  for (const id of missingProfileIds) messages.push(`Required profile "${id}" is not selected.`);
28094
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
+ }
28095
29175
  const violation = selectionRequiredUnmet || blockedPackNames.length > 0 || missingProfileIds.length > 0 || disallowedProfileIds.length > 0 || countMissingPacksAsViolation && (missingPackNames.length > 0 || unresolvedPacks.length > 0);
28096
- return {
29176
+ const result = {
28097
29177
  compliant: !violation,
28098
29178
  mode: policy.enforcementMode,
28099
29179
  missingPackNames,
28100
29180
  blockedPackNames,
28101
29181
  missingProfileIds,
28102
29182
  unresolvedPacks,
29183
+ unappliedProfileIds,
28103
29184
  messages
28104
29185
  };
29186
+ if (governingProfileId) result.governingProfileId = governingProfileId;
29187
+ return result;
28105
29188
  }
28106
29189
  function performInit(cfg, request, principal) {
28107
29190
  const policy = effectivePolicy(cfg.dataDir);
@@ -28121,17 +29204,24 @@ function performInit(cfg, request, principal) {
28121
29204
  request.ownerUnitId,
28122
29205
  principal ? principalSubject3(principal) : void 0
28123
29206
  );
28124
- installResolvedPacks(
28125
- cfg,
28126
- record2.id,
28127
- executeApprovedResolveGlobalPacks([
28128
- ...policy.requiredGlobalPacks,
28129
- ...policy.defaultProjectPacks,
28130
- ...requestPackNames(request)
28131
- ])
28132
- );
29207
+ const packResolution = executeApprovedResolveGlobalPacks([
29208
+ ...policy.requiredGlobalPacks,
29209
+ ...policy.defaultProjectPacks,
29210
+ ...requestPackNames(request)
29211
+ ]);
29212
+ installResolvedPacks(cfg, record2.id, packResolution);
28133
29213
  const selectedBy = principal ? principalSubject3(principal) : void 0;
28134
- 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];
28135
29225
  const actor = principal ? principalSubject3(principal) : SYSTEM_SUBJECT;
28136
29226
  tryAppendAudit2(
28137
29227
  cfg,
@@ -28140,7 +29230,18 @@ function performInit(cfg, request, principal) {
28140
29230
  "project.init.policy",
28141
29231
  "info",
28142
29232
  "project",
28143
- { 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
+ },
28144
29245
  principal?.tokenId
28145
29246
  )
28146
29247
  );
@@ -28181,6 +29282,7 @@ function evaluateProjectPolicy(cfg, credential, projectId) {
28181
29282
  const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
28182
29283
  if (!root) throw new Error(`Unknown project "${projectId}".`);
28183
29284
  const policy = effectivePolicy(cfg.dataDir);
29285
+ const governingProfileId = readProjectType(root);
28184
29286
  const selection = readProjectProfileSelection(root);
28185
29287
  return buildEvaluation({
28186
29288
  policy,
@@ -28188,7 +29290,8 @@ function evaluateProjectPolicy(cfg, credential, projectId) {
28188
29290
  selectedProfileIds: selection?.profileIds ?? [],
28189
29291
  hasSelection: !!selection,
28190
29292
  countMissingPacksAsViolation: true,
28191
- requiredDefaultResolution: executeApprovedResolveGlobalPacks(requiredDefaultNames(policy))
29293
+ requiredDefaultResolution: executeApprovedResolveGlobalPacks(requiredDefaultNames(policy)),
29294
+ governingProfileId
28192
29295
  });
28193
29296
  }
28194
29297
  function reconcileProjectPolicy(cfg, credential, projectId) {
@@ -28203,20 +29306,41 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
28203
29306
  const policy = effectivePolicy(cfg.dataDir);
28204
29307
  const selection = readProjectProfileSelection(root);
28205
29308
  const resolution = executeApprovedResolveGlobalPacks(requiredDefaultNames(policy));
28206
- let installed = installedPackNames(cfg, projectId);
28207
- const installedSet = new Set(installed);
29309
+ const installedSet = new Set(installedPackNames(cfg, projectId));
28208
29310
  const toApply = resolution.resolved.filter((p) => !installedSet.has(p.name));
29311
+ const appliedPackNames = toApply.map((p) => p.name);
28209
29312
  if (toApply.length > 0) {
28210
29313
  installResolvedPacks(cfg, projectId, { resolved: toApply, unresolved: [] });
28211
- 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
+ }
28212
29335
  }
28213
29336
  const result = buildEvaluation({
28214
29337
  policy,
28215
- presentPackNames: installed,
28216
- selectedProfileIds: selection?.profileIds ?? [],
29338
+ presentPackNames: installedPackNames(cfg, projectId),
29339
+ selectedProfileIds,
28217
29340
  hasSelection: !!selection,
28218
29341
  countMissingPacksAsViolation: true,
28219
- requiredDefaultResolution: resolution
29342
+ requiredDefaultResolution: resolution,
29343
+ governingProfileId
28220
29344
  });
28221
29345
  tryAppendAudit2(
28222
29346
  cfg,
@@ -28225,7 +29349,15 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
28225
29349
  "policy.reconcile",
28226
29350
  "info",
28227
29351
  "policy",
28228
- { 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
+ },
28229
29361
  principal.tokenId
28230
29362
  )
28231
29363
  );
@@ -28241,8 +29373,19 @@ function getProjectConfig(cfg, credential, projectId) {
28241
29373
  const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
28242
29374
  if (!root) throw new Error(`Unknown project "${projectId}".`);
28243
29375
  const projectType = readProjectType(root);
29376
+ const selection = readProjectProfileSelection(root);
29377
+ const classified = classifyProfile(projectType, executeApprovedListProjectProfiles(cfg, projectId));
28244
29378
  const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
28245
- 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;
28246
29389
  }
28247
29390
  function setProjectType(cfg, credential, projectId, projectType) {
28248
29391
  const principal = requirePrincipal4(cfg, credential);
@@ -28253,9 +29396,23 @@ function setProjectType(cfg, credential, projectId, projectType) {
28253
29396
  }
28254
29397
  const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
28255
29398
  if (!root) throw new Error(`Unknown project "${projectId}".`);
28256
- 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);
28257
29403
  const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
28258
- 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;
28259
29416
  }
28260
29417
  function getPackPolicy(cfg, credential) {
28261
29418
  requirePrincipal4(cfg, credential);
@@ -28425,11 +29582,8 @@ function mintToken(cfg, credential, request) {
28425
29582
  }
28426
29583
  assertNotReservedSubjectId(cfg, [request.ownerUserId]);
28427
29584
  const projects = request.projects?.length ? request.projects : ["*"];
28428
- const knownProjects = new Set(listProjectRecords(cfg.dataDir).map((p) => p.id));
28429
29585
  for (const p of projects) {
28430
- if (p !== "*" && !knownProjects.has(p)) {
28431
- throw new Error(`unknown project "${p}"`);
28432
- }
29586
+ assertMintableNarrowingEntry(cfg.dataDir, p);
28433
29587
  }
28434
29588
  const owner = findUserByRecordOrSubjectId(cfg.dataDir, request.ownerUserId);
28435
29589
  if (owner && owner.status !== "active") {
@@ -28466,12 +29620,21 @@ function revokeToken(cfg, credential, tokenId) {
28466
29620
  }
28467
29621
  function mintSelfToken(cfg, credential, projectId, write) {
28468
29622
  const principal = requirePrincipal5(cfg, credential);
28469
- 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") {
28470
29630
  throw new ForbiddenError("caller lacks project:read on the requested project");
28471
29631
  }
28472
- 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") {
28473
29633
  throw new ForbiddenError("caller lacks project:write on the requested project");
28474
29634
  }
29635
+ if (parsed.mounts.length > 0) {
29636
+ assertMintableNarrowingEntry(cfg.dataDir, projectId);
29637
+ }
28475
29638
  const token = "wk_" + crypto10.randomBytes(24).toString("hex");
28476
29639
  const owner = auditActor(principal);
28477
29640
  const record2 = {
@@ -29083,12 +30246,12 @@ function resolveVisibility(observerProjectId, units, placements) {
29083
30246
  const best = /* @__PURE__ */ new Map();
29084
30247
  for (const placement of placements) {
29085
30248
  if (placement.projectId === observerProjectId) continue;
29086
- const path62 = chainOf(placement.unitId, unitById);
29087
- if (!path62.length) continue;
29088
- 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));
29089
30252
  if (!closedOk) continue;
29090
- const crossTenant = !tenantRoots.has(path62[path62.length - 1].id);
29091
- if (crossTenant && !path62.some(grantedTo)) continue;
30253
+ const crossTenant = !tenantRoots.has(path61[path61.length - 1].id);
30254
+ if (crossTenant && !path61.some(grantedTo)) continue;
29092
30255
  if (crossTenant && directUnits.length === 0) continue;
29093
30256
  const distance = crossTenant ? "partner" : sameBranch(placement.unitId) ? "department" : "instance";
29094
30257
  const existing = best.get(placement.projectId);
@@ -29111,6 +30274,17 @@ function audienceDistance(resolution, targetProjectId) {
29111
30274
 
29112
30275
  // src/server/landscape.ts
29113
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
29114
30288
  var PROJECT_ADMIN_CAPABILITY3 = "project:admin";
29115
30289
  var PROJECT_READ_CAPABILITY3 = "project:read";
29116
30290
  var PROJECT_WRITE_CAPABILITY3 = "project:write";
@@ -29597,7 +30771,7 @@ function getProjectSurfaceForMcp(cfg, credential, currentProjectId, targetProjec
29597
30771
  const result = runWithProjectRoot(record2.rootPath, () => hostSurfaces.exportBoundSurface(maxAudience, "native"));
29598
30772
  return { ...result.snapshot, origin: "exchanged" };
29599
30773
  }
29600
- function exportProjectSurface(cfg, credential, projectId, format, maxAudience) {
30774
+ function exportProjectSurface(cfg, credential, projectId, format, maxAudience, portalId) {
29601
30775
  const principal = requirePrincipal6(cfg, credential);
29602
30776
  if (!permitsCap(cfg, principal, PROJECT_ADMIN_CAPABILITY3, "project", projectId)) {
29603
30777
  throw new ForbiddenError(
@@ -29611,16 +30785,37 @@ function exportProjectSurface(cfg, credential, projectId, format, maxAudience) {
29611
30785
  if (!root) throw new Error(`Unknown project "${projectId}".`);
29612
30786
  const result = runWithProjectRoot(root, () => hostSurfaces.exportBoundSurface(maxAudience, format));
29613
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
+ }
29614
30809
  return {
29615
- body: result.rendered ?? "{}",
30810
+ body: openApiIndexDocument(specs),
29616
30811
  contentType: "application/json",
29617
- filename: `${projectId}-surface.openapi.json`
30812
+ filename: `${safeFilenamePart(projectId)}-surface.openapi.index.json`
29618
30813
  };
29619
30814
  }
29620
30815
  return {
29621
30816
  body: yamlLib.dump(result.snapshot),
29622
30817
  contentType: "application/yaml",
29623
- filename: `${projectId}-surface.yaml`
30818
+ filename: `${safeFilenamePart(projectId)}-surface.yaml`
29624
30819
  };
29625
30820
  }
29626
30821
  function removeRelation(cfg, credential, id) {
@@ -29664,7 +30859,8 @@ function handleLandscapeRequest(cfg, credential, req, res, body, url) {
29664
30859
  credential,
29665
30860
  parts[2],
29666
30861
  url.searchParams.get("format") ?? "native",
29667
- url.searchParams.get("audience") ?? "instance"
30862
+ url.searchParams.get("audience") ?? "instance",
30863
+ url.searchParams.get("spec") ?? void 0
29668
30864
  );
29669
30865
  res.writeHead(200, {
29670
30866
  "content-type": artifact.contentType,
@@ -29994,10 +31190,9 @@ function migratePermissionModel(dataDir, apply) {
29994
31190
  // src/server/http.ts
29995
31191
  var http2 = __toESM(require("http"));
29996
31192
  var fs49 = __toESM(require("fs"));
29997
- var path59 = __toESM(require("path"));
31193
+ var path58 = __toESM(require("path"));
29998
31194
 
29999
31195
  // src/server/request.ts
30000
- var path58 = __toESM(require("path"));
30001
31196
  var import_streamableHttp = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
30002
31197
  init_fs();
30003
31198
 
@@ -30370,38 +31565,58 @@ function initializeProject(cfg, credential, request) {
30370
31565
  }
30371
31566
  }
30372
31567
  }
30373
- function lockProject2(cfg, credential, projectId) {
31568
+ function lockProject2(cfg, credential, projectId, subproject) {
30374
31569
  return lifecycleAction(cfg, credential, projectId, {
30375
31570
  action: "project:lock",
30376
31571
  verb: "Lock",
30377
31572
  noun: "lock",
31573
+ subproject,
30378
31574
  execute: () => {
30379
- const lock = executeApprovedLock(cfg, projectId);
31575
+ const lock = executeApprovedLock(cfg, projectId, subproject);
30380
31576
  return {
30381
31577
  status: "completed",
30382
31578
  action: "project:lock",
30383
- summary: `Locked project "${projectId}" (status: ${lock.status}).`,
31579
+ summary: `Locked project "${projectId}"${subprojectSuffix(subproject)} (status: ${lock.status}).`,
30384
31580
  lock
30385
31581
  };
30386
31582
  }
30387
31583
  });
30388
31584
  }
30389
- function promoteProject2(cfg, credential, projectId) {
31585
+ function promoteProject2(cfg, credential, projectId, subproject) {
30390
31586
  return lifecycleAction(cfg, credential, projectId, {
30391
31587
  action: "project:promote",
30392
31588
  verb: "Promote",
30393
31589
  noun: "promotion",
31590
+ subproject,
30394
31591
  execute: () => {
30395
- const promo = executeApprovedPromote(cfg, projectId);
31592
+ const promo = executeApprovedPromote(cfg, projectId, subproject);
30396
31593
  return {
30397
31594
  status: "completed",
30398
31595
  action: "project:promote",
30399
- summary: `Promotion of project "${projectId}": ${promo.status} \u2014 ${promo.message}`,
31596
+ summary: `Promotion of project "${projectId}"${subprojectSuffix(subproject)}: ${promo.status} \u2014 ${promo.message}`,
30400
31597
  promote: promo
30401
31598
  };
30402
31599
  }
30403
31600
  });
30404
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
+ }
30405
31620
  function lifecycleAction(cfg, credential, projectId, opts) {
30406
31621
  const principal = requirePrincipal7(cfg, credential);
30407
31622
  const effective = authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY4, "project", projectId);
@@ -30427,8 +31642,9 @@ function lifecycleAction(cfg, credential, projectId, opts) {
30427
31642
  const pending = buildPendingRequest(
30428
31643
  principal,
30429
31644
  opts.action,
30430
- `${opts.verb} project ${projectId}`,
30431
- projectId
31645
+ `${opts.verb} project ${projectId}${subprojectSuffix(opts.subproject)}`,
31646
+ projectId,
31647
+ subprojectScopePayload(opts.subproject)
30432
31648
  );
30433
31649
  return createPendingOutcome(cfg, principal, pending, opts.action);
30434
31650
  }
@@ -30502,12 +31718,14 @@ function executeApproved(cfg, req) {
30502
31718
  return `Initialized project "${rec.id}" under the active pack policy.`;
30503
31719
  }
30504
31720
  case "project:lock": {
30505
- const lock = executeApprovedLock(cfg, req.projectId ?? "");
30506
- 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}).`;
30507
31724
  }
30508
31725
  case "project:promote": {
30509
- const promo = executeApprovedPromote(cfg, req.projectId ?? "");
30510
- 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}`;
30511
31729
  }
30512
31730
  default:
30513
31731
  throw new Error(`Unsupported approval kind "${req.kind}".`);
@@ -31211,6 +32429,9 @@ function getProjectConfig2(cfg, credential, projectId) {
31211
32429
  function setProjectType2(cfg, credential, projectId, projectType) {
31212
32430
  return setProjectType(cfg, credential, projectId, projectType);
31213
32431
  }
32432
+ function listProjectProfiles2(cfg, credential, project2) {
32433
+ return listProjectProfiles(cfg, credential, project2);
32434
+ }
31214
32435
  function listProducers2(cfg, credential, project2) {
31215
32436
  return listProducers(cfg, credential, project2);
31216
32437
  }
@@ -31739,6 +32960,19 @@ var ShareSnapshotRegistry = class {
31739
32960
  return stored;
31740
32961
  }
31741
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
+ }
31742
32976
  var ShareSnapshotIndex = class {
31743
32977
  constructor(store) {
31744
32978
  this.store = store;
@@ -31746,12 +32980,12 @@ var ShareSnapshotIndex = class {
31746
32980
  get(snapshotId) {
31747
32981
  return this.store.read(snapshotId);
31748
32982
  }
31749
- getArtifact(snapshotId, kind) {
32983
+ getArtifact(snapshotId, kind, portalId) {
31750
32984
  const snap = this.store.read(snapshotId);
31751
32985
  if (!snap) return null;
31752
32986
  if (kind === "canvas") return snap.canvasModel ?? null;
31753
32987
  if (kind === "html") return snap.html ?? null;
31754
- if (kind === "openapi") return snap.openapi ?? null;
32988
+ if (kind === "openapi") return selectCapturedOpenApi(snap, portalId);
31755
32989
  return null;
31756
32990
  }
31757
32991
  };
@@ -31761,8 +32995,8 @@ function putSnapshot(dataDir, snapshot) {
31761
32995
  function getSnapshot2(dataDir, snapshotId) {
31762
32996
  return new ShareSnapshotIndex(new ShareSnapshotStore(dataDir)).get(snapshotId);
31763
32997
  }
31764
- function getSnapshotArtifact(dataDir, snapshotId, kind) {
31765
- 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);
31766
33000
  }
31767
33001
  function captureSnapshot(dataDir, principal, projectId, view, artifacts) {
31768
33002
  const root = resolveProjectRoot(dataDir, principal, projectId);
@@ -31781,7 +33015,11 @@ function captureSnapshot(dataDir, principal, projectId, view, artifacts) {
31781
33015
  }
31782
33016
  if (artifacts.includes("openapi")) {
31783
33017
  const result = hostSurfaces.exportBoundSurface("project", "openapi");
31784
- 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
+ }
31785
33023
  }
31786
33024
  return snapshot;
31787
33025
  });
@@ -32342,10 +33580,10 @@ function getWebProjectOpenApi(cfg, sessionId, projectId, portalId) {
32342
33580
  if (!root) throw new ForbiddenError("project not authorized or unknown");
32343
33581
  const result = runWithProjectRoot(root, () => hostSurfaces.exportBoundSurface("project", "openapi"));
32344
33582
  const specs = result.renderedSet ?? (result.rendered ? [{ portalId: "", name: projectId, document: result.rendered }] : []);
32345
- if (specs.length === 0) return swaggerUiPage("{}", projectId);
33583
+ if (specs.length === 0) return openApiIndexPage(projectId, []);
32346
33584
  if (portalId) {
32347
- const sel = specs.find((s) => s.portalId === portalId) ?? specs[0];
32348
- 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);
32349
33587
  }
32350
33588
  if (specs.length === 1) return swaggerUiPage(specs[0].document, specs[0].name);
32351
33589
  return openApiIndexPage(projectId, specs);
@@ -32353,7 +33591,7 @@ function getWebProjectOpenApi(cfg, sessionId, projectId, portalId) {
32353
33591
  function openApiIndexPage(projectId, specs) {
32354
33592
  const esc2 = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
32355
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("");
32356
- 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>`;
32357
33595
  }
32358
33596
  function reshapeLandscapeGraph(model, level) {
32359
33597
  const unitNodeIds = new Set(model.nodes.filter((n) => n.nodeKind === "orgUnit").map((n) => n.id));
@@ -32387,7 +33625,31 @@ function reshapeLandscapeGraph(model, level) {
32387
33625
  }
32388
33626
  const kept = nodes.filter((n) => n.level <= level);
32389
33627
  const keptIds = new Set(kept.map((n) => n.id));
32390
- 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
+ }
32391
33653
  return {
32392
33654
  tier: "landscape",
32393
33655
  nodes: kept,
@@ -34531,6 +35793,9 @@ function opsGetProjectConfig(cfg, sessionId, url, res) {
34531
35793
  function opsSetProjectConfig(cfg, sessionId, body, res) {
34532
35794
  sendJson(res, 200, setProjectType2(cfg, sessionId, String(body?.projectId ?? ""), String(body?.projectType ?? "")));
34533
35795
  }
35796
+ function opsListProjectProfiles(cfg, sessionId, url, res) {
35797
+ sendJson(res, 200, { profiles: listProjectProfiles2(cfg, sessionId, q(url, "projectId") ?? "") });
35798
+ }
34534
35799
  function opsListProducers(cfg, sessionId, url, res) {
34535
35800
  sendJson(res, 200, { producers: listProducers2(cfg, sessionId, q(url, "projectId") ?? "") });
34536
35801
  }
@@ -34770,6 +36035,9 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
34770
36035
  if (req.method === "POST" && parts.length === 3 && parts[2] === "config") {
34771
36036
  return opsSetProjectConfig(cfg, sessionId, body, res);
34772
36037
  }
36038
+ if (req.method === "GET" && parts.length === 3 && parts[2] === "profiles") {
36039
+ return opsListProjectProfiles(cfg, sessionId, url, res);
36040
+ }
34773
36041
  if (req.method === "GET" && parts.length === 3 && parts[2] === "policy") {
34774
36042
  return opsPolicyEvaluate(cfg, sessionId, url, res);
34775
36043
  }
@@ -34994,8 +36262,8 @@ var RealtimeHub = class {
34994
36262
  * complete the handshake, and register the connection. A bad path or session
34995
36263
  * destroys the socket. */
34996
36264
  handleUpgrade(cfg, req, socket) {
34997
- const path62 = (req.url ?? "/").split("?")[0];
34998
- if (path62 !== REALTIME_PATH || !isWebSocketUpgrade(req)) {
36265
+ const path61 = (req.url ?? "/").split("?")[0];
36266
+ if (path61 !== REALTIME_PATH || !isWebSocketUpgrade(req)) {
34999
36267
  socket.destroy();
35000
36268
  return;
35001
36269
  }
@@ -35140,7 +36408,7 @@ function deriveMcpOutcome(response) {
35140
36408
  if (r.result && typeof r.result === "object" && r.result.isError) return "failed";
35141
36409
  return "success";
35142
36410
  }
35143
- function auditToolCall(dataDir, principal, projectId, body, outcome) {
36411
+ function auditToolCall(dataDir, principal, projectId, body, outcome, subproject) {
35144
36412
  const target = mcpToolTarget(body);
35145
36413
  if (!target) return;
35146
36414
  const actor = principal.subject ?? {
@@ -35158,7 +36426,8 @@ function auditToolCall(dataDir, principal, projectId, body, outcome) {
35158
36426
  actor,
35159
36427
  tokenId: principal.tokenId,
35160
36428
  projectId,
35161
- target
36429
+ target,
36430
+ ...subproject ? { metadata: JSON.stringify({ subproject }) } : {}
35162
36431
  };
35163
36432
  try {
35164
36433
  appendAuditEvent(dataDir, event, DEFAULT_AUDIT_POLICY);
@@ -35189,6 +36458,12 @@ var PROJECT_OPS_TOOLS = /* @__PURE__ */ new Set([
35189
36458
  "sdd_host_produce",
35190
36459
  "sdd_host_commit_project"
35191
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
+ ]);
35192
36467
  function jsonRpcRequests(body) {
35193
36468
  const arr = Array.isArray(body) ? body : [body];
35194
36469
  return arr.filter((m) => !!m && typeof m === "object" && "method" in m);
@@ -35254,7 +36529,27 @@ function dataPlanePermissionError(cfg, principal, projectId, body) {
35254
36529
  }
35255
36530
  };
35256
36531
  }
35257
- 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) {
35258
36553
  const msg = jsonRpcRequest(body);
35259
36554
  if (!msg || msg.method !== "tools/call") return void 0;
35260
36555
  const name = msg.params?.name;
@@ -35271,10 +36566,10 @@ async function dispatchProjectLifecycleTool(cfg, credential, projectId, body) {
35271
36566
  value = initializeProject(cfg, credential, args);
35272
36567
  break;
35273
36568
  case "sdd_host_lock_project":
35274
- value = lockProject2(cfg, credential, projectId);
36569
+ value = lockProject2(cfg, credential, projectId, subproject);
35275
36570
  break;
35276
36571
  case "sdd_host_promote_project":
35277
- value = promoteProject2(cfg, credential, projectId);
36572
+ value = promoteProject2(cfg, credential, projectId, subproject);
35278
36573
  break;
35279
36574
  case "sdd_host_await_approval":
35280
36575
  value = await awaitApproval(
@@ -35362,8 +36657,8 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
35362
36657
  permissionSubject: { subjectId: "anonymous", roleBindings: [], instanceAdmin: true }
35363
36658
  };
35364
36659
  }
35365
- const root = resolveProjectRoot(cfg.dataDir, principal, projectSelector(req));
35366
- if (!root) {
36660
+ const binding = resolveProjectBinding(cfg.dataDir, principal, projectSelector(req));
36661
+ if (!binding) {
35367
36662
  sendJson(res, 403, { error: "project not authorized, unknown, or not specified" });
35368
36663
  return;
35369
36664
  }
@@ -35378,19 +36673,26 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
35378
36673
  });
35379
36674
  return;
35380
36675
  }
35381
- await runWithProjectRoot(root, async () => {
35382
- const projectId = path58.basename(root);
35383
- 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);
35384
36686
  if (dispatchedResponse !== void 0) {
35385
36687
  sendJson(res, 200, dispatchedResponse);
35386
- auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(dispatchedResponse));
36688
+ auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(dispatchedResponse), subproject);
35387
36689
  for (const ch of mcpChangeChannels(body, projectId, dispatchedResponse)) publishChange(ch);
35388
36690
  return;
35389
36691
  }
35390
36692
  const permissionError = dataPlanePermissionError(cfg, principal, projectId, body);
35391
36693
  if (permissionError !== void 0) {
35392
36694
  sendJson(res, 200, permissionError);
35393
- auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(permissionError));
36695
+ auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(permissionError), subproject);
35394
36696
  return;
35395
36697
  }
35396
36698
  const server = createScopedServer();
@@ -35411,7 +36713,7 @@ async function handleMcpRequest(cfg, req, res, body, credential) {
35411
36713
  };
35412
36714
  await server.connect(transport);
35413
36715
  await transport.handleRequest(req, res, body);
35414
- auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(response));
36716
+ auditToolCall(cfg.dataDir, principal, projectId, body, deriveMcpOutcome(response), subproject);
35415
36717
  for (const ch of mcpChangeChannels(body, projectId, response)) publishChange(ch);
35416
36718
  });
35417
36719
  }
@@ -35479,7 +36781,7 @@ var CONTENT_TYPE = {
35479
36781
  openapi: "application/json",
35480
36782
  canvas: "application/json"
35481
36783
  };
35482
- function downloadArtifact(cfg, token, kind, meta) {
36784
+ function downloadArtifact(cfg, token, kind, meta, portalId) {
35483
36785
  const link = linkByTokenHash(cfg.dataDir, hashToken(token));
35484
36786
  const check = usable(link);
35485
36787
  if ("outcome" in check) {
@@ -35491,7 +36793,7 @@ function downloadArtifact(cfg, token, kind, meta) {
35491
36793
  record(cfg.dataDir, check.link.id, meta, "denied-download");
35492
36794
  return { found: false, outcome: "denied-download" };
35493
36795
  }
35494
- const content = getSnapshotArtifact(cfg.dataDir, check.link.snapshotId, kind);
36796
+ const content = getSnapshotArtifact(cfg.dataDir, check.link.snapshotId, kind, portalId);
35495
36797
  if (content === null) {
35496
36798
  record(cfg.dataDir, check.link.id, meta, "not-found");
35497
36799
  return { found: false, outcome: "not-found" };
@@ -35501,6 +36803,35 @@ function downloadArtifact(cfg, token, kind, meta) {
35501
36803
  }
35502
36804
 
35503
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
+ }
35504
36835
  function shareRequestMeta(req) {
35505
36836
  const fwd = req.headers["x-forwarded-for"];
35506
36837
  const ip = (Array.isArray(fwd) ? fwd[0] : fwd)?.split(",")[0].trim() || req.socket?.remoteAddress || "unknown";
@@ -35552,14 +36883,16 @@ function serveSharedModel(cfg, token, req, res) {
35552
36883
  );
35553
36884
  }
35554
36885
  function serveSharedOpenApi(cfg, token, req, res) {
35555
- const result = downloadArtifact(cfg, token, "openapi", shareRequestMeta(req));
36886
+ const result = downloadArtifact(cfg, token, "openapi", shareRequestMeta(req), specParam(req));
35556
36887
  if (!result.found || result.content === void 0) return notFoundPage(res);
36888
+ const index = asOpenApiIndex(result.content);
35557
36889
  harden(res, void 0, "text/html; charset=utf-8");
35558
36890
  res.statusCode = 200;
35559
- res.end(swaggerUiPage(result.content, "Shared API"));
36891
+ res.end(index ? sharedOpenApiIndexPage(index) : swaggerUiPage(result.content, "Shared API"));
35560
36892
  }
35561
36893
  function serveSharedDownload(cfg, token, kind, req, res) {
35562
- const result = downloadArtifact(cfg, token, kind, shareRequestMeta(req));
36894
+ const portalId = specParam(req);
36895
+ const result = downloadArtifact(cfg, token, kind, shareRequestMeta(req), portalId);
35563
36896
  if (!result.found || result.content === void 0) {
35564
36897
  if (result.outcome === "denied-download") {
35565
36898
  harden(res, void 0, "text/plain");
@@ -35569,9 +36902,11 @@ function serveSharedDownload(cfg, token, kind, req, res) {
35569
36902
  }
35570
36903
  return notFoundPage(res);
35571
36904
  }
35572
- const ext = kind === "html" ? "html" : kind === "openapi" ? "openapi.json" : kind;
35573
36905
  harden(res, void 0, result.contentType ?? "application/octet-stream");
35574
- res.setHeader("content-disposition", `attachment; filename="shared-canvas.${ext}"`);
36906
+ res.setHeader(
36907
+ "content-disposition",
36908
+ `attachment; filename="${downloadFilename(kind, portalId, result.content)}"`
36909
+ );
35575
36910
  res.statusCode = 200;
35576
36911
  res.end(result.content);
35577
36912
  }
@@ -35813,7 +37148,7 @@ function routeData(cfg, req, res) {
35813
37148
  }
35814
37149
  function readExposurePolicyFile(dataDir) {
35815
37150
  try {
35816
- const raw = fs49.readFileSync(path59.join(dataDir, "exposure-policy.json"), "utf8");
37151
+ const raw = fs49.readFileSync(path58.join(dataDir, "exposure-policy.json"), "utf8");
35817
37152
  const parsed = JSON.parse(raw);
35818
37153
  return parsed && typeof parsed === "object" ? parsed : void 0;
35819
37154
  } catch {
@@ -36498,9 +37833,9 @@ function seedDemoTree() {
36498
37833
 
36499
37834
  // src/commands/host.ts
36500
37835
  function resolveHostConfig(options) {
36501
- 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");
36502
37837
  if (!process.env["WAIRON_PACKS_DIR"]) {
36503
- process.env["WAIRON_PACKS_DIR"] = path60.join(dataDir, "packs");
37838
+ process.env["WAIRON_PACKS_DIR"] = path59.join(dataDir, "packs");
36504
37839
  }
36505
37840
  const cfg = {
36506
37841
  host: options.host || "0.0.0.0",
@@ -36625,13 +37960,13 @@ function openBrowser(url) {
36625
37960
  }
36626
37961
  async function runDev(options = {}) {
36627
37962
  const cwd = process.cwd();
36628
- if (!fs50.existsSync(path60.join(cwd, ".wai"))) {
37963
+ if (!fs50.existsSync(path59.join(cwd, ".wai"))) {
36629
37964
  throw new WaironError(
36630
37965
  "No .wai/ found in the current directory. Run `wairon dev` from a wairon project root (or run `wairon init` first)."
36631
37966
  );
36632
37967
  }
36633
37968
  const hash = crypto20.createHash("sha256").update(cwd).digest("hex").slice(0, 16);
36634
- const dataDir = path60.join(os9.tmpdir(), "wairon-dev", hash);
37969
+ const dataDir = path59.join(os10.tmpdir(), "wairon-dev", hash);
36635
37970
  fs50.mkdirSync(dataDir, { recursive: true });
36636
37971
  registerLocalDevProject(dataDir, "local", cwd);
36637
37972
  const port = options.port ? Number(options.port) : 8080;
@@ -37040,8 +38375,8 @@ async function runHostPacks(action, options = {}) {
37040
38375
  }
37041
38376
  case "install": {
37042
38377
  if (!options.file) throw new WaironError("`--file <path>` (a declarative pack YAML) is required for install.");
37043
- const name = options.name ?? path60.basename(options.file).replace(/\.(ya?ml)$/i, "");
37044
- 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");
37045
38380
  const desc = project2 ? installProjectPack(cfg, cred, project2, name, content) : installGlobalPack(cfg, cred, name, content);
37046
38381
  logger.success(`Installed ${scope} pack "${desc.name}" (${desc.profiles} profile(s), ${desc.languages} language(s)).`);
37047
38382
  if (project2) logger.info("Committed with the project \u2014 every clone and CI will enforce it.");
@@ -37181,15 +38516,29 @@ async function runSurface(action, options = {}) {
37181
38516
  if (format !== "native" && format !== "openapi") {
37182
38517
  throw new WaironError(`Unknown format "${format}" (supported: native, openapi).`);
37183
38518
  }
37184
- 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);
37185
38523
  logger.success(
37186
38524
  `Projected surface of "${result.snapshot.projectName}": ${result.snapshot.interfaces.length} interface(s), ${result.snapshot.types.length} type(s) at audience \u2265 ${audience}.`
37187
38525
  );
37188
- if (result.writtenTo) {
37189
- 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}`);
37190
38532
  } else if (result.rendered) {
37191
38533
  process.stdout.write(`${result.rendered}
37192
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
+ }
37193
38542
  } else {
37194
38543
  for (const entry of result.snapshot.interfaces) {
37195
38544
  logger.info(` ${import_chalk19.default.cyan(entry.id)} (${entry.type}, ${entry.audience}) \u2014 ${entry.methods.length} method(s)`);
@@ -37233,13 +38582,27 @@ async function runSurface(action, options = {}) {
37233
38582
  for (const p of written) logger.info(` ${p}`);
37234
38583
  return;
37235
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
+ }
37236
38599
  default:
37237
- 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).`);
37238
38601
  }
37239
38602
  }
37240
38603
 
37241
38604
  // src/commands/subsystem.ts
37242
- var path61 = __toESM(require("path"));
38605
+ var path60 = __toESM(require("path"));
37243
38606
  init_logger();
37244
38607
  init_errors();
37245
38608
  init_fs();
@@ -37275,9 +38638,9 @@ async function runSubsystemAdd(id, options = {}) {
37275
38638
  updatedAt: now
37276
38639
  };
37277
38640
  createChainedSubsystem(subsystem, displayName);
37278
- const childDir = path61.resolve(getProjectRoot(), options.projectPath);
38641
+ const childDir = path60.resolve(getProjectRoot(), options.projectPath);
37279
38642
  logger.success(`Added external subsystem "${id}" \u2192 ${options.projectPath}`);
37280
- logger.info(`Scaffolded child project at ${path61.relative(process.cwd(), childDir) || "."}`);
38643
+ logger.info(`Scaffolded child project at ${path60.relative(process.cwd(), childDir) || "."}`);
37281
38644
  logger.info(`Design its spec tree from this parent using namespaced ids (e.g. ${id}::<component>).`);
37282
38645
  }
37283
38646
  async function runSubsystemMove(id, options = {}) {
@@ -37300,9 +38663,9 @@ async function runSubsystemExternalize(id, options = {}) {
37300
38663
  throw new WaironError("--project-path (the subproject destination) is required.");
37301
38664
  }
37302
38665
  externalizeSubsystem(id, options.projectPath);
37303
- const childDir = path61.resolve(getProjectRoot(), options.projectPath);
38666
+ const childDir = path60.resolve(getProjectRoot(), options.projectPath);
37304
38667
  logger.success(`Externalized subsystem "${id}" \u2192 ${options.projectPath}`);
37305
- 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).`);
37306
38669
  logger.info("Move the source code there yourself, then run `wairon validate` to confirm the tree.");
37307
38670
  }
37308
38671
  async function runSubsystemInternalize(id) {
@@ -37341,8 +38704,64 @@ program.command("generate").description("Generate agent output files from the sp
37341
38704
  dryRun: opts.dryRun
37342
38705
  });
37343
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
+ }
37344
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) => {
37345
- await runLock({ yes: opts.yes, subsystem: opts.subsystem, recursive: opts.recursive });
38764
+ await runLock2({ yes: opts.yes, subsystem: opts.subsystem, recursive: opts.recursive });
37346
38765
  });
37347
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) => {
37348
38767
  await runValidate({ ci: opts.ci, subsystem: opts.subsystem, recursive: opts.recursive });
@@ -37458,13 +38877,14 @@ mcpCmd.command("status").description("Show whether the wairon MCP server is regi
37458
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) => {
37459
38878
  await runProduce(target, { page: opts.page, token: opts.token });
37460
38879
  });
37461
- 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) => {
37462
38881
  await runSurface(action, {
37463
38882
  audience: opts.audience,
37464
38883
  format: opts.format,
37465
38884
  out: opts.out,
37466
38885
  source: opts.source,
37467
- origin: opts.origin
38886
+ origin: opts.origin,
38887
+ portal: opts.portal
37468
38888
  });
37469
38889
  });
37470
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) => {