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