@wairon/cli 5.0.2-dev.1 → 5.0.2-dev.11

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.
@@ -5259,10 +5408,12 @@ var MODEL = __MODEL_JSON__;
5259
5408
  if (kind === 'type' && state.view.kind !== 'types' && state.view.kind !== 'databases') {
5260
5409
  state.view = { kind: 'types', id: typesScopeFromView() };
5261
5410
  rebuild(true);
5411
+ notifyViewChange();
5262
5412
  } else if (kind === 'component' && (state.view.kind === 'types' || state.view.kind === 'databases')) {
5263
5413
  var c = compById[id];
5264
5414
  state.view = { kind: 'subsystem', id: c ? c.subsystem : null };
5265
5415
  rebuild(true);
5416
+ notifyViewChange();
5266
5417
  }
5267
5418
  state.selectedKind = kind;
5268
5419
  state.selected = id;
@@ -5321,6 +5472,14 @@ var MODEL = __MODEL_JSON__;
5321
5472
  if (!exposes || !openApiAllowed()) return '';
5322
5473
  return '<div class="openbtn"><button class="tbtn" data-openapi-tag="' + esc(tag || '') + '">\\u25A4 View OpenAPI \\u2197</button></div>';
5323
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
+ }
5324
5483
 
5325
5484
  function renderPanel() {
5326
5485
  var head = '', body = '';
@@ -5343,7 +5502,8 @@ var MODEL = __MODEL_JSON__;
5343
5502
  + (scopeFocus ? staticChip('current view') : '')
5344
5503
  + chip(c.subsystem, 'subsystem', c.subsystem)
5345
5504
  + (scopeFocus ? '' : openViewButton('component', c.id, c.owns.length > 0))
5346
- + openApiButton(componentExposesApi(c), c.apiTag);
5505
+ + openApiButton(componentExposesApi(c), c.id)
5506
+ + openSpecButton('component', c.id);
5347
5507
 
5348
5508
  var linkedTypes = MODEL.types.filter(function (t) { return t.componentClass === c.id; });
5349
5509
  if (linkedTypes.length) {
@@ -5353,6 +5513,14 @@ var MODEL = __MODEL_JSON__;
5353
5513
  }
5354
5514
  body += '<p class="desc">' + esc(c.description) + '</p>';
5355
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
+
5356
5524
  var depInner = (c.dependsOn.length ? c.dependsOn.map(function (d) { return chip(d, 'component', d); }).join('') : '<span class="desc">none</span>')
5357
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>' : '');
5358
5526
  body += section('Dependencies', c.dependsOn.length + c.owns.length, depInner, true);
@@ -5427,7 +5595,8 @@ var MODEL = __MODEL_JSON__;
5427
5595
  MODEL.types.forEach(function (t2) { if (t2.id === focusId) ty = t2; });
5428
5596
  if (ty) {
5429
5597
  head = '<h2>' + esc(ty.name) + '</h2>' + staticChip('\\u00AB' + ty.kind + '\\u00BB')
5430
- + (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);
5431
5600
 
5432
5601
  if (ty.componentClass) {
5433
5602
  head += '<div style="margin-top:6px"><b style="font-size:11px">Class Component:</b> ' + chip(ty.componentClass, 'component', ty.componentClass) + '</div>';
@@ -5503,7 +5672,8 @@ var MODEL = __MODEL_JSON__;
5503
5672
  + (s.status ? staticChip(s.status) : '')
5504
5673
  + (scopeFocus ? staticChip('current view') : '')
5505
5674
  + (scopeFocus ? '' : openViewButton('subsystem', s.id, subKids > 0))
5506
- + openApiButton(subsystemExposesApi(s.id), '');
5675
+ + openApiButton(subsystemExposesApi(s.id), '')
5676
+ + openSpecButton('subsystem', s.id);
5507
5677
  body += '<p class="desc">' + esc(s.description) + '</p>';
5508
5678
  if (s.trustedLinks.length) {
5509
5679
  body += section('Trusted links (fast lanes)', s.trustedLinks.length, s.trustedLinks.map(function (t2) {
@@ -5561,6 +5731,16 @@ var MODEL = __MODEL_JSON__;
5561
5731
  });
5562
5732
  })(oapis[oi]);
5563
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
+ }
5564
5744
  var flows = panel.querySelectorAll('[data-flow-comp]');
5565
5745
  for (var j = 0; j < flows.length; j++) {
5566
5746
  (function (b) {
@@ -5573,6 +5753,23 @@ var MODEL = __MODEL_JSON__;
5573
5753
  }
5574
5754
 
5575
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
+ })();
5576
5773
  })();
5577
5774
  </script>
5578
5775
  </body>
@@ -6011,6 +6208,16 @@ var init_type_references = __esm({
6011
6208
  }
6012
6209
  });
6013
6210
 
6211
+ // src/utils/filenames.ts
6212
+ function safeFilenamePart(value) {
6213
+ return value.replace(/[^a-zA-Z0-9._-]/g, "-");
6214
+ }
6215
+ var init_filenames = __esm({
6216
+ "src/utils/filenames.ts"() {
6217
+ "use strict";
6218
+ }
6219
+ });
6220
+
6014
6221
  // src/core/statehash.ts
6015
6222
  function computeStateId() {
6016
6223
  const tree = {
@@ -6113,11 +6320,63 @@ function operationFor(method, closureIds) {
6113
6320
  }
6114
6321
  return op;
6115
6322
  }
6116
- function toOpenApi(snapshot) {
6117
- const closureIds = new Set(snapshot.types.map((t) => t.id));
6118
- const httpEntries = snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
6323
+ function securitySchemeObject(auth) {
6324
+ if (auth.scheme === "none") return null;
6325
+ const desc = auth.description ? { description: auth.description } : {};
6326
+ switch (auth.scheme) {
6327
+ case "apiKey":
6328
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "X-API-Key", ...desc };
6329
+ case "bearer":
6330
+ return { type: "http", scheme: "bearer", ...auth.bearerFormat ? { bearerFormat: auth.bearerFormat } : {}, ...desc };
6331
+ case "basic":
6332
+ return { type: "http", scheme: "basic", ...desc };
6333
+ case "oauth2": {
6334
+ const flow = { scopes: Object.fromEntries((auth.scopes ?? []).map((s) => [s.name, s.description])) };
6335
+ if (auth.authorizationUrl) flow.authorizationUrl = auth.authorizationUrl;
6336
+ if (auth.tokenUrl) flow.tokenUrl = auth.tokenUrl;
6337
+ if (auth.refreshUrl) flow.refreshUrl = auth.refreshUrl;
6338
+ return { type: "oauth2", flows: { [auth.flow ?? "authorizationCode"]: flow }, ...desc };
6339
+ }
6340
+ case "openIdConnect":
6341
+ return { type: "openIdConnect", openIdConnectUrl: auth.openIdConnectUrl ?? "", ...desc };
6342
+ case "custom":
6343
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "Authorization", description: auth.description ?? auth.example ?? "Custom authentication scheme." };
6344
+ default:
6345
+ return null;
6346
+ }
6347
+ }
6348
+ function schemeBaseName(scheme) {
6349
+ return { apiKey: "ApiKeyAuth", bearer: "BearerAuth", basic: "BasicAuth", oauth2: "OAuth2", openIdConnect: "OpenIdConnect", custom: "CustomAuth" }[scheme] ?? "Auth";
6350
+ }
6351
+ function buildSecurity(entries) {
6352
+ const schemes = {};
6353
+ const nameByContent = /* @__PURE__ */ new Map();
6354
+ const securityByEntry = /* @__PURE__ */ new Map();
6355
+ for (const entry of entries) {
6356
+ if (!entry.auth) continue;
6357
+ const obj = securitySchemeObject(entry.auth);
6358
+ if (!obj) continue;
6359
+ const content = JSON.stringify(obj);
6360
+ let name = nameByContent.get(content);
6361
+ if (!name) {
6362
+ name = schemeBaseName(entry.auth.scheme);
6363
+ for (let n = 2; schemes[name]; n++) name = schemeBaseName(entry.auth.scheme) + n;
6364
+ schemes[name] = obj;
6365
+ nameByContent.set(content, name);
6366
+ }
6367
+ const scopeNames = entry.auth.scheme === "oauth2" ? (entry.auth.scopes ?? []).map((s) => s.name) : [];
6368
+ securityByEntry.set(entry.id, { [name]: scopeNames });
6369
+ }
6370
+ return { schemes, securityByEntry };
6371
+ }
6372
+ function httpEntriesOf(snapshot) {
6373
+ return snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
6374
+ }
6375
+ function renderDoc(snapshot, entries, closureIds, opts = {}) {
6376
+ const { schemes, securityByEntry } = buildSecurity(entries);
6119
6377
  const paths = {};
6120
- for (const entry of httpEntries) {
6378
+ for (const entry of entries) {
6379
+ const security = securityByEntry.get(entry.id);
6121
6380
  for (const method of entry.methods) {
6122
6381
  const endpoint = method.endpoint;
6123
6382
  if (!endpoint || endpoint.transport !== "HTTP") continue;
@@ -6125,7 +6384,8 @@ function toOpenApi(snapshot) {
6125
6384
  paths[p] = paths[p] ?? {};
6126
6385
  paths[p][endpoint.method.toLowerCase()] = {
6127
6386
  tags: [entry.id],
6128
- ...operationFor(method, closureIds)
6387
+ ...operationFor(method, closureIds),
6388
+ ...security ? { security: [security] } : {}
6129
6389
  };
6130
6390
  }
6131
6391
  }
@@ -6138,19 +6398,45 @@ function toOpenApi(snapshot) {
6138
6398
  required: t.fields.filter((f) => !f.optional).map((f) => f.name)
6139
6399
  };
6140
6400
  }
6141
- const doc = {
6401
+ const components = {};
6402
+ if (Object.keys(schemas).length) components.schemas = schemas;
6403
+ if (Object.keys(schemes).length) components.securitySchemes = schemes;
6404
+ const basePaths = [...new Set(entries.map((e) => e.basePath).filter((b) => !!b))];
6405
+ const servers = opts.servers && basePaths.length === 1 ? [{ url: basePaths[0] }] : void 0;
6406
+ return {
6142
6407
  openapi: "3.1.0",
6143
6408
  info: {
6144
- title: snapshot.projectName,
6409
+ title: opts.title ?? snapshot.projectName,
6145
6410
  version: snapshot.version ?? "0.0.0",
6146
6411
  ...snapshot.stateId ? { "x-wairon-state-id": snapshot.stateId } : {},
6147
6412
  "x-wairon-origin": snapshot.origin,
6148
6413
  "x-wairon-generated-at": snapshot.generatedAt
6149
6414
  },
6415
+ ...servers ? { servers } : {},
6150
6416
  paths,
6151
- ...Object.keys(schemas).length ? { components: { schemas } } : {}
6417
+ ...Object.keys(components).length ? { components } : {}
6152
6418
  };
6153
- return JSON.stringify(doc, null, 2);
6419
+ }
6420
+ function toOpenApi(snapshot) {
6421
+ const closureIds = new Set(snapshot.types.map((t) => t.id));
6422
+ return JSON.stringify(renderDoc(snapshot, httpEntriesOf(snapshot), closureIds), null, 2);
6423
+ }
6424
+ function toOpenApiSet(snapshot) {
6425
+ const closureIds = new Set(snapshot.types.map((t) => t.id));
6426
+ const byPortal = /* @__PURE__ */ new Map();
6427
+ const order = [];
6428
+ for (const entry of httpEntriesOf(snapshot)) {
6429
+ if (!byPortal.has(entry.component)) {
6430
+ byPortal.set(entry.component, []);
6431
+ order.push(entry.component);
6432
+ }
6433
+ byPortal.get(entry.component).push(entry);
6434
+ }
6435
+ return order.map((portalId) => {
6436
+ const entries = byPortal.get(portalId);
6437
+ const name = entries[0]?.name ?? portalId;
6438
+ return { portalId, name, document: JSON.stringify(renderDoc(snapshot, entries, closureIds, { title: name, servers: true }), null, 2) };
6439
+ });
6154
6440
  }
6155
6441
  function isOpenApiDocument(body) {
6156
6442
  try {
@@ -6172,6 +6458,39 @@ function typeRefFromSchema(schema) {
6172
6458
  if (typeof t === "string" && t !== "object") return t;
6173
6459
  return "json";
6174
6460
  }
6461
+ function authFromSecurityScheme(scheme) {
6462
+ const desc = typeof scheme.description === "string" ? { description: scheme.description } : {};
6463
+ if (scheme.type === "apiKey") {
6464
+ return {
6465
+ scheme: "apiKey",
6466
+ ...scheme.in === "header" || scheme.in === "query" || scheme.in === "cookie" ? { in: scheme.in } : {},
6467
+ ...typeof scheme.name === "string" ? { name: scheme.name } : {},
6468
+ ...desc
6469
+ };
6470
+ }
6471
+ if (scheme.type === "http") {
6472
+ if (scheme.scheme === "bearer") return { scheme: "bearer", ...typeof scheme.bearerFormat === "string" ? { bearerFormat: scheme.bearerFormat } : {}, ...desc };
6473
+ if (scheme.scheme === "basic") return { scheme: "basic", ...desc };
6474
+ }
6475
+ if (scheme.type === "oauth2") {
6476
+ const flows = scheme.flows ?? {};
6477
+ const flowKey = Object.keys(flows)[0];
6478
+ const f = flows[flowKey] ?? {};
6479
+ return {
6480
+ scheme: "oauth2",
6481
+ ...flowKey === "authorizationCode" || flowKey === "clientCredentials" || flowKey === "implicit" || flowKey === "password" ? { flow: flowKey } : {},
6482
+ ...typeof f.authorizationUrl === "string" ? { authorizationUrl: f.authorizationUrl } : {},
6483
+ ...typeof f.tokenUrl === "string" ? { tokenUrl: f.tokenUrl } : {},
6484
+ ...typeof f.refreshUrl === "string" ? { refreshUrl: f.refreshUrl } : {},
6485
+ scopes: Object.entries(f.scopes ?? {}).map(([name, description]) => ({ name, description })),
6486
+ ...desc
6487
+ };
6488
+ }
6489
+ if (scheme.type === "openIdConnect") {
6490
+ return { scheme: "openIdConnect", openIdConnectUrl: typeof scheme.openIdConnectUrl === "string" ? scheme.openIdConnectUrl : "", ...desc };
6491
+ }
6492
+ return void 0;
6493
+ }
6175
6494
  function fromOpenApi(document, projectName) {
6176
6495
  let parsed;
6177
6496
  try {
@@ -6249,6 +6568,9 @@ function fromOpenApi(document, projectName) {
6249
6568
  }))
6250
6569
  });
6251
6570
  }
6571
+ const securitySchemes = parsed.components?.securitySchemes ?? {};
6572
+ const firstScheme = Object.values(securitySchemes)[0];
6573
+ const importedAuth = firstScheme ? authFromSecurityScheme(firstScheme) : void 0;
6252
6574
  const entry = {
6253
6575
  id: `${projectName}-api`,
6254
6576
  name: typeof info.title === "string" ? info.title : projectName,
@@ -6257,7 +6579,8 @@ function fromOpenApi(document, projectName) {
6257
6579
  component: `${projectName}-api`,
6258
6580
  methods,
6259
6581
  details: typeof info.description === "string" ? info.description : `Imported OpenAPI surface of ${projectName}.`,
6260
- ...typeof info.version === "string" ? { version: info.version } : {}
6582
+ ...typeof info.version === "string" ? { version: info.version } : {},
6583
+ ...importedAuth ? { auth: importedAuth } : {}
6261
6584
  };
6262
6585
  return SurfaceSnapshotSchema.parse({
6263
6586
  projectName,
@@ -6372,6 +6695,10 @@ function projectOwnSurface(maxAudience) {
6372
6695
  component: comp.id,
6373
6696
  methods,
6374
6697
  ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
6698
+ // Project the backing Portal's auth + basePath so the codec can emit
6699
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
6700
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
6701
+ ...comp.basePath ? { basePath: comp.basePath } : {},
6375
6702
  details: raw.details ?? "",
6376
6703
  ...raw.version ? { version: raw.version } : {},
6377
6704
  ...raw.stability ? { stability: raw.stability } : {}
@@ -6421,20 +6748,52 @@ function removeSnapshot(projectName, rootDir = getProjectRoot()) {
6421
6748
  function loadSurfaceSnapshots() {
6422
6749
  return listSnapshots();
6423
6750
  }
6424
- function exportSurface(maxAudience, format, outPath) {
6751
+ function selectPortalSpec(renderedSet, portalId) {
6752
+ const hit = renderedSet.find((spec) => spec.portalId === portalId);
6753
+ if (!hit) {
6754
+ const known = renderedSet.map((s) => s.portalId).join(", ");
6755
+ throw new Error(`Unknown portal "${portalId}" \u2014 this surface renders: ${known || "(no portals)"}.`);
6756
+ }
6757
+ return [hit];
6758
+ }
6759
+ function perPortalPath(resolvedOut, portalId) {
6760
+ const ext = path5.extname(resolvedOut);
6761
+ const stem = ext ? resolvedOut.slice(0, -ext.length) : resolvedOut;
6762
+ return `${stem}.${safeFilenamePart(portalId)}${ext}`;
6763
+ }
6764
+ function writeSurfaceFile(outPath, snapshot, renderedSet) {
6765
+ const resolved = path5.resolve(outPath);
6766
+ fs4.mkdirSync(path5.dirname(resolved), { recursive: true });
6767
+ if (!renderedSet || renderedSet.length === 0) {
6768
+ writeYamlFile(resolved, snapshot);
6769
+ return [resolved];
6770
+ }
6771
+ if (renderedSet.length === 1) {
6772
+ fs4.writeFileSync(resolved, renderedSet[0].document);
6773
+ return [resolved];
6774
+ }
6775
+ return renderedSet.map((spec) => {
6776
+ const target = perPortalPath(resolved, spec.portalId);
6777
+ fs4.writeFileSync(target, spec.document);
6778
+ return target;
6779
+ });
6780
+ }
6781
+ function exportResult(snapshot, renderedSet, writtenPaths) {
6782
+ const rendered = renderedSet?.length === 1 ? renderedSet[0].document : void 0;
6783
+ return {
6784
+ snapshot,
6785
+ ...rendered !== void 0 ? { rendered } : {},
6786
+ ...renderedSet ? { renderedSet } : {},
6787
+ ...writtenPaths.length === 1 ? { writtenTo: writtenPaths[0] } : {},
6788
+ ...writtenPaths.length ? { writtenPaths } : {}
6789
+ };
6790
+ }
6791
+ function exportSurface(maxAudience, format, outPath, portalId) {
6425
6792
  const snapshot = projectOwnSurface(maxAudience);
6426
- const rendered = format === "openapi" ? toOpenApi(snapshot) : void 0;
6427
- let writtenTo;
6428
- if (outPath) {
6429
- fs4.mkdirSync(path5.dirname(path5.resolve(outPath)), { recursive: true });
6430
- if (rendered !== void 0) {
6431
- fs4.writeFileSync(path5.resolve(outPath), rendered);
6432
- } else {
6433
- writeYamlFile(path5.resolve(outPath), snapshot);
6434
- }
6435
- writtenTo = path5.resolve(outPath);
6436
- }
6437
- return { snapshot, ...rendered !== void 0 ? { rendered } : {}, ...writtenTo ? { writtenTo } : {} };
6793
+ let renderedSet = format === "openapi" ? toOpenApiSet(snapshot) : void 0;
6794
+ if (renderedSet && portalId) renderedSet = selectPortalSpec(renderedSet, portalId);
6795
+ const writtenPaths = outPath ? writeSurfaceFile(outPath, snapshot, renderedSet) : [];
6796
+ return exportResult(snapshot, renderedSet, writtenPaths);
6438
6797
  }
6439
6798
  function importSurface(sourcePath, origin) {
6440
6799
  const resolved = path5.resolve(sourcePath);
@@ -6500,6 +6859,7 @@ var init_surfaces = __esm({
6500
6859
  path5 = __toESM(require("path"));
6501
6860
  init_fs();
6502
6861
  init_yaml();
6862
+ init_filenames();
6503
6863
  init_models();
6504
6864
  init_specs2();
6505
6865
  init_statehash();
@@ -7694,13 +8054,16 @@ var init_conformance = __esm({
7694
8054
  if (isInChainedSubproject(component.subsystem, ctx)) continue;
7695
8055
  const draft = ctx.isImplementationDraft(impl);
7696
8056
  if (!impl.sourcePath) {
7697
- ctx.addIssue(
7698
- "warning",
7699
- "MISSING_SOURCE_PATH",
7700
- `Implementation "${impl.id}" declares no sourcePath \u2014 its contract "${impl.contract}" cannot be structurally checked against code.`,
7701
- impl.id,
7702
- draft
7703
- );
8057
+ const hasExternalSource = (component.externalLinks ?? []).some((l) => l.type === "implementation");
8058
+ if (!hasExternalSource) {
8059
+ ctx.addIssue(
8060
+ "warning",
8061
+ "MISSING_SOURCE_PATH",
8062
+ `Implementation "${impl.id}" declares no sourcePath \u2014 its contract "${impl.contract}" cannot be structurally checked against code.`,
8063
+ impl.id,
8064
+ draft
8065
+ );
8066
+ }
7704
8067
  continue;
7705
8068
  }
7706
8069
  const lookup = lookups.get(normalizeSourcePath(impl.sourcePath));
@@ -8089,13 +8452,14 @@ var init_portals = __esm({
8089
8452
  };
8090
8453
  portalsRule = {
8091
8454
  name: "portal-endpoints",
8092
- 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.",
8455
+ 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).",
8093
8456
  codes: [
8094
8457
  { code: "MISSING_PORTAL_TYPE", defaultSeverity: "error", summary: "Portal without a portalType" },
8095
8458
  { code: "MISSING_ENDPOINT", defaultSeverity: "error", summary: "Portal method without a wire endpoint binding" },
8096
8459
  { code: "ENDPOINT_TRANSPORT_MISMATCH", defaultSeverity: "error", summary: "Endpoint transport does not match the Portal portalType" },
8097
8460
  { code: "UNEXPECTED_PORTAL_FIELD", defaultSeverity: "error", summary: "Non-Portal component with portalType/basePath" },
8098
- { code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" }
8461
+ { code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" },
8462
+ { code: "AUTH_ON_NON_PORTAL", defaultSeverity: "warning", summary: "Non-Portal component declaring auth (auth is inbound transport auth, only meaningful on a Portal)" }
8099
8463
  ],
8100
8464
  check(ctx) {
8101
8465
  for (const comp of ctx.components) {
@@ -8147,6 +8511,15 @@ var init_portals = __esm({
8147
8511
  isDraftCtx
8148
8512
  );
8149
8513
  }
8514
+ if (comp.auth !== void 0) {
8515
+ ctx.addIssue(
8516
+ "warning",
8517
+ "AUTH_ON_NON_PORTAL",
8518
+ `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.`,
8519
+ comp.id,
8520
+ isDraftCtx
8521
+ );
8522
+ }
8150
8523
  const compInterfaces = ctx.interfaces.filter((i) => i.component === comp.id);
8151
8524
  for (const intf of compInterfaces) {
8152
8525
  for (const m of intf.methods) {
@@ -11230,6 +11603,95 @@ var init_lint_allows = __esm({
11230
11603
  }
11231
11604
  });
11232
11605
 
11606
+ // src/core/rules/portal-call-auth.ts
11607
+ var COMPONENT_REF_PREFIX, portalCallAuthRule;
11608
+ var init_portal_call_auth = __esm({
11609
+ "src/core/rules/portal-call-auth.ts"() {
11610
+ "use strict";
11611
+ COMPONENT_REF_PREFIX = "component:";
11612
+ portalCallAuthRule = {
11613
+ name: "portal-call-auth",
11614
+ 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.",
11615
+ codes: [
11616
+ { code: "PORTAL_AUTH_UNMET", defaultSeverity: "warning", summary: "A narrative call into another component's authed Portal does not declare where its credential loads from" },
11617
+ { 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)" },
11618
+ { code: "UNKNOWN_AUTH_SOURCE", defaultSeverity: "warning", summary: "auth.from references a component: source that does not exist" },
11619
+ { code: "AUTH_SOURCE_NOT_PROVIDER", defaultSeverity: "warning", summary: "auth.from references a component that is not an Adapter or Store" },
11620
+ { code: "AUTH_SOURCE_UNWIRED", defaultSeverity: "warning", summary: "The presenter declares a component: credential source it does not depend on or own" }
11621
+ ],
11622
+ check(ctx) {
11623
+ for (const impl of ctx.implementations) {
11624
+ const ownComponent = ctx.interfaceMap.get(impl.contract)?.component;
11625
+ const presenter = ownComponent ? ctx.componentMap.get(ownComponent) : void 0;
11626
+ const draft = ctx.isImplementationDraft(impl);
11627
+ for (const method of impl.methods ?? []) {
11628
+ for (const step of method.narrative ?? []) {
11629
+ if (step.type !== "call") continue;
11630
+ if (!step.targetComponent || step.targetComponent === ownComponent) continue;
11631
+ const target = ctx.componentMap.get(step.targetComponent);
11632
+ if (!target || target.componentType !== "Portal") continue;
11633
+ if (!target.auth || target.auth.scheme === "none") continue;
11634
+ if (presenter && presenter.componentType !== "Adapter") {
11635
+ ctx.addIssue(
11636
+ "warning",
11637
+ "AUTH_PRESENTER_NOT_ADAPTER",
11638
+ `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.`,
11639
+ impl.id,
11640
+ draft
11641
+ );
11642
+ }
11643
+ const from = step.auth?.from;
11644
+ if (!from) {
11645
+ ctx.addIssue(
11646
+ "warning",
11647
+ "PORTAL_AUTH_UNMET",
11648
+ `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.`,
11649
+ impl.id,
11650
+ draft
11651
+ );
11652
+ continue;
11653
+ }
11654
+ if (from.startsWith(COMPONENT_REF_PREFIX)) {
11655
+ const srcId = from.slice(COMPONENT_REF_PREFIX.length);
11656
+ const src = ctx.componentMap.get(srcId);
11657
+ if (!src) {
11658
+ ctx.addIssue(
11659
+ "warning",
11660
+ "UNKNOWN_AUTH_SOURCE",
11661
+ `Narrative step ${step.stepNumber} of "${method.name}" in "${impl.id}": auth.from references component "${srcId}", which does not exist.`,
11662
+ impl.id,
11663
+ draft
11664
+ );
11665
+ } else {
11666
+ if (src.componentType !== "Adapter" && src.componentType !== "Store") {
11667
+ ctx.addIssue(
11668
+ "warning",
11669
+ "AUTH_SOURCE_NOT_PROVIDER",
11670
+ `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).`,
11671
+ impl.id,
11672
+ draft
11673
+ );
11674
+ }
11675
+ const wired = (presenter?.dependsOn ?? []).includes(srcId) || (presenter?.owns ?? []).includes(srcId);
11676
+ if (presenter && !wired) {
11677
+ ctx.addIssue(
11678
+ "warning",
11679
+ "AUTH_SOURCE_UNWIRED",
11680
+ `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.`,
11681
+ impl.id,
11682
+ draft
11683
+ );
11684
+ }
11685
+ }
11686
+ }
11687
+ }
11688
+ }
11689
+ }
11690
+ }
11691
+ };
11692
+ }
11693
+ });
11694
+
11233
11695
  // src/core/rules/index.ts
11234
11696
  function composeRuleSequence(extraRules = []) {
11235
11697
  const base = SDD_RULES.filter((r) => r !== lintAllowsRule);
@@ -11479,6 +11941,7 @@ var init_rules = __esm({
11479
11941
  init_hidden_state();
11480
11942
  init_dependency_conformance();
11481
11943
  init_lint_allows();
11944
+ init_portal_call_auth();
11482
11945
  init_source_analysis();
11483
11946
  init_types();
11484
11947
  init_type_analysis();
@@ -11500,6 +11963,9 @@ var init_rules = __esm({
11500
11963
  narrativeAntipatternsRule,
11501
11964
  narrativeDetailRule,
11502
11965
  portalsRule,
11966
+ // Cross-call auth: a narrative call into an authed Portal must name its
11967
+ // credential source (rides with the portal family).
11968
+ portalCallAuthRule,
11503
11969
  stereotypeDepsRule,
11504
11970
  patternsRule,
11505
11971
  // Facade shape rides with pattern ownership: same §7 doctrine, narrative side.
@@ -11562,6 +12028,7 @@ var init_rules = __esm({
11562
12028
  // L4 expectations: implementations and their code linkage.
11563
12029
  MISSING_IMPLEMENTATION_METHOD: "implementations",
11564
12030
  MISSING_SOURCE_PATH: "implementations",
12031
+ PORTAL_AUTH_UNMET: "implementations",
11565
12032
  MISSING_SOURCE_FILE: "implementations",
11566
12033
  SOURCE_PATH_ESCAPES_ROOT: "implementations",
11567
12034
  UNREALIZED_METHOD: "implementations",
@@ -12020,6 +12487,28 @@ function buildGraphModel(level) {
12020
12487
  ...t.subsystem ? { parentId: t.subsystem } : {}
12021
12488
  });
12022
12489
  }
12490
+ const componentByInterface = /* @__PURE__ */ new Map();
12491
+ for (const c of model.components) {
12492
+ for (const intf of c.interfaces) componentByInterface.set(intf.id, c.id);
12493
+ }
12494
+ for (const impl of loadImplementationSpecs()) {
12495
+ const componentId = componentByInterface.get(impl.contract);
12496
+ if (!componentId) continue;
12497
+ nodes.push({
12498
+ id: impl.id,
12499
+ label: impl.name,
12500
+ kind: "implementation",
12501
+ level: 4,
12502
+ parentId: componentId,
12503
+ ...impl.status ? { status: impl.status } : {}
12504
+ });
12505
+ candidates.push({ from: componentId, to: impl.id, edgeKind: "owns" });
12506
+ }
12507
+ for (const c of model.components) {
12508
+ for (const memberId of c.owns) {
12509
+ candidates.push({ from: c.id, to: memberId, edgeKind: "owns" });
12510
+ }
12511
+ }
12023
12512
  for (const e of model.edges) {
12024
12513
  candidates.push({ from: e.from, to: e.to, edgeKind: "depends_on" });
12025
12514
  }
@@ -14946,6 +15435,8 @@ __export(src_exports, {
14946
15435
  EndpointSchema: () => EndpointSchema,
14947
15436
  EventBindingSchema: () => EventBindingSchema,
14948
15437
  ExtDataSchema: () => ExtDataSchema,
15438
+ ExternalLinkSchema: () => ExternalLinkSchema,
15439
+ ExternalLinkTypeSchema: () => ExternalLinkTypeSchema,
14949
15440
  GITHUB_REPO: () => GITHUB_REPO,
14950
15441
  GeminiExporter: () => GeminiExporter,
14951
15442
  GroupSpecSchema: () => GroupSpecSchema,
@@ -14963,6 +15454,7 @@ __export(src_exports, {
14963
15454
  MethodImplementationSchema: () => MethodImplementationSchema,
14964
15455
  MethodParamSchema: () => MethodParamSchema,
14965
15456
  MethodSignatureSchema: () => MethodSignatureSchema,
15457
+ NamedOpenApiSpecSchema: () => NamedOpenApiSpecSchema,
14966
15458
  NamingRuleConfigSchema: () => NamingRuleConfigSchema,
14967
15459
  NarrativeDetailSchema: () => NarrativeDetailSchema,
14968
15460
  NarrativeStepSchema: () => NarrativeStepSchema,
@@ -14976,6 +15468,8 @@ __export(src_exports, {
14976
15468
  PathsConfigSchema: () => PathsConfigSchema,
14977
15469
  PatternDefSchema: () => PatternDefSchema,
14978
15470
  PatternRefSchema: () => PatternRefSchema,
15471
+ PortalAuthSchema: () => PortalAuthSchema,
15472
+ PortalAuthSchemeSchema: () => PortalAuthSchemeSchema,
14979
15473
  PortalTypeSchema: () => PortalTypeSchema,
14980
15474
  ProfileDefSchema: () => ProfileDefSchema,
14981
15475
  ProjectConfigSchema: () => ProjectConfigSchema,
@@ -15195,6 +15689,7 @@ __export(src_exports, {
15195
15689
  syncContextFiles: () => syncContextFiles,
15196
15690
  toMarkdown: () => toMarkdown,
15197
15691
  toOpenApi: () => toOpenApi,
15692
+ toOpenApiSet: () => toOpenApiSet,
15198
15693
  updateSpec: () => updateSpec,
15199
15694
  validateAsComplete: () => validateAsComplete,
15200
15695
  validateProjectConfig: () => validateProjectConfig,
@@ -15230,7 +15725,7 @@ function defaultTargetConfig(type) {
15230
15725
  enabled: true
15231
15726
  };
15232
15727
  }
15233
- var WAIRON_VERSION = "5.0.2-dev.1";
15728
+ var WAIRON_VERSION = "5.0.2-dev.11";
15234
15729
  var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
15235
15730
  var ARCHITECT_AGENT_ID = "agent-architect";
15236
15731
  var ARCHITECT_TEMPLATE_ID = "architect";
@@ -16527,6 +17022,8 @@ init_yaml();
16527
17022
  EndpointSchema,
16528
17023
  EventBindingSchema,
16529
17024
  ExtDataSchema,
17025
+ ExternalLinkSchema,
17026
+ ExternalLinkTypeSchema,
16530
17027
  GITHUB_REPO,
16531
17028
  GeminiExporter,
16532
17029
  GroupSpecSchema,
@@ -16544,6 +17041,7 @@ init_yaml();
16544
17041
  MethodImplementationSchema,
16545
17042
  MethodParamSchema,
16546
17043
  MethodSignatureSchema,
17044
+ NamedOpenApiSpecSchema,
16547
17045
  NamingRuleConfigSchema,
16548
17046
  NarrativeDetailSchema,
16549
17047
  NarrativeStepSchema,
@@ -16557,6 +17055,8 @@ init_yaml();
16557
17055
  PathsConfigSchema,
16558
17056
  PatternDefSchema,
16559
17057
  PatternRefSchema,
17058
+ PortalAuthSchema,
17059
+ PortalAuthSchemeSchema,
16560
17060
  PortalTypeSchema,
16561
17061
  ProfileDefSchema,
16562
17062
  ProjectConfigSchema,
@@ -16776,6 +17276,7 @@ init_yaml();
16776
17276
  syncContextFiles,
16777
17277
  toMarkdown,
16778
17278
  toOpenApi,
17279
+ toOpenApiSet,
16779
17280
  updateSpec,
16780
17281
  validateAsComplete,
16781
17282
  validateProjectConfig,