@wairon/cli 5.0.1 → 5.0.2-dev.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +769 -102
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +525 -57
- 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.
|
|
@@ -4485,18 +4634,20 @@ var MODEL = __MODEL_JSON__;
|
|
|
4485
4634
|
return markers[id];
|
|
4486
4635
|
}
|
|
4487
4636
|
var collapsed = [];
|
|
4488
|
-
//
|
|
4489
|
-
//
|
|
4490
|
-
//
|
|
4491
|
-
//
|
|
4637
|
+
// Signed fit measure in px: positive = overflowing, negative = headroom.
|
|
4638
|
+
// The header is a flex row whose ONLY flex:1 child is the .spacer, so the
|
|
4639
|
+
// spacer's rendered width IS the free space -- it grows to absorb all slack
|
|
4640
|
+
// and collapses to 0 the instant the row is full. That makes a right-edge
|
|
4641
|
+
// measurement useless (the spacer keeps the trailing controls pinned to the
|
|
4642
|
+
// right padding at every width, so their edge always reads as a bare fit),
|
|
4643
|
+
// and scrollWidth clamps at clientWidth so it can't report headroom either.
|
|
4644
|
+
// So take headroom from the spacer's own (fractional) width, and true
|
|
4645
|
+
// overflow from scrollWidth - clientWidth (only ever > 0 once the spacer has
|
|
4646
|
+
// already collapsed to 0). The two terms are mutually exclusive.
|
|
4492
4647
|
function overflowPx() {
|
|
4493
|
-
var
|
|
4494
|
-
var
|
|
4495
|
-
|
|
4496
|
-
var cr = c.getBoundingClientRect();
|
|
4497
|
-
if (cr.width > 0 && cr.right > edge) edge = cr.right;
|
|
4498
|
-
}
|
|
4499
|
-
return edge - (box.right - 14); // 14 = the header's right padding
|
|
4648
|
+
var spacer = hdr.querySelector('.spacer');
|
|
4649
|
+
var slack = spacer ? spacer.getBoundingClientRect().width : 0;
|
|
4650
|
+
return (hdr.scrollWidth - hdr.clientWidth) - slack;
|
|
4500
4651
|
}
|
|
4501
4652
|
function reflow() {
|
|
4502
4653
|
// Not laid out (hidden tab, non-browser DOM) \u2014 measuring would misfire.
|
|
@@ -5257,10 +5408,12 @@ var MODEL = __MODEL_JSON__;
|
|
|
5257
5408
|
if (kind === 'type' && state.view.kind !== 'types' && state.view.kind !== 'databases') {
|
|
5258
5409
|
state.view = { kind: 'types', id: typesScopeFromView() };
|
|
5259
5410
|
rebuild(true);
|
|
5411
|
+
notifyViewChange();
|
|
5260
5412
|
} else if (kind === 'component' && (state.view.kind === 'types' || state.view.kind === 'databases')) {
|
|
5261
5413
|
var c = compById[id];
|
|
5262
5414
|
state.view = { kind: 'subsystem', id: c ? c.subsystem : null };
|
|
5263
5415
|
rebuild(true);
|
|
5416
|
+
notifyViewChange();
|
|
5264
5417
|
}
|
|
5265
5418
|
state.selectedKind = kind;
|
|
5266
5419
|
state.selected = id;
|
|
@@ -5319,6 +5472,14 @@ var MODEL = __MODEL_JSON__;
|
|
|
5319
5472
|
if (!exposes || !openApiAllowed()) return '';
|
|
5320
5473
|
return '<div class="openbtn"><button class="tbtn" data-openapi-tag="' + esc(tag || '') + '">\\u25A4 View OpenAPI \\u2197</button></div>';
|
|
5321
5474
|
}
|
|
5475
|
+
// "Open in Specs" \u2014 deep-links the focused spec into the hosted Specs value
|
|
5476
|
+
// editor via a host hook (opts.onOpenSpec). Hidden when no host provides it
|
|
5477
|
+
// (standalone file, shared page): those have no editor to open. kind is the
|
|
5478
|
+
// spec layer, id its qualified spec id.
|
|
5479
|
+
function openSpecButton(kind, id) {
|
|
5480
|
+
if (typeof opts === 'undefined' || !opts || typeof opts.onOpenSpec !== 'function') return '';
|
|
5481
|
+
return '<div class="openbtn"><button class="tbtn" data-openspec-kind="' + kind + '" data-openspec-id="' + esc(id) + '">\\u270E Open in Specs \\u2197</button></div>';
|
|
5482
|
+
}
|
|
5322
5483
|
|
|
5323
5484
|
function renderPanel() {
|
|
5324
5485
|
var head = '', body = '';
|
|
@@ -5341,7 +5502,8 @@ var MODEL = __MODEL_JSON__;
|
|
|
5341
5502
|
+ (scopeFocus ? staticChip('current view') : '')
|
|
5342
5503
|
+ chip(c.subsystem, 'subsystem', c.subsystem)
|
|
5343
5504
|
+ (scopeFocus ? '' : openViewButton('component', c.id, c.owns.length > 0))
|
|
5344
|
-
+ openApiButton(componentExposesApi(c), c.
|
|
5505
|
+
+ openApiButton(componentExposesApi(c), c.id)
|
|
5506
|
+
+ openSpecButton('component', c.id);
|
|
5345
5507
|
|
|
5346
5508
|
var linkedTypes = MODEL.types.filter(function (t) { return t.componentClass === c.id; });
|
|
5347
5509
|
if (linkedTypes.length) {
|
|
@@ -5351,6 +5513,14 @@ var MODEL = __MODEL_JSON__;
|
|
|
5351
5513
|
}
|
|
5352
5514
|
body += '<p class="desc">' + esc(c.description) + '</p>';
|
|
5353
5515
|
|
|
5516
|
+
if (c.externalLinks && c.externalLinks.length) {
|
|
5517
|
+
body += section('External links', c.externalLinks.length, c.externalLinks.map(function (l) {
|
|
5518
|
+
var label = l.label || l.url;
|
|
5519
|
+
var tag = l.type === 'implementation' ? staticChip('source') : '';
|
|
5520
|
+
return '<div class="method">' + tag + '<a href="' + esc(l.url) + '" target="_blank" rel="noopener" style="color:var(--accent);word-break:break-all">' + esc(label) + ' \\u2197</a></div>';
|
|
5521
|
+
}).join(''), true);
|
|
5522
|
+
}
|
|
5523
|
+
|
|
5354
5524
|
var depInner = (c.dependsOn.length ? c.dependsOn.map(function (d) { return chip(d, 'component', d); }).join('') : '<span class="desc">none</span>')
|
|
5355
5525
|
+ (c.owns.length ? '<div style="margin-top:8px"><b style="font-size:11px">Owns:</b><br>' + c.owns.map(function (d) { return chip(d, 'component', d); }).join('') + '</div>' : '');
|
|
5356
5526
|
body += section('Dependencies', c.dependsOn.length + c.owns.length, depInner, true);
|
|
@@ -5425,7 +5595,8 @@ var MODEL = __MODEL_JSON__;
|
|
|
5425
5595
|
MODEL.types.forEach(function (t2) { if (t2.id === focusId) ty = t2; });
|
|
5426
5596
|
if (ty) {
|
|
5427
5597
|
head = '<h2>' + esc(ty.name) + '</h2>' + staticChip('\\u00AB' + ty.kind + '\\u00BB')
|
|
5428
|
-
+ (ty.subsystem ? chip(ty.subsystem, 'subsystem', ty.subsystem) : staticChip('system-level shared'))
|
|
5598
|
+
+ (ty.subsystem ? chip(ty.subsystem, 'subsystem', ty.subsystem) : staticChip('system-level shared'))
|
|
5599
|
+
+ openSpecButton('type', ty.id);
|
|
5429
5600
|
|
|
5430
5601
|
if (ty.componentClass) {
|
|
5431
5602
|
head += '<div style="margin-top:6px"><b style="font-size:11px">Class Component:</b> ' + chip(ty.componentClass, 'component', ty.componentClass) + '</div>';
|
|
@@ -5501,7 +5672,8 @@ var MODEL = __MODEL_JSON__;
|
|
|
5501
5672
|
+ (s.status ? staticChip(s.status) : '')
|
|
5502
5673
|
+ (scopeFocus ? staticChip('current view') : '')
|
|
5503
5674
|
+ (scopeFocus ? '' : openViewButton('subsystem', s.id, subKids > 0))
|
|
5504
|
-
+ openApiButton(subsystemExposesApi(s.id), '')
|
|
5675
|
+
+ openApiButton(subsystemExposesApi(s.id), '')
|
|
5676
|
+
+ openSpecButton('subsystem', s.id);
|
|
5505
5677
|
body += '<p class="desc">' + esc(s.description) + '</p>';
|
|
5506
5678
|
if (s.trustedLinks.length) {
|
|
5507
5679
|
body += section('Trusted links (fast lanes)', s.trustedLinks.length, s.trustedLinks.map(function (t2) {
|
|
@@ -5559,6 +5731,16 @@ var MODEL = __MODEL_JSON__;
|
|
|
5559
5731
|
});
|
|
5560
5732
|
})(oapis[oi]);
|
|
5561
5733
|
}
|
|
5734
|
+
var ospecs = panel.querySelectorAll('[data-openspec-kind]');
|
|
5735
|
+
for (var si = 0; si < ospecs.length; si++) {
|
|
5736
|
+
(function (b) {
|
|
5737
|
+
b.addEventListener('click', function () {
|
|
5738
|
+
if (typeof opts !== 'undefined' && opts && typeof opts.onOpenSpec === 'function') {
|
|
5739
|
+
opts.onOpenSpec(b.getAttribute('data-openspec-kind'), b.getAttribute('data-openspec-id') || '');
|
|
5740
|
+
}
|
|
5741
|
+
});
|
|
5742
|
+
})(ospecs[si]);
|
|
5743
|
+
}
|
|
5562
5744
|
var flows = panel.querySelectorAll('[data-flow-comp]');
|
|
5563
5745
|
for (var j = 0; j < flows.length; j++) {
|
|
5564
5746
|
(function (b) {
|
|
@@ -5571,6 +5753,23 @@ var MODEL = __MODEL_JSON__;
|
|
|
5571
5753
|
}
|
|
5572
5754
|
|
|
5573
5755
|
renderPanel();
|
|
5756
|
+
// Stage G: a deep link may focus a component and/or open a method's narrative
|
|
5757
|
+
// modal once the seeded view + DOM + cy graph exist. The host parses the URL hash
|
|
5758
|
+
// into opts.initialSelect / opts.initialFlow \u2014 both carry the component id, so this
|
|
5759
|
+
// works whether the seeded view is the component itself or its parent subsystem
|
|
5760
|
+
// (a leaf component has no meaningful "inside", so Specs deep-links open the parent
|
|
5761
|
+
// and focus the component here).
|
|
5762
|
+
(function () {
|
|
5763
|
+
if (typeof opts === 'undefined' || !opts) return;
|
|
5764
|
+
var f = opts.initialFlow, s = opts.initialSelect;
|
|
5765
|
+
var focusComp = (f && f.comp) || (s && s.comp);
|
|
5766
|
+
if (focusComp && compById[focusComp]) {
|
|
5767
|
+
try { select('component', focusComp, true); } catch (e) { /* ignore */ }
|
|
5768
|
+
}
|
|
5769
|
+
if (f && f.comp && f.method && compById[f.comp]) {
|
|
5770
|
+
try { openFlow(f.comp, f.method, f.mode === 'steps' ? 'steps' : 'flow'); } catch (e) { /* ignore */ }
|
|
5771
|
+
}
|
|
5772
|
+
})();
|
|
5574
5773
|
})();
|
|
5575
5774
|
</script>
|
|
5576
5775
|
</body>
|
|
@@ -6111,11 +6310,63 @@ function operationFor(method, closureIds) {
|
|
|
6111
6310
|
}
|
|
6112
6311
|
return op;
|
|
6113
6312
|
}
|
|
6114
|
-
function
|
|
6115
|
-
|
|
6116
|
-
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);
|
|
6117
6367
|
const paths = {};
|
|
6118
|
-
for (const entry of
|
|
6368
|
+
for (const entry of entries) {
|
|
6369
|
+
const security = securityByEntry.get(entry.id);
|
|
6119
6370
|
for (const method of entry.methods) {
|
|
6120
6371
|
const endpoint = method.endpoint;
|
|
6121
6372
|
if (!endpoint || endpoint.transport !== "HTTP") continue;
|
|
@@ -6123,7 +6374,8 @@ function toOpenApi(snapshot) {
|
|
|
6123
6374
|
paths[p] = paths[p] ?? {};
|
|
6124
6375
|
paths[p][endpoint.method.toLowerCase()] = {
|
|
6125
6376
|
tags: [entry.id],
|
|
6126
|
-
...operationFor(method, closureIds)
|
|
6377
|
+
...operationFor(method, closureIds),
|
|
6378
|
+
...security ? { security: [security] } : {}
|
|
6127
6379
|
};
|
|
6128
6380
|
}
|
|
6129
6381
|
}
|
|
@@ -6136,19 +6388,45 @@ function toOpenApi(snapshot) {
|
|
|
6136
6388
|
required: t.fields.filter((f) => !f.optional).map((f) => f.name)
|
|
6137
6389
|
};
|
|
6138
6390
|
}
|
|
6139
|
-
const
|
|
6391
|
+
const components = {};
|
|
6392
|
+
if (Object.keys(schemas).length) components.schemas = schemas;
|
|
6393
|
+
if (Object.keys(schemes).length) components.securitySchemes = schemes;
|
|
6394
|
+
const basePaths = [...new Set(entries.map((e) => e.basePath).filter((b) => !!b))];
|
|
6395
|
+
const servers = opts.servers && basePaths.length === 1 ? [{ url: basePaths[0] }] : void 0;
|
|
6396
|
+
return {
|
|
6140
6397
|
openapi: "3.1.0",
|
|
6141
6398
|
info: {
|
|
6142
|
-
title: snapshot.projectName,
|
|
6399
|
+
title: opts.title ?? snapshot.projectName,
|
|
6143
6400
|
version: snapshot.version ?? "0.0.0",
|
|
6144
6401
|
...snapshot.stateId ? { "x-wairon-state-id": snapshot.stateId } : {},
|
|
6145
6402
|
"x-wairon-origin": snapshot.origin,
|
|
6146
6403
|
"x-wairon-generated-at": snapshot.generatedAt
|
|
6147
6404
|
},
|
|
6405
|
+
...servers ? { servers } : {},
|
|
6148
6406
|
paths,
|
|
6149
|
-
...Object.keys(
|
|
6407
|
+
...Object.keys(components).length ? { components } : {}
|
|
6150
6408
|
};
|
|
6151
|
-
|
|
6409
|
+
}
|
|
6410
|
+
function toOpenApi(snapshot) {
|
|
6411
|
+
const closureIds = new Set(snapshot.types.map((t) => t.id));
|
|
6412
|
+
return JSON.stringify(renderDoc(snapshot, httpEntriesOf(snapshot), closureIds), null, 2);
|
|
6413
|
+
}
|
|
6414
|
+
function toOpenApiSet(snapshot) {
|
|
6415
|
+
const closureIds = new Set(snapshot.types.map((t) => t.id));
|
|
6416
|
+
const byPortal = /* @__PURE__ */ new Map();
|
|
6417
|
+
const order = [];
|
|
6418
|
+
for (const entry of httpEntriesOf(snapshot)) {
|
|
6419
|
+
if (!byPortal.has(entry.component)) {
|
|
6420
|
+
byPortal.set(entry.component, []);
|
|
6421
|
+
order.push(entry.component);
|
|
6422
|
+
}
|
|
6423
|
+
byPortal.get(entry.component).push(entry);
|
|
6424
|
+
}
|
|
6425
|
+
return order.map((portalId) => {
|
|
6426
|
+
const entries = byPortal.get(portalId);
|
|
6427
|
+
const name = entries[0]?.name ?? portalId;
|
|
6428
|
+
return { portalId, name, document: JSON.stringify(renderDoc(snapshot, entries, closureIds, { title: name, servers: true }), null, 2) };
|
|
6429
|
+
});
|
|
6152
6430
|
}
|
|
6153
6431
|
function isOpenApiDocument(body) {
|
|
6154
6432
|
try {
|
|
@@ -6170,6 +6448,39 @@ function typeRefFromSchema(schema) {
|
|
|
6170
6448
|
if (typeof t === "string" && t !== "object") return t;
|
|
6171
6449
|
return "json";
|
|
6172
6450
|
}
|
|
6451
|
+
function authFromSecurityScheme(scheme) {
|
|
6452
|
+
const desc = typeof scheme.description === "string" ? { description: scheme.description } : {};
|
|
6453
|
+
if (scheme.type === "apiKey") {
|
|
6454
|
+
return {
|
|
6455
|
+
scheme: "apiKey",
|
|
6456
|
+
...scheme.in === "header" || scheme.in === "query" || scheme.in === "cookie" ? { in: scheme.in } : {},
|
|
6457
|
+
...typeof scheme.name === "string" ? { name: scheme.name } : {},
|
|
6458
|
+
...desc
|
|
6459
|
+
};
|
|
6460
|
+
}
|
|
6461
|
+
if (scheme.type === "http") {
|
|
6462
|
+
if (scheme.scheme === "bearer") return { scheme: "bearer", ...typeof scheme.bearerFormat === "string" ? { bearerFormat: scheme.bearerFormat } : {}, ...desc };
|
|
6463
|
+
if (scheme.scheme === "basic") return { scheme: "basic", ...desc };
|
|
6464
|
+
}
|
|
6465
|
+
if (scheme.type === "oauth2") {
|
|
6466
|
+
const flows = scheme.flows ?? {};
|
|
6467
|
+
const flowKey = Object.keys(flows)[0];
|
|
6468
|
+
const f = flows[flowKey] ?? {};
|
|
6469
|
+
return {
|
|
6470
|
+
scheme: "oauth2",
|
|
6471
|
+
...flowKey === "authorizationCode" || flowKey === "clientCredentials" || flowKey === "implicit" || flowKey === "password" ? { flow: flowKey } : {},
|
|
6472
|
+
...typeof f.authorizationUrl === "string" ? { authorizationUrl: f.authorizationUrl } : {},
|
|
6473
|
+
...typeof f.tokenUrl === "string" ? { tokenUrl: f.tokenUrl } : {},
|
|
6474
|
+
...typeof f.refreshUrl === "string" ? { refreshUrl: f.refreshUrl } : {},
|
|
6475
|
+
scopes: Object.entries(f.scopes ?? {}).map(([name, description]) => ({ name, description })),
|
|
6476
|
+
...desc
|
|
6477
|
+
};
|
|
6478
|
+
}
|
|
6479
|
+
if (scheme.type === "openIdConnect") {
|
|
6480
|
+
return { scheme: "openIdConnect", openIdConnectUrl: typeof scheme.openIdConnectUrl === "string" ? scheme.openIdConnectUrl : "", ...desc };
|
|
6481
|
+
}
|
|
6482
|
+
return void 0;
|
|
6483
|
+
}
|
|
6173
6484
|
function fromOpenApi(document, projectName) {
|
|
6174
6485
|
let parsed;
|
|
6175
6486
|
try {
|
|
@@ -6247,6 +6558,9 @@ function fromOpenApi(document, projectName) {
|
|
|
6247
6558
|
}))
|
|
6248
6559
|
});
|
|
6249
6560
|
}
|
|
6561
|
+
const securitySchemes = parsed.components?.securitySchemes ?? {};
|
|
6562
|
+
const firstScheme = Object.values(securitySchemes)[0];
|
|
6563
|
+
const importedAuth = firstScheme ? authFromSecurityScheme(firstScheme) : void 0;
|
|
6250
6564
|
const entry = {
|
|
6251
6565
|
id: `${projectName}-api`,
|
|
6252
6566
|
name: typeof info.title === "string" ? info.title : projectName,
|
|
@@ -6255,7 +6569,8 @@ function fromOpenApi(document, projectName) {
|
|
|
6255
6569
|
component: `${projectName}-api`,
|
|
6256
6570
|
methods,
|
|
6257
6571
|
details: typeof info.description === "string" ? info.description : `Imported OpenAPI surface of ${projectName}.`,
|
|
6258
|
-
...typeof info.version === "string" ? { version: info.version } : {}
|
|
6572
|
+
...typeof info.version === "string" ? { version: info.version } : {},
|
|
6573
|
+
...importedAuth ? { auth: importedAuth } : {}
|
|
6259
6574
|
};
|
|
6260
6575
|
return SurfaceSnapshotSchema.parse({
|
|
6261
6576
|
projectName,
|
|
@@ -6370,6 +6685,10 @@ function projectOwnSurface(maxAudience) {
|
|
|
6370
6685
|
component: comp.id,
|
|
6371
6686
|
methods,
|
|
6372
6687
|
...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
|
|
6688
|
+
// Project the backing Portal's auth + basePath so the codec can emit
|
|
6689
|
+
// OpenAPI security + per-portal servers self-contained from the snapshot.
|
|
6690
|
+
...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
|
|
6691
|
+
...comp.basePath ? { basePath: comp.basePath } : {},
|
|
6373
6692
|
details: raw.details ?? "",
|
|
6374
6693
|
...raw.version ? { version: raw.version } : {},
|
|
6375
6694
|
...raw.stability ? { stability: raw.stability } : {}
|
|
@@ -6419,20 +6738,28 @@ function removeSnapshot(projectName, rootDir = getProjectRoot()) {
|
|
|
6419
6738
|
function loadSurfaceSnapshots() {
|
|
6420
6739
|
return listSnapshots();
|
|
6421
6740
|
}
|
|
6741
|
+
function renderOpenApiForms(snapshot) {
|
|
6742
|
+
const renderedSet = toOpenApiSet(snapshot);
|
|
6743
|
+
return { renderedSet, ...renderedSet.length === 1 ? { rendered: renderedSet[0].document } : {} };
|
|
6744
|
+
}
|
|
6745
|
+
function writeSurfaceFile(outPath, body, snapshot) {
|
|
6746
|
+
const resolved = path5.resolve(outPath);
|
|
6747
|
+
fs4.mkdirSync(path5.dirname(resolved), { recursive: true });
|
|
6748
|
+
if (body !== void 0) fs4.writeFileSync(resolved, body);
|
|
6749
|
+
else writeYamlFile(resolved, snapshot);
|
|
6750
|
+
return resolved;
|
|
6751
|
+
}
|
|
6422
6752
|
function exportSurface(maxAudience, format, outPath) {
|
|
6423
6753
|
const snapshot = projectOwnSurface(maxAudience);
|
|
6424
|
-
const
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
|
|
6430
|
-
}
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
writtenTo = path5.resolve(outPath);
|
|
6434
|
-
}
|
|
6435
|
-
return { snapshot, ...rendered !== void 0 ? { rendered } : {}, ...writtenTo ? { writtenTo } : {} };
|
|
6754
|
+
const openapi = format === "openapi" ? renderOpenApiForms(snapshot) : void 0;
|
|
6755
|
+
const body = openapi?.rendered ?? openapi?.renderedSet[0]?.document;
|
|
6756
|
+
const writtenTo = outPath ? writeSurfaceFile(outPath, body, snapshot) : void 0;
|
|
6757
|
+
return {
|
|
6758
|
+
snapshot,
|
|
6759
|
+
...openapi?.rendered !== void 0 ? { rendered: openapi.rendered } : {},
|
|
6760
|
+
...openapi ? { renderedSet: openapi.renderedSet } : {},
|
|
6761
|
+
...writtenTo ? { writtenTo } : {}
|
|
6762
|
+
};
|
|
6436
6763
|
}
|
|
6437
6764
|
function importSurface(sourcePath, origin) {
|
|
6438
6765
|
const resolved = path5.resolve(sourcePath);
|
|
@@ -7692,13 +8019,16 @@ var init_conformance = __esm({
|
|
|
7692
8019
|
if (isInChainedSubproject(component.subsystem, ctx)) continue;
|
|
7693
8020
|
const draft = ctx.isImplementationDraft(impl);
|
|
7694
8021
|
if (!impl.sourcePath) {
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
8022
|
+
const hasExternalSource = (component.externalLinks ?? []).some((l) => l.type === "implementation");
|
|
8023
|
+
if (!hasExternalSource) {
|
|
8024
|
+
ctx.addIssue(
|
|
8025
|
+
"warning",
|
|
8026
|
+
"MISSING_SOURCE_PATH",
|
|
8027
|
+
`Implementation "${impl.id}" declares no sourcePath \u2014 its contract "${impl.contract}" cannot be structurally checked against code.`,
|
|
8028
|
+
impl.id,
|
|
8029
|
+
draft
|
|
8030
|
+
);
|
|
8031
|
+
}
|
|
7702
8032
|
continue;
|
|
7703
8033
|
}
|
|
7704
8034
|
const lookup = lookups.get(normalizeSourcePath(impl.sourcePath));
|
|
@@ -8087,13 +8417,14 @@ var init_portals = __esm({
|
|
|
8087
8417
|
};
|
|
8088
8418
|
portalsRule = {
|
|
8089
8419
|
name: "portal-endpoints",
|
|
8090
|
-
description: "A Portal declares its portalType and binds every interface method to a concrete endpoint of the matching transport. Non-Portal components carry no portalType, basePath, or
|
|
8420
|
+
description: "A Portal declares its portalType and binds every interface method to a concrete endpoint of the matching transport. Non-Portal components carry no portalType, basePath, endpoints, or auth (auth is inbound transport auth \u2014 a Gateway carries it on the Portal it owns).",
|
|
8091
8421
|
codes: [
|
|
8092
8422
|
{ code: "MISSING_PORTAL_TYPE", defaultSeverity: "error", summary: "Portal without a portalType" },
|
|
8093
8423
|
{ code: "MISSING_ENDPOINT", defaultSeverity: "error", summary: "Portal method without a wire endpoint binding" },
|
|
8094
8424
|
{ code: "ENDPOINT_TRANSPORT_MISMATCH", defaultSeverity: "error", summary: "Endpoint transport does not match the Portal portalType" },
|
|
8095
8425
|
{ code: "UNEXPECTED_PORTAL_FIELD", defaultSeverity: "error", summary: "Non-Portal component with portalType/basePath" },
|
|
8096
|
-
{ code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" }
|
|
8426
|
+
{ code: "ARCHITECTURE_VIOLATION_NON_PORTAL_ENDPOINT", defaultSeverity: "error", summary: "Non-Portal component method declaring an endpoint" },
|
|
8427
|
+
{ code: "AUTH_ON_NON_PORTAL", defaultSeverity: "warning", summary: "Non-Portal component declaring auth (auth is inbound transport auth, only meaningful on a Portal)" }
|
|
8097
8428
|
],
|
|
8098
8429
|
check(ctx) {
|
|
8099
8430
|
for (const comp of ctx.components) {
|
|
@@ -8145,6 +8476,15 @@ var init_portals = __esm({
|
|
|
8145
8476
|
isDraftCtx
|
|
8146
8477
|
);
|
|
8147
8478
|
}
|
|
8479
|
+
if (comp.auth !== void 0) {
|
|
8480
|
+
ctx.addIssue(
|
|
8481
|
+
"warning",
|
|
8482
|
+
"AUTH_ON_NON_PORTAL",
|
|
8483
|
+
`Component "${comp.id}" is a ${comp.componentType}, not a Portal, but declares "auth". Auth is inbound transport auth and is only meaningful on a Portal (a Gateway carries it on the Portal it owns). Move it to the exposed Portal, or remove it.`,
|
|
8484
|
+
comp.id,
|
|
8485
|
+
isDraftCtx
|
|
8486
|
+
);
|
|
8487
|
+
}
|
|
8148
8488
|
const compInterfaces = ctx.interfaces.filter((i) => i.component === comp.id);
|
|
8149
8489
|
for (const intf of compInterfaces) {
|
|
8150
8490
|
for (const m of intf.methods) {
|
|
@@ -11228,6 +11568,95 @@ var init_lint_allows = __esm({
|
|
|
11228
11568
|
}
|
|
11229
11569
|
});
|
|
11230
11570
|
|
|
11571
|
+
// src/core/rules/portal-call-auth.ts
|
|
11572
|
+
var COMPONENT_REF_PREFIX, portalCallAuthRule;
|
|
11573
|
+
var init_portal_call_auth = __esm({
|
|
11574
|
+
"src/core/rules/portal-call-auth.ts"() {
|
|
11575
|
+
"use strict";
|
|
11576
|
+
COMPONENT_REF_PREFIX = "component:";
|
|
11577
|
+
portalCallAuthRule = {
|
|
11578
|
+
name: "portal-call-auth",
|
|
11579
|
+
description: "Hardens authenticated cross-service calls. An OUTBOUND narrative `call` into ANOTHER component's Portal whose auth is not `none` must (a) be made by an Adapter \u2014 the only block that does external I/O \u2014 and (b) declare the credential source it presents via the step's `auth.from`. Absence of a source warns PORTAL_AUTH_UNMET; a non-Adapter presenter warns AUTH_PRESENTER_NOT_ADAPTER. When `auth.from` is a modeled reference (`component:<id>`) it must resolve to an Adapter/Store the presenter is wired to (UNKNOWN_AUTH_SOURCE / AUTH_SOURCE_NOT_PROVIDER / AUTH_SOURCE_UNWIRED). The actual secret is never stored in the spec. Dispatch steps (a portal's OWN inbound routing) and self-calls are not cross-service calls and are excluded.",
|
|
11580
|
+
codes: [
|
|
11581
|
+
{ code: "PORTAL_AUTH_UNMET", defaultSeverity: "warning", summary: "A narrative call into another component's authed Portal does not declare where its credential loads from" },
|
|
11582
|
+
{ code: "AUTH_PRESENTER_NOT_ADAPTER", defaultSeverity: "warning", summary: "A non-Adapter component authenticates an outbound call to a portal (external I/O must go through an Adapter)" },
|
|
11583
|
+
{ code: "UNKNOWN_AUTH_SOURCE", defaultSeverity: "warning", summary: "auth.from references a component: source that does not exist" },
|
|
11584
|
+
{ code: "AUTH_SOURCE_NOT_PROVIDER", defaultSeverity: "warning", summary: "auth.from references a component that is not an Adapter or Store" },
|
|
11585
|
+
{ code: "AUTH_SOURCE_UNWIRED", defaultSeverity: "warning", summary: "The presenter declares a component: credential source it does not depend on or own" }
|
|
11586
|
+
],
|
|
11587
|
+
check(ctx) {
|
|
11588
|
+
for (const impl of ctx.implementations) {
|
|
11589
|
+
const ownComponent = ctx.interfaceMap.get(impl.contract)?.component;
|
|
11590
|
+
const presenter = ownComponent ? ctx.componentMap.get(ownComponent) : void 0;
|
|
11591
|
+
const draft = ctx.isImplementationDraft(impl);
|
|
11592
|
+
for (const method of impl.methods ?? []) {
|
|
11593
|
+
for (const step of method.narrative ?? []) {
|
|
11594
|
+
if (step.type !== "call") continue;
|
|
11595
|
+
if (!step.targetComponent || step.targetComponent === ownComponent) continue;
|
|
11596
|
+
const target = ctx.componentMap.get(step.targetComponent);
|
|
11597
|
+
if (!target || target.componentType !== "Portal") continue;
|
|
11598
|
+
if (!target.auth || target.auth.scheme === "none") continue;
|
|
11599
|
+
if (presenter && presenter.componentType !== "Adapter") {
|
|
11600
|
+
ctx.addIssue(
|
|
11601
|
+
"warning",
|
|
11602
|
+
"AUTH_PRESENTER_NOT_ADAPTER",
|
|
11603
|
+
`Narrative step ${step.stepNumber} of "${method.name}" in "${impl.id}": "${presenter.id}" (${presenter.componentType}) authenticates an outbound call to portal "${target.id}". An authenticated cross-service call is external I/O and must be made by an Adapter (the only block that does external I/O) \u2014 route it through a client Adapter.`,
|
|
11604
|
+
impl.id,
|
|
11605
|
+
draft
|
|
11606
|
+
);
|
|
11607
|
+
}
|
|
11608
|
+
const from = step.auth?.from;
|
|
11609
|
+
if (!from) {
|
|
11610
|
+
ctx.addIssue(
|
|
11611
|
+
"warning",
|
|
11612
|
+
"PORTAL_AUTH_UNMET",
|
|
11613
|
+
`Narrative step ${step.stepNumber} of "${method.name}" in "${impl.id}" calls the authenticated portal "${target.id}" (auth scheme: ${target.auth.scheme}) but does not declare where its credential is loaded from. Add the credential source (the step's auth.from \u2014 a secret-store component, env var, config key, or vault ref), or drop the portal's auth if it needs none.`,
|
|
11614
|
+
impl.id,
|
|
11615
|
+
draft
|
|
11616
|
+
);
|
|
11617
|
+
continue;
|
|
11618
|
+
}
|
|
11619
|
+
if (from.startsWith(COMPONENT_REF_PREFIX)) {
|
|
11620
|
+
const srcId = from.slice(COMPONENT_REF_PREFIX.length);
|
|
11621
|
+
const src = ctx.componentMap.get(srcId);
|
|
11622
|
+
if (!src) {
|
|
11623
|
+
ctx.addIssue(
|
|
11624
|
+
"warning",
|
|
11625
|
+
"UNKNOWN_AUTH_SOURCE",
|
|
11626
|
+
`Narrative step ${step.stepNumber} of "${method.name}" in "${impl.id}": auth.from references component "${srcId}", which does not exist.`,
|
|
11627
|
+
impl.id,
|
|
11628
|
+
draft
|
|
11629
|
+
);
|
|
11630
|
+
} else {
|
|
11631
|
+
if (src.componentType !== "Adapter" && src.componentType !== "Store") {
|
|
11632
|
+
ctx.addIssue(
|
|
11633
|
+
"warning",
|
|
11634
|
+
"AUTH_SOURCE_NOT_PROVIDER",
|
|
11635
|
+
`Narrative step ${step.stepNumber} of "${method.name}" in "${impl.id}": auth.from references "${srcId}" (${src.componentType}); a credential source must be an Adapter (loads the secret via external I/O) or a Store (holds it).`,
|
|
11636
|
+
impl.id,
|
|
11637
|
+
draft
|
|
11638
|
+
);
|
|
11639
|
+
}
|
|
11640
|
+
const wired = (presenter?.dependsOn ?? []).includes(srcId) || (presenter?.owns ?? []).includes(srcId);
|
|
11641
|
+
if (presenter && !wired) {
|
|
11642
|
+
ctx.addIssue(
|
|
11643
|
+
"warning",
|
|
11644
|
+
"AUTH_SOURCE_UNWIRED",
|
|
11645
|
+
`Narrative step ${step.stepNumber} of "${method.name}" in "${impl.id}": "${presenter.id}" loads its credential from "${srcId}" but neither depends on nor owns it \u2014 declare the dependsOn edge so the credential wiring is real.`,
|
|
11646
|
+
impl.id,
|
|
11647
|
+
draft
|
|
11648
|
+
);
|
|
11649
|
+
}
|
|
11650
|
+
}
|
|
11651
|
+
}
|
|
11652
|
+
}
|
|
11653
|
+
}
|
|
11654
|
+
}
|
|
11655
|
+
}
|
|
11656
|
+
};
|
|
11657
|
+
}
|
|
11658
|
+
});
|
|
11659
|
+
|
|
11231
11660
|
// src/core/rules/index.ts
|
|
11232
11661
|
function composeRuleSequence(extraRules = []) {
|
|
11233
11662
|
const base = SDD_RULES.filter((r) => r !== lintAllowsRule);
|
|
@@ -11477,6 +11906,7 @@ var init_rules = __esm({
|
|
|
11477
11906
|
init_hidden_state();
|
|
11478
11907
|
init_dependency_conformance();
|
|
11479
11908
|
init_lint_allows();
|
|
11909
|
+
init_portal_call_auth();
|
|
11480
11910
|
init_source_analysis();
|
|
11481
11911
|
init_types();
|
|
11482
11912
|
init_type_analysis();
|
|
@@ -11498,6 +11928,9 @@ var init_rules = __esm({
|
|
|
11498
11928
|
narrativeAntipatternsRule,
|
|
11499
11929
|
narrativeDetailRule,
|
|
11500
11930
|
portalsRule,
|
|
11931
|
+
// Cross-call auth: a narrative call into an authed Portal must name its
|
|
11932
|
+
// credential source (rides with the portal family).
|
|
11933
|
+
portalCallAuthRule,
|
|
11501
11934
|
stereotypeDepsRule,
|
|
11502
11935
|
patternsRule,
|
|
11503
11936
|
// Facade shape rides with pattern ownership: same §7 doctrine, narrative side.
|
|
@@ -11560,6 +11993,7 @@ var init_rules = __esm({
|
|
|
11560
11993
|
// L4 expectations: implementations and their code linkage.
|
|
11561
11994
|
MISSING_IMPLEMENTATION_METHOD: "implementations",
|
|
11562
11995
|
MISSING_SOURCE_PATH: "implementations",
|
|
11996
|
+
PORTAL_AUTH_UNMET: "implementations",
|
|
11563
11997
|
MISSING_SOURCE_FILE: "implementations",
|
|
11564
11998
|
SOURCE_PATH_ESCAPES_ROOT: "implementations",
|
|
11565
11999
|
UNREALIZED_METHOD: "implementations",
|
|
@@ -12018,6 +12452,28 @@ function buildGraphModel(level) {
|
|
|
12018
12452
|
...t.subsystem ? { parentId: t.subsystem } : {}
|
|
12019
12453
|
});
|
|
12020
12454
|
}
|
|
12455
|
+
const componentByInterface = /* @__PURE__ */ new Map();
|
|
12456
|
+
for (const c of model.components) {
|
|
12457
|
+
for (const intf of c.interfaces) componentByInterface.set(intf.id, c.id);
|
|
12458
|
+
}
|
|
12459
|
+
for (const impl of loadImplementationSpecs()) {
|
|
12460
|
+
const componentId = componentByInterface.get(impl.contract);
|
|
12461
|
+
if (!componentId) continue;
|
|
12462
|
+
nodes.push({
|
|
12463
|
+
id: impl.id,
|
|
12464
|
+
label: impl.name,
|
|
12465
|
+
kind: "implementation",
|
|
12466
|
+
level: 4,
|
|
12467
|
+
parentId: componentId,
|
|
12468
|
+
...impl.status ? { status: impl.status } : {}
|
|
12469
|
+
});
|
|
12470
|
+
candidates.push({ from: componentId, to: impl.id, edgeKind: "owns" });
|
|
12471
|
+
}
|
|
12472
|
+
for (const c of model.components) {
|
|
12473
|
+
for (const memberId of c.owns) {
|
|
12474
|
+
candidates.push({ from: c.id, to: memberId, edgeKind: "owns" });
|
|
12475
|
+
}
|
|
12476
|
+
}
|
|
12021
12477
|
for (const e of model.edges) {
|
|
12022
12478
|
candidates.push({ from: e.from, to: e.to, edgeKind: "depends_on" });
|
|
12023
12479
|
}
|
|
@@ -14944,6 +15400,8 @@ __export(src_exports, {
|
|
|
14944
15400
|
EndpointSchema: () => EndpointSchema,
|
|
14945
15401
|
EventBindingSchema: () => EventBindingSchema,
|
|
14946
15402
|
ExtDataSchema: () => ExtDataSchema,
|
|
15403
|
+
ExternalLinkSchema: () => ExternalLinkSchema,
|
|
15404
|
+
ExternalLinkTypeSchema: () => ExternalLinkTypeSchema,
|
|
14947
15405
|
GITHUB_REPO: () => GITHUB_REPO,
|
|
14948
15406
|
GeminiExporter: () => GeminiExporter,
|
|
14949
15407
|
GroupSpecSchema: () => GroupSpecSchema,
|
|
@@ -14961,6 +15419,7 @@ __export(src_exports, {
|
|
|
14961
15419
|
MethodImplementationSchema: () => MethodImplementationSchema,
|
|
14962
15420
|
MethodParamSchema: () => MethodParamSchema,
|
|
14963
15421
|
MethodSignatureSchema: () => MethodSignatureSchema,
|
|
15422
|
+
NamedOpenApiSpecSchema: () => NamedOpenApiSpecSchema,
|
|
14964
15423
|
NamingRuleConfigSchema: () => NamingRuleConfigSchema,
|
|
14965
15424
|
NarrativeDetailSchema: () => NarrativeDetailSchema,
|
|
14966
15425
|
NarrativeStepSchema: () => NarrativeStepSchema,
|
|
@@ -14974,6 +15433,8 @@ __export(src_exports, {
|
|
|
14974
15433
|
PathsConfigSchema: () => PathsConfigSchema,
|
|
14975
15434
|
PatternDefSchema: () => PatternDefSchema,
|
|
14976
15435
|
PatternRefSchema: () => PatternRefSchema,
|
|
15436
|
+
PortalAuthSchema: () => PortalAuthSchema,
|
|
15437
|
+
PortalAuthSchemeSchema: () => PortalAuthSchemeSchema,
|
|
14977
15438
|
PortalTypeSchema: () => PortalTypeSchema,
|
|
14978
15439
|
ProfileDefSchema: () => ProfileDefSchema,
|
|
14979
15440
|
ProjectConfigSchema: () => ProjectConfigSchema,
|
|
@@ -15193,6 +15654,7 @@ __export(src_exports, {
|
|
|
15193
15654
|
syncContextFiles: () => syncContextFiles,
|
|
15194
15655
|
toMarkdown: () => toMarkdown,
|
|
15195
15656
|
toOpenApi: () => toOpenApi,
|
|
15657
|
+
toOpenApiSet: () => toOpenApiSet,
|
|
15196
15658
|
updateSpec: () => updateSpec,
|
|
15197
15659
|
validateAsComplete: () => validateAsComplete,
|
|
15198
15660
|
validateProjectConfig: () => validateProjectConfig,
|
|
@@ -15228,7 +15690,7 @@ function defaultTargetConfig(type) {
|
|
|
15228
15690
|
enabled: true
|
|
15229
15691
|
};
|
|
15230
15692
|
}
|
|
15231
|
-
var WAIRON_VERSION = "5.0.
|
|
15693
|
+
var WAIRON_VERSION = "5.0.2-dev.10";
|
|
15232
15694
|
var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
|
|
15233
15695
|
var ARCHITECT_AGENT_ID = "agent-architect";
|
|
15234
15696
|
var ARCHITECT_TEMPLATE_ID = "architect";
|
|
@@ -16525,6 +16987,8 @@ init_yaml();
|
|
|
16525
16987
|
EndpointSchema,
|
|
16526
16988
|
EventBindingSchema,
|
|
16527
16989
|
ExtDataSchema,
|
|
16990
|
+
ExternalLinkSchema,
|
|
16991
|
+
ExternalLinkTypeSchema,
|
|
16528
16992
|
GITHUB_REPO,
|
|
16529
16993
|
GeminiExporter,
|
|
16530
16994
|
GroupSpecSchema,
|
|
@@ -16542,6 +17006,7 @@ init_yaml();
|
|
|
16542
17006
|
MethodImplementationSchema,
|
|
16543
17007
|
MethodParamSchema,
|
|
16544
17008
|
MethodSignatureSchema,
|
|
17009
|
+
NamedOpenApiSpecSchema,
|
|
16545
17010
|
NamingRuleConfigSchema,
|
|
16546
17011
|
NarrativeDetailSchema,
|
|
16547
17012
|
NarrativeStepSchema,
|
|
@@ -16555,6 +17020,8 @@ init_yaml();
|
|
|
16555
17020
|
PathsConfigSchema,
|
|
16556
17021
|
PatternDefSchema,
|
|
16557
17022
|
PatternRefSchema,
|
|
17023
|
+
PortalAuthSchema,
|
|
17024
|
+
PortalAuthSchemeSchema,
|
|
16558
17025
|
PortalTypeSchema,
|
|
16559
17026
|
ProfileDefSchema,
|
|
16560
17027
|
ProjectConfigSchema,
|
|
@@ -16774,6 +17241,7 @@ init_yaml();
|
|
|
16774
17241
|
syncContextFiles,
|
|
16775
17242
|
toMarkdown,
|
|
16776
17243
|
toOpenApi,
|
|
17244
|
+
toOpenApiSet,
|
|
16777
17245
|
updateSpec,
|
|
16778
17246
|
validateAsComplete,
|
|
16779
17247
|
validateProjectConfig,
|