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 CHANGED
@@ -338,9 +338,13 @@ ai-spec init --force
338
338
 
339
339
  #### Constitution Rebase — 为什么需要定期整合
340
340
 
341
- `ai-spec review` 每次运行后会把审查 issue 追加到宪法 §9。长期运行后 §9 会积累大量条目(重复措辞、已修复问题、不再适用的早期教训)。宪法被注入每次 AI 调用,超过 2000 字符后会被硬截断,越积越多反而降低效果。
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 prompt 中可以看到这些文件的实际导出内容,确保路由/组件层 import 使用的函数名与 API 层一致(不再出现 `getTasks` vs `getTaskList` 的跨 task 幻觉)。当前 task 创建新路由模块文件时,也会自动携带对应 `routes/index.ts` 的精确注册指令。
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.slice(0, 2000)}\n`
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`
@@ -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
- // Include enough to see all export names (first 800 chars covers most API files)
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.slice(0, 2000)}\n`
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(", ")}`
@@ -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, 800)}`);
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");
@@ -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.slice(0, 1500)}\n` : ""}
57
+ ${constitution ? `\n=== Project Constitution (check consistency against this) ===\n${constitution}\n` : ""}
58
58
  === Feature Spec ===
59
59
  ${spec}`;
60
60
 
@@ -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.slice(0, 1500)}`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-spec-dev",
3
- "version": "0.14.1",
3
+ "version": "0.17.0",
4
4
  "description": "AI-driven Development Orchestrator SDK & CLI",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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.slice(0, 1500)}\n`
44
+ ? `\n=== Project Constitution (all changes must comply) ===\n${context.constitution}\n`
45
45
  : "";
46
46
 
47
47
  const dslSummary = existingDsl