@guanlin4924/devflow-pi 0.1.68 → 0.1.70
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/plugins/devflow-core/skills/workflow-project-context/SKILL.md +89 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/adapters/python_ast.py +2 -0
- package/plugins/devflow-intel/skills/workflow-code-index/scripts/code_index.py +8 -3
- package/src/constants.ts +1 -1
- package/src/project-documents.ts +690 -5
- package/src/project-initializer.ts +31 -3
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.
|
|
@@ -122,6 +209,8 @@ Run the database validator with identifiers returned by the scan:
|
|
|
122
209
|
devflow_skill_script(script="project-context-db-validate", args=["--database", "<control.db>", "--project-id", "<project-id>", "--workspace-id", "<workspace-id>", "--run-id", "<run-id>"])
|
|
123
210
|
```
|
|
124
211
|
|
|
212
|
+
The `--database` value must come from `devflow_project_context(action="scan").database` or the loaded project context `controlDatabasePath`. Never guess `./control.db`, `.devflow/control.db`, `.planning/code-index/index.sqlite`, an OMP agent database, or any SQLite found by searching user directories. Code-index SQLite is not the initialization control database and does not contain the initialization `schema_meta` table.
|
|
213
|
+
|
|
125
214
|
Completion requires a stable immutable input, matching context snapshot, accounted required categories, evidence for observed facts, a registered immutable index in full mode, revision-safe pointer publication, and separate document statuses. Workflow `STATE` and other non-initialization authorities must remain unchanged.
|
|
126
215
|
|
|
127
216
|
Report partial, unsupported, skipped, failed, and unknown coverage honestly. Initialization success does not mean tests passed, services ran, designs were approved, or requirements were delivered.
|
|
@@ -116,6 +116,8 @@ class _PythonVisitor(ast.NodeVisitor):
|
|
|
116
116
|
|
|
117
117
|
@staticmethod
|
|
118
118
|
def _signature(node):
|
|
119
|
+
if isinstance(node, ast.ClassDef):
|
|
120
|
+
return f"{node.name}()"
|
|
119
121
|
arguments = [argument.arg for argument in [*node.args.posonlyargs, *node.args.args, *node.args.kwonlyargs]]
|
|
120
122
|
if node.args.vararg:
|
|
121
123
|
arguments.append(f"*{node.args.vararg.arg}")
|
|
@@ -97,13 +97,15 @@ def _publish_snapshot(staging: Path, output: Path, counts: Dict[str, int], works
|
|
|
97
97
|
os.close(directory_fd)
|
|
98
98
|
|
|
99
99
|
|
|
100
|
-
def _workspace_state(root: Path, files: List[dict], changed: Set[str]) -> Dict[str, object]:
|
|
100
|
+
def _workspace_state(root: Path, files: List[dict], changed: Set[str], discovered_paths: Iterable[str] | None = None) -> Dict[str, object]:
|
|
101
101
|
indexed_paths = sorted(record["path"] for record in files)
|
|
102
|
+
discovered = sorted(set(discovered_paths or indexed_paths))
|
|
102
103
|
source_manifest_hash = _hash_lines(f"{record['path']}\0{record['content_hash']}" for record in files)
|
|
103
104
|
return {
|
|
104
105
|
"repositoryRevision": _git_output(root, ["rev-parse", "HEAD"]),
|
|
105
106
|
"contentManifestHash": source_manifest_hash,
|
|
106
107
|
"indexedPathsHash": _hash_lines(indexed_paths),
|
|
108
|
+
"discoveredPathsHash": _hash_lines(discovered),
|
|
107
109
|
"dirtyPathsAtBuild": sorted(changed),
|
|
108
110
|
"extractors": sorted(set(record.get("parser", "unknown") for record in files)),
|
|
109
111
|
"capabilities": {
|
|
@@ -210,12 +212,14 @@ def command_scan(args) -> int:
|
|
|
210
212
|
|
|
211
213
|
files, symbols, relations, diagnostics = _existing_after_removal(existing, changed)
|
|
212
214
|
indexed_paths = {record["path"] for record in files}
|
|
215
|
+
discovered_paths: Set[str] = set()
|
|
213
216
|
|
|
214
217
|
for codebase in codebases:
|
|
215
218
|
if not codebase.root.exists():
|
|
216
219
|
diagnostics.append(_diagnostic(codebase.name, "", 1, "warning", "skipped_file", f"codebase root does not exist: {codebase.root}", "discovery@1"))
|
|
217
220
|
continue
|
|
218
221
|
all_files = scan_files(codebase.root, excludes=args.exclude)
|
|
222
|
+
discovered_paths.update(_workspace_rel(root, path) for path in all_files)
|
|
219
223
|
if changed:
|
|
220
224
|
all_files = [path for path in all_files if _workspace_rel(root, path) in changed]
|
|
221
225
|
manifests = [path for path in all_files if path.name in {"package.json", "go.mod", "Cargo.toml", "pyproject.toml", "requirements.txt", "composer.json", "pubspec.yaml"}]
|
|
@@ -280,7 +284,7 @@ def command_scan(args) -> int:
|
|
|
280
284
|
"relations": len(relations),
|
|
281
285
|
"diagnostics": len(diagnostics),
|
|
282
286
|
},
|
|
283
|
-
_workspace_state(root, files, changed),
|
|
287
|
+
_workspace_state(root, files, changed, discovered_paths),
|
|
284
288
|
)
|
|
285
289
|
finally:
|
|
286
290
|
shutil.rmtree(staging, ignore_errors=True)
|
|
@@ -406,7 +410,8 @@ def _query_is_fresh(root: Path, snapshot: Dict[str, object], files: List[dict])
|
|
|
406
410
|
indexed_paths = sorted(record["path"] for record in files)
|
|
407
411
|
if not any(path.startswith("../") for path in indexed_paths):
|
|
408
412
|
current_paths = sorted(_workspace_rel(root, item) for item in scan_files(root))
|
|
409
|
-
|
|
413
|
+
expected_paths_hash = workspace.get("discoveredPathsHash") or workspace.get("indexedPathsHash")
|
|
414
|
+
if _hash_lines(current_paths) != expected_paths_hash:
|
|
410
415
|
return False
|
|
411
416
|
return True
|
|
412
417
|
|
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,6 +182,54 @@ 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(";")]));
|
|
@@ -317,11 +394,545 @@ function inboxRequirementBody(context: ProjectContext, source: SourceDocument, c
|
|
|
317
394
|
];
|
|
318
395
|
}
|
|
319
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", database: "<control.db>", projectId: "uuid", workspaceId: "uuid", planningRoot: ".planning", 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
|
+
|
|
320
930
|
function renderKnowledgeDocuments(context: ProjectContext, facts: Fact[], intro: string, snapshot: { id?: unknown; indexRef?: unknown }, knowledge: DocumentKnowledge): Array<{ targetPath: string; body: string }> {
|
|
321
931
|
const snapshotId = String(snapshot.id ?? "");
|
|
322
932
|
const generation = codeIndexGeneration(snapshot);
|
|
323
933
|
const outputs: Array<{ targetPath: string; body: string }> = [];
|
|
324
934
|
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` });
|
|
935
|
+
outputs.push({ targetPath: ".context/spec/index.md", body: `${frontmatter({ type: "compat-spec-index", scope: "project", project: context.manifest.name, status: "compat", source_snapshot: snapshotId, code_index_generation: generation, managed: true })}# 兼容 Spec 入口\n\n本文件是项目初始化的兼容入口,用于旧流程查找 \`.context/spec/index.md\`。新的权威入口如下:\n\n- 项目知识库:[../index.md](../index.md)\n- 项目级文档:[../project/index.md](../project/index.md)\n- OpenWiki 风格知识层:[../knowledge/index.md](../knowledge/index.md)\n- 初始化接口目录:[../knowledge/interfaces/index.md](../knowledge/interfaces/index.md)\n- 文档索引:\`.planning/document-index.json\`\n- 知识索引:\`.planning/knowledge-index.json\`\n- 接口索引:\`.planning/interface-index.json\`\n- 代码索引:\`.planning/code-index/\`\n\n此兼容入口不表示需求、设计或 API 已批准;人工确认前所有生成内容保持 draft/compat 状态。\n` });
|
|
325
936
|
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` });
|
|
326
937
|
for (const type of KNOWLEDGE_DOCUMENT_TYPES) outputs.push({ targetPath: `.context/project/${type}.md`, body: projectKnowledgeBody(context, type, facts, intro, snapshotId, knowledge, generation) });
|
|
327
938
|
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` });
|
|
@@ -333,7 +944,7 @@ function renderKnowledgeDocuments(context: ProjectContext, facts: Fact[], intro:
|
|
|
333
944
|
return outputs;
|
|
334
945
|
}
|
|
335
946
|
|
|
336
|
-
function renderDocumentIndex(context: ProjectContext, snapshot: { id?: unknown; indexRef?: unknown }, outputs: Array<{ targetPath: string }>, knowledge: DocumentKnowledge): { targetPath: string; body: string } {
|
|
947
|
+
function renderDocumentIndex(context: ProjectContext, snapshot: { id?: unknown; indexRef?: unknown }, outputs: Array<{ targetPath: string }>, knowledge: DocumentKnowledge, pages: KnowledgePage[]): { targetPath: string; body: string } {
|
|
337
948
|
const snapshotId = String(snapshot.id ?? "");
|
|
338
949
|
const generation = codeIndexGeneration(snapshot);
|
|
339
950
|
const documents = outputs
|
|
@@ -341,8 +952,10 @@ function renderDocumentIndex(context: ProjectContext, snapshot: { id?: unknown;
|
|
|
341
952
|
.map(output => {
|
|
342
953
|
const parts = output.targetPath.split("/");
|
|
343
954
|
const file = parts.at(-1) ?? "";
|
|
344
|
-
const scope = parts[1] === "project" ? "project" : parts[1] === "requirements" ? "requirement" : "project";
|
|
955
|
+
const scope: IndexedDocumentScope = parts[1] === "project" ? "project" : parts[1] === "requirements" ? "requirement" : parts[1] === "knowledge" ? "knowledge" : "project";
|
|
345
956
|
const type = output.targetPath === ".context/index.md" ? "knowledge-index"
|
|
957
|
+
: output.targetPath === ".context/knowledge/index.md" ? "knowledge-bundle-index"
|
|
958
|
+
: scope === "knowledge" ? `knowledge-${parts[2] === "log.md" ? "log" : parts[2] ?? "page"}`
|
|
346
959
|
: scope === "project" ? `project-${file.replace(/\.md$/, "")}`
|
|
347
960
|
: `requirement-${file.replace(/^requirement\.md$/, "requirements").replace(/\.md$/, "")}`;
|
|
348
961
|
return {
|
|
@@ -366,11 +979,79 @@ function renderDocumentIndex(context: ProjectContext, snapshot: { id?: unknown;
|
|
|
366
979
|
codeIndexGeneration: generation ?? null,
|
|
367
980
|
sourceDocuments: knowledge.sources,
|
|
368
981
|
classifications: knowledge.classifications,
|
|
982
|
+
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 })),
|
|
369
983
|
documents,
|
|
370
984
|
}, null, 2)}\n`,
|
|
371
985
|
};
|
|
372
986
|
}
|
|
373
987
|
|
|
988
|
+
function renderKnowledgeIndex(context: ProjectContext, snapshot: { id?: unknown; indexRef?: unknown }, knowledge: DocumentKnowledge, pages: KnowledgePage[], interfaces: InterfaceContract[]): { targetPath: string; body: string } {
|
|
989
|
+
const snapshotId = String(snapshot.id ?? "");
|
|
990
|
+
const generation = codeIndexGeneration(snapshot);
|
|
991
|
+
return {
|
|
992
|
+
targetPath: ".planning/knowledge-index.json",
|
|
993
|
+
body: `${JSON.stringify({
|
|
994
|
+
schemaVersion: 1,
|
|
995
|
+
project: context.manifest.name,
|
|
996
|
+
projectId: context.manifest.projectId,
|
|
997
|
+
sourceSnapshot: snapshotId,
|
|
998
|
+
codeIndexGeneration: generation ?? null,
|
|
999
|
+
bundleRoot: ".context/knowledge",
|
|
1000
|
+
sourceDocuments: knowledge.sources,
|
|
1001
|
+
classifications: knowledge.classifications,
|
|
1002
|
+
pages: pages.map(page => ({
|
|
1003
|
+
id: page.id,
|
|
1004
|
+
path: page.path,
|
|
1005
|
+
type: page.type,
|
|
1006
|
+
title: page.title,
|
|
1007
|
+
status: page.status,
|
|
1008
|
+
sourceRefs: page.sourceRefs,
|
|
1009
|
+
relatedRequirements: page.relatedRequirements,
|
|
1010
|
+
relatedCodeFacts: page.relatedCodeFacts,
|
|
1011
|
+
confidence: page.confidence,
|
|
1012
|
+
})),
|
|
1013
|
+
reservedBugKnowledgeNodes: [
|
|
1014
|
+
{ id: "BUG-TEMPLATE", path: ".context/knowledge/bugs/_template.md", type: "bug-report", purpose: "问题现象、影响、复现、根因和回归绑定模板" },
|
|
1015
|
+
{ id: "FIX-TEMPLATE", path: ".context/knowledge/fixes/_template.md", type: "bug-fix", purpose: "修复动作、变更原因、变更文件和回归覆盖模板" },
|
|
1016
|
+
{ id: "INC-TEMPLATE", path: ".context/knowledge/incidents/_template.md", type: "incident", purpose: "事故时间线、影响、缓解和后续行动模板" },
|
|
1017
|
+
{ id: "LESSON-TEMPLATE", path: ".context/knowledge/lessons/_template.md", type: "lesson", purpose: "可复用排障经验和检查规则模板" },
|
|
1018
|
+
],
|
|
1019
|
+
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) })),
|
|
1020
|
+
}, null, 2)}\n`,
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function renderInterfaceIndex(context: ProjectContext, snapshot: { id?: unknown; indexRef?: unknown }, interfaces: InterfaceContract[]): { targetPath: string; body: string } {
|
|
1025
|
+
const snapshotId = String(snapshot.id ?? "");
|
|
1026
|
+
const generation = codeIndexGeneration(snapshot);
|
|
1027
|
+
return {
|
|
1028
|
+
targetPath: ".planning/interface-index.json",
|
|
1029
|
+
body: `${JSON.stringify({
|
|
1030
|
+
schemaVersion: 1,
|
|
1031
|
+
project: context.manifest.name,
|
|
1032
|
+
projectId: context.manifest.projectId,
|
|
1033
|
+
sourceSnapshot: snapshotId,
|
|
1034
|
+
codeIndexGeneration: generation ?? null,
|
|
1035
|
+
interfaces: interfaces.map(contract => ({
|
|
1036
|
+
id: contract.id,
|
|
1037
|
+
name: contract.name,
|
|
1038
|
+
kind: contract.kind,
|
|
1039
|
+
path: contract.path,
|
|
1040
|
+
purpose: contract.purpose,
|
|
1041
|
+
approval: contract.approval,
|
|
1042
|
+
actions: contract.actions.map(action => ({
|
|
1043
|
+
name: action.name,
|
|
1044
|
+
purpose: action.purpose,
|
|
1045
|
+
request: action.request,
|
|
1046
|
+
response: action.response,
|
|
1047
|
+
reads: action.reads,
|
|
1048
|
+
writes: action.writes,
|
|
1049
|
+
})),
|
|
1050
|
+
})),
|
|
1051
|
+
}, null, 2)}\n`,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
|
|
374
1055
|
export async function generateProjectDocuments(context: ProjectContext, snapshotId: string, options: { mode: DocumentGenerationMode; organizer?: LowTierDocumentOrganizer }): Promise<DocumentGenerationResult[]> {
|
|
375
1056
|
const store = await InitializationStore.open(context);
|
|
376
1057
|
try {
|
|
@@ -389,8 +1070,12 @@ export async function generateProjectDocuments(context: ProjectContext, snapshot
|
|
|
389
1070
|
catch (error) { store.failDocumentJob(batchJob, error instanceof Error ? error.message : String(error)); throw error; }
|
|
390
1071
|
const snapshotMeta = (store.queryContext({ snapshotId, limit: 1 }) as { snapshot: { id?: unknown; indexRef?: unknown } }).snapshot;
|
|
391
1072
|
const knowledge = await discoverSourceDocuments(context.root);
|
|
392
|
-
const
|
|
393
|
-
|
|
1073
|
+
const openWiki = renderOpenWikiKnowledge(context, facts, snapshotMeta, knowledge);
|
|
1074
|
+
const interfaces = renderInterfaceOutputs(context, snapshotMeta);
|
|
1075
|
+
const outputs = [...renderDocuments(context, facts, organized.text, collections), ...renderKnowledgeDocuments(context, facts, organized.text, snapshotMeta, knowledge), ...openWiki.outputs, ...interfaces.outputs];
|
|
1076
|
+
outputs.push(renderDocumentIndex(context, snapshotMeta, outputs, knowledge, openWiki.pages));
|
|
1077
|
+
outputs.push(renderKnowledgeIndex(context, snapshotMeta, knowledge, openWiki.pages, interfaces.contracts));
|
|
1078
|
+
outputs.push(renderInterfaceIndex(context, snapshotMeta, interfaces.contracts));
|
|
394
1079
|
const results: DocumentGenerationResult[] = [];
|
|
395
1080
|
for (const output of outputs) {
|
|
396
1081
|
const job = store.createDocumentJob(snapshotId, { targetPath: output.targetPath, ownership: "generated", templateVersion: TEMPLATE_VERSION, modelRole: organized.modelRole, input: facts.map(fact => fact.objectKey) });
|
|
@@ -10,7 +10,17 @@ const TEXT_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".rb", ".g
|
|
|
10
10
|
|
|
11
11
|
type SourceFile = { repository: string; path: string; absolute: string; contentHash: string; size: number; text?: string };
|
|
12
12
|
type ScanDiagnostic = { repository: string; path: string; reason: "symlink" | "excluded" | "too_large" | "unreadable"; message?: string };
|
|
13
|
-
export type InitializationProgress = {
|
|
13
|
+
export type InitializationProgress = {
|
|
14
|
+
runId: string;
|
|
15
|
+
snapshotId?: string;
|
|
16
|
+
completeness?: "complete" | "partial";
|
|
17
|
+
diagnostics: string[];
|
|
18
|
+
documents?: Array<{ targetPath: string; status: string; outputPath?: string; error?: string }>;
|
|
19
|
+
database: string;
|
|
20
|
+
projectId: string;
|
|
21
|
+
workspaceId: string;
|
|
22
|
+
planningRoot: string;
|
|
23
|
+
};
|
|
14
24
|
|
|
15
25
|
function digest(value: string | Uint8Array): string { return `sha256:${createHash("sha256").update(value).digest("hex")}`; }
|
|
16
26
|
function extension(file: string): string { return path.extname(file).toLowerCase(); }
|
|
@@ -189,7 +199,15 @@ export async function runProjectInitialization(context: ProjectContext, input: {
|
|
|
189
199
|
const inputSnapshot = store.createInputSnapshot(workspace.id, storedConfig.id, manifest, false);
|
|
190
200
|
const run = store.startRun({ workspaceId: workspace.id, configId: storedConfig.id, inputSnapshotId: inputSnapshot.id, requestId: input.requestId });
|
|
191
201
|
activeRun = run;
|
|
192
|
-
if (run.reused) return {
|
|
202
|
+
if (run.reused) return {
|
|
203
|
+
runId: run.id,
|
|
204
|
+
snapshotId: store.getRun(run.id)?.publishedSnapshotId as string | undefined,
|
|
205
|
+
diagnostics,
|
|
206
|
+
database: context.controlDatabasePath,
|
|
207
|
+
projectId: context.manifest.projectId,
|
|
208
|
+
workspaceId: context.workspaceId,
|
|
209
|
+
planningRoot: context.planningRoot,
|
|
210
|
+
};
|
|
193
211
|
const grouped = collect(files, context, config); const required = new Set(config.requiredKinds ?? FACT_KINDS);
|
|
194
212
|
for (const repository of config.repositories) for (const kind of FACT_KINDS) {
|
|
195
213
|
const repositoryFacts = grouped.get(kind)!.filter(fact => fact.repository === repository.name);
|
|
@@ -241,7 +259,17 @@ export async function runProjectInitialization(context: ProjectContext, input: {
|
|
|
241
259
|
documents = [{ targetPath: ".planning/initialization/report.md", status: "failed", error: message }];
|
|
242
260
|
}
|
|
243
261
|
}
|
|
244
|
-
return {
|
|
262
|
+
return {
|
|
263
|
+
runId: run.id,
|
|
264
|
+
snapshotId: published.snapshotId,
|
|
265
|
+
completeness: result.snapshot.completeness,
|
|
266
|
+
diagnostics,
|
|
267
|
+
documents,
|
|
268
|
+
database: context.controlDatabasePath,
|
|
269
|
+
projectId: context.manifest.projectId,
|
|
270
|
+
workspaceId: context.workspaceId,
|
|
271
|
+
planningRoot: context.planningRoot,
|
|
272
|
+
};
|
|
245
273
|
} catch (error) {
|
|
246
274
|
const message = error instanceof Error ? error.message : String(error);
|
|
247
275
|
diagnostics.push(message);
|