@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/cli/index.js +659 -57
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +512 -46
- package/dist/index.js.map +1 -1
- package/dist/templates/skills/sdd-architect.md +1 -0
- package/package.json +1 -1
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:
|
|
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
|
-
|
|
4270
|
-
|
|
4271
|
-
document.getElementById('
|
|
4272
|
-
document.getElementById('
|
|
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.
|
|
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
|
|
6117
|
-
|
|
6118
|
-
const
|
|
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
|
|
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
|
|
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(
|
|
6407
|
+
...Object.keys(components).length ? { components } : {}
|
|
6152
6408
|
};
|
|
6153
|
-
|
|
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
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
}
|
|
6433
|
-
|
|
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
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
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
|
|
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.
|
|
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,
|