@wairon/cli 5.0.1 → 5.0.2-dev.10

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.1";
68
+ WAIRON_VERSION = "5.0.2-dev.10";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  ARCHITECT_AGENT_ID = "agent-architect";
71
71
  ARCHITECT_TEMPLATE_ID = "architect";
@@ -703,7 +703,7 @@ var init_template = __esm({
703
703
  });
704
704
 
705
705
  // src/models/specs.ts
706
- var import_zod6, SpecIdSchema, SpecStatusSchema, BoundaryItemSchema, RequirementItemSchema, DatabaseSpecSchema, DiagramConfigSchema, SURFACE_AUDIENCES, SurfaceAudienceSchema, SystemPublicInterfaceSchema, SystemSpecSchema, PublicInterfaceTypeSchema, PublicInterfaceSchema, TrustedLinkSchema, LintAllowSchema, LintConfigSchema, ExtDataSchema, LifecycleEntrypointSchema, SubsystemSpecSchema, ComponentTypeSchema, PATTERN_TYPES, PortalTypeSchema, DispatchBindingSchema, DurabilitySchema, PatternRefSchema, EventBindingSchema, ComponentSpecSchema, HttpMethodSchema, TransportSchema, EndpointSchema, SEMANTIC_GUARANTEES, GuaranteeSchema, MethodParamSchema, MethodSignatureSchema, InterfaceSpecSchema, NarrativeStepTypeSchema, LoopKindSchema, SwitchCaseSchema, CatchClauseSchema, ParallelBranchSchema, NarrativeStepSchema, NarrativeDetailSchema, ConformanceTierSchema, MethodImplementationSchema, ImplementationSpecSchema, TypeKindSchema, TypeFieldSchema, TypeMethodSchema, InvariantSchema, TypeSpecSchema, SurfaceOriginSchema, SurfaceTypeDefSchema, SurfaceContractEntrySchema, SurfaceSnapshotSchema, GroupSpecSchema;
706
+ var import_zod6, SpecIdSchema, SpecStatusSchema, BoundaryItemSchema, RequirementItemSchema, DatabaseSpecSchema, DiagramConfigSchema, SURFACE_AUDIENCES, SurfaceAudienceSchema, SystemPublicInterfaceSchema, SystemSpecSchema, PublicInterfaceTypeSchema, PublicInterfaceSchema, TrustedLinkSchema, LintAllowSchema, LintConfigSchema, ExtDataSchema, LifecycleEntrypointSchema, SubsystemSpecSchema, ComponentTypeSchema, PATTERN_TYPES, PortalTypeSchema, DispatchBindingSchema, DurabilitySchema, PatternRefSchema, EventBindingSchema, ExternalLinkTypeSchema, ExternalLinkSchema, PortalAuthSchemeSchema, PortalAuthSchema, ComponentSpecSchema, HttpMethodSchema, TransportSchema, EndpointSchema, SEMANTIC_GUARANTEES, GuaranteeSchema, MethodParamSchema, MethodSignatureSchema, InterfaceSpecSchema, NarrativeStepTypeSchema, LoopKindSchema, SwitchCaseSchema, CatchClauseSchema, ParallelBranchSchema, NarrativeStepSchema, NarrativeDetailSchema, ConformanceTierSchema, MethodImplementationSchema, ImplementationSpecSchema, TypeKindSchema, TypeFieldSchema, TypeMethodSchema, InvariantSchema, TypeSpecSchema, SurfaceOriginSchema, SurfaceTypeDefSchema, SurfaceContractEntrySchema, SurfaceSnapshotSchema, NamedOpenApiSpecSchema, GroupSpecSchema;
707
707
  var init_specs = __esm({
708
708
  "src/models/specs.ts"() {
709
709
  "use strict";
@@ -907,6 +907,28 @@ var init_specs = __esm({
907
907
  event: import_zod6.z.string().optional(),
908
908
  description: import_zod6.z.string().optional()
909
909
  });
910
+ ExternalLinkTypeSchema = import_zod6.z.enum(["implementation", "informative"]);
911
+ ExternalLinkSchema = import_zod6.z.object({
912
+ url: import_zod6.z.string(),
913
+ /** Defaults to 'informative' so an untyped link never silently satisfies the source requirement. */
914
+ type: ExternalLinkTypeSchema.default("informative"),
915
+ label: import_zod6.z.string().optional()
916
+ });
917
+ PortalAuthSchemeSchema = import_zod6.z.enum(["none", "apiKey", "bearer", "basic", "oauth2", "openIdConnect", "custom"]);
918
+ PortalAuthSchema = import_zod6.z.object({
919
+ scheme: PortalAuthSchemeSchema,
920
+ in: import_zod6.z.enum(["header", "query", "cookie"]).optional(),
921
+ name: import_zod6.z.string().optional(),
922
+ bearerFormat: import_zod6.z.string().optional(),
923
+ authorizationUrl: import_zod6.z.string().optional(),
924
+ tokenUrl: import_zod6.z.string().optional(),
925
+ refreshUrl: import_zod6.z.string().optional(),
926
+ scopes: import_zod6.z.array(import_zod6.z.object({ name: import_zod6.z.string(), description: import_zod6.z.string() })).optional(),
927
+ flow: import_zod6.z.enum(["authorizationCode", "clientCredentials", "implicit", "password"]).optional(),
928
+ openIdConnectUrl: import_zod6.z.string().optional(),
929
+ description: import_zod6.z.string().optional(),
930
+ example: import_zod6.z.string().optional()
931
+ });
910
932
  ComponentSpecSchema = import_zod6.z.object({
911
933
  id: SpecIdSchema,
912
934
  name: import_zod6.z.string(),
@@ -920,6 +942,10 @@ var init_specs = __esm({
920
942
  dependsOn: import_zod6.z.array(import_zod6.z.string()).default([]),
921
943
  portalType: PortalTypeSchema.optional(),
922
944
  basePath: import_zod6.z.string().optional(),
945
+ /** Portal-only: the API's authentication scheme (see PortalAuthSchema) — projected
946
+ * into the generated OpenAPI's securitySchemes/security. Portals with different auth
947
+ * must be separate components (one auth per portal ⇒ one OpenAPI spec per portal). */
948
+ auth: PortalAuthSchema.optional(),
923
949
  /** Portal-only: capability → component.method dispatch table (see DispatchBindingSchema). */
924
950
  dispatch: import_zod6.z.array(DispatchBindingSchema).optional(),
925
951
  /** Store-only: whether held state survives restart (see DurabilitySchema). */
@@ -932,6 +958,11 @@ var init_specs = __esm({
932
958
  patterns: import_zod6.z.array(PatternRefSchema).optional(),
933
959
  /** Optional component variant — a declared, base-anchored specialization of this component's stereotype (resolved against the variant registry; UNKNOWN_VARIANT / VARIANT_BASE_MISMATCH). */
934
960
  variant: import_zod6.z.string().optional(),
961
+ /** Opaque external references (see ExternalLinkSchema) — documented URLs wairon does
962
+ * not fetch or validate. An `implementation` link is the external source-of-record and
963
+ * satisfies the source requirement for a source-less implementation (suppresses
964
+ * MISSING_SOURCE_PATH); `informative` links are context only. */
965
+ externalLinks: import_zod6.z.array(ExternalLinkSchema).optional(),
935
966
  /** Per-spec lint suppressions (see LintConfigSchema). */
936
967
  lint: LintConfigSchema.optional(),
937
968
  /** Opaque pack/tool extension data (see ExtDataSchema) — preserved verbatim. */
@@ -1064,6 +1095,18 @@ var init_specs = __esm({
1064
1095
  // Required if type is 'call', references Method name on target interface
1065
1096
  capability: import_zod6.z.string().optional(),
1066
1097
  // Required if type is 'dispatch': the capability routed through the target Portal's dispatch table
1098
+ /**
1099
+ * call/dispatch only: the credential this step presents to an authed callee
1100
+ * Portal, and WHERE it is loaded from (`from`). Two forms: an OPAQUE source
1101
+ * (`env:API_KEY`, a config key, `vault:path`, a free note) — a design note
1102
+ * wairon never resolves; or a MODELED reference `component:<id>` pointing at
1103
+ * the Adapter/Store that provides the secret — validated to resolve, be an
1104
+ * Adapter/Store, and be wired to the presenter (a checked graph edge). The
1105
+ * actual secret is never stored here. Absence on a call into a Portal whose
1106
+ * `auth ≠ none` warns (PORTAL_AUTH_UNMET), so credential loading is never
1107
+ * overlooked.
1108
+ */
1109
+ auth: import_zod6.z.object({ from: import_zod6.z.string(), note: import_zod6.z.string().optional() }).optional(),
1067
1110
  assertsGuarantees: import_zod6.z.array(GuaranteeSchema).optional(),
1068
1111
  /**
1069
1112
  * Declared entity invariants this step upholds, as "<type-id>.<invariant-id>"
@@ -1280,7 +1323,12 @@ var init_specs = __esm({
1280
1323
  dispatch: import_zod6.z.array(DispatchBindingSchema).optional(),
1281
1324
  details: import_zod6.z.string().default(""),
1282
1325
  version: import_zod6.z.string().optional(),
1283
- stability: import_zod6.z.string().optional()
1326
+ stability: import_zod6.z.string().optional(),
1327
+ /** Projected copy of the backing Portal's auth (see PortalAuthSchema) — the codec
1328
+ * emits it as OpenAPI securitySchemes/security. */
1329
+ auth: PortalAuthSchema.optional(),
1330
+ /** The backing Portal's basePath — becomes the per-portal OpenAPI `servers` url. */
1331
+ basePath: import_zod6.z.string().optional()
1284
1332
  });
1285
1333
  SurfaceSnapshotSchema = import_zod6.z.object({
1286
1334
  /** Producing project/system name — the snapshot's resolution identity. */
@@ -1295,6 +1343,11 @@ var init_specs = __esm({
1295
1343
  /** Transitive type closure of every exported signature — self-contained. */
1296
1344
  types: import_zod6.z.array(SurfaceTypeDefSchema).default([])
1297
1345
  });
1346
+ NamedOpenApiSpecSchema = import_zod6.z.object({
1347
+ portalId: import_zod6.z.string(),
1348
+ name: import_zod6.z.string(),
1349
+ document: import_zod6.z.string()
1350
+ });
1298
1351
  GroupSpecSchema = import_zod6.z.object({
1299
1352
  kind: import_zod6.z.literal("group"),
1300
1353
  id: SpecIdSchema,
@@ -2652,6 +2705,7 @@ function buildCanvasModel(issues = []) {
2652
2705
  ...ownerOf.has(comp.id) ? { owner: ownerOf.get(comp.id) } : {},
2653
2706
  owns: comp.owns.filter((o) => componentIds.has(o)),
2654
2707
  dependsOn: comp.dependsOn,
2708
+ ...comp.externalLinks && comp.externalLinks.length ? { externalLinks: comp.externalLinks } : {},
2655
2709
  interfaces: compInterfaces.map((i) => ({
2656
2710
  id: i.id,
2657
2711
  name: i.name,
@@ -3304,12 +3358,21 @@ var MODEL = __MODEL_JSON__;
3304
3358
  var configuredLineStyle = ['bezier', 'straight', 'taxi'].indexOf(diagramConfig.lineStyle) >= 0 ? diagramConfig.lineStyle : 'bezier';
3305
3359
  var defaultViewKind = diagramConfig.defaultView === 'types' ? 'types' : (diagramConfig.defaultView === 'databases' && showDatabaseTab ? 'databases' : 'system');
3306
3360
 
3361
+ // Stage J: a deep link (opts.initialRoute) seeds the initial view directly so a
3362
+ // refresh / shared URL renders the right scope with NO root-first flash. Parsed
3363
+ // by the same resolver as openRoute (hoisted below). No onViewChange fires for
3364
+ // this initial seed.
3365
+ var initialView = { kind: defaultViewKind, id: null };
3366
+ if (typeof opts !== 'undefined' && opts && typeof opts.initialRoute === 'string' && opts.initialRoute.length) {
3367
+ initialView = resolveRoute(opts.initialRoute);
3368
+ }
3369
+
3307
3370
  var state = {
3308
- view: { kind: defaultViewKind, id: null },
3309
- internals: false,
3310
- externals: true,
3311
- dataCoupling: false,
3312
- showIssues: false,
3371
+ view: initialView,
3372
+ internals: typeof saved.internals === 'boolean' ? saved.internals : false,
3373
+ externals: typeof saved.externals === 'boolean' ? saved.externals : true,
3374
+ dataCoupling: typeof saved.dataCoupling === 'boolean' ? saved.dataCoupling : false,
3375
+ showIssues: typeof saved.showIssues === 'boolean' ? saved.showIssues : false,
3313
3376
  query: '',
3314
3377
  selected: null,
3315
3378
  selectedKind: null,
@@ -3324,6 +3387,9 @@ var MODEL = __MODEL_JSON__;
3324
3387
  // Set by buildTypeElements when the ERD is degraded for performance (huge
3325
3388
  // scopes); consumed by renderTypesNotice to explain the level-of-detail.
3326
3389
  var typesNotice = '';
3390
+ // Stage J: true while openRoute is applying a URL-driven view change, so the
3391
+ // onViewChange callback is suppressed and we do not loop URL -> engine -> URL.
3392
+ var applyingRoute = false;
3327
3393
 
3328
3394
  function viewKey() {
3329
3395
  // 'types2' + detail level: table sizes differ per detail, and the prefix
@@ -3343,6 +3409,10 @@ var MODEL = __MODEL_JSON__;
3343
3409
  lineStyle: state.lineStyle,
3344
3410
  panelOpen: state.panelOpen,
3345
3411
  panelWidth: state.panelWidth,
3412
+ internals: state.internals,
3413
+ externals: state.externals,
3414
+ dataCoupling: state.dataCoupling,
3415
+ showIssues: state.showIssues,
3346
3416
  }));
3347
3417
  } catch (e) { /* non-fatal */ }
3348
3418
  }
@@ -4749,6 +4819,78 @@ var MODEL = __MODEL_JSON__;
4749
4819
  state.typesRenderAll = false; // a fresh scope re-evaluates the LOD budget
4750
4820
  rebuild(true);
4751
4821
  renderPanel();
4822
+ notifyViewChange();
4823
+ }
4824
+
4825
+ // ---- Stage J: URL <-> view routing ----------------------------------------
4826
+ // The canvas navigation lives in the URL path (refresh-safe + shareable). A
4827
+ // route is the string AFTER /canvas/<project>: '' = system root; a '/'-joined
4828
+ // NAMESPACE (e.g. 'a/b') that resolves against the model to a subsystem or a
4829
+ // component; or the 'types'/'databases' view modes with an optional scope.
4830
+ // A segment is the '::' namespace with '/' as separator, each part encoded.
4831
+ function encSeg(s) { return encodeURIComponent(String(s)); }
4832
+ function routeOf(view) {
4833
+ if (!view) return '';
4834
+ var k = view.kind;
4835
+ if (k === 'system') return '';
4836
+ if (k === 'types' || k === 'databases') {
4837
+ return view.id ? k + '/' + view.id.split('::').map(encSeg).join('/') : k;
4838
+ }
4839
+ if (k === 'subsystem') {
4840
+ return view.id ? view.id.split('::').map(encSeg).join('/') : '';
4841
+ }
4842
+ if (k === 'component') {
4843
+ // A component's full namespace path IS its id: a chained subproject carries
4844
+ // the subsystem prefix in the id (a::b::comp), a flat project uses the bare
4845
+ // id (comp). Serializing comp.id split on '::' round-trips exactly via the
4846
+ // compById lookup in resolveRoute. Owner-pattern nesting is deliberately NOT
4847
+ // encoded (ownership is a separate axis; comp.id does not embed the owner).
4848
+ var c = compById[view.id];
4849
+ var full = c ? c.id : view.id;
4850
+ return full.split('::').map(encSeg).join('/');
4851
+ }
4852
+ return '';
4853
+ }
4854
+ function resolveRoute(routeStr) {
4855
+ var parts = String(routeStr || '').split('/').filter(function (s) { return s.length > 0; }).map(decodeURIComponent);
4856
+ if (!parts.length) return { kind: 'system', id: null };
4857
+ // NOTE: a subsystem literally named 'types'/'databases' is SHADOWED by these
4858
+ // view-mode routes (acceptable \u2014 the modes own those first segments).
4859
+ if (parts[0] === 'types') {
4860
+ return { kind: 'types', id: parts.length > 1 ? parts.slice(1).join('::') : null };
4861
+ }
4862
+ if (parts[0] === 'databases') {
4863
+ if (!showDatabaseTab) return { kind: 'system', id: null };
4864
+ return { kind: 'databases', id: parts.length > 1 ? parts.slice(1).join('::') : null };
4865
+ }
4866
+ var joined = parts.join('::');
4867
+ if (subById[joined]) return { kind: 'subsystem', id: joined };
4868
+ if (compById[joined]) return { kind: 'component', id: joined };
4869
+ return { kind: 'system', id: null }; // unknown id -> fall back to the root
4870
+ }
4871
+ // Apply a route (URL -> engine) WITHOUT echoing back through onViewChange.
4872
+ function openRoute(routeStr) {
4873
+ var v = resolveRoute(routeStr);
4874
+ if (state.view.kind === v.kind && state.view.id === v.id) return;
4875
+ applyingRoute = true;
4876
+ try {
4877
+ state.view = { kind: v.kind, id: v.id };
4878
+ state.selected = null;
4879
+ state.selectedKind = null;
4880
+ state.typesRenderAll = false;
4881
+ rebuild(true);
4882
+ renderPanel();
4883
+ } finally {
4884
+ applyingRoute = false;
4885
+ }
4886
+ }
4887
+ // Notify the embedder (engine -> URL) of the current view; suppressed while a
4888
+ // route is being applied so the URL is not driven in a loop.
4889
+ function notifyViewChange() {
4890
+ if (applyingRoute) return;
4891
+ if (typeof opts !== 'undefined' && opts && typeof opts.onViewChange === 'function') {
4892
+ try { opts.onViewChange(routeOf(state.view)); } catch (e) { /* ignore */ }
4893
+ }
4752
4894
  }
4753
4895
  // Explain (and offer to override) a performance-degraded ERD.
4754
4896
  function renderTypesNotice() {
@@ -4899,6 +5041,7 @@ var MODEL = __MODEL_JSON__;
4899
5041
  if (t.ghost) {
4900
5042
  state.view = parentViewOf(t.kind, t.id);
4901
5043
  rebuild(true);
5044
+ notifyViewChange();
4902
5045
  select(t.kind, t.id, true);
4903
5046
  return;
4904
5047
  }
@@ -4920,10 +5063,16 @@ var MODEL = __MODEL_JSON__;
4920
5063
  }
4921
5064
 
4922
5065
  document.getElementById('search').addEventListener('input', function (ev) { state.query = ev.target.value.trim(); rebuild(false); });
4923
- document.getElementById('internalsToggle').addEventListener('change', function (ev) { state.internals = ev.target.checked; rebuild(true); });
4924
- document.getElementById('externalsToggle').addEventListener('change', function (ev) { state.externals = ev.target.checked; rebuild(true); });
4925
- document.getElementById('dataCouplingToggle').addEventListener('change', function (ev) { state.dataCoupling = ev.target.checked; renderLegend(); rebuild(true); });
4926
- document.getElementById('issuesToggle').addEventListener('change', function (ev) { state.showIssues = ev.target.checked; rebuild(false); renderPanel(); });
5066
+ // Sync each View toggle's checkbox from the (possibly persisted) state, then
5067
+ // persist on change so the choices survive a refresh (see persist()/saved).
5068
+ document.getElementById('internalsToggle').checked = state.internals;
5069
+ document.getElementById('internalsToggle').addEventListener('change', function (ev) { state.internals = ev.target.checked; persist(); rebuild(true); });
5070
+ document.getElementById('externalsToggle').checked = state.externals;
5071
+ document.getElementById('externalsToggle').addEventListener('change', function (ev) { state.externals = ev.target.checked; persist(); rebuild(true); });
5072
+ document.getElementById('dataCouplingToggle').checked = state.dataCoupling;
5073
+ document.getElementById('dataCouplingToggle').addEventListener('change', function (ev) { state.dataCoupling = ev.target.checked; persist(); renderLegend(); rebuild(true); });
5074
+ document.getElementById('issuesToggle').checked = state.showIssues;
5075
+ document.getElementById('issuesToggle').addEventListener('change', function (ev) { state.showIssues = ev.target.checked; persist(); rebuild(false); renderPanel(); });
4927
5076
  document.getElementById('dragToggle').addEventListener('change', function (ev) { cy.autolock(!ev.target.checked); });
4928
5077
  // Mode seg: Components \u21C4 Types. Entering Types keeps the current subsystem
4929
5078
  // scope, so a subsystem's own types (plus shared ones) show scoped.
@@ -5139,18 +5288,20 @@ var MODEL = __MODEL_JSON__;
5139
5288
  return markers[id];
5140
5289
  }
5141
5290
  var collapsed = [];
5142
- // True content overflow in px, measured with FRACTIONAL rect precision:
5143
- // scrollWidth/clientWidth are rounded integers and scrollWidth never reads
5144
- // below clientWidth, so a sub-pixel overflow that still paints a scrollbar
5145
- // is invisible to them. Positive = overflowing; negative = headroom.
5291
+ // Signed fit measure in px: positive = overflowing, negative = headroom.
5292
+ // The header is a flex row whose ONLY flex:1 child is the .spacer, so the
5293
+ // spacer's rendered width IS the free space -- it grows to absorb all slack
5294
+ // and collapses to 0 the instant the row is full. That makes a right-edge
5295
+ // measurement useless (the spacer keeps the trailing controls pinned to the
5296
+ // right padding at every width, so their edge always reads as a bare fit),
5297
+ // and scrollWidth clamps at clientWidth so it can't report headroom either.
5298
+ // So take headroom from the spacer's own (fractional) width, and true
5299
+ // overflow from scrollWidth - clientWidth (only ever > 0 once the spacer has
5300
+ // already collapsed to 0). The two terms are mutually exclusive.
5146
5301
  function overflowPx() {
5147
- var box = hdr.getBoundingClientRect();
5148
- var edge = box.left;
5149
- for (var c = hdr.firstElementChild; c; c = c.nextElementSibling) {
5150
- var cr = c.getBoundingClientRect();
5151
- if (cr.width > 0 && cr.right > edge) edge = cr.right;
5152
- }
5153
- return edge - (box.right - 14); // 14 = the header's right padding
5302
+ var spacer = hdr.querySelector('.spacer');
5303
+ var slack = spacer ? spacer.getBoundingClientRect().width : 0;
5304
+ return (hdr.scrollWidth - hdr.clientWidth) - slack;
5154
5305
  }
5155
5306
  function reflow() {
5156
5307
  // Not laid out (hidden tab, non-browser DOM) \u2014 measuring would misfire.
@@ -5911,10 +6062,12 @@ var MODEL = __MODEL_JSON__;
5911
6062
  if (kind === 'type' && state.view.kind !== 'types' && state.view.kind !== 'databases') {
5912
6063
  state.view = { kind: 'types', id: typesScopeFromView() };
5913
6064
  rebuild(true);
6065
+ notifyViewChange();
5914
6066
  } else if (kind === 'component' && (state.view.kind === 'types' || state.view.kind === 'databases')) {
5915
6067
  var c = compById[id];
5916
6068
  state.view = { kind: 'subsystem', id: c ? c.subsystem : null };
5917
6069
  rebuild(true);
6070
+ notifyViewChange();
5918
6071
  }
5919
6072
  state.selectedKind = kind;
5920
6073
  state.selected = id;
@@ -5973,6 +6126,14 @@ var MODEL = __MODEL_JSON__;
5973
6126
  if (!exposes || !openApiAllowed()) return '';
5974
6127
  return '<div class="openbtn"><button class="tbtn" data-openapi-tag="' + esc(tag || '') + '">\\u25A4 View OpenAPI \\u2197</button></div>';
5975
6128
  }
6129
+ // "Open in Specs" \u2014 deep-links the focused spec into the hosted Specs value
6130
+ // editor via a host hook (opts.onOpenSpec). Hidden when no host provides it
6131
+ // (standalone file, shared page): those have no editor to open. kind is the
6132
+ // spec layer, id its qualified spec id.
6133
+ function openSpecButton(kind, id) {
6134
+ if (typeof opts === 'undefined' || !opts || typeof opts.onOpenSpec !== 'function') return '';
6135
+ return '<div class="openbtn"><button class="tbtn" data-openspec-kind="' + kind + '" data-openspec-id="' + esc(id) + '">\\u270E Open in Specs \\u2197</button></div>';
6136
+ }
5976
6137
 
5977
6138
  function renderPanel() {
5978
6139
  var head = '', body = '';
@@ -5995,7 +6156,8 @@ var MODEL = __MODEL_JSON__;
5995
6156
  + (scopeFocus ? staticChip('current view') : '')
5996
6157
  + chip(c.subsystem, 'subsystem', c.subsystem)
5997
6158
  + (scopeFocus ? '' : openViewButton('component', c.id, c.owns.length > 0))
5998
- + openApiButton(componentExposesApi(c), c.apiTag);
6159
+ + openApiButton(componentExposesApi(c), c.id)
6160
+ + openSpecButton('component', c.id);
5999
6161
 
6000
6162
  var linkedTypes = MODEL.types.filter(function (t) { return t.componentClass === c.id; });
6001
6163
  if (linkedTypes.length) {
@@ -6005,6 +6167,14 @@ var MODEL = __MODEL_JSON__;
6005
6167
  }
6006
6168
  body += '<p class="desc">' + esc(c.description) + '</p>';
6007
6169
 
6170
+ if (c.externalLinks && c.externalLinks.length) {
6171
+ body += section('External links', c.externalLinks.length, c.externalLinks.map(function (l) {
6172
+ var label = l.label || l.url;
6173
+ var tag = l.type === 'implementation' ? staticChip('source') : '';
6174
+ return '<div class="method">' + tag + '<a href="' + esc(l.url) + '" target="_blank" rel="noopener" style="color:var(--accent);word-break:break-all">' + esc(label) + ' \\u2197</a></div>';
6175
+ }).join(''), true);
6176
+ }
6177
+
6008
6178
  var depInner = (c.dependsOn.length ? c.dependsOn.map(function (d) { return chip(d, 'component', d); }).join('') : '<span class="desc">none</span>')
6009
6179
  + (c.owns.length ? '<div style="margin-top:8px"><b style="font-size:11px">Owns:</b><br>' + c.owns.map(function (d) { return chip(d, 'component', d); }).join('') + '</div>' : '');
6010
6180
  body += section('Dependencies', c.dependsOn.length + c.owns.length, depInner, true);
@@ -6079,7 +6249,8 @@ var MODEL = __MODEL_JSON__;
6079
6249
  MODEL.types.forEach(function (t2) { if (t2.id === focusId) ty = t2; });
6080
6250
  if (ty) {
6081
6251
  head = '<h2>' + esc(ty.name) + '</h2>' + staticChip('\\u00AB' + ty.kind + '\\u00BB')
6082
- + (ty.subsystem ? chip(ty.subsystem, 'subsystem', ty.subsystem) : staticChip('system-level shared'));
6252
+ + (ty.subsystem ? chip(ty.subsystem, 'subsystem', ty.subsystem) : staticChip('system-level shared'))
6253
+ + openSpecButton('type', ty.id);
6083
6254
 
6084
6255
  if (ty.componentClass) {
6085
6256
  head += '<div style="margin-top:6px"><b style="font-size:11px">Class Component:</b> ' + chip(ty.componentClass, 'component', ty.componentClass) + '</div>';
@@ -6155,7 +6326,8 @@ var MODEL = __MODEL_JSON__;
6155
6326
  + (s.status ? staticChip(s.status) : '')
6156
6327
  + (scopeFocus ? staticChip('current view') : '')
6157
6328
  + (scopeFocus ? '' : openViewButton('subsystem', s.id, subKids > 0))
6158
- + openApiButton(subsystemExposesApi(s.id), '');
6329
+ + openApiButton(subsystemExposesApi(s.id), '')
6330
+ + openSpecButton('subsystem', s.id);
6159
6331
  body += '<p class="desc">' + esc(s.description) + '</p>';
6160
6332
  if (s.trustedLinks.length) {
6161
6333
  body += section('Trusted links (fast lanes)', s.trustedLinks.length, s.trustedLinks.map(function (t2) {
@@ -6213,6 +6385,16 @@ var MODEL = __MODEL_JSON__;
6213
6385
  });
6214
6386
  })(oapis[oi]);
6215
6387
  }
6388
+ var ospecs = panel.querySelectorAll('[data-openspec-kind]');
6389
+ for (var si = 0; si < ospecs.length; si++) {
6390
+ (function (b) {
6391
+ b.addEventListener('click', function () {
6392
+ if (typeof opts !== 'undefined' && opts && typeof opts.onOpenSpec === 'function') {
6393
+ opts.onOpenSpec(b.getAttribute('data-openspec-kind'), b.getAttribute('data-openspec-id') || '');
6394
+ }
6395
+ });
6396
+ })(ospecs[si]);
6397
+ }
6216
6398
  var flows = panel.querySelectorAll('[data-flow-comp]');
6217
6399
  for (var j = 0; j < flows.length; j++) {
6218
6400
  (function (b) {
@@ -6225,6 +6407,23 @@ var MODEL = __MODEL_JSON__;
6225
6407
  }
6226
6408
 
6227
6409
  renderPanel();
6410
+ // Stage G: a deep link may focus a component and/or open a method's narrative
6411
+ // modal once the seeded view + DOM + cy graph exist. The host parses the URL hash
6412
+ // into opts.initialSelect / opts.initialFlow \u2014 both carry the component id, so this
6413
+ // works whether the seeded view is the component itself or its parent subsystem
6414
+ // (a leaf component has no meaningful "inside", so Specs deep-links open the parent
6415
+ // and focus the component here).
6416
+ (function () {
6417
+ if (typeof opts === 'undefined' || !opts) return;
6418
+ var f = opts.initialFlow, s = opts.initialSelect;
6419
+ var focusComp = (f && f.comp) || (s && s.comp);
6420
+ if (focusComp && compById[focusComp]) {
6421
+ try { select('component', focusComp, true); } catch (e) { /* ignore */ }
6422
+ }
6423
+ if (f && f.comp && f.method && compById[f.comp]) {
6424
+ try { openFlow(f.comp, f.method, f.mode === 'steps' ? 'steps' : 'flow'); } catch (e) { /* ignore */ }
6425
+ }
6426
+ })();
6228
6427
  })();
6229
6428
  </script>
6230
6429
  </body>
@@ -6762,11 +6961,63 @@ function operationFor(method2, closureIds) {
6762
6961
  }
6763
6962
  return op;
6764
6963
  }
6765
- function toOpenApi(snapshot) {
6766
- const closureIds = new Set(snapshot.types.map((t) => t.id));
6767
- const httpEntries = snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
6964
+ function securitySchemeObject(auth) {
6965
+ if (auth.scheme === "none") return null;
6966
+ const desc = auth.description ? { description: auth.description } : {};
6967
+ switch (auth.scheme) {
6968
+ case "apiKey":
6969
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "X-API-Key", ...desc };
6970
+ case "bearer":
6971
+ return { type: "http", scheme: "bearer", ...auth.bearerFormat ? { bearerFormat: auth.bearerFormat } : {}, ...desc };
6972
+ case "basic":
6973
+ return { type: "http", scheme: "basic", ...desc };
6974
+ case "oauth2": {
6975
+ const flow = { scopes: Object.fromEntries((auth.scopes ?? []).map((s) => [s.name, s.description])) };
6976
+ if (auth.authorizationUrl) flow.authorizationUrl = auth.authorizationUrl;
6977
+ if (auth.tokenUrl) flow.tokenUrl = auth.tokenUrl;
6978
+ if (auth.refreshUrl) flow.refreshUrl = auth.refreshUrl;
6979
+ return { type: "oauth2", flows: { [auth.flow ?? "authorizationCode"]: flow }, ...desc };
6980
+ }
6981
+ case "openIdConnect":
6982
+ return { type: "openIdConnect", openIdConnectUrl: auth.openIdConnectUrl ?? "", ...desc };
6983
+ case "custom":
6984
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "Authorization", description: auth.description ?? auth.example ?? "Custom authentication scheme." };
6985
+ default:
6986
+ return null;
6987
+ }
6988
+ }
6989
+ function schemeBaseName(scheme) {
6990
+ return { apiKey: "ApiKeyAuth", bearer: "BearerAuth", basic: "BasicAuth", oauth2: "OAuth2", openIdConnect: "OpenIdConnect", custom: "CustomAuth" }[scheme] ?? "Auth";
6991
+ }
6992
+ function buildSecurity(entries) {
6993
+ const schemes = {};
6994
+ const nameByContent = /* @__PURE__ */ new Map();
6995
+ const securityByEntry = /* @__PURE__ */ new Map();
6996
+ for (const entry of entries) {
6997
+ if (!entry.auth) continue;
6998
+ const obj = securitySchemeObject(entry.auth);
6999
+ if (!obj) continue;
7000
+ const content = JSON.stringify(obj);
7001
+ let name = nameByContent.get(content);
7002
+ if (!name) {
7003
+ name = schemeBaseName(entry.auth.scheme);
7004
+ for (let n = 2; schemes[name]; n++) name = schemeBaseName(entry.auth.scheme) + n;
7005
+ schemes[name] = obj;
7006
+ nameByContent.set(content, name);
7007
+ }
7008
+ const scopeNames = entry.auth.scheme === "oauth2" ? (entry.auth.scopes ?? []).map((s) => s.name) : [];
7009
+ securityByEntry.set(entry.id, { [name]: scopeNames });
7010
+ }
7011
+ return { schemes, securityByEntry };
7012
+ }
7013
+ function httpEntriesOf(snapshot) {
7014
+ return snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
7015
+ }
7016
+ function renderDoc(snapshot, entries, closureIds, opts = {}) {
7017
+ const { schemes, securityByEntry } = buildSecurity(entries);
6768
7018
  const paths = {};
6769
- for (const entry of httpEntries) {
7019
+ for (const entry of entries) {
7020
+ const security = securityByEntry.get(entry.id);
6770
7021
  for (const method2 of entry.methods) {
6771
7022
  const endpoint = method2.endpoint;
6772
7023
  if (!endpoint || endpoint.transport !== "HTTP") continue;
@@ -6774,7 +7025,8 @@ function toOpenApi(snapshot) {
6774
7025
  paths[p] = paths[p] ?? {};
6775
7026
  paths[p][endpoint.method.toLowerCase()] = {
6776
7027
  tags: [entry.id],
6777
- ...operationFor(method2, closureIds)
7028
+ ...operationFor(method2, closureIds),
7029
+ ...security ? { security: [security] } : {}
6778
7030
  };
6779
7031
  }
6780
7032
  }
@@ -6787,19 +7039,41 @@ function toOpenApi(snapshot) {
6787
7039
  required: t.fields.filter((f) => !f.optional).map((f) => f.name)
6788
7040
  };
6789
7041
  }
6790
- const doc = {
7042
+ const components = {};
7043
+ if (Object.keys(schemas).length) components.schemas = schemas;
7044
+ if (Object.keys(schemes).length) components.securitySchemes = schemes;
7045
+ const basePaths = [...new Set(entries.map((e) => e.basePath).filter((b) => !!b))];
7046
+ const servers = opts.servers && basePaths.length === 1 ? [{ url: basePaths[0] }] : void 0;
7047
+ return {
6791
7048
  openapi: "3.1.0",
6792
7049
  info: {
6793
- title: snapshot.projectName,
7050
+ title: opts.title ?? snapshot.projectName,
6794
7051
  version: snapshot.version ?? "0.0.0",
6795
7052
  ...snapshot.stateId ? { "x-wairon-state-id": snapshot.stateId } : {},
6796
7053
  "x-wairon-origin": snapshot.origin,
6797
7054
  "x-wairon-generated-at": snapshot.generatedAt
6798
7055
  },
7056
+ ...servers ? { servers } : {},
6799
7057
  paths,
6800
- ...Object.keys(schemas).length ? { components: { schemas } } : {}
7058
+ ...Object.keys(components).length ? { components } : {}
6801
7059
  };
6802
- return JSON.stringify(doc, null, 2);
7060
+ }
7061
+ function toOpenApiSet(snapshot) {
7062
+ const closureIds = new Set(snapshot.types.map((t) => t.id));
7063
+ const byPortal = /* @__PURE__ */ new Map();
7064
+ const order = [];
7065
+ for (const entry of httpEntriesOf(snapshot)) {
7066
+ if (!byPortal.has(entry.component)) {
7067
+ byPortal.set(entry.component, []);
7068
+ order.push(entry.component);
7069
+ }
7070
+ byPortal.get(entry.component).push(entry);
7071
+ }
7072
+ return order.map((portalId) => {
7073
+ const entries = byPortal.get(portalId);
7074
+ const name = entries[0]?.name ?? portalId;
7075
+ return { portalId, name, document: JSON.stringify(renderDoc(snapshot, entries, closureIds, { title: name, servers: true }), null, 2) };
7076
+ });
6803
7077
  }
6804
7078
  function isOpenApiDocument(body) {
6805
7079
  try {
@@ -6821,6 +7095,39 @@ function typeRefFromSchema(schema) {
6821
7095
  if (typeof t === "string" && t !== "object") return t;
6822
7096
  return "json";
6823
7097
  }
7098
+ function authFromSecurityScheme(scheme) {
7099
+ const desc = typeof scheme.description === "string" ? { description: scheme.description } : {};
7100
+ if (scheme.type === "apiKey") {
7101
+ return {
7102
+ scheme: "apiKey",
7103
+ ...scheme.in === "header" || scheme.in === "query" || scheme.in === "cookie" ? { in: scheme.in } : {},
7104
+ ...typeof scheme.name === "string" ? { name: scheme.name } : {},
7105
+ ...desc
7106
+ };
7107
+ }
7108
+ if (scheme.type === "http") {
7109
+ if (scheme.scheme === "bearer") return { scheme: "bearer", ...typeof scheme.bearerFormat === "string" ? { bearerFormat: scheme.bearerFormat } : {}, ...desc };
7110
+ if (scheme.scheme === "basic") return { scheme: "basic", ...desc };
7111
+ }
7112
+ if (scheme.type === "oauth2") {
7113
+ const flows = scheme.flows ?? {};
7114
+ const flowKey = Object.keys(flows)[0];
7115
+ const f = flows[flowKey] ?? {};
7116
+ return {
7117
+ scheme: "oauth2",
7118
+ ...flowKey === "authorizationCode" || flowKey === "clientCredentials" || flowKey === "implicit" || flowKey === "password" ? { flow: flowKey } : {},
7119
+ ...typeof f.authorizationUrl === "string" ? { authorizationUrl: f.authorizationUrl } : {},
7120
+ ...typeof f.tokenUrl === "string" ? { tokenUrl: f.tokenUrl } : {},
7121
+ ...typeof f.refreshUrl === "string" ? { refreshUrl: f.refreshUrl } : {},
7122
+ scopes: Object.entries(f.scopes ?? {}).map(([name, description]) => ({ name, description })),
7123
+ ...desc
7124
+ };
7125
+ }
7126
+ if (scheme.type === "openIdConnect") {
7127
+ return { scheme: "openIdConnect", openIdConnectUrl: typeof scheme.openIdConnectUrl === "string" ? scheme.openIdConnectUrl : "", ...desc };
7128
+ }
7129
+ return void 0;
7130
+ }
6824
7131
  function fromOpenApi(document, projectName) {
6825
7132
  let parsed;
6826
7133
  try {
@@ -6898,6 +7205,9 @@ function fromOpenApi(document, projectName) {
6898
7205
  }))
6899
7206
  });
6900
7207
  }
7208
+ const securitySchemes = parsed.components?.securitySchemes ?? {};
7209
+ const firstScheme = Object.values(securitySchemes)[0];
7210
+ const importedAuth = firstScheme ? authFromSecurityScheme(firstScheme) : void 0;
6901
7211
  const entry = {
6902
7212
  id: `${projectName}-api`,
6903
7213
  name: typeof info.title === "string" ? info.title : projectName,
@@ -6906,7 +7216,8 @@ function fromOpenApi(document, projectName) {
6906
7216
  component: `${projectName}-api`,
6907
7217
  methods,
6908
7218
  details: typeof info.description === "string" ? info.description : `Imported OpenAPI surface of ${projectName}.`,
6909
- ...typeof info.version === "string" ? { version: info.version } : {}
7219
+ ...typeof info.version === "string" ? { version: info.version } : {},
7220
+ ...importedAuth ? { auth: importedAuth } : {}
6910
7221
  };
6911
7222
  return SurfaceSnapshotSchema.parse({
6912
7223
  projectName,
@@ -7021,6 +7332,10 @@ function projectOwnSurface(maxAudience) {
7021
7332
  component: comp.id,
7022
7333
  methods,
7023
7334
  ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
7335
+ // Project the backing Portal's auth + basePath so the codec can emit
7336
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
7337
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
7338
+ ...comp.basePath ? { basePath: comp.basePath } : {},
7024
7339
  details: raw.details ?? "",
7025
7340
  ...raw.version ? { version: raw.version } : {},
7026
7341
  ...raw.stability ? { stability: raw.stability } : {}
@@ -7064,20 +7379,28 @@ function saveSnapshot(snapshot, rootDir = getProjectRoot()) {
7064
7379
  function loadSurfaceSnapshots() {
7065
7380
  return listSnapshots();
7066
7381
  }
7382
+ function renderOpenApiForms(snapshot) {
7383
+ const renderedSet = toOpenApiSet(snapshot);
7384
+ return { renderedSet, ...renderedSet.length === 1 ? { rendered: renderedSet[0].document } : {} };
7385
+ }
7386
+ function writeSurfaceFile(outPath, body, snapshot) {
7387
+ const resolved = path10.resolve(outPath);
7388
+ fs9.mkdirSync(path10.dirname(resolved), { recursive: true });
7389
+ if (body !== void 0) fs9.writeFileSync(resolved, body);
7390
+ else writeYamlFile(resolved, snapshot);
7391
+ return resolved;
7392
+ }
7067
7393
  function exportSurface(maxAudience, format, outPath) {
7068
7394
  const snapshot = projectOwnSurface(maxAudience);
7069
- const rendered = format === "openapi" ? toOpenApi(snapshot) : void 0;
7070
- let writtenTo;
7071
- if (outPath) {
7072
- fs9.mkdirSync(path10.dirname(path10.resolve(outPath)), { recursive: true });
7073
- if (rendered !== void 0) {
7074
- fs9.writeFileSync(path10.resolve(outPath), rendered);
7075
- } else {
7076
- writeYamlFile(path10.resolve(outPath), snapshot);
7077
- }
7078
- writtenTo = path10.resolve(outPath);
7079
- }
7080
- return { snapshot, ...rendered !== void 0 ? { rendered } : {}, ...writtenTo ? { writtenTo } : {} };
7395
+ const openapi = format === "openapi" ? renderOpenApiForms(snapshot) : void 0;
7396
+ const body = openapi?.rendered ?? openapi?.renderedSet[0]?.document;
7397
+ const writtenTo = outPath ? writeSurfaceFile(outPath, body, snapshot) : void 0;
7398
+ return {
7399
+ snapshot,
7400
+ ...openapi?.rendered !== void 0 ? { rendered: openapi.rendered } : {},
7401
+ ...openapi ? { renderedSet: openapi.renderedSet } : {},
7402
+ ...writtenTo ? { writtenTo } : {}
7403
+ };
7081
7404
  }
7082
7405
  function importSurface(sourcePath, origin) {
7083
7406
  const resolved = path10.resolve(sourcePath);
@@ -8337,13 +8660,16 @@ var init_conformance = __esm({
8337
8660
  if (isInChainedSubproject(component.subsystem, ctx)) continue;
8338
8661
  const draft = ctx.isImplementationDraft(impl);
8339
8662
  if (!impl.sourcePath) {
8340
- ctx.addIssue(
8341
- "warning",
8342
- "MISSING_SOURCE_PATH",
8343
- `Implementation "${impl.id}" declares no sourcePath \u2014 its contract "${impl.contract}" cannot be structurally checked against code.`,
8344
- impl.id,
8345
- draft
8346
- );
8663
+ const hasExternalSource = (component.externalLinks ?? []).some((l) => l.type === "implementation");
8664
+ if (!hasExternalSource) {
8665
+ ctx.addIssue(
8666
+ "warning",
8667
+ "MISSING_SOURCE_PATH",
8668
+ `Implementation "${impl.id}" declares no sourcePath \u2014 its contract "${impl.contract}" cannot be structurally checked against code.`,
8669
+ impl.id,
8670
+ draft
8671
+ );
8672
+ }
8347
8673
  continue;
8348
8674
  }
8349
8675
  const lookup = lookups.get(normalizeSourcePath(impl.sourcePath));
@@ -8732,13 +9058,14 @@ var init_portals = __esm({
8732
9058
  };
8733
9059
  portalsRule = {
8734
9060
  name: "portal-endpoints",
8735
- 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.",
9061
+ 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).",
8736
9062
  codes: [
8737
9063
  { code: "MISSING_PORTAL_TYPE", defaultSeverity: "error", summary: "Portal without a portalType" },
8738
9064
  { code: "MISSING_ENDPOINT", defaultSeverity: "error", summary: "Portal method without a wire endpoint binding" },
8739
9065
  { code: "ENDPOINT_TRANSPORT_MISMATCH", defaultSeverity: "error", summary: "Endpoint transport does not match the Portal portalType" },
8740
9066
  { code: "UNEXPECTED_PORTAL_FIELD", defaultSeverity: "error", summary: "Non-Portal component with portalType/basePath" },
8741
- { code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" }
9067
+ { code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" },
9068
+ { code: "AUTH_ON_NON_PORTAL", defaultSeverity: "warning", summary: "Non-Portal component declaring auth (auth is inbound transport auth, only meaningful on a Portal)" }
8742
9069
  ],
8743
9070
  check(ctx) {
8744
9071
  for (const comp of ctx.components) {
@@ -8790,6 +9117,15 @@ var init_portals = __esm({
8790
9117
  isDraftCtx
8791
9118
  );
8792
9119
  }
9120
+ if (comp.auth !== void 0) {
9121
+ ctx.addIssue(
9122
+ "warning",
9123
+ "AUTH_ON_NON_PORTAL",
9124
+ `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.`,
9125
+ comp.id,
9126
+ isDraftCtx
9127
+ );
9128
+ }
8793
9129
  const compInterfaces = ctx.interfaces.filter((i) => i.component === comp.id);
8794
9130
  for (const intf of compInterfaces) {
8795
9131
  for (const m of intf.methods) {
@@ -11873,6 +12209,95 @@ var init_lint_allows = __esm({
11873
12209
  }
11874
12210
  });
11875
12211
 
12212
+ // src/core/rules/portal-call-auth.ts
12213
+ var COMPONENT_REF_PREFIX, portalCallAuthRule;
12214
+ var init_portal_call_auth = __esm({
12215
+ "src/core/rules/portal-call-auth.ts"() {
12216
+ "use strict";
12217
+ COMPONENT_REF_PREFIX = "component:";
12218
+ portalCallAuthRule = {
12219
+ name: "portal-call-auth",
12220
+ 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.",
12221
+ codes: [
12222
+ { code: "PORTAL_AUTH_UNMET", defaultSeverity: "warning", summary: "A narrative call into another component's authed Portal does not declare where its credential loads from" },
12223
+ { 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)" },
12224
+ { code: "UNKNOWN_AUTH_SOURCE", defaultSeverity: "warning", summary: "auth.from references a component: source that does not exist" },
12225
+ { code: "AUTH_SOURCE_NOT_PROVIDER", defaultSeverity: "warning", summary: "auth.from references a component that is not an Adapter or Store" },
12226
+ { code: "AUTH_SOURCE_UNWIRED", defaultSeverity: "warning", summary: "The presenter declares a component: credential source it does not depend on or own" }
12227
+ ],
12228
+ check(ctx) {
12229
+ for (const impl of ctx.implementations) {
12230
+ const ownComponent = ctx.interfaceMap.get(impl.contract)?.component;
12231
+ const presenter = ownComponent ? ctx.componentMap.get(ownComponent) : void 0;
12232
+ const draft = ctx.isImplementationDraft(impl);
12233
+ for (const method2 of impl.methods ?? []) {
12234
+ for (const step of method2.narrative ?? []) {
12235
+ if (step.type !== "call") continue;
12236
+ if (!step.targetComponent || step.targetComponent === ownComponent) continue;
12237
+ const target = ctx.componentMap.get(step.targetComponent);
12238
+ if (!target || target.componentType !== "Portal") continue;
12239
+ if (!target.auth || target.auth.scheme === "none") continue;
12240
+ if (presenter && presenter.componentType !== "Adapter") {
12241
+ ctx.addIssue(
12242
+ "warning",
12243
+ "AUTH_PRESENTER_NOT_ADAPTER",
12244
+ `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.`,
12245
+ impl.id,
12246
+ draft
12247
+ );
12248
+ }
12249
+ const from = step.auth?.from;
12250
+ if (!from) {
12251
+ ctx.addIssue(
12252
+ "warning",
12253
+ "PORTAL_AUTH_UNMET",
12254
+ `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.`,
12255
+ impl.id,
12256
+ draft
12257
+ );
12258
+ continue;
12259
+ }
12260
+ if (from.startsWith(COMPONENT_REF_PREFIX)) {
12261
+ const srcId = from.slice(COMPONENT_REF_PREFIX.length);
12262
+ const src = ctx.componentMap.get(srcId);
12263
+ if (!src) {
12264
+ ctx.addIssue(
12265
+ "warning",
12266
+ "UNKNOWN_AUTH_SOURCE",
12267
+ `Narrative step ${step.stepNumber} of "${method2.name}" in "${impl.id}": auth.from references component "${srcId}", which does not exist.`,
12268
+ impl.id,
12269
+ draft
12270
+ );
12271
+ } else {
12272
+ if (src.componentType !== "Adapter" && src.componentType !== "Store") {
12273
+ ctx.addIssue(
12274
+ "warning",
12275
+ "AUTH_SOURCE_NOT_PROVIDER",
12276
+ `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).`,
12277
+ impl.id,
12278
+ draft
12279
+ );
12280
+ }
12281
+ const wired = (presenter?.dependsOn ?? []).includes(srcId) || (presenter?.owns ?? []).includes(srcId);
12282
+ if (presenter && !wired) {
12283
+ ctx.addIssue(
12284
+ "warning",
12285
+ "AUTH_SOURCE_UNWIRED",
12286
+ `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.`,
12287
+ impl.id,
12288
+ draft
12289
+ );
12290
+ }
12291
+ }
12292
+ }
12293
+ }
12294
+ }
12295
+ }
12296
+ }
12297
+ };
12298
+ }
12299
+ });
12300
+
11876
12301
  // src/core/rules/index.ts
11877
12302
  function makeScopeFilter(opts) {
11878
12303
  const { components, interfaces, implementations, types, scopeSubsystem } = opts;
@@ -12118,6 +12543,7 @@ var init_rules = __esm({
12118
12543
  init_hidden_state();
12119
12544
  init_dependency_conformance();
12120
12545
  init_lint_allows();
12546
+ init_portal_call_auth();
12121
12547
  init_source_analysis();
12122
12548
  init_types();
12123
12549
  init_type_analysis();
@@ -12139,6 +12565,9 @@ var init_rules = __esm({
12139
12565
  narrativeAntipatternsRule,
12140
12566
  narrativeDetailRule,
12141
12567
  portalsRule,
12568
+ // Cross-call auth: a narrative call into an authed Portal must name its
12569
+ // credential source (rides with the portal family).
12570
+ portalCallAuthRule,
12142
12571
  stereotypeDepsRule,
12143
12572
  patternsRule,
12144
12573
  // Facade shape rides with pattern ownership: same §7 doctrine, narrative side.
@@ -12201,6 +12630,7 @@ var init_rules = __esm({
12201
12630
  // L4 expectations: implementations and their code linkage.
12202
12631
  MISSING_IMPLEMENTATION_METHOD: "implementations",
12203
12632
  MISSING_SOURCE_PATH: "implementations",
12633
+ PORTAL_AUTH_UNMET: "implementations",
12204
12634
  MISSING_SOURCE_FILE: "implementations",
12205
12635
  SOURCE_PATH_ESCAPES_ROOT: "implementations",
12206
12636
  UNREALIZED_METHOD: "implementations",
@@ -12606,6 +13036,28 @@ function buildGraphModel(level) {
12606
13036
  ...t.subsystem ? { parentId: t.subsystem } : {}
12607
13037
  });
12608
13038
  }
13039
+ const componentByInterface = /* @__PURE__ */ new Map();
13040
+ for (const c of model.components) {
13041
+ for (const intf of c.interfaces) componentByInterface.set(intf.id, c.id);
13042
+ }
13043
+ for (const impl of loadImplementationSpecs()) {
13044
+ const componentId = componentByInterface.get(impl.contract);
13045
+ if (!componentId) continue;
13046
+ nodes.push({
13047
+ id: impl.id,
13048
+ label: impl.name,
13049
+ kind: "implementation",
13050
+ level: 4,
13051
+ parentId: componentId,
13052
+ ...impl.status ? { status: impl.status } : {}
13053
+ });
13054
+ candidates.push({ from: componentId, to: impl.id, edgeKind: "owns" });
13055
+ }
13056
+ for (const c of model.components) {
13057
+ for (const memberId of c.owns) {
13058
+ candidates.push({ from: c.id, to: memberId, edgeKind: "owns" });
13059
+ }
13060
+ }
12609
13061
  for (const e of model.edges) {
12610
13062
  candidates.push({ from: e.from, to: e.to, edgeKind: "depends_on" });
12611
13063
  }
@@ -16742,6 +17194,7 @@ NOTICE:
16742
17194
  type: import_zod9.z.enum(["local", "call", "dispatch", "branch", "switch", "loop", "try", "parallel", "jump", "return", "throw"]),
16743
17195
  targetComponent: import_zod9.z.string().optional().describe("call/dispatch: L2 component id (for dispatch, the Portal routed through)"),
16744
17196
  targetMethod: import_zod9.z.string().optional().describe("call: method name on the target"),
17197
+ 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)."),
16745
17198
  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)"),
16746
17199
  capability: import_zod9.z.string().optional().describe("dispatch: the capability routed through the target Portal's dispatch table (validated against it \u2014 UNSERVED_CAPABILITY)"),
16747
17200
  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)"),
@@ -22804,8 +23257,8 @@ function walkForPackages(projectRoot2, currentDir, depth, results) {
22804
23257
  }
22805
23258
  }
22806
23259
  function pathToId(relPath) {
22807
- const basename13 = path28.basename(relPath);
22808
- return basename13.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
23260
+ const basename12 = path28.basename(relPath);
23261
+ return basename12.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
22809
23262
  }
22810
23263
  function pathToName(relPath) {
22811
23264
  const id = pathToId(relPath);
@@ -25454,6 +25907,7 @@ init_validation();
25454
25907
  init_diagram();
25455
25908
  init_loader();
25456
25909
  init_extensions();
25910
+ init_types();
25457
25911
  init_server();
25458
25912
 
25459
25913
  // src/git/config.ts
@@ -25974,7 +26428,11 @@ var hostCore = {
25974
26428
  checkDeclarativePack: (raw) => {
25975
26429
  const result = DeclarativePackSchema.safeParse(raw);
25976
26430
  return result.success ? null : result.error.issues[0]?.message ?? "shape mismatch";
25977
- }
26431
+ },
26432
+ /** The ids of wairon's built-in architectural profiles, read from the core rules
26433
+ * registry's built-in profile set (BUILTIN_PROFILES) — a pure, side-effect-free
26434
+ * read of a bundled constant. */
26435
+ builtinProfileIds: () => [...BUILTIN_PROFILES]
25978
26436
  };
25979
26437
  function validateProjectAsComplete() {
25980
26438
  const config = loadProjectConfig();
@@ -26383,7 +26841,12 @@ function probe(loadRef, baseRoot, scope, displayRef) {
26383
26841
  ref: displayRef,
26384
26842
  profiles: Object.keys(loaded.profiles).length,
26385
26843
  languages: Object.keys(loaded.languages).length,
26386
- rules: loaded.rules.length
26844
+ rules: loaded.rules.length,
26845
+ // Structured id lists paralleling the counts above — a UI can present a
26846
+ // pack's profiles/languages/rules as choices instead of a bare number.
26847
+ profileIds: Object.keys(loaded.profiles),
26848
+ languageIds: Object.keys(loaded.languages),
26849
+ ruleIds: loaded.rules.map((r) => r.name)
26387
26850
  };
26388
26851
  }
26389
26852
  function writeFileAtomic(file, content) {
@@ -26413,6 +26876,71 @@ function storeListGlobalPacks() {
26413
26876
  );
26414
26877
  return [...instance, ...image];
26415
26878
  }
26879
+ function storeListAvailableProfiles() {
26880
+ const out = [];
26881
+ const seen = /* @__PURE__ */ new Set();
26882
+ const emit = (id, source, family) => {
26883
+ const key = JSON.stringify([id, source]);
26884
+ if (seen.has(key)) return;
26885
+ seen.add(key);
26886
+ out.push(family ? { id, source, family } : { id, source });
26887
+ };
26888
+ for (const id of hostCore.builtinProfileIds()) emit(id, "builtin");
26889
+ const scanTier = (dir) => {
26890
+ for (const full of hostCore.discoverPacks(dir)) {
26891
+ try {
26892
+ const loaded = hostCore.loadExtensionPacks([{ ref: full, scope: "global" }], path45.dirname(full));
26893
+ if (loaded.errors.length) continue;
26894
+ const source = loaded.packNames[0] ?? path45.basename(full);
26895
+ for (const [id, def] of Object.entries(loaded.profiles)) emit(id, source, def.family);
26896
+ } catch {
26897
+ }
26898
+ }
26899
+ };
26900
+ scanTier(hostCore.globalPacksDir());
26901
+ scanTier(imagePacksDir());
26902
+ return out;
26903
+ }
26904
+ function readPackContent(full) {
26905
+ const st = fs36.statSync(full);
26906
+ if (st.isFile()) return fs36.readFileSync(full, "utf8");
26907
+ if (st.isDirectory()) {
26908
+ for (const entry of ["pack.yaml", "pack.yml"]) {
26909
+ const p = path45.join(full, entry);
26910
+ if (fs36.existsSync(p) && fs36.statSync(p).isFile()) return fs36.readFileSync(p, "utf8");
26911
+ }
26912
+ }
26913
+ return null;
26914
+ }
26915
+ function storeResolveGlobalPacks(names) {
26916
+ const index = /* @__PURE__ */ new Map();
26917
+ const indexTier = (dir, tier) => {
26918
+ for (const full of hostCore.discoverPacks(dir)) {
26919
+ const manifestName = probe(full, path45.dirname(full), "global", path45.basename(full)).name;
26920
+ const candidate = { full, manifestName, tier };
26921
+ for (const key of [manifestName, stem(full), path45.basename(full)]) {
26922
+ if (!index.has(key)) index.set(key, candidate);
26923
+ }
26924
+ }
26925
+ };
26926
+ indexTier(hostCore.globalPacksDir(), "instance");
26927
+ indexTier(imagePacksDir(), "image");
26928
+ const resolved = [];
26929
+ const resolvedCanonical = /* @__PURE__ */ new Set();
26930
+ const unresolved = [];
26931
+ for (const requested of new Set(names)) {
26932
+ const candidate = index.get(requested);
26933
+ const content = candidate ? readPackContent(candidate.full) : null;
26934
+ if (!candidate || content == null) {
26935
+ unresolved.push(requested);
26936
+ continue;
26937
+ }
26938
+ if (resolvedCanonical.has(candidate.manifestName)) continue;
26939
+ resolvedCanonical.add(candidate.manifestName);
26940
+ resolved.push({ requestedName: requested, name: candidate.manifestName, content, tier: candidate.tier });
26941
+ }
26942
+ return { resolved, unresolved };
26943
+ }
26416
26944
  function storeInstallGlobalPack(name, content) {
26417
26945
  assertName(name);
26418
26946
  assertDeclarative(content);
@@ -26559,12 +27087,32 @@ function installProjectPack(cfg, credential, project2, name, content) {
26559
27087
  requireCap(cfg, credential, "project:admin", "project", project2, "Forbidden \u2014 installing a project pack requires project:admin over the project");
26560
27088
  return executeApprovedInstallProjectPack(cfg, project2, name, content);
26561
27089
  }
27090
+ function listAvailableProfiles(cfg, credential) {
27091
+ requirePrincipal2(cfg, credential);
27092
+ return storeListAvailableProfiles();
27093
+ }
27094
+ function listAdoptableProjectPacks(cfg, credential, project2) {
27095
+ requireCap(cfg, credential, "project:read", "project", project2, "Forbidden \u2014 listing adoptable packs requires project:read over the project");
27096
+ return storeListGlobalPacks();
27097
+ }
27098
+ function adoptProjectPack(cfg, credential, project2, name) {
27099
+ requireCap(cfg, credential, "project:admin", "project", project2, "Forbidden \u2014 adopting a project pack requires project:admin over the project");
27100
+ const resolution = executeApprovedResolveGlobalPacks([name]);
27101
+ if (resolution.resolved.length === 0) {
27102
+ throw new Error(`no such server-global pack "${name}" \u2014 cannot adopt a pack the instance does not carry`);
27103
+ }
27104
+ const resolved = resolution.resolved[0];
27105
+ return executeApprovedInstallProjectPack(cfg, project2, resolved.name, resolved.content);
27106
+ }
26562
27107
  function executeApprovedListProjectPacks(cfg, project2) {
26563
27108
  return runWithProjectRoot(boundProject2(cfg, project2), () => storeListProjectPacks());
26564
27109
  }
26565
27110
  function executeApprovedInstallProjectPack(cfg, project2, name, content) {
26566
27111
  return runWithProjectRoot(boundProject2(cfg, project2), () => storeInstallProjectPack(name, content));
26567
27112
  }
27113
+ function executeApprovedResolveGlobalPacks(names) {
27114
+ return storeResolveGlobalPacks(names);
27115
+ }
26568
27116
  function removeProjectPack(cfg, credential, project2, name) {
26569
27117
  requireCap(cfg, credential, "project:admin", "project", project2, "Forbidden \u2014 removing a project pack requires project:admin over the project");
26570
27118
  runWithProjectRoot(boundProject2(cfg, project2), () => storeRemoveProjectPack(name));
@@ -27476,6 +28024,7 @@ function removeIdentityProviderRecord(dataDir, id) {
27476
28024
  }
27477
28025
  var PROJECT_CREATE_CAPABILITY = "project:create";
27478
28026
  var PROJECT_WRITE_CAPABILITY = "project:write";
28027
+ var PROJECT_READ_CAPABILITY = "project:read";
27479
28028
  var POLICY_MANAGE_CAPABILITY = "project:admin";
27480
28029
  function carriesInstancePermission(cfg, principal, capability) {
27481
28030
  return authorize(cfg.dataDir, principal, capability, "instance", "").value === "yes";
@@ -27515,27 +28064,16 @@ function tryAppendAudit2(cfg, event) {
27515
28064
  );
27516
28065
  }
27517
28066
  }
27518
- function packStem(ref) {
27519
- return path47.basename(ref).replace(/\.(ya?ml|cjs|js)$/i, "");
27520
- }
27521
- function readGlobalPackContent(name) {
27522
- const dir = hostCore.globalPacksDir();
27523
- for (const candidate of [path47.join(dir, `${name}.yaml`), path47.join(dir, `${name}.yml`)]) {
27524
- if (fs38.existsSync(candidate) && fs38.statSync(candidate).isFile()) {
27525
- return fs38.readFileSync(candidate, "utf8");
27526
- }
27527
- }
27528
- const match = hostCore.discoverPacks(dir).find((ref) => packStem(ref) === name || path47.basename(ref) === name);
27529
- if (match && fs38.statSync(match).isFile()) return fs38.readFileSync(match, "utf8");
27530
- return null;
28067
+ var PACK_ENFORCEMENT_MODES = ["warn", "block", "auto_reconcile"];
28068
+ function requiredDefaultNames(policy) {
28069
+ return [.../* @__PURE__ */ new Set([...policy.requiredGlobalPacks, ...policy.defaultProjectPacks])];
27531
28070
  }
27532
28071
  function installedPackNames(cfg, projectId) {
27533
28072
  return executeApprovedListProjectPacks(cfg, projectId).map((d) => d.name);
27534
28073
  }
27535
- function installPacks(cfg, projectId, names) {
27536
- for (const name of new Set(names)) {
27537
- const content = readGlobalPackContent(name);
27538
- if (content) executeApprovedInstallProjectPack(cfg, projectId, name, content);
28074
+ function installResolvedPacks(cfg, projectId, resolution) {
28075
+ for (const pack of resolution.resolved) {
28076
+ executeApprovedInstallProjectPack(cfg, projectId, pack.name, pack.content);
27539
28077
  }
27540
28078
  }
27541
28079
  function readProjectProfileSelection(root) {
@@ -27551,6 +28089,19 @@ function recordProjectProfileSelection(root, selection) {
27551
28089
  writeYamlFile(AI_PATHS.projectConfig(), raw);
27552
28090
  });
27553
28091
  }
28092
+ function readProjectType(root) {
28093
+ return runWithProjectRoot(root, () => {
28094
+ const raw = readYamlFile(AI_PATHS.projectConfig());
28095
+ return raw?.projectType ?? "backend";
28096
+ });
28097
+ }
28098
+ function writeProjectType(root, projectType) {
28099
+ runWithProjectRoot(root, () => {
28100
+ const raw = readYamlFile(AI_PATHS.projectConfig()) ?? {};
28101
+ raw["projectType"] = projectType;
28102
+ writeYamlFile(AI_PATHS.projectConfig(), raw);
28103
+ });
28104
+ }
27554
28105
  function requestPackNames(request) {
27555
28106
  const sel = request.profileSelection;
27556
28107
  if (!sel) return [];
@@ -27568,10 +28119,25 @@ function resolvedSelection(request, policy, selectedBy) {
27568
28119
  return selection;
27569
28120
  }
27570
28121
  function buildEvaluation(input) {
27571
- const { policy, presentPackNames, selectedProfileIds, hasSelection, countMissingPacksAsViolation } = input;
28122
+ const {
28123
+ policy,
28124
+ presentPackNames,
28125
+ selectedProfileIds,
28126
+ hasSelection,
28127
+ countMissingPacksAsViolation,
28128
+ requiredDefaultResolution
28129
+ } = input;
27572
28130
  const present = new Set(presentPackNames);
27573
- const requiredDefault = [.../* @__PURE__ */ new Set([...policy.requiredGlobalPacks, ...policy.defaultProjectPacks])];
27574
- const missingPackNames = requiredDefault.filter((n) => !present.has(n));
28131
+ let missingPackNames;
28132
+ let unresolvedPacks;
28133
+ if (requiredDefaultResolution) {
28134
+ unresolvedPacks = requiredDefaultResolution.unresolved;
28135
+ const canonical = [...new Set(requiredDefaultResolution.resolved.map((r) => r.name))];
28136
+ missingPackNames = canonical.filter((n) => !present.has(n));
28137
+ } else {
28138
+ missingPackNames = requiredDefaultNames(policy).filter((n) => !present.has(n));
28139
+ unresolvedPacks = [];
28140
+ }
27575
28141
  const blocked = new Set(policy.blockedPackNames ?? []);
27576
28142
  const blockedPackNames = presentPackNames.filter((n) => blocked.has(n));
27577
28143
  const requiredProfileIds = policy.requiredProfileIds ?? [];
@@ -27585,16 +28151,20 @@ function buildEvaluation(input) {
27585
28151
  messages.push("Profile selection is required by policy but none was provided.");
27586
28152
  }
27587
28153
  for (const n of missingPackNames) messages.push(`Required pack "${n}" is not installed.`);
28154
+ for (const n of unresolvedPacks) {
28155
+ messages.push(`Required pack "${n}" is not available on the instance (no matching server-global pack).`);
28156
+ }
27588
28157
  for (const n of blockedPackNames) messages.push(`Pack "${n}" is blocked by policy.`);
27589
28158
  for (const id of missingProfileIds) messages.push(`Required profile "${id}" is not selected.`);
27590
28159
  for (const id of disallowedProfileIds) messages.push(`Profile "${id}" is not permitted by policy.`);
27591
- const violation = selectionRequiredUnmet || blockedPackNames.length > 0 || missingProfileIds.length > 0 || disallowedProfileIds.length > 0 || countMissingPacksAsViolation && missingPackNames.length > 0;
28160
+ const violation = selectionRequiredUnmet || blockedPackNames.length > 0 || missingProfileIds.length > 0 || disallowedProfileIds.length > 0 || countMissingPacksAsViolation && (missingPackNames.length > 0 || unresolvedPacks.length > 0);
27592
28161
  return {
27593
28162
  compliant: !violation,
27594
28163
  mode: policy.enforcementMode,
27595
28164
  missingPackNames,
27596
28165
  blockedPackNames,
27597
28166
  missingProfileIds,
28167
+ unresolvedPacks,
27598
28168
  messages
27599
28169
  };
27600
28170
  }
@@ -27616,11 +28186,15 @@ function performInit(cfg, request, principal) {
27616
28186
  request.ownerUnitId,
27617
28187
  principal ? principalSubject3(principal) : void 0
27618
28188
  );
27619
- installPacks(cfg, record2.id, [
27620
- ...policy.requiredGlobalPacks,
27621
- ...policy.defaultProjectPacks,
27622
- ...requestPackNames(request)
27623
- ]);
28189
+ installResolvedPacks(
28190
+ cfg,
28191
+ record2.id,
28192
+ executeApprovedResolveGlobalPacks([
28193
+ ...policy.requiredGlobalPacks,
28194
+ ...policy.defaultProjectPacks,
28195
+ ...requestPackNames(request)
28196
+ ])
28197
+ );
27624
28198
  const selectedBy = principal ? principalSubject3(principal) : void 0;
27625
28199
  recordProjectProfileSelection(record2.rootPath, resolvedSelection(request, policy, selectedBy));
27626
28200
  const actor = principal ? principalSubject3(principal) : SYSTEM_SUBJECT;
@@ -27678,7 +28252,8 @@ function evaluateProjectPolicy(cfg, credential, projectId) {
27678
28252
  presentPackNames: installedPackNames(cfg, projectId),
27679
28253
  selectedProfileIds: selection?.profileIds ?? [],
27680
28254
  hasSelection: !!selection,
27681
- countMissingPacksAsViolation: true
28255
+ countMissingPacksAsViolation: true,
28256
+ requiredDefaultResolution: executeApprovedResolveGlobalPacks(requiredDefaultNames(policy))
27682
28257
  });
27683
28258
  }
27684
28259
  function reconcileProjectPolicy(cfg, credential, projectId) {
@@ -27692,11 +28267,12 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
27692
28267
  if (!root) throw new Error(`Unknown project "${projectId}".`);
27693
28268
  const policy = effectivePolicy(cfg.dataDir);
27694
28269
  const selection = readProjectProfileSelection(root);
28270
+ const resolution = executeApprovedResolveGlobalPacks(requiredDefaultNames(policy));
27695
28271
  let installed = installedPackNames(cfg, projectId);
27696
- const requiredDefault = [.../* @__PURE__ */ new Set([...policy.requiredGlobalPacks, ...policy.defaultProjectPacks])];
27697
- const missing = requiredDefault.filter((n) => !installed.includes(n));
27698
- if (policy.enforcementMode === "auto_reconcile" && missing.length > 0) {
27699
- installPacks(cfg, projectId, missing);
28272
+ const installedSet = new Set(installed);
28273
+ const toApply = resolution.resolved.filter((p) => !installedSet.has(p.name));
28274
+ if (toApply.length > 0) {
28275
+ installResolvedPacks(cfg, projectId, { resolved: toApply, unresolved: [] });
27700
28276
  installed = installedPackNames(cfg, projectId);
27701
28277
  }
27702
28278
  const result = buildEvaluation({
@@ -27704,7 +28280,8 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
27704
28280
  presentPackNames: installed,
27705
28281
  selectedProfileIds: selection?.profileIds ?? [],
27706
28282
  hasSelection: !!selection,
27707
- countMissingPacksAsViolation: true
28283
+ countMissingPacksAsViolation: true,
28284
+ requiredDefaultResolution: resolution
27708
28285
  });
27709
28286
  tryAppendAudit2(
27710
28287
  cfg,
@@ -27719,6 +28296,32 @@ function reconcileProjectPolicy(cfg, credential, projectId) {
27719
28296
  );
27720
28297
  return result;
27721
28298
  }
28299
+ function getProjectConfig(cfg, credential, projectId) {
28300
+ const principal = requirePrincipal4(cfg, credential);
28301
+ if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY, "project", projectId).value !== "yes") {
28302
+ throw new ForbiddenError(
28303
+ "reading a project's configuration requires project:read over the project"
28304
+ );
28305
+ }
28306
+ const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
28307
+ if (!root) throw new Error(`Unknown project "${projectId}".`);
28308
+ const projectType = readProjectType(root);
28309
+ const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
28310
+ return { projectType, locked };
28311
+ }
28312
+ function setProjectType(cfg, credential, projectId, projectType) {
28313
+ const principal = requirePrincipal4(cfg, credential);
28314
+ if (authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY, "project", projectId).value !== "yes") {
28315
+ throw new ForbiddenError(
28316
+ "changing a project's configuration requires project:write over the project"
28317
+ );
28318
+ }
28319
+ const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
28320
+ if (!root) throw new Error(`Unknown project "${projectId}".`);
28321
+ writeProjectType(root, projectType);
28322
+ const locked = runWithProjectRoot(root, () => hostCore.readLockRecord() !== null);
28323
+ return { projectType, locked };
28324
+ }
27722
28325
  function getPackPolicy(cfg, credential) {
27723
28326
  requirePrincipal4(cfg, credential);
27724
28327
  return getPackPolicyRecord(cfg.dataDir) ?? PERMISSIVE_DEFAULT_POLICY;
@@ -27728,6 +28331,11 @@ function setPackPolicy(cfg, credential, policy) {
27728
28331
  if (!carriesInstancePermission(cfg, principal, POLICY_MANAGE_CAPABILITY)) {
27729
28332
  throw new ForbiddenError("policy administration requires instance-level project:admin");
27730
28333
  }
28334
+ if (!PACK_ENFORCEMENT_MODES.includes(policy.enforcementMode)) {
28335
+ throw new Error(
28336
+ `invalid enforcementMode "${policy.enforcementMode}" \u2014 must be one of ${PACK_ENFORCEMENT_MODES.join(", ")}`
28337
+ );
28338
+ }
27731
28339
  const stamped = { ...policy, updatedBy: principalSubject3(principal) };
27732
28340
  const stored = setPackPolicyRecord(cfg.dataDir, stamped);
27733
28341
  tryAppendAudit2(
@@ -27777,7 +28385,7 @@ function handlePolicyRequest(cfg, credential, req, res, body, url) {
27777
28385
 
27778
28386
  // src/server/identity.ts
27779
28387
  var PROJECT_ADMIN_CAPABILITY2 = "project:admin";
27780
- var PROJECT_READ_CAPABILITY = "project:read";
28388
+ var PROJECT_READ_CAPABILITY2 = "project:read";
27781
28389
  var PROJECT_WRITE_CAPABILITY2 = "project:write";
27782
28390
  function requireInstanceProjectAdmin(cfg, principal, what) {
27783
28391
  if (authorize(cfg.dataDir, principal, PROJECT_ADMIN_CAPABILITY2, "instance", "").value !== "yes") {
@@ -27923,7 +28531,7 @@ function revokeToken(cfg, credential, tokenId) {
27923
28531
  }
27924
28532
  function mintSelfToken(cfg, credential, projectId, write) {
27925
28533
  const principal = requirePrincipal5(cfg, credential);
27926
- if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY, "project", projectId).value !== "yes") {
28534
+ if (authorize(cfg.dataDir, principal, PROJECT_READ_CAPABILITY2, "project", projectId).value !== "yes") {
27927
28535
  throw new ForbiddenError("caller lacks project:read on the requested project");
27928
28536
  }
27929
28537
  if (write && authorize(cfg.dataDir, principal, PROJECT_WRITE_CAPABILITY2, "project", projectId).value !== "yes") {
@@ -28569,7 +29177,7 @@ function audienceDistance(resolution, targetProjectId) {
28569
29177
  // src/server/landscape.ts
28570
29178
  var yamlLib = __toESM(require("js-yaml"));
28571
29179
  var PROJECT_ADMIN_CAPABILITY3 = "project:admin";
28572
- var PROJECT_READ_CAPABILITY2 = "project:read";
29180
+ var PROJECT_READ_CAPABILITY3 = "project:read";
28573
29181
  var PROJECT_WRITE_CAPABILITY3 = "project:write";
28574
29182
  function requirePrincipal6(cfg, credential) {
28575
29183
  const principal = authenticateCredential(cfg.dataDir, credential);
@@ -28583,7 +29191,7 @@ function readView(cfg, principal) {
28583
29191
  if (isInstanceAdmin(principal)) {
28584
29192
  return { all: true, projectIds: /* @__PURE__ */ new Set(), unitIds: /* @__PURE__ */ new Set(), contextUnitIds: /* @__PURE__ */ new Set() };
28585
29193
  }
28586
- const read = visibleScopes(cfg.dataDir, principal, PROJECT_READ_CAPABILITY2);
29194
+ const read = visibleScopes(cfg.dataDir, principal, PROJECT_READ_CAPABILITY3);
28587
29195
  const admin = visibleScopes(cfg.dataDir, principal, PROJECT_ADMIN_CAPABILITY3);
28588
29196
  const unitIds = /* @__PURE__ */ new Set([...actionableUnitIds(read), ...actionableUnitIds(admin)]);
28589
29197
  const contextUnitIds = new Set(
@@ -28598,13 +29206,13 @@ function readView(cfg, principal) {
28598
29206
  }
28599
29207
  function requireLandscapeReach(cfg, principal, view, what) {
28600
29208
  if (view.all || view.projectIds.size > 0 || view.unitIds.size > 0) return;
28601
- if (permitsCap(cfg, principal, PROJECT_READ_CAPABILITY2, "instance", "") || permitsCap(cfg, principal, PROJECT_ADMIN_CAPABILITY3, "instance", "")) {
29209
+ if (permitsCap(cfg, principal, PROJECT_READ_CAPABILITY3, "instance", "") || permitsCap(cfg, principal, PROJECT_ADMIN_CAPABILITY3, "instance", "")) {
28602
29210
  return;
28603
29211
  }
28604
29212
  throw new ForbiddenError(`${what} requires project:read reach`);
28605
29213
  }
28606
29214
  function requireObserverScope(cfg, principal, observerProjectId) {
28607
- const covered = permitsCap(cfg, principal, PROJECT_READ_CAPABILITY2, "project", observerProjectId) || permitsCap(cfg, principal, PROJECT_WRITE_CAPABILITY3, "project", observerProjectId);
29215
+ const covered = permitsCap(cfg, principal, PROJECT_READ_CAPABILITY3, "project", observerProjectId) || permitsCap(cfg, principal, PROJECT_WRITE_CAPABILITY3, "project", observerProjectId);
28608
29216
  if (!covered) {
28609
29217
  throw new ForbiddenError("caller lacks scope over the current project");
28610
29218
  }
@@ -30641,6 +31249,15 @@ function installGlobalPackArchive2(cfg, credential, archive, name) {
30641
31249
  function installProjectPackArchive2(cfg, credential, project2, archive, name) {
30642
31250
  return installProjectPackArchive(cfg, credential, project2, archive, name);
30643
31251
  }
31252
+ function listAvailableProfiles2(cfg, credential) {
31253
+ return listAvailableProfiles(cfg, credential);
31254
+ }
31255
+ function listAdoptableProjectPacks2(cfg, credential, project2) {
31256
+ return listAdoptableProjectPacks(cfg, credential, project2);
31257
+ }
31258
+ function adoptProjectPack2(cfg, credential, project2, name) {
31259
+ return adoptProjectPack(cfg, credential, project2, name);
31260
+ }
30644
31261
  function getPackPolicy2(cfg, credential) {
30645
31262
  return getPackPolicy(cfg, credential);
30646
31263
  }
@@ -30653,6 +31270,12 @@ function evaluateProjectPolicy2(cfg, credential, projectId) {
30653
31270
  function reconcileProjectPolicy2(cfg, credential, projectId) {
30654
31271
  return reconcileProjectPolicy(cfg, credential, projectId);
30655
31272
  }
31273
+ function getProjectConfig2(cfg, credential, projectId) {
31274
+ return getProjectConfig(cfg, credential, projectId);
31275
+ }
31276
+ function setProjectType2(cfg, credential, projectId, projectType) {
31277
+ return setProjectType(cfg, credential, projectId, projectType);
31278
+ }
30656
31279
  function listProducers2(cfg, credential, project2) {
30657
31280
  return listProducers(cfg, credential, project2);
30658
31281
  }
@@ -31774,7 +32397,7 @@ function getWebProjectCanvasModel(cfg, sessionId, projectId) {
31774
32397
  }
31775
32398
  return runWithProjectRoot(root, () => hostCore.buildCanvasDataModel());
31776
32399
  }
31777
- function getWebProjectOpenApi(cfg, sessionId, projectId) {
32400
+ function getWebProjectOpenApi(cfg, sessionId, projectId, portalId) {
31778
32401
  const principal = authenticateSession(cfg.dataDir, sessionId);
31779
32402
  if (!principal.authenticated) throw new UnauthenticatedError();
31780
32403
  if (!listProjects2(cfg, sessionId).some((r) => r.id === projectId)) {
@@ -31783,7 +32406,19 @@ function getWebProjectOpenApi(cfg, sessionId, projectId) {
31783
32406
  const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
31784
32407
  if (!root) throw new ForbiddenError("project not authorized or unknown");
31785
32408
  const result = runWithProjectRoot(root, () => hostSurfaces.exportBoundSurface("project", "openapi"));
31786
- return swaggerUiPage(result.rendered ?? "{}", projectId);
32409
+ const specs = result.renderedSet ?? (result.rendered ? [{ portalId: "", name: projectId, document: result.rendered }] : []);
32410
+ if (specs.length === 0) return swaggerUiPage("{}", projectId);
32411
+ if (portalId) {
32412
+ const sel = specs.find((s) => s.portalId === portalId) ?? specs[0];
32413
+ return swaggerUiPage(sel.document, sel.name);
32414
+ }
32415
+ if (specs.length === 1) return swaggerUiPage(specs[0].document, specs[0].name);
32416
+ return openApiIndexPage(projectId, specs);
32417
+ }
32418
+ function openApiIndexPage(projectId, specs) {
32419
+ const esc2 = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
32420
+ 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("");
32421
+ 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>`;
31787
32422
  }
31788
32423
  function reshapeLandscapeGraph(model, level) {
31789
32424
  const unitNodeIds = new Set(model.nodes.filter((n) => n.nodeKind === "orgUnit").map((n) => n.id));
@@ -33934,6 +34569,15 @@ function opsInstallGlobalPackArchive(cfg, sessionId, req, body, res) {
33934
34569
  function opsInstallProjectPackArchive(cfg, sessionId, req, url, body, res) {
33935
34570
  sendJson(res, 200, installProjectPackArchive2(cfg, sessionId, q(url, "projectId") ?? "", archiveBody(body), packNameOverride(req)));
33936
34571
  }
34572
+ function opsListAvailableProfiles(cfg, sessionId, res) {
34573
+ sendJson(res, 200, { profiles: listAvailableProfiles2(cfg, sessionId) });
34574
+ }
34575
+ function opsListAdoptableProjectPacks(cfg, sessionId, url, res) {
34576
+ sendJson(res, 200, { packs: listAdoptableProjectPacks2(cfg, sessionId, q(url, "projectId") ?? "") });
34577
+ }
34578
+ function opsAdoptProjectPack(cfg, sessionId, body, res) {
34579
+ sendJson(res, 200, adoptProjectPack2(cfg, sessionId, String(body?.projectId ?? ""), String(body?.name ?? "")));
34580
+ }
33937
34581
  function opsGetPackPolicy(cfg, sessionId, res) {
33938
34582
  sendJson(res, 200, getPackPolicy2(cfg, sessionId));
33939
34583
  }
@@ -33946,6 +34590,12 @@ function opsPolicyEvaluate(cfg, sessionId, url, res) {
33946
34590
  function opsPolicyReconcile(cfg, sessionId, body, res) {
33947
34591
  sendJson(res, 200, reconcileProjectPolicy2(cfg, sessionId, String(body?.projectId ?? "")));
33948
34592
  }
34593
+ function opsGetProjectConfig(cfg, sessionId, url, res) {
34594
+ sendJson(res, 200, getProjectConfig2(cfg, sessionId, q(url, "projectId") ?? ""));
34595
+ }
34596
+ function opsSetProjectConfig(cfg, sessionId, body, res) {
34597
+ sendJson(res, 200, setProjectType2(cfg, sessionId, String(body?.projectId ?? ""), String(body?.projectType ?? "")));
34598
+ }
33949
34599
  function opsListProducers(cfg, sessionId, url, res) {
33950
34600
  sendJson(res, 200, { producers: listProducers2(cfg, sessionId, q(url, "projectId") ?? "") });
33951
34601
  }
@@ -34130,7 +34780,8 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
34130
34780
  }
34131
34781
  if (req.method === "GET" && parts.length === 2 && parts[1] === "openapi") {
34132
34782
  const projectId = url.searchParams.get("projectId") ?? "";
34133
- const html = getWebProjectOpenApi(cfg, sessionId, projectId);
34783
+ const spec = url.searchParams.get("spec") ?? void 0;
34784
+ const html = getWebProjectOpenApi(cfg, sessionId, projectId, spec);
34134
34785
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
34135
34786
  res.end(html);
34136
34787
  return;
@@ -34172,6 +34823,18 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
34172
34823
  if (req.method === "POST" && parts.length === 4 && parts[2] === "packs" && parts[3] === "remove") {
34173
34824
  return opsRemoveProjectPack(cfg, sessionId, body, res);
34174
34825
  }
34826
+ if (req.method === "GET" && parts.length === 4 && parts[2] === "packs" && parts[3] === "adoptable") {
34827
+ return opsListAdoptableProjectPacks(cfg, sessionId, url, res);
34828
+ }
34829
+ if (req.method === "POST" && parts.length === 4 && parts[2] === "packs" && parts[3] === "adopt") {
34830
+ return opsAdoptProjectPack(cfg, sessionId, body, res);
34831
+ }
34832
+ if (req.method === "GET" && parts.length === 3 && parts[2] === "config") {
34833
+ return opsGetProjectConfig(cfg, sessionId, url, res);
34834
+ }
34835
+ if (req.method === "POST" && parts.length === 3 && parts[2] === "config") {
34836
+ return opsSetProjectConfig(cfg, sessionId, body, res);
34837
+ }
34175
34838
  if (req.method === "GET" && parts.length === 3 && parts[2] === "policy") {
34176
34839
  return opsPolicyEvaluate(cfg, sessionId, url, res);
34177
34840
  }
@@ -34285,6 +34948,9 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
34285
34948
  if (req.method === "POST" && parts.length === 4 && parts[2] === "packs" && parts[3] === "remove") {
34286
34949
  return opsRemoveGlobalPack(cfg, sessionId, body, res);
34287
34950
  }
34951
+ if (req.method === "GET" && parts.length === 3 && parts[2] === "profiles") {
34952
+ return opsListAvailableProfiles(cfg, sessionId, res);
34953
+ }
34288
34954
  if (req.method === "GET" && parts.length === 3 && parts[2] === "policy") {
34289
34955
  return opsGetPackPolicy(cfg, sessionId, res);
34290
34956
  }
@@ -35086,6 +35752,7 @@ var WEB_MUTATION_PATHS = /* @__PURE__ */ new Set([
35086
35752
  "/web/projects/packs",
35087
35753
  "/web/projects/packs/upload",
35088
35754
  "/web/projects/packs/remove",
35755
+ "/web/projects/packs/adopt",
35089
35756
  "/web/projects/policy/reconcile",
35090
35757
  "/web/projects/producers",
35091
35758
  "/web/projects/producers/remove",