ai-spec-dev 0.29.1 → 0.30.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 +4 -3
- package/RELEASE_LOG.md +24 -0
- package/core/error-feedback.ts +102 -2
- package/core/frontend-context-loader.ts +117 -20
- package/dist/cli/index.js +113 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +113 -9
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.js +57 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +57 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/purpose.md +29 -4
package/README.md
CHANGED
|
@@ -1223,7 +1223,8 @@ Attempting auto-fix (3 error(s))...
|
|
|
1223
1223
|
✔ All checks passed after 2 cycle(s).
|
|
1224
1224
|
```
|
|
1225
1225
|
|
|
1226
|
-
-
|
|
1226
|
+
- 按文件分组错误,**依据 import 依赖图排序后**逐文件修复(被依赖文件先修,cascade 错误在 cycle 1 消除率更高)
|
|
1227
|
+
- 每个文件修复 prompt 携带 DSL 上下文,避免「把错误藏起来」式修复
|
|
1227
1228
|
- 任一 cycle 全部通过则提前结束
|
|
1228
1229
|
- 2 次后仍有错误 → 给出警告提示(不中断流水线)
|
|
1229
1230
|
|
|
@@ -1523,12 +1524,12 @@ ai-spec-dev-poc/
|
|
|
1523
1524
|
│ ├── combined-generator.ts # Spec + Tasks 合并单次 AI 调用
|
|
1524
1525
|
│ ├── reviewer.ts # AI 代码审查(git diff / 文件内容双模式)
|
|
1525
1526
|
│ ├── test-generator.ts # 测试骨架生成器(DSL → Jest/Vitest 骨架)
|
|
1526
|
-
│ ├── error-feedback.ts # 错误反馈自动修复(测试+lint检测 · AI修复循环)
|
|
1527
|
+
│ ├── error-feedback.ts # 错误反馈自动修复(测试+lint检测 · 依赖图排序修复 · AI修复循环)
|
|
1527
1528
|
│ ├── knowledge-memory.ts # 经验积累:审查 issue → 宪法§9
|
|
1528
1529
|
│ ├── workspace-loader.ts # [Phase 4] 工作区配置加载 + repo 类型自动检测
|
|
1529
1530
|
│ ├── requirement-decomposer.ts # [Phase 4] 需求跨 repo 拆分 + UX 决策生成
|
|
1530
1531
|
│ ├── contract-bridge.ts # [Phase 4] 后端 DSL → 前端 TS 接口契约桥接
|
|
1531
|
-
│ ├── frontend-context-loader.ts # [v0.8] 前端深度感知(hook/store/API封装/测试框架/分页 pattern
|
|
1532
|
+
│ ├── frontend-context-loader.ts # [v0.8] 前端深度感知(hook/store/API封装/测试框架/分页 pattern 检测;v0.30.0 升级为多行 import 解析,覆盖换行 named import 写法)
|
|
1532
1533
|
│ └── global-constitution.ts # [v0.8] 全局宪法:加载 / 合并 / 保存(跨项目共享规范)
|
|
1533
1534
|
├── git/
|
|
1534
1535
|
│ └── worktree.ts # Git Worktree 管理
|
package/RELEASE_LOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## [0.30.0] 2026-03-29 — 错误修复依赖图排序 + 前端 Import 多行感知解析
|
|
6
|
+
|
|
7
|
+
### 改进内容
|
|
8
|
+
|
|
9
|
+
**Improvement #1 — Error Repair 升级为依赖图排序(`core/error-feedback.ts`)**
|
|
10
|
+
|
|
11
|
+
- **问题**:`attemptFix()` 对出错文件的修复顺序完全取决于 `Map` 的插入顺序(即错误首次出现的顺序),与文件间的 import 关系无关。典型场景:`userService.ts` 导出一个类型,`userController.ts` 和 `userStore.ts` 同时 import 它,三个文件都出错时,若 controller/store 先被修复,修复 prompt 中的 service 仍是破损版本,cycle 1 无法消除 cascade 错误,只能等 cycle 2 补救。
|
|
12
|
+
- **修复**:新增 `parseRelativeImports()` + `buildRepairOrder()` 两个函数:
|
|
13
|
+
- `parseRelativeImports(content, fromFile)` — 从文件内容中解析相对 import 路径(`./foo`、`../foo`),规范化为项目根相对路径(不含扩展名),跳过 `import type`(仅类型,不影响运行时错误)
|
|
14
|
+
- `buildRepairOrder(errorsByFile, workingDir)` — 读取所有出错文件的 import 声明,构建「出错文件子图」,使用 Kahn 拓扑排序将被依赖的文件排在前面,有环依赖的文件追加到末尾
|
|
15
|
+
- `attemptFix()` 调用 `await buildRepairOrder()` 替换原先直接遍历 `errorsByFile`
|
|
16
|
+
- **效果**:上例中 `userService.ts` 先被修复,cycle 1 即可消除 controller / store 的 cascade 错误,2 轮上限的利用率提升,复杂跨文件依赖错误的一次性修复率提高
|
|
17
|
+
|
|
18
|
+
**Improvement #2 — `httpClientImport` / `layoutImport` 升级为多行感知解析(`core/frontend-context-loader.ts`)**
|
|
19
|
+
|
|
20
|
+
- **问题**:旧版 `httpClientImport` 提取使用单行正则 `httpImportRegex`,`[^}]+` 不跨换行符,导致所有多行 named import(`import {\n request\n} from '@/utils/http'`)静默匹配失败,回退到 `undefined`,AI 自由发挥 import 路径;`layoutImport` 同样依赖单行正则,多行动态 import 写法(`const Layout = defineAsyncComponent(\n () => import('...')\n)`)无法识别。
|
|
21
|
+
- **修复**:
|
|
22
|
+
- 新增 `parseImportStatements(content)` — 轻量 import 解析器:先将多行 named import block(`import { ... }`)折叠为单行,再逐行匹配,返回 `{ line, modulePath, specifiers }` 结构化对象,跳过 `import type`;不引入任何新依赖
|
|
23
|
+
- 新增 `HTTP_MODULE_PATTERNS` — 三类模式数组(路径别名 `@/` `~/` `#/` / 已知 HTTP 库名 / 含关键词的相对路径),独立于 import 行格式,匹配逻辑与解析逻辑解耦
|
|
24
|
+
- 新增 `findHttpClientImport(content)` — 组合以上两者,替换旧版 `httpImportRegex` 的调用处
|
|
25
|
+
- `extractRouteModuleContext()` 中 `layoutImport` 静态 import 改用 `parseImportStatements()` 提取;动态 import(`const Layout = ...`)增加多行折叠预处理后再匹配,覆盖 `defineAsyncComponent` 等包装写法
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
5
29
|
## [0.29.0] 2026-03-27 — 全量审查修复(RunLogger 插桩、update 快照、Score Trend 升级、死代码清理)
|
|
6
30
|
|
|
7
31
|
### 修复内容
|
package/core/error-feedback.ts
CHANGED
|
@@ -197,6 +197,104 @@ function parseErrors(output: string, source: ErrorEntry["source"]): ErrorEntry[]
|
|
|
197
197
|
return errors;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
// ─── Dependency-Ordered Repair ──────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Extract relative import paths from a file's content.
|
|
204
|
+
* Returns paths normalized to project-root-relative form (no extension).
|
|
205
|
+
* Only considers relative imports (./foo, ../foo) — skips aliases and node_modules.
|
|
206
|
+
*/
|
|
207
|
+
function parseRelativeImports(content: string, fromFileRel: string): string[] {
|
|
208
|
+
const relDir = path.dirname(fromFileRel);
|
|
209
|
+
const results: string[] = [];
|
|
210
|
+
|
|
211
|
+
// Normalize multi-line imports so `import {\n foo,\n} from '...'` becomes one line
|
|
212
|
+
const normalized = content.replace(/import\s*\{[^}]*\}/gs, (m) => m.replace(/\n\s*/g, " "));
|
|
213
|
+
|
|
214
|
+
for (const line of normalized.split("\n")) {
|
|
215
|
+
const trimmed = line.trim();
|
|
216
|
+
// Skip type-only imports — they don't affect runtime errors
|
|
217
|
+
if (/^import\s+type\b/.test(trimmed)) continue;
|
|
218
|
+
// Match only relative imports (starting with ./ or ../)
|
|
219
|
+
const match = trimmed.match(/^import\b[^'"]*from\s+['"](\.\.?\/[^'"]+)['"]/);
|
|
220
|
+
if (!match) continue;
|
|
221
|
+
|
|
222
|
+
const resolved = path.normalize(path.join(relDir, match[1]));
|
|
223
|
+
results.push(resolved);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return results;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Sort errored files so that dependencies (upstream exports) are fixed before their importers.
|
|
231
|
+
* Example: if A exports a type used by B and C, fix A first so cycle 1 can cascade correctly.
|
|
232
|
+
* Uses Kahn's topological sort; cycles are appended at the end (unavoidable, fix last).
|
|
233
|
+
*/
|
|
234
|
+
async function buildRepairOrder(
|
|
235
|
+
errorsByFile: Map<string, ErrorEntry[]>,
|
|
236
|
+
workingDir: string
|
|
237
|
+
): Promise<[string, ErrorEntry[]][]> {
|
|
238
|
+
const files = Array.from(errorsByFile.keys());
|
|
239
|
+
if (files.length <= 1) return Array.from(errorsByFile.entries());
|
|
240
|
+
|
|
241
|
+
// Build: file → errored files it depends on (imports from)
|
|
242
|
+
const deps = new Map<string, string[]>(files.map((f) => [f, []]));
|
|
243
|
+
|
|
244
|
+
for (const file of files) {
|
|
245
|
+
try {
|
|
246
|
+
const content = await fs.readFile(path.join(workingDir, file), "utf-8");
|
|
247
|
+
const importedPaths = parseRelativeImports(content, file);
|
|
248
|
+
|
|
249
|
+
for (const importedPath of importedPaths) {
|
|
250
|
+
const matched = files.find((f) => {
|
|
251
|
+
if (f === file) return false;
|
|
252
|
+
const fNoExt = f.replace(/\.[^.]+$/, "");
|
|
253
|
+
return (
|
|
254
|
+
importedPath === fNoExt ||
|
|
255
|
+
importedPath === f ||
|
|
256
|
+
`${importedPath}.ts` === f ||
|
|
257
|
+
`${importedPath}.tsx` === f ||
|
|
258
|
+
`${importedPath}.js` === f ||
|
|
259
|
+
`${importedPath}.jsx` === f
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
if (matched) deps.get(file)!.push(matched);
|
|
263
|
+
}
|
|
264
|
+
} catch {
|
|
265
|
+
// file unreadable — treat as no deps
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Reverse adjacency: dep → files that import it (its dependents)
|
|
270
|
+
const dependents = new Map<string, string[]>(files.map((f) => [f, []]));
|
|
271
|
+
for (const [file, fileDeps] of deps) {
|
|
272
|
+
for (const dep of fileDeps) dependents.get(dep)!.push(file);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Kahn's algorithm: files with no deps go first
|
|
276
|
+
const inDegree = new Map(files.map((f) => [f, deps.get(f)!.length]));
|
|
277
|
+
const queue = files.filter((f) => inDegree.get(f) === 0);
|
|
278
|
+
const sorted: string[] = [];
|
|
279
|
+
|
|
280
|
+
while (queue.length > 0) {
|
|
281
|
+
const file = queue.shift()!;
|
|
282
|
+
sorted.push(file);
|
|
283
|
+
for (const dependent of dependents.get(file) ?? []) {
|
|
284
|
+
const degree = (inDegree.get(dependent) ?? 1) - 1;
|
|
285
|
+
inDegree.set(dependent, degree);
|
|
286
|
+
if (degree === 0) queue.push(dependent);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Append remaining files (cycles) — fix them last since ordering is ambiguous
|
|
291
|
+
for (const f of files) {
|
|
292
|
+
if (!sorted.includes(f)) sorted.push(f);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return sorted.map((f) => [f, errorsByFile.get(f)!]);
|
|
296
|
+
}
|
|
297
|
+
|
|
200
298
|
// ─── Auto-Fix ───────────────────────────────────────────────────────────────────
|
|
201
299
|
|
|
202
300
|
async function attemptFix(
|
|
@@ -207,7 +305,7 @@ async function attemptFix(
|
|
|
207
305
|
): Promise<FixResult[]> {
|
|
208
306
|
const results: FixResult[] = [];
|
|
209
307
|
|
|
210
|
-
// Group errors by file
|
|
308
|
+
// Group errors by file, then sort by dependency order so upstream files are fixed first
|
|
211
309
|
const errorsByFile = new Map<string, ErrorEntry[]>();
|
|
212
310
|
for (const err of errors) {
|
|
213
311
|
const file = err.file || "(unknown)";
|
|
@@ -215,7 +313,9 @@ async function attemptFix(
|
|
|
215
313
|
errorsByFile.get(file)!.push(err);
|
|
216
314
|
}
|
|
217
315
|
|
|
218
|
-
|
|
316
|
+
const sortedEntries = await buildRepairOrder(errorsByFile, workingDir);
|
|
317
|
+
|
|
318
|
+
for (const [file, fileErrors] of sortedEntries) {
|
|
219
319
|
const fullPath = path.join(workingDir, file);
|
|
220
320
|
let existingContent = "";
|
|
221
321
|
try {
|
|
@@ -68,6 +68,84 @@ export interface FrontendContext {
|
|
|
68
68
|
paginationExample?: string;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// ─── Lightweight Import Parser ────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
interface ImportStatement {
|
|
74
|
+
/** The full original import line (for verbatim injection into prompts) */
|
|
75
|
+
line: string;
|
|
76
|
+
/** Resolved module path (e.g. '@/utils/http', 'axios', '../lib/request') */
|
|
77
|
+
modulePath: string;
|
|
78
|
+
/** Everything between `import` and `from` (specifiers) */
|
|
79
|
+
specifiers: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Parse all non-type import statements from a TypeScript/JavaScript file.
|
|
84
|
+
*
|
|
85
|
+
* Improvements over a single-line regex:
|
|
86
|
+
* - Handles multi-line named imports: `import {\n foo,\n bar\n} from '...'`
|
|
87
|
+
* - Skips `import type { ... }` to avoid false positives
|
|
88
|
+
* - Returns structured objects so callers can inspect specifiers vs module path
|
|
89
|
+
* without re-running a second regex
|
|
90
|
+
*/
|
|
91
|
+
function parseImportStatements(content: string): ImportStatement[] {
|
|
92
|
+
// 1. Strip block comments (/* ... */) to avoid matching imports inside comments
|
|
93
|
+
const stripped = content.replace(/\/\*[\s\S]*?\*\//g, (m) => "\n".repeat(m.split("\n").length - 1));
|
|
94
|
+
|
|
95
|
+
// 2. Collapse multi-line named import blocks onto a single logical line so
|
|
96
|
+
// a single-line pattern can match them reliably.
|
|
97
|
+
// e.g. `import {\n foo,\n bar\n} from 'x'` → `import { foo, bar } from 'x'`
|
|
98
|
+
const collapsed = stripped.replace(/import\s*\{[^}]*\}/gs, (m) => m.replace(/\n\s*/g, " "));
|
|
99
|
+
|
|
100
|
+
const results: ImportStatement[] = [];
|
|
101
|
+
|
|
102
|
+
for (const rawLine of collapsed.split("\n")) {
|
|
103
|
+
const line = rawLine.trim();
|
|
104
|
+
if (!line) continue;
|
|
105
|
+
// Skip type-only imports — they never affect runtime behaviour
|
|
106
|
+
if (/^import\s+type\b/.test(line)) continue;
|
|
107
|
+
if (!line.startsWith("import")) continue;
|
|
108
|
+
|
|
109
|
+
const match = line.match(/^(import\s+([\s\S]+?)\s+from\s+['"]([^'"]+)['"])/);
|
|
110
|
+
if (!match) continue;
|
|
111
|
+
|
|
112
|
+
results.push({
|
|
113
|
+
line: match[1],
|
|
114
|
+
modulePath: match[3],
|
|
115
|
+
specifiers: match[2],
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return results;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const HTTP_MODULE_PATTERNS: RegExp[] = [
|
|
123
|
+
// Project path aliases (@/, @@/, ~/, #/) — catches '@/utils/request', '~/lib/http', etc.
|
|
124
|
+
/^(?:@{1,2}|~|#)[/\\]/,
|
|
125
|
+
// Well-known HTTP libraries (exact name match)
|
|
126
|
+
/^(?:axios|ky(?:-universal)?|undici|node-fetch|cross-fetch|got|superagent|alova|openapi-fetch)$/,
|
|
127
|
+
// Relative imports whose path contains an HTTP-utility keyword
|
|
128
|
+
/\.{1,2}\/[^'"]*(?:http|request|fetch|client|api)[^'"]*/,
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Find the HTTP client import line from an API file's content.
|
|
133
|
+
* Returns the verbatim import statement or undefined if not found.
|
|
134
|
+
*
|
|
135
|
+
* More reliable than the old single-line regex because:
|
|
136
|
+
* - Handles multi-line named imports (e.g. `import {\n request\n} from '@/utils/http'`)
|
|
137
|
+
* - Module-path matching is done on the resolved path string, not the full line,
|
|
138
|
+
* so a long specifier list doesn't prevent a match
|
|
139
|
+
*/
|
|
140
|
+
function findHttpClientImport(content: string): string | undefined {
|
|
141
|
+
for (const stmt of parseImportStatements(content)) {
|
|
142
|
+
if (HTTP_MODULE_PATTERNS.some((p) => p.test(stmt.modulePath))) {
|
|
143
|
+
return stmt.line;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
71
149
|
// ─── Detection Maps ────────────────────────────────────────────────────────────
|
|
72
150
|
|
|
73
151
|
const STATE_MANAGEMENT_LIBS = [
|
|
@@ -303,18 +381,15 @@ export async function loadFrontendContext(
|
|
|
303
381
|
// e.g. "import request from '@/utils/http'" or "import axios from 'axios'"
|
|
304
382
|
// This is ground truth — prevents the AI from inventing a different import path.
|
|
305
383
|
//
|
|
306
|
-
//
|
|
307
|
-
//
|
|
308
|
-
//
|
|
309
|
-
// 3. Relative imports whose path contains http-utility keywords
|
|
310
|
-
const httpImportRegex =
|
|
311
|
-
/^import(?!\s+type)\s+(?:[\w*]+|\{[^}]+\})\s+from\s+['"]((?:@{1,2}|~|#)[/\\][^'"]+|\.{1,2}\/[^'"]*(?:http|request|fetch|client|api)[^'"]*|axios|ky(?:-universal)?|undici|node-fetch|cross-fetch|got|superagent|alova|openapi-fetch)['"]/im;
|
|
384
|
+
// Uses parseImportStatements() + HTTP_MODULE_PATTERNS instead of a single-line regex
|
|
385
|
+
// so multi-line named imports (e.g. `import {\n request\n} from '@/utils/http'`)
|
|
386
|
+
// are handled correctly without a secondary normalisation pass at call-site.
|
|
312
387
|
for (const relPath of ctx.existingApiFiles.slice(0, 5)) {
|
|
313
388
|
try {
|
|
314
389
|
const content = await fs.readFile(path.join(projectRoot, relPath), "utf-8");
|
|
315
|
-
const
|
|
316
|
-
if (
|
|
317
|
-
ctx.httpClientImport =
|
|
390
|
+
const found = findHttpClientImport(content);
|
|
391
|
+
if (found) {
|
|
392
|
+
ctx.httpClientImport = found;
|
|
318
393
|
break;
|
|
319
394
|
}
|
|
320
395
|
} catch {
|
|
@@ -498,23 +573,45 @@ async function extractRouteModuleContext(
|
|
|
498
573
|
|
|
499
574
|
if (moduleFiles.length === 0) return;
|
|
500
575
|
|
|
501
|
-
// Layout import
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
|
|
506
|
-
|
|
576
|
+
// Layout import extraction — handles two patterns:
|
|
577
|
+
// 1. Static: `import Layout from '@/layout/index.vue'`
|
|
578
|
+
// 2. Dynamic: `const Layout = () => import('@/layout/index.vue')`
|
|
579
|
+
// `const Layout = defineAsyncComponent(() => import('@/layout/index.vue'))`
|
|
580
|
+
//
|
|
581
|
+
// Pattern 1 is resolved via parseImportStatements() (handles multi-line named imports).
|
|
582
|
+
// Pattern 2 is matched with a targeted regex on the collapsed (single-line) content.
|
|
583
|
+
const dynamicLayoutRegex =
|
|
584
|
+
/const\s+Layout\s*=\s*(?:defineAsyncComponent\s*\(\s*)?(?:\(\s*\))?\s*(?:=>|function[^(]*\()\s*(?:[^)]*\))?\s*(?:=>)?\s*import\s*\(\s*['"]([^'"]+)['"]\s*\)/;
|
|
507
585
|
|
|
508
586
|
for (const relPath of moduleFiles) {
|
|
509
587
|
try {
|
|
510
588
|
const content = await fs.readFile(path.join(projectRoot, relPath), "utf-8");
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
589
|
+
|
|
590
|
+
// ── Pattern 1: static import Layout ────────────────────────────────────
|
|
591
|
+
const stmts = parseImportStatements(content);
|
|
592
|
+
const staticLayout = stmts.find(
|
|
593
|
+
(s) => /\bLayout\b/.test(s.specifiers) && /layout/i.test(s.modulePath)
|
|
594
|
+
);
|
|
595
|
+
if (staticLayout) {
|
|
596
|
+
ctx.layoutImport = staticLayout.line;
|
|
515
597
|
const preview = content.split("\n").slice(0, 100).join("\n");
|
|
516
598
|
ctx.routeModuleExample = { path: relPath, content: preview };
|
|
517
|
-
break;
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ── Pattern 2: dynamic / async-component import ─────────────────────────
|
|
603
|
+
// Collapse multi-line dynamic import declarations before matching
|
|
604
|
+
const singleLine = content.replace(/const\s+Layout\s*=[\s\S]*?import\s*\([^)]+\)/gm, (m) =>
|
|
605
|
+
m.replace(/\n\s*/g, " ")
|
|
606
|
+
);
|
|
607
|
+
const dynMatch = singleLine.match(dynamicLayoutRegex);
|
|
608
|
+
if (dynMatch) {
|
|
609
|
+
// Re-extract the full const declaration line from the original content
|
|
610
|
+
const constMatch = content.match(/^const\s+Layout\s*=.+/m);
|
|
611
|
+
ctx.layoutImport = constMatch ? constMatch[0].trim() : dynMatch[0].trim();
|
|
612
|
+
const preview = content.split("\n").slice(0, 100).join("\n");
|
|
613
|
+
ctx.routeModuleExample = { path: relPath, content: preview };
|
|
614
|
+
break;
|
|
518
615
|
}
|
|
519
616
|
} catch {
|
|
520
617
|
// skip
|
package/dist/cli/index.js
CHANGED
|
@@ -5518,6 +5518,41 @@ async function loadDslForSpec(specFilePath) {
|
|
|
5518
5518
|
// core/frontend-context-loader.ts
|
|
5519
5519
|
var fs7 = __toESM(require("fs-extra"));
|
|
5520
5520
|
var path6 = __toESM(require("path"));
|
|
5521
|
+
function parseImportStatements(content) {
|
|
5522
|
+
const stripped = content.replace(/\/\*[\s\S]*?\*\//g, (m) => "\n".repeat(m.split("\n").length - 1));
|
|
5523
|
+
const collapsed = stripped.replace(/import\s*\{[^}]*\}/gs, (m) => m.replace(/\n\s*/g, " "));
|
|
5524
|
+
const results = [];
|
|
5525
|
+
for (const rawLine of collapsed.split("\n")) {
|
|
5526
|
+
const line = rawLine.trim();
|
|
5527
|
+
if (!line) continue;
|
|
5528
|
+
if (/^import\s+type\b/.test(line)) continue;
|
|
5529
|
+
if (!line.startsWith("import")) continue;
|
|
5530
|
+
const match = line.match(/^(import\s+([\s\S]+?)\s+from\s+['"]([^'"]+)['"])/);
|
|
5531
|
+
if (!match) continue;
|
|
5532
|
+
results.push({
|
|
5533
|
+
line: match[1],
|
|
5534
|
+
modulePath: match[3],
|
|
5535
|
+
specifiers: match[2]
|
|
5536
|
+
});
|
|
5537
|
+
}
|
|
5538
|
+
return results;
|
|
5539
|
+
}
|
|
5540
|
+
var HTTP_MODULE_PATTERNS = [
|
|
5541
|
+
// Project path aliases (@/, @@/, ~/, #/) — catches '@/utils/request', '~/lib/http', etc.
|
|
5542
|
+
/^(?:@{1,2}|~|#)[/\\]/,
|
|
5543
|
+
// Well-known HTTP libraries (exact name match)
|
|
5544
|
+
/^(?:axios|ky(?:-universal)?|undici|node-fetch|cross-fetch|got|superagent|alova|openapi-fetch)$/,
|
|
5545
|
+
// Relative imports whose path contains an HTTP-utility keyword
|
|
5546
|
+
/\.{1,2}\/[^'"]*(?:http|request|fetch|client|api)[^'"]*/
|
|
5547
|
+
];
|
|
5548
|
+
function findHttpClientImport(content) {
|
|
5549
|
+
for (const stmt of parseImportStatements(content)) {
|
|
5550
|
+
if (HTTP_MODULE_PATTERNS.some((p) => p.test(stmt.modulePath))) {
|
|
5551
|
+
return stmt.line;
|
|
5552
|
+
}
|
|
5553
|
+
}
|
|
5554
|
+
return void 0;
|
|
5555
|
+
}
|
|
5521
5556
|
var STATE_MANAGEMENT_LIBS = [
|
|
5522
5557
|
"zustand",
|
|
5523
5558
|
"redux",
|
|
@@ -5706,13 +5741,12 @@ ${preview}`);
|
|
|
5706
5741
|
} catch {
|
|
5707
5742
|
}
|
|
5708
5743
|
}
|
|
5709
|
-
const httpImportRegex = /^import(?!\s+type)\s+(?:[\w*]+|\{[^}]+\})\s+from\s+['"]((?:@{1,2}|~|#)[/\\][^'"]+|\.{1,2}\/[^'"]*(?:http|request|fetch|client|api)[^'"]*|axios|ky(?:-universal)?|undici|node-fetch|cross-fetch|got|superagent|alova|openapi-fetch)['"]/im;
|
|
5710
5744
|
for (const relPath of ctx.existingApiFiles.slice(0, 5)) {
|
|
5711
5745
|
try {
|
|
5712
5746
|
const content = await fs7.readFile(path6.join(projectRoot, relPath), "utf-8");
|
|
5713
|
-
const
|
|
5714
|
-
if (
|
|
5715
|
-
ctx.httpClientImport =
|
|
5747
|
+
const found = findHttpClientImport(content);
|
|
5748
|
+
if (found) {
|
|
5749
|
+
ctx.httpClientImport = found;
|
|
5716
5750
|
break;
|
|
5717
5751
|
}
|
|
5718
5752
|
} catch {
|
|
@@ -5842,13 +5876,28 @@ async function extractRouteModuleContext(projectRoot, ctx) {
|
|
|
5842
5876
|
moduleFiles.push(...files);
|
|
5843
5877
|
}
|
|
5844
5878
|
if (moduleFiles.length === 0) return;
|
|
5845
|
-
const
|
|
5879
|
+
const dynamicLayoutRegex = /const\s+Layout\s*=\s*(?:defineAsyncComponent\s*\(\s*)?(?:\(\s*\))?\s*(?:=>|function[^(]*\()\s*(?:[^)]*\))?\s*(?:=>)?\s*import\s*\(\s*['"]([^'"]+)['"]\s*\)/;
|
|
5846
5880
|
for (const relPath of moduleFiles) {
|
|
5847
5881
|
try {
|
|
5848
5882
|
const content = await fs7.readFile(path6.join(projectRoot, relPath), "utf-8");
|
|
5849
|
-
const
|
|
5850
|
-
|
|
5851
|
-
|
|
5883
|
+
const stmts = parseImportStatements(content);
|
|
5884
|
+
const staticLayout = stmts.find(
|
|
5885
|
+
(s) => /\bLayout\b/.test(s.specifiers) && /layout/i.test(s.modulePath)
|
|
5886
|
+
);
|
|
5887
|
+
if (staticLayout) {
|
|
5888
|
+
ctx.layoutImport = staticLayout.line;
|
|
5889
|
+
const preview = content.split("\n").slice(0, 100).join("\n");
|
|
5890
|
+
ctx.routeModuleExample = { path: relPath, content: preview };
|
|
5891
|
+
break;
|
|
5892
|
+
}
|
|
5893
|
+
const singleLine = content.replace(
|
|
5894
|
+
/const\s+Layout\s*=[\s\S]*?import\s*\([^)]+\)/gm,
|
|
5895
|
+
(m) => m.replace(/\n\s*/g, " ")
|
|
5896
|
+
);
|
|
5897
|
+
const dynMatch = singleLine.match(dynamicLayoutRegex);
|
|
5898
|
+
if (dynMatch) {
|
|
5899
|
+
const constMatch = content.match(/^const\s+Layout\s*=.+/m);
|
|
5900
|
+
ctx.layoutImport = constMatch ? constMatch[0].trim() : dynMatch[0].trim();
|
|
5852
5901
|
const preview = content.split("\n").slice(0, 100).join("\n");
|
|
5853
5902
|
ctx.routeModuleExample = { path: relPath, content: preview };
|
|
5854
5903
|
break;
|
|
@@ -7833,6 +7882,60 @@ function parseErrors(output, source) {
|
|
|
7833
7882
|
}
|
|
7834
7883
|
return errors;
|
|
7835
7884
|
}
|
|
7885
|
+
function parseRelativeImports(content, fromFileRel) {
|
|
7886
|
+
const relDir = path15.dirname(fromFileRel);
|
|
7887
|
+
const results = [];
|
|
7888
|
+
const normalized = content.replace(/import\s*\{[^}]*\}/gs, (m) => m.replace(/\n\s*/g, " "));
|
|
7889
|
+
for (const line of normalized.split("\n")) {
|
|
7890
|
+
const trimmed = line.trim();
|
|
7891
|
+
if (/^import\s+type\b/.test(trimmed)) continue;
|
|
7892
|
+
const match = trimmed.match(/^import\b[^'"]*from\s+['"](\.\.?\/[^'"]+)['"]/);
|
|
7893
|
+
if (!match) continue;
|
|
7894
|
+
const resolved = path15.normalize(path15.join(relDir, match[1]));
|
|
7895
|
+
results.push(resolved);
|
|
7896
|
+
}
|
|
7897
|
+
return results;
|
|
7898
|
+
}
|
|
7899
|
+
async function buildRepairOrder(errorsByFile, workingDir) {
|
|
7900
|
+
const files = Array.from(errorsByFile.keys());
|
|
7901
|
+
if (files.length <= 1) return Array.from(errorsByFile.entries());
|
|
7902
|
+
const deps = new Map(files.map((f) => [f, []]));
|
|
7903
|
+
for (const file of files) {
|
|
7904
|
+
try {
|
|
7905
|
+
const content = await fs16.readFile(path15.join(workingDir, file), "utf-8");
|
|
7906
|
+
const importedPaths = parseRelativeImports(content, file);
|
|
7907
|
+
for (const importedPath of importedPaths) {
|
|
7908
|
+
const matched = files.find((f) => {
|
|
7909
|
+
if (f === file) return false;
|
|
7910
|
+
const fNoExt = f.replace(/\.[^.]+$/, "");
|
|
7911
|
+
return importedPath === fNoExt || importedPath === f || `${importedPath}.ts` === f || `${importedPath}.tsx` === f || `${importedPath}.js` === f || `${importedPath}.jsx` === f;
|
|
7912
|
+
});
|
|
7913
|
+
if (matched) deps.get(file).push(matched);
|
|
7914
|
+
}
|
|
7915
|
+
} catch {
|
|
7916
|
+
}
|
|
7917
|
+
}
|
|
7918
|
+
const dependents = new Map(files.map((f) => [f, []]));
|
|
7919
|
+
for (const [file, fileDeps] of deps) {
|
|
7920
|
+
for (const dep of fileDeps) dependents.get(dep).push(file);
|
|
7921
|
+
}
|
|
7922
|
+
const inDegree = new Map(files.map((f) => [f, deps.get(f).length]));
|
|
7923
|
+
const queue = files.filter((f) => inDegree.get(f) === 0);
|
|
7924
|
+
const sorted = [];
|
|
7925
|
+
while (queue.length > 0) {
|
|
7926
|
+
const file = queue.shift();
|
|
7927
|
+
sorted.push(file);
|
|
7928
|
+
for (const dependent of dependents.get(file) ?? []) {
|
|
7929
|
+
const degree = (inDegree.get(dependent) ?? 1) - 1;
|
|
7930
|
+
inDegree.set(dependent, degree);
|
|
7931
|
+
if (degree === 0) queue.push(dependent);
|
|
7932
|
+
}
|
|
7933
|
+
}
|
|
7934
|
+
for (const f of files) {
|
|
7935
|
+
if (!sorted.includes(f)) sorted.push(f);
|
|
7936
|
+
}
|
|
7937
|
+
return sorted.map((f) => [f, errorsByFile.get(f)]);
|
|
7938
|
+
}
|
|
7836
7939
|
async function attemptFix(provider, errors, workingDir, dsl) {
|
|
7837
7940
|
const results = [];
|
|
7838
7941
|
const errorsByFile = /* @__PURE__ */ new Map();
|
|
@@ -7841,7 +7944,8 @@ async function attemptFix(provider, errors, workingDir, dsl) {
|
|
|
7841
7944
|
if (!errorsByFile.has(file)) errorsByFile.set(file, []);
|
|
7842
7945
|
errorsByFile.get(file).push(err);
|
|
7843
7946
|
}
|
|
7844
|
-
|
|
7947
|
+
const sortedEntries = await buildRepairOrder(errorsByFile, workingDir);
|
|
7948
|
+
for (const [file, fileErrors] of sortedEntries) {
|
|
7845
7949
|
const fullPath = path15.join(workingDir, file);
|
|
7846
7950
|
let existingContent = "";
|
|
7847
7951
|
try {
|