@guanlin4924/devflow-pi 0.1.67 → 0.1.69
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
|
@@ -7,6 +7,8 @@ description: "Use when a project needs database-backed initialization, repositor
|
|
|
7
7
|
|
|
8
8
|
Initialize project context only. Do not create requirements, approved API contracts, prototypes, roadmaps, commits, tags, repositories, or workflow state.
|
|
9
9
|
|
|
10
|
+
This skill is the project-context interface wrapper. Prefer using it from a delegated sub-agent when the user asks to initialize, refresh, query, compare, or diagnose project context. The sub-agent owns interface selection, tool-call argument construction, evidence collection, and result packaging. The parent agent should receive a compact result pack instead of hand-building low-level `devflow_project_context` calls.
|
|
11
|
+
|
|
10
12
|
## Authority and ownership
|
|
11
13
|
|
|
12
14
|
The control database under the product data directory is the only machine authority for initialization. `.devflow/project.json` locates project identity. Workspaces, scan configurations, input snapshots, runs, facts, evidence, index references, current pointers, document jobs, and authority revisions are stored through `devflow_project_context`.
|
|
@@ -15,6 +17,91 @@ Never determine current initialization state from Markdown, file mtimes, or dupl
|
|
|
15
17
|
|
|
16
18
|
Human-readable files are projections. Existing unmanaged files are preserved; the generator writes a `.devflow-pending` sibling when it cannot prove that the current file is its previous output.
|
|
17
19
|
|
|
20
|
+
## External contract usage
|
|
21
|
+
|
|
22
|
+
Before choosing a tool action, inspect the generated external contract when it exists:
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
.planning/interface-index.json
|
|
26
|
+
.planning/knowledge-index.json
|
|
27
|
+
.context/knowledge/interfaces/
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`.planning/interface-index.json` is the machine-readable contract for external AI/tool callers. It lists callable interfaces, actions, request shapes, response shapes, read/write boundaries, and approval tier. `.context/knowledge/interfaces/` is the human-readable projection of the same contract. `.planning/knowledge-index.json` connects interface nodes, source documents, knowledge pages, code facts, and reserved BUG/FIX/INCIDENT/LESSON nodes.
|
|
31
|
+
|
|
32
|
+
If the contract files do not exist yet, use the rules in this skill to run initialization and then report that the interface contract was created. If they exist, do not invent action shapes from memory; follow the contract unless it conflicts with this skill's safety rules.
|
|
33
|
+
|
|
34
|
+
## Delegated agent usage
|
|
35
|
+
|
|
36
|
+
Use a delegated sub-agent for non-trivial project-context work:
|
|
37
|
+
|
|
38
|
+
- initialize or refresh a project;
|
|
39
|
+
- query APIs, pages, models, toolchain, requirements, knowledge, or BUG/FIX nodes;
|
|
40
|
+
- compare snapshots after code or documentation changes;
|
|
41
|
+
- refresh after a BUG fix;
|
|
42
|
+
- diagnose failed or partial initialization;
|
|
43
|
+
- rebuild documents from an existing snapshot.
|
|
44
|
+
|
|
45
|
+
The delegated sub-agent must:
|
|
46
|
+
|
|
47
|
+
1. read `.planning/interface-index.json` when available;
|
|
48
|
+
2. choose the narrowest safe action;
|
|
49
|
+
3. call `devflow_project_context` or `devflow_skill_script` only through the documented action shape;
|
|
50
|
+
4. read `.planning/knowledge-index.json` and `.planning/code-index/` only when needed for evidence;
|
|
51
|
+
5. return a result pack to the parent agent.
|
|
52
|
+
|
|
53
|
+
The parent agent should not directly compose multi-step initialization calls unless the task is a simple status read or the delegated agent path is unavailable.
|
|
54
|
+
|
|
55
|
+
## Supported intents and action selection
|
|
56
|
+
|
|
57
|
+
| Intent | Preferred action sequence | Notes |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| `initialize-project` | `workspaces` → `config` → `scan` → `project-context-db-validate` | Full mode by default; static only; no workflow state mutation. |
|
|
60
|
+
| `query-facts` | `query` | Requires a fixed `snapshotId`; follow pagination. |
|
|
61
|
+
| `query-api` | `query(kind="api")` plus code-index readback when implementation detail is needed | Do not scan just to answer from an existing fresh snapshot. |
|
|
62
|
+
| `query-knowledge` | read `knowledge-index.json` and relevant `.context/knowledge/*` | Use `query` only for machine facts. |
|
|
63
|
+
| `query-bug-knowledge` | read reserved/actual BUG/FIX/INCIDENT/LESSON nodes, then query/diff if code facts are needed | Do not invent BUG records. |
|
|
64
|
+
| `refresh-after-code-change` | `scan` → optional `diff` | Use a new request ID. Compare old and new snapshots when impact is requested. |
|
|
65
|
+
| `refresh-after-bug-fix` | verify BUG/FIX/LESSON docs if present → `scan` → optional `diff` | `scan` refreshes facts; it does not fix the bug. |
|
|
66
|
+
| `regenerate-docs` | `generate-docs` | Use when the snapshot is still valid and only projections need rebuilding. |
|
|
67
|
+
| `compare-snapshots` | `diff` | Incompatible configs are not deletion evidence. |
|
|
68
|
+
| `diagnose-initialization` | `run-status` → `query`/`config`/`workspaces` as needed | Report exact failing stage and recovery option. |
|
|
69
|
+
|
|
70
|
+
High-risk maintenance actions (`restore`, `switch-authority`, `backup`, `recover`, `cancel`) require an explicit user request or a clearly scoped recovery task. Never run them as part of ordinary query or explanation.
|
|
71
|
+
|
|
72
|
+
## Result pack contract
|
|
73
|
+
|
|
74
|
+
Every delegated project-context result must return this structure in prose or JSON-like Markdown:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
## Conclusion
|
|
78
|
+
- concise answer or initialization status
|
|
79
|
+
|
|
80
|
+
## Actions Used
|
|
81
|
+
- interface/action names and whether each was read or exec
|
|
82
|
+
|
|
83
|
+
## Snapshot
|
|
84
|
+
- runId
|
|
85
|
+
- snapshotId
|
|
86
|
+
- codeIndexGeneration
|
|
87
|
+
- sourceSnapshot
|
|
88
|
+
- interfaceContract: .planning/interface-index.json if used
|
|
89
|
+
|
|
90
|
+
## Evidence
|
|
91
|
+
- facts, knowledge pages, code facts, document paths, or diagnostics used
|
|
92
|
+
|
|
93
|
+
## Changes / Impact
|
|
94
|
+
- changed facts or documents when diff/refresh was requested
|
|
95
|
+
|
|
96
|
+
## Gaps
|
|
97
|
+
- missing evidence, unsupported adapters, stale index, conflicts, or manual files preserved as .devflow-pending
|
|
98
|
+
|
|
99
|
+
## Recommended Next Action
|
|
100
|
+
- query, diff, generate-docs, scan, human review, or no action
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The result pack must distinguish observed machine facts from draft knowledge, historical imports, model candidates, and human-confirmed content.
|
|
104
|
+
|
|
18
105
|
## Modes
|
|
19
106
|
|
|
20
107
|
- Full mode is the default. It performs bounded static fact collection and builds, validates, and audits an immutable code-index generation before the context snapshot becomes current.
|
package/src/constants.ts
CHANGED
package/src/project-documents.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
import { InitializationStore } from "./initialization-store.ts";
|
|
5
|
+
import { FACT_KINDS, InitializationStore } from "./initialization-store.ts";
|
|
6
6
|
import type { ProjectContext } from "./project-context.ts";
|
|
7
7
|
|
|
8
8
|
export interface LowTierDocumentOrganizer {
|
|
@@ -18,6 +18,35 @@ type Fact = { kind: string; repository: string; objectKey: string; value: Record
|
|
|
18
18
|
type SourceDocument = { id: string; path: string; contentHash: string; title: string; headings: string[]; excerpt: string };
|
|
19
19
|
type DocumentClassification = { sourceDocumentId: string; scope: "project" | "requirement" | "unknown"; documentType: KnowledgeDocumentType | "unknown"; requirementId?: string; requirementTitle?: string; confidence: "exact" | "inferred" | "heuristic"; reasons: string[] };
|
|
20
20
|
type DocumentKnowledge = { sources: SourceDocument[]; classifications: DocumentClassification[] };
|
|
21
|
+
type KnowledgePageType = "source-summary" | "entity" | "concept" | "requirement" | "synthesis" | "bug-report" | "bug-fix" | "incident" | "lesson";
|
|
22
|
+
type KnowledgePage = {
|
|
23
|
+
id: string;
|
|
24
|
+
path: string;
|
|
25
|
+
type: KnowledgePageType;
|
|
26
|
+
title: string;
|
|
27
|
+
status: "draft" | "inbox";
|
|
28
|
+
sourceRefs: string[];
|
|
29
|
+
relatedRequirements: string[];
|
|
30
|
+
relatedCodeFacts: string[];
|
|
31
|
+
confidence: "high" | "medium" | "low";
|
|
32
|
+
};
|
|
33
|
+
type IndexedDocumentScope = "project" | "requirement" | "knowledge";
|
|
34
|
+
type InterfaceContract = {
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
kind: "tool" | "skill" | "cli" | "artifact";
|
|
38
|
+
path: string;
|
|
39
|
+
purpose: string;
|
|
40
|
+
approval: "read" | "exec" | "none";
|
|
41
|
+
actions: Array<{
|
|
42
|
+
name: string;
|
|
43
|
+
purpose: string;
|
|
44
|
+
request: Record<string, unknown>;
|
|
45
|
+
response: Record<string, unknown>;
|
|
46
|
+
writes: string[];
|
|
47
|
+
reads: string[];
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
21
50
|
const TEMPLATE_VERSION = "project-context-v2";
|
|
22
51
|
const KNOWLEDGE_DOCUMENT_TYPES = [
|
|
23
52
|
"requirements",
|
|
@@ -153,10 +182,107 @@ function sourcesFor(type: KnowledgeDocumentType, knowledge: DocumentKnowledge, s
|
|
|
153
182
|
.map(classification => ({ classification, source: knowledge.sources.find(source => source.id === classification.sourceDocumentId)! }))
|
|
154
183
|
.filter(item => Boolean(item.source));
|
|
155
184
|
}
|
|
185
|
+
function factRef(fact: Fact): string {
|
|
186
|
+
if (fact.kind === "api") return `api_route:${String(fact.value.method ?? "UNKNOWN").toUpperCase()}:${apiPath(fact)}`;
|
|
187
|
+
return `${fact.kind}:${fact.repository}:${fact.objectKey}`;
|
|
188
|
+
}
|
|
189
|
+
function relatedCodeFactsForSource(source: SourceDocument, classification: DocumentClassification, facts: Fact[]): string[] {
|
|
190
|
+
const haystack = `${source.path}\n${source.title}\n${source.headings.join("\n")}\n${source.excerpt}`.toLowerCase();
|
|
191
|
+
const candidates = facts.filter(fact => {
|
|
192
|
+
const values = Object.values(fact.value).filter(value => typeof value === "string" || typeof value === "number").map(String);
|
|
193
|
+
return values.some(value => {
|
|
194
|
+
const token = value.toLowerCase();
|
|
195
|
+
return token.length >= 3 && (haystack.includes(token) || token.includes(slug(source.title).replaceAll("-", "")));
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
const fallback = classification.documentType === "api" ? facts.filter(fact => fact.kind === "api")
|
|
199
|
+
: classification.documentType === "frontend-tech" || classification.documentType === "product-design" ? facts.filter(fact => fact.kind === "page")
|
|
200
|
+
: classification.documentType === "backend-tech" || classification.documentType === "architecture" ? facts.filter(fact => ["api", "data_model", "integration"].includes(fact.kind))
|
|
201
|
+
: [];
|
|
202
|
+
return [...new Set([...candidates, ...fallback].slice(0, 12).map(factRef))];
|
|
203
|
+
}
|
|
204
|
+
function extractKnowledgeNames(source: SourceDocument): Array<{ kind: "entity" | "concept"; title: string; confidence: "medium" | "low" }> {
|
|
205
|
+
const names = new Map<string, { kind: "entity" | "concept"; title: string; confidence: "medium" | "low" }>();
|
|
206
|
+
const text = `${source.title}\n${source.headings.join("\n")}\n${source.excerpt}`;
|
|
207
|
+
for (const match of text.matchAll(/(?:用户|管理员|客户|商户|运营|后台|前台|服务|系统|订单|资源|内容|权限|支付|审核|任务|消息|通知|接口|页面|角色|数据|模型|流程)/g)) {
|
|
208
|
+
const title = match[0];
|
|
209
|
+
const kind: "entity" | "concept" = /用户|管理员|客户|商户|运营|服务|系统|订单|资源|内容|任务|消息|通知|角色|数据|模型/.test(title) ? "entity" : "concept";
|
|
210
|
+
names.set(`${kind}:${title}`, { kind, title, confidence: "low" });
|
|
211
|
+
}
|
|
212
|
+
for (const heading of source.headings.slice(0, 8)) {
|
|
213
|
+
const clean = heading.replace(/[:::].*$/, "").trim();
|
|
214
|
+
if (clean.length >= 2 && clean.length <= 32) names.set(`concept:${clean}`, { kind: "concept", title: clean, confidence: "medium" });
|
|
215
|
+
}
|
|
216
|
+
return [...names.values()].slice(0, 40);
|
|
217
|
+
}
|
|
218
|
+
function sourcePageId(source: SourceDocument): string { return `source-${source.id.toLowerCase()}`; }
|
|
219
|
+
function knowledgePagePath(type: KnowledgePageType, titleOrId: string): string {
|
|
220
|
+
const folder = ({
|
|
221
|
+
"source-summary": "sources",
|
|
222
|
+
entity: "entities",
|
|
223
|
+
concept: "concepts",
|
|
224
|
+
requirement: "requirements",
|
|
225
|
+
synthesis: "synthesis",
|
|
226
|
+
"bug-report": "bugs",
|
|
227
|
+
"bug-fix": "fixes",
|
|
228
|
+
incident: "incidents",
|
|
229
|
+
lesson: "lessons",
|
|
230
|
+
} satisfies Record<KnowledgePageType, string>)[type];
|
|
231
|
+
return `.context/knowledge/${folder}/${slug(titleOrId)}.md`;
|
|
232
|
+
}
|
|
156
233
|
function sourceTable(items: Array<{ source: SourceDocument; classification: DocumentClassification }>): string {
|
|
157
234
|
if (!items.length) return "未发现可归类的已有文档。";
|
|
158
235
|
return table(["来源", "标题", "置信度", "理由"], items.map(item => [item.source.path, item.source.title, item.classification.confidence, item.classification.reasons.join(";")]));
|
|
159
236
|
}
|
|
237
|
+
function apiPath(fact: Fact): string {
|
|
238
|
+
return String(fact.value.path ?? fact.objectKey.replace(/^route:[^:]+:/, ""));
|
|
239
|
+
}
|
|
240
|
+
function apiDomain(route: string): string {
|
|
241
|
+
const first = route.split("?")[0].split("/").filter(Boolean)[0];
|
|
242
|
+
if (!first) return "root";
|
|
243
|
+
if (/^\{.+\}$/.test(first) || first.startsWith(":")) return "parameterized";
|
|
244
|
+
return first;
|
|
245
|
+
}
|
|
246
|
+
function humanDomain(value: string): string {
|
|
247
|
+
return value === "root" ? "根路径" : value === "parameterized" ? "参数化根路径" : value;
|
|
248
|
+
}
|
|
249
|
+
function validModelName(value: unknown): string | undefined {
|
|
250
|
+
const name = String(value ?? "").trim();
|
|
251
|
+
if (!/^[A-Za-z_][A-Za-z0-9_.-]{2,63}$/.test(name)) return undefined;
|
|
252
|
+
const stop = new Set(["and", "are", "for", "not", "is", "etc", "data", "arguments", "options", "output", "phrase", "definition", "converts"]);
|
|
253
|
+
if (stop.has(name.toLowerCase().replace(/[.]+$/, ""))) return undefined;
|
|
254
|
+
return name;
|
|
255
|
+
}
|
|
256
|
+
function architectureBody(context: ProjectContext, facts: Fact[], intro: string, snapshotId: string, knowledge: DocumentKnowledge, generation?: string): string {
|
|
257
|
+
const repositories = [...new Set(facts.map(fact => fact.repository).filter(Boolean))].sort();
|
|
258
|
+
const apis = facts.filter(fact => fact.kind === "api");
|
|
259
|
+
const models = [...new Set(facts.filter(fact => fact.kind === "data_model").map(fact => validModelName(fact.value.name ?? fact.objectKey)).filter((item): item is string => Boolean(item)))].sort();
|
|
260
|
+
const integrations = facts.filter(fact => fact.kind === "integration");
|
|
261
|
+
const groupedApis = new Map<string, string[]>();
|
|
262
|
+
for (const fact of apis) {
|
|
263
|
+
const route = apiPath(fact);
|
|
264
|
+
const domain = apiDomain(route);
|
|
265
|
+
groupedApis.set(domain, [...(groupedApis.get(domain) ?? []), route]);
|
|
266
|
+
}
|
|
267
|
+
const apiRows = [...groupedApis.entries()]
|
|
268
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
269
|
+
.slice(0, 30)
|
|
270
|
+
.map(([domain, routes]) => [humanDomain(domain), [...new Set(routes)].slice(0, 8).join("、"), routes.length]);
|
|
271
|
+
const evidence = [
|
|
272
|
+
"",
|
|
273
|
+
"## 已发现资料",
|
|
274
|
+
"",
|
|
275
|
+
sourceTable(sourcesFor("architecture", knowledge, "project")),
|
|
276
|
+
"",
|
|
277
|
+
"## 证据绑定",
|
|
278
|
+
"",
|
|
279
|
+
`- 初始化快照:${snapshotId}`,
|
|
280
|
+
`- 代码索引 generation:${generation ?? "未生成或已跳过"}`,
|
|
281
|
+
"- 本文档由项目初始化创建骨架,人工确认前均为 draft。",
|
|
282
|
+
"",
|
|
283
|
+
].join("\n");
|
|
284
|
+
return `${frontmatter({ type: "project-architecture", scope: "project", project: context.manifest.name, status: "draft", source_snapshot: snapshotId, code_index_generation: generation, managed: true })}# 项目架构设计\n\n${intro}\n\n## 当前结论\n\n- 当前项目识别到 ${repositories.length || "未确认"} 个代码库:${repositories.join("、") || "未确认"}。\n- 后端/API 侧观察到 ${apis.length} 条静态路由声明,以下按路径前缀粗分能力域。\n- 数据模型候选已过滤明显普通词;仍需人工确认真实表、实体和持久化边界。\n- 本文档只陈述静态可观察事实,不确认部署拓扑、运行时依赖或权限链路。\n\n## 系统组成\n\n${table(["代码库", "观察到的文件数", "观察到的能力"], repositories.map(repository => [repository, facts.filter(fact => fact.repository === repository && fact.kind === "asset").length, [...new Set(facts.filter(fact => fact.repository === repository && ["api", "page", "data_model", "integration"].includes(fact.kind)).map(fact => fact.kind))].join("、") || "未发现"]))}\n\n## API 能力分组\n\n${apiRows.length ? table(["能力域", "代表路径", "路径数量"], apiRows) : "未发现可分组 API。"}\n\n## 数据模型候选\n\n${models.length ? table(["模型或表名"], models.slice(0, 80).map(name => [name])) : "未发现可信数据模型候选。"}\n\n## 集成与运行时线索\n\n${integrations.length ? table(["类型", "对象"], integrations.slice(0, 80).map(fact => [fact.value.kind ?? fact.value.name ? "配置/任务" : "外部主机", fact.value.host ?? fact.value.name ?? fact.objectKey])) : "未发现外部集成线索。"}\n\n## 待确认架构问题\n\n- 前端、后端、任务执行器和外部服务的部署边界是什么?\n- 认证、授权和用户上下文如何在前后端之间传递?\n- 是否存在异步任务队列、调度器、设备/浏览器执行器或后台 worker?\n- 核心数据模型、数据库类型和迁移策略是什么?\n- 关键能力域之间的数据流、失败降级和监控指标是什么?\n${evidence}`;
|
|
285
|
+
}
|
|
160
286
|
|
|
161
287
|
async function narrative(context: ProjectContext, document: string, facts: Fact[], mode: DocumentGenerationMode, organizer?: LowTierDocumentOrganizer): Promise<{ text: string; modelRole?: string }> {
|
|
162
288
|
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(" / ")}`);
|
|
@@ -233,7 +359,7 @@ function projectKnowledgeBody(context: ProjectContext, type: KnowledgeDocumentTy
|
|
|
233
359
|
].join("\n");
|
|
234
360
|
if (type === "requirements") return `${header}# 项目总体需求\n\n${intro}\n\n## 项目范围\n\n${table(["代码库", "扫描事实数"], repositoryNames.map(repository => [repository, facts.filter(fact => fact.repository === repository).length]))}\n\n## 已观察业务能力\n\n${table(["类型", "对象"], [...pages, ...apis, ...models].slice(0, 80).map(fact => [fact.kind, fact.value.route ?? fact.value.path ?? fact.value.name ?? fact.objectKey]))}\n\n## 待人工补充\n\n- 项目目标与非目标\n- 用户角色与核心流程\n- 业务约束与优先级\n${evidence}`;
|
|
235
361
|
if (type === "product-design") return `${header}# 项目产品与交互设计\n\n${intro}\n\n## 已发现页面/入口\n\n${table(["代码库", "页面或入口"], pages.slice(0, 100).map(fact => [fact.repository, fact.value.route ?? fact.value.componentPath ?? fact.objectKey]))}\n\n## 待人工补充\n\n- 信息架构\n- 页面状态与异常态\n- 文案、权限与可访问性约束\n${evidence}`;
|
|
236
|
-
if (type === "architecture") return
|
|
362
|
+
if (type === "architecture") return architectureBody(context, facts, intro, snapshotId, knowledge, generation);
|
|
237
363
|
if (type === "frontend-tech") return `${header}# 项目前端技术方案\n\n${intro}\n\n## 已观察前端技术\n\n${table(["声明", "来源"], tech.filter(fact => /react|vue|vite|router|zustand|tailwind|typescript|eslint|vitest/i.test(String(fact.value.name ?? fact.value.extension ?? ""))).slice(0, 120).map(fact => [fact.value.name ?? fact.value.extension, fact.value.declaration ?? fact.value.path ?? "文件统计"]))}\n\n## 待人工补充\n\n- 路由、状态管理与组件边界\n- API 调用约定\n- 构建、测试与发布规则\n${evidence}`;
|
|
238
364
|
if (type === "backend-tech") return `${header}# 项目后端技术方案\n\n${intro}\n\n## 已观察后端事实\n\n${table(["类型", "对象"], [...apis, ...models, ...integrations].slice(0, 120).map(fact => [fact.kind, fact.value.path ?? fact.value.name ?? fact.value.host ?? fact.objectKey]))}\n\n## 待人工补充\n\n- handler/service/repository 分层\n- 数据库迁移与任务调度规则\n- 鉴权、错误码、日志与监控\n${evidence}`;
|
|
239
365
|
if (type === "test-cases") return `${header}# 项目用例与验收\n\n${intro}\n\n## 已发现测试/命令线索\n\n${table(["代码库", "名称", "命令或路径"], commands.slice(0, 120).map(fact => [fact.repository, fact.value.name ?? "test-definition", fact.value.command ?? fact.value.testDefinition ?? fact.objectKey]))}\n\n## 待人工补充\n\n- 核心主流程验收用例\n- 异常路径与权限用例\n- 回归范围与自动化策略\n${evidence}`;
|
|
@@ -268,6 +394,539 @@ function inboxRequirementBody(context: ProjectContext, source: SourceDocument, c
|
|
|
268
394
|
];
|
|
269
395
|
}
|
|
270
396
|
|
|
397
|
+
function buildKnowledgePages(knowledge: DocumentKnowledge, facts: Fact[]): KnowledgePage[] {
|
|
398
|
+
const pages = new Map<string, KnowledgePage>();
|
|
399
|
+
for (const source of knowledge.sources) {
|
|
400
|
+
const classification = knowledge.classifications.find(item => item.sourceDocumentId === source.id);
|
|
401
|
+
const requirementId = classification?.requirementId ? classification.requirementId : classification?.scope === "requirement" ? source.id : undefined;
|
|
402
|
+
const sourceRef = sourcePageId(source);
|
|
403
|
+
const relatedCodeFacts = classification ? relatedCodeFactsForSource(source, classification, facts) : [];
|
|
404
|
+
pages.set(sourceRef, {
|
|
405
|
+
id: sourceRef,
|
|
406
|
+
path: knowledgePagePath("source-summary", sourceRef),
|
|
407
|
+
type: "source-summary",
|
|
408
|
+
title: source.title,
|
|
409
|
+
status: classification?.scope === "requirement" && classification.confidence !== "exact" ? "inbox" : "draft",
|
|
410
|
+
sourceRefs: [source.path],
|
|
411
|
+
relatedRequirements: requirementId ? [requirementId] : [],
|
|
412
|
+
relatedCodeFacts,
|
|
413
|
+
confidence: classification?.confidence === "exact" ? "high" : classification?.confidence === "inferred" ? "medium" : "low",
|
|
414
|
+
});
|
|
415
|
+
if (classification?.scope === "requirement") {
|
|
416
|
+
const id = requirementId ?? `REQ-${source.id}`;
|
|
417
|
+
pages.set(`requirement-${slug(id)}`, {
|
|
418
|
+
id: `requirement-${slug(id)}`,
|
|
419
|
+
path: knowledgePagePath("requirement", id),
|
|
420
|
+
type: "requirement",
|
|
421
|
+
title: classification.requirementTitle ?? source.title,
|
|
422
|
+
status: classification.confidence === "exact" ? "draft" : "inbox",
|
|
423
|
+
sourceRefs: [sourceRef],
|
|
424
|
+
relatedRequirements: [id],
|
|
425
|
+
relatedCodeFacts,
|
|
426
|
+
confidence: classification.confidence === "exact" ? "high" : "medium",
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
for (const extracted of extractKnowledgeNames(source)) {
|
|
430
|
+
const id = `${extracted.kind}-${slug(extracted.title)}`;
|
|
431
|
+
const existing = pages.get(id);
|
|
432
|
+
pages.set(id, {
|
|
433
|
+
id,
|
|
434
|
+
path: knowledgePagePath(extracted.kind, extracted.title),
|
|
435
|
+
type: extracted.kind,
|
|
436
|
+
title: extracted.title,
|
|
437
|
+
status: "draft",
|
|
438
|
+
sourceRefs: [...new Set([...(existing?.sourceRefs ?? []), sourceRef])],
|
|
439
|
+
relatedRequirements: [...new Set([...(existing?.relatedRequirements ?? []), ...(requirementId ? [requirementId] : [])])],
|
|
440
|
+
relatedCodeFacts: [...new Set([...(existing?.relatedCodeFacts ?? []), ...relatedCodeFacts])].slice(0, 20),
|
|
441
|
+
confidence: existing?.confidence === "medium" || extracted.confidence === "medium" ? "medium" : "low",
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const allSourceRefs = [...new Set([...pages.values()].flatMap(page => page.sourceRefs).filter(ref => ref.startsWith("source-")))];
|
|
446
|
+
pages.set("synthesis-project-overview", {
|
|
447
|
+
id: "synthesis-project-overview",
|
|
448
|
+
path: ".context/knowledge/synthesis/project-overview.md",
|
|
449
|
+
type: "synthesis",
|
|
450
|
+
title: "项目知识总览",
|
|
451
|
+
status: "draft",
|
|
452
|
+
sourceRefs: allSourceRefs,
|
|
453
|
+
relatedRequirements: [...new Set([...pages.values()].flatMap(page => page.relatedRequirements))].slice(0, 100),
|
|
454
|
+
relatedCodeFacts: [...new Set([...pages.values()].flatMap(page => page.relatedCodeFacts))].slice(0, 100),
|
|
455
|
+
confidence: "medium",
|
|
456
|
+
});
|
|
457
|
+
return [...pages.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function renderKnowledgePage(context: ProjectContext, page: KnowledgePage, sourceByRef: Map<string, SourceDocument>, snapshotId: string, generation?: string): string {
|
|
461
|
+
const sourceRows = page.sourceRefs.map(ref => {
|
|
462
|
+
const source = sourceByRef.get(ref);
|
|
463
|
+
return source ? [ref, source.path, source.title] : [ref, ref, "外部或原始来源"];
|
|
464
|
+
});
|
|
465
|
+
const overview = page.type === "source-summary"
|
|
466
|
+
? "本页是原始资料的结构化摘要,用于替代直接对原始长文档做向量召回。"
|
|
467
|
+
: page.type === "synthesis"
|
|
468
|
+
? "本页综合多个来源与代码事实,作为项目级知识入口。"
|
|
469
|
+
: "本页沉淀初始化阶段识别出的项目知识单元,人工确认前保持草稿状态。";
|
|
470
|
+
const sourceSummary = page.sourceRefs.map(ref => sourceByRef.get(ref)?.excerpt).filter(Boolean).slice(0, 5).join("\n\n");
|
|
471
|
+
return `${frontmatter({
|
|
472
|
+
type: page.type,
|
|
473
|
+
id: page.id,
|
|
474
|
+
title: page.title,
|
|
475
|
+
status: page.status,
|
|
476
|
+
project: context.manifest.name,
|
|
477
|
+
source_refs: page.sourceRefs,
|
|
478
|
+
related_requirements: page.relatedRequirements,
|
|
479
|
+
related_code_facts: page.relatedCodeFacts,
|
|
480
|
+
confidence: page.confidence,
|
|
481
|
+
source_snapshot: snapshotId,
|
|
482
|
+
code_index_generation: generation,
|
|
483
|
+
managed: true,
|
|
484
|
+
})}# ${page.title}
|
|
485
|
+
|
|
486
|
+
## Overview
|
|
487
|
+
|
|
488
|
+
${overview}
|
|
489
|
+
|
|
490
|
+
${sourceSummary ? `## Key Claims\n\n${sourceSummary.split(/[。;;\n]+/).map(item => item.trim()).filter(Boolean).slice(0, 12).map(item => `- ${item}`).join("\n")}\n\n` : ""}## Relationships
|
|
491
|
+
|
|
492
|
+
${page.relatedRequirements.length ? `- 关联需求:${page.relatedRequirements.join("、")}` : "- 关联需求:未确认"}
|
|
493
|
+
${page.relatedCodeFacts.length ? `- 关联代码事实:${page.relatedCodeFacts.slice(0, 20).join("、")}` : "- 关联代码事实:未确认"}
|
|
494
|
+
|
|
495
|
+
## Sources
|
|
496
|
+
|
|
497
|
+
${sourceRows.length ? table(["引用", "路径", "标题"], sourceRows) : "未绑定来源。"}
|
|
498
|
+
|
|
499
|
+
## Maintenance
|
|
500
|
+
|
|
501
|
+
- 状态:${page.status}
|
|
502
|
+
- 置信度:${page.confidence}
|
|
503
|
+
- 初始化快照:${snapshotId}
|
|
504
|
+
- 代码索引 generation:${generation ?? "未生成或已跳过"}
|
|
505
|
+
`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function bugKnowledgeTemplates(context: ProjectContext, snapshotId: string, generation?: string): Array<{ targetPath: string; body: string }> {
|
|
509
|
+
const common = { project: context.manifest.name, status: "template", source_snapshot: snapshotId, code_index_generation: generation, managed: true };
|
|
510
|
+
return [
|
|
511
|
+
{
|
|
512
|
+
targetPath: ".context/knowledge/bugs/_template.md",
|
|
513
|
+
body: `${frontmatter({ ...common, type: "bug-report", id: "BUG-TEMPLATE", title: "BUG 模板", source_refs: [], related_requirements: [], related_code_facts: [], related_fixes: [], regression_tests: [], confidence: "manual" })}# BUG 模板
|
|
514
|
+
|
|
515
|
+
## Symptom
|
|
516
|
+
|
|
517
|
+
- 用户或系统观察到的异常现象。
|
|
518
|
+
|
|
519
|
+
## Impact
|
|
520
|
+
|
|
521
|
+
- 影响范围、严重程度、受影响角色或能力。
|
|
522
|
+
|
|
523
|
+
## Reproduction
|
|
524
|
+
|
|
525
|
+
1. 复现步骤待补充。
|
|
526
|
+
|
|
527
|
+
## Root Cause
|
|
528
|
+
|
|
529
|
+
- 根因待确认;不能只凭猜测填写为 confirmed。
|
|
530
|
+
|
|
531
|
+
## Evidence
|
|
532
|
+
|
|
533
|
+
- 日志、截图、报错、source_refs、related_code_facts。
|
|
534
|
+
|
|
535
|
+
## Fix Link
|
|
536
|
+
|
|
537
|
+
- 关联 FIX 记录。
|
|
538
|
+
|
|
539
|
+
## Regression
|
|
540
|
+
|
|
541
|
+
- 回归测试、验证命令或人工验证证据。
|
|
542
|
+
`,
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
targetPath: ".context/knowledge/fixes/_template.md",
|
|
546
|
+
body: `${frontmatter({ ...common, type: "bug-fix", id: "FIX-TEMPLATE", title: "FIX 模板", bug_refs: [], changed_code_facts: [], regression_tests: [], confidence: "manual" })}# FIX 模板
|
|
547
|
+
|
|
548
|
+
## Change Summary
|
|
549
|
+
|
|
550
|
+
- 修复动作摘要。
|
|
551
|
+
|
|
552
|
+
## Why This Fix
|
|
553
|
+
|
|
554
|
+
- 为什么选择这个修复方式,替代方案和风险是什么。
|
|
555
|
+
|
|
556
|
+
## Files Changed
|
|
557
|
+
|
|
558
|
+
- 关联文件或 changed_code_facts。
|
|
559
|
+
|
|
560
|
+
## Regression Coverage
|
|
561
|
+
|
|
562
|
+
- 新增或复用的回归测试。
|
|
563
|
+
|
|
564
|
+
## Remaining Risk
|
|
565
|
+
|
|
566
|
+
- 尚未覆盖或需观察的风险。
|
|
567
|
+
`,
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
targetPath: ".context/knowledge/incidents/_template.md",
|
|
571
|
+
body: `${frontmatter({ ...common, type: "incident", id: "INC-TEMPLATE", title: "INCIDENT 模板", related_bugs: [], related_fixes: [], related_code_facts: [], confidence: "manual" })}# INCIDENT 模板
|
|
572
|
+
|
|
573
|
+
## Timeline
|
|
574
|
+
|
|
575
|
+
- 发现、止血、修复、恢复的时间线。
|
|
576
|
+
|
|
577
|
+
## User Impact
|
|
578
|
+
|
|
579
|
+
- 用户影响和业务影响。
|
|
580
|
+
|
|
581
|
+
## Root Cause
|
|
582
|
+
|
|
583
|
+
- 事故根因和促成因素。
|
|
584
|
+
|
|
585
|
+
## Mitigation
|
|
586
|
+
|
|
587
|
+
- 临时缓解措施和永久修复。
|
|
588
|
+
|
|
589
|
+
## Follow-ups
|
|
590
|
+
|
|
591
|
+
- 后续行动、负责人和验证方式。
|
|
592
|
+
`,
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
targetPath: ".context/knowledge/lessons/_template.md",
|
|
596
|
+
body: `${frontmatter({ ...common, type: "lesson", id: "LESSON-TEMPLATE", title: "LESSON 模板", related_bugs: [], related_fixes: [], related_code_facts: [], confidence: "manual" })}# LESSON 模板
|
|
597
|
+
|
|
598
|
+
## Lesson
|
|
599
|
+
|
|
600
|
+
- 可复用经验或规则。
|
|
601
|
+
|
|
602
|
+
## Applies To
|
|
603
|
+
|
|
604
|
+
- 适用模块、能力、需求或技术边界。
|
|
605
|
+
|
|
606
|
+
## Check Rule
|
|
607
|
+
|
|
608
|
+
- 下次如何检查同类问题。
|
|
609
|
+
|
|
610
|
+
## Evidence
|
|
611
|
+
|
|
612
|
+
- 关联 BUG、FIX、事故或代码事实。
|
|
613
|
+
`,
|
|
614
|
+
},
|
|
615
|
+
];
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function initializationInterfaceContracts(): InterfaceContract[] {
|
|
619
|
+
return [
|
|
620
|
+
{
|
|
621
|
+
id: "tool-devflow-project-context",
|
|
622
|
+
name: "devflow_project_context",
|
|
623
|
+
kind: "tool",
|
|
624
|
+
path: "xd://devflow_project_context",
|
|
625
|
+
purpose: "项目初始化控制面:扫描、查询、刷新、恢复、导入导出和权威切换。",
|
|
626
|
+
approval: "exec",
|
|
627
|
+
actions: [
|
|
628
|
+
{
|
|
629
|
+
name: "scan",
|
|
630
|
+
purpose: "执行项目初始化扫描并发布上下文快照。",
|
|
631
|
+
request: { action: "scan", mode: "full|fast", repositories: [{ name: "app", path: ".", role: "frontend|backend|shared|other" }], requiredKinds: FACT_KINDS, documentMode: "template|none", requestId: "stable-id" },
|
|
632
|
+
response: { runId: "uuid", snapshotId: "uuid", completeness: "complete|partial", diagnostics: [], documents: [{ targetPath: ".context/project/api.md", status: "complete|conflict|failed" }] },
|
|
633
|
+
reads: ["source tree", ".devflow/project.json"],
|
|
634
|
+
writes: ["control.db", ".planning/code-index/", ".planning/document-index.json", ".planning/knowledge-index.json", ".context/", ".planning/PROJECT.md"],
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
name: "query",
|
|
638
|
+
purpose: "按固定 snapshot 查询初始化事实。",
|
|
639
|
+
request: { action: "query", snapshotId: "uuid", kind: "api|page|data_model|...", repository: "optional", after: "cursor", limit: 100 },
|
|
640
|
+
response: { snapshot: {}, facts: [], truncated: false, nextCursor: null },
|
|
641
|
+
reads: ["control.db"],
|
|
642
|
+
writes: [],
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
name: "diff",
|
|
646
|
+
purpose: "比较两个明确快照,返回可比变化。",
|
|
647
|
+
request: { action: "diff", fromSnapshotId: "uuid", toSnapshotId: "uuid" },
|
|
648
|
+
response: { comparable: true, added: [], changed: [], removed: [], unavailable: [] },
|
|
649
|
+
reads: ["control.db"],
|
|
650
|
+
writes: [],
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
name: "generate-docs",
|
|
654
|
+
purpose: "针对已发布快照重试生成文档投影。",
|
|
655
|
+
request: { action: "generate-docs", snapshotId: "uuid" },
|
|
656
|
+
response: [{ targetPath: ".context/project/api.md", status: "complete|conflict|failed", outputPath: "path" }],
|
|
657
|
+
reads: ["control.db", ".planning/code-index/", "source documents"],
|
|
658
|
+
writes: [".context/", ".planning/document-index.json", ".planning/knowledge-index.json"],
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
name: "config",
|
|
662
|
+
purpose: "读取当前工作区的初始化扫描配置。",
|
|
663
|
+
request: { action: "config", workspaceId: "optional" },
|
|
664
|
+
response: { mode: "full|fast", repositories: [], requiredKinds: FACT_KINDS },
|
|
665
|
+
reads: ["control.db"],
|
|
666
|
+
writes: [],
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
name: "workspaces",
|
|
670
|
+
purpose: "列出当前项目已登记的工作区。",
|
|
671
|
+
request: { action: "workspaces" },
|
|
672
|
+
response: [{ id: "uuid", root: "path", active: true }],
|
|
673
|
+
reads: ["control.db"],
|
|
674
|
+
writes: [],
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
name: "rebind-workspace",
|
|
678
|
+
purpose: "将当前项目根重新绑定到指定 workspaceId。",
|
|
679
|
+
request: { action: "rebind-workspace", workspaceId: "uuid" },
|
|
680
|
+
response: { id: "uuid", root: "path", active: true },
|
|
681
|
+
reads: ["control.db", ".devflow/project.json"],
|
|
682
|
+
writes: ["control.db"],
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
name: "run-status",
|
|
686
|
+
purpose: "读取初始化 run 的状态和发布快照。",
|
|
687
|
+
request: { action: "run-status", runId: "uuid" },
|
|
688
|
+
response: { id: "uuid", status: "preparing|collecting|validating|finished|failed|cancelled", publishedSnapshotId: "uuid|null", error: "string|null" },
|
|
689
|
+
reads: ["control.db"],
|
|
690
|
+
writes: [],
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
name: "cancel",
|
|
694
|
+
purpose: "取消仍处于可写状态的初始化 run。",
|
|
695
|
+
request: { action: "cancel", runId: "uuid" },
|
|
696
|
+
response: { runId: "uuid", cancelled: true },
|
|
697
|
+
reads: ["control.db"],
|
|
698
|
+
writes: ["control.db"],
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
name: "recover",
|
|
702
|
+
purpose: "恢复租约过期但未结束的初始化 run。",
|
|
703
|
+
request: { action: "recover", runId: "uuid" },
|
|
704
|
+
response: { runId: "uuid", leaseToken: "opaque-token" },
|
|
705
|
+
reads: ["control.db"],
|
|
706
|
+
writes: ["control.db"],
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
name: "import-legacy",
|
|
710
|
+
purpose: "把旧版初始化产物导入数据库控制面。",
|
|
711
|
+
request: { action: "import-legacy", sources: [".planning/PROJECT.md"], requestId: "stable-id" },
|
|
712
|
+
response: { runId: "uuid", snapshotId: "uuid", imports: [] },
|
|
713
|
+
reads: ["legacy initialization files"],
|
|
714
|
+
writes: ["control.db"],
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
name: "export",
|
|
718
|
+
purpose: "把指定初始化快照导出为兼容文件。",
|
|
719
|
+
request: { action: "export", snapshotId: "uuid", targetPath: ".planning/project-context-export.json" },
|
|
720
|
+
response: { path: "path", contentHash: "sha256:..." },
|
|
721
|
+
reads: ["control.db"],
|
|
722
|
+
writes: ["targetPath"],
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
name: "authority",
|
|
726
|
+
purpose: "读取当前初始化权威来源和修订号。",
|
|
727
|
+
request: { action: "authority" },
|
|
728
|
+
response: { authority: "legacy|shadow|database", revision: 0 },
|
|
729
|
+
reads: ["control.db"],
|
|
730
|
+
writes: [],
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
name: "switch-authority",
|
|
734
|
+
purpose: "按期望 revision 切换初始化权威来源。",
|
|
735
|
+
request: { action: "switch-authority", authority: "legacy|shadow|database", expectedRevision: 0, compatibilityExport: "optional" },
|
|
736
|
+
response: { status: "action-specific", revision: "number when applicable" },
|
|
737
|
+
reads: ["control.db"],
|
|
738
|
+
writes: ["control.db", "compatibilityExport when requested"],
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
name: "backup",
|
|
742
|
+
purpose: "备份初始化控制数据库和内容状态。",
|
|
743
|
+
request: { action: "backup", targetPath: ".planning/backups/initialization.db" },
|
|
744
|
+
response: { backup: "absolute-path" },
|
|
745
|
+
reads: ["control.db", "managed initialization documents"],
|
|
746
|
+
writes: ["targetPath", "targetPath.manifest.json"],
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
name: "restore",
|
|
750
|
+
purpose: "从已验证备份恢复初始化控制数据库。",
|
|
751
|
+
request: { action: "restore", targetPath: ".planning/backups/initialization.db" },
|
|
752
|
+
response: { restored: true, database: "control.db" },
|
|
753
|
+
reads: ["targetPath", "targetPath.manifest.json"],
|
|
754
|
+
writes: ["control.db"],
|
|
755
|
+
},
|
|
756
|
+
],
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
id: "tool-devflow-skill-script",
|
|
760
|
+
name: "devflow_skill_script",
|
|
761
|
+
kind: "tool",
|
|
762
|
+
path: "xd://devflow_skill_script",
|
|
763
|
+
purpose: "执行 allowlisted 的确定性 skill 脚本,初始化主要使用数据库校验。",
|
|
764
|
+
approval: "exec",
|
|
765
|
+
actions: [
|
|
766
|
+
{
|
|
767
|
+
name: "project-context-db-validate",
|
|
768
|
+
purpose: "校验初始化控制数据库、run、workspace、project 和快照关系。",
|
|
769
|
+
request: { script: "project-context-db-validate", args: ["--database", "<control.db>", "--project-id", "<project-id>", "--workspace-id", "<workspace-id>", "--run-id", "<run-id>"] },
|
|
770
|
+
response: { ok: true, diagnostics: [] },
|
|
771
|
+
reads: ["control.db"],
|
|
772
|
+
writes: [],
|
|
773
|
+
},
|
|
774
|
+
],
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
id: "skill-workflow-project-context",
|
|
778
|
+
name: "skill://workflow-project-context",
|
|
779
|
+
kind: "skill",
|
|
780
|
+
path: "plugins/devflow-core/skills/workflow-project-context/SKILL.md",
|
|
781
|
+
purpose: "项目上下文接口包装 skill:供委托子 agent 选择 action、调用初始化 contract、整理证据包并返回给父 agent。",
|
|
782
|
+
approval: "none",
|
|
783
|
+
actions: [
|
|
784
|
+
{
|
|
785
|
+
name: "delegated-project-context-task",
|
|
786
|
+
purpose: "由子 agent 根据用户意图读取 interface-index/knowledge-index,调用 devflow_project_context 或 devflow_skill_script,并返回 result pack。",
|
|
787
|
+
request: {
|
|
788
|
+
intent: "initialize-project|refresh-after-code-change|refresh-after-bug-fix|query-facts|query-knowledge|query-api|query-bug-knowledge|compare-snapshots|regenerate-docs|diagnose-initialization",
|
|
789
|
+
snapshotId: "optional fixed snapshot",
|
|
790
|
+
repositories: "optional confirmed project repositories",
|
|
791
|
+
question: "parent-agent request",
|
|
792
|
+
},
|
|
793
|
+
response: {
|
|
794
|
+
conclusion: "concise answer or initialization status",
|
|
795
|
+
actionsUsed: [],
|
|
796
|
+
snapshot: { runId: "uuid|null", snapshotId: "uuid|null", codeIndexGeneration: "generation|null", sourceSnapshot: "uuid|null" },
|
|
797
|
+
evidence: [],
|
|
798
|
+
changesOrImpact: [],
|
|
799
|
+
gaps: [],
|
|
800
|
+
recommendedNextAction: "query|diff|generate-docs|scan|human-review|none",
|
|
801
|
+
},
|
|
802
|
+
reads: [".planning/interface-index.json", ".planning/knowledge-index.json", ".planning/code-index/", ".context/knowledge/", "control interfaces"],
|
|
803
|
+
writes: ["initialization projections through tools only"],
|
|
804
|
+
},
|
|
805
|
+
],
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
id: "skill-workflow-code-index",
|
|
809
|
+
name: "skill://workflow-code-index",
|
|
810
|
+
kind: "skill",
|
|
811
|
+
path: "plugins/devflow-intel/skills/workflow-code-index/SKILL.md",
|
|
812
|
+
purpose: "代码事实索引生命周期:scan、validate、audit、query。",
|
|
813
|
+
approval: "exec",
|
|
814
|
+
actions: [
|
|
815
|
+
{
|
|
816
|
+
name: "scan/validate/audit/query",
|
|
817
|
+
purpose: "生成并查询 .planning/code-index/ 机器代码事实。",
|
|
818
|
+
request: { action: "scan|validate|audit|query", index: ".planning/code-index", root: "." },
|
|
819
|
+
response: { generation: "uuid", files: "jsonl", symbols: "jsonl", relations: "jsonl", diagnostics: "jsonl" },
|
|
820
|
+
reads: ["source tree"],
|
|
821
|
+
writes: [".planning/code-index/"],
|
|
822
|
+
},
|
|
823
|
+
],
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
id: "cli-devflow-pi-init",
|
|
827
|
+
name: "devflow-pi init --scan",
|
|
828
|
+
kind: "cli",
|
|
829
|
+
path: "src/cli.ts",
|
|
830
|
+
purpose: "命令行初始化入口,创建项目身份并可触发 scan。",
|
|
831
|
+
approval: "exec",
|
|
832
|
+
actions: [
|
|
833
|
+
{
|
|
834
|
+
name: "init",
|
|
835
|
+
purpose: "创建 .devflow/project.json,输出 projectId/workspaceId/database/planning,可选执行 scan。",
|
|
836
|
+
request: { argv: ["devflow-pi", "init", "<project>", "--scan", "--request-id", "<id>"] },
|
|
837
|
+
response: { projectId: "uuid", workspaceId: "uuid", database: "control.db", initializationRun: "uuid", contextSnapshot: "uuid" },
|
|
838
|
+
reads: ["project path"],
|
|
839
|
+
writes: [".devflow/project.json", "control.db", "initialization projections when --scan"],
|
|
840
|
+
},
|
|
841
|
+
],
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
id: "artifact-knowledge-index",
|
|
845
|
+
name: ".planning/knowledge-index.json",
|
|
846
|
+
kind: "artifact",
|
|
847
|
+
path: ".planning/knowledge-index.json",
|
|
848
|
+
purpose: "知识页、来源文档、需求、代码事实和预留 BUG 节点的桥接索引。",
|
|
849
|
+
approval: "read",
|
|
850
|
+
actions: [
|
|
851
|
+
{
|
|
852
|
+
name: "read",
|
|
853
|
+
purpose: "让 LLM 或工具快速发现知识层节点和绑定关系。",
|
|
854
|
+
request: { path: ".planning/knowledge-index.json" },
|
|
855
|
+
response: { schemaVersion: 1, bundleRoot: ".context/knowledge", pages: [], reservedBugKnowledgeNodes: [] },
|
|
856
|
+
reads: [".planning/knowledge-index.json"],
|
|
857
|
+
writes: [],
|
|
858
|
+
},
|
|
859
|
+
],
|
|
860
|
+
},
|
|
861
|
+
];
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function renderInterfaceContract(context: ProjectContext, contract: InterfaceContract, snapshotId: string, generation?: string): string {
|
|
865
|
+
return `${frontmatter({ type: "interface-contract", id: contract.id, title: contract.name, interface_kind: contract.kind, project: context.manifest.name, status: "draft", source_snapshot: snapshotId, code_index_generation: generation, managed: true })}# ${contract.name}
|
|
866
|
+
|
|
867
|
+
## Purpose
|
|
868
|
+
|
|
869
|
+
${contract.purpose}
|
|
870
|
+
|
|
871
|
+
## Endpoint
|
|
872
|
+
|
|
873
|
+
- 类型:${contract.kind}
|
|
874
|
+
- 入口:\`${contract.path}\`
|
|
875
|
+
- 权限:${contract.approval}
|
|
876
|
+
|
|
877
|
+
## Actions
|
|
878
|
+
|
|
879
|
+
${contract.actions.map(action => `### ${action.name}
|
|
880
|
+
|
|
881
|
+
${action.purpose}
|
|
882
|
+
|
|
883
|
+
#### Request
|
|
884
|
+
|
|
885
|
+
\`\`\`json
|
|
886
|
+
${JSON.stringify(action.request, null, 2)}
|
|
887
|
+
\`\`\`
|
|
888
|
+
|
|
889
|
+
#### Response
|
|
890
|
+
|
|
891
|
+
\`\`\`json
|
|
892
|
+
${JSON.stringify(action.response, null, 2)}
|
|
893
|
+
\`\`\`
|
|
894
|
+
|
|
895
|
+
#### Reads
|
|
896
|
+
|
|
897
|
+
${action.reads.map(item => `- ${item}`).join("\n") || "- 无"}
|
|
898
|
+
|
|
899
|
+
#### Writes
|
|
900
|
+
|
|
901
|
+
${action.writes.map(item => `- ${item}`).join("\n") || "- 无"}
|
|
902
|
+
`).join("\n")}
|
|
903
|
+
`;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function renderInterfaceOutputs(context: ProjectContext, snapshot: { id?: unknown; indexRef?: unknown }): { outputs: Array<{ targetPath: string; body: string }>; contracts: InterfaceContract[] } {
|
|
907
|
+
const snapshotId = String(snapshot.id ?? "");
|
|
908
|
+
const generation = codeIndexGeneration(snapshot);
|
|
909
|
+
const contracts = initializationInterfaceContracts();
|
|
910
|
+
const outputs: Array<{ targetPath: string; body: string }> = [];
|
|
911
|
+
outputs.push({ targetPath: ".context/knowledge/interfaces/index.md", body: `${frontmatter({ type: "interface-index", title: "初始化接口目录", project: context.manifest.name, status: "draft", source_snapshot: snapshotId, code_index_generation: generation, managed: true })}# 初始化接口目录\n\n${contracts.map(contract => `- [${contract.name}](./${slug(contract.id)}.md) — ${contract.purpose}`).join("\n")}\n` });
|
|
912
|
+
for (const contract of contracts) outputs.push({ targetPath: `.context/knowledge/interfaces/${slug(contract.id)}.md`, body: renderInterfaceContract(context, contract, snapshotId, generation) });
|
|
913
|
+
return { outputs, contracts };
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function renderOpenWikiKnowledge(context: ProjectContext, facts: Fact[], snapshot: { id?: unknown; indexRef?: unknown }, knowledge: DocumentKnowledge): { outputs: Array<{ targetPath: string; body: string }>; pages: KnowledgePage[] } {
|
|
917
|
+
const snapshotId = String(snapshot.id ?? "");
|
|
918
|
+
const generation = codeIndexGeneration(snapshot);
|
|
919
|
+
const pages = buildKnowledgePages(knowledge, facts);
|
|
920
|
+
const sourceByRef = new Map(knowledge.sources.map(source => [sourcePageId(source), source] as const));
|
|
921
|
+
const outputs: Array<{ targetPath: string; body: string }> = [];
|
|
922
|
+
outputs.push({ targetPath: ".context/knowledge/index.md", body: `${frontmatter({ type: "knowledge-bundle-index", title: `${context.manifest.name} Knowledge`, project: context.manifest.name, status: "draft", source_snapshot: snapshotId, code_index_generation: generation, managed: true })}# ${context.manifest.name} Knowledge\n\n## Sources\n\n${pages.filter(page => page.type === "source-summary").map(page => `- [${page.title}](./sources/${path.basename(page.path)})`).join("\n") || "- 暂无"}\n\n## Entities\n\n${pages.filter(page => page.type === "entity").map(page => `- [${page.title}](./entities/${path.basename(page.path)})`).join("\n") || "- 暂无"}\n\n## Concepts\n\n${pages.filter(page => page.type === "concept").map(page => `- [${page.title}](./concepts/${path.basename(page.path)})`).join("\n") || "- 暂无"}\n\n## Requirements\n\n${pages.filter(page => page.type === "requirement").map(page => `- [${page.title}](./requirements/${path.basename(page.path)})`).join("\n") || "- 暂无"}\n\n## Synthesis\n\n${pages.filter(page => page.type === "synthesis").map(page => `- [${page.title}](./synthesis/${path.basename(page.path)})`).join("\n") || "- 暂无"}\n\n## Bugs / Fixes / Incidents / Lessons\n\n- [BUG 模板](./bugs/_template.md)\n- [FIX 模板](./fixes/_template.md)\n- [INCIDENT 模板](./incidents/_template.md)\n- [LESSON 模板](./lessons/_template.md)\n` });
|
|
923
|
+
outputs.push({ targetPath: ".context/knowledge/log.md", body: `${frontmatter({ type: "knowledge-log", title: "Knowledge Operation Log", project: context.manifest.name, status: "draft", source_snapshot: snapshotId, code_index_generation: generation, managed: true })}# Knowledge Operation Log\n\n- 初始化生成 OpenWiki 风格知识层。\n- 原始文档保持在项目原位置,source-summary 记录摘要和引用。\n- 代码事实来自 \`.planning/code-index/\`,桥接关系写入 \`.planning/knowledge-index.json\`。\n` });
|
|
924
|
+
outputs.push({ targetPath: ".context/knowledge/raw/README.md", body: `${frontmatter({ type: "raw-source-readme", title: "Raw Source Policy", project: context.manifest.name, status: "draft", managed: true })}# Raw Source Policy\n\n本目录保留给后续需要复制或归档的原始资料。当前初始化默认不复制原文,只在 source-summary 中记录原路径和内容哈希,避免制造第二份权威原文。\n` });
|
|
925
|
+
outputs.push(...bugKnowledgeTemplates(context, snapshotId, generation));
|
|
926
|
+
for (const page of pages) outputs.push({ targetPath: page.path, body: renderKnowledgePage(context, page, sourceByRef, snapshotId, generation) });
|
|
927
|
+
return { outputs, pages };
|
|
928
|
+
}
|
|
929
|
+
|
|
271
930
|
function renderKnowledgeDocuments(context: ProjectContext, facts: Fact[], intro: string, snapshot: { id?: unknown; indexRef?: unknown }, knowledge: DocumentKnowledge): Array<{ targetPath: string; body: string }> {
|
|
272
931
|
const snapshotId = String(snapshot.id ?? "");
|
|
273
932
|
const generation = codeIndexGeneration(snapshot);
|
|
@@ -284,7 +943,7 @@ function renderKnowledgeDocuments(context: ProjectContext, facts: Fact[], intro:
|
|
|
284
943
|
return outputs;
|
|
285
944
|
}
|
|
286
945
|
|
|
287
|
-
function renderDocumentIndex(context: ProjectContext, snapshot: { id?: unknown; indexRef?: unknown }, outputs: Array<{ targetPath: string }>, knowledge: DocumentKnowledge): { targetPath: string; body: string } {
|
|
946
|
+
function renderDocumentIndex(context: ProjectContext, snapshot: { id?: unknown; indexRef?: unknown }, outputs: Array<{ targetPath: string }>, knowledge: DocumentKnowledge, pages: KnowledgePage[]): { targetPath: string; body: string } {
|
|
288
947
|
const snapshotId = String(snapshot.id ?? "");
|
|
289
948
|
const generation = codeIndexGeneration(snapshot);
|
|
290
949
|
const documents = outputs
|
|
@@ -292,8 +951,10 @@ function renderDocumentIndex(context: ProjectContext, snapshot: { id?: unknown;
|
|
|
292
951
|
.map(output => {
|
|
293
952
|
const parts = output.targetPath.split("/");
|
|
294
953
|
const file = parts.at(-1) ?? "";
|
|
295
|
-
const scope = parts[1] === "project" ? "project" : parts[1] === "requirements" ? "requirement" : "project";
|
|
954
|
+
const scope: IndexedDocumentScope = parts[1] === "project" ? "project" : parts[1] === "requirements" ? "requirement" : parts[1] === "knowledge" ? "knowledge" : "project";
|
|
296
955
|
const type = output.targetPath === ".context/index.md" ? "knowledge-index"
|
|
956
|
+
: output.targetPath === ".context/knowledge/index.md" ? "knowledge-bundle-index"
|
|
957
|
+
: scope === "knowledge" ? `knowledge-${parts[2] === "log.md" ? "log" : parts[2] ?? "page"}`
|
|
297
958
|
: scope === "project" ? `project-${file.replace(/\.md$/, "")}`
|
|
298
959
|
: `requirement-${file.replace(/^requirement\.md$/, "requirements").replace(/\.md$/, "")}`;
|
|
299
960
|
return {
|
|
@@ -317,11 +978,79 @@ function renderDocumentIndex(context: ProjectContext, snapshot: { id?: unknown;
|
|
|
317
978
|
codeIndexGeneration: generation ?? null,
|
|
318
979
|
sourceDocuments: knowledge.sources,
|
|
319
980
|
classifications: knowledge.classifications,
|
|
981
|
+
knowledgePages: pages.map(page => ({ id: page.id, path: page.path, type: page.type, status: page.status, sourceRefs: page.sourceRefs, relatedRequirements: page.relatedRequirements, relatedCodeFacts: page.relatedCodeFacts, confidence: page.confidence })),
|
|
320
982
|
documents,
|
|
321
983
|
}, null, 2)}\n`,
|
|
322
984
|
};
|
|
323
985
|
}
|
|
324
986
|
|
|
987
|
+
function renderKnowledgeIndex(context: ProjectContext, snapshot: { id?: unknown; indexRef?: unknown }, knowledge: DocumentKnowledge, pages: KnowledgePage[], interfaces: InterfaceContract[]): { targetPath: string; body: string } {
|
|
988
|
+
const snapshotId = String(snapshot.id ?? "");
|
|
989
|
+
const generation = codeIndexGeneration(snapshot);
|
|
990
|
+
return {
|
|
991
|
+
targetPath: ".planning/knowledge-index.json",
|
|
992
|
+
body: `${JSON.stringify({
|
|
993
|
+
schemaVersion: 1,
|
|
994
|
+
project: context.manifest.name,
|
|
995
|
+
projectId: context.manifest.projectId,
|
|
996
|
+
sourceSnapshot: snapshotId,
|
|
997
|
+
codeIndexGeneration: generation ?? null,
|
|
998
|
+
bundleRoot: ".context/knowledge",
|
|
999
|
+
sourceDocuments: knowledge.sources,
|
|
1000
|
+
classifications: knowledge.classifications,
|
|
1001
|
+
pages: pages.map(page => ({
|
|
1002
|
+
id: page.id,
|
|
1003
|
+
path: page.path,
|
|
1004
|
+
type: page.type,
|
|
1005
|
+
title: page.title,
|
|
1006
|
+
status: page.status,
|
|
1007
|
+
sourceRefs: page.sourceRefs,
|
|
1008
|
+
relatedRequirements: page.relatedRequirements,
|
|
1009
|
+
relatedCodeFacts: page.relatedCodeFacts,
|
|
1010
|
+
confidence: page.confidence,
|
|
1011
|
+
})),
|
|
1012
|
+
reservedBugKnowledgeNodes: [
|
|
1013
|
+
{ id: "BUG-TEMPLATE", path: ".context/knowledge/bugs/_template.md", type: "bug-report", purpose: "问题现象、影响、复现、根因和回归绑定模板" },
|
|
1014
|
+
{ id: "FIX-TEMPLATE", path: ".context/knowledge/fixes/_template.md", type: "bug-fix", purpose: "修复动作、变更原因、变更文件和回归覆盖模板" },
|
|
1015
|
+
{ id: "INC-TEMPLATE", path: ".context/knowledge/incidents/_template.md", type: "incident", purpose: "事故时间线、影响、缓解和后续行动模板" },
|
|
1016
|
+
{ id: "LESSON-TEMPLATE", path: ".context/knowledge/lessons/_template.md", type: "lesson", purpose: "可复用排障经验和检查规则模板" },
|
|
1017
|
+
],
|
|
1018
|
+
interfaces: interfaces.map(item => ({ id: item.id, name: item.name, kind: item.kind, path: item.path, purpose: item.purpose, approval: item.approval, actions: item.actions.map(action => action.name) })),
|
|
1019
|
+
}, null, 2)}\n`,
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function renderInterfaceIndex(context: ProjectContext, snapshot: { id?: unknown; indexRef?: unknown }, interfaces: InterfaceContract[]): { targetPath: string; body: string } {
|
|
1024
|
+
const snapshotId = String(snapshot.id ?? "");
|
|
1025
|
+
const generation = codeIndexGeneration(snapshot);
|
|
1026
|
+
return {
|
|
1027
|
+
targetPath: ".planning/interface-index.json",
|
|
1028
|
+
body: `${JSON.stringify({
|
|
1029
|
+
schemaVersion: 1,
|
|
1030
|
+
project: context.manifest.name,
|
|
1031
|
+
projectId: context.manifest.projectId,
|
|
1032
|
+
sourceSnapshot: snapshotId,
|
|
1033
|
+
codeIndexGeneration: generation ?? null,
|
|
1034
|
+
interfaces: interfaces.map(contract => ({
|
|
1035
|
+
id: contract.id,
|
|
1036
|
+
name: contract.name,
|
|
1037
|
+
kind: contract.kind,
|
|
1038
|
+
path: contract.path,
|
|
1039
|
+
purpose: contract.purpose,
|
|
1040
|
+
approval: contract.approval,
|
|
1041
|
+
actions: contract.actions.map(action => ({
|
|
1042
|
+
name: action.name,
|
|
1043
|
+
purpose: action.purpose,
|
|
1044
|
+
request: action.request,
|
|
1045
|
+
response: action.response,
|
|
1046
|
+
reads: action.reads,
|
|
1047
|
+
writes: action.writes,
|
|
1048
|
+
})),
|
|
1049
|
+
})),
|
|
1050
|
+
}, null, 2)}\n`,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
|
|
325
1054
|
export async function generateProjectDocuments(context: ProjectContext, snapshotId: string, options: { mode: DocumentGenerationMode; organizer?: LowTierDocumentOrganizer }): Promise<DocumentGenerationResult[]> {
|
|
326
1055
|
const store = await InitializationStore.open(context);
|
|
327
1056
|
try {
|
|
@@ -340,8 +1069,12 @@ export async function generateProjectDocuments(context: ProjectContext, snapshot
|
|
|
340
1069
|
catch (error) { store.failDocumentJob(batchJob, error instanceof Error ? error.message : String(error)); throw error; }
|
|
341
1070
|
const snapshotMeta = (store.queryContext({ snapshotId, limit: 1 }) as { snapshot: { id?: unknown; indexRef?: unknown } }).snapshot;
|
|
342
1071
|
const knowledge = await discoverSourceDocuments(context.root);
|
|
343
|
-
const
|
|
344
|
-
|
|
1072
|
+
const openWiki = renderOpenWikiKnowledge(context, facts, snapshotMeta, knowledge);
|
|
1073
|
+
const interfaces = renderInterfaceOutputs(context, snapshotMeta);
|
|
1074
|
+
const outputs = [...renderDocuments(context, facts, organized.text, collections), ...renderKnowledgeDocuments(context, facts, organized.text, snapshotMeta, knowledge), ...openWiki.outputs, ...interfaces.outputs];
|
|
1075
|
+
outputs.push(renderDocumentIndex(context, snapshotMeta, outputs, knowledge, openWiki.pages));
|
|
1076
|
+
outputs.push(renderKnowledgeIndex(context, snapshotMeta, knowledge, openWiki.pages, interfaces.contracts));
|
|
1077
|
+
outputs.push(renderInterfaceIndex(context, snapshotMeta, interfaces.contracts));
|
|
345
1078
|
const results: DocumentGenerationResult[] = [];
|
|
346
1079
|
for (const output of outputs) {
|
|
347
1080
|
const job = store.createDocumentJob(snapshotId, { targetPath: output.targetPath, ownership: "generated", templateVersion: TEMPLATE_VERSION, modelRole: organized.modelRole, input: facts.map(fact => fact.objectKey) });
|