source-kb 0.2.2__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 (228) hide show
  1. cli/__init__.py +50 -0
  2. cli/__main__.py +5 -0
  3. cli/commands/__init__.py +1 -0
  4. cli/commands/anchor_fix.py +47 -0
  5. cli/commands/diff_doc.py +52 -0
  6. cli/commands/dispatch.py +77 -0
  7. cli/commands/extract.py +72 -0
  8. cli/commands/file_list.py +74 -0
  9. cli/commands/index.py +84 -0
  10. cli/commands/lock.py +89 -0
  11. cli/commands/merge.py +60 -0
  12. cli/commands/merge_delta.py +19 -0
  13. cli/commands/metadata.py +24 -0
  14. cli/commands/pipeline.py +45 -0
  15. cli/commands/post_merge.py +43 -0
  16. cli/commands/query.py +52 -0
  17. cli/commands/render.py +101 -0
  18. cli/commands/scan_repos.py +46 -0
  19. cli/commands/setup.py +94 -0
  20. cli/commands/split.py +196 -0
  21. cli/commands/stale_files.py +98 -0
  22. cli/commands/validate.py +191 -0
  23. core/__init__.py +32 -0
  24. core/config.py +261 -0
  25. core/docs/__init__.py +7 -0
  26. core/docs/section_updater.py +286 -0
  27. core/docs/shared.py +149 -0
  28. core/git.py +294 -0
  29. core/interfaces.py +249 -0
  30. core/monitor/__init__.py +5 -0
  31. core/monitor/progress.py +83 -0
  32. core/monitor/prompt_store.py +49 -0
  33. core/paths.py +141 -0
  34. core/preset.py +237 -0
  35. core/preset_accessors.py +202 -0
  36. core/preset_classify.py +132 -0
  37. core/preset_hooks.py +129 -0
  38. core/preset_profile.py +89 -0
  39. core/prompt/__init__.py +7 -0
  40. core/prompt/__main__.py +147 -0
  41. core/prompt/content.py +320 -0
  42. core/prompt/context_manager.py +164 -0
  43. core/prompt/renderer.py +236 -0
  44. core/prompt/response_parser.py +274 -0
  45. core/prompt/templates.py +357 -0
  46. core/prompt/validate_parity.py +162 -0
  47. core/prompt/variables.py +339 -0
  48. core/rag/__init__.py +22 -0
  49. core/rag/__main__.py +136 -0
  50. core/rag/bm25_index.py +268 -0
  51. core/rag/chunker.py +273 -0
  52. core/rag/embedder.py +151 -0
  53. core/rag/indexer.py +292 -0
  54. core/rag/loader.py +89 -0
  55. core/rag/retriever.py +82 -0
  56. core/skeleton/__init__.py +11 -0
  57. core/skeleton/__main__.py +934 -0
  58. core/skeleton/anchor_fix.py +250 -0
  59. core/skeleton/classify.py +331 -0
  60. core/skeleton/cmd_anchor_fix.py +43 -0
  61. core/skeleton/cmd_diff_doc.py +44 -0
  62. core/skeleton/cmd_lock.py +87 -0
  63. core/skeleton/cmd_merge_delta.py +41 -0
  64. core/skeleton/community.py +233 -0
  65. core/skeleton/dependency_graph.py +306 -0
  66. core/skeleton/diff_doc.py +248 -0
  67. core/skeleton/dispatch.py +273 -0
  68. core/skeleton/dispatch_render.py +319 -0
  69. core/skeleton/dispatch_source.py +111 -0
  70. core/skeleton/extract.py +218 -0
  71. core/skeleton/extract_methods.py +298 -0
  72. core/skeleton/file_list.py +239 -0
  73. core/skeleton/impact.py +278 -0
  74. core/skeleton/jar_download.py +177 -0
  75. core/skeleton/jar_resolver.py +186 -0
  76. core/skeleton/loader.py +162 -0
  77. core/skeleton/merge.py +278 -0
  78. core/skeleton/merge_delta.py +229 -0
  79. core/skeleton/metadata.py +96 -0
  80. core/skeleton/metadata_builders.py +264 -0
  81. core/skeleton/module_dag.py +330 -0
  82. core/skeleton/parsers/__init__.py +71 -0
  83. core/skeleton/parsers/jqassistant.py +300 -0
  84. core/skeleton/parsers/jqassistant_cypher.py +225 -0
  85. core/skeleton/parsers/regex.py +171 -0
  86. core/skeleton/parsers/treesitter.py +324 -0
  87. core/skeleton/parsers/treesitter_java.py +284 -0
  88. core/skeleton/parsers/treesitter_multi.py +289 -0
  89. core/skeleton/pom_parser.py +299 -0
  90. core/skeleton/post_merge.py +295 -0
  91. core/skeleton/post_merge_llm.py +82 -0
  92. core/skeleton/query.py +195 -0
  93. core/skeleton/shard_context.py +177 -0
  94. core/skeleton/split.py +180 -0
  95. core/skeleton/split_cache.py +107 -0
  96. core/skeleton/split_feedback.py +174 -0
  97. core/skeleton/split_plan.py +219 -0
  98. core/skeleton/split_plan_helpers.py +305 -0
  99. core/skeleton/split_plan_llm.py +274 -0
  100. core/utils.py +135 -0
  101. core/validators/__init__.py +65 -0
  102. core/validators/__main__.py +215 -0
  103. core/validators/consistency.py +203 -0
  104. core/validators/coverage.py +171 -0
  105. core/validators/duplicates.py +76 -0
  106. core/validators/engine.py +224 -0
  107. core/validators/links.py +76 -0
  108. core/validators/sampling.py +169 -0
  109. core/validators/structure.py +144 -0
  110. engine/__init__.py +7 -0
  111. engine/assembler.py +231 -0
  112. engine/confirm.py +65 -0
  113. engine/dedup.py +106 -0
  114. engine/main.py +211 -0
  115. engine/pipeline/__init__.py +163 -0
  116. engine/pipeline/recovery.py +250 -0
  117. engine/pipeline/steps/__init__.py +23 -0
  118. engine/pipeline/steps/audit.py +220 -0
  119. engine/pipeline/steps/audit_apply.py +195 -0
  120. engine/pipeline/steps/audit_helpers.py +155 -0
  121. engine/pipeline/steps/classify_llm.py +236 -0
  122. engine/pipeline/steps/classify_prompt.py +223 -0
  123. engine/pipeline/steps/finalize.py +160 -0
  124. engine/pipeline/steps/generate.py +169 -0
  125. engine/pipeline/steps/generate_batch.py +197 -0
  126. engine/pipeline/steps/generate_recovery.py +170 -0
  127. engine/pipeline/steps/llm_plan_split.py +253 -0
  128. engine/pipeline/steps/lock.py +64 -0
  129. engine/pipeline/steps/preflight.py +237 -0
  130. engine/pipeline/steps/preflight_adjust.py +147 -0
  131. engine/pipeline/steps/pregenerate.py +130 -0
  132. engine/pipeline/steps/quality.py +81 -0
  133. engine/pipeline/steps/skeleton.py +149 -0
  134. engine/pipeline/steps/source.py +163 -0
  135. engine/pipeline/steps/sync.py +117 -0
  136. engine/pipeline/steps/sync_finalize.py +237 -0
  137. engine/pipeline/steps/sync_update.py +341 -0
  138. engine/pipelines.py +91 -0
  139. engine/runner.py +335 -0
  140. engine/strategies/__init__.py +86 -0
  141. engine/strategies/api.py +128 -0
  142. engine/strategies/delegated.py +50 -0
  143. engine/strategies/dryrun.py +25 -0
  144. engine/two_phase.py +143 -0
  145. mcp_server/__init__.py +73 -0
  146. mcp_server/__main__.py +5 -0
  147. mcp_server/tools/__init__.py +1 -0
  148. mcp_server/tools/config.py +63 -0
  149. mcp_server/tools/discovery.py +276 -0
  150. mcp_server/tools/generation.py +184 -0
  151. mcp_server/tools/planning.py +144 -0
  152. mcp_server/tools/source.py +175 -0
  153. mcp_server/tools/validation.py +140 -0
  154. mcp_server/tools/workflow.py +166 -0
  155. mcp_server/workflow_loader.py +204 -0
  156. presets/generic/audit_dimensions.md +132 -0
  157. presets/generic/doc_types.yaml +152 -0
  158. presets/generic/preset.yaml +115 -0
  159. presets/java-spring/audit_dimensions.md +228 -0
  160. presets/java-spring/audit_dimensions.yaml +203 -0
  161. presets/java-spring/doc_types.yaml +269 -0
  162. presets/java-spring/hooks.py +122 -0
  163. presets/java-spring/preset.yaml +341 -0
  164. presets/java-spring/templates/README.md +34 -0
  165. presets/java-spring/templates/audit-system.md +15 -0
  166. presets/java-spring/templates/subagent-aop.md +105 -0
  167. presets/java-spring/templates/subagent-api.md +63 -0
  168. presets/java-spring/templates/subagent-architecture.md +111 -0
  169. presets/java-spring/templates/subagent-async-events.md +107 -0
  170. presets/java-spring/templates/subagent-audit-api-contracts.md +40 -0
  171. presets/java-spring/templates/subagent-audit-architecture.md +38 -0
  172. presets/java-spring/templates/subagent-audit-business.md +40 -0
  173. presets/java-spring/templates/subagent-audit-data-models.md +40 -0
  174. presets/java-spring/templates/subagent-business.md +129 -0
  175. presets/java-spring/templates/subagent-caching.md +75 -0
  176. presets/java-spring/templates/subagent-database-access.md +114 -0
  177. presets/java-spring/templates/subagent-enum.md +75 -0
  178. presets/java-spring/templates/subagent-error-handling.md +91 -0
  179. presets/java-spring/templates/subagent-external-integrations.md +80 -0
  180. presets/java-spring/templates/subagent-index.md +122 -0
  181. presets/java-spring/templates/subagent-messaging.md +97 -0
  182. presets/java-spring/templates/subagent-model.md +88 -0
  183. presets/java-spring/templates/subagent-observability.md +91 -0
  184. presets/java-spring/templates/subagent-scheduled.md +81 -0
  185. presets/java-spring/templates/subagent-security.md +102 -0
  186. presets/java-spring/templates/subagent-structure.md +101 -0
  187. presets/java-spring/templates/subagent-sync-section.md +34 -0
  188. presets/java-spring/templates/subagent-utils.md +73 -0
  189. presets/java-spring/templates/sync-system.md +8 -0
  190. presets/java-spring/workflow-extensions.md +112 -0
  191. skills/__init__.py +1 -0
  192. skills/_shared/README.md +30 -0
  193. skills/_shared/doc-coverage-shared.md +134 -0
  194. skills/_shared/doc-quality-standard.md +1058 -0
  195. skills/_shared/doc-subagent-rules.md +762 -0
  196. skills/_shared/windows-compat.md +89 -0
  197. skills/kb-audit/SKILL.md +52 -0
  198. skills/kb-audit/rules.md +88 -0
  199. skills/kb-audit/steps/step-01-prepare.md +75 -0
  200. skills/kb-audit/steps/step-02-audit.md +96 -0
  201. skills/kb-audit/steps/step-03-verify.md +65 -0
  202. skills/kb-audit/steps/step-04-report.md +64 -0
  203. skills/kb-init/SKILL.md +146 -0
  204. skills/kb-init/rules.md +187 -0
  205. skills/kb-init/steps/step-01-scope.md +62 -0
  206. skills/kb-init/steps/step-02-source.md +410 -0
  207. skills/kb-init/steps/step-03-generate.md +307 -0
  208. skills/kb-init/steps/step-04-quality.md +92 -0
  209. skills/kb-init/steps/step-05-finalize.md +132 -0
  210. skills/kb-init/templates/core/execution-modes.md +29 -0
  211. skills/kb-init/templates/core/output-only.md +4 -0
  212. skills/kb-init/templates/core/readwrite.md +33 -0
  213. skills/kb-search/SKILL.md +138 -0
  214. skills/kb-search/rules.md +64 -0
  215. skills/kb-sync/SKILL.md +43 -0
  216. skills/kb-sync/rules.md +70 -0
  217. skills/kb-sync/scripts/rebuild_module.py +91 -0
  218. skills/kb-sync/scripts/scan_repos.py +687 -0
  219. skills/kb-sync/steps/step-01-detect.md +72 -0
  220. skills/kb-sync/steps/step-02-update.md +71 -0
  221. skills/kb-sync/steps/step-03-verify.md +47 -0
  222. skills/kb-sync/steps/step-04-finalize.md +52 -0
  223. source_kb-0.2.2.dist-info/METADATA +194 -0
  224. source_kb-0.2.2.dist-info/RECORD +228 -0
  225. source_kb-0.2.2.dist-info/WHEEL +5 -0
  226. source_kb-0.2.2.dist-info/entry_points.txt +3 -0
  227. source_kb-0.2.2.dist-info/licenses/LICENSE +21 -0
  228. source_kb-0.2.2.dist-info/top_level.txt +6 -0
@@ -0,0 +1,286 @@
1
+ """Section-level document manipulation.
2
+
3
+ Locates and replaces/appends/removes specific sections in Markdown documents.
4
+ Shared by both sync and audit pipelines.
5
+
6
+ Usage:
7
+ from core.docs.section_updater import replace_section, append_section, remove_section, list_sections
8
+
9
+ success = replace_section(doc_path, "## User Management", new_content, level=2)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import re
15
+ import unicodedata
16
+ from pathlib import Path
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Public API
21
+ # ---------------------------------------------------------------------------
22
+
23
+
24
+ def replace_section(
25
+ doc_path: Path,
26
+ heading: str,
27
+ new_content: str,
28
+ level: int = 2,
29
+ ) -> bool:
30
+ """Replace content of a specific section.
31
+
32
+ Section boundary: from target heading (inclusive) to next heading of
33
+ equal or higher level (exclusive), or end of file.
34
+
35
+ Args:
36
+ doc_path: Path to the markdown file
37
+ heading: Full heading text (e.g., "## User Management") or just the text part
38
+ new_content: Replacement content (heading line is preserved, only body replaced)
39
+ level: Heading level (2 = ##, 3 = ###)
40
+
41
+ Returns:
42
+ True if section found and replaced, False if heading not found.
43
+ """
44
+ lines, bom = _read_lines(doc_path)
45
+ normalized_heading = _ensure_heading_prefix(heading, level)
46
+ bounds = find_section_boundaries(lines, normalized_heading, level)
47
+ if bounds is None:
48
+ return False
49
+
50
+ start_idx, end_idx = bounds
51
+
52
+ # Build replacement: keep heading line + new content
53
+ heading_line = lines[start_idx]
54
+ content_lines = new_content.rstrip("\n").split("\n") if new_content.strip() else []
55
+
56
+ # Ensure blank line after heading if content exists
57
+ replacement = [heading_line]
58
+ if content_lines:
59
+ if content_lines[0].strip(): # no blank line at start
60
+ replacement.append("")
61
+ replacement.extend(content_lines)
62
+ replacement.append("") # blank line before next section
63
+
64
+ new_lines = lines[:start_idx] + replacement + lines[end_idx:]
65
+ _write_lines(doc_path, new_lines, bom)
66
+ return True
67
+
68
+
69
+ def append_section(
70
+ doc_path: Path,
71
+ heading: str,
72
+ content: str,
73
+ parent_heading: str | None = None,
74
+ level: int = 2,
75
+ ) -> bool:
76
+ """Append a new section to the document.
77
+
78
+ If parent_heading is specified, inserts as last child of that section.
79
+ Otherwise appends at end of file.
80
+
81
+ Returns:
82
+ True if successfully appended.
83
+ """
84
+ lines, bom = _read_lines(doc_path)
85
+ normalized_heading = _ensure_heading_prefix(heading, level)
86
+ content_lines = content.rstrip("\n").split("\n") if content.strip() else []
87
+
88
+ section_block = ["", normalized_heading]
89
+ if content_lines:
90
+ section_block.append("")
91
+ section_block.extend(content_lines)
92
+ section_block.append("")
93
+
94
+ if parent_heading:
95
+ parent_normalized = _ensure_heading_prefix(parent_heading, level - 1 if level > 1 else level)
96
+ parent_level = _count_hashes(parent_normalized)
97
+ bounds = find_section_boundaries(lines, parent_normalized, parent_level)
98
+ if bounds is not None:
99
+ _, end_idx = bounds
100
+ # Insert before the end of parent section
101
+ new_lines = lines[:end_idx] + section_block + lines[end_idx:]
102
+ _write_lines(doc_path, new_lines, bom)
103
+ return True
104
+
105
+ # Fallback: append at end
106
+ # Ensure file ends with newline before appending
107
+ if lines and lines[-1].strip():
108
+ lines.append("")
109
+ new_lines = lines + section_block
110
+ _write_lines(doc_path, new_lines, bom)
111
+ return True
112
+
113
+
114
+ def remove_section(
115
+ doc_path: Path,
116
+ heading: str,
117
+ level: int = 2,
118
+ ) -> bool:
119
+ """Remove a section and its content.
120
+
121
+ Returns:
122
+ True if section found and removed, False if not found.
123
+ """
124
+ lines, bom = _read_lines(doc_path)
125
+ normalized_heading = _ensure_heading_prefix(heading, level)
126
+ bounds = find_section_boundaries(lines, normalized_heading, level)
127
+ if bounds is None:
128
+ return False
129
+
130
+ start_idx, end_idx = bounds
131
+ new_lines = lines[:start_idx] + lines[end_idx:]
132
+
133
+ # Clean up double blank lines at removal point
134
+ if start_idx > 0 and start_idx < len(new_lines):
135
+ if not new_lines[start_idx - 1].strip() and (
136
+ start_idx >= len(new_lines) or not new_lines[start_idx].strip()
137
+ ):
138
+ new_lines.pop(start_idx)
139
+
140
+ _write_lines(doc_path, new_lines, bom)
141
+ return True
142
+
143
+
144
+ def list_sections(
145
+ doc_path: Path,
146
+ level: int = 2,
147
+ ) -> list[str]:
148
+ """List all section headings at the specified level.
149
+
150
+ Returns:
151
+ List of heading strings (e.g., ["## User Management", "## Order Processing"])
152
+ """
153
+ lines, _ = _read_lines(doc_path)
154
+ prefix = "#" * level + " "
155
+ result: list[str] = []
156
+ for line in lines:
157
+ if line.startswith(prefix) and not line.startswith("#" * (level + 1)):
158
+ result.append(line)
159
+ return result
160
+
161
+
162
+ def find_section_boundaries(
163
+ lines: list[str],
164
+ heading: str,
165
+ level: int = 2,
166
+ ) -> tuple[int, int] | None:
167
+ """Find start and end line indices for a section.
168
+
169
+ Args:
170
+ lines: Document lines (no trailing newlines)
171
+ heading: Normalized heading with prefix (e.g., "## User Management")
172
+ level: Heading level
173
+
174
+ Returns:
175
+ (start_idx, end_idx) where start is the heading line (inclusive) and
176
+ end is the first line of the next section (exclusive), or len(lines).
177
+ Returns None if heading not found.
178
+ """
179
+ target_norm = _normalize_heading(heading)
180
+ start_idx: int | None = None
181
+
182
+ for i, line in enumerate(lines):
183
+ if _normalize_heading(line) == target_norm:
184
+ start_idx = i
185
+ break
186
+
187
+ if start_idx is None:
188
+ return None
189
+
190
+ # Find end: next heading of equal or higher level
191
+ target_level = _count_hashes(heading)
192
+ for j in range(start_idx + 1, len(lines)):
193
+ if lines[j].startswith("#"):
194
+ current_level = _count_hashes(lines[j])
195
+ if current_level <= target_level:
196
+ return (start_idx, j)
197
+
198
+ return (start_idx, len(lines))
199
+
200
+
201
+ # ---------------------------------------------------------------------------
202
+ # Internal helpers
203
+ # ---------------------------------------------------------------------------
204
+
205
+
206
+ _EMOJI_PATTERN = re.compile(
207
+ r"[\U0001F300-\U0001F9FF\U00002600-\U000027BF\U0001FA00-\U0001FA6F"
208
+ r"\U0001FA70-\U0001FAFF\U00002702-\U000027B0]+\s*"
209
+ )
210
+
211
+
212
+ def _normalize_heading(text: str) -> str:
213
+ """Normalize a heading for comparison.
214
+
215
+ Strips whitespace, removes emoji prefixes after #, lowercases.
216
+ """
217
+ text = text.strip()
218
+ if not text.startswith("#"):
219
+ return text.lower()
220
+
221
+ # Split into prefix (###) and title
222
+ match = re.match(r"(#+)\s*(.*)", text)
223
+ if not match:
224
+ return text.lower()
225
+
226
+ hashes = match.group(1)
227
+ title = match.group(2)
228
+
229
+ # Remove leading emoji from title
230
+ title = _EMOJI_PATTERN.sub("", title).strip()
231
+
232
+ return f"{hashes} {title}".lower()
233
+
234
+
235
+ def _count_hashes(line: str) -> int:
236
+ """Count leading # characters."""
237
+ count = 0
238
+ for ch in line:
239
+ if ch == "#":
240
+ count += 1
241
+ else:
242
+ break
243
+ return count
244
+
245
+
246
+ def _ensure_heading_prefix(heading: str, level: int) -> str:
247
+ """Ensure heading has the correct # prefix."""
248
+ heading = heading.strip()
249
+ if heading.startswith("#"):
250
+ return heading
251
+ return "#" * level + " " + heading
252
+
253
+
254
+ def _read_lines(doc_path: Path) -> tuple[list[str], str]:
255
+ """Read file into lines, detecting BOM.
256
+
257
+ Returns:
258
+ (lines_without_newlines, bom_prefix)
259
+ """
260
+ raw = doc_path.read_bytes()
261
+ bom = ""
262
+ if raw.startswith(b"\xef\xbb\xbf"):
263
+ bom = "\ufeff"
264
+ raw = raw[3:]
265
+
266
+ text = raw.decode("utf-8")
267
+ # Normalize line endings
268
+ text = text.replace("\r\n", "\n").replace("\r", "\n")
269
+ lines = text.split("\n")
270
+
271
+ # Remove trailing empty line if file ended with \n (split artifact)
272
+ if lines and lines[-1] == "":
273
+ lines.pop()
274
+
275
+ return lines, bom
276
+
277
+
278
+ def _write_lines(doc_path: Path, lines: list[str], bom: str = "") -> None:
279
+ """Write lines back to file, preserving BOM and ensuring trailing newline."""
280
+ content = "\n".join(lines)
281
+ if not content.endswith("\n"):
282
+ content += "\n"
283
+ if bom:
284
+ doc_path.write_bytes(b"\xef\xbb\xbf" + content.encode("utf-8"))
285
+ else:
286
+ doc_path.write_text(content, encoding="utf-8")
core/docs/shared.py ADDED
@@ -0,0 +1,149 @@
1
+ """Shared document generation — cross-module summaries.
2
+
3
+ Generates _shared/ documents that aggregate information across all modules.
4
+
5
+ Usage:
6
+ from core.docs.shared import generate_shared_docs
7
+
8
+ generated = generate_shared_docs(knowledge_dir, config, kb_name)
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import logging
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from core.paths import ensure_dir
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ def generate_shared_docs(
23
+ knowledge_dir: Path,
24
+ config: dict[str, Any],
25
+ kb_name: str,
26
+ ) -> list[str]:
27
+ """Generate _shared/ cross-module documents.
28
+
29
+ Returns list of generated file names.
30
+ """
31
+ shared_dir = knowledge_dir / "_shared"
32
+ ensure_dir(shared_dir)
33
+
34
+ generated: list[str] = []
35
+
36
+ # Always generate project overview
37
+ overview = _generate_project_overview(shared_dir, knowledge_dir, config, kb_name)
38
+ if overview:
39
+ generated.append("project-overview.md")
40
+
41
+ # Cross-module calls (if multiple modules)
42
+ modules = _get_module_dirs(knowledge_dir)
43
+ if len(modules) >= 2:
44
+ cross = _generate_cross_module_calls(shared_dir, modules)
45
+ if cross:
46
+ generated.append("cross-module-calls.md")
47
+
48
+ return generated
49
+
50
+
51
+ def _generate_project_overview(
52
+ shared_dir: Path, knowledge_dir: Path, config: dict, kb_name: str
53
+ ) -> bool:
54
+ """Generate project-overview.md from module index files."""
55
+ kb_config = config["knowledge_bases"][kb_name]
56
+ modules = _get_module_dirs(knowledge_dir)
57
+
58
+ lines = [f"# {kb_config.get('name', kb_name)} — Project Overview", ""]
59
+
60
+ for module_dir in modules:
61
+ index_file = module_dir / "index.md"
62
+ if index_file.exists():
63
+ # Extract first few lines as summary
64
+ content = index_file.read_text(encoding="utf-8")
65
+ first_lines = content.splitlines()[:5]
66
+ lines.append(f"## {module_dir.name}")
67
+ lines.extend(first_lines)
68
+ lines.append("")
69
+ else:
70
+ lines.append(f"## {module_dir.name}")
71
+ lines.append("(index.md not yet generated)")
72
+ lines.append("")
73
+
74
+ output = shared_dir / "project-overview.md"
75
+ output.write_text("\n".join(lines), encoding="utf-8")
76
+ return True
77
+
78
+
79
+ def _generate_cross_module_calls(shared_dir: Path, modules: list[Path]) -> bool:
80
+ """Generate cross-module-calls.md by scanning for inter-module references."""
81
+ lines = ["# Cross-module call relationships", ""]
82
+
83
+ for module_dir in modules:
84
+ bl_file = module_dir / "business-logic.md"
85
+ if not bl_file.exists():
86
+ continue
87
+ content = bl_file.read_text(encoding="utf-8")
88
+
89
+ # Find references to other modules
90
+ other_modules = [m.name for m in modules if m != module_dir]
91
+ refs = []
92
+ for other in other_modules:
93
+ if other in content:
94
+ refs.append(other)
95
+
96
+ if refs:
97
+ lines.append(f"## {module_dir.name}")
98
+ lines.append(f"References modules: {', '.join(refs)}")
99
+ lines.append("")
100
+
101
+ if len(lines) <= 2:
102
+ return False
103
+
104
+ output = shared_dir / "cross-module-calls.md"
105
+ output.write_text("\n".join(lines), encoding="utf-8")
106
+ return True
107
+
108
+
109
+ def _get_module_dirs(knowledge_dir: Path) -> list[Path]:
110
+ """Get all module directories (non-hidden, non-_shared)."""
111
+ if not knowledge_dir.is_dir():
112
+ return []
113
+ return sorted(
114
+ d for d in knowledge_dir.iterdir()
115
+ if d.is_dir() and not d.name.startswith(".") and d.name != "_shared"
116
+ )
117
+
118
+
119
+ def module_order_topo(config: dict, kb_name: str) -> list[str]:
120
+ """Compute topological order of modules based on inter-module dependencies.
121
+
122
+ Reads pom.xml or package.json to determine dependency DAG.
123
+ Falls back to alphabetical order if no dependencies detected.
124
+
125
+ Returns ordered list of module names (upstream first).
126
+ """
127
+ kb_config = config["knowledge_bases"][kb_name]
128
+ source = kb_config.get("source", {})
129
+
130
+ if source.get("structure") == "multi-repo":
131
+ modules = [r["name"] for r in source.get("repos", [])]
132
+ elif source.get("structure") == "monorepo":
133
+ modules = [m["name"] for m in source.get("modules", [])]
134
+ else:
135
+ return []
136
+
137
+ # TODO: Parse pom.xml/package.json for actual dependency graph
138
+ # For now, heuristic: base-lib types first, then services
139
+ base_libs = []
140
+ services = []
141
+ for mod_cfg in source.get("repos", source.get("modules", [])):
142
+ name = mod_cfg.get("name", "")
143
+ mod_type = mod_cfg.get("type", "service")
144
+ if mod_type in ("base-lib", "api-contract"):
145
+ base_libs.append(name)
146
+ else:
147
+ services.append(name)
148
+
149
+ return base_libs + services