@guanlin4924/devflow-pi 0.1.65 → 0.1.67

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.65",
3
+ "version": "0.1.67",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -413,7 +413,7 @@ export function isRequirementDraftToolAllowed(toolName: string, input: unknown,
413
413
  return normalized === "task" && taskAgents.length > 0 && taskAgents.every(agent => PLANNING_AGENTS.has(agent));
414
414
  }
415
415
 
416
- type InternalDevflowControlResource = "contract" | "state" | "mainflow" | "requirement_graph" | "requirement_coverage";
416
+ type InternalDevflowControlResource = "contract" | "state" | "mainflow" | "requirement_graph" | "requirement_coverage" | "project_context" | "skill_script";
417
417
 
418
418
  /** Resolve an xdev target only when all supplied target aliases agree.
419
419
  * A direct/eval caller can otherwise provide `path` for a real file plus an
@@ -440,6 +440,8 @@ export function internalDevflowControlResource(toolName: string, input: unknown)
440
440
  if (target === "xd://devflow_mainflow") return "mainflow";
441
441
  if (target === "xd://devflow_requirement_graph") return "requirement_graph";
442
442
  if (target === "xd://devflow_requirement_coverage") return "requirement_coverage";
443
+ if (target === "xd://devflow_project_context") return "project_context";
444
+ if (target === "xd://devflow_skill_script") return "skill_script";
443
445
  return undefined;
444
446
  }
445
447
 
package/src/constants.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.65";
2
+ export const PRODUCT_VERSION = "0.1.67";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5
 
@@ -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 renderKnowledgeDocuments(context: ProjectContext, facts: Fact[], intro: string, snapshot: { id?: unknown; indexRef?: unknown }): Array<{ targetPath: string; body: string }> {
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 }>): { targetPath: string; body: 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({ schemaVersion: 1, project: context.manifest.name, projectId: context.manifest.projectId, sourceSnapshot: snapshotId, codeIndexGeneration: generation ?? null, documents }, null, 2)}\n`,
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 outputs = [...renderDocuments(context, facts, organized.text, collections), ...renderKnowledgeDocuments(context, facts, organized.text, snapshotMeta)];
226
- outputs.push(renderDocumentIndex(context, snapshotMeta, 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) });