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
core/preset_classify.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""File classification logic — rule-based file→category mapping.
|
|
2
|
+
|
|
3
|
+
Extracted from core/preset.py to keep file sizes within 300-line limit.
|
|
4
|
+
All classification rules come from preset.yaml config.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
from core.preset_classify import classify_file
|
|
8
|
+
categories = classify_file(preset, "com/example/OrderService.java", skeleton_entry)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import fnmatch
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def classify_file(preset: dict, file_path: str, skeleton_entry: dict | None = None) -> list[str]:
|
|
18
|
+
"""Classify a single file into categories using preset rules.
|
|
19
|
+
|
|
20
|
+
Match priority: class_type > class_annotations > method_annotations > field_types > patterns.
|
|
21
|
+
Exclusion: exclude_if conditions checked after match.
|
|
22
|
+
"""
|
|
23
|
+
classification = preset.get("file_classification", {})
|
|
24
|
+
matched: list[str] = []
|
|
25
|
+
|
|
26
|
+
classes = (skeleton_entry or {}).get("classes", [])
|
|
27
|
+
methods = (skeleton_entry or {}).get("methods", [])
|
|
28
|
+
fields = (skeleton_entry or {}).get("fields", [])
|
|
29
|
+
|
|
30
|
+
class_types = {c.get("type", "").lower() for c in classes}
|
|
31
|
+
class_annos = _collect_annotations(classes)
|
|
32
|
+
method_annos = _collect_method_annotations(methods)
|
|
33
|
+
field_type_set = {f.get("type", "") for f in fields}
|
|
34
|
+
|
|
35
|
+
for cat_name, cfg in classification.items():
|
|
36
|
+
if not isinstance(cfg, dict):
|
|
37
|
+
continue
|
|
38
|
+
hit = _check_match(cfg, file_path, class_types, class_annos, method_annos, field_type_set)
|
|
39
|
+
if not hit:
|
|
40
|
+
continue
|
|
41
|
+
if _should_exclude(cfg, file_path, class_annos, method_annos):
|
|
42
|
+
continue
|
|
43
|
+
matched.append(cat_name)
|
|
44
|
+
|
|
45
|
+
matched = _apply_exclusions(classification, matched)
|
|
46
|
+
return matched
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _check_match(cfg: dict, file_path: str, class_types: set, class_annos: set,
|
|
50
|
+
method_annos: set, field_types: set) -> bool:
|
|
51
|
+
"""Check if a file matches a category's rules."""
|
|
52
|
+
if cfg.get("class_type") and cfg["class_type"].lower() in class_types:
|
|
53
|
+
return True
|
|
54
|
+
ca = cfg.get("class_annotations", [])
|
|
55
|
+
if ca and any(a in class_annos for a in ca):
|
|
56
|
+
return True
|
|
57
|
+
ma = cfg.get("method_annotations", [])
|
|
58
|
+
if ma and any(a in method_annos for a in ma):
|
|
59
|
+
return True
|
|
60
|
+
ft = cfg.get("field_types", [])
|
|
61
|
+
if ft and any(kw in t for kw in ft for t in field_types):
|
|
62
|
+
return True
|
|
63
|
+
patterns = cfg.get("patterns", [])
|
|
64
|
+
if patterns and _match_patterns(file_path, patterns):
|
|
65
|
+
return True
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _should_exclude(cfg: dict, file_path: str, class_annos: set, method_annos: set) -> bool:
|
|
70
|
+
"""Check exclude_if conditions."""
|
|
71
|
+
exclude = cfg.get("exclude_if")
|
|
72
|
+
if not exclude:
|
|
73
|
+
return False
|
|
74
|
+
path_patterns = exclude.get("path_contains", [])
|
|
75
|
+
if path_patterns and any(p.lower() in file_path.lower() for p in path_patterns):
|
|
76
|
+
return True
|
|
77
|
+
anno_list = exclude.get("has_annotation", [])
|
|
78
|
+
if anno_list and any(a in class_annos for a in anno_list):
|
|
79
|
+
return True
|
|
80
|
+
method_anno_list = exclude.get("has_method_annotation", [])
|
|
81
|
+
if method_anno_list and any(a in method_annos for a in method_anno_list):
|
|
82
|
+
return True
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _apply_exclusions(classification: dict, matched: list[str]) -> list[str]:
|
|
87
|
+
"""Apply matched_by exclusions and priority conflict resolution."""
|
|
88
|
+
if len(matched) <= 1:
|
|
89
|
+
return matched
|
|
90
|
+
matched_set = set(matched)
|
|
91
|
+
to_remove: set[str] = set()
|
|
92
|
+
for cat in matched:
|
|
93
|
+
cfg = classification.get(cat, {})
|
|
94
|
+
matched_by = cfg.get("exclude_if", {}).get("matched_by", [])
|
|
95
|
+
if matched_by and any(m in matched_set for m in matched_by):
|
|
96
|
+
to_remove.add(cat)
|
|
97
|
+
return [c for c in matched if c not in to_remove]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _match_patterns(file_path: str, patterns: list[str]) -> bool:
|
|
101
|
+
"""Check if file path matches any pattern (glob or substring)."""
|
|
102
|
+
file_lower = file_path.lower()
|
|
103
|
+
for p in patterns:
|
|
104
|
+
pl = p.lower()
|
|
105
|
+
if pl.startswith("*"):
|
|
106
|
+
if fnmatch.fnmatch(os.path.basename(file_lower), pl):
|
|
107
|
+
return True
|
|
108
|
+
elif pl in file_lower:
|
|
109
|
+
return True
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _collect_annotations(classes: list[dict]) -> set[str]:
|
|
114
|
+
annos: set[str] = set()
|
|
115
|
+
for c in classes:
|
|
116
|
+
for a in c.get("annotations", []):
|
|
117
|
+
if isinstance(a, dict):
|
|
118
|
+
annos.add(a.get("name", ""))
|
|
119
|
+
else:
|
|
120
|
+
annos.add(a.split("(")[0])
|
|
121
|
+
return annos
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _collect_method_annotations(methods: list[dict]) -> set[str]:
|
|
125
|
+
annos: set[str] = set()
|
|
126
|
+
for m in methods:
|
|
127
|
+
for a in m.get("annotations", []):
|
|
128
|
+
if isinstance(a, dict):
|
|
129
|
+
annos.add(a.get("name", ""))
|
|
130
|
+
else:
|
|
131
|
+
annos.add(a.split("(")[0])
|
|
132
|
+
return annos
|
core/preset_hooks.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Preset hooks — pluggable language/framework-specific logic.
|
|
2
|
+
|
|
3
|
+
Provides a base PresetHooks class with no-op defaults. Each preset can optionally
|
|
4
|
+
provide a hooks.py that subclasses PresetHooks and overrides methods for
|
|
5
|
+
language-specific behavior.
|
|
6
|
+
|
|
7
|
+
Core modules call hooks instead of hardcoding Java/Spring assumptions.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
from core.preset_hooks import load_hooks
|
|
11
|
+
hooks = load_hooks("java-spring")
|
|
12
|
+
framework_types = hooks.get_framework_types()
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import importlib.util
|
|
18
|
+
import logging
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PresetHooks:
|
|
26
|
+
"""Base hooks class — all methods return empty/default values.
|
|
27
|
+
|
|
28
|
+
Presets override only the methods they need. Core code calls these
|
|
29
|
+
methods instead of hardcoding language-specific logic.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def get_business_suffixes(self) -> tuple[str, ...]:
|
|
33
|
+
"""Class name suffixes that indicate business logic classes."""
|
|
34
|
+
return ()
|
|
35
|
+
|
|
36
|
+
def get_infra_suffixes(self) -> tuple[str, ...]:
|
|
37
|
+
"""Class name suffixes that indicate infrastructure classes."""
|
|
38
|
+
return ()
|
|
39
|
+
|
|
40
|
+
def get_data_suffixes(self) -> tuple[str, ...]:
|
|
41
|
+
"""Class name suffixes that indicate data/model classes."""
|
|
42
|
+
return ()
|
|
43
|
+
|
|
44
|
+
def get_framework_types(self) -> set[str]:
|
|
45
|
+
"""Types to exclude from dependency analysis (framework internals)."""
|
|
46
|
+
return set()
|
|
47
|
+
|
|
48
|
+
def get_common_types(self) -> frozenset[str]:
|
|
49
|
+
"""Common language types to exclude from import analysis."""
|
|
50
|
+
return frozenset()
|
|
51
|
+
|
|
52
|
+
def get_noise_terms(self) -> set[str]:
|
|
53
|
+
"""Terms to filter from keyword extraction."""
|
|
54
|
+
return set()
|
|
55
|
+
|
|
56
|
+
def get_inject_annotations(self) -> set[str]:
|
|
57
|
+
"""DI annotations for dependency extraction."""
|
|
58
|
+
return set()
|
|
59
|
+
|
|
60
|
+
def get_controller_annotations(self) -> list[str]:
|
|
61
|
+
"""Annotations that mark controller/endpoint classes."""
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
def get_entity_annotations(self) -> list[str]:
|
|
65
|
+
"""Annotations that mark entity/model classes."""
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
def get_source_extensions(self) -> list[str]:
|
|
69
|
+
"""File extensions for source files in this language."""
|
|
70
|
+
return []
|
|
71
|
+
|
|
72
|
+
def get_test_path_patterns(self) -> list[str]:
|
|
73
|
+
"""Path patterns that indicate test directories."""
|
|
74
|
+
return ["/test/", "/tests/", "/__tests__/"]
|
|
75
|
+
|
|
76
|
+
def get_split_name_suffixes(self) -> tuple[str, ...]:
|
|
77
|
+
"""Class suffixes used for deriving semantic split names."""
|
|
78
|
+
return ()
|
|
79
|
+
|
|
80
|
+
def count_source_files(self, source_cache: Path) -> tuple[int, int]:
|
|
81
|
+
"""Count (files, lines) for this language's source files.
|
|
82
|
+
|
|
83
|
+
Returns (0, 0) if not implemented — callers should handle gracefully.
|
|
84
|
+
"""
|
|
85
|
+
return 0, 0
|
|
86
|
+
|
|
87
|
+
def extract_package_from_fqn(self, fqn: str) -> str:
|
|
88
|
+
"""Extract package from a fully-qualified name."""
|
|
89
|
+
return ""
|
|
90
|
+
|
|
91
|
+
def get_focus_hint(self, doc_type: str) -> str:
|
|
92
|
+
"""Return a focus hint for LLM-based split planning."""
|
|
93
|
+
return ""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def load_hooks(preset_name: str) -> PresetHooks:
|
|
97
|
+
"""Load preset-specific hooks, or return base (no-op) hooks.
|
|
98
|
+
|
|
99
|
+
Looks for presets/{preset_name}/hooks.py with a class named
|
|
100
|
+
`Hooks` that subclasses PresetHooks.
|
|
101
|
+
"""
|
|
102
|
+
from core.preset import _find_preset_dir
|
|
103
|
+
|
|
104
|
+
preset_dir = _find_preset_dir(preset_name)
|
|
105
|
+
if preset_dir is None:
|
|
106
|
+
return PresetHooks()
|
|
107
|
+
|
|
108
|
+
hooks_path = preset_dir / "hooks.py"
|
|
109
|
+
if not hooks_path.exists():
|
|
110
|
+
return PresetHooks()
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
spec = importlib.util.spec_from_file_location(
|
|
114
|
+
f"presets.{preset_name}.hooks", hooks_path
|
|
115
|
+
)
|
|
116
|
+
if spec is None or spec.loader is None:
|
|
117
|
+
return PresetHooks()
|
|
118
|
+
module = importlib.util.module_from_spec(spec)
|
|
119
|
+
spec.loader.exec_module(module)
|
|
120
|
+
|
|
121
|
+
hooks_cls = getattr(module, "Hooks", None)
|
|
122
|
+
if hooks_cls and issubclass(hooks_cls, PresetHooks):
|
|
123
|
+
return hooks_cls()
|
|
124
|
+
|
|
125
|
+
logger.warning("hooks.py for '%s' has no Hooks class", preset_name)
|
|
126
|
+
return PresetHooks()
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.warning("failed to load hooks for '%s': %s", preset_name, e)
|
|
129
|
+
return PresetHooks()
|
core/preset_profile.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Preset profile resolution and show-config support.
|
|
2
|
+
|
|
3
|
+
Provides module-type-specific profile resolution and configuration inspection.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
from core.preset_profile import resolve_profile, show_config
|
|
7
|
+
|
|
8
|
+
effective = resolve_profile("java-spring", module_type="base-lib")
|
|
9
|
+
config = show_config("java-spring", section="split")
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Profile resolution
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resolve_profile(preset_name: str, module_type: str = "service") -> dict[str, Any]:
|
|
23
|
+
"""Resolve the effective profile for a given preset and module type.
|
|
24
|
+
|
|
25
|
+
Currently just returns the base preset — profile overrides are not yet used.
|
|
26
|
+
Module-type-specific skip rules are handled by module_types config in dispatch.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
preset_name: Name of the preset (e.g., "java-spring")
|
|
30
|
+
module_type: Module type (e.g., "service", "base-lib", "api-contract")
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Preset configuration dict.
|
|
34
|
+
"""
|
|
35
|
+
from core.preset import load_preset
|
|
36
|
+
return load_preset(preset_name)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Show-config support (data retrieval, no I/O)
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def show_config(preset_name: str, section: str | None = None) -> dict[str, Any]:
|
|
45
|
+
"""Return preset configuration for inspection.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
preset_name: Name of the preset to load.
|
|
49
|
+
section: Optional section to filter (e.g., "split", "doc_types", "limits").
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Full preset dict or specific section.
|
|
53
|
+
"""
|
|
54
|
+
from core.preset import load_preset
|
|
55
|
+
preset = load_preset(preset_name)
|
|
56
|
+
if section:
|
|
57
|
+
value = preset.get(section)
|
|
58
|
+
if value is None:
|
|
59
|
+
raise KeyError(f"Section '{section}' not found in preset '{preset_name}'")
|
|
60
|
+
return {section: value}
|
|
61
|
+
return preset
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# CLI entry point: python -m core.preset show-config --preset X --section Y
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
import argparse
|
|
70
|
+
import json as _json
|
|
71
|
+
import sys
|
|
72
|
+
|
|
73
|
+
parser = argparse.ArgumentParser(description="Preset configuration tool")
|
|
74
|
+
sub = parser.add_subparsers(dest="command")
|
|
75
|
+
|
|
76
|
+
p_show = sub.add_parser("show-config", help="Display preset configuration")
|
|
77
|
+
p_show.add_argument("--preset", required=True, help="Preset name")
|
|
78
|
+
p_show.add_argument("--section", default=None, help="Config section to show")
|
|
79
|
+
|
|
80
|
+
args = parser.parse_args()
|
|
81
|
+
if args.command == "show-config":
|
|
82
|
+
try:
|
|
83
|
+
data = show_config(args.preset, args.section)
|
|
84
|
+
print(_json.dumps(data, ensure_ascii=False, indent=2))
|
|
85
|
+
except (FileNotFoundError, KeyError) as e:
|
|
86
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
else:
|
|
89
|
+
parser.print_help()
|
core/prompt/__init__.py
ADDED
core/prompt/__main__.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""CLI entry point for prompt rendering.
|
|
2
|
+
|
|
3
|
+
Provides the render-prompt command for Agent mode:
|
|
4
|
+
python -m core.prompt render --template ... --module ... --doc-type ...
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cmd_render(args: argparse.Namespace) -> None:
|
|
18
|
+
"""Render a sub-agent prompt from template."""
|
|
19
|
+
from core.config import load_config
|
|
20
|
+
from core.preset import load_preset
|
|
21
|
+
from core.prompt.renderer import render_prompt
|
|
22
|
+
|
|
23
|
+
config = load_config(Path(args.config) if args.config else None)
|
|
24
|
+
kb_config = config.get_kb(args.kb)
|
|
25
|
+
preset_name = kb_config.get("preset", "generic")
|
|
26
|
+
preset = load_preset(preset_name)
|
|
27
|
+
|
|
28
|
+
# In Agent mode, use ReferenceAssembler (paths only, no inline content)
|
|
29
|
+
from core.prompt.variables import ReferencePromptAssembler
|
|
30
|
+
assembler = ReferencePromptAssembler(project_root=Path(".").resolve(), preset=preset)
|
|
31
|
+
|
|
32
|
+
# Resolve template: explicit --template takes priority, otherwise lookup from doc_types.yaml
|
|
33
|
+
template_name = args.template
|
|
34
|
+
if not template_name:
|
|
35
|
+
doc_types = preset.get("doc_types", {})
|
|
36
|
+
dt_cfg = doc_types.get(args.doc_type, {})
|
|
37
|
+
template_name = dt_cfg.get("template")
|
|
38
|
+
if not template_name:
|
|
39
|
+
print(f"Error: no template mapping for doc-type '{args.doc_type}' in {preset_name}/doc_types.yaml. "
|
|
40
|
+
f"Specify --template explicitly.", file=sys.stderr)
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
# Find template
|
|
44
|
+
template_path = _find_template(template_name, preset_name)
|
|
45
|
+
if not template_path:
|
|
46
|
+
print(f"Error: template not found: {template_name}", file=sys.stderr)
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
|
|
49
|
+
# Parse extras
|
|
50
|
+
extras = {}
|
|
51
|
+
if args.extra:
|
|
52
|
+
for item in args.extra:
|
|
53
|
+
if "=" in item:
|
|
54
|
+
k, v = item.split("=", 1)
|
|
55
|
+
extras[k] = v
|
|
56
|
+
|
|
57
|
+
# Load execution snippet
|
|
58
|
+
execution_snippet = ""
|
|
59
|
+
if args.mode == "readwrite":
|
|
60
|
+
snippet_path = Path("skills/kb-init/templates/core/readwrite.md")
|
|
61
|
+
if snippet_path.exists():
|
|
62
|
+
execution_snippet = snippet_path.read_text(encoding="utf-8")
|
|
63
|
+
elif args.mode == "output-only":
|
|
64
|
+
snippet_path = Path("skills/kb-init/templates/core/output-only.md")
|
|
65
|
+
if snippet_path.exists():
|
|
66
|
+
execution_snippet = snippet_path.read_text(encoding="utf-8")
|
|
67
|
+
|
|
68
|
+
rendered = render_prompt(
|
|
69
|
+
template_path=template_path,
|
|
70
|
+
config=config.raw,
|
|
71
|
+
kb_name=args.kb,
|
|
72
|
+
module_name=args.module,
|
|
73
|
+
doc_type=args.doc_type,
|
|
74
|
+
assembler=assembler,
|
|
75
|
+
extras=extras,
|
|
76
|
+
execution_snippet=execution_snippet,
|
|
77
|
+
preset=preset,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if args.output:
|
|
81
|
+
Path(args.output).parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
Path(args.output).write_text(rendered, encoding="utf-8")
|
|
83
|
+
print(f"Rendered to: {args.output} ({len(rendered)} chars)")
|
|
84
|
+
else:
|
|
85
|
+
# Default: write to .meta/prompts/{doc_type}.md to avoid flooding stdout
|
|
86
|
+
meta_prompts = Path(f"knowledge/{args.module}/.meta/prompts")
|
|
87
|
+
meta_prompts.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
out_path = meta_prompts / f"{args.doc_type}.md"
|
|
89
|
+
out_path.write_text(rendered, encoding="utf-8")
|
|
90
|
+
print(f"Rendered to: {out_path} ({len(rendered)} chars)")
|
|
91
|
+
|
|
92
|
+
print(json.dumps({"status": "ok", "chars": len(rendered)}, ensure_ascii=False), file=sys.stderr)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _find_template(template_name: str, preset_name: str) -> Path | None:
|
|
96
|
+
"""Find template file by name. Templates live in presets/{preset}/templates/."""
|
|
97
|
+
# If it's already a valid path
|
|
98
|
+
p = Path(template_name)
|
|
99
|
+
if p.exists():
|
|
100
|
+
return p
|
|
101
|
+
|
|
102
|
+
# Search in preset templates (single source of truth)
|
|
103
|
+
candidate = Path("presets") / preset_name / "templates" / template_name
|
|
104
|
+
if candidate.exists():
|
|
105
|
+
return candidate
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main():
|
|
110
|
+
import warnings
|
|
111
|
+
warnings.warn(
|
|
112
|
+
"Use 'source-kb render' instead of 'python -m core.prompt render'",
|
|
113
|
+
DeprecationWarning, stacklevel=2,
|
|
114
|
+
)
|
|
115
|
+
print("[DEPRECATED] Use 'source-kb render' instead of 'python -m core.prompt'",
|
|
116
|
+
file=sys.stderr)
|
|
117
|
+
parser = argparse.ArgumentParser(prog="python -m core.prompt", description="Prompt tools")
|
|
118
|
+
sub = parser.add_subparsers(dest="command")
|
|
119
|
+
|
|
120
|
+
p = sub.add_parser("render", help="Render a sub-agent prompt")
|
|
121
|
+
p.add_argument("--template", help="Template filename or path (auto-resolved from doc_types.yaml if omitted)")
|
|
122
|
+
p.add_argument("--module", required=True, help="Module name")
|
|
123
|
+
p.add_argument("--config", help="kb-project.yaml path")
|
|
124
|
+
p.add_argument("--kb", required=True, help="Knowledge base name")
|
|
125
|
+
p.add_argument("--doc-type", required=True, help="Document type")
|
|
126
|
+
p.add_argument("--mode", default="readwrite", choices=["readwrite", "output-only"])
|
|
127
|
+
p.add_argument("--output", help="Output file path")
|
|
128
|
+
p.add_argument("--extra", nargs="*", help="Extra variables (key=value)")
|
|
129
|
+
|
|
130
|
+
p = sub.add_parser("validate-parity", help="Validate CLI/Agent template parity")
|
|
131
|
+
p.add_argument("--preset", required=True, help="Preset name")
|
|
132
|
+
|
|
133
|
+
args = parser.parse_args()
|
|
134
|
+
if not args.command:
|
|
135
|
+
parser.print_help()
|
|
136
|
+
sys.exit(1)
|
|
137
|
+
|
|
138
|
+
if args.command == "render":
|
|
139
|
+
cmd_render(args)
|
|
140
|
+
elif args.command == "validate-parity":
|
|
141
|
+
from core.prompt.validate_parity import main as parity_main
|
|
142
|
+
# Re-parse with validate_parity's own parser
|
|
143
|
+
parity_main()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
main()
|