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,248 @@
|
|
|
1
|
+
"""Document-skeleton structural comparison tool.
|
|
2
|
+
|
|
3
|
+
Compares document content against skeleton entries to identify
|
|
4
|
+
inconsistencies (missing or stale items).
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
python -m core.skeleton diff-doc --doc-path business-logic.md --skeleton-path .skeleton.json
|
|
8
|
+
|
|
9
|
+
from core.skeleton.diff_doc import diff_doc
|
|
10
|
+
result = diff_doc(doc_path, skeleton_path, doc_type="business-logic")
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import re
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Literal
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Data classes
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class DiffFinding:
|
|
32
|
+
"""Single diff finding."""
|
|
33
|
+
|
|
34
|
+
item: str # class/method/field name
|
|
35
|
+
category: Literal["missing", "stale"]
|
|
36
|
+
doc_location: str = "" # section heading where found/expected
|
|
37
|
+
skeleton_location: str = "" # file path in skeleton
|
|
38
|
+
detail: str = ""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class DiffResult:
|
|
43
|
+
"""Complete diff comparison result."""
|
|
44
|
+
|
|
45
|
+
doc_type: str
|
|
46
|
+
findings: list[DiffFinding] = field(default_factory=list)
|
|
47
|
+
doc_items_count: int = 0
|
|
48
|
+
skeleton_items_count: int = 0
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def missing_count(self) -> int:
|
|
52
|
+
return sum(1 for f in self.findings if f.category == "missing")
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def stale_count(self) -> int:
|
|
56
|
+
return sum(1 for f in self.findings if f.category == "stale")
|
|
57
|
+
|
|
58
|
+
def to_json(self) -> list[dict]:
|
|
59
|
+
"""Convert to audit-compatible JSON format."""
|
|
60
|
+
return [
|
|
61
|
+
{
|
|
62
|
+
"dimension": f"diff_{f.category}",
|
|
63
|
+
"status": "fail",
|
|
64
|
+
"detail": f"{f.category}: {f.item} — {f.detail}",
|
|
65
|
+
"fix": "",
|
|
66
|
+
}
|
|
67
|
+
for f in self.findings
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Public API
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def diff_doc(
|
|
77
|
+
doc_path: Path,
|
|
78
|
+
skeleton_path: Path,
|
|
79
|
+
*,
|
|
80
|
+
doc_type: str | None = None,
|
|
81
|
+
) -> DiffResult:
|
|
82
|
+
"""Compare document against skeleton.
|
|
83
|
+
|
|
84
|
+
Comparison rules by doc_type:
|
|
85
|
+
- data-models: field-level (entity class names)
|
|
86
|
+
- api-contracts: endpoint-level (controller method names + URLs)
|
|
87
|
+
- business-logic: method-level (service method names)
|
|
88
|
+
- default: class-level (class names from skeleton files)
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
doc_path: Path to the markdown document
|
|
92
|
+
skeleton_path: Path to skeleton JSON file or directory
|
|
93
|
+
doc_type: Document type for type-specific comparison rules
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
DiffResult with findings in audit-compatible format
|
|
97
|
+
"""
|
|
98
|
+
result = DiffResult(doc_type=doc_type or "unknown")
|
|
99
|
+
|
|
100
|
+
# Load skeleton entries
|
|
101
|
+
skeleton_entries = _load_skeleton(skeleton_path)
|
|
102
|
+
if not skeleton_entries:
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
# Load document content
|
|
106
|
+
try:
|
|
107
|
+
doc_content = doc_path.read_text(encoding="utf-8", errors="replace")
|
|
108
|
+
except OSError:
|
|
109
|
+
return result
|
|
110
|
+
|
|
111
|
+
# Extract items from skeleton based on doc_type
|
|
112
|
+
skeleton_items = _extract_skeleton_items(skeleton_entries, doc_type)
|
|
113
|
+
result.skeleton_items_count = len(skeleton_items)
|
|
114
|
+
|
|
115
|
+
# Extract items mentioned in document
|
|
116
|
+
doc_items = _extract_doc_items(doc_content, doc_type)
|
|
117
|
+
result.doc_items_count = len(doc_items)
|
|
118
|
+
|
|
119
|
+
# Compare: skeleton items not in doc → "missing"
|
|
120
|
+
doc_items_lower = {item.lower() for item in doc_items}
|
|
121
|
+
for item, file_path in skeleton_items.items():
|
|
122
|
+
if item.lower() not in doc_items_lower:
|
|
123
|
+
result.findings.append(DiffFinding(
|
|
124
|
+
item=item,
|
|
125
|
+
category="missing",
|
|
126
|
+
skeleton_location=file_path,
|
|
127
|
+
detail=f"In skeleton ({file_path}) but not found in document",
|
|
128
|
+
))
|
|
129
|
+
|
|
130
|
+
# Compare: doc items not in skeleton → "stale"
|
|
131
|
+
skeleton_items_lower = {item.lower() for item in skeleton_items}
|
|
132
|
+
for item in doc_items:
|
|
133
|
+
if item.lower() not in skeleton_items_lower:
|
|
134
|
+
result.findings.append(DiffFinding(
|
|
135
|
+
item=item,
|
|
136
|
+
category="stale",
|
|
137
|
+
doc_location="document",
|
|
138
|
+
detail=f"In document but not found in skeleton (possibly deleted/renamed)",
|
|
139
|
+
))
|
|
140
|
+
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ---------------------------------------------------------------------------
|
|
145
|
+
# Internal helpers
|
|
146
|
+
# ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _load_skeleton(skeleton_path: Path) -> list[dict]:
|
|
150
|
+
"""Load skeleton entries from file or directory."""
|
|
151
|
+
if skeleton_path.is_dir():
|
|
152
|
+
entries = []
|
|
153
|
+
for f in sorted(skeleton_path.glob("*.json")):
|
|
154
|
+
try:
|
|
155
|
+
data = json.loads(f.read_text(encoding="utf-8"))
|
|
156
|
+
if isinstance(data, list):
|
|
157
|
+
entries.extend(data)
|
|
158
|
+
except (json.JSONDecodeError, OSError):
|
|
159
|
+
continue
|
|
160
|
+
return entries
|
|
161
|
+
elif skeleton_path.exists():
|
|
162
|
+
try:
|
|
163
|
+
data = json.loads(skeleton_path.read_text(encoding="utf-8"))
|
|
164
|
+
if isinstance(data, list):
|
|
165
|
+
return data
|
|
166
|
+
return data.get("data", data.get("files", []))
|
|
167
|
+
except (json.JSONDecodeError, OSError):
|
|
168
|
+
return []
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _extract_skeleton_items(entries: list[dict], doc_type: str | None) -> dict[str, str]:
|
|
173
|
+
"""Extract relevant items from skeleton based on doc_type.
|
|
174
|
+
|
|
175
|
+
Returns: {item_name: file_path}
|
|
176
|
+
"""
|
|
177
|
+
items: dict[str, str] = {}
|
|
178
|
+
|
|
179
|
+
for entry in entries:
|
|
180
|
+
file_path = entry.get("file", "")
|
|
181
|
+
classes = entry.get("classes", [])
|
|
182
|
+
methods = entry.get("methods", [])
|
|
183
|
+
|
|
184
|
+
if doc_type in ("data-models",):
|
|
185
|
+
# Extract class names (entities, DTOs)
|
|
186
|
+
for cls in classes:
|
|
187
|
+
name = cls.get("name", "")
|
|
188
|
+
if name:
|
|
189
|
+
items[name] = file_path
|
|
190
|
+
elif doc_type in ("api-contracts",):
|
|
191
|
+
# Extract method names from controllers
|
|
192
|
+
for m in methods:
|
|
193
|
+
name = m.get("name", "")
|
|
194
|
+
annotations = m.get("annotations", [])
|
|
195
|
+
# Look for mapping annotations
|
|
196
|
+
for a in annotations:
|
|
197
|
+
a_name = a if isinstance(a, str) else a.get("name", "")
|
|
198
|
+
if "Mapping" in a_name:
|
|
199
|
+
items[name] = file_path
|
|
200
|
+
break
|
|
201
|
+
elif doc_type in ("business-logic",):
|
|
202
|
+
# Extract service method names (high complexity preferred)
|
|
203
|
+
for m in methods:
|
|
204
|
+
name = m.get("name", "")
|
|
205
|
+
complexity = m.get("complexity", "")
|
|
206
|
+
if complexity == "high" and name:
|
|
207
|
+
items[name] = file_path
|
|
208
|
+
else:
|
|
209
|
+
# Default: class names
|
|
210
|
+
for cls in classes:
|
|
211
|
+
name = cls.get("name", "")
|
|
212
|
+
if name:
|
|
213
|
+
items[name] = file_path
|
|
214
|
+
|
|
215
|
+
return items
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _extract_doc_items(content: str, doc_type: str | None) -> list[str]:
|
|
219
|
+
"""Extract item names mentioned in the document."""
|
|
220
|
+
items: list[str] = []
|
|
221
|
+
|
|
222
|
+
# Extract from headings (## and ###)
|
|
223
|
+
for line in content.splitlines():
|
|
224
|
+
if line.startswith("## ") or line.startswith("### "):
|
|
225
|
+
heading_text = line.lstrip("#").strip()
|
|
226
|
+
# Extract class/method names from headings
|
|
227
|
+
# Common patterns: "## ClassName", "## ClassName (description)", "### methodName"
|
|
228
|
+
# Extract the first word/identifier
|
|
229
|
+
match = re.match(r'[`]?(\w+)[`]?', heading_text)
|
|
230
|
+
if match:
|
|
231
|
+
items.append(match.group(1))
|
|
232
|
+
|
|
233
|
+
# Extract from code blocks and inline code
|
|
234
|
+
for match in re.finditer(r'`(\w+(?:\.\w+)?)`', content):
|
|
235
|
+
word = match.group(1)
|
|
236
|
+
# Filter out common non-identifiers
|
|
237
|
+
if len(word) > 2 and word[0].isupper():
|
|
238
|
+
items.append(word)
|
|
239
|
+
|
|
240
|
+
# Deduplicate while preserving order
|
|
241
|
+
seen: set[str] = set()
|
|
242
|
+
unique: list[str] = []
|
|
243
|
+
for item in items:
|
|
244
|
+
if item not in seen:
|
|
245
|
+
seen.add(item)
|
|
246
|
+
unique.append(item)
|
|
247
|
+
|
|
248
|
+
return unique
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Dispatch plan computation — module->doc_type mapping, priority, split triggering.
|
|
2
|
+
|
|
3
|
+
Computes the full dispatch plan for document generation: which doc_types to generate,
|
|
4
|
+
how many shards each needs, and in what batch order.
|
|
5
|
+
|
|
6
|
+
No CLI I/O, no LLM calls. Pure computation from config + file system state.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from core.skeleton.dispatch import compute_dispatch_plan, DispatchPlan
|
|
10
|
+
|
|
11
|
+
plan = compute_dispatch_plan(
|
|
12
|
+
preset=preset, module_dir=module_dir, source_cache=source_cache,
|
|
13
|
+
mode="readwrite", module_type="service",
|
|
14
|
+
)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from core.skeleton.dispatch_source import (
|
|
26
|
+
get_file_list_with_stats, count_source_lines, skeleton_total_size_kb,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ShardPlan:
|
|
34
|
+
"""Plan for a single shard within a dispatch entry."""
|
|
35
|
+
name: str
|
|
36
|
+
files: list[str] = field(default_factory=list)
|
|
37
|
+
lines: int = 0
|
|
38
|
+
bytes: int = 0
|
|
39
|
+
file_count: int = 0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class DispatchEntry:
|
|
44
|
+
"""Single module dispatch plan entry (one doc_type or shard)."""
|
|
45
|
+
module_name: str = ""
|
|
46
|
+
doc_type: str = ""
|
|
47
|
+
doc_filename: str = ""
|
|
48
|
+
batch_id: str = ""
|
|
49
|
+
file_count: int = 0
|
|
50
|
+
total_lines: int = 0
|
|
51
|
+
total_bytes: int = 0
|
|
52
|
+
split_count: int = 1
|
|
53
|
+
priority: int = 0
|
|
54
|
+
shards: list[ShardPlan] = field(default_factory=list)
|
|
55
|
+
strategy: str = "single-agent"
|
|
56
|
+
note: str = ""
|
|
57
|
+
pending_grouping: str | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class DispatchPlan:
|
|
62
|
+
"""Complete dispatch plan for a module."""
|
|
63
|
+
module_name: str = ""
|
|
64
|
+
entries: list[DispatchEntry] = field(default_factory=list)
|
|
65
|
+
batch_order: list[tuple[str, list[str]]] = field(default_factory=list)
|
|
66
|
+
source_file_count: int = 0
|
|
67
|
+
source_total_lines: int = 0
|
|
68
|
+
skeleton_size_kb: int = 0
|
|
69
|
+
|
|
70
|
+
def total_shards(self) -> int:
|
|
71
|
+
return sum(max(1, e.split_count) for e in self.entries)
|
|
72
|
+
|
|
73
|
+
def total_entries(self) -> int:
|
|
74
|
+
return len(self.entries)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Batch order inference
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
_DEFAULT_BATCH_ORDER: list[tuple[str, list[str]]] = [
|
|
82
|
+
("1", ["source-tree-analysis"]),
|
|
83
|
+
("2A", ["data-models", "enums-and-constants"]),
|
|
84
|
+
("2B", ["api-contracts", "architecture", "caching", "messaging"]),
|
|
85
|
+
("3", ["business-logic", "utils"]),
|
|
86
|
+
("4", ["index"]),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
_LEVELS: dict[int, list[str]] = {
|
|
90
|
+
1: ["source-tree", "structure", "tree", "enums", "constants", "enum"],
|
|
91
|
+
2: ["data-models", "data-model", "entities", "entity", "models", "schema",
|
|
92
|
+
"database-access", "database", "dao", "mapper"],
|
|
93
|
+
3: ["api", "contract", "architecture", "config", "redis", "kafka", "mq", "cache",
|
|
94
|
+
"middleware", "scheduled", "error-handling", "security", "aop", "interceptor",
|
|
95
|
+
"async", "event", "external"],
|
|
96
|
+
4: ["business", "logic", "service", "workflow"],
|
|
97
|
+
5: ["utils", "util", "helper", "tool"],
|
|
98
|
+
6: ["index", "summary", "overview"],
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def infer_batch_order(preset: dict[str, Any]) -> list[tuple[str, list[str]]]:
|
|
103
|
+
"""Infer batch order from preset doc_types (language-agnostic)."""
|
|
104
|
+
from core.preset import get_doc_type_mapping
|
|
105
|
+
doc_mapping = get_doc_type_mapping(preset)
|
|
106
|
+
if not doc_mapping:
|
|
107
|
+
return list(_DEFAULT_BATCH_ORDER)
|
|
108
|
+
|
|
109
|
+
def _get_level(doc_type: str) -> int:
|
|
110
|
+
dt_lower = doc_type.lower()
|
|
111
|
+
for level, keywords in _LEVELS.items():
|
|
112
|
+
for kw in keywords:
|
|
113
|
+
if kw in dt_lower:
|
|
114
|
+
return level
|
|
115
|
+
return 4
|
|
116
|
+
|
|
117
|
+
level_groups: dict[int, list[str]] = {}
|
|
118
|
+
for dt in doc_mapping:
|
|
119
|
+
level_groups.setdefault(_get_level(dt), []).append(dt)
|
|
120
|
+
|
|
121
|
+
result = [(str(level), dts) for level, dts in sorted(level_groups.items())]
|
|
122
|
+
return result if result else list(_DEFAULT_BATCH_ORDER)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# Optional doc detection
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
_OPTIONAL_DOCS: frozenset[str] = frozenset({
|
|
130
|
+
"caching", "messaging", "external-integrations", "scheduled-tasks",
|
|
131
|
+
"error-handling", "security", "async-and-events", "aop-and-interceptors",
|
|
132
|
+
"database-access", "observability",
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def detect_optional_doc(source_cache: Path, preset: dict[str, Any], doc_filename: str) -> bool:
|
|
137
|
+
"""Scan source to detect if an optional doc type should be triggered."""
|
|
138
|
+
if not source_cache.is_dir():
|
|
139
|
+
return False
|
|
140
|
+
classification = preset.get("file_classification", {})
|
|
141
|
+
trigger_categories: set[str] = set()
|
|
142
|
+
for cat_name, cat_config in classification.items():
|
|
143
|
+
if isinstance(cat_config, dict) and doc_filename in cat_config.get("affects", []):
|
|
144
|
+
trigger_categories.add(cat_name)
|
|
145
|
+
if not trigger_categories:
|
|
146
|
+
return False
|
|
147
|
+
from core.preset_classify import classify_file
|
|
148
|
+
for root, _, filenames in os.walk(source_cache):
|
|
149
|
+
root_path = Path(root)
|
|
150
|
+
if "/test/" in str(root_path).replace("\\", "/"):
|
|
151
|
+
continue
|
|
152
|
+
for fn in filenames:
|
|
153
|
+
if not fn.endswith(".java"):
|
|
154
|
+
continue
|
|
155
|
+
rel = str((root_path / fn).relative_to(source_cache)).replace("\\", "/")
|
|
156
|
+
if trigger_categories & set(classify_file(preset, rel)):
|
|
157
|
+
return True
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
# Compute dispatch plan
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
_STRATEGY_LABELS = {
|
|
166
|
+
"community": "community",
|
|
167
|
+
"llm": "llm",
|
|
168
|
+
"package": "package",
|
|
169
|
+
"simple": "simple",
|
|
170
|
+
"cache": "cache",
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def compute_dispatch_plan(
|
|
175
|
+
preset: dict[str, Any], module_dir: Path, source_cache: Path,
|
|
176
|
+
mode: str = "readwrite", module_name: str = "", module_type: str = "service",
|
|
177
|
+
) -> DispatchPlan:
|
|
178
|
+
"""Compute the full dispatch plan for a module."""
|
|
179
|
+
from core.skeleton.split import SplitConfig, compute_splits
|
|
180
|
+
from core.skeleton.split_plan import plan_splits
|
|
181
|
+
from core.skeleton.loader import load_skeleton_entries
|
|
182
|
+
from core.preset import get_doc_type_mapping, get_doc_filename
|
|
183
|
+
|
|
184
|
+
dt_mapping = get_doc_type_mapping(preset)
|
|
185
|
+
batch_order = infer_batch_order(preset)
|
|
186
|
+
split_config = SplitConfig.from_preset(preset, mode=mode)
|
|
187
|
+
|
|
188
|
+
module_types_config = preset.get("module_types", {})
|
|
189
|
+
type_config = module_types_config.get(module_type, {})
|
|
190
|
+
skip_docs = set(type_config.get("skip", []))
|
|
191
|
+
skip_all = "*" in skip_docs
|
|
192
|
+
|
|
193
|
+
skel_size_kb = skeleton_total_size_kb(module_dir)
|
|
194
|
+
src_files, src_lines = count_source_lines(source_cache)
|
|
195
|
+
entries: list[DispatchEntry] = []
|
|
196
|
+
|
|
197
|
+
for batch_id, doc_types in batch_order:
|
|
198
|
+
for dt in doc_types:
|
|
199
|
+
if dt not in dt_mapping:
|
|
200
|
+
continue
|
|
201
|
+
doc_filename = dt_mapping[dt]
|
|
202
|
+
if skip_all or doc_filename in skip_docs:
|
|
203
|
+
continue
|
|
204
|
+
files = get_file_list_with_stats(module_dir, dt, source_cache, dt_mapping)
|
|
205
|
+
file_count = len(files)
|
|
206
|
+
if dt in _OPTIONAL_DOCS and file_count == 0:
|
|
207
|
+
doc_fn = get_doc_filename(preset, dt, strict=False)
|
|
208
|
+
if not detect_optional_doc(source_cache, preset, doc_fn):
|
|
209
|
+
continue
|
|
210
|
+
total_lines_dt = sum(f["lines"] for f in files)
|
|
211
|
+
total_bytes_dt = sum(f.get("bytes", 0) for f in files)
|
|
212
|
+
|
|
213
|
+
is_global_view = dt in split_config.skip_file_count_doc_types
|
|
214
|
+
if is_global_view:
|
|
215
|
+
total_bytes_dt = skel_size_kb * 1024
|
|
216
|
+
total_lines_dt = 0
|
|
217
|
+
|
|
218
|
+
n_splits = compute_splits(
|
|
219
|
+
split_config, total_bytes=total_bytes_dt, total_lines=total_lines_dt,
|
|
220
|
+
file_count=file_count, doc_type=dt,
|
|
221
|
+
)
|
|
222
|
+
if is_global_view:
|
|
223
|
+
n_splits = 1
|
|
224
|
+
|
|
225
|
+
shard_plans: list[ShardPlan] = []
|
|
226
|
+
pending_grouping: str | None = None
|
|
227
|
+
split_strategy_name: str = ""
|
|
228
|
+
if n_splits > 1 and files:
|
|
229
|
+
try:
|
|
230
|
+
skel_entries, _ = load_skeleton_entries(module_dir)
|
|
231
|
+
split_result = plan_splits(
|
|
232
|
+
entries=skel_entries, file_list=files,
|
|
233
|
+
split_config=split_config, doc_type=dt,
|
|
234
|
+
module_dir=module_dir,
|
|
235
|
+
)
|
|
236
|
+
split_strategy_name = split_result.strategy
|
|
237
|
+
if split_result.pending_agent_grouping:
|
|
238
|
+
pending_grouping = split_result.grouping_request_path
|
|
239
|
+
else:
|
|
240
|
+
for s in split_result.splits:
|
|
241
|
+
shard_plans.append(ShardPlan(
|
|
242
|
+
name=s.get("name", ""),
|
|
243
|
+
files=s.get("files", []),
|
|
244
|
+
lines=s.get("lines", 0),
|
|
245
|
+
bytes=s.get("bytes", 0),
|
|
246
|
+
file_count=s.get("file_count", 0),
|
|
247
|
+
))
|
|
248
|
+
if shard_plans:
|
|
249
|
+
n_splits = len(shard_plans)
|
|
250
|
+
except Exception as e:
|
|
251
|
+
logger.warning("plan_splits failed for %s/%s: %s", module_name, dt, e)
|
|
252
|
+
|
|
253
|
+
if n_splits <= 1:
|
|
254
|
+
strategy_display = "single-agent"
|
|
255
|
+
else:
|
|
256
|
+
label = _STRATEGY_LABELS.get(split_strategy_name, split_strategy_name)
|
|
257
|
+
strategy_display = f"{label} x{n_splits}"
|
|
258
|
+
|
|
259
|
+
entries.append(DispatchEntry(
|
|
260
|
+
module_name=module_name, doc_type=dt, doc_filename=doc_filename,
|
|
261
|
+
batch_id=batch_id, file_count=file_count, total_lines=total_lines_dt,
|
|
262
|
+
total_bytes=total_bytes_dt, split_count=n_splits,
|
|
263
|
+
shards=shard_plans,
|
|
264
|
+
strategy=strategy_display,
|
|
265
|
+
note="global_view: skeleton summary only" if is_global_view else "",
|
|
266
|
+
pending_grouping=pending_grouping,
|
|
267
|
+
))
|
|
268
|
+
|
|
269
|
+
return DispatchPlan(
|
|
270
|
+
module_name=module_name, entries=entries, batch_order=batch_order,
|
|
271
|
+
source_file_count=src_files, source_total_lines=src_lines,
|
|
272
|
+
skeleton_size_kb=skel_size_kb,
|
|
273
|
+
)
|