ai-spec-dev 0.42.0 → 0.46.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.
Files changed (49) hide show
  1. package/README.md +33 -17
  2. package/cli/commands/create.ts +232 -11
  3. package/cli/commands/init.ts +310 -107
  4. package/cli/commands/model.ts +7 -11
  5. package/cli/index.ts +1 -1
  6. package/cli/utils.ts +72 -4
  7. package/core/config-defaults.ts +44 -0
  8. package/core/constitution-generator.ts +2 -1
  9. package/core/dsl-extractor.ts +2 -1
  10. package/core/error-feedback.ts +3 -2
  11. package/core/openapi-exporter.ts +3 -2
  12. package/core/repo-store.ts +95 -0
  13. package/core/reviewer.ts +14 -13
  14. package/core/run-logger.ts +3 -4
  15. package/core/run-snapshot.ts +2 -3
  16. package/core/run-trend.ts +3 -4
  17. package/core/spec-generator.ts +27 -42
  18. package/core/token-budget.ts +3 -8
  19. package/core/vcr.ts +3 -1
  20. package/dist/cli/index.js +919 -519
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/cli/index.mjs +912 -512
  23. package/dist/cli/index.mjs.map +1 -1
  24. package/dist/index.d.mts +3 -2
  25. package/dist/index.d.ts +3 -2
  26. package/dist/index.js +43 -53
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +43 -53
  29. package/dist/index.mjs.map +1 -1
  30. package/package.json +1 -1
  31. package/demo-backend/.ai-spec-constitution.md +0 -65
  32. package/demo-backend/package.json +0 -21
  33. package/demo-backend/prisma/schema.prisma +0 -22
  34. package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json +0 -186
  35. package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.md +0 -211
  36. package/demo-backend/src/controllers/bookmark.controller.test.ts +0 -255
  37. package/demo-backend/src/controllers/bookmark.controller.ts +0 -187
  38. package/demo-backend/src/index.ts +0 -17
  39. package/demo-backend/src/routes/bookmark.routes.test.ts +0 -264
  40. package/demo-backend/src/routes/bookmark.routes.ts +0 -11
  41. package/demo-backend/src/routes/index.ts +0 -8
  42. package/demo-backend/src/services/bookmark.service.test.ts +0 -433
  43. package/demo-backend/src/services/bookmark.service.ts +0 -261
  44. package/demo-backend/tsconfig.json +0 -12
  45. package/demo-frontend/.ai-spec-constitution.md +0 -95
  46. package/demo-frontend/package.json +0 -23
  47. package/demo-frontend/src/App.tsx +0 -12
  48. package/demo-frontend/src/main.tsx +0 -9
  49. package/demo-frontend/tsconfig.json +0 -13
@@ -1,255 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
2
- import { Request, Response, NextFunction } from 'express';
3
- import {
4
- getBookmarks,
5
- createBookmark,
6
- updateBookmark,
7
- deleteBookmark,
8
- } from '../bookmark.controller';
9
- import * as bookmarkService from '../../services/bookmark.service';
10
- import { AppError } from '../../middleware/error.middleware';
11
-
12
- vi.mock('../../services/bookmark.service');
13
-
14
- const mockedBookmarkService = vi.mocked(bookmarkService);
15
-
16
- describe('Bookmark Controller', () => {
17
- let mockRequest: Partial<Request>;
18
- let mockResponse: Partial<Response>;
19
- let mockNext: NextFunction;
20
-
21
- beforeEach(() => {
22
- vi.clearAllMocks();
23
- mockResponse = {
24
- status: vi.fn().mockReturnThis(),
25
- json: vi.fn().mockReturnThis(),
26
- send: vi.fn().mockReturnThis(),
27
- };
28
- mockNext = vi.fn();
29
- });
30
-
31
- describe('getBookmarks', () => {
32
- it('should return 200 with paginated bookmarks', async () => {
33
- const mockBookmarks = [
34
- {
35
- id: '1',
36
- title: 'Test',
37
- url: 'https://test.com',
38
- tags: ['test'],
39
- createdAt: new Date(),
40
- updatedAt: new Date(),
41
- },
42
- ];
43
- const mockTotal = 1;
44
-
45
- mockRequest = {
46
- query: { limit: '10', offset: '0' },
47
- };
48
-
49
- mockedBookmarkService.getBookmarks.mockResolvedValue({
50
- bookmarks: mockBookmarks,
51
- total: mockTotal,
52
- });
53
-
54
- await getBookmarks(
55
- mockRequest as Request,
56
- mockResponse as Response,
57
- mockNext
58
- );
59
-
60
- expect(mockedBookmarkService.getBookmarks).toHaveBeenCalledWith(10, 0);
61
- expect(mockResponse.status).toHaveBeenCalledWith(200);
62
- expect(mockResponse.json).toHaveBeenCalledWith({
63
- code: 200,
64
- message: 'success',
65
- data: {
66
- bookmarks: mockBookmarks,
67
- total: mockTotal,
68
- },
69
- });
70
- });
71
-
72
- it('should call next with error when service throws', async () => {
73
- const mockError = new AppError('Database error', 500, 'INTERNAL_ERROR');
74
- mockRequest = {
75
- query: {},
76
- };
77
-
78
- mockedBookmarkService.getBookmarks.mockRejectedValue(mockError);
79
-
80
- await getBookmarks(
81
- mockRequest as Request,
82
- mockResponse as Response,
83
- mockNext
84
- );
85
-
86
- expect(mockNext).toHaveBeenCalledWith(mockError);
87
- });
88
- });
89
-
90
- describe('createBookmark', () => {
91
- it('should return 201 with created bookmark', async () => {
92
- const mockBookmark = {
93
- id: '1',
94
- title: 'Test',
95
- url: 'https://test.com',
96
- tags: ['test'],
97
- createdAt: new Date(),
98
- updatedAt: new Date(),
99
- };
100
-
101
- mockRequest = {
102
- body: {
103
- title: 'Test',
104
- url: 'https://test.com',
105
- tags: ['test'],
106
- },
107
- };
108
-
109
- mockedBookmarkService.createBookmark.mockResolvedValue(mockBookmark);
110
-
111
- await createBookmark(
112
- mockRequest as Request,
113
- mockResponse as Response,
114
- mockNext
115
- );
116
-
117
- expect(mockedBookmarkService.createBookmark).toHaveBeenCalledWith({
118
- title: 'Test',
119
- url: 'https://test.com',
120
- tags: ['test'],
121
- });
122
- expect(mockResponse.status).toHaveBeenCalledWith(201);
123
- expect(mockResponse.json).toHaveBeenCalledWith({
124
- code: 201,
125
- message: 'success',
126
- data: mockBookmark,
127
- });
128
- });
129
-
130
- it('should call next with validation error for invalid URL', async () => {
131
- const mockError = new AppError(
132
- 'Validation failed: "url" must be a valid URL',
133
- 400,
134
- 'VALIDATION_ERROR'
135
- );
136
- mockRequest = {
137
- body: {
138
- title: 'Test',
139
- url: 'invalid-url',
140
- },
141
- };
142
-
143
- mockedBookmarkService.createBookmark.mockRejectedValue(mockError);
144
-
145
- await createBookmark(
146
- mockRequest as Request,
147
- mockResponse as Response,
148
- mockNext
149
- );
150
-
151
- expect(mockNext).toHaveBeenCalledWith(mockError);
152
- });
153
- });
154
-
155
- describe('updateBookmark', () => {
156
- it('should return 200 with updated bookmark', async () => {
157
- const mockBookmark = {
158
- id: '1',
159
- title: 'Updated',
160
- url: 'https://updated.com',
161
- tags: ['updated'],
162
- createdAt: new Date(),
163
- updatedAt: new Date(),
164
- };
165
-
166
- mockRequest = {
167
- params: { id: '1' },
168
- body: {
169
- title: 'Updated',
170
- url: 'https://updated.com',
171
- tags: ['updated'],
172
- },
173
- };
174
-
175
- mockedBookmarkService.updateBookmark.mockResolvedValue(mockBookmark);
176
-
177
- await updateBookmark(
178
- mockRequest as Request,
179
- mockResponse as Response,
180
- mockNext
181
- );
182
-
183
- expect(mockedBookmarkService.updateBookmark).toHaveBeenCalledWith('1', {
184
- title: 'Updated',
185
- url: 'https://updated.com',
186
- tags: ['updated'],
187
- });
188
- expect(mockResponse.status).toHaveBeenCalledWith(200);
189
- expect(mockResponse.json).toHaveBeenCalledWith({
190
- code: 200,
191
- message: 'success',
192
- data: mockBookmark,
193
- });
194
- });
195
-
196
- it('should call next with 404 when bookmark not found', async () => {
197
- const mockError = new AppError('Bookmark not found', 404, 'NOT_FOUND');
198
- mockRequest = {
199
- params: { id: 'non-existent-id' },
200
- body: {
201
- title: 'Updated',
202
- url: 'https://updated.com',
203
- tags: [],
204
- },
205
- };
206
-
207
- mockedBookmarkService.updateBookmark.mockRejectedValue(mockError);
208
-
209
- await updateBookmark(
210
- mockRequest as Request,
211
- mockResponse as Response,
212
- mockNext
213
- );
214
-
215
- expect(mockNext).toHaveBeenCalledWith(mockError);
216
- });
217
- });
218
-
219
- describe('deleteBookmark', () => {
220
- it('should return 204 with no content', async () => {
221
- mockRequest = {
222
- params: { id: '1' },
223
- };
224
-
225
- mockedBookmarkService.deleteBookmark.mockResolvedValue(undefined);
226
-
227
- await deleteBookmark(
228
- mockRequest as Request,
229
- mockResponse as Response,
230
- mockNext
231
- );
232
-
233
- expect(mockedBookmarkService.deleteBookmark).toHaveBeenCalledWith('1');
234
- expect(mockResponse.status).toHaveBeenCalledWith(204);
235
- expect(mockResponse.send).toHaveBeenCalled();
236
- });
237
-
238
- it('should call next with 404 when bookmark not found', async () => {
239
- const mockError = new AppError('Bookmark not found', 404, 'NOT_FOUND');
240
- mockRequest = {
241
- params: { id: 'non-existent-id' },
242
- };
243
-
244
- mockedBookmarkService.deleteBookmark.mockRejectedValue(mockError);
245
-
246
- await deleteBookmark(
247
- mockRequest as Request,
248
- mockResponse as Response,
249
- mockNext
250
- );
251
-
252
- expect(mockNext).toHaveBeenCalledWith(mockError);
253
- });
254
- });
255
- });
@@ -1,187 +0,0 @@
1
- import { Request, Response, NextFunction } from 'express';
2
- import { AppError } from '../middleware/error.middleware';
3
- import * as bookmarkService from '../services/bookmark.service';
4
-
5
- interface BookmarkQueryParams {
6
- limit?: string;
7
- offset?: string;
8
- }
9
-
10
- interface CreateBookmarkBody {
11
- title?: string;
12
- url?: string;
13
- tags?: string[];
14
- }
15
-
16
- interface UpdateBookmarkBody {
17
- title?: string;
18
- url?: string;
19
- tags?: string[];
20
- }
21
-
22
- interface BookmarkParams {
23
- id?: string;
24
- }
25
-
26
- export class BookmarkController {
27
- static async getBookmarks(
28
- req: Request<{}, {}, {}, BookmarkQueryParams>,
29
- res: Response,
30
- next: NextFunction
31
- ) {
32
- try {
33
- const { limit: limitStr, offset: offsetStr } = req.query;
34
-
35
- const limit = limitStr ? parseInt(limitStr, 10) : 20;
36
- const offset = offsetStr ? parseInt(offsetStr, 10) : 0;
37
-
38
- if (isNaN(limit) || limit <= 0) {
39
- throw new AppError(400, '"limit" must be a positive integer');
40
- }
41
-
42
- if (isNaN(offset) || offset < 0) {
43
- throw new AppError(400, '"offset" must be a non-negative integer');
44
- }
45
-
46
- const result = await bookmarkService.getBookmarks(limit, offset);
47
-
48
- res.status(200).json({
49
- code: 200,
50
- message: 'success',
51
- data: {
52
- bookmarks: result.bookmarks,
53
- total: result.total
54
- }
55
- });
56
- } catch (error) {
57
- next(error);
58
- }
59
- }
60
-
61
- static async createBookmark(
62
- req: Request<{}, {}, CreateBookmarkBody>,
63
- res: Response,
64
- next: NextFunction
65
- ) {
66
- try {
67
- const { title, url, tags } = req.body;
68
-
69
- if (!title || typeof title !== 'string' || title.trim() === '') {
70
- throw new AppError(400, '"title" is required and must be a non-empty string');
71
- }
72
-
73
- if (!url || typeof url !== 'string') {
74
- throw new AppError(400, '"url" is required and must be a string');
75
- }
76
-
77
- try {
78
- new URL(url);
79
- } catch {
80
- throw new AppError(400, '"url" must be a valid URL');
81
- }
82
-
83
- if (tags !== undefined && !Array.isArray(tags)) {
84
- throw new AppError(400, '"tags" must be an array of strings');
85
- }
86
-
87
- if (tags && !tags.every(tag => typeof tag === 'string')) {
88
- throw new AppError(400, '"tags" must be an array of strings');
89
- }
90
-
91
- const bookmark = await bookmarkService.createBookmark({
92
- title: title.trim(),
93
- url: url.trim(),
94
- tags: tags || []
95
- });
96
-
97
- res.status(201).json({
98
- code: 201,
99
- message: 'success',
100
- data: bookmark
101
- });
102
- } catch (error) {
103
- next(error);
104
- }
105
- }
106
-
107
- static async updateBookmark(
108
- req: Request<BookmarkParams, {}, UpdateBookmarkBody>,
109
- res: Response,
110
- next: NextFunction
111
- ) {
112
- try {
113
- const { id } = req.params;
114
- const { title, url, tags } = req.body;
115
-
116
- if (!id) {
117
- throw new AppError(400, '"id" parameter is required');
118
- }
119
-
120
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
121
- if (!uuidRegex.test(id)) {
122
- throw new AppError(400, '"id" must be a valid UUID');
123
- }
124
-
125
- if (!title || typeof title !== 'string' || title.trim() === '') {
126
- throw new AppError(400, '"title" is required and must be a non-empty string');
127
- }
128
-
129
- if (!url || typeof url !== 'string') {
130
- throw new AppError(400, '"url" is required and must be a string');
131
- }
132
-
133
- try {
134
- new URL(url);
135
- } catch {
136
- throw new AppError(400, '"url" must be a valid URL');
137
- }
138
-
139
- if (!tags || !Array.isArray(tags)) {
140
- throw new AppError(400, '"tags" is required and must be an array of strings');
141
- }
142
-
143
- if (!tags.every(tag => typeof tag === 'string')) {
144
- throw new AppError(400, '"tags" must be an array of strings');
145
- }
146
-
147
- const bookmark = await bookmarkService.updateBookmark(id, {
148
- title: title.trim(),
149
- url: url.trim(),
150
- tags
151
- });
152
-
153
- res.status(200).json({
154
- code: 200,
155
- message: 'success',
156
- data: bookmark
157
- });
158
- } catch (error) {
159
- next(error);
160
- }
161
- }
162
-
163
- static async deleteBookmark(
164
- req: Request<BookmarkParams>,
165
- res: Response,
166
- next: NextFunction
167
- ) {
168
- try {
169
- const { id } = req.params;
170
-
171
- if (!id) {
172
- throw new AppError(400, '"id" parameter is required');
173
- }
174
-
175
- const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
176
- if (!uuidRegex.test(id)) {
177
- throw new AppError(400, '"id" must be a valid UUID');
178
- }
179
-
180
- await bookmarkService.deleteBookmark(id);
181
-
182
- res.status(204).send();
183
- } catch (error) {
184
- next(error);
185
- }
186
- }
187
- }
@@ -1,17 +0,0 @@
1
- import express from "express";
2
- import cors from "cors";
3
-
4
- const app = express();
5
- app.use(cors());
6
- app.use(express.json());
7
-
8
- app.get("/health", (_req, res) => {
9
- res.json({ status: "ok" });
10
- });
11
-
12
- const PORT = process.env.PORT || 3001;
13
- app.listen(PORT, () => {
14
- console.log(`Server running on http://localhost:${PORT}`);
15
- });
16
-
17
- export default app;