@guanlin4924/devflow-pi 0.1.65 → 0.1.66
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/package.json +1 -1
- package/src/constants.ts +1 -1
- package/src/project-documents.ts +128 -10
package/package.json
CHANGED
package/src/constants.ts
CHANGED
package/src/project-documents.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
3
4
|
|
|
4
5
|
import { InitializationStore } from "./initialization-store.ts";
|
|
5
6
|
import type { ProjectContext } from "./project-context.ts";
|
|
@@ -14,6 +15,9 @@ export type DocumentGenerationMode = "low-tier-model" | "template";
|
|
|
14
15
|
export type DocumentGenerationResult = { targetPath: string; status: "complete" | "conflict" | "failed"; outputPath?: string; error?: string };
|
|
15
16
|
|
|
16
17
|
type Fact = { kind: string; repository: string; objectKey: string; value: Record<string, unknown>; evidenceLevel: string };
|
|
18
|
+
type SourceDocument = { id: string; path: string; contentHash: string; title: string; headings: string[]; excerpt: string };
|
|
19
|
+
type DocumentClassification = { sourceDocumentId: string; scope: "project" | "requirement" | "unknown"; documentType: KnowledgeDocumentType | "unknown"; requirementId?: string; requirementTitle?: string; confidence: "exact" | "inferred" | "heuristic"; reasons: string[] };
|
|
20
|
+
type DocumentKnowledge = { sources: SourceDocument[]; classifications: DocumentClassification[] };
|
|
17
21
|
const TEMPLATE_VERSION = "project-context-v2";
|
|
18
22
|
const KNOWLEDGE_DOCUMENT_TYPES = [
|
|
19
23
|
"requirements",
|
|
@@ -69,6 +73,90 @@ function codeIndexGeneration(snapshot: { indexRef?: unknown }): string | undefin
|
|
|
69
73
|
const indexRef = snapshot.indexRef as Record<string, unknown> | undefined;
|
|
70
74
|
return typeof indexRef?.generation === "string" ? indexRef.generation : undefined;
|
|
71
75
|
}
|
|
76
|
+
function slug(value: string): string {
|
|
77
|
+
return value.trim().toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-|-$/g, "").slice(0, 48) || "document";
|
|
78
|
+
}
|
|
79
|
+
function titleFromMarkdown(relativePath: string, content: string): string {
|
|
80
|
+
const heading = content.match(/^#\s+(.+)$/m)?.[1]?.trim();
|
|
81
|
+
return heading || path.basename(relativePath).replace(/\.[^.]+$/, "");
|
|
82
|
+
}
|
|
83
|
+
function headingsFromMarkdown(content: string): string[] {
|
|
84
|
+
return [...content.matchAll(/^#{1,4}\s+(.+)$/gm)].map(match => match[1].trim()).slice(0, 40);
|
|
85
|
+
}
|
|
86
|
+
function excerpt(content: string): string {
|
|
87
|
+
return content.replace(/^---[\s\S]*?---\s*/m, "").replace(/\s+/g, " ").trim().slice(0, 500);
|
|
88
|
+
}
|
|
89
|
+
function classifyDocument(document: SourceDocument): DocumentClassification {
|
|
90
|
+
const haystack = `${document.path}\n${document.title}\n${document.headings.join("\n")}\n${document.excerpt}`.toLowerCase();
|
|
91
|
+
const reasons: string[] = [];
|
|
92
|
+
const hit = (type: KnowledgeDocumentType, patterns: RegExp[]) => {
|
|
93
|
+
const matched = patterns.find(pattern => pattern.test(haystack));
|
|
94
|
+
if (!matched) return false;
|
|
95
|
+
reasons.push(`${type}:${matched.source}`);
|
|
96
|
+
return true;
|
|
97
|
+
};
|
|
98
|
+
let documentType: KnowledgeDocumentType | "unknown" = "unknown";
|
|
99
|
+
if (hit("api", [/\bapi\b/, /接口/, /openapi/, /swagger/])) documentType = "api";
|
|
100
|
+
else if (hit("test-cases", [/test[-_ ]?case/, /测试/, /用例/, /验收/, /acceptance/])) documentType = "test-cases";
|
|
101
|
+
else if (hit("frontend-tech", [/frontend/, /前端/, /\bh5\b/, /admin/, /react|vue|vite|router|组件/])) documentType = "frontend-tech";
|
|
102
|
+
else if (hit("backend-tech", [/backend/, /后端/, /server/, /服务端/, /handler|service|repository|gorm|gin/])) documentType = "backend-tech";
|
|
103
|
+
else if (hit("architecture", [/architecture/, /架构/, /系统设计/, /数据流/, /部署/])) documentType = "architecture";
|
|
104
|
+
else if (hit("product-design", [/product[-_ ]?design/, /交互/, /产品设计/, /原型/, /页面设计/, /wireframe/])) documentType = "product-design";
|
|
105
|
+
else if (hit("requirements", [/requirement/, /\bprd\b/, /需求/, /feature/, /用户故事/])) documentType = "requirements";
|
|
106
|
+
const requirementMatch = haystack.match(/\b(req[-_ ]?\d{1,5})\b/i) ?? haystack.match(/需求[\s::]*([a-z0-9_-]{2,40})/i);
|
|
107
|
+
const requirementId = requirementMatch?.[1]?.toUpperCase().replace(/[_ ]/g, "-");
|
|
108
|
+
const pathLooksRequirement = /(^|\/)(features?|requirements?|需求|prd)(\/|$)/i.test(document.path) || Boolean(requirementId);
|
|
109
|
+
return {
|
|
110
|
+
sourceDocumentId: document.id,
|
|
111
|
+
scope: documentType === "unknown" ? "unknown" : pathLooksRequirement ? "requirement" : "project",
|
|
112
|
+
documentType,
|
|
113
|
+
requirementId,
|
|
114
|
+
requirementTitle: requirementId ? document.title : undefined,
|
|
115
|
+
confidence: requirementId ? "exact" : documentType === "unknown" ? "heuristic" : pathLooksRequirement ? "inferred" : "heuristic",
|
|
116
|
+
reasons: reasons.length ? reasons : ["no strong classifier match"],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function shouldInspectDocument(relativePath: string): boolean {
|
|
120
|
+
if (!/\.(?:md|mdx|txt|adoc|rst|yaml|yml|json)$/i.test(relativePath)) return false;
|
|
121
|
+
if (/^(?:\.git|node_modules|dist|build|coverage|vendor|target)\//.test(relativePath)) return false;
|
|
122
|
+
if (/^\.planning\/code-index\//.test(relativePath)) return false;
|
|
123
|
+
if (/^\.context\/requirements\/_template\//.test(relativePath)) return false;
|
|
124
|
+
return /^(?:README|docs\/|features\/|requirements\/|\.context\/|\.planning\/requirements\/|\.planning\/features\/|test-environment-guide|skit\/docs\/|text\/)/i.test(relativePath);
|
|
125
|
+
}
|
|
126
|
+
async function discoverSourceDocuments(root: string): Promise<DocumentKnowledge> {
|
|
127
|
+
const documents: SourceDocument[] = [];
|
|
128
|
+
async function walk(directory: string, relative = ""): Promise<void> {
|
|
129
|
+
let entries;
|
|
130
|
+
try { entries = await readdir(path.join(directory, relative), { withFileTypes: true }); } catch { return; }
|
|
131
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
132
|
+
const child = path.join(relative, entry.name).replaceAll("\\", "/");
|
|
133
|
+
if (entry.isDirectory()) {
|
|
134
|
+
if (/^(?:\.git|node_modules|dist|build|coverage|vendor|target)(\/|$)/.test(child)) continue;
|
|
135
|
+
await walk(directory, child); continue;
|
|
136
|
+
}
|
|
137
|
+
if (!entry.isFile() || !shouldInspectDocument(child)) continue;
|
|
138
|
+
const absolute = path.join(directory, child);
|
|
139
|
+
const info = await stat(absolute);
|
|
140
|
+
if (info.size > 512 * 1024) continue;
|
|
141
|
+
const content = await readFile(absolute, "utf8");
|
|
142
|
+
const contentHash = sha(content);
|
|
143
|
+
documents.push({ id: `DOC-${String(documents.length + 1).padStart(4, "0")}`, path: child, contentHash, title: titleFromMarkdown(child, content), headings: headingsFromMarkdown(content), excerpt: excerpt(content) });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
await walk(root);
|
|
147
|
+
const sources = documents.slice(0, 500);
|
|
148
|
+
return { sources, classifications: sources.map(classifyDocument) };
|
|
149
|
+
}
|
|
150
|
+
function sourcesFor(type: KnowledgeDocumentType, knowledge: DocumentKnowledge, scope?: "project" | "requirement"): Array<{ source: SourceDocument; classification: DocumentClassification }> {
|
|
151
|
+
return knowledge.classifications
|
|
152
|
+
.filter(classification => classification.documentType === type && (!scope || classification.scope === scope))
|
|
153
|
+
.map(classification => ({ classification, source: knowledge.sources.find(source => source.id === classification.sourceDocumentId)! }))
|
|
154
|
+
.filter(item => Boolean(item.source));
|
|
155
|
+
}
|
|
156
|
+
function sourceTable(items: Array<{ source: SourceDocument; classification: DocumentClassification }>): string {
|
|
157
|
+
if (!items.length) return "未发现可归类的已有文档。";
|
|
158
|
+
return table(["来源", "标题", "置信度", "理由"], items.map(item => [item.source.path, item.source.title, item.classification.confidence, item.classification.reasons.join(";")]));
|
|
159
|
+
}
|
|
72
160
|
|
|
73
161
|
async function narrative(context: ProjectContext, document: string, facts: Fact[], mode: DocumentGenerationMode, organizer?: LowTierDocumentOrganizer): Promise<{ text: string; modelRole?: string }> {
|
|
74
162
|
const knownFacts = facts.slice(0, 80).map(fact => `${fact.kind}: ${Object.values(fact.value).filter(value => typeof value === "string" || typeof value === "number").slice(0, 3).join(" / ")}`);
|
|
@@ -113,7 +201,7 @@ function renderDocuments(context: ProjectContext, facts: Fact[], intro: string,
|
|
|
113
201
|
return outputs;
|
|
114
202
|
}
|
|
115
203
|
|
|
116
|
-
function projectKnowledgeBody(context: ProjectContext, type: KnowledgeDocumentType, facts: Fact[], intro: string, snapshotId: string, generation?: string): string {
|
|
204
|
+
function projectKnowledgeBody(context: ProjectContext, type: KnowledgeDocumentType, facts: Fact[], intro: string, snapshotId: string, knowledge: DocumentKnowledge, generation?: string): string {
|
|
117
205
|
const apis = facts.filter(fact => fact.kind === "api");
|
|
118
206
|
const pages = facts.filter(fact => fact.kind === "page");
|
|
119
207
|
const models = facts.filter(fact => fact.kind === "data_model");
|
|
@@ -131,6 +219,10 @@ function projectKnowledgeBody(context: ProjectContext, type: KnowledgeDocumentTy
|
|
|
131
219
|
managed: true,
|
|
132
220
|
});
|
|
133
221
|
const evidence = [
|
|
222
|
+
"",
|
|
223
|
+
"## 已发现资料",
|
|
224
|
+
"",
|
|
225
|
+
sourceTable(sourcesFor(type, knowledge, "project")),
|
|
134
226
|
"",
|
|
135
227
|
"## 证据绑定",
|
|
136
228
|
"",
|
|
@@ -150,7 +242,7 @@ function projectKnowledgeBody(context: ProjectContext, type: KnowledgeDocumentTy
|
|
|
150
242
|
|
|
151
243
|
function requirementTemplateBody(context: ProjectContext, type: KnowledgeDocumentType, snapshotId: string, generation?: string): string {
|
|
152
244
|
const header = frontmatter({
|
|
153
|
-
type: `requirement-${type}`,
|
|
245
|
+
type: type === "requirements" ? "requirement-spec" : `requirement-${type}`,
|
|
154
246
|
scope: "requirement",
|
|
155
247
|
project: context.manifest.name,
|
|
156
248
|
requirement_id: "REQ-TEMPLATE",
|
|
@@ -164,19 +256,35 @@ function requirementTemplateBody(context: ProjectContext, type: KnowledgeDocumen
|
|
|
164
256
|
return `${header}# ${documentTitle(type)}\n\n本文件是需求级文档模板。创建具体需求时复制到 \`.context/requirements/REQ-xxx-短名/${type === "requirements" ? "requirement" : type}.md\`,并把 frontmatter 中的 \`requirement_id\`、\`status\`、关联路径和 API 补全。\n\n## 当前结论\n\n- 待补充。\n\n## 证据与约束\n\n- 初始化快照:${snapshotId}\n- 代码索引 generation:${generation ?? "未生成或已跳过"}\n- 需求级内容必须引用项目级文档和实际源码证据,不能只凭推断确认。\n\n## 待补充\n\n- 背景与目标\n- 范围与非目标\n- 设计决策\n- 验收条件\n`;
|
|
165
257
|
}
|
|
166
258
|
|
|
167
|
-
function
|
|
259
|
+
function inboxRequirementBody(context: ProjectContext, source: SourceDocument, classification: DocumentClassification, snapshotId: string, generation?: string): Array<{ targetPath: string; body: string }> {
|
|
260
|
+
const requirementId = classification.requirementId ?? source.id;
|
|
261
|
+
const folder = `${requirementId}-${slug(source.title)}`;
|
|
262
|
+
const base = `.context/requirements/_inbox/${folder}`;
|
|
263
|
+
const documentType = classification.documentType === "unknown" ? "requirements" : classification.documentType;
|
|
264
|
+
const body = `${frontmatter({ type: documentType === "requirements" ? "requirement-spec" : `requirement-${documentType}`, scope: "requirement", project: context.manifest.name, requirement_id: requirementId, status: "inbox", source_snapshot: snapshotId, code_index_generation: generation, source_documents: [source.path], managed: true })}# ${source.title}\n\n本文档由初始化阶段从已有资料归档而来,尚未确认是否为正式需求。\n\n## 来源摘要\n\n${source.excerpt || "无可用摘要。"}\n\n## 来源文档\n\n${table(["来源", "标题", "内容哈希"], [[source.path, source.title, source.contentHash]])}\n\n## 归类信息\n\n- 类型:${classification.documentType}\n- 置信度:${classification.confidence}\n- 理由:${classification.reasons.join(";")}\n\n## 待确认\n\n- 是否转为正式 REQ 编号\n- 是否拆分为多个需求\n- 是否补齐产品、技术、API 与用例文档\n`;
|
|
265
|
+
return [
|
|
266
|
+
{ targetPath: `${base}/index.md`, body },
|
|
267
|
+
{ targetPath: `${base}/${documentType === "requirements" ? "requirement" : documentType}.md`, body },
|
|
268
|
+
];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function renderKnowledgeDocuments(context: ProjectContext, facts: Fact[], intro: string, snapshot: { id?: unknown; indexRef?: unknown }, knowledge: DocumentKnowledge): Array<{ targetPath: string; body: string }> {
|
|
168
272
|
const snapshotId = String(snapshot.id ?? "");
|
|
169
273
|
const generation = codeIndexGeneration(snapshot);
|
|
170
274
|
const outputs: Array<{ targetPath: string; body: string }> = [];
|
|
171
275
|
outputs.push({ targetPath: ".context/index.md", body: `${frontmatter({ type: "knowledge-index", scope: "project", project: context.manifest.name, status: "draft", source_snapshot: snapshotId, code_index_generation: generation, managed: true })}# ${context.manifest.name} 项目知识库\n\n## 项目级文档\n\n${KNOWLEDGE_DOCUMENT_TYPES.map(type => `- [${documentTitle(type)}](/project/${type}.md)`).join("\n")}\n\n## 需求级文档\n\n- 新需求使用 [.context/requirements/_template/index.md](/requirements/_template/index.md) 作为文档包模板。\n\n## 机器索引\n\n- 文档索引:\`.planning/document-index.json\`\n- 代码索引:\`.planning/code-index/\`\n` });
|
|
172
276
|
outputs.push({ targetPath: ".context/project/index.md", body: `${frontmatter({ type: "project-document-index", scope: "project", project: context.manifest.name, status: "draft", source_snapshot: snapshotId, code_index_generation: generation, managed: true })}# 项目级文档\n\n${KNOWLEDGE_DOCUMENT_TYPES.map(type => `- [${documentTitle(type)}](./${type}.md)`).join("\n")}\n` });
|
|
173
|
-
for (const type of KNOWLEDGE_DOCUMENT_TYPES) outputs.push({ targetPath: `.context/project/${type}.md`, body: projectKnowledgeBody(context, type, facts, intro, snapshotId, generation) });
|
|
277
|
+
for (const type of KNOWLEDGE_DOCUMENT_TYPES) outputs.push({ targetPath: `.context/project/${type}.md`, body: projectKnowledgeBody(context, type, facts, intro, snapshotId, knowledge, generation) });
|
|
174
278
|
outputs.push({ targetPath: ".context/requirements/_template/index.md", body: `${frontmatter({ type: "requirement-document-index", scope: "requirement", project: context.manifest.name, requirement_id: "REQ-TEMPLATE", status: "template", source_snapshot: snapshotId, code_index_generation: generation, managed: true })}# 需求级文档模板\n\n${KNOWLEDGE_DOCUMENT_TYPES.map(type => `- [${documentTitle(type)}](./${type === "requirements" ? "requirement" : type}.md)`).join("\n")}\n` });
|
|
175
279
|
for (const type of KNOWLEDGE_DOCUMENT_TYPES) outputs.push({ targetPath: `.context/requirements/_template/${type === "requirements" ? "requirement" : type}.md`, body: requirementTemplateBody(context, type, snapshotId, generation) });
|
|
280
|
+
for (const item of knowledge.classifications.filter(item => item.scope === "requirement" && item.confidence !== "exact").slice(0, 100)) {
|
|
281
|
+
const source = knowledge.sources.find(source => source.id === item.sourceDocumentId);
|
|
282
|
+
if (source) outputs.push(...inboxRequirementBody(context, source, item, snapshotId, generation));
|
|
283
|
+
}
|
|
176
284
|
return outputs;
|
|
177
285
|
}
|
|
178
286
|
|
|
179
|
-
function renderDocumentIndex(context: ProjectContext, snapshot: { id?: unknown; indexRef?: unknown }, outputs: Array<{ targetPath: string }
|
|
287
|
+
function renderDocumentIndex(context: ProjectContext, snapshot: { id?: unknown; indexRef?: unknown }, outputs: Array<{ targetPath: string }>, knowledge: DocumentKnowledge): { targetPath: string; body: string } {
|
|
180
288
|
const snapshotId = String(snapshot.id ?? "");
|
|
181
289
|
const generation = codeIndexGeneration(snapshot);
|
|
182
290
|
const documents = outputs
|
|
@@ -193,7 +301,7 @@ function renderDocumentIndex(context: ProjectContext, snapshot: { id?: unknown;
|
|
|
193
301
|
type,
|
|
194
302
|
scope,
|
|
195
303
|
requirementId: scope === "requirement" ? (parts[2] === "_template" ? "REQ-TEMPLATE" : parts[2]) : undefined,
|
|
196
|
-
status: parts.includes("_template") ? "template" : "draft",
|
|
304
|
+
status: parts.includes("_template") ? "template" : parts.includes("_inbox") ? "inbox" : "draft",
|
|
197
305
|
managed: true,
|
|
198
306
|
sourceSnapshot: snapshotId,
|
|
199
307
|
codeIndexGeneration: generation ?? null,
|
|
@@ -201,7 +309,16 @@ function renderDocumentIndex(context: ProjectContext, snapshot: { id?: unknown;
|
|
|
201
309
|
});
|
|
202
310
|
return {
|
|
203
311
|
targetPath: ".planning/document-index.json",
|
|
204
|
-
body: `${JSON.stringify({
|
|
312
|
+
body: `${JSON.stringify({
|
|
313
|
+
schemaVersion: 2,
|
|
314
|
+
project: context.manifest.name,
|
|
315
|
+
projectId: context.manifest.projectId,
|
|
316
|
+
sourceSnapshot: snapshotId,
|
|
317
|
+
codeIndexGeneration: generation ?? null,
|
|
318
|
+
sourceDocuments: knowledge.sources,
|
|
319
|
+
classifications: knowledge.classifications,
|
|
320
|
+
documents,
|
|
321
|
+
}, null, 2)}\n`,
|
|
205
322
|
};
|
|
206
323
|
}
|
|
207
324
|
|
|
@@ -222,8 +339,9 @@ export async function generateProjectDocuments(context: ProjectContext, snapshot
|
|
|
222
339
|
try { organized = await narrative(context, "project-initialization", facts, options.mode, options.organizer); }
|
|
223
340
|
catch (error) { store.failDocumentJob(batchJob, error instanceof Error ? error.message : String(error)); throw error; }
|
|
224
341
|
const snapshotMeta = (store.queryContext({ snapshotId, limit: 1 }) as { snapshot: { id?: unknown; indexRef?: unknown } }).snapshot;
|
|
225
|
-
const
|
|
226
|
-
outputs.
|
|
342
|
+
const knowledge = await discoverSourceDocuments(context.root);
|
|
343
|
+
const outputs = [...renderDocuments(context, facts, organized.text, collections), ...renderKnowledgeDocuments(context, facts, organized.text, snapshotMeta, knowledge)];
|
|
344
|
+
outputs.push(renderDocumentIndex(context, snapshotMeta, outputs, knowledge));
|
|
227
345
|
const results: DocumentGenerationResult[] = [];
|
|
228
346
|
for (const output of outputs) {
|
|
229
347
|
const job = store.createDocumentJob(snapshotId, { targetPath: output.targetPath, ownership: "generated", templateVersion: TEMPLATE_VERSION, modelRole: organized.modelRole, input: facts.map(fact => fact.objectKey) });
|