ai-spec-dev 0.55.0 → 0.56.0

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.
@@ -694,7 +694,7 @@ var require_package = __commonJS({
694
694
  "package.json"(exports, module) {
695
695
  module.exports = {
696
696
  name: "ai-spec-dev",
697
- version: "0.55.0",
697
+ version: "0.56.0",
698
698
  description: "AI-driven Development Orchestrator SDK & CLI",
699
699
  main: "dist/index.js",
700
700
  types: "dist/index.d.ts",
@@ -10300,10 +10300,12 @@ async function walkSource(root) {
10300
10300
  function extractApiCallsFromSource(source, relFile) {
10301
10301
  const calls = [];
10302
10302
  const lines = source.split("\n");
10303
- const methodCallRegex = /\.(get|post|put|patch|delete)\s*\(\s*(['"`])([^'"`]+)\2/gi;
10303
+ const methodCallRegex = /\.(get|post|put|patch|delete)\s*\(\s*(['"`])([^'"`]+)\2(?!\s*\+)/gi;
10304
10304
  const fetchRegex = /\bfetch\s*\(\s*(['"`])([^'"`]+)\1([^)]*)\)/g;
10305
10305
  const useRequestRegex = /\buseRequest\s*\(\s*(['"`])([^'"`]+)\1([^)]*)\)/g;
10306
10306
  const genericRequestRegex = /\brequest\s*\(\s*(['"`])([^'"`]+)\1\s*(?:,\s*(['"`])(GET|POST|PUT|PATCH|DELETE)\3)?/gi;
10307
+ const concatMethodRegex = /\.(get|post|put|patch|delete)\s*\(\s*(['"`])([^'"`]+)\2\s*\+/gi;
10308
+ const concatFetchRegex = /\bfetch\s*\(\s*(['"`])([^'"`]+)\1\s*\+([^)]*)\)/g;
10307
10309
  function getLineNumber(offset) {
10308
10310
  let ln = 1;
10309
10311
  for (let i = 0; i < offset && i < source.length; i++) {
@@ -10320,6 +10322,10 @@ function extractApiCallsFromSource(source, relFile) {
10320
10322
  if (/\.(css|svg|png|jpe?g|gif|ico|woff2?|ttf|eot)$/i.test(p)) return false;
10321
10323
  return true;
10322
10324
  }
10325
+ function concatPath(prefix) {
10326
+ const stripped = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
10327
+ return stripped + "/*";
10328
+ }
10323
10329
  let match;
10324
10330
  while ((match = methodCallRegex.exec(source)) !== null) {
10325
10331
  const rawPath = match[3];
@@ -10373,12 +10379,41 @@ function extractApiCallsFromSource(source, relFile) {
10373
10379
  snippet: getSnippet(line)
10374
10380
  });
10375
10381
  }
10382
+ while ((match = concatMethodRegex.exec(source)) !== null) {
10383
+ const rawPrefix = match[3];
10384
+ if (!isApiLike(rawPrefix)) continue;
10385
+ const line = getLineNumber(match.index);
10386
+ calls.push({
10387
+ method: match[1].toUpperCase(),
10388
+ path: concatPath(rawPrefix),
10389
+ file: relFile,
10390
+ line,
10391
+ snippet: getSnippet(line),
10392
+ isConcatPath: true
10393
+ });
10394
+ }
10395
+ while ((match = concatFetchRegex.exec(source)) !== null) {
10396
+ const rawPrefix = match[2];
10397
+ if (!isApiLike(rawPrefix)) continue;
10398
+ const tail = match[3] ?? "";
10399
+ const methodMatch = tail.match(/method\s*:\s*['"`](GET|POST|PUT|PATCH|DELETE)['"`]/i);
10400
+ const line = getLineNumber(match.index);
10401
+ calls.push({
10402
+ method: methodMatch ? methodMatch[1].toUpperCase() : "GET",
10403
+ path: concatPath(rawPrefix),
10404
+ file: relFile,
10405
+ line,
10406
+ snippet: getSnippet(line),
10407
+ isConcatPath: true
10408
+ });
10409
+ }
10376
10410
  return calls;
10377
10411
  }
10378
10412
  function normalizePathSegments(p) {
10379
10413
  const withoutQs = p.split("?")[0];
10380
10414
  const segments = withoutQs.split("/").filter(Boolean);
10381
10415
  return segments.map((seg) => {
10416
+ if (seg === "*") return "*";
10382
10417
  if (seg.startsWith(":")) return "*";
10383
10418
  if (seg.includes("${") || seg.includes("{{")) return "*";
10384
10419
  if (/^\d+$/.test(seg)) return "*";
@@ -10430,8 +10465,10 @@ async function verifyCrossStackContract(backendDsl, frontendRoot, opts = {}) {
10430
10465
  const phantom = [];
10431
10466
  const methodMismatch = [];
10432
10467
  const matched = [];
10468
+ const unknownMethodCalls = [];
10433
10469
  const usedEndpointIds = /* @__PURE__ */ new Set();
10434
10470
  for (const call of allCalls) {
10471
+ if (call.method === "UNKNOWN") unknownMethodCalls.push(call);
10435
10472
  const pathMatches = backendEndpoints.filter((ep) => pathsMatch(ep.path, call.path));
10436
10473
  if (pathMatches.length === 0) {
10437
10474
  phantom.push(call);
@@ -10456,7 +10493,9 @@ async function verifyCrossStackContract(backendDsl, frontendRoot, opts = {}) {
10456
10493
  unused,
10457
10494
  methodMismatch,
10458
10495
  matched,
10459
- totalScannedFiles: files.length
10496
+ unknownMethodCalls,
10497
+ totalScannedFiles: files.length,
10498
+ hasViolations: phantom.length > 0 || methodMismatch.length > 0
10460
10499
  };
10461
10500
  }
10462
10501
  function printCrossStackReport(repoName, report) {
@@ -10465,11 +10504,13 @@ function printCrossStackReport(repoName, report) {
10465
10504
  const phantomCount = report.phantom.length;
10466
10505
  const mismatchCount = report.methodMismatch.length;
10467
10506
  const unusedCount = report.unused.length;
10507
+ const concatCount = report.frontendCalls.filter((c) => c.isConcatPath).length;
10508
+ const concatNote = concatCount > 0 ? ` (${concatCount} via string concat \u2014 approximate)` : "";
10468
10509
  console.log(chalk19.cyan(`
10469
10510
  \u2500\u2500\u2500 Cross-Stack Contract Verification [${repoName}] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
10470
10511
  console.log(
10471
10512
  chalk19.gray(
10472
- ` Scanned ${report.totalScannedFiles} file(s), found ${report.frontendCalls.length} HTTP call(s)`
10513
+ ` Scanned ${report.totalScannedFiles} file(s), found ${report.frontendCalls.length} HTTP call(s)${concatNote}`
10473
10514
  )
10474
10515
  );
10475
10516
  console.log(chalk19.gray(` Backend DSL endpoints: ${totalEp}`));
@@ -10511,7 +10552,22 @@ function printCrossStackReport(repoName, report) {
10511
10552
  console.log(chalk19.gray(` ... and ${unusedCount - 8} more`));
10512
10553
  }
10513
10554
  }
10514
- if (phantomCount === 0 && mismatchCount === 0 && unusedCount === 0 && matchedCount === totalEp && totalEp > 0) {
10555
+ if (report.unknownMethodCalls.length > 0) {
10556
+ console.log(
10557
+ chalk19.gray(
10558
+ `
10559
+ \xB7 Unknown method (${report.unknownMethodCalls.length}): HTTP method could not be determined \u2014 matched permissively`
10560
+ )
10561
+ );
10562
+ for (const call of report.unknownMethodCalls.slice(0, 5)) {
10563
+ console.log(chalk19.gray(` UNKNWN ${call.path}`));
10564
+ console.log(chalk19.gray(` ${call.file}:${call.line}`));
10565
+ }
10566
+ if (report.unknownMethodCalls.length > 5) {
10567
+ console.log(chalk19.gray(` ... and ${report.unknownMethodCalls.length - 5} more`));
10568
+ }
10569
+ }
10570
+ if (!report.hasViolations && unusedCount === 0 && matchedCount === totalEp && totalEp > 0) {
10515
10571
  console.log(chalk19.green(`
10516
10572
  \u2714 Contract fully aligned \u2014 all ${totalEp} endpoints consumed correctly.`));
10517
10573
  }
@@ -11991,6 +12047,13 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
11991
12047
  { scopedFiles: fe2.generatedFiles }
11992
12048
  );
11993
12049
  printCrossStackReport(fe2.repoName, report);
12050
+ if (report.hasViolations) {
12051
+ console.log(
12052
+ chalk22.yellow(
12053
+ ` \u26A0 [W5] ${fe2.repoName} has cross-stack violations (${report.phantom.length} phantom, ${report.methodMismatch.length} method mismatch). Review the report above and fix generated frontend code.`
12054
+ )
12055
+ );
12056
+ }
11994
12057
  } catch (err) {
11995
12058
  console.log(chalk22.yellow(` \u26A0 Verification failed for ${fe2.repoName}: ${err.message}`));
11996
12059
  }