@wairon/cli 5.0.2-dev.6 → 5.0.2-dev.8

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 CHANGED
@@ -65,7 +65,7 @@ var init_defaults = __esm({
65
65
  copilot: ".github/prompts",
66
66
  codex: ".codex/agents"
67
67
  };
68
- WAIRON_VERSION = "5.0.2-dev.6";
68
+ WAIRON_VERSION = "5.0.2-dev.8";
69
69
  GITHUB_REPO = "SYW-Apps/Waffle-AIron";
70
70
  ARCHITECT_AGENT_ID = "agent-architect";
71
71
  ARCHITECT_TEMPLATE_ID = "architect";
@@ -703,7 +703,7 @@ var init_template = __esm({
703
703
  });
704
704
 
705
705
  // src/models/specs.ts
706
- 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;
706
+ 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;
707
707
  var init_specs = __esm({
708
708
  "src/models/specs.ts"() {
709
709
  "use strict";
@@ -907,6 +907,28 @@ var init_specs = __esm({
907
907
  event: import_zod6.z.string().optional(),
908
908
  description: import_zod6.z.string().optional()
909
909
  });
910
+ ExternalLinkTypeSchema = import_zod6.z.enum(["implementation", "informative"]);
911
+ ExternalLinkSchema = import_zod6.z.object({
912
+ url: import_zod6.z.string(),
913
+ /** Defaults to 'informative' so an untyped link never silently satisfies the source requirement. */
914
+ type: ExternalLinkTypeSchema.default("informative"),
915
+ label: import_zod6.z.string().optional()
916
+ });
917
+ PortalAuthSchemeSchema = import_zod6.z.enum(["none", "apiKey", "bearer", "basic", "oauth2", "openIdConnect", "custom"]);
918
+ PortalAuthSchema = import_zod6.z.object({
919
+ scheme: PortalAuthSchemeSchema,
920
+ in: import_zod6.z.enum(["header", "query", "cookie"]).optional(),
921
+ name: import_zod6.z.string().optional(),
922
+ bearerFormat: import_zod6.z.string().optional(),
923
+ authorizationUrl: import_zod6.z.string().optional(),
924
+ tokenUrl: import_zod6.z.string().optional(),
925
+ refreshUrl: import_zod6.z.string().optional(),
926
+ scopes: import_zod6.z.array(import_zod6.z.object({ name: import_zod6.z.string(), description: import_zod6.z.string() })).optional(),
927
+ flow: import_zod6.z.enum(["authorizationCode", "clientCredentials", "implicit", "password"]).optional(),
928
+ openIdConnectUrl: import_zod6.z.string().optional(),
929
+ description: import_zod6.z.string().optional(),
930
+ example: import_zod6.z.string().optional()
931
+ });
910
932
  ComponentSpecSchema = import_zod6.z.object({
911
933
  id: SpecIdSchema,
912
934
  name: import_zod6.z.string(),
@@ -920,6 +942,10 @@ var init_specs = __esm({
920
942
  dependsOn: import_zod6.z.array(import_zod6.z.string()).default([]),
921
943
  portalType: PortalTypeSchema.optional(),
922
944
  basePath: import_zod6.z.string().optional(),
945
+ /** Portal-only: the API's authentication scheme (see PortalAuthSchema) — projected
946
+ * into the generated OpenAPI's securitySchemes/security. Portals with different auth
947
+ * must be separate components (one auth per portal ⇒ one OpenAPI spec per portal). */
948
+ auth: PortalAuthSchema.optional(),
923
949
  /** Portal-only: capability → component.method dispatch table (see DispatchBindingSchema). */
924
950
  dispatch: import_zod6.z.array(DispatchBindingSchema).optional(),
925
951
  /** Store-only: whether held state survives restart (see DurabilitySchema). */
@@ -932,6 +958,11 @@ var init_specs = __esm({
932
958
  patterns: import_zod6.z.array(PatternRefSchema).optional(),
933
959
  /** Optional component variant — a declared, base-anchored specialization of this component's stereotype (resolved against the variant registry; UNKNOWN_VARIANT / VARIANT_BASE_MISMATCH). */
934
960
  variant: import_zod6.z.string().optional(),
961
+ /** Opaque external references (see ExternalLinkSchema) — documented URLs wairon does
962
+ * not fetch or validate. An `implementation` link is the external source-of-record and
963
+ * satisfies the source requirement for a source-less implementation (suppresses
964
+ * MISSING_SOURCE_PATH); `informative` links are context only. */
965
+ externalLinks: import_zod6.z.array(ExternalLinkSchema).optional(),
935
966
  /** Per-spec lint suppressions (see LintConfigSchema). */
936
967
  lint: LintConfigSchema.optional(),
937
968
  /** Opaque pack/tool extension data (see ExtDataSchema) — preserved verbatim. */
@@ -1280,7 +1311,12 @@ var init_specs = __esm({
1280
1311
  dispatch: import_zod6.z.array(DispatchBindingSchema).optional(),
1281
1312
  details: import_zod6.z.string().default(""),
1282
1313
  version: import_zod6.z.string().optional(),
1283
- stability: import_zod6.z.string().optional()
1314
+ stability: import_zod6.z.string().optional(),
1315
+ /** Projected copy of the backing Portal's auth (see PortalAuthSchema) — the codec
1316
+ * emits it as OpenAPI securitySchemes/security. */
1317
+ auth: PortalAuthSchema.optional(),
1318
+ /** The backing Portal's basePath — becomes the per-portal OpenAPI `servers` url. */
1319
+ basePath: import_zod6.z.string().optional()
1284
1320
  });
1285
1321
  SurfaceSnapshotSchema = import_zod6.z.object({
1286
1322
  /** Producing project/system name — the snapshot's resolution identity. */
@@ -1295,6 +1331,11 @@ var init_specs = __esm({
1295
1331
  /** Transitive type closure of every exported signature — self-contained. */
1296
1332
  types: import_zod6.z.array(SurfaceTypeDefSchema).default([])
1297
1333
  });
1334
+ NamedOpenApiSpecSchema = import_zod6.z.object({
1335
+ portalId: import_zod6.z.string(),
1336
+ name: import_zod6.z.string(),
1337
+ document: import_zod6.z.string()
1338
+ });
1298
1339
  GroupSpecSchema = import_zod6.z.object({
1299
1340
  kind: import_zod6.z.literal("group"),
1300
1341
  id: SpecIdSchema,
@@ -2652,6 +2693,7 @@ function buildCanvasModel(issues = []) {
2652
2693
  ...ownerOf.has(comp.id) ? { owner: ownerOf.get(comp.id) } : {},
2653
2694
  owns: comp.owns.filter((o) => componentIds.has(o)),
2654
2695
  dependsOn: comp.dependsOn,
2696
+ ...comp.externalLinks && comp.externalLinks.length ? { externalLinks: comp.externalLinks } : {},
2655
2697
  interfaces: compInterfaces.map((i) => ({
2656
2698
  id: i.id,
2657
2699
  name: i.name,
@@ -6102,7 +6144,7 @@ var MODEL = __MODEL_JSON__;
6102
6144
  + (scopeFocus ? staticChip('current view') : '')
6103
6145
  + chip(c.subsystem, 'subsystem', c.subsystem)
6104
6146
  + (scopeFocus ? '' : openViewButton('component', c.id, c.owns.length > 0))
6105
- + openApiButton(componentExposesApi(c), c.apiTag)
6147
+ + openApiButton(componentExposesApi(c), c.id)
6106
6148
  + openSpecButton('component', c.id);
6107
6149
 
6108
6150
  var linkedTypes = MODEL.types.filter(function (t) { return t.componentClass === c.id; });
@@ -6113,6 +6155,14 @@ var MODEL = __MODEL_JSON__;
6113
6155
  }
6114
6156
  body += '<p class="desc">' + esc(c.description) + '</p>';
6115
6157
 
6158
+ if (c.externalLinks && c.externalLinks.length) {
6159
+ body += section('External links', c.externalLinks.length, c.externalLinks.map(function (l) {
6160
+ var label = l.label || l.url;
6161
+ var tag = l.type === 'implementation' ? staticChip('source') : '';
6162
+ 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>';
6163
+ }).join(''), true);
6164
+ }
6165
+
6116
6166
  var depInner = (c.dependsOn.length ? c.dependsOn.map(function (d) { return chip(d, 'component', d); }).join('') : '<span class="desc">none</span>')
6117
6167
  + (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>' : '');
6118
6168
  body += section('Dependencies', c.dependsOn.length + c.owns.length, depInner, true);
@@ -6899,11 +6949,63 @@ function operationFor(method2, closureIds) {
6899
6949
  }
6900
6950
  return op;
6901
6951
  }
6902
- function toOpenApi(snapshot) {
6903
- const closureIds = new Set(snapshot.types.map((t) => t.id));
6904
- const httpEntries = snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
6952
+ function securitySchemeObject(auth) {
6953
+ if (auth.scheme === "none") return null;
6954
+ const desc = auth.description ? { description: auth.description } : {};
6955
+ switch (auth.scheme) {
6956
+ case "apiKey":
6957
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "X-API-Key", ...desc };
6958
+ case "bearer":
6959
+ return { type: "http", scheme: "bearer", ...auth.bearerFormat ? { bearerFormat: auth.bearerFormat } : {}, ...desc };
6960
+ case "basic":
6961
+ return { type: "http", scheme: "basic", ...desc };
6962
+ case "oauth2": {
6963
+ const flow = { scopes: Object.fromEntries((auth.scopes ?? []).map((s) => [s.name, s.description])) };
6964
+ if (auth.authorizationUrl) flow.authorizationUrl = auth.authorizationUrl;
6965
+ if (auth.tokenUrl) flow.tokenUrl = auth.tokenUrl;
6966
+ if (auth.refreshUrl) flow.refreshUrl = auth.refreshUrl;
6967
+ return { type: "oauth2", flows: { [auth.flow ?? "authorizationCode"]: flow }, ...desc };
6968
+ }
6969
+ case "openIdConnect":
6970
+ return { type: "openIdConnect", openIdConnectUrl: auth.openIdConnectUrl ?? "", ...desc };
6971
+ case "custom":
6972
+ return { type: "apiKey", in: auth.in ?? "header", name: auth.name ?? "Authorization", description: auth.description ?? auth.example ?? "Custom authentication scheme." };
6973
+ default:
6974
+ return null;
6975
+ }
6976
+ }
6977
+ function schemeBaseName(scheme) {
6978
+ return { apiKey: "ApiKeyAuth", bearer: "BearerAuth", basic: "BasicAuth", oauth2: "OAuth2", openIdConnect: "OpenIdConnect", custom: "CustomAuth" }[scheme] ?? "Auth";
6979
+ }
6980
+ function buildSecurity(entries) {
6981
+ const schemes = {};
6982
+ const nameByContent = /* @__PURE__ */ new Map();
6983
+ const securityByEntry = /* @__PURE__ */ new Map();
6984
+ for (const entry of entries) {
6985
+ if (!entry.auth) continue;
6986
+ const obj = securitySchemeObject(entry.auth);
6987
+ if (!obj) continue;
6988
+ const content = JSON.stringify(obj);
6989
+ let name = nameByContent.get(content);
6990
+ if (!name) {
6991
+ name = schemeBaseName(entry.auth.scheme);
6992
+ for (let n = 2; schemes[name]; n++) name = schemeBaseName(entry.auth.scheme) + n;
6993
+ schemes[name] = obj;
6994
+ nameByContent.set(content, name);
6995
+ }
6996
+ const scopeNames = entry.auth.scheme === "oauth2" ? (entry.auth.scopes ?? []).map((s) => s.name) : [];
6997
+ securityByEntry.set(entry.id, { [name]: scopeNames });
6998
+ }
6999
+ return { schemes, securityByEntry };
7000
+ }
7001
+ function httpEntriesOf(snapshot) {
7002
+ return snapshot.interfaces.filter((e) => e.type === "REST" || e.methods.some((m) => m.endpoint?.transport === "HTTP"));
7003
+ }
7004
+ function renderDoc(snapshot, entries, closureIds, opts = {}) {
7005
+ const { schemes, securityByEntry } = buildSecurity(entries);
6905
7006
  const paths = {};
6906
- for (const entry of httpEntries) {
7007
+ for (const entry of entries) {
7008
+ const security = securityByEntry.get(entry.id);
6907
7009
  for (const method2 of entry.methods) {
6908
7010
  const endpoint = method2.endpoint;
6909
7011
  if (!endpoint || endpoint.transport !== "HTTP") continue;
@@ -6911,7 +7013,8 @@ function toOpenApi(snapshot) {
6911
7013
  paths[p] = paths[p] ?? {};
6912
7014
  paths[p][endpoint.method.toLowerCase()] = {
6913
7015
  tags: [entry.id],
6914
- ...operationFor(method2, closureIds)
7016
+ ...operationFor(method2, closureIds),
7017
+ ...security ? { security: [security] } : {}
6915
7018
  };
6916
7019
  }
6917
7020
  }
@@ -6924,19 +7027,41 @@ function toOpenApi(snapshot) {
6924
7027
  required: t.fields.filter((f) => !f.optional).map((f) => f.name)
6925
7028
  };
6926
7029
  }
6927
- const doc = {
7030
+ const components = {};
7031
+ if (Object.keys(schemas).length) components.schemas = schemas;
7032
+ if (Object.keys(schemes).length) components.securitySchemes = schemes;
7033
+ const basePaths = [...new Set(entries.map((e) => e.basePath).filter((b) => !!b))];
7034
+ const servers = opts.servers && basePaths.length === 1 ? [{ url: basePaths[0] }] : void 0;
7035
+ return {
6928
7036
  openapi: "3.1.0",
6929
7037
  info: {
6930
- title: snapshot.projectName,
7038
+ title: opts.title ?? snapshot.projectName,
6931
7039
  version: snapshot.version ?? "0.0.0",
6932
7040
  ...snapshot.stateId ? { "x-wairon-state-id": snapshot.stateId } : {},
6933
7041
  "x-wairon-origin": snapshot.origin,
6934
7042
  "x-wairon-generated-at": snapshot.generatedAt
6935
7043
  },
7044
+ ...servers ? { servers } : {},
6936
7045
  paths,
6937
- ...Object.keys(schemas).length ? { components: { schemas } } : {}
7046
+ ...Object.keys(components).length ? { components } : {}
6938
7047
  };
6939
- return JSON.stringify(doc, null, 2);
7048
+ }
7049
+ function toOpenApiSet(snapshot) {
7050
+ const closureIds = new Set(snapshot.types.map((t) => t.id));
7051
+ const byPortal = /* @__PURE__ */ new Map();
7052
+ const order = [];
7053
+ for (const entry of httpEntriesOf(snapshot)) {
7054
+ if (!byPortal.has(entry.component)) {
7055
+ byPortal.set(entry.component, []);
7056
+ order.push(entry.component);
7057
+ }
7058
+ byPortal.get(entry.component).push(entry);
7059
+ }
7060
+ return order.map((portalId) => {
7061
+ const entries = byPortal.get(portalId);
7062
+ const name = entries[0]?.name ?? portalId;
7063
+ return { portalId, name, document: JSON.stringify(renderDoc(snapshot, entries, closureIds, { title: name, servers: true }), null, 2) };
7064
+ });
6940
7065
  }
6941
7066
  function isOpenApiDocument(body) {
6942
7067
  try {
@@ -6958,6 +7083,39 @@ function typeRefFromSchema(schema) {
6958
7083
  if (typeof t === "string" && t !== "object") return t;
6959
7084
  return "json";
6960
7085
  }
7086
+ function authFromSecurityScheme(scheme) {
7087
+ const desc = typeof scheme.description === "string" ? { description: scheme.description } : {};
7088
+ if (scheme.type === "apiKey") {
7089
+ return {
7090
+ scheme: "apiKey",
7091
+ ...scheme.in === "header" || scheme.in === "query" || scheme.in === "cookie" ? { in: scheme.in } : {},
7092
+ ...typeof scheme.name === "string" ? { name: scheme.name } : {},
7093
+ ...desc
7094
+ };
7095
+ }
7096
+ if (scheme.type === "http") {
7097
+ if (scheme.scheme === "bearer") return { scheme: "bearer", ...typeof scheme.bearerFormat === "string" ? { bearerFormat: scheme.bearerFormat } : {}, ...desc };
7098
+ if (scheme.scheme === "basic") return { scheme: "basic", ...desc };
7099
+ }
7100
+ if (scheme.type === "oauth2") {
7101
+ const flows = scheme.flows ?? {};
7102
+ const flowKey = Object.keys(flows)[0];
7103
+ const f = flows[flowKey] ?? {};
7104
+ return {
7105
+ scheme: "oauth2",
7106
+ ...flowKey === "authorizationCode" || flowKey === "clientCredentials" || flowKey === "implicit" || flowKey === "password" ? { flow: flowKey } : {},
7107
+ ...typeof f.authorizationUrl === "string" ? { authorizationUrl: f.authorizationUrl } : {},
7108
+ ...typeof f.tokenUrl === "string" ? { tokenUrl: f.tokenUrl } : {},
7109
+ ...typeof f.refreshUrl === "string" ? { refreshUrl: f.refreshUrl } : {},
7110
+ scopes: Object.entries(f.scopes ?? {}).map(([name, description]) => ({ name, description })),
7111
+ ...desc
7112
+ };
7113
+ }
7114
+ if (scheme.type === "openIdConnect") {
7115
+ return { scheme: "openIdConnect", openIdConnectUrl: typeof scheme.openIdConnectUrl === "string" ? scheme.openIdConnectUrl : "", ...desc };
7116
+ }
7117
+ return void 0;
7118
+ }
6961
7119
  function fromOpenApi(document, projectName) {
6962
7120
  let parsed;
6963
7121
  try {
@@ -7035,6 +7193,9 @@ function fromOpenApi(document, projectName) {
7035
7193
  }))
7036
7194
  });
7037
7195
  }
7196
+ const securitySchemes = parsed.components?.securitySchemes ?? {};
7197
+ const firstScheme = Object.values(securitySchemes)[0];
7198
+ const importedAuth = firstScheme ? authFromSecurityScheme(firstScheme) : void 0;
7038
7199
  const entry = {
7039
7200
  id: `${projectName}-api`,
7040
7201
  name: typeof info.title === "string" ? info.title : projectName,
@@ -7043,7 +7204,8 @@ function fromOpenApi(document, projectName) {
7043
7204
  component: `${projectName}-api`,
7044
7205
  methods,
7045
7206
  details: typeof info.description === "string" ? info.description : `Imported OpenAPI surface of ${projectName}.`,
7046
- ...typeof info.version === "string" ? { version: info.version } : {}
7207
+ ...typeof info.version === "string" ? { version: info.version } : {},
7208
+ ...importedAuth ? { auth: importedAuth } : {}
7047
7209
  };
7048
7210
  return SurfaceSnapshotSchema.parse({
7049
7211
  projectName,
@@ -7158,6 +7320,10 @@ function projectOwnSurface(maxAudience) {
7158
7320
  component: comp.id,
7159
7321
  methods,
7160
7322
  ...comp.dispatch && comp.dispatch.length ? { dispatch: comp.dispatch } : {},
7323
+ // Project the backing Portal's auth + basePath so the codec can emit
7324
+ // OpenAPI security + per-portal servers self-contained from the snapshot.
7325
+ ...comp.auth && comp.auth.scheme !== "none" ? { auth: comp.auth } : {},
7326
+ ...comp.basePath ? { basePath: comp.basePath } : {},
7161
7327
  details: raw.details ?? "",
7162
7328
  ...raw.version ? { version: raw.version } : {},
7163
7329
  ...raw.stability ? { stability: raw.stability } : {}
@@ -7201,20 +7367,28 @@ function saveSnapshot(snapshot, rootDir = getProjectRoot()) {
7201
7367
  function loadSurfaceSnapshots() {
7202
7368
  return listSnapshots();
7203
7369
  }
7370
+ function renderOpenApiForms(snapshot) {
7371
+ const renderedSet = toOpenApiSet(snapshot);
7372
+ return { renderedSet, ...renderedSet.length === 1 ? { rendered: renderedSet[0].document } : {} };
7373
+ }
7374
+ function writeSurfaceFile(outPath, body, snapshot) {
7375
+ const resolved = path10.resolve(outPath);
7376
+ fs9.mkdirSync(path10.dirname(resolved), { recursive: true });
7377
+ if (body !== void 0) fs9.writeFileSync(resolved, body);
7378
+ else writeYamlFile(resolved, snapshot);
7379
+ return resolved;
7380
+ }
7204
7381
  function exportSurface(maxAudience, format, outPath) {
7205
7382
  const snapshot = projectOwnSurface(maxAudience);
7206
- const rendered = format === "openapi" ? toOpenApi(snapshot) : void 0;
7207
- let writtenTo;
7208
- if (outPath) {
7209
- fs9.mkdirSync(path10.dirname(path10.resolve(outPath)), { recursive: true });
7210
- if (rendered !== void 0) {
7211
- fs9.writeFileSync(path10.resolve(outPath), rendered);
7212
- } else {
7213
- writeYamlFile(path10.resolve(outPath), snapshot);
7214
- }
7215
- writtenTo = path10.resolve(outPath);
7216
- }
7217
- return { snapshot, ...rendered !== void 0 ? { rendered } : {}, ...writtenTo ? { writtenTo } : {} };
7383
+ const openapi = format === "openapi" ? renderOpenApiForms(snapshot) : void 0;
7384
+ const body = openapi?.rendered ?? openapi?.renderedSet[0]?.document;
7385
+ const writtenTo = outPath ? writeSurfaceFile(outPath, body, snapshot) : void 0;
7386
+ return {
7387
+ snapshot,
7388
+ ...openapi?.rendered !== void 0 ? { rendered: openapi.rendered } : {},
7389
+ ...openapi ? { renderedSet: openapi.renderedSet } : {},
7390
+ ...writtenTo ? { writtenTo } : {}
7391
+ };
7218
7392
  }
7219
7393
  function importSurface(sourcePath, origin) {
7220
7394
  const resolved = path10.resolve(sourcePath);
@@ -8474,13 +8648,16 @@ var init_conformance = __esm({
8474
8648
  if (isInChainedSubproject(component.subsystem, ctx)) continue;
8475
8649
  const draft = ctx.isImplementationDraft(impl);
8476
8650
  if (!impl.sourcePath) {
8477
- ctx.addIssue(
8478
- "warning",
8479
- "MISSING_SOURCE_PATH",
8480
- `Implementation "${impl.id}" declares no sourcePath \u2014 its contract "${impl.contract}" cannot be structurally checked against code.`,
8481
- impl.id,
8482
- draft
8483
- );
8651
+ const hasExternalSource = (component.externalLinks ?? []).some((l) => l.type === "implementation");
8652
+ if (!hasExternalSource) {
8653
+ ctx.addIssue(
8654
+ "warning",
8655
+ "MISSING_SOURCE_PATH",
8656
+ `Implementation "${impl.id}" declares no sourcePath \u2014 its contract "${impl.contract}" cannot be structurally checked against code.`,
8657
+ impl.id,
8658
+ draft
8659
+ );
8660
+ }
8484
8661
  continue;
8485
8662
  }
8486
8663
  const lookup = lookups.get(normalizeSourcePath(impl.sourcePath));
@@ -32103,7 +32280,7 @@ function getWebProjectCanvasModel(cfg, sessionId, projectId) {
32103
32280
  }
32104
32281
  return runWithProjectRoot(root, () => hostCore.buildCanvasDataModel());
32105
32282
  }
32106
- function getWebProjectOpenApi(cfg, sessionId, projectId) {
32283
+ function getWebProjectOpenApi(cfg, sessionId, projectId, portalId) {
32107
32284
  const principal = authenticateSession(cfg.dataDir, sessionId);
32108
32285
  if (!principal.authenticated) throw new UnauthenticatedError();
32109
32286
  if (!listProjects2(cfg, sessionId).some((r) => r.id === projectId)) {
@@ -32112,7 +32289,19 @@ function getWebProjectOpenApi(cfg, sessionId, projectId) {
32112
32289
  const root = resolveProjectRoot(cfg.dataDir, principal, projectId);
32113
32290
  if (!root) throw new ForbiddenError("project not authorized or unknown");
32114
32291
  const result = runWithProjectRoot(root, () => hostSurfaces.exportBoundSurface("project", "openapi"));
32115
- return swaggerUiPage(result.rendered ?? "{}", projectId);
32292
+ const specs = result.renderedSet ?? (result.rendered ? [{ portalId: "", name: projectId, document: result.rendered }] : []);
32293
+ if (specs.length === 0) return swaggerUiPage("{}", projectId);
32294
+ if (portalId) {
32295
+ const sel = specs.find((s) => s.portalId === portalId) ?? specs[0];
32296
+ return swaggerUiPage(sel.document, sel.name);
32297
+ }
32298
+ if (specs.length === 1) return swaggerUiPage(specs[0].document, specs[0].name);
32299
+ return openApiIndexPage(projectId, specs);
32300
+ }
32301
+ function openApiIndexPage(projectId, specs) {
32302
+ const esc2 = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
32303
+ const items = specs.map((s) => `<li><a href="/web/openapi?projectId=${encodeURIComponent(projectId)}&spec=${encodeURIComponent(s.portalId)}">${esc2(s.name)}</a> <code>${esc2(s.portalId)}</code></li>`).join("");
32304
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${esc2(projectId)} \u2014 API specs</title><style>body{font-family:system-ui,sans-serif;max-width:680px;margin:48px auto;padding:0 20px;color:#e6e6e6;background:#161616}h1{font-size:20px}a{color:#6ea8fe;text-decoration:none}a:hover{text-decoration:underline}li{margin:10px 0}code{color:#8a94a6;font-size:12px;margin-left:8px}</style></head><body><h1>${esc2(projectId)} \u2014 API specs</h1><p>This project exposes ${specs.length} separate public APIs, each with its own OpenAPI document and auth:</p><ul>${items}</ul></body></html>`;
32116
32305
  }
32117
32306
  function reshapeLandscapeGraph(model, level) {
32118
32307
  const unitNodeIds = new Set(model.nodes.filter((n) => n.nodeKind === "orgUnit").map((n) => n.id));
@@ -34474,7 +34663,8 @@ async function handleWebRequest(cfg, req, res, body, url, ctx) {
34474
34663
  }
34475
34664
  if (req.method === "GET" && parts.length === 2 && parts[1] === "openapi") {
34476
34665
  const projectId = url.searchParams.get("projectId") ?? "";
34477
- const html = getWebProjectOpenApi(cfg, sessionId, projectId);
34666
+ const spec = url.searchParams.get("spec") ?? void 0;
34667
+ const html = getWebProjectOpenApi(cfg, sessionId, projectId, spec);
34478
34668
  res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
34479
34669
  res.end(html);
34480
34670
  return;