@guanlin4924/devflow-pi 0.1.68 → 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guanlin4924/devflow-pi",
3
- "version": "0.1.68",
3
+ "version": "0.1.69",
4
4
  "type": "module",
5
5
  "description": "Devflow 专项研发 Pi,基于固定版本 OMP Runtime 组装研发状态机、代码智能、设计、实现与质量门禁。",
6
6
  "license": "UNLICENSED",
@@ -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
@@ -1,5 +1,5 @@
1
1
  export const PRODUCT_NAME = "Devflow Pi";
2
- export const PRODUCT_VERSION = "0.1.68";
2
+ export const PRODUCT_VERSION = "0.1.69";
3
3
  export const PROJECT_SCHEMA_VERSION = 1;
4
4
  export const OMP_RUNTIME_VERSION = "17.2.7";
5
5
 
@@ -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,6 +394,539 @@ 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", 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);
@@ -333,7 +943,7 @@ function renderKnowledgeDocuments(context: ProjectContext, facts: Fact[], intro:
333
943
  return outputs;
334
944
  }
335
945
 
336
- 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 } {
337
947
  const snapshotId = String(snapshot.id ?? "");
338
948
  const generation = codeIndexGeneration(snapshot);
339
949
  const documents = outputs
@@ -341,8 +951,10 @@ function renderDocumentIndex(context: ProjectContext, snapshot: { id?: unknown;
341
951
  .map(output => {
342
952
  const parts = output.targetPath.split("/");
343
953
  const file = parts.at(-1) ?? "";
344
- 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";
345
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"}`
346
958
  : scope === "project" ? `project-${file.replace(/\.md$/, "")}`
347
959
  : `requirement-${file.replace(/^requirement\.md$/, "requirements").replace(/\.md$/, "")}`;
348
960
  return {
@@ -366,11 +978,79 @@ function renderDocumentIndex(context: ProjectContext, snapshot: { id?: unknown;
366
978
  codeIndexGeneration: generation ?? null,
367
979
  sourceDocuments: knowledge.sources,
368
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 })),
369
982
  documents,
370
983
  }, null, 2)}\n`,
371
984
  };
372
985
  }
373
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
+
374
1054
  export async function generateProjectDocuments(context: ProjectContext, snapshotId: string, options: { mode: DocumentGenerationMode; organizer?: LowTierDocumentOrganizer }): Promise<DocumentGenerationResult[]> {
375
1055
  const store = await InitializationStore.open(context);
376
1056
  try {
@@ -389,8 +1069,12 @@ export async function generateProjectDocuments(context: ProjectContext, snapshot
389
1069
  catch (error) { store.failDocumentJob(batchJob, error instanceof Error ? error.message : String(error)); throw error; }
390
1070
  const snapshotMeta = (store.queryContext({ snapshotId, limit: 1 }) as { snapshot: { id?: unknown; indexRef?: unknown } }).snapshot;
391
1071
  const knowledge = await discoverSourceDocuments(context.root);
392
- const outputs = [...renderDocuments(context, facts, organized.text, collections), ...renderKnowledgeDocuments(context, facts, organized.text, snapshotMeta, knowledge)];
393
- outputs.push(renderDocumentIndex(context, snapshotMeta, outputs, knowledge));
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));
394
1078
  const results: DocumentGenerationResult[] = [];
395
1079
  for (const output of outputs) {
396
1080
  const job = store.createDocumentJob(snapshotId, { targetPath: output.targetPath, ownership: "generated", templateVersion: TEMPLATE_VERSION, modelRole: organized.modelRole, input: facts.map(fact => fact.objectKey) });