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/package.json
CHANGED
|
@@ -78,6 +78,41 @@ describe("extractApiCallsFromSource", () => {
|
|
|
78
78
|
const calls = extractApiCallsFromSource(src, "a.ts");
|
|
79
79
|
expect(calls[0].method).toBe("UNKNOWN");
|
|
80
80
|
});
|
|
81
|
+
|
|
82
|
+
it("extracts axios.get('/api/prefix/' + variable) as concat path", () => {
|
|
83
|
+
const src = `axios.get('/api/users/' + userId)`;
|
|
84
|
+
const calls = extractApiCallsFromSource(src, "a.ts");
|
|
85
|
+
expect(calls).toHaveLength(1);
|
|
86
|
+
expect(calls[0].method).toBe("GET");
|
|
87
|
+
expect(calls[0].isConcatPath).toBe(true);
|
|
88
|
+
// Path should end with /* wildcard
|
|
89
|
+
expect(calls[0].path).toBe("/api/users/*");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("extracts axios.post('/api/prefix/' + variable) as concat path with correct method", () => {
|
|
93
|
+
const src = `axios.post('/api/orders/' + id, body)`;
|
|
94
|
+
const calls = extractApiCallsFromSource(src, "a.ts");
|
|
95
|
+
const concatCall = calls.find((c) => c.isConcatPath);
|
|
96
|
+
expect(concatCall).toBeDefined();
|
|
97
|
+
expect(concatCall!.method).toBe("POST");
|
|
98
|
+
expect(concatCall!.path).toBe("/api/orders/*");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("does NOT double-count full-literal paths as concat", () => {
|
|
102
|
+
// '/api/users/' is the full path (no + follows), should not be marked concat
|
|
103
|
+
const src = `axios.get('/api/users/');`;
|
|
104
|
+
const calls = extractApiCallsFromSource(src, "a.ts");
|
|
105
|
+
expect(calls.every((c) => !c.isConcatPath)).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("extracts fetch('/api/prefix/' + variable) as concat path", () => {
|
|
109
|
+
const src = `fetch('/api/items/' + id, { method: 'DELETE' })`;
|
|
110
|
+
const calls = extractApiCallsFromSource(src, "a.ts");
|
|
111
|
+
const concatCall = calls.find((c) => c.isConcatPath);
|
|
112
|
+
expect(concatCall).toBeDefined();
|
|
113
|
+
expect(concatCall!.method).toBe("DELETE");
|
|
114
|
+
expect(concatCall!.path).toBe("/api/items/*");
|
|
115
|
+
});
|
|
81
116
|
});
|
|
82
117
|
|
|
83
118
|
// ─── Path normalization & matching ────────────────────────────────────────────
|
|
@@ -298,4 +333,70 @@ describe("verifyCrossStackContract", () => {
|
|
|
298
333
|
// Empty list is treated as "no scope" → walks whole tree
|
|
299
334
|
expect(report.matched).toHaveLength(1);
|
|
300
335
|
});
|
|
336
|
+
|
|
337
|
+
it("hasViolations is false when contract is clean", async () => {
|
|
338
|
+
await fs.writeFile(path.join(tmpDir, "a.ts"), `axios.get('/api/users');`);
|
|
339
|
+
const dsl = buildDsl([{ id: "EP-1", method: "GET", path: "/api/users" }]);
|
|
340
|
+
|
|
341
|
+
const report = await verifyCrossStackContract(dsl, tmpDir);
|
|
342
|
+
expect(report.hasViolations).toBe(false);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("hasViolations is true when there are phantom calls", async () => {
|
|
346
|
+
await fs.writeFile(path.join(tmpDir, "a.ts"), `axios.get('/api/ghost');`);
|
|
347
|
+
const dsl = buildDsl([{ id: "EP-1", method: "GET", path: "/api/users" }]);
|
|
348
|
+
|
|
349
|
+
const report = await verifyCrossStackContract(dsl, tmpDir);
|
|
350
|
+
expect(report.hasViolations).toBe(true);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("hasViolations is true when there are method mismatches", async () => {
|
|
354
|
+
await fs.writeFile(path.join(tmpDir, "a.ts"), `axios.get('/api/users');`);
|
|
355
|
+
const dsl = buildDsl([{ id: "EP-1", method: "POST", path: "/api/users" }]);
|
|
356
|
+
|
|
357
|
+
const report = await verifyCrossStackContract(dsl, tmpDir);
|
|
358
|
+
expect(report.hasViolations).toBe(true);
|
|
359
|
+
expect(report.methodMismatch).toHaveLength(1);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("unknownMethodCalls is populated for UNKNOWN method calls", async () => {
|
|
363
|
+
await fs.writeFile(
|
|
364
|
+
path.join(tmpDir, "a.ts"),
|
|
365
|
+
`request('/api/users'); axios.get('/api/users');`
|
|
366
|
+
);
|
|
367
|
+
const dsl = buildDsl([{ id: "EP-1", method: "GET", path: "/api/users" }]);
|
|
368
|
+
|
|
369
|
+
const report = await verifyCrossStackContract(dsl, tmpDir);
|
|
370
|
+
expect(report.unknownMethodCalls).toHaveLength(1);
|
|
371
|
+
expect(report.unknownMethodCalls[0].method).toBe("UNKNOWN");
|
|
372
|
+
// UNKNOWN is matched permissively — not a violation
|
|
373
|
+
expect(report.hasViolations).toBe(false);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("matches concat path axios.get('/api/users/' + id) against DSL /api/users/:id", async () => {
|
|
377
|
+
await fs.writeFile(
|
|
378
|
+
path.join(tmpDir, "a.ts"),
|
|
379
|
+
"axios.get('/api/users/' + userId);"
|
|
380
|
+
);
|
|
381
|
+
const dsl = buildDsl([{ id: "EP-1", method: "GET", path: "/api/users/:id" }]);
|
|
382
|
+
|
|
383
|
+
const report = await verifyCrossStackContract(dsl, tmpDir);
|
|
384
|
+
expect(report.phantom).toHaveLength(0);
|
|
385
|
+
expect(report.matched).toHaveLength(1);
|
|
386
|
+
expect(report.matched[0].call.isConcatPath).toBe(true);
|
|
387
|
+
expect(report.hasViolations).toBe(false);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it("flags concat path as phantom when no DSL endpoint matches the static prefix", async () => {
|
|
391
|
+
await fs.writeFile(
|
|
392
|
+
path.join(tmpDir, "a.ts"),
|
|
393
|
+
"axios.get('/api/ghost/' + id);"
|
|
394
|
+
);
|
|
395
|
+
const dsl = buildDsl([{ id: "EP-1", method: "GET", path: "/api/users/:id" }]);
|
|
396
|
+
|
|
397
|
+
const report = await verifyCrossStackContract(dsl, tmpDir);
|
|
398
|
+
expect(report.phantom).toHaveLength(1);
|
|
399
|
+
expect(report.phantom[0].isConcatPath).toBe(true);
|
|
400
|
+
expect(report.hasViolations).toBe(true);
|
|
401
|
+
});
|
|
301
402
|
});
|
package/.ai-spec-workspace.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "bookmark-demo",
|
|
3
|
-
"repos": [
|
|
4
|
-
{
|
|
5
|
-
"name": "demo-backend",
|
|
6
|
-
"path": "demo-backend",
|
|
7
|
-
"type": "node-express",
|
|
8
|
-
"role": "backend"
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
"name": "demo-frontend",
|
|
12
|
-
"path": "demo-frontend",
|
|
13
|
-
"type": "react",
|
|
14
|
-
"role": "frontend"
|
|
15
|
-
}
|
|
16
|
-
]
|
|
17
|
-
}
|