ai-spec-dev 0.42.0 → 0.55.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 (70) hide show
  1. package/README.md +86 -40
  2. package/cli/commands/config.ts +129 -1
  3. package/cli/commands/create.ts +246 -11
  4. package/cli/commands/fix-history.ts +176 -0
  5. package/cli/commands/init.ts +344 -106
  6. package/cli/index.ts +3 -7
  7. package/cli/pipeline/helpers.ts +6 -0
  8. package/cli/pipeline/multi-repo.ts +291 -26
  9. package/cli/pipeline/single-repo.ts +103 -2
  10. package/cli/utils.ts +95 -4
  11. package/core/code-generator.ts +63 -14
  12. package/core/config-defaults.ts +44 -0
  13. package/core/constitution-generator.ts +2 -1
  14. package/core/cross-stack-verifier.ts +395 -0
  15. package/core/dsl-extractor.ts +2 -1
  16. package/core/error-feedback.ts +3 -2
  17. package/core/fix-history.ts +333 -0
  18. package/core/import-fixer.ts +827 -0
  19. package/core/import-verifier.ts +569 -0
  20. package/core/knowledge-memory.ts +55 -6
  21. package/core/openapi-exporter.ts +3 -2
  22. package/core/repo-store.ts +95 -0
  23. package/core/reviewer.ts +14 -13
  24. package/core/run-logger.ts +3 -4
  25. package/core/run-snapshot.ts +2 -3
  26. package/core/run-trend.ts +3 -4
  27. package/core/self-evaluator.ts +44 -7
  28. package/core/spec-generator.ts +30 -45
  29. package/core/token-budget.ts +3 -8
  30. package/core/types-generator.ts +2 -2
  31. package/core/vcr.ts +3 -1
  32. package/dist/cli/index.js +3889 -1937
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/index.mjs +3888 -1936
  35. package/dist/cli/index.mjs.map +1 -1
  36. package/dist/index.d.mts +17 -2
  37. package/dist/index.d.ts +17 -2
  38. package/dist/index.js +292 -181
  39. package/dist/index.js.map +1 -1
  40. package/dist/index.mjs +292 -181
  41. package/dist/index.mjs.map +1 -1
  42. package/package.json +2 -2
  43. package/tests/cross-stack-verifier.test.ts +301 -0
  44. package/tests/fix-history.test.ts +335 -0
  45. package/tests/import-fixer.test.ts +944 -0
  46. package/tests/import-verifier.test.ts +420 -0
  47. package/tests/knowledge-memory.test.ts +40 -0
  48. package/tests/self-evaluator.test.ts +97 -0
  49. package/cli/commands/model.ts +0 -156
  50. package/cli/commands/scan.ts +0 -99
  51. package/cli/commands/workspace.ts +0 -219
  52. package/demo-backend/.ai-spec-constitution.md +0 -65
  53. package/demo-backend/package.json +0 -21
  54. package/demo-backend/prisma/schema.prisma +0 -22
  55. package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json +0 -186
  56. package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.md +0 -211
  57. package/demo-backend/src/controllers/bookmark.controller.test.ts +0 -255
  58. package/demo-backend/src/controllers/bookmark.controller.ts +0 -187
  59. package/demo-backend/src/index.ts +0 -17
  60. package/demo-backend/src/routes/bookmark.routes.test.ts +0 -264
  61. package/demo-backend/src/routes/bookmark.routes.ts +0 -11
  62. package/demo-backend/src/routes/index.ts +0 -8
  63. package/demo-backend/src/services/bookmark.service.test.ts +0 -433
  64. package/demo-backend/src/services/bookmark.service.ts +0 -261
  65. package/demo-backend/tsconfig.json +0 -12
  66. package/demo-frontend/.ai-spec-constitution.md +0 -95
  67. package/demo-frontend/package.json +0 -23
  68. package/demo-frontend/src/App.tsx +0 -12
  69. package/demo-frontend/src/main.tsx +0 -9
  70. package/demo-frontend/tsconfig.json +0 -13
@@ -1,156 +0,0 @@
1
- import { Command } from "commander";
2
- import * as path from "path";
3
- import * as fs from "fs-extra";
4
- import chalk from "chalk";
5
- import { input, select, confirm } from "@inquirer/prompts";
6
- import {
7
- DEFAULT_MODELS,
8
- ENV_KEY_MAP,
9
- PROVIDER_CATALOG,
10
- } from "../../core/spec-generator";
11
- import { AiSpecConfig, CONFIG_FILE, loadConfig } from "../utils";
12
-
13
- export function registerModel(program: Command): void {
14
- program
15
- .command("model")
16
- .description("Interactively switch the active AI provider/model and save to .ai-spec.json")
17
- .option("--list", "List all available providers and models")
18
- .action(async (opts) => {
19
- const currentDir = process.cwd();
20
- const configPath = path.join(currentDir, CONFIG_FILE);
21
-
22
- // ── --list ──────────────────────────────────────────────────────────────
23
- if (opts.list) {
24
- console.log(chalk.bold("\nAvailable providers & models:\n"));
25
- for (const [key, meta] of Object.entries(PROVIDER_CATALOG)) {
26
- console.log(
27
- ` ${chalk.bold.cyan(key.padEnd(10))} ${chalk.white(meta.displayName)}`
28
- );
29
- console.log(chalk.gray(` ${meta.description}`));
30
- console.log(
31
- chalk.gray(
32
- ` env: ${meta.envKey} | models: ${meta.models.join(", ")}`
33
- )
34
- );
35
- console.log();
36
- }
37
- return;
38
- }
39
-
40
- const existing: AiSpecConfig = await loadConfig(currentDir);
41
-
42
- console.log(chalk.blue("\n─── Model Switcher ─────────────────────────────"));
43
- if (Object.keys(existing).length > 0) {
44
- console.log(
45
- chalk.gray(
46
- ` Current: spec=${existing.provider ?? "gemini"}/${existing.model ?? DEFAULT_MODELS[existing.provider ?? "gemini"]}` +
47
- (existing.codegenProvider
48
- ? ` codegen=${existing.codegenProvider}/${existing.codegenModel ?? ""}`
49
- : "")
50
- )
51
- );
52
- }
53
- console.log();
54
-
55
- const target = await select({
56
- message: "Configure model for:",
57
- choices: [
58
- { name: "Spec generation (used for spec writing & refinement)", value: "spec" },
59
- { name: "Code generation (used when --codegen api is active)", value: "codegen" },
60
- { name: "Both (same provider/model for all tasks)", value: "both" },
61
- ],
62
- });
63
-
64
- async function pickProviderAndModel(label: string): Promise<{ provider: string; model: string }> {
65
- const providerKey = await select({
66
- message: `${label} — select provider:`,
67
- choices: Object.entries(PROVIDER_CATALOG).map(([key, meta]) => ({
68
- name: `${meta.displayName.padEnd(22)} ${chalk.gray(meta.description)}`,
69
- value: key,
70
- short: meta.displayName,
71
- })),
72
- });
73
-
74
- const meta = PROVIDER_CATALOG[providerKey];
75
- const modelChoices = [
76
- ...meta.models.map((m) => ({ name: m, value: m })),
77
- { name: chalk.italic("✎ Enter custom model name..."), value: "__custom__" },
78
- ];
79
-
80
- let chosenModel = await select({
81
- message: `${label} — select model (${meta.displayName}):`,
82
- choices: modelChoices,
83
- });
84
-
85
- if (chosenModel === "__custom__") {
86
- chosenModel = await input({
87
- message: "Enter model name:",
88
- validate: (v) => v.trim().length > 0 || "Model name cannot be empty",
89
- });
90
- }
91
-
92
- return { provider: providerKey, model: chosenModel };
93
- }
94
-
95
- const updated: AiSpecConfig = { ...existing };
96
-
97
- if (target === "spec" || target === "both") {
98
- const { provider, model } = await pickProviderAndModel("Spec");
99
- updated.provider = provider;
100
- updated.model = model;
101
- }
102
-
103
- if (target === "codegen" || target === "both") {
104
- if (target === "both") {
105
- updated.codegenProvider = updated.provider;
106
- updated.codegenModel = updated.model;
107
- } else {
108
- const { provider, model } = await pickProviderAndModel("Codegen");
109
- updated.codegenProvider = provider;
110
- updated.codegenModel = model;
111
- }
112
-
113
- const effectiveCodegenProvider = updated.codegenProvider ?? updated.provider ?? "gemini";
114
- if (effectiveCodegenProvider !== "claude") {
115
- if (!updated.codegen || updated.codegen === "claude-code") {
116
- updated.codegen = "api";
117
- console.log(
118
- chalk.yellow(
119
- `\n ⚠ provider "${effectiveCodegenProvider}" 不支持 "claude-code" 模式。`
120
- )
121
- );
122
- console.log(chalk.gray(` 已自动将 codegen 模式设为 "api"。`));
123
- }
124
- }
125
- }
126
-
127
- console.log(chalk.blue("\n Preview:"));
128
- console.log(chalk.gray(` spec → ${updated.provider}/${updated.model}`));
129
- if (updated.codegenProvider) {
130
- console.log(
131
- chalk.gray(
132
- ` codegen → ${updated.codegenProvider}/${updated.codegenModel} (mode: ${updated.codegen ?? "claude-code"})`
133
- )
134
- );
135
- }
136
-
137
- const ok = await confirm({ message: "Save to .ai-spec.json?", default: true });
138
- if (!ok) {
139
- console.log(chalk.gray(" Cancelled."));
140
- return;
141
- }
142
-
143
- await fs.writeJson(configPath, updated, { spaces: 2 });
144
- console.log(chalk.green(`\n ✔ Saved to ${configPath}`));
145
-
146
- const providerToCheck = updated.provider ?? "gemini";
147
- const envKey = ENV_KEY_MAP[providerToCheck];
148
- if (envKey && !process.env[envKey]) {
149
- console.log(
150
- chalk.yellow(
151
- ` ⚠ Remember to set ${envKey} in your environment or .env file.`
152
- )
153
- );
154
- }
155
- });
156
- }
@@ -1,99 +0,0 @@
1
- import { Command } from "commander";
2
- import chalk from "chalk";
3
- import * as path from "path";
4
- import { runScan, saveIndex, loadIndex, INDEX_FILE, ProjectEntry } from "../../core/project-index";
5
-
6
- const ROLE_COLOR: Record<string, (s: string) => string> = {
7
- backend: chalk.blue,
8
- frontend: chalk.green,
9
- mobile: chalk.magenta,
10
- shared: chalk.gray,
11
- };
12
-
13
- function formatEntry(entry: ProjectEntry): string {
14
- const roleColor = ROLE_COLOR[entry.role] ?? chalk.white;
15
- const role = roleColor(entry.role.padEnd(8));
16
- const type = chalk.gray(entry.type.padEnd(14));
17
- const name = (entry.missing ? chalk.strikethrough.gray : chalk.white)(entry.path.padEnd(30));
18
- const badges: string[] = [];
19
- if (entry.hasConstitution) badges.push(chalk.cyan("§C"));
20
- if (entry.hasWorkspace) badges.push(chalk.yellow("W"));
21
- if (entry.missing) badges.push(chalk.red("missing"));
22
- const stack = chalk.gray(entry.techStack.slice(0, 5).join(", "));
23
- return ` ${name} ${role} ${type} ${badges.join(" ")} ${stack}`;
24
- }
25
-
26
- export function registerScan(program: Command): void {
27
- program
28
- .command("scan")
29
- .description("Discover and index all projects under the current directory")
30
- .option("-d, --depth <n>", "Max directory depth to search", "2")
31
- .option("--list", "Just print the current index without rescanning")
32
- .action(async (opts) => {
33
- const cwd = process.cwd();
34
-
35
- // ── List mode ─────────────────────────────────────────────────────────
36
- if (opts.list) {
37
- const existing = await loadIndex(cwd);
38
- if (!existing || existing.projects.length === 0) {
39
- console.log(chalk.gray("No index found. Run: ai-spec scan"));
40
- return;
41
- }
42
-
43
- console.log(chalk.cyan(`\n─── Project Index (${existing.projects.length} projects) ─────────────────────────────`));
44
- console.log(chalk.gray(` Last scanned : ${existing.lastScanned.slice(0, 19).replace("T", " ")}`));
45
- console.log(chalk.gray(` Root : ${existing.scanRoot}\n`));
46
-
47
- const active = existing.projects.filter((p) => !p.missing);
48
- const missing = existing.projects.filter((p) => p.missing);
49
-
50
- for (const entry of active) {
51
- console.log(formatEntry(entry));
52
- }
53
- if (missing.length > 0) {
54
- console.log(chalk.gray(`\n (${missing.length} previously seen, now missing)`));
55
- for (const entry of missing) {
56
- console.log(formatEntry(entry));
57
- }
58
- }
59
-
60
- console.log(chalk.cyan("\n─".repeat(52)));
61
- console.log(chalk.gray(" §C = has constitution W = workspace root"));
62
- console.log(chalk.gray(` Index file: ${INDEX_FILE}`));
63
- return;
64
- }
65
-
66
- // ── Scan mode ─────────────────────────────────────────────────────────
67
- const maxDepth = parseInt(opts.depth, 10);
68
- console.log(chalk.blue(`\nScanning ${cwd} (depth: ${maxDepth})...`));
69
-
70
- const { index, added, updated, unchanged, nowMissing } = await runScan(cwd, maxDepth);
71
- await saveIndex(cwd, index);
72
-
73
- const active = index.projects.filter((p) => !p.missing);
74
-
75
- // ── Summary ───────────────────────────────────────────────────────────
76
- console.log(chalk.cyan(`\n─── Scan Results ────────────────────────────────────`));
77
- if (added.length > 0) console.log(chalk.green(` + ${added.length} new project(s) added`));
78
- if (updated.length > 0) console.log(chalk.yellow(` ~ ${updated.length} project(s) updated`));
79
- if (unchanged.length > 0) console.log(chalk.gray(` · ${unchanged.length} project(s) unchanged`));
80
- if (nowMissing.length > 0) console.log(chalk.red(` ✘ ${nowMissing.length} project(s) no longer found (marked missing)`));
81
-
82
- if (added.length === 0 && updated.length === 0 && nowMissing.length === 0) {
83
- console.log(chalk.gray(" Nothing changed."));
84
- }
85
-
86
- // ── Full listing ──────────────────────────────────────────────────────
87
- if (active.length > 0) {
88
- console.log(chalk.cyan(`\n Projects (${active.length}):`));
89
- for (const entry of active) {
90
- console.log(formatEntry(entry));
91
- }
92
- }
93
-
94
- console.log(chalk.cyan("\n─".repeat(52)));
95
- console.log(chalk.gray(" §C = has constitution W = workspace root"));
96
- console.log(chalk.gray(` Index saved : ${path.relative(cwd, path.join(cwd, INDEX_FILE))}`));
97
- console.log(chalk.gray(` Next steps : ai-spec scan --list | ai-spec init [--global]`));
98
- });
99
- }
@@ -1,219 +0,0 @@
1
- import { Command } from "commander";
2
- import * as path from "path";
3
- import * as fs from "fs-extra";
4
- import chalk from "chalk";
5
- import { input, select, confirm } from "@inquirer/prompts";
6
- import {
7
- WorkspaceLoader,
8
- WorkspaceConfig,
9
- RepoConfig,
10
- WORKSPACE_CONFIG_FILE,
11
- detectRepoType,
12
- } from "../../core/workspace-loader";
13
-
14
- export function registerWorkspace(program: Command): void {
15
- const workspaceCmd = program
16
- .command("workspace")
17
- .description("Manage multi-repo workspace configuration");
18
-
19
- // ── workspace init ──────────────────────────────────────────────────────────
20
- workspaceCmd
21
- .command("init")
22
- .description(`Interactive workspace setup — creates ${WORKSPACE_CONFIG_FILE}`)
23
- .action(async () => {
24
- const currentDir = process.cwd();
25
- const configPath = path.join(currentDir, WORKSPACE_CONFIG_FILE);
26
-
27
- if (await fs.pathExists(configPath)) {
28
- const overwrite = await confirm({
29
- message: `${WORKSPACE_CONFIG_FILE} already exists. Overwrite?`,
30
- default: false,
31
- });
32
- if (!overwrite) {
33
- console.log(chalk.gray(" Cancelled."));
34
- return;
35
- }
36
- }
37
-
38
- console.log(chalk.blue("\n─── Workspace Setup ────────────────────────────"));
39
-
40
- const workspaceName = await input({
41
- message: "Workspace name:",
42
- validate: (v) => v.trim().length > 0 || "Name cannot be empty",
43
- });
44
-
45
- const repos: RepoConfig[] = [];
46
-
47
- const useAutoScan = await confirm({
48
- message: "Auto-scan sibling directories for repos?",
49
- default: true,
50
- });
51
-
52
- if (useAutoScan) {
53
- const workspaceLoader = new WorkspaceLoader(currentDir);
54
- const detected = await workspaceLoader.autoDetect();
55
-
56
- if (detected.length === 0) {
57
- console.log(chalk.yellow(" No recognizable repos found in sibling directories."));
58
- } else {
59
- console.log(chalk.cyan("\n Detected repos:"));
60
- for (const r of detected) {
61
- console.log(chalk.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
62
- }
63
-
64
- const keepAll = await confirm({
65
- message: `Include all ${detected.length} detected repo(s)?`,
66
- default: true,
67
- });
68
-
69
- if (keepAll) {
70
- repos.push(...detected);
71
- } else {
72
- for (const r of detected) {
73
- const keep = await confirm({
74
- message: `Include "${r.name}" (${r.role}, ${r.type})?`,
75
- default: true,
76
- });
77
- if (keep) repos.push(r);
78
- }
79
- }
80
- console.log(chalk.green(` ✔ ${repos.length} repo(s) added from auto-scan.`));
81
- }
82
- }
83
-
84
- const repoTypeChoices = [
85
- { name: "node-express (Node.js/Express backend)", value: "node-express" },
86
- { name: "node-koa (Node.js/Koa backend)", value: "node-koa" },
87
- { name: "go (Go backend)", value: "go" },
88
- { name: "python (Python backend)", value: "python" },
89
- { name: "java (Java/Spring backend)", value: "java" },
90
- { name: "rust (Rust backend)", value: "rust" },
91
- { name: "php (PHP/Lumen/Laravel backend)", value: "php" },
92
- { name: "react (React frontend)", value: "react" },
93
- { name: "next (Next.js)", value: "next" },
94
- { name: "vue (Vue frontend)", value: "vue" },
95
- { name: "react-native (React Native mobile)", value: "react-native" },
96
- { name: "unknown", value: "unknown" },
97
- ];
98
-
99
- let addMore = await confirm({
100
- message: repos.length > 0 ? "Manually add more repos?" : "Add repos manually?",
101
- default: repos.length === 0,
102
- });
103
-
104
- while (addMore) {
105
- console.log(chalk.cyan(`\n Adding repo #${repos.length + 1}`));
106
-
107
- const repoName = await input({
108
- message: "Repo name (e.g. api, web, app):",
109
- validate: (v) => {
110
- if (!v.trim()) return "Name cannot be empty";
111
- if (repos.some((r) => r.name === v.trim())) return "Name already used";
112
- return true;
113
- },
114
- });
115
-
116
- const repoPath = await input({
117
- message: `Relative path to "${repoName}" from here (default: ./${repoName}):`,
118
- default: `./${repoName}`,
119
- });
120
-
121
- const absPath = path.resolve(currentDir, repoPath);
122
- let detectedType = "unknown";
123
- let detectedRole = "shared";
124
-
125
- if (await fs.pathExists(absPath)) {
126
- const { type, role } = await detectRepoType(absPath);
127
- detectedType = type;
128
- detectedRole = role;
129
- console.log(chalk.gray(` Auto-detected: type=${type}, role=${role}`));
130
- } else {
131
- console.log(chalk.yellow(` Path "${absPath}" not found — type/role will be manual.`));
132
- }
133
-
134
- const repoType = await select({
135
- message: `Repo type for "${repoName}":`,
136
- choices: repoTypeChoices,
137
- default: detectedType,
138
- });
139
-
140
- const repoRole = await select({
141
- message: `Repo role for "${repoName}":`,
142
- choices: [
143
- { name: "backend", value: "backend" },
144
- { name: "frontend", value: "frontend" },
145
- { name: "mobile", value: "mobile" },
146
- { name: "shared", value: "shared" },
147
- ],
148
- default: detectedRole,
149
- });
150
-
151
- repos.push({
152
- name: repoName,
153
- path: repoPath,
154
- type: repoType as RepoConfig["type"],
155
- role: repoRole as RepoConfig["role"],
156
- });
157
-
158
- console.log(chalk.green(` ✔ Added: ${repoName} (${repoRole}, ${repoType})`));
159
-
160
- addMore = await confirm({
161
- message: "Add another repo?",
162
- default: false,
163
- });
164
- }
165
-
166
- const workspaceConfig: WorkspaceConfig = { name: workspaceName, repos };
167
-
168
- console.log(chalk.cyan("\n Workspace summary:"));
169
- console.log(chalk.gray(` Name: ${workspaceName}`));
170
- for (const r of repos) {
171
- console.log(chalk.gray(` - ${r.name}: ${r.role} (${r.type}) at ${r.path}`));
172
- }
173
-
174
- const ok = await confirm({ message: `Save to ${WORKSPACE_CONFIG_FILE}?`, default: true });
175
- if (!ok) {
176
- console.log(chalk.gray(" Cancelled."));
177
- return;
178
- }
179
-
180
- const loader = new WorkspaceLoader(currentDir);
181
- const saved = await loader.save(workspaceConfig);
182
- console.log(chalk.green(`\n ✔ Workspace saved: ${saved}`));
183
- console.log(chalk.gray(` Run \`ai-spec create "your feature"\` — workspace mode will activate automatically.`));
184
- });
185
-
186
- // ── workspace status ────────────────────────────────────────────────────────
187
- workspaceCmd
188
- .command("status")
189
- .description("Show current workspace configuration")
190
- .action(async () => {
191
- const currentDir = process.cwd();
192
- const loader = new WorkspaceLoader(currentDir);
193
- const config = await loader.load();
194
-
195
- if (!config) {
196
- console.log(chalk.yellow(`No ${WORKSPACE_CONFIG_FILE} found in ${currentDir}`));
197
- console.log(chalk.gray(" Run `ai-spec workspace init` to create one."));
198
- return;
199
- }
200
-
201
- console.log(chalk.bold(`\nWorkspace: ${config.name}`));
202
- console.log(chalk.gray(` Config: ${path.join(currentDir, WORKSPACE_CONFIG_FILE)}`));
203
- console.log(chalk.gray(` Repos (${config.repos.length}):\n`));
204
-
205
- for (const repo of config.repos) {
206
- const absPath = loader.resolveAbsPath(repo);
207
- const exists = await fs.pathExists(absPath);
208
- const status = exists ? chalk.green("found") : chalk.red("not found");
209
-
210
- console.log(
211
- ` ${chalk.bold(repo.name.padEnd(12))} ${repo.role.padEnd(10)} ${repo.type.padEnd(16)} ${status}`
212
- );
213
- console.log(chalk.gray(` path: ${absPath}`));
214
- if (repo.constitution) {
215
- console.log(chalk.green(` constitution: found`));
216
- }
217
- }
218
- });
219
- }
@@ -1,65 +0,0 @@
1
- # Project Constitution
2
-
3
- ## 1. 架构规则 (Architecture Rules)
4
- - **分层架构**:项目采用三层架构 `routes → controllers → services → database`。
5
- - **禁止跨层调用**:
6
- - `routes` 层仅负责定义路由和中间件,不能包含业务逻辑。
7
- - `controllers` 层负责接收请求、调用 `services` 和返回响应,不能直接操作数据库。
8
- - `services` 层封装核心业务逻辑,是唯一允许直接调用数据访问层(如 Prisma)的层级。
9
- - **模块组织**:功能模块按领域划分,每个模块包含独立的 `routes.ts`, `controller.ts`, `service.ts` 文件。
10
-
11
- ## 2. 命名规范 (Naming Conventions)
12
- - **文件命名**:使用 `kebab-case`(如 `user-profile.controller.ts`)。
13
- - **变量/函数**:使用 `camelCase`(如 `getUserById`)。
14
- - **类/接口**:使用 `PascalCase`(如 `UserService`)。
15
- - **路由路径**:使用 `kebab-case`,资源名词复数(如 `/api/v1/user-profiles`)。
16
-
17
- ## 3. API 规范 (API Patterns)
18
- - **路由前缀**:
19
- - 客户端 API:`/api/v1/...`
20
- - 管理端 API:`/api/v1/admin/...`
21
- - **统一响应结构**:
22
- ```json
23
- {
24
- "code": 200,
25
- "message": "success",
26
- "data": {}
27
- }
28
- ```
29
- - **错误码规范**:
30
- - `400`:客户端请求错误(参数错误、验证失败)
31
- - `401`:未认证
32
- - `403`:无权限
33
- - `404`:资源不存在
34
- - `500`:服务器内部错误
35
- - **认证/鉴权**:使用 `auth.middleware.ts` 保护需要认证的路由,位于 `middleware/` 目录。
36
-
37
- ## 4. 数据层规范 (Data Layer Rules)
38
- - **数据库访问**:仅允许在 `service` 层使用 Prisma Client 进行数据库操作。
39
- - **模型命名**:Prisma model 使用 `PascalCase`(如 `UserProfile`),表名使用 `snake_case`(如 `user_profiles`)。
40
- - **事务处理**:复杂业务逻辑使用 Prisma 的 `$transaction` API 保证数据一致性。
41
-
42
- ## 5. 错误处理规范 (Error Handling Patterns)
43
- - **统一错误中间件**:使用 `error.middleware.ts` 捕获所有错误,位于 `middleware/` 目录。
44
- - **错误抛出**:在 `service` 或 `controller` 中抛出 `AppError`(自定义错误类,包含 `code` 和 `message`)。
45
- - **已知错误码**:
46
- - `VALIDATION_ERROR` (400)
47
- - `UNAUTHORIZED` (401)
48
- - `FORBIDDEN` (403)
49
- - `NOT_FOUND` (404)
50
- - `INTERNAL_ERROR` (500)
51
-
52
- ## 6. 禁区 (Red Lines — Never Violate)
53
- - [ ] **禁止**在 `controller` 或 `route` 层直接操作数据库(必须通过 `service`)。
54
- - [ ] **禁止**创建重复的配置文件(如错误码、路由定义),必须追加到现有文件。
55
- - [ ] **禁止**使用 `any` 类型,必须定义明确的接口或类型。
56
- - [ ] **禁止**提交未通过 `vitest` 测试的代码。
57
-
58
- ## 7. 测试规范 (Testing Rules)
59
- - **测试文件位置**:与源文件同目录,命名规则为 `*.test.ts` 或 `*.spec.ts`。
60
- - **测试覆盖**:必须为所有 `service` 和 `controller` 方法编写单元测试,关键业务流程需包含集成测试。
61
- - **测试框架**:使用 `vitest` 进行单元测试和集成测试,使用 `supertest` 进行 HTTP 接口测试。
62
-
63
- ## 8. 共享配置文件清单 (Shared Config Files — Append-Only)
64
-
65
- (No shared config files detected — will be populated on first run)
@@ -1,21 +0,0 @@
1
- {
2
- "name": "demo-backend",
3
- "version": "0.1.0",
4
- "private": true,
5
- "scripts": {
6
- "dev": "ts-node src/index.ts",
7
- "build": "tsc",
8
- "test": "vitest run"
9
- },
10
- "dependencies": {
11
- "express": "^4.18.2",
12
- "cors": "^2.8.5"
13
- },
14
- "devDependencies": {
15
- "typescript": "^5.7.0",
16
- "@types/express": "^4.17.21",
17
- "@types/cors": "^2.8.17",
18
- "ts-node": "^10.9.2",
19
- "vitest": "^2.1.0"
20
- }
21
- }
@@ -1,22 +0,0 @@
1
- // This is your Prisma schema file,
2
- // learn more about it in the docs: https://pris.ly/d/prisma-schema
3
-
4
- generator client {
5
- provider = "prisma-client-js"
6
- }
7
-
8
- datasource db {
9
- provider = "postgresql"
10
- url = env("DATABASE_URL")
11
- }
12
-
13
- model Bookmark {
14
- id String @id @default(uuid())
15
- title String
16
- url String
17
- tags String[]
18
- createdAt DateTime @default(now())
19
- updatedAt DateTime @updatedAt
20
-
21
- @@map("bookmarks")
22
- }