campfire-cli 0.1.0__py3-none-any.whl

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.
Files changed (154) hide show
  1. campfire_cli/__init__.py +3 -0
  2. campfire_cli/alembic/__init__.py +1 -0
  3. campfire_cli/alembic/env.py +37 -0
  4. campfire_cli/alembic/script.py.mako +22 -0
  5. campfire_cli/alembic/versions/001_initial_schema.py +81 -0
  6. campfire_cli/alembic/versions/002_workspace_project_registry.py +47 -0
  7. campfire_cli/alembic/versions/003_unified_workspace_state.py +113 -0
  8. campfire_cli/alembic/versions/004_restructure_workspace_domain.py +23 -0
  9. campfire_cli/alembic/versions/005_decision_channel.py +71 -0
  10. campfire_cli/alembic/versions/006_workspace_topology_index.py +61 -0
  11. campfire_cli/alembic/versions/007_workspace_adoption.py +38 -0
  12. campfire_cli/alembic/versions/__init__.py +1 -0
  13. campfire_cli/app/SPEC.md +12 -0
  14. campfire_cli/app/__init__.py +1 -0
  15. campfire_cli/app/base/SPEC.md +3 -0
  16. campfire_cli/app/base/__init__.py +1 -0
  17. campfire_cli/app/base/cli/base_cli.py +37 -0
  18. campfire_cli/app/base/repository/base_repository.py +17 -0
  19. campfire_cli/app/base/schema/base_schema.py +18 -0
  20. campfire_cli/app/base/service/base_service.py +129 -0
  21. campfire_cli/app/decision/SPEC.md +16 -0
  22. campfire_cli/app/decision/__init__.py +1 -0
  23. campfire_cli/app/decision/cli/decision_cli.py +112 -0
  24. campfire_cli/app/decision/repository/decision_repository.py +199 -0
  25. campfire_cli/app/decision/schema/decision_schema.py +56 -0
  26. campfire_cli/app/decision/service/decision_projection_service.py +151 -0
  27. campfire_cli/app/decision/service/decision_service.py +124 -0
  28. campfire_cli/app/document/SPEC.md +51 -0
  29. campfire_cli/app/document/__init__.py +1 -0
  30. campfire_cli/app/document/cli/__init__.py +1 -0
  31. campfire_cli/app/document/cli/document_cli.py +63 -0
  32. campfire_cli/app/document/cli/profile_cli.py +65 -0
  33. campfire_cli/app/document/cli/type_cli.py +49 -0
  34. campfire_cli/app/document/repository/__init__.py +1 -0
  35. campfire_cli/app/document/repository/document_profile_repository.py +17 -0
  36. campfire_cli/app/document/repository/document_type_repository.py +17 -0
  37. campfire_cli/app/document/service/__init__.py +1 -0
  38. campfire_cli/app/document/service/document_profile_service.py +66 -0
  39. campfire_cli/app/document/service/document_rule_service.py +316 -0
  40. campfire_cli/app/document/service/document_scanner.py +38 -0
  41. campfire_cli/app/document/service/document_service.py +139 -0
  42. campfire_cli/app/document/service/document_type_service.py +28 -0
  43. campfire_cli/app/document/service/frontmatter_apply.py +98 -0
  44. campfire_cli/app/document/service/frontmatter_formatter.py +104 -0
  45. campfire_cli/app/document/service/frontmatter_plan.py +75 -0
  46. campfire_cli/app/document/service/profile_registry.py +166 -0
  47. campfire_cli/app/document/service/type_apply.py +185 -0
  48. campfire_cli/app/document/service/type_plan.py +80 -0
  49. campfire_cli/app/maintenance/SPEC.md +22 -0
  50. campfire_cli/app/maintenance/__init__.py +1 -0
  51. campfire_cli/app/maintenance/cli/maintenance_cli.py +103 -0
  52. campfire_cli/app/maintenance/repository/maintenance_repository.py +115 -0
  53. campfire_cli/app/maintenance/schema/maintenance_schema.py +109 -0
  54. campfire_cli/app/maintenance/service/archive_service.py +255 -0
  55. campfire_cli/app/maintenance/service/link_service.py +91 -0
  56. campfire_cli/app/maintenance/service/maintenance_protocol.py +25 -0
  57. campfire_cli/app/maintenance/service/maintenance_service.py +461 -0
  58. campfire_cli/app/maintenance/service/moc_service.py +310 -0
  59. campfire_cli/app/maintenance/service/plan_service.py +411 -0
  60. campfire_cli/app/skill/SPEC.md +3 -0
  61. campfire_cli/app/skill/__init__.py +1 -0
  62. campfire_cli/app/skill/cli/skill_cli.py +44 -0
  63. campfire_cli/app/skill/repository/skill_repository.py +33 -0
  64. campfire_cli/app/skill/schema/skill_schema.py +18 -0
  65. campfire_cli/app/skill/service/skill_service.py +146 -0
  66. campfire_cli/app/workspace/SPEC.md +25 -0
  67. campfire_cli/app/workspace/__init__.py +1 -0
  68. campfire_cli/app/workspace/cli/__init__.py +1 -0
  69. campfire_cli/app/workspace/cli/adoption_cli.py +83 -0
  70. campfire_cli/app/workspace/cli/config_cli.py +30 -0
  71. campfire_cli/app/workspace/cli/domain_cli.py +101 -0
  72. campfire_cli/app/workspace/cli/project_cli.py +135 -0
  73. campfire_cli/app/workspace/cli/restructure_cli.py +133 -0
  74. campfire_cli/app/workspace/cli/space_cli.py +65 -0
  75. campfire_cli/app/workspace/cli/workspace_cli.py +145 -0
  76. campfire_cli/app/workspace/repository/__init__.py +1 -0
  77. campfire_cli/app/workspace/repository/adoption_repository.py +55 -0
  78. campfire_cli/app/workspace/repository/manifest_repository.py +38 -0
  79. campfire_cli/app/workspace/repository/restructure_repository.py +93 -0
  80. campfire_cli/app/workspace/repository/workspace_repository.py +125 -0
  81. campfire_cli/app/workspace/schema/__init__.py +1 -0
  82. campfire_cli/app/workspace/schema/adoption_schema.py +47 -0
  83. campfire_cli/app/workspace/schema/restructure_schema.py +64 -0
  84. campfire_cli/app/workspace/schema/workspace_schema.py +225 -0
  85. campfire_cli/app/workspace/service/__init__.py +1 -0
  86. campfire_cli/app/workspace/service/adoption_protocol.py +11 -0
  87. campfire_cli/app/workspace/service/adoption_service.py +364 -0
  88. campfire_cli/app/workspace/service/config_service.py +189 -0
  89. campfire_cli/app/workspace/service/domain_restructure_service.py +316 -0
  90. campfire_cli/app/workspace/service/project_service.py +389 -0
  91. campfire_cli/app/workspace/service/restructure_protocol.py +19 -0
  92. campfire_cli/app/workspace/service/restructure_service.py +392 -0
  93. campfire_cli/app/workspace/service/restructure_verifier.py +83 -0
  94. campfire_cli/app/workspace/service/structure_service.py +494 -0
  95. campfire_cli/app/workspace/service/workspace_protocol.py +24 -0
  96. campfire_cli/app/workspace/service/workspace_service.py +304 -0
  97. campfire_cli/common/SPEC.md +12 -0
  98. campfire_cli/common/__init__.py +1 -0
  99. campfire_cli/common/database/__init__.py +4 -0
  100. campfire_cli/common/database/migrations.py +18 -0
  101. campfire_cli/common/database/models.py +215 -0
  102. campfire_cli/common/database/session.py +30 -0
  103. campfire_cli/common/documents/__init__.py +1 -0
  104. campfire_cli/common/documents/document_types.py +119 -0
  105. campfire_cli/common/documents/frontmatter_schema.py +42 -0
  106. campfire_cli/common/documents/markdown.py +66 -0
  107. campfire_cli/common/exceptions/__init__.py +7 -0
  108. campfire_cli/common/exceptions/base.py +19 -0
  109. campfire_cli/common/filesystem/__init__.py +6 -0
  110. campfire_cli/common/filesystem/atomic.py +30 -0
  111. campfire_cli/common/filesystem/locking.py +55 -0
  112. campfire_cli/common/governance/__init__.py +13 -0
  113. campfire_cli/common/governance/issues.py +38 -0
  114. campfire_cli/common/governance/locking.py +31 -0
  115. campfire_cli/common/governance/snapshots.py +26 -0
  116. campfire_cli/common/hashing.py +10 -0
  117. campfire_cli/common/reports/__init__.py +1 -0
  118. campfire_cli/common/reports/json_report.py +8 -0
  119. campfire_cli/common/reports/markdown_report.py +29 -0
  120. campfire_cli/config/SPEC.md +5 -0
  121. campfire_cli/config/defaults.py +64 -0
  122. campfire_cli/config/settings.py +50 -0
  123. campfire_cli/container.py +130 -0
  124. campfire_cli/main.py +133 -0
  125. campfire_cli/resources/__init__.py +1 -0
  126. campfire_cli/resources/bases/Agent/346/211/247/350/241/214/345/267/245/344/275/234/345/217/260.base +35 -0
  127. campfire_cli/resources/bases/SPEC.md +3 -0
  128. campfire_cli/resources/bases//344/273/273/345/212/241/345/267/245/344/275/234/345/217/260.base +49 -0
  129. campfire_cli/resources/bases//345/206/263/347/255/226/345/267/245/344/275/234/345/217/260.base +40 -0
  130. campfire_cli/resources/bases//345/276/205/345/275/222/346/241/243/346/226/207/346/241/243.base +28 -0
  131. campfire_cli/resources/bases//351/241/271/347/233/256/346/226/207/346/241/243.base +35 -0
  132. campfire_cli/resources/defaults/__init__.py +1 -0
  133. campfire_cli/resources/defaults/config.yml +458 -0
  134. campfire_cli/resources/skills/SPEC.md +3 -0
  135. campfire_cli/resources/skills/campfire-context-bootstrap/SKILL.md +62 -0
  136. campfire_cli/resources/skills/campfire-context-bootstrap/agents/openai.yaml +3 -0
  137. campfire_cli/resources/skills/campfire-conversation-router/SKILL.md +53 -0
  138. campfire_cli/resources/skills/campfire-conversation-router/agents/openai.yaml +3 -0
  139. campfire_cli/resources/skills/campfire-conversation-router/references/inbox-handoff.md +19 -0
  140. campfire_cli/resources/skills/campfire-conversation-router/references/learning-intake.md +31 -0
  141. campfire_cli/resources/skills/campfire-conversation-router/references/task-intake.md +28 -0
  142. campfire_cli/resources/skills/campfire-document-capture/SKILL.md +109 -0
  143. campfire_cli/resources/skills/campfire-inbox-triage/SKILL.md +23 -0
  144. campfire_cli/resources/skills/campfire-weekly-report-writing/SKILL.md +21 -0
  145. campfire_cli/resources/skills/campfire-weekly-report-writing/agents/openai.yaml +3 -0
  146. campfire_cli/resources/skills/campfire-workspace-adoption/SKILL.md +42 -0
  147. campfire_cli/resources/skills/campfire-workspace-maintenance/SKILL.md +74 -0
  148. campfire_cli/resources/skills/campfire-workspace-maintenance/references//344/273/273/345/212/241/346/255/243/346/226/207/347/273/223/346/236/204.md +23 -0
  149. campfire_cli/resources/skills/campfire-workspace-restructure/SKILL.md +52 -0
  150. campfire_cli-0.1.0.dist-info/METADATA +141 -0
  151. campfire_cli-0.1.0.dist-info/RECORD +154 -0
  152. campfire_cli-0.1.0.dist-info/WHEEL +4 -0
  153. campfire_cli-0.1.0.dist-info/entry_points.txt +2 -0
  154. campfire_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,310 @@
1
+ """
2
+ SPEC:
3
+ name: moc_service
4
+ purpose: 幂等生成领域 MOC 自动区域、相关文档页面、索引缓存和治理报告
5
+ default_env_file: none
6
+ env_override: none
7
+ idempotent: true
8
+ behavior:
9
+ - 从领域声明和 Markdown 正文生成派生内容
10
+ - 内容未变化时不重写文件
11
+ - 支持 dry-run 和 JSON 输出
12
+ safety:
13
+ - 不移动、重命名、合并或删除原始笔记
14
+ - 只覆盖成对标记之间的 MOC 自动区域
15
+ idempotency_keys:
16
+ domain: domain_id
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import re
22
+ from collections import Counter
23
+ from pathlib import Path
24
+ from typing import Any
25
+
26
+ from campfire_cli.app.workspace.schema.workspace_schema import Domain
27
+ from campfire_cli.app.workspace.service.structure_service import parse_marker as parse_frontmatter
28
+ from campfire_cli.config.defaults import config_section
29
+
30
+ START_MARKER = "<!-- AUTO-GENERATED:DOMAIN-INDEX:START -->"
31
+ END_MARKER = "<!-- AUTO-GENERATED:DOMAIN-INDEX:END -->"
32
+
33
+ WIKILINK_RE = re.compile(r"\[\[([^\]|#]+)(?:[|#][^\]]*)?\]\]")
34
+ LATIN_RE = re.compile(r"[A-Za-z][A-Za-z0-9.+#_-]{1,}")
35
+ CHINESE_RE = re.compile(r"[\u4e00-\u9fff]{2,}")
36
+
37
+
38
+ def note_title(path: Path) -> str:
39
+ text = path.read_text(encoding="utf-8")
40
+ match = re.search(r"^#\s+(.+)$", text, re.MULTILINE)
41
+ return match.group(1).strip() if match else path.stem
42
+
43
+
44
+ def note_terms(path: Path) -> Counter[str]:
45
+ text = path.read_text(encoding="utf-8")
46
+ terms: list[str] = [token.lower() for token in LATIN_RE.findall(text)]
47
+ for block in CHINESE_RE.findall(text):
48
+ terms.extend(block[index : index + 2] for index in range(len(block) - 1))
49
+ ignored = {
50
+ "text",
51
+ "true",
52
+ "false",
53
+ "none",
54
+ "一个",
55
+ "可以",
56
+ "使用",
57
+ "通过",
58
+ "这个",
59
+ "如果",
60
+ "需要",
61
+ "进行",
62
+ "我们",
63
+ "什么",
64
+ "如何",
65
+ "例如",
66
+ "文件",
67
+ "目录",
68
+ "实现",
69
+ "这里",
70
+ "下面",
71
+ "对于",
72
+ "就是",
73
+ "因为",
74
+ "所以",
75
+ }
76
+ return Counter(term for term in terms if term not in ignored)
77
+
78
+
79
+ def similarity(left: Counter[str], right: Counter[str]) -> tuple[float, list[str]]:
80
+ left_terms = set(left)
81
+ right_terms = set(right)
82
+ union = left_terms | right_terms
83
+ if not union:
84
+ return 0.0, []
85
+ common = left_terms & right_terms
86
+ score = len(common) / len(union)
87
+ reasons = [
88
+ term
89
+ for term, _ in sorted(
90
+ ((term, left[term] + right[term]) for term in common),
91
+ key=lambda item: (-item[1], item[0]),
92
+ )[:5]
93
+ ]
94
+ return score, reasons
95
+
96
+
97
+ def title_keywords(path: Path) -> set[str]:
98
+ title = f"{path.stem} {note_title(path)}"
99
+ keywords = {token.lower() for token in LATIN_RE.findall(title)}
100
+ ignored = {"go", "agent", "development", "document", "guide", "note", "over", "and", "with"}
101
+ return keywords - ignored
102
+
103
+
104
+ def direct_notes(domain: Domain, marker_name: str) -> list[Path]:
105
+ excluded = {marker_name, f"{domain.moc}.md"}
106
+ return sorted(
107
+ [path for path in domain.path.glob("*.md") if path.name not in excluded],
108
+ key=lambda path: path.name.casefold(),
109
+ )
110
+
111
+
112
+ def generate_relations(
113
+ notes: list[Path],
114
+ domain_by_note: dict[Path, str],
115
+ vault_root: Path,
116
+ limit: int,
117
+ minimum: float,
118
+ cross_limit: int,
119
+ cross_minimum: float,
120
+ ) -> dict[Path, list[dict[str, Any]]]:
121
+ vectors = {note: note_terms(note) for note in notes}
122
+ texts = {note: note.read_text(encoding="utf-8") for note in notes}
123
+ explicit_links = {
124
+ note: {Path(value).stem for value in WIKILINK_RE.findall(texts[note])} for note in notes
125
+ }
126
+ relations: dict[Path, list[dict[str, Any]]] = {}
127
+ for source in notes:
128
+ strong: list[dict[str, Any]] = []
129
+ same_domain_semantic: list[dict[str, Any]] = []
130
+ cross_domain_semantic: list[dict[str, Any]] = []
131
+ for target in notes:
132
+ if source == target:
133
+ continue
134
+ score, reasons = similarity(vectors[source], vectors[target])
135
+ if target.stem in explicit_links[source]:
136
+ relation_type = "direct-link"
137
+ score = 1.0
138
+ reasons = ["正文直接链接"]
139
+ elif source.stem in explicit_links[target]:
140
+ relation_type = "backlink"
141
+ score = 1.0
142
+ reasons = ["目标文档引用本文"]
143
+ else:
144
+ same_domain = domain_by_note[source] == domain_by_note[target]
145
+ relation_type = (
146
+ "same-domain-similarity" if same_domain else "cross-domain-similarity"
147
+ )
148
+ if not same_domain:
149
+ common_title_keywords = sorted(title_keywords(source) & title_keywords(target))
150
+ if not common_title_keywords:
151
+ continue
152
+ reasons = [
153
+ f"标题共同关键词:{value}" for value in common_title_keywords
154
+ ] + reasons
155
+ threshold = cross_minimum if relation_type == "cross-domain-similarity" else minimum
156
+ if relation_type in {"direct-link", "backlink"} or score >= threshold:
157
+ item = {
158
+ "target": str(target.relative_to(vault_root).with_suffix("")),
159
+ "target_name": target.stem,
160
+ "target_domain": domain_by_note[target],
161
+ "type": relation_type,
162
+ "score": round(score, 4),
163
+ "reasons": reasons,
164
+ }
165
+ if relation_type in {"direct-link", "backlink"}:
166
+ strong.append(item)
167
+ elif relation_type == "same-domain-similarity":
168
+ same_domain_semantic.append(item)
169
+ else:
170
+ cross_domain_semantic.append(item)
171
+ strong.sort(key=lambda item: (item["type"], item["target"].casefold()))
172
+ same_domain_semantic.sort(key=lambda item: (-item["score"], item["target"].casefold()))
173
+ cross_domain_semantic.sort(key=lambda item: (-item["score"], item["target"].casefold()))
174
+ relations[source] = (
175
+ strong + same_domain_semantic[:limit] + cross_domain_semantic[:cross_limit]
176
+ )
177
+ return relations
178
+
179
+
180
+ def replace_generated_region(original: str, generated: str) -> str:
181
+ if original.count(START_MARKER) != 1 or original.count(END_MARKER) != 1:
182
+ raise ValueError("MOC 自动生成标记必须且只能各出现一次")
183
+ start = original.index(START_MARKER) + len(START_MARKER)
184
+ end = original.index(END_MARKER)
185
+ return original[:start] + "\n\n" + generated.rstrip() + "\n\n" + original[end:]
186
+
187
+
188
+ def write_if_changed(
189
+ path: Path, content: str, dry_run: bool, changes: list[str], vault_root: Path
190
+ ) -> None:
191
+ previous = path.read_text(encoding="utf-8") if path.exists() else None
192
+ if previous == content:
193
+ return
194
+ changes.append(str(path.relative_to(vault_root)))
195
+ if not dry_run:
196
+ path.parent.mkdir(parents=True, exist_ok=True)
197
+ path.write_text(content, encoding="utf-8")
198
+
199
+
200
+ def generate_domain_content(
201
+ domain: Domain, domains: list[Domain], notes: list[Path], relation_page: Path | None
202
+ ) -> str:
203
+ children = sorted(
204
+ [item for item in domains if item.parent_domain == domain.id],
205
+ key=lambda item: item.name.casefold(),
206
+ )
207
+ parent = next((item for item in domains if item.id == domain.parent_domain), None)
208
+ lines = ["## 领域位置", ""]
209
+ lines.append(f"- 父领域:[[{parent.moc}|{parent.name}]]" if parent else "- 父领域:当前治理根")
210
+ lines.extend(["", "## 子领域", ""])
211
+ lines.extend([f"- [[{child.moc}|{child.name}]]" for child in children] or ["- 暂无"])
212
+ lines.extend(["", "## 本领域文档", ""])
213
+ lines.extend([f"- [[{note.stem}]]" for note in notes] or ["- 暂无"])
214
+ lines.extend(["", "## 自动关系", ""])
215
+ lines.append(f"- [[{relation_page.stem}|相关文档计算结果]]" if relation_page else "- 暂无")
216
+ return "\n".join(lines)
217
+
218
+
219
+ def generate_project_domain_content(
220
+ domain: Domain,
221
+ domains: list[Domain],
222
+ notes: list[Path],
223
+ marker_name: str,
224
+ project_doc_types: dict[str, dict[str, str]] | None = None,
225
+ ) -> str:
226
+ """project-docs 领域的 MOC 自动区域:按文档类型分组并列出状态,不计算相似度关系。"""
227
+ children = sorted(
228
+ [item for item in domains if item.parent_domain == domain.id],
229
+ key=lambda item: item.name.casefold(),
230
+ )
231
+ parent = next((item for item in domains if item.id == domain.parent_domain), None)
232
+ lines = ["## 领域位置", ""]
233
+ lines.append(f"- 父领域:[[{parent.moc}|{parent.name}]]" if parent else "- 父领域:当前治理根")
234
+ lines.extend(["", "## 子领域", ""])
235
+ lines.extend([f"- [[{child.moc}|{child.name}]]" for child in children] or ["- 暂无"])
236
+ lines.extend(["", "## 文档索引", ""])
237
+ if project_doc_types is None:
238
+ config = config_section("document_types")
239
+ project_doc_types = {
240
+ name: config["types"][name] for name in config["profiles"]["project-docs"]
241
+ }
242
+ by_type: dict[str, list[tuple[Path, str]]] = {}
243
+ for note in notes:
244
+ if note.name in {"README.md", "CLAUDE.md"}:
245
+ continue
246
+ meta = parse_frontmatter(note)
247
+ doc_type = meta.get("type", "") if meta else ""
248
+ status = meta.get("status", "") if meta else ""
249
+ if not meta:
250
+ status = "缺 frontmatter"
251
+ if doc_type not in project_doc_types:
252
+ doc_type = "未分类"
253
+ by_type.setdefault(doc_type, []).append((note, status))
254
+ for doc_type in [*project_doc_types, "未分类"]:
255
+ entries = sorted(by_type.get(doc_type, []), key=lambda item: item[0].name.casefold())
256
+ if not entries:
257
+ continue
258
+ heading = "未分类" if doc_type == "未分类" else project_doc_types[doc_type]["label"]
259
+ lines.extend([f"### {heading}", ""])
260
+ for note, status in entries:
261
+ suffix = f" `{status}`" if status else ""
262
+ lines.append(f"- [[{note.stem}]]{suffix}")
263
+ lines.append("")
264
+ record_dirs = [d for d in ["记录", "_总览"] if (domain.path / d).is_dir()]
265
+ for d in record_dirs:
266
+ count = len(list((domain.path / d).rglob("*.md")))
267
+ if count:
268
+ lines.extend([f"### {d}/", "", f"- {count} 篇,见目录", ""])
269
+ archive_dir = domain.path / "archive"
270
+ if archive_dir.is_dir():
271
+ archived_notes = sorted(
272
+ archive_dir.rglob("*.md"),
273
+ key=lambda path: str(path.relative_to(archive_dir)).casefold(),
274
+ )
275
+ if archived_notes:
276
+ lines.extend(["### 已归档", ""])
277
+ for archived_note in archived_notes:
278
+ meta = parse_frontmatter(archived_note)
279
+ reason = meta.get("archive_reason", "待补原因") if meta else "待补元数据"
280
+ successor = meta.get("superseded_by", "") if meta else ""
281
+ suffix = f";替代:{successor}" if successor and successor != "[]" else ""
282
+ rel = archived_note.relative_to(domain.path).with_suffix("")
283
+ lines.append(f"- [[{rel}|{archived_note.stem}]]:`{reason}`{suffix}")
284
+ lines.append("")
285
+ return "\n".join(lines).rstrip()
286
+
287
+
288
+ def relation_markdown(
289
+ domain: Domain, notes: list[Path], relations: dict[Path, list[dict[str, Any]]]
290
+ ) -> str:
291
+ lines = [
292
+ f"# {domain.name}相关文档",
293
+ "",
294
+ "> 本页由 `governance_sync.py` 自动生成,请勿手工编辑。",
295
+ "",
296
+ ]
297
+ for note in notes:
298
+ lines.extend([f"## [[{note.stem}]]", ""])
299
+ items = relations.get(note, [])
300
+ if not items:
301
+ lines.extend(["- 暂无达到阈值的相关文档", ""])
302
+ continue
303
+ for item in items:
304
+ reason = "、".join(item["reasons"]) or "同领域内容相似"
305
+ lines.append(
306
+ f"- [[{item['target']}|{item['target_name']}]]:"
307
+ f"`{item['type']}`,得分 `{item['score']:.4f}`;依据:{reason}"
308
+ )
309
+ lines.append("")
310
+ return "\n".join(lines).rstrip() + "\n"