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.
- package/cli/pipeline/multi-repo.ts +9 -0
- package/core/cross-stack-verifier.ts +90 -3
- package/dist/cli/index.js +68 -5
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +68 -5
- package/dist/cli/index.mjs.map +1 -1
- package/package.json +1 -1
- package/tests/cross-stack-verifier.test.ts +101 -0
- package/.ai-spec-workspace.json +0 -17
- package/.ai-spec.json +0 -7
package/dist/cli/index.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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 (
|
|
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
|
}
|