@wairon/cli 5.1.1-dev.68 → 5.1.1-dev.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3458,6 +3458,7 @@ __export(src_exports, {
3458
3458
  PatternRefSchema: () => PatternRefSchema,
3459
3459
  PortalAuthSchema: () => PortalAuthSchema,
3460
3460
  PortalAuthSchemeSchema: () => PortalAuthSchemeSchema,
3461
+ PortalMountSchema: () => PortalMountSchema,
3461
3462
  PortalTypeSchema: () => PortalTypeSchema,
3462
3463
  ProfileSelectionSubjectSchema: () => ProfileSelectionSubjectSchema,
3463
3464
  ProjectConfigSchema: () => ProjectConfigSchema,
@@ -5369,6 +5370,24 @@ var PortalAuthSchema = import_zod8.z.object({
5369
5370
  description: import_zod8.z.string().optional(),
5370
5371
  example: import_zod8.z.string().optional()
5371
5372
  });
5373
+ var PortalMountSchema = import_zod8.z.object({
5374
+ /** The mounted Portal's component id. */
5375
+ portal: import_zod8.z.string(),
5376
+ /**
5377
+ * The path prefixes this listener routes to the portal. A path lies under a
5378
+ * prefix when it EQUALS it or continues it past a slash — so `/` covers the
5379
+ * root itself and nothing else, which is what lets an app shell sit at `GET /`
5380
+ * without swallowing every other route.
5381
+ */
5382
+ prefixes: import_zod8.z.array(import_zod8.z.string()),
5383
+ /**
5384
+ * The router entry the listener calls to hand the portal its request,
5385
+ * exported by the PORTAL's own file (so export-conformance holds that file to
5386
+ * it). Absent when the listener calls the portal's contract methods directly,
5387
+ * route by route — then there is no entry to name.
5388
+ */
5389
+ via: import_zod8.z.string().optional()
5390
+ });
5372
5391
  var ComponentSpecSchema = import_zod8.z.object({
5373
5392
  id: SpecIdSchema,
5374
5393
  name: import_zod8.z.string(),
@@ -5388,6 +5407,13 @@ var ComponentSpecSchema = import_zod8.z.object({
5388
5407
  auth: PortalAuthSchema.optional(),
5389
5408
  /** Portal-only: capability → component.method dispatch table (see DispatchBindingSchema). */
5390
5409
  dispatch: import_zod8.z.array(DispatchBindingSchema).optional(),
5410
+ /**
5411
+ * Portal-only: the portals this LISTENER serves (see PortalMountSchema).
5412
+ * Declaring the field — even as an empty array — is what marks a portal as a
5413
+ * listener: the one kind of portal the host starts directly, so nothing else
5414
+ * needs to mount it. Absent means "a portal something else must serve".
5415
+ */
5416
+ mounts: import_zod8.z.array(PortalMountSchema).optional(),
5391
5417
  /** Store-only: whether held state survives restart (see DurabilitySchema). */
5392
5418
  durability: DurabilitySchema.optional(),
5393
5419
  /** Orchestrator-only: what the logic may depend on; unset means a workflow (see DependencyClassSchema). */
@@ -11567,7 +11593,7 @@ function defaultTargetConfig(type) {
11567
11593
  enabled: true
11568
11594
  };
11569
11595
  }
11570
- var WAIRON_VERSION = "5.1.1-dev.68";
11596
+ var WAIRON_VERSION = "5.1.1-dev.69";
11571
11597
  var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
11572
11598
  var ARCHITECT_AGENT_ID = "agent-architect";
11573
11599
  var ARCHITECT_TEMPLATE_ID = "architect";
@@ -15331,6 +15357,84 @@ var lifecycleRule = {
15331
15357
  }
15332
15358
  };
15333
15359
 
15360
+ // src/core/rules/wiring/portal-mounts.ts
15361
+ function liesUnder(path28, prefix) {
15362
+ return path28 === prefix || path28.startsWith(prefix + "/");
15363
+ }
15364
+ var portalMountsRule = {
15365
+ name: "portal-mounts",
15366
+ description: "Which listener serves which portal: a portal's routes are its methods' endpoint bindings, but nothing said which listener hands it its requests, so a portal no listener serves was unreachable without anyone noticing, and the call that mounts one crossed a boundary no contract described. A listener Portal declares its mounts \u2014 each a portal, the path prefixes routed to it, and optionally the router entry it calls \u2014 and this rule checks them against the tree: a mount must name a Portal, every HTTP endpoint of a mounted portal must lie under one of its prefixes, and every portal with HTTP endpoints must be mounted by some listener unless it is one. A path lies under a prefix when it equals it or continues it past a slash, so `/` covers only the root. Whether the named entry is really exported is the code's question, and export-conformance asks it.",
15367
+ codes: [
15368
+ {
15369
+ code: "MOUNT_TARGET_NOT_PORTAL",
15370
+ defaultSeverity: "error",
15371
+ summary: "A listener mounts something that is not a Portal \u2014 an unknown component, or one whose type cannot serve routes"
15372
+ },
15373
+ {
15374
+ code: "ENDPOINT_OUTSIDE_MOUNT",
15375
+ defaultSeverity: "warning",
15376
+ summary: "A mounted portal declares an HTTP endpoint under none of the prefixes its listener routes to it \u2014 a route the contract promises that no request can reach"
15377
+ },
15378
+ {
15379
+ code: "UNMOUNTED_PORTAL",
15380
+ defaultSeverity: "warning",
15381
+ summary: "A portal declares HTTP endpoints but no listener mounts it and it declares no mounts of its own \u2014 a whole surface nothing serves"
15382
+ }
15383
+ ],
15384
+ check(ctx) {
15385
+ const httpRoutesOf = (compId) => {
15386
+ const routes = [];
15387
+ for (const method of ctx.interfaceMethodsOf(compId)) {
15388
+ const ep = method.endpoint;
15389
+ if (ep?.transport === "HTTP") routes.push({ method: method.name, verb: ep.method, path: ep.path });
15390
+ }
15391
+ return routes;
15392
+ };
15393
+ const mounted = /* @__PURE__ */ new Set();
15394
+ for (const listener of ctx.components) {
15395
+ if (listener.componentType !== "Portal" || !listener.mounts) continue;
15396
+ const listenerDraft = ctx.isComponentDraft(listener.id);
15397
+ for (const mount of listener.mounts) {
15398
+ const target = ctx.componentMap.get(mount.portal);
15399
+ if (!target || target.componentType !== "Portal") {
15400
+ ctx.addIssue(
15401
+ "error",
15402
+ "MOUNT_TARGET_NOT_PORTAL",
15403
+ target ? `Listener "${listener.id}" mounts "${mount.portal}" under ${mount.prefixes.map((p) => `"${p}"`).join(", ") || "no prefixes"}, but "${target.id}" is a ${target.componentType} \u2014 only a Portal serves routes, so this mount routes requests to nothing.` : `Listener "${listener.id}" mounts "${mount.portal}" under ${mount.prefixes.map((p) => `"${p}"`).join(", ") || "no prefixes"}, but no component "${mount.portal}" exists \u2014 this mount routes requests to nothing.`,
15404
+ listener.id,
15405
+ listenerDraft || (target ? ctx.isComponentDraft(target.id) : false)
15406
+ );
15407
+ continue;
15408
+ }
15409
+ mounted.add(target.id);
15410
+ for (const route of httpRoutesOf(target.id)) {
15411
+ if (mount.prefixes.some((prefix) => liesUnder(route.path, prefix))) continue;
15412
+ ctx.addIssue(
15413
+ "warning",
15414
+ "ENDPOINT_OUTSIDE_MOUNT",
15415
+ `Portal "${target.id}" binds "${route.method}" to ${route.verb} ${route.path}, but listener "${listener.id}" routes it only ${mount.prefixes.map((p) => `"${p}"`).join(", ") || "no prefixes at all"} \u2014 a route the contract promises that no request through this listener can reach. A path lies under a prefix when it equals it or continues it past a slash. Add the prefix to the mount, or move the endpoint under one it already routes.`,
15416
+ listener.id,
15417
+ listenerDraft || ctx.isComponentDraft(target.id)
15418
+ );
15419
+ }
15420
+ }
15421
+ }
15422
+ for (const portal of ctx.components) {
15423
+ if (portal.componentType !== "Portal") continue;
15424
+ if (portal.mounts !== void 0 || mounted.has(portal.id)) continue;
15425
+ const routes = httpRoutesOf(portal.id);
15426
+ if (routes.length === 0) continue;
15427
+ ctx.addIssue(
15428
+ "warning",
15429
+ "UNMOUNTED_PORTAL",
15430
+ `Portal "${portal.id}" declares ${routes.length} HTTP endpoint(s), but no listener mounts it and it declares no mounts of its own \u2014 a whole surface nothing serves. Mount it on the listener that routes its requests, or, if it IS a listener the host starts directly, declare its mounts (an empty list says it serves only its own routes).`,
15431
+ portal.id,
15432
+ ctx.isComponentDraft(portal.id)
15433
+ );
15434
+ }
15435
+ }
15436
+ };
15437
+
15334
15438
  // src/core/rules/narrative-graph-projector.ts
15335
15439
  var methodKey = (compId, methodName) => `${compId}#${methodName}`;
15336
15440
  function stepEdges(step, ctx, followRegisterEdges) {
@@ -17109,7 +17213,7 @@ var unclaimedSourceRule = {
17109
17213
  var NOTHING_PROMISED = /* @__PURE__ */ new Set();
17110
17214
  var exportConformanceRule = {
17111
17215
  name: "export-conformance",
17112
- description: "Code-to-contract for the SURFACE: does this file publish anything the components it realizes never promised, that another component then takes? The rest of the conformance set reads contract-to-code \u2014 it asks whether the code holds what a spec claims \u2014 so a file could export whatever it liked under a component's name and nothing looked. That is one half of the same hole as a contract promising a parameter it never passes. A method's `exportedVia` names the export a consumer imports to REACH it \u2014 the value that composes it, which `symbol` cannot name because `symbol` names the function inside \u2014 and a declared handle the file does not actually export is itself a finding, so naming one can never become a free-text suppression. Both questions are asked only of a file read at exact grade, and a taker counts only when its own import specifier resolves to the file it is accused of taking from: below that nothing separates an export from a mention, or a shared word from a shared module.",
17216
+ description: "Code-to-contract for the SURFACE: does this file publish anything the components it realizes never promised, that another component then takes? The rest of the conformance set reads contract-to-code \u2014 it asks whether the code holds what a spec claims \u2014 so a file could export whatever it liked under a component's name and nothing looked. That is one half of the same hole as a contract promising a parameter it never passes. A method's `exportedVia` names the export a consumer imports to REACH it \u2014 the value that composes it, which `symbol` cannot name because `symbol` names the function inside \u2014 and a listener's mount names the router entry it calls to hand a portal its requests; both are declared publication, and a declared handle the file does not actually export is itself a finding, so naming one can never become a free-text suppression. Both questions are asked only of a file read at exact grade, and a taker counts only when its own import specifier resolves to the file it is accused of taking from: below that nothing separates an export from a mention, or a shared word from a shared module.",
17113
17217
  codes: [
17114
17218
  {
17115
17219
  code: "UNDECLARED_EXPORT",
@@ -17122,7 +17226,7 @@ var exportConformanceRule = {
17122
17226
  {
17123
17227
  code: "UNREALIZED_EXPORT_HANDLE",
17124
17228
  defaultSeverity: "warning",
17125
- summary: "An implementation method declares an `exportedVia` handle its own source file does not export \u2014 the spec names a published route to the method that nobody can import"
17229
+ summary: "A declared publication handle \u2014 a method's `exportedVia`, or the router entry a listener mounts a portal through \u2014 names an export its own source file does not have: the spec names a route to the code that nobody can import"
17126
17230
  // Deliberately NOT carryable, and so it hands over no `parts`: a wrong
17127
17231
  // handle is one string to correct, and a parking space for it could
17128
17232
  // only ever be a parking space for a lie.
@@ -17168,6 +17272,30 @@ var exportConformanceRule = {
17168
17272
  draftContext
17169
17273
  );
17170
17274
  }
17275
+ for (const listener of ctx.components) {
17276
+ if (listener.componentType !== "Portal") continue;
17277
+ for (const mount of listener.mounts ?? []) {
17278
+ const handle = mount.via;
17279
+ if (!handle) continue;
17280
+ const portal = ctx.componentMap.get(mount.portal);
17281
+ if (!portal) continue;
17282
+ const files = realization.filesOf(portal.id).map(pathKey);
17283
+ const exact = files.filter((file) => {
17284
+ const facts = code.factsAt(file);
17285
+ return !!facts && facts.status === "analyzed" && facts.analysisGrade === "exact";
17286
+ });
17287
+ const exporters = exact.filter((file) => code.factsAt(file).exportedNames.includes(handle));
17288
+ for (const file of exporters) promise(file, handle);
17289
+ if (exporters.length > 0 || exact.length === 0 || exact.length < files.length) continue;
17290
+ ctx.addIssue(
17291
+ "warning",
17292
+ "UNREALIZED_EXPORT_HANDLE",
17293
+ `Listener "${listener.id}" mounts portal "${portal.id}" through "${handle}", but ${files.map((f) => `"${f}"`).join(", ")} \u2014 the file(s) realizing "${portal.id}" \u2014 export no such name. A mount's \`via\` names the router entry the listener calls to hand the portal its requests, published by the portal's own files, so one they do not publish promises a route nobody can import. It allows nothing either: every export of those files is still read against the contracts. Name the entry the portal's file actually exports, export it, or drop \`via\` if the listener calls the portal's contract methods directly.`,
17294
+ listener.id,
17295
+ ctx.isComponentDraft(listener.id) || ctx.isComponentDraft(portal.id)
17296
+ );
17297
+ }
17298
+ }
17171
17299
  for (const type of ctx.types) {
17172
17300
  if (!type.sourcePath) continue;
17173
17301
  promise(type.sourcePath, type.symbol ?? type.name);
@@ -18486,6 +18614,9 @@ var SDD_RULES = [
18486
18614
  dispatchTableBindingsRule,
18487
18615
  dispatchStepRoutingRule,
18488
18616
  lifecycleRule,
18617
+ // Which listener serves which portal: another declared edge into a
18618
+ // component, so its validity reads beside dispatch and lifecycle.
18619
+ portalMountsRule,
18489
18620
  // The declared entrypoint's own prose before the walk that its declaration
18490
18621
  // silences, then the walk, then the types no walk can reach.
18491
18622
  invokedByDescriptionRule,
@@ -21126,6 +21257,10 @@ function stripNamespaceFromComponent(spec, prefix) {
21126
21257
  dispatch: spec.dispatch?.map((b) => ({
21127
21258
  ...b,
21128
21259
  component: relativizeId(b.component, prefix)
21260
+ })),
21261
+ mounts: spec.mounts?.map((m) => ({
21262
+ ...m,
21263
+ portal: relativizeId(m.portal, prefix)
21129
21264
  }))
21130
21265
  };
21131
21266
  }
@@ -21276,6 +21411,7 @@ function rewriteSpecRefs(raw, remap) {
21276
21411
  rewriteEach(raw.dependsOn, "component");
21277
21412
  rewriteEach(raw.owns, "component");
21278
21413
  for (const binding of entries(raw.dispatch)) rewritePair(binding, "component", "method");
21414
+ for (const mount of entries(raw.mounts)) rewrite(mount, "portal", "component");
21279
21415
  } else if (kind === "subsystem") {
21280
21416
  for (const entrypoint of entries(raw.lifecycle)) rewritePair(entrypoint, "component", "method");
21281
21417
  rewritePublished(raw.publicInterfaces);
@@ -21330,6 +21466,9 @@ function identityKeyOf(field, item) {
21330
21466
  switch (field) {
21331
21467
  case "dispatch":
21332
21468
  return str(o.capability);
21469
+ // A listener mounts each portal once, so the portal IS the mount's identity.
21470
+ case "mounts":
21471
+ return str(o.portal);
21333
21472
  case "lifecycle":
21334
21473
  return `${String(o.phase)} ${String(o.component)} ${String(o.method)}`;
21335
21474
  case "emits":
@@ -21720,6 +21859,12 @@ var SpecWorkspace = class {
21720
21859
  dispatch: comp.dispatch?.map((b) => ({
21721
21860
  ...b,
21722
21861
  component: qualifyId(b.component, namespacePrefix, this.rootSubsystems)
21862
+ })),
21863
+ // A listener's mount names a portal id, qualified like any other
21864
+ // component reference so a chained listener still finds its portals.
21865
+ mounts: comp.mounts?.map((m) => ({
21866
+ ...m,
21867
+ portal: qualifyId(m.portal, namespacePrefix, this.rootSubsystems)
21723
21868
  }))
21724
21869
  }));
21725
21870
  index.interfaces = index.interfaces.map((intf) => ({
@@ -23264,6 +23409,9 @@ var SpecWorkspace = class {
23264
23409
  if (Array.isArray(out.dispatch)) {
23265
23410
  out.dispatch = out.dispatch.map((b) => typeof b?.component === "string" ? { ...b, component: q(b.component) } : b);
23266
23411
  }
23412
+ if (Array.isArray(out.mounts)) {
23413
+ out.mounts = out.mounts.map((m) => typeof m?.portal === "string" ? { ...m, portal: q(m.portal) } : m);
23414
+ }
23267
23415
  break;
23268
23416
  case "interface":
23269
23417
  if (typeof out.component === "string") out.component = q(out.component);
@@ -26644,6 +26792,7 @@ function loadProjectConfig4() {
26644
26792
  PatternRefSchema,
26645
26793
  PortalAuthSchema,
26646
26794
  PortalAuthSchemeSchema,
26795
+ PortalMountSchema,
26647
26796
  PortalTypeSchema,
26648
26797
  ProfileSelectionSubjectSchema,
26649
26798
  ProjectConfigSchema,