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.
- package/README.md +33 -17
- package/cli/commands/create.ts +232 -11
- package/cli/commands/init.ts +310 -107
- package/cli/commands/model.ts +7 -11
- package/cli/index.ts +1 -1
- package/cli/utils.ts +72 -4
- package/core/config-defaults.ts +44 -0
- package/core/constitution-generator.ts +2 -1
- package/core/dsl-extractor.ts +2 -1
- package/core/error-feedback.ts +3 -2
- package/core/openapi-exporter.ts +3 -2
- package/core/repo-store.ts +95 -0
- package/core/reviewer.ts +14 -13
- package/core/run-logger.ts +3 -4
- package/core/run-snapshot.ts +2 -3
- package/core/run-trend.ts +3 -4
- package/core/spec-generator.ts +27 -42
- package/core/token-budget.ts +3 -8
- package/core/vcr.ts +3 -1
- package/dist/cli/index.js +919 -519
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +912 -512
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +43 -53
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +43 -53
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/demo-backend/.ai-spec-constitution.md +0 -65
- package/demo-backend/package.json +0 -21
- package/demo-backend/prisma/schema.prisma +0 -22
- package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json +0 -186
- package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.md +0 -211
- package/demo-backend/src/controllers/bookmark.controller.test.ts +0 -255
- package/demo-backend/src/controllers/bookmark.controller.ts +0 -187
- package/demo-backend/src/index.ts +0 -17
- package/demo-backend/src/routes/bookmark.routes.test.ts +0 -264
- package/demo-backend/src/routes/bookmark.routes.ts +0 -11
- package/demo-backend/src/routes/index.ts +0 -8
- package/demo-backend/src/services/bookmark.service.test.ts +0 -433
- package/demo-backend/src/services/bookmark.service.ts +0 -261
- package/demo-backend/tsconfig.json +0 -12
- package/demo-frontend/.ai-spec-constitution.md +0 -95
- package/demo-frontend/package.json +0 -23
- package/demo-frontend/src/App.tsx +0 -12
- package/demo-frontend/src/main.tsx +0 -9
- 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;
|