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,319 @@
|
|
|
1
|
+
"""Dispatch plan rendering — Markdown preview table and statistics formatting.
|
|
2
|
+
|
|
3
|
+
Renders a DispatchPlan into human-readable Markdown tables for Gate 3A preview.
|
|
4
|
+
No CLI I/O, no LLM calls.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from core.skeleton.dispatch import compute_dispatch_plan
|
|
8
|
+
from core.skeleton.dispatch_render import render_markdown, render_note
|
|
9
|
+
|
|
10
|
+
plan = compute_dispatch_plan(...)
|
|
11
|
+
markdown = render_markdown(plan)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from core.skeleton.dispatch import DispatchEntry, DispatchPlan
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def write_shard_files(plan: DispatchPlan, module_dir: Path) -> int:
|
|
22
|
+
"""Write shard file lists to .meta/shards/ for split entries.
|
|
23
|
+
|
|
24
|
+
Returns the number of shard files written.
|
|
25
|
+
"""
|
|
26
|
+
count = 0
|
|
27
|
+
shards_dir = module_dir / ".meta" / "shards"
|
|
28
|
+
for entry in plan.entries:
|
|
29
|
+
if entry.split_count > 1 and entry.shards:
|
|
30
|
+
shards_dir.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
for idx, shard in enumerate(entry.shards, 1):
|
|
32
|
+
shard_path = shards_dir / f"{entry.doc_type}-shard-{idx:02d}.txt"
|
|
33
|
+
file_paths = shard.files if shard.files else []
|
|
34
|
+
if file_paths:
|
|
35
|
+
shard_path.write_text("\n".join(file_paths) + "\n", encoding="utf-8")
|
|
36
|
+
count += 1
|
|
37
|
+
return count
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Note computation
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def compute_note(
|
|
46
|
+
entry: DispatchEntry,
|
|
47
|
+
mode: str = "readwrite",
|
|
48
|
+
skeleton_size_kb: int = 0,
|
|
49
|
+
) -> str:
|
|
50
|
+
"""Generate meaningful note content for a dispatch entry.
|
|
51
|
+
|
|
52
|
+
Priority: risk warning > execution constraint > doc-type hint.
|
|
53
|
+
"""
|
|
54
|
+
notes: list[str] = []
|
|
55
|
+
# Use mode-specific defaults (these match doc_types.yaml split config)
|
|
56
|
+
_DISPLAY_MAX_BYTES = {"readwrite": 2097152, "output-only": 307200}
|
|
57
|
+
max_bytes = _DISPLAY_MAX_BYTES.get(mode, 512000)
|
|
58
|
+
|
|
59
|
+
# 1. Risk: approaching byte limit
|
|
60
|
+
if entry.total_bytes > 0 and entry.doc_type not in ("source-tree-analysis", "index"):
|
|
61
|
+
usage_pct = entry.total_bytes / max_bytes
|
|
62
|
+
if usage_pct > 0.9:
|
|
63
|
+
notes.append(f"⚠️ Approaching limit ({entry.total_bytes // 1024}KB/{max_bytes // 1024}KB)")
|
|
64
|
+
elif usage_pct > 0.75:
|
|
65
|
+
notes.append(f"Large input ({int(usage_pct * 100)}%)")
|
|
66
|
+
|
|
67
|
+
# 2. Doc-type specific constraints
|
|
68
|
+
if entry.doc_type == "architecture":
|
|
69
|
+
notes.append("Must not produce redis/kafka/business-logic content")
|
|
70
|
+
elif entry.doc_type == "index":
|
|
71
|
+
notes.append("Depends on all prior tasks completing")
|
|
72
|
+
elif entry.doc_type == "source-tree-analysis" and skeleton_size_kb > 50:
|
|
73
|
+
notes.append("Use skeleton-summary (skeleton too large)")
|
|
74
|
+
elif entry.doc_type == "business-logic":
|
|
75
|
+
notes.append("Organize by business flow, not by class name")
|
|
76
|
+
|
|
77
|
+
# 3. Large file count warning
|
|
78
|
+
if entry.file_count > 80 and entry.doc_type not in ("source-tree-analysis", "index"):
|
|
79
|
+
notes.append(f"{entry.file_count} files, requires batched reading")
|
|
80
|
+
|
|
81
|
+
return " | ".join(notes) if notes else ""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Markdown rendering
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def render_markdown(plan: DispatchPlan, mode: str = "readwrite") -> str:
|
|
90
|
+
"""Render a DispatchPlan as a Markdown document.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
plan: Computed dispatch plan
|
|
94
|
+
mode: Execution mode for note computation
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Formatted Markdown string with header, table, shard details, and summary
|
|
98
|
+
"""
|
|
99
|
+
lines: list[str] = []
|
|
100
|
+
|
|
101
|
+
# Title
|
|
102
|
+
lines.append(f"# Dispatch Plan: {plan.module_name}")
|
|
103
|
+
lines.append("")
|
|
104
|
+
lines.append(f"Module: {plan.module_name} ({plan.source_file_count} source files, "
|
|
105
|
+
f"{plan.source_total_lines} lines)")
|
|
106
|
+
lines.append(f"Skeleton size: {plan.skeleton_size_kb}KB")
|
|
107
|
+
lines.append(f"Mode: {'Agent (readwrite)' if mode == 'readwrite' else 'CLI (output-only)'}")
|
|
108
|
+
lines.append("")
|
|
109
|
+
|
|
110
|
+
# Dispatch table
|
|
111
|
+
lines.append("## Dispatch Plan")
|
|
112
|
+
lines.append("")
|
|
113
|
+
lines.append("| Batch | Sub-agent | Output Doc | Input Files | Input Lines | Input Bytes | Split Strategy | Notes |")
|
|
114
|
+
lines.append("|------|--------|---------|-----------|-----------|-----------|---------|------|")
|
|
115
|
+
|
|
116
|
+
for entry in plan.entries:
|
|
117
|
+
note = compute_note(entry, mode=mode, skeleton_size_kb=plan.skeleton_size_kb)
|
|
118
|
+
row = _format_row(entry, note)
|
|
119
|
+
lines.append(row)
|
|
120
|
+
|
|
121
|
+
# Shard details (for entries with split_count > 1)
|
|
122
|
+
sharded = [e for e in plan.entries if e.split_count > 1]
|
|
123
|
+
if sharded:
|
|
124
|
+
lines.append("")
|
|
125
|
+
lines.append("## Shard Details")
|
|
126
|
+
lines.append("")
|
|
127
|
+
for entry in sharded:
|
|
128
|
+
lines.append(f"### {entry.doc_type} (split into {entry.split_count})")
|
|
129
|
+
lines.append("")
|
|
130
|
+
if entry.pending_grouping:
|
|
131
|
+
lines.append(f"> **Pending Agent grouping**: Read the file list in `{entry.pending_grouping}`,")
|
|
132
|
+
lines.append(f"> split into {entry.split_count} groups by business domain, then run `split-apply` to backfill.")
|
|
133
|
+
lines.append("")
|
|
134
|
+
lines.append("| Shard | Domain | Files |")
|
|
135
|
+
lines.append("|------|--------|--------|")
|
|
136
|
+
if entry.shards:
|
|
137
|
+
for i, shard in enumerate(entry.shards, 1):
|
|
138
|
+
name = shard.name or f"group-{i}"
|
|
139
|
+
lines.append(f"| shard-{i:02d} | {name} | {shard.file_count} |")
|
|
140
|
+
else:
|
|
141
|
+
per_shard = entry.file_count // entry.split_count if entry.file_count else 0
|
|
142
|
+
for i in range(1, entry.split_count + 1):
|
|
143
|
+
lines.append(f"| shard-{i:02d} | (pending Agent grouping) | ~{per_shard} |")
|
|
144
|
+
lines.append("")
|
|
145
|
+
|
|
146
|
+
# Summary
|
|
147
|
+
lines.append("## Summary")
|
|
148
|
+
lines.append("")
|
|
149
|
+
lines.append(f"- Sub-agent tasks: {plan.total_shards()}")
|
|
150
|
+
lines.append(f"- Total input files: {plan.source_file_count}")
|
|
151
|
+
lines.append(f"- Total input lines: {plan.source_total_lines}")
|
|
152
|
+
|
|
153
|
+
return "\n".join(lines)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def render_summary(plan: DispatchPlan) -> str:
|
|
157
|
+
"""Render a brief summary of the dispatch plan.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Short summary string (e.g., "12 tasks across 4 batches, 3 need splitting")
|
|
161
|
+
"""
|
|
162
|
+
total = len(plan.entries)
|
|
163
|
+
batches = len(set(e.batch_id for e in plan.entries))
|
|
164
|
+
splits = sum(1 for e in plan.entries if e.split_count > 1)
|
|
165
|
+
|
|
166
|
+
parts = [f"{total} tasks", f"{batches} batches"]
|
|
167
|
+
if splits:
|
|
168
|
+
parts.append(f"{splits} need splitting")
|
|
169
|
+
return ", ".join(parts)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def render_batch_groups(plan: DispatchPlan) -> list[tuple[str, list[DispatchEntry]]]:
|
|
173
|
+
"""Group entries by batch for sequential execution.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
List of (batch_id, entries) tuples in batch order
|
|
177
|
+
"""
|
|
178
|
+
groups: dict[str, list[DispatchEntry]] = {}
|
|
179
|
+
for entry in plan.entries:
|
|
180
|
+
groups.setdefault(entry.batch_id, []).append(entry)
|
|
181
|
+
|
|
182
|
+
# Maintain batch_order ordering
|
|
183
|
+
result: list[tuple[str, list[DispatchEntry]]] = []
|
|
184
|
+
seen: set[str] = set()
|
|
185
|
+
for batch_id, _ in plan.batch_order:
|
|
186
|
+
if batch_id in groups and batch_id not in seen:
|
|
187
|
+
result.append((batch_id, groups[batch_id]))
|
|
188
|
+
seen.add(batch_id)
|
|
189
|
+
|
|
190
|
+
# Append any remaining
|
|
191
|
+
for batch_id, entries in groups.items():
|
|
192
|
+
if batch_id not in seen:
|
|
193
|
+
result.append((batch_id, entries))
|
|
194
|
+
|
|
195
|
+
return result
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
# Task manifest generation (shared by CLI and Agent modes)
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def plan_to_tasks(
|
|
204
|
+
plan: DispatchPlan,
|
|
205
|
+
kb_name: str,
|
|
206
|
+
preset_name: str,
|
|
207
|
+
preset: dict,
|
|
208
|
+
knowledge_dir,
|
|
209
|
+
mode: str = "readwrite",
|
|
210
|
+
) -> dict:
|
|
211
|
+
"""Convert DispatchPlan to task manifest JSON.
|
|
212
|
+
|
|
213
|
+
Shared by CLI DispatchPlanStep and Agent dispatch-preview command.
|
|
214
|
+
"""
|
|
215
|
+
from pathlib import Path
|
|
216
|
+
from core.preset import get_template_path
|
|
217
|
+
|
|
218
|
+
module_name = plan.module_name
|
|
219
|
+
module_dir = Path(knowledge_dir) / module_name
|
|
220
|
+
|
|
221
|
+
tasks: list[dict] = []
|
|
222
|
+
for entry in plan.entries:
|
|
223
|
+
dt = entry.doc_type
|
|
224
|
+
template_name = get_template_path(preset, dt, preset_name) or ""
|
|
225
|
+
|
|
226
|
+
if entry.split_count <= 1:
|
|
227
|
+
output_file = str((module_dir / entry.doc_filename).resolve())
|
|
228
|
+
render_cmd = (
|
|
229
|
+
f"py -3.12 -X utf8 -m core.prompt render"
|
|
230
|
+
f" --template \"{template_name}\""
|
|
231
|
+
f" --module {module_name}"
|
|
232
|
+
f" --config \"kb-project.yaml\""
|
|
233
|
+
f" --kb {kb_name}"
|
|
234
|
+
f" --doc-type {dt}"
|
|
235
|
+
f" --mode {mode}"
|
|
236
|
+
)
|
|
237
|
+
tasks.append({
|
|
238
|
+
"id": dt,
|
|
239
|
+
"batch": entry.batch_id,
|
|
240
|
+
"doc_type": dt,
|
|
241
|
+
"doc_filename": entry.doc_filename,
|
|
242
|
+
"shard": None,
|
|
243
|
+
"total_shards": 1,
|
|
244
|
+
"file_count": entry.file_count,
|
|
245
|
+
"line_count": entry.total_lines,
|
|
246
|
+
"output_file": output_file,
|
|
247
|
+
"render_cmd": render_cmd,
|
|
248
|
+
"template": template_name,
|
|
249
|
+
})
|
|
250
|
+
else:
|
|
251
|
+
for shard_idx in range(1, entry.split_count + 1):
|
|
252
|
+
shard_filename = f"{entry.doc_filename.replace('.md', '')}-shard-{shard_idx:02d}.md"
|
|
253
|
+
output_file = str((module_dir / shard_filename).resolve())
|
|
254
|
+
fl_override = str(
|
|
255
|
+
(module_dir / ".meta" / "shards" / f"{dt}-shard-{shard_idx:02d}.txt").resolve()
|
|
256
|
+
)
|
|
257
|
+
render_cmd = (
|
|
258
|
+
f"py -3.12 -X utf8 -m core.prompt render"
|
|
259
|
+
f" --template \"{template_name}\""
|
|
260
|
+
f" --module {module_name}"
|
|
261
|
+
f" --config \"kb-project.yaml\""
|
|
262
|
+
f" --kb {kb_name}"
|
|
263
|
+
f" --doc-type {dt}"
|
|
264
|
+
f" --mode {mode}"
|
|
265
|
+
f" --extra file_list_override={fl_override}"
|
|
266
|
+
)
|
|
267
|
+
# Use actual shard data when available
|
|
268
|
+
if entry.shards and shard_idx <= len(entry.shards):
|
|
269
|
+
shard_info = entry.shards[shard_idx - 1]
|
|
270
|
+
shard_file_count = shard_info.file_count
|
|
271
|
+
shard_line_count = shard_info.lines
|
|
272
|
+
else:
|
|
273
|
+
shard_file_count = entry.file_count // entry.split_count
|
|
274
|
+
shard_line_count = entry.total_lines // entry.split_count
|
|
275
|
+
tasks.append({
|
|
276
|
+
"id": f"{dt}-shard-{shard_idx:02d}",
|
|
277
|
+
"batch": entry.batch_id,
|
|
278
|
+
"doc_type": dt,
|
|
279
|
+
"doc_filename": entry.doc_filename,
|
|
280
|
+
"shard": shard_idx,
|
|
281
|
+
"total_shards": entry.split_count,
|
|
282
|
+
"file_count": shard_file_count,
|
|
283
|
+
"line_count": shard_line_count,
|
|
284
|
+
"output_file": output_file,
|
|
285
|
+
"render_cmd": render_cmd,
|
|
286
|
+
"template": template_name,
|
|
287
|
+
"shard_file_list": fl_override,
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
"module": module_name,
|
|
292
|
+
"kb": kb_name,
|
|
293
|
+
"mode": mode,
|
|
294
|
+
"preset": preset_name,
|
|
295
|
+
"tasks": tasks,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ---------------------------------------------------------------------------
|
|
300
|
+
# Internal helpers
|
|
301
|
+
# ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _format_row(entry: DispatchEntry, note: str) -> str:
|
|
305
|
+
"""Format a single table row."""
|
|
306
|
+
fc = str(entry.file_count) if entry.file_count else "—"
|
|
307
|
+
lc = str(entry.total_lines) if entry.total_lines else "—"
|
|
308
|
+
bc = f"{entry.total_bytes // 1024}KB" if entry.total_bytes > 0 else "—"
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
f"| {entry.batch_id:<4} "
|
|
312
|
+
f"| {entry.doc_type:<14} "
|
|
313
|
+
f"| {entry.doc_filename:<28} "
|
|
314
|
+
f"| {fc:<9} "
|
|
315
|
+
f"| {lc:<9} "
|
|
316
|
+
f"| {bc:<9} "
|
|
317
|
+
f"| {entry.strategy:<7} "
|
|
318
|
+
f"| {note} |"
|
|
319
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Dispatch source helpers — file list resolution and source statistics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_file_list_with_stats(
|
|
11
|
+
module_dir: Path, doc_type: str, source_cache: Path, dt_mapping: dict[str, str],
|
|
12
|
+
) -> list[dict[str, Any]]:
|
|
13
|
+
"""Get file list with path, lines, and bytes info for a doc type."""
|
|
14
|
+
from core.paths import resolve_file_list
|
|
15
|
+
if doc_type not in dt_mapping:
|
|
16
|
+
return []
|
|
17
|
+
fl = resolve_file_list(module_dir, doc_type)
|
|
18
|
+
if fl is None:
|
|
19
|
+
doc_basename = dt_mapping[doc_type].replace(".md", "")
|
|
20
|
+
if doc_basename != doc_type:
|
|
21
|
+
fl = resolve_file_list(module_dir, doc_basename)
|
|
22
|
+
if fl is None:
|
|
23
|
+
return []
|
|
24
|
+
files: list[dict[str, Any]] = []
|
|
25
|
+
for line in fl.read_text(encoding="utf-8").splitlines():
|
|
26
|
+
line = line.strip()
|
|
27
|
+
if not line:
|
|
28
|
+
continue
|
|
29
|
+
rel_path = line.replace("\\", "/")
|
|
30
|
+
file_path = source_cache / rel_path
|
|
31
|
+
if not file_path.exists() and Path(line).is_absolute():
|
|
32
|
+
file_path = Path(line)
|
|
33
|
+
file_bytes, line_count = 0, 0
|
|
34
|
+
if file_path.exists():
|
|
35
|
+
try:
|
|
36
|
+
file_bytes = file_path.stat().st_size
|
|
37
|
+
except OSError:
|
|
38
|
+
pass
|
|
39
|
+
try:
|
|
40
|
+
with open(file_path, encoding="utf-8", errors="replace") as f:
|
|
41
|
+
line_count = sum(1 for _ in f)
|
|
42
|
+
except OSError:
|
|
43
|
+
pass
|
|
44
|
+
package = _extract_package_from_path(rel_path)
|
|
45
|
+
files.append({"path": str(file_path), "name": Path(rel_path).name,
|
|
46
|
+
"lines": line_count, "bytes": file_bytes, "rel_path": rel_path,
|
|
47
|
+
"package": package})
|
|
48
|
+
return files
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _extract_package_from_path(rel_path: str) -> str:
|
|
52
|
+
"""Extract package name from a relative file path.
|
|
53
|
+
|
|
54
|
+
Skips standard source directory prefixes (src/main/java/, etc.)
|
|
55
|
+
and joins remaining directory parts with '.' as package name.
|
|
56
|
+
"""
|
|
57
|
+
parts = rel_path.replace("\\", "/").split("/")
|
|
58
|
+
skip_dirs = {"src", "main", "java", "python", "go", "kotlin", "scala",
|
|
59
|
+
"groovy", "lib", "app", "pkg", "internal", "cmd", "resources", "test"}
|
|
60
|
+
|
|
61
|
+
start = 0
|
|
62
|
+
i = 0
|
|
63
|
+
while i < len(parts) - 1:
|
|
64
|
+
if parts[i].lower() in skip_dirs:
|
|
65
|
+
j = i
|
|
66
|
+
while j < len(parts) - 1 and parts[j].lower() in skip_dirs:
|
|
67
|
+
j += 1
|
|
68
|
+
start = j
|
|
69
|
+
i = j
|
|
70
|
+
else:
|
|
71
|
+
i += 1
|
|
72
|
+
segments = [p for p in parts[start:-1] if p]
|
|
73
|
+
return ".".join(segments)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def count_source_lines(source_cache: Path, hooks=None) -> tuple[int, int]:
|
|
77
|
+
"""Return (source_file_count, total_lines) for source files."""
|
|
78
|
+
if hooks:
|
|
79
|
+
result = hooks.count_source_files(source_cache)
|
|
80
|
+
if result != (0, 0):
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
total_files, total_lines = 0, 0
|
|
84
|
+
if not source_cache.is_dir():
|
|
85
|
+
return 0, 0
|
|
86
|
+
extensions = hooks.get_source_extensions() if hooks else [".java"]
|
|
87
|
+
test_patterns = hooks.get_test_path_patterns() if hooks else ["/test/"]
|
|
88
|
+
for root, _, filenames in os.walk(source_cache):
|
|
89
|
+
root_str = str(Path(root)).replace("\\", "/")
|
|
90
|
+
if any(pat in root_str for pat in test_patterns):
|
|
91
|
+
continue
|
|
92
|
+
for fn in filenames:
|
|
93
|
+
if any(fn.endswith(ext) for ext in extensions):
|
|
94
|
+
total_files += 1
|
|
95
|
+
try:
|
|
96
|
+
with open(Path(root) / fn, encoding="utf-8", errors="replace") as f:
|
|
97
|
+
total_lines += sum(1 for _ in f)
|
|
98
|
+
except OSError:
|
|
99
|
+
pass
|
|
100
|
+
return total_files, total_lines
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def skeleton_total_size_kb(module_dir: Path) -> int:
|
|
104
|
+
"""Get total skeleton size in KB."""
|
|
105
|
+
from core.paths import resolve_skeleton
|
|
106
|
+
skel = resolve_skeleton(module_dir)
|
|
107
|
+
if skel is None:
|
|
108
|
+
return 0
|
|
109
|
+
if skel.is_dir():
|
|
110
|
+
return sum(f.stat().st_size for f in skel.glob("*.json")) // 1024
|
|
111
|
+
return skel.stat().st_size // 1024
|
core/skeleton/extract.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Skeleton extraction orchestrator.
|
|
2
|
+
|
|
3
|
+
Tries parsers in priority order (from preset config) until one succeeds.
|
|
4
|
+
Handles output modes: single file, split-by-package, summary generation.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from core.skeleton.extract import extract_skeleton
|
|
8
|
+
|
|
9
|
+
entries = extract_skeleton(repo_path, preset, ref="origin/main")
|
|
10
|
+
# entries: list of skeleton dicts [{file, total_lines, methods, classes, ...}]
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from core.paths import (
|
|
21
|
+
skeleton_path, skeleton_shards_dir, skeleton_summary_path, skeleton_stats_path, ensure_dir,
|
|
22
|
+
)
|
|
23
|
+
from core.skeleton.parsers import get_parser_chain
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def extract_skeleton(
|
|
29
|
+
repo_path: Path,
|
|
30
|
+
preset: dict[str, Any],
|
|
31
|
+
*,
|
|
32
|
+
ref: str = "HEAD",
|
|
33
|
+
subpath: str | None = None,
|
|
34
|
+
output_dir: Path | None = None,
|
|
35
|
+
split_by_package: bool = False,
|
|
36
|
+
compact: bool = True,
|
|
37
|
+
files: list[str] | None = None,
|
|
38
|
+
) -> list[dict[str, Any]]:
|
|
39
|
+
"""Extract skeleton from a repository using the parser chain.
|
|
40
|
+
|
|
41
|
+
Tries each parser in preset-declared order until one succeeds.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
repo_path: Path to the git repository
|
|
45
|
+
preset: Preset configuration dict
|
|
46
|
+
ref: Git reference to parse
|
|
47
|
+
subpath: Subdirectory to scope extraction (monorepo module path)
|
|
48
|
+
output_dir: Module dir to write output (None = don't write)
|
|
49
|
+
split_by_package: Split output into per-package shard files
|
|
50
|
+
compact: Remove low-value entries (empty methods, etc.)
|
|
51
|
+
files: If provided, only extract these file paths (incremental mode)
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
List of skeleton entries
|
|
55
|
+
"""
|
|
56
|
+
chain = get_parser_chain(preset)
|
|
57
|
+
entries: list[dict[str, Any]] = []
|
|
58
|
+
used_parser = "none"
|
|
59
|
+
|
|
60
|
+
for parser in chain:
|
|
61
|
+
if not parser.can_parse(repo_path, preset):
|
|
62
|
+
logger.debug("Parser '%s' cannot parse %s, skipping", parser.name, repo_path)
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
logger.info("Using parser '%s' for %s", parser.name, repo_path.name)
|
|
66
|
+
try:
|
|
67
|
+
entries = parser.parse(repo_path, preset, ref=ref, subpath=subpath)
|
|
68
|
+
used_parser = parser.name
|
|
69
|
+
break
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.warning("Parser '%s' failed for %s: %s", parser.name, repo_path.name, e)
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
if not entries:
|
|
75
|
+
logger.warning("No parser produced results for %s", repo_path.name)
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
# Filter to specified files if in incremental mode
|
|
79
|
+
if files:
|
|
80
|
+
file_set = set(files)
|
|
81
|
+
before_count = len(entries)
|
|
82
|
+
entries = [e for e in entries if e.get("file", "") in file_set]
|
|
83
|
+
skipped = file_set - {e.get("file", "") for e in entries}
|
|
84
|
+
if skipped:
|
|
85
|
+
logger.warning("[extract] %d files not found in skeleton: %s",
|
|
86
|
+
len(skipped), list(skipped)[:5])
|
|
87
|
+
logger.info("[extract] Incremental: %d/%d entries (from %d requested files)",
|
|
88
|
+
len(entries), before_count, len(files))
|
|
89
|
+
|
|
90
|
+
if compact:
|
|
91
|
+
entries = _compact(entries)
|
|
92
|
+
|
|
93
|
+
# Write output if requested
|
|
94
|
+
if output_dir:
|
|
95
|
+
_write_output(entries, output_dir, split_by_package)
|
|
96
|
+
_write_summary(entries, output_dir)
|
|
97
|
+
_write_stats(entries, output_dir)
|
|
98
|
+
_write_parser_info(output_dir, used_parser, preset.get("preset_name", ""))
|
|
99
|
+
|
|
100
|
+
logger.info("Extracted %d entries (%d methods) using '%s'",
|
|
101
|
+
len(entries), sum(len(e.get("methods", [])) for e in entries), used_parser)
|
|
102
|
+
return entries
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _compact(entries: list[dict]) -> list[dict]:
|
|
106
|
+
"""Remove entries with no methods and no classes (low value)."""
|
|
107
|
+
return [e for e in entries if e.get("methods") or e.get("classes")]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _write_output(entries: list[dict], module_dir: Path, split_by_package: bool) -> None:
|
|
111
|
+
"""Write skeleton entries to disk."""
|
|
112
|
+
if split_by_package and len(entries) > 50:
|
|
113
|
+
_write_shards(entries, module_dir)
|
|
114
|
+
else:
|
|
115
|
+
_write_single(entries, module_dir)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _write_single(entries: list[dict], module_dir: Path) -> None:
|
|
119
|
+
"""Write all entries to a single skeleton.json file."""
|
|
120
|
+
out = skeleton_path(module_dir)
|
|
121
|
+
ensure_dir(out.parent)
|
|
122
|
+
out.write_text(json.dumps(entries, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
123
|
+
logger.debug("Wrote skeleton: %s (%d entries)", out, len(entries))
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _write_shards(entries: list[dict], module_dir: Path) -> None:
|
|
127
|
+
"""Split entries by package prefix and write per-package shard files."""
|
|
128
|
+
shards_dir = skeleton_shards_dir(module_dir)
|
|
129
|
+
ensure_dir(shards_dir)
|
|
130
|
+
|
|
131
|
+
# Group by top-level package
|
|
132
|
+
groups: dict[str, list[dict]] = {}
|
|
133
|
+
for entry in entries:
|
|
134
|
+
filepath = entry.get("file", "")
|
|
135
|
+
pkg = _extract_package(filepath)
|
|
136
|
+
groups.setdefault(pkg, []).append(entry)
|
|
137
|
+
|
|
138
|
+
for pkg_name, pkg_entries in groups.items():
|
|
139
|
+
safe_name = pkg_name.replace(".", "_").replace("/", "_") or "root"
|
|
140
|
+
shard_file = shards_dir / f"{safe_name}.json"
|
|
141
|
+
shard_file.write_text(
|
|
142
|
+
json.dumps(pkg_entries, ensure_ascii=False, indent=1), encoding="utf-8"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
logger.debug("Wrote %d skeleton shards to %s", len(groups), shards_dir)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _write_summary(entries: list[dict], module_dir: Path) -> None:
|
|
149
|
+
"""Write a compact summary (file + method count + high methods) for large skeletons."""
|
|
150
|
+
summary = []
|
|
151
|
+
for entry in entries:
|
|
152
|
+
methods = entry.get("methods", [])
|
|
153
|
+
high_methods = [m for m in methods if m.get("complexity") == "high"]
|
|
154
|
+
summary.append({
|
|
155
|
+
"file": entry.get("file", ""),
|
|
156
|
+
"total_lines": entry.get("total_lines", 0),
|
|
157
|
+
"method_count": len(methods),
|
|
158
|
+
"high_complexity_methods": [
|
|
159
|
+
{"name": m["name"], "line_count": m.get("line_count", 0)}
|
|
160
|
+
for m in high_methods
|
|
161
|
+
],
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
out = skeleton_summary_path(module_dir)
|
|
165
|
+
ensure_dir(out.parent)
|
|
166
|
+
out.write_text(json.dumps(summary, ensure_ascii=False, indent=1), encoding="utf-8")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _write_stats(entries: list[dict], module_dir: Path) -> None:
|
|
170
|
+
"""Write global statistics for downstream consumers."""
|
|
171
|
+
total_methods = sum(len(e.get("methods", [])) for e in entries)
|
|
172
|
+
total_classes = sum(len(e.get("classes", [])) for e in entries)
|
|
173
|
+
total_fields = sum(len(e.get("fields", [])) for e in entries)
|
|
174
|
+
total_lines = sum(e.get("total_lines", 0) for e in entries)
|
|
175
|
+
high_complexity = sum(
|
|
176
|
+
1 for e in entries
|
|
177
|
+
for m in e.get("methods", [])
|
|
178
|
+
if m.get("complexity") == "high"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
stats = {
|
|
182
|
+
"files": len(entries),
|
|
183
|
+
"methods": total_methods,
|
|
184
|
+
"classes": total_classes,
|
|
185
|
+
"fields": total_fields,
|
|
186
|
+
"total_lines": total_lines,
|
|
187
|
+
"high_complexity_methods": high_complexity,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
out = skeleton_stats_path(module_dir)
|
|
191
|
+
ensure_dir(out.parent)
|
|
192
|
+
out.write_text(json.dumps(stats, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _write_parser_info(module_dir: Path, parser_name: str, preset_name: str) -> None:
|
|
196
|
+
"""Write parser metadata for audit trail."""
|
|
197
|
+
from core.paths import meta_dir
|
|
198
|
+
info_path = meta_dir(module_dir) / "parser-info.json"
|
|
199
|
+
ensure_dir(info_path.parent)
|
|
200
|
+
info_path.write_text(
|
|
201
|
+
json.dumps({"parser": parser_name, "preset": preset_name}, ensure_ascii=False, indent=2),
|
|
202
|
+
encoding="utf-8",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _extract_package(filepath: str, depth: int = 2) -> str:
|
|
207
|
+
"""Extract top-level package from file path for sharding."""
|
|
208
|
+
parts = filepath.replace("\\", "/").split("/")
|
|
209
|
+
# Skip standard source prefixes
|
|
210
|
+
skip = {"src", "main", "java", "python", "go", "kotlin", "lib", "app", "pkg"}
|
|
211
|
+
start = 0
|
|
212
|
+
for i, p in enumerate(parts[:-1]):
|
|
213
|
+
if p.lower() in skip:
|
|
214
|
+
start = i + 1
|
|
215
|
+
else:
|
|
216
|
+
break
|
|
217
|
+
remaining = parts[start:-1]
|
|
218
|
+
return ".".join(remaining[:depth]) if remaining else "root"
|