@wairon/cli 5.0.2-dev.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.
@@ -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>
@@ -6113,11 +6310,63 @@ function operationFor(method, closureIds) {
6113
6310
  }
6114
6311
  return op;
6115
6312
  }
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"));
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);
6119
6367
  const paths = {};
6120
- for (const entry of httpEntries) {
6368
+ for (const entry of entries) {
6369
+ const security = securityByEntry.get(entry.id);
6121
6370
  for (const method of entry.methods) {
6122
6371
  const endpoint = method.endpoint;
6123
6372
  if (!endpoint || endpoint.transport !== "HTTP") continue;
@@ -6125,7 +6374,8 @@ function toOpenApi(snapshot) {
6125
6374
  paths[p] = paths[p] ?? {};
6126
6375
  paths[p][endpoint.method.toLowerCase()] = {
6127
6376
  tags: [entry.id],
6128
- ...operationFor(method, closureIds)
6377
+ ...operationFor(method, closureIds),
6378
+ ...security ? { security: [security] } : {}
6129
6379
  };
6130
6380
  }
6131
6381
  }
@@ -6138,19 +6388,45 @@ function toOpenApi(snapshot) {
6138
6388
  required: t.fields.filter((f) => !f.optional).map((f) => f.name)
6139
6389
  };
6140
6390
  }
6141
- 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 {
6142
6397
  openapi: "3.1.0",
6143
6398
  info: {
6144
- title: snapshot.projectName,
6399
+ title: opts.title ?? snapshot.projectName,
6145
6400
  version: snapshot.version ?? "0.0.0",
6146
6401
  ...snapshot.stateId ? { "x-wairon-state-id": snapshot.stateId } : {},
6147
6402
  "x-wairon-origin": snapshot.origin,
6148
6403
  "x-wairon-generated-at": snapshot.generatedAt
6149
6404
  },
6405
+ ...servers ? { servers } : {},
6150
6406
  paths,
6151
- ...Object.keys(schemas).length ? { components: { schemas } } : {}
6407
+ ...Object.keys(components).length ? { components } : {}
6152
6408
  };
6153
- 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
+ });
6154
6430
  }
6155
6431
  function isOpenApiDocument(body) {
6156
6432
  try {
@@ -6172,6 +6448,39 @@ function typeRefFromSchema(schema) {
6172
6448
  if (typeof t === "string" && t !== "object") return t;
6173
6449
  return "json";
6174
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
+ }
6175
6484
  function fromOpenApi(document, projectName) {
6176
6485
  let parsed;
6177
6486
  try {
@@ -6249,6 +6558,9 @@ function fromOpenApi(document, projectName) {
6249
6558
  }))
6250
6559
  });
6251
6560
  }
6561
+ const securitySchemes = parsed.components?.securitySchemes ?? {};
6562
+ const firstScheme = Object.values(securitySchemes)[0];
6563
+ const importedAuth = firstScheme ? authFromSecurityScheme(firstScheme) : void 0;
6252
6564
  const entry = {
6253
6565
  id: `${projectName}-api`,
6254
6566
  name: typeof info.title === "string" ? info.title : projectName,
@@ -6257,7 +6569,8 @@ function fromOpenApi(document, projectName) {
6257
6569
  component: `${projectName}-api`,
6258
6570
  methods,
6259
6571
  details: typeof info.description === "string" ? info.description : `Imported OpenAPI surface of ${projectName}.`,
6260
- ...typeof info.version === "string" ? { version: info.version } : {}
6572
+ ...typeof info.version === "string" ? { version: info.version } : {},
6573
+ ...importedAuth ? { auth: importedAuth } : {}
6261
6574
  };
6262
6575
  return SurfaceSnapshotSchema.parse({
6263
6576
  projectName,
@@ -6372,6 +6685,10 @@ function projectOwnSurface(maxAudience) {
6372
6685
  component: comp.id,
6373
6686
  methods,
6374
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 } : {},
6375
6692
  details: raw.details ?? "",
6376
6693
  ...raw.version ? { version: raw.version } : {},
6377
6694
  ...raw.stability ? { stability: raw.stability } : {}
@@ -6421,20 +6738,28 @@ function removeSnapshot(projectName, rootDir = getProjectRoot()) {
6421
6738
  function loadSurfaceSnapshots() {
6422
6739
  return listSnapshots();
6423
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
+ }
6424
6752
  function exportSurface(maxAudience, format, outPath) {
6425
6753
  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 } : {} };
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
+ };
6438
6763
  }
6439
6764
  function importSurface(sourcePath, origin) {
6440
6765
  const resolved = path5.resolve(sourcePath);
@@ -7694,13 +8019,16 @@ var init_conformance = __esm({
7694
8019
  if (isInChainedSubproject(component.subsystem, ctx)) continue;
7695
8020
  const draft = ctx.isImplementationDraft(impl);
7696
8021
  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
- );
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
+ }
7704
8032
  continue;
7705
8033
  }
7706
8034
  const lookup = lookups.get(normalizeSourcePath(impl.sourcePath));
@@ -8089,13 +8417,14 @@ var init_portals = __esm({
8089
8417
  };
8090
8418
  portalsRule = {
8091
8419
  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.",
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).",
8093
8421
  codes: [
8094
8422
  { code: "MISSING_PORTAL_TYPE", defaultSeverity: "error", summary: "Portal without a portalType" },
8095
8423
  { code: "MISSING_ENDPOINT", defaultSeverity: "error", summary: "Portal method without a wire endpoint binding" },
8096
8424
  { code: "ENDPOINT_TRANSPORT_MISMATCH", defaultSeverity: "error", summary: "Endpoint transport does not match the Portal portalType" },
8097
8425
  { 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" }
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)" }
8099
8428
  ],
8100
8429
  check(ctx) {
8101
8430
  for (const comp of ctx.components) {
@@ -8147,6 +8476,15 @@ var init_portals = __esm({
8147
8476
  isDraftCtx
8148
8477
  );
8149
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
+ }
8150
8488
  const compInterfaces = ctx.interfaces.filter((i) => i.component === comp.id);
8151
8489
  for (const intf of compInterfaces) {
8152
8490
  for (const m of intf.methods) {
@@ -11230,6 +11568,95 @@ var init_lint_allows = __esm({
11230
11568
  }
11231
11569
  });
11232
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
+
11233
11660
  // src/core/rules/index.ts
11234
11661
  function composeRuleSequence(extraRules = []) {
11235
11662
  const base = SDD_RULES.filter((r) => r !== lintAllowsRule);
@@ -11479,6 +11906,7 @@ var init_rules = __esm({
11479
11906
  init_hidden_state();
11480
11907
  init_dependency_conformance();
11481
11908
  init_lint_allows();
11909
+ init_portal_call_auth();
11482
11910
  init_source_analysis();
11483
11911
  init_types();
11484
11912
  init_type_analysis();
@@ -11500,6 +11928,9 @@ var init_rules = __esm({
11500
11928
  narrativeAntipatternsRule,
11501
11929
  narrativeDetailRule,
11502
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,
11503
11934
  stereotypeDepsRule,
11504
11935
  patternsRule,
11505
11936
  // Facade shape rides with pattern ownership: same §7 doctrine, narrative side.
@@ -11562,6 +11993,7 @@ var init_rules = __esm({
11562
11993
  // L4 expectations: implementations and their code linkage.
11563
11994
  MISSING_IMPLEMENTATION_METHOD: "implementations",
11564
11995
  MISSING_SOURCE_PATH: "implementations",
11996
+ PORTAL_AUTH_UNMET: "implementations",
11565
11997
  MISSING_SOURCE_FILE: "implementations",
11566
11998
  SOURCE_PATH_ESCAPES_ROOT: "implementations",
11567
11999
  UNREALIZED_METHOD: "implementations",
@@ -12020,6 +12452,28 @@ function buildGraphModel(level) {
12020
12452
  ...t.subsystem ? { parentId: t.subsystem } : {}
12021
12453
  });
12022
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
+ }
12023
12477
  for (const e of model.edges) {
12024
12478
  candidates.push({ from: e.from, to: e.to, edgeKind: "depends_on" });
12025
12479
  }
@@ -14946,6 +15400,8 @@ __export(src_exports, {
14946
15400
  EndpointSchema: () => EndpointSchema,
14947
15401
  EventBindingSchema: () => EventBindingSchema,
14948
15402
  ExtDataSchema: () => ExtDataSchema,
15403
+ ExternalLinkSchema: () => ExternalLinkSchema,
15404
+ ExternalLinkTypeSchema: () => ExternalLinkTypeSchema,
14949
15405
  GITHUB_REPO: () => GITHUB_REPO,
14950
15406
  GeminiExporter: () => GeminiExporter,
14951
15407
  GroupSpecSchema: () => GroupSpecSchema,
@@ -14963,6 +15419,7 @@ __export(src_exports, {
14963
15419
  MethodImplementationSchema: () => MethodImplementationSchema,
14964
15420
  MethodParamSchema: () => MethodParamSchema,
14965
15421
  MethodSignatureSchema: () => MethodSignatureSchema,
15422
+ NamedOpenApiSpecSchema: () => NamedOpenApiSpecSchema,
14966
15423
  NamingRuleConfigSchema: () => NamingRuleConfigSchema,
14967
15424
  NarrativeDetailSchema: () => NarrativeDetailSchema,
14968
15425
  NarrativeStepSchema: () => NarrativeStepSchema,
@@ -14976,6 +15433,8 @@ __export(src_exports, {
14976
15433
  PathsConfigSchema: () => PathsConfigSchema,
14977
15434
  PatternDefSchema: () => PatternDefSchema,
14978
15435
  PatternRefSchema: () => PatternRefSchema,
15436
+ PortalAuthSchema: () => PortalAuthSchema,
15437
+ PortalAuthSchemeSchema: () => PortalAuthSchemeSchema,
14979
15438
  PortalTypeSchema: () => PortalTypeSchema,
14980
15439
  ProfileDefSchema: () => ProfileDefSchema,
14981
15440
  ProjectConfigSchema: () => ProjectConfigSchema,
@@ -15195,6 +15654,7 @@ __export(src_exports, {
15195
15654
  syncContextFiles: () => syncContextFiles,
15196
15655
  toMarkdown: () => toMarkdown,
15197
15656
  toOpenApi: () => toOpenApi,
15657
+ toOpenApiSet: () => toOpenApiSet,
15198
15658
  updateSpec: () => updateSpec,
15199
15659
  validateAsComplete: () => validateAsComplete,
15200
15660
  validateProjectConfig: () => validateProjectConfig,
@@ -15230,7 +15690,7 @@ function defaultTargetConfig(type) {
15230
15690
  enabled: true
15231
15691
  };
15232
15692
  }
15233
- var WAIRON_VERSION = "5.0.2-dev.1";
15693
+ var WAIRON_VERSION = "5.0.2-dev.10";
15234
15694
  var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
15235
15695
  var ARCHITECT_AGENT_ID = "agent-architect";
15236
15696
  var ARCHITECT_TEMPLATE_ID = "architect";
@@ -16527,6 +16987,8 @@ init_yaml();
16527
16987
  EndpointSchema,
16528
16988
  EventBindingSchema,
16529
16989
  ExtDataSchema,
16990
+ ExternalLinkSchema,
16991
+ ExternalLinkTypeSchema,
16530
16992
  GITHUB_REPO,
16531
16993
  GeminiExporter,
16532
16994
  GroupSpecSchema,
@@ -16544,6 +17006,7 @@ init_yaml();
16544
17006
  MethodImplementationSchema,
16545
17007
  MethodParamSchema,
16546
17008
  MethodSignatureSchema,
17009
+ NamedOpenApiSpecSchema,
16547
17010
  NamingRuleConfigSchema,
16548
17011
  NarrativeDetailSchema,
16549
17012
  NarrativeStepSchema,
@@ -16557,6 +17020,8 @@ init_yaml();
16557
17020
  PathsConfigSchema,
16558
17021
  PatternDefSchema,
16559
17022
  PatternRefSchema,
17023
+ PortalAuthSchema,
17024
+ PortalAuthSchemeSchema,
16560
17025
  PortalTypeSchema,
16561
17026
  ProfileDefSchema,
16562
17027
  ProjectConfigSchema,
@@ -16776,6 +17241,7 @@ init_yaml();
16776
17241
  syncContextFiles,
16777
17242
  toMarkdown,
16778
17243
  toOpenApi,
17244
+ toOpenApiSet,
16779
17245
  updateSpec,
16780
17246
  validateAsComplete,
16781
17247
  validateProjectConfig,