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
|
@@ -662,6 +662,15 @@ export async function runMultiRepoPipeline(
|
|
|
662
662
|
{ scopedFiles: fe.generatedFiles }
|
|
663
663
|
);
|
|
664
664
|
printCrossStackReport(fe.repoName, report);
|
|
665
|
+
if (report.hasViolations) {
|
|
666
|
+
console.log(
|
|
667
|
+
chalk.yellow(
|
|
668
|
+
` ⚠ [W5] ${fe.repoName} has cross-stack violations` +
|
|
669
|
+
` (${report.phantom.length} phantom, ${report.methodMismatch.length} method mismatch).` +
|
|
670
|
+
` Review the report above and fix generated frontend code.`
|
|
671
|
+
)
|
|
672
|
+
);
|
|
673
|
+
}
|
|
665
674
|
} catch (err) {
|
|
666
675
|
console.log(chalk.yellow(` ⚠ Verification failed for ${fe.repoName}: ${(err as Error).message}`));
|
|
667
676
|
}
|
|
@@ -11,6 +11,9 @@ export interface FrontendApiCall {
|
|
|
11
11
|
file: string; // relative path from frontend root
|
|
12
12
|
line: number; // 1-indexed line number
|
|
13
13
|
snippet: string; // one-line source snippet
|
|
14
|
+
/** True when path was extracted from a string concatenation (e.g. '/api/' + id).
|
|
15
|
+
* The path ends with /* to represent the unknown suffix — matching is approximate. */
|
|
16
|
+
isConcatPath?: boolean;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
export interface CrossStackReport {
|
|
@@ -24,7 +27,12 @@ export interface CrossStackReport {
|
|
|
24
27
|
methodMismatch: Array<{ call: FrontendApiCall; expectedMethod: string }>;
|
|
25
28
|
/** Calls whose method+path both match the DSL */
|
|
26
29
|
matched: Array<{ call: FrontendApiCall; endpointId: string }>;
|
|
30
|
+
/** Calls with UNKNOWN method (generic `request('/path')` helpers without a method arg).
|
|
31
|
+
* These are counted as matched (permissive) but surfaced for visibility. */
|
|
32
|
+
unknownMethodCalls: FrontendApiCall[];
|
|
27
33
|
totalScannedFiles: number;
|
|
34
|
+
/** True when there are phantom calls or method mismatches — use to fail CI / pipeline steps. */
|
|
35
|
+
hasViolations: boolean;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
// ─── File scanning ────────────────────────────────────────────────────────────
|
|
@@ -92,8 +100,9 @@ export function extractApiCallsFromSource(
|
|
|
92
100
|
|
|
93
101
|
// Pattern 1: .get('/path') / .post('/path') / .delete('/path') / .put('/path') / .patch('/path')
|
|
94
102
|
// Matches things like: axios.get('/api/users'), api.post(`/api/users/${id}`)
|
|
103
|
+
// Negative lookahead (?!\s*\+) ensures we don't match string concatenation (handled by Pattern 5).
|
|
95
104
|
const methodCallRegex =
|
|
96
|
-
/\.(get|post|put|patch|delete)\s*\(\s*(['"`])([^'"`]+)\2/gi;
|
|
105
|
+
/\.(get|post|put|patch|delete)\s*\(\s*(['"`])([^'"`]+)\2(?!\s*\+)/gi;
|
|
97
106
|
|
|
98
107
|
// Pattern 2: fetch('/path', { method: 'POST' })
|
|
99
108
|
// We detect fetch( + URL + optional method in the next ~100 chars
|
|
@@ -107,6 +116,15 @@ export function extractApiCallsFromSource(
|
|
|
107
116
|
const genericRequestRegex =
|
|
108
117
|
/\brequest\s*\(\s*(['"`])([^'"`]+)\1\s*(?:,\s*(['"`])(GET|POST|PUT|PATCH|DELETE)\3)?/gi;
|
|
109
118
|
|
|
119
|
+
// Pattern 5: axios.get('/api/prefix/' + variable) — string concatenation with static prefix.
|
|
120
|
+
// We capture the static prefix and treat the unknown suffix as a wildcard segment.
|
|
121
|
+
// Only the method-call variant is handled here; fetch+concat is covered separately below.
|
|
122
|
+
const concatMethodRegex =
|
|
123
|
+
/\.(get|post|put|patch|delete)\s*\(\s*(['"`])([^'"`]+)\2\s*\+/gi;
|
|
124
|
+
|
|
125
|
+
// Pattern 6: fetch('/api/prefix/' + variable, ...) — concat inside fetch
|
|
126
|
+
const concatFetchRegex = /\bfetch\s*\(\s*(['"`])([^'"`]+)\1\s*\+([^)]*)\)/g;
|
|
127
|
+
|
|
110
128
|
function getLineNumber(offset: number): number {
|
|
111
129
|
// Count newlines up to offset
|
|
112
130
|
let ln = 1;
|
|
@@ -131,6 +149,15 @@ export function extractApiCallsFromSource(
|
|
|
131
149
|
return true;
|
|
132
150
|
}
|
|
133
151
|
|
|
152
|
+
/** Build a wildcard-terminated path from a static concat prefix.
|
|
153
|
+
* '/api/users/' → '/api/users/*'
|
|
154
|
+
* '/api/users' → '/api/users/*'
|
|
155
|
+
*/
|
|
156
|
+
function concatPath(prefix: string): string {
|
|
157
|
+
const stripped = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
158
|
+
return stripped + "/*";
|
|
159
|
+
}
|
|
160
|
+
|
|
134
161
|
let match: RegExpExecArray | null;
|
|
135
162
|
|
|
136
163
|
while ((match = methodCallRegex.exec(source)) !== null) {
|
|
@@ -189,6 +216,39 @@ export function extractApiCallsFromSource(
|
|
|
189
216
|
});
|
|
190
217
|
}
|
|
191
218
|
|
|
219
|
+
// Pattern 5: axios.get('/api/prefix/' + variable)
|
|
220
|
+
// Pattern 1's negative lookahead excludes these cases, so no dedup needed.
|
|
221
|
+
while ((match = concatMethodRegex.exec(source)) !== null) {
|
|
222
|
+
const rawPrefix = match[3];
|
|
223
|
+
if (!isApiLike(rawPrefix)) continue;
|
|
224
|
+
const line = getLineNumber(match.index);
|
|
225
|
+
calls.push({
|
|
226
|
+
method: match[1].toUpperCase(),
|
|
227
|
+
path: concatPath(rawPrefix),
|
|
228
|
+
file: relFile,
|
|
229
|
+
line,
|
|
230
|
+
snippet: getSnippet(line),
|
|
231
|
+
isConcatPath: true,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Pattern 6: fetch('/api/prefix/' + variable, ...)
|
|
236
|
+
while ((match = concatFetchRegex.exec(source)) !== null) {
|
|
237
|
+
const rawPrefix = match[2];
|
|
238
|
+
if (!isApiLike(rawPrefix)) continue;
|
|
239
|
+
const tail = match[3] ?? "";
|
|
240
|
+
const methodMatch = tail.match(/method\s*:\s*['"`](GET|POST|PUT|PATCH|DELETE)['"`]/i);
|
|
241
|
+
const line = getLineNumber(match.index);
|
|
242
|
+
calls.push({
|
|
243
|
+
method: methodMatch ? methodMatch[1].toUpperCase() : "GET",
|
|
244
|
+
path: concatPath(rawPrefix),
|
|
245
|
+
file: relFile,
|
|
246
|
+
line,
|
|
247
|
+
snippet: getSnippet(line),
|
|
248
|
+
isConcatPath: true,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
192
252
|
return calls;
|
|
193
253
|
}
|
|
194
254
|
|
|
@@ -211,6 +271,7 @@ export function normalizePathSegments(p: string): string[] {
|
|
|
211
271
|
const withoutQs = p.split("?")[0];
|
|
212
272
|
const segments = withoutQs.split("/").filter(Boolean);
|
|
213
273
|
return segments.map((seg) => {
|
|
274
|
+
if (seg === "*") return "*"; // explicit wildcard (concat paths)
|
|
214
275
|
if (seg.startsWith(":")) return "*";
|
|
215
276
|
if (seg.includes("${") || seg.includes("{{")) return "*";
|
|
216
277
|
if (/^\d+$/.test(seg)) return "*";
|
|
@@ -286,9 +347,13 @@ export async function verifyCrossStackContract(
|
|
|
286
347
|
const phantom: FrontendApiCall[] = [];
|
|
287
348
|
const methodMismatch: Array<{ call: FrontendApiCall; expectedMethod: string }> = [];
|
|
288
349
|
const matched: Array<{ call: FrontendApiCall; endpointId: string }> = [];
|
|
350
|
+
const unknownMethodCalls: FrontendApiCall[] = [];
|
|
289
351
|
const usedEndpointIds = new Set<string>();
|
|
290
352
|
|
|
291
353
|
for (const call of allCalls) {
|
|
354
|
+
// Track UNKNOWN-method calls for visibility regardless of matching outcome.
|
|
355
|
+
if (call.method === "UNKNOWN") unknownMethodCalls.push(call);
|
|
356
|
+
|
|
292
357
|
// Find all DSL endpoints whose path matches this call's path.
|
|
293
358
|
const pathMatches = backendEndpoints.filter((ep) => pathsMatch(ep.path, call.path));
|
|
294
359
|
if (pathMatches.length === 0) {
|
|
@@ -296,6 +361,7 @@ export async function verifyCrossStackContract(
|
|
|
296
361
|
continue;
|
|
297
362
|
}
|
|
298
363
|
// Check if any path-match also matches the method.
|
|
364
|
+
// UNKNOWN is treated permissively — matched against the first path hit.
|
|
299
365
|
const methodMatch = pathMatches.find(
|
|
300
366
|
(ep) => call.method === "UNKNOWN" || ep.method === call.method
|
|
301
367
|
);
|
|
@@ -319,7 +385,9 @@ export async function verifyCrossStackContract(
|
|
|
319
385
|
unused,
|
|
320
386
|
methodMismatch,
|
|
321
387
|
matched,
|
|
388
|
+
unknownMethodCalls,
|
|
322
389
|
totalScannedFiles: files.length,
|
|
390
|
+
hasViolations: phantom.length > 0 || methodMismatch.length > 0,
|
|
323
391
|
};
|
|
324
392
|
}
|
|
325
393
|
|
|
@@ -332,10 +400,12 @@ export function printCrossStackReport(repoName: string, report: CrossStackReport
|
|
|
332
400
|
const mismatchCount = report.methodMismatch.length;
|
|
333
401
|
const unusedCount = report.unused.length;
|
|
334
402
|
|
|
403
|
+
const concatCount = report.frontendCalls.filter((c) => c.isConcatPath).length;
|
|
404
|
+
const concatNote = concatCount > 0 ? ` (${concatCount} via string concat — approximate)` : "";
|
|
335
405
|
console.log(chalk.cyan(`\n─── Cross-Stack Contract Verification [${repoName}] ─────────────`));
|
|
336
406
|
console.log(
|
|
337
407
|
chalk.gray(
|
|
338
|
-
` Scanned ${report.totalScannedFiles} file(s), found ${report.frontendCalls.length} HTTP call(s)`
|
|
408
|
+
` Scanned ${report.totalScannedFiles} file(s), found ${report.frontendCalls.length} HTTP call(s)${concatNote}`
|
|
339
409
|
)
|
|
340
410
|
);
|
|
341
411
|
console.log(chalk.gray(` Backend DSL endpoints: ${totalEp}`));
|
|
@@ -387,8 +457,25 @@ export function printCrossStackReport(repoName: string, report: CrossStackReport
|
|
|
387
457
|
}
|
|
388
458
|
}
|
|
389
459
|
|
|
460
|
+
// ── UNKNOWN method calls ─────────────────────────────────────────────────────
|
|
461
|
+
// Surface for visibility; they were matched permissively and may hide real mismatches.
|
|
462
|
+
if (report.unknownMethodCalls.length > 0) {
|
|
463
|
+
console.log(
|
|
464
|
+
chalk.gray(
|
|
465
|
+
`\n · Unknown method (${report.unknownMethodCalls.length}): HTTP method could not be determined — matched permissively`
|
|
466
|
+
)
|
|
467
|
+
);
|
|
468
|
+
for (const call of report.unknownMethodCalls.slice(0, 5)) {
|
|
469
|
+
console.log(chalk.gray(` UNKNWN ${call.path}`));
|
|
470
|
+
console.log(chalk.gray(` ${call.file}:${call.line}`));
|
|
471
|
+
}
|
|
472
|
+
if (report.unknownMethodCalls.length > 5) {
|
|
473
|
+
console.log(chalk.gray(` ... and ${report.unknownMethodCalls.length - 5} more`));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
390
477
|
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
391
|
-
if (
|
|
478
|
+
if (!report.hasViolations && unusedCount === 0 && matchedCount === totalEp && totalEp > 0) {
|
|
392
479
|
console.log(chalk.green(`\n ✔ Contract fully aligned — all ${totalEp} endpoints consumed correctly.`));
|
|
393
480
|
}
|
|
394
481
|
console.log(chalk.cyan("─".repeat(65)));
|
package/dist/cli/index.js
CHANGED
|
@@ -715,7 +715,7 @@ var require_package = __commonJS({
|
|
|
715
715
|
"package.json"(exports2, module2) {
|
|
716
716
|
module2.exports = {
|
|
717
717
|
name: "ai-spec-dev",
|
|
718
|
-
version: "0.
|
|
718
|
+
version: "0.56.0",
|
|
719
719
|
description: "AI-driven Development Orchestrator SDK & CLI",
|
|
720
720
|
main: "dist/index.js",
|
|
721
721
|
types: "dist/index.d.ts",
|
|
@@ -10321,10 +10321,12 @@ async function walkSource(root) {
|
|
|
10321
10321
|
function extractApiCallsFromSource(source, relFile) {
|
|
10322
10322
|
const calls = [];
|
|
10323
10323
|
const lines = source.split("\n");
|
|
10324
|
-
const methodCallRegex = /\.(get|post|put|patch|delete)\s*\(\s*(['"`])([^'"`]+)\2/gi;
|
|
10324
|
+
const methodCallRegex = /\.(get|post|put|patch|delete)\s*\(\s*(['"`])([^'"`]+)\2(?!\s*\+)/gi;
|
|
10325
10325
|
const fetchRegex = /\bfetch\s*\(\s*(['"`])([^'"`]+)\1([^)]*)\)/g;
|
|
10326
10326
|
const useRequestRegex = /\buseRequest\s*\(\s*(['"`])([^'"`]+)\1([^)]*)\)/g;
|
|
10327
10327
|
const genericRequestRegex = /\brequest\s*\(\s*(['"`])([^'"`]+)\1\s*(?:,\s*(['"`])(GET|POST|PUT|PATCH|DELETE)\3)?/gi;
|
|
10328
|
+
const concatMethodRegex = /\.(get|post|put|patch|delete)\s*\(\s*(['"`])([^'"`]+)\2\s*\+/gi;
|
|
10329
|
+
const concatFetchRegex = /\bfetch\s*\(\s*(['"`])([^'"`]+)\1\s*\+([^)]*)\)/g;
|
|
10328
10330
|
function getLineNumber(offset) {
|
|
10329
10331
|
let ln = 1;
|
|
10330
10332
|
for (let i = 0; i < offset && i < source.length; i++) {
|
|
@@ -10341,6 +10343,10 @@ function extractApiCallsFromSource(source, relFile) {
|
|
|
10341
10343
|
if (/\.(css|svg|png|jpe?g|gif|ico|woff2?|ttf|eot)$/i.test(p)) return false;
|
|
10342
10344
|
return true;
|
|
10343
10345
|
}
|
|
10346
|
+
function concatPath(prefix) {
|
|
10347
|
+
const stripped = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
|
|
10348
|
+
return stripped + "/*";
|
|
10349
|
+
}
|
|
10344
10350
|
let match;
|
|
10345
10351
|
while ((match = methodCallRegex.exec(source)) !== null) {
|
|
10346
10352
|
const rawPath = match[3];
|
|
@@ -10394,12 +10400,41 @@ function extractApiCallsFromSource(source, relFile) {
|
|
|
10394
10400
|
snippet: getSnippet(line)
|
|
10395
10401
|
});
|
|
10396
10402
|
}
|
|
10403
|
+
while ((match = concatMethodRegex.exec(source)) !== null) {
|
|
10404
|
+
const rawPrefix = match[3];
|
|
10405
|
+
if (!isApiLike(rawPrefix)) continue;
|
|
10406
|
+
const line = getLineNumber(match.index);
|
|
10407
|
+
calls.push({
|
|
10408
|
+
method: match[1].toUpperCase(),
|
|
10409
|
+
path: concatPath(rawPrefix),
|
|
10410
|
+
file: relFile,
|
|
10411
|
+
line,
|
|
10412
|
+
snippet: getSnippet(line),
|
|
10413
|
+
isConcatPath: true
|
|
10414
|
+
});
|
|
10415
|
+
}
|
|
10416
|
+
while ((match = concatFetchRegex.exec(source)) !== null) {
|
|
10417
|
+
const rawPrefix = match[2];
|
|
10418
|
+
if (!isApiLike(rawPrefix)) continue;
|
|
10419
|
+
const tail = match[3] ?? "";
|
|
10420
|
+
const methodMatch = tail.match(/method\s*:\s*['"`](GET|POST|PUT|PATCH|DELETE)['"`]/i);
|
|
10421
|
+
const line = getLineNumber(match.index);
|
|
10422
|
+
calls.push({
|
|
10423
|
+
method: methodMatch ? methodMatch[1].toUpperCase() : "GET",
|
|
10424
|
+
path: concatPath(rawPrefix),
|
|
10425
|
+
file: relFile,
|
|
10426
|
+
line,
|
|
10427
|
+
snippet: getSnippet(line),
|
|
10428
|
+
isConcatPath: true
|
|
10429
|
+
});
|
|
10430
|
+
}
|
|
10397
10431
|
return calls;
|
|
10398
10432
|
}
|
|
10399
10433
|
function normalizePathSegments(p) {
|
|
10400
10434
|
const withoutQs = p.split("?")[0];
|
|
10401
10435
|
const segments = withoutQs.split("/").filter(Boolean);
|
|
10402
10436
|
return segments.map((seg) => {
|
|
10437
|
+
if (seg === "*") return "*";
|
|
10403
10438
|
if (seg.startsWith(":")) return "*";
|
|
10404
10439
|
if (seg.includes("${") || seg.includes("{{")) return "*";
|
|
10405
10440
|
if (/^\d+$/.test(seg)) return "*";
|
|
@@ -10451,8 +10486,10 @@ async function verifyCrossStackContract(backendDsl, frontendRoot, opts = {}) {
|
|
|
10451
10486
|
const phantom = [];
|
|
10452
10487
|
const methodMismatch = [];
|
|
10453
10488
|
const matched = [];
|
|
10489
|
+
const unknownMethodCalls = [];
|
|
10454
10490
|
const usedEndpointIds = /* @__PURE__ */ new Set();
|
|
10455
10491
|
for (const call of allCalls) {
|
|
10492
|
+
if (call.method === "UNKNOWN") unknownMethodCalls.push(call);
|
|
10456
10493
|
const pathMatches = backendEndpoints.filter((ep) => pathsMatch(ep.path, call.path));
|
|
10457
10494
|
if (pathMatches.length === 0) {
|
|
10458
10495
|
phantom.push(call);
|
|
@@ -10477,7 +10514,9 @@ async function verifyCrossStackContract(backendDsl, frontendRoot, opts = {}) {
|
|
|
10477
10514
|
unused,
|
|
10478
10515
|
methodMismatch,
|
|
10479
10516
|
matched,
|
|
10480
|
-
|
|
10517
|
+
unknownMethodCalls,
|
|
10518
|
+
totalScannedFiles: files.length,
|
|
10519
|
+
hasViolations: phantom.length > 0 || methodMismatch.length > 0
|
|
10481
10520
|
};
|
|
10482
10521
|
}
|
|
10483
10522
|
function printCrossStackReport(repoName, report) {
|
|
@@ -10486,11 +10525,13 @@ function printCrossStackReport(repoName, report) {
|
|
|
10486
10525
|
const phantomCount = report.phantom.length;
|
|
10487
10526
|
const mismatchCount = report.methodMismatch.length;
|
|
10488
10527
|
const unusedCount = report.unused.length;
|
|
10528
|
+
const concatCount = report.frontendCalls.filter((c) => c.isConcatPath).length;
|
|
10529
|
+
const concatNote = concatCount > 0 ? ` (${concatCount} via string concat \u2014 approximate)` : "";
|
|
10489
10530
|
console.log(import_chalk19.default.cyan(`
|
|
10490
10531
|
\u2500\u2500\u2500 Cross-Stack Contract Verification [${repoName}] \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`));
|
|
10491
10532
|
console.log(
|
|
10492
10533
|
import_chalk19.default.gray(
|
|
10493
|
-
` Scanned ${report.totalScannedFiles} file(s), found ${report.frontendCalls.length} HTTP call(s)`
|
|
10534
|
+
` Scanned ${report.totalScannedFiles} file(s), found ${report.frontendCalls.length} HTTP call(s)${concatNote}`
|
|
10494
10535
|
)
|
|
10495
10536
|
);
|
|
10496
10537
|
console.log(import_chalk19.default.gray(` Backend DSL endpoints: ${totalEp}`));
|
|
@@ -10532,7 +10573,22 @@ function printCrossStackReport(repoName, report) {
|
|
|
10532
10573
|
console.log(import_chalk19.default.gray(` ... and ${unusedCount - 8} more`));
|
|
10533
10574
|
}
|
|
10534
10575
|
}
|
|
10535
|
-
if (
|
|
10576
|
+
if (report.unknownMethodCalls.length > 0) {
|
|
10577
|
+
console.log(
|
|
10578
|
+
import_chalk19.default.gray(
|
|
10579
|
+
`
|
|
10580
|
+
\xB7 Unknown method (${report.unknownMethodCalls.length}): HTTP method could not be determined \u2014 matched permissively`
|
|
10581
|
+
)
|
|
10582
|
+
);
|
|
10583
|
+
for (const call of report.unknownMethodCalls.slice(0, 5)) {
|
|
10584
|
+
console.log(import_chalk19.default.gray(` UNKNWN ${call.path}`));
|
|
10585
|
+
console.log(import_chalk19.default.gray(` ${call.file}:${call.line}`));
|
|
10586
|
+
}
|
|
10587
|
+
if (report.unknownMethodCalls.length > 5) {
|
|
10588
|
+
console.log(import_chalk19.default.gray(` ... and ${report.unknownMethodCalls.length - 5} more`));
|
|
10589
|
+
}
|
|
10590
|
+
}
|
|
10591
|
+
if (!report.hasViolations && unusedCount === 0 && matchedCount === totalEp && totalEp > 0) {
|
|
10536
10592
|
console.log(import_chalk19.default.green(`
|
|
10537
10593
|
\u2714 Contract fully aligned \u2014 all ${totalEp} endpoints consumed correctly.`));
|
|
10538
10594
|
}
|
|
@@ -12012,6 +12068,13 @@ async function runMultiRepoPipeline(idea, workspace, opts, currentDir, config2)
|
|
|
12012
12068
|
{ scopedFiles: fe2.generatedFiles }
|
|
12013
12069
|
);
|
|
12014
12070
|
printCrossStackReport(fe2.repoName, report);
|
|
12071
|
+
if (report.hasViolations) {
|
|
12072
|
+
console.log(
|
|
12073
|
+
import_chalk22.default.yellow(
|
|
12074
|
+
` \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.`
|
|
12075
|
+
)
|
|
12076
|
+
);
|
|
12077
|
+
}
|
|
12015
12078
|
} catch (err) {
|
|
12016
12079
|
console.log(import_chalk22.default.yellow(` \u26A0 Verification failed for ${fe2.repoName}: ${err.message}`));
|
|
12017
12080
|
}
|