ai-spec-dev 0.14.1 → 0.17.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 +7 -3
- package/RELEASE_LOG.md +45 -0
- package/cli/index.ts +21 -1
- package/core/code-generator.ts +21 -4
- package/core/context-loader.ts +1 -1
- package/core/spec-assessor.ts +1 -1
- package/core/task-generator.ts +1 -1
- package/package.json +1 -1
- package/prompts/update.prompt.ts +1 -1
package/README.md
CHANGED
|
@@ -338,9 +338,13 @@ ai-spec init --force
|
|
|
338
338
|
|
|
339
339
|
#### Constitution Rebase — 为什么需要定期整合
|
|
340
340
|
|
|
341
|
-
`ai-spec review` 每次运行后会把审查 issue 追加到宪法 §9。长期运行后 §9
|
|
341
|
+
`ai-spec review` 每次运行后会把审查 issue 追加到宪法 §9。长期运行后 §9 会积累大量条目(重复措辞、已修复问题、不再适用的早期教训)。宪法全文注入每次 AI 调用——内容越长,AI 对中间章节的注意力越分散,效果反而下降。当宪法超过 6,000 字符时,CLI 会自动显示警告提示整合。
|
|
342
342
|
|
|
343
|
-
**建议频率**:§9 达到 8
|
|
343
|
+
**建议频率**:§9 达到 8 条以上时(系统会自动提示),或出现以下警告时,运行一次整合:
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
⚠ Constitution is long (8,432 chars). Consider running: ai-spec init --consolidate
|
|
347
|
+
```
|
|
344
348
|
|
|
345
349
|
```bash
|
|
346
350
|
# 预览效果(不写入)
|
|
@@ -996,7 +1000,7 @@ specs/
|
|
|
996
1000
|
~ src/routes/client/index.ts... ✔
|
|
997
1001
|
```
|
|
998
1002
|
|
|
999
|
-
**跨 Task 一致性保障**:每个 task 完成后,写入的 `src/api*` / `src/service*` / `src/store*` / `src/composable*` 文件会被读回并缓存;后续 task
|
|
1003
|
+
**跨 Task 一致性保障**:每个 task 完成后,写入的 `src/api*` / `src/service*` / `src/store*` / `src/composable*` 文件会被读回并缓存;后续 task 的 prompt 中注入这些文件的**所有 export 声明行**(而非前 N 字符的截断),确保路由/组件层 import 使用的函数名与 API 层完全一致(不再出现 `getTasks` vs `getTaskList` 的跨 task 幻觉)。当前 task 创建新路由模块文件时,也会自动携带对应 `routes/index.ts` 的精确注册指令。
|
|
1000
1004
|
|
|
1001
1005
|
**分页参数 ground-truth 注入**:`frontend-context-loader` 自动扫描 `src/apis/`(及 `src/api/`、`src/services/` 等)中现有的 API 文件,找到包含分页字段(`pageIndex`/`pageSize`/`pageNum`/`current` 等)的 interface 及对应导出函数,作为 `paginationExample` 注入 prompt,并标注 "COPY THIS EXACTLY"。生成的列表接口将与项目现有接口使用完全相同的分页参数名称和 HTTP 方法(POST body 或 GET query)。
|
|
1002
1006
|
|
package/RELEASE_LOG.md
CHANGED
|
@@ -2,6 +2,51 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## [0.17.0] 2026-03-24 — 宪法全文注入 · Export 精准缓存 · 宪法长度警告
|
|
6
|
+
|
|
7
|
+
### 1. 宪法全文注入(移除硬截断)
|
|
8
|
+
|
|
9
|
+
**问题**:宪法在所有 prompt 中存在硬截断(`codegen`/`task-generator`/`spec-assessor`/`update` 各处分别为 1500–2000 字符),§9 最新教训恰好位于宪法末尾,最容易被截掉——工具越用越长的宪法反而越来越被忽视。
|
|
10
|
+
|
|
11
|
+
**修复**(涉及 6 处):
|
|
12
|
+
- `core/code-generator.ts` — 代码生成 prompt 中的宪法注入
|
|
13
|
+
- `cli/index.ts` — update 命令中的宪法注入
|
|
14
|
+
- `prompts/update.prompt.ts` — update prompt 构建函数
|
|
15
|
+
- `core/task-generator.ts` — task 生成 prompt
|
|
16
|
+
- `core/spec-assessor.ts` — Spec 质量评估 prompt
|
|
17
|
+
- `core/context-loader.ts` — 项目文件预览从 800 → 2000 字符
|
|
18
|
+
|
|
19
|
+
现代模型(Claude/Gemini/Qwen3)上下文窗口充足,宪法通常 3000–8000 字符,全文注入不会造成负担,但能确保 §9 始终被 AI 读到。
|
|
20
|
+
|
|
21
|
+
### 2. Generated File Cache — Export 精准提取
|
|
22
|
+
|
|
23
|
+
**问题**:跨 task 一致性保障依赖 `generatedFileCache`,原实现截取每个缓存文件的前 800 字符注入后续 task prompt。对于超过 30 行的 service/api 文件,大量 export 函数名落在 800 字符之外,跨 task 函数名幻觉问题依然存在于大文件场景。
|
|
24
|
+
|
|
25
|
+
**修复**(`core/code-generator.ts`):新增 `extractExportSignatures()` 函数,从文件全文中提取**所有以 `export` 开头的声明行**,注入后续 task prompt。无论文件长度,所有导出名称完整可见;对无显式 export 的文件(CommonJS)回退到前 3000 字符。
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
// Before: 取前 800 字符,大文件的 export 被截掉
|
|
29
|
+
content.slice(0, 800)
|
|
30
|
+
|
|
31
|
+
// After: 提取所有 export 声明行,精准且不浪费 token
|
|
32
|
+
export function getUserList(...): Promise<...>
|
|
33
|
+
export function createUser(...): Promise<...>
|
|
34
|
+
export const updateUser = ...
|
|
35
|
+
// ...全部导出,不含实现细节
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 3. 宪法长度警告
|
|
39
|
+
|
|
40
|
+
**新增**:当宪法超过 **6,000 字符**时,`create` / `update` / workspace 各命令加载上下文后自动输出提示:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
⚠ Constitution is long (8,432 chars). Consider running: ai-spec init --consolidate
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
设计原则:不阻断流程,纯提示性;帮助用户在宪法真正影响 AI 注意力之前主动整合,而不是等到效果已经变差才发现。
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
5
50
|
## [0.16.0] 2026-03-24 — Spec 质量预评估 · 分层代码审查 · TDD 模式
|
|
6
51
|
|
|
7
52
|
### 1. Spec 质量预评估(`core/spec-assessor.ts`, `prompts/spec-assess.prompt.ts`)
|
package/cli/index.ts
CHANGED
|
@@ -86,6 +86,22 @@ async function loadConfig(dir: string): Promise<AiSpecConfig> {
|
|
|
86
86
|
return {};
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// ─── Constitution Length Warning ──────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
const CONSTITUTION_WARN_CHARS = 6000;
|
|
92
|
+
|
|
93
|
+
function warnIfConstitutionLong(context: { constitution?: string }): void {
|
|
94
|
+
if (!context.constitution) return;
|
|
95
|
+
const len = context.constitution.length;
|
|
96
|
+
if (len > CONSTITUTION_WARN_CHARS) {
|
|
97
|
+
console.log(
|
|
98
|
+
chalk.yellow(
|
|
99
|
+
` ⚠ Constitution is long (${len.toLocaleString()} chars). Consider running: ai-spec init --consolidate`
|
|
100
|
+
)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
89
105
|
// ─── API Key Resolution ────────────────────────────────────────────────────────
|
|
90
106
|
|
|
91
107
|
async function resolveApiKey(
|
|
@@ -247,6 +263,7 @@ program
|
|
|
247
263
|
const { type: detectedRepoType } = await detectRepoType(currentDir);
|
|
248
264
|
console.log(chalk.gray(` Tech stack : ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
249
265
|
console.log(chalk.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
266
|
+
warnIfConstitutionLong(context);
|
|
250
267
|
console.log(chalk.gray(` API files : ${context.apiStructure.length} files`));
|
|
251
268
|
if (context.schema) {
|
|
252
269
|
console.log(chalk.gray(` Prisma schema: found`));
|
|
@@ -1038,6 +1055,7 @@ async function runSingleRepoPipelineInWorkspace(opts: {
|
|
|
1038
1055
|
|
|
1039
1056
|
console.log(chalk.gray(` Tech stack: ${context.techStack.join(", ") || "unknown"} [${detectedRepoType}]`));
|
|
1040
1057
|
console.log(chalk.gray(` Dependencies: ${context.dependencies.length} packages`));
|
|
1058
|
+
warnIfConstitutionLong(context);
|
|
1041
1059
|
|
|
1042
1060
|
if (!context.constitution) {
|
|
1043
1061
|
console.log(chalk.yellow(` Constitution: not found — auto-generating...`));
|
|
@@ -1221,6 +1239,7 @@ async function runMultiRepoPipeline(
|
|
|
1221
1239
|
try {
|
|
1222
1240
|
const loader = new ContextLoader(repoAbsPath);
|
|
1223
1241
|
const ctx = await loader.loadProjectContext();
|
|
1242
|
+
warnIfConstitutionLong(ctx);
|
|
1224
1243
|
contexts.set(repo.name, ctx);
|
|
1225
1244
|
|
|
1226
1245
|
// Load frontend context for frontend/mobile repos
|
|
@@ -1684,6 +1703,7 @@ program
|
|
|
1684
1703
|
console.log(chalk.gray(" Loading project context..."));
|
|
1685
1704
|
const loader = new ContextLoader(currentDir);
|
|
1686
1705
|
const context = await loader.loadProjectContext();
|
|
1706
|
+
warnIfConstitutionLong(context);
|
|
1687
1707
|
|
|
1688
1708
|
// ── Detect repo type ──────────────────────────────────────────────────────
|
|
1689
1709
|
const { detectRepoType: _detectRepoType } = await import("../core/workspace-loader");
|
|
@@ -1728,7 +1748,7 @@ program
|
|
|
1728
1748
|
|
|
1729
1749
|
const specContent = await fs.readFile(result.newSpecPath, "utf-8");
|
|
1730
1750
|
const constitutionSection = context.constitution
|
|
1731
|
-
? `\n=== Project Constitution (MUST follow) ===\n${context.constitution
|
|
1751
|
+
? `\n=== Project Constitution (MUST follow) ===\n${context.constitution}\n`
|
|
1732
1752
|
: "";
|
|
1733
1753
|
const dslSection = result.updatedDsl
|
|
1734
1754
|
? `\n=== DSL Context ===\n${JSON.stringify(result.updatedDsl, null, 2).slice(0, 3000)}\n`
|
package/core/code-generator.ts
CHANGED
|
@@ -38,6 +38,25 @@ function buildInstalledPackagesSection(context?: ProjectContext): string {
|
|
|
38
38
|
* Injected before generating files that may import from those paths (e.g., route files
|
|
39
39
|
* importing from API files generated in an earlier task).
|
|
40
40
|
*/
|
|
41
|
+
/**
|
|
42
|
+
* Extract all export declaration lines from a file's content.
|
|
43
|
+
* This gives precise signal about every exported name without including
|
|
44
|
+
* implementation details, and works regardless of file length.
|
|
45
|
+
* Falls back to first 3000 chars for CommonJS or files with no explicit exports.
|
|
46
|
+
*/
|
|
47
|
+
function extractExportSignatures(content: string): string {
|
|
48
|
+
const exportLines = content
|
|
49
|
+
.split("\n")
|
|
50
|
+
.filter((line) => line.trimStart().startsWith("export "));
|
|
51
|
+
|
|
52
|
+
if (exportLines.length > 0) {
|
|
53
|
+
return exportLines.join("\n");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Fallback for CommonJS or files without top-level export keywords
|
|
57
|
+
return content.slice(0, 3000) + (content.length > 3000 ? "\n... (truncated)" : "");
|
|
58
|
+
}
|
|
59
|
+
|
|
41
60
|
function buildGeneratedFilesSection(cache: Map<string, string>): string {
|
|
42
61
|
if (cache.size === 0) return "";
|
|
43
62
|
const lines = [
|
|
@@ -45,9 +64,7 @@ function buildGeneratedFilesSection(cache: Map<string, string>): string {
|
|
|
45
64
|
];
|
|
46
65
|
for (const [filePath, content] of cache) {
|
|
47
66
|
lines.push(`\n--- ${filePath} ---`);
|
|
48
|
-
|
|
49
|
-
lines.push(content.slice(0, 800));
|
|
50
|
-
if (content.length > 800) lines.push("... (truncated)");
|
|
67
|
+
lines.push(extractExportSignatures(content));
|
|
51
68
|
}
|
|
52
69
|
return lines.join("\n") + "\n";
|
|
53
70
|
}
|
|
@@ -308,7 +325,7 @@ export class CodeGenerator {
|
|
|
308
325
|
|
|
309
326
|
const spec = await fs.readFile(specFilePath, "utf-8");
|
|
310
327
|
const constitutionSection = context?.constitution
|
|
311
|
-
? `\n=== Project Constitution (MUST follow) ===\n${context.constitution
|
|
328
|
+
? `\n=== Project Constitution (MUST follow) ===\n${context.constitution}\n`
|
|
312
329
|
: "";
|
|
313
330
|
const contextSummary = context
|
|
314
331
|
? `Tech Stack: ${context.techStack.join(", ")}\nExisting files: ${context.fileStructure.slice(0, 20).join(", ")}`
|
package/core/context-loader.ts
CHANGED
|
@@ -244,7 +244,7 @@ export class ContextLoader {
|
|
|
244
244
|
for (const f of propFiles.slice(0, 2)) {
|
|
245
245
|
try {
|
|
246
246
|
const content = await fs.readFile(path.join(this.projectRoot, f), "utf-8");
|
|
247
|
-
parts.push(`// ${f}\n${content.slice(0,
|
|
247
|
+
parts.push(`// ${f}\n${content.slice(0, 2000)}`);
|
|
248
248
|
} catch { /* skip */ }
|
|
249
249
|
}
|
|
250
250
|
if (parts.length > 0) context.routeSummary = parts.join("\n\n");
|
package/core/spec-assessor.ts
CHANGED
|
@@ -54,7 +54,7 @@ export async function assessSpec(
|
|
|
54
54
|
constitution?: string
|
|
55
55
|
): Promise<SpecAssessment | null> {
|
|
56
56
|
const prompt = `Assess the following feature specification.
|
|
57
|
-
${constitution ? `\n=== Project Constitution (check consistency against this) ===\n${constitution
|
|
57
|
+
${constitution ? `\n=== Project Constitution (check consistency against this) ===\n${constitution}\n` : ""}
|
|
58
58
|
=== Feature Spec ===
|
|
59
59
|
${spec}`;
|
|
60
60
|
|
package/core/task-generator.ts
CHANGED
|
@@ -53,7 +53,7 @@ export function buildTaskPrompt(spec: string, context?: ProjectContext): string
|
|
|
53
53
|
const parts: string[] = [spec];
|
|
54
54
|
|
|
55
55
|
if (context.constitution) {
|
|
56
|
-
parts.push(`\n=== Project Constitution (rules to follow) ===\n${context.constitution
|
|
56
|
+
parts.push(`\n=== Project Constitution (rules to follow) ===\n${context.constitution}`);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
if (context.techStack.length > 0) {
|
package/package.json
CHANGED
package/prompts/update.prompt.ts
CHANGED
|
@@ -41,7 +41,7 @@ export function buildSpecUpdatePrompt(
|
|
|
41
41
|
context?: ProjectContext
|
|
42
42
|
): string {
|
|
43
43
|
const constitutionSection = context?.constitution
|
|
44
|
-
? `\n=== Project Constitution (all changes must comply) ===\n${context.constitution
|
|
44
|
+
? `\n=== Project Constitution (all changes must comply) ===\n${context.constitution}\n`
|
|
45
45
|
: "";
|
|
46
46
|
|
|
47
47
|
const dslSummary = existingDsl
|