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.
- cli/__init__.py +50 -0
- cli/__main__.py +5 -0
- cli/commands/__init__.py +1 -0
- cli/commands/anchor_fix.py +47 -0
- cli/commands/diff_doc.py +52 -0
- cli/commands/dispatch.py +77 -0
- cli/commands/extract.py +72 -0
- cli/commands/file_list.py +74 -0
- cli/commands/index.py +84 -0
- cli/commands/lock.py +89 -0
- cli/commands/merge.py +60 -0
- cli/commands/merge_delta.py +19 -0
- cli/commands/metadata.py +24 -0
- cli/commands/pipeline.py +45 -0
- cli/commands/post_merge.py +43 -0
- cli/commands/query.py +52 -0
- cli/commands/render.py +101 -0
- cli/commands/scan_repos.py +46 -0
- cli/commands/setup.py +94 -0
- cli/commands/split.py +196 -0
- cli/commands/stale_files.py +98 -0
- cli/commands/validate.py +191 -0
- core/__init__.py +32 -0
- core/config.py +261 -0
- core/docs/__init__.py +7 -0
- core/docs/section_updater.py +286 -0
- core/docs/shared.py +149 -0
- core/git.py +294 -0
- core/interfaces.py +249 -0
- core/monitor/__init__.py +5 -0
- core/monitor/progress.py +83 -0
- core/monitor/prompt_store.py +49 -0
- core/paths.py +141 -0
- core/preset.py +237 -0
- core/preset_accessors.py +202 -0
- core/preset_classify.py +132 -0
- core/preset_hooks.py +129 -0
- core/preset_profile.py +89 -0
- core/prompt/__init__.py +7 -0
- core/prompt/__main__.py +147 -0
- core/prompt/content.py +320 -0
- core/prompt/context_manager.py +164 -0
- core/prompt/renderer.py +236 -0
- core/prompt/response_parser.py +274 -0
- core/prompt/templates.py +357 -0
- core/prompt/validate_parity.py +162 -0
- core/prompt/variables.py +339 -0
- core/rag/__init__.py +22 -0
- core/rag/__main__.py +136 -0
- core/rag/bm25_index.py +268 -0
- core/rag/chunker.py +273 -0
- core/rag/embedder.py +151 -0
- core/rag/indexer.py +292 -0
- core/rag/loader.py +89 -0
- core/rag/retriever.py +82 -0
- core/skeleton/__init__.py +11 -0
- core/skeleton/__main__.py +934 -0
- core/skeleton/anchor_fix.py +250 -0
- core/skeleton/classify.py +331 -0
- core/skeleton/cmd_anchor_fix.py +43 -0
- core/skeleton/cmd_diff_doc.py +44 -0
- core/skeleton/cmd_lock.py +87 -0
- core/skeleton/cmd_merge_delta.py +41 -0
- core/skeleton/community.py +233 -0
- core/skeleton/dependency_graph.py +306 -0
- core/skeleton/diff_doc.py +248 -0
- core/skeleton/dispatch.py +273 -0
- core/skeleton/dispatch_render.py +319 -0
- core/skeleton/dispatch_source.py +111 -0
- core/skeleton/extract.py +218 -0
- core/skeleton/extract_methods.py +298 -0
- core/skeleton/file_list.py +239 -0
- core/skeleton/impact.py +278 -0
- core/skeleton/jar_download.py +177 -0
- core/skeleton/jar_resolver.py +186 -0
- core/skeleton/loader.py +162 -0
- core/skeleton/merge.py +278 -0
- core/skeleton/merge_delta.py +229 -0
- core/skeleton/metadata.py +96 -0
- core/skeleton/metadata_builders.py +264 -0
- core/skeleton/module_dag.py +330 -0
- core/skeleton/parsers/__init__.py +71 -0
- core/skeleton/parsers/jqassistant.py +300 -0
- core/skeleton/parsers/jqassistant_cypher.py +225 -0
- core/skeleton/parsers/regex.py +171 -0
- core/skeleton/parsers/treesitter.py +324 -0
- core/skeleton/parsers/treesitter_java.py +284 -0
- core/skeleton/parsers/treesitter_multi.py +289 -0
- core/skeleton/pom_parser.py +299 -0
- core/skeleton/post_merge.py +295 -0
- core/skeleton/post_merge_llm.py +82 -0
- core/skeleton/query.py +195 -0
- core/skeleton/shard_context.py +177 -0
- core/skeleton/split.py +180 -0
- core/skeleton/split_cache.py +107 -0
- core/skeleton/split_feedback.py +174 -0
- core/skeleton/split_plan.py +219 -0
- core/skeleton/split_plan_helpers.py +305 -0
- core/skeleton/split_plan_llm.py +274 -0
- core/utils.py +135 -0
- core/validators/__init__.py +65 -0
- core/validators/__main__.py +215 -0
- core/validators/consistency.py +203 -0
- core/validators/coverage.py +171 -0
- core/validators/duplicates.py +76 -0
- core/validators/engine.py +224 -0
- core/validators/links.py +76 -0
- core/validators/sampling.py +169 -0
- core/validators/structure.py +144 -0
- engine/__init__.py +7 -0
- engine/assembler.py +231 -0
- engine/confirm.py +65 -0
- engine/dedup.py +106 -0
- engine/main.py +211 -0
- engine/pipeline/__init__.py +163 -0
- engine/pipeline/recovery.py +250 -0
- engine/pipeline/steps/__init__.py +23 -0
- engine/pipeline/steps/audit.py +220 -0
- engine/pipeline/steps/audit_apply.py +195 -0
- engine/pipeline/steps/audit_helpers.py +155 -0
- engine/pipeline/steps/classify_llm.py +236 -0
- engine/pipeline/steps/classify_prompt.py +223 -0
- engine/pipeline/steps/finalize.py +160 -0
- engine/pipeline/steps/generate.py +169 -0
- engine/pipeline/steps/generate_batch.py +197 -0
- engine/pipeline/steps/generate_recovery.py +170 -0
- engine/pipeline/steps/llm_plan_split.py +253 -0
- engine/pipeline/steps/lock.py +64 -0
- engine/pipeline/steps/preflight.py +237 -0
- engine/pipeline/steps/preflight_adjust.py +147 -0
- engine/pipeline/steps/pregenerate.py +130 -0
- engine/pipeline/steps/quality.py +81 -0
- engine/pipeline/steps/skeleton.py +149 -0
- engine/pipeline/steps/source.py +163 -0
- engine/pipeline/steps/sync.py +117 -0
- engine/pipeline/steps/sync_finalize.py +237 -0
- engine/pipeline/steps/sync_update.py +341 -0
- engine/pipelines.py +91 -0
- engine/runner.py +335 -0
- engine/strategies/__init__.py +86 -0
- engine/strategies/api.py +128 -0
- engine/strategies/delegated.py +50 -0
- engine/strategies/dryrun.py +25 -0
- engine/two_phase.py +143 -0
- mcp_server/__init__.py +73 -0
- mcp_server/__main__.py +5 -0
- mcp_server/tools/__init__.py +1 -0
- mcp_server/tools/config.py +63 -0
- mcp_server/tools/discovery.py +276 -0
- mcp_server/tools/generation.py +184 -0
- mcp_server/tools/planning.py +144 -0
- mcp_server/tools/source.py +175 -0
- mcp_server/tools/validation.py +140 -0
- mcp_server/tools/workflow.py +166 -0
- mcp_server/workflow_loader.py +204 -0
- presets/generic/audit_dimensions.md +132 -0
- presets/generic/doc_types.yaml +152 -0
- presets/generic/preset.yaml +115 -0
- presets/java-spring/audit_dimensions.md +228 -0
- presets/java-spring/audit_dimensions.yaml +203 -0
- presets/java-spring/doc_types.yaml +269 -0
- presets/java-spring/hooks.py +122 -0
- presets/java-spring/preset.yaml +341 -0
- presets/java-spring/templates/README.md +34 -0
- presets/java-spring/templates/audit-system.md +15 -0
- presets/java-spring/templates/subagent-aop.md +105 -0
- presets/java-spring/templates/subagent-api.md +63 -0
- presets/java-spring/templates/subagent-architecture.md +111 -0
- presets/java-spring/templates/subagent-async-events.md +107 -0
- presets/java-spring/templates/subagent-audit-api-contracts.md +40 -0
- presets/java-spring/templates/subagent-audit-architecture.md +38 -0
- presets/java-spring/templates/subagent-audit-business.md +40 -0
- presets/java-spring/templates/subagent-audit-data-models.md +40 -0
- presets/java-spring/templates/subagent-business.md +129 -0
- presets/java-spring/templates/subagent-caching.md +75 -0
- presets/java-spring/templates/subagent-database-access.md +114 -0
- presets/java-spring/templates/subagent-enum.md +75 -0
- presets/java-spring/templates/subagent-error-handling.md +91 -0
- presets/java-spring/templates/subagent-external-integrations.md +80 -0
- presets/java-spring/templates/subagent-index.md +122 -0
- presets/java-spring/templates/subagent-messaging.md +97 -0
- presets/java-spring/templates/subagent-model.md +88 -0
- presets/java-spring/templates/subagent-observability.md +91 -0
- presets/java-spring/templates/subagent-scheduled.md +81 -0
- presets/java-spring/templates/subagent-security.md +102 -0
- presets/java-spring/templates/subagent-structure.md +101 -0
- presets/java-spring/templates/subagent-sync-section.md +34 -0
- presets/java-spring/templates/subagent-utils.md +73 -0
- presets/java-spring/templates/sync-system.md +8 -0
- presets/java-spring/workflow-extensions.md +112 -0
- skills/__init__.py +1 -0
- skills/_shared/README.md +30 -0
- skills/_shared/doc-coverage-shared.md +134 -0
- skills/_shared/doc-quality-standard.md +1058 -0
- skills/_shared/doc-subagent-rules.md +762 -0
- skills/_shared/windows-compat.md +89 -0
- skills/kb-audit/SKILL.md +52 -0
- skills/kb-audit/rules.md +88 -0
- skills/kb-audit/steps/step-01-prepare.md +75 -0
- skills/kb-audit/steps/step-02-audit.md +96 -0
- skills/kb-audit/steps/step-03-verify.md +65 -0
- skills/kb-audit/steps/step-04-report.md +64 -0
- skills/kb-init/SKILL.md +146 -0
- skills/kb-init/rules.md +187 -0
- skills/kb-init/steps/step-01-scope.md +62 -0
- skills/kb-init/steps/step-02-source.md +410 -0
- skills/kb-init/steps/step-03-generate.md +307 -0
- skills/kb-init/steps/step-04-quality.md +92 -0
- skills/kb-init/steps/step-05-finalize.md +132 -0
- skills/kb-init/templates/core/execution-modes.md +29 -0
- skills/kb-init/templates/core/output-only.md +4 -0
- skills/kb-init/templates/core/readwrite.md +33 -0
- skills/kb-search/SKILL.md +138 -0
- skills/kb-search/rules.md +64 -0
- skills/kb-sync/SKILL.md +43 -0
- skills/kb-sync/rules.md +70 -0
- skills/kb-sync/scripts/rebuild_module.py +91 -0
- skills/kb-sync/scripts/scan_repos.py +687 -0
- skills/kb-sync/steps/step-01-detect.md +72 -0
- skills/kb-sync/steps/step-02-update.md +71 -0
- skills/kb-sync/steps/step-03-verify.md +47 -0
- skills/kb-sync/steps/step-04-finalize.md +52 -0
- source_kb-0.2.2.dist-info/METADATA +194 -0
- source_kb-0.2.2.dist-info/RECORD +228 -0
- source_kb-0.2.2.dist-info/WHEEL +5 -0
- source_kb-0.2.2.dist-info/entry_points.txt +3 -0
- source_kb-0.2.2.dist-info/licenses/LICENSE +21 -0
- 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
|