@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/index.js CHANGED
@@ -393,7 +393,7 @@ var init_template = __esm({
393
393
  });
394
394
 
395
395
  // src/models/specs.ts
396
- 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;
396
+ 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;
397
397
  var init_specs = __esm({
398
398
  "src/models/specs.ts"() {
399
399
  "use strict";
@@ -597,6 +597,28 @@ var init_specs = __esm({
597
597
  event: import_zod6.z.string().optional(),
598
598
  description: import_zod6.z.string().optional()
599
599
  });
600
+ ExternalLinkTypeSchema = import_zod6.z.enum(["implementation", "informative"]);
601
+ ExternalLinkSchema = import_zod6.z.object({
602
+ url: import_zod6.z.string(),
603
+ /** Defaults to 'informative' so an untyped link never silently satisfies the source requirement. */
604
+ type: ExternalLinkTypeSchema.default("informative"),
605
+ label: import_zod6.z.string().optional()
606
+ });
607
+ PortalAuthSchemeSchema = import_zod6.z.enum(["none", "apiKey", "bearer", "basic", "oauth2", "openIdConnect", "custom"]);
608
+ PortalAuthSchema = import_zod6.z.object({
609
+ scheme: PortalAuthSchemeSchema,
610
+ in: import_zod6.z.enum(["header", "query", "cookie"]).optional(),
611
+ name: import_zod6.z.string().optional(),
612
+ bearerFormat: import_zod6.z.string().optional(),
613
+ authorizationUrl: import_zod6.z.string().optional(),
614
+ tokenUrl: import_zod6.z.string().optional(),
615
+ refreshUrl: import_zod6.z.string().optional(),
616
+ scopes: import_zod6.z.array(import_zod6.z.object({ name: import_zod6.z.string(), description: import_zod6.z.string() })).optional(),
617
+ flow: import_zod6.z.enum(["authorizationCode", "clientCredentials", "implicit", "password"]).optional(),
618
+ openIdConnectUrl: import_zod6.z.string().optional(),
619
+ description: import_zod6.z.string().optional(),
620
+ example: import_zod6.z.string().optional()
621
+ });
600
622
  ComponentSpecSchema = import_zod6.z.object({
601
623
  id: SpecIdSchema,
602
624
  name: import_zod6.z.string(),
@@ -610,6 +632,10 @@ var init_specs = __esm({
610
632
  dependsOn: import_zod6.z.array(import_zod6.z.string()).default([]),
611
633
  portalType: PortalTypeSchema.optional(),
612
634
  basePath: import_zod6.z.string().optional(),
635
+ /** Portal-only: the API's authentication scheme (see PortalAuthSchema) — projected
636
+ * into the generated OpenAPI's securitySchemes/security. Portals with different auth
637
+ * must be separate components (one auth per portal ⇒ one OpenAPI spec per portal). */
638
+ auth: PortalAuthSchema.optional(),
613
639
  /** Portal-only: capability → component.method dispatch table (see DispatchBindingSchema). */
614
640
  dispatch: import_zod6.z.array(DispatchBindingSchema).optional(),
615
641
  /** Store-only: whether held state survives restart (see DurabilitySchema). */
@@ -622,6 +648,11 @@ var init_specs = __esm({
622
648
  patterns: import_zod6.z.array(PatternRefSchema).optional(),
623
649
  /** Optional component variant — a declared, base-anchored specialization of this component's stereotype (resolved against the variant registry; UNKNOWN_VARIANT / VARIANT_BASE_MISMATCH). */
624
650
  variant: import_zod6.z.string().optional(),
651
+ /** Opaque external references (see ExternalLinkSchema) — documented URLs wairon does
652
+ * not fetch or validate. An `implementation` link is the external source-of-record and
653
+ * satisfies the source requirement for a source-less implementation (suppresses
654
+ * MISSING_SOURCE_PATH); `informative` links are context only. */
655
+ externalLinks: import_zod6.z.array(ExternalLinkSchema).optional(),
625
656
  /** Per-spec lint suppressions (see LintConfigSchema). */
626
657
  lint: LintConfigSchema.optional(),
627
658
  /** Opaque pack/tool extension data (see ExtDataSchema) — preserved verbatim. */
@@ -754,6 +785,18 @@ var init_specs = __esm({
754
785
  // Required if type is 'call', references Method name on target interface
755
786
  capability: import_zod6.z.string().optional(),
756
787
  // Required if type is 'dispatch': the capability routed through the target Portal's dispatch table
788
+ /**
789
+ * call/dispatch only: the credential this step presents to an authed callee
790
+ * Portal, and WHERE it is loaded from (`from`). Two forms: an OPAQUE source
791
+ * (`env:API_KEY`, a config key, `vault:path`, a free note) — a design note
792
+ * wairon never resolves; or a MODELED reference `component:<id>` pointing at
793
+ * the Adapter/Store that provides the secret — validated to resolve, be an
794
+ * Adapter/Store, and be wired to the presenter (a checked graph edge). The
795
+ * actual secret is never stored here. Absence on a call into a Portal whose
796
+ * `auth ≠ none` warns (PORTAL_AUTH_UNMET), so credential loading is never
797
+ * overlooked.
798
+ */
799
+ auth: import_zod6.z.object({ from: import_zod6.z.string(), note: import_zod6.z.string().optional() }).optional(),
757
800
  assertsGuarantees: import_zod6.z.array(GuaranteeSchema).optional(),
758
801
  /**
759
802
  * Declared entity invariants this step upholds, as "<type-id>.<invariant-id>"
@@ -970,7 +1013,12 @@ var init_specs = __esm({
970
1013
  dispatch: import_zod6.z.array(DispatchBindingSchema).optional(),
971
1014
  details: import_zod6.z.string().default(""),
972
1015
  version: import_zod6.z.string().optional(),
973
- stability: import_zod6.z.string().optional()
1016
+ stability: import_zod6.z.string().optional(),
1017
+ /** Projected copy of the backing Portal's auth (see PortalAuthSchema) — the codec
1018
+ * emits it as OpenAPI securitySchemes/security. */
1019
+ auth: PortalAuthSchema.optional(),
1020
+ /** The backing Portal's basePath — becomes the per-portal OpenAPI `servers` url. */
1021
+ basePath: import_zod6.z.string().optional()
974
1022
  });
975
1023
  SurfaceSnapshotSchema = import_zod6.z.object({
976
1024
  /** Producing project/system name — the snapshot's resolution identity. */
@@ -985,6 +1033,11 @@ var init_specs = __esm({
985
1033
  /** Transitive type closure of every exported signature — self-contained. */
986
1034
  types: import_zod6.z.array(SurfaceTypeDefSchema).default([])
987
1035
  });
1036
+ NamedOpenApiSpecSchema = import_zod6.z.object({
1037
+ portalId: import_zod6.z.string(),
1038
+ name: import_zod6.z.string(),
1039
+ document: import_zod6.z.string()
1040
+ });
988
1041
  GroupSpecSchema = import_zod6.z.object({
989
1042
  kind: import_zod6.z.literal("group"),
990
1043
  id: SpecIdSchema,
@@ -1998,6 +2051,7 @@ function buildCanvasModel(issues = []) {
1998
2051
  ...ownerOf.has(comp.id) ? { owner: ownerOf.get(comp.id) } : {},
1999
2052
  owns: comp.owns.filter((o) => componentIds.has(o)),
2000
2053
  dependsOn: comp.dependsOn,
2054
+ ...comp.externalLinks && comp.externalLinks.length ? { externalLinks: comp.externalLinks } : {},
2001
2055
  interfaces: compInterfaces.map((i) => ({
2002
2056
  id: i.id,
2003
2057
  name: i.name,
@@ -2650,12 +2704,21 @@ var MODEL = __MODEL_JSON__;
2650
2704
  var configuredLineStyle = ['bezier', 'straight', 'taxi'].indexOf(diagramConfig.lineStyle) >= 0 ? diagramConfig.lineStyle : 'bezier';
2651
2705
  var defaultViewKind = diagramConfig.defaultView === 'types' ? 'types' : (diagramConfig.defaultView === 'databases' && showDatabaseTab ? 'databases' : 'system');
2652
2706
 
2707
+ // Stage J: a deep link (opts.initialRoute) seeds the initial view directly so a
2708
+ // refresh / shared URL renders the right scope with NO root-first flash. Parsed
2709
+ // by the same resolver as openRoute (hoisted below). No onViewChange fires for
2710
+ // this initial seed.
2711
+ var initialView = { kind: defaultViewKind, id: null };
2712
+ if (typeof opts !== 'undefined' && opts && typeof opts.initialRoute === 'string' && opts.initialRoute.length) {
2713
+ initialView = resolveRoute(opts.initialRoute);
2714
+ }
2715
+
2653
2716
  var state = {
2654
- view: { kind: defaultViewKind, id: null },
2655
- internals: false,
2656
- externals: true,
2657
- dataCoupling: false,
2658
- showIssues: false,
2717
+ view: initialView,
2718
+ internals: typeof saved.internals === 'boolean' ? saved.internals : false,
2719
+ externals: typeof saved.externals === 'boolean' ? saved.externals : true,
2720
+ dataCoupling: typeof saved.dataCoupling === 'boolean' ? saved.dataCoupling : false,
2721
+ showIssues: typeof saved.showIssues === 'boolean' ? saved.showIssues : false,
2659
2722
  query: '',
2660
2723
  selected: null,
2661
2724
  selectedKind: null,
@@ -2670,6 +2733,9 @@ var MODEL = __MODEL_JSON__;
2670
2733
  // Set by buildTypeElements when the ERD is degraded for performance (huge
2671
2734
  // scopes); consumed by renderTypesNotice to explain the level-of-detail.
2672
2735
  var typesNotice = '';
2736
+ // Stage J: true while openRoute is applying a URL-driven view change, so the
2737
+ // onViewChange callback is suppressed and we do not loop URL -> engine -> URL.
2738
+ var applyingRoute = false;
2673
2739
 
2674
2740
  function viewKey() {
2675
2741
  // 'types2' + detail level: table sizes differ per detail, and the prefix
@@ -2689,6 +2755,10 @@ var MODEL = __MODEL_JSON__;
2689
2755
  lineStyle: state.lineStyle,
2690
2756
  panelOpen: state.panelOpen,
2691
2757
  panelWidth: state.panelWidth,
2758
+ internals: state.internals,
2759
+ externals: state.externals,
2760
+ dataCoupling: state.dataCoupling,
2761
+ showIssues: state.showIssues,
2692
2762
  }));
2693
2763
  } catch (e) { /* non-fatal */ }
2694
2764
  }
@@ -4095,6 +4165,78 @@ var MODEL = __MODEL_JSON__;
4095
4165
  state.typesRenderAll = false; // a fresh scope re-evaluates the LOD budget
4096
4166
  rebuild(true);
4097
4167
  renderPanel();
4168
+ notifyViewChange();
4169
+ }
4170
+
4171
+ // ---- Stage J: URL <-> view routing ----------------------------------------
4172
+ // The canvas navigation lives in the URL path (refresh-safe + shareable). A
4173
+ // route is the string AFTER /canvas/<project>: '' = system root; a '/'-joined
4174
+ // NAMESPACE (e.g. 'a/b') that resolves against the model to a subsystem or a
4175
+ // component; or the 'types'/'databases' view modes with an optional scope.
4176
+ // A segment is the '::' namespace with '/' as separator, each part encoded.
4177
+ function encSeg(s) { return encodeURIComponent(String(s)); }
4178
+ function routeOf(view) {
4179
+ if (!view) return '';
4180
+ var k = view.kind;
4181
+ if (k === 'system') return '';
4182
+ if (k === 'types' || k === 'databases') {
4183
+ return view.id ? k + '/' + view.id.split('::').map(encSeg).join('/') : k;
4184
+ }
4185
+ if (k === 'subsystem') {
4186
+ return view.id ? view.id.split('::').map(encSeg).join('/') : '';
4187
+ }
4188
+ if (k === 'component') {
4189
+ // A component's full namespace path IS its id: a chained subproject carries
4190
+ // the subsystem prefix in the id (a::b::comp), a flat project uses the bare
4191
+ // id (comp). Serializing comp.id split on '::' round-trips exactly via the
4192
+ // compById lookup in resolveRoute. Owner-pattern nesting is deliberately NOT
4193
+ // encoded (ownership is a separate axis; comp.id does not embed the owner).
4194
+ var c = compById[view.id];
4195
+ var full = c ? c.id : view.id;
4196
+ return full.split('::').map(encSeg).join('/');
4197
+ }
4198
+ return '';
4199
+ }
4200
+ function resolveRoute(routeStr) {
4201
+ var parts = String(routeStr || '').split('/').filter(function (s) { return s.length > 0; }).map(decodeURIComponent);
4202
+ if (!parts.length) return { kind: 'system', id: null };
4203
+ // NOTE: a subsystem literally named 'types'/'databases' is SHADOWED by these
4204
+ // view-mode routes (acceptable \u2014 the modes own those first segments).
4205
+ if (parts[0] === 'types') {
4206
+ return { kind: 'types', id: parts.length > 1 ? parts.slice(1).join('::') : null };
4207
+ }
4208
+ if (parts[0] === 'databases') {
4209
+ if (!showDatabaseTab) return { kind: 'system', id: null };
4210
+ return { kind: 'databases', id: parts.length > 1 ? parts.slice(1).join('::') : null };
4211
+ }
4212
+ var joined = parts.join('::');
4213
+ if (subById[joined]) return { kind: 'subsystem', id: joined };
4214
+ if (compById[joined]) return { kind: 'component', id: joined };
4215
+ return { kind: 'system', id: null }; // unknown id -> fall back to the root
4216
+ }
4217
+ // Apply a route (URL -> engine) WITHOUT echoing back through onViewChange.
4218
+ function openRoute(routeStr) {
4219
+ var v = resolveRoute(routeStr);
4220
+ if (state.view.kind === v.kind && state.view.id === v.id) return;
4221
+ applyingRoute = true;
4222
+ try {
4223
+ state.view = { kind: v.kind, id: v.id };
4224
+ state.selected = null;
4225
+ state.selectedKind = null;
4226
+ state.typesRenderAll = false;
4227
+ rebuild(true);
4228
+ renderPanel();
4229
+ } finally {
4230
+ applyingRoute = false;
4231
+ }
4232
+ }
4233
+ // Notify the embedder (engine -> URL) of the current view; suppressed while a
4234
+ // route is being applied so the URL is not driven in a loop.
4235
+ function notifyViewChange() {
4236
+ if (applyingRoute) return;
4237
+ if (typeof opts !== 'undefined' && opts && typeof opts.onViewChange === 'function') {
4238
+ try { opts.onViewChange(routeOf(state.view)); } catch (e) { /* ignore */ }
4239
+ }
4098
4240
  }
4099
4241
  // Explain (and offer to override) a performance-degraded ERD.
4100
4242
  function renderTypesNotice() {
@@ -4245,6 +4387,7 @@ var MODEL = __MODEL_JSON__;
4245
4387
  if (t.ghost) {
4246
4388
  state.view = parentViewOf(t.kind, t.id);
4247
4389
  rebuild(true);
4390
+ notifyViewChange();
4248
4391
  select(t.kind, t.id, true);
4249
4392
  return;
4250
4393
  }
@@ -4266,10 +4409,16 @@ var MODEL = __MODEL_JSON__;
4266
4409
  }
4267
4410
 
4268
4411
  document.getElementById('search').addEventListener('input', function (ev) { state.query = ev.target.value.trim(); rebuild(false); });
4269
- document.getElementById('internalsToggle').addEventListener('change', function (ev) { state.internals = ev.target.checked; rebuild(true); });
4270
- document.getElementById('externalsToggle').addEventListener('change', function (ev) { state.externals = ev.target.checked; rebuild(true); });
4271
- document.getElementById('dataCouplingToggle').addEventListener('change', function (ev) { state.dataCoupling = ev.target.checked; renderLegend(); rebuild(true); });
4272
- document.getElementById('issuesToggle').addEventListener('change', function (ev) { state.showIssues = ev.target.checked; rebuild(false); renderPanel(); });
4412
+ // Sync each View toggle's checkbox from the (possibly persisted) state, then
4413
+ // persist on change so the choices survive a refresh (see persist()/saved).
4414
+ document.getElementById('internalsToggle').checked = state.internals;
4415
+ document.getElementById('internalsToggle').addEventListener('change', function (ev) { state.internals = ev.target.checked; persist(); rebuild(true); });
4416
+ document.getElementById('externalsToggle').checked = state.externals;
4417
+ document.getElementById('externalsToggle').addEventListener('change', function (ev) { state.externals = ev.target.checked; persist(); rebuild(true); });
4418
+ document.getElementById('dataCouplingToggle').checked = state.dataCoupling;
4419
+ document.getElementById('dataCouplingToggle').addEventListener('change', function (ev) { state.dataCoupling = ev.target.checked; persist(); renderLegend(); rebuild(true); });
4420
+ document.getElementById('issuesToggle').checked = state.showIssues;
4421
+ document.getElementById('issuesToggle').addEventListener('change', function (ev) { state.showIssues = ev.target.checked; persist(); rebuild(false); renderPanel(); });
4273
4422
  document.getElementById('dragToggle').addEventListener('change', function (ev) { cy.autolock(!ev.target.checked); });
4274
4423
  // Mode seg: Components \u21C4 Types. Entering Types keeps the current subsystem
4275
4424
  // scope, so a subsystem's own types (plus shared ones) show scoped.
@@ -4485,18 +4634,20 @@ var MODEL = __MODEL_JSON__;
4485
4634
  return markers[id];
4486
4635
  }
4487
4636
  var collapsed = [];
4488
- // True content overflow in px, measured with FRACTIONAL rect precision:
4489
- // scrollWidth/clientWidth are rounded integers and scrollWidth never reads
4490
- // below clientWidth, so a sub-pixel overflow that still paints a scrollbar
4491
- // is invisible to them. Positive = overflowing; negative = headroom.
4637
+ // Signed fit measure in px: positive = overflowing, negative = headroom.
4638
+ // The header is a flex row whose ONLY flex:1 child is the .spacer, so the
4639
+ // spacer's rendered width IS the free space -- it grows to absorb all slack
4640
+ // and collapses to 0 the instant the row is full. That makes a right-edge
4641
+ // measurement useless (the spacer keeps the trailing controls pinned to the
4642
+ // right padding at every width, so their edge always reads as a bare fit),
4643
+ // and scrollWidth clamps at clientWidth so it can't report headroom either.
4644
+ // So take headroom from the spacer's own (fractional) width, and true
4645
+ // overflow from scrollWidth - clientWidth (only ever > 0 once the spacer has
4646
+ // already collapsed to 0). The two terms are mutually exclusive.
4492
4647
  function overflowPx() {
4493
- var box = hdr.getBoundingClientRect();
4494
- var edge = box.left;
4495
- for (var c = hdr.firstElementChild; c; c = c.nextElementSibling) {
4496
- var cr = c.getBoundingClientRect();
4497
- if (cr.width > 0 && cr.right > edge) edge = cr.right;
4498
- }
4499
- return edge - (box.right - 14); // 14 = the header's right padding
4648
+ var spacer = hdr.querySelector('.spacer');
4649
+ var slack = spacer ? spacer.getBoundingClientRect().width : 0;
4650
+ return (hdr.scrollWidth - hdr.clientWidth) - slack;
4500
4651
  }
4501
4652
  function reflow() {
4502
4653
  // Not laid out (hidden tab, non-browser DOM) \u2014 measuring would misfire.
@@ -5257,10 +5408,12 @@ var MODEL = __MODEL_JSON__;
5257
5408
  if (kind === 'type' && state.view.kind !== 'types' && state.view.kind !== 'databases') {
5258
5409
  state.view = { kind: 'types', id: typesScopeFromView() };
5259
5410
  rebuild(true);
5411
+ notifyViewChange();
5260
5412
  } else if (kind === 'component' && (state.view.kind === 'types' || state.view.kind === 'databases')) {
5261
5413
  var c = compById[id];
5262
5414
  state.view = { kind: 'subsystem', id: c ? c.subsystem : null };
5263
5415
  rebuild(true);
5416
+ notifyViewChange();
5264
5417
  }
5265
5418
  state.selectedKind = kind;
5266
5419
  state.selected = id;
@@ -5319,6 +5472,14 @@ var MODEL = __MODEL_JSON__;
5319
5472
  if (!exposes || !openApiAllowed()) return '';
5320
5473
  return '<div class="openbtn"><button class="tbtn" data-openapi-tag="' + esc(tag || '') + '">\\u25A4 View OpenAPI \\u2197</button></div>';
5321
5474
  }
5475
+ // "Open in Specs" \u2014 deep-links the focused spec into the hosted Specs value
5476
+ // editor via a host hook (opts.onOpenSpec). Hidden when no host provides it
5477
+ // (standalone file, shared page): those have no editor to open. kind is the
5478
+ // spec layer, id its qualified spec id.
5479
+ function openSpecButton(kind, id) {
5480
+ if (typeof opts === 'undefined' || !opts || typeof opts.onOpenSpec !== 'function') return '';
5481
+ return '<div class="openbtn"><button class="tbtn" data-openspec-kind="' + kind + '" data-openspec-id="' + esc(id) + '">\\u270E Open in Specs \\u2197</button></div>';
5482
+ }
5322
5483
 
5323
5484
  function renderPanel() {
5324
5485
  var head = '', body = '';
@@ -5341,7 +5502,8 @@ var MODEL = __MODEL_JSON__;
5341
5502
  + (scopeFocus ? staticChip('current view') : '')
5342
5503
  + chip(c.subsystem, 'subsystem', c.subsystem)
5343
5504
  + (scopeFocus ? '' : openViewButton('component', c.id, c.owns.length > 0))
5344
- + openApiButton(componentExposesApi(c), c.apiTag);
5505
+ + openApiButton(componentExposesApi(c), c.id)
5506
+ + openSpecButton('component', c.id);
5345
5507
 
5346
5508
  var linkedTypes = MODEL.types.filter(function (t) { return t.componentClass === c.id; });
5347
5509
  if (linkedTypes.length) {
@@ -5351,6 +5513,14 @@ var MODEL = __MODEL_JSON__;
5351
5513
  }
5352
5514
  body += '<p class="desc">' + esc(c.description) + '</p>';
5353
5515
 
5516
+ if (c.externalLinks && c.externalLinks.length) {
5517
+ body += section('External links', c.externalLinks.length, c.externalLinks.map(function (l) {
5518
+ var label = l.label || l.url;
5519
+ var tag = l.type === 'implementation' ? staticChip('source') : '';
5520
+ 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>';
5521
+ }).join(''), true);
5522
+ }
5523
+
5354
5524
  var depInner = (c.dependsOn.length ? c.dependsOn.map(function (d) { return chip(d, 'component', d); }).join('') : '<span class="desc">none</span>')
5355
5525
  + (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>' : '');
5356
5526
  body += section('Dependencies', c.dependsOn.length + c.owns.length, depInner, true);
@@ -5425,7 +5595,8 @@ var MODEL = __MODEL_JSON__;
5425
5595
  MODEL.types.forEach(function (t2) { if (t2.id === focusId) ty = t2; });
5426
5596
  if (ty) {
5427
5597
  head = '<h2>' + esc(ty.name) + '</h2>' + staticChip('\\u00AB' + ty.kind + '\\u00BB')
5428
- + (ty.subsystem ? chip(ty.subsystem, 'subsystem', ty.subsystem) : staticChip('system-level shared'));
5598
+ + (ty.subsystem ? chip(ty.subsystem, 'subsystem', ty.subsystem) : staticChip('system-level shared'))
5599
+ + openSpecButton('type', ty.id);
5429
5600
 
5430
5601
  if (ty.componentClass) {
5431
5602
  head += '<div style="margin-top:6px"><b style="font-size:11px">Class Component:</b> ' + chip(ty.componentClass, 'component', ty.componentClass) + '</div>';
@@ -5501,7 +5672,8 @@ var MODEL = __MODEL_JSON__;
5501
5672
  + (s.status ? staticChip(s.status) : '')
5502
5673
  + (scopeFocus ? staticChip('current view') : '')
5503
5674
  + (scopeFocus ? '' : openViewButton('subsystem', s.id, subKids > 0))
5504
- + openApiButton(subsystemExposesApi(s.id), '');
5675
+ + openApiButton(subsystemExposesApi(s.id), '')
5676
+ + openSpecButton('subsystem', s.id);
5505
5677
  body += '<p class="desc">' + esc(s.description) + '</p>';
5506
5678
  if (s.trustedLinks.length) {
5507
5679
  body += section('Trusted links (fast lanes)', s.trustedLinks.length, s.trustedLinks.map(function (t2) {
@@ -5559,6 +5731,16 @@ var MODEL = __MODEL_JSON__;
5559
5731
  });
5560
5732
  })(oapis[oi]);
5561
5733
  }
5734
+ var ospecs = panel.querySelectorAll('[data-openspec-kind]');
5735
+ for (var si = 0; si < ospecs.length; si++) {
5736
+ (function (b) {
5737
+ b.addEventListener('click', function () {
5738
+ if (typeof opts !== 'undefined' && opts && typeof opts.onOpenSpec === 'function') {
5739
+ opts.onOpenSpec(b.getAttribute('data-openspec-kind'), b.getAttribute('data-openspec-id') || '');
5740
+ }
5741
+ });
5742
+ })(ospecs[si]);
5743
+ }
5562
5744
  var flows = panel.querySelectorAll('[data-flow-comp]');
5563
5745
  for (var j = 0; j < flows.length; j++) {
5564
5746
  (function (b) {
@@ -5571,6 +5753,23 @@ var MODEL = __MODEL_JSON__;
5571
5753
  }
5572
5754
 
5573
5755
  renderPanel();
5756
+ // Stage G: a deep link may focus a component and/or open a method's narrative
5757
+ // modal once the seeded view + DOM + cy graph exist. The host parses the URL hash
5758
+ // into opts.initialSelect / opts.initialFlow \u2014 both carry the component id, so this
5759
+ // works whether the seeded view is the component itself or its parent subsystem
5760
+ // (a leaf component has no meaningful "inside", so Specs deep-links open the parent
5761
+ // and focus the component here).
5762
+ (function () {
5763
+ if (typeof opts === 'undefined' || !opts) return;
5764
+ var f = opts.initialFlow, s = opts.initialSelect;
5765
+ var focusComp = (f && f.comp) || (s && s.comp);
5766
+ if (focusComp && compById[focusComp]) {
5767
+ try { select('component', focusComp, true); } catch (e) { /* ignore */ }
5768
+ }
5769
+ if (f && f.comp && f.method && compById[f.comp]) {
5770
+ try { openFlow(f.comp, f.method, f.mode === 'steps' ? 'steps' : 'flow'); } catch (e) { /* ignore */ }
5771
+ }
5772
+ })();
5574
5773
  })();
5575
5774
  </script>
5576
5775
  </body>
@@ -6111,11 +6310,63 @@ function operationFor(method, closureIds) {
6111
6310
  }
6112
6311
  return op;
6113
6312
  }
6114
- function toOpenApi(snapshot) {
6115
- const closureIds = new Set(snapshot.types.map((t) => t.id));
6116
- const httpEntries = snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
6313
+ function securitySchemeObject(auth) {
6314
+ if (auth.scheme === "none") return null;
6315
+ const desc = auth.description ? { description: auth.description } : {};
6316
+ switch (auth.scheme) {
6317
+ case "apiKey":
6318
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "X-API-Key", ...desc };
6319
+ case "bearer":
6320
+ return { type: "http", scheme: "bearer", ...auth.bearerFormat ? { bearerFormat: auth.bearerFormat } : {}, ...desc };
6321
+ case "basic":
6322
+ return { type: "http", scheme: "basic", ...desc };
6323
+ case "oauth2": {
6324
+ const flow = { scopes: Object.fromEntries((auth.scopes ?? []).map((s) => [s.name, s.description])) };
6325
+ if (auth.authorizationUrl) flow.authorizationUrl = auth.authorizationUrl;
6326
+ if (auth.tokenUrl) flow.tokenUrl = auth.tokenUrl;
6327
+ if (auth.refreshUrl) flow.refreshUrl = auth.refreshUrl;
6328
+ return { type: "oauth2", flows: { [auth.flow ?? "authorizationCode"]: flow }, ...desc };
6329
+ }
6330
+ case "openIdConnect":
6331
+ return { type: "openIdConnect", openIdConnectUrl: auth.openIdConnectUrl ?? "", ...desc };
6332
+ case "custom":
6333
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "Authorization", description: auth.description ?? auth.example ?? "Custom authentication scheme." };
6334
+ default:
6335
+ return null;
6336
+ }
6337
+ }
6338
+ function schemeBaseName(scheme) {
6339
+ return { apiKey: "ApiKeyAuth", bearer: "BearerAuth", basic: "BasicAuth", oauth2: "OAuth2", openIdConnect: "OpenIdConnect", custom: "CustomAuth" }[scheme] ?? "Auth";
6340
+ }
6341
+ function buildSecurity(entries) {
6342
+ const schemes = {};
6343
+ const nameByContent = /* @__PURE__ */ new Map();
6344
+ const securityByEntry = /* @__PURE__ */ new Map();
6345
+ for (const entry of entries) {
6346
+ if (!entry.auth) continue;
6347
+ const obj = securitySchemeObject(entry.auth);
6348
+ if (!obj) continue;
6349
+ const content = JSON.stringify(obj);
6350
+ let name = nameByContent.get(content);
6351
+ if (!name) {
6352
+ name = schemeBaseName(entry.auth.scheme);
6353
+ for (let n = 2; schemes[name]; n++) name = schemeBaseName(entry.auth.scheme) + n;
6354
+ schemes[name] = obj;
6355
+ nameByContent.set(content, name);
6356
+ }
6357
+ const scopeNames = entry.auth.scheme === "oauth2" ? (entry.auth.scopes ?? []).map((s) => s.name) : [];
6358
+ securityByEntry.set(entry.id, { [name]: scopeNames });
6359
+ }
6360
+ return { schemes, securityByEntry };
6361
+ }
6362
+ function httpEntriesOf(snapshot) {
6363
+ return snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
6364
+ }
6365
+ function renderDoc(snapshot, entries, closureIds, opts = {}) {
6366
+ const { schemes, securityByEntry } = buildSecurity(entries);
6117
6367
  const paths = {};
6118
- for (const entry of httpEntries) {
6368
+ for (const entry of entries) {
6369
+ const security = securityByEntry.get(entry.id);
6119
6370
  for (const method of entry.methods) {
6120
6371
  const endpoint = method.endpoint;
6121
6372
  if (!endpoint || endpoint.transport !== "HTTP") continue;
@@ -6123,7 +6374,8 @@ function toOpenApi(snapshot) {
6123
6374
  paths[p] = paths[p] ?? {};
6124
6375
  paths[p][endpoint.method.toLowerCase()] = {
6125
6376
  tags: [entry.id],
6126
- ...operationFor(method, closureIds)
6377
+ ...operationFor(method, closureIds),
6378
+ ...security ? { security: [security] } : {}
6127
6379
  };
6128
6380
  }
6129
6381
  }
@@ -6136,19 +6388,45 @@ function toOpenApi(snapshot) {
6136
6388
  required: t.fields.filter((f) => !f.optional).map((f) => f.name)
6137
6389
  };
6138
6390
  }
6139
- const doc = {
6391
+ const components = {};
6392
+ if (Object.keys(schemas).length) components.schemas = schemas;
6393
+ if (Object.keys(schemes).length) components.securitySchemes = schemes;
6394
+ const basePaths = [...new Set(entries.map((e) => e.basePath).filter((b) => !!b))];
6395
+ const servers = opts.servers && basePaths.length === 1 ? [{ url: basePaths[0] }] : void 0;
6396
+ return {
6140
6397
  openapi: "3.1.0",
6141
6398
  info: {
6142
- title: snapshot.projectName,
6399
+ title: opts.title ?? snapshot.projectName,
6143
6400
  version: snapshot.version ?? "0.0.0",
6144
6401
  ...snapshot.stateId ? { "x-wairon-state-id": snapshot.stateId } : {},
6145
6402
  "x-wairon-origin": snapshot.origin,
6146
6403
  "x-wairon-generated-at": snapshot.generatedAt
6147
6404
  },
6405
+ ...servers ? { servers } : {},
6148
6406
  paths,
6149
- ...Object.keys(schemas).length ? { components: { schemas } } : {}
6407
+ ...Object.keys(components).length ? { components } : {}
6150
6408
  };
6151
- return JSON.stringify(doc, null, 2);
6409
+ }
6410
+ function toOpenApi(snapshot) {
6411
+ const closureIds = new Set(snapshot.types.map((t) => t.id));
6412
+ return JSON.stringify(renderDoc(snapshot, httpEntriesOf(snapshot), closureIds), null, 2);
6413
+ }
6414
+ function toOpenApiSet(snapshot) {
6415
+ const closureIds = new Set(snapshot.types.map((t) => t.id));
6416
+ const byPortal = /* @__PURE__ */ new Map();
6417
+ const order = [];
6418
+ for (const entry of httpEntriesOf(snapshot)) {
6419
+ if (!byPortal.has(entry.component)) {
6420
+ byPortal.set(entry.component, []);
6421
+ order.push(entry.component);
6422
+ }
6423
+ byPortal.get(entry.component).push(entry);
6424
+ }
6425
+ return order.map((portalId) => {
6426
+ const entries = byPortal.get(portalId);
6427
+ const name = entries[0]?.name ?? portalId;
6428
+ return { portalId, name, document: JSON.stringify(renderDoc(snapshot, entries, closureIds, { title: name, servers: true }), null, 2) };
6429
+ });
6152
6430
  }
6153
6431
  function isOpenApiDocument(body) {
6154
6432
  try {
@@ -6170,6 +6448,39 @@ function typeRefFromSchema(schema) {
6170
6448
  if (typeof t === "string" && t !== "object") return t;
6171
6449
  return "json";
6172
6450
  }
6451
+ function authFromSecurityScheme(scheme) {
6452
+ const desc = typeof scheme.description === "string" ? { description: scheme.description } : {};
6453
+ if (scheme.type === "apiKey") {
6454
+ return {
6455
+ scheme: "apiKey",
6456
+ ...scheme.in === "header" || scheme.in === "query" || scheme.in === "cookie" ? { in: scheme.in } : {},
6457
+ ...typeof scheme.name === "string" ? { name: scheme.name } : {},
6458
+ ...desc
6459
+ };
6460
+ }
6461
+ if (scheme.type === "http") {
6462
+ if (scheme.scheme === "bearer") return { scheme: "bearer", ...typeof scheme.bearerFormat === "string" ? { bearerFormat: scheme.bearerFormat } : {}, ...desc };
6463
+ if (scheme.scheme === "basic") return { scheme: "basic", ...desc };
6464
+ }
6465
+ if (scheme.type === "oauth2") {
6466
+ const flows = scheme.flows ?? {};
6467
+ const flowKey = Object.keys(flows)[0];
6468
+ const f = flows[flowKey] ?? {};
6469
+ return {
6470
+ scheme: "oauth2",
6471
+ ...flowKey === "authorizationCode" || flowKey === "clientCredentials" || flowKey === "implicit" || flowKey === "password" ? { flow: flowKey } : {},
6472
+ ...typeof f.authorizationUrl === "string" ? { authorizationUrl: f.authorizationUrl } : {},
6473
+ ...typeof f.tokenUrl === "string" ? { tokenUrl: f.tokenUrl } : {},
6474
+ ...typeof f.refreshUrl === "string" ? { refreshUrl: f.refreshUrl } : {},
6475
+ scopes: Object.entries(f.scopes ?? {}).map(([name, description]) => ({ name, description })),
6476
+ ...desc
6477
+ };
6478
+ }
6479
+ if (scheme.type === "openIdConnect") {
6480
+ return { scheme: "openIdConnect", openIdConnectUrl: typeof scheme.openIdConnectUrl === "string" ? scheme.openIdConnectUrl : "", ...desc };
6481
+ }
6482
+ return void 0;
6483
+ }
6173
6484
  function fromOpenApi(document, projectName) {
6174
6485
  let parsed;
6175
6486
  try {
@@ -6247,6 +6558,9 @@ function fromOpenApi(document, projectName) {
6247
6558
  }))
6248
6559
  });
6249
6560
  }
6561
+ const securitySchemes = parsed.components?.securitySchemes ?? {};
6562
+ const firstScheme = Object.values(securitySchemes)[0];
6563
+ const importedAuth = firstScheme ? authFromSecurityScheme(firstScheme) : void 0;
6250
6564
  const entry = {
6251
6565
  id: `${projectName}-api`,
6252
6566
  name: typeof info.title === "string" ? info.title : projectName,
@@ -6255,7 +6569,8 @@ function fromOpenApi(document, projectName) {
6255
6569
  component: `${projectName}-api`,
6256
6570
  methods,
6257
6571
  details: typeof info.description === "string" ? info.description : `Imported OpenAPI surface of ${projectName}.`,
6258
- ...typeof info.version === "string" ? { version: info.version } : {}
6572
+ ...typeof info.version === "string" ? { version: info.version } : {},
6573
+ ...importedAuth ? { auth: importedAuth } : {}
6259
6574
  };
6260
6575
  return SurfaceSnapshotSchema.parse({
6261
6576
  projectName,
@@ -6370,6 +6685,10 @@ function projectOwnSurface(maxAudience) {
6370
6685
  component: comp.id,
6371
6686
  methods,
6372
6687
  ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
6688
+ // Project the backing Portal's auth + basePath so the codec can emit
6689
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
6690
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
6691
+ ...comp.basePath ? { basePath: comp.basePath } : {},
6373
6692
  details: raw.details ?? "",
6374
6693
  ...raw.version ? { version: raw.version } : {},
6375
6694
  ...raw.stability ? { stability: raw.stability } : {}
@@ -6419,20 +6738,28 @@ function removeSnapshot(projectName, rootDir = getProjectRoot()) {
6419
6738
  function loadSurfaceSnapshots() {
6420
6739
  return listSnapshots();
6421
6740
  }
6741
+ function renderOpenApiForms(snapshot) {
6742
+ const renderedSet = toOpenApiSet(snapshot);
6743
+ return { renderedSet, ...renderedSet.length === 1 ? { rendered: renderedSet[0].document } : {} };
6744
+ }
6745
+ function writeSurfaceFile(outPath, body, snapshot) {
6746
+ const resolved = path5.resolve(outPath);
6747
+ fs4.mkdirSync(path5.dirname(resolved), { recursive: true });
6748
+ if (body !== void 0) fs4.writeFileSync(resolved, body);
6749
+ else writeYamlFile(resolved, snapshot);
6750
+ return resolved;
6751
+ }
6422
6752
  function exportSurface(maxAudience, format, outPath) {
6423
6753
  const snapshot = projectOwnSurface(maxAudience);
6424
- const rendered = format === "openapi" ? toOpenApi(snapshot) : void 0;
6425
- let writtenTo;
6426
- if (outPath) {
6427
- fs4.mkdirSync(path5.dirname(path5.resolve(outPath)), { recursive: true });
6428
- if (rendered !== void 0) {
6429
- fs4.writeFileSync(path5.resolve(outPath), rendered);
6430
- } else {
6431
- writeYamlFile(path5.resolve(outPath), snapshot);
6432
- }
6433
- writtenTo = path5.resolve(outPath);
6434
- }
6435
- return { snapshot, ...rendered !== void 0 ? { rendered } : {}, ...writtenTo ? { writtenTo } : {} };
6754
+ const openapi = format === "openapi" ? renderOpenApiForms(snapshot) : void 0;
6755
+ const body = openapi?.rendered ?? openapi?.renderedSet[0]?.document;
6756
+ const writtenTo = outPath ? writeSurfaceFile(outPath, body, snapshot) : void 0;
6757
+ return {
6758
+ snapshot,
6759
+ ...openapi?.rendered !== void 0 ? { rendered: openapi.rendered } : {},
6760
+ ...openapi ? { renderedSet: openapi.renderedSet } : {},
6761
+ ...writtenTo ? { writtenTo } : {}
6762
+ };
6436
6763
  }
6437
6764
  function importSurface(sourcePath, origin) {
6438
6765
  const resolved = path5.resolve(sourcePath);
@@ -7692,13 +8019,16 @@ var init_conformance = __esm({
7692
8019
  if (isInChainedSubproject(component.subsystem, ctx)) continue;
7693
8020
  const draft = ctx.isImplementationDraft(impl);
7694
8021
  if (!impl.sourcePath) {
7695
- ctx.addIssue(
7696
- "warning",
7697
- "MISSING_SOURCE_PATH",
7698
- `Implementation "${impl.id}" declares no sourcePath \u2014 its contract "${impl.contract}" cannot be structurally checked against code.`,
7699
- impl.id,
7700
- draft
7701
- );
8022
+ const hasExternalSource = (component.externalLinks ?? []).some((l) => l.type === "implementation");
8023
+ if (!hasExternalSource) {
8024
+ ctx.addIssue(
8025
+ "warning",
8026
+ "MISSING_SOURCE_PATH",
8027
+ `Implementation "${impl.id}" declares no sourcePath \u2014 its contract "${impl.contract}" cannot be structurally checked against code.`,
8028
+ impl.id,
8029
+ draft
8030
+ );
8031
+ }
7702
8032
  continue;
7703
8033
  }
7704
8034
  const lookup = lookups.get(normalizeSourcePath(impl.sourcePath));
@@ -8087,13 +8417,14 @@ var init_portals = __esm({
8087
8417
  };
8088
8418
  portalsRule = {
8089
8419
  name: "portal-endpoints",
8090
- 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.",
8420
+ 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).",
8091
8421
  codes: [
8092
8422
  { code: "MISSING_PORTAL_TYPE", defaultSeverity: "error", summary: "Portal without a portalType" },
8093
8423
  { code: "MISSING_ENDPOINT", defaultSeverity: "error", summary: "Portal method without a wire endpoint binding" },
8094
8424
  { code: "ENDPOINT_TRANSPORT_MISMATCH", defaultSeverity: "error", summary: "Endpoint transport does not match the Portal portalType" },
8095
8425
  { code: "UNEXPECTED_PORTAL_FIELD", defaultSeverity: "error", summary: "Non-Portal component with portalType/basePath" },
8096
- { code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" }
8426
+ { code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" },
8427
+ { code: "AUTH_ON_NON_PORTAL", defaultSeverity: "warning", summary: "Non-Portal component declaring auth (auth is inbound transport auth, only meaningful on a Portal)" }
8097
8428
  ],
8098
8429
  check(ctx) {
8099
8430
  for (const comp of ctx.components) {
@@ -8145,6 +8476,15 @@ var init_portals = __esm({
8145
8476
  isDraftCtx
8146
8477
  );
8147
8478
  }
8479
+ if (comp.auth !== void 0) {
8480
+ ctx.addIssue(
8481
+ "warning",
8482
+ "AUTH_ON_NON_PORTAL",
8483
+ `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.`,
8484
+ comp.id,
8485
+ isDraftCtx
8486
+ );
8487
+ }
8148
8488
  const compInterfaces = ctx.interfaces.filter((i) => i.component === comp.id);
8149
8489
  for (const intf of compInterfaces) {
8150
8490
  for (const m of intf.methods) {
@@ -11228,6 +11568,95 @@ var init_lint_allows = __esm({
11228
11568
  }
11229
11569
  });
11230
11570
 
11571
+ // src/core/rules/portal-call-auth.ts
11572
+ var COMPONENT_REF_PREFIX, portalCallAuthRule;
11573
+ var init_portal_call_auth = __esm({
11574
+ "src/core/rules/portal-call-auth.ts"() {
11575
+ "use strict";
11576
+ COMPONENT_REF_PREFIX = "component:";
11577
+ portalCallAuthRule = {
11578
+ name: "portal-call-auth",
11579
+ 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.",
11580
+ codes: [
11581
+ { code: "PORTAL_AUTH_UNMET", defaultSeverity: "warning", summary: "A narrative call into another component's authed Portal does not declare where its credential loads from" },
11582
+ { 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)" },
11583
+ { code: "UNKNOWN_AUTH_SOURCE", defaultSeverity: "warning", summary: "auth.from references a component: source that does not exist" },
11584
+ { code: "AUTH_SOURCE_NOT_PROVIDER", defaultSeverity: "warning", summary: "auth.from references a component that is not an Adapter or Store" },
11585
+ { code: "AUTH_SOURCE_UNWIRED", defaultSeverity: "warning", summary: "The presenter declares a component: credential source it does not depend on or own" }
11586
+ ],
11587
+ check(ctx) {
11588
+ for (const impl of ctx.implementations) {
11589
+ const ownComponent = ctx.interfaceMap.get(impl.contract)?.component;
11590
+ const presenter = ownComponent ? ctx.componentMap.get(ownComponent) : void 0;
11591
+ const draft = ctx.isImplementationDraft(impl);
11592
+ for (const method of impl.methods ?? []) {
11593
+ for (const step of method.narrative ?? []) {
11594
+ if (step.type !== "call") continue;
11595
+ if (!step.targetComponent || step.targetComponent === ownComponent) continue;
11596
+ const target = ctx.componentMap.get(step.targetComponent);
11597
+ if (!target || target.componentType !== "Portal") continue;
11598
+ if (!target.auth || target.auth.scheme === "none") continue;
11599
+ if (presenter && presenter.componentType !== "Adapter") {
11600
+ ctx.addIssue(
11601
+ "warning",
11602
+ "AUTH_PRESENTER_NOT_ADAPTER",
11603
+ `Narrative step ${step.stepNumber} of "${method.name}" in "${impl.id}": "${presenter.id}" (${presenter.componentType}) authenticates an outbound call to portal "${target.id}". An authenticated cross-service call is external I/O and must be made by an Adapter (the only block that does external I/O) \u2014 route it through a client Adapter.`,
11604
+ impl.id,
11605
+ draft
11606
+ );
11607
+ }
11608
+ const from = step.auth?.from;
11609
+ if (!from) {
11610
+ ctx.addIssue(
11611
+ "warning",
11612
+ "PORTAL_AUTH_UNMET",
11613
+ `Narrative step ${step.stepNumber} of "${method.name}" in "${impl.id}" calls the authenticated portal "${target.id}" (auth scheme: ${target.auth.scheme}) but does not declare where its credential is loaded from. Add the credential source (the step's auth.from \u2014 a secret-store component, env var, config key, or vault ref), or drop the portal's auth if it needs none.`,
11614
+ impl.id,
11615
+ draft
11616
+ );
11617
+ continue;
11618
+ }
11619
+ if (from.startsWith(COMPONENT_REF_PREFIX)) {
11620
+ const srcId = from.slice(COMPONENT_REF_PREFIX.length);
11621
+ const src = ctx.componentMap.get(srcId);
11622
+ if (!src) {
11623
+ ctx.addIssue(
11624
+ "warning",
11625
+ "UNKNOWN_AUTH_SOURCE",
11626
+ `Narrative step ${step.stepNumber} of "${method.name}" in "${impl.id}": auth.from references component "${srcId}", which does not exist.`,
11627
+ impl.id,
11628
+ draft
11629
+ );
11630
+ } else {
11631
+ if (src.componentType !== "Adapter" && src.componentType !== "Store") {
11632
+ ctx.addIssue(
11633
+ "warning",
11634
+ "AUTH_SOURCE_NOT_PROVIDER",
11635
+ `Narrative step ${step.stepNumber} of "${method.name}" in "${impl.id}": auth.from references "${srcId}" (${src.componentType}); a credential source must be an Adapter (loads the secret via external I/O) or a Store (holds it).`,
11636
+ impl.id,
11637
+ draft
11638
+ );
11639
+ }
11640
+ const wired = (presenter?.dependsOn ?? []).includes(srcId) || (presenter?.owns ?? []).includes(srcId);
11641
+ if (presenter && !wired) {
11642
+ ctx.addIssue(
11643
+ "warning",
11644
+ "AUTH_SOURCE_UNWIRED",
11645
+ `Narrative step ${step.stepNumber} of "${method.name}" in "${impl.id}": "${presenter.id}" loads its credential from "${srcId}" but neither depends on nor owns it \u2014 declare the dependsOn edge so the credential wiring is real.`,
11646
+ impl.id,
11647
+ draft
11648
+ );
11649
+ }
11650
+ }
11651
+ }
11652
+ }
11653
+ }
11654
+ }
11655
+ }
11656
+ };
11657
+ }
11658
+ });
11659
+
11231
11660
  // src/core/rules/index.ts
11232
11661
  function composeRuleSequence(extraRules = []) {
11233
11662
  const base = SDD_RULES.filter((r) => r !== lintAllowsRule);
@@ -11477,6 +11906,7 @@ var init_rules = __esm({
11477
11906
  init_hidden_state();
11478
11907
  init_dependency_conformance();
11479
11908
  init_lint_allows();
11909
+ init_portal_call_auth();
11480
11910
  init_source_analysis();
11481
11911
  init_types();
11482
11912
  init_type_analysis();
@@ -11498,6 +11928,9 @@ var init_rules = __esm({
11498
11928
  narrativeAntipatternsRule,
11499
11929
  narrativeDetailRule,
11500
11930
  portalsRule,
11931
+ // Cross-call auth: a narrative call into an authed Portal must name its
11932
+ // credential source (rides with the portal family).
11933
+ portalCallAuthRule,
11501
11934
  stereotypeDepsRule,
11502
11935
  patternsRule,
11503
11936
  // Facade shape rides with pattern ownership: same §7 doctrine, narrative side.
@@ -11560,6 +11993,7 @@ var init_rules = __esm({
11560
11993
  // L4 expectations: implementations and their code linkage.
11561
11994
  MISSING_IMPLEMENTATION_METHOD: "implementations",
11562
11995
  MISSING_SOURCE_PATH: "implementations",
11996
+ PORTAL_AUTH_UNMET: "implementations",
11563
11997
  MISSING_SOURCE_FILE: "implementations",
11564
11998
  SOURCE_PATH_ESCAPES_ROOT: "implementations",
11565
11999
  UNREALIZED_METHOD: "implementations",
@@ -12018,6 +12452,28 @@ function buildGraphModel(level) {
12018
12452
  ...t.subsystem ? { parentId: t.subsystem } : {}
12019
12453
  });
12020
12454
  }
12455
+ const componentByInterface = /* @__PURE__ */ new Map();
12456
+ for (const c of model.components) {
12457
+ for (const intf of c.interfaces) componentByInterface.set(intf.id, c.id);
12458
+ }
12459
+ for (const impl of loadImplementationSpecs()) {
12460
+ const componentId = componentByInterface.get(impl.contract);
12461
+ if (!componentId) continue;
12462
+ nodes.push({
12463
+ id: impl.id,
12464
+ label: impl.name,
12465
+ kind: "implementation",
12466
+ level: 4,
12467
+ parentId: componentId,
12468
+ ...impl.status ? { status: impl.status } : {}
12469
+ });
12470
+ candidates.push({ from: componentId, to: impl.id, edgeKind: "owns" });
12471
+ }
12472
+ for (const c of model.components) {
12473
+ for (const memberId of c.owns) {
12474
+ candidates.push({ from: c.id, to: memberId, edgeKind: "owns" });
12475
+ }
12476
+ }
12021
12477
  for (const e of model.edges) {
12022
12478
  candidates.push({ from: e.from, to: e.to, edgeKind: "depends_on" });
12023
12479
  }
@@ -14944,6 +15400,8 @@ __export(src_exports, {
14944
15400
  EndpointSchema: () => EndpointSchema,
14945
15401
  EventBindingSchema: () => EventBindingSchema,
14946
15402
  ExtDataSchema: () => ExtDataSchema,
15403
+ ExternalLinkSchema: () => ExternalLinkSchema,
15404
+ ExternalLinkTypeSchema: () => ExternalLinkTypeSchema,
14947
15405
  GITHUB_REPO: () => GITHUB_REPO,
14948
15406
  GeminiExporter: () => GeminiExporter,
14949
15407
  GroupSpecSchema: () => GroupSpecSchema,
@@ -14961,6 +15419,7 @@ __export(src_exports, {
14961
15419
  MethodImplementationSchema: () => MethodImplementationSchema,
14962
15420
  MethodParamSchema: () => MethodParamSchema,
14963
15421
  MethodSignatureSchema: () => MethodSignatureSchema,
15422
+ NamedOpenApiSpecSchema: () => NamedOpenApiSpecSchema,
14964
15423
  NamingRuleConfigSchema: () => NamingRuleConfigSchema,
14965
15424
  NarrativeDetailSchema: () => NarrativeDetailSchema,
14966
15425
  NarrativeStepSchema: () => NarrativeStepSchema,
@@ -14974,6 +15433,8 @@ __export(src_exports, {
14974
15433
  PathsConfigSchema: () => PathsConfigSchema,
14975
15434
  PatternDefSchema: () => PatternDefSchema,
14976
15435
  PatternRefSchema: () => PatternRefSchema,
15436
+ PortalAuthSchema: () => PortalAuthSchema,
15437
+ PortalAuthSchemeSchema: () => PortalAuthSchemeSchema,
14977
15438
  PortalTypeSchema: () => PortalTypeSchema,
14978
15439
  ProfileDefSchema: () => ProfileDefSchema,
14979
15440
  ProjectConfigSchema: () => ProjectConfigSchema,
@@ -15193,6 +15654,7 @@ __export(src_exports, {
15193
15654
  syncContextFiles: () => syncContextFiles,
15194
15655
  toMarkdown: () => toMarkdown,
15195
15656
  toOpenApi: () => toOpenApi,
15657
+ toOpenApiSet: () => toOpenApiSet,
15196
15658
  updateSpec: () => updateSpec,
15197
15659
  validateAsComplete: () => validateAsComplete,
15198
15660
  validateProjectConfig: () => validateProjectConfig,
@@ -15228,7 +15690,7 @@ function defaultTargetConfig(type) {
15228
15690
  enabled: true
15229
15691
  };
15230
15692
  }
15231
- var WAIRON_VERSION = "5.0.1";
15693
+ var WAIRON_VERSION = "5.0.2-dev.10";
15232
15694
  var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
15233
15695
  var ARCHITECT_AGENT_ID = "agent-architect";
15234
15696
  var ARCHITECT_TEMPLATE_ID = "architect";
@@ -16525,6 +16987,8 @@ init_yaml();
16525
16987
  EndpointSchema,
16526
16988
  EventBindingSchema,
16527
16989
  ExtDataSchema,
16990
+ ExternalLinkSchema,
16991
+ ExternalLinkTypeSchema,
16528
16992
  GITHUB_REPO,
16529
16993
  GeminiExporter,
16530
16994
  GroupSpecSchema,
@@ -16542,6 +17006,7 @@ init_yaml();
16542
17006
  MethodImplementationSchema,
16543
17007
  MethodParamSchema,
16544
17008
  MethodSignatureSchema,
17009
+ NamedOpenApiSpecSchema,
16545
17010
  NamingRuleConfigSchema,
16546
17011
  NarrativeDetailSchema,
16547
17012
  NarrativeStepSchema,
@@ -16555,6 +17020,8 @@ init_yaml();
16555
17020
  PathsConfigSchema,
16556
17021
  PatternDefSchema,
16557
17022
  PatternRefSchema,
17023
+ PortalAuthSchema,
17024
+ PortalAuthSchemeSchema,
16558
17025
  PortalTypeSchema,
16559
17026
  ProfileDefSchema,
16560
17027
  ProjectConfigSchema,
@@ -16774,6 +17241,7 @@ init_yaml();
16774
17241
  syncContextFiles,
16775
17242
  toMarkdown,
16776
17243
  toOpenApi,
17244
+ toOpenApiSet,
16777
17245
  updateSpec,
16778
17246
  validateAsComplete,
16779
17247
  validateProjectConfig,