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

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
@@ -11593,7 +11593,7 @@ function defaultTargetConfig(type) {
11593
11593
  enabled: true
11594
11594
  };
11595
11595
  }
11596
- var WAIRON_VERSION = "5.1.1-dev.69";
11596
+ var WAIRON_VERSION = "5.1.1-dev.70";
11597
11597
  var GITHUB_REPO = "SYW-Apps/Waffle-AIron";
11598
11598
  var ARCHITECT_AGENT_ID = "agent-architect";
11599
11599
  var ARCHITECT_TEMPLATE_ID = "architect";
@@ -17163,6 +17163,128 @@ var paramConformanceRule = {
17163
17163
  }
17164
17164
  };
17165
17165
 
17166
+ // src/core/rules/conformance/route-coverage.ts
17167
+ function liesUnder2(path28, prefix) {
17168
+ return path28 === prefix || path28.startsWith(prefix + "/");
17169
+ }
17170
+ function normalizeSegment(segment) {
17171
+ return /^\{.+\}$/.test(segment) || segment.startsWith(":") ? "*" : segment;
17172
+ }
17173
+ function completed(route, heads) {
17174
+ const variants = route.segments[0] === "*" && heads.length > 0 ? heads.map((head) => [head, ...route.segments.slice(1)]) : [route.segments];
17175
+ return variants.map((segments) => ({
17176
+ verb: route.verb.toUpperCase(),
17177
+ segments: segments.map(normalizeSegment),
17178
+ exactLength: route.exactLength,
17179
+ key: `${route.verb.toUpperCase()} /${segments.join("/")}${route.exactLength ? "" : "/\u2026"}`
17180
+ }));
17181
+ }
17182
+ function answers(route, endpoint) {
17183
+ if (route.verb !== endpoint.verb) return false;
17184
+ if (route.exactLength ? endpoint.segments.length !== route.segments.length : endpoint.segments.length < route.segments.length) return false;
17185
+ return route.segments.every((segment, index) => segment === endpoint.segments[index]);
17186
+ }
17187
+ var routeCoverageRule = {
17188
+ name: "route-coverage",
17189
+ description: "Code-to-contract for the ROUTES: does every route a router actually answers have a contract endpoint, and does every contract endpoint have a route that answers it? A portal's endpoints are what its contract promises; the router is what the code serves; nothing compared the two, so a route with no contract \u2014 including a write \u2014 could run for months without a single rule noticing. The listener's mount names the router function (its `via`), and the routes are read out of that function's guards; the path segment the router never checks is the one the mount's prefix guarantees. One idiom is read \u2014 a method comparison with comparisons on the path's split segments \u2014 and a mounted router that yields no route in it is reported as unread, never passed: a check that cannot see a router must say so rather than stay quiet.",
17190
+ codes: [
17191
+ {
17192
+ code: "UNDECLARED_ROUTE",
17193
+ defaultSeverity: "warning",
17194
+ summary: "A router answers a route that no contract endpoint of the portal it serves declares \u2014 a surface the code serves and the design never promised",
17195
+ // Measured code↔contract drift, route by route, at the mount's router
17196
+ // entry — which is why every one of them hands over `parts`.
17197
+ carryable: true
17198
+ },
17199
+ {
17200
+ code: "UNROUTED_ENDPOINT",
17201
+ defaultSeverity: "warning",
17202
+ summary: "A contract endpoint no route of the portal's router answers \u2014 the contract promises a route that no request can reach",
17203
+ carryable: true
17204
+ },
17205
+ {
17206
+ code: "UNREADABLE_ROUTER",
17207
+ defaultSeverity: "warning",
17208
+ summary: "A mounted portal's router yields no route this analysis can read, so its routes were not checked against the contract at all",
17209
+ carryable: true
17210
+ }
17211
+ ],
17212
+ check(ctx) {
17213
+ const code = ctx.codeIndex();
17214
+ const realization = ctx.realizationIndex();
17215
+ for (const listener of ctx.components) {
17216
+ if (listener.componentType !== "Portal") continue;
17217
+ for (const mount of listener.mounts ?? []) {
17218
+ const via = mount.via;
17219
+ if (!via) continue;
17220
+ const portal = ctx.componentMap.get(mount.portal);
17221
+ if (!portal || portal.componentType !== "Portal") continue;
17222
+ const [anchor] = realization.implementationsOf(portal.id).map((impl) => impl.id).sort();
17223
+ if (!anchor) continue;
17224
+ const holders = realization.filesOf(portal.id).map(pathKey).filter((file) => {
17225
+ const facts = code.factsAt(file);
17226
+ return !!facts && facts.status === "analyzed" && facts.analysisGrade === "exact" && !!facts.functionParams && Object.prototype.hasOwnProperty.call(facts.functionParams, via);
17227
+ });
17228
+ if (holders.length === 0) continue;
17229
+ const read = holders.flatMap((file) => {
17230
+ const routes = code.factsAt(file).functionRoutes;
17231
+ return routes && Object.prototype.hasOwnProperty.call(routes, via) ? routes[via] : [];
17232
+ });
17233
+ const draftContext = ctx.isComponentDraft(listener.id) || ctx.isComponentDraft(portal.id);
17234
+ const where = holders.map((file) => `"${file}"`).join(", ");
17235
+ if (read.length === 0) {
17236
+ ctx.addIssue(
17237
+ "warning",
17238
+ "UNREADABLE_ROUTER",
17239
+ `Listener "${listener.id}" mounts portal "${portal.id}" through "${via}" in ${where}, but no branch of "${via}" reads as a route \u2014 so none of its routes were checked against the contract at all. Only one idiom is read: an \`if\` whose conditions, together with those of every enclosing \`if\`, compare \`<request>.method\` with a string and \`parts[i]\` / \`parts.length\` with literals. Silence here would read as a clean router; it is only one this analysis cannot see. Write the router in that idiom, or carry this finding with the reason it cannot be.`,
17240
+ anchor,
17241
+ draftContext,
17242
+ void 0,
17243
+ { at: via }
17244
+ );
17245
+ continue;
17246
+ }
17247
+ const heads = [...new Set(mount.prefixes.map((prefix) => prefix.split("/").filter(Boolean)[0]).filter((head) => !!head))];
17248
+ const served = read.map((route) => completed(route, heads));
17249
+ const endpoints = ctx.interfaceMethodsOf(portal.id).flatMap((method) => {
17250
+ const endpoint = method.endpoint;
17251
+ if (endpoint?.transport !== "HTTP") return [];
17252
+ if (!mount.prefixes.some((prefix) => liesUnder2(endpoint.path, prefix))) return [];
17253
+ return [{
17254
+ verb: endpoint.method.toUpperCase(),
17255
+ segments: endpoint.path.split("/").filter(Boolean).map(normalizeSegment),
17256
+ key: `${endpoint.method.toUpperCase()} ${endpoint.path}`
17257
+ }];
17258
+ });
17259
+ const undeclared = [...new Set(served.filter((variants) => !variants.some((route) => endpoints.some((endpoint) => answers(route, endpoint)))).flatMap((variants) => variants.map((route) => route.key)))].sort();
17260
+ if (undeclared.length > 0) {
17261
+ ctx.addIssue(
17262
+ "warning",
17263
+ "UNDECLARED_ROUTE",
17264
+ `Portal "${portal.id}"'s router "${via}" (${where}), mounted by listener "${listener.id}", answers ${undeclared.length} route(s) no contract endpoint of "${portal.id}" declares \u2014 ${undeclared.map((key) => `"${key}"`).join(", ")}. A surface the code serves and the design never promised is how a write runs with no contract: no brief, no auth review, no rule reading it. Declare each as a contract method with its endpoint, or take the route out of the router.`,
17265
+ anchor,
17266
+ draftContext,
17267
+ void 0,
17268
+ { at: via, covers: undeclared }
17269
+ );
17270
+ }
17271
+ const unrouted = [...new Set(endpoints.filter((endpoint) => !served.some((variants) => variants.some((route) => answers(route, endpoint)))).map((endpoint) => endpoint.key))].sort();
17272
+ if (unrouted.length > 0) {
17273
+ ctx.addIssue(
17274
+ "warning",
17275
+ "UNROUTED_ENDPOINT",
17276
+ `Portal "${portal.id}" binds ${unrouted.length} HTTP endpoint(s) under listener "${listener.id}"'s mount that no route of its router "${via}" (${where}) answers \u2014 ${unrouted.map((key) => `"${key}"`).join(", ")}. The contract promises a route no request can reach. Route it in the router, or drop the endpoint from the contract.`,
17277
+ anchor,
17278
+ draftContext,
17279
+ void 0,
17280
+ { at: via, covers: unrouted }
17281
+ );
17282
+ }
17283
+ }
17284
+ }
17285
+ }
17286
+ };
17287
+
17166
17288
  // src/core/rules/conformance/unclaimed-source.ts
17167
17289
  var unclaimedSourceRule = {
17168
17290
  name: "unclaimed-source",
@@ -17464,14 +17586,21 @@ function isPureForwarder(methods) {
17464
17586
  const narrated = methods.filter((m) => (m.narrative ?? []).length > 0);
17465
17587
  return narrated.length > 0 && narrated.every((m) => isForwardingMethod(m.narrative ?? []));
17466
17588
  }
17467
- function pureForwarderComponents(ctx) {
17589
+ function reachesCollaborator(narrative) {
17590
+ return narrative.some((step) => step.type === "call" || step.type === "dispatch" || step.type === "register");
17591
+ }
17592
+ function isRoutingTable(methods) {
17593
+ const reaching = methods.map((m) => m.narrative ?? []).filter(reachesCollaborator);
17594
+ return reaching.length > 0 && reaching.every(isForwardingMethod);
17595
+ }
17596
+ function routingTableComponents(ctx) {
17468
17597
  const realized = /* @__PURE__ */ new Map();
17469
17598
  for (const impl of ctx.implementations) {
17470
17599
  const intf = ctx.interfaceMap.get(impl.contract);
17471
17600
  if (!intf) continue;
17472
17601
  realized.set(intf.component, [...realized.get(intf.component) ?? [], ...impl.methods]);
17473
17602
  }
17474
- return new Set([...realized].filter(([, methods]) => isPureForwarder(methods)).map(([id]) => id));
17603
+ return new Set([...realized].filter(([, methods]) => isRoutingTable(methods)).map(([id]) => id));
17475
17604
  }
17476
17605
  var methodCohesionRule = {
17477
17606
  name: "method-cohesion",
@@ -17524,7 +17653,7 @@ var couplingRule = {
17524
17653
  { code: "MUTUAL_SUBSYSTEM_DEPENDENCY", defaultSeverity: "warning", summary: "Subsystems depend on each other without a declared trusted link" },
17525
17654
  { code: "INVALID_TRUSTED_LINK", defaultSeverity: "error", summary: "trustedLinks references a non-existent subsystem" },
17526
17655
  { code: "UNUSED_TRUSTED_LINK", defaultSeverity: "warning", summary: "trustedLinks declares a peer no dependency actually reaches" },
17527
- { code: "GOD_COMPONENT", defaultSeverity: "warning", summary: "Component with excessive dependency fan-out; not reported on a pure forwarder, whose every narrated method is a single hand-off" }
17656
+ { code: "GOD_COMPONENT", defaultSeverity: "warning", summary: "Component with excessive dependency fan-out; not reported on a routing table, which reaches every collaborator it has only through single hand-offs" }
17528
17657
  ],
17529
17658
  check(ctx) {
17530
17659
  const subsystemDeps = /* @__PURE__ */ new Map();
@@ -17596,10 +17725,10 @@ var couplingRule = {
17596
17725
  );
17597
17726
  }
17598
17727
  }
17599
- const forwarders = pureForwarderComponents(ctx);
17728
+ const routingTables = routingTableComponents(ctx);
17600
17729
  for (const comp of ctx.components) {
17601
17730
  const threshold = ctx.complexityConfigFor(comp.subsystem)?.maxComponentDependencies ?? DEFAULT_GOD_COMPONENT_THRESHOLD;
17602
- if (comp.dependsOn.length > threshold && !forwarders.has(comp.id)) {
17731
+ if (comp.dependsOn.length > threshold && !routingTables.has(comp.id)) {
17603
17732
  ctx.addIssue(
17604
17733
  "warning",
17605
17734
  "GOD_COMPONENT",
@@ -18191,7 +18320,7 @@ var complexityRule = {
18191
18320
  { code: "DESCRIPTION_TOO_SHORT", defaultSeverity: "warning", summary: "Description is shorter than the configured minimum length" },
18192
18321
  { code: "EXCESSIVE_METHODS", defaultSeverity: "warning", summary: "Interface declares more methods than the configured limit" },
18193
18322
  { code: "EXCESSIVE_METHOD_PARAMS", defaultSeverity: "warning", summary: "Interface method declares more parameters than the configured limit" },
18194
- { code: "EXCESSIVE_DEPENDENCIES", defaultSeverity: "warning", summary: "Component has more dependencies than the configured limit; not reported on a pure forwarder, whose fan-out is a routing table rather than knowledge" },
18323
+ { code: "EXCESSIVE_DEPENDENCIES", defaultSeverity: "warning", summary: "Component has more dependencies than the configured limit; not reported on a routing table, which reaches every collaborator it has only through single hand-offs" },
18195
18324
  { code: "EXCESSIVE_SUBSYSTEM_COMPONENTS", defaultSeverity: "warning", summary: "Subsystem has more direct components than the configured limit" }
18196
18325
  ],
18197
18326
  check(ctx) {
@@ -18211,7 +18340,7 @@ var complexityRule = {
18211
18340
  );
18212
18341
  }
18213
18342
  }
18214
- const forwarders = pureForwarderComponents(ctx);
18343
+ const routingTables = routingTableComponents(ctx);
18215
18344
  for (const comp of ctx.components) {
18216
18345
  const docConfig = ctx.documentationConfigFor(comp.subsystem);
18217
18346
  const complexityConfig = ctx.complexityConfigFor(comp.subsystem);
@@ -18225,7 +18354,7 @@ var complexityRule = {
18225
18354
  "Component",
18226
18355
  isDraft
18227
18356
  );
18228
- if (complexityConfig?.maxComponentDependencies !== void 0 && comp.dependsOn.length > complexityConfig.maxComponentDependencies && !forwarders.has(comp.id)) {
18357
+ if (complexityConfig?.maxComponentDependencies !== void 0 && comp.dependsOn.length > complexityConfig.maxComponentDependencies && !routingTables.has(comp.id)) {
18229
18358
  ctx.addIssue(
18230
18359
  "warning",
18231
18360
  "EXCESSIVE_DEPENDENCIES",
@@ -18675,6 +18804,7 @@ var SDD_RULES = [
18675
18804
  typeRealizationRule,
18676
18805
  typeShapeRule,
18677
18806
  paramConformanceRule,
18807
+ routeCoverageRule,
18678
18808
  unclaimedSourceRule,
18679
18809
  exportConformanceRule,
18680
18810
  couplingRule,
@@ -18993,6 +19123,7 @@ function walkExact(ts, sourceText, fileName) {
18993
19123
  const localTypes = /* @__PURE__ */ new Map();
18994
19124
  const typeShapes = /* @__PURE__ */ new Map();
18995
19125
  const functionParams = /* @__PURE__ */ new Map();
19126
+ const functionRoutes = /* @__PURE__ */ new Map();
18996
19127
  const mutableBindings = /* @__PURE__ */ new Set();
18997
19128
  const schemaConstants = /* @__PURE__ */ new Map();
18998
19129
  const derivedAliases = [];
@@ -19164,6 +19295,67 @@ function walkExact(ts, sourceText, fileName) {
19164
19295
  if (fn.body) count(fn.body);
19165
19296
  return { score, callees };
19166
19297
  };
19298
+ const conjunctsOf = (expression, out = []) => {
19299
+ if (ts.isParenthesizedExpression(expression)) return conjunctsOf(expression.expression, out);
19300
+ if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
19301
+ conjunctsOf(expression.left, out);
19302
+ conjunctsOf(expression.right, out);
19303
+ return out;
19304
+ }
19305
+ out.push(expression);
19306
+ return out;
19307
+ };
19308
+ const isPartsRef = (expression) => ts.isIdentifier(expression) && expression.text === "parts" || ts.isPropertyAccessExpression(expression) && expression.name.text === "parts";
19309
+ const constraintOf = (conjunct) => {
19310
+ if (!ts.isBinaryExpression(conjunct) || conjunct.operatorToken.kind !== ts.SyntaxKind.EqualsEqualsEqualsToken) {
19311
+ return void 0;
19312
+ }
19313
+ const { left, right } = conjunct;
19314
+ if (ts.isPropertyAccessExpression(left) && left.name.text === "method" && ts.isStringLiteral(right)) {
19315
+ return { verb: right.text };
19316
+ }
19317
+ if (ts.isPropertyAccessExpression(left) && left.name.text === "length" && isPartsRef(left.expression) && ts.isNumericLiteral(right)) {
19318
+ return { length: Number(right.text) };
19319
+ }
19320
+ if (ts.isElementAccessExpression(left) && isPartsRef(left.expression) && ts.isNumericLiteral(left.argumentExpression) && ts.isStringLiteral(right)) {
19321
+ return { index: Number(left.argumentExpression.text), literal: right.text };
19322
+ }
19323
+ return void 0;
19324
+ };
19325
+ const routeOf = (conjuncts) => {
19326
+ let verb;
19327
+ let length;
19328
+ const fixed = /* @__PURE__ */ new Map();
19329
+ for (const conjunct of conjuncts) {
19330
+ const constraint = constraintOf(conjunct);
19331
+ if (!constraint) continue;
19332
+ if ("verb" in constraint) verb = constraint.verb;
19333
+ else if ("length" in constraint) length = constraint.length;
19334
+ else fixed.set(constraint.index, constraint.literal);
19335
+ }
19336
+ if (verb === void 0) return void 0;
19337
+ const count = length ?? (fixed.size > 0 ? Math.max(...fixed.keys()) + 1 : 0);
19338
+ const segments = [];
19339
+ for (let index = 0; index < count; index++) segments.push(fixed.get(index) ?? "*");
19340
+ return { verb, segments, exactLength: length !== void 0 };
19341
+ };
19342
+ const collectRoutes = (fn) => {
19343
+ const routes = /* @__PURE__ */ new Map();
19344
+ const walk2 = (node, enclosing) => {
19345
+ if (namedFunctionName(node) !== void 0) return;
19346
+ if (ts.isIfStatement(node)) {
19347
+ const guarded = [...enclosing, ...conjunctsOf(node.expression)];
19348
+ const route = routeOf(guarded);
19349
+ if (route) routes.set(`${route.verb} /${route.segments.join("/")}${route.exactLength ? "" : "/\u2026"}`, route);
19350
+ walk2(node.thenStatement, guarded);
19351
+ if (node.elseStatement) walk2(node.elseStatement, enclosing);
19352
+ return;
19353
+ }
19354
+ ts.forEachChild(node, (child) => walk2(child, enclosing));
19355
+ };
19356
+ if (fn.body) walk2(fn.body, []);
19357
+ return routes;
19358
+ };
19167
19359
  const visit = (node) => {
19168
19360
  const fnName = namedFunctionName(node);
19169
19361
  if (fnName && node.body) {
@@ -19175,6 +19367,12 @@ function walkExact(ts, sourceText, fileName) {
19175
19367
  const signatures = functionParams.get(fnName) ?? [];
19176
19368
  signatures.push(declaredParameters(node));
19177
19369
  functionParams.set(fnName, signatures);
19370
+ const routes = collectRoutes(node);
19371
+ if (routes.size > 0) {
19372
+ const known = functionRoutes.get(fnName) ?? /* @__PURE__ */ new Map();
19373
+ for (const [key, route] of routes) known.set(key, route);
19374
+ functionRoutes.set(fnName, known);
19375
+ }
19178
19376
  }
19179
19377
  if (ts.isPropertyDeclaration(node)) recordFieldType(node.name, node.type);
19180
19378
  else if (ts.isParameter(node) && node.parent && ts.isConstructorDeclaration(node.parent) && isParameterProperty(node)) {
@@ -19282,7 +19480,7 @@ function walkExact(ts, sourceText, fileName) {
19282
19480
  typeShapes.set(alias.name, { origin: "derived", fields: schemaMembers(literal), methods: [] });
19283
19481
  }
19284
19482
  const reexportOnly = sf.statements.length > 0 && sf.statements.every((st) => ts.isExportDeclaration(st) && !!st.moduleSpecifier);
19285
- return { declared, anchors, exported, imports, reexports, starExports, namedReexports, complexity, calls, importBindings, typeOnlyBindings, fieldTypes, localTypes, typeShapes, functionParams, mutableBindings, reexportOnly };
19483
+ return { declared, anchors, exported, imports, reexports, starExports, namedReexports, complexity, calls, importBindings, typeOnlyBindings, fieldTypes, localTypes, typeShapes, functionParams, functionRoutes, mutableBindings, reexportOnly };
19286
19484
  }
19287
19485
  function resolveRelativeModule(fromFile, specifier) {
19288
19486
  if (!specifier.startsWith(".")) return null;
@@ -19466,6 +19664,7 @@ function buildCodeModel(implementations, types, projectRoot, sourceRoots = [], e
19466
19664
  localTypes: Object.fromEntries([...facts.localTypes].map(([k, v]) => [k, [...v]])),
19467
19665
  typeShapes: Object.fromEntries(facts.typeShapes),
19468
19666
  functionParams: Object.fromEntries(facts.functionParams),
19667
+ functionRoutes: Object.fromEntries([...facts.functionRoutes].map(([k, v]) => [k, [...v.values()]])),
19469
19668
  topLevelMutableBindings: [...facts.mutableBindings],
19470
19669
  reexportOnly: facts.reexportOnly
19471
19670
  };